@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,37 @@
1
+ /**
2
+ * Type-level test for the style-engine seam. Compiled by `tsc` (the `lint:types`
3
+ * check) but never executed — `const x: T =` assertions pin the public shapes,
4
+ * and `@ts-expect-error` asserts a compile error. A regression of the types
5
+ * fails the typecheck.
6
+ */
7
+ import type { DisplayCaseConfig, StyleCollector, StyleEngine } from './index'
8
+
9
+ // A StyleEngine is a zero-arg factory returning a StyleCollector.
10
+ const engine: StyleEngine = () => ({
11
+ wrap: (node) => node,
12
+ collect: (html) => `<style>${html.length}</style>`,
13
+ })
14
+
15
+ // A collector's methods have the expected signatures.
16
+ const collector: StyleCollector = engine()
17
+ const _wrapped = collector.wrap(null)
18
+ const _css: string = collector.collect('<div/>')
19
+ void _wrapped
20
+ void _css
21
+
22
+ // `styleEngines` is an optional array of engines on the config.
23
+ const config: DisplayCaseConfig = {
24
+ title: 'T',
25
+ roots: [],
26
+ styleEngines: [engine],
27
+ }
28
+ void config
29
+
30
+ // collect must return a string — returning a non-string is a type error.
31
+ // @ts-expect-error — collect returns void, not string
32
+ const _bad: StyleCollector = { wrap: (n) => n, collect: () => {} }
33
+ void _bad
34
+
35
+ // @ts-expect-error — a StyleEngine takes no arguments
36
+ const _badEngine: StyleEngine = (_x: number) => engine()
37
+ void _badEngine
@@ -0,0 +1,27 @@
1
+ import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'
2
+ import { tmpdir } from 'node:os'
3
+ import { dirname, join } from 'node:path'
4
+
5
+ /**
6
+ * Shared scaffolding for the unit suites: build a throwaway package directory on
7
+ * disk, populate it from a `{ relativePath: contents }` map, and clean it up.
8
+ * Filesystem-backed because the modules under test (config resolution, case
9
+ * discovery, the token scanner, the init scaffolder) all read real files.
10
+ */
11
+
12
+ /** Make a unique temp directory; the caller is responsible for removing it. */
13
+ export async function makeTempDir(): Promise<string> {
14
+ return mkdtemp(join(tmpdir(), 'dc-test-'))
15
+ }
16
+
17
+ /** Write a map of package-relative paths → file contents, creating parents. */
18
+ export async function writeFiles(
19
+ dir: string,
20
+ files: Record<string, string>,
21
+ ): Promise<void> {
22
+ for (const [rel, content] of Object.entries(files)) {
23
+ const abs = join(dir, rel)
24
+ await mkdir(dirname(abs), { recursive: true })
25
+ await writeFile(abs, content)
26
+ }
27
+ }
@@ -0,0 +1,12 @@
1
+ // pixelmatch v6 ships no bundled type declarations; this is the minimal surface
2
+ // Display Case uses.
3
+ declare module 'pixelmatch' {
4
+ export default function pixelmatch(
5
+ img1: Uint8Array | Uint8ClampedArray,
6
+ img2: Uint8Array | Uint8ClampedArray,
7
+ output: Uint8Array | Uint8ClampedArray | null,
8
+ width: number,
9
+ height: number,
10
+ options?: { threshold?: number; includeAA?: boolean },
11
+ ): number
12
+ }
@@ -0,0 +1,51 @@
1
+ import { StrictMode } from 'react'
2
+ import { createRoot, hydrateRoot } from 'react-dom/client'
3
+ import { Shell } from './shell'
4
+ import { parseRoute } from './shell-core'
5
+ import type { ShellSeed } from './use-shell'
6
+
7
+ // Entry for the browse chrome. The server pre-renders the shell into #root and
8
+ // inlines the seed it rendered from (`window.__dcSeed`: the manifest, theme,
9
+ // and a11y flag). The route comes from the live address — which equals the
10
+ // request path the server rendered — so server and client derive the same route
11
+ // and the seeded initial state matches. The client then adopts (hydrates) the
12
+ // markup; case modules are still bundled only into the render entry, never here.
13
+ const inlined = (
14
+ globalThis as {
15
+ __dcSeed?: {
16
+ manifest: ShellSeed['manifest']
17
+ theme: ShellSeed['theme']
18
+ a11y: boolean
19
+ }
20
+ }
21
+ ).__dcSeed
22
+
23
+ const rootEl = document.getElementById('root') as HTMLElement
24
+ if (!inlined) throw new Error('Display Case: missing shell seed (__dcSeed)')
25
+
26
+ const seed: ShellSeed = {
27
+ manifest: inlined.manifest,
28
+ route: parseRoute(window.location.pathname, window.location.search),
29
+ theme: inlined.theme,
30
+ a11y: inlined.a11y,
31
+ }
32
+
33
+ const tree = (
34
+ <StrictMode>
35
+ <Shell seed={seed} />
36
+ </StrictMode>
37
+ )
38
+
39
+ // Adopt the server-rendered shell when present (`data-ssr="1"`); mount fresh
40
+ // otherwise.
41
+ if (rootEl.dataset.ssr === '1') {
42
+ hydrateRoot(rootEl, tree, {
43
+ onRecoverableError: (err) =>
44
+ console.warn(
45
+ '[display-case] shell adopt mismatch; client re-rendered:',
46
+ err,
47
+ ),
48
+ })
49
+ } else {
50
+ createRoot(rootEl).render(tree)
51
+ }
@@ -0,0 +1,485 @@
1
+ /* Display Case browsing chrome — "The Vitrine".
2
+ Styled entirely from the package's own design system (see
3
+ ./design-system/). Every value is a `--dc-*` token; nothing here
4
+ hard-codes a color, radius, or font. The chrome is warm, flat, and
5
+ recessive so the showcased component — the exhibit — owns the weight.
6
+ The server inlines the design-system tokens ahead of this file. */
7
+
8
+ .dc-app {
9
+ display: grid;
10
+ grid-template-columns: var(--dc-sidebar-w) 1fr;
11
+ grid-template-rows: auto 1fr;
12
+ grid-template-areas: "header header" "sidebar main";
13
+ height: 100vh;
14
+ font-family: var(--dc-font-sans);
15
+ font-size: var(--dc-text-base);
16
+ line-height: var(--dc-leading-normal);
17
+ color: var(--dc-fg);
18
+ background: var(--dc-bg);
19
+ -webkit-font-smoothing: antialiased;
20
+ }
21
+
22
+ /* Collapsed nav: drop the sidebar column and let main span full width.
23
+ (The `.dcui-sidebar` display:none override lives just after the base
24
+ `.dcui-sidebar` rule below, to keep selector specificity ascending.) */
25
+ .dc-app[data-nav="collapsed"] {
26
+ grid-template-columns: 1fr;
27
+ grid-template-areas: "header" "main";
28
+ }
29
+
30
+ /* ── Header ─────────────────────────────────────────────────────── */
31
+ .dc-header {
32
+ grid-area: header;
33
+ display: flex;
34
+ align-items: center;
35
+ justify-content: space-between;
36
+ gap: var(--dc-space-8);
37
+ padding: var(--dc-space-5) var(--dc-space-8);
38
+ border-bottom: var(--dc-border-line);
39
+ background: var(--dc-bg-subtle);
40
+ }
41
+ .dc-header-left {
42
+ display: flex;
43
+ align-items: center;
44
+ gap: var(--dc-space-5);
45
+ }
46
+ /* The bracketed wordmark is the Wordmark component (owns its own dcui-wordmark
47
+ styles); the chrome only places it in the header-left cluster. */
48
+ .dc-controls {
49
+ display: flex;
50
+ align-items: center;
51
+ gap: var(--dc-space-4);
52
+ flex-wrap: wrap;
53
+ justify-content: flex-end;
54
+ }
55
+ /* The library-only controls live in a fading group (so they crossfade with the
56
+ mode switch) but must still read as the same inline cluster — same axis, gap,
57
+ and wrapping as their `.dc-controls` parent. */
58
+ .dc-controls-extra {
59
+ display: flex;
60
+ align-items: center;
61
+ gap: var(--dc-space-4);
62
+ flex-wrap: wrap;
63
+ justify-content: flex-end;
64
+ }
65
+ /* Size the screen-size <select> to its current value, not its widest option
66
+ (without this a native select reserves room for "iPhone Pro Max" even when
67
+ "Full" is showing). Dropdown still lists full labels. */
68
+ .dc-controls .dcui-select-el {
69
+ field-sizing: content;
70
+ }
71
+
72
+ /* Group related inputs into a single bordered cluster — never float a
73
+ lone control. The W×H + rotate fields live in one box. */
74
+ .dc-dims {
75
+ display: flex;
76
+ align-items: center;
77
+ gap: var(--dc-space-1);
78
+ height: 26px;
79
+ border: var(--dc-border-line);
80
+ border-radius: var(--dc-radius-sm);
81
+ background: var(--dc-surface);
82
+ /* No right padding: the bare rotate button sits flush to the box edge. */
83
+ padding: 0 0 0 var(--dc-space-2);
84
+ }
85
+ .dc-dim {
86
+ /* Room for 4 digits (max sensible pixel dimension) plus a hair of breathing
87
+ space — `field-sizing: content` isn't supported everywhere, and the fallback
88
+ is the browser's very wide default <input> width. */
89
+ width: 4.5ch;
90
+ border: 0;
91
+ background: none;
92
+ padding: 0;
93
+ font: inherit;
94
+ font-size: var(--dc-text-sm);
95
+ font-variant-numeric: tabular-nums;
96
+ text-align: center;
97
+ color: var(--dc-fg);
98
+ /* Flat design: drop the native number spinners (they add width and chrome). */
99
+ appearance: textfield;
100
+ -moz-appearance: textfield;
101
+ }
102
+ .dc-dim::-webkit-outer-spin-button,
103
+ .dc-dim::-webkit-inner-spin-button {
104
+ -webkit-appearance: none;
105
+ margin: 0;
106
+ }
107
+ .dc-dim::placeholder {
108
+ color: var(--dc-fg-subtle);
109
+ }
110
+ .dc-dim:focus,
111
+ .dc-dim:focus-visible {
112
+ outline: none;
113
+ }
114
+ .dc-dim:disabled {
115
+ opacity: 0.5;
116
+ }
117
+ .dc-dim-x {
118
+ color: var(--dc-fg-subtle);
119
+ font-family: var(--dc-font-mono);
120
+ font-size: var(--dc-text-sm);
121
+ }
122
+
123
+ /* ── Sidebar nav ────────────────────────────────────────────────── */
124
+ /* The Sidebar component owns the surface (border, backdrop, padding); the chrome
125
+ re-tasks it as a non-scrolling column so the mode switch can pin to the top and
126
+ the scroll/fade live on an inner region — keeping the rail's border crisp (a
127
+ mask on the scroll region can't touch a border that isn't on it). */
128
+ .dcui-sidebar {
129
+ grid-area: sidebar;
130
+ }
131
+ .dc-app .dcui-sidebar {
132
+ display: flex;
133
+ flex-direction: column;
134
+ overflow: hidden;
135
+ }
136
+ .dc-app[data-nav="collapsed"] .dcui-sidebar {
137
+ display: none;
138
+ }
139
+ /* The scroll region: fills the rail below the pinned switch and carries the
140
+ content's scroll. Its native scrollbar is hidden (it can't fade, so on a
141
+ crossfade it would pop in as a step change; only the long Cases nav overflows,
142
+ so a reserved track would sit empty in the short Primer view) — replaced by a
143
+ soft gradient fade at whichever edge has more content off-screen. `data-fade-*`
144
+ are toggled from the scroll position (see shell.tsx); the mask is fixed to this
145
+ region's edges, so it tracks the viewport, not the scrolled content, and no
146
+ edge fades when it's fully docked. */
147
+ .dc-nav-scroll {
148
+ --dc-nav-fade: var(--dc-space-12);
149
+ flex: 1 1 0;
150
+ min-height: 0;
151
+ overflow-y: auto;
152
+ scrollbar-width: none; /* Firefox */
153
+ }
154
+ .dc-nav-scroll::-webkit-scrollbar {
155
+ display: none; /* WebKit/Blink */
156
+ }
157
+ .dc-nav-scroll[data-fade-top="true"][data-fade-bottom="true"] {
158
+ -webkit-mask-image: linear-gradient(
159
+ to bottom,
160
+ transparent 0,
161
+ #000 var(--dc-nav-fade),
162
+ #000 calc(100% - var(--dc-nav-fade)),
163
+ transparent 100%
164
+ );
165
+ mask-image: linear-gradient(
166
+ to bottom,
167
+ transparent 0,
168
+ #000 var(--dc-nav-fade),
169
+ #000 calc(100% - var(--dc-nav-fade)),
170
+ transparent 100%
171
+ );
172
+ }
173
+ .dc-nav-scroll[data-fade-top="true"]:not([data-fade-bottom="true"]) {
174
+ -webkit-mask-image: linear-gradient(
175
+ to bottom,
176
+ transparent 0,
177
+ #000 var(--dc-nav-fade)
178
+ );
179
+ mask-image: linear-gradient(
180
+ to bottom,
181
+ transparent 0,
182
+ #000 var(--dc-nav-fade)
183
+ );
184
+ }
185
+ .dc-nav-scroll:not([data-fade-top="true"])[data-fade-bottom="true"] {
186
+ -webkit-mask-image: linear-gradient(
187
+ to bottom,
188
+ #000 calc(100% - var(--dc-nav-fade)),
189
+ transparent 100%
190
+ );
191
+ mask-image: linear-gradient(
192
+ to bottom,
193
+ #000 calc(100% - var(--dc-nav-fade)),
194
+ transparent 100%
195
+ );
196
+ }
197
+ .dc-group {
198
+ margin-bottom: var(--dc-space-8);
199
+ }
200
+ /* Primer TOC groups are far more numerous than the component-tree levels, so
201
+ they sit closer together — enough to read the `##` heading as a group break,
202
+ not so much it fragments the nav. */
203
+ .dc-primer-group {
204
+ margin-bottom: var(--dc-space-3);
205
+ }
206
+ /* Eyebrow styling is the Eyebrow component's; this only positions the label. */
207
+ .dc-group-label {
208
+ margin: 0 0 var(--dc-space-3) var(--dc-space-3);
209
+ }
210
+ /* Nav rows are the NavItem component; this only spaces the groups. */
211
+ .dc-nav-component {
212
+ margin-bottom: var(--dc-space-1);
213
+ }
214
+
215
+ /* ── Main + stage ───────────────────────────────────────────────── */
216
+ .dc-main {
217
+ grid-area: main;
218
+ overflow-y: auto;
219
+ padding: var(--dc-space-10);
220
+ display: flex;
221
+ flex-direction: column;
222
+ min-height: 0;
223
+ }
224
+
225
+ /* The stage lays the content column beside the (optional) doc panel, filling
226
+ the main area's height so the preview can expand fully. */
227
+ .dc-stage {
228
+ display: flex;
229
+ align-items: stretch;
230
+ gap: var(--dc-space-8);
231
+ /* basis 0 (not auto) so height is a share of the bounded grid cell, never
232
+ content-driven — otherwise the measured preview height feeds back. */
233
+ flex: 1 1 0;
234
+ min-height: 0;
235
+ }
236
+ .dc-content {
237
+ flex: 1;
238
+ min-width: 0;
239
+ /* min-height: 0 lets the column shrink below its content; overflow-y keeps the
240
+ Tweaks + Accessibility panels reachable when the stage's min-height plus the
241
+ panels exceed the available height (e.g. a short viewport, or the a11y panel
242
+ loading its violations) instead of pushing them off the bottom edge. */
243
+ min-height: 0;
244
+ overflow-y: auto;
245
+ display: flex;
246
+ flex-direction: column;
247
+ gap: var(--dc-space-8);
248
+ }
249
+
250
+ .dc-doc-panel {
251
+ flex: 0 0 var(--dc-doc-w, var(--dc-doc-panel-w));
252
+ align-self: stretch;
253
+ border: var(--dc-border-line);
254
+ border-radius: var(--dc-radius-md);
255
+ background: var(--dc-surface);
256
+ position: sticky;
257
+ top: 0;
258
+ /* Positioning context for the edge handle; clips the (scrolling) content to
259
+ the rounded corners. The inner wrapper does the scrolling so the handle
260
+ stays pinned to the left edge. */
261
+ overflow: hidden;
262
+ }
263
+ .dc-doc-scroll {
264
+ height: 100%;
265
+ overflow-y: auto;
266
+ padding: var(--dc-space-6) var(--dc-space-8);
267
+ }
268
+ /* The resize grip IS the panel's left edge: a full-height strip pinned to the
269
+ left that brightens to marigold on hover; drag (or arrow keys) to resize. */
270
+ .dc-doc-resize {
271
+ position: absolute;
272
+ left: 0;
273
+ top: 0;
274
+ bottom: 0;
275
+ width: 10px;
276
+ z-index: 1;
277
+ cursor: col-resize;
278
+ touch-action: none;
279
+ }
280
+ .dc-doc-resize::after {
281
+ content: "";
282
+ position: absolute;
283
+ left: 0;
284
+ top: 0;
285
+ bottom: 0;
286
+ width: 1px;
287
+ background: transparent;
288
+ transition: background var(--dc-transition-fast);
289
+ }
290
+ .dc-doc-resize:hover::after,
291
+ .dc-doc-resize:focus-visible::after,
292
+ .dc-doc-resize:active::after {
293
+ background: var(--dc-brand);
294
+ }
295
+ .dc-doc-resize:focus-visible {
296
+ outline: none;
297
+ }
298
+ .dc-doc-head {
299
+ display: flex;
300
+ align-items: center;
301
+ justify-content: space-between;
302
+ gap: var(--dc-space-4);
303
+ margin-bottom: var(--dc-space-4);
304
+ }
305
+
306
+ @media (max-width: 900px) {
307
+ .dc-stage {
308
+ flex-wrap: wrap;
309
+ }
310
+ .dc-doc-panel {
311
+ flex-basis: 100%;
312
+ position: static;
313
+ }
314
+ /* Stacked layout: the horizontal drag handle is meaningless. */
315
+ .dc-doc-resize {
316
+ display: none;
317
+ }
318
+ }
319
+
320
+ /* The Accessibility panel, its severity tags, and the nav-rail a11y markers are
321
+ self-contained components that inject their own styles (showcase/A11yPanel,
322
+ showcase/ImpactTag, showcase/A11yBadge). */
323
+
324
+ /* The preview is the stable centering viewport that fills the content column.
325
+ It measures the available area (for responsive sizing) and centers the stage
326
+ frame inside it, scrolling to every edge when the (possibly scaled or zoomed)
327
+ frame is larger than the panel. `safe center` keeps the start reachable
328
+ instead of clipping it on overflow. */
329
+ .dc-preview {
330
+ flex: 1 1 0;
331
+ min-height: 12rem;
332
+ display: flex;
333
+ justify-content: safe center;
334
+ align-items: safe center;
335
+ overflow: auto;
336
+ }
337
+
338
+ /* The vitrine frame (border, rounded surface, grid + corner ticks, hug/fill
339
+ sizing) is the Stage component (dcui-stage); see
340
+ ./design-system/components/showcase/Stage.tsx. The shell only positions it
341
+ (centered in `.dc-preview`) and supplies the scaled frame-box below. */
342
+
343
+ /* Occupies the *scaled* footprint so centering + scrolling are correct; the
344
+ iframe is scaled from its top-left corner within. */
345
+ .dc-frame-box {
346
+ position: relative;
347
+ flex: 0 0 auto;
348
+ /* Clips the iframe to the visible (content) height: in Responsive mode the
349
+ iframe stays panel-tall so the component's viewport is stable, and the box
350
+ reveals only its measured height — the rest is grid, not blank surface. */
351
+ overflow: hidden;
352
+ }
353
+ .dc-frame {
354
+ position: absolute;
355
+ top: 0;
356
+ left: 0;
357
+ border: 0;
358
+ /* Transparent so the stage's surface + grid show through wherever the render
359
+ document is itself transparent (decorated components). Pages/flows paint
360
+ their own opaque body background over this. */
361
+ background: transparent;
362
+ }
363
+
364
+ /* Flow nav, the tweaks panel, and the button / icon-button / select / input
365
+ controls are now the design-system components (dcui-*); see
366
+ ./design-system/components/. Only shell *layout* lives in this file. */
367
+
368
+ /* ── Zoom cluster ───────────────────────────────────────────────── */
369
+ .dc-zoom {
370
+ display: flex;
371
+ align-items: center;
372
+ gap: var(--dc-space-1);
373
+ height: 26px;
374
+ border: var(--dc-border-line);
375
+ border-radius: var(--dc-radius-sm);
376
+ background: var(--dc-surface);
377
+ }
378
+ .dc-zoom-level {
379
+ font: inherit;
380
+ font-family: var(--dc-font-mono);
381
+ font-size: var(--dc-text-sm);
382
+ font-variant-numeric: tabular-nums;
383
+ min-width: 2.75rem;
384
+ text-align: center;
385
+ border: 0;
386
+ background: none;
387
+ color: var(--dc-fg);
388
+ cursor: pointer;
389
+ padding: var(--dc-space-2);
390
+ border-radius: var(--dc-radius-sm);
391
+ }
392
+ .dc-zoom-level:hover {
393
+ background: var(--dc-hover);
394
+ }
395
+ .dc-zoom-fit {
396
+ cursor: default;
397
+ color: var(--dc-fg-muted);
398
+ }
399
+ .dc-zoom-fit:hover {
400
+ background: none;
401
+ }
402
+
403
+ /* ── Documentation markdown ─────────────────────────────────────── */
404
+ .dc-doc-md {
405
+ margin-top: var(--dc-space-6);
406
+ line-height: var(--dc-leading-relaxed);
407
+ }
408
+ .dc-doc-md pre {
409
+ background: var(--dc-bg-subtle);
410
+ border: var(--dc-border-line);
411
+ padding: var(--dc-space-6);
412
+ border-radius: var(--dc-radius-sm);
413
+ overflow-x: auto;
414
+ }
415
+ .dc-doc-md code {
416
+ font-family: var(--dc-font-mono);
417
+ font-size: 0.85em;
418
+ }
419
+ .dc-doc-md table {
420
+ border-collapse: collapse;
421
+ }
422
+ .dc-doc-md th,
423
+ .dc-doc-md td {
424
+ border: var(--dc-border-line);
425
+ padding: var(--dc-space-2) var(--dc-space-4);
426
+ }
427
+
428
+ /* ── Empty / loading / error states ─────────────────────────────── */
429
+ .dc-loading,
430
+ .dc-empty {
431
+ display: flex;
432
+ flex-direction: column;
433
+ align-items: center;
434
+ justify-content: center;
435
+ height: 100vh;
436
+ color: var(--dc-fg-muted);
437
+ background: var(--dc-bg);
438
+ gap: var(--dc-space-2);
439
+ }
440
+ .dc-empty-hint {
441
+ font-size: var(--dc-text-sm);
442
+ color: var(--dc-fg-subtle);
443
+ }
444
+ .dc-empty code,
445
+ .dc-empty-hint code {
446
+ font-family: var(--dc-font-mono);
447
+ font-size: 0.9em;
448
+ color: var(--dc-fg-muted);
449
+ }
450
+ .dc-render-missing {
451
+ padding: var(--dc-space-16);
452
+ font-family: var(--dc-font-sans);
453
+ color: var(--dc-danger);
454
+ }
455
+
456
+ /* ── Primer ────────────────────────────────────────────────────── */
457
+ /* Mode switch placement: the SegmentedToggle (primer vs cases) pinned above the
458
+ sidebar scroll region — never shrinks, so nav items scroll and fade beneath
459
+ it. The control's own appearance lives with the SegmentedToggle component. */
460
+ .dc-modeswitch {
461
+ flex: 0 0 auto;
462
+ margin-bottom: var(--dc-space-8);
463
+ }
464
+
465
+ /* The Primer host shares the `main` grid area with the library stage; the
466
+ inactive one is `hidden`. The reading page owns its own scroll inside the
467
+ isolated iframe, so the host is just an unpadded, edge-to-edge frame. */
468
+ .dc-primer-host {
469
+ grid-area: main;
470
+ display: flex;
471
+ min-height: 0;
472
+ }
473
+ .dc-primer-frame {
474
+ flex: 1;
475
+ width: 100%;
476
+ border: 0;
477
+ }
478
+ /* `.dc-main`/`.dc-primer-host` set `display`, which outranks the UA `[hidden]`
479
+ rule — so the inactive view would still paint (and bleed through). Re-assert
480
+ the hide explicitly. The library stays in the DOM (its render iframe keeps its
481
+ handshake); it's just not displayed. */
482
+ .dc-main[hidden],
483
+ .dc-primer-host[hidden] {
484
+ display: none;
485
+ }
@@ -0,0 +1,88 @@
1
+ # Display Case Design System — "The Vitrine"
2
+
3
+ The self-contained visual identity for Display Case. It lives **inside** the
4
+ package so Display Case dogfoods it: the browse chrome (`../chrome.css`) is
5
+ styled entirely from these `--dc-*` tokens — no host-app library, no
6
+ ui tokens.
7
+
8
+ ## Identity in one breath
9
+
10
+ Warm paper neutrals, a warm-ink primary, and a single **marigold** accent.
11
+ Flat and border-led so the showcased component is the exhibit. Has charm
12
+ (bracketed wordmark, stage corner ticks) but never decorates.
13
+
14
+ - **Type** — Hanken Grotesk (UI) + JetBrains Mono (labels / values / code /
15
+ wordmark). Dense 14px base. Eyebrow labels are UPPERCASE mono, `0.08em`.
16
+ - **Icons** — Unicode glyphs only. No icon font, no SVG, no emoji.
17
+ - **Themes** — light + dark via `data-theme` on any scope. Dark is warm
18
+ charcoal, never pure black.
19
+
20
+ ## Files
21
+
22
+ | Path | What |
23
+ |---|---|
24
+ | `styles.css` | Entry point. `@import`s the four token files below. |
25
+ | `tokens/fonts.css` | Webfont declaration (Hanken Grotesk + JetBrains Mono). |
26
+ | `tokens/colors.css` | Paper ramp, marigold ramp, status hues, light + dark aliases. |
27
+ | `tokens/typography.css` | Families, size scale, weights, tracking, the eyebrow role token. |
28
+ | `tokens/spacing.css` | Spacing scale, radii, borders, elevation, motion. |
29
+
30
+ ## Components
31
+
32
+ `components/` holds the library as **pure React components** — each keeps its
33
+ `dcui-*` CSS in a **co-located `.css` file** (`Button.tsx` → `Button.css`) and
34
+ consumes only the `--dc-*` tokens. The server reads every component `.css`, the
35
+ shell `chrome.css`, and the primer `primer.css`, concatenates them into one
36
+ **Vitrine stylesheet**, and inlines it into every document `<style>` before
37
+ scripts run (`readVitrineCss` in `../../server/server.ts`). There is **no
38
+ runtime style injection** — the components no longer touch `document` to paint.
39
+
40
+ | Group | Components |
41
+ |---|---|
42
+ | `components/controls/` | **Button**, **IconButton**, **Input**, **Select** |
43
+ | `components/showcase/` | **Eyebrow**, **Chip**, **NavItem**, **Sidebar**, **Stage**, **FlowNav**, **TweaksPanel**, **RenderAddress**, **Wordmark** |
44
+ | `components/primer-specimen/` | **ColorRamp**, **SwatchGrid**, **StatusList**, **GlyphGrid**, **DefinitionList**, **LayoutMock**, **TypeScale**, **FontFamilies**, **WeightSpecimen**, **SpacingScale**, **SpecimenBoxRow** |
45
+
46
+ The `components/primer-specimen/` group is **generic, prop-driven foundation
47
+ specimens** for building a Primer — feed them your own ramps, swatches, type
48
+ scale, spacing steps, and glyphs. Display Case's own Primer (the
49
+ `primer-specimens/` wrappers) is the worked example: each wrapper supplies
50
+ Display-Case-specific data to one of these primitives.
51
+
52
+ The browse chrome (`../shell.tsx`) is built from these, and Display Case
53
+ **dogfoods them as its own showcased components**: each has a colocated
54
+ `*.case.tsx` + `*.placard.md`, surfaced by the repo-root `display-case.config.ts`.
55
+ Browse them with:
56
+
57
+ ```bash
58
+ bun run display-case # or: bun src/cli.ts .
59
+ ```
60
+
61
+ `chrome.css` keeps only shell *layout* (grid regions, the stage/preview sizing,
62
+ the doc panel); the component-level styling lives with the components.
63
+
64
+ ### The chrome as a pure component (`components/shell/`)
65
+
66
+ The browse chrome is split so it is itself a pure, exhibitable component:
67
+ `ui/use-shell.ts` (the `useShell()` state machine) → `components/shell/ShellView.tsx`
68
+ (a pure function of the `ShellViewModel`, with the live iframes passed in as
69
+ `renderFrame` / `primerFrame` slots) → `ui/shell.tsx` (a thin container). That
70
+ lets Display Case **dogfood its own layout**: `components/shell/` adds `template`
71
+ cases (Case/Primer templates, placeholder slots), `page` cases (Button,
72
+ RenderAddress, Sidebar, Primer, Case-template pages — real content slotted in),
73
+ and a `flow` (`ShellView.case.tsx`, Primer → Cases). They share
74
+ `shell-fixtures.tsx` (a `// display-case: no-case` helper). `ShellView` paints
75
+ inside the isolated `/render` doc because the server inlines the whole Vitrine
76
+ stylesheet — `chrome.css` included — into *every* document (the shell pages use
77
+ its `.dc-*` layout). See `../../../contributing/NOTES.md` for the gotchas
78
+ (render-doc CSS, snapshot determinism, primer skeleton widths).
79
+
80
+ ## How the chrome consumes the tokens
81
+
82
+ `startDisplayCase` (in `../../server/server.ts`) inlines the token CSS into the
83
+ browse-shell `<style>` ahead of `chrome.css`, and injects the webfont
84
+ `<link>`s into the document head. Everything references `var(--dc-*)`; nothing
85
+ hard-codes a color, radius, or font.
86
+
87
+ Source of truth: the **Display Case Design System** project on
88
+ claude.ai/design, kept in sync via `/design-sync`.