diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 1971dc0d19..4e51acbce6 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -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. diff --git a/frontend-modern/scripts/shared-template-registry.json b/frontend-modern/scripts/shared-template-registry.json index 399effaa3b..cc0169b7eb 100644 --- a/frontend-modern/scripts/shared-template-registry.json +++ b/frontend-modern/scripts/shared-template-registry.json @@ -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", diff --git a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts index fd0b50483a..d2340f2732 100644 --- a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts +++ b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts @@ -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(