feat: add shared workspace layouts
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -116,6 +116,8 @@ 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 WorkspaceLayout } from "./components/WorkspaceLayout";
|
||||
export type { WorkspaceLayoutProps, WorkspaceLayoutVariant, WorkspacePrimarySize } from "./components/WorkspaceLayout";
|
||||
export { default as PasswordField } from "./components/PasswordField";
|
||||
export { default as PasswordGeneratorDialog } from "./components/PasswordGeneratorDialog";
|
||||
export { default as PeoplePicker } from "./components/people/PeoplePicker";
|
||||
|
||||
@@ -82,6 +82,16 @@
|
||||
.ghost-button { border: 0; background: transparent; color: var(--muted); font-weight: 700; }
|
||||
.app-content { min-height: 0; overflow: hidden; }
|
||||
.workspace { height: 100%; min-height: 0; display: grid; grid-template-columns: 198px minmax(0, 1fr); }
|
||||
.workspace-layout-primary,
|
||||
.workspace-layout-content { min-width: 0; min-height: 0; }
|
||||
.workspace-layout-pane-scrollable { overflow: auto; }
|
||||
.workspace-layout-pane-contained { overflow: hidden; }
|
||||
.workspace-layout-primary > .section-sidebar { width: 100%; height: 100%; box-sizing: border-box; }
|
||||
.workspace-layout-navigation.workspace-layout-primary-compact { grid-template-columns: 168px minmax(0, 1fr); }
|
||||
.workspace-layout-navigation.workspace-layout-primary-wide { grid-template-columns: 240px minmax(0, 1fr); }
|
||||
.workspace-layout-split { grid-template-columns: minmax(280px, 360px) minmax(0, 1fr); }
|
||||
.workspace-layout-split.workspace-layout-primary-compact { grid-template-columns: minmax(250px, 320px) minmax(0, 1fr); }
|
||||
.workspace-layout-split.workspace-layout-primary-wide { grid-template-columns: minmax(340px, 440px) minmax(0, 1fr); }
|
||||
.section-sidebar { background: var(--sidebar-bg); border-right: var(--border-line-dark); padding: 18px 0; border-left: 3px solid var(--sidebar-border); box-shadow: var(--shadow-chrome); z-index: 80; overflow-y: scroll; }
|
||||
.section-title { font-size: 12px; font-weight: 800; color: var(--muted); padding: 0 22px 14px; letter-spacing: .06em; }
|
||||
.section-title-lower { margin-top: 28px; }
|
||||
@@ -142,6 +152,7 @@
|
||||
overflow: visible;
|
||||
}
|
||||
.ui-no-sticky-section-sidebars .workspace-content { overflow: visible; }
|
||||
.ui-no-sticky-section-sidebars .workspace-layout-primary { overflow: visible; }
|
||||
.content-pad { min-width: 0; max-width: 100%; box-sizing: border-box; padding: 28px 34px; }
|
||||
.page-heading { margin-bottom: 22px; }
|
||||
.page-heading h1 { margin: 0; font-size: 26px; color: var(--text-strong); font-weight: 600; }
|
||||
@@ -187,6 +198,8 @@
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
.page-layout-notices { display: grid; gap: 12px; min-width: 0; }
|
||||
.page-layout-notices > .alert { margin-bottom: 0; }
|
||||
.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; }
|
||||
@@ -254,6 +267,8 @@
|
||||
@media (max-width: 900px) {
|
||||
.api-mini { display: none; }
|
||||
.workspace { grid-template-columns: 1fr; }
|
||||
.workspace-layout-navigation > .workspace-layout-primary { display: none; }
|
||||
.workspace-layout-split { grid-template-columns: 1fr; grid-template-rows: minmax(160px, 34%) minmax(0, 1fr); }
|
||||
.section-sidebar { display: none; }
|
||||
.wizard-card { grid-template-columns: 1fr; }
|
||||
.metric-grid, .dashboard-grid, .settings-grid { grid-template-columns: 1fr; }
|
||||
|
||||
Reference in New Issue
Block a user