@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,19 @@
1
+ import { type A11yImpact, defineCases } from '@awarebydefault/display-case'
2
+ import { ImpactTag } from './ImpactTag'
3
+
4
+ const IMPACTS: A11yImpact[] = ['critical', 'serious', 'moderate', 'minor']
5
+
6
+ export default defineCases(
7
+ 'ImpactTag',
8
+ {
9
+ // The full severity scale, worst → least (the order a sorted list shows).
10
+ 'All severities': () => (
11
+ <div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
12
+ {IMPACTS.map((impact) => (
13
+ <ImpactTag key={impact} impact={impact} />
14
+ ))}
15
+ </div>
16
+ ),
17
+ },
18
+ { level: 'atom' },
19
+ )
@@ -0,0 +1,36 @@
1
+ .dcui-impact-tag {
2
+ flex: 0 0 auto;
3
+ align-self: center;
4
+ min-width: 4.2rem;
5
+ text-align: center;
6
+ padding: 0 var(--dc-space-2);
7
+ border-radius: var(--dc-radius-sm);
8
+ font-family: var(--dc-font-mono);
9
+ font-size: var(--dc-text-2xs);
10
+ font-weight: var(--dc-weight-medium);
11
+ text-transform: uppercase;
12
+ letter-spacing: 0.04em;
13
+ /* Each severity carries its own text colour so the fill+text pair clears AA
14
+ theme-independently — a single --dc-brand-fg (white in light, ink in dark)
15
+ could not, since the fills span a light amber to a dark red. The fills are
16
+ the fixed ramp hues (not theme tokens), so the gradient — critical hottest
17
+ → minor coolest — and the contrast both hold in light and dark alike. */
18
+ color: #ffffff;
19
+ background: var(--dc-paper-700);
20
+ }
21
+ .dcui-impact-tag[data-impact="critical"] {
22
+ background: var(--dc-red-700);
23
+ color: #ffffff;
24
+ }
25
+ .dcui-impact-tag[data-impact="serious"] {
26
+ background: var(--dc-red-600);
27
+ color: #ffffff;
28
+ }
29
+ .dcui-impact-tag[data-impact="moderate"] {
30
+ background: var(--dc-amber-500);
31
+ color: var(--dc-paper-900);
32
+ }
33
+ .dcui-impact-tag[data-impact="minor"] {
34
+ background: var(--dc-paper-700);
35
+ color: #ffffff;
36
+ }
@@ -0,0 +1,14 @@
1
+ **ImpactTag** — a severity tag for an accessibility violation, colour-graded by axe `impact` so the worst findings read hottest. Used in the [A11yPanel](./A11yPanel.placard.md) violation list.
2
+
3
+ ```tsx
4
+ <ImpactTag impact="critical" />
5
+ <ImpactTag impact="serious" />
6
+ ```
7
+
8
+ `impact` is one of `critical` · `serious` · `moderate` · `minor` (the `A11yImpact` type). critical is the deepest red, minor the calmest.
9
+
10
+ The companion `impactRank(impact)` returns a sort key (worst = 0, unclassified last) — sort a violation list with it before mapping each to a tag:
11
+
12
+ ```tsx
13
+ [...violations].sort((a, b) => impactRank(a.impact) - impactRank(b.impact))
14
+ ```
@@ -0,0 +1,40 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { renderToStaticMarkup } from 'react-dom/server'
3
+ import { ImpactTag, impactRank } from './ImpactTag'
4
+
5
+ describe('impactRank', () => {
6
+ test('orders the impacts worst-first', () => {
7
+ expect(impactRank('critical')).toBeLessThan(impactRank('serious'))
8
+ expect(impactRank('serious')).toBeLessThan(impactRank('moderate'))
9
+ expect(impactRank('moderate')).toBeLessThan(impactRank('minor'))
10
+ })
11
+
12
+ test('sorts an unclassified (null) impact last', () => {
13
+ expect(impactRank(null)).toBeGreaterThan(impactRank('minor'))
14
+ })
15
+
16
+ test('is usable as an Array.sort comparator', () => {
17
+ const sorted = ['minor', 'critical', 'moderate', 'serious'].sort(
18
+ (a, b) => impactRank(a as never) - impactRank(b as never),
19
+ )
20
+ expect(sorted).toEqual(['critical', 'serious', 'moderate', 'minor'])
21
+ })
22
+ })
23
+
24
+ describe('ImpactTag', () => {
25
+ test('labels the tag with the impact and grades it via data-impact', () => {
26
+ const html = renderToStaticMarkup(<ImpactTag impact="critical" />)
27
+ expect(html).toContain('data-impact="critical"')
28
+ expect(html).toContain('title="Severity: critical"')
29
+ expect(html).toContain('>critical<')
30
+ })
31
+
32
+ test('reflects each severity level', () => {
33
+ expect(renderToStaticMarkup(<ImpactTag impact="minor" />)).toContain(
34
+ 'data-impact="minor"',
35
+ )
36
+ expect(renderToStaticMarkup(<ImpactTag impact="moderate" />)).toContain(
37
+ 'data-impact="moderate"',
38
+ )
39
+ })
40
+ })
@@ -0,0 +1,35 @@
1
+ import type { A11yImpact } from '../../../../index'
2
+
3
+ /**
4
+ * Display Case — ImpactTag
5
+ * A severity tag for an accessibility violation, colour-graded by axe impact
6
+ * (critical → minor) so the worst findings read hottest. Used in the
7
+ * {@link A11yPanel} violation list; `impactRank` orders a list worst-first.
8
+ */
9
+
10
+ // Worst → least, so a list sorts top-down. Unclassified (null) sorts last.
11
+ const RANK: Record<A11yImpact, number> = {
12
+ critical: 0,
13
+ serious: 1,
14
+ moderate: 2,
15
+ minor: 3,
16
+ }
17
+
18
+ /** Sort key for an impact (worst first); `null`/unclassified sorts last. */
19
+ export const impactRank = (impact: A11yImpact | null): number =>
20
+ impact ? RANK[impact] : 4
21
+
22
+ export interface ImpactTagProps {
23
+ impact: A11yImpact
24
+ }
25
+
26
+ export function ImpactTag({ impact }: ImpactTagProps) {
27
+ return (
28
+ <span
29
+ className="dcui-impact-tag"
30
+ data-impact={impact}
31
+ title={`Severity: ${impact}`}>
32
+ {impact}
33
+ </span>
34
+ )
35
+ }
@@ -0,0 +1,86 @@
1
+ import { defineCases, tweak } from '@awarebydefault/display-case'
2
+ import { NavItem } from './NavItem'
3
+ import { Sidebar } from './Sidebar'
4
+
5
+ export default defineCases(
6
+ 'NavItem',
7
+ {
8
+ // Wrapped in Sidebar — NavItem rows are transparent and only read correctly
9
+ // against the sidebar's `--dc-bg-subtle` surface.
10
+ Playground: {
11
+ tweaks: {
12
+ kind: tweak.choice(['component', 'case'], 'component'),
13
+ label: tweak.text('Button'),
14
+ withCount: tweak.boolean(true),
15
+ count: tweak.number(4),
16
+ alert: tweak.number(0),
17
+ current: tweak.boolean(false),
18
+ expanded: tweak.boolean(false),
19
+ },
20
+ render: (t) => (
21
+ <Sidebar style={{ width: '15rem' }}>
22
+ <NavItem
23
+ kind={t.kind as 'component' | 'case'}
24
+ label={t.label}
25
+ count={t.withCount ? t.count : undefined}
26
+ alert={t.alert}
27
+ current={t.current}
28
+ expanded={t.expanded}
29
+ onToggle={() => {}}
30
+ onSelect={() => {}}
31
+ />
32
+ </Sidebar>
33
+ ),
34
+ },
35
+ // Accessibility-violation markers. Collapsed, a component shows the summed
36
+ // count (the danger pill). Expanded, the parent shows a plain dot while the
37
+ // per-variant counts move onto the case rows. See the "A11y page" exhibit
38
+ // for the markers in the full chrome.
39
+ Alert: () => (
40
+ <Sidebar style={{ width: '15rem' }}>
41
+ <NavItem
42
+ kind="component"
43
+ label="Input"
44
+ count={2}
45
+ alert={5}
46
+ onToggle={() => {}}
47
+ onSelect={() => {}}
48
+ />
49
+ <NavItem
50
+ kind="component"
51
+ label="Button"
52
+ count={3}
53
+ alert="dot"
54
+ expanded
55
+ onToggle={() => {}}
56
+ onSelect={() => {}}
57
+ />
58
+ <NavItem kind="case" label="Playground" alert={2} onSelect={() => {}} />
59
+ <NavItem kind="case" label="Variants" alert={3} onSelect={() => {}} />
60
+ <NavItem kind="case" label="Sizes" onSelect={() => {}} />
61
+ </Sidebar>
62
+ ),
63
+ Tree: () => (
64
+ <Sidebar style={{ width: '15rem' }}>
65
+ <NavItem
66
+ kind="component"
67
+ label="Button"
68
+ count={4}
69
+ expanded
70
+ onToggle={() => {}}
71
+ onSelect={() => {}}
72
+ />
73
+ <NavItem kind="case" label="Playground" onSelect={() => {}} />
74
+ <NavItem kind="case" label="Variants" current onSelect={() => {}} />
75
+ <NavItem kind="case" label="Sizes" onSelect={() => {}} />
76
+ <NavItem
77
+ kind="component"
78
+ label="Checkbox"
79
+ onToggle={() => {}}
80
+ onSelect={() => {}}
81
+ />
82
+ </Sidebar>
83
+ ),
84
+ },
85
+ { level: 'molecule' },
86
+ )
@@ -0,0 +1,111 @@
1
+ .dcui-navrow {
2
+ display: flex;
3
+ align-items: center;
4
+ gap: var(--dc-space-1);
5
+ width: 100%;
6
+ box-sizing: border-box;
7
+ position: relative;
8
+ }
9
+ .dcui-nav-disclosure {
10
+ flex: 0 0 auto;
11
+ display: inline-flex;
12
+ align-items: center;
13
+ justify-content: center;
14
+ width: 20px;
15
+ height: 22px;
16
+ border: 0;
17
+ background: none;
18
+ color: var(--dc-fg-subtle);
19
+ cursor: pointer;
20
+ border-radius: var(--dc-radius-sm);
21
+ padding: 0;
22
+ transition:
23
+ color var(--dc-transition-fast),
24
+ background var(--dc-transition-fast);
25
+ }
26
+ .dcui-nav-disclosure:hover {
27
+ color: var(--dc-fg);
28
+ background: var(--dc-hover);
29
+ }
30
+ /* A non-expandable component row (single case) keeps the chevron's footprint so
31
+ its name still lines up under expandable siblings — just with no glyph. */
32
+ .dcui-nav-disclosure-spacer {
33
+ flex: 0 0 auto;
34
+ width: 20px;
35
+ height: 22px;
36
+ }
37
+ .dcui-chevron {
38
+ font-family: var(--dc-font-mono);
39
+ font-size: 0.65rem;
40
+ line-height: 1;
41
+ transition: transform var(--dc-transition-fast);
42
+ }
43
+ .dcui-chevron[data-expanded="true"] {
44
+ transform: rotate(90deg);
45
+ }
46
+ .dcui-nav-name {
47
+ flex: 1;
48
+ min-width: 0;
49
+ display: inline-flex;
50
+ align-items: center;
51
+ gap: var(--dc-space-3);
52
+ text-align: left;
53
+ font-family: var(--dc-font-sans);
54
+ font-size: var(--dc-text-base);
55
+ font-weight: var(--dc-weight-medium);
56
+ color: var(--dc-fg);
57
+ border: 0;
58
+ background: none;
59
+ cursor: pointer;
60
+ padding: 0.25rem var(--dc-space-3);
61
+ border-radius: var(--dc-radius-sm);
62
+ overflow: hidden;
63
+ transition:
64
+ background var(--dc-transition-fast),
65
+ color var(--dc-transition-fast);
66
+ }
67
+ /* The name text itself truncates; the alert pill beside it never shrinks. */
68
+ .dcui-nav-label {
69
+ min-width: 0;
70
+ overflow: hidden;
71
+ text-overflow: ellipsis;
72
+ white-space: nowrap;
73
+ }
74
+ .dcui-nav-name:hover {
75
+ background: var(--dc-hover);
76
+ }
77
+ .dcui-nav-count {
78
+ font-family: var(--dc-font-mono);
79
+ font-size: var(--dc-text-xs);
80
+ color: var(--dc-fg-subtle);
81
+ flex: 0 0 auto;
82
+ padding-right: var(--dc-space-3);
83
+ }
84
+ /* Case rows indent so their text lines up under the component name (chevron
85
+ width + row gap, then the name's own text padding). */
86
+ .dcui-navrow[data-kind="case"] {
87
+ padding-left: calc(20px + var(--dc-space-1));
88
+ }
89
+ .dcui-navrow[data-kind="case"] .dcui-nav-name {
90
+ font-weight: var(--dc-weight-normal);
91
+ font-size: var(--dc-text-sm);
92
+ color: var(--dc-fg-muted);
93
+ }
94
+ .dcui-navrow[data-kind="case"] .dcui-nav-name:hover {
95
+ color: var(--dc-fg);
96
+ }
97
+ .dcui-navrow[data-current="true"] .dcui-nav-name {
98
+ color: var(--dc-brand);
99
+ font-weight: var(--dc-weight-medium);
100
+ }
101
+ .dcui-navrow[data-current="true"]::before {
102
+ content: "";
103
+ position: absolute;
104
+ left: 0;
105
+ top: 50%;
106
+ transform: translateY(-50%);
107
+ width: 2px;
108
+ height: 1rem;
109
+ border-radius: 1px;
110
+ background: var(--dc-brand);
111
+ }
@@ -0,0 +1,13 @@
1
+ **NavItem** — one row in the sidebar tree; reach for it to render a component or case entry in the navigation.
2
+
3
+ ```tsx
4
+ <NavItem kind="component" label="Button" count={4} expanded
5
+ onToggle={toggle} onSelect={select} />
6
+ <NavItem kind="case" label="Variants" current onSelect={select} />
7
+ ```
8
+
9
+ `kind="component"` (the default) renders a disclosure chevron + name + optional case `count`; `kind="case"` renders an indented case link aligned under the component name. The chevron and `onToggle` exist only for `kind="component"`. The active row (`current`) is marigold with a left tick.
10
+
11
+ Place inside `Sidebar`.
12
+
13
+ `onSelect` (name click) and `onToggle` (chevron click) are argument-less. A11y: component rows expose `aria-expanded` with a dynamic Expand/Collapse label; the name button gets `aria-current` when `current`.
@@ -0,0 +1,65 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { renderToStaticMarkup } from 'react-dom/server'
3
+ import { NavItem } from './NavItem'
4
+
5
+ describe('NavItem', () => {
6
+ test('a toggleable component row leads with a disclosure chevron', () => {
7
+ const html = renderToStaticMarkup(
8
+ <NavItem label="Button" expanded={false} onToggle={() => {}} count={3} />,
9
+ )
10
+ expect(html).toContain('class="dcui-nav-disclosure"')
11
+ expect(html).toContain('aria-expanded="false"')
12
+ expect(html).toContain('aria-label="Expand Button"')
13
+ expect(html).toContain('data-kind="component"')
14
+ })
15
+
16
+ test('the chevron aria-label flips with the expanded state', () => {
17
+ const html = renderToStaticMarkup(
18
+ <NavItem label="Button" expanded onToggle={() => {}} />,
19
+ )
20
+ expect(html).toContain('aria-label="Collapse Button"')
21
+ expect(html).toContain('data-expanded="true"')
22
+ })
23
+
24
+ test('a single-case component row swaps the chevron for an aligning spacer', () => {
25
+ const html = renderToStaticMarkup(<NavItem label="Solo" />)
26
+ expect(html).toContain('class="dcui-nav-disclosure-spacer"')
27
+ expect(html).not.toContain('class="dcui-nav-disclosure"')
28
+ })
29
+
30
+ test('a case row is indented and carries no disclosure', () => {
31
+ const html = renderToStaticMarkup(<NavItem kind="case" label="default" />)
32
+ expect(html).toContain('data-kind="case"')
33
+ expect(html).not.toContain('dcui-nav-disclosure')
34
+ })
35
+
36
+ test('the active row sets data-current and aria-current', () => {
37
+ const html = renderToStaticMarkup(<NavItem label="Button" current />)
38
+ expect(html).toContain('data-current="true"')
39
+ expect(html).toContain('aria-current="true"')
40
+ })
41
+
42
+ test('renders the a11y badge only for a positive count or "dot"', () => {
43
+ expect(renderToStaticMarkup(<NavItem label="x" alert={2} />)).toContain(
44
+ 'dcui-a11y-badge',
45
+ )
46
+ expect(renderToStaticMarkup(<NavItem label="x" alert="dot" />)).toContain(
47
+ 'data-dot="true"',
48
+ )
49
+ expect(renderToStaticMarkup(<NavItem label="x" alert={0} />)).not.toContain(
50
+ 'dcui-a11y-badge',
51
+ )
52
+ expect(renderToStaticMarkup(<NavItem label="x" />)).not.toContain(
53
+ 'dcui-a11y-badge',
54
+ )
55
+ })
56
+
57
+ test('renders the case count only when provided', () => {
58
+ expect(
59
+ renderToStaticMarkup(<NavItem label="x" onToggle={() => {}} count={5} />),
60
+ ).toContain('dcui-nav-count')
61
+ expect(
62
+ renderToStaticMarkup(<NavItem label="x" onToggle={() => {}} />),
63
+ ).not.toContain('dcui-nav-count')
64
+ })
65
+ })
@@ -0,0 +1,95 @@
1
+ import type { ReactNode } from 'react'
2
+ import { A11yBadge } from './A11yBadge'
3
+
4
+ /**
5
+ * Display Case — NavItem
6
+ * One row in the sidebar tree. `kind="component"` renders a disclosure chevron +
7
+ * name (+ optional case count); when no `onToggle` is given (a component with a
8
+ * single case) the chevron is replaced by a same-width spacer and the count is
9
+ * omitted, so the row reads as a plain leaf that still aligns with its siblings.
10
+ * `kind="case"` renders an indented case link that lines up under the component
11
+ * name. The active row is marigold with a left tick.
12
+ */
13
+
14
+ export type NavItemKind = 'component' | 'case'
15
+
16
+ export interface NavItemProps {
17
+ kind?: NavItemKind
18
+ label: ReactNode
19
+ count?: ReactNode
20
+ current?: boolean
21
+ expanded?: boolean
22
+ /** Accessibility marker. A positive number renders a counted danger pill; the
23
+ * string `'dot'` renders an unnumbered danger dot — used on an *expanded*
24
+ * component whose per-variant counts have moved onto its case rows, so the
25
+ * parent flags "issues here" without competing with the child numbers. Omit
26
+ * or `0` for none. */
27
+ alert?: number | 'dot'
28
+ onSelect?: () => void
29
+ onToggle?: () => void
30
+ /** `data-testid` for the select (name) button. */
31
+ testId?: string
32
+ /** `data-testid` for the disclosure chevron (component rows only). */
33
+ toggleTestId?: string
34
+ /** `data-testid` for the a11y-violation marker. */
35
+ alertTestId?: string
36
+ }
37
+
38
+ export function NavItem({
39
+ kind = 'component',
40
+ label,
41
+ count,
42
+ current = false,
43
+ expanded = false,
44
+ alert,
45
+ onSelect,
46
+ onToggle,
47
+ testId,
48
+ toggleTestId,
49
+ alertTestId,
50
+ }: NavItemProps) {
51
+ // The a11y marker (a counted pill, or a bare 'dot' on an expanded parent) is
52
+ // the standalone A11yBadge; render it only for a positive count / 'dot'.
53
+ const showAlert = alert === 'dot' || (typeof alert === 'number' && alert > 0)
54
+ // Component rows lead with a disclosure chevron; a component with no `onToggle`
55
+ // (a single case) gets a same-width spacer instead so its name still aligns
56
+ // under expandable siblings. Case rows lead with nothing.
57
+ let disclosure: ReactNode = null
58
+ if (kind === 'component') {
59
+ disclosure = onToggle ? (
60
+ <button
61
+ type="button"
62
+ className="dcui-nav-disclosure"
63
+ aria-label={expanded ? `Collapse ${label}` : `Expand ${label}`}
64
+ aria-expanded={expanded}
65
+ data-testid={toggleTestId}
66
+ onClick={onToggle}>
67
+ <span className="dcui-chevron" data-expanded={expanded}>
68
+
69
+ </span>
70
+ </button>
71
+ ) : (
72
+ <span className="dcui-nav-disclosure-spacer" aria-hidden="true" />
73
+ )
74
+ }
75
+ return (
76
+ <div
77
+ className="dcui-navrow"
78
+ data-kind={kind}
79
+ data-current={current ? 'true' : undefined}>
80
+ {disclosure}
81
+ <button
82
+ type="button"
83
+ className="dcui-nav-name"
84
+ aria-current={current ? 'true' : undefined}
85
+ data-testid={testId}
86
+ onClick={onSelect}>
87
+ <span className="dcui-nav-label">{label}</span>
88
+ {showAlert && alert !== undefined && (
89
+ <A11yBadge value={alert} testId={alertTestId} />
90
+ )}
91
+ </button>
92
+ {count != null ? <span className="dcui-nav-count">{count}</span> : null}
93
+ </div>
94
+ )
95
+ }
@@ -0,0 +1,21 @@
1
+ import { defineCases, tweak } from '@awarebydefault/display-case'
2
+ import { RenderAddress } from './RenderAddress'
3
+
4
+ export default defineCases(
5
+ 'RenderAddress',
6
+ {
7
+ Playground: {
8
+ tweaks: {
9
+ method: tweak.text('GET'),
10
+ url: tweak.text(
11
+ '/render/button/playground?theme=light&t.variant=accent',
12
+ ),
13
+ },
14
+ render: (t) => <RenderAddress method={t.method} url={t.url} />,
15
+ },
16
+ Default: () => (
17
+ <RenderAddress url="/render/button/playground?theme=light&t.variant=accent" />
18
+ ),
19
+ },
20
+ { level: 'molecule' },
21
+ )
@@ -0,0 +1,35 @@
1
+ .dcui-address {
2
+ display: flex;
3
+ align-items: center;
4
+ gap: var(--dc-space-4);
5
+ width: 100%;
6
+ /* border-box so the 100% width + padding + border stays inside the container
7
+ (matches the docked tweaks panel's right edge instead of overflowing it). */
8
+ box-sizing: border-box;
9
+ border: var(--dc-border-line);
10
+ border-radius: var(--dc-radius-sm);
11
+ background: var(--dc-surface);
12
+ padding: var(--dc-space-3) var(--dc-space-4);
13
+ }
14
+ /* A solid accent tag (the accent Button's fill), but inert — it's just a label. */
15
+ .dcui-address-method {
16
+ flex: 0 0 auto;
17
+ line-height: 1;
18
+ font-family: var(--dc-font-mono);
19
+ font-size: var(--dc-text-xs);
20
+ font-weight: var(--dc-weight-medium);
21
+ color: var(--dc-brand-fg);
22
+ background: var(--dc-brand);
23
+ padding: 0.3125rem var(--dc-space-3);
24
+ border-radius: var(--dc-radius-sm);
25
+ }
26
+ .dcui-address-url {
27
+ font-family: var(--dc-font-mono);
28
+ font-size: var(--dc-text-sm);
29
+ color: var(--dc-fg);
30
+ flex: 1;
31
+ min-width: 0;
32
+ overflow: hidden;
33
+ text-overflow: ellipsis;
34
+ white-space: nowrap;
35
+ }
@@ -0,0 +1,7 @@
1
+ **RenderAddress** — a monospace address bar with an HTTP method tag, a URL that scrolls if it overflows, and a copy button; reach for it to show a deterministic render URL (or any endpoint) the reader can copy.
2
+
3
+ ```tsx
4
+ <RenderAddress method="GET" url="/render/button/playground?theme=light" />
5
+ ```
6
+
7
+ Clicking the button copies `url` to the clipboard, flips the glyph to ✓ for ~1.2s, then reverts. It degrades silently when the clipboard is unavailable (e.g. an isolated frame) — the address still reads.
@@ -0,0 +1,26 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { renderToStaticMarkup } from 'react-dom/server'
3
+ import { RenderAddress } from './RenderAddress'
4
+
5
+ describe('RenderAddress', () => {
6
+ test('renders the URL with a default GET method tag and a copy control', () => {
7
+ const html = renderToStaticMarkup(
8
+ <RenderAddress url="/render/button/default" />,
9
+ )
10
+ expect(html).toContain('class="dcui-address-method"')
11
+ expect(html).toContain('>GET<')
12
+ expect(html).toContain('/render/button/default')
13
+ expect(html).toContain('aria-label="Copy address"')
14
+ })
15
+
16
+ test('renders the copy glyph in its un-copied resting state', () => {
17
+ const html = renderToStaticMarkup(<RenderAddress url="/x" />)
18
+ expect(html).toContain('⧉')
19
+ expect(html).not.toContain('✓')
20
+ })
21
+
22
+ test('reflects a custom method tag', () => {
23
+ const html = renderToStaticMarkup(<RenderAddress url="/x" method="POST" />)
24
+ expect(html).toContain('>POST<')
25
+ })
26
+ })
@@ -0,0 +1,43 @@
1
+ import { useState } from 'react'
2
+ import { IconButton } from '../controls/IconButton'
3
+
4
+ /**
5
+ * Display Case — RenderAddress
6
+ * A monospace address bar: an HTTP method tag, a URL that truncates with an
7
+ * ellipsis if it overflows, and a copy button. Shows a deterministic render URL (or any
8
+ * endpoint) the reader can copy — the browse chrome uses it for the live
9
+ * exhibit's address.
10
+ */
11
+
12
+ export interface RenderAddressProps {
13
+ /** The address shown and copied. */
14
+ url: string
15
+ /** HTTP method tag on the left. */
16
+ method?: string
17
+ }
18
+
19
+ export function RenderAddress({ url, method = 'GET' }: RenderAddressProps) {
20
+ const [copied, setCopied] = useState(false)
21
+ const copy = () => {
22
+ try {
23
+ navigator.clipboard?.writeText(url)
24
+ } catch {
25
+ // Clipboard may be unavailable in an isolated frame — the address still reads.
26
+ }
27
+ setCopied(true)
28
+ setTimeout(() => setCopied(false), 1200)
29
+ }
30
+ return (
31
+ <div className="dcui-address">
32
+ <span className="dcui-address-method">{method}</span>
33
+ <span className="dcui-address-url">{url}</span>
34
+ <IconButton
35
+ glyph={copied ? '✓' : '⧉'}
36
+ label="Copy address"
37
+ variant="bare"
38
+ size="sm"
39
+ onClick={copy}
40
+ />
41
+ </div>
42
+ )
43
+ }