@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,65 @@
1
+ ---
2
+ name: display-case-author-placard-doc
3
+ description: >
4
+ Write or improve a component's `<name>.placard.md` — the prose doc panel that
5
+ tells an AI agent (and a human) how to use the component correctly on the first
6
+ try. Use when adding a new shared component, when a component has a `.case.tsx`
7
+ but no `.placard.md`, or when asked to "write a placard doc", "document this
8
+ component", "add a doc panel", or "write usage docs for <component>".
9
+ ---
10
+
11
+ Author a colocated `<name>.placard.md` for a component, following
12
+ `../../docs/writing-placard-docs.md`. The doc is **not** required by any lint
13
+ (only `.case.tsx` is) — its whole value is quality, so apply the bar deliberately.
14
+
15
+ ## The one principle
16
+
17
+ The reader already holds the **source** (every prop, type, default) and the
18
+ **manifest** (every case, `renderUrl`, tweak schema). The doc earns its place only
19
+ by carrying what neither can express: **intent, judgement, and contract.** Aim for
20
+ a doc that lets a reader pick the component and use it correctly **without opening
21
+ the source.** Restate *meaning*, never the type signature.
22
+
23
+ ## Steps
24
+
25
+ 1. **Gather intent, not just signatures.** Read `<name>.tsx` for the exact export
26
+ name, props, defaults, and — critically — *what each callback emits* (the
27
+ changed item vs. the whole next value: invisible in the type, guessed wrong).
28
+ Read `<name>.case.tsx` for the real variants, and any existing `.placard.md` to
29
+ improve rather than replace.
30
+ 2. **Find the decision boundary.** Scan the package's component inventory (its
31
+ `README.md` / sibling components) for the parts this one is easily confused
32
+ with — the long-list vs. short-list control, the inline notice vs. the toast.
33
+ You will name the right sibling so a reader doesn't misuse this one.
34
+ 3. **Draft top-down, highest value first** (stop once the source is unnecessary):
35
+ - **Identity line** — bold name, em-dash, one sentence: what it is + the single
36
+ most common reason to reach for it. Must stand alone in a library scan.
37
+ - **Canonical example** — one minimal, **correct, copy-pasteable** `tsx` snippet
38
+ of the idiomatic call. A second snippet only for a genuinely different mode.
39
+ - **Variants and when to pick each** — meaning and use, not the type union; name
40
+ the default; a GFM table once there are more than three.
41
+ - **Decision boundary** — "use this for X; for Y reach for `Sibling`." The
42
+ single biggest defense against picking the wrong primitive.
43
+ - **State & callback contract** — controlled vs. uncontrolled; exactly what each
44
+ callback emits; what fires on mount.
45
+ - **Composition & a11y** — required wrappers (`FormField`…), what the component
46
+ handles itself (so the reader doesn't double up a `role`), what the caller
47
+ must supply (a label, alt text).
48
+ - **Gotchas / anti-patterns** — one bullet each; skip if none.
49
+ 4. **Cut what rots or duplicates:** no prop tables retyping TypeScript, no list of
50
+ the component's cases / `renderUrl`s / tweak schema (the manifest owns those),
51
+ no styling internals, no changelog, nothing the name already says.
52
+ 5. **Write for the medium.** CommonMark + GFM, but **no raw HTML** (stripped) and
53
+ **no syntax highlighting** (use fences for structure only). Be dense — the file
54
+ is ingested into a context window; every line earns its tokens. Bold lead line
55
+ always; `##` headings only once the doc has enough sections to need them. A
56
+ simple atom is ~5 lines (see `../../../ui/src/components/button.placard.md`).
57
+ 6. **Verify.** Open the doc panel (`bun run display-case`) to confirm it renders,
58
+ and re-check that every example is correct and copy-pasteable — a stale example
59
+ is a bug, since those lines get pasted verbatim.
60
+
61
+ ## Reference
62
+
63
+ Full guide: [`../../docs/writing-placard-docs.md`](../../docs/writing-placard-docs.md).
64
+ Annotated specimen: [`../../docs/examples/tweak-control.placard.md`](../../docs/examples/tweak-control.placard.md).
65
+ How the panel renders (GFM, the two limits): [`../../docs/documentation-panel.md`](../../docs/documentation-panel.md).
@@ -0,0 +1,19 @@
1
+ # display-case-review
2
+
3
+ Run Display Case's checks and triage the findings into fixes.
4
+
5
+ ## What it does
6
+
7
+ Runs `display-case check` — accessibility (axe, WCAG A/AA), visual regression (pixel diff vs baselines), and design-token conformance — then classifies each finding (case-authoring vs component/token issue vs intended visual change) and proposes the fix at the right layer.
8
+
9
+ ## When it triggers
10
+
11
+ "Review the components", "check accessibility/contrast", "run the display-case checks", or verifying the showcase after a UI change.
12
+
13
+ ## How it works
14
+
15
+ 1. `bun run display-case:check` (or a single phase via `--a11y` / `--visual` / `--tokens`).
16
+ 2. Triage: fix the case when an example is malformed; fix the component/tokens when the component itself fails; re-record baselines with `--update` only for intended changes.
17
+ 3. Re-run until clean. Never silence a real finding.
18
+
19
+ Details: [`../../docs/testing.md`](../../docs/testing.md).
@@ -0,0 +1,30 @@
1
+ ---
2
+ name: display-case-review
3
+ description: >
4
+ Run Display Case's accessibility, visual-regression, and design-token checks
5
+ over the component showcase, then triage the findings and propose fixes. Use
6
+ when asked to "review the components", "check accessibility / contrast", "run
7
+ the display-case checks", or to verify the showcase after a UI change.
8
+ ---
9
+
10
+ Run the Display Case `check` runner and turn its output into actionable fixes.
11
+
12
+ ## Steps
13
+
14
+ 1. **Run the checks**: `bun run display-case:check` (or `bunx @awarebydefault/display-case check <pkgDir>`). Phases:
15
+ - **a11y** — axe, WCAG 2 A/AA, per case × theme.
16
+ - **visual** — pixel-diff each case (light + dark) against recorded baselines.
17
+ - **tokens** — design-token conformance.
18
+ - Run one phase with a flag, e.g. `bunx @awarebydefault/display-case check <pkgDir> --a11y`.
19
+ 2. **Triage each finding** by category:
20
+ - **a11y** (`label`, `color-contrast`, `aria-*`, …): if the case renders a bare control, fix the *case* (add a label/`aria-label`); if the component itself fails (contrast tokens, missing accessible name), fix the *component*/tokens.
21
+ - **visual** diffs: inspect the written `*.diff.png`; if the change is intended, re-record with `--update`; otherwise it's a regression to fix.
22
+ - **tokens**: resolve the flagged custom property, or add it to the config `tokens.allow` if it's supplied by a host stylesheet.
23
+ 3. **Fix at the right layer** — prefer fixing the component/tokens over weakening the check. Never silence a real finding.
24
+ 4. **Re-run** until the relevant phase is clean (exit 0).
25
+
26
+ ## Notes
27
+
28
+ - Visual baselines live in the gitignored `.display-case/baselines/` by default (or the configured `baselineDir`); `--update` re-records them.
29
+ - The runner drives the same `/render` endpoint a viewer sees, so findings reflect real appearance.
30
+ - Deeper detail: `../../docs/testing.md`.
@@ -0,0 +1,20 @@
1
+ # display-case-snapshot
2
+
3
+ Render and screenshot a single component variant in isolation — in light and dark — without booting the app.
4
+
5
+ ## What it does
6
+
7
+ Resolves a component's case from the Display Case manifest, builds the deterministic `/render/<component>/<case>?theme=…&t.<tweak>=…` URL, and captures the chrome-free HTML with a headless browser. Because the full state lives in the URL, the same inputs always produce the same image.
8
+
9
+ ## When it triggers
10
+
11
+ "Show me the Button", "screenshot the Alert error state", "what does the survey form look like in dark mode", or any request to see/capture a component's appearance for review.
12
+
13
+ ## How it works
14
+
15
+ 1. Ensure `bun run display-case` is running (port 3100).
16
+ 2. Read `/manifest.json` (or `--print-manifest`) to find the case's `renderUrl` and `tweaks`.
17
+ 3. Append `theme`, optional `width`, and `t.<name>` params.
18
+ 4. Screenshot the page with a headless browser, light and dark.
19
+
20
+ See [`../../docs/ai-agents.md`](../../docs/ai-agents.md) for the endpoint reference.
@@ -0,0 +1,29 @@
1
+ ---
2
+ name: display-case-snapshot
3
+ description: >
4
+ Render and screenshot a single component variant in isolation, in light and
5
+ dark themes, without booting the app. Use when asked to "show me <component>",
6
+ "screenshot a component", "what does <case> look like", or to capture a
7
+ component's appearance for review. Drives the Display Case dev server's
8
+ chrome-free /render endpoint.
9
+ ---
10
+
11
+ Capture one component case as an image, deterministically, via Display Case.
12
+
13
+ ## Steps
14
+
15
+ 1. **Ensure the server is running.** Start it if needed: `bun run display-case` (serves at `http://localhost:3100`). It needs no database or app — just the showcase.
16
+ 2. **Enumerate.** `GET http://localhost:3100/manifest.json` (or `bunx @awarebydefault/display-case <pkgDir> --print-manifest`, no server needed) to find the component and its cases. Each case has a `renderUrl` like `/render/tweak-control/playground` and an optional `tweaks` schema.
17
+ 3. **Build the render URL.** Append query params to pin the exact state:
18
+ - `theme=light|dark`
19
+ - `width=<px>` (optional, constrains to a centered max-width)
20
+ - `t.<name>=<value>` per tweak (`boolean`→`1`/`0`; `number`→numeric; text/choice verbatim)
21
+ - e.g. `http://localhost:3100/render/tweak-control/playground?theme=dark&t.kind=choice`
22
+ 4. **Rasterize.** The endpoint returns chrome-free **HTML**, so capture it with a headless browser (Playwright `page.goto(url); page.screenshot()`). Capture both `theme=light` and `theme=dark` unless told otherwise.
23
+ 5. **Report** the screenshots and the exact URLs used, so the state is reproducible.
24
+
25
+ ## Notes
26
+
27
+ - The render is isolated (its own document, `data-theme` on the root), so it never includes the browse sidebar/controls — ideal for clean snapshots.
28
+ - Same URL → same render. Pin tweaks in the URL rather than interacting with the page.
29
+ - For the full contract see `../../docs/ai-agents.md`.
@@ -0,0 +1,240 @@
1
+ import { afterEach, expect, test } from 'bun:test'
2
+ import { readFileSync, statSync } from 'node:fs'
3
+ import { mkdir, rm, writeFile } from 'node:fs/promises'
4
+ import { join, relative } from 'node:path'
5
+ import { cacheDir } from '../core/discovery'
6
+ import type {
7
+ A11yViolation,
8
+ DisplayCaseConfig,
9
+ RenderDriver,
10
+ RenderedPage,
11
+ } from '../index'
12
+ import { makeTempDir, writeFiles } from '../testing/test-helpers'
13
+ import {
14
+ type A11yScanStatus,
15
+ type A11yVariant,
16
+ createA11yScanner,
17
+ } from './a11y-scanner'
18
+
19
+ const dirs: string[] = []
20
+ afterEach(async () => {
21
+ while (dirs.length)
22
+ await rm(dirs.pop() as string, { recursive: true, force: true })
23
+ })
24
+
25
+ // Display Case's own version, mirrored exactly so the cache entries we hand-write
26
+ // are accepted (a `toolVersion` mismatch discards an entry).
27
+ const TOOL_VERSION = (() => {
28
+ try {
29
+ return (
30
+ (
31
+ JSON.parse(
32
+ readFileSync(
33
+ join(import.meta.dir, '..', '..', 'package.json'),
34
+ 'utf8',
35
+ ),
36
+ ) as { version?: string }
37
+ ).version ?? '0'
38
+ )
39
+ } catch {
40
+ return '0'
41
+ }
42
+ })()
43
+
44
+ const VIOLATION: A11yViolation = {
45
+ id: 'color-contrast',
46
+ help: 'Elements must have sufficient contrast',
47
+ nodes: 1,
48
+ impact: 'serious',
49
+ }
50
+
51
+ /** A render driver whose audit always returns one violation; records opens. */
52
+ function fakeDriver(opens: string[]): RenderDriver {
53
+ const page: RenderedPage = {
54
+ screenshot: async () => new Uint8Array(),
55
+ audit: async () => [VIOLATION],
56
+ dispose: async () => {},
57
+ }
58
+ return {
59
+ open: async (url) => {
60
+ opens.push(url)
61
+ return page
62
+ },
63
+ close: async () => {},
64
+ }
65
+ }
66
+
67
+ /** A driver that cannot launch — stands in for a missing browser/axe. */
68
+ function failingDriver(): RenderDriver {
69
+ throw new Error('no browser')
70
+ }
71
+
72
+ interface Harness {
73
+ pkgDir: string
74
+ caseAbs: Record<string, string>
75
+ }
76
+
77
+ /** Temp package with `n` case files, each importing nothing. */
78
+ async function harness(ids: string[]): Promise<Harness> {
79
+ const pkgDir = await makeTempDir()
80
+ dirs.push(pkgDir)
81
+ const files: Record<string, string> = {}
82
+ const caseAbs: Record<string, string> = {}
83
+ for (const id of ids) {
84
+ files[`src/${id}.case.tsx`] = `export const cases = {} // ${id}\n`
85
+ caseAbs[id] = join(pkgDir, `src/${id}.case.tsx`)
86
+ }
87
+ await writeFiles(pkgDir, files)
88
+ return { pkgDir, caseAbs }
89
+ }
90
+
91
+ /** Hand-write a cache entry whose recorded stats match the case file on disk, so
92
+ * the scanner's stat fast-path accepts it as reusable without a content hash. */
93
+ async function seedCache(
94
+ h: Harness,
95
+ componentId: string,
96
+ caseId: string,
97
+ theme: string,
98
+ violations: A11yViolation[],
99
+ ): Promise<void> {
100
+ const dir = join(cacheDir(h.pkgDir), 'a11y')
101
+ await mkdir(dir, { recursive: true })
102
+ const abs = h.caseAbs[componentId]
103
+ const st = statSync(abs)
104
+ const entry = {
105
+ toolVersion: TOOL_VERSION,
106
+ hash: 'unused-when-stats-match',
107
+ files: [
108
+ { path: relative(h.pkgDir, abs), mtimeMs: st.mtimeMs, size: st.size },
109
+ ],
110
+ violations,
111
+ scannedAt: 1,
112
+ }
113
+ await writeFile(
114
+ join(dir, `${componentId}__${caseId}__${theme}.json`),
115
+ JSON.stringify(entry),
116
+ )
117
+ }
118
+
119
+ function makeScanner(
120
+ h: Harness,
121
+ driverFactory: () => RenderDriver,
122
+ onResult: (
123
+ c: string,
124
+ cs: string,
125
+ th: 'light' | 'dark',
126
+ s: A11yScanStatus,
127
+ ) => void,
128
+ ) {
129
+ const config: DisplayCaseConfig = {
130
+ title: 'T',
131
+ roots: ['src/**/*.case.tsx'],
132
+ providers: { driver: driverFactory },
133
+ }
134
+ return createA11yScanner({
135
+ pkgDir: h.pkgDir,
136
+ config,
137
+ baseUrl: () => 'http://localhost:0',
138
+ caseFileAbs: (id) => h.caseAbs[id] ?? null,
139
+ onResult,
140
+ })
141
+ }
142
+
143
+ const variant = (
144
+ componentId: string,
145
+ caseId: string,
146
+ theme: 'light' | 'dark',
147
+ ): A11yVariant => ({ componentId, caseId, theme })
148
+
149
+ test('off mode is a no-op: no results, no scans', async () => {
150
+ const h = await harness(['foo'])
151
+ await seedCache(h, 'foo', 'default', 'light', [VIOLATION])
152
+ const opens: string[] = []
153
+ const calls: string[] = []
154
+ const scanner = makeScanner(
155
+ h,
156
+ () => fakeDriver(opens),
157
+ (c, cs, th) => calls.push(`${c}/${cs}/${th}`),
158
+ )
159
+ await scanner.populateAtStartup([variant('foo', 'default', 'light')], 'off')
160
+ expect(calls).toEqual([])
161
+ expect(opens).toEqual([])
162
+ await scanner.close()
163
+ })
164
+
165
+ test('cached mode emits reusable cached results and runs no scans', async () => {
166
+ const h = await harness(['foo', 'bar'])
167
+ await seedCache(h, 'foo', 'default', 'light', [VIOLATION])
168
+ // `bar` has no cache entry → it must stay unmarked (no emit) in cached mode.
169
+ const opens: string[] = []
170
+ const results: Array<{ id: string; status: A11yScanStatus }> = []
171
+ const scanner = makeScanner(
172
+ h,
173
+ () => fakeDriver(opens),
174
+ (c, cs, th, status) => results.push({ id: `${c}/${cs}/${th}`, status }),
175
+ )
176
+ await scanner.populateAtStartup(
177
+ [variant('foo', 'default', 'light'), variant('bar', 'default', 'light')],
178
+ 'cached',
179
+ )
180
+ expect(results).toHaveLength(1)
181
+ expect(results[0].id).toBe('foo/default/light')
182
+ expect(results[0].status).toEqual({ status: 'ok', violations: [VIOLATION] })
183
+ expect(opens).toEqual([]) // never launched the driver
184
+ await scanner.close()
185
+ })
186
+
187
+ test('refresh mode reuses fresh cache and scans uncached/stale variants', async () => {
188
+ const h = await harness(['foo', 'bar'])
189
+ await seedCache(h, 'foo', 'default', 'light', [VIOLATION])
190
+ // `bar` is uncached → refresh must scan it via the driver.
191
+ const opens: string[] = []
192
+ const results: Array<{ id: string; status: A11yScanStatus }> = []
193
+ let resolveScan: () => void = () => {}
194
+ const scanned = new Promise<void>((r) => {
195
+ resolveScan = r
196
+ })
197
+ const scanner = makeScanner(
198
+ h,
199
+ () => fakeDriver(opens),
200
+ (c, cs, th, status) => {
201
+ results.push({ id: `${c}/${cs}/${th}`, status })
202
+ if (c === 'bar') resolveScan()
203
+ },
204
+ )
205
+ await scanner.populateAtStartup(
206
+ [variant('foo', 'default', 'light'), variant('bar', 'default', 'light')],
207
+ 'refresh',
208
+ )
209
+ await scanned
210
+ const byId = Object.fromEntries(results.map((r) => [r.id, r.status]))
211
+ // foo came straight from cache (no scan); bar was scanned.
212
+ expect(byId['foo/default/light']).toEqual({
213
+ status: 'ok',
214
+ violations: [VIOLATION],
215
+ })
216
+ expect(byId['bar/default/light']).toEqual({
217
+ status: 'ok',
218
+ violations: [VIOLATION],
219
+ })
220
+ expect(opens).toHaveLength(1)
221
+ expect(opens[0]).toContain('/render/bar/default')
222
+ await scanner.close()
223
+ })
224
+
225
+ test('refresh mode emits nothing extra when the scan prerequisite is unavailable', async () => {
226
+ const h = await harness(['foo', 'bar'])
227
+ await seedCache(h, 'foo', 'default', 'light', [VIOLATION])
228
+ const results: string[] = []
229
+ const scanner = makeScanner(h, failingDriver, (c, cs, th) =>
230
+ results.push(`${c}/${cs}/${th}`),
231
+ )
232
+ await scanner.populateAtStartup(
233
+ [variant('foo', 'default', 'light'), variant('bar', 'default', 'light')],
234
+ 'refresh',
235
+ )
236
+ // The reusable cache hit still surfaces; the uncached variant is not scanned
237
+ // because the driver could not launch — no flood of `unavailable` events.
238
+ expect(results).toEqual(['foo/default/light'])
239
+ await scanner.close()
240
+ })