@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,377 @@
1
+ import type { KeyboardEvent as ReactKeyboardEvent, ReactNode } from 'react'
2
+ import { useEffect, useId, useLayoutEffect, useRef, useState } from 'react'
3
+ import { createPortal } from 'react-dom'
4
+
5
+ /**
6
+ * Display Case — SelectMenu
7
+ * An accessible custom single-select, built to the WAI-ARIA "select-only
8
+ * combobox" pattern: a `role="combobox"` trigger that owns a popup
9
+ * `role="listbox"` of `role="option"`s, with focus kept on the trigger and the
10
+ * active option tracked via `aria-activedescendant`. Screen readers announce and
11
+ * operate it like a native `<select>`, but it commits on click with no OS popup
12
+ * menu — so the bound value updates *instantly* (a native `<select>` on macOS
13
+ * defers its `change` event until the native menu finishes dismissing, which
14
+ * reads as a lag when the bound view updates live).
15
+ *
16
+ * Keyboard: ↑/↓ move the active option, Home/End jump to the ends, type-ahead
17
+ * matches by label, Enter/Space commit, Esc closes without change, Tab commits
18
+ * then moves on. The popup portals to `document.body` so an `overflow`-clipping
19
+ * or `position: fixed` ancestor can't trap it.
20
+ */
21
+
22
+ export type SelectMenuSize = 'sm' | 'md'
23
+ export type SelectMenuOption =
24
+ | string
25
+ /** A non-interactive option renders as a quiet group header — not selectable,
26
+ * skipped by keyboard nav and type-ahead. */
27
+ | { value: string; label?: ReactNode; disabled?: boolean }
28
+
29
+ export interface SelectMenuProps {
30
+ options: SelectMenuOption[]
31
+ value: string
32
+ onChange: (value: string) => void
33
+ size?: SelectMenuSize
34
+ disabled?: boolean
35
+ 'aria-label'?: string
36
+ id?: string
37
+ }
38
+
39
+ interface NormOption {
40
+ value: string
41
+ label: ReactNode
42
+ /** Lowercased text used for type-ahead matching. */
43
+ text: string
44
+ /** A header row: rendered, but not selectable / navigable / matchable. */
45
+ disabled: boolean
46
+ }
47
+
48
+ function normalize(options: SelectMenuOption[]): NormOption[] {
49
+ return options.map((o) => {
50
+ if (typeof o === 'string')
51
+ return { value: o, label: o, text: o.toLowerCase(), disabled: false }
52
+ const text = typeof o.label === 'string' ? o.label : o.value
53
+ return {
54
+ value: o.value,
55
+ label: o.label ?? o.value,
56
+ text: text.toLowerCase(),
57
+ disabled: !!o.disabled,
58
+ }
59
+ })
60
+ }
61
+
62
+ /** Next option (by index, wrapping) whose label starts with the typed buffer.
63
+ * Disabled headers are skipped. */
64
+ function findMatch(opts: NormOption[], buffer: string, from: number): number {
65
+ const n = opts.length
66
+ if (n === 0) return -1
67
+ // A single character cycles forward (skip the current match); a longer buffer
68
+ // may need to land on the current option, so start inclusive.
69
+ const startK = buffer.length === 1 ? 1 : 0
70
+ for (let k = startK; k <= n; k++) {
71
+ const i = (from + k) % n
72
+ if (!opts[i].disabled && opts[i].text.startsWith(buffer)) return i
73
+ }
74
+ return -1
75
+ }
76
+
77
+ /** First selectable index, or 0 if none. */
78
+ function firstEnabled(opts: NormOption[]): number {
79
+ const i = opts.findIndex((o) => !o.disabled)
80
+ return i < 0 ? 0 : i
81
+ }
82
+
83
+ /** Last selectable index, or `opts.length - 1` if none. */
84
+ function lastEnabled(opts: NormOption[]): number {
85
+ for (let i = opts.length - 1; i >= 0; i--) if (!opts[i].disabled) return i
86
+ return opts.length - 1
87
+ }
88
+
89
+ /** Nearest selectable index strictly past `from` in `dir` (+1/-1); stays put if
90
+ * there is none, so a header can never become the active row. */
91
+ function moveEnabled(opts: NormOption[], from: number, dir: 1 | -1): number {
92
+ for (let i = from + dir; i >= 0 && i < opts.length; i += dir) {
93
+ if (!opts[i].disabled) return i
94
+ }
95
+ return from
96
+ }
97
+
98
+ interface Coords {
99
+ left: number
100
+ top: number
101
+ minWidth: number
102
+ maxHeight: number
103
+ placement: 'down' | 'up'
104
+ }
105
+
106
+ export function SelectMenu({
107
+ options,
108
+ value,
109
+ onChange,
110
+ size = 'md',
111
+ disabled = false,
112
+ 'aria-label': ariaLabel,
113
+ id,
114
+ }: SelectMenuProps) {
115
+ const opts = normalize(options)
116
+ const reactId = useId()
117
+ const baseId = id ?? reactId
118
+ const listboxId = `${baseId}-listbox`
119
+ const optionId = (i: number) => `${baseId}-opt-${i}`
120
+
121
+ const rawSelected = opts.findIndex((o) => o.value === value)
122
+ const selectedIndex = rawSelected >= 0 ? rawSelected : firstEnabled(opts)
123
+
124
+ const [open, setOpen] = useState(false)
125
+ const [activeIndex, setActiveIndex] = useState(selectedIndex)
126
+ const [coords, setCoords] = useState<Coords | null>(null)
127
+
128
+ const triggerRef = useRef<HTMLDivElement | null>(null)
129
+ const listRef = useRef<HTMLDivElement | null>(null)
130
+ const activeRef = useRef(activeIndex)
131
+ activeRef.current = activeIndex
132
+ // Type-ahead buffer + its reset timer.
133
+ const bufferRef = useRef('')
134
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
135
+
136
+ const openAt = (index: number) => {
137
+ setActiveIndex(index)
138
+ setOpen(true)
139
+ }
140
+ const close = (focusTrigger = true) => {
141
+ setOpen(false)
142
+ if (focusTrigger) triggerRef.current?.focus()
143
+ }
144
+ const commit = (index: number) => {
145
+ const opt = opts[index]
146
+ if (!opt || opt.disabled) return // headers aren't selectable
147
+ if (opt.value !== value) onChange(opt.value)
148
+ close()
149
+ }
150
+
151
+ const typeahead = (char: string) => {
152
+ bufferRef.current += char.toLowerCase()
153
+ if (timerRef.current) clearTimeout(timerRef.current)
154
+ timerRef.current = setTimeout(() => {
155
+ bufferRef.current = ''
156
+ }, 500)
157
+ const match = findMatch(opts, bufferRef.current, activeRef.current)
158
+ if (match >= 0) setActiveIndex(match)
159
+ }
160
+
161
+ // Position the portaled popup against the trigger, flipping above when there
162
+ // isn't room below. Measured before paint so it never flashes mispositioned.
163
+ useLayoutEffect(() => {
164
+ if (!open || !triggerRef.current) return
165
+ const r = triggerRef.current.getBoundingClientRect()
166
+ const margin = 8
167
+ const spaceBelow = window.innerHeight - r.bottom - margin
168
+ const spaceAbove = r.top - margin
169
+ const up = spaceBelow < 160 && spaceAbove > spaceBelow
170
+ setCoords({
171
+ left: r.left,
172
+ top: up ? r.top : r.bottom,
173
+ minWidth: r.width,
174
+ maxHeight: Math.max(96, up ? spaceAbove : spaceBelow),
175
+ placement: up ? 'up' : 'down',
176
+ })
177
+ }, [open])
178
+
179
+ // Keep the active option scrolled into view as it moves. `optionId` is a
180
+ // stable id formatter (baseId is fixed for the component's life).
181
+ // biome-ignore lint/correctness/useExhaustiveDependencies: optionId is stable; open/activeIndex drive the effect
182
+ useEffect(() => {
183
+ if (!open) return
184
+ document
185
+ .getElementById(optionId(activeIndex))
186
+ ?.scrollIntoView({ block: 'nearest' })
187
+ }, [open, activeIndex])
188
+
189
+ // While open, dismiss on an outside press or on any ancestor scroll/resize
190
+ // (the popup is fixed-positioned, so a scroll would otherwise strand it).
191
+ useEffect(() => {
192
+ if (!open) return
193
+ const onDown = (e: PointerEvent) => {
194
+ const t = e.target as Node
195
+ if (triggerRef.current?.contains(t) || listRef.current?.contains(t))
196
+ return
197
+ setOpen(false)
198
+ }
199
+ const onScroll = (e: Event) => {
200
+ // Ignore the popup's own internal scroll.
201
+ if (e.target instanceof Node && listRef.current?.contains(e.target))
202
+ return
203
+ setOpen(false)
204
+ }
205
+ const onResize = () => setOpen(false)
206
+ document.addEventListener('pointerdown', onDown, true)
207
+ window.addEventListener('scroll', onScroll, true)
208
+ window.addEventListener('resize', onResize)
209
+ return () => {
210
+ document.removeEventListener('pointerdown', onDown, true)
211
+ window.removeEventListener('scroll', onScroll, true)
212
+ window.removeEventListener('resize', onResize)
213
+ }
214
+ }, [open])
215
+
216
+ // Drop a stale type-ahead timer on unmount.
217
+ useEffect(
218
+ () => () => {
219
+ if (timerRef.current) clearTimeout(timerRef.current)
220
+ },
221
+ [],
222
+ )
223
+
224
+ const isPrintable = (e: ReactKeyboardEvent) =>
225
+ e.key.length === 1 && !e.metaKey && !e.ctrlKey && !e.altKey
226
+
227
+ const onKeyDown = (e: ReactKeyboardEvent<HTMLDivElement>) => {
228
+ if (disabled) return
229
+ if (!open) {
230
+ switch (e.key) {
231
+ case 'ArrowDown':
232
+ case 'ArrowUp':
233
+ case 'Enter':
234
+ case ' ':
235
+ case 'Spacebar':
236
+ e.preventDefault()
237
+ openAt(selectedIndex)
238
+ return
239
+ case 'Home':
240
+ e.preventDefault()
241
+ openAt(firstEnabled(opts))
242
+ return
243
+ case 'End':
244
+ e.preventDefault()
245
+ openAt(lastEnabled(opts))
246
+ return
247
+ default:
248
+ if (isPrintable(e)) {
249
+ e.preventDefault()
250
+ setOpen(true)
251
+ typeahead(e.key)
252
+ }
253
+ return
254
+ }
255
+ }
256
+ switch (e.key) {
257
+ case 'ArrowDown':
258
+ e.preventDefault()
259
+ setActiveIndex((i) => moveEnabled(opts, i, 1))
260
+ break
261
+ case 'ArrowUp':
262
+ e.preventDefault()
263
+ setActiveIndex((i) => moveEnabled(opts, i, -1))
264
+ break
265
+ case 'Home':
266
+ e.preventDefault()
267
+ setActiveIndex(firstEnabled(opts))
268
+ break
269
+ case 'End':
270
+ e.preventDefault()
271
+ setActiveIndex(lastEnabled(opts))
272
+ break
273
+ case 'Enter':
274
+ case ' ':
275
+ case 'Spacebar':
276
+ e.preventDefault()
277
+ commit(activeIndex)
278
+ break
279
+ case 'Escape':
280
+ e.preventDefault()
281
+ close()
282
+ break
283
+ case 'Tab':
284
+ // Commit the active option, then let focus move on naturally.
285
+ commit(activeIndex)
286
+ break
287
+ default:
288
+ if (isPrintable(e)) {
289
+ e.preventDefault()
290
+ typeahead(e.key)
291
+ }
292
+ }
293
+ }
294
+
295
+ const onTriggerClick = () => {
296
+ if (disabled) return
297
+ if (open) setOpen(false)
298
+ else openAt(selectedIndex)
299
+ }
300
+
301
+ const selected = opts[selectedIndex]
302
+
303
+ return (
304
+ <span className="dcui-selectmenu dcui-select" data-size={size}>
305
+ <div
306
+ ref={triggerRef}
307
+ role="combobox"
308
+ tabIndex={disabled ? -1 : 0}
309
+ aria-haspopup="listbox"
310
+ aria-expanded={open}
311
+ aria-controls={listboxId}
312
+ aria-label={ariaLabel}
313
+ aria-disabled={disabled || undefined}
314
+ aria-activedescendant={open ? optionId(activeIndex) : undefined}
315
+ className="dcui-select-el dcui-selectmenu-trigger"
316
+ onClick={onTriggerClick}
317
+ onKeyDown={onKeyDown}>
318
+ <span className="dcui-selectmenu-value">
319
+ {selected?.label ?? value}
320
+ </span>
321
+ </div>
322
+ <span className="dcui-select-caret" aria-hidden="true">
323
+
324
+ </span>
325
+ {open &&
326
+ coords &&
327
+ createPortal(
328
+ <div
329
+ ref={listRef}
330
+ id={listboxId}
331
+ role="listbox"
332
+ aria-label={ariaLabel}
333
+ className="dcui-selectmenu-list"
334
+ data-size={size}
335
+ style={{
336
+ left: coords.left,
337
+ minWidth: coords.minWidth,
338
+ maxHeight: coords.maxHeight,
339
+ ...(coords.placement === 'up'
340
+ ? { top: coords.top, transform: 'translateY(-100%)' }
341
+ : { top: coords.top }),
342
+ }}>
343
+ {opts.map((o, i) => {
344
+ const isSelected = !o.disabled && o.value === value
345
+ return (
346
+ // Options are pointer affordances only: focus stays on the
347
+ // combobox trigger (aria-activedescendant) and all keyboard
348
+ // handling lives there, so these two rules don't apply here.
349
+ // biome-ignore lint/a11y/useFocusableInteractive: combobox keeps focus; option is tracked via aria-activedescendant
350
+ // biome-ignore lint/a11y/useKeyWithClickEvents: keyboard handled on the combobox trigger, not per-option
351
+ <div
352
+ key={o.value}
353
+ id={optionId(i)}
354
+ role="option"
355
+ aria-selected={isSelected}
356
+ aria-disabled={o.disabled || undefined}
357
+ data-active={!o.disabled && i === activeIndex}
358
+ className="dcui-selectmenu-option"
359
+ onMouseEnter={
360
+ o.disabled ? undefined : () => setActiveIndex(i)
361
+ }
362
+ // Keep focus on the combobox trigger (the menu commits on click).
363
+ onMouseDown={(e) => e.preventDefault()}
364
+ onClick={o.disabled ? undefined : () => commit(i)}>
365
+ <span className="dcui-selectmenu-check" aria-hidden="true">
366
+ {isSelected ? '✓' : ''}
367
+ </span>
368
+ {o.label}
369
+ </div>
370
+ )
371
+ })}
372
+ </div>,
373
+ document.body,
374
+ )}
375
+ </span>
376
+ )
377
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Display Case design-system components — "The Vitrine".
3
+ * Self-contained, pure React components (each brings its own CSS). Used by the
4
+ * browse chrome and dogfooded as Display Case's own showcased components.
5
+ */
6
+
7
+ export type { ButtonProps, ButtonSize, ButtonVariant } from './controls/Button'
8
+ export { Button } from './controls/Button'
9
+ export type {
10
+ IconButtonProps,
11
+ IconButtonSize,
12
+ IconButtonVariant,
13
+ } from './controls/IconButton'
14
+ export { IconButton } from './controls/IconButton'
15
+ export type { InputProps, InputSize } from './controls/Input'
16
+ export { Input } from './controls/Input'
17
+ export type {
18
+ SelectItem,
19
+ SelectOption,
20
+ SelectOptionGroup,
21
+ SelectProps,
22
+ SelectSize,
23
+ } from './controls/Select'
24
+ export { Select } from './controls/Select'
25
+ export type {
26
+ SelectMenuOption,
27
+ SelectMenuProps,
28
+ SelectMenuSize,
29
+ } from './controls/SelectMenu'
30
+ export { SelectMenu } from './controls/SelectMenu'
31
+ // Reusable Primer foundation specimens (ramps, swatches, type scale, glyphs…)
32
+ // — generic, prop-driven primitives for building a Primer.
33
+ export * from './primer-specimen'
34
+ export type { A11yBadgeProps } from './showcase/A11yBadge'
35
+ export { A11yBadge } from './showcase/A11yBadge'
36
+ export type { A11yPanelProps } from './showcase/A11yPanel'
37
+ export { A11yPanel } from './showcase/A11yPanel'
38
+ export type { ChipProps, ChipVariant } from './showcase/Chip'
39
+ export { Chip } from './showcase/Chip'
40
+ export type { EyebrowProps, EyebrowTone } from './showcase/Eyebrow'
41
+ export { Eyebrow } from './showcase/Eyebrow'
42
+ export type { FlowNavProps, FlowStep } from './showcase/FlowNav'
43
+ export { FlowNav } from './showcase/FlowNav'
44
+ export type { ImpactTagProps } from './showcase/ImpactTag'
45
+ export { ImpactTag, impactRank } from './showcase/ImpactTag'
46
+ export type { NavItemKind, NavItemProps } from './showcase/NavItem'
47
+ export { NavItem } from './showcase/NavItem'
48
+ export type { RenderAddressProps } from './showcase/RenderAddress'
49
+ export { RenderAddress } from './showcase/RenderAddress'
50
+ export type {
51
+ SegmentedOption,
52
+ SegmentedToggleProps,
53
+ } from './showcase/SegmentedToggle'
54
+ export { SegmentedToggle } from './showcase/SegmentedToggle'
55
+ export type { SidebarProps } from './showcase/Sidebar'
56
+ export { Sidebar } from './showcase/Sidebar'
57
+ export type { StageProps } from './showcase/Stage'
58
+ export { Stage } from './showcase/Stage'
59
+ export type {
60
+ TweakItem,
61
+ TweaksMode,
62
+ TweaksPanelProps,
63
+ } from './showcase/TweaksPanel'
64
+ export { Row as TweakRow, TweaksPanel } from './showcase/TweaksPanel'
65
+ export type { WordmarkProps } from './showcase/Wordmark'
66
+ export { Wordmark } from './showcase/Wordmark'
@@ -0,0 +1,44 @@
1
+ import { defineCases, tweak } from '@awarebydefault/display-case'
2
+ import { ColorRamp, type ColorStop } from './ColorRamp'
3
+
4
+ const marigold: ColorStop[] = [
5
+ { name: 'marigold-300', color: 'var(--dc-marigold-300)', caption: '#f6c878' },
6
+ { name: 'marigold-400', color: 'var(--dc-marigold-400)', caption: '#f0a23b' },
7
+ { name: 'marigold-500', color: 'var(--dc-marigold-500)', caption: '#e0820b' },
8
+ {
9
+ name: 'marigold-600',
10
+ color: 'var(--dc-marigold-600)',
11
+ caption: '#c2690a',
12
+ star: true,
13
+ },
14
+ { name: 'marigold-700', color: 'var(--dc-marigold-700)', caption: '#9a4f0a' },
15
+ ]
16
+
17
+ const paper: ColorStop[] = [
18
+ { name: 'paper-100', color: 'var(--dc-paper-100)', caption: '#f4f1e9' },
19
+ { name: 'paper-300', color: 'var(--dc-paper-300)', caption: '#d6cebe' },
20
+ { name: 'paper-500', color: 'var(--dc-paper-500)', caption: '#8a8073' },
21
+ { name: 'paper-700', color: 'var(--dc-paper-700)', caption: '#4a423a' },
22
+ { name: 'paper-900', color: 'var(--dc-paper-900)', caption: '#211d18' },
23
+ ]
24
+
25
+ export default defineCases(
26
+ 'ColorRamp',
27
+ {
28
+ Playground: {
29
+ tweaks: {
30
+ chipHeight: tweak.number(56),
31
+ star: tweak.boolean(true),
32
+ },
33
+ render: (t) => (
34
+ <ColorRamp
35
+ chipHeight={t.chipHeight}
36
+ stops={marigold.map((s) => ({ ...s, star: s.star && t.star }))}
37
+ />
38
+ ),
39
+ },
40
+ Accent: () => <ColorRamp stops={marigold} />,
41
+ Neutral: () => <ColorRamp stops={paper} chipHeight={48} />,
42
+ },
43
+ { level: 'molecule' },
44
+ )
@@ -0,0 +1,15 @@
1
+ **ColorRamp** — a horizontal ramp of an ORDERED single-family palette (an accent ramp, a neutral ramp), each stop a chip over a label and optional caption. Reach for it when the stops have a meaningful sequence.
2
+
3
+ ```tsx
4
+ <ColorRamp
5
+ chipHeight={56}
6
+ stops={[
7
+ { name: 'marigold-500', color: 'var(--dc-marigold-500)', caption: '#e0820b' },
8
+ { name: 'marigold-600', color: 'var(--dc-marigold-600)', caption: '#c2690a', star: true },
9
+ ]}
10
+ />
11
+ ```
12
+
13
+ For unordered semantic role tokens, reach for `SwatchGrid`.
14
+
15
+ Keep each `color` as the whole `var(--dc-…)` literal so the token check resolves it. `star` marks the canonical stop with a marigold ★.
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Display Case — ColorRamp
3
+ * A horizontal ramp of colour stops with a label + caption under each chip.
4
+ * Generic specimen primitive for a Primer: feed it any ordered set of stops
5
+ * (an accent ramp, a neutral ramp, a brand palette) and it renders the swatches.
6
+ *
7
+ * Pass each stop's colour as a complete CSS value (`var(--dc-marigold-600)` or
8
+ * `#c2690a`) — keep the whole `var(...)` string literal rather than templating
9
+ * it, so the token-conformance check can statically resolve the reference.
10
+ */
11
+
12
+ export interface ColorStop {
13
+ /** Display label under the chip (also the React key). */
14
+ name: string
15
+ /** Complete CSS colour value painted on the chip, e.g. `var(--dc-marigold-600)`. */
16
+ color: string
17
+ /** Optional caption under the name — typically the resolved hex. */
18
+ caption?: string
19
+ /** Marks the canonical stop with a marigold star. */
20
+ star?: boolean
21
+ }
22
+
23
+ export interface ColorRampProps {
24
+ stops: ColorStop[]
25
+ /** Chip height in pixels. */
26
+ chipHeight?: number
27
+ }
28
+
29
+ export function ColorRamp({ stops, chipHeight = 56 }: ColorRampProps) {
30
+ return (
31
+ <div
32
+ className="dcpl-ramp"
33
+ style={{ gridTemplateColumns: `repeat(${stops.length}, 1fr)` }}>
34
+ {stops.map((s) => (
35
+ <div className="dcpl-sw" key={s.name}>
36
+ <div
37
+ className="dcpl-sw-chip"
38
+ style={{ height: `${chipHeight}px`, background: s.color }}
39
+ />
40
+ <div className="dcpl-sw-meta">
41
+ <div className="dcpl-sw-name">
42
+ {s.name}
43
+ {s.star ? <span className="dcpl-sw-star"> ★</span> : null}
44
+ </div>
45
+ {s.caption ? <div className="dcpl-sw-hex">{s.caption}</div> : null}
46
+ </div>
47
+ </div>
48
+ ))}
49
+ </div>
50
+ )
51
+ }
@@ -0,0 +1,38 @@
1
+ import { defineCases, tweak } from '@awarebydefault/display-case'
2
+ import { type DefEntry, DefinitionList } from './DefinitionList'
3
+
4
+ const voice: DefEntry[] = [
5
+ {
6
+ term: 'Voice',
7
+ description:
8
+ 'Plain, confident, technical-but-warm. It explains why, briefly, then moves on.',
9
+ },
10
+ {
11
+ term: 'Casing',
12
+ description: (
13
+ <>
14
+ Sentence case everywhere. The one exception is the{' '}
15
+ <strong>eyebrow label</strong>: uppercase mono with wide tracking.
16
+ </>
17
+ ),
18
+ },
19
+ {
20
+ term: 'Length',
21
+ description:
22
+ 'Terse. Buttons are one or two words; labels are a single token.',
23
+ },
24
+ ]
25
+
26
+ export default defineCases(
27
+ 'DefinitionList',
28
+ {
29
+ Playground: {
30
+ tweaks: { termWidth: tweak.text('7.5rem') },
31
+ render: (t) => (
32
+ <DefinitionList entries={voice} termWidth={t.termWidth || undefined} />
33
+ ),
34
+ },
35
+ Voice: () => <DefinitionList entries={voice} />,
36
+ },
37
+ { level: 'molecule' },
38
+ )
@@ -0,0 +1,15 @@
1
+ **DefinitionList** — a bordered list of term / description rows, the term a mono uppercase accent eyebrow; reach for it to lay out voice rules, content fundamentals, or any keyed reference prose.
2
+
3
+ ```tsx
4
+ <DefinitionList
5
+ termWidth="7.5rem"
6
+ entries={[
7
+ { term: 'Voice', description: 'Plain, confident, technical-but-warm.' },
8
+ { term: 'Casing', description: <>Sentence case, except the <strong>eyebrow</strong>.</> },
9
+ ]}
10
+ />
11
+ ```
12
+
13
+ Use this for free-form prose keyed by a term. For a fixed legend of dot-keyed statuses, reach for `StatusList`.
14
+
15
+ `description` accepts rich nodes (`<strong>` and other emphasis welcome), not just plain text.
@@ -0,0 +1,41 @@
1
+ import type { CSSProperties, ReactNode } from 'react'
2
+
3
+ /**
4
+ * Display Case — DefinitionList
5
+ * A bordered list of term / description rows. The term is a mono uppercase
6
+ * eyebrow in the accent colour; the description is body text that may include
7
+ * `<strong>` emphasis. Generic specimen primitive for a Primer — use it to lay
8
+ * out voice rules, content fundamentals, or any keyed reference.
9
+ */
10
+
11
+ export interface DefEntry {
12
+ /** Mono uppercase term shown in the accent colour (also the React key). */
13
+ term: string
14
+ /** Description body — plain text or rich nodes (e.g. `<strong>`). */
15
+ description: ReactNode
16
+ }
17
+
18
+ export interface DefinitionListProps {
19
+ entries: DefEntry[]
20
+ /** Width of the term column (any CSS length). */
21
+ termWidth?: string
22
+ }
23
+
24
+ export function DefinitionList({
25
+ entries,
26
+ termWidth = '7.5rem',
27
+ }: DefinitionListProps) {
28
+ return (
29
+ <div className="dcpl-deflist">
30
+ {entries.map((e) => (
31
+ <div
32
+ className="dcpl-defrow"
33
+ key={e.term}
34
+ style={{ '--dcpl-term-width': termWidth } as CSSProperties}>
35
+ <div className="dcpl-defterm">{e.term}</div>
36
+ <div className="dcpl-defdesc">{e.description}</div>
37
+ </div>
38
+ ))}
39
+ </div>
40
+ )
41
+ }
@@ -0,0 +1,24 @@
1
+ import { defineCases } from '@awarebydefault/display-case'
2
+ import { FontFamilies, type FontFamily } from './FontFamilies'
3
+
4
+ const pairing: FontFamily[] = [
5
+ {
6
+ tag: 'Sans · UI',
7
+ sample: 'Display Case shows the work, not itself',
8
+ note: 'Hanken Grotesk, ui-sans-serif, system-ui…',
9
+ },
10
+ {
11
+ tag: 'Mono · Code',
12
+ sample: '/render/<component>/<case>?theme=dark',
13
+ note: 'JetBrains Mono, ui-monospace, "SF Mono", Menlo…',
14
+ mono: true,
15
+ },
16
+ ]
17
+
18
+ export default defineCases(
19
+ 'FontFamilies',
20
+ {
21
+ Pairing: () => <FontFamilies families={pairing} />,
22
+ },
23
+ { level: 'molecule' },
24
+ )
@@ -0,0 +1,12 @@
1
+ **FontFamilies** — a stack of font-family rows, each a mono tag, a large sample in the family, and a mono note listing the stack; reach for it to document a type pairing (a UI sans, a code mono).
2
+
3
+ ```tsx
4
+ <FontFamilies
5
+ families={[
6
+ { tag: 'Sans · UI', sample: 'The quick brown fox', note: 'Hanken Grotesk, system-ui…' },
7
+ { tag: 'Mono · Code', sample: 'render(case)', note: 'JetBrains Mono…', mono: true },
8
+ ]}
9
+ />
10
+ ```
11
+
12
+ This documents which families exist. For size steps reach for `TypeScale`; for weights reach for `WeightSpecimen`.