Add PageLayout and PageTabs components matching Angular page-layout structure
Port the Angular page-layout wrapper and flights-page-tabs navigation to React, preserving identical DOM structure and CSS class names so global SCSS styles apply without modification.
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
@use "../../styles/variables" as vars;
|
||||
@use "../../styles/screen" as screen;
|
||||
|
||||
.page-layout {
|
||||
&__row {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: flex-start;
|
||||
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
max-width: vars.$site-width;
|
||||
|
||||
@include screen.print {
|
||||
width: 100% !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
@include screen.smTablet {
|
||||
flex-flow: column wrap;
|
||||
}
|
||||
}
|
||||
|
||||
&__column-left {
|
||||
position: sticky;
|
||||
top: 20px;
|
||||
bottom: 20px;
|
||||
z-index: 1001;
|
||||
|
||||
flex-shrink: 0;
|
||||
margin-left: 0;
|
||||
margin-right: 1.5%;
|
||||
width: vars.$left-aside-width;
|
||||
|
||||
@media (max-width: vars.$media-breakpoint-desktop) {
|
||||
width: vars.$left-aside-width-desktop;
|
||||
}
|
||||
|
||||
@media (max-width: vars.$media-breakpoint-tablet) {
|
||||
position: relative;
|
||||
top: 0;
|
||||
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__column-right {
|
||||
position: relative;
|
||||
width: calc(100% - #{vars.$left-aside-width} - #{vars.$column-spacing});
|
||||
|
||||
@include screen.print {
|
||||
width: 100% !important;
|
||||
margin-top: 20px !important;
|
||||
}
|
||||
|
||||
@media (max-width: vars.$media-breakpoint-desktop) {
|
||||
width: calc(100% - #{vars.$left-aside-width-desktop} - 1.5%);
|
||||
}
|
||||
|
||||
@media (max-width: vars.$media-breakpoint-tablet) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&__header-right {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@include screen.smTablet {
|
||||
flex-direction: column;
|
||||
order: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&__header-left {
|
||||
margin-top: auto;
|
||||
|
||||
@include screen.smTablet {
|
||||
order: 2;
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__header {
|
||||
position: relative;
|
||||
z-index: 1001;
|
||||
|
||||
padding-top: vars.$space-top-site;
|
||||
margin-bottom: vars.$space-xl;
|
||||
|
||||
@include screen.mobile {
|
||||
margin-bottom: vars.$space-m;
|
||||
}
|
||||
}
|
||||
|
||||
&__content &__column-left {
|
||||
@include screen.smTablet {
|
||||
margin-bottom: vars.$space-xl;
|
||||
}
|
||||
|
||||
@include screen.mobile {
|
||||
margin-bottom: vars.$space-m;
|
||||
}
|
||||
}
|
||||
|
||||
&__content &__column-left:empty {
|
||||
@include screen.smTablet {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__sticky-content {
|
||||
@include screen.gt-mobile {
|
||||
position: sticky;
|
||||
z-index: 1000;
|
||||
top: 60px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Shared page layout wrapper matching the Angular `page-layout` component.
|
||||
*
|
||||
* Produces the same DOM structure and CSS class names so global SCSS
|
||||
* styles apply identically.
|
||||
*/
|
||||
|
||||
import type { ReactNode, FC } from "react";
|
||||
import "./PageLayout.scss";
|
||||
|
||||
export interface PageLayoutProps {
|
||||
/** Content rendered in the header left column (e.g. page tabs). */
|
||||
headerLeft?: ReactNode;
|
||||
/** Page title rendered in the header right column. */
|
||||
title?: ReactNode;
|
||||
/** Content in the left column of the main content area (e.g. filter). */
|
||||
contentLeft?: ReactNode;
|
||||
/** Sticky content in the right column above main children. */
|
||||
stickyContent?: ReactNode;
|
||||
/** Main content rendered in the right column. */
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const PageLayout: FC<PageLayoutProps> = ({
|
||||
headerLeft,
|
||||
title,
|
||||
contentLeft,
|
||||
stickyContent,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<div className="page-layout">
|
||||
<div className="page-layout__row page-layout__header">
|
||||
<aside className="page-layout__column-left page-layout__header-left">
|
||||
{headerLeft}
|
||||
</aside>
|
||||
<div className="page-layout__column-right page-layout__header-right">
|
||||
<div className="page-layout__title">
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="page-layout__row page-layout__content">
|
||||
<aside className="page-layout__column-left">
|
||||
{contentLeft}
|
||||
</aside>
|
||||
<main className="page-layout__column-right">
|
||||
<div className="page-layout__sticky-content">
|
||||
{stickyContent}
|
||||
</div>
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
@use "../../styles/variables" as vars;
|
||||
@use "../../styles/fonts" as fonts;
|
||||
@use "../../styles/colors" as colors;
|
||||
|
||||
.tabs {
|
||||
.tabs__row {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.tabs__tab {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: vars.$button-height;
|
||||
border: 1px solid colors.$white;
|
||||
color: colors.$white;
|
||||
text-decoration: none;
|
||||
background-color: transparent;
|
||||
@include fonts.font-overflow();
|
||||
|
||||
&:first-child {
|
||||
border-radius: vars.$border-radius 0 0 vars.$border-radius;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 vars.$border-radius vars.$border-radius 0;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: colors.$white;
|
||||
font-weight: fonts.$font-bold;
|
||||
color: colors.$blue;
|
||||
}
|
||||
}
|
||||
|
||||
&__tab--full {
|
||||
border-radius: vars.$border-radius vars.$border-radius vars.$border-radius vars.$border-radius !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Page navigation tabs matching the Angular `flights-page-tabs` component.
|
||||
*
|
||||
* Renders "Online Timetable" / "Schedule" / "Flights Map" tab buttons
|
||||
* using the same `.tabs` class names as the Angular version.
|
||||
*/
|
||||
|
||||
import type { FC } from "react";
|
||||
import { Link, useParams } from "@modern-js/runtime/router";
|
||||
import { useTranslation } from "@/i18n/provider.js";
|
||||
import "./PageTabs.scss";
|
||||
|
||||
export type ViewType = "onlineboard" | "schedule" | "flights-map";
|
||||
|
||||
export interface PageTabsProps {
|
||||
viewType: ViewType;
|
||||
showFlightsMap?: boolean;
|
||||
}
|
||||
|
||||
export const PageTabs: FC<PageTabsProps> = ({
|
||||
viewType,
|
||||
showFlightsMap = false,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const routeParams = useParams<{ lang: string }>();
|
||||
const lang = routeParams.lang ?? "ru";
|
||||
|
||||
return (
|
||||
<div className="tabs">
|
||||
<div className="tabs__row">
|
||||
<Link
|
||||
className={`tabs__tab${viewType === "onlineboard" ? " active" : ""}`}
|
||||
to={`/${lang}/onlineboard`}
|
||||
data-testid="onlineboard-tab"
|
||||
>
|
||||
{t("BOARD.TITLE")}
|
||||
</Link>
|
||||
<Link
|
||||
className={`tabs__tab${viewType === "schedule" ? " active" : ""}`}
|
||||
to={`/${lang}/schedule`}
|
||||
data-testid="schedule-tab"
|
||||
>
|
||||
{t("SCHEDULE.TITLE-TAB")}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{showFlightsMap && (
|
||||
<div className="tabs__row">
|
||||
<Link
|
||||
className={`tabs__tab tabs__tab--full${viewType === "flights-map" ? " active" : ""}`}
|
||||
to={`/${lang}/flights-map`}
|
||||
data-testid="flights-map-tab"
|
||||
>
|
||||
{t("FLIGHTS-MAP.TITLE")}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user