feat: centralize shared page layouts
This commit is contained in:
@@ -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>;
|
||||
}
|
||||
@@ -1,11 +1,8 @@
|
||||
import type { ReactNode } from "react";
|
||||
import DismissibleAlert from "../DismissibleAlert";
|
||||
import LoadingFrame from "../LoadingFrame";
|
||||
import PageTitle from "../PageTitle";
|
||||
import { usePlatformLanguage } from "../../i18n/LanguageContext";
|
||||
import type { PlatformInterfaceIdentityProps } from "../../types";
|
||||
import PageLayout from "../PageLayout";
|
||||
|
||||
type Props = PlatformInterfaceIdentityProps & {
|
||||
export type AdminPageLayoutProps = PlatformInterfaceIdentityProps & {
|
||||
title: string;
|
||||
description: string;
|
||||
loading?: boolean;
|
||||
@@ -31,31 +28,27 @@ export default function AdminPageLayout({
|
||||
helpContextId,
|
||||
helpModuleId,
|
||||
helpTopicId
|
||||
}: Props) {
|
||||
const { translateText } = usePlatformLanguage();
|
||||
}: AdminPageLayoutProps) {
|
||||
return (
|
||||
<div
|
||||
<PageLayout
|
||||
title={title}
|
||||
description={description}
|
||||
loading={loading}
|
||||
loadingLabel={loadingLabel}
|
||||
error={error}
|
||||
success={success}
|
||||
actions={actions}
|
||||
mode="embedded"
|
||||
documentationType="admin"
|
||||
className={`admin-section-page ${className}`.trim()}
|
||||
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="admin"
|
||||
data-help-key={title}
|
||||
headerClassName="admin-page-heading"
|
||||
interfaceId={interfaceId}
|
||||
helpContextId={helpContextId}
|
||||
helpModuleId={helpModuleId}
|
||||
helpTopicId={helpTopicId}
|
||||
>
|
||||
<div className="page-heading split workspace-heading admin-page-heading">
|
||||
<div>
|
||||
<PageTitle loading={loading}>{title}</PageTitle>
|
||||
<p>{translateText(description)}</p>
|
||||
</div>
|
||||
{actions && <div className="button-row compact-actions admin-page-actions">{actions}</div>}
|
||||
</div>
|
||||
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
|
||||
{success && <DismissibleAlert tone="success" resetKey={success} floating>{success}</DismissibleAlert>}
|
||||
<LoadingFrame loading={loading} label={translateText(loadingLabel)}>
|
||||
{children}
|
||||
</LoadingFrame>
|
||||
</div>);
|
||||
{children}
|
||||
</PageLayout>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
import Card from "../../components/Card";
|
||||
import MetricCard from "../../components/MetricCard";
|
||||
import PageScrollViewport from "../../components/PageScrollViewport";
|
||||
import PageTitle from "../../components/PageTitle";
|
||||
import PageLayout from "../../components/PageLayout";
|
||||
import { usePlatformModules } from "../../platform/ModuleContext";
|
||||
|
||||
export default function DashboardPage() {
|
||||
const modules = usePlatformModules();
|
||||
|
||||
return (
|
||||
<PageScrollViewport className="core-dashboard-page">
|
||||
<div className="content-pad workspace-data-page">
|
||||
<div className="page-heading split workspace-heading">
|
||||
<div>
|
||||
<PageTitle>i18n:govoplan-core.dashboard.d87f47b4</PageTitle>
|
||||
<p>Install and enable the dashboard module to make this page configurable.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PageLayout
|
||||
title="i18n:govoplan-core.dashboard.d87f47b4"
|
||||
description="Install and enable the dashboard module to make this page configurable."
|
||||
viewportClassName="core-dashboard-page"
|
||||
>
|
||||
<div className="metric-grid">
|
||||
<MetricCard label="i18n:govoplan-core.installed_modules.32b3e799" value={modules.length} tone="info" detail={modules.length ? moduleLabels(modules).join(", ") : "i18n:govoplan-core.core_only.d46ce7d9"} />
|
||||
<MetricCard label="Dashboard module" value="not installed" tone="neutral" detail="Core fallback is active." />
|
||||
@@ -39,8 +34,7 @@ export default function DashboardPage() {
|
||||
<p className="muted">This minimal core home is only shown while no dashboard WebUI module is available. Feature modules own their pages and can expose dashboard widgets once the dashboard module is installed.</p>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</PageScrollViewport>);
|
||||
</PageLayout>);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ export { default as AdvancedOptionsPanel } from "./components/AdvancedOptionsPan
|
||||
export { default as AdminIconButton } from "./components/admin/AdminIconButton";
|
||||
export type { AdminIconButtonProps } from "./components/admin/AdminIconButton";
|
||||
export { default as AdminPageLayout } from "./components/admin/AdminPageLayout";
|
||||
export type { AdminPageLayoutProps } from "./components/admin/AdminPageLayout";
|
||||
export { default as AdminSelectionList } from "./components/admin/AdminSelectionList";
|
||||
export { adminErrorMessage, formatAdminDateTime, joinLabels } from "./components/admin/adminUtils";
|
||||
export { default as Button } from "./components/Button";
|
||||
@@ -111,6 +112,8 @@ export { default as MetricCard } from "./components/MetricCard";
|
||||
export { default as MessageDisplayPanel } from "./components/MessageDisplayPanel";
|
||||
export type { MessageDisplayAttachment, MessageDisplayField } from "./components/MessageDisplayPanel";
|
||||
export { default as PageTitle } from "./components/PageTitle";
|
||||
export { default as PageLayout, PageHeader } from "./components/PageLayout";
|
||||
export type { PageHeaderProps, PageLayoutMode, PageLayoutProps } from "./components/PageLayout";
|
||||
export { default as PageScrollViewport } from "./components/PageScrollViewport";
|
||||
export type { PageScrollViewportProps } from "./components/PageScrollViewport";
|
||||
export { default as PasswordField } from "./components/PasswordField";
|
||||
|
||||
@@ -159,6 +159,34 @@
|
||||
margin: -28px -34px 22px;
|
||||
padding: 18px 34px 16px;
|
||||
}
|
||||
.page-layout {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.page-layout-header {
|
||||
align-items: flex-start;
|
||||
gap: 18px;
|
||||
min-width: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.page-layout-heading-copy,
|
||||
.page-layout-body {
|
||||
min-width: 0;
|
||||
}
|
||||
.page-layout-actions {
|
||||
align-self: flex-start;
|
||||
justify-content: flex-end;
|
||||
min-width: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
.page-layout > .page-layout-body {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
.workspace-heading .mono-small { margin-top: 8px; }
|
||||
.panel, .card { background: var(--panel); border: var(--border-line); border-radius: var(--radius); box-shadow: var(--shadow); }
|
||||
.panel { overflow: hidden; }
|
||||
@@ -324,6 +352,11 @@
|
||||
.docs-unavailable-reason { margin-top: 16px; border-left: 3px solid var(--amber); padding-left: 12px; }
|
||||
@media (max-width: 900px) {
|
||||
.page-heading.split { align-items: flex-start; flex-direction: column; }
|
||||
.page-layout-actions {
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
}
|
||||
.summary-grid, .detail-list div { grid-template-columns: 1fr; }
|
||||
.docs-workspace { grid-template-columns: 1fr; }
|
||||
.docs-outline { display: none; }
|
||||
|
||||
Reference in New Issue
Block a user