@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,678 @@
|
|
|
1
|
+
// display-case: no-placard — Display Case's own browse chrome, cased for
|
|
2
|
+
// dogfooding (ShellView.case.tsx); internal plumbing, not a consumer primitive.
|
|
3
|
+
import type { CSSProperties, ReactNode } from 'react'
|
|
4
|
+
import type { TweakDescriptor } from '../../../../index'
|
|
5
|
+
import { DocMarkdown } from '../../../markdown'
|
|
6
|
+
import {
|
|
7
|
+
buildUrl,
|
|
8
|
+
DEVICES,
|
|
9
|
+
DOC_MAX_W,
|
|
10
|
+
DOC_MIN_W,
|
|
11
|
+
LEVEL_LABEL,
|
|
12
|
+
type Mode,
|
|
13
|
+
RESPONSIVE,
|
|
14
|
+
STAGE_FADE_MS,
|
|
15
|
+
ZOOM_MAX,
|
|
16
|
+
ZOOM_MIN,
|
|
17
|
+
ZOOM_STEP,
|
|
18
|
+
} from '../../../shell-core'
|
|
19
|
+
import { DcTestIds } from '../../../test-ids'
|
|
20
|
+
import type { ShellViewModel } from '../../../use-shell'
|
|
21
|
+
import type { SegmentedOption } from '..'
|
|
22
|
+
import {
|
|
23
|
+
A11yPanel,
|
|
24
|
+
Button,
|
|
25
|
+
Eyebrow,
|
|
26
|
+
FlowNav,
|
|
27
|
+
IconButton,
|
|
28
|
+
Input,
|
|
29
|
+
NavItem,
|
|
30
|
+
RenderAddress,
|
|
31
|
+
SegmentedToggle,
|
|
32
|
+
SelectMenu,
|
|
33
|
+
Sidebar,
|
|
34
|
+
Stage,
|
|
35
|
+
TweaksPanel,
|
|
36
|
+
Wordmark,
|
|
37
|
+
} from '..'
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The browse chrome, as a pure function of its {@link ShellViewModel}. Every
|
|
41
|
+
* piece of state, every derived layout number, and every event handler is fed
|
|
42
|
+
* in — `ShellView` only arranges them into the header, nav rail, stage, and
|
|
43
|
+
* Primer host. The live render/Primer iframes are injected as `renderFrame` /
|
|
44
|
+
* `primerFrame` slots, so the same view paints either the running chrome (real
|
|
45
|
+
* iframes, from {@link useShell}) or a static page/flow exhibit (a stub slot
|
|
46
|
+
* over a hand-built model). This is what lets Display Case dogfood its own
|
|
47
|
+
* layout as a template, page, and flow.
|
|
48
|
+
*/
|
|
49
|
+
export interface ShellViewProps extends ShellViewModel {
|
|
50
|
+
/** The stage's preview surface — the live `<iframe>` in the app; a static
|
|
51
|
+
* stand-in (a rendered component, a placeholder box) in a page/template case. */
|
|
52
|
+
renderFrame: ReactNode
|
|
53
|
+
/** The Primer reading surface — the live `<iframe>` in the app; a static
|
|
54
|
+
* stand-in in a Primer page/template case. */
|
|
55
|
+
primerFrame: ReactNode
|
|
56
|
+
/** Make the stage's frame box fill the stage edge-to-edge instead of sizing to
|
|
57
|
+
* the measured `boxW`/`boxH`. The live chrome leaves this off (it measures the
|
|
58
|
+
* panel); a static page/flow *exhibit* sets it so a full-screen page or flow
|
|
59
|
+
* fills the whole stage rather than sitting in a small centred box. */
|
|
60
|
+
fillFrame?: boolean
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function ShellView(props: ShellViewProps) {
|
|
64
|
+
const { manifest, theme, shownMode, mode, setMode, modeFadeStyle } = props
|
|
65
|
+
return (
|
|
66
|
+
<div
|
|
67
|
+
className="dc-app"
|
|
68
|
+
data-testid={DcTestIds.app}
|
|
69
|
+
data-theme={theme}
|
|
70
|
+
data-nav={props.navCollapsed ? 'collapsed' : 'open'}>
|
|
71
|
+
<ShellHeader {...props} />
|
|
72
|
+
|
|
73
|
+
<Sidebar
|
|
74
|
+
data-testid={DcTestIds.sidebar}
|
|
75
|
+
label={shownMode === 'primer' ? 'Primer contents' : 'Components'}>
|
|
76
|
+
{/* The ModeSwitch is pinned above the scroll region (a non-scrolling row),
|
|
77
|
+
so it stays put while nav items scroll and fade beneath it, and its
|
|
78
|
+
highlight box keeps lerping during the crossfade. The scroll region's
|
|
79
|
+
fading body tracks `shownMode` so it swaps mid-fade, in step with the
|
|
80
|
+
screen content. */}
|
|
81
|
+
{manifest.primer && <ModeSwitch mode={mode} onMode={setMode} />}
|
|
82
|
+
<NavContents {...props} />
|
|
83
|
+
</Sidebar>
|
|
84
|
+
|
|
85
|
+
{/* Library main and the Primer host both occupy the `main` grid area; the
|
|
86
|
+
inactive one is `hidden`. Keeping the library mounted preserves the
|
|
87
|
+
render-frame handshake across mode switches. Visibility tracks
|
|
88
|
+
`shownMode` (the swap happens mid-fade) and the whole region rides the
|
|
89
|
+
shared opacity crossfade so the screen content fades out and back in. */}
|
|
90
|
+
<main
|
|
91
|
+
className="dc-main"
|
|
92
|
+
hidden={shownMode === 'primer'}
|
|
93
|
+
style={modeFadeStyle}>
|
|
94
|
+
<LibraryStage {...props} />
|
|
95
|
+
</main>
|
|
96
|
+
|
|
97
|
+
{manifest.primer && (
|
|
98
|
+
<section
|
|
99
|
+
className="dc-primer-host"
|
|
100
|
+
aria-label="Primer"
|
|
101
|
+
hidden={shownMode !== 'primer'}
|
|
102
|
+
style={modeFadeStyle}>
|
|
103
|
+
{props.primerFrame}
|
|
104
|
+
</section>
|
|
105
|
+
)}
|
|
106
|
+
</div>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function ShellHeader(props: ShellViewProps) {
|
|
111
|
+
const {
|
|
112
|
+
manifest,
|
|
113
|
+
theme,
|
|
114
|
+
setTheme,
|
|
115
|
+
navCollapsed,
|
|
116
|
+
setNavCollapsed,
|
|
117
|
+
shownMode,
|
|
118
|
+
modeFadeStyle,
|
|
119
|
+
sizeId,
|
|
120
|
+
setSizeId,
|
|
121
|
+
widthInputValue,
|
|
122
|
+
fixed,
|
|
123
|
+
editDim,
|
|
124
|
+
rotateDims,
|
|
125
|
+
fitted,
|
|
126
|
+
scale,
|
|
127
|
+
manualZoom,
|
|
128
|
+
setManualZoom,
|
|
129
|
+
stageDecor,
|
|
130
|
+
showGrid,
|
|
131
|
+
setShowGrid,
|
|
132
|
+
component,
|
|
133
|
+
docOpen,
|
|
134
|
+
changeDocsOpen,
|
|
135
|
+
} = props
|
|
136
|
+
return (
|
|
137
|
+
<header className="dc-header">
|
|
138
|
+
<div className="dc-header-left">
|
|
139
|
+
<IconButton
|
|
140
|
+
glyph="☰"
|
|
141
|
+
label="Toggle navigation"
|
|
142
|
+
aria-expanded={!navCollapsed}
|
|
143
|
+
onClick={() => setNavCollapsed((c) => !c)}
|
|
144
|
+
/>
|
|
145
|
+
<Wordmark data-testid={DcTestIds.wordmark}>{manifest.title}</Wordmark>
|
|
146
|
+
</div>
|
|
147
|
+
<div className="dc-controls">
|
|
148
|
+
{/* The device toolbar, zoom, grid and docs controls act on the stage,
|
|
149
|
+
so they're library-only; the Primer view keeps just the theme
|
|
150
|
+
toggle. Gated on `shownMode` (not `mode`) and wrapped in a fading
|
|
151
|
+
group so they fade out before the swap to Primer and fade in after
|
|
152
|
+
the swap to Cases — in step with the nav and screen crossfade. */}
|
|
153
|
+
{shownMode === 'library' && (
|
|
154
|
+
<div className="dc-controls-extra" style={modeFadeStyle}>
|
|
155
|
+
{/* The custom listbox (not a native <select>) so a pick commits
|
|
156
|
+
instantly and the trigger styling matches the tweak controls.
|
|
157
|
+
Disabled options stand in as the Responsive / Devices group
|
|
158
|
+
headers. */}
|
|
159
|
+
<SelectMenu
|
|
160
|
+
size="sm"
|
|
161
|
+
value={sizeId}
|
|
162
|
+
onChange={setSizeId}
|
|
163
|
+
aria-label="Screen size"
|
|
164
|
+
options={[
|
|
165
|
+
{ value: '__responsive', label: 'Responsive', disabled: true },
|
|
166
|
+
...RESPONSIVE.map((r) => ({ value: r.id, label: r.label })),
|
|
167
|
+
{ value: '__devices', label: 'Devices', disabled: true },
|
|
168
|
+
...DEVICES.map((d) => ({ value: d.id, label: d.label })),
|
|
169
|
+
{ value: 'custom', label: 'Custom…' },
|
|
170
|
+
]}
|
|
171
|
+
/>
|
|
172
|
+
<div className="dc-dims">
|
|
173
|
+
<input
|
|
174
|
+
className="dc-dim"
|
|
175
|
+
type="number"
|
|
176
|
+
min={1}
|
|
177
|
+
aria-label="Width (px)"
|
|
178
|
+
disabled={!fixed}
|
|
179
|
+
value={widthInputValue}
|
|
180
|
+
placeholder={fixed ? undefined : 'auto'}
|
|
181
|
+
onChange={(e) => editDim('w', e.target.value)}
|
|
182
|
+
/>
|
|
183
|
+
<span className="dc-dim-x">×</span>
|
|
184
|
+
<input
|
|
185
|
+
className="dc-dim"
|
|
186
|
+
type="number"
|
|
187
|
+
min={1}
|
|
188
|
+
aria-label="Height (px)"
|
|
189
|
+
disabled={!fixed}
|
|
190
|
+
value={fixed ? fixed.h : ''}
|
|
191
|
+
placeholder={fixed ? undefined : 'auto'}
|
|
192
|
+
onChange={(e) => editDim('h', e.target.value)}
|
|
193
|
+
/>
|
|
194
|
+
<IconButton
|
|
195
|
+
glyph="⟲"
|
|
196
|
+
label="Rotate (swap width and height)"
|
|
197
|
+
variant="bare"
|
|
198
|
+
size="sm"
|
|
199
|
+
disabled={!fixed}
|
|
200
|
+
onClick={rotateDims}
|
|
201
|
+
/>
|
|
202
|
+
</div>
|
|
203
|
+
{fitted ? (
|
|
204
|
+
<span
|
|
205
|
+
className="dc-zoom-level dc-zoom-fit"
|
|
206
|
+
title="Scaled to fit the panel">
|
|
207
|
+
{Math.round(scale * 100)}%
|
|
208
|
+
</span>
|
|
209
|
+
) : (
|
|
210
|
+
<div className="dc-zoom">
|
|
211
|
+
<IconButton
|
|
212
|
+
glyph="−"
|
|
213
|
+
label="Zoom out"
|
|
214
|
+
variant="bare"
|
|
215
|
+
size="sm"
|
|
216
|
+
disabled={manualZoom <= ZOOM_MIN}
|
|
217
|
+
onClick={() =>
|
|
218
|
+
setManualZoom((z) =>
|
|
219
|
+
Math.max(ZOOM_MIN, Math.round((z - ZOOM_STEP) * 10) / 10),
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
/>
|
|
223
|
+
<button
|
|
224
|
+
type="button"
|
|
225
|
+
className="dc-zoom-level"
|
|
226
|
+
aria-label="Reset zoom"
|
|
227
|
+
onClick={() => setManualZoom(() => 1)}>
|
|
228
|
+
{Math.round(manualZoom * 100)}%
|
|
229
|
+
</button>
|
|
230
|
+
<IconButton
|
|
231
|
+
glyph="+"
|
|
232
|
+
label="Zoom in"
|
|
233
|
+
variant="bare"
|
|
234
|
+
size="sm"
|
|
235
|
+
disabled={manualZoom >= ZOOM_MAX}
|
|
236
|
+
onClick={() =>
|
|
237
|
+
setManualZoom((z) =>
|
|
238
|
+
Math.min(ZOOM_MAX, Math.round((z + ZOOM_STEP) * 10) / 10),
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
/>
|
|
242
|
+
</div>
|
|
243
|
+
)}
|
|
244
|
+
{stageDecor && (
|
|
245
|
+
<Button
|
|
246
|
+
data-testid={DcTestIds.gridButton}
|
|
247
|
+
aria-pressed={showGrid}
|
|
248
|
+
title="Toggle the stage grid (vs the app background)"
|
|
249
|
+
onClick={() => setShowGrid((g) => !g)}>
|
|
250
|
+
Grid
|
|
251
|
+
</Button>
|
|
252
|
+
)}
|
|
253
|
+
{component?.placardDoc && (
|
|
254
|
+
<Button
|
|
255
|
+
data-testid={DcTestIds.docsButton}
|
|
256
|
+
aria-pressed={docOpen}
|
|
257
|
+
aria-expanded={docOpen}
|
|
258
|
+
onClick={() => changeDocsOpen(!docOpen)}>
|
|
259
|
+
Docs
|
|
260
|
+
</Button>
|
|
261
|
+
)}
|
|
262
|
+
</div>
|
|
263
|
+
)}
|
|
264
|
+
<Button
|
|
265
|
+
data-testid={DcTestIds.themeToggle}
|
|
266
|
+
onClick={() => setTheme((t) => (t === 'light' ? 'dark' : 'light'))}>
|
|
267
|
+
{theme === 'light' ? 'Dark' : 'Light'}
|
|
268
|
+
</Button>
|
|
269
|
+
</div>
|
|
270
|
+
</header>
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function NavContents(props: ShellViewProps) {
|
|
275
|
+
const {
|
|
276
|
+
shownMode,
|
|
277
|
+
modeFadeStyle,
|
|
278
|
+
navScrollRef,
|
|
279
|
+
navBodyRef,
|
|
280
|
+
primerGroups,
|
|
281
|
+
primerActive,
|
|
282
|
+
primerExpanded,
|
|
283
|
+
togglePrimerGroup,
|
|
284
|
+
scrollToSection,
|
|
285
|
+
groups,
|
|
286
|
+
expanded,
|
|
287
|
+
sel,
|
|
288
|
+
toggleExpanded,
|
|
289
|
+
selectComponent,
|
|
290
|
+
select,
|
|
291
|
+
} = props
|
|
292
|
+
return (
|
|
293
|
+
<div ref={navScrollRef} className="dc-nav-scroll">
|
|
294
|
+
<div ref={navBodyRef} className="dc-nav-body" style={modeFadeStyle}>
|
|
295
|
+
{shownMode === 'primer'
|
|
296
|
+
? primerGroups.map((g) => {
|
|
297
|
+
const items = g.items.map((s) => (
|
|
298
|
+
<NavItem
|
|
299
|
+
key={s.id}
|
|
300
|
+
kind="case"
|
|
301
|
+
label={s.title}
|
|
302
|
+
current={primerActive === s.id}
|
|
303
|
+
onSelect={() => scrollToSection(s.id)}
|
|
304
|
+
/>
|
|
305
|
+
))
|
|
306
|
+
// Displays before the first `##` heading get a plain "Contents"
|
|
307
|
+
// label (there is at most one such leading group).
|
|
308
|
+
const heading = g.heading
|
|
309
|
+
if (!heading) {
|
|
310
|
+
return (
|
|
311
|
+
<div key="primer-lead" className="dc-primer-group">
|
|
312
|
+
<Eyebrow className="dc-group-label">Contents</Eyebrow>
|
|
313
|
+
{items}
|
|
314
|
+
</div>
|
|
315
|
+
)
|
|
316
|
+
}
|
|
317
|
+
// A heading with no Displays under it is a leaf: it scrolls but
|
|
318
|
+
// has no accordion (no chevron, no count, no expand state) — same
|
|
319
|
+
// treatment as a single-case component in the library tree.
|
|
320
|
+
const hasItems = g.items.length > 0
|
|
321
|
+
const collapsed = !primerExpanded.has(heading.id)
|
|
322
|
+
// When a group is collapsed, a Display in view has no visible row,
|
|
323
|
+
// so stand its heading in as the active marker — the highlight
|
|
324
|
+
// stays put while reading instead of vanishing into the fold.
|
|
325
|
+
const headingActive =
|
|
326
|
+
primerActive === heading.id ||
|
|
327
|
+
(hasItems &&
|
|
328
|
+
collapsed &&
|
|
329
|
+
g.items.some((it) => it.id === primerActive))
|
|
330
|
+
return (
|
|
331
|
+
<div key={heading.id} className="dc-primer-group">
|
|
332
|
+
<NavItem
|
|
333
|
+
kind="component"
|
|
334
|
+
label={heading.title}
|
|
335
|
+
count={hasItems ? g.items.length : undefined}
|
|
336
|
+
current={headingActive}
|
|
337
|
+
expanded={hasItems ? !collapsed : undefined}
|
|
338
|
+
onToggle={
|
|
339
|
+
hasItems ? () => togglePrimerGroup(heading.id) : undefined
|
|
340
|
+
}
|
|
341
|
+
onSelect={() => scrollToSection(heading.id)}
|
|
342
|
+
/>
|
|
343
|
+
{hasItems && !collapsed && items}
|
|
344
|
+
</div>
|
|
345
|
+
)
|
|
346
|
+
})
|
|
347
|
+
: groups.map(({ key, components }) => (
|
|
348
|
+
<div key={key} className="dc-group">
|
|
349
|
+
<Eyebrow className="dc-group-label">{LEVEL_LABEL[key]}</Eyebrow>
|
|
350
|
+
{components.map((c) => {
|
|
351
|
+
// A single-case component reads as a leaf: no chevron, no
|
|
352
|
+
// count, no case row — the lone case lives in the stage caption
|
|
353
|
+
// and URL. Selecting the row routes straight to that case.
|
|
354
|
+
const single = c.cases.length === 1
|
|
355
|
+
const isExpanded = !single && expanded.has(c.id)
|
|
356
|
+
// Per-variant violation counts. A collapsed multi-case parent
|
|
357
|
+
// (and a single-case leaf, which never expands) shows the sum
|
|
358
|
+
// across variants directly; an expanded parent shows a plain
|
|
359
|
+
// dot instead and the per-variant counts move onto its case
|
|
360
|
+
// rows. A leaf has no children, so it always shows its number.
|
|
361
|
+
const variants = props.a11y?.byVariant[c.id]
|
|
362
|
+
const total = variants
|
|
363
|
+
? Object.values(variants).reduce((a, b) => a + b, 0)
|
|
364
|
+
: 0
|
|
365
|
+
let parentAlert: number | 'dot' | undefined
|
|
366
|
+
if (total > 0) parentAlert = isExpanded ? 'dot' : total
|
|
367
|
+
return (
|
|
368
|
+
<div key={c.id} className="dc-nav-component">
|
|
369
|
+
<NavItem
|
|
370
|
+
kind="component"
|
|
371
|
+
label={c.name}
|
|
372
|
+
count={single ? undefined : c.cases.length}
|
|
373
|
+
alert={parentAlert}
|
|
374
|
+
current={sel?.componentId === c.id}
|
|
375
|
+
expanded={isExpanded}
|
|
376
|
+
testId={DcTestIds.navComponent(c.id)}
|
|
377
|
+
toggleTestId={
|
|
378
|
+
single
|
|
379
|
+
? undefined
|
|
380
|
+
: DcTestIds.navComponentToggle(c.id)
|
|
381
|
+
}
|
|
382
|
+
alertTestId={DcTestIds.navAlert(c.id)}
|
|
383
|
+
onToggle={
|
|
384
|
+
single ? undefined : () => toggleExpanded(c.id)
|
|
385
|
+
}
|
|
386
|
+
onSelect={() => selectComponent(c)}
|
|
387
|
+
/>
|
|
388
|
+
{isExpanded &&
|
|
389
|
+
c.cases.map((cs) => (
|
|
390
|
+
<NavItem
|
|
391
|
+
key={cs.id}
|
|
392
|
+
kind="case"
|
|
393
|
+
label={cs.name}
|
|
394
|
+
alert={variants?.[cs.id]}
|
|
395
|
+
current={
|
|
396
|
+
sel?.componentId === c.id && sel?.caseId === cs.id
|
|
397
|
+
}
|
|
398
|
+
testId={DcTestIds.navCase(c.id, cs.id)}
|
|
399
|
+
alertTestId={`${DcTestIds.navAlert(c.id)}-${cs.id}`}
|
|
400
|
+
onSelect={() =>
|
|
401
|
+
select({
|
|
402
|
+
componentId: c.id,
|
|
403
|
+
caseId: cs.id,
|
|
404
|
+
tweaks: {},
|
|
405
|
+
})
|
|
406
|
+
}
|
|
407
|
+
/>
|
|
408
|
+
))}
|
|
409
|
+
</div>
|
|
410
|
+
)
|
|
411
|
+
})}
|
|
412
|
+
</div>
|
|
413
|
+
))}
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function LibraryStage(props: ShellViewProps) {
|
|
420
|
+
const {
|
|
421
|
+
component,
|
|
422
|
+
activeCase,
|
|
423
|
+
addressUrl,
|
|
424
|
+
sel,
|
|
425
|
+
select,
|
|
426
|
+
attachPreview,
|
|
427
|
+
stageDecor,
|
|
428
|
+
showGrid,
|
|
429
|
+
sizeMeta,
|
|
430
|
+
padX,
|
|
431
|
+
padY,
|
|
432
|
+
stageShown,
|
|
433
|
+
boxW,
|
|
434
|
+
boxH,
|
|
435
|
+
fillFrame,
|
|
436
|
+
renderFrame,
|
|
437
|
+
tweaksFloating,
|
|
438
|
+
setTweaksFloating,
|
|
439
|
+
docOpen,
|
|
440
|
+
changeDocsOpen,
|
|
441
|
+
docText,
|
|
442
|
+
docWidth,
|
|
443
|
+
startDocResize,
|
|
444
|
+
onDocResizeKey,
|
|
445
|
+
} = props
|
|
446
|
+
if (!component || !activeCase) {
|
|
447
|
+
return <div className="dc-empty">Select a case.</div>
|
|
448
|
+
}
|
|
449
|
+
// The same crossfade the stage uses (driven by `stageShown`): on navigation the
|
|
450
|
+
// tweaks and accessibility panels fade out, then back in once the new exhibit
|
|
451
|
+
// has swapped in behind the fade — so the whole content column transitions as
|
|
452
|
+
// one instead of the panels snapping while the stage fades.
|
|
453
|
+
const fade: CSSProperties = {
|
|
454
|
+
opacity: stageShown ? 1 : 0,
|
|
455
|
+
transition: `opacity ${STAGE_FADE_MS}ms ease`,
|
|
456
|
+
}
|
|
457
|
+
return (
|
|
458
|
+
<div className="dc-stage">
|
|
459
|
+
<div className="dc-content">
|
|
460
|
+
{/* The exhibit's shareable address: the standalone /render URL for
|
|
461
|
+
what's on the stage (theme + tweak overrides), copyable. */}
|
|
462
|
+
<RenderAddress url={addressUrl} />
|
|
463
|
+
{component.isFlow && (
|
|
464
|
+
<FlowNav
|
|
465
|
+
steps={component.cases.map((cs) => ({
|
|
466
|
+
id: cs.id,
|
|
467
|
+
label: cs.name,
|
|
468
|
+
}))}
|
|
469
|
+
activeId={activeCase.id}
|
|
470
|
+
onSelect={(caseId) =>
|
|
471
|
+
select({ componentId: component.id, caseId, tweaks: {} })
|
|
472
|
+
}
|
|
473
|
+
/>
|
|
474
|
+
)}
|
|
475
|
+
{/* The preview is the stable centering viewport (measured for the
|
|
476
|
+
available area); the stage frame inside carries the border,
|
|
477
|
+
grid + corner ticks and hugs the exhibit (decorated) or fills
|
|
478
|
+
the viewport edge-to-edge (pages/flows). */}
|
|
479
|
+
<div className="dc-preview" ref={attachPreview}>
|
|
480
|
+
<Stage
|
|
481
|
+
caption={activeCase.name}
|
|
482
|
+
meta={sizeMeta}
|
|
483
|
+
frame={stageDecor ? 'hug' : 'fill'}
|
|
484
|
+
grid={stageDecor && showGrid}
|
|
485
|
+
corners={stageDecor}
|
|
486
|
+
surface={
|
|
487
|
+
stageDecor && !showGrid
|
|
488
|
+
? 'var(--color-bg, var(--dc-surface))'
|
|
489
|
+
: undefined
|
|
490
|
+
}
|
|
491
|
+
padX={stageDecor ? padX : undefined}
|
|
492
|
+
padY={stageDecor ? padY : undefined}
|
|
493
|
+
// Crossfade: fade out on navigation, back in once the new
|
|
494
|
+
// exhibit has swapped in (behind the fade) and been measured.
|
|
495
|
+
style={{
|
|
496
|
+
opacity: stageShown ? 1 : 0,
|
|
497
|
+
transition: `opacity ${STAGE_FADE_MS}ms ease`,
|
|
498
|
+
}}>
|
|
499
|
+
<div
|
|
500
|
+
className="dc-frame-box"
|
|
501
|
+
style={
|
|
502
|
+
fillFrame
|
|
503
|
+
? { width: '100%', height: '100%' }
|
|
504
|
+
: { width: `${boxW}px`, height: `${boxH}px` }
|
|
505
|
+
}>
|
|
506
|
+
{renderFrame}
|
|
507
|
+
</div>
|
|
508
|
+
</Stage>
|
|
509
|
+
</div>
|
|
510
|
+
|
|
511
|
+
{activeCase.tweaks && (
|
|
512
|
+
<div style={fade}>
|
|
513
|
+
<TweaksPanel
|
|
514
|
+
mode={tweaksFloating ? 'floating' : 'docked'}
|
|
515
|
+
// Surface the live shareable address (the same `/c/…?t.*` the
|
|
516
|
+
// browser is on) so the tweaked state is one copy away.
|
|
517
|
+
url={buildUrl(
|
|
518
|
+
component.id,
|
|
519
|
+
activeCase.id,
|
|
520
|
+
sel?.tweaks ?? {},
|
|
521
|
+
docOpen,
|
|
522
|
+
)}
|
|
523
|
+
onToggleMode={() => setTweaksFloating((f) => !f)}
|
|
524
|
+
items={Object.entries(activeCase.tweaks).map(([key, desc]) => ({
|
|
525
|
+
label: key,
|
|
526
|
+
control: (
|
|
527
|
+
<TweakControl
|
|
528
|
+
name={key}
|
|
529
|
+
desc={desc}
|
|
530
|
+
current={sel?.tweaks?.[key] ?? String(desc.default)}
|
|
531
|
+
onChange={(v) =>
|
|
532
|
+
sel &&
|
|
533
|
+
select({
|
|
534
|
+
...sel,
|
|
535
|
+
tweaks: { ...(sel.tweaks ?? {}), [key]: v },
|
|
536
|
+
})
|
|
537
|
+
}
|
|
538
|
+
/>
|
|
539
|
+
),
|
|
540
|
+
}))}
|
|
541
|
+
/>
|
|
542
|
+
</div>
|
|
543
|
+
)}
|
|
544
|
+
|
|
545
|
+
{/* The audit verdict for what's on the stage: the live variant's WCAG
|
|
546
|
+
violations, read in place rather than buried in a CLI log. Sits below
|
|
547
|
+
the Tweaks panel — it's the consequence of the tweaked state (cause →
|
|
548
|
+
effect), and a read-only report tolerates being pushed around by the
|
|
549
|
+
interactive panel above better than the reverse. Mounted only when
|
|
550
|
+
a11y scanning is configured (`a11y` present); otherwise there is no
|
|
551
|
+
panel at all. */}
|
|
552
|
+
{props.a11y && (
|
|
553
|
+
<div style={fade}>
|
|
554
|
+
<A11yPanel
|
|
555
|
+
violations={props.a11y.current}
|
|
556
|
+
reveal={props.a11y.reveal}
|
|
557
|
+
onRescan={props.rescanA11y}
|
|
558
|
+
// Snap the verdict colour for the whole navigation crossfade so it
|
|
559
|
+
// returns in the right colour instead of easing the previous
|
|
560
|
+
// exhibit's colour across the fade (in-place changes still ease).
|
|
561
|
+
instantColor={props.colorSnap}
|
|
562
|
+
/>
|
|
563
|
+
</div>
|
|
564
|
+
)}
|
|
565
|
+
</div>
|
|
566
|
+
|
|
567
|
+
{component.placardDoc && docOpen && (
|
|
568
|
+
<aside
|
|
569
|
+
className="dc-doc-panel"
|
|
570
|
+
data-testid={DcTestIds.docPanel}
|
|
571
|
+
aria-label="Documentation"
|
|
572
|
+
style={{ '--dc-doc-w': `${docWidth}px` } as CSSProperties}>
|
|
573
|
+
{/* biome-ignore lint/a11y/useSemanticElements: a draggable splitter, not a thematic break */}
|
|
574
|
+
<div
|
|
575
|
+
className="dc-doc-resize"
|
|
576
|
+
role="separator"
|
|
577
|
+
aria-orientation="vertical"
|
|
578
|
+
aria-label="Resize documentation panel"
|
|
579
|
+
aria-valuenow={docWidth}
|
|
580
|
+
aria-valuemin={DOC_MIN_W}
|
|
581
|
+
aria-valuemax={DOC_MAX_W}
|
|
582
|
+
tabIndex={0}
|
|
583
|
+
onPointerDown={startDocResize}
|
|
584
|
+
onKeyDown={onDocResizeKey}
|
|
585
|
+
/>
|
|
586
|
+
<div className="dc-doc-scroll">
|
|
587
|
+
<div className="dc-doc-head">
|
|
588
|
+
<Eyebrow>Documentation</Eyebrow>
|
|
589
|
+
<IconButton
|
|
590
|
+
glyph="✕"
|
|
591
|
+
variant="bare"
|
|
592
|
+
label="Close documentation"
|
|
593
|
+
onClick={() => changeDocsOpen(false)}
|
|
594
|
+
/>
|
|
595
|
+
</div>
|
|
596
|
+
{docText ? <DocMarkdown source={docText} /> : <p>Loading…</p>}
|
|
597
|
+
</div>
|
|
598
|
+
</aside>
|
|
599
|
+
)}
|
|
600
|
+
</div>
|
|
601
|
+
)
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Segmented control at the top of the sidebar: switch between the Primer
|
|
605
|
+
// (reading page) and the Cases library. Shown only when a Primer is configured.
|
|
606
|
+
// Just the two-option binding of the generic SegmentedToggle; `dc-modeswitch`
|
|
607
|
+
// supplies the sidebar-pinning layout (see chrome.css).
|
|
608
|
+
const MODE_SEGMENTS: SegmentedOption<Mode>[] = [
|
|
609
|
+
{ id: 'primer', label: 'Primer' },
|
|
610
|
+
{ id: 'library', label: 'Cases' },
|
|
611
|
+
]
|
|
612
|
+
|
|
613
|
+
function ModeSwitch({
|
|
614
|
+
mode,
|
|
615
|
+
onMode,
|
|
616
|
+
}: {
|
|
617
|
+
mode: Mode
|
|
618
|
+
onMode: (m: Mode) => void
|
|
619
|
+
}) {
|
|
620
|
+
return (
|
|
621
|
+
<SegmentedToggle
|
|
622
|
+
className="dc-modeswitch"
|
|
623
|
+
label="View mode"
|
|
624
|
+
options={MODE_SEGMENTS}
|
|
625
|
+
value={mode}
|
|
626
|
+
onChange={onMode}
|
|
627
|
+
testId={DcTestIds.modeSwitch}
|
|
628
|
+
/>
|
|
629
|
+
)
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Renders one tweak control using the design-system primitives. The TweaksPanel
|
|
633
|
+
// row supplies the label, so the control just needs an accessible name.
|
|
634
|
+
function TweakControl({
|
|
635
|
+
name,
|
|
636
|
+
desc,
|
|
637
|
+
current,
|
|
638
|
+
onChange,
|
|
639
|
+
}: {
|
|
640
|
+
name: string
|
|
641
|
+
desc: TweakDescriptor
|
|
642
|
+
current: string
|
|
643
|
+
onChange: (value: string) => void
|
|
644
|
+
}) {
|
|
645
|
+
if (desc.kind === 'boolean') {
|
|
646
|
+
return (
|
|
647
|
+
<input
|
|
648
|
+
type="checkbox"
|
|
649
|
+
aria-label={name}
|
|
650
|
+
checked={current === '1' || current === 'true'}
|
|
651
|
+
onChange={(e) => onChange(e.target.checked ? '1' : '0')}
|
|
652
|
+
/>
|
|
653
|
+
)
|
|
654
|
+
}
|
|
655
|
+
if (desc.kind === 'choice') {
|
|
656
|
+
// An accessible custom listbox (not a native <select>) so the picked value
|
|
657
|
+
// commits instantly — a native select on macOS defers its change event until
|
|
658
|
+
// the OS popup dismisses, which lagged the live stage update.
|
|
659
|
+
return (
|
|
660
|
+
<SelectMenu
|
|
661
|
+
size="sm"
|
|
662
|
+
aria-label={name}
|
|
663
|
+
options={desc.options}
|
|
664
|
+
value={current}
|
|
665
|
+
onChange={onChange}
|
|
666
|
+
/>
|
|
667
|
+
)
|
|
668
|
+
}
|
|
669
|
+
return (
|
|
670
|
+
<Input
|
|
671
|
+
size="sm"
|
|
672
|
+
aria-label={name}
|
|
673
|
+
type={desc.kind === 'number' ? 'number' : 'text'}
|
|
674
|
+
value={current}
|
|
675
|
+
onChange={(e) => onChange(e.target.value)}
|
|
676
|
+
/>
|
|
677
|
+
)
|
|
678
|
+
}
|