Register selection card group governance

This commit is contained in:
rcourtman 2026-06-12 14:24:56 +01:00
parent 52928706d9
commit 36af6620d3
3 changed files with 99 additions and 0 deletions

View file

@ -2426,6 +2426,12 @@ column chooser trigger, panel title, reset action, empty-state copy, hidden
count badge, dropdown width, and outside-click lifecycle through the shared
shell/state/model split. Table surfaces must compose that primitive instead of
recreating local column chooser buttons or panels.
Selection-card groups are registry-backed too. `SelectionCardGroup` owns the
compact/detail card grid, active-card tone, disabled selection behavior,
pressed-state semantics, title/description styling, and icon container
treatment through the shared shell/state/model split. Settings and feature
surfaces must compose that primitive instead of copying compact/detail
border-card styling locally.
Grouped/list table-mode controls are registry-backed as well.
`GroupedTableModeSegmentedControl` owns the shared `Group by` label,
`Grouped` / `List` labels, tooltip titles, and icons for table mode switching.

View file

@ -273,6 +273,24 @@
"scripts/shared-template-audit.mjs"
]
},
{
"id": "selection-card-group-shell",
"category": "selection-card-group",
"summary": "Selectable settings card groups must compose SelectionCardGroup instead of recreating compact/detail active-card styling and disabled selection behavior locally.",
"canonical": {
"path": "src/components/shared/SelectionCardGroup.tsx",
"export": "SelectionCardGroup"
},
"requiredConsumers": [
{ "path": "src/components/Settings/AISettingsDialogs.tsx" },
{ "path": "src/components/Settings/UpdatesSettingsPanel.tsx" }
],
"proof": [
"src/components/shared/SharedPrimitives.guardrails.test.ts",
"src/components/shared/SelectionCardGroup.test.tsx",
"scripts/shared-template-audit.mjs"
]
},
{
"id": "grouped-table-mode-segmented-control",
"category": "table-mode-control",
@ -424,6 +442,27 @@
"scripts/shared-template-audit.mjs"
]
},
{
"id": "selection-card-group-local-active-card-styles",
"category": "selection-card-group",
"summary": "Settings and feature surfaces must not copy the compact/detail active-card styling owned by SelectionCardGroup.",
"canonical": {
"path": "src/components/shared/selectionCardGroupModel.ts",
"export": "getSelectionCardButtonClass"
},
"scopes": ["src/components/Settings", "src/features", "src/pages"],
"extensions": [".tsx"],
"allPatterns": [
"p-4 rounded-md border-2 transition-all text-left",
"p-3 rounded-md border-2 transition-all text-center"
],
"legacyReason": "Retired migration debt. Selectable settings card groups must use SelectionCardGroup; local compact/detail active-card styling forks are not allowed.",
"allowedPaths": [],
"proof": [
"src/components/shared/SharedPrimitives.guardrails.test.ts",
"scripts/shared-template-audit.mjs"
]
},
{
"id": "grouped-table-mode-local-segmented-control",
"category": "table-mode-control",

View file

@ -244,20 +244,74 @@ describe('shared primitive guardrails', () => {
});
it('routes selectable settings cards through SelectionCardGroup', () => {
const registry = JSON.parse(sharedTemplateRegistrySource) as {
rules?: Array<{
id: string;
canonical?: { path?: string; export?: string };
requiredConsumers?: Array<{ path?: string }>;
}>;
patternGuards?: Array<{
id: string;
canonical?: { path?: string; export?: string };
allPatterns?: string[];
scopes?: string[];
allowedPaths?: string[];
}>;
};
const registeredRule = registry.rules?.find(
(rule) => rule.id === 'selection-card-group-shell',
);
const registeredGuard = registry.patternGuards?.find(
(guard) => guard.id === 'selection-card-group-local-active-card-styles',
);
expect(registeredRule?.canonical?.path).toBe(
'src/components/shared/SelectionCardGroup.tsx',
);
expect(registeredRule?.canonical?.export).toBe('SelectionCardGroup');
expect(registeredRule?.requiredConsumers?.map((consumer) => consumer.path)).toEqual([
'src/components/Settings/AISettingsDialogs.tsx',
'src/components/Settings/UpdatesSettingsPanel.tsx',
]);
expect(registeredGuard?.canonical?.path).toBe(
'src/components/shared/selectionCardGroupModel.ts',
);
expect(registeredGuard?.canonical?.export).toBe(
'getSelectionCardButtonClass',
);
expect(registeredGuard?.allPatterns).toEqual([
'p-4 rounded-md border-2 transition-all text-left',
'p-3 rounded-md border-2 transition-all text-center',
]);
expect(registeredGuard?.allowedPaths ?? []).toHaveLength(0);
expect(registeredGuard?.scopes).toEqual(
expect.arrayContaining([
'src/components/Settings',
'src/features',
'src/pages',
]),
);
expect(selectionCardGroupSource).toContain('useSelectionCardGroupState');
expect(selectionCardGroupSource).toContain('getSelectionCardGroupClass');
expect(selectionCardGroupSource).toContain('getSelectionCardButtonClass');
expect(selectionCardGroupSource).toContain('getSelectionCardTitleClass');
expect(selectionCardGroupSource).toContain('aria-pressed');
expect(selectionCardGroupSource).toContain('disabled={selectionCardGroup.isOptionDisabled');
expect(selectionCardGroupSource).not.toContain('resolveSelectionCardTone');
expect(selectionCardGroupSource).not.toContain('props.onChange(option.value)');
expect(selectionCardGroupStateSource).toContain('export function useSelectionCardGroupState');
expect(selectionCardGroupStateSource).toContain('createMemo');
expect(selectionCardGroupStateSource).toContain('resolveSelectionCardTone');
expect(selectionCardGroupStateSource).toContain('props.disabled || option.disabled');
expect(selectionCardGroupStateSource).toContain('props.onChange(option.value)');
expect(selectionCardGroupModelSource).toContain('resolveSelectionCardGroupVariant');
expect(selectionCardGroupModelSource).toContain('resolveSelectionCardTone');
expect(selectionCardGroupModelSource).toContain('getSelectionCardButtonClass');
expect(selectionCardGroupModelSource).toContain("compact: 'grid grid-cols-2 gap-2'");
expect(selectionCardGroupModelSource).toContain(
"detail: 'grid grid-cols-1 gap-3'",
);
expect(aiSettingsDialogsSource).toContain('SelectionCardGroup');
expect(aiSettingsDialogsSource).toContain('variant="compact"');
expect(aiSettingsDialogsSource).not.toContain(