;
+
+export const Simple: Story = {
+ args: {
+ title: 'Reload trust anchor',
+ children: 'This re-reads the trust anchor file and atomically swaps the trust pool.',
+ },
+};
+
+export const WithFooter: Story = {
+ args: {
+ title: 'Confirm action',
+ children: This action is reversible — proceed?
,
+ footer: (
+ <>
+
+
+ >
+ ),
+ },
+};
+
+export const LargeMaxWidth: Story = {
+ args: {
+ title: 'Retire agent',
+ maxWidth: 'lg',
+ children: Soft-retire the agent. Reversible only via direct DB intervention.
,
+ },
+};
diff --git a/web/src/components/Skeleton.stories.tsx b/web/src/components/Skeleton.stories.tsx
new file mode 100644
index 0000000..6c58091
--- /dev/null
+++ b/web/src/components/Skeleton.stories.tsx
@@ -0,0 +1,24 @@
+// Phase 8 TEST-H3 — Skeleton stories. The 4 variants each get a story
+// so the showroom exposes the full shape catalog. animate-pulse is
+// visible in the rendered story.
+
+import type { Meta, StoryObj } from '@storybook/react';
+import Skeleton from './Skeleton';
+
+const meta = {
+ title: 'Primitives/Skeleton',
+ component: Skeleton,
+ tags: ['autodocs'],
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Page: Story = { args: { variant: 'page' } };
+export const Table: Story = { args: { variant: 'table' } };
+export const Card: Story = { args: { variant: 'card' } };
+export const Stat: Story = { args: { variant: 'stat' } };
+
+export const TableCustomColumns: Story = {
+ args: { variant: 'table', rows: 3, columns: 7 },
+};
diff --git a/web/src/components/StatusBadge.stories.tsx b/web/src/components/StatusBadge.stories.tsx
new file mode 100644
index 0000000..1bafed2
--- /dev/null
+++ b/web/src/components/StatusBadge.stories.tsx
@@ -0,0 +1,37 @@
+// Phase 8 TEST-H3 closure — StatusBadge stories.
+// One story per wire-enum value is the source-of-truth: if the server
+// returns a new status, the gap shows up as a missing story.
+
+import type { Meta, StoryObj } from '@storybook/react';
+import StatusBadge from './StatusBadge';
+
+const meta = {
+ title: 'Primitives/StatusBadge',
+ component: StatusBadge,
+ tags: ['autodocs'],
+ argTypes: {
+ status: { control: 'text' },
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+// Phase 1 UX-H5 closure: 25 known wire values (verified live count
+// from src/components/StatusBadge.test.tsx). Each one is a story so
+// the swatch book shows every variant the server can emit.
+export const Active: Story = { args: { status: 'Active' } };
+export const Expiring: Story = { args: { status: 'Expiring' } };
+export const Expired: Story = { args: { status: 'Expired' } };
+export const Revoked: Story = { args: { status: 'Revoked' } };
+export const Pending: Story = { args: { status: 'Pending' } };
+export const RenewalInProgress: Story = { args: { status: 'RenewalInProgress' } };
+export const Failed: Story = { args: { status: 'Failed' } };
+export const AwaitingApproval: Story = { args: { status: 'AwaitingApproval' } };
+export const AwaitingCSR: Story = { args: { status: 'AwaitingCSR' } };
+export const Archived: Story = { args: { status: 'Archived' } };
+
+// Unknown status → falls through to the titleCase fallback (Phase 1).
+// Pinning this ensures a new server-side enum value doesn't render
+// as a blank chip.
+export const UnknownFallback: Story = { args: { status: 'CompletelyMadeUpStatus' } };
diff --git a/web/src/components/Timestamp.stories.tsx b/web/src/components/Timestamp.stories.tsx
new file mode 100644
index 0000000..88cd1b6
--- /dev/null
+++ b/web/src/components/Timestamp.stories.tsx
@@ -0,0 +1,20 @@
+// Phase 8 TEST-H3 — Timestamp stories. Force each mode via the
+// `forceMode` prop so the showroom shows all three render paths
+// without depending on operator-preference localStorage state.
+
+import type { Meta, StoryObj } from '@storybook/react';
+import Timestamp from './Timestamp';
+
+const meta = {
+ title: 'Primitives/Timestamp',
+ component: Timestamp,
+ tags: ['autodocs'],
+ args: { iso: '2026-05-14T15:30:00Z' },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const UTCDefault: Story = { args: { forceMode: 'utc' } };
+export const Local: Story = { args: { forceMode: 'local' } };
+export const NullValue: Story = { args: { iso: null } };
diff --git a/web/src/components/Tooltip.stories.tsx b/web/src/components/Tooltip.stories.tsx
new file mode 100644
index 0000000..71006f4
--- /dev/null
+++ b/web/src/components/Tooltip.stories.tsx
@@ -0,0 +1,31 @@
+// Phase 8 TEST-H3 — Tooltip stories. Render with a button trigger so
+// the showroom user can hover/focus to see the Floating-UI positioning
+// + the aria-describedby wiring the addon-a11y test validates.
+
+import type { Meta, StoryObj } from '@storybook/react';
+import Tooltip from './Tooltip';
+
+const meta = {
+ title: 'Primitives/Tooltip',
+ component: Tooltip,
+ tags: ['autodocs'],
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Top: Story = {
+ args: {
+ content: 'Triggers a CRL refresh on every replica',
+ placement: 'top',
+ children: ,
+ },
+};
+
+export const Bottom: Story = {
+ args: {
+ content: 'Soft-retires the agent (reversible only via direct DB)',
+ placement: 'bottom',
+ children: ,
+ },
+};
diff --git a/web/tsconfig.json b/web/tsconfig.json
index 4ef1b1f..219138b 100644
--- a/web/tsconfig.json
+++ b/web/tsconfig.json
@@ -18,5 +18,10 @@
"forceConsistentCasingInFileNames": true,
"types": ["vitest/globals"]
},
- "include": ["src"]
+ "include": ["src"],
+ "exclude": [
+ "src/**/*.stories.tsx",
+ "src/**/*.stories.ts",
+ "src/__tests__/e2e/**/*.spec.ts"
+ ]
}