@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 { describe, expect, test } from 'bun:test'
2
+ import { renderToStaticMarkup } from 'react-dom/server'
3
+ import { Stage } from './Stage'
4
+
5
+ describe('Stage', () => {
6
+ test('renders the framed body with four corner ticks by default', () => {
7
+ const html = renderToStaticMarkup(
8
+ <Stage frame="hug">
9
+ <p>exhibit</p>
10
+ </Stage>,
11
+ )
12
+ expect(html).toContain('class="dcui-stage"')
13
+ expect(html).toContain('data-frame="hug"')
14
+ expect(html).toContain('class="dcui-stage-body"')
15
+ expect((html.match(/dcui-stage-corner/g) ?? []).length).toBe(4)
16
+ expect(html).toContain('exhibit')
17
+ })
18
+
19
+ test('omits the corner ticks when corners is false', () => {
20
+ const html = renderToStaticMarkup(
21
+ <Stage frame="fill" corners={false}>
22
+ x
23
+ </Stage>,
24
+ )
25
+ expect(html).not.toContain('dcui-stage-corner')
26
+ expect(html).toContain('data-frame="fill"')
27
+ })
28
+
29
+ test('enables the dotted grid only when grid is set', () => {
30
+ expect(
31
+ renderToStaticMarkup(
32
+ <Stage frame="hug" grid>
33
+ x
34
+ </Stage>,
35
+ ),
36
+ ).toContain('data-grid="true"')
37
+ expect(renderToStaticMarkup(<Stage frame="hug">x</Stage>)).not.toContain(
38
+ 'data-grid',
39
+ )
40
+ })
41
+
42
+ test('renders the caption strip with optional meta only when a caption is given', () => {
43
+ const withCaption = renderToStaticMarkup(
44
+ <Stage frame="hug" caption="Button" meta="atom">
45
+ x
46
+ </Stage>,
47
+ )
48
+ expect(withCaption).toContain('dcui-stage-caption')
49
+ expect(withCaption).toContain('Button')
50
+ expect(withCaption).toContain('dcui-stage-caption-meta')
51
+ expect(withCaption).toContain('atom')
52
+
53
+ const noCaption = renderToStaticMarkup(<Stage frame="hug">x</Stage>)
54
+ expect(noCaption).not.toContain('dcui-stage-caption')
55
+ })
56
+
57
+ test('omits the meta span when no caption meta is provided', () => {
58
+ const html = renderToStaticMarkup(
59
+ <Stage frame="hug" caption="Button">
60
+ x
61
+ </Stage>,
62
+ )
63
+ expect(html).toContain('dcui-stage-caption')
64
+ expect(html).not.toContain('dcui-stage-caption-meta')
65
+ })
66
+
67
+ test('applies dynamic grid-margin padding to the body when both pads are set', () => {
68
+ const html = renderToStaticMarkup(
69
+ <Stage frame="hug" padX={4} padY={8}>
70
+ x
71
+ </Stage>,
72
+ )
73
+ expect(html).toContain('padding:8px 4px')
74
+ })
75
+
76
+ test('overrides the body backdrop via the surface prop', () => {
77
+ const html = renderToStaticMarkup(
78
+ <Stage frame="fill" surface="#123456">
79
+ x
80
+ </Stage>,
81
+ )
82
+ expect(html).toContain('background:#123456')
83
+ })
84
+ })
@@ -0,0 +1,97 @@
1
+ import type { CSSProperties, ReactNode } from 'react'
2
+
3
+ /**
4
+ * Display Case — Stage
5
+ * The vitrine: the framed surface a component is exhibited on. Hairline border,
6
+ * soft corner ticks (the "case" motif), an optional dotted graph-paper grid, and
7
+ * an optional mono caption strip. Keep it quiet — the exhibit leads.
8
+ *
9
+ * The browse chrome's preview stage, sized by `frame`: `hug` shrinks to the
10
+ * exhibit with a minimum size + a dynamic grid margin (`padX`/`padY`); `fill`
11
+ * stretches edge-to-edge for full pages. `surface` overrides the body backdrop
12
+ * (e.g. the consumer app's own bg).
13
+ */
14
+
15
+ export type StageFrame = 'hug' | 'fill'
16
+
17
+ export interface StageProps {
18
+ caption?: ReactNode
19
+ meta?: ReactNode
20
+ grid?: boolean
21
+ corners?: boolean
22
+ /** Sizing mode: `hug` (shrink to the exhibit + min size) or `fill`
23
+ * (stretch edge-to-edge). */
24
+ frame: StageFrame
25
+ /** Dynamic grid-margin padding (px) for `frame="hug"`. */
26
+ padX?: number
27
+ padY?: number
28
+ /** Override the body backdrop colour (e.g. the consumer app's `--color-bg`). */
29
+ surface?: string
30
+ children?: ReactNode
31
+ style?: CSSProperties
32
+ }
33
+
34
+ export function Stage({
35
+ caption,
36
+ meta,
37
+ grid = false,
38
+ corners = true,
39
+ frame,
40
+ padX,
41
+ padY,
42
+ surface,
43
+ children,
44
+ style,
45
+ }: StageProps) {
46
+ const outerStyle: CSSProperties | undefined =
47
+ surface || style
48
+ ? { ...(surface ? { background: surface } : {}), ...style }
49
+ : undefined
50
+ const bodyStyle: CSSProperties | undefined =
51
+ padX != null && padY != null
52
+ ? { padding: `${padY}px ${padX}px` }
53
+ : undefined
54
+ return (
55
+ <div
56
+ className="dcui-stage"
57
+ data-grid={grid ? 'true' : undefined}
58
+ data-frame={frame}
59
+ style={outerStyle}>
60
+ {caption != null ? (
61
+ <div className="dcui-stage-caption">
62
+ <span className="dcui-stage-caption-label">{caption}</span>
63
+ {meta != null ? (
64
+ <span className="dcui-stage-caption-meta">{meta}</span>
65
+ ) : null}
66
+ </div>
67
+ ) : null}
68
+ <div className="dcui-stage-body" style={bodyStyle}>
69
+ {corners ? (
70
+ <>
71
+ <span
72
+ className="dcui-stage-corner"
73
+ data-c="tl"
74
+ aria-hidden="true"
75
+ />
76
+ <span
77
+ className="dcui-stage-corner"
78
+ data-c="tr"
79
+ aria-hidden="true"
80
+ />
81
+ <span
82
+ className="dcui-stage-corner"
83
+ data-c="bl"
84
+ aria-hidden="true"
85
+ />
86
+ <span
87
+ className="dcui-stage-corner"
88
+ data-c="br"
89
+ aria-hidden="true"
90
+ />
91
+ </>
92
+ ) : null}
93
+ {children}
94
+ </div>
95
+ </div>
96
+ )
97
+ }
@@ -0,0 +1,81 @@
1
+ import { defineCases, tweak } from '@awarebydefault/display-case'
2
+ import { Input } from '../controls/Input'
3
+ import { Select } from '../controls/Select'
4
+ import { TweaksPanel } from './TweaksPanel'
5
+
6
+ const items = [
7
+ {
8
+ label: 'kind',
9
+ control: (
10
+ <Select
11
+ aria-label="kind"
12
+ size="sm"
13
+ options={['text', 'number', 'boolean']}
14
+ defaultValue="number"
15
+ />
16
+ ),
17
+ },
18
+ {
19
+ label: 'label',
20
+ control: <Input aria-label="label" size="sm" defaultValue="Save changes" />,
21
+ },
22
+ {
23
+ label: 'disabled',
24
+ control: <input type="checkbox" aria-label="disabled" />,
25
+ },
26
+ ]
27
+
28
+ export default defineCases(
29
+ 'TweaksPanel',
30
+ {
31
+ Playground: {
32
+ tweaks: {
33
+ title: tweak.text('Tweaks'),
34
+ mode: tweak.choice(['docked', 'floating'], 'docked'),
35
+ url: tweak.text('?t.kind=number&t.disabled=1'),
36
+ },
37
+ render: (t) => {
38
+ const floating = t.mode === 'floating'
39
+ const panel = (
40
+ <TweaksPanel
41
+ title={t.title || undefined}
42
+ mode={t.mode as 'docked' | 'floating'}
43
+ url={t.url || undefined}
44
+ items={items}
45
+ onToggleMode={() => {}}
46
+ />
47
+ )
48
+ // Floating uses position:fixed. The `transform` makes this surface a
49
+ // containing block, so the panel anchors to its corner (a stand-in for
50
+ // the app viewport) instead of escaping the Stage's render frame. The
51
+ // surface needs an explicit size — the panel is out-of-flow, so a
52
+ // percentage width would collapse to zero.
53
+ return floating ? (
54
+ <div
55
+ style={{
56
+ position: 'relative',
57
+ transform: 'translateZ(0)',
58
+ width: '30rem',
59
+ height: '18rem',
60
+ border: '1px dashed var(--dc-border)',
61
+ borderRadius: 'var(--dc-radius-md)',
62
+ }}>
63
+ {panel}
64
+ </div>
65
+ ) : (
66
+ <div style={{ width: '26rem' }}>{panel}</div>
67
+ )
68
+ },
69
+ },
70
+ Docked: () => (
71
+ <div style={{ width: '26rem' }}>
72
+ <TweaksPanel
73
+ url="?t.kind=number&t.disabled=1"
74
+ items={items}
75
+ onToggleMode={() => {}}
76
+ />
77
+ </div>
78
+ ),
79
+ },
80
+ { level: 'molecule' },
81
+ )
@@ -0,0 +1,169 @@
1
+ .dcui-tweaks {
2
+ border: 1px solid var(--dc-border);
3
+ border-radius: var(--dc-radius-md);
4
+ background: var(--dc-surface);
5
+ padding: var(--dc-space-6) var(--dc-space-8);
6
+ }
7
+ /* Undocked: a free overlay over a large exhibit, so it shrinks its own
8
+ footprint — tighter padding, narrower column, smaller type (below). */
9
+ .dcui-tweaks[data-mode="floating"] {
10
+ position: fixed;
11
+ right: var(--dc-space-8);
12
+ bottom: var(--dc-space-8);
13
+ width: 16rem;
14
+ max-width: calc(100vw - var(--dc-space-12));
15
+ max-height: calc(100vh - var(--dc-space-12));
16
+ overflow-y: auto;
17
+ padding: var(--dc-space-4) var(--dc-space-6);
18
+ border-radius: var(--dc-radius-lg);
19
+ box-shadow: var(--dc-shadow-overlay);
20
+ z-index: 50;
21
+ }
22
+ .dcui-tweaks-head {
23
+ display: flex;
24
+ align-items: center;
25
+ gap: var(--dc-space-4);
26
+ margin-bottom: var(--dc-space-4);
27
+ }
28
+ .dcui-tweaks[data-mode="floating"] .dcui-tweaks-head {
29
+ cursor: grab;
30
+ touch-action: none;
31
+ user-select: none;
32
+ gap: var(--dc-space-3);
33
+ /* margins negate the (tighter) floating panel padding to span its edges */
34
+ margin: calc(-1 * var(--dc-space-4)) calc(-1 * var(--dc-space-6))
35
+ var(--dc-space-3);
36
+ padding: var(--dc-space-3) var(--dc-space-6);
37
+ border-bottom: 1px solid var(--dc-border);
38
+ transition: margin-bottom var(--dc-transition-base);
39
+ }
40
+ .dcui-tweaks[data-mode="floating"][data-dragging="true"] .dcui-tweaks-head {
41
+ cursor: grabbing;
42
+ }
43
+ .dcui-tweaks-grip {
44
+ flex: 0 0 auto;
45
+ font-family: var(--dc-font-mono);
46
+ font-size: var(--dc-text-sm);
47
+ line-height: 1;
48
+ color: var(--dc-fg-subtle);
49
+ }
50
+ .dcui-tweaks[data-mode="docked"] .dcui-tweaks-grip {
51
+ display: none;
52
+ }
53
+ .dcui-tweaks-url {
54
+ margin-left: auto;
55
+ font-family: var(--dc-font-mono);
56
+ font-size: var(--dc-text-xs);
57
+ color: var(--dc-fg-subtle);
58
+ overflow: hidden;
59
+ text-overflow: ellipsis;
60
+ white-space: nowrap;
61
+ }
62
+ .dcui-tweaks-toggle {
63
+ margin-left: auto;
64
+ flex: 0 0 auto;
65
+ }
66
+ .dcui-tweaks-url + .dcui-tweaks-toggle {
67
+ margin-left: var(--dc-space-2);
68
+ }
69
+ .dcui-tweaks-rows {
70
+ display: flex;
71
+ flex-direction: column;
72
+ }
73
+ .dcui-tweak-row {
74
+ display: flex;
75
+ align-items: center;
76
+ justify-content: space-between;
77
+ gap: var(--dc-space-8);
78
+ padding: var(--dc-space-3) 0;
79
+ }
80
+ .dcui-tweak-row + .dcui-tweak-row {
81
+ border-top: 1px solid var(--dc-border);
82
+ }
83
+ .dcui-tweak-label {
84
+ font-family: var(--dc-font-mono);
85
+ font-size: var(--dc-text-sm);
86
+ color: var(--dc-fg-muted);
87
+ }
88
+ .dcui-tweak-control {
89
+ display: flex;
90
+ align-items: center;
91
+ gap: var(--dc-space-4);
92
+ }
93
+
94
+ /* --- Undocked compaction — smaller type, tighter rows --------- */
95
+ .dcui-tweaks[data-mode="floating"] .dcui-eyebrow,
96
+ .dcui-tweaks[data-mode="floating"] .dcui-tweaks-grip,
97
+ .dcui-tweaks[data-mode="floating"] .dcui-tweaks-url {
98
+ font-size: var(--dc-text-2xs);
99
+ }
100
+ .dcui-tweaks[data-mode="floating"] .dcui-tweak-row {
101
+ gap: var(--dc-space-6);
102
+ padding: var(--dc-space-2) 0;
103
+ }
104
+ .dcui-tweaks[data-mode="floating"] .dcui-tweak-label {
105
+ font-size: var(--dc-text-2xs);
106
+ }
107
+ /* Shrink the controls too — shorter, smaller type than their own "sm".
108
+ The .dcui-tweak-control hop outweighs each control's [data-size="sm"]. */
109
+ .dcui-tweaks[data-mode="floating"] .dcui-tweak-control .dcui-field,
110
+ .dcui-tweaks[data-mode="floating"] .dcui-tweak-control .dcui-select-el {
111
+ height: 22px;
112
+ font-size: var(--dc-text-2xs);
113
+ }
114
+ .dcui-tweaks[data-mode="floating"] .dcui-tweak-control input[type="checkbox"] {
115
+ width: 13px;
116
+ height: 13px;
117
+ }
118
+
119
+ /* --- Undocked collapse — chevron hides/shows the rows ---------- */
120
+ .dcui-tweaks-collapse-btn {
121
+ flex: 0 0 auto;
122
+ }
123
+ /* The disclosure triangle (matches the nav). It centers cleanly in its em box,
124
+ so it sits square with the title. Open → points down; collapsed → sideways. */
125
+ .dcui-tweaks-chevron {
126
+ display: inline-block;
127
+ font-size: 0.65rem;
128
+ line-height: 1;
129
+ transform: rotate(90deg);
130
+ transition: transform var(--dc-transition-base);
131
+ }
132
+ .dcui-tweaks[data-collapsed="true"] .dcui-tweaks-chevron {
133
+ transform: rotate(0deg);
134
+ }
135
+ /* The rows live in a grid whose single track animates 1fr → 0fr. The
136
+ wrapper is inert when docked, so focus rings there are never clipped. */
137
+ .dcui-tweaks[data-mode="floating"] .dcui-tweaks-collapse {
138
+ display: grid;
139
+ grid-template-rows: 1fr;
140
+ transition: grid-template-rows var(--dc-transition-base);
141
+ }
142
+ .dcui-tweaks[data-mode="floating"] .dcui-tweaks-collapse > .dcui-tweaks-rows {
143
+ overflow: hidden;
144
+ min-height: 0;
145
+ }
146
+ .dcui-tweaks[data-mode="floating"][data-collapsed="true"]
147
+ .dcui-tweaks-collapse {
148
+ grid-template-rows: 0fr;
149
+ }
150
+ .dcui-tweaks[data-mode="floating"][data-collapsed="true"] .dcui-tweaks-head {
151
+ margin-bottom: 0;
152
+ }
153
+ /* Docked collapse: no height animation (the docked wrapper is left un-clipped so
154
+ focus rings show), just hide the rows and drop the header's bottom margin so
155
+ the panel shrinks to its header bar — reclaiming stage space on a crowded
156
+ page. */
157
+ .dcui-tweaks[data-mode="docked"][data-collapsed="true"] .dcui-tweaks-collapse {
158
+ display: none;
159
+ }
160
+ .dcui-tweaks[data-mode="docked"][data-collapsed="true"] .dcui-tweaks-head {
161
+ margin-bottom: 0;
162
+ }
163
+ @media (prefers-reduced-motion: reduce) {
164
+ .dcui-tweaks-chevron,
165
+ .dcui-tweaks[data-mode="floating"] .dcui-tweaks-head,
166
+ .dcui-tweaks[data-mode="floating"] .dcui-tweaks-collapse {
167
+ transition: none;
168
+ }
169
+ }
@@ -0,0 +1,20 @@
1
+ **TweaksPanel** — groups a case's tweak controls into one bordered card (Display Case never floats controls — except this panel itself); reach for it to expose a case's live tweaks. Mono label left, control right.
2
+
3
+ ```tsx
4
+ <TweaksPanel
5
+ title="Tweaks"
6
+ url="?t.kind=number&t.disabled=1"
7
+ mode={floating ? 'floating' : 'docked'}
8
+ onToggleMode={toggle}
9
+ items={[
10
+ { label: 'kind', control: <Select options={['text', 'number']} /> },
11
+ { label: 'disabled', control: <input type="checkbox" /> },
12
+ ]}
13
+ />
14
+ ```
15
+
16
+ Modes: `docked` (default, beneath the stage) · `floating` (a free, draggable overlay — the one sanctioned floating surface, so it earns the overlay shadow; drag the head to roam over the nav, header, docs). `onToggleMode` renders the dock/float toggle.
17
+
18
+ - **items**: `{ label, control }[]` — or compose `<Row label>…</Row>` children (the exported per-row component; one `TweakControl` per row)
19
+ - **title**: header label (defaults `'Tweaks'`)
20
+ - **url**: the encoded, shareable tweaked-state URL
@@ -0,0 +1,230 @@
1
+ import type {
2
+ CSSProperties,
3
+ ReactNode,
4
+ PointerEvent as ReactPointerEvent,
5
+ } from 'react'
6
+ import { useEffect, useRef, useState } from 'react'
7
+ import { IconButton } from '../controls/IconButton'
8
+ import { Eyebrow } from './Eyebrow'
9
+
10
+ /**
11
+ * Display Case — TweaksPanel
12
+ * The grouped controls panel — tweaks bundled into one bordered card, mono label
13
+ * left, control right. Two modes:
14
+ * · "docked" — sits in flow beneath the stage (the calm default).
15
+ * · "floating" — a free, draggable overlay (position:fixed, the one sanctioned
16
+ * floating surface, so it earns the overlay shadow). Drag the
17
+ * head to roam anywhere — over the nav, header, and docs.
18
+ * Pass `onToggleMode` to render the dock/float switch in the header.
19
+ */
20
+
21
+ export type TweaksMode = 'docked' | 'floating'
22
+
23
+ export interface TweakItem {
24
+ label: ReactNode
25
+ control: ReactNode
26
+ }
27
+
28
+ export interface TweaksPanelProps {
29
+ title?: ReactNode
30
+ /** The shareable, snapshottable tweaked-state URL (an AI-forward affordance). */
31
+ url?: ReactNode
32
+ mode?: TweaksMode
33
+ onToggleMode?: () => void
34
+ items?: TweakItem[]
35
+ children?: ReactNode
36
+ }
37
+
38
+ /**
39
+ * The box a `position: fixed` child is laid out against: normally the viewport,
40
+ * but an ancestor with a transform / filter / perspective / will-change /
41
+ * contain establishes its own containing block. Display Case traps the floating
42
+ * panel in such an ancestor (so it stays inside the stage), so drag-clamping has
43
+ * to measure against whichever one actually applies — not always the viewport.
44
+ */
45
+ function fixedBounds(el: HTMLElement) {
46
+ for (let p = el.parentElement; p; p = p.parentElement) {
47
+ const cs = getComputedStyle(p)
48
+ if (
49
+ cs.transform !== 'none' ||
50
+ cs.perspective !== 'none' ||
51
+ cs.filter !== 'none' ||
52
+ cs.willChange === 'transform' ||
53
+ cs.contain
54
+ .split(' ')
55
+ .some(
56
+ (v) =>
57
+ v === 'layout' ||
58
+ v === 'paint' ||
59
+ v === 'strict' ||
60
+ v === 'content',
61
+ )
62
+ ) {
63
+ const r = p.getBoundingClientRect()
64
+ return { left: r.left, top: r.top, width: r.width, height: r.height }
65
+ }
66
+ }
67
+ return {
68
+ left: 0,
69
+ top: 0,
70
+ width: window.innerWidth,
71
+ height: window.innerHeight,
72
+ }
73
+ }
74
+
75
+ export function Row({
76
+ label,
77
+ children,
78
+ }: {
79
+ label: ReactNode
80
+ children: ReactNode
81
+ }) {
82
+ return (
83
+ <div className="dcui-tweak-row">
84
+ <span className="dcui-tweak-label">{label}</span>
85
+ <div className="dcui-tweak-control">{children}</div>
86
+ </div>
87
+ )
88
+ }
89
+
90
+ export function TweaksPanel({
91
+ title = 'Tweaks',
92
+ url,
93
+ mode = 'docked',
94
+ onToggleMode,
95
+ items,
96
+ children,
97
+ }: TweaksPanelProps) {
98
+ const floating = mode === 'floating'
99
+ const ref = useRef<HTMLElement | null>(null)
100
+ const drag = useRef<{
101
+ dx: number
102
+ dy: number
103
+ w: number
104
+ h: number
105
+ bx: number
106
+ by: number
107
+ bw: number
108
+ bh: number
109
+ } | null>(null)
110
+ const [pos, setPos] = useState<{ left: number; top: number } | null>(null)
111
+ const [dragging, setDragging] = useState(false)
112
+ // Undocked-only: collapse the rows to a header-sized card.
113
+ const [collapsed, setCollapsed] = useState(false)
114
+
115
+ // Re-anchor (drop the custom position) whenever we leave floating mode.
116
+ useEffect(() => {
117
+ if (!floating) setPos(null)
118
+ }, [floating])
119
+
120
+ const onPointerDown = (e: ReactPointerEvent<HTMLDivElement>) => {
121
+ if (!floating || !ref.current) return
122
+ // ignore drags that start on a header button (dock toggle, collapse chevron)
123
+ if ((e.target as HTMLElement).closest('button')) return
124
+ const r = ref.current.getBoundingClientRect()
125
+ const b = fixedBounds(ref.current)
126
+ drag.current = {
127
+ dx: e.clientX - r.left,
128
+ dy: e.clientY - r.top,
129
+ w: r.width,
130
+ h: r.height,
131
+ bx: b.left,
132
+ by: b.top,
133
+ bw: b.width,
134
+ bh: b.height,
135
+ }
136
+ setDragging(true)
137
+ e.currentTarget.setPointerCapture(e.pointerId)
138
+ }
139
+ const onPointerMove = (e: ReactPointerEvent<HTMLDivElement>) => {
140
+ const d = drag.current
141
+ if (!d) return
142
+ // Clamp inside the containing block (the viewport in the app, the trapping
143
+ // surface in the stage) so the panel can't be dragged off its frame; left/
144
+ // top are expressed relative to that block, matching position:fixed.
145
+ const left = Math.max(0, Math.min(e.clientX - d.dx - d.bx, d.bw - d.w))
146
+ const top = Math.max(0, Math.min(e.clientY - d.dy - d.by, d.bh - d.h))
147
+ setPos({ left, top })
148
+ }
149
+ const endDrag = (e: ReactPointerEvent<HTMLDivElement>) => {
150
+ if (!drag.current) return
151
+ drag.current = null
152
+ setDragging(false)
153
+ try {
154
+ e.currentTarget.releasePointerCapture(e.pointerId)
155
+ } catch {
156
+ // capture may already be released
157
+ }
158
+ }
159
+
160
+ // Once dragged, switch from right/bottom anchoring to explicit left/top.
161
+ const posStyle: CSSProperties | undefined =
162
+ floating && pos
163
+ ? { left: pos.left, top: pos.top, right: 'auto', bottom: 'auto' }
164
+ : undefined
165
+
166
+ return (
167
+ <section
168
+ ref={ref}
169
+ className="dcui-tweaks"
170
+ data-mode={mode}
171
+ data-dragging={dragging ? 'true' : undefined}
172
+ data-collapsed={collapsed ? 'true' : undefined}
173
+ style={posStyle}>
174
+ <div
175
+ className="dcui-tweaks-head"
176
+ onPointerDown={onPointerDown}
177
+ onPointerMove={onPointerMove}
178
+ onPointerUp={endDrag}
179
+ onPointerCancel={endDrag}>
180
+ {floating ? (
181
+ <span className="dcui-tweaks-grip" aria-hidden="true">
182
+
183
+ </span>
184
+ ) : null}
185
+ <Eyebrow>{title}</Eyebrow>
186
+ {/* Collapse the rows in both modes — floating shrinks the overlay,
187
+ docked saves vertical space when the stage is crowded. */}
188
+ <span className="dcui-tweaks-collapse-btn">
189
+ <IconButton
190
+ size="sm"
191
+ variant="bare"
192
+ aria-expanded={!collapsed}
193
+ glyph={
194
+ <span className="dcui-tweaks-chevron" aria-hidden="true">
195
+
196
+ </span>
197
+ }
198
+ label={collapsed ? 'Show tweak options' : 'Hide tweak options'}
199
+ onClick={() => setCollapsed((c) => !c)}
200
+ />
201
+ </span>
202
+ {url != null ? <span className="dcui-tweaks-url">{url}</span> : null}
203
+ {onToggleMode ? (
204
+ <span className="dcui-tweaks-toggle">
205
+ <IconButton
206
+ size="sm"
207
+ variant="bare"
208
+ active={floating}
209
+ glyph={floating ? '▭' : '⬓'}
210
+ label={floating ? 'Dock tweaks panel' : 'Float tweaks panel'}
211
+ onClick={onToggleMode}
212
+ />
213
+ </span>
214
+ ) : null}
215
+ </div>
216
+ <div className="dcui-tweaks-collapse">
217
+ <div className="dcui-tweaks-rows">
218
+ {items
219
+ ? items.map((it, i) => (
220
+ // biome-ignore lint/suspicious/noArrayIndexKey: tweak rows are a fixed, ordered list
221
+ <Row key={i} label={it.label}>
222
+ {it.control}
223
+ </Row>
224
+ ))
225
+ : children}
226
+ </div>
227
+ </div>
228
+ </section>
229
+ )
230
+ }