feat: centralize shared page layouts

This commit is contained in:
2026-08-18 01:03:33 +02:00
parent d307e29145
commit 934db6d44b
11 changed files with 261 additions and 41 deletions
+126
View File
@@ -0,0 +1,126 @@
import type { ReactNode } from "react";
import { translateReactNode, usePlatformLanguage } from "../i18n/LanguageContext";
import type { PlatformInterfaceIdentityProps } from "../types";
import DismissibleAlert from "./DismissibleAlert";
import LoadingFrame from "./LoadingFrame";
import PageScrollViewport from "./PageScrollViewport";
import PageTitle from "./PageTitle";
export type PageLayoutMode = "standalone" | "embedded";
export type PageHeaderProps = {
title: ReactNode;
description?: ReactNode;
actions?: ReactNode;
loading?: boolean;
sticky?: boolean;
className?: string;
};
export function PageHeader({
title,
description,
actions,
loading = false,
sticky = true,
className = ""
}: PageHeaderProps) {
const { translateText } = usePlatformLanguage();
const classes = [
"page-heading",
"split",
sticky ? "workspace-heading" : "",
"page-layout-header",
className
].filter(Boolean).join(" ");
return (
<header className={classes}>
<div className="page-layout-heading-copy">
<PageTitle loading={loading}>{title}</PageTitle>
{description && <p>{translateReactNode(description, translateText)}</p>}
</div>
{actions && <div className="button-row compact-actions page-layout-actions">{actions}</div>}
</header>
);
}
export type PageLayoutProps = PlatformInterfaceIdentityProps & {
title: ReactNode;
description?: ReactNode;
actions?: ReactNode;
children: ReactNode;
loading?: boolean;
loadingLabel?: string;
error?: string;
success?: string;
mode?: PageLayoutMode;
scrollable?: boolean;
stickyHeader?: boolean;
documentationType?: "user" | "admin";
className?: string;
viewportClassName?: string;
headerClassName?: string;
bodyClassName?: string;
};
export default function PageLayout({
title,
description,
actions,
children,
loading = false,
loadingLabel = "i18n:govoplan-core.loading_page_data.85fe9edf",
error = "",
success = "",
mode = "standalone",
scrollable,
stickyHeader = true,
documentationType = "user",
className = "",
viewportClassName = "",
headerClassName = "",
bodyClassName = "",
interfaceId,
helpContextId,
helpModuleId,
helpTopicId
}: PageLayoutProps) {
const shouldScroll = scrollable ?? mode === "standalone";
const layoutClasses = [
"page-layout",
`page-layout-${mode}`,
mode === "standalone" ? "content-pad workspace-data-page" : "",
className
].filter(Boolean).join(" ");
const layout = (
<div
className={layoutClasses}
data-help-scope="page"
data-interface-id={interfaceId}
data-help-context-id={helpContextId}
data-help-module-id={helpModuleId}
data-help-topic-id={helpTopicId}
data-help-documentation-type={documentationType}
data-help-key={typeof title === "string" ? title : undefined}
>
<PageHeader
title={title}
description={description}
actions={actions}
loading={loading}
sticky={stickyHeader}
className={headerClassName}
/>
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
{success && <DismissibleAlert tone="success" resetKey={success} floating>{success}</DismissibleAlert>}
<LoadingFrame loading={loading} label={loadingLabel} className={["page-layout-body", bodyClassName].filter(Boolean).join(" ")}>
{children}
</LoadingFrame>
</div>
);
if (!shouldScroll) return layout;
return <PageScrollViewport className={viewportClassName}>{layout}</PageScrollViewport>;
}