@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AwareByDefault
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,309 @@
1
+ # Display Case
2
+
3
+ A Bun-native, AI-friendly component showcase — a lightweight alternative to Storybook. Colocate `*.case.tsx` files next to your components, run one command, and browse every variant in an isolated preview. There is no Vite, no Webpack, and no config server: discovery, bundling, and serving are all done with Bun's built-in bundler and `Bun.serve`. It is strictly a development tool and is never bundled into an app build.
4
+
5
+ ```tsx
6
+ // tweak-control.case.tsx — colocated with tweak-control.tsx
7
+ import { defineCases } from '@awarebydefault/display-case'
8
+ import { TweakControl } from './tweak-control'
9
+
10
+ export default defineCases('TweakControl', {
11
+ Text: () => <TweakControl kind="text" label="Label" value="Save" />,
12
+ Boolean: () => <TweakControl kind="boolean" label="Disabled" value={false} />,
13
+ }, { level: 'atom' })
14
+ ```
15
+
16
+ ```bash
17
+ bunx @awarebydefault/display-case . # browse at http://localhost:3100
18
+ ```
19
+
20
+ > Display Case **dogfoods itself**: the example above (and throughout these docs) showcases its own UI parts — `TweakControl`, `FlowNav`, `TweaksPanel`, `Sidebar`, `Shell`.
21
+
22
+ ## Why not Storybook
23
+
24
+ - **No bundler config.** Storybook ships its own Vite/Webpack stack and addon ecosystem. Display Case uses the Bun bundler and `Bun.serve` directly — the only moving parts are the case files you write.
25
+ - **Plain data, lazy renders.** A case file default-exports a value, not a registry of side effects. Render functions are lazy thunks, so the server can import every module to build a manifest *without rendering anything*. This keeps discovery fast and import-safe.
26
+ - **Built for machine readers.** A single `/manifest.json` enumerates every component, case, doc, and tweak as file references. Any case renders in isolation at a deterministic URL, so an AI agent (or a screenshot tool) can snapshot exactly one variant. See [AI agents](docs/ai-agents.md).
27
+ - **Atomic Design hierarchy.** Cases declare a `level` (`atom` → `flow`); the sidebar groups by it. Each component's variants collapse under its name (collapsed by default — the chevron toggles them); clicking the name opens its first variant, so [order the most exploratory variant first](docs/writing-cases.md#order-the-default-landing-variant-first). Multi-step behavioural flows are first-class via `defineFlow`.
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ bun add -D @awarebydefault/display-case # dev dependency — it never ships in your app build
33
+ ```
34
+
35
+ Display Case is **Bun-native at runtime**, so it is published as TypeScript
36
+ source that Bun runs directly. It requires the [Bun](https://bun.sh) runtime
37
+ (not just Bun as an installer); running the CLI under Node exits with a notice.
38
+
39
+ ## Prerequisites
40
+
41
+ - [Bun](https://bun.sh) ≥ 1.2 (the dev server and bundler are Bun-native, and
42
+ the CLI runs on the Bun runtime).
43
+ - React 19 (peer dependency).
44
+ - For a default-backed `check` (a11y + visual regression): the visual toolchain (`playwright`, `@axe-core/playwright`, `pixelmatch`, `pngjs` + Chromium). These are **optional** and loaded lazily — browsing, `--print-manifest`, and `/render` snapshotting need none of them. Set it up on demand with `display-case init <pkgDir> --with-visual` (or `bunx playwright install chromium` after adding the deps), or replace it entirely with a custom `providers` backend. See [Testing](docs/testing.md#the-default-backend-is-lazy-and-optional).
45
+
46
+ ## Usage
47
+
48
+ Display Case is a per-package tool: one config, one manifest, one port. Point it at the package whose components you want to browse. Three steps get you running.
49
+
50
+ 1. **Add a config** (`display-case.config.ts` at the package root) — see the [60-second quick start](#60-second-quick-start) below.
51
+ 2. **Write cases** — colocate a `*.case.tsx` next to each component.
52
+ 3. **Run it** — wire up two npm scripts and use them:
53
+
54
+ ```jsonc
55
+ // package.json
56
+ {
57
+ "scripts": {
58
+ "display-case": "display-case .",
59
+ "display-case:check": "display-case check ."
60
+ }
61
+ }
62
+ ```
63
+
64
+ ```bash
65
+ bun run display-case # dev server → http://localhost:3100
66
+ bun run display-case:check # all phases: structure + tokens + ssr + a11y + visual-regression
67
+ bun run display-case:check --structure --tokens --ssr # static phases only (no browser)
68
+ ```
69
+
70
+ Or invoke the CLI directly without scripts:
71
+
72
+ ```bash
73
+ bunx @awarebydefault/display-case . # dev server
74
+ bunx @awarebydefault/display-case check . # all checks
75
+ ```
76
+
77
+ The target is `.` for the current package or an explicit `<pkgDir>`. `.`
78
+ resolves the nearest `display-case.config.ts` walking up from the current
79
+ directory, so it works from a package root or any subdirectory. The bare
80
+ `bunx @awarebydefault/display-case` (no argument) is **identical to `.`** — same cwd-based
81
+ resolution — so `bunx @awarebydefault/display-case` and `bunx @awarebydefault/display-case .` behave the same.
82
+
83
+ Display Case **dogfoods itself**: its own UI parts — `TweakControl`, `FlowNav`, `TweaksPanel`, `DocPanel`, `Sidebar`, `Shell` — are the kind of components you would case, and the [examples](docs/examples/) use them as their subjects. Page/flow cases that need app chrome typically live in a dedicated `page-cases/` directory (not colocated with route files) so the coverage lint isn't forced onto every route.
84
+
85
+ The dogfooding goes one layer deeper: the browse chrome has its **own design system** — "The Vitrine" — that lives inside this package at [`src/ui/design-system/`](src/ui/design-system/). Warm paper neutrals, a single marigold accent, Hanken Grotesk + JetBrains Mono, flat and border-led so the showcased component owns the visual weight. `chrome.css` is styled entirely from its `--dc-*` tokens — no host-app library, no borrowed tokens. The server inlines the token layer ahead of `chrome.css` and links the webfonts; the design system is kept in sync with its [claude.ai/design](https://claude.ai/design) source via `/design-sync`.
86
+
87
+ ### Developing Display Case itself
88
+
89
+ Display Case **dogfoods itself** — `display-case.config.ts` points the showcase
90
+ at its own "Vitrine" design-system components, so `bun dev` browses the very
91
+ chrome you're editing. Package scripts that target work on the tool (not on
92
+ consumer cases):
93
+
94
+ ```bash
95
+ bun run setup # first-time setup: deps + the Playwright Chromium browser
96
+ bun dev # showcase this package, with live reload of the app chrome
97
+ bun run check # static gate: structure + tokens + ssr (browser-free)
98
+ bun test # unit / type tests
99
+ bun run e2e # Playwright e2e tests for the browse chrome (boots its own server)
100
+ ```
101
+
102
+ `bun run setup` is idempotent — it runs `bun install` and installs the Chromium
103
+ binary the e2e suite drives. There is no Docker, no service stack, and no `.env`
104
+ to provision; Display Case is a self-contained dev tool.
105
+
106
+ Contributor guides — coding/testing/linting best practices, worktree-safe
107
+ execution, and the OpenSpec specs — live under [`contributing/`](contributing/)
108
+ (kept separate from this product `docs/` tree). See [AGENTS.md](AGENTS.md).
109
+
110
+ The browse chrome has its own Playwright e2e suite under [`e2e/`](e2e/) — it
111
+ launches a real Display Case server and drives the shell, navigation, docs panel,
112
+ and Primer. Locators are the `data-testid`s in
113
+ [`src/ui/test-ids.ts`](src/ui/test-ids.ts). Run `bun run e2e:install` once to
114
+ fetch Chromium. See [e2e/README.md](e2e/README.md).
115
+
116
+ The running server watches and live-reloads by default: editing a case, a component's implementation, a style, a doc, or the primer rebuilds and reloads the stage iframe in place (and refetches the manifest, so added/removed cases appear) — the selection is preserved. `bun dev` adds `--dev` on top, for iterating on **Display Case itself**: it also watches the chrome's own source and re-reads the inlined chrome CSS, and the shell does a full page reload on rebuild (the chrome bundle may have changed). Backend edits (the server, discovery, …) still need a manual restart; the page auto-reloads on the reconnect that follows. (We deliberately don't wrap it in `bun --watch`: re-invoking `Bun.build` inside a watch process corrupts module resolution.)
117
+
118
+ With `a11y.enabled` set (see [Configuration](docs/configuration.md#a11y)), the running chrome also surfaces accessibility results live — a per-variant marker in the nav and an Accessibility panel beside the case — scanned on demand for the viewed variant, cached, and re-evaluated when you edit the component. It's opt-in because it uses the same optional Playwright + axe toolchain as `check`; without it the panel shows an *unavailable* state and the server still browses normally.
119
+
120
+ With no phase flag, `check` runs every phase (minus any opted out via `check.defaultPhases`); naming a phase (`--structure`, `--a11y`, `--visual`, `--tokens`, `--ssr`) runs only those. The **structure** phase is a static set of best-practice rules — case+prompt coverage, hierarchy-level classification, primer presence, snapshot setup, flow/slug/tweak integrity, interactive cases keyed for in-place swaps, and opt-in composition (import-graph) rules — each with a `warn`/`error` severity (only errors fail the run; `--strict` escalates warnings). The **token** phase is a static parse — it flags any `var(--token)` that resolves to no custom property the package defines (in `globalStyles` or an inline `style` object), catching foreign/typo'd token names that silently fall back to a hardcoded value. A `var(--x, fallback)` is still flagged: the rule is vocabulary conformance, not CSS validity. Exempt a reference with an `allow: unknown-token` comment, or list host-app-provided tokens under `tokens.allow` in the config. The **ssr** phase renders every case on the server (no browser) and fails on any that can't render before scripts run — i.e. that touch a browser-only API *during render*. It's the enforced form of "keep render pure" and a precise alternative to a static "no browser APIs" lint (which would wrongly flag legitimate effect/handler usage). A component that genuinely needs a browser opts out with `browserOnly: true` in its case meta, which also makes Display Case render it on the client wherever it appears.
121
+
122
+ The three static phases — **`--structure`**, **`--tokens`**, and **`--ssr`** — are the ones worth gating a showcased package on in CI: they need no browser and run in milliseconds. The structure phase's `case-placard-coverage` rule subsumes the standalone "every component has a colocated `*.case.tsx`" check. Wire `display-case check --structure --tokens --ssr` into your lint/CI step. Full rule list and escape hatches: [Testing → Structure checks](docs/testing.md#structure-checks); per-rule config: [Configuration → `check`](docs/configuration.md#check).
123
+
124
+ ## Agent setup (`init` / `uninstall`)
125
+
126
+ Display Case is built to be driven by AI agents, and one command wires a repo up for that:
127
+
128
+ ```bash
129
+ display-case init <pkgDir> [--agent=claude] [--with-visual] [--dry-run] [--json]
130
+ display-case uninstall <pkgDir> [--agent=claude] [--dry-run] [--json]
131
+ ```
132
+
133
+ `init` makes a repo agent-ready by, **idempotently**:
134
+
135
+ - merging a `display-case` entry into the agent's launch config (`.claude/launch.json` for Claude Code) — never touching your other entries;
136
+ - installing the bundled skills (`display-case-snapshot`, `display-case-author-case`, `display-case-author-placard-doc`, `display-case-review`) into the agent's skills directory (`.claude/skills/`);
137
+ - adding a sentinel-marked **agent-guide pointer** to your instructions file (`AGENTS.md` if present, else `CLAUDE.md`).
138
+
139
+ `uninstall` reverses exactly those, and **only** those — it removes the `display-case` launch entry, the bundled skills, and the pointer block, leaving anything you authored untouched.
140
+
141
+ Both commands are safe to re-run: a second `init` converges to the bundled state — unchanged artifacts report `skipped`, and any that drifted (a skill, the launch entry, or the agent-guide pointer block) are reconciled in place and reported as `updated`. `uninstall` on a clean repo reports nothing to remove. Guarantees:
142
+
143
+ - **`--dry-run`** — print the plan without writing or removing anything.
144
+ - **`--json`** — emit the plan/report as machine-readable JSON (`{ command, agent, dryRun, items: [{ artifact, action, detail }] }`) for agent consumption; the default output is human-readable.
145
+ - **`--agent=<id>`** — choose the target agent (default `claude`); an unsupported agent fails fast and writes nothing.
146
+ - **`--with-visual`** (init only) — also set up the optional visual-regression toolchain (`bun add --dev playwright @axe-core/playwright pixelmatch pngjs`, then `bunx playwright install chromium`). Omitted, the step is skipped; in an interactive TTY, `init` prompts for it. Needed only for a default-backed `check` — see [Testing](docs/testing.md#the-default-backend-is-lazy-and-optional).
147
+
148
+ Once installed, an agent can browse, snapshot, document, and review components via the bundled skills and the [AI-agent guide](docs/ai-agents.md).
149
+
150
+ ### Worktree-safe by default
151
+
152
+ Agents often work in a **git worktree** (an isolated checkout) so their edits don't touch your working copy. Display Case is built for this: it holds no global or main-repo state. Resolution, the `.display-case/` build cache, and repo-relative paths all anchor to the package you point it at — so a worktree's run stays entirely inside that worktree, and two checkouts never clobber each other's output. An agent's component edits show up in the showcase it serves.
153
+
154
+ There are just two ways to name the target, both anchored where you'd expect:
155
+
156
+ - **`.` (or no argument)** — discovers the nearest `display-case.config.ts` walking up from the current directory. Run from anywhere inside the worktree (any depth) and you get that worktree's package.
157
+ - **explicit `<pkgDir>`** (`display-case apps/foo`) — used as given, and validated to contain a `display-case.config.ts` (a wrong directory fails loudly rather than serving an empty showcase). Relative paths resolve against the current directory, so a worktree-relative path stays in the worktree.
158
+
159
+ The one rule: launch it from inside the worktree (cwd within the checkout), or pass a worktree path explicitly — don't rely on a process cwd that points at a sibling checkout. Agent launch configs (`.claude/launch.json`) pass an explicit package path for exactly this reason.
160
+
161
+ ## 60-second quick start
162
+
163
+ 1. Add a config at your package root:
164
+
165
+ ```ts
166
+ // display-case.config.ts
167
+ import { defineConfig } from '@awarebydefault/display-case'
168
+
169
+ export default defineConfig({
170
+ title: 'Display Case',
171
+ roots: ['src/components/**/*.case.tsx'],
172
+ globalStyles: ['./src/tokens.css', './src/components.css'],
173
+ })
174
+ ```
175
+
176
+ 2. Write a case file next to a component:
177
+
178
+ ```tsx
179
+ // src/components/tweak-control.case.tsx
180
+ import { defineCases } from '@awarebydefault/display-case'
181
+ import { TweakControl } from './tweak-control'
182
+
183
+ export default defineCases('TweakControl', {
184
+ Variants: () => (
185
+ <div style={{ display: 'flex', gap: '0.5rem' }}>
186
+ <TweakControl kind="text" label="Label" value="Save" />
187
+ <TweakControl kind="boolean" label="Disabled" value={false} />
188
+ </div>
189
+ ),
190
+ }, { level: 'atom' })
191
+ ```
192
+
193
+ 3. Start the server and open the printed URL:
194
+
195
+ ```bash
196
+ bunx @awarebydefault/display-case . # or `bun run display-case` once the script is wired up
197
+ ```
198
+
199
+ Full walkthrough: [Quick start](docs/quick-start.md).
200
+
201
+ ## Feature tour
202
+
203
+ **Typed tweaks** — interactive controls whose values are URL-encoded, so a tweaked state is shareable and snapshottable.
204
+
205
+ ```tsx
206
+ import { defineCases, tweak } from '@awarebydefault/display-case'
207
+ import { TweakControl } from './tweak-control'
208
+
209
+ export default defineCases('TweakControl', {
210
+ Playground: {
211
+ tweaks: {
212
+ label: tweak.text('Opacity'),
213
+ kind: tweak.choice(['text', 'boolean', 'choice'], 'choice'),
214
+ disabled: tweak.boolean(false),
215
+ },
216
+ render: (t) => (
217
+ <TweakControl
218
+ kind={t.kind as 'text' | 'boolean' | 'choice'}
219
+ label={t.label}
220
+ disabled={t.disabled}
221
+ />
222
+ ),
223
+ },
224
+ })
225
+ ```
226
+
227
+ **Flows** — interactive multi-step flows, each step individually addressable, with in-step `goto` transitions and preset step state.
228
+
229
+ ```tsx
230
+ import { defineFlow } from '@awarebydefault/display-case'
231
+
232
+ export default defineFlow('Sign-in flow', {
233
+ steps: {
234
+ 'Request link': {
235
+ transitions: ['Check email'],
236
+ render: ({ goto }) => <RequestLink onSubmit={() => goto('Check email')} />,
237
+ },
238
+ 'Check email': { render: () => <CheckEmail /> },
239
+ },
240
+ })
241
+ ```
242
+
243
+ **Documentation panel** — a component's sibling `<component>.placard.md` renders alongside its cases as full CommonMark + GFM.
244
+
245
+ **Isolated render endpoint** — every case is also reachable at `/render/<component>/<case>`, the exact document the browse iframe embeds and the check runner screenshots.
246
+
247
+ **Pre-scripting (server) rendering** — the isolated `/render/<component>/<case>` document and the `/render/primer` document are rendered to complete, themed HTML on the server *before* the page's scripts run. Fetch the address without executing it (a crawler, a screenshot before scripts settle, plain `curl`) and the case content — under the requested `?theme=` and tweaks — is already there; the client then *adopts* that markup (hydrates) to drive interactivity (tweaks, in-place swaps, the primer's scrollspy). This is the groundwork for hosting a Display Case as a real webapp beyond localhost. Two things to know as an author:
248
+
249
+ - **Browser-only cases fall back automatically.** A case (or component) that touches a browser-only API (`window`, layout measurement, canvas…) *while rendering* throws on the server; Display Case catches it, delivers that case's document empty, and the client renders it — the surrounding surface is unaffected. The console logs which case fell back.
250
+ - **Keep render deterministic.** Pre-rendering means the server and client must produce the same markup. A case that uses `Date.now()`, `Math.random()`, or locale/timezone-dependent formatting *during render* will mismatch on adopt (the client re-renders it and logs `adopt mismatch`). These already make a case a poor snapshot subject — pass such values in as fixed tweaks instead.
251
+
252
+ **App chrome** — the [`decorator`](docs/configuration.md#decorator) receives each case's `level`, `sourcePath`, and `area`, so a consuming app can render **page**/**flow** cases inside their real navigation/layout (and leave smaller components bare). Tag a case with `meta.area`, or organize cases into area folders and read `sourcePath`.
253
+
254
+ **CSS-in-JS (Material UI / emotion)** — components styled by a runtime CSS-in-JS library are first-class. A [**style engine**](docs/style-engines.md) collects the styling emitted *during* the server render and delivers it before scripting, so emotion/MUI (and styled-components) cases are styled in the chrome-free snapshot with **no flash** — not a `browserOnly` opt-out. Configure `styleEngines` (server-side extraction) alongside the `decorator` (the `ThemeProvider`); Display Case takes on no runtime dependency on the styling library. Static CSS (Tailwind output, design tokens) goes through [`globalStyles`](docs/configuration.md#globalstyles) instead.
255
+
256
+ **Primer** — point `primer` at an authored `.mdx` document and the chrome gains a **Primer / Cases** mode switch in the sidebar. The Primer is long-form "wall text": a scrolling reading page with embedded **live specimens**, rendered in its own isolated frame (like `/render`). The MDX may import any component — case files *and* arbitrary `.tsx` — and wraps each specimen in the `<Display>` contract:
257
+
258
+ ```mdx
259
+ import { Button } from './components'
260
+
261
+ # Our design system
262
+
263
+ <Display title="Button" subtitle="The one true action" theme="dark">
264
+ <Button variant="accent">Snapshot</Button>
265
+ </Display>
266
+ ```
267
+
268
+ `<Display>` takes `title` (also the sidebar table-of-contents entry + scroll anchor), an optional `subtitle`, and an optional `theme` that forces a light/dark scope inside that card only — so a dark-mode component sits correctly on a light page. `<Display>` is provided to the MDX automatically; no import needed. In the table of contents, each `<Display>` nests under the `#`/`##` heading above it; those headings are themselves navigable, collapsible group headers (the `#` page title is the "top of page" entry), and long entries truncate with an ellipsis.
269
+
270
+ The Primer is a real browse route — `/primer` (the chrome-free document lives at the reserved `/render/primer`) — so the mode switch is a navigation step that back/forward cross and a copied link reopens. By default the chrome lands on the Primer at the root path; set [`landing: 'cases'`](docs/configuration.md#landing) to make the Cases library the bare-`/` landing instead. The explicit `/primer` route always opens the Primer, and a case deep link always opens the library, regardless of the setting.
271
+
272
+ **Device toolbar** — the browse chrome's header carries the viewport controls: a **Responsive** mode (full / preset widths with manual zoom) and **fixed device sizes** (1080p, 4K, iPhone, iPad, Pixel, … or a custom `W × H`, with a rotate button) that render the iframe at exact pixels and auto-scale to fit the panel — like Chrome DevTools' device mode.
273
+
274
+ **Browser-safe bundling** — the render bundle inlines the consumer's `BUN_PUBLIC_*` env (so a `process.env.*` read in app code doesn't throw `process is not defined` and blank the showcase), neutralizes anchor clicks that would unload the isolated frame, and shows an explanatory banner if a bundled module still references a Node/Bun runtime global.
275
+
276
+ **Checks** — `display-case check` runs five phases: structure best-practice rules, design-token conformance (a static parse of `var()` references against the package's defined tokens), server-render safety (`ssr`: every case must render before scripts run), axe-core accessibility audits, and pixel-diff visual regression against stored baselines. The first three are browser-free and CI-friendly.
277
+
278
+ ## Publishing (hosting beyond localhost)
279
+
280
+ The dev server is for authoring on `localhost`. To host a showcase for a team, build a self-contained, deployable artifact:
281
+
282
+ ```bash
283
+ display-case publish <pkgDir> [--out=<dir>] [--base=<path>] [--static]
284
+ ```
285
+
286
+ Every surface — the browse shell (`/` and `/c/<component>/<case>` deep links), each isolated `/render/<component>/<case>`, and the primer — is **server-rendered before scripts run** and hydrates on the client, so the build reads (and screenshots, and crawls) without executing JS. The build is production-grade by default: minified, **content-hashed** assets (cached `immutable`), HTML served `no-cache`, **no** development machinery (no file watching, no live-reload stream, no on-demand a11y, no dev endpoints), and reproducible output.
287
+
288
+ Two forms:
289
+
290
+ - **Served (default)** — a thin production server (`server.ts`) plus the hashed asset bundle, a frozen manifest, a `package.json`, and a `Dockerfile` (with a `/health` check). Run it with `bun server.ts`, or build the Dockerfile and deploy it like any other service (one Dockerfile per service). It renders documents on request, so address-encoded themes/tweaks (`?theme=dark`, `?t.x=…`) are server-rendered at full fidelity. `--base=/showcase` hosts it under a subpath.
291
+ - **Static (`--static`)** — crawls every address and writes complete HTML files plus the bundle, hostable on any static file host with **no running server**. Files are keyed by path (default theme/tweaks); a query-encoded variation that has no per-path file resolves on the client after hydration (logged, not silently dropped).
292
+
293
+ The live accessibility scanner is a dev-only surface and is omitted from a published build. Display Case is still never bundled into a *consuming* application — a published showcase is its own, separate artifact you deploy on purpose.
294
+
295
+ ## Documentation
296
+
297
+ - [Quick start](docs/quick-start.md) — install, configure, and browse in a few minutes.
298
+ - [Writing cases](docs/writing-cases.md) — the `defineCases` / `defineFlow` authoring API.
299
+ - [Hierarchy](docs/hierarchy.md) — Atomic Design levels and flows.
300
+ - [Tweaks](docs/tweaks.md) — typed, URL-encoded controls.
301
+ - [Theming](docs/theming.md) — global styles, decorators, light/dark, viewport width.
302
+ - [Style engines](docs/style-engines.md) — CSS-in-JS (Material UI / emotion) styled before scripting.
303
+ - [Documentation panel](docs/documentation-panel.md) — rendering `.placard.md`.
304
+ - [Writing placard docs](docs/writing-placard-docs.md) — what to put in a `.placard.md`, for agents and humans.
305
+ - [Testing](docs/testing.md) — a11y + visual-regression checks and baselines.
306
+ - [CLI](docs/cli.md) — every command and flag.
307
+ - [AI agents](docs/ai-agents.md) — the manifest, render snapshotting, recommended workflow.
308
+ - [Configuration](docs/configuration.md) — full `defineConfig` reference.
309
+ - [Examples](docs/examples/) — runnable example case and placard-doc files.
@@ -0,0 +1,64 @@
1
+ **Display Case** — a Bun-native component showcase. This file is the authoring reference for writing **case files**; see [`docs/`](docs/) for the full guides and [`docs/ai-agents.md`](docs/ai-agents.md) for driving the tool as an agent.
2
+
3
+ ## Writing a case file
4
+
5
+ A case file is colocated next to its component: `tweak-control.tsx` → `tweak-control.case.tsx`. It **default-exports** `defineCases(...)`. Every showcased component must have one (the `display-case-coverage` lint enforces it).
6
+
7
+ ```tsx
8
+ import { defineCases, tweak } from '@awarebydefault/display-case'
9
+ import { TweakControl } from './tweak-control'
10
+
11
+ export default defineCases(
12
+ 'TweakControl', // display name
13
+ {
14
+ Variants: () => ( // a case = name → () => ReactNode
15
+ <div style={{ display: 'flex', gap: '0.5rem' }}>
16
+ <TweakControl kind="text" label="Label" />
17
+ <TweakControl kind="boolean" label="Disabled" />
18
+ </div>
19
+ ),
20
+ Playground: { // a case with interactive tweaks
21
+ tweaks: {
22
+ label: tweak.text('Variant'),
23
+ kind: tweak.choice(['text', 'boolean', 'number', 'choice'], 'choice'),
24
+ disabled: tweak.boolean(false),
25
+ },
26
+ render: (t) => (
27
+ <TweakControl
28
+ kind={t.kind as 'text' | 'boolean' | 'number' | 'choice'}
29
+ label={t.label}
30
+ disabled={t.disabled}
31
+ />
32
+ ),
33
+ },
34
+ },
35
+ { level: 'atom' }, // Atomic Design level (see below)
36
+ )
37
+ ```
38
+
39
+ ## Rules & conventions
40
+
41
+ - **No side effects at module top level.** Cases are lazy thunks — the render function must not run at import time. The dev server imports every case module to build the manifest *without* calling render, so a top-level effect (DOM access, network, etc.) would break it. Need state? Define a small component above the export and reference it: `Controlled: () => <MyDemo />`. **If you reuse that stateful wrapper across ≥2 cases, give each a distinct `key`** (`Two: () => <MyDemo key="two" …/>`) — the browse chrome swaps cases *in place* without unmounting, so an unkeyed wrapper keeps the previous case's state on switch (the `interactive-cases-keyed` structure check enforces this; full explanation in [docs/writing-cases.md](docs/writing-cases.md#authoring-rules)).
42
+ - **Tweaks** are typed controls: `tweak.text(default)`, `tweak.boolean(default)`, `tweak.number(default)`, `tweak.choice(options, default)`. Values are serializable and URL-encoded (`?t.<name>=`), so a tweaked state is shareable and snapshottable. Cast a `choice` value when passing it into a union-typed prop.
43
+ - **Hierarchy `level`** (third arg `{ level }`) groups the component in the sidebar, ordered: `atom → molecule → organism → template → page → flow`. Omit it and the component lands in an "Unclassified" group.
44
+ - **App chrome `area`** (third arg `{ area }`; also `defineFlow(name, { area, steps })`) is a free-form tag the package's `decorator` can use to wrap a `page`/`flow` case in app layout/nav. The decorator interprets it (Display Case mandates no values); it overrides folder-based detection via `sourcePath`. Omit to render bare.
45
+ - **Flows** showcase an interactive multi-step flow. Use `defineFlow('Sign-in flow', { steps: { 'Request link': { transitions: ['Check email'], render: ({ goto }) => <…/> }, … } })`; each step is ordered, individually addressable, and may advance the flow in place via the injected `goto`. Keep views pure — wire `goto` into the view's callbacks.
46
+ - **First case = default landing variant.** Insertion order is preserved, and clicking a component in the sidebar opens its first case. Lead with the most exploratory variant (a tweaked `Playground`, or a stateful "do-anything" demo); keep isolated single-state variants (`Disabled`, `With error`) after it — those are mainly for snapshots/visual-regression. Flow steps are ordered in flow sequence instead.
47
+ - **Edits are picked up on save; reload to see them.** The server watches case files and rebuilds on every change, manifest-shape included (order, names, `level`, tweak schema). No in-page HMR — reload the browser after an edit. (Rebuilds read the manifest in a fresh subprocess to dodge Bun's per-process module cache.)
48
+ - **Exhibits are centered by default.** Decorated components (`atom`…`template` — not `page`/`flow`) have their content centered in the frame: when a case's root flex/grid wraps or is narrower than the frame, its rows sit centered rather than top-left. This is a stylesheet default (`justify-content`/`align-content: center`), so an inline `style={{ justifyContent: 'flex-start' }}` on the case's root still wins if you need left alignment.
49
+ - **Naming**: display names and case names are sentence case ("With error", "Playground"). Slugs are derived automatically.
50
+ - **Docs panel**: a sibling `<component>.placard.md` is rendered in the preview as CommonMark + GFM.
51
+
52
+ ## Config
53
+
54
+ The consuming package has a `display-case.config.ts` (`defineConfig({ title, roots, globalStyles, decorator?, baselineDir?, tokens?, providers? })`) — `roots` are the globs that locate `*.case.tsx` files. Launch with `bun run display-case`; check accessibility + visual regression + tokens with `bun run display-case:check`.
55
+
56
+ ## Driving the tool (for agents)
57
+
58
+ The enumerate → snapshot → verify loop:
59
+
60
+ 1. **Enumerate** — `bun run display-case -- --print-manifest` (no server/browser needed) lists every component, case, hierarchy level, tweak schema, and the `caseFile`/`placardDoc` paths.
61
+ 2. **Snapshot** — with the server up (`bun run display-case`, port 3100), open `/render/<component>/<case>?theme=light|dark&t.<tweak>=<value>` — a chrome-free HTML document; rasterize it with a headless browser. Same URL → same render.
62
+ 3. **Verify** — `bun run display-case:check` runs a11y + visual + token checks (exit non-zero on failure).
63
+
64
+ Full reference: [docs/ai-agents.md](docs/ai-agents.md).
@@ -0,0 +1,126 @@
1
+ # AI agents
2
+
3
+ > Nav: [Quick start](quick-start.md) · [Writing cases](writing-cases.md) · [Hierarchy](hierarchy.md) · [Tweaks](tweaks.md) · [Theming](theming.md) · [Documentation panel](documentation-panel.md) · [Writing placard docs](writing-placard-docs.md) · [Testing](testing.md) · [CLI](cli.md) · **AI agents** · [Configuration](configuration.md)
4
+
5
+ Display Case is built to be read by machines as easily as by people. Two facts make it agent-friendly:
6
+
7
+ 1. A single **manifest** enumerates every component, case, doc, and tweak — as file references, not inlined content.
8
+ 2. Every case has a **deterministic render URL** that produces exactly one variant in isolation.
9
+
10
+ Together these let an agent discover what exists, read the source it points to, and snapshot any single state reproducibly.
11
+
12
+ ## Running the tool
13
+
14
+ Display Case requires Bun. Invoke it with `bunx @awarebydefault/display-case <pkgDir>` (or wrap it in a `bun run display-case` npm script — see [CLI](cli.md)).
15
+
16
+ - **Enumerate without a browser or server:** `bunx @awarebydefault/display-case <pkgDir> --print-manifest` prints the manifest JSON to stdout and exits. This is the cheapest way to discover what exists.
17
+ - **Start the server** (needed for `/render`, `/manifest.json`, `/doc`): `bunx @awarebydefault/display-case <pkgDir>` serves at `http://localhost:3100` (override with `--port`). It needs no database or app — only the showcased package. It runs until killed.
18
+ - **Accessibility result for a variant** (only when `a11y.enabled`): `GET /a11y?component=<id>&case=<id>&theme=light|dark` returns `{ status: 'ok', violations: [{ id, help, nodes, impact, details? }] }` when cached, `{ status: 'pending' }` when a scan was just enqueued (poll again, or it's pushed over the SSE stream as an `a11y` event), or `{ status: 'unavailable', reason }` when the scan toolchain can't run. The headless CI form is `display-case check --a11y` (exits non-zero on any violation).
19
+ - **Read the full a11y findings without re-running:** `display-case check --a11y` prints each violation's affected nodes and writes the complete run to `.display-case/a11y/last-check.json` — `{ scannedAt, total, results: [{ component, case, theme, violations }] }`, where each violation carries `details: [{ target, html, failureSummary?, contrast?: { foreground, background, ratio, required, fontSize?, fontWeight? } }]`. For a colour-contrast finding this is the exact element and failing pair; read this file to fix violations without driving a browser yourself.
20
+ - **Rasterize a render:** `/render/...` returns a complete **HTML document**, not an image. To capture it, drive a headless browser with Playwright:
21
+ ```ts
22
+ const page = await browser.newPage()
23
+ await page.goto('http://localhost:3100/render/tweak-control/playground?theme=dark')
24
+ await page.evaluate(() => document.fonts.ready)
25
+ await page.screenshot({ path: 'tweak-control-dark.png' })
26
+ ```
27
+
28
+ ## The manifest is a directory, not a dump
29
+
30
+ `/manifest.json` (or `--print-manifest`) returns a lightweight index. It contains *paths* to the real artifacts, so an agent reads the manifest first, then opens only the files it needs.
31
+
32
+ ```jsonc
33
+ {
34
+ "title": "Display Case",
35
+ "components": [
36
+ {
37
+ "id": "tweak-control",
38
+ "name": "TweakControl",
39
+ "level": "atom",
40
+ "isFlow": false,
41
+ "caseFile": "src/components/tweak-control.case.tsx",
42
+ "placardDoc": "src/components/tweak-control.placard.md",
43
+ "cases": [
44
+ {
45
+ "id": "variants",
46
+ "name": "Variants",
47
+ "browseUrl": "/c/tweak-control/variants",
48
+ "renderUrl": "/render/tweak-control/variants",
49
+ "tweaks": null,
50
+ "transitions": []
51
+ },
52
+ {
53
+ "id": "playground",
54
+ "name": "Playground",
55
+ "browseUrl": "/c/tweak-control/playground",
56
+ "renderUrl": "/render/tweak-control/playground",
57
+ "tweaks": {
58
+ "label": { "kind": "text", "default": "Variant" },
59
+ "kind": { "kind": "choice", "options": ["text", "boolean", "number", "choice"], "default": "choice" },
60
+ "disabled": { "kind": "boolean", "default": false }
61
+ },
62
+ "transitions": []
63
+ }
64
+ ]
65
+ }
66
+ ]
67
+ }
68
+ ```
69
+
70
+ Per component you get its `level`, `isFlow`, the package-relative `caseFile`, and the package-relative `placardDoc` (or `null`). Per case you get its `browseUrl`, its `renderUrl`, the declared `tweaks` schema (or `null`), and its `transitions` — the slugified ids of steps it can advance to (a flow's steps; `[]` for a regular case).
71
+
72
+ ## Snapshotting a single case
73
+
74
+ `renderUrl` points at the isolated render document — the same one the browse iframe embeds and the check runner screenshots. It accepts query parameters so an agent can pin an exact state:
75
+
76
+ ```
77
+ /render/tweak-control/playground?theme=dark&width=480&t.label=Variant&t.kind=choice&t.disabled=1
78
+ ```
79
+
80
+ | Parameter | Meaning |
81
+ | --- | --- |
82
+ | `theme=light\|dark` | Sets `data-theme` on the document root. Anything but `dark` is light. |
83
+ | `width=<px>` | Constrains the render to a centered `max-width` container. |
84
+ | `t.<name>=<value>` | Sets a tweak. `boolean` → `1`/`true`; `number` → numeric; text/choice verbatim. Missing values fall back to defaults. |
85
+
86
+ Because the state lives entirely in the URL, the same URL always produces the same render — ideal for deterministic screenshots and diffs. See [Tweaks](tweaks.md) and [Theming](theming.md) for the encoding rules.
87
+
88
+ ## Reading docs
89
+
90
+ If `placardDoc` is non-null, the raw Markdown is served verbatim at `/doc/<component>` (content type `text/markdown`). An agent can fetch it directly for usage guidance, or just read the `placardDoc` path from the filesystem. See [Documentation panel](documentation-panel.md).
91
+
92
+ To *write* a `placardDoc` rather than read one, follow [Writing placard docs](writing-placard-docs.md) — the bundled `display-case-author-placard-doc` skill drives exactly that workflow.
93
+
94
+ ## Endpoints
95
+
96
+ The dev server exposes:
97
+
98
+ | Path | Returns |
99
+ | --- | --- |
100
+ | `/` and `/c/<component>/<case>` | The browsing shell (client-side routed). |
101
+ | `/render/<component>/<case>` | The isolated render document (accepts `theme`, `width`, `t.*`). |
102
+ | `/manifest.json` | The manifest described above. |
103
+ | `/doc/<component>` | The raw `.placard.md` for that component (404 if none). |
104
+ | `/health` | `ok`. |
105
+
106
+ ## Render isolation & bundling
107
+
108
+ Cases render in an isolated browser bundle, which shapes a few behaviors worth knowing when a render looks wrong:
109
+
110
+ - **Public env is inlined.** The bundle inlines the consumer package's `BUN_PUBLIC_*` env (from `<pkg>/.env[.local]`) via `Bun.build`'s `define`, mirroring the app's own build. So app code that reads `process.env.BUN_PUBLIC_*` (e.g. an API base URL) gets a value instead of throwing `process is not defined` and blanking the whole single-bundle showcase. Non-public env is **not** inlined — a bundled module that reads `process.cwd()`, `process.env.NODE_ENV`, `Bun.*`, etc. still throws.
111
+ - **Runtime-global guard.** If a bundled module does reference an undefined Node/Bun global at load, the render document catches it and paints an explanatory banner (instead of a silent blank).
112
+ - **No frame navigation.** Anchor clicks that would unload the isolated frame are neutralized (a case has no router); same-document `#hash` links and `target=_blank` are left alone.
113
+
114
+ ## Scaffolding integration (`init` / `uninstall`)
115
+
116
+ `display-case init [pkgDir] [--agent=claude] [--with-visual] [--dry-run] [--json]` wires Display Case into a repo: it merges a launch entry into the agent's launch config, installs the bundled skills, and adds an agent-guide pointer — idempotently. A re-init is self-healing: skills, the launch entry, and the sentinel-delimited pointer block are each reconciled against the bundled content, so a drifted artifact is refreshed (reported as `updated`) rather than skipped. `display-case uninstall` reverses exactly those, removing only Display Case's own artifacts. Both default to a human-readable report; pass `--json` to get a machine-parsable plan/report (`{ command, agent, dryRun, items: [{ artifact, action, detail }] }`) and `--dry-run` to preview without writing.
117
+
118
+ `--with-visual` additionally sets up the optional visual-regression toolchain — it runs `bun add --dev playwright @axe-core/playwright pixelmatch pngjs` then `bunx playwright install chromium`. Without the flag the step is skipped (reported as a `skipped` plan item); when run interactively in a TTY, `init` prompts for it. You need this only for a **default-backed `check`** — browsing, `--print-manifest`, and `/render` snapshotting need no browser or visual deps. See [Testing → the default backend is lazy and optional](testing.md#the-default-backend-is-lazy-and-optional).
119
+
120
+ ## Recommended agent workflow
121
+
122
+ 1. **Enumerate.** Run `bunx @awarebydefault/display-case <pkgDir> --print-manifest` (no server needed), or `GET /manifest.json` against a running server.
123
+ 2. **Locate.** From the manifest, pick the `caseFile` / `placardDoc` paths to read for source and guidance, and note each case's `renderUrl` and `tweaks`.
124
+ 3. **Snapshot.** Hit `renderUrl` with the desired `theme`, `width`, and `t.*` parameters to render exactly the variant you want — then screenshot or scrape it.
125
+ 4. **Document.** When a component has no `placardDoc` (or a thin one), author `<name>.placard.md` per [Writing placard docs](writing-placard-docs.md) so the next agent can use it without reading the source.
126
+ 5. **Verify.** Use `bunx @awarebydefault/display-case check <pkgDir>` to confirm the structure best-practice rules, a11y, and visual baselines still pass after a change; `bunx @awarebydefault/display-case check <pkgDir> --structure --tokens` runs just the fast, browser-free phases (see [Testing](testing.md)).
package/docs/cli.md ADDED
@@ -0,0 +1,99 @@
1
+ # CLI
2
+
3
+ > Nav: [Quick start](quick-start.md) · [Writing cases](writing-cases.md) · [Hierarchy](hierarchy.md) · [Tweaks](tweaks.md) · [Theming](theming.md) · [Documentation panel](documentation-panel.md) · [Writing placard docs](writing-placard-docs.md) · [Testing](testing.md) · **CLI** · [AI agents](ai-agents.md) · [Configuration](configuration.md)
4
+
5
+ The CLI takes a package directory (`<pkgDir>`) — the directory containing the `display-case.config.ts`. It defaults to the current directory when omitted.
6
+
7
+ ```bash
8
+ display-case <pkgDir> [--port=N]
9
+ display-case <pkgDir> --print-manifest
10
+ display-case check <pkgDir> [--tokens] [--a11y] [--visual] [--update] [--port=N]
11
+ display-case init <pkgDir> [--agent=claude] [--with-visual] [--dry-run] [--json]
12
+ display-case uninstall <pkgDir> [--agent=claude] [--dry-run] [--json]
13
+ ```
14
+
15
+ `init` / `uninstall` scaffold (or remove) Display Case's AI-agent integration — a launch entry, the bundled skills, and an agent-guide pointer. See [AI agents → Scaffolding integration](ai-agents.md#scaffolding-integration-init--uninstall).
16
+
17
+ Display Case requires Bun. Invoke it with `bunx`:
18
+
19
+ ```bash
20
+ bunx @awarebydefault/display-case <pkgDir> # dev server
21
+ bunx @awarebydefault/display-case check <pkgDir> # checks
22
+ ```
23
+
24
+ For day-to-day use, add npm scripts and run them via `bun run`:
25
+
26
+ ```jsonc
27
+ // package.json
28
+ {
29
+ "scripts": {
30
+ "display-case": "display-case .",
31
+ "display-case:check": "display-case check ."
32
+ }
33
+ }
34
+ ```
35
+
36
+ ```bash
37
+ bun run display-case # dev server
38
+ bun run display-case:check # checks
39
+ ```
40
+
41
+ ## `display-case <pkgDir>` — dev server
42
+
43
+ Discovers cases, bundles them with Bun, and serves the browsing UI. The server URL is printed on start, and it rebuilds automatically when a `*.case.tsx` or `*.placard.md` file under `src/` changes (refresh to pick up — there is no in-page HMR).
44
+
45
+ | Flag | Default | Description |
46
+ | --- | --- | --- |
47
+ | `--port=N` | `3100` | Port to serve on. |
48
+
49
+ ```bash
50
+ display-case .
51
+ display-case . --port=4000
52
+ ```
53
+
54
+ The served endpoints are documented in [AI agents](ai-agents.md#endpoints).
55
+
56
+ ## `display-case <pkgDir> --print-manifest`
57
+
58
+ Builds the manifest once, prints it to stdout as formatted JSON, and exits `0`. No server is started. This is the recommended way for a machine reader to enumerate everything available.
59
+
60
+ ```bash
61
+ display-case . --print-manifest
62
+ ```
63
+
64
+ See [AI agents](ai-agents.md) for the manifest shape and how to use it.
65
+
66
+ ## `display-case check <pkgDir>` — structure + token + a11y + visual checks
67
+
68
+ Runs four phases: **structure** best-practice rules (static; no browser), design-token conformance (a static `var()` parse, no browser), headless accessibility (axe-core), and visual-regression (pixel diff). The a11y and visual phases run over every case in both light and dark themes; structure and tokens need neither a browser nor the server. Exits `0` when everything passes, `1` when any **error**-severity finding is produced.
69
+
70
+ The a11y phase prints each violation's affected nodes (for colour-contrast, the failing element and the measured-vs-required pair) and writes the full run to `.display-case/a11y/last-check.json` for reading without re-running — see [Testing → Accessibility](testing.md#accessibility).
71
+
72
+ | Flag | Default | Description |
73
+ | --- | --- | --- |
74
+ | `--structure` | — | Run the static best-practice rules (coverage, levels, primer, setup, composition…). See [Testing](testing.md#structure-checks). |
75
+ | `--tokens` | — | Run design-token conformance (static; no browser). See [Testing](testing.md#token-conformance). |
76
+ | `--a11y` | — | Run accessibility checks. |
77
+ | `--visual` | — | Run visual-regression checks. |
78
+ | `--update` | off | (Re)record visual baselines from the current renders. |
79
+ | `--strict` | off | Treat structure warnings as errors for this run. |
80
+ | `--only=ids` | — | Scope the render phases (a11y/visual) to these component ids or globs (comma-separated). |
81
+ | `--changed[=ref]` | — | Scope the render phases to components a change touched since `ref` (default the base branch, or `DISPLAY_CASE_BASE_REF`). |
82
+ | `--port=N` | ephemeral | Port for the internal server the checks drive. |
83
+
84
+ **Default behavior:** if no phase flag is given, **all** phases run — except any a config opts out via [`check.defaultPhases`](configuration.md). Pass one or more phase flags to narrow to just those phases; an explicit flag always runs that phase regardless of config.
85
+
86
+ **Change-scoping the render checks.** `--only` / `--changed` restrict the (slow, browser-backed) a11y and visual phases to a subset of components — the static phases are unaffected. With `--changed`, a component is in scope when a changed file is in its import closure; a change to a globally-applied stylesheet or the shared render path scopes to **all** components, and a change with no render inputs (docs, tests) scopes to **none** (the render phases pass without launching a browser). This is what the CI a11y/visual jobs use. See [Testing → Change-scoped checks](testing.md#change-scoped-checks).
87
+
88
+ ```bash
89
+ display-case check . # all phases (minus config opt-outs)
90
+ display-case check . --structure # best-practice rules only (fast, no browser)
91
+ display-case check . --tokens # token conformance only (fast, no browser)
92
+ display-case check . --a11y # a11y only
93
+ display-case check . --structure --strict # structure rules, warnings fail the run
94
+ display-case check . --visual --update # record/refresh visual baselines
95
+ display-case check . --a11y --only=button # a11y for one component
96
+ display-case check . --a11y --visual --changed=origin/main # only what this branch touched
97
+ ```
98
+
99
+ Full details — the structure rules and their escape hatches, baselines, diff outputs, exit codes — are in [Testing](testing.md). Per-rule severity and enable/disable live in [Configuration](configuration.md).