@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,59 @@
1
+ # Hierarchy
2
+
3
+ > Nav: [Quick start](quick-start.md) · [Writing cases](writing-cases.md) · **Hierarchy** · [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
+ Each component declares where it sits in an Atomic Design hierarchy. The level drives how the sidebar groups and orders components, so the showcase reads from smallest building block to largest assembled flow. The first five levels mirror Brad Frost's [five stages of atomic design](https://atomicdesign.bradfrost.com/chapter-2/); `flow` is a Display Case addition for walkable multi-step journeys.
6
+
7
+ ```tsx
8
+ export default defineCases('TweakControl', { /* … */ }, { level: 'atom' })
9
+ export default defineCases('FlowNav', { /* … */ }, { level: 'molecule' })
10
+ export default defineCases('TweaksPanel', { /* … */ }, { level: 'organism' })
11
+ ```
12
+
13
+ ## The levels
14
+
15
+ Levels are ordered by increasing composition. The descriptions below follow the [five stages of atomic design](https://atomicdesign.bradfrost.com/chapter-2/):
16
+
17
+ | Level | Meaning | Example |
18
+ | --- | --- | --- |
19
+ | `atom` | A foundational building block — the smallest primitive that can't be broken down without losing meaning | TweakControl (a single tweak input) |
20
+ | `molecule` | A relatively simple group of atoms functioning together as a unit | FlowNav (a flow stepper) |
21
+ | `organism` | A relatively complex section composed of groups of molecules and/or atoms | TweaksPanel, Sidebar |
22
+ | `template` | A page-level layout that places components into a structure, with no real data | The Shell browse chrome, empty |
23
+ | `page` | A specific instance of a template shown with representative content | The Shell with a component selected |
24
+ | `flow` | A multi-step behavioural journey (Display Case's own level, beyond atomic design) | A sign-in sequence |
25
+
26
+ The internal order is fixed as `atom, molecule, organism, template, page, flow`. Components are sorted by level (atoms first), then alphabetically by name. A component with **no** declared level is treated as "unclassified" and sorts after everything else.
27
+
28
+ ## Flows
29
+
30
+ A flow is a multi-step behavioural journey rather than a set of independent variants. Author it with `defineFlow` — it is always placed at the `flow` level, and you do not pass a `level` yourself.
31
+
32
+ ```tsx
33
+ import { defineFlow } from '@awarebydefault/display-case'
34
+
35
+ export default defineFlow('Sign-in flow', {
36
+ steps: {
37
+ 'Request link': {
38
+ transitions: ['Check email'],
39
+ render: ({ goto }) => <RequestLink onSubmit={() => goto('Check email')} />,
40
+ },
41
+ 'Check email': { render: () => <CheckEmail /> },
42
+ 'Signed in': { render: () => <SignedIn /> },
43
+ },
44
+ })
45
+ ```
46
+
47
+ How a flow differs from a regular component:
48
+
49
+ - **Ordered steps, not variants.** The `steps` keep their declared order and represent states of one journey.
50
+ - **Per-step addresses.** Each step is individually addressable and snapshottable — `/c/sign-in-flow/request-link`, `/c/sign-in-flow/check-email`, and so on. The manifest marks the component with `isFlow: true` and lists the steps as its `cases`, each with its outgoing `transitions`.
51
+ - **In-step transitions.** A step's `render` receives a `goto` that advances the flow in place; wire it into a view's callbacks. `goto(step, overrides?)` re-enters the target step with optional preset tweak values.
52
+ - **Preset state.** A step may declare `tweaks`; their defaults are the step's preset state, so one view serves several steps. A flow with no transitions is a static, walkable sequence.
53
+
54
+ This lets you walk a flow step by step in the browser, click through it via in-step buttons, deep-link to any single step, or screenshot just one state of the journey.
55
+
56
+ ## See also
57
+
58
+ - [Writing cases](writing-cases.md) for the authoring API.
59
+ - [AI agents](ai-agents.md) for how `level` and `isFlow` appear in the manifest.
@@ -0,0 +1,78 @@
1
+ # Quick start
2
+
3
+ > Nav: **Quick start** · [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
+ Get a component browsing in a few minutes. Everything here runs on Bun — no bundler config, no separate dev server.
6
+
7
+ ## 1. Write a config
8
+
9
+ Display Case looks for a `display-case.config.ts` at the root of the package it is pointed at. It must default-export `defineConfig(...)`.
10
+
11
+ ```ts
12
+ // display-case.config.ts
13
+ import { defineConfig } from '@awarebydefault/display-case'
14
+
15
+ export default defineConfig({
16
+ title: 'Display Case',
17
+ roots: ['src/components/**/*.case.tsx'],
18
+ globalStyles: ['./src/tokens.css', './src/components.css'],
19
+ })
20
+ ```
21
+
22
+ - `title` shows in the browsing chrome and the manifest.
23
+ - `roots` are globs (relative to the package) that locate your case files.
24
+ - `globalStyles` are CSS entrypoints injected into every preview, so components render with their real tokens and styles.
25
+
26
+ Full reference: [Configuration](configuration.md).
27
+
28
+ ## 2. Write a case file
29
+
30
+ A case file is `*.case.tsx`, colocated with the component it showcases. It default-exports `defineCases(...)`.
31
+
32
+ ```tsx
33
+ // src/components/tweak-control.case.tsx
34
+ import { defineCases } from '@awarebydefault/display-case'
35
+ import { TweakControl } from './tweak-control'
36
+
37
+ export default defineCases('TweakControl', {
38
+ Variants: () => (
39
+ <div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
40
+ <TweakControl kind="text" label="Label" value="Save" />
41
+ <TweakControl kind="choice" label="Variant" options={['default', 'outline']} value="default" />
42
+ <TweakControl kind="boolean" label="Disabled" value={false} />
43
+ </div>
44
+ ),
45
+ }, { level: 'atom' })
46
+ ```
47
+
48
+ The render functions are lazy thunks — they only run when a case is viewed or screenshotted. Keep the module side-effect-free at the top level; the server imports it to build the manifest without ever calling them.
49
+
50
+ ## 3. Start the server
51
+
52
+ Point the CLI at the package directory:
53
+
54
+ ```bash
55
+ bunx @awarebydefault/display-case .
56
+ ```
57
+
58
+ Or wire up an npm script and use that:
59
+
60
+ ```jsonc
61
+ // package.json
62
+ { "scripts": { "display-case": "display-case ." } }
63
+ ```
64
+
65
+ ```bash
66
+ bun run display-case
67
+ ```
68
+
69
+ The server prints its URL (default `http://localhost:3100`). Open it and you'll see your components grouped by hierarchy level in the sidebar, with each case rendered in an isolated frame.
70
+
71
+ Change a `*.case.tsx` or `*.placard.md` file and the server rebuilds automatically. There is no in-page hot reload — refresh to pick up the change.
72
+
73
+ ## 4. Next steps
74
+
75
+ - Add interactive controls: [Tweaks](tweaks.md).
76
+ - Group and order components: [Hierarchy](hierarchy.md).
77
+ - Document a component inline: [Documentation panel](documentation-panel.md).
78
+ - Catch regressions: [Testing](testing.md).
@@ -0,0 +1,180 @@
1
+ # Style engines (CSS-in-JS: MUI / emotion)
2
+
3
+ > Nav: [Quick start](quick-start.md) · [Writing cases](writing-cases.md) · [Hierarchy](hierarchy.md) · [Tweaks](tweaks.md) · [Theming](theming.md) · **Style engines** · [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
+ Display Case delivers every surface **rendered and styled before scripts run**.
6
+ For static CSS that means [`globalStyles`](configuration.md#globalstyles); for a
7
+ theme provider or context it means the [`decorator`](configuration.md#decorator).
8
+ But libraries like **Material UI** style components with **emotion**, a runtime
9
+ CSS-in-JS engine that emits CSS *while the component renders*. Server rendering
10
+ keeps the markup but, without help, throws that styling away — so a MUI snapshot
11
+ comes back unstyled and a live preview flashes unstyled before the client runtime
12
+ catches up.
13
+
14
+ A **style engine** closes that gap. It does the two things a runtime CSS-in-JS
15
+ library needs on the server: give each render an isolated style store, then read
16
+ that render's critical CSS back into the document head **before scripting**.
17
+
18
+ ## The shape of an engine
19
+
20
+ ```ts
21
+ import type { StyleEngine } from '@awarebydefault/display-case'
22
+
23
+ // A StyleEngine is a factory called once per server render. It returns a
24
+ // collector with two methods:
25
+ // wrap(node) — wrap the tree in the library's provider over a FRESH store
26
+ // collect(html) — after render, return the <head> markup (style tags) it used
27
+ ```
28
+
29
+ A fresh store per render is what keeps one case's styling out of another's
30
+ snapshot, so the engine must be a **factory** (`() => …`), not a single object.
31
+ Engines are configured on [`styleEngines`](configuration.md#styleengines), and
32
+ apply to the isolated `/render` document and the Primer — the two surfaces that
33
+ render your components in-document.
34
+
35
+ ## Material UI / emotion (the flagship)
36
+
37
+ Two pieces, working together:
38
+
39
+ 1. **`styleEngines`** — the server-only emotion extractor (this seam).
40
+ 2. **`decorator`** — the `ThemeProvider` your components need, on both server and
41
+ client. On the client, emotion's default cache adopts the server styles
42
+ automatically, so there is no flash and no duplication.
43
+
44
+ ### 1. The emotion engine
45
+
46
+ ```tsx
47
+ // display-case.style-engine.tsx
48
+ import createCache from '@emotion/cache'
49
+ import { CacheProvider } from '@emotion/react'
50
+ import createEmotionServer from '@emotion/server/create-instance'
51
+ import type { StyleEngine } from '@awarebydefault/display-case'
52
+
53
+ export const emotionEngine: StyleEngine = () => {
54
+ // A fresh cache per render → per-render isolation. Key `css` is emotion's
55
+ // default, which the client runtime adopts automatically (no client wiring).
56
+ const cache = createCache({ key: 'css' })
57
+ cache.compat = true // required for extractCritical-style extraction
58
+ const { extractCriticalToChunks, constructStyleTagsFromChunks } =
59
+ createEmotionServer(cache)
60
+ return {
61
+ wrap: (node) => <CacheProvider value={cache}>{node}</CacheProvider>,
62
+ collect: (html) =>
63
+ constructStyleTagsFromChunks(extractCriticalToChunks(html)),
64
+ }
65
+ }
66
+ ```
67
+
68
+ ### 2. Wire it into the config
69
+
70
+ ```tsx
71
+ // display-case.config.tsx
72
+ import { defineConfig } from '@awarebydefault/display-case'
73
+ import { ThemeProvider, createTheme } from '@mui/material/styles'
74
+ import CssBaseline from '@mui/material/CssBaseline'
75
+ import { emotionEngine } from './display-case.style-engine'
76
+
77
+ const theme = createTheme({ /* your palette, typography, … */ })
78
+
79
+ export default defineConfig({
80
+ title: 'My MUI library',
81
+ roots: ['src/**/*.case.tsx'],
82
+ // Server: extract emotion's critical CSS into the document head.
83
+ styleEngines: [emotionEngine],
84
+ // Server + client: the provider your MUI components need. CssBaseline is
85
+ // optional but makes the preview match your app.
86
+ decorator: ({ children }) => (
87
+ <ThemeProvider theme={theme}>
88
+ <CssBaseline />
89
+ {children}
90
+ </ThemeProvider>
91
+ ),
92
+ })
93
+ ```
94
+
95
+ That is the whole integration. Now:
96
+
97
+ - `/render/<component>/<case>?theme=dark` fetched **without scripts** comes back
98
+ **fully styled** — a real snapshot, not a skeleton.
99
+ - The live preview paints styled on first frame: **no flash of unstyled content**.
100
+ - When scripts run, emotion finds its `data-emotion` style tags already in the
101
+ document and **adopts** them — nothing is re-injected or duplicated.
102
+
103
+ ### Why the pairing
104
+
105
+ | Concern | Where it lives | Runs on |
106
+ | --- | --- | --- |
107
+ | Fresh emotion cache + critical-CSS extraction | `styleEngines` | server only |
108
+ | `ThemeProvider` / `CssBaseline` (and the client cache emotion adopts into) | `decorator` | server + client |
109
+
110
+ Keep their cache keys aligned. With emotion's default key (`css`) the client cache
111
+ is implicit and adoption is automatic — which is why MUI needs no client-side
112
+ engine code.
113
+
114
+ ## Other emotion-like libraries
115
+
116
+ Any runtime CSS-in-JS library that exposes "provide a store, then read critical
117
+ CSS" fits the same `wrap` + `collect` contract.
118
+
119
+ **styled-components:**
120
+
121
+ ```tsx
122
+ import { ServerStyleSheet, StyleSheetManager } from 'styled-components'
123
+ import type { StyleEngine } from '@awarebydefault/display-case'
124
+
125
+ export const styledComponentsEngine: StyleEngine = () => {
126
+ const sheet = new ServerStyleSheet()
127
+ return {
128
+ wrap: (node) => (
129
+ <StyleSheetManager sheet={sheet.instance}>{node}</StyleSheetManager>
130
+ ),
131
+ collect: () => sheet.getStyleTags(), // reads the sheet, not the html
132
+ }
133
+ }
134
+ ```
135
+
136
+ styled-components does **not** auto-adopt the way emotion's default key does, so
137
+ add a matching client provider in your `decorator` if you see a re-style on
138
+ hydration.
139
+
140
+ ## Multiple engines
141
+
142
+ [`styleEngines`](configuration.md#styleengines) is an ordered array; engines nest
143
+ in array order (the first is outermost) and their head output is concatenated. Use
144
+ this only if a single showcase genuinely mixes two runtimes:
145
+
146
+ ```ts
147
+ styleEngines: [emotionEngine, styledComponentsEngine]
148
+ ```
149
+
150
+ ## When you do *not* need a style engine
151
+
152
+ - **Static CSS** (Tailwind's compiled output, hand-written stylesheets, design
153
+ tokens) → list it in [`globalStyles`](configuration.md#globalstyles). Tailwind:
154
+ build your CSS, then point `globalStyles` at the output file.
155
+ - **Zero-runtime CSS-in-JS** (Vanilla Extract, Linaria, **MUI Pigment CSS**) →
156
+ these emit static CSS at build time; treat the emitted file as `globalStyles`.
157
+ - **A component that must run in a browser anyway** → mark it
158
+ [`browserOnly`](writing-cases.md); it is exempt from server styling and mounts
159
+ (and styles) in the client. You lose the pre-scripting snapshot for that case.
160
+
161
+ ## Contract for a custom engine
162
+
163
+ If you write your own engine, it must:
164
+
165
+ - Be a **factory** — return a new collector (and a new store) on every call. A
166
+ shared store leaks one render's styling into another's document.
167
+ - Have an **idempotent `collect`** — the case tree renders inside `StrictMode`
168
+ (it may render twice); `collect` must return the same styling regardless.
169
+ - Return **complete head markup** from `collect` (e.g. `<style …>…</style>`),
170
+ including whatever attributes your client runtime keys on to adopt the styles —
171
+ Display Case places the string verbatim, after the document's static styles, and
172
+ does not parse it.
173
+ - Pair with a **client provider in `decorator`** if your library does not adopt
174
+ server styles automatically.
175
+
176
+ ## See also
177
+
178
+ - [Theming](theming.md) — `globalStyles`, `decorator`, and `data-theme`.
179
+ - [Configuration › `styleEngines`](configuration.md#styleengines) — the field reference.
180
+ - [Configuration › `decorator`](configuration.md#decorator) — the provider you pair with an engine.
@@ -0,0 +1,245 @@
1
+ # Testing
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** · [CLI](cli.md) · [AI agents](ai-agents.md) · [Configuration](configuration.md)
4
+
5
+ `display-case check` runs three kinds of audit over your cases:
6
+
7
+ - **Token conformance** — a static parse that flags `var(--token)` references which resolve to no custom property the package defines. No browser.
8
+ - **Accessibility** — an axe-core audit of every rendered case.
9
+ - **Visual regression** — a pixel diff of every rendered case against a stored baseline.
10
+
11
+ The a11y and visual phases drive the same `/render/<component>/<case>` endpoint the browse iframe uses, so what is tested is exactly what a viewer sees. Each case is exercised in **both** light and dark themes.
12
+
13
+ ```bash
14
+ display-case check . # run all phases
15
+ display-case check . --tokens # token conformance only (static, no browser)
16
+ display-case check . --a11y # a11y only
17
+ display-case check . --visual # visual only
18
+ display-case check . --update # (re)record visual baselines
19
+ ```
20
+
21
+ With no phase flag, every phase runs; naming any phase flag runs only the named phase(s).
22
+
23
+ Wrapped as an npm script (see [CLI](cli.md)):
24
+
25
+ ```bash
26
+ bun run display-case:check
27
+ ```
28
+
29
+ > The default runner uses Playwright's Chromium. Those packages are **optional** (see below); when present, pages are rendered at a fixed 1024×768 viewport with reduced motion, and the runner waits for the network to settle and fonts to load before auditing.
30
+
31
+ ## The default backend is lazy and optional
32
+
33
+ The capture/audit driver and the image diff are pluggable (config [`providers`](configuration.md#providers)). When you leave them unset, Display Case uses a built-in default: a Playwright + axe driver and a pixelmatch/pngjs diff. That default backs `playwright`, `@axe-core/playwright`, `pixelmatch`, and `pngjs` as **`optionalDependencies`** — and it is imported **lazily**, only when a default-backed `check --a11y`/`--visual` actually runs.
34
+
35
+ So the four packages are needed *only* for a default-backed check. Browsing, snapshotting (`/render`, `--print-manifest`), the token phase (`check --tokens`), and `init` never touch a browser and never trigger the import.
36
+
37
+ If a default-backed check runs without those packages installed, it fails with an actionable error — install the toolchain, run `init --with-visual`, or inject your own providers:
38
+
39
+ ```
40
+ Visual/a11y checks need the default toolchain. Install it with
41
+ `bun add -d playwright @axe-core/playwright pixelmatch pngjs && bunx playwright install chromium`
42
+ (or run `display-case init --with-visual`), or set `providers.driver`/`providers.diff` in display-case.config.ts.
43
+ ```
44
+
45
+ The on-demand setup step is the easiest path:
46
+
47
+ ```bash
48
+ display-case init <pkgDir> --with-visual # bun add the deps + install Chromium
49
+ ```
50
+
51
+ See [AI agents → Scaffolding integration](ai-agents.md#scaffolding-integration-init--uninstall).
52
+
53
+ ### Injecting a custom provider
54
+
55
+ Setting a [provider](configuration.md#providers) replaces the corresponding default and removes the need for the four packages. Point `diff` at a hosted service or a looser comparator, or point `driver` at a different headless browser:
56
+
57
+ ```ts
58
+ // display-case.config.ts
59
+ export default defineConfig({
60
+ title: 'Display Case',
61
+ roots: ['src/components/**/*.case.tsx'],
62
+ providers: {
63
+ driver: () => myDriver(), // unset half still falls back to the built-in
64
+ diff: myDiff,
65
+ },
66
+ })
67
+ ```
68
+
69
+ Each provider receives the `CaseContext` (`componentId`, `caseId`, `theme`, `width`), so identity-aware providers can vary per case while pure ones ignore it. The built-ins double as reference implementations: [`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). See [Configuration → `providers`](configuration.md#providers) for the full interfaces and a worked per-case-tolerance diff.
70
+
71
+ ## Structure checks
72
+
73
+ The structure phase (`check --structure`) is a set of static best-practice rules — no browser, no server. Each finding carries a **severity**: `error` findings fail the run, `warn` findings are reported but non-fatal (unless `--strict` escalates them). Rules are grouped by what they read:
74
+
75
+ **File / config rules** (default on, error):
76
+
77
+ - `case-placard-coverage` — every showcasable component has a sibling `*.case.tsx` **and** `*.placard.md`.
78
+ - `no-orphaned-placard-doc` — every `*.placard.md` has a sibling `*.case.tsx` (a component module is not required).
79
+ - `primer-present-and-used` — a primer is configured, exists, embeds ≥1 `<Display>` specimen, and has prose (parsed, not regex-scanned).
80
+ - `setup-present` — custom `providers` are configured, or the default snapshot toolchain resolves from either the showcase or the Display Case package itself (so a toolchain provided transitively via Display Case is not a false miss).
81
+ - `config-paths-exist` — `globalStyles` entries (and `baselineDir`) resolve.
82
+
83
+ **Catalog-integrity rules** (default on, error):
84
+
85
+ - `levels-classified` — every component declares a hierarchy level (none left unclassified).
86
+ - `cases-load` — every case file loads.
87
+ - `flow-transitions-resolve` — every flow step transition targets an existing step.
88
+ - `flow-multi-step` — a flow has more than one step.
89
+ - `unique-slugs` — no two components, or two cases within a component, collide on their address slug.
90
+ - `tweak-defaults-valid` — a `choice` tweak's default is one of its options.
91
+
92
+ **Case-content rules** (default on, error):
93
+
94
+ - `interactive-cases-keyed` — a locally-defined stateful specimen (a wrapper that calls `useState`/`useReducer`) reused across **≥2** cases carries a `key` on every usage. The browse chrome swaps cases in place without unmounting, so an unkeyed wrapper keeps the previous case's state on switch (see [Writing cases → Authoring rules](writing-cases.md#authoring-rules)). Single-use specimens are exempt (they always remount). Heuristic, regex-based: it checks for *presence* of a `key`, not that the keys are distinct.
95
+
96
+ **Composition (import-graph) rules** (opt-in, default **off**):
97
+
98
+ - `atom-purity` (error) — an `atom` imports no other showcased component.
99
+ - `no-downward-dependency` (error) — no component imports a strictly-higher-level component (same-level is allowed; an organism may compose other organisms).
100
+ - `composes-lower-level` (warn) — a non-atom imports at least one lower-level showcased component. An organism built only of atoms passes.
101
+ - `level-fit` (warn) — a component composing more lower-level parts than its level's threshold is flagged for promotion.
102
+
103
+ Composition rules scan imports and resolve them to levels, following workspace re-exports so a component that imports its atoms from another showcase in the same workspace is understood. An import that can't be resolved to a showcased component is never an error; a workspace-showcase import the resolver can't follow is reported as a warning.
104
+
105
+ ```
106
+ structure ✗ src/components/button.tsx: missing colocated usage doc (expected a sibling *.placard.md) (case-placard-coverage)
107
+ structure ⚠ src/sections/header.tsx: organism "Header" composes no lower-level showcased component (composes-lower-level)
108
+ ```
109
+
110
+ Escapes:
111
+
112
+ - **Per file** — a `display-case: <token>` comment. `no-case` marks a module non-showcasable (exempt from coverage entirely); `no-placard` waives only the prompt; `allow-orphan` (in a `*.placard.md`) waives the orphan rule; `unclassified` (in a `*.case.tsx`) waives the level rule; any other rule accepts `allow-<rule-id>` in the relevant source file. Include a reason after the token.
113
+ - **Per path** — `check.structure.rules.<id>.ignore` globs (see [Configuration](configuration.md)); the only escape for findings not tied to one editable file (e.g. `unique-slugs`).
114
+ - **Per rule** — set the rule to `false` to disable it, or `'warn'`/`'error'` to retune its severity.
115
+
116
+ ## Token conformance
117
+
118
+ A package's design tokens are a closed vocabulary: the custom properties its `globalStyles` define, plus any set at runtime via an inline `style={{ '--x': … }}` object. This phase scans the package source (component CSS/TSX **and** case files) and flags every `var(--token)` whose name is in neither set — the class of bug where a component borrows a foreign design system's token (`var(--muted-foreground, #6b7280)`) that never resolves and silently falls back to a hardcoded value.
119
+
120
+ ```
121
+ tokens ✗ src/components/tweak-control.case.tsx:18:30 unknown token --muted-foreground (fallback does not excuse it)
122
+ ```
123
+
124
+ It is deliberately strict: a `var(--x, fallback)` is still flagged even though the fallback makes it valid CSS — the rule is conformance to *this* package's vocabulary, not CSS validity. Comments and string contents are handled so a `var()` inside a comment is ignored while one inside a JS string (real usage) is checked.
125
+
126
+ Escapes:
127
+
128
+ - **Per reference** — an `allow: unknown-token` comment on the offending line or the line directly above it.
129
+ - **Per token** — list names the package legitimately references but does not define (e.g. tokens a host app supplies) under `tokens.allow` in the config:
130
+
131
+ ```ts
132
+ export default defineConfig({
133
+ title: 'Display Case',
134
+ roots: ['src/components/**/*.case.tsx'],
135
+ globalStyles: ['./src/tokens.css', './src/components.css'],
136
+ tokens: { allow: ['--app-provided-token'] },
137
+ })
138
+ ```
139
+
140
+ ## Accessibility
141
+
142
+ For every case in every theme, the runner analyzes the page with axe-core and reports each violation, followed by the affected nodes — for colour-contrast, the failing element and the exact measured-vs-required pair, so a finding is fixable without re-running a browser:
143
+
144
+ ```
145
+ a11y ✗ tweak-control/variants [dark] color-contrast: Elements must meet contrast ratio (2 node(s))
146
+ ↳ .dcui-tweak-label #8a8073 on #ffffff = 3.87:1 (need 4.5:1) [12.0pt (16px) normal]
147
+ ↳ .dcui-tweak-url #8a8073 on #ffffff = 3.87:1 (need 4.5:1) [12.0pt (16px) normal]
148
+ ```
149
+
150
+ The inline list is capped per violation (with a `+N more` note); the **complete** results of every run — each failing case with its per-node detail — are written to `.display-case/a11y/last-check.json` (under the gitignored cache, overwritten each run, empty on a clean run). Read that file to inspect the exact failing colours/elements later without re-running the checks. The per-node detail is present whenever the audit mechanism provides it (the built-in axe driver always does); a custom [`providers.driver`](configuration.md#providers) MAY populate or omit it.
151
+
152
+ The detail rides on the same machine-readable violation shape the in-app surface uses, but the running Accessibility panel deliberately shows only the per-variant verdict (rule, severity, node count) — the per-node detail stays in the gate output and the persisted report.
153
+
154
+ The isolated render document is a complete page (a `<title>`, `lang`, and a single `<main>` landmark) so the audit reports real component issues rather than harness chrome.
155
+
156
+ ## Visual regression
157
+
158
+ For every case in every theme, the runner takes a screenshot and compares it to a baseline PNG:
159
+
160
+ - **No baseline yet** → the screenshot is recorded as the new baseline (counted as "recorded", not a failure).
161
+ - **Baseline matches** → pass.
162
+ - **Baseline differs** → failure. A `<case>.<theme>.diff.png` is written next to the baseline so you can inspect the change.
163
+ - **Dimensions changed** → failure. The new render is saved as `<case>.<theme>.actual.png` for inspection.
164
+
165
+ ```
166
+ visual ✗ tweak-control/variants [light] differs from baseline
167
+ ```
168
+
169
+ The diff threshold is strict: any differing pixel counts as a change.
170
+
171
+ ### Recording and updating baselines
172
+
173
+ Pass `--update` to (re)record every baseline from the current renders. Do this after an intentional visual change, then review the new PNGs before committing.
174
+
175
+ ```bash
176
+ display-case check . --visual --update
177
+ ```
178
+
179
+ When baselines are committed and diffed in CI (Linux), record them in that same environment so they match — this repo provides two paths:
180
+
181
+ - **Locally** — `bun run baselines:record` records inside the pinned Playwright Docker image (`scripts/record-baselines.ts`). Requires Docker. Review the diff and commit the PNGs.
182
+ - **From CI** — run the **Update visual baselines** workflow (`.github/workflows/update-baselines.yml`) via *Actions → Run workflow*. Pick the branch, optionally an `only` filter, and it records in the CI container and commits the refreshed baselines back to that branch (`[skip ci]`). Use this when you don't have Docker locally.
183
+
184
+ ### Where baselines live
185
+
186
+ By default baselines are written to the gitignored cache at `.display-case/baselines/`, organized as `<component>/<case>.<theme>.png`. These are local-only and will be recorded fresh on a clean checkout.
187
+
188
+ To **commit** baselines and gate CI on them, point `baselineDir` at a tracked directory in your config:
189
+
190
+ ```ts
191
+ // display-case.config.ts
192
+ export default defineConfig({
193
+ title: 'Display Case',
194
+ roots: ['src/components/**/*.case.tsx'],
195
+ baselineDir: 'baselines', // committed; resolved relative to the package
196
+ })
197
+ ```
198
+
199
+ `baselineDir` accepts a path relative to the package or an absolute path. See [Configuration](configuration.md#baselinedir).
200
+
201
+ > **Committed baselines must match the environment that diffs them.** Pixel
202
+ > renders differ across operating systems (fonts, antialiasing). If CI diffs in
203
+ > Linux, record the committed baselines in that same Linux environment, not on a
204
+ > developer's machine — otherwise every CI run reports false changes. This repo
205
+ > records them in the pinned Playwright Docker image via `bun run
206
+ > baselines:record`; see [contributing/testing-best-practices.md](../contributing/testing-best-practices.md).
207
+ >
208
+ > Because of this, a repo with committed (Linux) baselines should opt `visual`
209
+ > out of the **default** run so a bare `display-case check .` on a contributor's
210
+ > machine doesn't report off-platform false diffs. Set
211
+ > [`check.defaultPhases`](configuration.md)`: { visual: false }`; the phase still
212
+ > runs when asked explicitly (`--visual`) and in CI. This repo does exactly that.
213
+
214
+ ## Change-scoped checks
215
+
216
+ The a11y and visual phases re-render every case, which is wasteful in CI when a
217
+ change touched only a few components. Two flags restrict them to a subset (the
218
+ static phases are unaffected):
219
+
220
+ - `--only=<ids/globs>` — check only the named components (comma-separated).
221
+ - `--changed[=ref]` — check only the components a change touched since `ref`
222
+ (default the base branch; override with `=ref` or `DISPLAY_CASE_BASE_REF`).
223
+
224
+ With `--changed`, a component is in scope when a changed file is in its **import
225
+ closure** — the case, the component, and everything they reference transitively
226
+ (including stylesheets reached by `@import`). Two deliberate fallbacks keep it
227
+ sound:
228
+
229
+ - A render-relevant change that **no** component's closure claims — a
230
+ globally-applied stylesheet, the shared render path, other shared source —
231
+ scopes to **every** component, so a regression is never silently skipped.
232
+ - A change touching **no** render input (docs, specs, tests, tooling) scopes to
233
+ **nothing**: the render phases report success without launching a browser.
234
+
235
+ When both flags are given, the scope is their intersection. This is what the CI
236
+ a11y/visual jobs use to gate a PR on only the components it could have affected.
237
+
238
+ ```bash
239
+ display-case check . --a11y --only=button
240
+ display-case check . --a11y --visual --changed=origin/main
241
+ ```
242
+
243
+ ## Exit codes
244
+
245
+ `display-case check` exits **0** when there are zero token violations, zero a11y violations, zero visual changes, and zero **error**-severity structure findings, and **1** otherwise. Structure **warnings** are reported but do not, by themselves, cause a non-zero exit unless `--strict` (or `check.structure.strict`) escalates them. Recording new baselines does not, by itself, cause a non-zero exit. This makes the command safe to use as a CI gate.
@@ -0,0 +1,97 @@
1
+ # Theming
2
+
3
+ > Nav: [Quick start](quick-start.md) · [Writing cases](writing-cases.md) · [Hierarchy](hierarchy.md) · [Tweaks](tweaks.md) · **Theming** · [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](configuration.md)
4
+
5
+ Components render in an isolated document so what you see (and what the check runner screenshots) is exactly the component, with no showcase chrome leaking in. Theming is controlled in three places: global styles, an optional decorator, and per-render URL parameters. (For components styled by a runtime CSS-in-JS library like emotion/MUI, see [Render-time styling](#render-time-styling-css-in-js--mui) below.)
6
+
7
+ ## Global styles
8
+
9
+ List your CSS entrypoints in the config. Their contents are concatenated and injected into both the browse shell and the isolated render document, so components render with their real tokens and styles.
10
+
11
+ ```ts
12
+ // display-case.config.ts
13
+ import { defineConfig } from '@awarebydefault/display-case'
14
+
15
+ export default defineConfig({
16
+ title: 'Display Case',
17
+ roots: ['src/components/**/*.case.tsx'],
18
+ globalStyles: ['./src/tokens.css', './src/components.css'],
19
+ })
20
+ ```
21
+
22
+ Paths are resolved relative to the package. A listed file that does not exist is silently skipped.
23
+
24
+ ## Light and dark via `data-theme`
25
+
26
+ The isolated render reads a `theme` query parameter and sets it on the document root before rendering:
27
+
28
+ ```
29
+ /render/tweak-control/variants?theme=light
30
+ /render/tweak-control/variants?theme=dark
31
+ ```
32
+
33
+ It applies as `<html data-theme="light">` (or `"dark"`). The render document also sets `data-theme-pref` to the same value, so an app `ThemeProvider` rendered via the [decorator](configuration.md#decorator) (e.g. behind a nav `ThemeToggle`) initializes to the harness theme instead of re-resolving from the OS and fighting the `?theme=` selection. Any value other than `dark` is treated as light. Author your tokens against this attribute:
34
+
35
+ ```css
36
+ :root[data-theme='dark'] {
37
+ --bg: #111;
38
+ --fg: #eee;
39
+ }
40
+ ```
41
+
42
+ The check runner exercises **both** themes for every case, so a baseline is captured per theme. See [Testing](testing.md).
43
+
44
+ ## Decorator
45
+
46
+ A decorator is a single React wrapper rendered around every case — the place for a theme provider, context, or a fixed frame.
47
+
48
+ ```tsx
49
+ // display-case.config.ts
50
+ import { defineConfig } from '@awarebydefault/display-case'
51
+ import { ThemeProvider } from './src/components/theme-provider'
52
+
53
+ export default defineConfig({
54
+ title: 'Display Case',
55
+ roots: ['src/components/**/*.case.tsx'],
56
+ decorator: ThemeProvider,
57
+ })
58
+ ```
59
+
60
+ The decorator accepts `{ children }` plus the active case's `level`, `sourcePath`, and `area` — so beyond cross-cutting context it can wrap page/flow cases in app chrome (nav/header/footer). It wraps the rendered case (and the viewport-width wrapper, if any) inside React `StrictMode`. Use it for cross-cutting context that every component needs; prefer per-case composition for anything component-specific. See [Configuration › decorator](configuration.md#decorator) for the full signature and the per-area chrome pattern.
61
+
62
+ ## Render-time styling (CSS-in-JS / MUI)
63
+
64
+ Global styles and the decorator cover static CSS and providers. But **Material UI**
65
+ (and any emotion / styled-components library) emits its CSS *while a component
66
+ renders* — so server rendering keeps the markup but loses the styling, giving an
67
+ unstyled snapshot and a flash on first paint.
68
+
69
+ A **style engine** fixes that: it collects the render-time CSS during the server
70
+ render and delivers it before scripting. Configure it alongside the decorator —
71
+ the engine handles the server-side extraction, the decorator provides the
72
+ `ThemeProvider` your components need:
73
+
74
+ ```ts
75
+ styleEngines: [emotionEngine],
76
+ decorator: ({ children }) => <ThemeProvider theme={theme}>{children}</ThemeProvider>,
77
+ ```
78
+
79
+ See [Style engines](style-engines.md) for the full emotion/MUI recipe and the
80
+ contract for other libraries.
81
+
82
+ ## Viewport width
83
+
84
+ In the **browse chrome**, the header carries a Chrome-DevTools-style device toolbar: a **Responsive** mode (full or a preset width — Desktop/Tablet/Mobile — with manual zoom) and **fixed device sizes** (1080p, 4K, laptop, iPad, iPhone, Pixel, Galaxy, or a custom `W × H` with a rotate button) that render the iframe at exact pixels and auto-scale to fit the panel. No URL parameter is involved — the toolbar sizes the iframe element directly.
85
+
86
+ For the **standalone `/render` endpoint** (snapshots, direct navigation), constrain the width with the `width` query parameter (in pixels) instead:
87
+
88
+ ```
89
+ /render/page/dashboard?width=480
90
+ ```
91
+
92
+ The case is wrapped in a centered container with `max-width: <width>px`. This is handy for previewing responsive behavior or capturing a narrow snapshot. Omit it for the default full-width render.
93
+
94
+ ## See also
95
+
96
+ - [Configuration](configuration.md) for the full `globalStyles` / `decorator` reference and defaults.
97
+ - [AI agents](ai-agents.md) for combining `theme`, `width`, and tweak parameters into a single deterministic render URL.