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:
2026-04-15 18:58:44 +03:00
parent 64321f8150
commit 74be36b705
4 changed files with 289 additions and 0 deletions
+128
View File
@@ -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;
}
}
}
+56
View File
@@ -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>
);
};
+45
View File
@@ -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;
}
}
+60
View File
@@ -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>
);
};