@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,410 @@
1
+ # Configuration
2
+
3
+ > Nav: [Quick start](quick-start.md) · [Writing cases](writing-cases.md) · [Hierarchy](hierarchy.md) · [Tweaks](tweaks.md) · [Theming](theming.md) · [Style engines](style-engines.md) · [Documentation panel](documentation-panel.md) · [Writing placard docs](writing-placard-docs.md) · [Testing](testing.md) · [CLI](cli.md) · [AI agents](ai-agents.md) · **Configuration**
4
+
5
+ Display Case is configured by a single `display-case.config.ts` (or `.tsx`) file at the root of the package it showcases. The file must **default-export** `defineConfig(...)`; if it does not, the CLI errors. `defineConfig` is an identity helper that exists purely to give the config file full type-checking and inference.
6
+
7
+ ```ts
8
+ // display-case.config.ts
9
+ import { defineConfig } from '@awarebydefault/display-case'
10
+ import { ThemeProvider } from './src/components/theme-provider'
11
+
12
+ export default defineConfig({
13
+ title: 'Display Case',
14
+ roots: ['src/components/**/*.case.tsx'],
15
+ globalStyles: ['./src/tokens.css', './src/components.css'],
16
+ decorator: ThemeProvider,
17
+ baselineDir: 'baselines',
18
+ })
19
+ ```
20
+
21
+ The CLI looks for `display-case.config.ts` then `display-case.config.tsx` in the package directory.
22
+
23
+ ## Reference
24
+
25
+ | Key | Type | Required | Default | Description |
26
+ | --- | --- | --- | --- | --- |
27
+ | `title` | `string` | yes | — | Shown in the browsing chrome and the manifest. |
28
+ | `roots` | `string[]` | yes | — | Globs (relative to the package) locating `*.case.tsx` files. |
29
+ | `primer` | `string` | no | none | Path (relative to the package) to an `.mdx` document rendered as the Primer reading page. When set, the chrome shows a Primer / Cases mode switch. See [`primer`](#primer). |
30
+ | `landing` | `'primer' \| 'cases'` | no | `'primer'` | Which view the chrome lands on at `/` when a Primer is configured. Ignored without a Primer. See [`landing`](#landing). |
31
+ | `globalStyles` | `string[]` | no | none | CSS entrypoints (relative to the package) injected into previews. |
32
+ | `decorator` | `ComponentType<{ children, level?, sourcePath?, area? }>` | no | none | Wrapper rendered around every case; also receives the active case's `level`, `sourcePath`, and `area` so it can wrap page/flow cases in app chrome. |
33
+ | `styleEngines` | `StyleEngine[]` | no | none | Engines that collect render-time (CSS-in-JS) styling — emotion/MUI, styled-components — during the server render and deliver it before scripting. Pair with `decorator`. See [`styleEngines`](#styleengines) and [Style engines](style-engines.md). |
34
+ | `baselineDir` | `string` | no | `.display-case/baselines` | Where visual-regression baselines are stored. |
35
+ | `tokens` | `{ allow?: string[] }` | no | none | Design-token conformance options for `--tokens`. `allow` lists custom-property names the package may reference but does not itself define (e.g. host-app-provided tokens). See [Testing](testing.md#token-conformance). |
36
+ | `providers` | `{ driver?, diff? }` | no | built-in | Override the visual-regression backend. When unset, the built-in Playwright/axe driver and pixelmatch/pngjs diff are loaded lazily. See [`providers`](#providers). |
37
+ | `check` | `{ defaultPhases?, structure? }` | no | none | Tune the `check` command: which phases run by default, and the structure phase's rules and severities. See [`check`](#check). |
38
+ | `a11y` | `{ enabled?, themes?, exclude?, startup? }` | no | off | Live accessibility surfacing in the running browse chrome. See [`a11y`](#a11y). |
39
+
40
+ ### `title`
41
+
42
+ The library name. Appears in the browse shell header and in `/manifest.json` as the top-level `title`.
43
+
44
+ ### `roots`
45
+
46
+ One or more glob patterns, resolved relative to the package directory, that select your case files. Matches under any `node_modules/` are ignored, and results are de-duplicated. Most libraries need just one:
47
+
48
+ ```ts
49
+ roots: ['src/components/**/*.case.tsx']
50
+ ```
51
+
52
+ ### `globalStyles`
53
+
54
+ CSS files concatenated and injected into both the browse shell and the isolated render document, so components render with their real tokens and styles. Paths are resolved relative to the package; a listed file that does not exist is skipped silently. See [Theming](theming.md).
55
+
56
+ ```ts
57
+ globalStyles: ['./src/tokens.css', './src/components.css']
58
+ ```
59
+
60
+ ### `primer`
61
+
62
+ Path (relative to the package) to an `.mdx` document rendered as the **Primer** — a long-form reading page with embedded live specimens, shown via a Primer / Cases mode switch the chrome adds to the sidebar. The MDX is bundled into its own isolated frame (like `/render`), so it can import any component — case files *and* arbitrary `.tsx` — without risking the browse chrome.
63
+
64
+ ```ts
65
+ primer: './src/design-system/primer.mdx'
66
+ ```
67
+
68
+ Wrap each live specimen in the `<Display>` contract (provided to the MDX automatically — no import needed):
69
+
70
+ ```mdx
71
+ import { Button } from './components'
72
+
73
+ # Our design system
74
+
75
+ The wall text that orients you before browsing the cases.
76
+
77
+ <Display title="Button" subtitle="The one true action" theme="dark">
78
+ <Button variant="accent">Snapshot</Button>
79
+ </Display>
80
+ ```
81
+
82
+ `<Display>` props:
83
+
84
+ | Prop | Type | Description |
85
+ | --- | --- | --- |
86
+ | `title` | `string` | Specimen title; also the sidebar table-of-contents entry and scroll anchor. |
87
+ | `subtitle` | `string` | Optional one-line description under the title. |
88
+ | `theme` | `'light' \| 'dark'` | Optional — forces a theme scope inside the card only, so a dark-mode component reads correctly on a light page (and vice versa). Omit to inherit the page theme. |
89
+ | `flush` | `boolean` | Optional — drop the card's own border and padding so a single self-bordered child fills the box edge-to-edge (avoids a box-within-a-box). |
90
+ | `appSurface` | `boolean` | Optional — paint the card with the consumer design system's own canvas (`--color-bg`/`--color-fg`, the same tokens the `/render` frame paints) instead of the Vitrine's `--dc-bg`, so the specimen sits on the exact background the real app gives it. Combine with `theme` for the app's themed surface; degrades to `--dc-bg` when no `--color-bg` is defined. |
91
+
92
+ The sidebar reflects each `<Display>` title as a table-of-contents entry, grouped under the `#`/`##` heading that precedes it — each heading is itself a navigable, collapsible group header (the `#` page title doubles as the "top of page" entry). Any `<Display>`s before the first heading fall into a leading "Contents" group. Long entries truncate with an ellipsis. Scrollspy highlights the heading or section in view. Toggling the chrome's light/dark theme drives the Primer too.
93
+
94
+ #### Supported Primer syntax
95
+
96
+ The Primer is compiled by Display Case's own small MDX compiler (`mdx-lite`), not the full MDX toolchain. It supports a **deliberately constrained dialect** — exactly enough for prose interleaved with live specimens — and nothing more. Within these rules a Primer behaves like ordinary TypeScript + Markdown:
97
+
98
+ - **ES `import` / `export`** statements at the start of a line (column 0), single- or multi-line. They are passed through verbatim and resolved by the bundler, so a Primer imports components exactly like any `.tsx` module.
99
+ - **Prose** in the same CommonMark + GFM flavour as [placard docs](documentation-panel.md) — headings, bold/italic, inline code, lists, links, GFM tables, strikethrough. Raw HTML is not rendered, and fenced code is not syntax-highlighted (the [same two limits](documentation-panel.md#two-intentional-limits)).
100
+ - **Block-level specimens** — a JSX element that begins a line at column 0 (a capitalized tag like `<Display …>` or a `<>` fragment), consumed through its matching close. Standard JSX **expression props** work, e.g. `style={{ fontSize: '1rem' }}`. Author-imported components resolve through their imports; `<Display>` is provided automatically.
101
+ - A fenced code block stays prose **even when its contents look like a specimen** — a ` ```mdx ` sample containing `<Display>` renders as code, never as a live element.
102
+
103
+ Not supported (by construction — keep specimens as their own blocks):
104
+
105
+ - **Inline JSX inside a prose paragraph.** Put a specimen on its own line at column 0, set off by blank lines.
106
+ - **Markdown syntax inside JSX children.** Text inside a specimen is literal JSX, not Markdown.
107
+ - **`{expression}` interpolation in prose.** Expressions belong inside a specimen's JSX, not in the surrounding text.
108
+
109
+ These constraints are checked: a Primer that can't be parsed, or that has no `<Display>` specimen or no prose, fails the [`primer-present-and-used`](testing.md#structure-checks) structure check rather than rendering wrong.
110
+
111
+ ### `landing`
112
+
113
+ Which view the chrome shows first when you open the root path (`/`): the Primer reading page (`'primer'`, the default) or the Cases library (`'cases'`). Use `'cases'` when the components are the main event and the Primer is supplementary reference:
114
+
115
+ ```ts
116
+ primer: './src/design-system/primer.mdx',
117
+ landing: 'cases',
118
+ ```
119
+
120
+ This setting only governs the **bare `/` landing**. The canonical [`/primer`](#primer) route always opens the Primer (it's an explicit request), and a case deep link (`/c/...`) always opens the library — both regardless of `landing`. The option only takes effect when a `primer` is configured; without one there is no Primer to land on, so the library is always the landing view.
121
+
122
+ ### `decorator`
123
+
124
+ A single React component that wraps every rendered case (for a theme provider, context, or a fixed frame). It is applied inside React `StrictMode`. See [Theming](theming.md#decorator).
125
+
126
+ ```ts
127
+ import { ThemeProvider } from './src/components/theme-provider'
128
+ // …
129
+ decorator: ThemeProvider,
130
+ ```
131
+
132
+ Besides `children`, the decorator receives the active case's identity so it can
133
+ wrap **page** and **flow** cases in app chrome (a nav header, sidebar, footer)
134
+ while leaving smaller components bare:
135
+
136
+ ```ts
137
+ decorator?: ComponentType<{
138
+ children: ReactNode
139
+ level?: HierarchyLevel // 'atom' | … | 'page' | 'flow' (from defineCases/defineFlow)
140
+ sourcePath?: string // case file path, package-relative (folder convention)
141
+ area?: string // free-form tag from the case's meta (overrides sourcePath)
142
+ }>
143
+ ```
144
+
145
+ **Per-area app chrome.** A consuming app's decorator can render a page the way it
146
+ looks in the real app — inside its actual navigation/layout — by branching on
147
+ these:
148
+
149
+ Say a consuming app has two areas — a `marketing` site and the signed-in `app` —
150
+ each with its own header. A decorator can wrap page/flow cases in the matching
151
+ chrome and leave smaller components bare:
152
+
153
+ ```tsx
154
+ function Decorator({ children, level, sourcePath, area }) {
155
+ const inApp = level === 'page' || level === 'flow'
156
+ // Resolve which chrome to use: an explicit `area` tag wins; otherwise infer
157
+ // from the `page-cases/<area>/…` folder; otherwise render bare.
158
+ const which =
159
+ area ?? sourcePath?.match(/(?:^|\/)page-cases\/([^/]+)\//)?.[1]
160
+ let content = children
161
+ if (inApp && which === 'marketing') content = <SiteHeader variant="marketing">{children}</SiteHeader>
162
+ else if (inApp && which === 'app') content = <SiteHeader variant="app">{children}</SiteHeader>
163
+ return <Providers>{content}</Providers>
164
+ }
165
+ ```
166
+
167
+ Cases tagged `marketing` (or under `page-cases/marketing/…`) render inside the
168
+ marketing header; `app` cases render inside the signed-in header.
169
+
170
+ Display Case is unopinionated here: it supplies the signals and mandates no
171
+ vocabulary or folder layout. Tag a case via `meta.area` (see
172
+ [Writing cases](writing-cases.md)), or organize cases into area folders and read
173
+ `sourcePath` — whichever suits the package. Chrome that itself renders router
174
+ `<Link>`s (e.g. a nav bar) needs a router in context; if your nav uses a router,
175
+ provide a tiny in-memory router inside the chrome so the links resolve.
176
+
177
+ ### `styleEngines`
178
+
179
+ For components styled by a **runtime CSS-in-JS** library — emotion (and therefore
180
+ **Material UI**), styled-components, and peers — that emit their CSS as a side
181
+ effect of rendering. A style engine collects that styling during the server
182
+ render and delivers it in the isolated `/render` and Primer documents **before
183
+ scripting**, so those surfaces are styled without executing scripts (no flash, and
184
+ chrome-free snapshots come back styled).
185
+
186
+ ```ts
187
+ styleEngines: [emotionEngine]
188
+ ```
189
+
190
+ Each engine is a factory invoked **once per render** for an isolated style store:
191
+
192
+ ```ts
193
+ type StyleEngine = () => StyleCollector
194
+ interface StyleCollector {
195
+ wrap(node: ReactNode): ReactNode // provide the library's store (e.g. emotion CacheProvider)
196
+ collect(renderedHtml: string): string // return the <head> style markup that render used
197
+ }
198
+ ```
199
+
200
+ `styleEngines` (the server-side extractor) pairs with [`decorator`](#decorator)
201
+ (the client/SSR provider, e.g. a MUI `ThemeProvider`). Omit `styleEngines`
202
+ entirely and the documents are byte-identical to their engine-free form. The full
203
+ recipe — emotion/MUI flagship, styled-components, and when to use `globalStyles`
204
+ instead — is in [Style engines](style-engines.md).
205
+
206
+ ### `baselineDir`
207
+
208
+ Where the visual-regression runner reads and writes baseline PNGs. Defaults to the gitignored cache at `.display-case/baselines` (local-only). Provide a path — relative to the package or absolute — to point at a committed directory and gate CI on shared baselines. See [Testing](testing.md#where-baselines-live).
209
+
210
+ ```ts
211
+ baselineDir: 'baselines' // committed, relative to the package
212
+ // or
213
+ baselineDir: '/abs/path/to/baselines'
214
+ ```
215
+
216
+ ### `providers`
217
+
218
+ The visual-regression backend is pluggable. `providers` lets you replace either half of it — the **driver** that opens a case render URL and captures it, the **diff** that compares a capture against its baseline, or both.
219
+
220
+ ```ts
221
+ export default defineConfig({
222
+ title: 'Display Case',
223
+ roots: ['src/components/**/*.case.tsx'],
224
+ providers: {
225
+ driver: () => myDriver(), // optional; default = built-in Playwright + axe
226
+ diff: myDiff, // optional; default = built-in pixelmatch/pngjs
227
+ },
228
+ })
229
+ ```
230
+
231
+ When `providers` is omitted (or a half is left unset), Display Case falls back to its built-in default for that half. The default is **lazy and optional**: the Playwright/axe driver and pixelmatch/pngjs diff are imported only when a default-backed `check --a11y`/`--visual` actually runs, so browsing, snapshotting, and `init` never need them. See [Testing](testing.md#the-default-backend-is-lazy-and-optional). Setting a custom provider replaces the default for that half and removes the need for those packages entirely.
232
+
233
+ The reference implementations are the built-ins themselves — [`src/checks/providers/playwright-driver.ts`](../src/checks/providers/playwright-driver.ts) and [`src/checks/providers/pixelmatch-diff.ts`](../src/checks/providers/pixelmatch-diff.ts). A custom provider need only satisfy the interface.
234
+
235
+ #### The `CaseContext` argument
236
+
237
+ Both providers receive the identity of the case being rendered, so an identity-aware provider can vary its behavior per case (a per-case tolerance, a name-keyed hosted service such as Percy or Chromatic, richer reporting). Pure providers simply ignore it.
238
+
239
+ ```ts
240
+ interface CaseContext {
241
+ componentId: string
242
+ caseId: string
243
+ theme: 'light' | 'dark'
244
+ width: number
245
+ }
246
+ ```
247
+
248
+ #### `diff`
249
+
250
+ ```ts
251
+ type DiffFn = (
252
+ input: { baseline: Uint8Array; actual: Uint8Array },
253
+ ctx: CaseContext & { baselinePath: string },
254
+ ) => DiffResult | Promise<DiffResult>
255
+
256
+ interface DiffResult {
257
+ changed: boolean
258
+ mismatch?: number // e.g. count of differing pixels, for reporting
259
+ diffImage?: Uint8Array // written next to the baseline on a change
260
+ }
261
+ ```
262
+
263
+ A custom diff that loosens tolerance for one noisy case and leaves every other case strict:
264
+
265
+ ```ts
266
+ import pixelmatch from 'pixelmatch'
267
+ import { PNG } from 'pngjs'
268
+ import { defineConfig, type DiffFn } from '@awarebydefault/display-case'
269
+
270
+ const tolerantDiff: DiffFn = ({ baseline, actual }, ctx) => {
271
+ const a = PNG.sync.read(Buffer.from(baseline))
272
+ const b = PNG.sync.read(Buffer.from(actual))
273
+ if (a.width !== b.width || a.height !== b.height) return { changed: true }
274
+ // Allow a few stray pixels on the gradient case; everything else stays exact.
275
+ const allowed = ctx.caseId === 'gradient' ? 50 : 0
276
+ const diff = new PNG({ width: a.width, height: a.height })
277
+ const mismatch = pixelmatch(a.data, b.data, diff.data, a.width, a.height, {
278
+ threshold: 0.1,
279
+ })
280
+ return mismatch > allowed
281
+ ? { changed: true, mismatch, diffImage: PNG.sync.write(diff) }
282
+ : { changed: false, mismatch }
283
+ }
284
+
285
+ export default defineConfig({
286
+ title: 'Display Case',
287
+ roots: ['src/components/**/*.case.tsx'],
288
+ providers: { diff: tolerantDiff },
289
+ })
290
+ ```
291
+
292
+ #### `driver`
293
+
294
+ ```ts
295
+ interface RenderDriver {
296
+ open(url: string, ctx: CaseContext): Promise<RenderedPage>
297
+ close(): Promise<void>
298
+ }
299
+
300
+ interface RenderedPage {
301
+ screenshot(): Promise<Uint8Array>
302
+ audit(): Promise<A11yViolation[]> // [] if the driver skips auditing
303
+ dispose(): Promise<void>
304
+ }
305
+
306
+ interface A11yViolation {
307
+ id: string
308
+ help: string
309
+ nodes: number
310
+ }
311
+ ```
312
+
313
+ `driver` is a factory: it is called once, returns a `RenderDriver` reused across every case, and `close()` runs when the check finishes. Each `open()` yields a `RenderedPage` you can `screenshot()` and `audit()`, then `dispose()`. A sketch:
314
+
315
+ ```ts
316
+ import { defineConfig, type RenderDriver } from '@awarebydefault/display-case'
317
+
318
+ function myDriver(): RenderDriver {
319
+ const browser = /* launch your headless browser once */
320
+ return {
321
+ async open(url, ctx) {
322
+ const page = /* open `url`; ctx.theme / ctx.width are already in the URL */
323
+ return {
324
+ screenshot: () => page.capture(),
325
+ audit: async () => [], // return [] to skip a11y auditing
326
+ dispose: () => page.close(),
327
+ }
328
+ },
329
+ close: () => browser.close(),
330
+ }
331
+ }
332
+
333
+ export default defineConfig({
334
+ title: 'Display Case',
335
+ roots: ['src/components/**/*.case.tsx'],
336
+ providers: { driver: myDriver },
337
+ })
338
+ ```
339
+
340
+ Compare against the built-in [`createPlaywrightDriver`](../src/checks/providers/playwright-driver.ts), which launches Chromium at a fixed 1024×768 viewport with reduced motion and runs a WCAG 2 A/AA axe audit.
341
+
342
+ ### `check`
343
+
344
+ Tunes the `check` command. Two independent parts:
345
+
346
+ ```ts
347
+ check: {
348
+ // Which phases run in the default (no-flag) run. Unset ⇒ included; set false to
349
+ // opt out. An opted-out phase still runs when named explicitly (e.g. --visual).
350
+ defaultPhases: { visual: false },
351
+
352
+ structure: {
353
+ // Treat every structure warning as an error for the run (same as --strict).
354
+ strict: false,
355
+ // Per-rule overrides. Each rule is on at its default severity unless set here.
356
+ rules: {
357
+ 'primer-present-and-used': false, // disable a rule
358
+ 'composes-lower-level': 'error', // enable/retune severity
359
+ 'case-placard-coverage': { ignore: ['**/internal/**'] }, // skip paths
360
+ 'atom-purity': {}, // enable an opt-in rule at default severity
361
+ 'level-fit': { thresholds: { molecule: 8 } }, // rule-specific options
362
+ },
363
+ },
364
+ }
365
+ ```
366
+
367
+ - **`defaultPhases`** — a `Partial<Record<'tokens' | 'a11y' | 'visual' | 'structure', boolean>>`. Drop a phase from the bare `check` run (e.g. `visual` when no baselines are committed) while keeping it available via its flag.
368
+ - **`structure.rules[id]`** — `false` disables the rule; `'warn'`/`'error'` enables it at that severity; an options object (`{ severity?, ignore?, thresholds? }`) enables it with overrides. Unset ⇒ the rule's default. The rule ids, defaults, and escape-hatch markers are listed in [Testing → Structure checks](testing.md#structure-checks).
369
+ - **`structure.strict`** — escalate all structure warnings to errors (the config equivalent of `check --strict`).
370
+
371
+ ### `a11y`
372
+
373
+ Surface accessibility results **in the running browse chrome** — a per-variant
374
+ marker in the nav rail and an Accessibility panel beside the rendered case.
375
+
376
+ ```ts
377
+ a11y: {
378
+ enabled: true, // default false — opt-in
379
+ themes: ['light', 'dark'], // default both; scanned + reflected per theme
380
+ exclude: ['color-contrast'], // axe rule ids to skip
381
+ startup: 'cached', // default 'off' — how the nav is populated at boot
382
+ }
383
+ ```
384
+
385
+ - **Off by default.** It needs the same optional Playwright + axe toolchain as a
386
+ default-backed `check` (see [`providers`](#providers)); when that toolchain
387
+ can't launch, the panel shows an *unavailable* state and the server still
388
+ browses normally — it never fails to start.
389
+ - **On demand + cached.** Only the variant you're viewing is scanned, lazily;
390
+ results are cached under `.display-case/a11y/` and reused until that variant's
391
+ rendered output changes (judged by a transitive-import content hash). Scans run
392
+ on a background queue and never block browsing.
393
+ - **`startup` populates the nav at boot** (only meaningful with `enabled`; default
394
+ `'off'`). It does not change the on-demand-per-viewed-variant behavior — it only
395
+ decides what the nav shows *before* you start clicking:
396
+ - `'off'` — no boot-time population; a variant's marker appears only once viewed.
397
+ - `'cached'` — fill markers from reusable cached results, running **no** scans;
398
+ uncached or stale variants stay unmarked until viewed.
399
+ - `'refresh'` — additionally scan every uncached or stale variant at boot,
400
+ surfacing each verdict as it lands (reusing fresh cache without re-scanning).
401
+ Work rides the same background queue, so browsing stays responsive. Nothing is
402
+ scanned if the toolchain can't launch.
403
+ - **`enabled` gates only the live surface.** The `display-case check --a11y` CI
404
+ gate runs whenever invoked regardless of `enabled`, but honors the shared
405
+ `themes` / `exclude` here so the panel and the gate agree on what counts as a
406
+ violation.
407
+
408
+ ## The cache directory
409
+
410
+ Display Case writes generated artifacts (the bundled output and the auto-generated render entry) to `.display-case/` inside the package, and — unless overridden — keeps baselines under `.display-case/baselines/`. Add `.display-case/` to `.gitignore`; it is a derived cache.
@@ -0,0 +1,50 @@
1
+ # Documentation panel
2
+
3
+ > Nav: [Quick start](quick-start.md) · [Writing cases](writing-cases.md) · [Hierarchy](hierarchy.md) · [Tweaks](tweaks.md) · [Theming](theming.md) · **Documentation panel** · [Writing placard docs](writing-placard-docs.md) · [Testing](testing.md) · [CLI](cli.md) · [AI agents](ai-agents.md) · [Configuration](configuration.md)
4
+
5
+ Each component can carry prose documentation alongside its cases. Drop a `<component>.placard.md` file next to the component and Display Case renders it in a panel beside the showcase.
6
+
7
+ ```
8
+ src/components/
9
+ tweak-control.tsx
10
+ tweak-control.case.tsx
11
+ tweak-control.placard.md ← rendered as the doc panel for "TweakControl"
12
+ ```
13
+
14
+ There is nothing to wire up: the doc file is discovered automatically as the case file's sibling (same basename, `.placard.md` instead of `.case.tsx`). If the file is absent, the component simply has no doc panel — the manifest reports `placardDoc: null` for it.
15
+
16
+ ## What renders
17
+
18
+ The Markdown is rendered as **full CommonMark + GFM** (GitHub Flavored Markdown via `markdown-to-jsx`), so you get tables, task lists, strikethrough, and autolinks in addition to the core syntax:
19
+
20
+ ```md
21
+ # TweakControl
22
+
23
+ A single typed tweak input.
24
+
25
+ ## Kinds
26
+
27
+ | Kind | Use for |
28
+ | --------- | -------------------------------- |
29
+ | `text` | free-form string values |
30
+ | `boolean` | on/off toggles |
31
+ | `choice` | selecting from fixed options |
32
+
33
+ ## Guidance
34
+
35
+ - Label every control from the tweak key.
36
+ - ~~Never~~ avoid free text where a `choice` would do.
37
+ ```
38
+
39
+ The raw file is also served verbatim at `/doc/<component>` with a `text/markdown` content type, which is useful for machine readers.
40
+
41
+ ## Two intentional limits
42
+
43
+ - **Raw HTML is disabled.** Embedded `<div>`, `<script>`, `<style>`, etc. in a doc file are not rendered as markup — this prevents a doc from injecting into the showcase chrome. Stick to Markdown syntax.
44
+ - **No syntax highlighting.** Fenced code blocks render as plain styled `<pre><code>`. This is a deliberate non-goal for now; code is readable but not colorized.
45
+
46
+ ## Why `.placard.md`
47
+
48
+ The doc lives in prompt-friendly Markdown, next to the component. Authoring usage guidance once, beside the code, means it surfaces both in the showcase and to any tool that reads the file directly — see [AI agents](ai-agents.md).
49
+
50
+ This page covers *how the panel renders*. For *what to put in the file* — the content that earns its place beside the types — see [Writing placard docs](writing-placard-docs.md).
@@ -0,0 +1,14 @@
1
+ # Examples
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](../ai-agents.md) · [Configuration](../configuration.md)
4
+
5
+ Example files that illustrate the snippets used throughout the guides. Display Case dogfoods itself, so these examples case Display Case's own UI parts — `TweakControl` (an atom) and `FlowNav` (a molecule). They are illustrative: these components aren't actually cased in the package yet, and this folder is **not** type-checked, so the colocated `./tweak-control` / `./flow-nav` imports are imaginary stand-ins.
6
+
7
+ | File | Shows | Subject | Guide |
8
+ | --- | --- | --- | --- |
9
+ | [`plain.case.tsx`](plain.case.tsx) | A single named case with a render thunk. | `TweakControl` | [Writing cases](../writing-cases.md) |
10
+ | [`tweaks.case.tsx`](tweaks.case.tsx) | A `Playground` case with text, choice, and boolean tweaks. | `TweakControl` | [Tweaks](../tweaks.md) |
11
+ | [`multi-variant.case.tsx`](multi-variant.case.tsx) | Several named cases for one component. | `FlowNav` | [Writing cases](../writing-cases.md) |
12
+ | [`tweak-control.placard.md`](tweak-control.placard.md) | A component doc panel, annotated section by section. | `TweakControl` | [Writing placard docs](../writing-placard-docs.md) |
13
+
14
+ To use a `*.case.tsx`, copy it next to a component as `<component>.case.tsx`, point the import at your real component, and make sure your config's `roots` glob matches its location. To use the `.placard.md`, copy everything above its `---` rule to `<component>.placard.md` and rewrite it for your component. See [Quick start](../quick-start.md).
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Example: a multi-variant case.
3
+ *
4
+ * Several named cases for one component, each demonstrating a different facet.
5
+ * Case keys preserve insertion order, so the sidebar lists them in the order
6
+ * written. A single case can also compose multiple instances side by side.
7
+ *
8
+ * Subject: `FlowNav`, Display Case's own molecule — the flow stepper. Display
9
+ * Case dogfoods itself, so its UI parts make good case subjects.
10
+ *
11
+ * These fixed-input cases are the stable surface for visual regression — see
12
+ * ../testing.md.
13
+ */
14
+ import { defineCases } from '@awarebydefault/display-case'
15
+ import { FlowNav } from './flow-nav'
16
+
17
+ const steps = ['Request link', 'Check email', 'Signed in']
18
+
19
+ export default defineCases(
20
+ 'FlowNav',
21
+ {
22
+ // A mid-flow state: second step active, the first already done.
23
+ InProgress: () => <FlowNav steps={steps} current={1} />,
24
+ // The first step, nothing completed yet.
25
+ Start: () => <FlowNav steps={steps} current={0} />,
26
+ // The final step, every prior step completed.
27
+ Complete: () => <FlowNav steps={steps} current={2} />,
28
+ },
29
+ { level: 'molecule' },
30
+ )
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Example: a plain case.
3
+ *
4
+ * The simplest shape — one component, one named case whose value is a thunk
5
+ * returning a React node. No tweaks, no flow. Copy this next to your component
6
+ * as `<component>.case.tsx`.
7
+ *
8
+ * Subject: `TweakControl`, Display Case's own atom — a single tweak input.
9
+ * Display Case dogfoods itself, so its UI parts make good case subjects.
10
+ *
11
+ * See ../writing-cases.md for the full authoring API.
12
+ */
13
+ import { defineCases } from '@awarebydefault/display-case'
14
+ import { TweakControl } from './tweak-control'
15
+
16
+ export default defineCases(
17
+ 'TweakControl',
18
+ {
19
+ Default: () => <TweakControl kind="text" label="Label" value="Save" />,
20
+ },
21
+ { level: 'atom' },
22
+ )
@@ -0,0 +1,80 @@
1
+ <!--
2
+ Example: a component placard doc.
3
+
4
+ This is the canonical specimen referenced by ../writing-placard-docs.md. The
5
+ part above the `---` is the doc as it would ship next to a component — clean and
6
+ copy-pasteable. The annotations below the rule explain why each block is there;
7
+ a real `<component>.placard.md` stops at the rule and keeps none of them.
8
+
9
+ Subject: `TweakControl`, Display Case's own atom — a single typed tweak input.
10
+ Display Case dogfoods itself, so its UI parts make good specimens. (Like the
11
+ sibling `*.case.tsx` examples, the component here is an illustrative stand-in.)
12
+ -->
13
+
14
+ **TweakControl** — one typed tweak input, the row a [`TweaksPanel`](../tweaks.md)
15
+ is built from; reach for it to render a single live control for one tweak value.
16
+
17
+ ```tsx
18
+ <TweakControl kind="text" label="Label" value={label} onChange={setLabel} />
19
+ ```
20
+
21
+ A `choice` renders a segmented selector; a `boolean` renders a switch:
22
+
23
+ ```tsx
24
+ <TweakControl
25
+ kind="choice"
26
+ label="Variant"
27
+ value={variant}
28
+ options={['ghost', 'primary', 'accent']}
29
+ onChange={setVariant}
30
+ />
31
+ <TweakControl kind="boolean" label="Disabled" value={disabled} onChange={setDisabled} />
32
+ ```
33
+
34
+ | `kind` | Renders | Use for |
35
+ | --------- | ---------------- | ---------------------------------------- |
36
+ | `text` | a line input | free-form strings (the default) |
37
+ | `number` | a numeric input | integers and decimals; emits a `number` |
38
+ | `boolean` | a switch | on/off toggles |
39
+ | `choice` | a segmented row | one value from a short, fixed `options` list |
40
+
41
+ Use this for a **single** control. To render a whole panel from a case's `tweaks`
42
+ schema, use [`TweaksPanel`](../tweaks.md) — it lays out one `TweakControl` per
43
+ tweak and handles URL encoding. Don't build a panel by hand out of these.
44
+
45
+ Controlled only — always pass `value` and `onChange`. `onChange` emits the new
46
+ *value* (already typed to the `kind`: a `string` for `text`/`choice`, a `number`
47
+ for `number`, a `boolean` for `boolean`), never an event. `choice` requires
48
+ `options`; the other kinds ignore it. `label` is the visible, required control
49
+ label — there is no separate `id`/`htmlFor` to wire.
50
+
51
+ ---
52
+
53
+ ## Why this doc is shaped this way
54
+
55
+ Each block maps to a section of [Writing placard docs](../writing-placard-docs.md).
56
+ Read it alongside that guide.
57
+
58
+ - **Identity line.** Bold name, em-dash, one sentence of *what it is* + the *one
59
+ reason to reach for it*. It stands alone in a library scan, and it links the
60
+ sibling (`TweaksPanel`) a reader will want next.
61
+ - **Canonical example first.** The single most common call — `text`, controlled —
62
+ before anything else, so it can be copied without reading further. The second
63
+ block is added *only* because `choice`/`boolean` render and are called
64
+ differently enough to guess wrong.
65
+ - **Variant table over a type union.** Four kinds crosses the "use a table"
66
+ threshold. Each row gives *meaning and use*, not the TypeScript — and notes the
67
+ default (`text`) and the one non-obvious return type (`number`). It does **not**
68
+ restate the `kind` union; the source already has that.
69
+ - **Decision boundary.** The "use this for one control; for a panel use
70
+ `TweaksPanel`" line is the highest-value sentence here: it stops an agent
71
+ hand-rolling a panel out of atoms. It names the sibling instead of just saying
72
+ "don't."
73
+ - **State & callback contract.** "Controlled only" and *what `onChange` emits*
74
+ (the typed value, not an event) are invisible in a signature and guessed wrong
75
+ constantly — so they get explicit prose. The `options`-required-for-`choice`
76
+ rule is a constraint the type may not force.
77
+ - **What's deliberately absent.** No prop table retyping the source, no list of
78
+ the component's cases or `renderUrl`s (the manifest owns those), no styling or
79
+ DOM internals, no changelog. The doc stops once an agent could use the component
80
+ correctly without opening the `.tsx`.
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Example: a case with tweaks.
3
+ *
4
+ * A tweaked case is an object with a `tweaks` schema and a `render` function
5
+ * that receives the resolved values. Each tweak becomes an interactive control,
6
+ * and its value is URL-encoded as `t.<name>` so the state is shareable and
7
+ * snapshottable.
8
+ *
9
+ * Subject: `TweakControl`, Display Case's own atom — a single tweak input that
10
+ * comes in text, choice, and boolean variants. Display Case dogfoods itself, so
11
+ * its UI parts make good case subjects.
12
+ *
13
+ * See ../tweaks.md for the four control kinds and encoding rules.
14
+ */
15
+ import { defineCases, tweak } from '@awarebydefault/display-case'
16
+ import { TweakControl } from './tweak-control'
17
+
18
+ export default defineCases(
19
+ 'TweakControl',
20
+ {
21
+ Playground: {
22
+ tweaks: {
23
+ kind: tweak.choice(['text', 'choice', 'boolean'], 'text'),
24
+ label: tweak.text('Save changes'),
25
+ value: tweak.text('Save'),
26
+ disabled: tweak.boolean(false),
27
+ },
28
+ render: (t) => (
29
+ <TweakControl
30
+ kind={t.kind as 'text' | 'choice' | 'boolean'}
31
+ label={t.label}
32
+ value={t.value}
33
+ disabled={t.disabled}
34
+ />
35
+ ),
36
+ },
37
+ },
38
+ { level: 'atom' },
39
+ )