feat: add shared workspace layouts

This commit is contained in:
2026-08-18 02:17:13 +02:00
parent 934db6d44b
commit dd7ad4d9c7
11 changed files with 207 additions and 11 deletions
+12 -4
View File
@@ -6,7 +6,7 @@ import LoadingFrame from "./LoadingFrame";
import PageScrollViewport from "./PageScrollViewport";
import PageTitle from "./PageTitle";
export type PageLayoutMode = "standalone" | "embedded";
export type PageLayoutMode = "standalone" | "workspace" | "embedded";
export type PageHeaderProps = {
title: ReactNode;
@@ -38,7 +38,9 @@ export function PageHeader({
<header className={classes}>
<div className="page-layout-heading-copy">
<PageTitle loading={loading}>{title}</PageTitle>
{description && <p>{translateReactNode(description, translateText)}</p>}
{description && (typeof description === "string"
? <p className="page-layout-description">{translateText(description)}</p>
: <div className="page-layout-description">{translateReactNode(description, translateText)}</div>)}
</div>
{actions && <div className="button-row compact-actions page-layout-actions">{actions}</div>}
</header>
@@ -51,9 +53,11 @@ export type PageLayoutProps = PlatformInterfaceIdentityProps & {
actions?: ReactNode;
children: ReactNode;
loading?: boolean;
headerLoading?: boolean;
loadingLabel?: string;
error?: string;
success?: string;
notices?: ReactNode;
mode?: PageLayoutMode;
scrollable?: boolean;
stickyHeader?: boolean;
@@ -70,9 +74,11 @@ export default function PageLayout({
actions,
children,
loading = false,
headerLoading,
loadingLabel = "i18n:govoplan-core.loading_page_data.85fe9edf",
error = "",
success = "",
notices,
mode = "standalone",
scrollable,
stickyHeader = true,
@@ -87,10 +93,11 @@ export default function PageLayout({
helpTopicId
}: PageLayoutProps) {
const shouldScroll = scrollable ?? mode === "standalone";
const shouldInset = mode !== "embedded";
const layoutClasses = [
"page-layout",
`page-layout-${mode}`,
mode === "standalone" ? "content-pad workspace-data-page" : "",
shouldInset ? "content-pad workspace-data-page" : "",
className
].filter(Boolean).join(" ");
@@ -109,12 +116,13 @@ export default function PageLayout({
title={title}
description={description}
actions={actions}
loading={loading}
loading={headerLoading ?? loading}
sticky={stickyHeader}
className={headerClassName}
/>
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
{success && <DismissibleAlert tone="success" resetKey={success} floating>{success}</DismissibleAlert>}
{notices && <div className="page-layout-notices">{notices}</div>}
<LoadingFrame loading={loading} label={loadingLabel} className={["page-layout-body", bodyClassName].filter(Boolean).join(" ")}>
{children}
</LoadingFrame>
+84
View File
@@ -0,0 +1,84 @@
import type { ReactNode } from "react";
import { usePlatformLanguage } from "../i18n/LanguageContext";
import type { PlatformInterfaceIdentityProps } from "../types";
export type WorkspaceLayoutVariant = "navigation" | "split";
export type WorkspacePrimarySize = "compact" | "default" | "wide";
export type WorkspaceLayoutProps = PlatformInterfaceIdentityProps & {
primary: ReactNode;
children: ReactNode;
variant?: WorkspaceLayoutVariant;
primarySize?: WorkspacePrimarySize;
primaryLabel?: string;
contentLabel?: string;
documentationType?: "user" | "admin";
primaryScrollable?: boolean;
contentScrollable?: boolean;
className?: string;
primaryClassName?: string;
contentClassName?: string;
};
export default function WorkspaceLayout({
primary,
children,
variant = "navigation",
primarySize = "default",
primaryLabel,
contentLabel,
documentationType = "user",
primaryScrollable,
contentScrollable = true,
className = "",
primaryClassName = "",
contentClassName = "",
interfaceId,
helpContextId,
helpModuleId,
helpTopicId
}: WorkspaceLayoutProps) {
const { translateText } = usePlatformLanguage();
const shouldScrollPrimary = primaryScrollable ?? variant === "split";
return (
<div
className={[
"workspace",
"workspace-layout",
`workspace-layout-${variant}`,
`workspace-layout-primary-${primarySize}`,
className
].filter(Boolean).join(" ")}
data-help-scope="workspace"
data-interface-id={interfaceId}
data-help-context-id={helpContextId}
data-help-module-id={helpModuleId}
data-help-topic-id={helpTopicId}
data-help-documentation-type={documentationType}
>
<div
className={[
"workspace-layout-primary",
shouldScrollPrimary ? "workspace-layout-pane-scrollable" : "workspace-layout-pane-contained",
primaryClassName
].filter(Boolean).join(" ")}
role={primaryLabel ? "region" : undefined}
aria-label={primaryLabel ? translateText(primaryLabel) : undefined}
>
{primary}
</div>
<section
className={[
"workspace-content",
"workspace-layout-content",
contentScrollable ? "workspace-layout-pane-scrollable" : "workspace-layout-pane-contained",
contentClassName
].filter(Boolean).join(" ")}
aria-label={contentLabel ? translateText(contentLabel) : undefined}
>
{children}
</section>
</div>
);
}