diff --git a/docs/CONTEXTUAL_HELP_CONTRACT.md b/docs/CONTEXTUAL_HELP_CONTRACT.md index 6d98fc8..36b8295 100644 --- a/docs/CONTEXTUAL_HELP_CONTRACT.md +++ b/docs/CONTEXTUAL_HELP_CONTRACT.md @@ -55,8 +55,9 @@ than adding custom `F1` listeners: - `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. + headed pages. `WorkspaceLayout` owns the full-canvas workspace scope and its + labelled primary/content panes; pages inside it use `PageLayout` in + `workspace` mode and retain their own route-level help identity. 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 2f3f59b..f1cae0d 100644 --- a/docs/INTERFACE_PATTERN_MIGRATION.md +++ b/docs/INTERFACE_PATTERN_MIGRATION.md @@ -13,7 +13,8 @@ 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 page frame | Domain-neutral headed page layout used by Core and optional modules | Standalone, workspace, and embedded modes make inset and scroll ownership explicit; sticky heading, rich descriptions, route actions, 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, Campaign pages, and `check-shared-webui-layouts.py` | +| Full-canvas workspace | Navigation/content and list/detail canvases that own pane geometry and scrolling | Navigation and split variants, primary-pane width, pane-owned or contained scrolling, responsive stacking or navigation collapse, pane labels, and contextual-help identity are centralized without encoding domain navigation | `WorkspaceLayout.tsx`, `workspace-layout.test.tsx`, Campaign workspace, Campaign module workspace, Templates workspace, Approvals list/detail workspace, 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 @@ -28,7 +29,8 @@ 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. +Raw page-frame and full-canvas workspace debt is registered in the meta +repository and may only decrease. New headed pages use `PageLayout`; pages +inside `WorkspaceLayout` use its `workspace` mode so the pane owns scrolling +while the page retains the standard inset. Module CSS continues to own domain +content layout, never the shared page or workspace frame. diff --git a/webui/package.json b/webui/package.json index 15a8441..e51e4a3 100644 --- a/webui/package.json +++ b/webui/package.json @@ -44,6 +44,7 @@ "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:workspace-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/workspace-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 5130988..1003ee4 100644 --- a/webui/scripts/test-core-interface-patterns.mjs +++ b/webui/scripts/test-core-interface-patterns.mjs @@ -17,6 +17,7 @@ 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 workspaceLayout = read("src/components/WorkspaceLayout.tsx"); const adminPageLayout = read("src/components/admin/AdminPageLayout.tsx"); const layoutStyles = read("src/styles/layout.css"); const authGateStyles = read("src/styles/auth-gate.css"); @@ -49,6 +50,8 @@ assert.match(pageLayout, /`page-layout-\$\{mode\}`/, "shared standalone and embe assert.match(pageLayout, /\s*} + primaryLabel="Campaign sections" + contentLabel="Campaign content" + interfaceId="campaign.workspace" + helpModuleId="campaign" + > +
Selected section
+ + +); + +assert(navigationMarkup.includes("workspace-layout-navigation"), "navigation workspaces use the central navigation geometry"); +assert(navigationMarkup.includes("workspace-layout-primary-default"), "the default primary-pane width is explicit"); +assert(navigationMarkup.includes("workspace-layout-pane-contained"), "navigation panes defer scrolling to their navigation component"); +assert(navigationMarkup.includes('aria-label="Campaign sections"'), "the primary pane can be named"); +assert(navigationMarkup.includes('aria-label="Campaign content"'), "the content pane can be named"); +assert(navigationMarkup.includes('data-help-scope="workspace"'), "workspace contextual-help scope is centralized"); +assert(navigationMarkup.includes('data-interface-id="campaign.workspace"'), "workspace identity reaches the root"); +assert(navigationMarkup.includes('data-help-documentation-type="user"'), "workspace documentation type is explicit"); + +const splitMarkup = renderToStaticMarkup( + + Collection} + > + Detail + + +); + +assert(splitMarkup.includes("workspace-layout-split"), "list-detail workspaces opt into split-pane geometry"); +assert(splitMarkup.includes("workspace-layout-primary-wide"), "split workspaces use central width variants"); +assert(splitMarkup.includes("workspace-layout-pane-scrollable"), "split primary panes can own scrolling"); +assert(splitMarkup.includes("workspace-layout-content workspace-layout-pane-contained"), "contained content panes can delegate scrolling to their children"); diff --git a/webui/tsconfig.component-tests.json b/webui/tsconfig.component-tests.json index b16f6fd..36fc1c4 100644 --- a/webui/tsconfig.component-tests.json +++ b/webui/tsconfig.component-tests.json @@ -28,6 +28,7 @@ "tests/mail-components.test.tsx", "tests/metric-card.test.tsx", "tests/page-layout.test.tsx", + "tests/workspace-layout.test.tsx", "tests/people-picker.test.tsx", "tests/password-generator.test.tsx", "tests/resource-access-explanation.test.tsx", @@ -48,6 +49,7 @@ "src/components/PageLayout.tsx", "src/components/PageScrollViewport.tsx", "src/components/PageTitle.tsx", + "src/components/WorkspaceLayout.tsx", "src/components/LoadingFrame.tsx", "src/components/LoadingIndicator.tsx", "src/components/people/PeoplePicker.tsx",