@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,52 @@
1
+ import { defineCases, tweak } from '@awarebydefault/display-case'
2
+ import { Button } from './Button'
3
+
4
+ export default defineCases(
5
+ 'Button',
6
+ {
7
+ Playground: {
8
+ tweaks: {
9
+ label: tweak.text('Button'),
10
+ variant: tweak.choice(
11
+ ['ghost', 'primary', 'accent', 'subtle'],
12
+ 'ghost',
13
+ ),
14
+ size: tweak.choice(['sm', 'md', 'lg'], 'md'),
15
+ pressed: tweak.boolean(false),
16
+ disabled: tweak.boolean(false),
17
+ },
18
+ render: (t) => (
19
+ <Button
20
+ variant={t.variant as 'ghost' | 'primary' | 'accent' | 'subtle'}
21
+ size={t.size as 'sm' | 'md' | 'lg'}
22
+ aria-pressed={t.pressed}
23
+ disabled={t.disabled}>
24
+ {t.label}
25
+ </Button>
26
+ ),
27
+ },
28
+ Variants: () => (
29
+ <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
30
+ <Button>Ghost</Button>
31
+ <Button variant="primary">Primary</Button>
32
+ <Button variant="accent">Accent</Button>
33
+ <Button variant="subtle">Subtle</Button>
34
+ </div>
35
+ ),
36
+ Sizes: () => (
37
+ <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
38
+ <Button size="sm">Small</Button>
39
+ <Button size="md">Medium</Button>
40
+ <Button size="lg">Large</Button>
41
+ </div>
42
+ ),
43
+ Toggle: () => (
44
+ <div style={{ display: 'flex', gap: 8 }}>
45
+ <Button aria-pressed={false}>Off</Button>
46
+ <Button aria-pressed>On</Button>
47
+ </div>
48
+ ),
49
+ Disabled: () => <Button disabled>Disabled</Button>,
50
+ },
51
+ { level: 'atom' },
52
+ )
@@ -0,0 +1,89 @@
1
+ .dcui-btn {
2
+ --_bg: var(--dc-surface);
3
+ --_fg: var(--dc-fg);
4
+ --_bd: var(--dc-border);
5
+ display: inline-flex;
6
+ align-items: center;
7
+ justify-content: center;
8
+ gap: var(--dc-space-2);
9
+ font-family: var(--dc-font-sans);
10
+ font-size: var(--dc-text-base);
11
+ font-weight: var(--dc-weight-medium);
12
+ line-height: 1;
13
+ color: var(--_fg);
14
+ background: var(--_bg);
15
+ border: 1px solid var(--_bd);
16
+ border-radius: var(--dc-radius-sm);
17
+ padding: 0 var(--dc-space-6);
18
+ height: 30px;
19
+ cursor: pointer;
20
+ white-space: nowrap;
21
+ transition:
22
+ background var(--dc-transition-fast),
23
+ border-color var(--dc-transition-fast),
24
+ color var(--dc-transition-fast),
25
+ transform var(--dc-transition-fast);
26
+ }
27
+ .dcui-btn:hover {
28
+ background: var(--dc-hover);
29
+ }
30
+ .dcui-btn:active {
31
+ transform: translateY(0.5px);
32
+ }
33
+ .dcui-btn:focus-visible {
34
+ outline: 2px solid var(--dc-focus-ring);
35
+ outline-offset: 1px;
36
+ }
37
+ .dcui-btn[data-size="sm"] {
38
+ height: 26px;
39
+ font-size: var(--dc-text-sm);
40
+ padding: 0 var(--dc-space-4);
41
+ }
42
+ .dcui-btn[data-size="lg"] {
43
+ height: 36px;
44
+ padding: 0 var(--dc-space-8);
45
+ }
46
+
47
+ .dcui-btn[data-variant="primary"] {
48
+ --_bg: var(--dc-ink);
49
+ --_fg: var(--dc-ink-fg);
50
+ --_bd: var(--dc-ink);
51
+ }
52
+ .dcui-btn[data-variant="primary"]:hover {
53
+ background: var(--dc-ink-hover);
54
+ border-color: var(--dc-ink-hover);
55
+ }
56
+ .dcui-btn[data-variant="accent"] {
57
+ --_bg: var(--dc-brand);
58
+ --_fg: var(--dc-brand-fg);
59
+ --_bd: var(--dc-brand);
60
+ }
61
+ .dcui-btn[data-variant="accent"]:hover {
62
+ background: var(--dc-brand-hover);
63
+ border-color: var(--dc-brand-hover);
64
+ }
65
+ .dcui-btn[data-variant="subtle"] {
66
+ --_bg: transparent;
67
+ --_bd: transparent;
68
+ --_fg: var(--dc-fg-muted);
69
+ }
70
+ .dcui-btn[data-variant="subtle"]:hover {
71
+ background: var(--dc-hover);
72
+ color: var(--dc-fg);
73
+ }
74
+
75
+ /* Toggle "on" — marigold (the active selection colour). */
76
+ .dcui-btn[aria-pressed="true"] {
77
+ --_fg: var(--dc-brand);
78
+ --_bd: var(--dc-brand);
79
+ --_bg: var(--dc-brand-subtle);
80
+ }
81
+
82
+ .dcui-btn:disabled {
83
+ opacity: 0.45;
84
+ cursor: not-allowed;
85
+ }
86
+ .dcui-btn:disabled:hover {
87
+ background: var(--_bg);
88
+ transform: none;
89
+ }
@@ -0,0 +1,14 @@
1
+ **Button** — the quiet, bordered text control the Display Case chrome leans on; reach for it for any labelled action.
2
+
3
+ ```tsx
4
+ <Button>Docs</Button>
5
+ <Button variant="primary">Run check</Button>
6
+ <Button variant="accent">Send magic link</Button>
7
+ <Button aria-pressed>Grid</Button>
8
+ ```
9
+
10
+ Variants: `ghost` (default, recedes) · `primary` (warm ink, the emphatic action) · `accent` (marigold, rare standout) · `subtle` (borderless). A toggle button lights marigold when you pass `aria-pressed`.
11
+
12
+ For a glyph-only square control, reach for `IconButton`.
13
+
14
+ It is a real `<button>` — pass `disabled`, `onClick`, `aria-pressed`, etc.
@@ -0,0 +1,45 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { renderToStaticMarkup } from 'react-dom/server'
3
+ import { Button } from './Button'
4
+
5
+ describe('Button', () => {
6
+ test('defaults to a ghost, medium, type=button control', () => {
7
+ const html = renderToStaticMarkup(<Button>Go</Button>)
8
+ expect(html).toContain('class="dcui-btn"')
9
+ expect(html).toContain('data-variant="ghost"')
10
+ expect(html).toContain('data-size="md"')
11
+ expect(html).toContain('type="button"')
12
+ expect(html).toContain('Go')
13
+ })
14
+
15
+ test('reflects the variant and size props as data attributes', () => {
16
+ const html = renderToStaticMarkup(
17
+ <Button variant="accent" size="lg">
18
+ Buy
19
+ </Button>,
20
+ )
21
+ expect(html).toContain('data-variant="accent"')
22
+ expect(html).toContain('data-size="lg"')
23
+ })
24
+
25
+ test('a caller-supplied type overrides the button default', () => {
26
+ const html = renderToStaticMarkup(<Button type="submit">Send</Button>)
27
+ expect(html).toContain('type="submit"')
28
+ })
29
+
30
+ test('renders the marigold toggle-on state from aria-pressed', () => {
31
+ const html = renderToStaticMarkup(<Button aria-pressed={true}>On</Button>)
32
+ expect(html).toContain('aria-pressed="true"')
33
+ })
34
+
35
+ test('passes arbitrary button attributes through to the element', () => {
36
+ const html = renderToStaticMarkup(
37
+ <Button disabled data-testid="x" title="hint">
38
+ Off
39
+ </Button>,
40
+ )
41
+ expect(html).toContain('disabled')
42
+ expect(html).toContain('data-testid="x"')
43
+ expect(html).toContain('title="hint"')
44
+ })
45
+ })
@@ -0,0 +1,41 @@
1
+ import type { ButtonHTMLAttributes, ReactNode } from 'react'
2
+
3
+ /**
4
+ * Display Case — Button
5
+ * The quiet, bordered control the chrome leans on. `ghost` is the default (it
6
+ * recedes); `primary` (warm ink) and `accent` (marigold) are for the rare
7
+ * emphatic action; `subtle` is borderless. A toggle button lights marigold via
8
+ * `aria-pressed`.
9
+ *
10
+ * Styling lives in the sibling `Button.css`, concatenated into the Vitrine
11
+ * stylesheet and inlined into every document head server-side (see
12
+ * `readVitrineCss` in server.ts) — no runtime injection.
13
+ */
14
+
15
+ export type ButtonVariant = 'ghost' | 'primary' | 'accent' | 'subtle'
16
+ export type ButtonSize = 'sm' | 'md' | 'lg'
17
+
18
+ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
19
+ variant?: ButtonVariant
20
+ size?: ButtonSize
21
+ children?: ReactNode
22
+ }
23
+
24
+ export function Button({
25
+ variant = 'ghost',
26
+ size = 'md',
27
+ type = 'button',
28
+ children,
29
+ ...rest
30
+ }: ButtonProps) {
31
+ return (
32
+ <button
33
+ type={type}
34
+ className="dcui-btn"
35
+ data-variant={variant}
36
+ data-size={size}
37
+ {...rest}>
38
+ {children}
39
+ </button>
40
+ )
41
+ }
@@ -0,0 +1,52 @@
1
+ import { defineCases, tweak } from '@awarebydefault/display-case'
2
+ import { IconButton } from './IconButton'
3
+
4
+ export default defineCases(
5
+ 'IconButton',
6
+ {
7
+ Playground: {
8
+ tweaks: {
9
+ glyph: tweak.text('✕'),
10
+ label: tweak.text('Close'),
11
+ size: tweak.choice(['sm', 'md', 'lg'], 'md'),
12
+ variant: tweak.choice(['outline', 'bare'], 'outline'),
13
+ active: tweak.boolean(false),
14
+ disabled: tweak.boolean(false),
15
+ },
16
+ render: (t) => (
17
+ <IconButton
18
+ glyph={t.glyph}
19
+ label={t.label}
20
+ size={t.size as 'sm' | 'md' | 'lg'}
21
+ variant={t.variant as 'outline' | 'bare'}
22
+ active={t.active}
23
+ disabled={t.disabled}
24
+ />
25
+ ),
26
+ },
27
+ Glyphs: () => (
28
+ <div style={{ display: 'flex', gap: 8 }}>
29
+ <IconButton glyph="☰" label="Toggle navigation" />
30
+ <IconButton glyph="⟲" label="Rotate" />
31
+ <IconButton glyph="✕" label="Close" />
32
+ <IconButton glyph="+" label="Zoom in" />
33
+ </div>
34
+ ),
35
+ Sizes: () => (
36
+ <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
37
+ <IconButton glyph="✕" label="Small" size="sm" />
38
+ <IconButton glyph="✕" label="Medium" size="md" />
39
+ <IconButton glyph="✕" label="Large" size="lg" />
40
+ </div>
41
+ ),
42
+ States: () => (
43
+ <div style={{ display: 'flex', gap: 8 }}>
44
+ <IconButton glyph="⬓" label="Default" />
45
+ <IconButton glyph="▭" label="Active" active />
46
+ <IconButton glyph="▭" label="Bare" variant="bare" />
47
+ <IconButton glyph="✕" label="Disabled" disabled />
48
+ </div>
49
+ ),
50
+ },
51
+ { level: 'atom' },
52
+ )
@@ -0,0 +1,67 @@
1
+ .dcui-iconbtn {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ justify-content: center;
5
+ width: 30px;
6
+ height: 30px;
7
+ font-family: var(--dc-font-mono);
8
+ font-size: var(--dc-text-md);
9
+ line-height: 1;
10
+ color: var(--dc-fg);
11
+ background: var(--dc-surface);
12
+ border: 1px solid var(--dc-border);
13
+ border-radius: var(--dc-radius-sm);
14
+ cursor: pointer;
15
+ transition:
16
+ background var(--dc-transition-fast),
17
+ color var(--dc-transition-fast),
18
+ border-color var(--dc-transition-fast),
19
+ transform var(--dc-transition-fast);
20
+ }
21
+ .dcui-iconbtn:hover {
22
+ background: var(--dc-hover);
23
+ }
24
+ .dcui-iconbtn:active {
25
+ background: var(--dc-active);
26
+ transform: translateY(0.5px);
27
+ }
28
+ .dcui-iconbtn:focus-visible {
29
+ outline: 2px solid var(--dc-focus-ring);
30
+ outline-offset: 1px;
31
+ }
32
+ .dcui-iconbtn[data-size="sm"] {
33
+ width: 26px;
34
+ height: 26px;
35
+ font-size: var(--dc-text-base);
36
+ }
37
+ .dcui-iconbtn[data-size="lg"] {
38
+ width: 36px;
39
+ height: 36px;
40
+ font-size: var(--dc-text-lg);
41
+ }
42
+ .dcui-iconbtn[aria-pressed="true"],
43
+ .dcui-iconbtn[data-active="true"] {
44
+ color: var(--dc-brand);
45
+ border-color: var(--dc-brand);
46
+ background: var(--dc-brand-subtle);
47
+ }
48
+ .dcui-iconbtn[data-variant="bare"] {
49
+ border-color: transparent;
50
+ background: transparent;
51
+ color: var(--dc-fg-muted);
52
+ }
53
+ .dcui-iconbtn[data-variant="bare"]:hover {
54
+ background: var(--dc-hover);
55
+ color: var(--dc-fg);
56
+ }
57
+ .dcui-iconbtn:disabled {
58
+ opacity: 0.45;
59
+ cursor: not-allowed;
60
+ }
61
+ .dcui-iconbtn:disabled:hover {
62
+ background: var(--dc-surface);
63
+ transform: none;
64
+ }
65
+ .dcui-iconbtn[data-variant="bare"]:disabled:hover {
66
+ background: transparent;
67
+ }
@@ -0,0 +1,13 @@
1
+ **IconButton** — a square control carrying a single Unicode glyph (☰ ⟲ ✕ + −); reach for it for compact, glyph-only chrome actions. Display Case uses no icon font and no SVG — just glyphs.
2
+
3
+ ```tsx
4
+ <IconButton glyph="☰" label="Toggle navigation" />
5
+ <IconButton glyph="⟲" label="Rotate" variant="bare" />
6
+ <IconButton glyph="▭" label="Dock" active />
7
+ ```
8
+
9
+ `label` is required — it is the accessible name, since the button shows no text. Variants: `outline` (default) · `bare` (no border).
10
+
11
+ `active` is persistent emphasis (a row that stays lit); `aria-pressed` is a true toggle (on/off). Both light marigold. For text or labelled actions, use `Button`.
12
+
13
+ It is a real `<button>` — pass `disabled`, `onClick`, etc.
@@ -0,0 +1,39 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { renderToStaticMarkup } from 'react-dom/server'
3
+ import { IconButton } from './IconButton'
4
+
5
+ describe('IconButton', () => {
6
+ test('renders the glyph and exposes the required label as the accessible name', () => {
7
+ const html = renderToStaticMarkup(<IconButton glyph="☰" label="Menu" />)
8
+ expect(html).toContain('class="dcui-iconbtn"')
9
+ expect(html).toContain('aria-label="Menu"')
10
+ expect(html).toContain('☰')
11
+ expect(html).toContain('type="button"')
12
+ })
13
+
14
+ test('defaults to the outline variant at medium size', () => {
15
+ const html = renderToStaticMarkup(<IconButton glyph="+" label="Add" />)
16
+ expect(html).toContain('data-variant="outline"')
17
+ expect(html).toContain('data-size="md"')
18
+ })
19
+
20
+ test('falls back to children when no glyph is given', () => {
21
+ const html = renderToStaticMarkup(<IconButton label="Close">✕</IconButton>)
22
+ expect(html).toContain('✕')
23
+ })
24
+
25
+ test('lights the active state via data-active only when active', () => {
26
+ const on = renderToStaticMarkup(<IconButton glyph="●" label="On" active />)
27
+ const off = renderToStaticMarkup(<IconButton glyph="●" label="Off" />)
28
+ expect(on).toContain('data-active="true"')
29
+ expect(off).not.toContain('data-active')
30
+ })
31
+
32
+ test('reflects the bare variant and size', () => {
33
+ const html = renderToStaticMarkup(
34
+ <IconButton glyph="⟲" label="Reset" variant="bare" size="sm" />,
35
+ )
36
+ expect(html).toContain('data-variant="bare"')
37
+ expect(html).toContain('data-size="sm"')
38
+ })
39
+ })
@@ -0,0 +1,47 @@
1
+ import type { ButtonHTMLAttributes, ReactNode } from 'react'
2
+
3
+ /**
4
+ * Display Case — IconButton
5
+ * A square control carrying a single Unicode glyph (☰ ⟲ ✕ + −). Display Case
6
+ * uses no icon font and no SVG icons — just glyphs. `bare` drops the border;
7
+ * `active` / `aria-pressed` light it marigold.
8
+ */
9
+
10
+ export type IconButtonSize = 'sm' | 'md' | 'lg'
11
+ export type IconButtonVariant = 'outline' | 'bare'
12
+
13
+ export interface IconButtonProps
14
+ extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'aria-label'> {
15
+ /** The glyph to render (alternatively pass `children`). */
16
+ glyph?: ReactNode
17
+ size?: IconButtonSize
18
+ variant?: IconButtonVariant
19
+ /** Persistent "on" state (marigold), for non-toggle emphasis. */
20
+ active?: boolean
21
+ /** Accessible name — required since the button is glyph-only. */
22
+ label: string
23
+ }
24
+
25
+ export function IconButton({
26
+ glyph,
27
+ size = 'md',
28
+ variant = 'outline',
29
+ active = false,
30
+ type = 'button',
31
+ label,
32
+ children,
33
+ ...rest
34
+ }: IconButtonProps) {
35
+ return (
36
+ <button
37
+ type={type}
38
+ className="dcui-iconbtn"
39
+ data-size={size}
40
+ data-variant={variant}
41
+ data-active={active ? 'true' : undefined}
42
+ aria-label={label}
43
+ {...rest}>
44
+ {glyph ?? children}
45
+ </button>
46
+ )
47
+ }
@@ -0,0 +1,50 @@
1
+ import { defineCases, tweak } from '@awarebydefault/display-case'
2
+ import { Input } from './Input'
3
+
4
+ export default defineCases(
5
+ 'Input',
6
+ {
7
+ Playground: {
8
+ tweaks: {
9
+ placeholder: tweak.text('filter by name'),
10
+ value: tweak.text(''),
11
+ prefix: tweak.text(''),
12
+ suffix: tweak.text(''),
13
+ type: tweak.choice(['text', 'number', 'search'], 'text'),
14
+ size: tweak.choice(['sm', 'md'], 'md'),
15
+ disabled: tweak.boolean(false),
16
+ },
17
+ render: (t) => (
18
+ <Input
19
+ placeholder={t.placeholder}
20
+ defaultValue={t.value || undefined}
21
+ prefix={t.prefix || undefined}
22
+ suffix={t.suffix || undefined}
23
+ type={t.type}
24
+ size={t.size as 'sm' | 'md'}
25
+ disabled={t.disabled}
26
+ wrapperStyle={{ width: '14rem' }}
27
+ />
28
+ ),
29
+ },
30
+ Default: () => <Input placeholder="filter by name" />,
31
+ WithAffixes: () => (
32
+ <Input
33
+ aria-label="Width in pixels"
34
+ type="number"
35
+ defaultValue={1280}
36
+ prefix="W"
37
+ suffix="px"
38
+ wrapperStyle={{ width: '7rem' }}
39
+ />
40
+ ),
41
+ Sizes: () => (
42
+ <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
43
+ <Input size="sm" placeholder="Small" />
44
+ <Input size="md" placeholder="Medium" />
45
+ </div>
46
+ ),
47
+ Disabled: () => <Input placeholder="disabled" disabled />,
48
+ },
49
+ { level: 'atom' },
50
+ )
@@ -0,0 +1,52 @@
1
+ .dcui-field {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ gap: var(--dc-space-2);
5
+ font-family: var(--dc-font-sans);
6
+ font-size: var(--dc-text-base);
7
+ color: var(--dc-fg);
8
+ background: var(--dc-surface);
9
+ border: 1px solid var(--dc-border);
10
+ border-radius: var(--dc-radius-sm);
11
+ padding: 0 var(--dc-space-4);
12
+ height: 30px;
13
+ transition:
14
+ border-color var(--dc-transition-fast),
15
+ box-shadow var(--dc-transition-fast);
16
+ }
17
+ .dcui-field:focus-within {
18
+ border-color: var(--dc-focus-ring);
19
+ box-shadow: 0 0 0 2px var(--dc-brand-subtle);
20
+ }
21
+ .dcui-field[data-size="sm"] {
22
+ height: 26px;
23
+ font-size: var(--dc-text-sm);
24
+ }
25
+ .dcui-field[aria-disabled="true"] {
26
+ opacity: 0.5;
27
+ }
28
+ .dcui-field-input {
29
+ flex: 1;
30
+ min-width: 0;
31
+ font: inherit;
32
+ color: inherit;
33
+ background: none;
34
+ border: 0;
35
+ outline: none;
36
+ padding: 0;
37
+ width: 100%;
38
+ }
39
+ .dcui-field-input::placeholder {
40
+ color: var(--dc-fg-subtle);
41
+ }
42
+ .dcui-field-affix {
43
+ font-family: var(--dc-font-mono);
44
+ font-size: var(--dc-text-xs);
45
+ color: var(--dc-fg-muted);
46
+ flex: 0 0 auto;
47
+ }
48
+ /* tame the native number spinner so it doesn't fight the chrome */
49
+ .dcui-field-input[type="number"]::-webkit-inner-spin-button,
50
+ .dcui-field-input[type="number"]::-webkit-outer-spin-button {
51
+ opacity: 0.4;
52
+ }
@@ -0,0 +1,12 @@
1
+ **Input** — a single-line text/number field with optional mono `prefix`/`suffix` affixes sitting inside the bordered, marigold-focus box (e.g. the device-dimension fields, `1280 × 800`).
2
+
3
+ ```tsx
4
+ <Input placeholder="filter by name" value={q} onChange={(e) => setQ(e.target.value)} />
5
+ <Input type="number" prefix="W" suffix="px" defaultValue={1280} />
6
+ ```
7
+
8
+ `onChange` emits a native event — read `event.target.value`. Controlled via `value` + `onChange`; uncontrolled via `defaultValue`. It renders no label: the caller supplies a `<label>` or `aria-label`.
9
+
10
+ For a fixed set of choices, use `Select`.
11
+
12
+ Sizes: `sm` · `md` (default). Use `wrapperStyle`/`wrapperClassName` to size the field box; other native `<input>` props spread onto the inner input.
@@ -0,0 +1,43 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { renderToStaticMarkup } from 'react-dom/server'
3
+ import { Input } from './Input'
4
+
5
+ describe('Input', () => {
6
+ test('wraps a borderless input in the field shell at the default size', () => {
7
+ const html = renderToStaticMarkup(<Input placeholder="name" />)
8
+ expect(html).toContain('class="dcui-field"')
9
+ expect(html).toContain('data-size="md"')
10
+ expect(html).toContain('class="dcui-field-input"')
11
+ expect(html).toContain('placeholder="name"')
12
+ })
13
+
14
+ test('renders the prefix and suffix affix slots only when provided', () => {
15
+ const both = renderToStaticMarkup(<Input prefix="W" suffix="px" />)
16
+ const affixes = both.match(/dcui-field-affix/g) ?? []
17
+ expect(affixes).toHaveLength(2)
18
+ expect(both).toContain('>W<')
19
+ expect(both).toContain('>px<')
20
+
21
+ const none = renderToStaticMarkup(<Input />)
22
+ expect(none).not.toContain('dcui-field-affix')
23
+ })
24
+
25
+ test('disabling marks the wrapper aria-disabled and the input disabled', () => {
26
+ const html = renderToStaticMarkup(<Input disabled />)
27
+ expect(html).toContain('aria-disabled="true"')
28
+ expect(html).toContain('disabled')
29
+ })
30
+
31
+ test('appends a caller class to the wrapper rather than replacing it', () => {
32
+ const html = renderToStaticMarkup(<Input wrapperClassName="w-32" />)
33
+ expect(html).toContain('class="dcui-field w-32"')
34
+ })
35
+
36
+ test('forwards native input attributes to the inner field', () => {
37
+ const html = renderToStaticMarkup(
38
+ <Input type="number" value={800} readOnly />,
39
+ )
40
+ expect(html).toContain('type="number"')
41
+ expect(html).toContain('value="800"')
42
+ })
43
+ })
@@ -0,0 +1,45 @@
1
+ import type { CSSProperties, InputHTMLAttributes, ReactNode } from 'react'
2
+
3
+ /**
4
+ * Display Case — Input
5
+ * Text / number field. Supports a leading label slot and a trailing unit suffix
6
+ * (used by the device-dimension fields: 1280 × 800). The border + marigold
7
+ * focus ring live on the wrapper so prefix/suffix sit inside the field.
8
+ */
9
+
10
+ export type InputSize = 'sm' | 'md'
11
+
12
+ export interface InputProps
13
+ extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size' | 'prefix'> {
14
+ size?: InputSize
15
+ /** Leading label slot (mono). */
16
+ prefix?: ReactNode
17
+ /** Trailing unit suffix (mono). */
18
+ suffix?: ReactNode
19
+ /** Style applied to the field wrapper (e.g. a fixed width). */
20
+ wrapperStyle?: CSSProperties
21
+ /** Class applied to the field wrapper. */
22
+ wrapperClassName?: string
23
+ }
24
+
25
+ export function Input({
26
+ size = 'md',
27
+ prefix,
28
+ suffix,
29
+ disabled = false,
30
+ wrapperStyle,
31
+ wrapperClassName,
32
+ ...rest
33
+ }: InputProps) {
34
+ return (
35
+ <span
36
+ className={['dcui-field', wrapperClassName].filter(Boolean).join(' ')}
37
+ data-size={size}
38
+ aria-disabled={disabled ? 'true' : undefined}
39
+ style={wrapperStyle}>
40
+ {prefix ? <span className="dcui-field-affix">{prefix}</span> : null}
41
+ <input className="dcui-field-input" disabled={disabled} {...rest} />
42
+ {suffix ? <span className="dcui-field-affix">{suffix}</span> : null}
43
+ </span>
44
+ )
45
+ }