@scaffold-ui-kit/preview 0.0.0-stage → 2.3.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 +1868 -2
- package/application/host/bootstrap.ts +43 -0
- package/application/host/main.ts +31 -0
- package/application/host/shell-host.ts +47 -0
- package/application/index.html +12 -0
- package/application/lib/base-url.ts +15 -0
- package/application/lib/no-code-block.ts +7 -0
- package/application/lib/no-setup.ts +3 -0
- package/application/lib/no-views.ts +6 -0
- package/application/preview/code/code.html +12 -0
- package/application/preview/code/main.ts +89 -0
- package/application/preview/globals.ts +254 -0
- package/application/preview/html/main.ts +93 -0
- package/application/preview/html/preview.html +12 -0
- package/application/preview/react/main.tsx +146 -0
- package/application/preview/react/preview.html +12 -0
- package/application/preview/svelte/Preview.svelte +104 -0
- package/application/preview/svelte/main.ts +13 -0
- package/application/preview/svelte/preview.html +12 -0
- package/application/preview/svelte/story-props.ts +172 -0
- package/application/preview/view/main.ts +124 -0
- package/application/preview/view/view.html +12 -0
- package/application/preview/vue/main.ts +145 -0
- package/application/preview/vue/preview.html +12 -0
- package/application/shells/charcell/components/Application/index.css +37 -0
- package/application/shells/charcell/components/Application/index.tsx +65 -0
- package/application/shells/charcell/components/Bar/index.css +68 -0
- package/application/shells/charcell/components/Bar/index.tsx +60 -0
- package/application/shells/charcell/components/CodePane/index.css +60 -0
- package/application/shells/charcell/components/CodePane/index.tsx +113 -0
- package/application/shells/charcell/components/ComponentStage/index.css +26 -0
- package/application/shells/charcell/components/ComponentStage/index.tsx +110 -0
- package/application/shells/charcell/components/Control/index.css +20 -0
- package/application/shells/charcell/components/Control/index.tsx +118 -0
- package/application/shells/charcell/components/CopyLink/index.tsx +35 -0
- package/application/shells/charcell/components/Description/index.css +6 -0
- package/application/shells/charcell/components/Description/index.tsx +24 -0
- package/application/shells/charcell/components/EditorAddDialog/index.css +9 -0
- package/application/shells/charcell/components/EditorAddDialog/index.tsx +158 -0
- package/application/shells/charcell/components/EditorPanel/index.css +48 -0
- package/application/shells/charcell/components/EditorPanel/index.tsx +496 -0
- package/application/shells/charcell/components/EventsRow/index.css +42 -0
- package/application/shells/charcell/components/EventsRow/index.tsx +68 -0
- package/application/shells/charcell/components/HeadingRow/index.css +11 -0
- package/application/shells/charcell/components/HeadingRow/index.tsx +16 -0
- package/application/shells/charcell/components/Hint/index.css +16 -0
- package/application/shells/charcell/components/Hint/index.tsx +81 -0
- package/application/shells/charcell/components/HtmlWarnings/index.tsx +45 -0
- package/application/shells/charcell/components/KeyHints/index.css +27 -0
- package/application/shells/charcell/components/KeyHints/index.tsx +106 -0
- package/application/shells/charcell/components/KitCodeBlock/index.css +55 -0
- package/application/shells/charcell/components/KitCodeBlock/index.tsx +18 -0
- package/application/shells/charcell/components/Markdown/index.tsx +16 -0
- package/application/shells/charcell/components/Navigation/index.css +13 -0
- package/application/shells/charcell/components/Navigation/index.tsx +98 -0
- package/application/shells/charcell/components/NavigationSplit/index.css +16 -0
- package/application/shells/charcell/components/NavigationSplit/index.tsx +82 -0
- package/application/shells/charcell/components/OptionControls/index.tsx +93 -0
- package/application/shells/charcell/components/PaneStatus/index.css +7 -0
- package/application/shells/charcell/components/PaneStatus/index.tsx +47 -0
- package/application/shells/charcell/components/PaneWidth/index.tsx +45 -0
- package/application/shells/charcell/components/PreviewGrid/index.css +39 -0
- package/application/shells/charcell/components/PreviewGrid/index.tsx +344 -0
- package/application/shells/charcell/components/PreviewPane/index.css +127 -0
- package/application/shells/charcell/components/PreviewPane/index.tsx +182 -0
- package/application/shells/charcell/components/PromptBar/index.tsx +133 -0
- package/application/shells/charcell/components/ScenarioRow/index.css +36 -0
- package/application/shells/charcell/components/ScenarioRow/index.tsx +71 -0
- package/application/shells/charcell/components/Select/index.tsx +48 -0
- package/application/shells/charcell/components/SlotOutlet/index.css +7 -0
- package/application/shells/charcell/components/SlotOutlet/index.tsx +25 -0
- package/application/shells/charcell/components/StageBar/index.css +7 -0
- package/application/shells/charcell/components/StageBar/index.tsx +109 -0
- package/application/shells/charcell/components/StageSplit/index.css +11 -0
- package/application/shells/charcell/components/StageSplit/index.tsx +99 -0
- package/application/shells/charcell/components/StatusBar/index.tsx +30 -0
- package/application/shells/charcell/components/ViewControls/index.css +15 -0
- package/application/shells/charcell/components/ViewControls/index.tsx +58 -0
- package/application/shells/charcell/components/ViewHeading/index.css +13 -0
- package/application/shells/charcell/components/ViewHeading/index.tsx +8 -0
- package/application/shells/charcell/components/ViewStage/index.css +9 -0
- package/application/shells/charcell/components/ViewStage/index.tsx +25 -0
- package/application/shells/charcell/components/WrapToggle/index.tsx +27 -0
- package/application/shells/charcell/configuration.ts +45 -0
- package/application/shells/charcell/controller-context.ts +27 -0
- package/application/shells/charcell/fit-labels.ts +42 -0
- package/application/shells/charcell/html-warnings.ts +18 -0
- package/application/shells/charcell/index.tsx +82 -0
- package/application/shells/charcell/page-loading.html +57 -0
- package/application/shells/charcell/placement.ts +24 -0
- package/application/shells/charcell/shell-options.ts +23 -0
- package/application/shells/charcell/shell.css +37 -0
- package/application/shells/charcell/shell.tsx +25 -0
- package/application/shells/charcell/split-reset.ts +47 -0
- package/application/shells/charcell/stacked-rows.ts +117 -0
- package/application/shells/charcell/stage-toggles.ts +93 -0
- package/application/types/manifest.d.ts +6 -0
- package/application/types/preview-configuration.d.ts +10 -0
- package/application/types/preview-shell.d.ts +7 -0
- package/application/types/react-registry.d.ts +15 -0
- package/application/types/vue-registry.d.ts +6 -0
- package/dist/build.d.ts +24 -0
- package/dist/build.js +34 -0
- package/dist/check.d.ts +48 -0
- package/dist/check.js +781 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +88 -0
- package/dist/context.d.ts +179 -0
- package/dist/context.js +783 -0
- package/dist/controller/catalog.d.ts +339 -0
- package/dist/controller/catalog.js +367 -0
- package/dist/controller/create-preview-controller.d.ts +532 -0
- package/dist/controller/create-preview-controller.js +1707 -0
- package/dist/controller/editor-tree.d.ts +161 -0
- package/dist/controller/editor-tree.js +373 -0
- package/dist/controller/html-source.d.ts +62 -0
- package/dist/controller/html-source.js +61 -0
- package/dist/controller/index.d.ts +9 -0
- package/dist/controller/index.js +12 -0
- package/dist/controller/persistence.d.ts +191 -0
- package/dist/controller/persistence.js +315 -0
- package/dist/controller/regeneration.d.ts +30 -0
- package/dist/controller/regeneration.js +27 -0
- package/dist/controller/source.d.ts +80 -0
- package/dist/controller/source.js +236 -0
- package/dist/controller/store.d.ts +27 -0
- package/dist/controller/store.js +40 -0
- package/dist/declarations/application/shells/charcell/components/Application/index.d.ts +2 -0
- package/dist/declarations/application/shells/charcell/components/Bar/index.d.ts +11 -0
- package/dist/declarations/application/shells/charcell/components/CodePane/index.d.ts +5 -0
- package/dist/declarations/application/shells/charcell/components/ComponentStage/index.d.ts +2 -0
- package/dist/declarations/application/shells/charcell/components/Control/index.d.ts +9 -0
- package/dist/declarations/application/shells/charcell/components/CopyLink/index.d.ts +1 -0
- package/dist/declarations/application/shells/charcell/components/Description/index.d.ts +5 -0
- package/dist/declarations/application/shells/charcell/components/EditorAddDialog/index.d.ts +6 -0
- package/dist/declarations/application/shells/charcell/components/EditorPanel/index.d.ts +2 -0
- package/dist/declarations/application/shells/charcell/components/EventsRow/index.d.ts +4 -0
- package/dist/declarations/application/shells/charcell/components/HeadingRow/index.d.ts +6 -0
- package/dist/declarations/application/shells/charcell/components/Hint/index.d.ts +11 -0
- package/dist/declarations/application/shells/charcell/components/HtmlWarnings/index.d.ts +5 -0
- package/dist/declarations/application/shells/charcell/components/KeyHints/index.d.ts +2 -0
- package/dist/declarations/application/shells/charcell/components/KitCodeBlock/index.d.ts +5 -0
- package/dist/declarations/application/shells/charcell/components/Markdown/index.d.ts +3 -0
- package/dist/declarations/application/shells/charcell/components/Navigation/index.d.ts +8 -0
- package/dist/declarations/application/shells/charcell/components/NavigationSplit/index.d.ts +7 -0
- package/dist/declarations/application/shells/charcell/components/OptionControls/index.d.ts +7 -0
- package/dist/declarations/application/shells/charcell/components/PaneStatus/index.d.ts +2 -0
- package/dist/declarations/application/shells/charcell/components/PaneWidth/index.d.ts +4 -0
- package/dist/declarations/application/shells/charcell/components/PreviewGrid/index.d.ts +7 -0
- package/dist/declarations/application/shells/charcell/components/PreviewPane/index.d.ts +9 -0
- package/dist/declarations/application/shells/charcell/components/PromptBar/index.d.ts +1 -0
- package/dist/declarations/application/shells/charcell/components/ScenarioRow/index.d.ts +2 -0
- package/dist/declarations/application/shells/charcell/components/Select/index.d.ts +16 -0
- package/dist/declarations/application/shells/charcell/components/SlotOutlet/index.d.ts +5 -0
- package/dist/declarations/application/shells/charcell/components/StageBar/index.d.ts +2 -0
- package/dist/declarations/application/shells/charcell/components/StageSplit/index.d.ts +17 -0
- package/dist/declarations/application/shells/charcell/components/StatusBar/index.d.ts +1 -0
- package/dist/declarations/application/shells/charcell/components/ViewControls/index.d.ts +9 -0
- package/dist/declarations/application/shells/charcell/components/ViewHeading/index.d.ts +4 -0
- package/dist/declarations/application/shells/charcell/components/ViewStage/index.d.ts +4 -0
- package/dist/declarations/application/shells/charcell/components/WrapToggle/index.d.ts +1 -0
- package/dist/declarations/application/shells/charcell/configuration.d.ts +31 -0
- package/dist/declarations/application/shells/charcell/controller-context.d.ts +9 -0
- package/dist/declarations/application/shells/charcell/fit-labels.d.ts +18 -0
- package/dist/declarations/application/shells/charcell/html-warnings.d.ts +5 -0
- package/dist/declarations/application/shells/charcell/index.d.ts +39 -0
- package/dist/declarations/application/shells/charcell/placement.d.ts +7 -0
- package/dist/declarations/application/shells/charcell/shell-options.d.ts +15 -0
- package/dist/declarations/application/shells/charcell/shell.d.ts +9 -0
- package/dist/declarations/application/shells/charcell/split-reset.d.ts +30 -0
- package/dist/declarations/application/shells/charcell/stacked-rows.d.ts +58 -0
- package/dist/declarations/application/shells/charcell/stage-toggles.d.ts +28 -0
- package/dist/declarations/src/controller/catalog.d.ts +339 -0
- package/dist/declarations/src/controller/create-preview-controller.d.ts +532 -0
- package/dist/declarations/src/controller/editor-tree.d.ts +161 -0
- package/dist/declarations/src/controller/html-source.d.ts +62 -0
- package/dist/declarations/src/controller/index.d.ts +9 -0
- package/dist/declarations/src/controller/persistence.d.ts +191 -0
- package/dist/declarations/src/controller/regeneration.d.ts +30 -0
- package/dist/declarations/src/controller/source.d.ts +80 -0
- package/dist/declarations/src/controller/store.d.ts +27 -0
- package/dist/declarations/src/emit/common.d.ts +103 -0
- package/dist/declarations/src/emit/manifest.d.ts +138 -0
- package/dist/declarations/src/emit/source.d.ts +87 -0
- package/dist/declarations/src/lib/jsx-roots.d.ts +22 -0
- package/dist/declarations/src/lib/module-of-source.d.ts +18 -0
- package/dist/declarations/src/lib/naming.d.ts +29 -0
- package/dist/declarations/src/lib/react-spelling.d.ts +68 -0
- package/dist/declarations/src/lib/scenario-story.d.ts +41 -0
- package/dist/declarations/src/lib/slot-fill.d.ts +15 -0
- package/dist/declarations/src/lib/types.d.ts +741 -0
- package/dist/declarations/src/options.d.ts +224 -0
- package/dist/declarations/src/protocol.d.ts +541 -0
- package/dist/declarations/src/react-view.d.ts +22 -0
- package/dist/declarations/src/react.d.ts +17 -0
- package/dist/declarations/src/shell.d.ts +99 -0
- package/dist/declarations/src/views.d.ts +152 -0
- package/dist/development-server.d.ts +121 -0
- package/dist/development-server.js +331 -0
- package/dist/emit/common.d.ts +103 -0
- package/dist/emit/common.js +170 -0
- package/dist/emit/html-source.d.ts +46 -0
- package/dist/emit/html-source.js +322 -0
- package/dist/emit/manifest.d.ts +138 -0
- package/dist/emit/manifest.js +74 -0
- package/dist/emit/react.d.ts +16 -0
- package/dist/emit/react.js +210 -0
- package/dist/emit/scratch.d.ts +40 -0
- package/dist/emit/scratch.js +116 -0
- package/dist/emit/source.d.ts +87 -0
- package/dist/emit/source.js +789 -0
- package/dist/emit/svelte.d.ts +9 -0
- package/dist/emit/svelte.js +177 -0
- package/dist/emit/vue.d.ts +2 -0
- package/dist/emit/vue.js +152 -0
- package/dist/enumerate.d.ts +2 -0
- package/dist/enumerate.js +162 -0
- package/dist/generate.d.ts +43 -0
- package/dist/generate.js +302 -0
- package/dist/html-stylesheet.d.ts +43 -0
- package/dist/html-stylesheet.js +145 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +21 -0
- package/dist/landing.d.ts +16 -0
- package/dist/landing.js +45 -0
- package/dist/lib/exported-types.d.ts +42 -0
- package/dist/lib/exported-types.js +86 -0
- package/dist/lib/html-attributes.d.ts +2 -0
- package/dist/lib/html-attributes.js +111 -0
- package/dist/lib/html-warnings.d.ts +19 -0
- package/dist/lib/html-warnings.js +79 -0
- package/dist/lib/jsx-roots.d.ts +22 -0
- package/dist/lib/jsx-roots.js +243 -0
- package/dist/lib/module-of-source.d.ts +18 -0
- package/dist/lib/module-of-source.js +24 -0
- package/dist/lib/naming.d.ts +29 -0
- package/dist/lib/naming.js +56 -0
- package/dist/lib/parse-type.d.ts +32 -0
- package/dist/lib/parse-type.js +103 -0
- package/dist/lib/react-spelling.d.ts +68 -0
- package/dist/lib/react-spelling.js +197 -0
- package/dist/lib/scenario-story.d.ts +41 -0
- package/dist/lib/scenario-story.js +84 -0
- package/dist/lib/slot-fill.d.ts +15 -0
- package/dist/lib/slot-fill.js +92 -0
- package/dist/lib/template-files.d.ts +14 -0
- package/dist/lib/template-files.js +25 -0
- package/dist/lib/types.d.ts +741 -0
- package/dist/lib/types.js +13 -0
- package/dist/lib/void-elements.d.ts +7 -0
- package/dist/lib/void-elements.js +13 -0
- package/dist/load.d.ts +37 -0
- package/dist/load.js +82 -0
- package/dist/model.d.ts +44 -0
- package/dist/model.js +464 -0
- package/dist/options.d.ts +224 -0
- package/dist/options.js +223 -0
- package/dist/prop-types.d.ts +26 -0
- package/dist/prop-types.js +102 -0
- package/dist/props-from.d.ts +84 -0
- package/dist/props-from.js +510 -0
- package/dist/protocol.d.ts +541 -0
- package/dist/protocol.js +819 -0
- package/dist/react-view.d.ts +22 -0
- package/dist/react-view.js +36 -0
- package/dist/react.d.ts +17 -0
- package/dist/react.js +23 -0
- package/dist/sass-importers.d.ts +26 -0
- package/dist/sass-importers.js +32 -0
- package/dist/shell-acceptance.d.ts +21 -0
- package/dist/shell-acceptance.js +37 -0
- package/dist/shell-page.d.ts +36 -0
- package/dist/shell-page.js +90 -0
- package/dist/shell-peers.d.ts +38 -0
- package/dist/shell-peers.js +61 -0
- package/dist/shell.d.ts +99 -0
- package/dist/shell.js +52 -0
- package/dist/shells/charcell/page.d.ts +8 -0
- package/dist/shells/charcell/page.js +69 -0
- package/dist/story-source.d.ts +113 -0
- package/dist/story-source.js +271 -0
- package/dist/story.d.ts +5 -0
- package/dist/story.js +28 -0
- package/dist/testing.d.ts +218 -0
- package/dist/testing.js +284 -0
- package/dist/views.d.ts +152 -0
- package/dist/views.js +7 -0
- package/dist/vite-configuration.d.ts +114 -0
- package/dist/vite-configuration.js +578 -0
- package/package.json +123 -3
- package/src/build.ts +51 -0
- package/src/check.ts +1062 -0
- package/src/cli.ts +123 -0
- package/src/context.ts +1168 -0
- package/src/controller/catalog.ts +719 -0
- package/src/controller/create-preview-controller.ts +2683 -0
- package/src/controller/editor-tree.ts +530 -0
- package/src/controller/html-source.ts +114 -0
- package/src/controller/index.ts +105 -0
- package/src/controller/persistence.ts +403 -0
- package/src/controller/regeneration.ts +55 -0
- package/src/controller/source.ts +333 -0
- package/src/controller/store.ts +55 -0
- package/src/development-server.ts +442 -0
- package/src/emit/common.ts +201 -0
- package/src/emit/html-source.ts +402 -0
- package/src/emit/manifest.ts +241 -0
- package/src/emit/react.ts +263 -0
- package/src/emit/scratch.ts +175 -0
- package/src/emit/source.ts +1094 -0
- package/src/emit/svelte.ts +230 -0
- package/src/emit/vue.ts +194 -0
- package/src/enumerate.ts +220 -0
- package/src/generate.ts +443 -0
- package/src/html-stylesheet.ts +173 -0
- package/src/index.ts +60 -0
- package/src/landing.ts +58 -0
- package/src/lib/exported-types.ts +114 -0
- package/src/lib/html-attributes.ts +113 -0
- package/src/lib/html-warnings.ts +81 -0
- package/src/lib/jsx-roots.ts +238 -0
- package/src/lib/module-of-source.ts +26 -0
- package/src/lib/naming.ts +65 -0
- package/src/lib/parse-type.ts +143 -0
- package/src/lib/react-spelling.ts +235 -0
- package/src/lib/scenario-story.ts +97 -0
- package/src/lib/slot-fill.ts +102 -0
- package/src/lib/template-files.ts +31 -0
- package/src/lib/types.ts +778 -0
- package/src/lib/void-elements.ts +15 -0
- package/src/load.ts +144 -0
- package/src/model.ts +568 -0
- package/src/options.ts +435 -0
- package/src/prop-types.ts +133 -0
- package/src/props-from.ts +678 -0
- package/src/protocol.ts +1135 -0
- package/src/react-view.ts +45 -0
- package/src/react.ts +34 -0
- package/src/sass-importers.ts +45 -0
- package/src/shell-acceptance.ts +45 -0
- package/src/shell-page.ts +133 -0
- package/src/shell-peers.ts +70 -0
- package/src/shell.ts +194 -0
- package/src/shells/charcell/page.ts +82 -0
- package/src/story-source.ts +391 -0
- package/src/story.ts +42 -0
- package/src/testing.ts +497 -0
- package/src/views.ts +183 -0
- package/src/vite-configuration.ts +675 -0
package/src/protocol.ts
ADDED
|
@@ -0,0 +1,1135 @@
|
|
|
1
|
+
// The messages the preview host and its iframes exchange over postMessage, the
|
|
2
|
+
// flag an iframe sets once it can receive them, and the URLs the iframes load.
|
|
3
|
+
// The host, the five iframe entries and the testing helper all speak through
|
|
4
|
+
// this module, so the vocabulary lives in one place. Browser-safe - no node
|
|
5
|
+
// imports; type-only imports keep it free of runtime dependencies.
|
|
6
|
+
|
|
7
|
+
import type { OptionValue, PreviewOption } from './options.js';
|
|
8
|
+
import type {
|
|
9
|
+
CodeBlockContent,
|
|
10
|
+
ViewContextComponent,
|
|
11
|
+
ViewContextSource,
|
|
12
|
+
ViewFramework,
|
|
13
|
+
} from './views.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* What a story pane renders in: a scripted target, which a component preview
|
|
17
|
+
* iframe runs the story's script in, or `html`, the markup the story renders
|
|
18
|
+
* to with no JavaScript of its own.
|
|
19
|
+
*/
|
|
20
|
+
export const PREVIEW_TARGETS = ['react', 'vue', 'svelte', 'html'] as const;
|
|
21
|
+
|
|
22
|
+
/** What a story pane renders in: one of {@link PREVIEW_TARGETS}. */
|
|
23
|
+
export type PreviewTarget = (typeof PREVIEW_TARGETS)[number];
|
|
24
|
+
|
|
25
|
+
/** A target a component preview iframe runs a story's script in: every
|
|
26
|
+
* target but `html`. */
|
|
27
|
+
export type PreviewScriptedTarget = Exclude<PreviewTarget, 'html'>;
|
|
28
|
+
|
|
29
|
+
/** The scripted targets, in the preview's order: {@link PREVIEW_TARGETS}
|
|
30
|
+
* without `html`. */
|
|
31
|
+
export const PREVIEW_SCRIPTED_TARGETS = [
|
|
32
|
+
'react',
|
|
33
|
+
'vue',
|
|
34
|
+
'svelte',
|
|
35
|
+
] as const satisfies readonly PreviewScriptedTarget[];
|
|
36
|
+
|
|
37
|
+
/** The `type` of each message, keyed by a short name. */
|
|
38
|
+
export const PREVIEW_MESSAGE = {
|
|
39
|
+
/** Iframe → host: the iframe listens for messages now (sent on each load). */
|
|
40
|
+
ready: 'preview:ready',
|
|
41
|
+
/** Host → iframe: mount a story by id with its root props. */
|
|
42
|
+
setStory: 'preview:set-story',
|
|
43
|
+
/** Host → iframe: replace the mounted story's props in place. */
|
|
44
|
+
props: 'preview:props',
|
|
45
|
+
/** Host → iframe: the merged option values to apply to the document. */
|
|
46
|
+
globals: 'preview:globals',
|
|
47
|
+
/** Host → view iframe: the catalog a custom view builds its context from. */
|
|
48
|
+
catalog: 'preview:catalog',
|
|
49
|
+
/** View iframe → host: open a component story, as a navigation click does. */
|
|
50
|
+
select: 'preview:select',
|
|
51
|
+
/** Iframe → host: the size of what the iframe shows, sent once it renders
|
|
52
|
+
* and again on each change. */
|
|
53
|
+
contentSize: 'preview:content-size',
|
|
54
|
+
/** Host → html iframe: the markup to show, or why there is none. */
|
|
55
|
+
markup: 'preview:markup',
|
|
56
|
+
/** Iframe → host: the story threw as it rendered, or, with no message,
|
|
57
|
+
* the story it mounts since renders. */
|
|
58
|
+
renderError: 'preview:render-error',
|
|
59
|
+
/** Host → code iframe: the source the kit's code block shows. */
|
|
60
|
+
code: 'preview:code',
|
|
61
|
+
/** Iframe → host: the story called one of its events. */
|
|
62
|
+
event: 'preview:event',
|
|
63
|
+
/** Iframe → host: its first content has mounted and painted, so the host
|
|
64
|
+
* can show the iframe. Sent once per document. */
|
|
65
|
+
shown: 'preview:shown',
|
|
66
|
+
} as const;
|
|
67
|
+
|
|
68
|
+
/** The `type` of a preview message. */
|
|
69
|
+
export type PreviewMessageType =
|
|
70
|
+
(typeof PREVIEW_MESSAGE)[keyof typeof PREVIEW_MESSAGE];
|
|
71
|
+
|
|
72
|
+
/** Sent by an iframe once its message listener is attached. */
|
|
73
|
+
export interface PreviewReadyMessage {
|
|
74
|
+
type: typeof PREVIEW_MESSAGE.ready;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Sent by an iframe once its first content has mounted and painted. */
|
|
78
|
+
export interface PreviewShownMessage {
|
|
79
|
+
type: typeof PREVIEW_MESSAGE.shown;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Mounts the story `id` afresh with `props` as its root props. */
|
|
83
|
+
export interface PreviewSetStoryMessage {
|
|
84
|
+
type: typeof PREVIEW_MESSAGE.setStory;
|
|
85
|
+
id: string;
|
|
86
|
+
props: Record<string, unknown>;
|
|
87
|
+
/** The story's events: the function props a pane records the calls of,
|
|
88
|
+
* reporting each as a `preview:event`. */
|
|
89
|
+
events?: string[];
|
|
90
|
+
/** The story's two-way props and the event each reports its new value
|
|
91
|
+
* through. A pane may take a value a story's given prop reports as that
|
|
92
|
+
* prop at once, as the host does after it (see {@link withWriteBacks}). */
|
|
93
|
+
bindings?: PreviewWriteBack[];
|
|
94
|
+
/** Which of the host's story mounts this is: a pane tags each event it
|
|
95
|
+
* records with it, so the host can tell an event of a story it has since
|
|
96
|
+
* replaced (see {@link withEventRecorders}). */
|
|
97
|
+
mount?: number;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** A two-way prop and the event prop it reports each new value through. */
|
|
101
|
+
export interface PreviewWriteBack {
|
|
102
|
+
/** The two-way prop. */
|
|
103
|
+
prop: string;
|
|
104
|
+
/** The event prop called with each new value. */
|
|
105
|
+
changeProp: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Replaces the mounted story's root props, keeping its component instance. */
|
|
109
|
+
export interface PreviewPropsMessage {
|
|
110
|
+
type: typeof PREVIEW_MESSAGE.props;
|
|
111
|
+
props: Record<string, unknown>;
|
|
112
|
+
/** The newest of the receiving pane's own write-backs the host had taken
|
|
113
|
+
* when it sent the props (the `write` of the pane's events, see
|
|
114
|
+
* {@link PendingWriteBacks}): a pane keeps a later write of its own over
|
|
115
|
+
* the value these props hold. Absent, the pane takes the props as they
|
|
116
|
+
* are. */
|
|
117
|
+
answers?: number;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The option values (global and story-scoped, merged) the iframe applies. */
|
|
121
|
+
export interface PreviewGlobalsMessage {
|
|
122
|
+
type: typeof PREVIEW_MESSAGE.globals;
|
|
123
|
+
options: Record<string, OptionValue>;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The catalog a custom view reads: the kit's name, its sources and its stories. */
|
|
127
|
+
export interface PreviewCatalogPayload {
|
|
128
|
+
kitName: string;
|
|
129
|
+
sources: ViewContextSource[];
|
|
130
|
+
components: ViewContextComponent[];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Delivers the {@link PreviewCatalogPayload} to the view iframe. */
|
|
134
|
+
export interface PreviewCatalogMessage extends PreviewCatalogPayload {
|
|
135
|
+
type: typeof PREVIEW_MESSAGE.catalog;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Asks the host to open a component story, optionally with root props. */
|
|
139
|
+
export interface PreviewSelectMessage {
|
|
140
|
+
type: typeof PREVIEW_MESSAGE.select;
|
|
141
|
+
id: string;
|
|
142
|
+
props?: Record<string, unknown>;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** The size of what a preview iframe shows, in whole CSS pixels. */
|
|
146
|
+
export interface PreviewContentSize {
|
|
147
|
+
width: number;
|
|
148
|
+
height: number;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Reports the size of what an iframe shows: its story or view, with the
|
|
153
|
+
* padding, borders and margins of the elements around it, rounded up to
|
|
154
|
+
* whole CSS pixels, so an iframe of that size shows it all.
|
|
155
|
+
*/
|
|
156
|
+
export interface PreviewContentSizeMessage extends PreviewContentSize {
|
|
157
|
+
type: typeof PREVIEW_MESSAGE.contentSize;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The html iframe's story: its `markup`, or the `reason` it has none. With
|
|
162
|
+
* neither, the markup is being printed, and the iframe keeps what it shows.
|
|
163
|
+
*/
|
|
164
|
+
export interface PreviewMarkupMessage {
|
|
165
|
+
type: typeof PREVIEW_MESSAGE.markup;
|
|
166
|
+
markup?: string;
|
|
167
|
+
/** The styles of the components the markup renders, shown with it. */
|
|
168
|
+
stylesheet?: string;
|
|
169
|
+
reason?: string;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Reports what an iframe's story threw as it rendered (`message`), shown in
|
|
174
|
+
* the story's place; with no `message`, the story mounted since renders.
|
|
175
|
+
*/
|
|
176
|
+
export interface PreviewRenderErrorMessage {
|
|
177
|
+
type: typeof PREVIEW_MESSAGE.renderError;
|
|
178
|
+
message?: string;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** The source a code iframe's block shows: a target's, in its language, or
|
|
182
|
+
* why it has none; with neither, it is being printed. */
|
|
183
|
+
export interface PreviewCodeMessage extends CodeBlockContent {
|
|
184
|
+
type: typeof PREVIEW_MESSAGE.code;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Reports a story's call of one of its events: its `name`, a readable
|
|
188
|
+
* summary of what it was called with, and its first argument as `value`
|
|
189
|
+
* when that is plain data (what JSON holds), such as the new value a
|
|
190
|
+
* two-way prop's change callback reports. */
|
|
191
|
+
export interface PreviewEventMessage {
|
|
192
|
+
type: typeof PREVIEW_MESSAGE.event;
|
|
193
|
+
name: string;
|
|
194
|
+
payload: string;
|
|
195
|
+
value?: unknown;
|
|
196
|
+
/** The `mount` of the set-story message the pane was showing. */
|
|
197
|
+
mount?: number;
|
|
198
|
+
/** The number of the newest value the pane wrote back itself since its
|
|
199
|
+
* last report, when it wrote one (see {@link PendingWriteBacks}): the
|
|
200
|
+
* host answers it on the pane's next props message. */
|
|
201
|
+
write?: number;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Any message the host and the iframes exchange. */
|
|
205
|
+
export type PreviewMessage =
|
|
206
|
+
| PreviewReadyMessage
|
|
207
|
+
| PreviewSetStoryMessage
|
|
208
|
+
| PreviewPropsMessage
|
|
209
|
+
| PreviewGlobalsMessage
|
|
210
|
+
| PreviewCatalogMessage
|
|
211
|
+
| PreviewSelectMessage
|
|
212
|
+
| PreviewContentSizeMessage
|
|
213
|
+
| PreviewMarkupMessage
|
|
214
|
+
| PreviewRenderErrorMessage
|
|
215
|
+
| PreviewCodeMessage
|
|
216
|
+
| PreviewEventMessage
|
|
217
|
+
| PreviewShownMessage;
|
|
218
|
+
|
|
219
|
+
/** The message whose `type` is `Type`. */
|
|
220
|
+
export type PreviewMessageOf<Type extends PreviewMessageType> = Extract<
|
|
221
|
+
PreviewMessage,
|
|
222
|
+
{ type: Type }
|
|
223
|
+
>;
|
|
224
|
+
|
|
225
|
+
const MESSAGE_TYPES = new Set<string>(Object.values(PREVIEW_MESSAGE));
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Read posted data as a preview message: the data itself when it is an object
|
|
229
|
+
* whose `type` is one of the preview's, otherwise `undefined`. Only the `type`
|
|
230
|
+
* is checked; a receiver validates the payload it relies on.
|
|
231
|
+
*
|
|
232
|
+
* @param data - A `MessageEvent`'s `data`.
|
|
233
|
+
* @returns The data as a {@link PreviewMessage}, or `undefined`.
|
|
234
|
+
*/
|
|
235
|
+
export function readPreviewMessage(data: unknown): PreviewMessage | undefined {
|
|
236
|
+
if (!data || typeof data !== 'object') return undefined;
|
|
237
|
+
const type = (data as { type?: unknown }).type;
|
|
238
|
+
return typeof type === 'string' && MESSAGE_TYPES.has(type)
|
|
239
|
+
? (data as PreviewMessage)
|
|
240
|
+
: undefined;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Whether posted data is a preview message of one type.
|
|
245
|
+
*
|
|
246
|
+
* @param data - A `MessageEvent`'s `data`.
|
|
247
|
+
* @param type - The message type to match.
|
|
248
|
+
* @returns `true` when `data` is an object whose `type` is `type`.
|
|
249
|
+
*/
|
|
250
|
+
export function isPreviewMessage<Type extends PreviewMessageType>(
|
|
251
|
+
data: unknown,
|
|
252
|
+
type: Type
|
|
253
|
+
): data is PreviewMessageOf<Type> {
|
|
254
|
+
return readPreviewMessage(data)?.type === type;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** @returns A {@link PreviewReadyMessage}. */
|
|
258
|
+
export function readyMessage(): PreviewReadyMessage {
|
|
259
|
+
return { type: PREVIEW_MESSAGE.ready };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* @param id - The story's id.
|
|
264
|
+
* @param props - Its root props.
|
|
265
|
+
* @param events - The function props a pane records the calls of.
|
|
266
|
+
* @param bindings - The two-way props and the events they report through.
|
|
267
|
+
* @param mount - Which of the host's story mounts this is.
|
|
268
|
+
* @returns A {@link PreviewSetStoryMessage}.
|
|
269
|
+
*/
|
|
270
|
+
export function setStoryMessage(
|
|
271
|
+
id: string,
|
|
272
|
+
props: Record<string, unknown>,
|
|
273
|
+
events?: string[],
|
|
274
|
+
bindings?: readonly PreviewWriteBack[],
|
|
275
|
+
mount?: number
|
|
276
|
+
): PreviewSetStoryMessage {
|
|
277
|
+
return {
|
|
278
|
+
type: PREVIEW_MESSAGE.setStory,
|
|
279
|
+
id,
|
|
280
|
+
props,
|
|
281
|
+
...(mount !== undefined ? { mount } : {}),
|
|
282
|
+
...(events && events.length > 0 ? { events } : {}),
|
|
283
|
+
...(bindings && bindings.length > 0
|
|
284
|
+
? {
|
|
285
|
+
bindings: bindings.map(({ prop, changeProp }) => ({
|
|
286
|
+
prop,
|
|
287
|
+
changeProp,
|
|
288
|
+
})),
|
|
289
|
+
}
|
|
290
|
+
: {}),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* The story's props with each two-way prop's change event writing the value
|
|
296
|
+
* it reports back as that prop at once, through `write`, while the story
|
|
297
|
+
* gives the prop: what the host does with the value after it hears of the
|
|
298
|
+
* call, done inside the pane, so a framework that restores a controlled
|
|
299
|
+
* input to its prop's value as the event ends (React) finds the new value
|
|
300
|
+
* there and keeps the caret where it was. The host's own write-back follows
|
|
301
|
+
* with the same value. Only plain data is written, as the host takes only
|
|
302
|
+
* plain data; a prop the story leaves out stays the component's own.
|
|
303
|
+
*
|
|
304
|
+
* @param props - The story's root props, its events' recorders in place.
|
|
305
|
+
* @param bindings - The two-way props and the events they report through.
|
|
306
|
+
* @param write - Takes a two-way prop's new value.
|
|
307
|
+
* @returns The props, each given two-way prop's change event writing back.
|
|
308
|
+
* @example
|
|
309
|
+
* ```ts
|
|
310
|
+
* withWriteBacks(props, [{ prop: 'value', changeProp: 'onValueChange' }], (prop, value) =>
|
|
311
|
+
* setProps((current) => ({ ...current, [prop]: value }))
|
|
312
|
+
* );
|
|
313
|
+
* ```
|
|
314
|
+
*/
|
|
315
|
+
export function withWriteBacks(
|
|
316
|
+
props: Record<string, unknown>,
|
|
317
|
+
bindings: readonly PreviewWriteBack[] = [],
|
|
318
|
+
write: (prop: string, value: unknown) => void
|
|
319
|
+
): Record<string, unknown> {
|
|
320
|
+
// The given two-way props by the change prop they report through: one
|
|
321
|
+
// change prop may report several, each written back.
|
|
322
|
+
const byChangeProp = new Map<string, string[]>();
|
|
323
|
+
for (const { prop, changeProp } of bindings) {
|
|
324
|
+
if (props[prop] === undefined || changeProp === prop) continue;
|
|
325
|
+
byChangeProp.set(changeProp, [
|
|
326
|
+
...(byChangeProp.get(changeProp) ?? []),
|
|
327
|
+
prop,
|
|
328
|
+
]);
|
|
329
|
+
}
|
|
330
|
+
if (byChangeProp.size === 0) return props;
|
|
331
|
+
const next = { ...props };
|
|
332
|
+
for (const [changeProp, written] of byChangeProp) {
|
|
333
|
+
const own = props[changeProp];
|
|
334
|
+
next[changeProp] = (...parameters: unknown[]) => {
|
|
335
|
+
const copy = plainValue(parameters[0]);
|
|
336
|
+
if ('value' in copy) for (const prop of written) write(prop, copy.value);
|
|
337
|
+
return typeof own === 'function' ? own(...parameters) : undefined;
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
return next;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* The values a pane has written back itself (see {@link withWriteBacks}) and
|
|
345
|
+
* the host has not yet answered, per two-way prop, oldest first. Each write
|
|
346
|
+
* gets a number, counting up for the life of the pane, which the next event
|
|
347
|
+
* the pane reports carries (`write`); each props message from the host
|
|
348
|
+
* names the newest write it had taken when it sent them (`answers`). Props
|
|
349
|
+
* the host sent before it took a write hold an older value for that prop,
|
|
350
|
+
* whether they answer the write's own echo or a change of another control,
|
|
351
|
+
* so `reconcile` keeps the pane's newest value over them.
|
|
352
|
+
*
|
|
353
|
+
* @example
|
|
354
|
+
* ```ts
|
|
355
|
+
* const pending = new PendingWriteBacks();
|
|
356
|
+
* pending.wrote('value', 'ab'); // 1
|
|
357
|
+
* pending.wrote('value', 'abc'); // 2
|
|
358
|
+
* pending.reconcile({ value: 'ab' }, 1); // { value: 'abc' }, still waiting for 2
|
|
359
|
+
* pending.reconcile({ value: 'abc' }, 2); // { value: 'abc' }, nothing waiting
|
|
360
|
+
* ```
|
|
361
|
+
*/
|
|
362
|
+
export class PendingWriteBacks {
|
|
363
|
+
private readonly pending = new Map<
|
|
364
|
+
string,
|
|
365
|
+
{ write: number; value: unknown }[]
|
|
366
|
+
>();
|
|
367
|
+
private count = 0;
|
|
368
|
+
private unreported: number | undefined;
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Note a value the pane has written back for a prop.
|
|
372
|
+
*
|
|
373
|
+
* @param prop - The two-way prop.
|
|
374
|
+
* @param value - The value, plain data.
|
|
375
|
+
* @returns The write's number.
|
|
376
|
+
*/
|
|
377
|
+
wrote(prop: string, value: unknown): number {
|
|
378
|
+
this.count += 1;
|
|
379
|
+
const values = this.pending.get(prop) ?? [];
|
|
380
|
+
values.push({ write: this.count, value });
|
|
381
|
+
this.pending.set(prop, values);
|
|
382
|
+
this.unreported = this.count;
|
|
383
|
+
return this.count;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* The number of the newest write since the last call, for the event the
|
|
388
|
+
* pane reports next to carry; `undefined` when there has been none.
|
|
389
|
+
*
|
|
390
|
+
* @returns The write's number, or `undefined`.
|
|
391
|
+
*/
|
|
392
|
+
takeUnreported(): number | undefined {
|
|
393
|
+
const write = this.unreported;
|
|
394
|
+
this.unreported = undefined;
|
|
395
|
+
return write;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* The host's props as the pane takes them. The writes up to `answers` are
|
|
400
|
+
* done with: the host had them when it sent the props. A two-way prop the
|
|
401
|
+
* host gives with a later write still waiting keeps the newest value the
|
|
402
|
+
* pane wrote. Without `answers` the props are taken as they are, and
|
|
403
|
+
* every write is done with.
|
|
404
|
+
*
|
|
405
|
+
* @param props - The props the host sends.
|
|
406
|
+
* @param answers - The newest of the pane's writes the host had taken.
|
|
407
|
+
* @returns The props to take.
|
|
408
|
+
*/
|
|
409
|
+
reconcile(
|
|
410
|
+
props: Record<string, unknown>,
|
|
411
|
+
answers?: number
|
|
412
|
+
): Record<string, unknown> {
|
|
413
|
+
if (answers === undefined) {
|
|
414
|
+
this.pending.clear();
|
|
415
|
+
return props;
|
|
416
|
+
}
|
|
417
|
+
let taken = props;
|
|
418
|
+
for (const [prop, values] of this.pending) {
|
|
419
|
+
const waiting = values.filter((written) => written.write > answers);
|
|
420
|
+
if (waiting.length === 0) {
|
|
421
|
+
this.pending.delete(prop);
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
this.pending.set(prop, waiting);
|
|
425
|
+
// A prop the host leaves out is no longer given: the pane writes it
|
|
426
|
+
// back no more, and the component keeps its own value.
|
|
427
|
+
if (props[prop] === undefined) continue;
|
|
428
|
+
taken = { ...taken, [prop]: waiting[waiting.length - 1].value };
|
|
429
|
+
}
|
|
430
|
+
return taken;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** Forget every write, as a new story starts. The count goes on. */
|
|
434
|
+
clear(): void {
|
|
435
|
+
this.pending.clear();
|
|
436
|
+
this.unreported = undefined;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* The story's props with a recorder for each of its events: a function that
|
|
442
|
+
* reports its call to the host as a `preview:event`, the event's name and a
|
|
443
|
+
* readable summary of what it was called with. A function the props hold
|
|
444
|
+
* already for an event is called too.
|
|
445
|
+
*
|
|
446
|
+
* @param props - The story's root props.
|
|
447
|
+
* @param events - The function props to record the calls of.
|
|
448
|
+
* @param target - The pane's window.
|
|
449
|
+
* @param mount - The `mount` of the set-story message the pane shows, which
|
|
450
|
+
* each report carries.
|
|
451
|
+
* @param writes - The pane's own write-backs: a report carries the number of
|
|
452
|
+
* the newest write since the last one (`write`), for the host to answer.
|
|
453
|
+
* @returns The props, an event's recorder in its place.
|
|
454
|
+
*/
|
|
455
|
+
export function withEventRecorders(
|
|
456
|
+
props: Record<string, unknown>,
|
|
457
|
+
events: readonly string[] = [],
|
|
458
|
+
target: Window = window,
|
|
459
|
+
mount?: number,
|
|
460
|
+
writes?: PendingWriteBacks
|
|
461
|
+
): Record<string, unknown> {
|
|
462
|
+
if (events.length === 0) return props;
|
|
463
|
+
const recorded = { ...props };
|
|
464
|
+
for (const name of events) {
|
|
465
|
+
const own = props[name];
|
|
466
|
+
recorded[name] = (...parameters: unknown[]) => {
|
|
467
|
+
const write = writes?.takeUnreported();
|
|
468
|
+
const report: PreviewEventMessage = {
|
|
469
|
+
type: PREVIEW_MESSAGE.event,
|
|
470
|
+
name,
|
|
471
|
+
payload: summarizeArguments(parameters),
|
|
472
|
+
...plainValue(parameters[0]),
|
|
473
|
+
...(mount !== undefined ? { mount } : {}),
|
|
474
|
+
...(write !== undefined ? { write } : {}),
|
|
475
|
+
};
|
|
476
|
+
target.parent?.postMessage(report, '*');
|
|
477
|
+
return typeof own === 'function' ? own(...parameters) : undefined;
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
return recorded;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** An event's first argument as `{ value }`, a copy, when it is plain data:
|
|
484
|
+
* `null`, a boolean, a finite number, a string, or an array or plain object
|
|
485
|
+
* of these. Anything else, such as a browser event or an element, gives
|
|
486
|
+
* nothing. */
|
|
487
|
+
function plainValue(value: unknown): { value?: unknown } {
|
|
488
|
+
return isPlainData(value, 0)
|
|
489
|
+
? { value: JSON.parse(JSON.stringify(value)) }
|
|
490
|
+
: {};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Whether a value is plain data, which survives `postMessage` and JSON as it
|
|
495
|
+
* is: `null`, a boolean, a finite number, a string, or an array or plain
|
|
496
|
+
* object of these.
|
|
497
|
+
*
|
|
498
|
+
* @param value - The value.
|
|
499
|
+
* @param depth - How deep the value sits in the one being read.
|
|
500
|
+
* @returns `true` when the value is plain data.
|
|
501
|
+
*/
|
|
502
|
+
export function isPlainData(value: unknown, depth = 0): boolean {
|
|
503
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean')
|
|
504
|
+
return true;
|
|
505
|
+
if (typeof value === 'number') return Number.isFinite(value);
|
|
506
|
+
if (typeof value !== 'object' || depth > 32) return false;
|
|
507
|
+
if (Array.isArray(value))
|
|
508
|
+
return value.every((entry) => isPlainData(entry, depth + 1));
|
|
509
|
+
const prototype = Object.getPrototypeOf(value);
|
|
510
|
+
if (prototype !== Object.prototype && prototype !== null) return false;
|
|
511
|
+
return Object.values(value).every((entry) => isPlainData(entry, depth + 1));
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/** A readable, one-line summary of what an event was called with. */
|
|
515
|
+
export function summarizeArguments(parameters: unknown[]): string {
|
|
516
|
+
return parameters.map(summarizeValue).join(', ');
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function summarizeValue(value: unknown): string {
|
|
520
|
+
if (value === undefined) return 'undefined';
|
|
521
|
+
if (typeof value === 'function') return 'function';
|
|
522
|
+
if (typeof Event !== 'undefined' && value instanceof Event)
|
|
523
|
+
return `${value.constructor.name} ${value.type}`;
|
|
524
|
+
// React hands a handler its own wrapper of the browser's event, which
|
|
525
|
+
// holds the event itself: summarized alike, it reads as Vue's and
|
|
526
|
+
// Svelte's do.
|
|
527
|
+
if (
|
|
528
|
+
typeof Event !== 'undefined' &&
|
|
529
|
+
typeof value === 'object' &&
|
|
530
|
+
value !== null &&
|
|
531
|
+
(value as { nativeEvent?: unknown }).nativeEvent instanceof Event
|
|
532
|
+
)
|
|
533
|
+
return summarizeValue((value as { nativeEvent: Event }).nativeEvent);
|
|
534
|
+
if (typeof Element !== 'undefined' && value instanceof Element)
|
|
535
|
+
return `<${value.tagName.toLowerCase()}>`;
|
|
536
|
+
try {
|
|
537
|
+
const text = JSON.stringify(value);
|
|
538
|
+
return text.length > 200 ? `${text.slice(0, 197)}...` : text;
|
|
539
|
+
} catch {
|
|
540
|
+
// An object JSON cannot hold, such as one that refers to itself, by
|
|
541
|
+
// the name of what made it.
|
|
542
|
+
return typeof value === 'object' && value !== null
|
|
543
|
+
? (value.constructor?.name ?? 'Object')
|
|
544
|
+
: String(value);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* @param props - The mounted story's new root props.
|
|
550
|
+
* @param answers - The newest of the receiving pane's write-backs the host
|
|
551
|
+
* has taken.
|
|
552
|
+
* @returns A {@link PreviewPropsMessage}.
|
|
553
|
+
*/
|
|
554
|
+
export function propsMessage(
|
|
555
|
+
props: Record<string, unknown>,
|
|
556
|
+
answers?: number
|
|
557
|
+
): PreviewPropsMessage {
|
|
558
|
+
return {
|
|
559
|
+
type: PREVIEW_MESSAGE.props,
|
|
560
|
+
props,
|
|
561
|
+
...(answers !== undefined ? { answers } : {}),
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* @param options - The option values, keyed by option id.
|
|
567
|
+
* @returns A {@link PreviewGlobalsMessage}.
|
|
568
|
+
*/
|
|
569
|
+
export function globalsMessage(
|
|
570
|
+
options: Record<string, OptionValue>
|
|
571
|
+
): PreviewGlobalsMessage {
|
|
572
|
+
return { type: PREVIEW_MESSAGE.globals, options };
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* @param catalog - The kit's name, sources and browseable stories.
|
|
577
|
+
* @returns A {@link PreviewCatalogMessage}.
|
|
578
|
+
*/
|
|
579
|
+
export function catalogMessage(
|
|
580
|
+
catalog: PreviewCatalogPayload
|
|
581
|
+
): PreviewCatalogMessage {
|
|
582
|
+
return { type: PREVIEW_MESSAGE.catalog, ...catalog };
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* @param id - The component story to open.
|
|
587
|
+
* @param props - Root props to open it with, over the component's defaults.
|
|
588
|
+
* @returns A {@link PreviewSelectMessage}.
|
|
589
|
+
*/
|
|
590
|
+
export function selectMessage(
|
|
591
|
+
id: string,
|
|
592
|
+
props?: Record<string, unknown>
|
|
593
|
+
): PreviewSelectMessage {
|
|
594
|
+
return { type: PREVIEW_MESSAGE.select, id, props };
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* @param content - The target's source, its language, or why it has none.
|
|
599
|
+
* @returns A {@link PreviewCodeMessage}.
|
|
600
|
+
*/
|
|
601
|
+
export function codeMessage(content: CodeBlockContent): PreviewCodeMessage {
|
|
602
|
+
return { type: PREVIEW_MESSAGE.code, ...content };
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Tell the host what the iframe's story threw as it rendered, or, with no
|
|
607
|
+
* message, that the story mounted since renders.
|
|
608
|
+
*
|
|
609
|
+
* @param message - The error's message; omitted when a new story mounts.
|
|
610
|
+
* @param target - The iframe's window.
|
|
611
|
+
*/
|
|
612
|
+
export function reportRenderError(
|
|
613
|
+
message?: string,
|
|
614
|
+
target: Window = window
|
|
615
|
+
): void {
|
|
616
|
+
const report: PreviewRenderErrorMessage = {
|
|
617
|
+
type: PREVIEW_MESSAGE.renderError,
|
|
618
|
+
};
|
|
619
|
+
if (message !== undefined) report.message = message;
|
|
620
|
+
(target as unknown as Record<string, unknown>)[PREVIEW_RENDER_ERROR_FLAG] =
|
|
621
|
+
message ?? null;
|
|
622
|
+
target.parent?.postMessage(report, '*');
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* @param size - The content's width and height in CSS pixels.
|
|
627
|
+
* @returns A {@link PreviewContentSizeMessage}.
|
|
628
|
+
*/
|
|
629
|
+
export function contentSizeMessage(
|
|
630
|
+
size: PreviewContentSize
|
|
631
|
+
): PreviewContentSizeMessage {
|
|
632
|
+
return {
|
|
633
|
+
type: PREVIEW_MESSAGE.contentSize,
|
|
634
|
+
width: size.width,
|
|
635
|
+
height: size.height,
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* @param story - The markup to show, the reason there is none, or neither
|
|
641
|
+
* while it is being printed.
|
|
642
|
+
* @returns A {@link PreviewMarkupMessage}.
|
|
643
|
+
*/
|
|
644
|
+
export function markupMessage(
|
|
645
|
+
story:
|
|
646
|
+
| { markup: string; stylesheet?: string }
|
|
647
|
+
| { reason: string }
|
|
648
|
+
| Record<string, never>
|
|
649
|
+
): PreviewMarkupMessage {
|
|
650
|
+
return { type: PREVIEW_MESSAGE.markup, ...story };
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Read posted data as a content size: its width and height when both are
|
|
655
|
+
* finite numbers of zero or more, otherwise `undefined`.
|
|
656
|
+
*
|
|
657
|
+
* @param message - A {@link PreviewContentSizeMessage} as it was received.
|
|
658
|
+
* @returns The size, or `undefined` for a malformed message.
|
|
659
|
+
*/
|
|
660
|
+
export function readContentSize(
|
|
661
|
+
message: PreviewContentSizeMessage
|
|
662
|
+
): PreviewContentSize | undefined {
|
|
663
|
+
const { width, height } = message as { width?: unknown; height?: unknown };
|
|
664
|
+
const valid = (value: unknown): value is number =>
|
|
665
|
+
typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
666
|
+
return valid(width) && valid(height) ? { width, height } : undefined;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/** The window property an iframe sets to `true` once it can receive messages,
|
|
670
|
+
* for a test that loads the iframe's page on its own (where the ready message
|
|
671
|
+
* has no parent to reach). */
|
|
672
|
+
export const PREVIEW_READY_FLAG = '__previewReady';
|
|
673
|
+
|
|
674
|
+
/** The window property a story iframe sets to the ids of the stories it can
|
|
675
|
+
* mount, for a test helper to reject an id that would mount nothing. */
|
|
676
|
+
export const PREVIEW_STORIES_FLAG = '__previewStories';
|
|
677
|
+
|
|
678
|
+
/** The window property a preview iframe sets to the kit's options, each with
|
|
679
|
+
* the values a choice option offers, for a test helper to reject an id or a
|
|
680
|
+
* value the iframe would ignore. */
|
|
681
|
+
export const PREVIEW_OPTIONS_FLAG = '__previewOptions';
|
|
682
|
+
|
|
683
|
+
/** The window property a story iframe sets to the message of what its story
|
|
684
|
+
* threw as it rendered, or to `null` while the story mounted since renders,
|
|
685
|
+
* for a test that loads the iframe's page on its own (where the report has no
|
|
686
|
+
* parent to reach). The message is a string, and may be empty. */
|
|
687
|
+
export const PREVIEW_RENDER_ERROR_FLAG = '__previewRenderError';
|
|
688
|
+
|
|
689
|
+
/** The window property a story iframe sets to the id of the story it last
|
|
690
|
+
* finished mounting, or to the empty string once a story has unmounted with
|
|
691
|
+
* none set after it. Set after the story's first render and its effects, so
|
|
692
|
+
* a test that loads the iframe's page on its own can tell a story that
|
|
693
|
+
* rendered nothing from one still mounting. */
|
|
694
|
+
export const PREVIEW_MOUNTED_STORY_FLAG = '__previewMountedStory';
|
|
695
|
+
|
|
696
|
+
/** The id of the element each preview iframe mounts its story or view in. */
|
|
697
|
+
export const PREVIEW_MOUNT_ELEMENT_ID = 'app';
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Announce that an iframe shows its first content: post a
|
|
701
|
+
* {@link PreviewShownMessage} to the parent window once the content mounted
|
|
702
|
+
* now has painted, two animation frames on. Call it after the iframe mounts
|
|
703
|
+
* the first content the host sends; a later call in the same document sends
|
|
704
|
+
* nothing.
|
|
705
|
+
*
|
|
706
|
+
* @param target - The iframe's window.
|
|
707
|
+
*/
|
|
708
|
+
export function announcePreviewShown(target: Window = window): void {
|
|
709
|
+
const flags = target as unknown as Record<string, unknown>;
|
|
710
|
+
if (flags[PREVIEW_SHOWN_FLAG]) return;
|
|
711
|
+
flags[PREVIEW_SHOWN_FLAG] = true;
|
|
712
|
+
target.requestAnimationFrame(() =>
|
|
713
|
+
target.requestAnimationFrame(() => {
|
|
714
|
+
if (target.parent !== target)
|
|
715
|
+
target.parent.postMessage({ type: PREVIEW_MESSAGE.shown }, '*');
|
|
716
|
+
})
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Record under {@link PREVIEW_MOUNTED_STORY_FLAG} that the iframe has finished
|
|
722
|
+
* mounting a story. Call it once the framework has committed the mount,
|
|
723
|
+
* whatever the story rendered.
|
|
724
|
+
*
|
|
725
|
+
* @param id - The story's id, or the empty string after an unmount.
|
|
726
|
+
* @param target - The iframe's window.
|
|
727
|
+
*/
|
|
728
|
+
export function announceStoryMounted(
|
|
729
|
+
id: string,
|
|
730
|
+
target: Window = window
|
|
731
|
+
): void {
|
|
732
|
+
(target as unknown as Record<string, unknown>)[PREVIEW_MOUNTED_STORY_FLAG] =
|
|
733
|
+
id;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/** Set on an iframe's window once it has announced its first content. */
|
|
737
|
+
const PREVIEW_SHOWN_FLAG = '__previewShown';
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* List the stories an iframe can mount under {@link PREVIEW_STORIES_FLAG}.
|
|
741
|
+
* Call it before {@link announcePreviewReady}, so a test that waits for the
|
|
742
|
+
* iframe finds the list.
|
|
743
|
+
*
|
|
744
|
+
* @param ids - The ids of the stories.
|
|
745
|
+
* @param target - The iframe's window.
|
|
746
|
+
*/
|
|
747
|
+
export function announcePreviewStories(
|
|
748
|
+
ids: readonly string[],
|
|
749
|
+
target: Window = window
|
|
750
|
+
): void {
|
|
751
|
+
(target as unknown as Record<string, unknown>)[PREVIEW_STORIES_FLAG] = [
|
|
752
|
+
...ids,
|
|
753
|
+
];
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/** An option as a preview iframe lists it: its id and, for a choice, the
|
|
757
|
+
* values it offers; a boolean option has no `values`. */
|
|
758
|
+
export interface PreviewListedOption {
|
|
759
|
+
/** The option's id. */
|
|
760
|
+
id: string;
|
|
761
|
+
/** The values of a choice option. */
|
|
762
|
+
values?: string[];
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* List the kit's options under {@link PREVIEW_OPTIONS_FLAG}.
|
|
767
|
+
*
|
|
768
|
+
* @param options - The options.
|
|
769
|
+
* @param target - The iframe's window.
|
|
770
|
+
*/
|
|
771
|
+
export function announcePreviewOptions(
|
|
772
|
+
options: readonly Pick<PreviewOption, 'id' | 'values'>[],
|
|
773
|
+
target: Window = window
|
|
774
|
+
): void {
|
|
775
|
+
(target as unknown as Record<string, unknown>)[PREVIEW_OPTIONS_FLAG] =
|
|
776
|
+
options.map(
|
|
777
|
+
({ id, values }): PreviewListedOption =>
|
|
778
|
+
values ? { id, values: values.map((choice) => choice.value) } : { id }
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Announce that an iframe listens for messages: set {@link PREVIEW_READY_FLAG}
|
|
784
|
+
* and post a {@link PreviewReadyMessage} to the parent window. Call it right
|
|
785
|
+
* after attaching the message listener.
|
|
786
|
+
*
|
|
787
|
+
* @param target - The iframe's window.
|
|
788
|
+
*/
|
|
789
|
+
export function announcePreviewReady(target: Window = window): void {
|
|
790
|
+
(target as unknown as Record<string, unknown>)[PREVIEW_READY_FLAG] = true;
|
|
791
|
+
// A pane opened on its own, with no host, gives itself the story its
|
|
792
|
+
// address names, as a host would.
|
|
793
|
+
const standalone =
|
|
794
|
+
target.parent === target ? standaloneMessages(target.location.search) : [];
|
|
795
|
+
if (standalone.length > 0) {
|
|
796
|
+
for (const message of standalone) target.postMessage(message, '*');
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
target.parent?.postMessage(readyMessage(), '*');
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/** The pixel value of a computed length such as `12px`; `0` for anything else. */
|
|
803
|
+
function pixels(value: string): number {
|
|
804
|
+
const number = parseFloat(value);
|
|
805
|
+
return Number.isFinite(number) ? number : 0;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/** Whether the mount's height may follow the layout around it rather than its
|
|
809
|
+
* content: it holds a minimum height of its own, or it is a flex or grid
|
|
810
|
+
* item, which the container may stretch. */
|
|
811
|
+
function heightFollowsLayout(mount: HTMLElement, view: Window): boolean {
|
|
812
|
+
if (pixels(view.getComputedStyle(mount).minHeight) > 0) return true;
|
|
813
|
+
const parent = mount.parentElement;
|
|
814
|
+
if (!parent) return false;
|
|
815
|
+
const display = view.getComputedStyle(parent).display;
|
|
816
|
+
return /flex|grid/.test(display);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/** The height the mount's children span, each with its vertical margins:
|
|
820
|
+
* fixed-position children overlay the page and are left out. */
|
|
821
|
+
function childrenHeight(mount: HTMLElement, view: Window): number {
|
|
822
|
+
let top = Infinity;
|
|
823
|
+
let bottom = -Infinity;
|
|
824
|
+
const range = mount.ownerDocument.createRange();
|
|
825
|
+
for (const node of mount.childNodes) {
|
|
826
|
+
if (node.nodeType === 1) {
|
|
827
|
+
const element = node as Element;
|
|
828
|
+
const style = view.getComputedStyle(element);
|
|
829
|
+
if (style.display === 'none' || style.position === 'fixed') continue;
|
|
830
|
+
const rectangle = element.getBoundingClientRect();
|
|
831
|
+
top = Math.min(top, rectangle.top - pixels(style.marginTop));
|
|
832
|
+
bottom = Math.max(bottom, rectangle.bottom + pixels(style.marginBottom));
|
|
833
|
+
} else if (node.nodeType === 3 && node.textContent?.trim()) {
|
|
834
|
+
range.selectNodeContents(node);
|
|
835
|
+
const rectangle = range.getBoundingClientRect();
|
|
836
|
+
top = Math.min(top, rectangle.top);
|
|
837
|
+
bottom = Math.max(bottom, rectangle.bottom);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
return bottom > top ? bottom - top : 0;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Measure what an iframe shows: the mount element's own box, with the
|
|
845
|
+
* padding, borders and margins of the mount and every element around it up
|
|
846
|
+
* to the body, rounded up to whole CSS pixels. Nothing in it depends on the
|
|
847
|
+
* height of the iframe, so a host that sizes the iframe to it does not make
|
|
848
|
+
* it grow: a wrapper's `min-height: 100vh` adds nothing but its padding.
|
|
849
|
+
* Where the mount's own height may follow the layout around it (a minimum
|
|
850
|
+
* height of its own, or a flex or grid item), its children's extent stands
|
|
851
|
+
* in for it.
|
|
852
|
+
*
|
|
853
|
+
* @param mount - The element the story or view is mounted in.
|
|
854
|
+
* @returns The content's width and height.
|
|
855
|
+
*/
|
|
856
|
+
export function measureContentSize(mount: HTMLElement): PreviewContentSize {
|
|
857
|
+
const view = mount.ownerDocument.defaultView;
|
|
858
|
+
if (!view) return { width: 0, height: 0 };
|
|
859
|
+
const box = mount.getBoundingClientRect();
|
|
860
|
+
const mountStyle = view.getComputedStyle(mount);
|
|
861
|
+
let width = box.width;
|
|
862
|
+
let height = heightFollowsLayout(mount, view)
|
|
863
|
+
? childrenHeight(mount, view) +
|
|
864
|
+
pixels(mountStyle.paddingTop) +
|
|
865
|
+
pixels(mountStyle.paddingBottom) +
|
|
866
|
+
pixels(mountStyle.borderTopWidth) +
|
|
867
|
+
pixels(mountStyle.borderBottomWidth)
|
|
868
|
+
: box.height;
|
|
869
|
+
const root = mount.ownerDocument.documentElement;
|
|
870
|
+
for (
|
|
871
|
+
let element: HTMLElement | null = mount;
|
|
872
|
+
element && element !== root;
|
|
873
|
+
element = element.parentElement
|
|
874
|
+
) {
|
|
875
|
+
const style = view.getComputedStyle(element);
|
|
876
|
+
height += pixels(style.marginTop) + pixels(style.marginBottom);
|
|
877
|
+
width += pixels(style.marginLeft) + pixels(style.marginRight);
|
|
878
|
+
if (element === mount) continue;
|
|
879
|
+
height +=
|
|
880
|
+
pixels(style.paddingTop) +
|
|
881
|
+
pixels(style.paddingBottom) +
|
|
882
|
+
pixels(style.borderTopWidth) +
|
|
883
|
+
pixels(style.borderBottomWidth);
|
|
884
|
+
width +=
|
|
885
|
+
pixels(style.paddingLeft) +
|
|
886
|
+
pixels(style.paddingRight) +
|
|
887
|
+
pixels(style.borderLeftWidth) +
|
|
888
|
+
pixels(style.borderRightWidth);
|
|
889
|
+
}
|
|
890
|
+
return { width: Math.ceil(width), height: Math.ceil(height) };
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
const reporting = new WeakMap<HTMLElement, () => void>();
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Report the size of what an iframe shows to the parent window as a
|
|
897
|
+
* {@link PreviewContentSizeMessage}: once now, and again whenever it
|
|
898
|
+
* changes, at most once per animation frame and only when the size differs
|
|
899
|
+
* from the last one reported. It watches the mount, its children and the
|
|
900
|
+
* elements around it for a change of size, the mount's content for a change
|
|
901
|
+
* of nodes or text, the elements around it for a change of class or style,
|
|
902
|
+
* and the window for a resize. Call it once the message listener is
|
|
903
|
+
* attached; a second call for the same mount returns the first one's stop.
|
|
904
|
+
*
|
|
905
|
+
* @param mount - The element the story or view is mounted in.
|
|
906
|
+
* @param target - The iframe's window.
|
|
907
|
+
* @returns A function that stops the reports.
|
|
908
|
+
*/
|
|
909
|
+
export function reportContentSize(
|
|
910
|
+
mount: HTMLElement,
|
|
911
|
+
target: Window = window
|
|
912
|
+
): () => void {
|
|
913
|
+
const running = reporting.get(mount);
|
|
914
|
+
if (running) return running;
|
|
915
|
+
const root = mount.ownerDocument.documentElement;
|
|
916
|
+
let pending: number | undefined;
|
|
917
|
+
let last = '';
|
|
918
|
+
|
|
919
|
+
function report(): void {
|
|
920
|
+
pending = undefined;
|
|
921
|
+
const size = measureContentSize(mount);
|
|
922
|
+
const key = `${size.width}x${size.height}`;
|
|
923
|
+
if (key === last) return;
|
|
924
|
+
last = key;
|
|
925
|
+
target.parent?.postMessage(contentSizeMessage(size), '*');
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function schedule(): void {
|
|
929
|
+
if (pending === undefined) pending = target.requestAnimationFrame(report);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const observed = new Set<Element>();
|
|
933
|
+
const resizeObserver =
|
|
934
|
+
typeof ResizeObserver === 'function'
|
|
935
|
+
? new ResizeObserver(schedule)
|
|
936
|
+
: undefined;
|
|
937
|
+
const surroundings = new MutationObserver(schedule);
|
|
938
|
+
|
|
939
|
+
function observeElements(): void {
|
|
940
|
+
const elements: Element[] = [...mount.children];
|
|
941
|
+
for (
|
|
942
|
+
let element: HTMLElement | null = mount;
|
|
943
|
+
element && element !== root;
|
|
944
|
+
element = element.parentElement
|
|
945
|
+
) {
|
|
946
|
+
elements.push(element);
|
|
947
|
+
}
|
|
948
|
+
for (const element of elements) {
|
|
949
|
+
if (observed.has(element)) continue;
|
|
950
|
+
observed.add(element);
|
|
951
|
+
resizeObserver?.observe(element, { box: 'border-box' });
|
|
952
|
+
}
|
|
953
|
+
for (const element of [...observed]) {
|
|
954
|
+
if (elements.includes(element)) continue;
|
|
955
|
+
observed.delete(element);
|
|
956
|
+
resizeObserver?.unobserve(element);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
const content = new MutationObserver(() => {
|
|
961
|
+
observeElements();
|
|
962
|
+
schedule();
|
|
963
|
+
});
|
|
964
|
+
content.observe(mount, {
|
|
965
|
+
childList: true,
|
|
966
|
+
subtree: true,
|
|
967
|
+
characterData: true,
|
|
968
|
+
});
|
|
969
|
+
for (
|
|
970
|
+
let element: HTMLElement | null = mount;
|
|
971
|
+
element;
|
|
972
|
+
element = element.parentElement
|
|
973
|
+
) {
|
|
974
|
+
surroundings.observe(element, {
|
|
975
|
+
attributes: true,
|
|
976
|
+
attributeFilter: ['class', 'style'],
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
observeElements();
|
|
980
|
+
target.addEventListener('resize', schedule);
|
|
981
|
+
schedule();
|
|
982
|
+
|
|
983
|
+
const stop = (): void => {
|
|
984
|
+
if (pending !== undefined) target.cancelAnimationFrame(pending);
|
|
985
|
+
pending = undefined;
|
|
986
|
+
resizeObserver?.disconnect();
|
|
987
|
+
content.disconnect();
|
|
988
|
+
surroundings.disconnect();
|
|
989
|
+
target.removeEventListener('resize', schedule);
|
|
990
|
+
reporting.delete(mount);
|
|
991
|
+
};
|
|
992
|
+
reporting.set(mount, stop);
|
|
993
|
+
return stop;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
/** The query parameter that names the view the view iframe renders. */
|
|
997
|
+
export const VIEW_PARAMETER = 'view';
|
|
998
|
+
|
|
999
|
+
/** The query parameter that names the framework of the view the view iframe
|
|
1000
|
+
* renders, so the iframe loads that framework's setup module before the view. */
|
|
1001
|
+
export const VIEW_FRAMEWORK_PARAMETER = 'framework';
|
|
1002
|
+
|
|
1003
|
+
/** The query parameter that names the target whose source a code iframe
|
|
1004
|
+
* shows. */
|
|
1005
|
+
export const CODE_TARGET_PARAMETER = 'target';
|
|
1006
|
+
|
|
1007
|
+
/** The query parameter a component iframe's URL carries so that changing it
|
|
1008
|
+
* reloads the iframe. */
|
|
1009
|
+
export const RELOAD_PARAMETER = 'r';
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* The URL of a story pane's iframe: a target's, or the html one's.
|
|
1013
|
+
*
|
|
1014
|
+
* @param target - What the iframe renders in.
|
|
1015
|
+
* @param reload - A counter whose change reloads the iframe; omitted, the URL
|
|
1016
|
+
* carries no reload parameter.
|
|
1017
|
+
* @param baseUrl - The URL the preview is served under, ending in `/`; the
|
|
1018
|
+
* root of the origin, `/`, by default.
|
|
1019
|
+
* @returns The page's URL, such as `/preview/vue/preview.html?r=0`.
|
|
1020
|
+
*/
|
|
1021
|
+
export function componentFrameUrl(
|
|
1022
|
+
target: PreviewTarget,
|
|
1023
|
+
reload?: number,
|
|
1024
|
+
baseUrl = '/'
|
|
1025
|
+
): string {
|
|
1026
|
+
const path = `${baseUrl}preview/${target}/preview.html`;
|
|
1027
|
+
return reload === undefined ? path : `${path}?${RELOAD_PARAMETER}=${reload}`;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/** The query parameters a target's pane, opened on its own, reads its
|
|
1031
|
+
* story from: the story's id, its root props and the option values, the
|
|
1032
|
+
* latter two as JSON. */
|
|
1033
|
+
export const STANDALONE_PARAMETERS = {
|
|
1034
|
+
story: 'story',
|
|
1035
|
+
props: 'props',
|
|
1036
|
+
options: 'options',
|
|
1037
|
+
} as const;
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* The URL of a target's pane opened on its own, outside the preview: it
|
|
1041
|
+
* shows `story` with its props under its option values, as the preview's pane
|
|
1042
|
+
* does, with no host to follow.
|
|
1043
|
+
*
|
|
1044
|
+
* @param target - The target to render the story in.
|
|
1045
|
+
* @param story - The story's id, root props and option values.
|
|
1046
|
+
* @param baseUrl - The URL the preview is served under, ending in `/`.
|
|
1047
|
+
* @returns The pane's URL.
|
|
1048
|
+
*/
|
|
1049
|
+
export function standaloneFrameUrl(
|
|
1050
|
+
target: PreviewScriptedTarget,
|
|
1051
|
+
story: {
|
|
1052
|
+
id: string;
|
|
1053
|
+
props: Record<string, unknown>;
|
|
1054
|
+
options: Record<string, OptionValue>;
|
|
1055
|
+
},
|
|
1056
|
+
baseUrl = '/'
|
|
1057
|
+
): string {
|
|
1058
|
+
const query = new URLSearchParams({
|
|
1059
|
+
[STANDALONE_PARAMETERS.story]: story.id,
|
|
1060
|
+
[STANDALONE_PARAMETERS.props]: JSON.stringify(story.props),
|
|
1061
|
+
[STANDALONE_PARAMETERS.options]: JSON.stringify(story.options),
|
|
1062
|
+
});
|
|
1063
|
+
return `${componentFrameUrl(target, undefined, baseUrl)}?${query}`;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/**
|
|
1067
|
+
* The messages a pane opened on its own gives itself, from its address: the
|
|
1068
|
+
* story to mount and the option values. None when the address names no story.
|
|
1069
|
+
*
|
|
1070
|
+
* @param search - The pane's `location.search`.
|
|
1071
|
+
* @returns A set-story and a globals message, or none.
|
|
1072
|
+
*/
|
|
1073
|
+
export function standaloneMessages(search: string): PreviewMessage[] {
|
|
1074
|
+
const query = new URLSearchParams(search);
|
|
1075
|
+
const id = query.get(STANDALONE_PARAMETERS.story);
|
|
1076
|
+
if (!id) return [];
|
|
1077
|
+
const parsed = (name: string): Record<string, unknown> => {
|
|
1078
|
+
try {
|
|
1079
|
+
const value: unknown = JSON.parse(query.get(name) ?? '{}');
|
|
1080
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
1081
|
+
? (value as Record<string, unknown>)
|
|
1082
|
+
: {};
|
|
1083
|
+
} catch {
|
|
1084
|
+
return {};
|
|
1085
|
+
}
|
|
1086
|
+
};
|
|
1087
|
+
return [
|
|
1088
|
+
setStoryMessage(id, parsed(STANDALONE_PARAMETERS.props)),
|
|
1089
|
+
globalsMessage(
|
|
1090
|
+
parsed(STANDALONE_PARAMETERS.options) as Record<string, OptionValue>
|
|
1091
|
+
),
|
|
1092
|
+
];
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
/**
|
|
1096
|
+
* The URL of a code iframe: the kit's code block, showing one target's
|
|
1097
|
+
* source.
|
|
1098
|
+
*
|
|
1099
|
+
* @param target - The target whose source the block shows.
|
|
1100
|
+
* @param framework - The block's framework; a framework other than `'dom'`
|
|
1101
|
+
* is named in the URL, so the iframe loads its setup module first.
|
|
1102
|
+
* @param baseUrl - The URL the preview is served under, ending in `/`.
|
|
1103
|
+
* @returns The page's URL, such as `/preview/code/code.html?target=vue`.
|
|
1104
|
+
*/
|
|
1105
|
+
export function codeFrameUrl(
|
|
1106
|
+
target: PreviewTarget,
|
|
1107
|
+
framework: ViewFramework = 'dom',
|
|
1108
|
+
baseUrl = '/'
|
|
1109
|
+
): string {
|
|
1110
|
+
const path = `${baseUrl}preview/code/code.html?${CODE_TARGET_PARAMETER}=${target}`;
|
|
1111
|
+
return framework === 'dom'
|
|
1112
|
+
? path
|
|
1113
|
+
: `${path}&${VIEW_FRAMEWORK_PARAMETER}=${framework}`;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
/**
|
|
1117
|
+
* The URL of the view iframe rendering one custom view.
|
|
1118
|
+
*
|
|
1119
|
+
* @param viewId - The view's id.
|
|
1120
|
+
* @param framework - The view's framework; a framework other than `'dom'` is
|
|
1121
|
+
* named in the URL, so the iframe loads its setup module ahead of the views.
|
|
1122
|
+
* @param baseUrl - The URL the preview is served under, ending in `/`; the
|
|
1123
|
+
* root of the origin, `/`, by default.
|
|
1124
|
+
* @returns The page's URL, such as `/preview/view/view.html?view=tokens`.
|
|
1125
|
+
*/
|
|
1126
|
+
export function viewFrameUrl(
|
|
1127
|
+
viewId: string,
|
|
1128
|
+
framework: ViewFramework = 'dom',
|
|
1129
|
+
baseUrl = '/'
|
|
1130
|
+
): string {
|
|
1131
|
+
const path = `${baseUrl}preview/view/view.html?${VIEW_PARAMETER}=${viewId}`;
|
|
1132
|
+
return framework === 'dom'
|
|
1133
|
+
? path
|
|
1134
|
+
: `${path}&${VIEW_FRAMEWORK_PARAMETER}=${framework}`;
|
|
1135
|
+
}
|