@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.
- package/LICENSE +21 -0
- package/README.md +309 -0
- package/display-case.prompt.md +64 -0
- package/docs/ai-agents.md +126 -0
- package/docs/cli.md +99 -0
- package/docs/configuration.md +410 -0
- package/docs/documentation-panel.md +50 -0
- package/docs/examples/README.md +14 -0
- package/docs/examples/multi-variant.case.tsx +30 -0
- package/docs/examples/plain.case.tsx +22 -0
- package/docs/examples/tweak-control.placard.md +80 -0
- package/docs/examples/tweaks.case.tsx +39 -0
- package/docs/hierarchy.md +59 -0
- package/docs/quick-start.md +78 -0
- package/docs/style-engines.md +180 -0
- package/docs/testing.md +245 -0
- package/docs/theming.md +97 -0
- package/docs/tweaks.md +75 -0
- package/docs/writing-cases.md +144 -0
- package/docs/writing-placard-docs.md +194 -0
- package/package.json +113 -0
- package/skills/display-case-author-case/README.md +20 -0
- package/skills/display-case-author-case/SKILL.md +40 -0
- package/skills/display-case-author-placard-doc/README.md +24 -0
- package/skills/display-case-author-placard-doc/SKILL.md +65 -0
- package/skills/display-case-review/README.md +19 -0
- package/skills/display-case-review/SKILL.md +30 -0
- package/skills/display-case-snapshot/README.md +20 -0
- package/skills/display-case-snapshot/SKILL.md +29 -0
- package/src/checks/a11y-scanner.test.ts +240 -0
- package/src/checks/a11y-scanner.ts +410 -0
- package/src/checks/check-text.test.ts +53 -0
- package/src/checks/check-text.ts +78 -0
- package/src/checks/check.test.ts +194 -0
- package/src/checks/check.ts +473 -0
- package/src/checks/providers/pixelmatch-diff.test.ts +79 -0
- package/src/checks/providers/pixelmatch-diff.ts +30 -0
- package/src/checks/providers/playwright-driver.ts +104 -0
- package/src/checks/ssr-check.test.ts +73 -0
- package/src/checks/ssr-check.ts +96 -0
- package/src/checks/structure-check.cross-package.test.ts +165 -0
- package/src/checks/structure-check.test.ts +651 -0
- package/src/checks/structure-check.ts +988 -0
- package/src/checks/tokens-check.test.ts +159 -0
- package/src/checks/tokens-check.ts +162 -0
- package/src/cli.ts +218 -0
- package/src/commands/agents.test.ts +24 -0
- package/src/commands/agents.ts +28 -0
- package/src/commands/init-run.test.ts +123 -0
- package/src/commands/init.test.ts +63 -0
- package/src/commands/init.ts +412 -0
- package/src/commands/publish.test.ts +210 -0
- package/src/commands/publish.ts +292 -0
- package/src/core/affected.test.ts +99 -0
- package/src/core/affected.ts +144 -0
- package/src/core/catalog.test.ts +152 -0
- package/src/core/catalog.ts +92 -0
- package/src/core/discovery.test.ts +184 -0
- package/src/core/discovery.ts +250 -0
- package/src/core/manifest.ts +41 -0
- package/src/core/mdx-lite/__fixtures__/box-stub.tsx +7 -0
- package/src/core/mdx-lite/index.ts +393 -0
- package/src/core/mdx-lite/mdx-lite.test.ts +345 -0
- package/src/core/mdx-plugin.test.ts +60 -0
- package/src/core/mdx-plugin.ts +30 -0
- package/src/flow-step.test-d.ts +39 -0
- package/src/index.test.ts +100 -0
- package/src/index.ts +564 -0
- package/src/render/collect-styles.emotion.test.tsx +114 -0
- package/src/render/collect-styles.test.tsx +72 -0
- package/src/render/collect-styles.ts +33 -0
- package/src/render/documents.test.ts +184 -0
- package/src/render/documents.ts +88 -0
- package/src/render/render-node.test.tsx +160 -0
- package/src/render/render-node.tsx +133 -0
- package/src/render/ssr-primer.test.tsx +25 -0
- package/src/render/ssr-primer.tsx +54 -0
- package/src/render/ssr-render.test.tsx +142 -0
- package/src/render/ssr-render.tsx +63 -0
- package/src/render/ssr-shell.test.tsx +57 -0
- package/src/render/ssr-shell.tsx +54 -0
- package/src/server/prod-server.ts +237 -0
- package/src/server/server.test.ts +117 -0
- package/src/server/server.ts +1039 -0
- package/src/style-engine.test-d.ts +37 -0
- package/src/testing/test-helpers.ts +27 -0
- package/src/types/pixelmatch.d.ts +12 -0
- package/src/ui/browser-entry.tsx +51 -0
- package/src/ui/chrome.css +485 -0
- package/src/ui/design-system/README.md +88 -0
- package/src/ui/design-system/components/controls/Button.case.tsx +52 -0
- package/src/ui/design-system/components/controls/Button.css +89 -0
- package/src/ui/design-system/components/controls/Button.placard.md +14 -0
- package/src/ui/design-system/components/controls/Button.test.tsx +45 -0
- package/src/ui/design-system/components/controls/Button.tsx +41 -0
- package/src/ui/design-system/components/controls/IconButton.case.tsx +52 -0
- package/src/ui/design-system/components/controls/IconButton.css +67 -0
- package/src/ui/design-system/components/controls/IconButton.placard.md +13 -0
- package/src/ui/design-system/components/controls/IconButton.test.tsx +39 -0
- package/src/ui/design-system/components/controls/IconButton.tsx +47 -0
- package/src/ui/design-system/components/controls/Input.case.tsx +50 -0
- package/src/ui/design-system/components/controls/Input.css +52 -0
- package/src/ui/design-system/components/controls/Input.placard.md +12 -0
- package/src/ui/design-system/components/controls/Input.test.tsx +43 -0
- package/src/ui/design-system/components/controls/Input.tsx +45 -0
- package/src/ui/design-system/components/controls/Select.case.tsx +48 -0
- package/src/ui/design-system/components/controls/Select.css +44 -0
- package/src/ui/design-system/components/controls/Select.placard.md +15 -0
- package/src/ui/design-system/components/controls/Select.test.tsx +57 -0
- package/src/ui/design-system/components/controls/Select.tsx +58 -0
- package/src/ui/design-system/components/controls/SelectMenu.case.tsx +100 -0
- package/src/ui/design-system/components/controls/SelectMenu.css +72 -0
- package/src/ui/design-system/components/controls/SelectMenu.placard.md +18 -0
- package/src/ui/design-system/components/controls/SelectMenu.test.tsx +66 -0
- package/src/ui/design-system/components/controls/SelectMenu.tsx +377 -0
- package/src/ui/design-system/components/index.ts +66 -0
- package/src/ui/design-system/components/primer-specimen/ColorRamp.case.tsx +44 -0
- package/src/ui/design-system/components/primer-specimen/ColorRamp.placard.md +15 -0
- package/src/ui/design-system/components/primer-specimen/ColorRamp.tsx +51 -0
- package/src/ui/design-system/components/primer-specimen/DefinitionList.case.tsx +38 -0
- package/src/ui/design-system/components/primer-specimen/DefinitionList.placard.md +15 -0
- package/src/ui/design-system/components/primer-specimen/DefinitionList.tsx +41 -0
- package/src/ui/design-system/components/primer-specimen/FontFamilies.case.tsx +24 -0
- package/src/ui/design-system/components/primer-specimen/FontFamilies.placard.md +12 -0
- package/src/ui/design-system/components/primer-specimen/FontFamilies.tsx +41 -0
- package/src/ui/design-system/components/primer-specimen/GlyphGrid.case.tsx +27 -0
- package/src/ui/design-system/components/primer-specimen/GlyphGrid.placard.md +13 -0
- package/src/ui/design-system/components/primer-specimen/GlyphGrid.tsx +34 -0
- package/src/ui/design-system/components/primer-specimen/LayoutMock.case.tsx +36 -0
- package/src/ui/design-system/components/primer-specimen/LayoutMock.placard.md +7 -0
- package/src/ui/design-system/components/primer-specimen/LayoutMock.tsx +36 -0
- package/src/ui/design-system/components/primer-specimen/SpacingScale.case.tsx +20 -0
- package/src/ui/design-system/components/primer-specimen/SpacingScale.placard.md +12 -0
- package/src/ui/design-system/components/primer-specimen/SpacingScale.tsx +33 -0
- package/src/ui/design-system/components/primer-specimen/SpecimenBoxRow.case.tsx +56 -0
- package/src/ui/design-system/components/primer-specimen/SpecimenBoxRow.placard.md +17 -0
- package/src/ui/design-system/components/primer-specimen/SpecimenBoxRow.tsx +45 -0
- package/src/ui/design-system/components/primer-specimen/StatusList.case.tsx +17 -0
- package/src/ui/design-system/components/primer-specimen/StatusList.placard.md +16 -0
- package/src/ui/design-system/components/primer-specimen/StatusList.tsx +39 -0
- package/src/ui/design-system/components/primer-specimen/SwatchGrid.case.tsx +26 -0
- package/src/ui/design-system/components/primer-specimen/SwatchGrid.placard.md +15 -0
- package/src/ui/design-system/components/primer-specimen/SwatchGrid.tsx +42 -0
- package/src/ui/design-system/components/primer-specimen/TypeScale.case.tsx +23 -0
- package/src/ui/design-system/components/primer-specimen/TypeScale.placard.md +14 -0
- package/src/ui/design-system/components/primer-specimen/TypeScale.tsx +34 -0
- package/src/ui/design-system/components/primer-specimen/WeightSpecimen.case.tsx +28 -0
- package/src/ui/design-system/components/primer-specimen/WeightSpecimen.placard.md +15 -0
- package/src/ui/design-system/components/primer-specimen/WeightSpecimen.tsx +46 -0
- package/src/ui/design-system/components/primer-specimen/index.ts +31 -0
- package/src/ui/design-system/components/primer-specimen/styles.css +476 -0
- package/src/ui/design-system/components/shell/A11yPage.case.tsx +237 -0
- package/src/ui/design-system/components/shell/A11yPage.placard.md +15 -0
- package/src/ui/design-system/components/shell/CaseTemplate.case.tsx +32 -0
- package/src/ui/design-system/components/shell/CaseTemplate.placard.md +5 -0
- package/src/ui/design-system/components/shell/CasesPage.case.tsx +141 -0
- package/src/ui/design-system/components/shell/CasesPage.placard.md +12 -0
- package/src/ui/design-system/components/shell/PrimerPage.case.tsx +22 -0
- package/src/ui/design-system/components/shell/PrimerPage.placard.md +3 -0
- package/src/ui/design-system/components/shell/PrimerTemplate.case.tsx +22 -0
- package/src/ui/design-system/components/shell/PrimerTemplate.placard.md +5 -0
- package/src/ui/design-system/components/shell/ShellView.case.tsx +57 -0
- package/src/ui/design-system/components/shell/ShellView.placard.md +5 -0
- package/src/ui/design-system/components/shell/ShellView.tsx +678 -0
- package/src/ui/design-system/components/shell/shell-fixtures.tsx +727 -0
- package/src/ui/design-system/components/showcase/A11yBadge.case.tsx +46 -0
- package/src/ui/design-system/components/showcase/A11yBadge.css +27 -0
- package/src/ui/design-system/components/showcase/A11yBadge.placard.md +11 -0
- package/src/ui/design-system/components/showcase/A11yBadge.test.tsx +31 -0
- package/src/ui/design-system/components/showcase/A11yBadge.tsx +41 -0
- package/src/ui/design-system/components/showcase/A11yPanel.case.tsx +121 -0
- package/src/ui/design-system/components/showcase/A11yPanel.css +198 -0
- package/src/ui/design-system/components/showcase/A11yPanel.placard.md +19 -0
- package/src/ui/design-system/components/showcase/A11yPanel.test.tsx +81 -0
- package/src/ui/design-system/components/showcase/A11yPanel.tsx +144 -0
- package/src/ui/design-system/components/showcase/Chip.case.tsx +48 -0
- package/src/ui/design-system/components/showcase/Chip.css +51 -0
- package/src/ui/design-system/components/showcase/Chip.placard.md +13 -0
- package/src/ui/design-system/components/showcase/Chip.test.tsx +46 -0
- package/src/ui/design-system/components/showcase/Chip.tsx +54 -0
- package/src/ui/design-system/components/showcase/Eyebrow.case.tsx +30 -0
- package/src/ui/design-system/components/showcase/Eyebrow.css +16 -0
- package/src/ui/design-system/components/showcase/Eyebrow.placard.md +10 -0
- package/src/ui/design-system/components/showcase/Eyebrow.test.tsx +38 -0
- package/src/ui/design-system/components/showcase/Eyebrow.tsx +29 -0
- package/src/ui/design-system/components/showcase/FlowNav.case.tsx +35 -0
- package/src/ui/design-system/components/showcase/FlowNav.css +29 -0
- package/src/ui/design-system/components/showcase/FlowNav.placard.md +13 -0
- package/src/ui/design-system/components/showcase/FlowNav.test.tsx +48 -0
- package/src/ui/design-system/components/showcase/FlowNav.tsx +58 -0
- package/src/ui/design-system/components/showcase/ImpactTag.case.tsx +19 -0
- package/src/ui/design-system/components/showcase/ImpactTag.css +36 -0
- package/src/ui/design-system/components/showcase/ImpactTag.placard.md +14 -0
- package/src/ui/design-system/components/showcase/ImpactTag.test.tsx +40 -0
- package/src/ui/design-system/components/showcase/ImpactTag.tsx +35 -0
- package/src/ui/design-system/components/showcase/NavItem.case.tsx +86 -0
- package/src/ui/design-system/components/showcase/NavItem.css +111 -0
- package/src/ui/design-system/components/showcase/NavItem.placard.md +13 -0
- package/src/ui/design-system/components/showcase/NavItem.test.tsx +65 -0
- package/src/ui/design-system/components/showcase/NavItem.tsx +95 -0
- package/src/ui/design-system/components/showcase/RenderAddress.case.tsx +21 -0
- package/src/ui/design-system/components/showcase/RenderAddress.css +35 -0
- package/src/ui/design-system/components/showcase/RenderAddress.placard.md +7 -0
- package/src/ui/design-system/components/showcase/RenderAddress.test.tsx +26 -0
- package/src/ui/design-system/components/showcase/RenderAddress.tsx +43 -0
- package/src/ui/design-system/components/showcase/SegmentedToggle.case.tsx +84 -0
- package/src/ui/design-system/components/showcase/SegmentedToggle.css +61 -0
- package/src/ui/design-system/components/showcase/SegmentedToggle.placard.md +21 -0
- package/src/ui/design-system/components/showcase/SegmentedToggle.test.tsx +81 -0
- package/src/ui/design-system/components/showcase/SegmentedToggle.tsx +75 -0
- package/src/ui/design-system/components/showcase/Sidebar.case.tsx +67 -0
- package/src/ui/design-system/components/showcase/Sidebar.css +6 -0
- package/src/ui/design-system/components/showcase/Sidebar.placard.md +14 -0
- package/src/ui/design-system/components/showcase/Sidebar.test.tsx +32 -0
- package/src/ui/design-system/components/showcase/Sidebar.tsx +30 -0
- package/src/ui/design-system/components/showcase/Stage.case.tsx +51 -0
- package/src/ui/design-system/components/showcase/Stage.css +91 -0
- package/src/ui/design-system/components/showcase/Stage.placard.md +15 -0
- package/src/ui/design-system/components/showcase/Stage.test.tsx +84 -0
- package/src/ui/design-system/components/showcase/Stage.tsx +97 -0
- package/src/ui/design-system/components/showcase/TweaksPanel.case.tsx +81 -0
- package/src/ui/design-system/components/showcase/TweaksPanel.css +169 -0
- package/src/ui/design-system/components/showcase/TweaksPanel.placard.md +20 -0
- package/src/ui/design-system/components/showcase/TweaksPanel.tsx +230 -0
- package/src/ui/design-system/components/showcase/Wordmark.case.tsx +42 -0
- package/src/ui/design-system/components/showcase/Wordmark.css +31 -0
- package/src/ui/design-system/components/showcase/Wordmark.placard.md +10 -0
- package/src/ui/design-system/components/showcase/Wordmark.test.tsx +22 -0
- package/src/ui/design-system/components/showcase/Wordmark.tsx +22 -0
- package/src/ui/design-system/primer-specimens/brand.tsx +26 -0
- package/src/ui/design-system/primer-specimens/colors.tsx +83 -0
- package/src/ui/design-system/primer-specimens/components.tsx +308 -0
- package/src/ui/design-system/primer-specimens/foundations.tsx +71 -0
- package/src/ui/design-system/primer-specimens/index.ts +25 -0
- package/src/ui/design-system/primer-specimens/showcase.tsx +68 -0
- package/src/ui/design-system/primer-specimens/spacing.tsx +101 -0
- package/src/ui/design-system/primer-specimens/type.tsx +75 -0
- package/src/ui/design-system/primer.mdx +236 -0
- package/src/ui/design-system/styles.css +14 -0
- package/src/ui/design-system/tokens/colors.css +172 -0
- package/src/ui/design-system/tokens/fonts.css +18 -0
- package/src/ui/design-system/tokens/spacing.css +48 -0
- package/src/ui/design-system/tokens/typography.css +49 -0
- package/src/ui/markdown.test.tsx +54 -0
- package/src/ui/markdown.tsx +19 -0
- package/src/ui/primer-mount.tsx +76 -0
- package/src/ui/primer.css +175 -0
- package/src/ui/primer.tsx +277 -0
- package/src/ui/render-mount.tsx +284 -0
- package/src/ui/shell-core.test.ts +340 -0
- package/src/ui/shell-core.ts +295 -0
- package/src/ui/shell.tsx +60 -0
- package/src/ui/test-ids.ts +53 -0
- 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
|
+
}
|