Centralize metric and description layouts

This commit is contained in:
2026-08-18 11:30:39 +02:00
parent 6814a41ae4
commit ee5c881df9
12 changed files with 241 additions and 45 deletions
+7 -3
View File
@@ -18,6 +18,8 @@ domain modules own their compositions.
| Responsive action toolbar | Domain-neutral action and filter grouping for pages, workspaces, editors, and overlays | Density, surface, grouping, flexible space, accessible naming, toolbar help identity, and responsive wrapping are centralized while modules retain action wording, authority, and consequence | `ActionToolbar.tsx`, `layout-primitives.test.tsx`, WYSIWYG, Calendar, Files, Forms, Templates, and the product-wide primitive check |
| Content and form grids | Equal-column content, field, and native-form geometry | Explicit 14 columns, standard gaps, item spans, alignment, and named narrow/workspace/standard/wide collapse points replace generic and module-prefixed copies; unequal domain tracks remain local | `ContentGrid.tsx`, `layout-primitives.test.tsx`, Core dashboard/settings/mail, Calendar dialogs, Forms editor, Datasources, Postbox, Campaign, administration surfaces, and the product-wide primitive check |
| Form sections | Reusable heading/description/action/content grouping inside forms | Plain, separated, and panel variants centralize hierarchy and narrow action placement without moving validation, permissions, values, or domain wording into Core | `FormSection.tsx`, `layout-primitives.test.tsx`, Addresses contact editing, and Quick Access preferences |
| Metric groups | Reusable responsive grouping around metric cards | Fixed one-to-five and auto-fit columns, minimum card widths, density, block/inset/zero spacing, and named collapse points replace the product-wide `metric-grid` class and cross-module dashboard overrides | `MetricGrid.tsx`, `MetricCard.tsx`, `layout-primitives.test.tsx`, Core and Dashboard summaries, administration, Campaign, Ops, Files, Search, and dashboard widgets |
| Description lists | Semantic property and fact presentation | Stacked and inline variants, one-to-five list columns, density, term width, wrapping, and responsive collapse replace both `admin-details-grid` and `detail-list`; `DescriptionItem` preserves native `dt`/`dd` anatomy | `DescriptionList.tsx`, `layout-primitives.test.tsx`, Access and Tenancy administration, Audit, Policy, Campaign reports/imports, Docs, Settings, Ops, and Reporting |
| Dialog anatomy | Shared outer dialog plus composable body and footer regions | Size and administration variants, body padding, descriptions, notices, fixed action wrapping, native form flow, and section grouping are centralized; focus trapping and stack lifecycle remain unchanged | `Dialog.tsx`, `DialogAnatomy.tsx`, `dialog-focus.test.tsx`, `layout-primitives.test.tsx`, Addresses, Calendar, Records, Datasources, Distribution Lists, Files, and Templates |
| 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 |
@@ -38,6 +40,8 @@ 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. Raw toolbar tags,
retired generic grid classes, raw dialog-form wrappers, and module-local copies
of the shared toolbar/grid/form-section/dialog-anatomy components are rejected
by `check-shared-webui-primitives.py`.
retired generic grid and description-list classes, raw dialog-form wrappers,
and module-local copies of the shared toolbar/grid/form-section/dialog-anatomy
components are rejected by `check-shared-webui-primitives.py`. That check also
requires standard dialog widths to use `Dialog size` and keeps every remaining
domain-specific width in a reviewed, decrease-only exception baseline.
+1
View File
@@ -38,6 +38,7 @@ export default function ConfirmDialog({
open={open}
title={title}
role="alertdialog"
size="small"
className="confirm-dialog"
footerClassName="button-row compact-actions"
closeOnBackdrop={!busy}
@@ -512,7 +512,6 @@ export default function CredentialEnvelopeManager({
helpModuleId="access"
onClose={closeEditor}
closeDisabled={saving}
className="adaptive-config-dialog"
footerClassName="button-row compact-actions"
footer={
<>
+59
View File
@@ -0,0 +1,59 @@
import type { HTMLAttributes, ReactNode } from "react";
export type DescriptionListColumns = 1 | 2 | 3 | 4 | 5;
export type DescriptionListDensity = "compact" | "default";
export type DescriptionListCollapseAt = "narrow" | "workspace" | "standard" | "wide" | "never";
export type DescriptionListVariant = "stacked" | "inline";
export type DescriptionListTermWidth = "compact" | "default" | "wide";
export type DescriptionListProps = HTMLAttributes<HTMLDListElement> & {
children: ReactNode;
columns?: DescriptionListColumns;
density?: DescriptionListDensity;
collapseAt?: DescriptionListCollapseAt;
variant?: DescriptionListVariant;
termWidth?: DescriptionListTermWidth;
};
export default function DescriptionList({
children,
columns,
density = "default",
collapseAt = "workspace",
variant = "stacked",
termWidth = "default",
className = "",
...props
}: DescriptionListProps) {
const resolvedColumns = columns ?? (variant === "inline" ? 1 : 2);
return (
<dl
{...props}
className={[
"description-list-layout",
`description-list-columns-${resolvedColumns}`,
`description-list-density-${density}`,
`description-list-collapse-${collapseAt}`,
`description-list-${variant}`,
`description-list-term-${termWidth}`,
className
].filter(Boolean).join(" ")}
>
{children}
</dl>
);
}
export type DescriptionItemProps = HTMLAttributes<HTMLDivElement> & {
term: ReactNode;
children: ReactNode;
};
export function DescriptionItem({ term, children, className = "", ...props }: DescriptionItemProps) {
return (
<div {...props} className={["description-list-item", className].filter(Boolean).join(" ")}>
<dt>{term}</dt>
<dd>{children}</dd>
</div>
);
}
+44
View File
@@ -0,0 +1,44 @@
import type { HTMLAttributes, ReactNode } from "react";
export type MetricGridColumns = 1 | 2 | 3 | 4 | 5 | "auto";
export type MetricGridDensity = "compact" | "default" | "loose";
export type MetricGridSpacing = "block" | "inset" | "none";
export type MetricGridMinimum = "fluid" | "compact" | "default" | "wide";
export type MetricGridCollapseAt = "narrow" | "workspace" | "standard" | "wide" | "never";
export type MetricGridProps = HTMLAttributes<HTMLDivElement> & {
children: ReactNode;
columns?: MetricGridColumns;
density?: MetricGridDensity;
spacing?: MetricGridSpacing;
minimum?: MetricGridMinimum;
collapseAt?: MetricGridCollapseAt;
};
export default function MetricGrid({
children,
columns = 4,
density = "default",
spacing = "block",
minimum = "default",
collapseAt = "workspace",
className = "",
...props
}: MetricGridProps) {
return (
<div
{...props}
className={[
"metric-group-layout",
`metric-group-columns-${columns}`,
`metric-group-density-${density}`,
`metric-group-spacing-${spacing}`,
`metric-group-minimum-${minimum}`,
`metric-group-collapse-${collapseAt}`,
className
].filter(Boolean).join(" ")}
>
{children}
</div>
);
}
@@ -1,6 +1,8 @@
import ContentGrid from "../../components/ContentGrid";
import Card from "../../components/Card";
import MetricCard from "../../components/MetricCard";
import MetricGrid from "../../components/MetricGrid";
import DescriptionList from "../../components/DescriptionList";
import PageLayout from "../../components/PageLayout";
import { usePlatformModules } from "../../platform/ModuleContext";
@@ -13,22 +15,22 @@ export default function DashboardPage() {
description="Install and enable the dashboard module to make this page configurable."
viewportClassName="core-dashboard-page"
>
<div className="metric-grid">
<MetricGrid>
<MetricCard label="i18n:govoplan-core.installed_modules.32b3e799" value={modules.length} tone="info" detail={modules.length ? moduleLabels(modules).join(", ") : "i18n:govoplan-core.core_only.d46ce7d9"} />
<MetricCard label="Dashboard module" value="not installed" tone="neutral" detail="Core fallback is active." />
</div>
</MetricGrid>
<ContentGrid columns={2} collapseAt="workspace" className="">
<Card title="i18n:govoplan-core.installed_modules.32b3e799">
{modules.length === 0 ? <p className="muted">i18n:govoplan-core.no_feature_modules_are_active.e9c2d936</p> :
<dl className="detail-list">
<DescriptionList variant="inline">
{modules.map((module) =>
<div key={module.id}>
<dt>{module.id}</dt>
<dd><strong>{module.label}</strong><span className="muted"> i18n:govoplan-core.v.26c12f1e{module.version}</span></dd>
</div>
)}
</dl>
</DescriptionList>
}
</Card>
<Card title="Home">
+7 -6
View File
@@ -1,3 +1,4 @@
import DescriptionList from "../../components/DescriptionList";
import ContentGrid, { FormGrid } from "../../components/ContentGrid";
import { useEffect, useMemo, useState } from "react";
import { useSearchParams } from "react-router";
@@ -344,12 +345,12 @@ export default function SettingsPage({
</FormGrid>
</Card>
<Card title="i18n:govoplan-core.account_context.5bb0a918">
<dl className="detail-list compact-detail-list">
<DescriptionList variant="inline" density="compact">
<div><dt>i18n:govoplan-core.account_id.c5e0db28</dt><dd>{auth.user.account_id}</dd></div>
<div><dt>i18n:govoplan-core.active_tenant.39b16ee9</dt><dd>{auth.active_tenant?.name || auth.tenant.name}</dd></div>
<div><dt>i18n:govoplan-core.tenant_roles.51aca82d</dt><dd>{auth.roles.filter((role) => role.level !== "system").map((role) => role.name).join(", ") || "i18n:govoplan-core.none.6eef6648"}</dd></div>
<div><dt>i18n:govoplan-core.system_roles.a9461aa6</dt><dd>{auth.roles.filter((role) => role.level === "system").map((role) => role.name).join(", ") || "i18n:govoplan-core.none.6eef6648"}</dd></div>
</dl>
</DescriptionList>
<p className="muted small-note">i18n:govoplan-core.email_changes_and_password_management_require_de.f7443b69</p>
</Card>
</ContentGrid>
@@ -448,14 +449,14 @@ export default function SettingsPage({
ariaLabel="i18n:govoplan-core.theme.a797e309" />
</FormField>
<ThemePreview theme={theme} />
<dl className="detail-list compact-detail-list">
<DescriptionList variant="inline" density="compact">
<div><dt>i18n:govoplan-core.theme.a797e309</dt><dd>{themeLabel(theme)}</dd></div>
<div><dt>i18n:govoplan-core.accent_color.e49578ed</dt><dd>i18n:govoplan-core.default_brand_accent.606ae693</dd></div>
<div><dt>i18n:govoplan-core.language.89b86ab0</dt><dd>{languageLabel}</dd></div>
<div><dt>i18n:govoplan-core.enabled.df174a3f</dt><dd>{enabledLanguages.map((item) => item.code.toUpperCase()).join(", ")}</dd></div>
<div><dt>i18n:govoplan-core.available.7c62a142</dt><dd>{availableLanguages.map((item) => item.code.toUpperCase()).join(", ")}</dd></div>
<div><dt>i18n:govoplan-core.density.f9160c22</dt><dd>{compactTables ? "i18n:govoplan-core.compact_preview.3e06901d" : "i18n:govoplan-core.comfortable.2313707a"}</dd></div>
</dl>
</DescriptionList>
</FormGrid>
</Card>
</ContentGrid>
@@ -530,11 +531,11 @@ export default function SettingsPage({
</FormGrid>
</Card>
<Card title="i18n:govoplan-core.session_state.b7a3d0f4">
<dl className="detail-list compact-detail-list">
<DescriptionList variant="inline" density="compact">
<div><dt>i18n:govoplan-core.browser_session.cc021a31</dt><dd>i18n:govoplan-core.active_via_httponly_cookie.9d05e6ba</dd></div>
<div><dt>i18n:govoplan-core.automation_key.78f1ddbc</dt><dd>{settings.apiKey ? "i18n:govoplan-core.configured.668c5fff" : "i18n:govoplan-core.not_configured.811931bb"}</dd></div>
<div><dt>i18n:govoplan-core.backend_mode.c863723a</dt><dd>{settings.apiBaseUrl ? "i18n:govoplan-core.explicit_api_url.b117b4b8" : "i18n:govoplan-core.same_origin_proxied.c39e6e2b"}</dd></div>
</dl>
</DescriptionList>
<p className="muted small-note">i18n:govoplan-core.tenant_user_mail_server_and_policy_administratio.2f551271</p>
</Card>
</ContentGrid>
+4
View File
@@ -90,6 +90,8 @@ export { default as Dialog } from "./components/Dialog";
export type { DialogBodyPadding, DialogProps, DialogSize, DialogVariant } from "./components/Dialog";
export { DialogActions, DialogForm, DialogSection } from "./components/DialogAnatomy";
export type { DialogActionAlignment, DialogActionsProps, DialogFormProps, DialogSectionProps } from "./components/DialogAnatomy";
export { default as DescriptionList, DescriptionItem } from "./components/DescriptionList";
export type { DescriptionItemProps, DescriptionListCollapseAt, DescriptionListColumns, DescriptionListDensity, DescriptionListProps, DescriptionListTermWidth, DescriptionListVariant } from "./components/DescriptionList";
export { default as DisabledActionTooltip } from "./components/DisabledActionTooltip";
export type { DisabledActionTooltipProps } from "./components/DisabledActionTooltip";
export { default as DismissibleAlert } from "./components/DismissibleAlert";
@@ -118,6 +120,8 @@ export { default as LoadingIndicator } from "./components/LoadingIndicator";
export { default as ExplorerTree } from "./components/ExplorerTree";
export type { ExplorerTreeNodeContext, ExplorerTreeProps } from "./components/ExplorerTree";
export { default as MetricCard } from "./components/MetricCard";
export { default as MetricGrid } from "./components/MetricGrid";
export type { MetricGridCollapseAt, MetricGridColumns, MetricGridDensity, MetricGridMinimum, MetricGridProps, MetricGridSpacing } from "./components/MetricGrid";
export { default as MessageDisplayPanel } from "./components/MessageDisplayPanel";
export type { MessageDisplayAttachment, MessageDisplayField } from "./components/MessageDisplayPanel";
export { default as PageTitle } from "./components/PageTitle";
+50 -18
View File
@@ -522,10 +522,6 @@
gap: 10px;
}
.module-management-metrics {
grid-template-columns: repeat(5, minmax(120px, 1fr));
}
.module-management-row {
display: grid;
grid-template-columns: minmax(0, 1fr) 170px;
@@ -1179,10 +1175,6 @@
min-width: 0;
}
.adaptive-config-dialog {
width: min(1040px, calc(100vw - 40px));
}
.adaptive-config-dialog-body {
background: var(--panel-soft);
}
@@ -3539,9 +3531,6 @@
color: var(--muted);
font-weight: 700;
}
.compact-detail-list {
gap: 8px;
}
.settings-list {
display: grid;
gap: 12px;
@@ -3576,26 +3565,51 @@
.admin-audit-grid .data-grid-header-cell {
top: 0;
}
.admin-details-grid {
.description-list-layout {
--description-list-term-width: 145px;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px 18px;
margin: 0;
}
.admin-details-grid > div {
.description-list-columns-1 { grid-template-columns: minmax(0, 1fr); }
.description-list-columns-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.description-list-columns-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.description-list-columns-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.description-list-columns-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
.description-list-stacked { gap: 10px 18px; }
.description-list-stacked > div {
min-width: 0;
padding: 10px 0;
border-bottom: var(--border-line);
}
.admin-details-grid dt {
.description-list-stacked.description-list-density-compact { gap: 6px 14px; }
.description-list-stacked.description-list-density-compact > div { padding: 6px 0; }
.description-list-inline { gap: 12px; }
.description-list-inline > div {
min-width: 0;
display: grid;
grid-template-columns: var(--description-list-term-width) minmax(0, 1fr);
align-items: center;
gap: 18px;
padding-bottom: 12px;
border-bottom: var(--border-line);
}
.description-list-inline > div:last-child { padding-bottom: 0; border-bottom: 0; }
.description-list-inline.description-list-density-compact { gap: 8px; }
.description-list-term-compact { --description-list-term-width: minmax(92px, auto); }
.description-list-term-default { --description-list-term-width: 145px; }
.description-list-term-wide { --description-list-term-width: 200px; }
.description-list-layout dt {
color: var(--muted);
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
}
.admin-details-grid dd {
.description-list-stacked dd {
margin: 4px 0 0;
overflow-wrap: anywhere;
}
.description-list-inline dt { font-weight: 800; letter-spacing: .04em; }
.description-list-inline dd { min-width: 0; margin: 0; overflow-wrap: anywhere; }
.admin-governance-mode {
display: grid;
gap: 8px;
@@ -3748,10 +3762,28 @@ a.dashboard-contribution-row:hover {
margin-top: 12px;
}
@media (max-width: 900px) {
.admin-details-grid,
.description-list-collapse-workspace,
.admin-tenant-assignment-row {
grid-template-columns: 1fr;
}
.description-list-inline.description-list-collapse-workspace > div {
grid-template-columns: minmax(0, 1fr);
}
}
@media (max-width: 1100px) {
.description-list-collapse-wide { grid-template-columns: 1fr; }
.description-list-inline.description-list-collapse-wide > div { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 980px) {
.description-list-collapse-standard { grid-template-columns: 1fr; }
.description-list-inline.description-list-collapse-standard > div { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 700px) {
.description-list-collapse-narrow { grid-template-columns: 1fr; }
.description-list-inline.description-list-collapse-narrow > div { grid-template-columns: minmax(0, 1fr); }
}
.stage-rail {
max-width: 100%;
-4
View File
@@ -177,10 +177,6 @@
.dialog-section-separated { padding-top: 16px; border-top: var(--border-line); }
.dialog-section-inset { padding: 14px; border: var(--border-line); border-radius: var(--radius); background: var(--panel-soft); }
.confirm-dialog {
width: min(460px, calc(100vw - 40px));
}
.confirm-dialog > .dialog-header,
.confirm-dialog > .dialog-footer {
background: var(--panel);
+35 -9
View File
@@ -217,8 +217,27 @@
.card-header h2 { margin: 0; font-size: 16px; color: var(--text-strong); }
.card-actions { margin-left: auto; display: flex; gap: 10px; flex-wrap: wrap;}
.card-body { padding: 22px 24px; }
.metric-grid { display: grid; grid-template-columns: repeat(4, minmax(140px, 1fr)); gap: 12px; margin-bottom: 18px; }
.metric-grid.inside { margin: 14px 0; }
.metric-group-layout {
--metric-group-column-minimum: 140px;
min-width: 0;
display: grid;
}
.metric-group-columns-1 { grid-template-columns: minmax(0, 1fr); }
.metric-group-columns-2 { grid-template-columns: repeat(2, minmax(var(--metric-group-column-minimum), 1fr)); }
.metric-group-columns-3 { grid-template-columns: repeat(3, minmax(var(--metric-group-column-minimum), 1fr)); }
.metric-group-columns-4 { grid-template-columns: repeat(4, minmax(var(--metric-group-column-minimum), 1fr)); }
.metric-group-columns-5 { grid-template-columns: repeat(5, minmax(var(--metric-group-column-minimum), 1fr)); }
.metric-group-columns-auto { grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--metric-group-column-minimum)), 1fr)); }
.metric-group-density-compact { gap: 8px; }
.metric-group-density-default { gap: 12px; }
.metric-group-density-loose { gap: 18px; }
.metric-group-spacing-block { margin: 0 0 18px; }
.metric-group-spacing-inset { margin: 14px 0; }
.metric-group-spacing-none { margin: 0; }
.metric-group-minimum-fluid { --metric-group-column-minimum: 0px; }
.metric-group-minimum-compact { --metric-group-column-minimum: 112px; }
.metric-group-minimum-default { --metric-group-column-minimum: 140px; }
.metric-group-minimum-wide { --metric-group-column-minimum: 180px; }
.metric-card { background: var(--panel); border: var(--border-line); border-radius: var(--radius); padding: 18px; box-shadow: var(--shadow); border-top: 4px solid var(--line-dark); }
.metric-good { border-top-color: var(--green); } .metric-warning { border-top-color: var(--amber); } .metric-danger { border-top-color: var(--red); } .metric-info { border-top-color: var(--blue); }
.metric-label { color: var(--muted); font-size: 12px; text-transform: uppercase; font-weight: 800; letter-spacing: .05em; }
@@ -280,10 +299,22 @@
.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 { grid-template-columns: 1fr; }
.metric-group-collapse-workspace { grid-template-columns: minmax(0, 1fr); }
.concurrency-conflict-values { grid-template-columns: 1fr; }
}
@media (max-width: 1100px) {
.metric-group-collapse-wide { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 980px) {
.metric-group-collapse-standard { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 700px) {
.metric-group-collapse-narrow { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 700px) {
.action-toolbar-wrap-responsive { align-items: stretch; flex-wrap: wrap; }
.action-toolbar-wrap-responsive > .action-toolbar-group { flex: 1 1 100%; }
@@ -315,11 +346,6 @@
.crumb-link:hover { background: var(--sidebar-hover-bg); color: var(--text-strong); }
.compact-actions { margin: 0; align-items: center; }
.below-grid { margin-top: 18px; }
.detail-list { display: grid; gap: 12px; margin: 0; }
.detail-list div { display: grid; grid-template-columns: 145px minmax(0, 1fr); align-items: center; gap: 18px; padding-bottom: 12px; border-bottom: var(--border-line); }
.detail-list div:last-child { border-bottom: 0; padding-bottom: 0; }
.detail-list dt { color: var(--muted); font-size: 12px; text-transform: uppercase; font-weight: 800; letter-spacing: .04em; }
.detail-list dd { margin: 0; min-width: 0; }
.next-action-card { border-left: 4px solid var(--blue); padding-left: 16px; }
.next-action-card h2 { margin: 0; color: var(--text-strong); font-size: 19px; }
.next-action-card p { margin: 8px 0 0; color: var(--muted); }
@@ -387,7 +413,7 @@
width: 100%;
margin-left: 0;
}
.summary-grid, .detail-list div { grid-template-columns: 1fr; }
.summary-grid { grid-template-columns: 1fr; }
.docs-workspace { grid-template-columns: 1fr; }
.docs-outline { display: none; }
.docs-content { grid-template-columns: 1fr; }
+28
View File
@@ -7,8 +7,10 @@ import ActionToolbar, { ToolbarGroup, ToolbarSpacer } from "../src/components/Ac
import ContentGrid, { FormGrid, FormLayout, GridItem } from "../src/components/ContentGrid";
import Dialog from "../src/components/Dialog";
import { DialogForm, DialogSection } from "../src/components/DialogAnatomy";
import DescriptionList, { DescriptionItem } from "../src/components/DescriptionList";
import FormSection from "../src/components/FormSection";
import { PlatformLanguageProvider } from "../src/i18n/LanguageContext";
import MetricGrid from "../src/components/MetricGrid";
const toolbarMarkup = renderToStaticMarkup(
<PlatformLanguageProvider>
@@ -39,6 +41,32 @@ assert(gridMarkup.includes("content-grid-columns-3"), "content grids expose expl
assert(gridMarkup.includes("content-grid-collapse-wide"), "content grids expose explicit collapse ownership");
assert(gridMarkup.includes("content-grid-item-full"), "grid items can span the complete layout");
const metricMarkup = renderToStaticMarkup(
<MetricGrid columns="auto" density="compact" spacing="inset" minimum="compact" collapseAt="standard">
<div>Open</div><div>Closed</div>
</MetricGrid>
);
assert(metricMarkup.includes("metric-group-columns-auto"), "metric groups own responsive column geometry");
assert(metricMarkup.includes("metric-group-spacing-inset"), "metric groups own their surrounding rhythm");
assert(metricMarkup.includes("metric-group-collapse-standard"), "metric groups expose explicit collapse ownership");
const descriptionMarkup = renderToStaticMarkup(
<DescriptionList columns={3} density="compact" collapseAt="wide">
<DescriptionItem term="Status">Ready</DescriptionItem>
</DescriptionList>
);
assert(descriptionMarkup.startsWith('<dl class="description-list-layout'), "description lists preserve native list semantics");
assert(descriptionMarkup.includes("description-list-columns-3"), "description lists own their column geometry");
assert(descriptionMarkup.includes("<dt>Status</dt><dd>Ready</dd>"), "description items preserve term and description anatomy");
const inlineDescriptionMarkup = renderToStaticMarkup(
<DescriptionList variant="inline" termWidth="compact"><DescriptionItem term="Owner">Ada</DescriptionItem></DescriptionList>
);
assert(inlineDescriptionMarkup.includes("description-list-columns-1"), "inline description lists default to one list column");
assert(inlineDescriptionMarkup.includes("description-list-term-compact"), "inline description lists centralize term sizing");
const formMarkup = renderToStaticMarkup(
<PlatformLanguageProvider>
<FormSection title="Identity" description="Stable identifying values" actions={<button type="button">Reset</button>}>