diff --git a/docs/architecture/INTERFACE_PATTERN_LANGUAGE.md b/docs/architecture/INTERFACE_PATTERN_LANGUAGE.md index 0654186..5f7632d 100644 --- a/docs/architecture/INTERFACE_PATTERN_LANGUAGE.md +++ b/docs/architecture/INTERFACE_PATTERN_LANGUAGE.md @@ -115,18 +115,29 @@ The current structural vocabulary is: - `FormSection` owns form-section heading, description, actions, content flow, separation, and panel presentation. It does not own field values, validation, permissions, or domain wording. +- `MetricGrid` owns the responsive grouping around `MetricCard`: fixed one-to-five + columns or auto-fit, minimum card width, density, surrounding rhythm, and a + named collapse point. Modules provide the metric, tone, and consequence; + they do not recreate the group grid or reach across module CSS to size it. +- `DescriptionList` and `DescriptionItem` own semantic property presentation. + The stacked variant supports compact multi-column facts; the inline variant + supports one-column term/value rows with a standard term width. Both own + density, wrapping, and responsive collapse while modules retain the terms, + values, provenance, and actions. - `Dialog` owns size and administration variants, body padding, description, notices, and fixed footer placement. `DialogActions`, `DialogForm`, and `DialogSection` own the footer action flow, native form flow, and body grouping used inside it. Modules compose fields and consequences rather than recreating dialog anatomy. -Raw toolbar tags, the former generic grid classes, raw dialog-form wrappers, -and module-local definitions of these contracts are rejected by the focused -workspace checks. The next promotion candidates are metric groups, -description/property lists, filter/search composition, responsive data -summaries, and unified empty/error state composition. Their APIs must remain -composable; a central component is not a single oversized page template. +Raw toolbar tags, the former generic grid and property-list classes, raw +dialog-form wrappers, and module-local definitions of these contracts are +rejected by the focused workspace checks. Dialog widths matching the Core size +scale must use `Dialog size`; other local widths require a reviewed exception +and may only decrease. The next promotion candidates are assignment/picker +composition, filter/search composition, and unified empty/error state +composition. Their APIs must remain composable; a central component is not a +single oversized page template. ## Surface Archetypes diff --git a/docs/evidence/SHARED_WEBUI_PRIMITIVE_INVENTORY.md b/docs/evidence/SHARED_WEBUI_PRIMITIVE_INVENTORY.md index 0b5edbb..5ab610e 100644 --- a/docs/evidence/SHARED_WEBUI_PRIMITIVE_INVENTORY.md +++ b/docs/evidence/SHARED_WEBUI_PRIMITIVE_INVENTORY.md @@ -1,7 +1,8 @@ # Shared WebUI Primitive Inventory -This 2026-08-18 inventory records the implementation state after the first -product-wide toolbar, grid, form-section, and dialog-anatomy migration. It is +This 2026-08-18 inventory records the implementation state after the +product-wide toolbar, grid, form-section, dialog-anatomy, metric-group, and +description-list migrations. It is evidence for prioritization, not a substitute for the normative [interface pattern language](../architecture/INTERFACE_PATTERN_LANGUAGE.md). @@ -16,25 +17,38 @@ evidence for prioritization, not a substitute for the normative | `DialogActions` | Every Core `Dialog` footer | Footer wrapping and alignment are no longer repeated by consumers. | | `DialogForm` | 6 source files | Raw `*-dialog-form` form wrappers are rejected. | | `DialogSection` | 6 source files | Repeated dialog field/content grouping is available as a shared primitive. | +| `MetricGrid` | 29 uses in 25 source files | The former `metric-grid` class and module-specific column overrides were removed. Core now owns 1–5/auto-fit columns, minimum width, density, block/inset/zero spacing, and collapse. | +| `DescriptionList` and `DescriptionItem` | 51 lists in 28 source files; item composition in 11 files | The former `admin-details-grid` and `detail-list` classes were removed. Core now owns stacked and inline property layouts, density, term width, wrapping, and collapse while preserving native `dl`/`dt`/`dd` semantics. | +| Standard dialog sizing | 9 consumers and 6 duplicated width rules removed | Confirmations and compatible Calendar, credential, Campaign, Files, and Voting dialogs use the Core small/large/wide scale. The 61 remaining specialized selectors across 22 CSS files are registered and decrease-only. | `tools/checks/check-shared-webui-primitives.py` verifies Core exports and ownership, representative consumers, the absence of the retired raw anatomy, and composition of every `Dialog` footer through `DialogActions`. +## Dialog Width Classification + +The remaining 61 width selectors do not duplicate the Core 460/560/680/1040/ +1440px scale. They cover bounded editor widths between scale steps, high-density +definition and governance editors, preview/chooser canvases, message and file +overlays with coupled height behavior, and responsive full-canvas workflows. +Their exact selector set lives in +`tools/checks/shared-webui-dialog-width-exceptions.txt`. The focused check fails +for a new selector, a stale baseline entry, or any local width that duplicates +the Core scale. + ## Remaining Promotion Candidates -The post-migration scan found 101 uses of 30 non-Core grid class names across -54 files. Most are intentionally domain-specific; repeated groups identify the -next useful primitives: +The post-migration scan still finds domain-specific grids, but the repeated +generic metric and property-list geometry is gone. Remaining grids are mostly +unequal-track editors, visualizations, workflow facts, import mappings, and +collection layouts. The next useful candidates depend on interaction semantics: | Priority | Remaining pattern | Evidence | Proposed contract | | --- | --- | ---: | --- | -| 1 | Metric-card groups | `metric-grid`: 29 uses in 25 files | `MetricGrid` or `MetricGroup` owning 1–4/auto-fit columns, compact spacing, inset mode, and narrow stacking around the existing `MetricCard`. | -| 2 | Property/description lists | `admin-details-grid`: 13 uses in 11 files | Semantic `DescriptionList` with compact/default density, 1–2 columns, wrapping, and narrow collapse. | -| 3 | Assignment/picker groups | `admin-assignment-grid`: 5 uses in 4 files | A selection/assignment layout only after its list, search, empty, and permission variants are compared; plain geometry can already use `ContentGrid`. | -| 4 | Filter and search rows | Toolbars are centralized, but filter fields, result counts, bulk selection, and action grouping still vary | `FilterBar` composed from `ActionToolbar`, with an explicit search region, filters, result summary, and bulk actions. | -| 5 | Dialog sizing exceptions | Dialog consumers still use 62 module class tokens across 53 files; 26 module/Core CSS files contain dialog/modal width declarations needing review | Migrate standard widths to `Dialog size`; retain only documented full-canvas, preview, chooser, and domain-editor exceptions. | -| 6 | State and collection anatomy | Loading components exist, but empty/error/retry/action placement is not yet one composition | A composable `StatePanel`/`CollectionState` covering empty, recoverable error, permission block, partial result, and next action without hiding domain consequences. | +| 1 | Empty and collection state anatomy | More than 70 module-prefixed empty/state class uses remain; only loading, alerts, blockers, and DataGrid empty actions are centralized | A composable `StatePanel`/`CollectionState` covering empty, recoverable error, permission block, partial result, and next action without hiding domain consequences. First distinguish a whole-surface state from a compact empty row or optional-value placeholder. | +| 2 | Filter and search rows | More than 50 filter/search class tokens cover simple text search, facets, popovers, result counts, active filters, bulk selection, and overlay search | Define `FilterBar` only after the input/submit, live filtering, facet, result-summary, and bulk-action accessibility variants have been compared. Plain action placement already uses `ActionToolbar`. | +| 3 | Assignment/picker groups | `admin-assignment-grid`: 5 uses in 4 files | Promote a selection/assignment composition only after its list, search, empty, policy, and permission variants are compared; plain geometry can already use `ContentGrid`. | +| 4 | Repeated domain fact and statistic grids | `campaign-header-grid` (8), `review-flow-fact-grid` (6), `postbox-form-grid` (5), plus smaller families | Compare semantics before promotion. Some can use `ContentGrid` or `DescriptionList`; others intentionally own unequal tracks or workflow visualization. | The other custom grids are mostly bounded domain visualizations or unequal-track editors: conflict mappings, import mapping, campaign review facts, charts, @@ -45,8 +59,8 @@ created merely because two implementations both use CSS Grid. ## Next Audit -The next bounded slice should implement the metric and description-list -contracts, migrate their consumers, and then classify dialog width declarations -as standard-size migrations or registered exceptions. After that, compare -filter/search rows before defining their composition; their accessibility and -bulk-selection behavior matter more than visual similarity. +The next bounded slice should compare whole-surface empty states across list, +detail, permission, capability, and recoverable-error contexts and promote only +their shared anatomy. Filter/search composition follows after its live versus +submitted filtering and bulk-selection behavior is explicit. Assignment grids +remain deferred until their permission and policy variants are understood. diff --git a/tools/checks/check-shared-webui-primitives.py b/tools/checks/check-shared-webui-primitives.py index 67c1665..989e89b 100644 --- a/tools/checks/check-shared-webui-primitives.py +++ b/tools/checks/check-shared-webui-primitives.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Enforce Core ownership of repeated WebUI toolbar, grid and dialog anatomy.""" +"""Enforce Core ownership of repeated WebUI layout and dialog anatomy.""" from __future__ import annotations @@ -11,6 +11,7 @@ import sys META_ROOT = pathlib.Path(__file__).resolve().parents[2] REPOS_ROOT = META_ROOT.parent CORE_INDEX = pathlib.Path("govoplan-core/webui/src/index.ts") +DIALOG_WIDTH_EXCEPTIONS = META_ROOT / "tools/checks/shared-webui-dialog-width-exceptions.txt" RAW_ELEMENT = re.compile( r'<(?Pdiv|span|header|section|form)\b(?P[^>]*?)' @@ -24,6 +25,9 @@ LEGACY_LAYOUT_TOKENS = { "settings-grid", "admin-dialog", "admin-dialog-wide", + "admin-details-grid", + "detail-list", + "metric-grid", } CENTRAL_COMPONENTS = { "ActionToolbar": pathlib.Path( @@ -55,6 +59,15 @@ CENTRAL_COMPONENTS = { "DialogSection": pathlib.Path( "govoplan-core/webui/src/components/DialogAnatomy.tsx" ), + "DescriptionList": pathlib.Path( + "govoplan-core/webui/src/components/DescriptionList.tsx" + ), + "DescriptionItem": pathlib.Path( + "govoplan-core/webui/src/components/DescriptionList.tsx" + ), + "MetricGrid": pathlib.Path( + "govoplan-core/webui/src/components/MetricGrid.tsx" + ), } REQUIRED_CONSUMERS = { "ActionToolbar": ( @@ -89,7 +102,42 @@ REQUIRED_CONSUMERS = { pathlib.Path("govoplan-files/webui/src/features/files/components/FileShareDialog.tsx"), pathlib.Path("govoplan-templates/webui/src/features/templates/TemplatesPage.tsx"), ), + "DescriptionList": ( + pathlib.Path("govoplan-access/webui/src/features/admin/UsersPanel.tsx"), + pathlib.Path("govoplan-campaign/webui/src/features/campaigns/CampaignReportPage.tsx"), + pathlib.Path("govoplan-docs/webui/src/features/docs/DocsPage.tsx"), + pathlib.Path("govoplan-policy/webui/src/features/policy/ViewPoliciesPanel.tsx"), + ), + "MetricGrid": ( + pathlib.Path("govoplan-core/webui/src/features/dashboard/DashboardPage.tsx"), + pathlib.Path("govoplan-admin/webui/src/features/admin/ModuleManagementPanel.tsx"), + pathlib.Path("govoplan-campaign/webui/src/features/operator/OperatorQueuePage.tsx"), + pathlib.Path("govoplan-notifications/webui/src/features/notifications/NotificationSummaryWidget.tsx"), + ), } +LEGACY_CSS_SELECTOR = re.compile( + r"(? list[pathlib.Path]: @@ -101,6 +149,15 @@ def source_paths() -> list[pathlib.Path]: return paths +def css_paths() -> list[pathlib.Path]: + paths: list[pathlib.Path] = [] + for repository in sorted(REPOS_ROOT.glob("govoplan*")): + source_root = repository / "webui" / "src" + if source_root.is_dir(): + paths.extend(sorted(source_root.rglob("*.css"))) + return paths + + def relative(path: pathlib.Path) -> pathlib.Path: return path.relative_to(REPOS_ROOT) @@ -123,9 +180,47 @@ def raw_reason(classes: str) -> str | None: return None +def normalized_css_selector(selector: str) -> str: + return " ".join(selector.split()) + + +def dialog_width_exceptions(styles: dict[pathlib.Path, str]) -> dict[str, str]: + exceptions: dict[str, str] = {} + central_dialog_styles = pathlib.Path("govoplan-core/webui/src/styles/dialogs.css") + for path, content in styles.items(): + if path == central_dialog_styles: + continue + without_comments = CSS_COMMENT.sub("", content) + for match in CSS_BLOCK.finditer(without_comments): + selector = normalized_css_selector(match.group(1)) + declarations = match.group(2) + if not DIALOG_WIDTH.search(declarations): + continue + dialog_classes = DIALOG_CLASS.findall(selector) + if not dialog_classes: + continue + if all(name.lower().endswith(DIALOG_INTERNAL_SUFFIXES) for name in dialog_classes): + continue + signature = f"{path}|{selector}" + exceptions[signature] = declarations + return exceptions + + +def exception_baseline() -> set[str]: + if not DIALOG_WIDTH_EXCEPTIONS.exists(): + return set() + return { + line.strip() + for line in DIALOG_WIDTH_EXCEPTIONS.read_text(encoding="utf-8").splitlines() + if line.strip() and not line.lstrip().startswith("#") + } + + def main() -> int: sources = source_paths() source_text = {relative(path): path.read_text(encoding="utf-8") for path in sources} + styles = css_paths() + style_text = {relative(path): path.read_text(encoding="utf-8") for path in styles} errors: list[str] = [] for path, content in source_text.items(): @@ -138,6 +233,24 @@ def main() -> int: f"Raw {match.group('tag')} repeats shared anatomy ({reason}): {path}:{line}" ) + for path, content in style_text.items(): + legacy_match = LEGACY_CSS_SELECTOR.search(CSS_COMMENT.sub("", content)) + if legacy_match: + line = content.count("\n", 0, legacy_match.start()) + 1 + errors.append(f"Legacy shared layout selector is not allowed: {path}:{line}") + + dialog_exceptions = dialog_width_exceptions(style_text) + baseline = exception_baseline() + for signature in sorted(set(dialog_exceptions) - baseline): + errors.append(f"Unreviewed local dialog width; use Dialog size or register a justified exception: {signature}") + for signature in sorted(baseline - set(dialog_exceptions)): + errors.append(f"Stale dialog width exception can be removed: {signature}") + for signature, declarations in sorted(dialog_exceptions.items()): + width_values = " ".join(DIALOG_WIDTH_VALUE.findall(declarations)) + standard = STANDARD_DIALOG_WIDTH.search(width_values) + if standard: + errors.append(f"Local dialog width duplicates Core size {standard.group(0)}: {signature}") + for name, owner in CENTRAL_COMPONENTS.items(): definition = re.compile( rf"\b(?:function|class)\s+{name}\b|\bconst\s+{name}\s*=" @@ -178,9 +291,9 @@ def main() -> int: migrated = ", ".join( f"{name}={usage_counts[name]} files" - for name in ("ActionToolbar", "ContentGrid", "FormGrid", "FormSection", "DialogForm", "DialogSection") + for name in ("ActionToolbar", "ContentGrid", "FormGrid", "FormSection", "DialogForm", "DialogSection", "MetricGrid", "DescriptionList") ) - print(f"Shared WebUI primitive contract passed: {migrated}; no raw legacy anatomy.") + print(f"Shared WebUI primitive contract passed: {migrated}; {len(dialog_exceptions)} reviewed dialog width exceptions; no raw legacy anatomy.") return 0 diff --git a/tools/checks/shared-webui-dialog-width-exceptions.txt b/tools/checks/shared-webui-dialog-width-exceptions.txt new file mode 100644 index 0000000..28ef0e7 --- /dev/null +++ b/tools/checks/shared-webui-dialog-width-exceptions.txt @@ -0,0 +1,67 @@ +# Reviewed module-specific Dialog and modal width selectors. +# +# Standard Core widths (460/560/680/1040/1440px) are never valid here. +# Remove an entry when its consumer moves to Dialog size; additions require +# an explicit design review and an inventory explanation. +govoplan-addresses/webui/src/styles/addresses.css|.address-governance-dialog .dialog-panel +govoplan-addresses/webui/src/styles/addresses.css|.address-import-dialog +govoplan-addresses/webui/src/styles/addresses.css|.address-sync-dialog +govoplan-addresses/webui/src/styles/addresses.css|.dialog-panel.address-contact-dialog +govoplan-addresses/webui/src/styles/addresses.css|.dialog-panel.address-member-dialog +govoplan-addresses/webui/src/styles/addresses.css|.dialog-panel.address-quality-dialog, .address-quality-dialog .dialog-panel +govoplan-approvals/webui/src/styles/approvals.css|.approval-request-dialog, .approval-template-dialog +govoplan-approvals/webui/src/styles/approvals.css|.approval-template-history-dialog +govoplan-calendar/webui/src/styles/calendar.css|.calendar-delete-dialog +govoplan-calendar/webui/src/styles/calendar.css|.calendar-migration-dialog +govoplan-calendar/webui/src/styles/calendar.css|.calendar-outbox-dialog +govoplan-calendar/webui/src/styles/calendar.css|.calendar-vevent-dialog +govoplan-campaign/webui/src/styles/campaign-workspace.css|.attachment-linking-detail-modal +govoplan-campaign/webui/src/styles/campaign-workspace.css|.attachment-rules-modal +govoplan-campaign/webui/src/styles/campaign-workspace.css|.campaign-content-library-dialog +govoplan-campaign/webui/src/styles/campaign-workspace.css|.campaign-copy-dialog +govoplan-campaign/webui/src/styles/campaign-workspace.css|.campaign-schedule-dialog +govoplan-campaign/webui/src/styles/campaign-workspace.css|.dialog-panel-wide +govoplan-campaign/webui/src/styles/campaign-workspace.css|.message-preview-modal +govoplan-campaign/webui/src/styles/campaign-workspace.css|.recipient-address-editor-modal +govoplan-campaign/webui/src/styles/campaign-workspace.css|.recipient-import-modal +govoplan-campaign/webui/src/styles/campaign-workspace.css|.template-action-dialog +govoplan-campaign/webui/src/styles/campaign-workspace.css|.template-expression-dialog +govoplan-campaign/webui/src/styles/campaign-workspace.css|.template-preview-modal +govoplan-cases/webui/src/styles/cases.css|.case-decision-dialog +govoplan-cases/webui/src/styles/cases.css|.case-share-dialog +govoplan-committee/webui/src/styles/committee.css|.committee-ballot-dialog +govoplan-committee/webui/src/styles/committee.css|.committee-record-dialog +govoplan-core/webui/src/styles/components.css|.dialog-panel.wysiwyg-editor-dialog +govoplan-core/webui/src/styles/components.css|.guided-config-dialog +govoplan-core/webui/src/styles/components.css|.password-generator-dialog +govoplan-core/webui/src/styles/components.css|.unsaved-changes-dialog +govoplan-core/webui/src/styles/layout.css|.concurrency-conflict-dialog +govoplan-dashboard/webui/src/styles/dashboard.css|.dashboard-widget-config-dialog +govoplan-dataflow/webui/src/styles/dataflow.css|.dataflow-decision-dialog +govoplan-dataflow/webui/src/styles/dataflow.css|.dataflow-definition-dialog +govoplan-dataflow/webui/src/styles/dataflow.css|.dataflow-run-dialog +govoplan-dataflow/webui/src/styles/dataflow.css|.dataflow-source-dialog +govoplan-dataflow/webui/src/styles/dataflow.css|.dataflow-triggers-dialog +govoplan-datasources/webui/src/styles/datasources.css|.datasources-add-dialog +govoplan-datasources/webui/src/styles/datasources.css|.datasources-governance-dialog +govoplan-dist-lists/webui/src/styles/dist-lists.css|.dist-lists-entry-dialog +govoplan-dist-lists/webui/src/styles/dist-lists.css|.dist-lists-explanation-dialog +govoplan-files/webui/src/styles/file-manager.css|.file-dialog +govoplan-files/webui/src/styles/file-manager.css|.file-dialog:has(.archive-preview) +govoplan-files/webui/src/styles/file-manager.css|.file-dialog:has(.connector-sync-grid) +govoplan-files/webui/src/styles/file-manager.css|.file-dialog:has(.file-share-dialog-content) +govoplan-files/webui/src/styles/file-manager.css|.managed-file-chooser-dialog +govoplan-forms-runtime/webui/src/styles/forms-runtime.css|.form-intake-dialog +govoplan-forms/webui/src/styles/forms.css|.form-definition-dialog +govoplan-organizations/webui/src/styles/organizations.css|.organization-upgrade-dialog +govoplan-postbox/webui/src/styles/postbox.css|.postbox-dialog +govoplan-postbox/webui/src/styles/postbox.css|.postbox-message-dialog +govoplan-postbox/webui/src/styles/postbox.css|.postbox-template-dialog +govoplan-projects/webui/src/styles/projects.css|.project-editor-dialog +govoplan-records/webui/src/styles/records.css|.records-dialog +govoplan-reporting/webui/src/styles/reporting.css|.reporting-drill-dialog +govoplan-views/webui/src/styles/views.css|.views-assignment-dialog +govoplan-workflow/webui/src/styles/workflow.css|.workflow-definition-dialog +govoplan-workflow/webui/src/styles/workflow.css|.workflow-runs-dialog +govoplan-workflow/webui/src/styles/workflow.css|.workflow-standard-comparison-dialog +