@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,277 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { useCallback, useEffect, useRef } from 'react'
|
|
3
|
+
import { slugify } from '../core/catalog'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The Primer — Display Case's long-form "wall text". A consumer authors an
|
|
7
|
+
* `.mdx` document (referenced from `display-case.config.ts`), and it renders
|
|
8
|
+
* here as a scrolling reading page with embedded LIVE specimens. The MDX can
|
|
9
|
+
* import any component — case files *and* arbitrary `.tsx` — and wraps each
|
|
10
|
+
* specimen in the {@link Display} contract below.
|
|
11
|
+
*
|
|
12
|
+
* This module is bundled into the isolated `/render/primer` document (never the browse
|
|
13
|
+
* chrome), so a specimen that throws on load can't blank the chrome — the same
|
|
14
|
+
* isolation the `/render` frame gives a case. It talks to the chrome over
|
|
15
|
+
* `postMessage`: it reports its section list (for the sidebar table of contents)
|
|
16
|
+
* and the active section on scroll, and accepts scroll-to / theme messages back.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export interface DisplayProps {
|
|
20
|
+
/** Specimen title — also the sidebar table-of-contents label and scroll anchor. */
|
|
21
|
+
title: string
|
|
22
|
+
/** Optional one-line description shown under the title. */
|
|
23
|
+
subtitle?: string
|
|
24
|
+
/** Force a theme inside this specimen (e.g. show a dark-mode component on a
|
|
25
|
+
* light primer). Omit to inherit the primer's current theme. */
|
|
26
|
+
theme?: 'light' | 'dark'
|
|
27
|
+
/** Drop the specimen's own border and padding so a single self-bordered child
|
|
28
|
+
* (e.g. a DefinitionList) fills the box edge-to-edge — avoids a
|
|
29
|
+
* box-within-a-box. The child supplies the border; this frame just clips it. */
|
|
30
|
+
flush?: boolean
|
|
31
|
+
/** Paint the specimen box with the consumer design system's own canvas
|
|
32
|
+
* (`--color-bg`/`--color-fg`) instead of the Vitrine's `--dc-bg`, so the
|
|
33
|
+
* component sits on the exact background the real app gives it. Opt-in;
|
|
34
|
+
* degrades to `--dc-bg` when the consumer defines no `--color-bg`. Combine
|
|
35
|
+
* with `theme` to show the app's themed surface. */
|
|
36
|
+
appSurface?: boolean
|
|
37
|
+
children?: ReactNode
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The contract an `.mdx` primer wraps each live specimen in. Renders a titled
|
|
42
|
+
* card; the body is a flex row the specimen lays out in. `theme` forces a
|
|
43
|
+
* light/dark scope local to the card.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* <Display title="Button" subtitle="The quiet bordered control" theme="dark">
|
|
47
|
+
* <Button variant="accent">Snapshot</Button>
|
|
48
|
+
* </Display>
|
|
49
|
+
*/
|
|
50
|
+
export function Display({
|
|
51
|
+
title,
|
|
52
|
+
subtitle,
|
|
53
|
+
theme,
|
|
54
|
+
flush,
|
|
55
|
+
appSurface,
|
|
56
|
+
children,
|
|
57
|
+
}: DisplayProps) {
|
|
58
|
+
return (
|
|
59
|
+
<section
|
|
60
|
+
className="dc-display"
|
|
61
|
+
id={`section-${slugify(title)}`}
|
|
62
|
+
data-dc-section=""
|
|
63
|
+
data-dc-title={title}>
|
|
64
|
+
<div className="dc-display-head">
|
|
65
|
+
<div className="dc-display-title">{title}</div>
|
|
66
|
+
{subtitle ? <div className="dc-display-sub">{subtitle}</div> : null}
|
|
67
|
+
</div>
|
|
68
|
+
<div
|
|
69
|
+
className="dc-display-specimen"
|
|
70
|
+
data-theme={theme}
|
|
71
|
+
data-flush={flush ? '' : undefined}
|
|
72
|
+
data-app-surface={appSurface ? '' : undefined}>
|
|
73
|
+
{children}
|
|
74
|
+
</div>
|
|
75
|
+
</section>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface PrimerSection {
|
|
80
|
+
id: string
|
|
81
|
+
title: string
|
|
82
|
+
/** `heading` is a `##` group header; `display` is a specimen card under it. */
|
|
83
|
+
kind: 'heading' | 'display'
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Flatten a React node tree to its text content (for a heading's slug + label). */
|
|
87
|
+
function textOf(node: ReactNode): string {
|
|
88
|
+
if (node == null || typeof node === 'boolean') return ''
|
|
89
|
+
if (typeof node === 'string' || typeof node === 'number') return String(node)
|
|
90
|
+
if (Array.isArray(node)) return node.map(textOf).join('')
|
|
91
|
+
if (typeof node === 'object' && 'props' in node)
|
|
92
|
+
return textOf(
|
|
93
|
+
(node as { props?: { children?: ReactNode } }).props?.children,
|
|
94
|
+
)
|
|
95
|
+
return ''
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The `#`/`##` headings in a primer MDX. Beyond rendering the prose heading,
|
|
100
|
+
* each becomes a navigable group header in the sidebar table of contents: the
|
|
101
|
+
* Displays that follow it nest under it (see {@link PrimerRoot} reporting). The
|
|
102
|
+
* H1 thus doubles as the "top of page" entry, with its intro specimens nested.
|
|
103
|
+
*/
|
|
104
|
+
function PrimerHeading({
|
|
105
|
+
tag: Tag,
|
|
106
|
+
children,
|
|
107
|
+
}: {
|
|
108
|
+
tag: 'h1' | 'h2'
|
|
109
|
+
children?: ReactNode
|
|
110
|
+
}) {
|
|
111
|
+
const text = textOf(children)
|
|
112
|
+
return (
|
|
113
|
+
<Tag
|
|
114
|
+
id={`heading-${slugify(text)}`}
|
|
115
|
+
data-dc-heading=""
|
|
116
|
+
data-dc-title={text}>
|
|
117
|
+
{children}
|
|
118
|
+
</Tag>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Components map handed to the compiled MDX — resolves `<Display>` and `#`/`##`. */
|
|
123
|
+
export const primerComponents = {
|
|
124
|
+
Display,
|
|
125
|
+
h1: ({ children }: { children?: ReactNode }) => (
|
|
126
|
+
<PrimerHeading tag="h1">{children}</PrimerHeading>
|
|
127
|
+
),
|
|
128
|
+
h2: ({ children }: { children?: ReactNode }) => (
|
|
129
|
+
<PrimerHeading tag="h2">{children}</PrimerHeading>
|
|
130
|
+
),
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The scrolling reading page. Renders the compiled MDX document and wires the
|
|
135
|
+
* scrollspy ↔ chrome messaging: it reports the section list and the active
|
|
136
|
+
* section, and reacts to scroll-to / theme messages from the parent chrome.
|
|
137
|
+
*/
|
|
138
|
+
export function PrimerRoot({
|
|
139
|
+
content,
|
|
140
|
+
}: {
|
|
141
|
+
content: (props: { components?: unknown }) => ReactNode
|
|
142
|
+
}) {
|
|
143
|
+
// Capitalize for JSX use (the compiled MDX document is a component).
|
|
144
|
+
const Content = content
|
|
145
|
+
const scrollRef = useRef<HTMLDivElement | null>(null)
|
|
146
|
+
// During a click-driven smooth scroll, the highlight is locked to the target
|
|
147
|
+
// and scrollspy is paused — otherwise the active row flickers across every
|
|
148
|
+
// section the scroll passes over (many hidden inside collapsed nav groups).
|
|
149
|
+
const programmatic = useRef(false)
|
|
150
|
+
const settleTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
151
|
+
const embedded = typeof window !== 'undefined' && window.parent !== window
|
|
152
|
+
|
|
153
|
+
const post = useCallback(
|
|
154
|
+
(message: unknown) => {
|
|
155
|
+
if (embedded) window.parent.postMessage(message, '*')
|
|
156
|
+
},
|
|
157
|
+
[embedded],
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
// Both `##` headings and Displays anchor the table of contents; in document
|
|
161
|
+
// order they let the chrome nest each Display under its heading.
|
|
162
|
+
const sectionEls = useCallback((): HTMLElement[] => {
|
|
163
|
+
const root = scrollRef.current
|
|
164
|
+
if (!root) return []
|
|
165
|
+
return Array.from(
|
|
166
|
+
root.querySelectorAll<HTMLElement>(
|
|
167
|
+
'[data-dc-section], [data-dc-heading]',
|
|
168
|
+
),
|
|
169
|
+
)
|
|
170
|
+
}, [])
|
|
171
|
+
|
|
172
|
+
const reportSections = useCallback(() => {
|
|
173
|
+
const sections: PrimerSection[] = sectionEls().map((el) => ({
|
|
174
|
+
id: el.id,
|
|
175
|
+
title: el.dataset.dcTitle ?? el.id,
|
|
176
|
+
kind: el.hasAttribute('data-dc-heading') ? 'heading' : 'display',
|
|
177
|
+
}))
|
|
178
|
+
post({ type: 'dc-primer-sections', sections })
|
|
179
|
+
}, [post, sectionEls])
|
|
180
|
+
|
|
181
|
+
const reportActive = useCallback(() => {
|
|
182
|
+
const root = scrollRef.current
|
|
183
|
+
if (!root) return
|
|
184
|
+
const els = sectionEls()
|
|
185
|
+
if (!els.length) return
|
|
186
|
+
// At (or near) the bottom the last section can't scroll to the top, so the
|
|
187
|
+
// "topmost past the line" rule would never reach it — pin it active there.
|
|
188
|
+
const atBottom = root.scrollTop + root.clientHeight >= root.scrollHeight - 4
|
|
189
|
+
let active = els[0].id
|
|
190
|
+
if (atBottom) {
|
|
191
|
+
active = els[els.length - 1].id
|
|
192
|
+
} else {
|
|
193
|
+
const top = root.getBoundingClientRect().top
|
|
194
|
+
for (const el of els) {
|
|
195
|
+
if (el.getBoundingClientRect().top - top <= 80) active = el.id
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
post({ type: 'dc-primer-active', id: active })
|
|
199
|
+
}, [post, sectionEls])
|
|
200
|
+
|
|
201
|
+
// Resume scrollspy once a programmatic scroll has settled (no scroll events
|
|
202
|
+
// for a beat) and re-sync the active section to where it actually landed. Also
|
|
203
|
+
// covers the no-op case where the target is already at the top, so nothing
|
|
204
|
+
// ever scrolls and `onScroll` never fires to release the lock.
|
|
205
|
+
const armSettle = useCallback(() => {
|
|
206
|
+
if (settleTimer.current) clearTimeout(settleTimer.current)
|
|
207
|
+
settleTimer.current = setTimeout(() => {
|
|
208
|
+
programmatic.current = false
|
|
209
|
+
reportActive()
|
|
210
|
+
}, 150)
|
|
211
|
+
}, [reportActive])
|
|
212
|
+
|
|
213
|
+
// Report the section list once mounted (and whenever the content resizes, e.g.
|
|
214
|
+
// late fonts/images shifting offsets), and keep the active section current.
|
|
215
|
+
useEffect(() => {
|
|
216
|
+
const root = scrollRef.current
|
|
217
|
+
if (!root) return
|
|
218
|
+
reportSections()
|
|
219
|
+
reportActive()
|
|
220
|
+
post({ type: 'dc-primer-ready' })
|
|
221
|
+
const onScroll = () => {
|
|
222
|
+
// While a click-driven scroll animates, hold the highlight on the target
|
|
223
|
+
// and just keep pushing the settle deadline out until the motion stops.
|
|
224
|
+
if (programmatic.current) {
|
|
225
|
+
armSettle()
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
reportActive()
|
|
229
|
+
}
|
|
230
|
+
root.addEventListener('scroll', onScroll, { passive: true })
|
|
231
|
+
const ro = new ResizeObserver(() => {
|
|
232
|
+
reportSections()
|
|
233
|
+
reportActive()
|
|
234
|
+
})
|
|
235
|
+
ro.observe(root)
|
|
236
|
+
return () => {
|
|
237
|
+
root.removeEventListener('scroll', onScroll)
|
|
238
|
+
ro.disconnect()
|
|
239
|
+
if (settleTimer.current) clearTimeout(settleTimer.current)
|
|
240
|
+
}
|
|
241
|
+
}, [reportSections, reportActive, armSettle, post])
|
|
242
|
+
|
|
243
|
+
// Accept scroll-to (sidebar TOC click) and theme messages from the chrome.
|
|
244
|
+
useEffect(() => {
|
|
245
|
+
function onMessage(e: MessageEvent) {
|
|
246
|
+
if (e.source !== window.parent) return
|
|
247
|
+
const data = e.data as { type?: string; id?: string; theme?: string }
|
|
248
|
+
if (data?.type === 'dc-primer-scroll' && data.id) {
|
|
249
|
+
// Section ids are slug-safe (`section-<kebab>`), so id lookup needs no
|
|
250
|
+
// escaping — and the local `CSS` here is the style string, not the global.
|
|
251
|
+
const el = document.getElementById(data.id)
|
|
252
|
+
if (el) {
|
|
253
|
+
// Lock the highlight to the clicked target and pause scrollspy until
|
|
254
|
+
// the smooth scroll settles, so it doesn't crawl across intermediate
|
|
255
|
+
// (often hidden) sections on the way there.
|
|
256
|
+
programmatic.current = true
|
|
257
|
+
post({ type: 'dc-primer-active', id: data.id })
|
|
258
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
259
|
+
armSettle()
|
|
260
|
+
}
|
|
261
|
+
} else if (data?.type === 'dc-primer-theme' && data.theme) {
|
|
262
|
+
document.documentElement.dataset.theme = data.theme
|
|
263
|
+
document.documentElement.dataset.themePref = data.theme
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
window.addEventListener('message', onMessage)
|
|
267
|
+
return () => window.removeEventListener('message', onMessage)
|
|
268
|
+
}, [post, armSettle])
|
|
269
|
+
|
|
270
|
+
return (
|
|
271
|
+
<div className="dc-primer" ref={scrollRef}>
|
|
272
|
+
<div className="dc-primer-inner">
|
|
273
|
+
<Content components={primerComponents} />
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
)
|
|
277
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { flushSync } from 'react-dom'
|
|
2
|
+
import type { Root } from 'react-dom/client'
|
|
3
|
+
import { createRoot, hydrateRoot } from 'react-dom/client'
|
|
4
|
+
import { slugify } from '../core/catalog'
|
|
5
|
+
import type { CaseModule, DisplayCaseConfig, GotoFn } from '../index'
|
|
6
|
+
import { caseTree, encodeOverrides } from '../render/render-node'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Entry point for the isolated `/render/:component/:case` document. Renders
|
|
10
|
+
* exactly one case (wrapped in the optional decorator) into #root and nothing
|
|
11
|
+
* else — this is what the browse iframe embeds and what screenshot tools
|
|
12
|
+
* capture.
|
|
13
|
+
*
|
|
14
|
+
* Three ways to drive it:
|
|
15
|
+
* - **Standalone** (snapshot/screenshot tools, direct navigation): the target
|
|
16
|
+
* case + theme + width + tweaks are read from the URL on load.
|
|
17
|
+
* - **Embedded** (browse chrome iframe): after announcing readiness, the parent
|
|
18
|
+
* pushes `dc-render` messages to swap case/theme/width/tweaks *in place*, so
|
|
19
|
+
* the iframe never reloads or remounts — no flicker on switch or tweak.
|
|
20
|
+
* - **In-flow transition**: a flow step calls its injected `goto`, which makes
|
|
21
|
+
* the target step active in place, updates the address (so the new step is
|
|
22
|
+
* deep-linkable/snapshottable), and notifies the chrome via `dc-step-changed`.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
interface RenderState {
|
|
26
|
+
componentId: string
|
|
27
|
+
caseId: string
|
|
28
|
+
theme: 'light' | 'dark'
|
|
29
|
+
width: number | null
|
|
30
|
+
tweaks: Record<string, string>
|
|
31
|
+
/** Shrink-wrap #root to the case's natural width (so a small component
|
|
32
|
+
* doesn't stretch to fill the frame). Driven by the browse chrome. */
|
|
33
|
+
fit: boolean
|
|
34
|
+
/** Drop the document background so the component shows on the stage's grid.
|
|
35
|
+
* Decorated components only; the chrome never sets it for pages/flows. */
|
|
36
|
+
transparent: boolean
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface NavOptions {
|
|
40
|
+
/** Push the active step to history so it is directly addressable. */
|
|
41
|
+
pushUrl?: boolean
|
|
42
|
+
/** Tell the parent chrome the active step changed (in-flow transitions). */
|
|
43
|
+
notifyParent?: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function stateFromUrl(): RenderState {
|
|
47
|
+
const params = new URLSearchParams(window.location.search)
|
|
48
|
+
const parts = window.location.pathname.split('/').filter(Boolean)
|
|
49
|
+
const widthParam = params.get('width')
|
|
50
|
+
const tweaks: Record<string, string> = {}
|
|
51
|
+
for (const [k, v] of params) {
|
|
52
|
+
if (k.startsWith('t.')) tweaks[k.slice(2)] = v
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
// Path shape: /render/<component>/<case>
|
|
56
|
+
componentId: parts[1] ?? '',
|
|
57
|
+
caseId: parts[2] ?? '',
|
|
58
|
+
theme: params.get('theme') === 'dark' ? 'dark' : 'light',
|
|
59
|
+
width: widthParam ? Number(widthParam) : null,
|
|
60
|
+
tweaks,
|
|
61
|
+
fit: params.get('fit') === '1',
|
|
62
|
+
transparent: params.get('transparent') === '1',
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Write the active step into the address so it can be deep-linked/snapshotted. */
|
|
67
|
+
function pushStepUrl(state: RenderState): void {
|
|
68
|
+
const params = new URLSearchParams()
|
|
69
|
+
params.set('theme', state.theme)
|
|
70
|
+
if (state.width) params.set('width', String(state.width))
|
|
71
|
+
for (const [k, v] of Object.entries(state.tweaks)) params.set(`t.${k}`, v)
|
|
72
|
+
window.history.pushState(
|
|
73
|
+
null,
|
|
74
|
+
'',
|
|
75
|
+
`/render/${state.componentId}/${state.caseId}?${params.toString()}`,
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Apply a render's document-level effects — the parts that live outside the
|
|
81
|
+
* React tree and so are set imperatively, not rendered. The server bakes these
|
|
82
|
+
* same values into the delivered document (so the first paint is correct and
|
|
83
|
+
* hydration matches); re-applying them here is idempotent on first load and
|
|
84
|
+
* carries an in-place swap (theme/width/transparent change) that the server
|
|
85
|
+
* never sees.
|
|
86
|
+
*/
|
|
87
|
+
function applyDocEffects(state: RenderState): void {
|
|
88
|
+
document.documentElement.dataset.theme = state.theme
|
|
89
|
+
// Also set the explicit preference so a `ThemeProvider` used inside app chrome
|
|
90
|
+
// (e.g. Navbar's ThemeToggle) initializes to the harness theme
|
|
91
|
+
// rather than re-resolving from the OS and fighting the `?theme=` selection.
|
|
92
|
+
document.documentElement.dataset.themePref = state.theme
|
|
93
|
+
|
|
94
|
+
// Shrink-wrap the mount to the case's natural width when asked, so a
|
|
95
|
+
// block/flex-rooted component (which would otherwise fill the full-width
|
|
96
|
+
// frame) renders at its intrinsic size. The chrome measures the result and
|
|
97
|
+
// hugs the stage to it. Cleared (full width) for pages/flows and presets.
|
|
98
|
+
const mount = document.getElementById('root')
|
|
99
|
+
if (mount) mount.style.width = state.fit ? 'fit-content' : ''
|
|
100
|
+
|
|
101
|
+
// Drop the document background so a decorated component sits directly on the
|
|
102
|
+
// stage's dotted grid (the chrome's stage frame paints the surface + grid
|
|
103
|
+
// behind the transparent iframe). Cleared back to the token bg otherwise.
|
|
104
|
+
document.body.style.background = state.transparent ? 'transparent' : ''
|
|
105
|
+
|
|
106
|
+
// Mark decorated exhibits (atoms…templates — never pages/flows, which lay out
|
|
107
|
+
// their own full-bleed structure) so the render doc can center the exhibit's
|
|
108
|
+
// content in the frame by default. Keyed off the same flag as `transparent`.
|
|
109
|
+
document.body.toggleAttribute('data-decorated', state.transparent)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Build the case's React tree — the identical tree the server pre-rendered —
|
|
113
|
+
* wiring a flow step's `goto` to drive an in-place transition. */
|
|
114
|
+
function treeFor(
|
|
115
|
+
modules: CaseModule[],
|
|
116
|
+
config: DisplayCaseConfig,
|
|
117
|
+
state: RenderState,
|
|
118
|
+
navigate: (next: RenderState, opts?: NavOptions) => void,
|
|
119
|
+
) {
|
|
120
|
+
const goto: GotoFn = (target, overrides) => {
|
|
121
|
+
// A transition makes the target step active in place; the new step gets its
|
|
122
|
+
// own address (overrides become its preset state) and the chrome is told to
|
|
123
|
+
// follow.
|
|
124
|
+
navigate(
|
|
125
|
+
{ ...state, caseId: slugify(target), tweaks: encodeOverrides(overrides) },
|
|
126
|
+
{ pushUrl: true, notifyParent: true },
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
return caseTree(
|
|
130
|
+
modules,
|
|
131
|
+
config,
|
|
132
|
+
{
|
|
133
|
+
componentId: state.componentId,
|
|
134
|
+
caseId: state.caseId,
|
|
135
|
+
width: state.width,
|
|
136
|
+
tweaks: state.tweaks,
|
|
137
|
+
},
|
|
138
|
+
goto,
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Neutralize anchor clicks that would unload the render frame. A case or flow
|
|
144
|
+
* step can legitimately render a real `<a href="/dashboard">` (the route would
|
|
145
|
+
* supply a router `<Link>`), but in this isolated frame there is no router, so a
|
|
146
|
+
* click does a full-document navigation to a non-render path. The server then
|
|
147
|
+
* serves the browse shell *into the frame*, nesting a shell and severing the
|
|
148
|
+
* parent↔frame handshake — every case/flow appears broken until a manual reload.
|
|
149
|
+
* Same-document hash links (in-page scroll) and `target=_blank` (new context)
|
|
150
|
+
* are harmless and left alone.
|
|
151
|
+
*/
|
|
152
|
+
function blockFrameNavigation(): void {
|
|
153
|
+
document.addEventListener(
|
|
154
|
+
'click',
|
|
155
|
+
(e) => {
|
|
156
|
+
if (
|
|
157
|
+
e.defaultPrevented ||
|
|
158
|
+
e.button !== 0 ||
|
|
159
|
+
e.metaKey ||
|
|
160
|
+
e.ctrlKey ||
|
|
161
|
+
e.shiftKey ||
|
|
162
|
+
e.altKey
|
|
163
|
+
) {
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
const anchor = (e.target as HTMLElement | null)?.closest?.('a')
|
|
167
|
+
const href = anchor?.getAttribute('href')
|
|
168
|
+
if (!anchor || !href) return
|
|
169
|
+
if (anchor.target && anchor.target !== '_self') return // new tab/window
|
|
170
|
+
const url = new URL(href, window.location.href)
|
|
171
|
+
const sameDocumentHash =
|
|
172
|
+
url.pathname === window.location.pathname &&
|
|
173
|
+
url.search === window.location.search &&
|
|
174
|
+
url.hash !== ''
|
|
175
|
+
if (sameDocumentHash) return // in-page scroll, doesn't unload the frame
|
|
176
|
+
e.preventDefault()
|
|
177
|
+
},
|
|
178
|
+
true,
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function mountRender(
|
|
183
|
+
modules: CaseModule[],
|
|
184
|
+
config: DisplayCaseConfig,
|
|
185
|
+
): void {
|
|
186
|
+
blockFrameNavigation()
|
|
187
|
+
const rootEl = document.getElementById('root') as HTMLElement
|
|
188
|
+
// The server pre-rendered the case into #root and flagged it `data-ssr="1"`;
|
|
189
|
+
// adopt that markup instead of mounting from scratch. A browser-only case is
|
|
190
|
+
// delivered empty (`data-ssr="0"`) and mounted fresh on the client.
|
|
191
|
+
const ssr = rootEl.dataset.ssr === '1'
|
|
192
|
+
let root: Root | null = null
|
|
193
|
+
let state = stateFromUrl()
|
|
194
|
+
const embedded = !!(window.parent && window.parent !== window)
|
|
195
|
+
|
|
196
|
+
// Report the rendered content's natural size to the browse chrome so it can
|
|
197
|
+
// shrink the iframe to the component (instead of stretching it to fill the
|
|
198
|
+
// panel) and center it on the stage. Measured off #root: its block height is
|
|
199
|
+
// the content height regardless of the iframe's (possibly taller) viewport,
|
|
200
|
+
// which `documentElement.scrollHeight` would clamp to. Hoisted above navigate
|
|
201
|
+
// so every swap re-announces the size — the ResizeObserver below only fires on
|
|
202
|
+
// an actual size *change*, so a swap between two same-size cases would never
|
|
203
|
+
// re-report, leaving the chrome (which hides the stage until the new size
|
|
204
|
+
// lands) waiting forever.
|
|
205
|
+
const postSize = (): void => {
|
|
206
|
+
if (!embedded) return
|
|
207
|
+
window.parent.postMessage(
|
|
208
|
+
{
|
|
209
|
+
type: 'dc-size',
|
|
210
|
+
size: {
|
|
211
|
+
height: Math.ceil(rootEl.scrollHeight),
|
|
212
|
+
width: Math.ceil(rootEl.scrollWidth),
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
'*',
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Single entry point for every state change — initial render, parent-driven
|
|
220
|
+
// `dc-render` swaps, and in-flow `goto` transitions all flow through here.
|
|
221
|
+
const navigate = (next: RenderState, opts: NavOptions = {}): void => {
|
|
222
|
+
state = next
|
|
223
|
+
if (opts.pushUrl) pushStepUrl(state)
|
|
224
|
+
if (opts.notifyParent && embedded) {
|
|
225
|
+
window.parent.postMessage(
|
|
226
|
+
{
|
|
227
|
+
type: 'dc-step-changed',
|
|
228
|
+
state: {
|
|
229
|
+
componentId: state.componentId,
|
|
230
|
+
caseId: state.caseId,
|
|
231
|
+
tweaks: state.tweaks,
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
'*',
|
|
235
|
+
)
|
|
236
|
+
}
|
|
237
|
+
applyDocEffects(state)
|
|
238
|
+
const tree = treeFor(modules, config, state, navigate)
|
|
239
|
+
if (!root) {
|
|
240
|
+
// First commit: adopt the server markup (hydrate) when present, otherwise
|
|
241
|
+
// mount fresh. Hydration is its own commit, so it isn't wrapped in
|
|
242
|
+
// flushSync; a recoverable mismatch (a non-deterministic case) is logged
|
|
243
|
+
// and React re-renders that subtree on the client.
|
|
244
|
+
if (ssr) {
|
|
245
|
+
root = hydrateRoot(rootEl, tree, {
|
|
246
|
+
onRecoverableError: (err) =>
|
|
247
|
+
console.warn(
|
|
248
|
+
'[display-case] adopt mismatch; client re-rendered:',
|
|
249
|
+
err,
|
|
250
|
+
),
|
|
251
|
+
})
|
|
252
|
+
} else {
|
|
253
|
+
root = createRoot(rootEl)
|
|
254
|
+
flushSync(() => (root as Root).render(tree))
|
|
255
|
+
}
|
|
256
|
+
} else {
|
|
257
|
+
// Commit synchronously so the measurement reads this case's layout (not the
|
|
258
|
+
// previous one's), then re-announce the size for this navigation.
|
|
259
|
+
flushSync(() => (root as Root).render(tree))
|
|
260
|
+
}
|
|
261
|
+
postSize()
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
navigate(state)
|
|
265
|
+
|
|
266
|
+
// Async layout changes (late images/fonts, content that resizes after mount)
|
|
267
|
+
// re-announce via the observer; navigate() covers the synchronous swaps.
|
|
268
|
+
if (embedded) new ResizeObserver(postSize).observe(rootEl)
|
|
269
|
+
|
|
270
|
+
// Embedded mode: accept in-place updates from the browse chrome so switching
|
|
271
|
+
// case / theme / width / tweaks never reloads the iframe.
|
|
272
|
+
window.addEventListener('message', (e: MessageEvent) => {
|
|
273
|
+
if (e.source !== window.parent) return
|
|
274
|
+
const data = e.data as { type?: string; state?: Partial<RenderState> }
|
|
275
|
+
if (data?.type !== 'dc-render' || !data.state) return
|
|
276
|
+
navigate({ ...state, ...data.state })
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
// Announce readiness so the parent can push the current selection. Harmless
|
|
280
|
+
// when standalone (parent is self; no dc-render messages are sent).
|
|
281
|
+
if (window.parent && window.parent !== window) {
|
|
282
|
+
window.parent.postMessage({ type: 'dc-ready' }, '*')
|
|
283
|
+
}
|
|
284
|
+
}
|