@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,678 @@
1
+ // display-case: no-placard — Display Case's own browse chrome, cased for
2
+ // dogfooding (ShellView.case.tsx); internal plumbing, not a consumer primitive.
3
+ import type { CSSProperties, ReactNode } from 'react'
4
+ import type { TweakDescriptor } from '../../../../index'
5
+ import { DocMarkdown } from '../../../markdown'
6
+ import {
7
+ buildUrl,
8
+ DEVICES,
9
+ DOC_MAX_W,
10
+ DOC_MIN_W,
11
+ LEVEL_LABEL,
12
+ type Mode,
13
+ RESPONSIVE,
14
+ STAGE_FADE_MS,
15
+ ZOOM_MAX,
16
+ ZOOM_MIN,
17
+ ZOOM_STEP,
18
+ } from '../../../shell-core'
19
+ import { DcTestIds } from '../../../test-ids'
20
+ import type { ShellViewModel } from '../../../use-shell'
21
+ import type { SegmentedOption } from '..'
22
+ import {
23
+ A11yPanel,
24
+ Button,
25
+ Eyebrow,
26
+ FlowNav,
27
+ IconButton,
28
+ Input,
29
+ NavItem,
30
+ RenderAddress,
31
+ SegmentedToggle,
32
+ SelectMenu,
33
+ Sidebar,
34
+ Stage,
35
+ TweaksPanel,
36
+ Wordmark,
37
+ } from '..'
38
+
39
+ /**
40
+ * The browse chrome, as a pure function of its {@link ShellViewModel}. Every
41
+ * piece of state, every derived layout number, and every event handler is fed
42
+ * in — `ShellView` only arranges them into the header, nav rail, stage, and
43
+ * Primer host. The live render/Primer iframes are injected as `renderFrame` /
44
+ * `primerFrame` slots, so the same view paints either the running chrome (real
45
+ * iframes, from {@link useShell}) or a static page/flow exhibit (a stub slot
46
+ * over a hand-built model). This is what lets Display Case dogfood its own
47
+ * layout as a template, page, and flow.
48
+ */
49
+ export interface ShellViewProps extends ShellViewModel {
50
+ /** The stage's preview surface — the live `<iframe>` in the app; a static
51
+ * stand-in (a rendered component, a placeholder box) in a page/template case. */
52
+ renderFrame: ReactNode
53
+ /** The Primer reading surface — the live `<iframe>` in the app; a static
54
+ * stand-in in a Primer page/template case. */
55
+ primerFrame: ReactNode
56
+ /** Make the stage's frame box fill the stage edge-to-edge instead of sizing to
57
+ * the measured `boxW`/`boxH`. The live chrome leaves this off (it measures the
58
+ * panel); a static page/flow *exhibit* sets it so a full-screen page or flow
59
+ * fills the whole stage rather than sitting in a small centred box. */
60
+ fillFrame?: boolean
61
+ }
62
+
63
+ export function ShellView(props: ShellViewProps) {
64
+ const { manifest, theme, shownMode, mode, setMode, modeFadeStyle } = props
65
+ return (
66
+ <div
67
+ className="dc-app"
68
+ data-testid={DcTestIds.app}
69
+ data-theme={theme}
70
+ data-nav={props.navCollapsed ? 'collapsed' : 'open'}>
71
+ <ShellHeader {...props} />
72
+
73
+ <Sidebar
74
+ data-testid={DcTestIds.sidebar}
75
+ label={shownMode === 'primer' ? 'Primer contents' : 'Components'}>
76
+ {/* The ModeSwitch is pinned above the scroll region (a non-scrolling row),
77
+ so it stays put while nav items scroll and fade beneath it, and its
78
+ highlight box keeps lerping during the crossfade. The scroll region's
79
+ fading body tracks `shownMode` so it swaps mid-fade, in step with the
80
+ screen content. */}
81
+ {manifest.primer && <ModeSwitch mode={mode} onMode={setMode} />}
82
+ <NavContents {...props} />
83
+ </Sidebar>
84
+
85
+ {/* Library main and the Primer host both occupy the `main` grid area; the
86
+ inactive one is `hidden`. Keeping the library mounted preserves the
87
+ render-frame handshake across mode switches. Visibility tracks
88
+ `shownMode` (the swap happens mid-fade) and the whole region rides the
89
+ shared opacity crossfade so the screen content fades out and back in. */}
90
+ <main
91
+ className="dc-main"
92
+ hidden={shownMode === 'primer'}
93
+ style={modeFadeStyle}>
94
+ <LibraryStage {...props} />
95
+ </main>
96
+
97
+ {manifest.primer && (
98
+ <section
99
+ className="dc-primer-host"
100
+ aria-label="Primer"
101
+ hidden={shownMode !== 'primer'}
102
+ style={modeFadeStyle}>
103
+ {props.primerFrame}
104
+ </section>
105
+ )}
106
+ </div>
107
+ )
108
+ }
109
+
110
+ function ShellHeader(props: ShellViewProps) {
111
+ const {
112
+ manifest,
113
+ theme,
114
+ setTheme,
115
+ navCollapsed,
116
+ setNavCollapsed,
117
+ shownMode,
118
+ modeFadeStyle,
119
+ sizeId,
120
+ setSizeId,
121
+ widthInputValue,
122
+ fixed,
123
+ editDim,
124
+ rotateDims,
125
+ fitted,
126
+ scale,
127
+ manualZoom,
128
+ setManualZoom,
129
+ stageDecor,
130
+ showGrid,
131
+ setShowGrid,
132
+ component,
133
+ docOpen,
134
+ changeDocsOpen,
135
+ } = props
136
+ return (
137
+ <header className="dc-header">
138
+ <div className="dc-header-left">
139
+ <IconButton
140
+ glyph="☰"
141
+ label="Toggle navigation"
142
+ aria-expanded={!navCollapsed}
143
+ onClick={() => setNavCollapsed((c) => !c)}
144
+ />
145
+ <Wordmark data-testid={DcTestIds.wordmark}>{manifest.title}</Wordmark>
146
+ </div>
147
+ <div className="dc-controls">
148
+ {/* The device toolbar, zoom, grid and docs controls act on the stage,
149
+ so they're library-only; the Primer view keeps just the theme
150
+ toggle. Gated on `shownMode` (not `mode`) and wrapped in a fading
151
+ group so they fade out before the swap to Primer and fade in after
152
+ the swap to Cases — in step with the nav and screen crossfade. */}
153
+ {shownMode === 'library' && (
154
+ <div className="dc-controls-extra" style={modeFadeStyle}>
155
+ {/* The custom listbox (not a native <select>) so a pick commits
156
+ instantly and the trigger styling matches the tweak controls.
157
+ Disabled options stand in as the Responsive / Devices group
158
+ headers. */}
159
+ <SelectMenu
160
+ size="sm"
161
+ value={sizeId}
162
+ onChange={setSizeId}
163
+ aria-label="Screen size"
164
+ options={[
165
+ { value: '__responsive', label: 'Responsive', disabled: true },
166
+ ...RESPONSIVE.map((r) => ({ value: r.id, label: r.label })),
167
+ { value: '__devices', label: 'Devices', disabled: true },
168
+ ...DEVICES.map((d) => ({ value: d.id, label: d.label })),
169
+ { value: 'custom', label: 'Custom…' },
170
+ ]}
171
+ />
172
+ <div className="dc-dims">
173
+ <input
174
+ className="dc-dim"
175
+ type="number"
176
+ min={1}
177
+ aria-label="Width (px)"
178
+ disabled={!fixed}
179
+ value={widthInputValue}
180
+ placeholder={fixed ? undefined : 'auto'}
181
+ onChange={(e) => editDim('w', e.target.value)}
182
+ />
183
+ <span className="dc-dim-x">×</span>
184
+ <input
185
+ className="dc-dim"
186
+ type="number"
187
+ min={1}
188
+ aria-label="Height (px)"
189
+ disabled={!fixed}
190
+ value={fixed ? fixed.h : ''}
191
+ placeholder={fixed ? undefined : 'auto'}
192
+ onChange={(e) => editDim('h', e.target.value)}
193
+ />
194
+ <IconButton
195
+ glyph="⟲"
196
+ label="Rotate (swap width and height)"
197
+ variant="bare"
198
+ size="sm"
199
+ disabled={!fixed}
200
+ onClick={rotateDims}
201
+ />
202
+ </div>
203
+ {fitted ? (
204
+ <span
205
+ className="dc-zoom-level dc-zoom-fit"
206
+ title="Scaled to fit the panel">
207
+ {Math.round(scale * 100)}%
208
+ </span>
209
+ ) : (
210
+ <div className="dc-zoom">
211
+ <IconButton
212
+ glyph="−"
213
+ label="Zoom out"
214
+ variant="bare"
215
+ size="sm"
216
+ disabled={manualZoom <= ZOOM_MIN}
217
+ onClick={() =>
218
+ setManualZoom((z) =>
219
+ Math.max(ZOOM_MIN, Math.round((z - ZOOM_STEP) * 10) / 10),
220
+ )
221
+ }
222
+ />
223
+ <button
224
+ type="button"
225
+ className="dc-zoom-level"
226
+ aria-label="Reset zoom"
227
+ onClick={() => setManualZoom(() => 1)}>
228
+ {Math.round(manualZoom * 100)}%
229
+ </button>
230
+ <IconButton
231
+ glyph="+"
232
+ label="Zoom in"
233
+ variant="bare"
234
+ size="sm"
235
+ disabled={manualZoom >= ZOOM_MAX}
236
+ onClick={() =>
237
+ setManualZoom((z) =>
238
+ Math.min(ZOOM_MAX, Math.round((z + ZOOM_STEP) * 10) / 10),
239
+ )
240
+ }
241
+ />
242
+ </div>
243
+ )}
244
+ {stageDecor && (
245
+ <Button
246
+ data-testid={DcTestIds.gridButton}
247
+ aria-pressed={showGrid}
248
+ title="Toggle the stage grid (vs the app background)"
249
+ onClick={() => setShowGrid((g) => !g)}>
250
+ Grid
251
+ </Button>
252
+ )}
253
+ {component?.placardDoc && (
254
+ <Button
255
+ data-testid={DcTestIds.docsButton}
256
+ aria-pressed={docOpen}
257
+ aria-expanded={docOpen}
258
+ onClick={() => changeDocsOpen(!docOpen)}>
259
+ Docs
260
+ </Button>
261
+ )}
262
+ </div>
263
+ )}
264
+ <Button
265
+ data-testid={DcTestIds.themeToggle}
266
+ onClick={() => setTheme((t) => (t === 'light' ? 'dark' : 'light'))}>
267
+ {theme === 'light' ? 'Dark' : 'Light'}
268
+ </Button>
269
+ </div>
270
+ </header>
271
+ )
272
+ }
273
+
274
+ function NavContents(props: ShellViewProps) {
275
+ const {
276
+ shownMode,
277
+ modeFadeStyle,
278
+ navScrollRef,
279
+ navBodyRef,
280
+ primerGroups,
281
+ primerActive,
282
+ primerExpanded,
283
+ togglePrimerGroup,
284
+ scrollToSection,
285
+ groups,
286
+ expanded,
287
+ sel,
288
+ toggleExpanded,
289
+ selectComponent,
290
+ select,
291
+ } = props
292
+ return (
293
+ <div ref={navScrollRef} className="dc-nav-scroll">
294
+ <div ref={navBodyRef} className="dc-nav-body" style={modeFadeStyle}>
295
+ {shownMode === 'primer'
296
+ ? primerGroups.map((g) => {
297
+ const items = g.items.map((s) => (
298
+ <NavItem
299
+ key={s.id}
300
+ kind="case"
301
+ label={s.title}
302
+ current={primerActive === s.id}
303
+ onSelect={() => scrollToSection(s.id)}
304
+ />
305
+ ))
306
+ // Displays before the first `##` heading get a plain "Contents"
307
+ // label (there is at most one such leading group).
308
+ const heading = g.heading
309
+ if (!heading) {
310
+ return (
311
+ <div key="primer-lead" className="dc-primer-group">
312
+ <Eyebrow className="dc-group-label">Contents</Eyebrow>
313
+ {items}
314
+ </div>
315
+ )
316
+ }
317
+ // A heading with no Displays under it is a leaf: it scrolls but
318
+ // has no accordion (no chevron, no count, no expand state) — same
319
+ // treatment as a single-case component in the library tree.
320
+ const hasItems = g.items.length > 0
321
+ const collapsed = !primerExpanded.has(heading.id)
322
+ // When a group is collapsed, a Display in view has no visible row,
323
+ // so stand its heading in as the active marker — the highlight
324
+ // stays put while reading instead of vanishing into the fold.
325
+ const headingActive =
326
+ primerActive === heading.id ||
327
+ (hasItems &&
328
+ collapsed &&
329
+ g.items.some((it) => it.id === primerActive))
330
+ return (
331
+ <div key={heading.id} className="dc-primer-group">
332
+ <NavItem
333
+ kind="component"
334
+ label={heading.title}
335
+ count={hasItems ? g.items.length : undefined}
336
+ current={headingActive}
337
+ expanded={hasItems ? !collapsed : undefined}
338
+ onToggle={
339
+ hasItems ? () => togglePrimerGroup(heading.id) : undefined
340
+ }
341
+ onSelect={() => scrollToSection(heading.id)}
342
+ />
343
+ {hasItems && !collapsed && items}
344
+ </div>
345
+ )
346
+ })
347
+ : groups.map(({ key, components }) => (
348
+ <div key={key} className="dc-group">
349
+ <Eyebrow className="dc-group-label">{LEVEL_LABEL[key]}</Eyebrow>
350
+ {components.map((c) => {
351
+ // A single-case component reads as a leaf: no chevron, no
352
+ // count, no case row — the lone case lives in the stage caption
353
+ // and URL. Selecting the row routes straight to that case.
354
+ const single = c.cases.length === 1
355
+ const isExpanded = !single && expanded.has(c.id)
356
+ // Per-variant violation counts. A collapsed multi-case parent
357
+ // (and a single-case leaf, which never expands) shows the sum
358
+ // across variants directly; an expanded parent shows a plain
359
+ // dot instead and the per-variant counts move onto its case
360
+ // rows. A leaf has no children, so it always shows its number.
361
+ const variants = props.a11y?.byVariant[c.id]
362
+ const total = variants
363
+ ? Object.values(variants).reduce((a, b) => a + b, 0)
364
+ : 0
365
+ let parentAlert: number | 'dot' | undefined
366
+ if (total > 0) parentAlert = isExpanded ? 'dot' : total
367
+ return (
368
+ <div key={c.id} className="dc-nav-component">
369
+ <NavItem
370
+ kind="component"
371
+ label={c.name}
372
+ count={single ? undefined : c.cases.length}
373
+ alert={parentAlert}
374
+ current={sel?.componentId === c.id}
375
+ expanded={isExpanded}
376
+ testId={DcTestIds.navComponent(c.id)}
377
+ toggleTestId={
378
+ single
379
+ ? undefined
380
+ : DcTestIds.navComponentToggle(c.id)
381
+ }
382
+ alertTestId={DcTestIds.navAlert(c.id)}
383
+ onToggle={
384
+ single ? undefined : () => toggleExpanded(c.id)
385
+ }
386
+ onSelect={() => selectComponent(c)}
387
+ />
388
+ {isExpanded &&
389
+ c.cases.map((cs) => (
390
+ <NavItem
391
+ key={cs.id}
392
+ kind="case"
393
+ label={cs.name}
394
+ alert={variants?.[cs.id]}
395
+ current={
396
+ sel?.componentId === c.id && sel?.caseId === cs.id
397
+ }
398
+ testId={DcTestIds.navCase(c.id, cs.id)}
399
+ alertTestId={`${DcTestIds.navAlert(c.id)}-${cs.id}`}
400
+ onSelect={() =>
401
+ select({
402
+ componentId: c.id,
403
+ caseId: cs.id,
404
+ tweaks: {},
405
+ })
406
+ }
407
+ />
408
+ ))}
409
+ </div>
410
+ )
411
+ })}
412
+ </div>
413
+ ))}
414
+ </div>
415
+ </div>
416
+ )
417
+ }
418
+
419
+ function LibraryStage(props: ShellViewProps) {
420
+ const {
421
+ component,
422
+ activeCase,
423
+ addressUrl,
424
+ sel,
425
+ select,
426
+ attachPreview,
427
+ stageDecor,
428
+ showGrid,
429
+ sizeMeta,
430
+ padX,
431
+ padY,
432
+ stageShown,
433
+ boxW,
434
+ boxH,
435
+ fillFrame,
436
+ renderFrame,
437
+ tweaksFloating,
438
+ setTweaksFloating,
439
+ docOpen,
440
+ changeDocsOpen,
441
+ docText,
442
+ docWidth,
443
+ startDocResize,
444
+ onDocResizeKey,
445
+ } = props
446
+ if (!component || !activeCase) {
447
+ return <div className="dc-empty">Select a case.</div>
448
+ }
449
+ // The same crossfade the stage uses (driven by `stageShown`): on navigation the
450
+ // tweaks and accessibility panels fade out, then back in once the new exhibit
451
+ // has swapped in behind the fade — so the whole content column transitions as
452
+ // one instead of the panels snapping while the stage fades.
453
+ const fade: CSSProperties = {
454
+ opacity: stageShown ? 1 : 0,
455
+ transition: `opacity ${STAGE_FADE_MS}ms ease`,
456
+ }
457
+ return (
458
+ <div className="dc-stage">
459
+ <div className="dc-content">
460
+ {/* The exhibit's shareable address: the standalone /render URL for
461
+ what's on the stage (theme + tweak overrides), copyable. */}
462
+ <RenderAddress url={addressUrl} />
463
+ {component.isFlow && (
464
+ <FlowNav
465
+ steps={component.cases.map((cs) => ({
466
+ id: cs.id,
467
+ label: cs.name,
468
+ }))}
469
+ activeId={activeCase.id}
470
+ onSelect={(caseId) =>
471
+ select({ componentId: component.id, caseId, tweaks: {} })
472
+ }
473
+ />
474
+ )}
475
+ {/* The preview is the stable centering viewport (measured for the
476
+ available area); the stage frame inside carries the border,
477
+ grid + corner ticks and hugs the exhibit (decorated) or fills
478
+ the viewport edge-to-edge (pages/flows). */}
479
+ <div className="dc-preview" ref={attachPreview}>
480
+ <Stage
481
+ caption={activeCase.name}
482
+ meta={sizeMeta}
483
+ frame={stageDecor ? 'hug' : 'fill'}
484
+ grid={stageDecor && showGrid}
485
+ corners={stageDecor}
486
+ surface={
487
+ stageDecor && !showGrid
488
+ ? 'var(--color-bg, var(--dc-surface))'
489
+ : undefined
490
+ }
491
+ padX={stageDecor ? padX : undefined}
492
+ padY={stageDecor ? padY : undefined}
493
+ // Crossfade: fade out on navigation, back in once the new
494
+ // exhibit has swapped in (behind the fade) and been measured.
495
+ style={{
496
+ opacity: stageShown ? 1 : 0,
497
+ transition: `opacity ${STAGE_FADE_MS}ms ease`,
498
+ }}>
499
+ <div
500
+ className="dc-frame-box"
501
+ style={
502
+ fillFrame
503
+ ? { width: '100%', height: '100%' }
504
+ : { width: `${boxW}px`, height: `${boxH}px` }
505
+ }>
506
+ {renderFrame}
507
+ </div>
508
+ </Stage>
509
+ </div>
510
+
511
+ {activeCase.tweaks && (
512
+ <div style={fade}>
513
+ <TweaksPanel
514
+ mode={tweaksFloating ? 'floating' : 'docked'}
515
+ // Surface the live shareable address (the same `/c/…?t.*` the
516
+ // browser is on) so the tweaked state is one copy away.
517
+ url={buildUrl(
518
+ component.id,
519
+ activeCase.id,
520
+ sel?.tweaks ?? {},
521
+ docOpen,
522
+ )}
523
+ onToggleMode={() => setTweaksFloating((f) => !f)}
524
+ items={Object.entries(activeCase.tweaks).map(([key, desc]) => ({
525
+ label: key,
526
+ control: (
527
+ <TweakControl
528
+ name={key}
529
+ desc={desc}
530
+ current={sel?.tweaks?.[key] ?? String(desc.default)}
531
+ onChange={(v) =>
532
+ sel &&
533
+ select({
534
+ ...sel,
535
+ tweaks: { ...(sel.tweaks ?? {}), [key]: v },
536
+ })
537
+ }
538
+ />
539
+ ),
540
+ }))}
541
+ />
542
+ </div>
543
+ )}
544
+
545
+ {/* The audit verdict for what's on the stage: the live variant's WCAG
546
+ violations, read in place rather than buried in a CLI log. Sits below
547
+ the Tweaks panel — it's the consequence of the tweaked state (cause →
548
+ effect), and a read-only report tolerates being pushed around by the
549
+ interactive panel above better than the reverse. Mounted only when
550
+ a11y scanning is configured (`a11y` present); otherwise there is no
551
+ panel at all. */}
552
+ {props.a11y && (
553
+ <div style={fade}>
554
+ <A11yPanel
555
+ violations={props.a11y.current}
556
+ reveal={props.a11y.reveal}
557
+ onRescan={props.rescanA11y}
558
+ // Snap the verdict colour for the whole navigation crossfade so it
559
+ // returns in the right colour instead of easing the previous
560
+ // exhibit's colour across the fade (in-place changes still ease).
561
+ instantColor={props.colorSnap}
562
+ />
563
+ </div>
564
+ )}
565
+ </div>
566
+
567
+ {component.placardDoc && docOpen && (
568
+ <aside
569
+ className="dc-doc-panel"
570
+ data-testid={DcTestIds.docPanel}
571
+ aria-label="Documentation"
572
+ style={{ '--dc-doc-w': `${docWidth}px` } as CSSProperties}>
573
+ {/* biome-ignore lint/a11y/useSemanticElements: a draggable splitter, not a thematic break */}
574
+ <div
575
+ className="dc-doc-resize"
576
+ role="separator"
577
+ aria-orientation="vertical"
578
+ aria-label="Resize documentation panel"
579
+ aria-valuenow={docWidth}
580
+ aria-valuemin={DOC_MIN_W}
581
+ aria-valuemax={DOC_MAX_W}
582
+ tabIndex={0}
583
+ onPointerDown={startDocResize}
584
+ onKeyDown={onDocResizeKey}
585
+ />
586
+ <div className="dc-doc-scroll">
587
+ <div className="dc-doc-head">
588
+ <Eyebrow>Documentation</Eyebrow>
589
+ <IconButton
590
+ glyph="✕"
591
+ variant="bare"
592
+ label="Close documentation"
593
+ onClick={() => changeDocsOpen(false)}
594
+ />
595
+ </div>
596
+ {docText ? <DocMarkdown source={docText} /> : <p>Loading…</p>}
597
+ </div>
598
+ </aside>
599
+ )}
600
+ </div>
601
+ )
602
+ }
603
+
604
+ // Segmented control at the top of the sidebar: switch between the Primer
605
+ // (reading page) and the Cases library. Shown only when a Primer is configured.
606
+ // Just the two-option binding of the generic SegmentedToggle; `dc-modeswitch`
607
+ // supplies the sidebar-pinning layout (see chrome.css).
608
+ const MODE_SEGMENTS: SegmentedOption<Mode>[] = [
609
+ { id: 'primer', label: 'Primer' },
610
+ { id: 'library', label: 'Cases' },
611
+ ]
612
+
613
+ function ModeSwitch({
614
+ mode,
615
+ onMode,
616
+ }: {
617
+ mode: Mode
618
+ onMode: (m: Mode) => void
619
+ }) {
620
+ return (
621
+ <SegmentedToggle
622
+ className="dc-modeswitch"
623
+ label="View mode"
624
+ options={MODE_SEGMENTS}
625
+ value={mode}
626
+ onChange={onMode}
627
+ testId={DcTestIds.modeSwitch}
628
+ />
629
+ )
630
+ }
631
+
632
+ // Renders one tweak control using the design-system primitives. The TweaksPanel
633
+ // row supplies the label, so the control just needs an accessible name.
634
+ function TweakControl({
635
+ name,
636
+ desc,
637
+ current,
638
+ onChange,
639
+ }: {
640
+ name: string
641
+ desc: TweakDescriptor
642
+ current: string
643
+ onChange: (value: string) => void
644
+ }) {
645
+ if (desc.kind === 'boolean') {
646
+ return (
647
+ <input
648
+ type="checkbox"
649
+ aria-label={name}
650
+ checked={current === '1' || current === 'true'}
651
+ onChange={(e) => onChange(e.target.checked ? '1' : '0')}
652
+ />
653
+ )
654
+ }
655
+ if (desc.kind === 'choice') {
656
+ // An accessible custom listbox (not a native <select>) so the picked value
657
+ // commits instantly — a native select on macOS defers its change event until
658
+ // the OS popup dismisses, which lagged the live stage update.
659
+ return (
660
+ <SelectMenu
661
+ size="sm"
662
+ aria-label={name}
663
+ options={desc.options}
664
+ value={current}
665
+ onChange={onChange}
666
+ />
667
+ )
668
+ }
669
+ return (
670
+ <Input
671
+ size="sm"
672
+ aria-label={name}
673
+ type={desc.kind === 'number' ? 'number' : 'text'}
674
+ value={current}
675
+ onChange={(e) => onChange(e.target.value)}
676
+ />
677
+ )
678
+ }