@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,84 @@
1
+ import { defineCases, tweak } from '@awarebydefault/display-case'
2
+ import { useState } from 'react'
3
+ import { type SegmentedOption, SegmentedToggle } from './SegmentedToggle'
4
+
5
+ const two: SegmentedOption<string>[] = [
6
+ { id: 'primer', label: 'Primer' },
7
+ { id: 'library', label: 'Cases' },
8
+ ]
9
+
10
+ const three: SegmentedOption<string>[] = [
11
+ { id: 'light', label: 'Light' },
12
+ { id: 'auto', label: 'Auto' },
13
+ { id: 'dark', label: 'Dark' },
14
+ ]
15
+
16
+ const five: SegmentedOption<string>[] = [
17
+ { id: 'xs', label: 'XS' },
18
+ { id: 'sm', label: 'SM' },
19
+ { id: 'md', label: 'MD' },
20
+ { id: 'lg', label: 'LG' },
21
+ { id: 'xl', label: 'XL' },
22
+ ]
23
+
24
+ // SegmentedToggle is controlled, so each specimen owns its selected value —
25
+ // click a segment in the stage and the thumb lerps to it, no tweaks needed.
26
+ function Demo({
27
+ label,
28
+ options,
29
+ initial,
30
+ }: {
31
+ label: string
32
+ options: SegmentedOption<string>[]
33
+ initial: string
34
+ }) {
35
+ const [value, setValue] = useState(initial)
36
+ return (
37
+ <SegmentedToggle
38
+ label={label}
39
+ options={options}
40
+ value={value}
41
+ onChange={setValue}
42
+ />
43
+ )
44
+ }
45
+
46
+ export default defineCases(
47
+ 'SegmentedToggle',
48
+ {
49
+ // The browse chrome swaps cases *in place* (one persistent render root), so a
50
+ // bare <Demo> would be reconciled as the same instance across cases and keep
51
+ // its stale `value` — which, between cases with different option ids, leaves
52
+ // no segment active. A distinct `key` per case forces a remount so each
53
+ // re-seeds from its own `initial`. (Same reason the Playground keys by count.)
54
+ Playground: {
55
+ tweaks: {
56
+ count: tweak.choice(['2', '3', '5'], '5'),
57
+ },
58
+ render: (t) => {
59
+ const byCount: Record<string, SegmentedOption<string>[]> = {
60
+ '2': two,
61
+ '3': three,
62
+ '5': five,
63
+ }
64
+ const options = byCount[t.count] ?? five
65
+ return (
66
+ <Demo
67
+ key={`pg-${t.count}`}
68
+ label="Size"
69
+ options={options}
70
+ initial={options[0].id}
71
+ />
72
+ )
73
+ },
74
+ },
75
+ Two: () => (
76
+ <Demo key="two" label="View mode" options={two} initial="library" />
77
+ ),
78
+ Three: () => (
79
+ <Demo key="three" label="Theme" options={three} initial="auto" />
80
+ ),
81
+ Five: () => <Demo key="five" label="Size" options={five} initial="lg" />,
82
+ },
83
+ { level: 'molecule' },
84
+ )
@@ -0,0 +1,61 @@
1
+ .dcui-segmented {
2
+ position: relative;
3
+ display: grid;
4
+ grid-template-columns: repeat(var(--seg-count), 1fr);
5
+ gap: var(--dc-space-1);
6
+ padding: var(--dc-space-1);
7
+ border: var(--dc-border-line);
8
+ border-radius: var(--dc-radius-sm);
9
+ background: var(--dc-bg);
10
+ }
11
+ /* The highlight box: one cell-wide thumb pinned inside the padding box. The gap
12
+ between cells (and the padding around them) is one --dc-space-1; its width is a
13
+ single grid cell — (track − the N+1 gaps) divided by N — so translating by its
14
+ own width plus one gap, times the active index, lands it exactly over any
15
+ segment. The transform transition does the linear lerp across the list. */
16
+ .dcui-segmented-thumb {
17
+ position: absolute;
18
+ top: var(--dc-space-1);
19
+ bottom: var(--dc-space-1);
20
+ left: var(--dc-space-1);
21
+ width: calc(
22
+ (100% - (var(--seg-count) + 1) * var(--dc-space-1)) /
23
+ var(--seg-count)
24
+ );
25
+ transform: translateX(calc((100% + var(--dc-space-1)) * var(--seg-index)));
26
+ border-radius: calc(var(--dc-radius-sm) - 1px);
27
+ background: var(--dc-brand);
28
+ transition: transform var(--dc-transition-base);
29
+ pointer-events: none;
30
+ }
31
+ .dcui-segmented-seg {
32
+ position: relative;
33
+ z-index: 1;
34
+ appearance: none;
35
+ border: 0;
36
+ border-radius: calc(var(--dc-radius-sm) - 1px);
37
+ padding: var(--dc-space-2) var(--dc-space-4);
38
+ font-family: var(--dc-font-mono);
39
+ font-size: var(--dc-text-xs);
40
+ font-weight: var(--dc-weight-medium);
41
+ letter-spacing: var(--dc-tracking-label);
42
+ text-transform: uppercase;
43
+ color: var(--dc-fg-muted);
44
+ background: transparent;
45
+ cursor: pointer;
46
+ transition: color var(--dc-transition-base);
47
+ }
48
+ /* Only the inactive segments take a hover fill — the active one sits over the
49
+ thumb, which already carries the brand background. */
50
+ .dcui-segmented-seg:not([data-active="true"]):hover {
51
+ color: var(--dc-fg);
52
+ background: var(--dc-hover);
53
+ }
54
+ .dcui-segmented-seg[data-active="true"] {
55
+ color: var(--dc-brand-fg);
56
+ }
57
+ @media (prefers-reduced-motion: reduce) {
58
+ .dcui-segmented-thumb {
59
+ transition: none;
60
+ }
61
+ }
@@ -0,0 +1,21 @@
1
+ **SegmentedToggle** — an isolated, multi-option segmented control; reach for it whenever you're switching between a small fixed set of mutually-exclusive views or modes (2, 3, 5 — any count) and want the selection to slide rather than blink.
2
+
3
+ ```tsx
4
+ <SegmentedToggle
5
+ label="View mode"
6
+ options={[
7
+ { id: 'primer', label: 'Primer' },
8
+ { id: 'library', label: 'Cases' },
9
+ ]}
10
+ value={mode}
11
+ onChange={setMode}
12
+ />
13
+ ```
14
+
15
+ It's controlled: pass `value` (one option's `id`) and `onChange`. A single brand-filled thumb is sized to one cell and translated by the active index, so the highlight animates **linearly across the list** for any number of options — there is no per-count CSS. The geometry is driven by `--seg-count` / `--seg-index` set inline on the root; the transition respects `prefers-reduced-motion`.
16
+
17
+ `label` is the tablist's accessible name (the control is `role="tablist"`, each segment `role="tab"`). Options render in array order. An unknown `value` parks the thumb on the first cell.
18
+
19
+ Pass `testId(id)` for per-segment `data-testid`s (the sidebar mode switch uses `DcTestIds.modeSwitch`). Pass `className` for layout-context placement (e.g. the sidebar adds `dc-modeswitch` for pinning); the control's own appearance stays self-contained.
20
+
21
+ For a single pill, reach for `Chip`; for a stepper with Prev/Next, `FlowNav`.
@@ -0,0 +1,81 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { renderToStaticMarkup } from 'react-dom/server'
3
+ import { SegmentedToggle } from './SegmentedToggle'
4
+
5
+ const options = [
6
+ { id: 'light', label: 'Light' },
7
+ { id: 'dark', label: 'Dark' },
8
+ { id: 'system', label: 'System' },
9
+ ] as const
10
+
11
+ const noop = () => {}
12
+
13
+ describe('SegmentedToggle', () => {
14
+ test('renders a labelled tablist of tabs', () => {
15
+ const html = renderToStaticMarkup(
16
+ <SegmentedToggle
17
+ options={[...options]}
18
+ value="light"
19
+ onChange={noop}
20
+ label="Theme"
21
+ />,
22
+ )
23
+ expect(html).toContain('role="tablist"')
24
+ expect(html).toContain('aria-label="Theme"')
25
+ expect((html.match(/role="tab"/g) ?? []).length).toBe(3)
26
+ })
27
+
28
+ test('marks the selected option as the active, selected tab', () => {
29
+ const html = renderToStaticMarkup(
30
+ <SegmentedToggle
31
+ options={[...options]}
32
+ value="dark"
33
+ onChange={noop}
34
+ label="Theme"
35
+ />,
36
+ )
37
+ // Exactly one tab is selected/active.
38
+ expect((html.match(/aria-selected="true"/g) ?? []).length).toBe(1)
39
+ expect((html.match(/data-active="true"/g) ?? []).length).toBe(1)
40
+ })
41
+
42
+ test('drives the thumb geometry from --seg-count and --seg-index', () => {
43
+ const html = renderToStaticMarkup(
44
+ <SegmentedToggle
45
+ options={[...options]}
46
+ value="system"
47
+ onChange={noop}
48
+ label="Theme"
49
+ />,
50
+ )
51
+ expect(html).toContain('--seg-count:3')
52
+ expect(html).toContain('--seg-index:2')
53
+ })
54
+
55
+ test('clamps an unknown value to the first cell rather than a negative index', () => {
56
+ const html = renderToStaticMarkup(
57
+ <SegmentedToggle
58
+ options={[...options]}
59
+ value={'gone' as never}
60
+ onChange={noop}
61
+ label="Theme"
62
+ />,
63
+ )
64
+ expect(html).toContain('--seg-index:0')
65
+ expect(html).not.toContain('aria-selected="true"')
66
+ })
67
+
68
+ test('applies a per-segment test id factory when given', () => {
69
+ const html = renderToStaticMarkup(
70
+ <SegmentedToggle
71
+ options={[...options]}
72
+ value="light"
73
+ onChange={noop}
74
+ label="Theme"
75
+ testId={(id) => `seg-${id}`}
76
+ />,
77
+ )
78
+ expect(html).toContain('data-testid="seg-light"')
79
+ expect(html).toContain('data-testid="seg-system"')
80
+ })
81
+ })
@@ -0,0 +1,75 @@
1
+ import type { CSSProperties, ReactNode } from 'react'
2
+
3
+ /**
4
+ * Display Case — SegmentedToggle
5
+ * An isolated, multi-option segmented control. Takes any number of options and
6
+ * animates a single brand-filled thumb linearly from one to the next: the thumb
7
+ * is sized to one cell and translated by `index` cells, so the highlight lerps
8
+ * across the list however many options there are — no per-count CSS.
9
+ *
10
+ * The geometry is driven entirely by two custom properties set inline on the
11
+ * root (`--seg-count`, `--seg-index`); the CSS below is option-count agnostic.
12
+ */
13
+
14
+ export interface SegmentedOption<T extends string> {
15
+ id: T
16
+ label: ReactNode
17
+ }
18
+
19
+ export interface SegmentedToggleProps<T extends string> {
20
+ /** The options, in display order. The thumb slides across them by index. */
21
+ options: SegmentedOption<T>[]
22
+ /** The currently selected option id. */
23
+ value: T
24
+ /** Called with the chosen option id when a segment is activated. */
25
+ onChange: (id: T) => void
26
+ /** Accessible name for the tablist (e.g. "View mode"). */
27
+ label: string
28
+ /** Optional className applied to the root, for layout-context placement. */
29
+ className?: string
30
+ /** Optional per-segment `data-testid` factory. */
31
+ testId?: (id: T) => string
32
+ }
33
+
34
+ export function SegmentedToggle<T extends string>({
35
+ options,
36
+ value,
37
+ onChange,
38
+ label,
39
+ className,
40
+ testId,
41
+ }: SegmentedToggleProps<T>) {
42
+ // Clamp to 0 so an unknown `value` parks the thumb on the first cell rather
43
+ // than translating it off the track by a negative index.
44
+ const activeIndex = Math.max(
45
+ 0,
46
+ options.findIndex((o) => o.id === value),
47
+ )
48
+ return (
49
+ <div
50
+ className={className ? `dcui-segmented ${className}` : 'dcui-segmented'}
51
+ role="tablist"
52
+ aria-label={label}
53
+ style={
54
+ {
55
+ '--seg-count': options.length,
56
+ '--seg-index': activeIndex,
57
+ } as CSSProperties
58
+ }>
59
+ <span className="dcui-segmented-thumb" aria-hidden="true" />
60
+ {options.map((opt) => (
61
+ <button
62
+ key={opt.id}
63
+ type="button"
64
+ role="tab"
65
+ aria-selected={value === opt.id}
66
+ className="dcui-segmented-seg"
67
+ data-testid={testId?.(opt.id)}
68
+ data-active={value === opt.id ? 'true' : undefined}
69
+ onClick={() => onChange(opt.id)}>
70
+ {opt.label}
71
+ </button>
72
+ ))}
73
+ </div>
74
+ )
75
+ }
@@ -0,0 +1,67 @@
1
+ import { defineCases, tweak } from '@awarebydefault/display-case'
2
+ import { Eyebrow } from './Eyebrow'
3
+ import { NavItem } from './NavItem'
4
+ import { Sidebar } from './Sidebar'
5
+
6
+ export default defineCases(
7
+ 'Sidebar',
8
+ {
9
+ Playground: {
10
+ tweaks: {
11
+ label: tweak.text('Components'),
12
+ width: tweak.number(15),
13
+ maxHeight: tweak.number(24),
14
+ showEyebrow: tweak.boolean(true),
15
+ },
16
+ render: (t) => (
17
+ <Sidebar
18
+ label={t.label}
19
+ style={{ width: `${t.width}rem`, maxHeight: `${t.maxHeight}rem` }}>
20
+ {t.showEyebrow && (
21
+ <Eyebrow style={{ margin: '0 0 0.5rem 0.5rem' }}>Atoms</Eyebrow>
22
+ )}
23
+ <NavItem
24
+ kind="component"
25
+ label="Button"
26
+ count={4}
27
+ expanded
28
+ onToggle={() => {}}
29
+ onSelect={() => {}}
30
+ />
31
+ <NavItem kind="case" label="Playground" onSelect={() => {}} />
32
+ <NavItem kind="case" label="Variants" current onSelect={() => {}} />
33
+ <NavItem kind="case" label="Sizes" onSelect={() => {}} />
34
+ <NavItem
35
+ kind="component"
36
+ label="Checkbox"
37
+ onToggle={() => {}}
38
+ onSelect={() => {}}
39
+ />
40
+ </Sidebar>
41
+ ),
42
+ },
43
+ Tree: () => (
44
+ <Sidebar style={{ width: '15rem', maxHeight: '24rem' }}>
45
+ <Eyebrow style={{ margin: '0 0 0.5rem 0.5rem' }}>Atoms</Eyebrow>
46
+ <NavItem
47
+ kind="component"
48
+ label="Button"
49
+ count={4}
50
+ expanded
51
+ onToggle={() => {}}
52
+ onSelect={() => {}}
53
+ />
54
+ <NavItem kind="case" label="Playground" onSelect={() => {}} />
55
+ <NavItem kind="case" label="Variants" current onSelect={() => {}} />
56
+ <NavItem kind="case" label="Sizes" onSelect={() => {}} />
57
+ <NavItem
58
+ kind="component"
59
+ label="Checkbox"
60
+ onToggle={() => {}}
61
+ onSelect={() => {}}
62
+ />
63
+ </Sidebar>
64
+ ),
65
+ },
66
+ { level: 'organism' },
67
+ )
@@ -0,0 +1,6 @@
1
+ .dcui-sidebar {
2
+ overflow-y: auto;
3
+ padding: var(--dc-space-6);
4
+ border-right: var(--dc-border-line);
5
+ background: var(--dc-bg-subtle);
6
+ }
@@ -0,0 +1,14 @@
1
+ **Sidebar** — the nav rail: a scrolling, hairline-bordered column on the subtle backdrop that holds the component tree; reach for it as the ground `NavItem` rows sit on (rows are transparent and only read correctly against this surface).
2
+
3
+ ```tsx
4
+ <Sidebar label="Components">
5
+ <NavItem kind="component" label="Button" count={4} expanded onToggle={toggle} />
6
+ <NavItem kind="case" label="Variants" current onSelect={select} />
7
+ </Sidebar>
8
+ ```
9
+
10
+ Layout (grid placement, collapse) stays the chrome's; the surface is the component's.
11
+
12
+ - **label**: accessible name for the `<nav>` landmark; pass the contextual name (default `"Navigation"` is a generic fallback)
13
+ - **children**: `NavItem` rows (typically grouped by hierarchy level)
14
+ - spreads remaining props onto the `<nav>`
@@ -0,0 +1,32 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { renderToStaticMarkup } from 'react-dom/server'
3
+ import { Sidebar } from './Sidebar'
4
+
5
+ describe('Sidebar', () => {
6
+ test('renders a nav landmark with a generic default label', () => {
7
+ const html = renderToStaticMarkup(
8
+ <Sidebar>
9
+ <span>rows</span>
10
+ </Sidebar>,
11
+ )
12
+ expect(html).toContain('<nav')
13
+ expect(html).toContain('class="dcui-sidebar"')
14
+ expect(html).toContain('aria-label="Navigation"')
15
+ expect(html).toContain('rows')
16
+ })
17
+
18
+ test('uses a contextual landmark label when given', () => {
19
+ const html = renderToStaticMarkup(<Sidebar label="Components">x</Sidebar>)
20
+ expect(html).toContain('aria-label="Components"')
21
+ })
22
+
23
+ test('forwards arbitrary attributes to the nav element', () => {
24
+ const html = renderToStaticMarkup(
25
+ <Sidebar id="rail" data-testid="sidebar">
26
+ x
27
+ </Sidebar>,
28
+ )
29
+ expect(html).toContain('id="rail"')
30
+ expect(html).toContain('data-testid="sidebar"')
31
+ })
32
+ })
@@ -0,0 +1,30 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react'
2
+
3
+ /**
4
+ * Display Case — Sidebar
5
+ * The nav rail: a scrolling, hairline-bordered column on the subtle backdrop
6
+ * that holds the component tree. It's the ground NavItem rows are designed to
7
+ * sit on — the rows are transparent and only read correctly against this
8
+ * `--dc-bg-subtle` surface. Layout (grid placement, collapse) is the chrome's;
9
+ * the surface is the component's.
10
+ */
11
+
12
+ export interface SidebarProps extends HTMLAttributes<HTMLElement> {
13
+ /** Accessible name for the nav landmark; consumers should pass the contextual
14
+ * name (the chrome uses "Components"). Defaults to a generic fallback so the
15
+ * landmark always has an accessible name. */
16
+ label?: string
17
+ children?: ReactNode
18
+ }
19
+
20
+ export function Sidebar({
21
+ label = 'Navigation',
22
+ children,
23
+ ...rest
24
+ }: SidebarProps) {
25
+ return (
26
+ <nav className="dcui-sidebar" aria-label={label} {...rest}>
27
+ {children}
28
+ </nav>
29
+ )
30
+ }
@@ -0,0 +1,51 @@
1
+ import { defineCases, tweak } from '@awarebydefault/display-case'
2
+ import { Button } from '../controls/Button'
3
+ import { Stage } from './Stage'
4
+
5
+ const box = { width: '24rem', height: '12rem' }
6
+
7
+ export default defineCases(
8
+ 'Stage',
9
+ {
10
+ Playground: {
11
+ tweaks: {
12
+ caption: tweak.text('button / playground'),
13
+ meta: tweak.text('390 × 844'),
14
+ grid: tweak.boolean(true),
15
+ corners: tweak.boolean(false),
16
+ },
17
+ render: (t) => (
18
+ <Stage
19
+ caption={t.caption || undefined}
20
+ meta={t.meta || undefined}
21
+ frame="fill"
22
+ grid={t.grid}
23
+ corners={t.corners}
24
+ style={box}>
25
+ <Button>On the stage</Button>
26
+ </Stage>
27
+ ),
28
+ },
29
+ Default: () => (
30
+ <Stage frame="fill" style={box}>
31
+ <Button>On the stage</Button>
32
+ </Stage>
33
+ ),
34
+ Grid: () => (
35
+ <Stage frame="fill" grid style={box}>
36
+ <Button variant="accent">On the grid</Button>
37
+ </Stage>
38
+ ),
39
+ Captioned: () => (
40
+ <Stage
41
+ caption="button / playground"
42
+ meta="390 × 844"
43
+ frame="fill"
44
+ grid
45
+ style={box}>
46
+ <Button>Captioned</Button>
47
+ </Stage>
48
+ ),
49
+ },
50
+ { level: 'molecule' },
51
+ )
@@ -0,0 +1,91 @@
1
+ .dcui-stage {
2
+ display: flex;
3
+ flex-direction: column;
4
+ box-sizing: border-box;
5
+ border: 1px solid var(--dc-border);
6
+ border-radius: var(--dc-radius-md);
7
+ background: var(--dc-surface);
8
+ overflow: hidden;
9
+ }
10
+ /* Live-frame sizing (the browse chrome's preview). */
11
+ .dcui-stage[data-frame="hug"] {
12
+ flex: 0 0 auto;
13
+ min-width: min(22rem, 100%);
14
+ min-height: min(11rem, 100%);
15
+ max-width: 100%;
16
+ max-height: 100%;
17
+ }
18
+ .dcui-stage[data-frame="fill"] {
19
+ align-self: stretch;
20
+ width: 100%;
21
+ }
22
+ .dcui-stage-caption {
23
+ display: flex;
24
+ align-items: center;
25
+ justify-content: space-between;
26
+ gap: var(--dc-space-4);
27
+ padding: var(--dc-space-4) var(--dc-space-6);
28
+ border-bottom: 1px solid var(--dc-border);
29
+ background: var(--dc-bg-subtle);
30
+ }
31
+ .dcui-stage-caption-label {
32
+ font-family: var(--dc-font-mono);
33
+ font-size: var(--dc-text-xs);
34
+ font-weight: var(--dc-weight-medium);
35
+ letter-spacing: var(--dc-tracking-label);
36
+ text-transform: uppercase;
37
+ color: var(--dc-fg-muted);
38
+ }
39
+ .dcui-stage-caption-meta {
40
+ font-family: var(--dc-font-mono);
41
+ font-size: var(--dc-text-xs);
42
+ color: var(--dc-fg-subtle);
43
+ }
44
+ .dcui-stage-body {
45
+ position: relative;
46
+ flex: 1;
47
+ display: flex;
48
+ align-items: center;
49
+ justify-content: center;
50
+ min-height: 0;
51
+ padding: 0;
52
+ }
53
+ /* hug-frame sets the body padding inline (the dynamic grid margin); a filled
54
+ frame stays edge-to-edge. */
55
+ .dcui-stage[data-grid="true"] .dcui-stage-body {
56
+ background-image: radial-gradient(var(--dc-border) 1px, transparent 1px);
57
+ background-size: 16px 16px;
58
+ background-position: -1px -1px;
59
+ }
60
+ .dcui-stage-corner {
61
+ position: absolute;
62
+ width: 9px;
63
+ height: 9px;
64
+ border: 1.5px solid var(--dc-border-strong);
65
+ pointer-events: none;
66
+ z-index: 1;
67
+ }
68
+ .dcui-stage-corner[data-c="tl"] {
69
+ top: 8px;
70
+ left: 8px;
71
+ border-right: 0;
72
+ border-bottom: 0;
73
+ }
74
+ .dcui-stage-corner[data-c="tr"] {
75
+ top: 8px;
76
+ right: 8px;
77
+ border-left: 0;
78
+ border-bottom: 0;
79
+ }
80
+ .dcui-stage-corner[data-c="bl"] {
81
+ bottom: 8px;
82
+ left: 8px;
83
+ border-right: 0;
84
+ border-top: 0;
85
+ }
86
+ .dcui-stage-corner[data-c="br"] {
87
+ bottom: 8px;
88
+ right: 8px;
89
+ border-left: 0;
90
+ border-top: 0;
91
+ }
@@ -0,0 +1,15 @@
1
+ **Stage** — the vitrine: the framed surface a component is exhibited on (hairline border, soft corner ticks, optional dotted grid, optional mono caption); reach for it to present one component. Keep it quiet — the exhibit leads.
2
+
3
+ The browse chrome's preview stage, sized by `frame` rather than centering a card. `frame="hug"` shrinks to the exhibit with a minimum size and a dynamic grid margin (`padX`/`padY`, in px); `frame="fill"` stretches edge-to-edge for full pages.
4
+
5
+ ```tsx
6
+ <Stage frame="hug" padX={48} padY={32} surface="var(--color-bg)">
7
+ <App />
8
+ </Stage>
9
+ ```
10
+
11
+ - **frame** (required): `hug` (shrink to the exhibit) · `fill` (stretch edge-to-edge)
12
+ - **caption** / **meta**: mono caption strip (label left, meta right)
13
+ - **grid**: dotted graph-paper backdrop · **corners**: corner ticks (default on)
14
+ - **padX** / **padY**: dynamic grid-margin padding (px) for `frame="hug"`
15
+ - **surface**: override the body backdrop colour