From 934db6d44bb0935b08d9464a51afdf4406a1c8f9 Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Tue, 18 Aug 2026 01:03:33 +0200 Subject: [PATCH] feat: centralize shared page layouts --- docs/CONTEXTUAL_HELP_CONTRACT.md | 3 + docs/INTERFACE_PATTERN_MIGRATION.md | 6 + webui/package.json | 1 + .../scripts/test-core-interface-patterns.mjs | 7 + webui/src/components/PageLayout.tsx | 126 ++++++++++++++++++ .../src/components/admin/AdminPageLayout.tsx | 49 +++---- .../src/features/dashboard/DashboardPage.tsx | 20 +-- webui/src/index.ts | 3 + webui/src/styles/layout.css | 33 +++++ webui/tests/page-layout.test.tsx | 48 +++++++ webui/tsconfig.component-tests.json | 6 + 11 files changed, 261 insertions(+), 41 deletions(-) create mode 100644 webui/src/components/PageLayout.tsx create mode 100644 webui/tests/page-layout.test.tsx diff --git a/docs/CONTEXTUAL_HELP_CONTRACT.md b/docs/CONTEXTUAL_HELP_CONTRACT.md index fce7e99..6d98fc8 100644 --- a/docs/CONTEXTUAL_HELP_CONTRACT.md +++ b/docs/CONTEXTUAL_HELP_CONTRACT.md @@ -54,6 +54,9 @@ than adding custom `F1` listeners: controls. - `TableActionGroup` action definitions carry the same identities so focused row actions can resolve consequence-specific help. +- `PageLayout` owns the page help scope and documentation identity for ordinary + headed pages; specialized full-canvas workspaces reuse `PageHeader` while + keeping their route-level help identity on the owning workspace. Module routes, public routes, settings sections, and administration sections may also declare `helpContextId` and `helpTopicId`. Each module must keep a diff --git a/docs/INTERFACE_PATTERN_MIGRATION.md b/docs/INTERFACE_PATTERN_MIGRATION.md index f4af96b..2f3f59b 100644 --- a/docs/INTERFACE_PATTERN_MIGRATION.md +++ b/docs/INTERFACE_PATTERN_MIGRATION.md @@ -13,6 +13,7 @@ domain modules own their compositions. | Reusable credentials | Repeated administration with an adaptive create/edit dialog, optional password generator, and destructive confirmation | Secret values are write-only; generated candidates use the browser cryptographic API without a weak fallback and do not replace the field until explicitly confirmed; scope/permission blockers name the required action, responsible actor, and destination; unavailable row actions remain keyboard-explainable | `CredentialEnvelopeManager.tsx`, shared `PasswordField`, `PasswordGeneratorDialog`, `ActionBlockerHint`, `Button`, `TableActionGroup`, and `ConfirmDialog` | | Retention policy | Effective-policy editor with inherited source paths and typed, narrowing-only controls | Parent locks and missing write authority are explicit; the save action distinguishes locks, missing target, loading, clean draft, and active save | `RetentionPolicyManagement.tsx`, policy logic tests, `test-core-interface-patterns.mjs` | | Module lifecycle | Guided operator projection over durable installer-queue evidence | Preflight, handoff, progress, stale evidence, recovery, and rollback consequences remain visible | Admin module lifecycle tests and the Core installer-queue contract | +| Shared page frame | Domain-neutral headed page layout used by Core and optional modules | Scroll ownership, sticky heading, route actions, page notices, loading, narrow-layout collapse, and contextual-help identity are centralized; `AdminPageLayout` composes the same contract | `PageLayout.tsx`, `page-layout.test.tsx`, Core fallback dashboard, Dashboard module, Ops module, and `check-shared-webui-layouts.py` | | Shared configuration primitives | Cross-module component contract | Dialog focus, blocker structure, disabled-action focus, route/page/field/action F1 help, unsaved changes, confirmation, loading, alerts, problem lists, and policy provenance are centralized | Core component tests, `CONTEXTUAL_HELP_CONTRACT.md`, and module-permutation build | ## Boundary @@ -26,3 +27,8 @@ they are not reasons to add sibling-private behavior to Core. Raw JSON remains permitted only for diagnostics, expert inspection, interchange, or conflict evidence. It is not a primary Core configuration editor. + +Raw page-frame debt is registered in the meta repository and may only decrease. +New headed pages use `PageLayout`; full-canvas explorers may use `PageHeader` +while the next shared workspace/split-pane contract is developed. Module CSS +continues to own domain content layout, never the shared page frame. diff --git a/webui/package.json b/webui/package.json index d58de46..15a8441 100644 --- a/webui/package.json +++ b/webui/package.json @@ -43,6 +43,7 @@ "test:module-permutations": "node scripts/test-module-permutations.mjs", "test:mail-components": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/mail-components.test.js", "test:metric-card": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/metric-card.test.js", + "test:page-layout": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/page-layout.test.js", "test:people-picker": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/people-picker.test.js", "test:password-field": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/password-generator.test.js", "test:resource-access": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/resource-access-explanation.test.js", diff --git a/webui/scripts/test-core-interface-patterns.mjs b/webui/scripts/test-core-interface-patterns.mjs index 178d360..5130988 100644 --- a/webui/scripts/test-core-interface-patterns.mjs +++ b/webui/scripts/test-core-interface-patterns.mjs @@ -16,6 +16,8 @@ const titlebar = read("src/layout/Titlebar.tsx"); const temporalDataMenu = read("src/layout/TemporalDataMenu.tsx"); const helpMenu = read("src/layout/HelpMenu.tsx"); const helpContext = read("src/utils/helpContext.ts"); +const pageLayout = read("src/components/PageLayout.tsx"); +const adminPageLayout = read("src/components/admin/AdminPageLayout.tsx"); const layoutStyles = read("src/styles/layout.css"); const authGateStyles = read("src/styles/auth-gate.css"); @@ -43,6 +45,11 @@ assert.match(credentials, /helpModuleId="access"/, "embedded credential controls assert.match(credentials, /disabledReason: writeDisabledReason/, "credential row actions retain actionable disabled reasons"); assert.doesNotMatch(credentials, /