@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,48 @@
1
+ /* ============================================================
2
+ Display Case — Spacing, radius, border & elevation tokens
3
+ Dense, rem-based spacing keeps the chrome compact so the
4
+ exhibit gets the room. Corners are modest (5px controls,
5
+ 8px panels). Elevation is mostly border-led — the one place
6
+ we lift is floating overlays (menus, popovers).
7
+ ============================================================ */
8
+
9
+ :root {
10
+ /* --- Spacing scale (rem) ----------------------------------- */
11
+ --dc-space-0: 0;
12
+ --dc-space-1: 0.125rem; /* 2px — icon gaps */
13
+ --dc-space-2: 0.25rem; /* 4px — control padding-y */
14
+ --dc-space-3: 0.375rem; /* 6px — nav-case padding */
15
+ --dc-space-4: 0.5rem; /* 8px — control padding-x, gaps */
16
+ --dc-space-5: 0.625rem; /* 10px — header padding-y */
17
+ --dc-space-6: 0.75rem; /* 12px — sidebar / panel padding */
18
+ --dc-space-8: 1rem; /* 16px — header padding-x, stage gap*/
19
+ --dc-space-10: 1.25rem; /* 20px — main padding */
20
+ --dc-space-12: 1.5rem; /* 24px */
21
+ --dc-space-16: 2rem; /* 32px — empty-state inset */
22
+
23
+ /* --- Layout sizes ------------------------------------------ */
24
+ --dc-sidebar-w: 15rem; /* fixed nav column */
25
+ --dc-doc-panel-w: 22rem; /* docs aside */
26
+
27
+ /* --- Radius (modest, precise) ------------------------------ */
28
+ --dc-radius-sm: 5px; /* buttons, inputs, chips, nav rows */
29
+ --dc-radius-md: 8px; /* panels, stage frame, doc panel */
30
+ --dc-radius-lg: 12px; /* overlays / large callouts */
31
+ --dc-radius-full: 999px;
32
+
33
+ /* --- Borders ----------------------------------------------- */
34
+ --dc-border-width: 1px;
35
+ --dc-border-line: var(--dc-border-width) solid var(--dc-border);
36
+
37
+ /* --- Elevation (rare — borders do most of the work) -------- */
38
+ --dc-shadow-none: none;
39
+ --dc-shadow-sm: 0 1px 2px rgba(33, 29, 24, 0.05);
40
+ --dc-shadow-overlay: 0 12px 32px -8px rgba(33, 29, 24, 0.22);
41
+
42
+ /* --- Motion ------------------------------------------------ */
43
+ --dc-ease: cubic-bezier(0.2, 0.8, 0.2, 1);
44
+ --dc-duration-fast: 0.12s; /* chevron rotate, hover fills */
45
+ --dc-duration-base: 0.2s;
46
+ --dc-transition-fast: var(--dc-duration-fast) var(--dc-ease);
47
+ --dc-transition-base: var(--dc-duration-base) var(--dc-ease);
48
+ }
@@ -0,0 +1,49 @@
1
+ /* ============================================================
2
+ Display Case — Typography tokens
3
+ Hanken Grotesk drives the chrome (warm, legible, quiet).
4
+ JetBrains Mono carries developer texture — labels, code,
5
+ values, the wordmark. The chrome lives at small sizes so the
6
+ showcased component owns the visual weight.
7
+ ============================================================ */
8
+
9
+ :root {
10
+ /* --- Families ---------------------------------------------- */
11
+ --dc-font-sans:
12
+ "Hanken Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI",
13
+ Roboto, sans-serif;
14
+ --dc-font-mono:
15
+ "JetBrains Mono", ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas,
16
+ monospace;
17
+
18
+ /* --- Size scale (px; dense chrome) ------------------------- */
19
+ --dc-text-2xs: 10px; /* compact chrome — undocked tweaks */
20
+ --dc-text-xs: 11px; /* eyebrow labels, dim hints */
21
+ --dc-text-sm: 12px; /* zoom %, secondary, captions */
22
+ --dc-text-base: 14px; /* the chrome baseline */
23
+ --dc-text-md: 16px; /* icon glyphs, emphasis */
24
+ --dc-text-lg: 20px;
25
+ --dc-text-xl: 28px; /* doc panel headings */
26
+ --dc-text-2xl: 40px; /* wordmark / specimen display */
27
+
28
+ /* --- Weights ----------------------------------------------- */
29
+ --dc-weight-normal: 400;
30
+ --dc-weight-medium: 500; /* active nav, control labels */
31
+ --dc-weight-semibold: 600; /* title, group labels */
32
+ --dc-weight-bold: 700; /* wordmark, rare emphasis */
33
+
34
+ /* --- Line height ------------------------------------------- */
35
+ --dc-leading-tight: 1.2;
36
+ --dc-leading-normal: 1.45;
37
+ --dc-leading-relaxed: 1.6; /* doc / markdown copy */
38
+
39
+ /* --- Letter spacing ---------------------------------------- */
40
+ --dc-tracking-label: 0.08em; /* uppercase mono eyebrow labels */
41
+ --dc-tracking-tight: -0.01em; /* display / wordmark */
42
+ --dc-tracking-normal: 0;
43
+
44
+ /* --- Composite role tokens --------------------------------- */
45
+ /* Uppercase mono eyebrow — the signature label treatment. */
46
+ --dc-eyebrow:
47
+ var(--dc-weight-medium) var(--dc-text-xs) / var(--dc-leading-tight)
48
+ var(--dc-font-mono);
49
+ }
@@ -0,0 +1,54 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { renderToStaticMarkup } from 'react-dom/server'
3
+ import { DocMarkdown } from './markdown'
4
+
5
+ describe('DocMarkdown', () => {
6
+ test('renders the placard CommonMark + GFM subset', () => {
7
+ const html = renderToStaticMarkup(
8
+ <DocMarkdown
9
+ source={[
10
+ '# Title',
11
+ '',
12
+ 'A **bold** and *italic* word with `code`.',
13
+ '',
14
+ '- one',
15
+ '- two',
16
+ '',
17
+ '[link](https://example.com)',
18
+ '',
19
+ '```ts',
20
+ 'const x = 1',
21
+ '```',
22
+ ].join('\n')}
23
+ />,
24
+ )
25
+ expect(html).toContain('class="dc-doc-md"')
26
+ expect(html).toMatch(/<h1[^>]*>Title<\/h1>/)
27
+ expect(html).toContain('<strong>bold</strong>')
28
+ expect(html).toContain('<em>italic</em>')
29
+ expect(html).toContain('<code>code</code>')
30
+ expect(html).toContain('<li>one</li>')
31
+ expect(html).toMatch(/<a[^>]*href="https:\/\/example\.com"/)
32
+ expect(html).toContain('<pre>')
33
+ expect(html).toContain('const x = 1')
34
+ })
35
+
36
+ test('applies GFM extensions like tables and strikethrough', () => {
37
+ const html = renderToStaticMarkup(
38
+ <DocMarkdown source={'| A | B |\n| - | - |\n| 1 | 2 |\n\n~~gone~~'} />,
39
+ )
40
+ expect(html).toMatch(/<table/)
41
+ expect(html).toMatch(/<th[^>]*>A<\/th>/)
42
+ expect(html).toMatch(/<td[^>]*>1<\/td>/)
43
+ expect(html).toContain('<del>gone</del>')
44
+ })
45
+
46
+ test('does not inject raw HTML from the source into the chrome', () => {
47
+ const html = renderToStaticMarkup(
48
+ <DocMarkdown source={'<script>alert(1)</script>\n\n<b>x</b> text'} />,
49
+ )
50
+ // disableParsingRawHTML: tags are escaped to text, never live elements.
51
+ expect(html).not.toContain('<script>')
52
+ expect(html).not.toContain('<b>x</b>')
53
+ })
54
+ })
@@ -0,0 +1,19 @@
1
+ import Markdown from 'markdown-to-jsx'
2
+
3
+ /**
4
+ * Renders a component's authored `.placard.md` as full CommonMark + GFM via
5
+ * `markdown-to-jsx` — a ~zero-dependency renderer that emits React elements
6
+ * (no `dangerouslySetInnerHTML`). Raw HTML is intentionally NOT rendered
7
+ * (`disableParsingRawHTML`), so a doc file can't inject markup into the chrome.
8
+ * Syntax highlighting is a deliberate non-goal — fenced code renders as plain
9
+ * styled <pre><code>.
10
+ */
11
+ const OPTIONS = { disableParsingRawHTML: true } as const
12
+
13
+ export function DocMarkdown({ source }: { source: string }) {
14
+ return (
15
+ <div className="dc-doc-md">
16
+ <Markdown options={OPTIONS}>{source}</Markdown>
17
+ </div>
18
+ )
19
+ }
@@ -0,0 +1,76 @@
1
+ import type { ReactNode } from 'react'
2
+ import { StrictMode } from 'react'
3
+ import { createRoot, hydrateRoot } from 'react-dom/client'
4
+ import { PrimerRoot } from './primer'
5
+
6
+ /**
7
+ * Entry point for the isolated `/render/primer` document. Mounts the compiled MDX
8
+ * primer into #root and sets the initial theme from the URL (`?theme=`); later
9
+ * theme changes arrive over `postMessage` (handled in {@link PrimerRoot}).
10
+ *
11
+ * Driven two ways, mirroring the `/render` frame:
12
+ * - **Standalone** (direct navigation / snapshot): theme comes from the URL.
13
+ * - **Embedded** (browse chrome iframe): the chrome pushes theme + scroll
14
+ * messages and reads back the section list + active section.
15
+ */
16
+ type MDXContent = (props: { components?: unknown }) => ReactNode
17
+
18
+ /**
19
+ * Neutralize anchor clicks that would unload the primer frame. Authored prose
20
+ * can contain real `<a href>` links; in this isolated frame a click would do a
21
+ * full-document navigation and sever the chrome↔frame handshake. In-page hash
22
+ * links and `target=_blank` are harmless and left alone.
23
+ */
24
+ function blockFrameNavigation(): void {
25
+ document.addEventListener(
26
+ 'click',
27
+ (e) => {
28
+ if (
29
+ e.defaultPrevented ||
30
+ e.button !== 0 ||
31
+ e.metaKey ||
32
+ e.ctrlKey ||
33
+ e.shiftKey ||
34
+ e.altKey
35
+ ) {
36
+ return
37
+ }
38
+ const anchor = (e.target as HTMLElement | null)?.closest?.('a')
39
+ const href = anchor?.getAttribute('href')
40
+ if (!anchor || !href) return
41
+ if (anchor.target && anchor.target !== '_self') return
42
+ const url = new URL(href, window.location.href)
43
+ if (url.pathname === window.location.pathname && url.hash !== '') return
44
+ e.preventDefault()
45
+ },
46
+ true,
47
+ )
48
+ }
49
+
50
+ export function mountPrimer(Content: MDXContent): void {
51
+ blockFrameNavigation()
52
+ const params = new URLSearchParams(window.location.search)
53
+ const theme = params.get('theme') === 'dark' ? 'dark' : 'light'
54
+ document.documentElement.dataset.theme = theme
55
+ document.documentElement.dataset.themePref = theme
56
+
57
+ const rootEl = document.getElementById('root') as HTMLElement
58
+ const tree = (
59
+ <StrictMode>
60
+ <PrimerRoot content={Content} />
61
+ </StrictMode>
62
+ )
63
+ // Adopt the server-rendered primer when present (`data-ssr="1"`); otherwise
64
+ // mount fresh (a browser-only specimen forced a client-render fallback).
65
+ if (rootEl.dataset.ssr === '1') {
66
+ hydrateRoot(rootEl, tree, {
67
+ onRecoverableError: (err) =>
68
+ console.warn(
69
+ '[display-case] primer adopt mismatch; client re-rendered:',
70
+ err,
71
+ ),
72
+ })
73
+ } else {
74
+ createRoot(rootEl).render(tree)
75
+ }
76
+ }
@@ -0,0 +1,175 @@
1
+ .dc-primer {
2
+ height: 100vh;
3
+ overflow-y: auto;
4
+ background: var(--dc-bg);
5
+ color: var(--dc-fg);
6
+ font-family: var(--dc-font-sans);
7
+ font-size: var(--dc-text-base);
8
+ line-height: var(--dc-leading-relaxed);
9
+ }
10
+ .dc-primer-inner {
11
+ max-width: 56rem;
12
+ margin: 0 auto;
13
+ padding: var(--dc-space-16) var(--dc-space-12) 6rem;
14
+ }
15
+
16
+ /* Prose — quiet, legible long-form copy. */
17
+ .dc-primer h1 {
18
+ font-size: var(--dc-text-2xl);
19
+ font-weight: var(--dc-weight-bold);
20
+ letter-spacing: var(--dc-tracking-tight);
21
+ line-height: var(--dc-leading-tight);
22
+ margin: 0 0 var(--dc-space-8);
23
+ }
24
+ .dc-primer h2 {
25
+ font-size: var(--dc-text-xl);
26
+ font-weight: var(--dc-weight-semibold);
27
+ letter-spacing: var(--dc-tracking-tight);
28
+ line-height: var(--dc-leading-tight);
29
+ margin: 3rem 0 var(--dc-space-8);
30
+ scroll-margin-top: var(--dc-space-8);
31
+ }
32
+ .dc-primer h3 {
33
+ font-size: var(--dc-text-lg);
34
+ font-weight: var(--dc-weight-semibold);
35
+ margin: var(--dc-space-12) 0 var(--dc-space-4);
36
+ }
37
+ .dc-primer p {
38
+ margin: 0 0 var(--dc-space-8);
39
+ color: var(--dc-fg);
40
+ }
41
+ .dc-primer a {
42
+ color: var(--dc-brand);
43
+ text-decoration: none;
44
+ }
45
+ .dc-primer a:hover {
46
+ text-decoration: underline;
47
+ }
48
+ .dc-primer strong {
49
+ font-weight: var(--dc-weight-semibold);
50
+ }
51
+ .dc-primer ul,
52
+ .dc-primer ol {
53
+ margin: 0 0 var(--dc-space-8);
54
+ padding-left: var(--dc-space-12);
55
+ }
56
+ .dc-primer li {
57
+ margin: var(--dc-space-2) 0;
58
+ }
59
+ .dc-primer code {
60
+ font-family: var(--dc-font-mono);
61
+ font-size: 0.9em;
62
+ background: var(--dc-bg-subtle);
63
+ border: var(--dc-border-line);
64
+ border-radius: var(--dc-radius-sm);
65
+ padding: 0.05em 0.35em;
66
+ }
67
+ .dc-primer pre {
68
+ font-family: var(--dc-font-mono);
69
+ font-size: var(--dc-text-sm);
70
+ line-height: var(--dc-leading-normal);
71
+ background: var(--dc-bg-subtle);
72
+ border: var(--dc-border-line);
73
+ border-radius: var(--dc-radius-md);
74
+ padding: var(--dc-space-8);
75
+ overflow-x: auto;
76
+ margin: 0 0 var(--dc-space-8);
77
+ }
78
+ .dc-primer pre code {
79
+ background: none;
80
+ border: 0;
81
+ padding: 0;
82
+ }
83
+
84
+ /* Tables (GFM) — border-led and flat, like every other Vitrine surface: a
85
+ hairline frame, a quiet mono header on the subtle fill, and hairline row
86
+ rules. Authored as Markdown pipe-tables; this gives them the chrome's look
87
+ without per-document styling. */
88
+ .dc-primer table {
89
+ width: 100%;
90
+ border-collapse: collapse;
91
+ margin: 0 0 var(--dc-space-8);
92
+ font-size: var(--dc-text-sm);
93
+ border: var(--dc-border-line);
94
+ }
95
+ .dc-primer thead th {
96
+ text-align: left;
97
+ padding: var(--dc-space-5) var(--dc-space-6);
98
+ background: var(--dc-bg-subtle);
99
+ color: var(--dc-fg-muted);
100
+ font-family: var(--dc-font-mono);
101
+ font-size: var(--dc-text-2xs);
102
+ font-weight: var(--dc-weight-medium);
103
+ text-transform: uppercase;
104
+ letter-spacing: 0.05em;
105
+ border-bottom: var(--dc-border-line);
106
+ }
107
+ .dc-primer tbody td {
108
+ padding: var(--dc-space-5) var(--dc-space-6);
109
+ vertical-align: top;
110
+ color: var(--dc-fg);
111
+ }
112
+ .dc-primer tbody tr + tr td {
113
+ border-top: var(--dc-border-line);
114
+ }
115
+
116
+ /* Display — the specimen card contract. The title and subtitle sit as plain
117
+ wall-text *above* the box; the box holds only the live specimen. */
118
+ .dc-display {
119
+ margin: 0 0 var(--dc-space-12);
120
+ scroll-margin-top: var(--dc-space-8);
121
+ }
122
+ .dc-display-head {
123
+ margin-bottom: var(--dc-space-5);
124
+ }
125
+ .dc-display-title {
126
+ font-size: var(--dc-text-lg);
127
+ font-weight: var(--dc-weight-semibold);
128
+ letter-spacing: var(--dc-tracking-tight);
129
+ line-height: var(--dc-leading-tight);
130
+ color: var(--dc-fg);
131
+ }
132
+ .dc-display-sub {
133
+ margin-top: var(--dc-space-1);
134
+ font-size: var(--dc-text-sm);
135
+ color: var(--dc-fg-muted);
136
+ }
137
+ .dc-display-specimen {
138
+ border: var(--dc-border-line);
139
+ border-radius: var(--dc-radius-md);
140
+ background: var(--dc-bg);
141
+ padding: var(--dc-space-12);
142
+ display: flex;
143
+ flex-wrap: wrap;
144
+ gap: var(--dc-space-8);
145
+ align-items: center;
146
+ justify-content: center;
147
+ }
148
+ /* A forced-theme specimen re-scopes the design tokens for its subtree, so it
149
+ must repaint its own surface (the value the consumer app would give it). */
150
+ .dc-display-specimen[data-theme] {
151
+ background: var(--dc-bg);
152
+ color: var(--dc-fg);
153
+ }
154
+ /* App surface — opt in (appSurface) to paint the consumer design system's own
155
+ canvas (--color-bg/--color-fg, the same tokens the /render frame paints)
156
+ instead of the Vitrine's --dc-bg, so a specimen sits on the exact background
157
+ the real app gives it. Degrades to --dc-bg when the consumer defines no
158
+ --color-bg. Listed after the [data-theme] rule (equal specificity → source
159
+ order wins), and the [data-theme] combination is spelled out so a forced-theme
160
+ app-surface specimen resolves the themed --color-bg of its own subtree. */
161
+ .dc-display-specimen[data-app-surface],
162
+ .dc-display-specimen[data-app-surface][data-theme] {
163
+ background: var(--color-bg, var(--dc-bg));
164
+ color: var(--color-fg, var(--dc-fg));
165
+ }
166
+ /* Flush — the specimen draws no frame or padding of its own. A single
167
+ self-bordered child (e.g. a DefinitionList) fills the box edge-to-edge and
168
+ supplies the one border, so there's no box-within-a-box; the rounded, clipped
169
+ background still backs the child's corners. */
170
+ .dc-display-specimen[data-flush] {
171
+ display: block;
172
+ padding: 0;
173
+ border: 0;
174
+ overflow: hidden;
175
+ }