@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/docs/tweaks.md ADDED
@@ -0,0 +1,75 @@
1
+ # Tweaks
2
+
3
+ > Nav: [Quick start](quick-start.md) · [Writing cases](writing-cases.md) · [Hierarchy](hierarchy.md) · **Tweaks** · [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
+ Tweaks are typed, interactive controls attached to a case. Instead of writing one variant per prop combination, declare the props as tweaks and let the viewer adjust them live.
6
+
7
+ ```tsx
8
+ import { defineCases, tweak } from '@awarebydefault/display-case'
9
+ import { TweakControl } from './tweak-control'
10
+
11
+ export default defineCases('TweakControl', {
12
+ Playground: {
13
+ tweaks: {
14
+ label: tweak.text('Save changes'),
15
+ kind: tweak.choice(['text', 'boolean', 'choice'], 'text'),
16
+ disabled: tweak.boolean(false),
17
+ },
18
+ render: (t) => (
19
+ <TweakControl
20
+ kind={t.kind as 'text' | 'boolean' | 'choice'}
21
+ label={t.label}
22
+ disabled={t.disabled}
23
+ />
24
+ ),
25
+ },
26
+ }, { level: 'atom' })
27
+ ```
28
+
29
+ A tweaked case is an object with two keys: a `tweaks` schema and a `render` function that receives the resolved values.
30
+
31
+ ## Control kinds
32
+
33
+ There are four serializable tweak builders, all imported from the `tweak` namespace:
34
+
35
+ | Builder | Resolved value type | Default argument |
36
+ | --- | --- | --- |
37
+ | `tweak.text(default?)` | `string` | optional, defaults to `''` |
38
+ | `tweak.boolean(default?)` | `boolean` | optional, defaults to `false` |
39
+ | `tweak.number(default?)` | `number` | optional, defaults to `0` |
40
+ | `tweak.choice(options, default)` | `string` | both required |
41
+
42
+ ```ts
43
+ tweak.text('Hello') // text field
44
+ tweak.boolean(true) // toggle
45
+ tweak.number(8) // number field
46
+ tweak.choice(['sm', 'md', 'lg'], 'md') // select from fixed options
47
+ ```
48
+
49
+ The keys of the `tweaks` object become the control labels and the property names on the `render` argument.
50
+
51
+ ## Values are URL-encoded
52
+
53
+ A tweak's current value is serialized into the render URL as `t.<name>`, so any tweaked state is a shareable, snapshottable link:
54
+
55
+ ```
56
+ /render/tweak-control/playground?theme=dark&t.label=Delete&t.kind=choice&t.disabled=1
57
+ ```
58
+
59
+ Encoding rules:
60
+
61
+ - `boolean` — `1` / `true` is true; anything else is false.
62
+ - `number` — parsed with `Number(...)`.
63
+ - `text` and `choice` — used verbatim.
64
+ - A missing `t.<name>` falls back to the declared `default`.
65
+
66
+ This is what lets an AI agent or screenshot tool reproduce an exact tweaked state deterministically — see [AI agents](ai-agents.md).
67
+
68
+ ## Typing
69
+
70
+ `choice` returns a plain `string` at runtime, so when feeding it back into a union-typed prop you may need a cast (as in the example above). The other kinds resolve to their natural types (`string`, `boolean`, `number`).
71
+
72
+ ## When to reach for tweaks
73
+
74
+ - **Use a tweaked case** for an exploratory "playground" where many prop combinations matter.
75
+ - **Use plain cases** for the canonical, named variants you want to keep visible and snapshot in tests — these render with fixed inputs and are the stable surface for visual regression. See [Testing](testing.md).
@@ -0,0 +1,144 @@
1
+ # Writing cases
2
+
3
+ > Nav: [Quick start](quick-start.md) · **Writing cases** · [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
+ A case file is the unit of the showcase: a `*.case.tsx` file colocated with the component it demonstrates, default-exporting one call to `defineCases` or `defineFlow`.
6
+
7
+ ```tsx
8
+ // src/components/tweak-control.case.tsx
9
+ import { defineCases } from '@awarebydefault/display-case'
10
+ import { TweakControl } from './tweak-control'
11
+
12
+ export default defineCases('TweakControl', {
13
+ Kinds: () => (
14
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
15
+ <TweakControl kind="text" label="Label" value="Save" />
16
+ <TweakControl kind="boolean" label="Disabled" value={false} />
17
+ <TweakControl kind="choice" label="Variant" options={['sm', 'md', 'lg']} value="md" />
18
+ </div>
19
+ ),
20
+ Boolean: () => <TweakControl kind="boolean" label="Disabled" value={true} />,
21
+ Disabled: () => <TweakControl kind="text" label="Label" value="Save" disabled />,
22
+ }, { level: 'atom' })
23
+ ```
24
+
25
+ ## `defineCases(component, cases, meta?)`
26
+
27
+ | Argument | Type | Notes |
28
+ | --- | --- | --- |
29
+ | `component` | `string` | The display name shown in the sidebar. Its slug (kebab-case) forms the URL, e.g. `TweakControl` → `/c/tweak-control`. |
30
+ | `cases` | `Record<string, Case>` | Keyed by display name; **insertion order is preserved**. Each value is either a simple render thunk or a tweaked case (see below). |
31
+ | `meta.level` | `HierarchyLevel?` | One of `atom`, `molecule`, `organism`, `template`, `page` (`flow` is set automatically by `defineFlow`). Drives sidebar grouping. Omit to leave it "unclassified" (sorted last). See [Hierarchy](hierarchy.md). |
32
+ | `meta.area` | `string?` | Free-form layout tag passed to the [`decorator`](configuration.md#decorator) so it can wrap this case in app chrome (nav/header/footer). Display Case mandates no vocabulary — the decorator interprets the value. Takes precedence over folder-based detection via `sourcePath`; omit to fall back to that (or to render bare). |
33
+
34
+ ### Two shapes of case
35
+
36
+ A **simple case** is a thunk returning a React node:
37
+
38
+ ```tsx
39
+ Disabled: () => <TweakControl kind="text" label="Label" value="Save" disabled />,
40
+ ```
41
+
42
+ A **tweaked case** declares typed controls and receives their resolved values:
43
+
44
+ ```tsx
45
+ Playground: {
46
+ tweaks: { label: tweak.text('Save') },
47
+ render: (t) => <TweakControl kind="text" label="Label" value={t.label} />,
48
+ },
49
+ ```
50
+
51
+ See [Tweaks](tweaks.md) for the full control set.
52
+
53
+ ### Order the default-landing variant first
54
+
55
+ Clicking a component in the sidebar navigates to its **first** case, so lead with the most exploratory variant — a tweaked `Playground` case, or a "do-anything" interactive demo (e.g. a stateful, clickable example). Keep isolated single-state variants (one `Disabled`, one `With error`) *after* it: those exist mainly for snapshots and visual-regression, not as the thing a reader should land on. Flow steps are the exception — order them in flow sequence (`defineFlow` below).
56
+
57
+ ## `defineFlow(name, { steps })`
58
+
59
+ For behavioural multi-step flows — a wizard, a sign-in sequence — use `defineFlow`. Each step is an ordered, individually addressable, snapshottable state. A step may declare preset `tweaks`, `transitions` to other steps, and wire its injected `goto` into a presentational view's callbacks so an in-step button advances the flow in place.
60
+
61
+ ```tsx
62
+ // src/components/sign-in-flow.case.tsx
63
+ import { defineFlow } from '@awarebydefault/display-case'
64
+ import { RequestLink, CheckEmail, SignedIn } from './sign-in-screens'
65
+
66
+ export default defineFlow('Sign-in flow', {
67
+ steps: {
68
+ 'Request link': {
69
+ transitions: ['Check email'],
70
+ render: ({ goto }) => <RequestLink onSubmit={() => goto('Check email')} />,
71
+ },
72
+ 'Check email': {
73
+ transitions: ['Signed in'],
74
+ render: ({ goto }) => <CheckEmail onOpen={() => goto('Signed in')} />,
75
+ },
76
+ 'Signed in': { render: () => <SignedIn /> },
77
+ },
78
+ })
79
+ ```
80
+
81
+ Keep the views pure: a step wires `goto` to the view's callbacks; the view never imports navigation, so the same view is reused in the real route. `goto(step, overrides?)` re-enters the target step with optional preset tweak values. A flow whose steps declare no transitions is a static, walkable sequence. A flow is always at the `flow` level. See [Hierarchy](hierarchy.md#flows) for how flows differ from regular cases.
82
+
83
+ `defineFlow` also accepts an optional `area` alongside `steps` — the same free-form layout tag as [`meta.area`](#definecasescomponent-cases-meta) — so a flow can be wrapped in app chrome by the decorator:
84
+
85
+ ```tsx
86
+ export default defineFlow('Checkout', { area: 'app', steps: { /* … */ } })
87
+ ```
88
+
89
+ **Typed step values.** A bare step object has loosely-typed `values`. To read typed preset values (`values.error` as `boolean`), wrap the step in the `flowStep` helper, which infers the step's own tweak schema:
90
+
91
+ ```tsx
92
+ import { defineFlow, flowStep, tweak } from '@awarebydefault/display-case'
93
+
94
+ export default defineFlow('Sign-in', {
95
+ steps: {
96
+ 'Check email': flowStep({
97
+ tweaks: { error: tweak.boolean(false) },
98
+ render: ({ values, goto }) => (
99
+ <CheckEmail error={values.error} onVerify={() => goto('Signed in')} />
100
+ ),
101
+ }),
102
+ 'Signed in': { render: () => <SignedIn /> },
103
+ },
104
+ })
105
+ ```
106
+
107
+ `goto`/`transitions` targets are not key-checked at compile time — an unknown target renders the not-found step at runtime.
108
+
109
+ ## Authoring rules
110
+
111
+ - **Default-export the definition.** A file with no valid default export (or one whose `component` is not a string) is skipped and reported as a load error; the rest still load.
112
+ - **No top-level side effects.** Render functions are lazy. The server imports the module to build the manifest without rendering — so don't call hooks, fetch, or touch the DOM at module top level.
113
+ - **Give an interactive specimen a distinct per-case `key`.** This is a foot-gun worth understanding. A controlled component needs a little wrapper that owns its state (`function Demo({ initial }) { const [v, setV] = useState(initial); … }`), and you'll reuse that one wrapper across several cases. But the browse chrome **swaps cases in place** — it re-renders one persistent root with `root.render()` and never unmounts (so theme/tweak changes don't flicker). React then sees the *same* `<Demo>` at the *same* position across cases and **keeps its `useState` value** instead of re-seeding from the new case's `initial`. Between cases whose props differ — a different selected id, or a disjoint set of options — the leaked value shows the wrong selection, or (if it isn't in the new options) *no* selection at all. Fix it by giving each case's wrapper a distinct `key` so React remounts it:
114
+
115
+ ```tsx
116
+ function Demo({ options, initial }: { options: Opt[]; initial: string }) {
117
+ const [value, setValue] = useState(initial)
118
+ return <Toggle options={options} value={value} onChange={setValue} />
119
+ }
120
+
121
+ export default defineCases('Toggle', {
122
+ // A bare <Demo …/> here would carry the previous case's value across the swap.
123
+ Two: () => <Demo key="two" options={two} initial="b" />,
124
+ Five: () => <Demo key="five" options={five} initial="lg" />,
125
+ })
126
+ ```
127
+
128
+ A tweaked `Playground` that re-seeds from a tweak follows the same rule — key it by the seeding tweak (`key={`pg-${t.count}`}`) so changing the tweak remounts with the new `initial`. A specimen rendered in only *one* case is safe (switching to any sibling case mounts a different element, which remounts it anyway). The `interactive-cases-keyed` structure check flags a stateful wrapper reused across cases with a missing `key`; waive a deliberate exception with a `// display-case: allow-interactive-cases-keyed <reason>` comment.
129
+ - **Compose freely inside a render.** Layout wrappers, multiple instances, sample data — anything that returns a React node is fine.
130
+ - **One component (or flow) per file.** Coverage tooling expects a `<name>.case.tsx` sibling for each component module.
131
+ - **Edits are picked up on save; reload to see them.** The dev server watches case files and rebuilds on every change — including manifest-shape edits (case *order*, case/component *names*, hierarchy `level`, tweak schema). There is no in-page HMR, so reload the browser to pick up a rebuild. (The rebuild reads the manifest in a fresh subprocess because Bun caches ES modules by path for a process's lifetime; without that, an in-process re-import would return the stale module.)
132
+
133
+ ## Coverage
134
+
135
+ The coverage check fails if a showcased component module has no colocated `*.case.tsx`. Wire it into your lint or CI step to keep every component browsable. To exempt a non-showcasable module, add a comment anywhere in the component file:
136
+
137
+ ```tsx
138
+ // display-case: no-case this is an internal helper, not a visual component
139
+ ```
140
+
141
+ ## See also
142
+
143
+ - Runnable examples: [examples/](examples/) — a plain case, a tweaks case, and a multi-variant case.
144
+ - [Configuration](configuration.md) for `roots` globs that decide which files are discovered.
@@ -0,0 +1,194 @@
1
+ # Writing placard docs
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** · [Testing](testing.md) · [CLI](cli.md) · [AI agents](ai-agents.md) · [Configuration](configuration.md)
4
+
5
+ A `<component>.placard.md` is the component's **prose contract**: the one place that
6
+ tells a reader what the types can't. [Documentation panel](documentation-panel.md)
7
+ covers how the file is discovered and rendered; this guide covers what to *put in
8
+ it*. For a complete, annotated specimen see
9
+ [`examples/tweak-control.placard.md`](examples/tweak-control.placard.md).
10
+
11
+ ## Who reads it, and why that decides everything
12
+
13
+ The primary reader is an **AI agent** assembling a UI; the secondary reader is a
14
+ human skimming the doc panel. Both arrive already holding two things you should
15
+ never restate:
16
+
17
+ - the **source** — every prop name, type, and default is in the `.tsx`;
18
+ - the **manifest** — every case, `renderUrl`, and `tweaks` schema is already
19
+ enumerated for machine readers (see [AI agents](ai-agents.md)).
20
+
21
+ So the placard doc earns its tokens only by carrying what *neither* of those can
22
+ express: **intent, judgement, and contract.** A good doc lets a reader choose the
23
+ component and use it correctly on the **first try, without opening the source.**
24
+ That is the whole bar.
25
+
26
+ > **The one principle:** document what the types can't. A signature says a prop is
27
+ > `(next: string[]) => void`; only prose says whether `next` is *the toggled item*
28
+ > or *the full next array*. Spend your words there.
29
+
30
+ ## What to include
31
+
32
+ Ordered by value. Lead with the highest; stop when a reader could use the
33
+ component correctly without the source. Most atoms need only the first three.
34
+
35
+ 1. **Identity line.** A bold name, an em-dash, and one sentence: *what it is* and
36
+ *the single most common reason to reach for it*. This is what shows when the
37
+ library is scanned, so it must stand alone. Lead with the conclusion.
38
+
39
+ 2. **Canonical example.** One minimal, **correct, copy-pasteable** `tsx` snippet
40
+ of the idiomatic call — the common case, not every prop. Agents paste it
41
+ verbatim, so a wrong example is worse than none. Add a second snippet only when
42
+ a distinct mode (a different `kind`, a controlled vs. composed form) genuinely
43
+ needs one.
44
+
45
+ 3. **Variants and when to pick each.** Map each variant/mode to its *meaning and
46
+ use*, not its type union. Name the default. Use a GFM table once there are more
47
+ than three; a sentence suffices below that. Restate semantics, never the
48
+ signature — semantics drift more slowly than types.
49
+
50
+ 4. **Decision boundary — when *not* to use it.** The highest-value, most-skipped
51
+ content. Point to the sibling that fits the case you're excluding, by name, so
52
+ the reader navigates the library instead of misusing this part: *"for long or
53
+ searchable lists, reach for `Combobox`"*; *"inline notice, not a transient
54
+ toast — use `Toast` for those."* This is the single biggest defence against an
55
+ agent picking the wrong primitive.
56
+
57
+ 5. **State & callback contract.** Controlled or uncontrolled? What does each
58
+ callback *emit* — the changed item or the whole next value? What fires on
59
+ mount? None of this is visible in a type and all of it is guessed wrong.
60
+
61
+ 6. **Composition & required wrappers.** Relationships the type system permits but
62
+ the design requires: *"wrap in `FormField` for the label and error"*; *"at most
63
+ one Other choice per question."* Constraints that aren't compile errors.
64
+
65
+ 7. **Accessibility behaviour.** What the component handles for you (so the reader
66
+ doesn't double up a `role`) and what the caller **must** supply (a label, alt
67
+ text). State it in one line; omit if there's nothing non-obvious.
68
+
69
+ 8. **Gotchas & anti-patterns.** The non-obvious rule and the tempting wrong use.
70
+ One bullet each; skip the section if there are none.
71
+
72
+ ## What to leave out
73
+
74
+ Every line here is either drift waiting to happen or a duplicate of a better
75
+ source:
76
+
77
+ - **Prop tables that retype the TypeScript.** Restate a prop only to add meaning
78
+ the type lacks. The source is the signature of record.
79
+ - **The case list, render URLs, or tweak schema.** The manifest owns these and
80
+ stays in sync automatically; a copy here only rots.
81
+ - **Styling internals** — CSS variables, class names, DOM structure, token math.
82
+ - **Implementation detail** — how it works inside. Document the contract, not the
83
+ mechanism.
84
+ - **Changelog or version history.** That is what git is for.
85
+ - **Anything the name already says.** `<Spinner>` spins.
86
+
87
+ ## Form: write for the medium
88
+
89
+ The doc renders as **CommonMark + GFM** (tables, task lists, strikethrough,
90
+ autolinks) — but with [two limits](documentation-panel.md#two-intentional-limits):
91
+
92
+ - **No raw HTML.** Embedded `<div>`/`<span>`/`<style>` is stripped, not rendered.
93
+ Stay in Markdown.
94
+ - **No syntax highlighting.** Fenced blocks are plain `<pre>`. Use fences for
95
+ *structure*, never to imply colour.
96
+
97
+ And because the file is ingested into a context window as often as it is read on a
98
+ screen:
99
+
100
+ - **Be dense.** Every sentence earns its tokens. The model of a great doc is
101
+ short — see [`Button.placard.md`](../src/ui/design-system/components/controls/Button.placard.md):
102
+ identity line, example, one variant sentence. Scale up only for real complexity.
103
+ - **Be scannable.** Bold lead line always; `##` headings only once the doc has
104
+ enough distinct sections to need them. A five-line atom needs no headings.
105
+ - **Be present-tense and declarative**, in the calm house voice — no marketing,
106
+ no hedging.
107
+ - **Keep examples runnable and current.** They are the most-copied lines in the
108
+ file; treat a stale example as a bug.
109
+
110
+ ## Length is a function of complexity, not a target
111
+
112
+ | Component shape | Doc shape |
113
+ | --- | --- |
114
+ | Atom, 1–2 props, one behaviour | Identity line + one example + a variant sentence. ~5 lines. |
115
+ | Several variants or modes | Add a variant table and a decision-boundary line. |
116
+ | Non-obvious callback or composition | Add the state/callback contract and required-wrapper notes. |
117
+ | Subtle a11y or footguns | Add an accessibility line and a gotchas bullet. |
118
+
119
+ Stop at the point where an agent could use the component correctly without the
120
+ source. Past that, more words are liability, not value.
121
+
122
+ ## Pages, templates, and flows
123
+
124
+ Everything above assumes a **reusable component** (atom / molecule / organism) —
125
+ something you instantiate, so the doc revolves around the *call*: identity,
126
+ canonical example, variants, decision boundary, prop/callback contract.
127
+ Template-, page-, and flow-level exhibits aren't parts you instantiate, so that
128
+ shape mostly doesn't apply. There is **no idiomatic call to paste, no prop API,
129
+ no variant union** — drop the canonical-example snippet, the variant table, and
130
+ the state/callback contract. The one principle holds, but what you document turns
131
+ **behavioural / structural**, and the [manifest](ai-agents.md) already enumerates
132
+ the cases / steps / `renderUrl`s / `transitions` — restate their *meaning*, never
133
+ re-list them.
134
+
135
+ - **Template** (page-level layout, no real data) — document **expected usage**:
136
+ the regions/slots the layout defines and what each is meant to hold, and when to
137
+ reach for this layout over a sibling. Structure, not data, not behaviour.
138
+ - **Page** (a template filled with representative content) — document **expected
139
+ behaviour**: what screen it represents, what a viewer can do on it, the states
140
+ it exercises, and which template + content it composes. What it *does*.
141
+ - **Flow** (a multi-step journey) — document **expected behaviour end to end**:
142
+ the ordered steps and what each represents, and — critically — *what advances
143
+ the flow between them* (the trigger on each transition), plus entry/exit and any
144
+ preset step state ([Hierarchy](hierarchy.md#flows) explains the flow model).
145
+
146
+ Skeletons for the three:
147
+
148
+ ````md
149
+ <!-- template -->
150
+ **Name** — the layout it is; the one reason to reach for it.
151
+
152
+ Regions: `header` (…) · `main` (…) · `aside` (…) — what each holds.
153
+
154
+ Use this layout for X; for Y reach for `OtherTemplate`.
155
+ ````
156
+
157
+ ````md
158
+ <!-- page -->
159
+ **Name** — the screen it represents; what it demonstrates.
160
+
161
+ Behaviour: what the viewer can do; the states shown.
162
+
163
+ Composes `SomeTemplate` with `…` content.
164
+ ````
165
+
166
+ ````md
167
+ <!-- flow -->
168
+ **Name** — the journey and its outcome.
169
+
170
+ Steps, in order: `step-one` (…) → `step-two` (…) → `done` (…).
171
+
172
+ Advances when: `step-one`'s submit → `step-two`; … . Entry: … . Preset: … .
173
+ ````
174
+
175
+ ## A skeleton to copy
176
+
177
+ ````md
178
+ **ComponentName** — what it is; the one reason to reach for it.
179
+
180
+ ```tsx
181
+ <ComponentName prop="idiomatic" onChange={handle} />
182
+ ```
183
+
184
+ Variants: `default` (…, the default) · `alt` (…). Restate what each *means*.
185
+
186
+ Use this for X. For Y, reach for `SiblingComponent` instead.
187
+
188
+ Controlled — pass `value`/`onChange`; `onChange` emits the exact contract.
189
+ Wrap in `FormField` for a label and error. Handles `role=…` itself.
190
+ ````
191
+
192
+ Drop any line that would only restate the type or the name. The
193
+ [canonical example](examples/tweak-control.placard.md) fills this skeleton in for a
194
+ real component, with margin notes on each choice.
package/package.json ADDED
@@ -0,0 +1,113 @@
1
+ {
2
+ "name": "@awarebydefault/display-case",
3
+ "version": "1.0.0",
4
+ "description": "A Bun-native, AI-friendly component showcase — a lightweight alternative to Storybook.",
5
+ "license": "MIT",
6
+ "author": "Jake Uskoski <jake@awarebydefault.com>",
7
+ "type": "module",
8
+ "sideEffects": false,
9
+ "homepage": "https://github.com/AwareByDefault/display-case#readme",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/AwareByDefault/display-case.git"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/AwareByDefault/display-case/issues"
16
+ },
17
+ "keywords": [
18
+ "bun",
19
+ "component",
20
+ "showcase",
21
+ "storybook",
22
+ "storybook-alternative",
23
+ "react",
24
+ "ssr",
25
+ "design-system",
26
+ "ai-agents",
27
+ "preview",
28
+ "playground",
29
+ "visual-regression",
30
+ "accessibility"
31
+ ],
32
+ "engines": {
33
+ "bun": ">=1.2.0"
34
+ },
35
+ "packageManager": "bun@1.3.14",
36
+ "publishConfig": {
37
+ "access": "public",
38
+ "provenance": true
39
+ },
40
+ "main": "./src/index.ts",
41
+ "types": "./src/index.ts",
42
+ "bin": {
43
+ "display-case": "src/cli.ts"
44
+ },
45
+ "exports": {
46
+ ".": "./src/index.ts",
47
+ "./tokens-check": "./src/checks/tokens-check.ts",
48
+ "./prod-server": "./src/server/prod-server.ts"
49
+ },
50
+ "files": [
51
+ "src",
52
+ "skills",
53
+ "docs",
54
+ "display-case.prompt.md",
55
+ "README.md",
56
+ "LICENSE"
57
+ ],
58
+ "scripts": {
59
+ "setup": "bun scripts/setup.ts",
60
+ "clean": "rm -rf .display-case dist dist-showcase",
61
+ "dev": "bun src/cli.ts . --dev",
62
+ "display-case": "bun src/cli.ts .",
63
+ "display-case:check": "bun src/cli.ts check .",
64
+ "lint": "biome check . && bun tools/lint/index.ts",
65
+ "lint:fix": "biome check --write . && bun tools/lint/index.ts --fix",
66
+ "lint:checks": "bun tools/lint/index.ts",
67
+ "typecheck": "tsc --noEmit",
68
+ "openspec": "openspec",
69
+ "check": "bun src/cli.ts check . --structure --tokens --ssr",
70
+ "baselines:record": "bun scripts/record-baselines.ts",
71
+ "test": "bun test",
72
+ "test:container": "bun test ./test/publish-container.test.ts",
73
+ "e2e": "playwright test",
74
+ "e2e:headed": "playwright test --headed",
75
+ "e2e:install": "playwright install chromium",
76
+ "release": "semantic-release",
77
+ "release:dry": "semantic-release --dry-run --no-ci",
78
+ "prepare": "husky"
79
+ },
80
+ "peerDependencies": {
81
+ "react": "^19",
82
+ "react-dom": "^19"
83
+ },
84
+ "dependencies": {
85
+ "@parcel/watcher": "^2.5.6",
86
+ "markdown-to-jsx": "^9.8.2"
87
+ },
88
+ "optionalDependencies": {
89
+ "@axe-core/playwright": "^4.10.1",
90
+ "pixelmatch": "^6.0.0",
91
+ "playwright": "^1.50.1",
92
+ "pngjs": "^7.0.0"
93
+ },
94
+ "devDependencies": {
95
+ "@biomejs/biome": "^2.5.0",
96
+ "@commitlint/cli": "^19.6.0",
97
+ "@commitlint/config-conventional": "^19.6.0",
98
+ "@emotion/cache": "^11.14.0",
99
+ "@emotion/react": "^11.14.0",
100
+ "@emotion/server": "^11.11.0",
101
+ "@fission-ai/openspec": "1.4.1",
102
+ "@playwright/test": "^1.60.0",
103
+ "@semantic-release/changelog": "^6.0.3",
104
+ "@semantic-release/git": "^10.0.1",
105
+ "@types/bun": "^1.2.0",
106
+ "@types/pngjs": "^6.0.5",
107
+ "@types/react": "^19",
108
+ "@types/react-dom": "^19",
109
+ "husky": "^9.1.7",
110
+ "semantic-release": "^25.0.5",
111
+ "typescript": "^5.7.0"
112
+ }
113
+ }
@@ -0,0 +1,20 @@
1
+ # display-case-author-case
2
+
3
+ Scaffold a `*.case.tsx` for a component that doesn't have one yet.
4
+
5
+ ## What it does
6
+
7
+ Reads a component's source for its real props, then writes a colocated case file that default-exports `defineCases(...)` — a `Default`, the meaningful variants, and a `Playground` with typed tweaks — tagged with the right Atomic Design `level`. The result shows up in the showcase and satisfies the `display-case-coverage` lint.
8
+
9
+ ## When it triggers
10
+
11
+ The coverage check fails for a component, a new shared component is added, or someone asks to "add a case" / "showcase this component".
12
+
13
+ ## How it works
14
+
15
+ 1. Read `<name>.tsx` (and `<name>.placard.md`) for exports and prop types.
16
+ 2. Write `<name>.case.tsx` with variants + a tweaks playground, choosing the hierarchy `level` by composition.
17
+ 3. Keep cases side-effect-free (lazy thunks; stateful demos as inner components).
18
+ 4. Verify with `bun run display-case`; the `display-case-coverage` lint passes.
19
+
20
+ Authoring spec: [`../../display-case.prompt.md`](../../display-case.prompt.md).
@@ -0,0 +1,40 @@
1
+ ---
2
+ name: display-case-author-case
3
+ description: >
4
+ Write a Display Case *.case.tsx file for a component that lacks one, so it
5
+ appears in the showcase and passes the display-case-coverage lint. Use when
6
+ the coverage check fails, when adding a new shared component, or when asked to
7
+ "add a case", "showcase this component", or "cover <component> in Display Case".
8
+ ---
9
+
10
+ Author a colocated `*.case.tsx` for a component so it shows up in Display Case.
11
+
12
+ ## Steps
13
+
14
+ 1. **Read the component source** (`<name>.tsx`) to get the exact exported name and prop types. Read the sibling `<name>.placard.md` if present for realistic usage.
15
+ 2. **Create `<name>.case.tsx`** next to it, default-exporting `defineCases`:
16
+ ```tsx
17
+ import { defineCases, tweak } from '@awarebydefault/display-case'
18
+ import { TweakControl } from './tweak-control'
19
+
20
+ export default defineCases(
21
+ 'TweakControl',
22
+ {
23
+ Default: () => <TweakControl kind="text" label="Label" />,
24
+ Variants: () => (/* one instance per meaningful variant */),
25
+ Playground: {
26
+ tweaks: { label: tweak.text('Variant'), disabled: tweak.boolean(false) },
27
+ render: (t) => <TweakControl kind="text" label={t.label} disabled={t.disabled} />,
28
+ },
29
+ },
30
+ { level: 'atom' }, // atom|molecule|organism|template|page
31
+ )
32
+ ```
33
+ 3. **Pick the hierarchy `level`** by composition: primitives→`atom`, small composites→`molecule`, sections→`organism`, layouts→`template`, full screens→`page`. A behavioural multi-step flow uses `defineFlow(name, { steps, area? })` instead (level `flow`). For a `page`/`flow` case that should render inside app chrome (nav/header), add a free-form `area` tag — `defineCases(name, cases, { level: 'page', area: 'app' })` or `defineFlow(name, { area: 'app', steps })` — which the package's decorator maps to a layout (it overrides folder-based detection via the case's path).
34
+ 4. **Add tweaks** for the interesting props (`tweak.text/boolean/number/choice`); cast a `choice` value into a union-typed prop.
35
+ 5. **Keep it side-effect-free**: cases are lazy thunks. For controlled components, define a tiny stateful demo component above the export and reference it.
36
+ 6. **Verify**: `bun run display-case` shows it; the `display-case-coverage` lint passes.
37
+
38
+ ## Reference
39
+
40
+ `../../display-case.prompt.md` is the authoring spec; `../../docs/writing-cases.md`, `../../docs/hierarchy.md`, and `../../docs/tweaks.md` go deeper.
@@ -0,0 +1,24 @@
1
+ # display-case-author-placard-doc
2
+
3
+ Write a `<component>.placard.md` — the prose doc panel — that lets a reader use the component correctly without opening its source.
4
+
5
+ ## What it does
6
+
7
+ Reads a component's source for its real props, defaults, and callback contracts (and its case file for variants), then writes a colocated `.placard.md` following the [Writing placard docs](../../docs/writing-placard-docs.md) best practices: an identity line, a copy-pasteable canonical example, variant semantics, the decision boundary to sibling components, the state/callback contract, and composition/a11y notes — and deliberately *omits* anything the source, the manifest, or the component name already says.
8
+
9
+ ## When it triggers
10
+
11
+ A new shared component is added, a component has a `.case.tsx` but no `.placard.md`, or someone asks to "write a placard doc", "document this component", "add a doc panel", or "write usage docs for `<component>`".
12
+
13
+ ## How it works
14
+
15
+ 1. Read `<name>.tsx` (props, defaults, what callbacks emit) and `<name>.case.tsx` (variants); improve an existing `.placard.md` rather than replace it.
16
+ 2. Find the decision boundary — the sibling components this one is easily confused with — by scanning the package's component inventory.
17
+ 3. Draft top-down, highest value first (identity → example → variants → decision boundary → state/callback contract → composition/a11y → gotchas), stopping once the source is unnecessary.
18
+ 4. Restate meaning, never type signatures; cut prop tables, case/render-URL lists, styling internals, and changelogs.
19
+ 5. Write for the medium: GFM, no raw HTML, no syntax highlighting, dense.
20
+ 6. Verify it renders in the doc panel and that every example is correct and copy-pasteable.
21
+
22
+ Unlike a `.case.tsx`, a `.placard.md` is not enforced by any lint — its only value is quality, so the bar is applied by judgement.
23
+
24
+ Authoring guide: [`../../docs/writing-placard-docs.md`](../../docs/writing-placard-docs.md). Annotated specimen: [`../../docs/examples/tweak-control.placard.md`](../../docs/examples/tweak-control.placard.md).