diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 913c9fb07..64a29fb4c 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -520,6 +520,9 @@ not a replacement status card, CTA band, or page-local nested card. and reporting exports use the shared `success`, `successOutline`, and `successGhost` Button variants instead of carrying page-local emerald action shells. + Shared error-boundary fallback actions are also command buttons: reset, + reload, and retry controls must compose `Button` so emergency UI does not + become a separate local button vocabulary. Compact icon-only row, inline, and floating action controls belong to `ActionIconButton`. Feature surfaces may own the icon choice, click handler, label text, and layout slot, but icon-button size, tone, focus ring, @@ -3391,6 +3394,10 @@ credential slots own the source-specific lifecycle or API meaning, while The `settings-connection-editor-local-*-callout-shell` pattern guards block future connection-editor files from reintroducing amber, red, or rose local notice shells. +Shared error-boundary fallbacks use the same boundary: the fallback owns error +copy and reset/reload handlers, while `CalloutCard` owns danger tone, spacing, +dark-mode styling, and alert layout instead of inline red panels or raw SVG +alert glyphs. Settings loading placeholders must route through the shared `SettingsLoadingSkeleton` primitive instead of local `animate-pulse` blocks. diff --git a/frontend-modern/scripts/shared-template-registry.json b/frontend-modern/scripts/shared-template-registry.json index 3a3cf0690..0b4d8b578 100644 --- a/frontend-modern/scripts/shared-template-registry.json +++ b/frontend-modern/scripts/shared-template-registry.json @@ -389,6 +389,32 @@ "scripts/shared-template-audit.mjs" ] }, + { + "id": "error-boundary-callout-shell", + "category": "callout", + "summary": "Shared error-boundary fallback notices must compose CalloutCard and library icons for tone, spacing, and dark-mode styling instead of recreating local red panels or inline SVG alert glyphs.", + "canonical": { + "path": "src/components/shared/CalloutCard.tsx", + "export": "CalloutCard" + }, + "requiredConsumers": [{ "path": "src/components/ErrorBoundary.tsx" }], + "forbiddenPatterns": [ + { + "path": "src/components/ErrorBoundary.tsx", + "patterns": [ + " void }> = (pr
- - - +
-
-

- Please try again or reload the page. If the problem persists, contact your - administrator. -

-
+
- - +
@@ -102,36 +92,18 @@ export const ComponentErrorBoundary: Component<{ return ( ( -
-
- - - - -
-

{error.message}

- -
+ + )} onError={(error) => { logError(`Error in component ${props.name}`, error); diff --git a/frontend-modern/src/components/__tests__/ErrorBoundary.test.tsx b/frontend-modern/src/components/__tests__/ErrorBoundary.test.tsx index 3221820ef..7a715b591 100644 --- a/frontend-modern/src/components/__tests__/ErrorBoundary.test.tsx +++ b/frontend-modern/src/components/__tests__/ErrorBoundary.test.tsx @@ -1,6 +1,7 @@ import { cleanup, fireEvent, render, screen } from '@solidjs/testing-library'; import type { JSX } from 'solid-js'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import errorBoundarySource from '@/components/ErrorBoundary.tsx?raw'; /* ------------------------------------------------------------------ */ /* Mocks */ @@ -90,6 +91,17 @@ describe('ErrorBoundary', () => { expect(screen.getByText(/Technical details are suppressed/)).toBeInTheDocument(); }); + it('keeps fallback chrome on shared primitives', () => { + expect(errorBoundarySource).toContain('CalloutCard'); + expect(errorBoundarySource).toContain('Button'); + expect(errorBoundarySource).toContain('lucide-solid/icons/alert-triangle'); + expect(errorBoundarySource).not.toContain(' { diff --git a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts index 917082144..9070edeaa 100644 --- a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts +++ b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts @@ -115,6 +115,7 @@ import inlineDetailTableRowSource from '@/components/shared/InlineDetailTableRow import sharedTemplateRegistrySource from '../../../scripts/shared-template-registry.json?raw'; import discoveryTabSource from '@/components/Discovery/DiscoveryTab.tsx?raw'; import emailProviderSelectSource from '@/components/Alerts/EmailProviderSelect.tsx?raw'; +import errorBoundarySource from '@/components/ErrorBoundary.tsx?raw'; import incidentTimelinePanelSource from '@/components/Alerts/IncidentTimelinePanel.tsx?raw'; import alertDetailPresentationSource from '@/utils/alertDetailPresentation.ts?raw'; import alertSeverityPresentationSource from '@/utils/alertSeverityPresentation.ts?raw'; @@ -2881,6 +2882,7 @@ describe('shared primitive guardrails', () => { expect(registeredRule?.canonical?.export).toBe('Button'); expect(registeredRule?.requiredConsumers?.map((consumer) => consumer.path)).toEqual([ 'src/components/AI/Chat/ChatMessages.tsx', + 'src/components/ErrorBoundary.tsx', 'src/components/Infrastructure/ResourceDetailDrawer.tsx', 'src/components/Infrastructure/ResourceDetailDrawerDebugTab.tsx', 'src/components/Settings/AgentProfilesPanel.tsx', @@ -2951,8 +2953,26 @@ describe('shared primitive guardrails', () => { 'inline-flex items-center justify-center rounded-md px-3 py-2 text-sm font-medium text-emerald-900 hover:bg-emerald-100', ]), }), + expect.objectContaining({ + path: 'src/components/ErrorBoundary.tsx', + patterns: expect.arrayContaining([ + 'px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700', + 'px-4 py-2 bg-slate-600 text-white rounded hover:bg-slate-700', + 'text-xs px-2 py-1 bg-red-600 text-white rounded hover:bg-red-700', + ]), + }), ]), ); + expect(errorBoundarySource).toContain('Button'); + expect(errorBoundarySource).not.toContain( + 'px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700', + ); + expect(errorBoundarySource).not.toContain( + 'px-4 py-2 bg-slate-600 text-white rounded hover:bg-slate-700', + ); + expect(errorBoundarySource).not.toContain( + 'text-xs px-2 py-1 bg-red-600 text-white rounded hover:bg-red-700', + ); expect(registeredGuard?.canonical?.path).toBe('src/components/shared/buttonModel.ts'); expect(registeredGuard?.canonical?.export).toBe('getButtonClass'); expect(registeredGuard?.allPatterns).toEqual([ @@ -3677,6 +3697,44 @@ describe('shared primitive guardrails', () => { ); }); + it('routes shared error-boundary fallbacks through shared callout and button primitives', () => { + const registry = JSON.parse(sharedTemplateRegistrySource) as { + rules?: Array<{ + id: string; + canonical?: { path?: string; export?: string }; + requiredConsumers?: Array<{ path?: string }>; + forbiddenPatterns?: Array<{ path?: string; patterns?: string[] }>; + }>; + }; + const registeredRule = registry.rules?.find( + (rule) => rule.id === 'error-boundary-callout-shell', + ); + + expect(registeredRule?.canonical?.path).toBe('src/components/shared/CalloutCard.tsx'); + expect(registeredRule?.canonical?.export).toBe('CalloutCard'); + expect(registeredRule?.requiredConsumers?.map((consumer) => consumer.path)).toEqual([ + 'src/components/ErrorBoundary.tsx', + ]); + expect(registeredRule?.forbiddenPatterns).toEqual([ + { + path: 'src/components/ErrorBoundary.tsx', + patterns: [ + ' { const registry = JSON.parse(sharedTemplateRegistrySource) as { rules?: Array<{