@awarebydefault/display-case 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +309 -0
  3. package/display-case.prompt.md +64 -0
  4. package/docs/ai-agents.md +126 -0
  5. package/docs/cli.md +99 -0
  6. package/docs/configuration.md +410 -0
  7. package/docs/documentation-panel.md +50 -0
  8. package/docs/examples/README.md +14 -0
  9. package/docs/examples/multi-variant.case.tsx +30 -0
  10. package/docs/examples/plain.case.tsx +22 -0
  11. package/docs/examples/tweak-control.placard.md +80 -0
  12. package/docs/examples/tweaks.case.tsx +39 -0
  13. package/docs/hierarchy.md +59 -0
  14. package/docs/quick-start.md +78 -0
  15. package/docs/style-engines.md +180 -0
  16. package/docs/testing.md +245 -0
  17. package/docs/theming.md +97 -0
  18. package/docs/tweaks.md +75 -0
  19. package/docs/writing-cases.md +144 -0
  20. package/docs/writing-placard-docs.md +194 -0
  21. package/package.json +113 -0
  22. package/skills/display-case-author-case/README.md +20 -0
  23. package/skills/display-case-author-case/SKILL.md +40 -0
  24. package/skills/display-case-author-placard-doc/README.md +24 -0
  25. package/skills/display-case-author-placard-doc/SKILL.md +65 -0
  26. package/skills/display-case-review/README.md +19 -0
  27. package/skills/display-case-review/SKILL.md +30 -0
  28. package/skills/display-case-snapshot/README.md +20 -0
  29. package/skills/display-case-snapshot/SKILL.md +29 -0
  30. package/src/checks/a11y-scanner.test.ts +240 -0
  31. package/src/checks/a11y-scanner.ts +410 -0
  32. package/src/checks/check-text.test.ts +53 -0
  33. package/src/checks/check-text.ts +78 -0
  34. package/src/checks/check.test.ts +194 -0
  35. package/src/checks/check.ts +473 -0
  36. package/src/checks/providers/pixelmatch-diff.test.ts +79 -0
  37. package/src/checks/providers/pixelmatch-diff.ts +30 -0
  38. package/src/checks/providers/playwright-driver.ts +104 -0
  39. package/src/checks/ssr-check.test.ts +73 -0
  40. package/src/checks/ssr-check.ts +96 -0
  41. package/src/checks/structure-check.cross-package.test.ts +165 -0
  42. package/src/checks/structure-check.test.ts +651 -0
  43. package/src/checks/structure-check.ts +988 -0
  44. package/src/checks/tokens-check.test.ts +159 -0
  45. package/src/checks/tokens-check.ts +162 -0
  46. package/src/cli.ts +218 -0
  47. package/src/commands/agents.test.ts +24 -0
  48. package/src/commands/agents.ts +28 -0
  49. package/src/commands/init-run.test.ts +123 -0
  50. package/src/commands/init.test.ts +63 -0
  51. package/src/commands/init.ts +412 -0
  52. package/src/commands/publish.test.ts +210 -0
  53. package/src/commands/publish.ts +292 -0
  54. package/src/core/affected.test.ts +99 -0
  55. package/src/core/affected.ts +144 -0
  56. package/src/core/catalog.test.ts +152 -0
  57. package/src/core/catalog.ts +92 -0
  58. package/src/core/discovery.test.ts +184 -0
  59. package/src/core/discovery.ts +250 -0
  60. package/src/core/manifest.ts +41 -0
  61. package/src/core/mdx-lite/__fixtures__/box-stub.tsx +7 -0
  62. package/src/core/mdx-lite/index.ts +393 -0
  63. package/src/core/mdx-lite/mdx-lite.test.ts +345 -0
  64. package/src/core/mdx-plugin.test.ts +60 -0
  65. package/src/core/mdx-plugin.ts +30 -0
  66. package/src/flow-step.test-d.ts +39 -0
  67. package/src/index.test.ts +100 -0
  68. package/src/index.ts +564 -0
  69. package/src/render/collect-styles.emotion.test.tsx +114 -0
  70. package/src/render/collect-styles.test.tsx +72 -0
  71. package/src/render/collect-styles.ts +33 -0
  72. package/src/render/documents.test.ts +184 -0
  73. package/src/render/documents.ts +88 -0
  74. package/src/render/render-node.test.tsx +160 -0
  75. package/src/render/render-node.tsx +133 -0
  76. package/src/render/ssr-primer.test.tsx +25 -0
  77. package/src/render/ssr-primer.tsx +54 -0
  78. package/src/render/ssr-render.test.tsx +142 -0
  79. package/src/render/ssr-render.tsx +63 -0
  80. package/src/render/ssr-shell.test.tsx +57 -0
  81. package/src/render/ssr-shell.tsx +54 -0
  82. package/src/server/prod-server.ts +237 -0
  83. package/src/server/server.test.ts +117 -0
  84. package/src/server/server.ts +1039 -0
  85. package/src/style-engine.test-d.ts +37 -0
  86. package/src/testing/test-helpers.ts +27 -0
  87. package/src/types/pixelmatch.d.ts +12 -0
  88. package/src/ui/browser-entry.tsx +51 -0
  89. package/src/ui/chrome.css +485 -0
  90. package/src/ui/design-system/README.md +88 -0
  91. package/src/ui/design-system/components/controls/Button.case.tsx +52 -0
  92. package/src/ui/design-system/components/controls/Button.css +89 -0
  93. package/src/ui/design-system/components/controls/Button.placard.md +14 -0
  94. package/src/ui/design-system/components/controls/Button.test.tsx +45 -0
  95. package/src/ui/design-system/components/controls/Button.tsx +41 -0
  96. package/src/ui/design-system/components/controls/IconButton.case.tsx +52 -0
  97. package/src/ui/design-system/components/controls/IconButton.css +67 -0
  98. package/src/ui/design-system/components/controls/IconButton.placard.md +13 -0
  99. package/src/ui/design-system/components/controls/IconButton.test.tsx +39 -0
  100. package/src/ui/design-system/components/controls/IconButton.tsx +47 -0
  101. package/src/ui/design-system/components/controls/Input.case.tsx +50 -0
  102. package/src/ui/design-system/components/controls/Input.css +52 -0
  103. package/src/ui/design-system/components/controls/Input.placard.md +12 -0
  104. package/src/ui/design-system/components/controls/Input.test.tsx +43 -0
  105. package/src/ui/design-system/components/controls/Input.tsx +45 -0
  106. package/src/ui/design-system/components/controls/Select.case.tsx +48 -0
  107. package/src/ui/design-system/components/controls/Select.css +44 -0
  108. package/src/ui/design-system/components/controls/Select.placard.md +15 -0
  109. package/src/ui/design-system/components/controls/Select.test.tsx +57 -0
  110. package/src/ui/design-system/components/controls/Select.tsx +58 -0
  111. package/src/ui/design-system/components/controls/SelectMenu.case.tsx +100 -0
  112. package/src/ui/design-system/components/controls/SelectMenu.css +72 -0
  113. package/src/ui/design-system/components/controls/SelectMenu.placard.md +18 -0
  114. package/src/ui/design-system/components/controls/SelectMenu.test.tsx +66 -0
  115. package/src/ui/design-system/components/controls/SelectMenu.tsx +377 -0
  116. package/src/ui/design-system/components/index.ts +66 -0
  117. package/src/ui/design-system/components/primer-specimen/ColorRamp.case.tsx +44 -0
  118. package/src/ui/design-system/components/primer-specimen/ColorRamp.placard.md +15 -0
  119. package/src/ui/design-system/components/primer-specimen/ColorRamp.tsx +51 -0
  120. package/src/ui/design-system/components/primer-specimen/DefinitionList.case.tsx +38 -0
  121. package/src/ui/design-system/components/primer-specimen/DefinitionList.placard.md +15 -0
  122. package/src/ui/design-system/components/primer-specimen/DefinitionList.tsx +41 -0
  123. package/src/ui/design-system/components/primer-specimen/FontFamilies.case.tsx +24 -0
  124. package/src/ui/design-system/components/primer-specimen/FontFamilies.placard.md +12 -0
  125. package/src/ui/design-system/components/primer-specimen/FontFamilies.tsx +41 -0
  126. package/src/ui/design-system/components/primer-specimen/GlyphGrid.case.tsx +27 -0
  127. package/src/ui/design-system/components/primer-specimen/GlyphGrid.placard.md +13 -0
  128. package/src/ui/design-system/components/primer-specimen/GlyphGrid.tsx +34 -0
  129. package/src/ui/design-system/components/primer-specimen/LayoutMock.case.tsx +36 -0
  130. package/src/ui/design-system/components/primer-specimen/LayoutMock.placard.md +7 -0
  131. package/src/ui/design-system/components/primer-specimen/LayoutMock.tsx +36 -0
  132. package/src/ui/design-system/components/primer-specimen/SpacingScale.case.tsx +20 -0
  133. package/src/ui/design-system/components/primer-specimen/SpacingScale.placard.md +12 -0
  134. package/src/ui/design-system/components/primer-specimen/SpacingScale.tsx +33 -0
  135. package/src/ui/design-system/components/primer-specimen/SpecimenBoxRow.case.tsx +56 -0
  136. package/src/ui/design-system/components/primer-specimen/SpecimenBoxRow.placard.md +17 -0
  137. package/src/ui/design-system/components/primer-specimen/SpecimenBoxRow.tsx +45 -0
  138. package/src/ui/design-system/components/primer-specimen/StatusList.case.tsx +17 -0
  139. package/src/ui/design-system/components/primer-specimen/StatusList.placard.md +16 -0
  140. package/src/ui/design-system/components/primer-specimen/StatusList.tsx +39 -0
  141. package/src/ui/design-system/components/primer-specimen/SwatchGrid.case.tsx +26 -0
  142. package/src/ui/design-system/components/primer-specimen/SwatchGrid.placard.md +15 -0
  143. package/src/ui/design-system/components/primer-specimen/SwatchGrid.tsx +42 -0
  144. package/src/ui/design-system/components/primer-specimen/TypeScale.case.tsx +23 -0
  145. package/src/ui/design-system/components/primer-specimen/TypeScale.placard.md +14 -0
  146. package/src/ui/design-system/components/primer-specimen/TypeScale.tsx +34 -0
  147. package/src/ui/design-system/components/primer-specimen/WeightSpecimen.case.tsx +28 -0
  148. package/src/ui/design-system/components/primer-specimen/WeightSpecimen.placard.md +15 -0
  149. package/src/ui/design-system/components/primer-specimen/WeightSpecimen.tsx +46 -0
  150. package/src/ui/design-system/components/primer-specimen/index.ts +31 -0
  151. package/src/ui/design-system/components/primer-specimen/styles.css +476 -0
  152. package/src/ui/design-system/components/shell/A11yPage.case.tsx +237 -0
  153. package/src/ui/design-system/components/shell/A11yPage.placard.md +15 -0
  154. package/src/ui/design-system/components/shell/CaseTemplate.case.tsx +32 -0
  155. package/src/ui/design-system/components/shell/CaseTemplate.placard.md +5 -0
  156. package/src/ui/design-system/components/shell/CasesPage.case.tsx +141 -0
  157. package/src/ui/design-system/components/shell/CasesPage.placard.md +12 -0
  158. package/src/ui/design-system/components/shell/PrimerPage.case.tsx +22 -0
  159. package/src/ui/design-system/components/shell/PrimerPage.placard.md +3 -0
  160. package/src/ui/design-system/components/shell/PrimerTemplate.case.tsx +22 -0
  161. package/src/ui/design-system/components/shell/PrimerTemplate.placard.md +5 -0
  162. package/src/ui/design-system/components/shell/ShellView.case.tsx +57 -0
  163. package/src/ui/design-system/components/shell/ShellView.placard.md +5 -0
  164. package/src/ui/design-system/components/shell/ShellView.tsx +678 -0
  165. package/src/ui/design-system/components/shell/shell-fixtures.tsx +727 -0
  166. package/src/ui/design-system/components/showcase/A11yBadge.case.tsx +46 -0
  167. package/src/ui/design-system/components/showcase/A11yBadge.css +27 -0
  168. package/src/ui/design-system/components/showcase/A11yBadge.placard.md +11 -0
  169. package/src/ui/design-system/components/showcase/A11yBadge.test.tsx +31 -0
  170. package/src/ui/design-system/components/showcase/A11yBadge.tsx +41 -0
  171. package/src/ui/design-system/components/showcase/A11yPanel.case.tsx +121 -0
  172. package/src/ui/design-system/components/showcase/A11yPanel.css +198 -0
  173. package/src/ui/design-system/components/showcase/A11yPanel.placard.md +19 -0
  174. package/src/ui/design-system/components/showcase/A11yPanel.test.tsx +81 -0
  175. package/src/ui/design-system/components/showcase/A11yPanel.tsx +144 -0
  176. package/src/ui/design-system/components/showcase/Chip.case.tsx +48 -0
  177. package/src/ui/design-system/components/showcase/Chip.css +51 -0
  178. package/src/ui/design-system/components/showcase/Chip.placard.md +13 -0
  179. package/src/ui/design-system/components/showcase/Chip.test.tsx +46 -0
  180. package/src/ui/design-system/components/showcase/Chip.tsx +54 -0
  181. package/src/ui/design-system/components/showcase/Eyebrow.case.tsx +30 -0
  182. package/src/ui/design-system/components/showcase/Eyebrow.css +16 -0
  183. package/src/ui/design-system/components/showcase/Eyebrow.placard.md +10 -0
  184. package/src/ui/design-system/components/showcase/Eyebrow.test.tsx +38 -0
  185. package/src/ui/design-system/components/showcase/Eyebrow.tsx +29 -0
  186. package/src/ui/design-system/components/showcase/FlowNav.case.tsx +35 -0
  187. package/src/ui/design-system/components/showcase/FlowNav.css +29 -0
  188. package/src/ui/design-system/components/showcase/FlowNav.placard.md +13 -0
  189. package/src/ui/design-system/components/showcase/FlowNav.test.tsx +48 -0
  190. package/src/ui/design-system/components/showcase/FlowNav.tsx +58 -0
  191. package/src/ui/design-system/components/showcase/ImpactTag.case.tsx +19 -0
  192. package/src/ui/design-system/components/showcase/ImpactTag.css +36 -0
  193. package/src/ui/design-system/components/showcase/ImpactTag.placard.md +14 -0
  194. package/src/ui/design-system/components/showcase/ImpactTag.test.tsx +40 -0
  195. package/src/ui/design-system/components/showcase/ImpactTag.tsx +35 -0
  196. package/src/ui/design-system/components/showcase/NavItem.case.tsx +86 -0
  197. package/src/ui/design-system/components/showcase/NavItem.css +111 -0
  198. package/src/ui/design-system/components/showcase/NavItem.placard.md +13 -0
  199. package/src/ui/design-system/components/showcase/NavItem.test.tsx +65 -0
  200. package/src/ui/design-system/components/showcase/NavItem.tsx +95 -0
  201. package/src/ui/design-system/components/showcase/RenderAddress.case.tsx +21 -0
  202. package/src/ui/design-system/components/showcase/RenderAddress.css +35 -0
  203. package/src/ui/design-system/components/showcase/RenderAddress.placard.md +7 -0
  204. package/src/ui/design-system/components/showcase/RenderAddress.test.tsx +26 -0
  205. package/src/ui/design-system/components/showcase/RenderAddress.tsx +43 -0
  206. package/src/ui/design-system/components/showcase/SegmentedToggle.case.tsx +84 -0
  207. package/src/ui/design-system/components/showcase/SegmentedToggle.css +61 -0
  208. package/src/ui/design-system/components/showcase/SegmentedToggle.placard.md +21 -0
  209. package/src/ui/design-system/components/showcase/SegmentedToggle.test.tsx +81 -0
  210. package/src/ui/design-system/components/showcase/SegmentedToggle.tsx +75 -0
  211. package/src/ui/design-system/components/showcase/Sidebar.case.tsx +67 -0
  212. package/src/ui/design-system/components/showcase/Sidebar.css +6 -0
  213. package/src/ui/design-system/components/showcase/Sidebar.placard.md +14 -0
  214. package/src/ui/design-system/components/showcase/Sidebar.test.tsx +32 -0
  215. package/src/ui/design-system/components/showcase/Sidebar.tsx +30 -0
  216. package/src/ui/design-system/components/showcase/Stage.case.tsx +51 -0
  217. package/src/ui/design-system/components/showcase/Stage.css +91 -0
  218. package/src/ui/design-system/components/showcase/Stage.placard.md +15 -0
  219. package/src/ui/design-system/components/showcase/Stage.test.tsx +84 -0
  220. package/src/ui/design-system/components/showcase/Stage.tsx +97 -0
  221. package/src/ui/design-system/components/showcase/TweaksPanel.case.tsx +81 -0
  222. package/src/ui/design-system/components/showcase/TweaksPanel.css +169 -0
  223. package/src/ui/design-system/components/showcase/TweaksPanel.placard.md +20 -0
  224. package/src/ui/design-system/components/showcase/TweaksPanel.tsx +230 -0
  225. package/src/ui/design-system/components/showcase/Wordmark.case.tsx +42 -0
  226. package/src/ui/design-system/components/showcase/Wordmark.css +31 -0
  227. package/src/ui/design-system/components/showcase/Wordmark.placard.md +10 -0
  228. package/src/ui/design-system/components/showcase/Wordmark.test.tsx +22 -0
  229. package/src/ui/design-system/components/showcase/Wordmark.tsx +22 -0
  230. package/src/ui/design-system/primer-specimens/brand.tsx +26 -0
  231. package/src/ui/design-system/primer-specimens/colors.tsx +83 -0
  232. package/src/ui/design-system/primer-specimens/components.tsx +308 -0
  233. package/src/ui/design-system/primer-specimens/foundations.tsx +71 -0
  234. package/src/ui/design-system/primer-specimens/index.ts +25 -0
  235. package/src/ui/design-system/primer-specimens/showcase.tsx +68 -0
  236. package/src/ui/design-system/primer-specimens/spacing.tsx +101 -0
  237. package/src/ui/design-system/primer-specimens/type.tsx +75 -0
  238. package/src/ui/design-system/primer.mdx +236 -0
  239. package/src/ui/design-system/styles.css +14 -0
  240. package/src/ui/design-system/tokens/colors.css +172 -0
  241. package/src/ui/design-system/tokens/fonts.css +18 -0
  242. package/src/ui/design-system/tokens/spacing.css +48 -0
  243. package/src/ui/design-system/tokens/typography.css +49 -0
  244. package/src/ui/markdown.test.tsx +54 -0
  245. package/src/ui/markdown.tsx +19 -0
  246. package/src/ui/primer-mount.tsx +76 -0
  247. package/src/ui/primer.css +175 -0
  248. package/src/ui/primer.tsx +277 -0
  249. package/src/ui/render-mount.tsx +284 -0
  250. package/src/ui/shell-core.test.ts +340 -0
  251. package/src/ui/shell-core.ts +295 -0
  252. package/src/ui/shell.tsx +60 -0
  253. package/src/ui/test-ids.ts +53 -0
  254. package/src/ui/use-shell.ts +1230 -0
@@ -0,0 +1,144 @@
1
+ import { useState } from 'react'
2
+ import type { A11yViolation } from '../../../../index'
3
+ import { DcTestIds } from '../../../test-ids'
4
+ import { IconButton } from '../controls/IconButton'
5
+ import { Eyebrow } from './Eyebrow'
6
+ import { ImpactTag, impactRank } from './ImpactTag'
7
+
8
+ /**
9
+ * Display Case — A11yPanel
10
+ * The stage's accessibility verdict for the variant on the stage. Four states:
11
+ * `'pending'` while a scan is in flight (a calm, pulsing "Scanning…" bar),
12
+ * `'unavailable'` when the scan prerequisite can't run, an empty array for a
13
+ * clean pass (green bar), or the violations (danger bar + a collapsible list,
14
+ * ordered worst-first with an {@link ImpactTag}). Only the violations state has a
15
+ * body to expand/collapse — the others say everything in the single header bar.
16
+ * Pass `onRescan` to show the ⟳ re-scan control.
17
+ *
18
+ * Height-capped + internally scrollable, with a sticky header so the verdict and
19
+ * controls never scroll away. Padding lives on the head and body (not the
20
+ * section) so the sticky header's background spans edge-to-edge while scrolling.
21
+ */
22
+
23
+ export interface A11yPanelProps {
24
+ /** The active variant's verdict: `'pending'`, `'unavailable'`, `[]` (clean),
25
+ * or the violations. */
26
+ violations: A11yViolation[] | 'pending' | 'unavailable'
27
+ /** How the resolved verdict animates in. `'cascade'` (the verdict just resolved
28
+ * from a live scan): violation rows fade + rise with a per-row stagger.
29
+ * `'all'` (default — already scanned, e.g. navigated to a cached variant):
30
+ * the verdict fades in at once, like the stage and tweaks panel. */
31
+ reveal?: 'cascade' | 'all'
32
+ /** Re-run the audit for the viewed variant. When omitted (a static exhibit
33
+ * that doesn't wire it), the ⟳ re-scan control is hidden. */
34
+ onRescan?: () => void
35
+ /** Snap the accent/status colour instead of easing it. Set while the panel is
36
+ * mid-navigation crossfade (faded out) so the new exhibit's verdict colour is
37
+ * in place before it fades back in, rather than easing the old colour across
38
+ * the fade. The colour ease stays on for in-place state changes. */
39
+ instantColor?: boolean
40
+ }
41
+
42
+ export function A11yPanel({
43
+ violations,
44
+ reveal = 'all',
45
+ onRescan,
46
+ instantColor,
47
+ }: A11yPanelProps) {
48
+ // Show/hide is a local UI concern (no need to persist like the docs panel).
49
+ // Only the violations list collapses; the other states are a single bar.
50
+ const [open, setOpen] = useState(true)
51
+ const list = Array.isArray(violations) ? violations : []
52
+ const pending = violations === 'pending'
53
+ const unavailable = violations === 'unavailable'
54
+ const passing = Array.isArray(violations) && violations.length === 0
55
+ const collapsible = list.length > 0
56
+ let state: 'pending' | 'unavailable' | 'pass' | 'fail'
57
+ if (pending) state = 'pending'
58
+ else if (unavailable) state = 'unavailable'
59
+ else if (passing) state = 'pass'
60
+ else state = 'fail'
61
+ let status: string
62
+ if (pending) status = 'Scanning…'
63
+ else if (unavailable) status = 'Unavailable'
64
+ else if (passing) status = 'Passes WCAG A/AA'
65
+ else status = `${list.length} violation${list.length === 1 ? '' : 's'}`
66
+ // Worst first, then most-affected first — so the most urgent fixes lead.
67
+ const sorted = collapsible
68
+ ? [...list].sort(
69
+ (a, b) =>
70
+ impactRank(a.impact) - impactRank(b.impact) || b.nodes - a.nodes,
71
+ )
72
+ : []
73
+ return (
74
+ <section
75
+ className="dcui-a11y"
76
+ data-testid={DcTestIds.a11yPanel}
77
+ data-state={state}
78
+ data-reveal={reveal}
79
+ data-instant-color={instantColor ? 'true' : undefined}
80
+ data-open={collapsible && open ? 'true' : undefined}
81
+ aria-label="Accessibility">
82
+ <div className="dcui-a11y-head">
83
+ <Eyebrow>Accessibility</Eyebrow>
84
+ <div className="dcui-a11y-head-right">
85
+ <span className="dcui-a11y-status">{status}</span>
86
+ {/* Re-scan the viewed variant. Hidden while a scan is in flight, and
87
+ absent entirely when no handler is wired. */}
88
+ {onRescan && !pending && (
89
+ <IconButton
90
+ glyph="⟳"
91
+ variant="bare"
92
+ size="sm"
93
+ data-testid={DcTestIds.a11yRescan}
94
+ label="Re-scan accessibility"
95
+ onClick={onRescan}
96
+ />
97
+ )}
98
+ {/* Only the violations list is collapsible — the other states are a
99
+ single self-explaining bar with no body to toggle. */}
100
+ {collapsible && (
101
+ <IconButton
102
+ glyph={open ? '▾' : '▸'}
103
+ variant="bare"
104
+ size="sm"
105
+ data-testid={DcTestIds.a11yToggle}
106
+ aria-expanded={open}
107
+ label={
108
+ open
109
+ ? 'Hide accessibility details'
110
+ : 'Show accessibility details'
111
+ }
112
+ onClick={() => setOpen((o) => !o)}
113
+ />
114
+ )}
115
+ </div>
116
+ </div>
117
+ {/* Always render the wrapper so the 0fr→1fr height transition fires when a
118
+ scan resolves to violations; the list mounts inside only when there are
119
+ violations to show. */}
120
+ <div className="dcui-a11y-collapse">
121
+ {collapsible && (
122
+ <ul className="dcui-a11y-list">
123
+ {sorted.map((v, i) => (
124
+ <li
125
+ key={v.id}
126
+ className="dcui-a11y-item"
127
+ data-testid={DcTestIds.a11yViolation(v.id)}
128
+ // Stagger the cascade; reduced-motion drops the animation, so this
129
+ // delay has no effect there (everything appears at once).
130
+ style={{ animationDelay: `${i * 100}ms` }}>
131
+ {v.impact && <ImpactTag impact={v.impact} />}
132
+ <code className="dcui-a11y-id">{v.id}</code>
133
+ <span className="dcui-a11y-help">{v.help}</span>
134
+ <span className="dcui-a11y-nodes">
135
+ {v.nodes} node{v.nodes === 1 ? '' : 's'}
136
+ </span>
137
+ </li>
138
+ ))}
139
+ </ul>
140
+ )}
141
+ </div>
142
+ </section>
143
+ )
144
+ }
@@ -0,0 +1,48 @@
1
+ import { defineCases, tweak } from '@awarebydefault/display-case'
2
+ import { Chip } from './Chip'
3
+
4
+ export default defineCases(
5
+ 'Chip',
6
+ {
7
+ Playground: {
8
+ tweaks: {
9
+ label: tweak.text('atom'),
10
+ variant: tweak.choice(['default', 'accent', 'solid'], 'default'),
11
+ current: tweak.boolean(false),
12
+ withIndex: tweak.boolean(false),
13
+ index: tweak.number(1),
14
+ clickable: tweak.boolean(false),
15
+ },
16
+ render: (t) => (
17
+ <Chip
18
+ variant={t.variant as 'default' | 'accent' | 'solid'}
19
+ current={t.current}
20
+ index={t.withIndex ? t.index : undefined}
21
+ onClick={t.clickable ? () => {} : undefined}>
22
+ {t.label}
23
+ </Chip>
24
+ ),
25
+ },
26
+ Variants: () => (
27
+ <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
28
+ <Chip>atom</Chip>
29
+ <Chip variant="accent">accent</Chip>
30
+ <Chip variant="solid">solid</Chip>
31
+ </div>
32
+ ),
33
+ Steps: () => (
34
+ <div style={{ display: 'flex', gap: 8 }}>
35
+ <Chip index={1} onClick={() => {}}>
36
+ Request link
37
+ </Chip>
38
+ <Chip index={2} current onClick={() => {}}>
39
+ Check email
40
+ </Chip>
41
+ <Chip index={3} onClick={() => {}}>
42
+ Signed in
43
+ </Chip>
44
+ </div>
45
+ ),
46
+ },
47
+ { level: 'atom' },
48
+ )
@@ -0,0 +1,51 @@
1
+ .dcui-chip {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ gap: var(--dc-space-2);
5
+ font-family: var(--dc-font-mono);
6
+ font-size: var(--dc-text-xs);
7
+ font-weight: var(--dc-weight-medium);
8
+ line-height: 1;
9
+ color: var(--dc-fg-muted);
10
+ background: var(--dc-bg-subtle);
11
+ border: 1px solid var(--dc-border);
12
+ border-radius: var(--dc-radius-full);
13
+ padding: 0.3125rem var(--dc-space-4);
14
+ white-space: nowrap;
15
+ }
16
+ button.dcui-chip {
17
+ cursor: pointer;
18
+ transition:
19
+ border-color var(--dc-transition-fast),
20
+ color var(--dc-transition-fast),
21
+ background var(--dc-transition-fast);
22
+ }
23
+ button.dcui-chip:hover {
24
+ color: var(--dc-fg);
25
+ border-color: var(--dc-border-strong);
26
+ }
27
+ button.dcui-chip:focus-visible {
28
+ outline: 2px solid var(--dc-focus-ring);
29
+ outline-offset: 1px;
30
+ }
31
+ .dcui-chip[data-variant="accent"] {
32
+ color: var(--dc-brand);
33
+ background: var(--dc-brand-subtle);
34
+ border-color: var(--dc-brand);
35
+ }
36
+ .dcui-chip[data-variant="solid"] {
37
+ color: var(--dc-ink-fg);
38
+ background: var(--dc-ink);
39
+ border-color: var(--dc-ink);
40
+ }
41
+ .dcui-chip[aria-current="true"] {
42
+ color: var(--dc-brand);
43
+ border-color: var(--dc-brand);
44
+ background: var(--dc-brand-subtle);
45
+ }
46
+ .dcui-chip-index {
47
+ color: var(--dc-fg-subtle);
48
+ }
49
+ .dcui-chip[aria-current="true"] .dcui-chip-index {
50
+ color: var(--dc-brand);
51
+ }
@@ -0,0 +1,13 @@
1
+ **Chip** — a small pill for hierarchy levels (atom, molecule…), flow steps, tweak tokens, and counts; reach for it to tag or label one item compactly. Static by default; pass `onClick` and it becomes a button.
2
+
3
+ ```tsx
4
+ <Chip>atom</Chip>
5
+ <Chip variant="accent">accent</Chip>
6
+ <Chip index={2} current onClick={() => goto('check-email')}>Check email</Chip>
7
+ ```
8
+
9
+ Variants: `default` (muted) · `accent` (marigold outline, soft fill) · `solid` (filled ink, for the one chip that must stand out against the others). `current` is the marigold "active" state; `index` prepends a dimmed mono number.
10
+
11
+ For a multi-step stepper, reach for `FlowNav`.
12
+
13
+ Pass `label` for the accessible name: with `onClick` (button form) it becomes `aria-label`; without (span form) it becomes the `title`. `onClick` emits nothing — the caller already knows which chip it wired.
@@ -0,0 +1,46 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { renderToStaticMarkup } from 'react-dom/server'
3
+ import { Chip } from './Chip'
4
+
5
+ describe('Chip', () => {
6
+ test('renders as a static span by default with the label as a title', () => {
7
+ const html = renderToStaticMarkup(<Chip label="atom">atom</Chip>)
8
+ expect(html).toContain('<span')
9
+ expect(html).not.toContain('<button')
10
+ expect(html).toContain('title="atom"')
11
+ expect(html).toContain('data-variant="default"')
12
+ })
13
+
14
+ test('becomes a button when an onClick handler is given', () => {
15
+ const html = renderToStaticMarkup(
16
+ <Chip label="Step 1" onClick={() => {}}>
17
+ Step 1
18
+ </Chip>,
19
+ )
20
+ expect(html).toContain('<button')
21
+ expect(html).toContain('type="button"')
22
+ expect(html).toContain('aria-label="Step 1"')
23
+ })
24
+
25
+ test('marks the current chip with aria-current', () => {
26
+ const current = renderToStaticMarkup(<Chip current>x</Chip>)
27
+ const plain = renderToStaticMarkup(<Chip>x</Chip>)
28
+ expect(current).toContain('aria-current="true"')
29
+ expect(plain).not.toContain('aria-current')
30
+ })
31
+
32
+ test('renders a leading index slot when provided', () => {
33
+ const html = renderToStaticMarkup(<Chip index={2}>Configure</Chip>)
34
+ expect(html).toContain('class="dcui-chip-index"')
35
+ expect(html).toContain('>2<')
36
+ })
37
+
38
+ test('reflects the variant', () => {
39
+ expect(renderToStaticMarkup(<Chip variant="accent">x</Chip>)).toContain(
40
+ 'data-variant="accent"',
41
+ )
42
+ expect(renderToStaticMarkup(<Chip variant="solid">x</Chip>)).toContain(
43
+ 'data-variant="solid"',
44
+ )
45
+ })
46
+ })
@@ -0,0 +1,54 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ /**
4
+ * Display Case — Chip
5
+ * A small pill for hierarchy levels (atom, molecule…), flow steps, tweak
6
+ * tokens, and counts. Static by default; pass `onClick` and it becomes a button
7
+ * (used for flow-step selection).
8
+ */
9
+
10
+ export type ChipVariant = 'default' | 'accent' | 'solid'
11
+
12
+ export interface ChipProps {
13
+ variant?: ChipVariant
14
+ current?: boolean
15
+ /** Leading index/number (mono, dimmed) — used by flow-step chips. */
16
+ index?: ReactNode
17
+ /** When set, the chip renders as a button. */
18
+ onClick?: () => void
19
+ label?: string
20
+ children?: ReactNode
21
+ }
22
+
23
+ export function Chip({
24
+ variant = 'default',
25
+ current = false,
26
+ index,
27
+ onClick,
28
+ label,
29
+ children,
30
+ }: ChipProps) {
31
+ const inner = (
32
+ <>
33
+ {index != null ? <span className="dcui-chip-index">{index}</span> : null}
34
+ {children}
35
+ </>
36
+ )
37
+ const common = {
38
+ className: 'dcui-chip',
39
+ 'data-variant': variant,
40
+ 'aria-current': current ? ('true' as const) : undefined,
41
+ }
42
+ if (onClick) {
43
+ return (
44
+ <button type="button" {...common} aria-label={label} onClick={onClick}>
45
+ {inner}
46
+ </button>
47
+ )
48
+ }
49
+ return (
50
+ <span {...common} title={label}>
51
+ {inner}
52
+ </span>
53
+ )
54
+ }
@@ -0,0 +1,30 @@
1
+ import { defineCases, tweak } from '@awarebydefault/display-case'
2
+ import { Eyebrow } from './Eyebrow'
3
+
4
+ export default defineCases(
5
+ 'Eyebrow',
6
+ {
7
+ Playground: {
8
+ tweaks: {
9
+ label: tweak.text('Components'),
10
+ tone: tweak.choice(['muted', 'accent', 'strong'], 'muted'),
11
+ as: tweak.choice(['div', 'span', 'p'], 'div'),
12
+ },
13
+ render: (t) => (
14
+ <Eyebrow
15
+ tone={t.tone as 'muted' | 'accent' | 'strong'}
16
+ as={t.as as 'div' | 'span' | 'p'}>
17
+ {t.label}
18
+ </Eyebrow>
19
+ ),
20
+ },
21
+ Tones: () => (
22
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
23
+ <Eyebrow>Components</Eyebrow>
24
+ <Eyebrow tone="accent">Tweaks</Eyebrow>
25
+ <Eyebrow tone="strong">Documentation</Eyebrow>
26
+ </div>
27
+ ),
28
+ },
29
+ { level: 'atom' },
30
+ )
@@ -0,0 +1,16 @@
1
+ .dcui-eyebrow {
2
+ font-family: var(--dc-font-mono);
3
+ font-size: var(--dc-text-xs);
4
+ font-weight: var(--dc-weight-medium);
5
+ letter-spacing: var(--dc-tracking-label);
6
+ text-transform: uppercase;
7
+ color: var(--dc-fg-muted);
8
+ line-height: var(--dc-leading-tight);
9
+ margin: 0;
10
+ }
11
+ .dcui-eyebrow[data-tone="accent"] {
12
+ color: var(--dc-brand);
13
+ }
14
+ .dcui-eyebrow[data-tone="strong"] {
15
+ color: var(--dc-fg);
16
+ }
@@ -0,0 +1,10 @@
1
+ **Eyebrow** — the signature section label: uppercase JetBrains Mono, wide tracking, muted; reach for it to mark a group header or panel title in the chrome.
2
+
3
+ ```tsx
4
+ <Eyebrow>Components</Eyebrow>
5
+ <Eyebrow tone="accent">Tweaks</Eyebrow>
6
+ ```
7
+
8
+ Renders a `<div>` by default. Override `as="span"` only when the label sits inline inside other text (a block `<div>` would break the flow).
9
+
10
+ Tones: `muted` (default) · `accent` (marigold) · `strong` (full ink).
@@ -0,0 +1,38 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { renderToStaticMarkup } from 'react-dom/server'
3
+ import { Eyebrow } from './Eyebrow'
4
+
5
+ describe('Eyebrow', () => {
6
+ test('renders a muted div by default', () => {
7
+ const html = renderToStaticMarkup(<Eyebrow>Tweaks</Eyebrow>)
8
+ expect(html).toContain('<div')
9
+ expect(html).toContain('class="dcui-eyebrow"')
10
+ expect(html).toContain('data-tone="muted"')
11
+ expect(html).toContain('Tweaks')
12
+ })
13
+
14
+ test('renders with the requested element tag', () => {
15
+ const html = renderToStaticMarkup(<Eyebrow as="span">Docs</Eyebrow>)
16
+ expect(html).toContain('<span')
17
+ expect(html).not.toContain('<div')
18
+ })
19
+
20
+ test('reflects the tone', () => {
21
+ expect(renderToStaticMarkup(<Eyebrow tone="accent">x</Eyebrow>)).toContain(
22
+ 'data-tone="accent"',
23
+ )
24
+ expect(renderToStaticMarkup(<Eyebrow tone="strong">x</Eyebrow>)).toContain(
25
+ 'data-tone="strong"',
26
+ )
27
+ })
28
+
29
+ test('forwards arbitrary attributes to the element', () => {
30
+ const html = renderToStaticMarkup(
31
+ <Eyebrow id="section-label" title="hint">
32
+ x
33
+ </Eyebrow>,
34
+ )
35
+ expect(html).toContain('id="section-label"')
36
+ expect(html).toContain('title="hint"')
37
+ })
38
+ })
@@ -0,0 +1,29 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react'
2
+
3
+ /**
4
+ * Display Case — Eyebrow
5
+ * The signature label: uppercase JetBrains Mono, wide tracking, muted. Marks
6
+ * every section in the chrome (group headers, panel titles, "Tweaks",
7
+ * "Documentation").
8
+ */
9
+
10
+ export type EyebrowTone = 'muted' | 'accent' | 'strong'
11
+
12
+ export interface EyebrowProps extends HTMLAttributes<HTMLElement> {
13
+ as?: 'div' | 'span' | 'p'
14
+ tone?: EyebrowTone
15
+ children?: ReactNode
16
+ }
17
+
18
+ export function Eyebrow({
19
+ as: Tag = 'div',
20
+ tone = 'muted',
21
+ children,
22
+ ...rest
23
+ }: EyebrowProps) {
24
+ return (
25
+ <Tag className="dcui-eyebrow" data-tone={tone} {...rest}>
26
+ {children}
27
+ </Tag>
28
+ )
29
+ }
@@ -0,0 +1,35 @@
1
+ import { defineCases, tweak } from '@awarebydefault/display-case'
2
+ import { FlowNav } from './FlowNav'
3
+
4
+ const steps = [
5
+ { id: 'request-link', label: 'Request link' },
6
+ { id: 'check-email', label: 'Check email' },
7
+ { id: 'signed-in', label: 'Signed in' },
8
+ ]
9
+
10
+ export default defineCases(
11
+ 'FlowNav',
12
+ {
13
+ Playground: {
14
+ tweaks: {
15
+ active: tweak.choice(
16
+ ['request-link', 'check-email', 'signed-in'],
17
+ 'check-email',
18
+ ),
19
+ },
20
+ render: (t) => (
21
+ <FlowNav steps={steps} activeId={t.active} onSelect={() => {}} />
22
+ ),
23
+ },
24
+ Start: () => (
25
+ <FlowNav steps={steps} activeId="request-link" onSelect={() => {}} />
26
+ ),
27
+ Middle: () => (
28
+ <FlowNav steps={steps} activeId="check-email" onSelect={() => {}} />
29
+ ),
30
+ End: () => (
31
+ <FlowNav steps={steps} activeId="signed-in" onSelect={() => {}} />
32
+ ),
33
+ },
34
+ { level: 'molecule' },
35
+ )
@@ -0,0 +1,29 @@
1
+ .dcui-flownav {
2
+ display: flex;
3
+ align-items: center;
4
+ gap: var(--dc-space-6);
5
+ flex-wrap: wrap;
6
+ padding: var(--dc-space-4) var(--dc-space-6);
7
+ border: 1px solid var(--dc-border);
8
+ border-radius: var(--dc-radius-md);
9
+ background: var(--dc-bg-subtle);
10
+ }
11
+ /* Scope the steps reset under .dcui-flownav so the no-margin/no-padding list
12
+ wins over ambient prose styles (e.g. a Primer's .dc-primer ol rule), which
13
+ would otherwise add a bottom margin that pushes the chips off the bar's
14
+ centre line, out of vertical alignment with the Prev / Next rail. */
15
+ .dcui-flownav .dcui-flownav-steps {
16
+ display: flex;
17
+ align-items: center;
18
+ gap: var(--dc-space-3);
19
+ flex-wrap: wrap;
20
+ list-style: none;
21
+ margin: 0;
22
+ padding: 0;
23
+ flex: 1;
24
+ }
25
+ .dcui-flownav-rail {
26
+ margin-left: auto;
27
+ display: flex;
28
+ gap: var(--dc-space-3);
29
+ }
@@ -0,0 +1,13 @@
1
+ **FlowNav** — the stepper for multi-step flows: numbered step chips plus Prev/Next, grouped on one bar; reach for it when a flow's steps are individually addressable.
2
+
3
+ ```tsx
4
+ <FlowNav
5
+ steps={[{ id: 'a', label: 'Request link' }, { id: 'b', label: 'Check email' }]}
6
+ activeId="a"
7
+ onSelect={(id) => goto(id)}
8
+ />
9
+ ```
10
+
11
+ `onSelect(id)` fires from both chip clicks and the Prev/Next buttons. Prev/Next resolve to the neighboring step's `id` (not a delta or direction), so the handler is always "go to this step". They auto-disable at the first and last step.
12
+
13
+ For a single pill, reach for `Chip`.
@@ -0,0 +1,48 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { renderToStaticMarkup } from 'react-dom/server'
3
+ import { FlowNav, type FlowStep } from './FlowNav'
4
+
5
+ const steps: FlowStep[] = [
6
+ { id: 'a', label: 'Empty' },
7
+ { id: 'b', label: 'Filled' },
8
+ { id: 'c', label: 'Submitted' },
9
+ ]
10
+
11
+ describe('FlowNav', () => {
12
+ test('renders one numbered step chip per step', () => {
13
+ const html = renderToStaticMarkup(<FlowNav steps={steps} activeId="a" />)
14
+ expect(html).toContain('Empty')
15
+ expect(html).toContain('Filled')
16
+ expect(html).toContain('Submitted')
17
+ // The mono index column on each chip.
18
+ expect((html.match(/dcui-chip-index/g) ?? []).length).toBe(3)
19
+ })
20
+
21
+ test('marks the active step chip as current', () => {
22
+ const html = renderToStaticMarkup(<FlowNav steps={steps} activeId="b" />)
23
+ expect(html).toContain('aria-current="true"')
24
+ })
25
+
26
+ test('disables Prev on the first step', () => {
27
+ const html = renderToStaticMarkup(<FlowNav steps={steps} activeId="a" />)
28
+ // The Prev button (← Prev) precedes Next and is the only disabled control.
29
+ const prevIdx = html.indexOf('← Prev')
30
+ const slice = html.slice(0, prevIdx)
31
+ expect(slice.lastIndexOf('disabled')).toBeGreaterThan(
32
+ slice.lastIndexOf('<button'),
33
+ )
34
+ })
35
+
36
+ test('disables Next on the last step but not Prev', () => {
37
+ const html = renderToStaticMarkup(<FlowNav steps={steps} activeId="c" />)
38
+ expect(html).toContain('← Prev')
39
+ expect(html).toContain('Next →')
40
+ // Exactly one rail button is disabled at an end.
41
+ expect((html.match(/disabled/g) ?? []).length).toBe(1)
42
+ })
43
+
44
+ test('enables both rail buttons in the middle of the flow', () => {
45
+ const html = renderToStaticMarkup(<FlowNav steps={steps} activeId="b" />)
46
+ expect(html).not.toContain('disabled')
47
+ })
48
+ })
@@ -0,0 +1,58 @@
1
+ import type { ReactNode } from 'react'
2
+ import { Button } from '../controls/Button'
3
+ import { Chip } from './Chip'
4
+
5
+ /**
6
+ * Display Case — FlowNav
7
+ * The stepper for multi-step flows: numbered step chips + Prev / Next, grouped
8
+ * on one bar. Each step is individually addressable (click a chip), mirroring
9
+ * the flow's `goto` transitions.
10
+ */
11
+
12
+ export interface FlowStep {
13
+ id: string
14
+ label: ReactNode
15
+ }
16
+
17
+ export interface FlowNavProps {
18
+ steps: FlowStep[]
19
+ activeId: string
20
+ onSelect?: (id: string) => void
21
+ }
22
+
23
+ export function FlowNav({ steps, activeId, onSelect }: FlowNavProps) {
24
+ const idx = steps.findIndex((s) => s.id === activeId)
25
+ const prev = steps[idx - 1]
26
+ const next = steps[idx + 1]
27
+ return (
28
+ <div className="dcui-flownav">
29
+ <ol className="dcui-flownav-steps">
30
+ {steps.map((s, i) => (
31
+ <li key={s.id}>
32
+ <Chip
33
+ index={i + 1}
34
+ current={s.id === activeId}
35
+ onClick={() => onSelect?.(s.id)}>
36
+ {s.label}
37
+ </Chip>
38
+ </li>
39
+ ))}
40
+ </ol>
41
+ <div className="dcui-flownav-rail">
42
+ <Button
43
+ size="sm"
44
+ variant="subtle"
45
+ disabled={!prev}
46
+ onClick={() => prev && onSelect?.(prev.id)}>
47
+ ← Prev
48
+ </Button>
49
+ <Button
50
+ size="sm"
51
+ disabled={!next}
52
+ onClick={() => next && onSelect?.(next.id)}>
53
+ Next →
54
+ </Button>
55
+ </div>
56
+ </div>
57
+ )
58
+ }