@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/README.md
CHANGED
|
@@ -1,3 +1,1869 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @scaffold-ui-kit/preview
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A preview for kits built with [scaffold-ui-kit](https://github.com/djurnamn/scaffold-ui-kit).
|
|
4
|
+
It renders every component of a kit side by side in React, Vue and Svelte,
|
|
5
|
+
each in its own iframe running that framework natively, and as the markup of
|
|
6
|
+
its html target where the kit builds one, with controls for the
|
|
7
|
+
component's props, named scenarios, and an editor for composing its slot
|
|
8
|
+
content. Everything kit-specific comes from the kit's configuration, so the
|
|
9
|
+
same package previews any kit.
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install --save-dev @scaffold-ui-kit/preview charcell @zag-js/react
|
|
13
|
+
npx scaffold-ui-kit-preview dev
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`charcell` and `@zag-js/react` are peers of the charcell shell, the shell the
|
|
17
|
+
preview ships with (below); a kit using another shell leaves them out. A kit
|
|
18
|
+
made with `scaffold-ui-kit initialize` has all of this set up.
|
|
19
|
+
|
|
20
|
+
## What you get
|
|
21
|
+
|
|
22
|
+
- **One pane per target.** A component renders in each target it is built
|
|
23
|
+
for, next to the others, so a difference between targets is visible at a
|
|
24
|
+
glance. A toggle per target hides the ones you don't need. The targets
|
|
25
|
+
are the kit's own `targets`: a kit built for React and Vue gets those two
|
|
26
|
+
panes and no Svelte one. A story that throws as it renders shows
|
|
27
|
+
`render error:` and the error's message in its pane, in each target.
|
|
28
|
+
- **An html pane.** A kit whose `targets` include `html` gets one more pane
|
|
29
|
+
and toggle, after the other targets': the markup the story renders to, under
|
|
30
|
+
the kit's theme, options and `mount` frame, with no JavaScript.
|
|
31
|
+
The markup shows under the styles the kit's build wrote for the html
|
|
32
|
+
target, for the story's component and each component it composes: the
|
|
33
|
+
`<style>` blocks of each page in `dist/html` where the kit's
|
|
34
|
+
`stylesheetPlacement` is `in-file`, or each component's stylesheet there
|
|
35
|
+
where it is `separate-file`, an SCSS one
|
|
36
|
+
compiled with the kit's own `sass`. The markup follows the templates as
|
|
37
|
+
they are saved, the page reloading, and the styles follow the last
|
|
38
|
+
`npm run build`.
|
|
39
|
+
It follows the scenario and every prop and editor change. It runs none of
|
|
40
|
+
the story's behavior: html writes a declared event as a comment, and the
|
|
41
|
+
pane takes out anything in the markup that would run. What the markup
|
|
42
|
+
cannot evaluate at rest, such as a part shown on a condition the
|
|
43
|
+
component's script holds, is left out, and the pane's legend says how many
|
|
44
|
+
warnings that gives, the count opening them in a popover. The expressions one component cannot evaluate for one reason, such
|
|
45
|
+
as several reading a per-instance id its script generates, count as one
|
|
46
|
+
warning.
|
|
47
|
+
- **Pane widths.** The panes fit the space beside each other, or every one
|
|
48
|
+
is laid out at a width you pick, such as `large` 1280 pixels, so a
|
|
49
|
+
component that responds to its width shows as it does on a screen that
|
|
50
|
+
wide; the default shell stacks the panes as rows when the width needs the
|
|
51
|
+
room. A kit can name its own widths.
|
|
52
|
+
- **Controls from the template.** Each declared prop gets a control: a select
|
|
53
|
+
for a union of string literals, a toggle for a boolean, a text or number
|
|
54
|
+
input otherwise. A union of booleans and string literals, such as a
|
|
55
|
+
checkbox's `boolean | 'indeterminate'`, gets a select of `true`, `false` and
|
|
56
|
+
the literals, and the link keeps the choice as the value it is. A union that
|
|
57
|
+
also takes any string (`string & {}`) or a structured form, such as a list
|
|
58
|
+
of breakpoint values, still gets a select of its literals, and a type the
|
|
59
|
+
template imports by name reads as its members. Edits reach every pane at
|
|
60
|
+
once without a reload.
|
|
61
|
+
- **Two-way props.** A prop whose template declares a `bindable` object,
|
|
62
|
+
such as a toggle's `pressed`, keeps its own value while a story leaves it
|
|
63
|
+
out, in every pane, however the story's other props change. A story that
|
|
64
|
+
gives it is controlled, as a consumer's state would be:
|
|
65
|
+
each value the component reports through its change callback becomes the
|
|
66
|
+
story's prop, so a click in any pane changes every pane, the html, the
|
|
67
|
+
controls and the link alike. A pane takes the value at once, as a
|
|
68
|
+
consumer's state does, so typing mid-text in a controlled text field
|
|
69
|
+
keeps the caret where it is. Its code binds the prop as a consumer does,
|
|
70
|
+
so copied code responds like the story: a controlled `Toggle` prints
|
|
71
|
+
`const [pressed, setPressed] = useState(true);` and `<Toggle
|
|
72
|
+
pressed={pressed} onPressedChange={setPressed}>` in React, `v-model:pressed`
|
|
73
|
+
on a `ref` in Vue and `bind:pressed` in Svelte, each starting at the
|
|
74
|
+
story's value (see A story's source). The static build is the exception
|
|
75
|
+
for the html: it prints the html of each scenario alone, so once a click
|
|
76
|
+
writes back a value other than the scenario's, the html pane says it has
|
|
77
|
+
none until the value is the scenario's again.
|
|
78
|
+
- **Scenarios.** Named prop sets a kit curates per component, plus one
|
|
79
|
+
generated per enum value and per flipped boolean, and an `Empty` scenario
|
|
80
|
+
for a component that takes content.
|
|
81
|
+
- **A scenario editor.** A component's slot content as an editable tree:
|
|
82
|
+
add components, plain HTML tags and text, reorder them by dragging, edit
|
|
83
|
+
each node's props. Every edit regenerates the story and reloads the panes,
|
|
84
|
+
so what you compose renders as the kit's real components.
|
|
85
|
+
- **A story's code.** What a consumer writes to get the story on screen, in
|
|
86
|
+
React, Vue and Svelte, and the markup it renders to as html, following
|
|
87
|
+
every prop edit, scenario pick and editor change.
|
|
88
|
+
- **Kit options and themes.** Toggles and selects the kit declares (a color
|
|
89
|
+
mode, a density, a frame around the story), and the kit's compiled theme
|
|
90
|
+
stylesheets.
|
|
91
|
+
- **Custom views.** Panels the kit writes itself, such as a tokens page or
|
|
92
|
+
an icon gallery, listed beside the components.
|
|
93
|
+
- **A shell of your choosing.** The interface around the panes is a shell
|
|
94
|
+
the kit names. The charcell shell ships with the preview, with slots for a
|
|
95
|
+
kit's own content; a shell can also be written from scratch, in React, Vue,
|
|
96
|
+
Svelte or plain DOM, on the preview's shell contract.
|
|
97
|
+
|
|
98
|
+
## Setting it up
|
|
99
|
+
|
|
100
|
+
The preview reads the `preview` block of the kit configuration,
|
|
101
|
+
`scaffold-ui-kit.ts` (or `scaffold-ui-kit.json`) in the kit root. The build
|
|
102
|
+
leaves the block alone; this package owns and validates it. A key the block
|
|
103
|
+
does not take, or one an object inside it does not (a source, `codeBlock`,
|
|
104
|
+
`setupModules`, `landing`, a hand-written component's mapping), stops
|
|
105
|
+
`check`, `build` and `dev`, named with the keys allowed there, as in `unknown key
|
|
106
|
+
'preview.persistance'; the 'preview' block takes shell, sources, ...`, or with
|
|
107
|
+
what to use instead where one key is meant.
|
|
108
|
+
|
|
109
|
+
<!-- example: preview-configuration -->
|
|
110
|
+
```ts
|
|
111
|
+
/// <reference types="@scaffold-ui-kit/preview" />
|
|
112
|
+
import { defineKit } from 'scaffold-ui-kit';
|
|
113
|
+
|
|
114
|
+
export default defineKit({
|
|
115
|
+
name: 'my-ui-kit',
|
|
116
|
+
targets: ['react', 'vue', 'svelte'],
|
|
117
|
+
styling: [],
|
|
118
|
+
preview: {
|
|
119
|
+
shell: '@scaffold-ui-kit/preview/shells/charcell',
|
|
120
|
+
sources: [
|
|
121
|
+
{
|
|
122
|
+
id: 'core',
|
|
123
|
+
label: 'Components',
|
|
124
|
+
order: 0,
|
|
125
|
+
moduleBase: 'my-ui-kit',
|
|
126
|
+
templatesDirectory: 'src/components',
|
|
127
|
+
storiesDirectory: 'preview/stories',
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
themes: [
|
|
131
|
+
{ id: 'default', label: 'Default', css: 'dist/themes/default.css', assets: 'dist/fonts' },
|
|
132
|
+
],
|
|
133
|
+
canvas: { namePrefix: 'my-ui-kit' },
|
|
134
|
+
options: [
|
|
135
|
+
{
|
|
136
|
+
id: 'theme',
|
|
137
|
+
label: 'theme',
|
|
138
|
+
values: [{ value: 'default', label: 'Default' }],
|
|
139
|
+
default: 'default',
|
|
140
|
+
group: 'kit',
|
|
141
|
+
scope: 'global',
|
|
142
|
+
apply: { type: 'stylesheet' },
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
id: 'mode',
|
|
146
|
+
label: 'mode',
|
|
147
|
+
values: [
|
|
148
|
+
{ value: 'light', label: 'light' },
|
|
149
|
+
{ value: 'dark', label: 'dark' },
|
|
150
|
+
],
|
|
151
|
+
default: 'light',
|
|
152
|
+
scope: 'global',
|
|
153
|
+
apply: { type: 'root-attribute', attribute: 'data-mode' },
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
id: 'outline',
|
|
157
|
+
label: 'outline',
|
|
158
|
+
default: false,
|
|
159
|
+
group: 'canvas',
|
|
160
|
+
scope: 'global',
|
|
161
|
+
apply: { type: 'class', target: 'body', className: 'is-outlined' },
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The first line loads this package's types for the block, so an editor
|
|
169
|
+
checks it as it checks the rest of the configuration.
|
|
170
|
+
|
|
171
|
+
Every command finds the kit configuration the same way: the file
|
|
172
|
+
`--configuration <path>` names, relative to the directory the command runs
|
|
173
|
+
in, else the one the kit's `package.json` names
|
|
174
|
+
as `"scaffold-ui-kit": { "configuration": "<path>" }`, relative to the kit
|
|
175
|
+
root, else `scaffold-ui-kit.ts` or `scaffold-ui-kit.json` in the kit root.
|
|
176
|
+
Both in the root is an error.
|
|
177
|
+
|
|
178
|
+
The keys the preview's pages read, `options`, `canvas`, `themes`, `mount` and
|
|
179
|
+
`viewCanvas` (see The kit's vocabulary), reach the browser as JSON, so they
|
|
180
|
+
hold plain data: a function, a regular expression or `undefined` in a list
|
|
181
|
+
there stops `check`, `build` and `dev`, naming where it sits, as in
|
|
182
|
+
`preview.options[0].apply.className is a function, which can't be sent to
|
|
183
|
+
the browser as JSON`. The kit configuration itself never reaches the
|
|
184
|
+
browser, so `scaffold-ui-kit.ts` can import what it computes these from,
|
|
185
|
+
such as the kit's tokens. `controlHints` and `labels` are read in Node
|
|
186
|
+
alone, so in `scaffold-ui-kit.ts` their hooks can be functions; a kit
|
|
187
|
+
configured in `scaffold-ui-kit.json` leaves those hooks out.
|
|
188
|
+
|
|
189
|
+
- `sources` - the packages whose components the preview shows, each under its
|
|
190
|
+
own navigation section. A source's components import from
|
|
191
|
+
`<moduleBase>/<target>`, so the preview renders the kit as a consumer gets
|
|
192
|
+
it. The kit is the first source; another package's components can be
|
|
193
|
+
another. For the kit's own source that means importing the kit by its
|
|
194
|
+
package name, so its `package.json` needs an `exports` entry per target (`"./react":
|
|
195
|
+
"./dist/react/index.ts"`, and likewise for `vue` and `svelte`). A
|
|
196
|
+
`moduleBase` containing `{target}` names the module itself, with the
|
|
197
|
+
target in its place: a kit built into one package per target (its
|
|
198
|
+
configuration's `packages`) sets it to the pattern its target packages
|
|
199
|
+
share, such as `'@acme/{target}'` for `@acme/react` and `@acme/vue`. Each
|
|
200
|
+
of those packages, and the styles package, has to resolve from the kit
|
|
201
|
+
root, as a workspace package or a dev dependency of the kit root.
|
|
202
|
+
- `templatesDirectory` - the component templates to read: TypeScript,
|
|
203
|
+
JSON and JavaScript ones (`.ts`, `.json`, `.js`, `.mjs`, `.cjs`) alike,
|
|
204
|
+
as the kit's build reads them.
|
|
205
|
+
- `storiesDirectory` - the story files that curate each component's story
|
|
206
|
+
(below).
|
|
207
|
+
- `manualModule` - a module whose default export returns the components
|
|
208
|
+
that have no template, such as a hand-written driver, which gets its
|
|
209
|
+
story this way, as `() => ManualComponentSpecification[]`. Each gives its
|
|
210
|
+
`name`, the `file` it lives in (informational), and `targets`
|
|
211
|
+
when it renders in only some, and gives its props inline in `props`, or
|
|
212
|
+
names its React props type in
|
|
213
|
+
`propsFrom: '<file>#<TypeName>'`, a file relative to the kit root. The
|
|
214
|
+
preview reads that type's members from source, following imports and
|
|
215
|
+
intersections. A type built on a template component reads too:
|
|
216
|
+
`ComponentProps<typeof ButtonVisual>` takes the template's props, through
|
|
217
|
+
an `Omit<..., 'name'>` and a generic helper such as
|
|
218
|
+
`Props extends unknown ? Omit<Props, 'onChange'> : never`. A member whose
|
|
219
|
+
type is not a control is kept as a hidden prop.
|
|
220
|
+
- `options`, `canvas`, `themes`, `mount`, `viewCanvas`, `paneWidths`,
|
|
221
|
+
`controlHints` and `labels` - the kit's vocabulary (below).
|
|
222
|
+
- `shell` - the shell that renders the preview's interface (below). Required:
|
|
223
|
+
a package specifier, resolved from the kit as its own imports are, or a
|
|
224
|
+
module path relative to the kit root, whose default export is a shell.
|
|
225
|
+
`@scaffold-ui-kit/preview/shells/charcell` is the shell shipped with the
|
|
226
|
+
preview; a kit that names it installs `charcell` and `@zag-js/react`. The
|
|
227
|
+
long form, `{ module, page }`, names the shell's page module beside it:
|
|
228
|
+
what the host page shows before the shell has loaded (below).
|
|
229
|
+
- `viewsModule` - the kit's custom views (below).
|
|
230
|
+
- `codeBlock` - the kit's own code block for the code view, as
|
|
231
|
+
`{ module, framework }` (below).
|
|
232
|
+
- `setupModules` - per target, a module each preview iframe imports once for
|
|
233
|
+
its side effects, such as mounting a toast container a story assumes.
|
|
234
|
+
- `htmlStylesheets` - stylesheets the html pane loads beside the theme's and
|
|
235
|
+
its components' own, such as a preflight a framework's setup module would
|
|
236
|
+
import. The html pane runs no script, so this is its counterpart to a
|
|
237
|
+
setup module.
|
|
238
|
+
- `aliases` - import aliases for the preview only.
|
|
239
|
+
- `scssLoadPaths` - sass load paths for per-component SCSS.
|
|
240
|
+
- `optimizeDepsExclude` - packages Vite should not prebundle, such as a
|
|
241
|
+
dependency that ships raw `.svelte` files.
|
|
242
|
+
- `dedupe` - packages the preview loads one copy of, beside React, Vue and
|
|
243
|
+
Svelte, which it always loads once. Name a package the shell and the kit's
|
|
244
|
+
components both import, when the two must share its instances: a library
|
|
245
|
+
with a plugin registry or other module-level state fails when the shell's
|
|
246
|
+
copy meets the kit's. Every import of a listed package, and of its
|
|
247
|
+
subpaths, resolves to the copy the kit root resolves, prebundled once, so
|
|
248
|
+
each must be installed where the kit root finds it; one that is not is an
|
|
249
|
+
error. List the packages whose instances meet, such as a library's core
|
|
250
|
+
and the packages around it that hold state, and keep them out of
|
|
251
|
+
`optimizeDepsExclude`. After changing the list, clear the kit's
|
|
252
|
+
`.preview-generated/.vite` directory, Vite's dependency cache.
|
|
253
|
+
- `omitPattern` - a regular expression, as a string, matched against each
|
|
254
|
+
component's name; a match gets no story, as a story file's `omit` does. For a
|
|
255
|
+
kit whose naming marks a compound's parts, such as `"(Driver|Visual)$"`.
|
|
256
|
+
Unset, no component is left out by name.
|
|
257
|
+
- `handWrittenComponents` - what each component written by hand renders as
|
|
258
|
+
in a story's `html`: `"content"` for one that renders only the content
|
|
259
|
+
passed into it, such as a driver that adds behavior to markup it is given,
|
|
260
|
+
or the name of the template it renders, which then takes its props and
|
|
261
|
+
content (its own props the template does not declare are dropped). Where
|
|
262
|
+
the component puts content in a different slot of that template, give
|
|
263
|
+
`{ "template": "FieldVisual", "slots": { "default": "inner" } }`. Either
|
|
264
|
+
way one comment around it says its behavior is not included.
|
|
265
|
+
- `port` - the dev server's port. Default `6100`; `dev --port` overrides it,
|
|
266
|
+
and a taken port moves to the next free one.
|
|
267
|
+
- `host` - the addresses the dev server listens on: an address such as
|
|
268
|
+
`"0.0.0.0"`, or `true` for every address, so another device on the network
|
|
269
|
+
can open the preview. Default: this machine only; `dev --host` overrides
|
|
270
|
+
it.
|
|
271
|
+
- `persistence` - where the preview remembers what a person chose: `"url"`
|
|
272
|
+
(the default) or `"url-and-local-storage"` (below).
|
|
273
|
+
- `landing` - the entry the preview opens on when its address names no
|
|
274
|
+
story or view: `{ "view": "foundations" }` for a custom view, or
|
|
275
|
+
`{ "component": "components-button", "scenario": "ghost" }` for a story,
|
|
276
|
+
on one of its scenarios or, without `scenario`, on the scenario it starts
|
|
277
|
+
on. The ids are the ones the address carries (below). `check` and
|
|
278
|
+
`generate` fail on a story or scenario the kit has none of; a view the
|
|
279
|
+
views module does not register opens the first story instead, with a
|
|
280
|
+
warning in the browser's console. The `empty` scenario renders through
|
|
281
|
+
the development server alone, so the static build opens that story on
|
|
282
|
+
the scenario it starts on, with a warning in the console. Default: the
|
|
283
|
+
first story the navigation lists.
|
|
284
|
+
|
|
285
|
+
Paths are relative to the kit root, and the kit's modules are TypeScript the
|
|
286
|
+
preview compiles as it loads them.
|
|
287
|
+
|
|
288
|
+
## Story files
|
|
289
|
+
|
|
290
|
+
A component's story file, `<Name>.story.ts` in its source's
|
|
291
|
+
`storiesDirectory`, curates the story the preview shows for it: the
|
|
292
|
+
props it starts from, its controls, its slot content and its scenarios. The
|
|
293
|
+
file default-exports a `Story` object. Every field is optional, and a
|
|
294
|
+
component without a story file still gets a story from its template.
|
|
295
|
+
|
|
296
|
+
<!-- example: preview-story -->
|
|
297
|
+
```ts
|
|
298
|
+
import type { Story } from '@scaffold-ui-kit/preview/types';
|
|
299
|
+
|
|
300
|
+
const story: Story = {
|
|
301
|
+
category: 'Inputs',
|
|
302
|
+
defaultProps: { label: 'Save' },
|
|
303
|
+
controls: { size: { number: { min: 1, max: 4, step: 1 } }, onClick: false },
|
|
304
|
+
slots: { default: { kind: 'text', value: 'Save changes' } },
|
|
305
|
+
scenarios: [{ id: 'destructive', label: 'Destructive', props: { variant: 'danger' } }],
|
|
306
|
+
};
|
|
307
|
+
export default story;
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
- `defaultProps` - the props the story starts from. Beside the declared
|
|
311
|
+
props, a component that forwards native attributes (a `passthrough`
|
|
312
|
+
element, or a `rest` it spreads) takes HTML attributes such as
|
|
313
|
+
`aria-label` and `class` here and in scenarios. A story spells them as
|
|
314
|
+
HTML does, the form Vue and Svelte take. React takes them as React
|
|
315
|
+
spells them, in the React pane and in the React code: `class` as
|
|
316
|
+
`className` (joined with a `className` the story also gives), a `style`
|
|
317
|
+
string as a style object (without `!important`, which a style object
|
|
318
|
+
cannot hold), and the attributes React
|
|
319
|
+
renames by their React names (`tabindex` as `tabIndex`, `for` as
|
|
320
|
+
`htmlFor`, `readonly` as `readOnly`). A component that declares a prop by
|
|
321
|
+
the exact name takes it as written. The same goes for an `element` fill's
|
|
322
|
+
`props` and an `html` fill's (see Slot content, below).
|
|
323
|
+
- `controls` - per prop, tweaks to its control, or `false` to leave the prop
|
|
324
|
+
out of the controls and the generated scenarios.
|
|
325
|
+
- `fixed` - props always passed with the given value and never shown as
|
|
326
|
+
controls. Like `defaultProps`, it takes `children` and the native
|
|
327
|
+
attributes a component forwards.
|
|
328
|
+
- `generate` - which props get generated scenarios: `false` for none, a list
|
|
329
|
+
of prop names for only those, or `{ exclude: [...] }` for all but those.
|
|
330
|
+
- `slots` - the story's slot content: `default` for the default slot and
|
|
331
|
+
`named` for the others, each a fill (see Slot content, below). Without
|
|
332
|
+
`slots` or a `children` text, the panes show the component's name in its
|
|
333
|
+
default slot, so the slot is visible; the story's code leaves it empty.
|
|
334
|
+
- `around` - content the story renders beside the component, as its
|
|
335
|
+
siblings: `before` it and `after` it (see Content beside the component,
|
|
336
|
+
below).
|
|
337
|
+
- `scenarios` - named prop sets, one of which may be marked `default`. A
|
|
338
|
+
scenario's `slots` give it slot content of its own, in the shape of the
|
|
339
|
+
story's `slots`: a slot it fills shows the scenario's fill in place of the
|
|
340
|
+
story's, and every other slot keeps the story's, as in
|
|
341
|
+
`{ id: 'failed', props: { tone: 'loud' }, slots: { default: { kind: 'text', value: 'Could not save.' } } }`.
|
|
342
|
+
The panes, each target's code, the html, the editor and a static build
|
|
343
|
+
all show that content when the scenario is picked. A
|
|
344
|
+
scenario that takes a generated scenario's id (`variant-solid` for
|
|
345
|
+
`variant: 'solid'`, or `disabled` for a boolean) replaces it, so it should
|
|
346
|
+
set that value. `empty` is the preview's own Empty scenario's id, which no
|
|
347
|
+
curated scenario can take; a boolean named `empty` gets `empty-true`
|
|
348
|
+
instead; one that sets a value alone stands in for its generated
|
|
349
|
+
scenario under its own id. A scenario's `description` says what it shows,
|
|
350
|
+
for a shell to show beside it; markdown is allowed. The component's own
|
|
351
|
+
description is the template's `description`. A scenario's `options` set
|
|
352
|
+
the kit's options when it is picked, so it can show the component in a
|
|
353
|
+
setting: `{ id: 'on-dark', options: { mode: 'dark', surface: '3' } }`.
|
|
354
|
+
They are set as if chosen by hand: a global value stays when another
|
|
355
|
+
scenario is picked, unless that one sets it too, and a story value lasts
|
|
356
|
+
until another story is selected. A scenario unsets a `defaultProps` value
|
|
357
|
+
with `null`, not `undefined`: `undefined` doesn't reach the panes, which
|
|
358
|
+
keep the `defaultProps` value, and `check` warns of a scenario prop set to
|
|
359
|
+
`undefined` that such a value covers (an error under `--strict`). Every
|
|
360
|
+
pane leaves a prop given `null` out, as the story's code does, so the
|
|
361
|
+
component's declared default applies. A two-way prop is the exception:
|
|
362
|
+
given `null`, it is still controlled, at `null`, and the code binds state
|
|
363
|
+
starting at `null`;
|
|
364
|
+
`check` reports one whose declared type does not take `null`, as that
|
|
365
|
+
code would not type-check. A scenario that controls a two-way prop need
|
|
366
|
+
not unset its default prop (a `defaultValue` beside `value`): the code
|
|
367
|
+
leaves it out once the story gives the prop. An empty string is a value
|
|
368
|
+
like any other: a prop given `''`, by a story, a control or a component's
|
|
369
|
+
own change, reaches every pane as `''`.
|
|
370
|
+
- `options` - per-story values for the kit's story-scoped options, or `false`
|
|
371
|
+
to make an option unavailable for this component.
|
|
372
|
+
- `omit` - leave the component out (a part only its parent renders); a kit
|
|
373
|
+
can leave out a whole naming pattern with `omitPattern`.
|
|
374
|
+
- `standalone` - whether the component has a story of its own. Default
|
|
375
|
+
`true`. `standalone: false` keeps the component out of the navigation but
|
|
376
|
+
offers it in the editor: a compound's subcomponents, a shared building
|
|
377
|
+
block, a component only a fixture needs. A component its compound's root
|
|
378
|
+
lists in `parts` - in the root's template, or in the kit configuration's
|
|
379
|
+
`parts` for a root with no template - is `standalone: false` without the
|
|
380
|
+
flag. `check`
|
|
381
|
+
warns of `standalone: false` on a component no root lists, since its
|
|
382
|
+
compound is not copied whole either, unless the component is no one compound's part: two or
|
|
383
|
+
more templates render it (each template counts, a component's omitted
|
|
384
|
+
counterparts included), a story of its source labeled with its name stands
|
|
385
|
+
in for it, or it lists parts of its own.
|
|
386
|
+
- `unlisted` - keep the story out of what a reader browses: the
|
|
387
|
+
navigation, the editor's component picker, the component count and the
|
|
388
|
+
catalog a custom view reads. It is generated and renders in every
|
|
389
|
+
target as any story does, and opens at its address
|
|
390
|
+
(`?component=<id>`) and through the testing helpers, so it suits a story
|
|
391
|
+
that exists for a kit's own browser tests. A component written by hand
|
|
392
|
+
takes `unlisted: true` in its manual spec. An omitted component or one
|
|
393
|
+
that is `standalone: false` has no story to keep out, so `check` reports
|
|
394
|
+
`unlisted` beside `omit` or `standalone: false` as an error.
|
|
395
|
+
- `label` - the name shown in the navigation, when it should differ. A story
|
|
396
|
+
labeled with the name of a component in its source that is
|
|
397
|
+
`standalone: false` is that component's story.
|
|
398
|
+
|
|
399
|
+
### Slot content
|
|
400
|
+
|
|
401
|
+
A fill is one node of slot content, its `kind` naming what it is. This
|
|
402
|
+
story file fills a `DataList`'s default slot with text, its `header` slot with a
|
|
403
|
+
kit component that has a named slot of its own, and its scoped `row` slot,
|
|
404
|
+
which exposes each `row`:
|
|
405
|
+
|
|
406
|
+
<!-- example: preview-slot-content -->
|
|
407
|
+
```ts
|
|
408
|
+
const story: Story = {
|
|
409
|
+
defaultProps: { rows: [{ id: '1', name: 'Ada' }, { id: '2', name: 'Grace' }] },
|
|
410
|
+
slots: {
|
|
411
|
+
default: { kind: 'text', value: 'Two people' },
|
|
412
|
+
named: {
|
|
413
|
+
header: {
|
|
414
|
+
kind: 'element',
|
|
415
|
+
component: 'IconLabel',
|
|
416
|
+
props: { size: 'small' },
|
|
417
|
+
slots: {
|
|
418
|
+
icon: { kind: 'html', tag: 'span', props: { 'aria-hidden': 'true' }, children: { kind: 'text', value: '#' } },
|
|
419
|
+
},
|
|
420
|
+
children: { kind: 'text', value: 'People' },
|
|
421
|
+
},
|
|
422
|
+
row: {
|
|
423
|
+
kind: 'scoped',
|
|
424
|
+
slot: { react: 'row', vue: 'row', svelte: 'row' },
|
|
425
|
+
parameters: ['row'],
|
|
426
|
+
react: '<strong>{row.name}</strong>',
|
|
427
|
+
vue: '<strong>{{ row.name }}</strong>',
|
|
428
|
+
svelte: '<strong>{row.name}</strong>',
|
|
429
|
+
html: [{ type: 'element', tag: 'strong', children: [{ type: 'text', expression: 'row.name' }] }],
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
},
|
|
433
|
+
};
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
| `kind` | Fields | What it fills |
|
|
437
|
+
|---|---|---|
|
|
438
|
+
| `text` | `value` | Text. |
|
|
439
|
+
| `element` | `component`, `props`, `children`, `slots` | A kit component. `children` is one fill for its default slot, and `slots` maps its named slots to fills; a `default` key there is an error. |
|
|
440
|
+
| `html` | `tag`, `props`, `children` | A plain HTML element, with one fill as its content. |
|
|
441
|
+
| `fragment` | `children` | A list of fills, side by side. |
|
|
442
|
+
| `raw` | `react`, `vue`, `svelte`, `html`, `imports` | Source written per target: a JSX expression for React, markup for Vue and Svelte. `imports` names the kit components the source uses, as tags or as values (`:as="Button"` in Vue), as a module's imports are. |
|
|
443
|
+
| `scoped` | `slot`, `parameters`, `react`, `vue`, `svelte`, `reactScope`, `html`, `imports` | A slot that exposes values, as a top-level fill in `default` or `named` only. `slot` names the slot in each target, since a render prop, a Vue slot and a Svelte snippet may be named differently. `parameters` are the exposed values each body reads. `reactScope: 'positional'` passes them to React as separate arguments, for a hand-written component whose render prop takes them that way. A React body that is not one expression, two or more sibling roots or text such as `{item}` or `remove <Button />`, is returned in a fragment, `<>...</>`; a body opening with a word reads as an expression unless two words or a word and a placeholder or tag stand side by side. Vue and Svelte take the body as it is. A slot whose template sets `scopeRequired` takes this fill alone: the check refuses any other there, and the editor neither offers nor takes a drop of plain content for it. |
|
|
444
|
+
| `part` | `name`, `parameters`, `parameterTypes`, `props`, `react`, `vue`, `svelte`, `html`, `imports` | A call to content the story declares once by `name` (below). Its React body goes in a fragment as a `scoped` fill's does. |
|
|
445
|
+
|
|
446
|
+
`props`, `children` (of an `element` or `html` fill), `slots`, `html`,
|
|
447
|
+
`imports`, `reactScope` and `parameterTypes` are optional. `raw`,
|
|
448
|
+
`scoped` and `part` fills take an `html` for the story's `html` source: the
|
|
449
|
+
markup the fill renders to at rest, as a string (not for `part`) or as
|
|
450
|
+
template nodes, which render as the kit's templates do and may compose the
|
|
451
|
+
kit's components. A node composes a component by naming it as its `tag`,
|
|
452
|
+
with the component's props as `attributes`:
|
|
453
|
+
`{ type: 'element', tag: 'Button', attributes: { variant: 'outline' }, children: [{ type: 'text', content: 'Cancel' }] }`
|
|
454
|
+
renders the kit's outline Button. A `scoped` fill's nodes read the slot's
|
|
455
|
+
values under its `parameters`. Without an `html`, the story has no `html`
|
|
456
|
+
source.
|
|
457
|
+
|
|
458
|
+
A `part` fill calls content the story declares once by `name`, with the
|
|
459
|
+
`props` it passes; its body per target reads them by the names in
|
|
460
|
+
`parameters` and may call the part again, so a tree or lists nested in each
|
|
461
|
+
other's items render as deep as their data goes. React declares a part as a
|
|
462
|
+
function component, Vue as a component (in the story's source, a single-file
|
|
463
|
+
component of its own, printed after the story's), and Svelte as a snippet at
|
|
464
|
+
the top of the markup. A body calls it as `<Name ... />` in React and Vue and
|
|
465
|
+
`{@render Name({ ... })}` in Svelte. Its `html` is template nodes that read
|
|
466
|
+
`parameters`, where a `Name` element renders the part again; every call of
|
|
467
|
+
one name carries the same declaration. Its `parameterTypes` give each
|
|
468
|
+
parameter a TypeScript type for the printed source: React's props and
|
|
469
|
+
Svelte's snippet are typed with them (`any` where a parameter has none), and
|
|
470
|
+
Vue's `defineProps` once any is given.
|
|
471
|
+
|
|
472
|
+
A scenario sets `id`, `label`, `description`, `props`, `options` and
|
|
473
|
+
`default`, and nothing else: slot content belongs to the story file, shared by
|
|
474
|
+
its scenarios.
|
|
475
|
+
|
|
476
|
+
### Content beside the component
|
|
477
|
+
|
|
478
|
+
`around` renders content beside the component rather than inside it, such
|
|
479
|
+
as a hint whose id the component's `aria-describedby` names. `before` and
|
|
480
|
+
`after` each take a fill of any kind but `scoped`, which needs a slot to
|
|
481
|
+
give it values:
|
|
482
|
+
|
|
483
|
+
<!-- example: preview-around -->
|
|
484
|
+
```ts
|
|
485
|
+
const story: Story = {
|
|
486
|
+
defaultProps: { label: 'Save', 'aria-describedby': 'save-hint' },
|
|
487
|
+
around: {
|
|
488
|
+
after: {
|
|
489
|
+
kind: 'html',
|
|
490
|
+
tag: 'p',
|
|
491
|
+
props: { id: 'save-hint' },
|
|
492
|
+
children: { kind: 'text', value: 'Saves a draft you can return to.' },
|
|
493
|
+
},
|
|
494
|
+
},
|
|
495
|
+
};
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
Every pane renders it as the component's siblings, inside the
|
|
499
|
+
`mount` node the component mounts in, so the kit's canvas CSS lays
|
|
500
|
+
them out. The story's code prints them the same way: React returns them in
|
|
501
|
+
a fragment with the component, and Vue's template and Svelte's markup list
|
|
502
|
+
them beside it. The html pane and the html source render them beside the
|
|
503
|
+
component's markup, from the fill's html as slot content's is. The scenario
|
|
504
|
+
editor shows them as locked rows before and after its root, which no edit
|
|
505
|
+
changes. Like `slots`, `around` belongs to the story file, so every scenario of
|
|
506
|
+
the story shows it. `check` reports a scoped fill there, a key other than
|
|
507
|
+
`before` and `after`, and the mistakes it reports in slot content.
|
|
508
|
+
|
|
509
|
+
### Checking story files
|
|
510
|
+
|
|
511
|
+
`scaffold-ui-kit-preview check` compares the story files with the components.
|
|
512
|
+
A key a story file does not take is an error, named with the keys it does or
|
|
513
|
+
with what to use instead. A prop, slot or value a story file names that the
|
|
514
|
+
component lacks is an error,
|
|
515
|
+
and so is a prop or named slot a fill gives a component it composes, at
|
|
516
|
+
every depth of the slot content. A value outside a prop's choices is
|
|
517
|
+
reported with the choices, adding "any string" or "a structured value"
|
|
518
|
+
where the prop's type takes them beside its literals.
|
|
519
|
+
A scenario's option value that no kit option takes, or that its option
|
|
520
|
+
cannot hold or the story file makes unavailable, is an error too, as is an
|
|
521
|
+
`html` fill giving a void element (`img`, `input`, `br`) children, and a
|
|
522
|
+
target the kit builds, or a `specifier` an `add.shared` entry
|
|
523
|
+
still names for the kit's own package, that its `package.json` has no
|
|
524
|
+
`exports` entry for. A
|
|
525
|
+
prop spelled as React spells an attribute (`autoComplete`) is reported
|
|
526
|
+
with the HTML spelling a story gives it by (`autocomplete`). A `landing`
|
|
527
|
+
entry naming a story the kit has none of, or a scenario its story lacks, is
|
|
528
|
+
an error. An enum value or boolean that no scenario sets is a warning, and
|
|
529
|
+
an error under `--strict`. Props the preview cannot give a control are
|
|
530
|
+
listed, and so are the props `generate` leaves out.
|
|
531
|
+
|
|
532
|
+
The check also reports what `scaffold-ui-kit build` reports for each
|
|
533
|
+
template in the kit's `src/components/`, whichever sources the preview reads,
|
|
534
|
+
without writing any output. It asks the scaffolder the kit has installed,
|
|
535
|
+
or the preview's own when the kit has none (with a warning when the kit's
|
|
536
|
+
copy cannot be loaded or is too old to report), and a kit with no
|
|
537
|
+
`src/components/` gets nothing from it. Each line names the template's path,
|
|
538
|
+
as the build prints it: a template the build refuses, such as one with a
|
|
539
|
+
Tailwind class outside the conversion subset, is an error, and each build
|
|
540
|
+
warning, such as two nodes giving one class different styles, is a warning,
|
|
541
|
+
which `--strict` leaves a warning. A component the kit cannot build doesn't
|
|
542
|
+
pass the check. It covers validating and rendering the templates; a driver
|
|
543
|
+
colliding with a generated file and the stylesheets compiled under
|
|
544
|
+
`stylesheetLink: 'both'` are reported by `scaffold-ui-kit build` alone.
|
|
545
|
+
|
|
546
|
+
## The kit's vocabulary
|
|
547
|
+
|
|
548
|
+
The preview holds no vocabulary of its own: the kit's themes, options and
|
|
549
|
+
the frame around each story are keys of the `preview` block, as in the
|
|
550
|
+
example above.
|
|
551
|
+
|
|
552
|
+
- `themes` - the compiled theme stylesheets, and optionally a
|
|
553
|
+
directory of assets (fonts) to serve beside each.
|
|
554
|
+
- `canvas` - the prefix for the attributes and classes the preview sets in
|
|
555
|
+
each iframe, an optional page background, and extra CSS. Without it the
|
|
556
|
+
prefix is the kit's `name`, each character other than a letter, a digit,
|
|
557
|
+
`-` or `_` made `-`.
|
|
558
|
+
- `options` - the kit's options. An option says what it means, and
|
|
559
|
+
the preview's interface decides how to show it:
|
|
560
|
+
- Its kind follows from its fields: with `values` it is a choice among
|
|
561
|
+
them, its `default` one of them; without, it is a boolean with a boolean
|
|
562
|
+
`default`. The charcell shell shows a select for a choice and a toggle
|
|
563
|
+
for a boolean; another shell picks its own. `optionKind(option)` gives
|
|
564
|
+
the kind, and `generate` and `check`
|
|
565
|
+
fail on an option that is neither.
|
|
566
|
+
- A `global` option has one value for the session; a `story` option is set
|
|
567
|
+
per story, seeded from the story file, and lasts until another story is
|
|
568
|
+
selected.
|
|
569
|
+
- `group` is a placement hint for the shell: `context` (the default) for
|
|
570
|
+
the setting a component is shown in, such as its color mode, a surface
|
|
571
|
+
or the padding around it; `kit` for the kit's own settings, such as its
|
|
572
|
+
theme; or a name of the kit's choosing, such as `canvas`, for a set that
|
|
573
|
+
belongs together. Where each group shows is the shell's decision; the
|
|
574
|
+
charcell shell's placement, and how a kit changes it, is described
|
|
575
|
+
below.
|
|
576
|
+
|
|
577
|
+
Each declares in `apply` how its value reaches the iframe:
|
|
578
|
+
|
|
579
|
+
| `type` | Fields | Effect |
|
|
580
|
+
|---|---|---|
|
|
581
|
+
| `root-attribute` | `attribute`, `target`, `omitAtDefault` | Sets `attribute` to the value. With `omitAtDefault: true`, the option's default value leaves the attribute off, for a kit whose default mode renders with no attribute. |
|
|
582
|
+
| `class` | `className`, `target` | Toggles `className` by the value's truth; without `className`, sets the value as a `data-<namePrefix>-preview-class` attribute for the kit's CSS to read. |
|
|
583
|
+
| `css-var` | `var`, `target` | Sets the custom property `var`, named in full (`'--surface'`), to the value. |
|
|
584
|
+
| `stylesheet` | none | Switches to the `themes` entry whose `id` is the value. |
|
|
585
|
+
|
|
586
|
+
`false` or an empty value removes the attribute or property. `target` is
|
|
587
|
+
optional: `'root'` for the iframe's `<html>` (the default), `'body'`, or
|
|
588
|
+
`{ ref }` for a node of `mount`, as the `outline` option above names
|
|
589
|
+
`'body'`. A `class` apply also takes `on: 'root' | 'body'`, naming the
|
|
590
|
+
same two elements; `target` wins where both are given.
|
|
591
|
+
|
|
592
|
+
`check`, `generate`, `build` and `dev` report a key an option, one of its
|
|
593
|
+
`values` or an `apply` of these four types does not take, such as a
|
|
594
|
+
`description` where an option takes `title`, naming the keys it takes.
|
|
595
|
+
- `paneWidths` - the widths a shell offers to lay a story's panes
|
|
596
|
+
out at, as a list of `{ label: 'small', width: 375 }`, each width a whole
|
|
597
|
+
number of CSS pixels up to 7680, so the kit can derive them from its own
|
|
598
|
+
breakpoints. Default: `small` 375, `medium` 768 and `large` 1280. No
|
|
599
|
+
label or width appears twice. The list replaces the default, so name
|
|
600
|
+
every width to offer; an empty list offers none. A shell lists them
|
|
601
|
+
narrowest first. `check` and `generate` report a list that breaks these
|
|
602
|
+
rules, naming the key.
|
|
603
|
+
- `mount` - an optional tree of elements wrapped around every story,
|
|
604
|
+
such as a frame that pads it or paints a surface, each node addressable by
|
|
605
|
+
a `ref`.
|
|
606
|
+
- `viewCanvas` - the page background and extra CSS for custom views, which
|
|
607
|
+
have no `mount` frame. Without it a view gets `canvas.pageBackground` and
|
|
608
|
+
nothing else. An option targeting a `mount` node by `ref` does nothing in
|
|
609
|
+
a view.
|
|
610
|
+
- `controlHints` - hints for the controls panel, read in Node alone, both
|
|
611
|
+
optional:
|
|
612
|
+
- `colorTokens` - the kit's color tokens, such as
|
|
613
|
+
`[null, 'primary', 'accent']`, `null` (or, in `scaffold-ui-kit.ts`,
|
|
614
|
+
`undefined`) being the unset choice. When the list holds any token, a
|
|
615
|
+
string-typed prop whose name ends in `color`, in any case (`color`,
|
|
616
|
+
`borderColor`), gets a select of
|
|
617
|
+
these tokens in place of its own control, and `check` holds the values a
|
|
618
|
+
story file sets for it to the list. Such a prop gets no generated
|
|
619
|
+
scenarios. Without tokens, the prop is treated by its declared type like
|
|
620
|
+
any other: a union is a select with a scenario per value.
|
|
621
|
+
- `componentSizeRem(componentName)` - a function, so in
|
|
622
|
+
`scaffold-ui-kit.ts` alone: the starting value, in rem, of a number prop
|
|
623
|
+
named `size` that the component gives no default, or `undefined` for
|
|
624
|
+
none.
|
|
625
|
+
- `labels` - the preview's own labels, in the kit's casing, read in Node
|
|
626
|
+
alone: `category`, `emptyScenario`, and, in `scaffold-ui-kit.ts`,
|
|
627
|
+
`generatedScenario`, a function from a prop and the value a generated
|
|
628
|
+
scenario sets to its label (by default `variant: solid`, or `disabled` for
|
|
629
|
+
a boolean).
|
|
630
|
+
|
|
631
|
+
A `mount` frame that gives every story a gutter, with an option that paints
|
|
632
|
+
the frame's surface, as keys of the `preview` block:
|
|
633
|
+
|
|
634
|
+
<!-- example: preview-mount -->
|
|
635
|
+
```ts
|
|
636
|
+
mount: {
|
|
637
|
+
classes: ['story-frame'],
|
|
638
|
+
ref: 'frame',
|
|
639
|
+
children: [{ classes: ['story-gutter'], mount: true }],
|
|
640
|
+
},
|
|
641
|
+
canvas: {
|
|
642
|
+
namePrefix: 'my-ui-kit',
|
|
643
|
+
extraCss: [
|
|
644
|
+
'.story-frame { min-height: 100vh; background: var(--surface); }',
|
|
645
|
+
'.story-gutter { padding: 1.5rem; }',
|
|
646
|
+
].join('\n'),
|
|
647
|
+
},
|
|
648
|
+
options: [
|
|
649
|
+
{
|
|
650
|
+
id: 'surface',
|
|
651
|
+
label: 'surface',
|
|
652
|
+
values: [
|
|
653
|
+
{ value: 'white', label: 'white' },
|
|
654
|
+
{ value: 'whitesmoke', label: 'smoke' },
|
|
655
|
+
],
|
|
656
|
+
default: 'white',
|
|
657
|
+
scope: 'global',
|
|
658
|
+
apply: { type: 'css-var', var: '--surface', target: { ref: 'frame' } },
|
|
659
|
+
},
|
|
660
|
+
],
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
The story mounts inside the node marked `mount: true`, so the gutter's
|
|
664
|
+
padding surrounds it; without a marked node it mounts in the outermost one.
|
|
665
|
+
`extraCss` is appended to the stylesheet the preview injects into every
|
|
666
|
+
story's iframe, where it styles the frame's nodes by their classes, and the
|
|
667
|
+
`surface` option sets `--surface` on the node whose `ref` is `frame`, which
|
|
668
|
+
`extraCss` paints with.
|
|
669
|
+
|
|
670
|
+
### A kit styled with Tailwind
|
|
671
|
+
|
|
672
|
+
A kit built with the `tailwind` styling extension, in its default `classes`
|
|
673
|
+
output, ships utility classes that need a Tailwind build. The preview loads
|
|
674
|
+
that build as the kit's theme. A kit scaffolded with `--styling tailwind`
|
|
675
|
+
gets this setup; another kit adds it in four steps.
|
|
676
|
+
|
|
677
|
+
1. Install `tailwindcss` and `@tailwindcss/cli` as dev dependencies.
|
|
678
|
+
2. Write an entry stylesheet, such as `preview/tailwind.css`, that names the
|
|
679
|
+
built components and the story files as sources. Tailwind skips git-ignored
|
|
680
|
+
files, `dist` among them, unless a `@source` names them:
|
|
681
|
+
|
|
682
|
+
```css
|
|
683
|
+
@import "tailwindcss";
|
|
684
|
+
|
|
685
|
+
@source "../dist";
|
|
686
|
+
@source "../src/components";
|
|
687
|
+
@source "./stories";
|
|
688
|
+
|
|
689
|
+
/* dark: utilities follow a mode option that sets data-mode="dark" */
|
|
690
|
+
@custom-variant dark (&:where([data-mode="dark"], [data-mode="dark"] *));
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
3. Build the kit and compile it before the preview starts or builds:
|
|
694
|
+
|
|
695
|
+
```json
|
|
696
|
+
{
|
|
697
|
+
"scripts": {
|
|
698
|
+
"preview:css": "tailwindcss --input preview/tailwind.css --output preview/tailwind.generated.css",
|
|
699
|
+
"preview": "npm run build && npm run preview:css && scaffold-ui-kit-preview dev",
|
|
700
|
+
"preview:build": "npm run build && npm run preview:css && scaffold-ui-kit-preview build"
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
The other targets' panes show the built components, and Tailwind reads their
|
|
706
|
+
classes from `dist`, so the kit builds first. `generate` and `build` copy
|
|
707
|
+
the theme as it is, so `preview:css` has to run before either, including
|
|
708
|
+
when they run on their own.
|
|
709
|
+
|
|
710
|
+
4. Load the compiled file as a theme, in the `preview` block:
|
|
711
|
+
|
|
712
|
+
<!-- example: preview-tailwind-theme -->
|
|
713
|
+
```ts
|
|
714
|
+
themes: [
|
|
715
|
+
{ id: 'tailwind', label: 'Tailwind', css: 'preview/tailwind.generated.css' },
|
|
716
|
+
],
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
The `@custom-variant` line pairs with a `root-attribute` option, such as the
|
|
720
|
+
`mode` option above, so a story's `dark:` utilities follow it. The dev server
|
|
721
|
+
reloads the theme when the compiled file changes, so
|
|
722
|
+
`npm run preview:css -- --watch` beside it follows class changes once the kit
|
|
723
|
+
is built again. Ignore the compiled file in `.gitignore`.
|
|
724
|
+
|
|
725
|
+
A kit with `tailwind: { output: 'styles' }` has no Tailwind build: it sets
|
|
726
|
+
`tailwind.darkSelector: '[data-mode="dark"]'` instead, so its converted
|
|
727
|
+
`dark:` styles follow the same option.
|
|
728
|
+
|
|
729
|
+
### A kit whose stylesheets are not linked
|
|
730
|
+
|
|
731
|
+
A kit built with `stylesheetLink: 'none'` emits each component's stylesheet
|
|
732
|
+
and links it from nowhere, so the other targets' panes show its components
|
|
733
|
+
unstyled. The html pane reads the html target's stylesheets itself. Give the
|
|
734
|
+
other targets' panes the same styles as a theme: join the kit's reset and one
|
|
735
|
+
scripted target's built stylesheets into one file, and load that file as
|
|
736
|
+
the theme.
|
|
737
|
+
|
|
738
|
+
```json
|
|
739
|
+
{
|
|
740
|
+
"scripts": {
|
|
741
|
+
"preview:css": "cat src/styles/reset.css dist/react/*.css > preview/components.generated.css",
|
|
742
|
+
"preview": "npm run build && npm run preview:css && scaffold-ui-kit-preview dev",
|
|
743
|
+
"preview:build": "npm run build && npm run preview:css && scaffold-ui-kit-preview build"
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
<!-- example: preview-unlinked-theme -->
|
|
749
|
+
```ts
|
|
750
|
+
themes: [
|
|
751
|
+
{ id: 'default', label: 'Default', css: 'preview/components.generated.css' },
|
|
752
|
+
],
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
The preview copies a theme as it is, so an `@import` in it would not resolve:
|
|
756
|
+
join the files instead. A kit built with `stylingLanguage: 'scss'` compiles
|
|
757
|
+
its sheets with sass into that file instead. A kit using Tailwind's `output: 'styles'` needs a reset
|
|
758
|
+
that works without a Tailwind build (see the Tailwind extension's README):
|
|
759
|
+
Tailwind's own `preflight.css` reads its theme through `--theme()`, which
|
|
760
|
+
only a Tailwind build resolves.
|
|
761
|
+
|
|
762
|
+
## Custom views
|
|
763
|
+
|
|
764
|
+
A `viewsModule` exports views: panels the preview lists beside the component
|
|
765
|
+
stories and mounts in an iframe of their own, under the live theme and
|
|
766
|
+
options, such as a tokens page or an icon gallery. A view is
|
|
767
|
+
`{ id, label, source?, order?, framework?, mount }`. The iframe loads the
|
|
768
|
+
kit's setup module for the view's `framework` (`react`, `vue`, `svelte`, or
|
|
769
|
+
`dom`, the default, which loads none), then calls
|
|
770
|
+
`mount(element, context, updates)`, which renders into `element` and may
|
|
771
|
+
return a function that unmounts it. `context` holds the current option
|
|
772
|
+
values, the kit's sources and component index (the stories a reader
|
|
773
|
+
browses, unlisted ones left out), and `selectComponent(id, props?)`, which
|
|
774
|
+
opens a component's story, an unlisted one included; `updates.subscribe(listener)`
|
|
775
|
+
delivers a new context each time the options change or the catalog arrives,
|
|
776
|
+
and the view stays mounted throughout. `reactView(render)` makes a React view
|
|
777
|
+
from a render function, re-rendering it in place with each context. An entry
|
|
778
|
+
`{ id, label, componentTree: true }` places the source's component tree among
|
|
779
|
+
its views. To open the preview on a view, such as a foundations page, name
|
|
780
|
+
it as the block's `landing` entry: `"landing": { "view": "foundations" }`.
|
|
781
|
+
|
|
782
|
+
<!-- example: preview-views -->
|
|
783
|
+
```tsx
|
|
784
|
+
import { reactView, type PreviewView } from '@scaffold-ui-kit/preview/views';
|
|
785
|
+
|
|
786
|
+
export const views: PreviewView[] = [
|
|
787
|
+
{
|
|
788
|
+
id: 'foundations',
|
|
789
|
+
label: 'Foundations',
|
|
790
|
+
order: 10,
|
|
791
|
+
...reactView((context) => <Foundations theme={String(context.options.theme)} />),
|
|
792
|
+
},
|
|
793
|
+
{
|
|
794
|
+
id: 'swatches',
|
|
795
|
+
label: 'Swatches',
|
|
796
|
+
order: 20,
|
|
797
|
+
mount(element, context, updates) {
|
|
798
|
+
const paint = (current: typeof context) => {
|
|
799
|
+
element.textContent = `theme: ${String(current.options.theme)}`;
|
|
800
|
+
};
|
|
801
|
+
paint(context);
|
|
802
|
+
return updates.subscribe(paint);
|
|
803
|
+
},
|
|
804
|
+
},
|
|
805
|
+
{ id: 'components', label: 'Components', order: 30, componentTree: true },
|
|
806
|
+
];
|
|
807
|
+
|
|
808
|
+
export const viewById = (id: string) => views.find((view) => view.id === id);
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
## A code block of the kit's own
|
|
812
|
+
|
|
813
|
+
The code view shows each target's source of the story in the shell's own
|
|
814
|
+
code block. A kit with a code block of its own, such as a highlighted one in
|
|
815
|
+
the kit's look, names it as `codeBlock`: a module whose default export mounts
|
|
816
|
+
the block, and the framework it mounts with (`dom`, the default, loads no
|
|
817
|
+
framework's setup module). The preview mounts it in an iframe in each code
|
|
818
|
+
pane, as it mounts a custom view, and sends it the target's source, its
|
|
819
|
+
language (`tsx`, `vue`, `svelte` or `html`), or why the target has none,
|
|
820
|
+
each time the story, its props or its content change. Each content also
|
|
821
|
+
carries `wrap`, whether the shell wraps its code panes' long lines (the
|
|
822
|
+
stock shell's `w`), sent again each time it is toggled: the block wraps long
|
|
823
|
+
lines while it is true and scrolls sideways while it is false. A shell with
|
|
824
|
+
no such choice sends `false`. The block shows source, not a story: it mounts
|
|
825
|
+
into an element that fills its iframe, with no `mount` wrapper around
|
|
826
|
+
it, and only the global options apply there, so it follows the page's mode
|
|
827
|
+
and theme while the story options (a box, padding) leave it alone. The
|
|
828
|
+
iframe's page is the block's: its root carries `data-preview-frame="code"`,
|
|
829
|
+
and the kit may style the frame's `html` and `body` by it, for example in
|
|
830
|
+
the block's background so an overscroll bounces over the same color.
|
|
831
|
+
|
|
832
|
+
```css
|
|
833
|
+
html[data-preview-frame='code'] { background: var(--code-background); }
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
<!-- example: preview-code-block-configuration -->
|
|
837
|
+
```ts
|
|
838
|
+
codeBlock: { module: 'preview/code-block.ts' },
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
<!-- example: preview-code-block -->
|
|
842
|
+
```ts
|
|
843
|
+
import type { CodeBlockMount } from '@scaffold-ui-kit/preview/views';
|
|
844
|
+
|
|
845
|
+
const mount: CodeBlockMount = (element, content, updates) => {
|
|
846
|
+
const show = (next: typeof content) => {
|
|
847
|
+
const pre = document.createElement('pre');
|
|
848
|
+
pre.style.whiteSpace = next.wrap ? 'pre-wrap' : 'pre';
|
|
849
|
+
pre.textContent = next.source ?? next.reason ?? '';
|
|
850
|
+
element.replaceChildren(pre);
|
|
851
|
+
};
|
|
852
|
+
show(content);
|
|
853
|
+
return updates.subscribe(show);
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
export default mount;
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
## Links and persistence
|
|
860
|
+
|
|
861
|
+
The preview keeps what is showing in its address, so a link opens it as it
|
|
862
|
+
was. These query parameters are the preview's:
|
|
863
|
+
|
|
864
|
+
- `component=<id>` or `view=<id>` - the selected story or view, a story by
|
|
865
|
+
the id described under the testing helpers below, `components-taginput` for
|
|
866
|
+
a `TagInput` in the default category,
|
|
867
|
+
- `scenario=<id>` - the story's scenario, when it is not the one the story
|
|
868
|
+
starts on,
|
|
869
|
+
- `props=<JSON>` - the props that differ from the scenario's, as one JSON
|
|
870
|
+
object, so a story whose props hold a lot of data keeps a short link. A
|
|
871
|
+
prop the link leaves out takes the scenario's value, the story file's
|
|
872
|
+
`defaultProps` beneath it, so a hand-written link can name just the props
|
|
873
|
+
it changes. A prop of the scenario's removed from the story is written as
|
|
874
|
+
`null`, which a link reads as left out. Picking a scenario or another
|
|
875
|
+
story clears them. The scenario editor's content never goes in the address, apart from
|
|
876
|
+
the root's lone text where it is the story's `children`,
|
|
877
|
+
- `targets=<list>` - the visible targets, comma-separated, `html`
|
|
878
|
+
among them where the kit builds it (`targets=react,svelte,html`), while
|
|
879
|
+
not every one of the kit's is on,
|
|
880
|
+
- `width=<pixels>` - the width the story panes are laid out at, while they
|
|
881
|
+
do not fit the space the shell gives them (`width=768`). A link can name
|
|
882
|
+
any whole number of pixels up to 7680, whether or not the kit's
|
|
883
|
+
`paneWidths` list it,
|
|
884
|
+
- `option.<id>=<value>` - each option set away from its default (`true` or
|
|
885
|
+
`false` for a boolean), a shell's own as `option.shell.<id>`. A story
|
|
886
|
+
option appears only while its story is selected.
|
|
887
|
+
|
|
888
|
+
An address that names no story or view opens on the kit's `landing` entry,
|
|
889
|
+
or on the first story the navigation lists without one.
|
|
890
|
+
|
|
891
|
+
Opening a link reads them all, and what the kit does not know is ignored: an
|
|
892
|
+
unknown story, scenario or target, a value an option cannot hold, and in
|
|
893
|
+
`props` a prop the component does not declare or a value its control does
|
|
894
|
+
not accept (such a value keeps the scenario's).
|
|
895
|
+
|
|
896
|
+
The preview writes the address from the first change a person makes on, in
|
|
897
|
+
place, adding no browser history. A page that is only opened keeps the
|
|
898
|
+
address it was opened with, so a shell's own pages stay as they are linked.
|
|
899
|
+
The preview rewrites only its own parameters and keeps every other query
|
|
900
|
+
parameter, the path and the hash, so a shell may keep state of its own in
|
|
901
|
+
the address, such as a tab or a route. It is all query parameters, so links
|
|
902
|
+
work on the static build too.
|
|
903
|
+
|
|
904
|
+
`persistence` decides what else is remembered. `"url"`, the default, keeps
|
|
905
|
+
everything in the address alone. `"url-and-local-storage"` also remembers the
|
|
906
|
+
global option values for this kit in the browser across sessions, a value in
|
|
907
|
+
the address winning over a remembered one. The selection, the scenario, the
|
|
908
|
+
props, the visible targets and the pane width always live in the address
|
|
909
|
+
alone.
|
|
910
|
+
|
|
911
|
+
## The charcell shell
|
|
912
|
+
|
|
913
|
+
`@scaffold-ui-kit/preview/shells/charcell` draws the preview in
|
|
914
|
+
[charcell](https://www.npmjs.com/package/charcell)'s terminal look:
|
|
915
|
+
|
|
916
|
+
- a top bar with the kit and its version, the kit's own settings (the `kit`
|
|
917
|
+
group), and two groups of toggles: the views, `navigation`, then `scenario`,
|
|
918
|
+
`output`, `code` and `events`, the stage's four rows, and the targets, its
|
|
919
|
+
columns;
|
|
920
|
+
- the navigation, the kit's components and views, one tab stop moved through
|
|
921
|
+
with the arrow keys;
|
|
922
|
+
- a stage bar with a select of everything the navigation lists, with an info button for the component's description,
|
|
923
|
+
its markdown rendered;
|
|
924
|
+
- the stage's views, each under a heading row whose end holds the view's
|
|
925
|
+
options and controls: the scenario row, with the scenario picker and the
|
|
926
|
+
props and content editor; the output row, a preview pane per target, with
|
|
927
|
+
the context the story is shown in (the story's options and the global
|
|
928
|
+
options of the `context` group, or of none) and a `width` select in an
|
|
929
|
+
options popover; and
|
|
930
|
+
the code row, each target's source under its pane, with a `wrap` toggle;
|
|
931
|
+
and the events row, a log of the events the story's panes fire: each call
|
|
932
|
+
of an event the component declares, a function prop that returns nothing,
|
|
933
|
+
such as `onSelect`, with the pane and what it was called with, and a
|
|
934
|
+
`clear`. A function prop that returns something, such as a formatter, is
|
|
935
|
+
left as the story gives it;
|
|
936
|
+
- and a status bar with the panes' state, a link to what the preview shows,
|
|
937
|
+
the shell's keys and any other group of the kit's.
|
|
938
|
+
|
|
939
|
+
A kit that names it installs its two peers beside the preview:
|
|
940
|
+
|
|
941
|
+
```sh
|
|
942
|
+
npm install --save-dev charcell @zag-js/react
|
|
943
|
+
```
|
|
944
|
+
|
|
945
|
+
`check`, `build` and `dev` name either peer the kit has not installed,
|
|
946
|
+
before anything is bundled, whether the kit names the shell as it is or
|
|
947
|
+
through a module of its own that configures it (below).
|
|
948
|
+
|
|
949
|
+
Named as it is, it comes with no content of the kit's. To fill its slots or
|
|
950
|
+
move an option group, the kit names a module of its own as its `shell`, which
|
|
951
|
+
configures it, and the charcell shell's page beside it:
|
|
952
|
+
|
|
953
|
+
<!-- example: preview-shell-configuration -->
|
|
954
|
+
```ts
|
|
955
|
+
shell: {
|
|
956
|
+
module: 'preview/shell.tsx',
|
|
957
|
+
page: '@scaffold-ui-kit/preview/shells/charcell/page',
|
|
958
|
+
},
|
|
959
|
+
```
|
|
960
|
+
|
|
961
|
+
The page is what the host page shows before the shell has loaded: charcell's
|
|
962
|
+
dark ground from the first frame, its face preloaded, and a `loading` note
|
|
963
|
+
set in that face on a whole cell, after a moment. Named as it is, the shell
|
|
964
|
+
brings the page itself; a module of the kit's names it, as above, or starts
|
|
965
|
+
from an empty page, which `check`, `build` and `dev` warn of.
|
|
966
|
+
|
|
967
|
+
<!-- example: preview-charcell-shell -->
|
|
968
|
+
```tsx
|
|
969
|
+
import { charcellShell, reactFill } from '@scaffold-ui-kit/preview/shells/charcell';
|
|
970
|
+
import { usePreview } from '@scaffold-ui-kit/preview/react';
|
|
971
|
+
|
|
972
|
+
export default charcellShell({
|
|
973
|
+
slots: {
|
|
974
|
+
'top-bar-end': reactFill((controller) => {
|
|
975
|
+
const selection = usePreview(controller, (state) => state.selection);
|
|
976
|
+
return <span>{selection.id}</span>;
|
|
977
|
+
}),
|
|
978
|
+
'status-bar': (element, controller) => {
|
|
979
|
+
element.textContent = `${controller.catalog.kitName} ${controller.catalog.kitVersion ?? ''}`;
|
|
980
|
+
},
|
|
981
|
+
},
|
|
982
|
+
groups: { canvas: 'status-bar' },
|
|
983
|
+
});
|
|
984
|
+
```
|
|
985
|
+
|
|
986
|
+
- `slots` - content for the shell's named slots: `top-bar-end`, at the end
|
|
987
|
+
of the top bar after the target toggles; `stage-bar-end`, at the end of the
|
|
988
|
+
stage bar; `scenario-end`, `output-end`, `code-end` and `events-end`, at
|
|
989
|
+
the end of each view's heading row, before the view's own control; and
|
|
990
|
+
`status-bar`, at the end of the status bar.
|
|
991
|
+
A fill is `(element, controller) => unmount`. It mounts into `element` in
|
|
992
|
+
any framework or in plain DOM, reads and drives the preview through
|
|
993
|
+
`controller` (below), and may return a function that unmounts it.
|
|
994
|
+
`reactFill(render)` makes a fill from a React render function, rendered as
|
|
995
|
+
a component of its own, so it can use hooks such as `usePreview`.
|
|
996
|
+
- `groups` - where each option group goes, by group name: `top-bar`,
|
|
997
|
+
`stage-bar`, `scenario`, `output`, `code`, `events` or `status-bar`, for
|
|
998
|
+
options of any scope. Without an entry, a story option and a global option
|
|
999
|
+
of the `context` group (or of none) sit with the output, a global option of
|
|
1000
|
+
the `kit` group in the top bar, and any other group in the status bar.
|
|
1001
|
+
- `popovers` - which views hold their options in an options popover at the
|
|
1002
|
+
end of their heading row rather than in the row itself, such as
|
|
1003
|
+
`{ output: false, code: true }`. The output's are in a popover unless it
|
|
1004
|
+
says otherwise.
|
|
1005
|
+
|
|
1006
|
+
Each view can be hidden and shown, and each target, so a reader keeps the
|
|
1007
|
+
rows and columns they want. The panes stay loaded while views come and go,
|
|
1008
|
+
and the other panes while a target does; a row or column comes back at the
|
|
1009
|
+
size it had. The views are the shell's own
|
|
1010
|
+
options, so the address keeps them (`option.shell.scenario=true`,
|
|
1011
|
+
`option.shell.output=false`, `option.shell.code=true`) and a link opens the
|
|
1012
|
+
story as it was laid out. The output row shows to start with; with no view
|
|
1013
|
+
showing, the stage says how to show one.
|
|
1014
|
+
|
|
1015
|
+
The scenario row starts at its content's height, at most half the stage. Its editor
|
|
1016
|
+
is open while the row shows, where the preview can regenerate: hiding the row
|
|
1017
|
+
closes it and keeps its tree until another story or scenario is picked. In
|
|
1018
|
+
the static build the row holds the picker, with a note that editing a
|
|
1019
|
+
scenario needs the preview's development server.
|
|
1020
|
+
|
|
1021
|
+
The output and code rows sit in a grid that resizes both ways, its columns
|
|
1022
|
+
the visible targets. The columns start at equal widths. A target turned off
|
|
1023
|
+
gives its width to the others in proportion to theirs, so three equal
|
|
1024
|
+
columns become two halves, and 40/20/40 without the last becomes 67/33; a
|
|
1025
|
+
target turned back on returns at the width it had, the others giving way in
|
|
1026
|
+
proportion. The output row starts at the height of its stories,
|
|
1027
|
+
until it is resized. A double-click on a handle, or Enter while it has the
|
|
1028
|
+
focus, puts its split back where it started: the columns at equal widths,
|
|
1029
|
+
the output and code rows at their starting heights. The code row shows, under each pane, the story's code
|
|
1030
|
+
for that target, or its html under the html pane, with a copy button; the
|
|
1031
|
+
`wrap` toggle wraps every code pane's long lines, on to start with and kept
|
|
1032
|
+
in the address while off (`option.shell.wrap=false`).
|
|
1033
|
+
|
|
1034
|
+
The `width` select, the last of the output's options, fits each pane to
|
|
1035
|
+
its column (`fit`, to start with), or lays every story pane out at one of
|
|
1036
|
+
the kit's pane widths, such as `medium 768`, each pane as tall as its
|
|
1037
|
+
story. While that width is wider
|
|
1038
|
+
than a column would be, the output stacks: a row per target, each the
|
|
1039
|
+
stage's full width, with the code panes side by side in the code row under
|
|
1040
|
+
them. Each row starts at its story's height, and the space left under the
|
|
1041
|
+
stories goes to the code row, or to the last row without one. When the
|
|
1042
|
+
stories are too tall to fit together, the rows share the stage equally,
|
|
1043
|
+
and a story shorter than its share keeps its own height. A row keeps at
|
|
1044
|
+
least ten rows of text, its frame and about seven rows of story, or its
|
|
1045
|
+
whole story when that takes fewer; the code row keeps eight. When those
|
|
1046
|
+
don't fit on the stage together, as with several tall stories and the code
|
|
1047
|
+
row on a short window, the stage scrolls down instead of squeezing a row.
|
|
1048
|
+
The rows follow the stories and the stage until you drag a handle between
|
|
1049
|
+
them or step it with a key, and again after a double-click or Enter on one
|
|
1050
|
+
of those handles. A story
|
|
1051
|
+
taller than its row scrolls down, and one wider than its row scrolls
|
|
1052
|
+
across, with a scrollbar on a row of its own, so it never covers the
|
|
1053
|
+
story; one narrower than its column is centered in it.
|
|
1054
|
+
|
|
1055
|
+
The shell's CSS custom properties are its styling surface; its class names
|
|
1056
|
+
and markup are its own.
|
|
1057
|
+
|
|
1058
|
+
## Writing a shell
|
|
1059
|
+
|
|
1060
|
+
A shell is the whole interface around the panes: the navigation, the option
|
|
1061
|
+
controls, the scenario picker, the layout and the scenario editor. The
|
|
1062
|
+
preview provides the behavior as a controller, and the shell renders it. It
|
|
1063
|
+
decides which options show and as which controls, and where the panes go. The
|
|
1064
|
+
preview owns the iframes: a shell hands it an element to mount a pane in and
|
|
1065
|
+
never talks to a pane itself.
|
|
1066
|
+
|
|
1067
|
+
A shell module default-exports a shell made with `defineShell` from
|
|
1068
|
+
`@scaffold-ui-kit/preview/shell`. That entry also exports everything a shell
|
|
1069
|
+
reads: the controller's types, the catalog's, the editor tree's and the
|
|
1070
|
+
option helpers.
|
|
1071
|
+
|
|
1072
|
+
<!-- example: preview-define-shell -->
|
|
1073
|
+
```ts
|
|
1074
|
+
import { defineShell } from '@scaffold-ui-kit/preview/shell';
|
|
1075
|
+
|
|
1076
|
+
export default defineShell({
|
|
1077
|
+
apiVersion: 1,
|
|
1078
|
+
options: [{ id: 'dense', label: 'dense', default: false }],
|
|
1079
|
+
mount(root, controller) {
|
|
1080
|
+
// render into root, read and drive controller
|
|
1081
|
+
return () => root.replaceChildren();
|
|
1082
|
+
},
|
|
1083
|
+
});
|
|
1084
|
+
```
|
|
1085
|
+
|
|
1086
|
+
- `apiVersion` - the contract version the shell was written against, `1`.
|
|
1087
|
+
The preview refuses a shell of a version it does not serve, naming both.
|
|
1088
|
+
- `options` - the shell's own options, such as its density, in the shape of a
|
|
1089
|
+
kit option without `apply` or `scope`. The preview keeps their values with
|
|
1090
|
+
the kit's, under `shell.<id>` in the state and the address
|
|
1091
|
+
(`shellOptionId('dense')` gives `'shell.dense'`), and never sends them to
|
|
1092
|
+
the panes. They are global. `options` may also be a function of the
|
|
1093
|
+
catalog, called once before the preview starts, for an option that depends
|
|
1094
|
+
on the kit, such as a choice among its targets:
|
|
1095
|
+
`options: (catalog) => [{ id: 'pane', label: 'pane', values:
|
|
1096
|
+
catalog.targets.map((target) => ({ value: target, label: target })),
|
|
1097
|
+
default: catalog.targets[0] }]`.
|
|
1098
|
+
- `mount(root, controller)` - renders the shell into the host page's root
|
|
1099
|
+
element, which the preview empties first, and may return a function that
|
|
1100
|
+
unmounts it.
|
|
1101
|
+
|
|
1102
|
+
The host page provides little: a `<title>`, a viewport `<meta>` for small
|
|
1103
|
+
screens, the root element and a script that mounts the shell. It has no CSS
|
|
1104
|
+
of its own, so the body keeps the browser's default margin; from the root
|
|
1105
|
+
element on, the document is the shell's to style and lay out.
|
|
1106
|
+
|
|
1107
|
+
### The page before the shell mounts
|
|
1108
|
+
|
|
1109
|
+
The shell's script takes a moment to load, on the development server a few
|
|
1110
|
+
seconds. What the host page holds until then is the shell's too, given by a
|
|
1111
|
+
page module: a module evaluated in Node as the preview starts or builds,
|
|
1112
|
+
whose default export is a page made with `defineShellPage` from
|
|
1113
|
+
`@scaffold-ui-kit/preview/shell`.
|
|
1114
|
+
|
|
1115
|
+
<!-- example: preview-shell-page -->
|
|
1116
|
+
```ts
|
|
1117
|
+
import { fileURLToPath } from 'node:url';
|
|
1118
|
+
import { defineShellPage } from '@scaffold-ui-kit/preview/shell';
|
|
1119
|
+
|
|
1120
|
+
const font = fileURLToPath(new URL('./fonts/face.woff2', import.meta.url));
|
|
1121
|
+
|
|
1122
|
+
export default defineShellPage({
|
|
1123
|
+
head: ({ asset }) =>
|
|
1124
|
+
`<link rel="preload" as="font" type="font/woff2" crossorigin href="${asset(font)}" />
|
|
1125
|
+
<style>html { background: #101010; }</style>`,
|
|
1126
|
+
root: '<p role="status">loading</p>',
|
|
1127
|
+
});
|
|
1128
|
+
```
|
|
1129
|
+
|
|
1130
|
+
- `head` - markup written at the end of the page's `<head>`: styles, a font
|
|
1131
|
+
face, links that preload what the shell will need. It stays for the life
|
|
1132
|
+
of the page.
|
|
1133
|
+
- `root` - markup written into the root element, shown until the shell
|
|
1134
|
+
mounts: a loading note, with the styles only it needs. Mounting empties
|
|
1135
|
+
the root, so nothing of it is left under the shell.
|
|
1136
|
+
|
|
1137
|
+
Each is markup, or a function that returns it, which may be asynchronous.
|
|
1138
|
+
A function is given `asset(file)`: the address of a file the markup
|
|
1139
|
+
references, by its absolute path. The development server serves the file
|
|
1140
|
+
from where it is, and the static build copies it into its output, so write
|
|
1141
|
+
the address `asset` gives into an `href`, a `src` or a CSS `url()` rather
|
|
1142
|
+
than a path of your own. The module runs in Node, so it reads files and
|
|
1143
|
+
resolves packages freely, and holds nothing that needs a browser. It is
|
|
1144
|
+
loaded once, when the preview starts: restart `dev` after editing it.
|
|
1145
|
+
|
|
1146
|
+
The preview finds the page two ways. A shell named as a package specifier
|
|
1147
|
+
brings its own: for `shell: '@acme/shells/compact'` the preview uses the
|
|
1148
|
+
module at `@acme/shells/compact/page` when the package exports one, under
|
|
1149
|
+
the `default` condition as the shell itself is. And a
|
|
1150
|
+
kit names one in the long form of `shell`, as a package specifier or a
|
|
1151
|
+
module path relative to the kit root, which is how a shell in the kit gets
|
|
1152
|
+
a page:
|
|
1153
|
+
|
|
1154
|
+
<!-- example: preview-shell-page-configuration -->
|
|
1155
|
+
```ts
|
|
1156
|
+
shell: { module: 'preview/shell.ts', page: 'preview/page.ts' },
|
|
1157
|
+
```
|
|
1158
|
+
|
|
1159
|
+
A shell with no page starts from an empty root. `check` loads the page as
|
|
1160
|
+
`dev` and `build` do, and reports one that cannot be loaded or is no page.
|
|
1161
|
+
|
|
1162
|
+
A shell takes its own settings through a factory of its own, as
|
|
1163
|
+
`charcellShell({ slots, groups })` does. The `preview` block reaches the
|
|
1164
|
+
panes as JSON data, so it is no place for them.
|
|
1165
|
+
|
|
1166
|
+
### The controller
|
|
1167
|
+
|
|
1168
|
+
`controller.catalog` is what stays put for the session:
|
|
1169
|
+
|
|
1170
|
+
- `kitName`, `kitVersion`, and `targets`, the kit's `targets`, in order:
|
|
1171
|
+
one pane and one toggle each. `html` comes last where the kit builds it.
|
|
1172
|
+
The shell entry types them as `PreviewTarget` (`PREVIEW_TARGETS`), and the
|
|
1173
|
+
targets other than `html` as `PreviewScriptedTarget`
|
|
1174
|
+
(`PREVIEW_SCRIPTED_TARGETS`).
|
|
1175
|
+
- `navigation` - the tree of stories and views to list, as `view`,
|
|
1176
|
+
`component` and `group` entries, the components that are not standalone
|
|
1177
|
+
and unlisted stories left out.
|
|
1178
|
+
- `components` - every component, `standalone` (`false` for a compound's
|
|
1179
|
+
parts and the others with no story of their own) and
|
|
1180
|
+
stories kept out of what a reader browses marked `unlisted`, with
|
|
1181
|
+
its `title`, `shortTitle`, scenarios, props, slots and `targets`, and its
|
|
1182
|
+
`description` from the template when it has one, as each scenario has its
|
|
1183
|
+
story file's. A scenario's `options` are the option
|
|
1184
|
+
values picking it sets, by option id. Each
|
|
1185
|
+
entry of `propTypes` is one prop: its control (`control`, `options`,
|
|
1186
|
+
`number`, and `include`, `false` for a prop shown no control), its
|
|
1187
|
+
`description`, and what the component declares: `declaredType`, the
|
|
1188
|
+
type as written (`"'solid' | 'ghost'"`), `required`, and
|
|
1189
|
+
`declaredDefault`, the component's own default, absent when it has none,
|
|
1190
|
+
and `documentedDefault`, a default the component applies without declaring
|
|
1191
|
+
it, as the code a consumer reads (`"'start'"`). The component's, each
|
|
1192
|
+
scenario's and each prop's `description`, and an option's `title`, are
|
|
1193
|
+
markdown, handed out as written for a shell to render; the stock shell
|
|
1194
|
+
renders them, showing raw html in them as text.
|
|
1195
|
+
`defaultProps` is where the story starts, which can differ: it adds the
|
|
1196
|
+
story file's `defaultProps` and the kit's hints over the declared defaults.
|
|
1197
|
+
`events` names the props that are events, functions that return nothing
|
|
1198
|
+
such as `onSelect`, which the panes record. `bindings` lists the
|
|
1199
|
+
component's two-way props, those whose template declares a `bindable`
|
|
1200
|
+
object, in prop order: each as `{ prop, defaultProp, changeProp }`, the
|
|
1201
|
+
latter two absent when the template names none, with `vueModel: true` on
|
|
1202
|
+
the one a Vue consumer binds with a bare `v-model`. When the story gives
|
|
1203
|
+
`prop` and a pane reports `changeProp`, the controller sets the reported
|
|
1204
|
+
value as the story's `prop`, as `setProps` would: every pane, the html and
|
|
1205
|
+
the address follow, and the event is logged as any other. A story that
|
|
1206
|
+
leaves `prop` out lets each pane's component keep its own value.
|
|
1207
|
+
- `views`, `sources`, `htmlTags` (the tags an editor offers for a wrapper) and
|
|
1208
|
+
`colorTokens` (the values a color control offers).
|
|
1209
|
+
- `paneWidths` - the widths to offer for laying the story panes out at, as
|
|
1210
|
+
`{ label, width }` in CSS pixels, narrowest first: the `preview` block's
|
|
1211
|
+
`paneWidths`, else `DEFAULT_PANE_WIDTHS` (`small` 375,
|
|
1212
|
+
`medium` 768, `large` 1280) from the shell entry.
|
|
1213
|
+
- `landing` - the kit's `landing` entry, as `{ kind: 'view', id }` or
|
|
1214
|
+
`{ kind: 'component', id, scenario? }`, absent without one. The controller
|
|
1215
|
+
starts on it when the address names no story or view.
|
|
1216
|
+
|
|
1217
|
+
`controller.options` lists the options to show controls for, the kit's and
|
|
1218
|
+
then the shell's, each with an `origin` of `kit` or `shell`.
|
|
1219
|
+
`optionKind(option)` gives `choice` or `boolean`, and `optionGroup(option)`
|
|
1220
|
+
the group, `context` (`CONTEXT_OPTION_GROUP`) when none is set; the kit's own
|
|
1221
|
+
settings are `kit` (`KIT_OPTION_GROUP`).
|
|
1222
|
+
|
|
1223
|
+
`controller.getState()` is the current state, and
|
|
1224
|
+
`controller.subscribe(listener)` calls the listener with each new one and
|
|
1225
|
+
returns a function that stops it. A state is plain data, replaced on each
|
|
1226
|
+
change with its unchanged fields kept by reference:
|
|
1227
|
+
|
|
1228
|
+
- `selection` - `{ kind: 'component' | 'view', id }`.
|
|
1229
|
+
- `scenario` - the selected scenario's id, `''` for the default.
|
|
1230
|
+
- `props` - the story's root props the panes show: the scenario's over the
|
|
1231
|
+
component's defaults, then whatever `setProps` or the editor's root set.
|
|
1232
|
+
The editor holds a string `children` the story draws its default slot from
|
|
1233
|
+
as the root's text, a text node in that slot, so it can be edited, wrapped
|
|
1234
|
+
or replaced like any node; while the editor holds a tree, `children` is
|
|
1235
|
+
that text when the default slot holds one text node and nothing else, and
|
|
1236
|
+
absent otherwise, as other content there is what the panes show in its
|
|
1237
|
+
place. A story with default-slot content of its own (from its story file)
|
|
1238
|
+
shows that content and not `children`, so its props carry no `children`
|
|
1239
|
+
while the editor holds a tree.
|
|
1240
|
+
- `options` - every option's value, by id.
|
|
1241
|
+
- `unavailableOptions` - the options whose controls to hide: those the
|
|
1242
|
+
selected story marks unavailable, and every story option while a view is
|
|
1243
|
+
selected.
|
|
1244
|
+
- `targets` - `supported` (the selected story's), `visible` (turned on)
|
|
1245
|
+
and `panes`, the ones to mount, in order. Where the kit builds html, every
|
|
1246
|
+
story supports `html`; a story with no markup has its html pane say why.
|
|
1247
|
+
- `paneWidth` - the width in CSS pixels to lay each of the selection's story
|
|
1248
|
+
panes out at, html's among them, so a component that responds to its
|
|
1249
|
+
width shows as it does on a screen that wide: usually one of
|
|
1250
|
+
`catalog.paneWidths`, though a link can name any. Absent while the panes
|
|
1251
|
+
fit the shell's space. The shell sizes the iframe to it and decides how a
|
|
1252
|
+
pane narrower than that shows, scrolling across it or scaling it down. It
|
|
1253
|
+
outlasts the story; a view's pane does not follow it.
|
|
1254
|
+
- `frames` - the readiness of the selection's panes, `loading` or `ready`,
|
|
1255
|
+
by target (`html` among them), and of a view's pane under `view`.
|
|
1256
|
+
- `storyFrames` - the readiness of each detached story pane (below), by the
|
|
1257
|
+
key `storyFrameKey(target, story)` gives.
|
|
1258
|
+
- `frameSizes` and `storyFrameSizes` - the size of what each pane shows,
|
|
1259
|
+
`{ width, height }` in whole CSS pixels, beside `frames` and `storyFrames`
|
|
1260
|
+
and under the same keys: the story or view with the room the kit's canvas
|
|
1261
|
+
puts around it, as the pane last reported it. An entry appears once the
|
|
1262
|
+
pane renders, changes whenever its content does, keeps its last size while
|
|
1263
|
+
the pane reloads, and goes when the pane is unmounted. The width is the
|
|
1264
|
+
content's at the pane's current width, since block content fills the pane.
|
|
1265
|
+
- `frameErrors` - what each of the selection's panes threw as its story
|
|
1266
|
+
rendered, the error's message by target, which the pane also shows in the
|
|
1267
|
+
story's place. An entry goes when the pane mounts a story that renders,
|
|
1268
|
+
and when the pane is unmounted. The stock shell's status bar names the
|
|
1269
|
+
targets (`render error · react`), their messages in a hint over them.
|
|
1270
|
+
- `events` - the events the selection's panes recorded, oldest first and the
|
|
1271
|
+
last hundred at most: each call a story made of one of its events (a
|
|
1272
|
+
component's `events` in the catalog), as `{ id, target, name, payload,
|
|
1273
|
+
time }`. `id` counts the events recorded, for a stable key; `target` is
|
|
1274
|
+
the pane it was recorded in; `name` is the function prop called;
|
|
1275
|
+
`payload` is a one-line summary of what it was called with; `time` is in
|
|
1276
|
+
milliseconds since the epoch. The log empties when another story or a
|
|
1277
|
+
view is selected, and on `actions.clearEvents()`.
|
|
1278
|
+
- `editor` - whether the editor is `open`, its `tree`, and `modified`: whether
|
|
1279
|
+
the tree's content differs from what it was seeded with, a node added,
|
|
1280
|
+
removed, moved or replaced, or a nested node's props or a text changed.
|
|
1281
|
+
The root's props do not count, as they are `props`; a change of the root's
|
|
1282
|
+
text counts, though `props` shows it as `children`. Content is compared by
|
|
1283
|
+
value, so an edit undone by hand is no change, and a new seed (another
|
|
1284
|
+
story, or another scenario with the editor open) starts unmodified. While
|
|
1285
|
+
it is `true`, the address may not reproduce the panes.
|
|
1286
|
+
- `regeneration` - the editor's regeneration, `idle`, `pending` or `error` with a
|
|
1287
|
+
`message`.
|
|
1288
|
+
- `code` - the code of what the selection's panes show (below), absent
|
|
1289
|
+
while a view is selected.
|
|
1290
|
+
|
|
1291
|
+
`controller.actions` change it:
|
|
1292
|
+
|
|
1293
|
+
- `select(selection)` shows what a navigation entry names, a story or a
|
|
1294
|
+
view; `selectComponent(id, props?)` and `selectView(id)` do the same for
|
|
1295
|
+
one kind.
|
|
1296
|
+
- `clearEvents()` empties `events`.
|
|
1297
|
+
- `setPaneWidth(width)` lays the story panes out at `width` pixels, and
|
|
1298
|
+
`setPaneWidth(undefined)` fits them again. A width that is not a whole
|
|
1299
|
+
number from 1 to 7680 (`MAXIMUM_PANE_WIDTH`) is ignored.
|
|
1300
|
+
- `pickScenario(id)`, `setOption(id, value)` and
|
|
1301
|
+
`setTargetVisible(target, visible)`. Picking a scenario, or selecting
|
|
1302
|
+
a story on its baseline scenario, sets the option values the scenario
|
|
1303
|
+
carries (`scenario.options`) as `setOption` would, so the panes, the
|
|
1304
|
+
controls and the address follow. Values it does not set stay as they are,
|
|
1305
|
+
and picking another scenario leaves them, apart from those the next one
|
|
1306
|
+
sets. A shell option's value never reaches the panes, so setting one posts
|
|
1307
|
+
nothing to them.
|
|
1308
|
+
- `setProps(props)` replaces the story's root props, live, with no
|
|
1309
|
+
regeneration, whether or not the editor is open; the editor's root takes
|
|
1310
|
+
them too. It does not merge: pass `{ ...state.props, tone: 'accent' }` to
|
|
1311
|
+
change one. A prop set to `undefined` is dropped, and it does nothing while
|
|
1312
|
+
a view is selected. `selectComponent(id, props)` lands its props the same
|
|
1313
|
+
way, over the story's defaults. While the editor holds a tree, `children`
|
|
1314
|
+
is the root's text, as `props` shows it: a new string replaces the root's
|
|
1315
|
+
default-slot content with that text, and leaving `children` out removes a
|
|
1316
|
+
lone text. Either is an edit of the tree's content, which regenerates the
|
|
1317
|
+
story, so a shell passes `{ ...state.props, ... }` to keep the text. A
|
|
1318
|
+
story with default-slot content of its own takes no `children` then.
|
|
1319
|
+
- `scenarioChoices(component,
|
|
1320
|
+
controller.capabilities)`, from the shell entry, lists a scenario picker's
|
|
1321
|
+
choices, the default first, and none when there is no choice to make. It
|
|
1322
|
+
leaves out the Empty scenario where the preview cannot regenerate, as in
|
|
1323
|
+
the static build, and `pickScenario` ignores Empty there.
|
|
1324
|
+
|
|
1325
|
+
`controller.frames` places the panes:
|
|
1326
|
+
|
|
1327
|
+
- `mount(element, target)` puts a target's pane in `element` and
|
|
1328
|
+
`mountView(element, viewId)` a view's. Each returns a function that removes
|
|
1329
|
+
it. The controller loads the iframe, reloads it after each regeneration and
|
|
1330
|
+
answers its messages.
|
|
1331
|
+
- `mount(element, 'html')` puts the html pane in `element`. It shows the
|
|
1332
|
+
markup of `code.html` (below) as the story, its props or the editor's
|
|
1333
|
+
tree change, keeps what it shows while the next is printed, and shows the
|
|
1334
|
+
reason when there is none. It loads the kit's theme, canvas and
|
|
1335
|
+
`mount` frame and follows the options as the other panes do,
|
|
1336
|
+
but loads no framework and no setup module, and runs none of the story's
|
|
1337
|
+
behavior. A regeneration does not reload it.
|
|
1338
|
+
- A pane is a bare `<iframe>` appended to `element`, with no size or styling
|
|
1339
|
+
of its own beyond the browser's defaults. The shell sizes it, for example
|
|
1340
|
+
`element > iframe { width: 100%; height: 100%; border: 0 }` inside an
|
|
1341
|
+
element that has a size.
|
|
1342
|
+
- To size a pane to its story instead, as a documentation page or a grid of
|
|
1343
|
+
variants does, set the iframe's height to the `height` in `frameSizes` (or
|
|
1344
|
+
`storyFrameSizes`), with a fallback until the first report. The size does
|
|
1345
|
+
not depend on the iframe's own height, so a pane sized to it stays put: a
|
|
1346
|
+
canvas that fills the viewport (`min-height: 100vh`) adds only its padding.
|
|
1347
|
+
The pane measures itself and posts the size to the host, so a shell never
|
|
1348
|
+
reaches into the iframe.
|
|
1349
|
+
- The pane's readiness in `frames` changes after `mount` returns, never
|
|
1350
|
+
during the call, so a subscriber does not run inside the shell's own
|
|
1351
|
+
mounting code.
|
|
1352
|
+
- `reload(target)` loads the selection's pane for `target` (or a view's
|
|
1353
|
+
pane, as `'view'`) again: its readiness goes back to `loading` until it
|
|
1354
|
+
answers, and it shows the selection as it is now.
|
|
1355
|
+
- `standaloneUrl(target)` gives the address of the selection's pane for a
|
|
1356
|
+
scripted target opened on its own, outside the preview, for a shell's
|
|
1357
|
+
"open in a window" action: the story it shows now (the editor's content
|
|
1358
|
+
while it holds a tree), with its props and the kit's option values. The
|
|
1359
|
+
page follows nothing after it opens.
|
|
1360
|
+
- Every iframe the controller mounts (a pane, a view, a detached story, a
|
|
1361
|
+
code block) stays hidden until it has painted its first content, and again
|
|
1362
|
+
while it reloads, so the shell's own ground shows rather than the
|
|
1363
|
+
browser's blank page and what the pane paints on the way. Once shown, the
|
|
1364
|
+
iframe carries `data-preview-shown` (`PREVIEW_FRAME_SHOWN_ATTRIBUTE`), so a
|
|
1365
|
+
shell's CSS can draw a loading note in the pane until then:
|
|
1366
|
+
`.pane:has(> iframe:not([data-preview-shown]))::after { content: 'loading' }`.
|
|
1367
|
+
The stock shell does, after 300ms.
|
|
1368
|
+
- `mountStory(element, target, { component, scenario?, props? })` puts a
|
|
1369
|
+
read-only pane of any story in `element`, apart from the selection, for a
|
|
1370
|
+
grid of variants or a documentation page that lists every scenario live.
|
|
1371
|
+
Its props are the scenario's over the component's defaults, with `props`
|
|
1372
|
+
over those; `scenario` omitted is the one the story starts on, and `''`
|
|
1373
|
+
the anonymous default. It follows the global options, and its story
|
|
1374
|
+
options are the component's own, as selecting the story would seed them;
|
|
1375
|
+
the option values its scenario sets stay over both.
|
|
1376
|
+
It never shows the editor's content and is not reloaded by a
|
|
1377
|
+
regeneration. Its readiness is reported in `storyFrames` under
|
|
1378
|
+
`storyFrameKey(target, story)`, after the call returns, never in
|
|
1379
|
+
`frames`, which stays the selection's. It throws for a component that is
|
|
1380
|
+
unknown or not standalone, a target the component does not render in
|
|
1381
|
+
(`html` among them: a detached pane renders in a scripted target), and an
|
|
1382
|
+
unknown
|
|
1383
|
+
scenario or the Empty one, which renders through the selection alone; list
|
|
1384
|
+
`component.scenarios.filter((scenario) => !scenario.empty)`.
|
|
1385
|
+
- `mountCode(element, target)` puts the kit's own code block (see "A code
|
|
1386
|
+
block of the kit's own") in `element`, showing `target`'s source of the
|
|
1387
|
+
selection. `wrapCode(wrap)` tells every code block whether the shell wraps
|
|
1388
|
+
its code panes' long lines; a shell with that choice calls it as the
|
|
1389
|
+
choice changes, and the blocks see `false` until it does.
|
|
1390
|
+
|
|
1391
|
+
`controller.editor` owns the scenario editor's tree and its operations; drag
|
|
1392
|
+
and drop, if any, is the shell's:
|
|
1393
|
+
|
|
1394
|
+
- `open()` seeds the tree from the selected story, and `close()` hides it.
|
|
1395
|
+
The content beside the root is not the tree's: a shell reads it from the
|
|
1396
|
+
catalog's component (`around`) and shows it as locked, no operation
|
|
1397
|
+
changes it, and every regeneration renders it as the story gives it.
|
|
1398
|
+
- `createNode(request)` makes a node to insert. The request is
|
|
1399
|
+
`{ kind: 'text', value }`, `{ kind: 'html', tag }`, or
|
|
1400
|
+
`{ kind: 'component', name, importAs? }`, `name` being the component's
|
|
1401
|
+
export name and `importAs` what it comes in with: nothing (`'empty'`, the
|
|
1402
|
+
default), its defaults and content (`'default'`), or a scenario's props
|
|
1403
|
+
(`{ scenario }`). The node it returns is the tree's own: a text node
|
|
1404
|
+
`{ kind: 'text', value }`, an HTML node `{ kind: 'html', tag, props, slots }`,
|
|
1405
|
+
or a component's `{ kind: 'element', component, props, slots }`, its
|
|
1406
|
+
`component` the export name.
|
|
1407
|
+
- `insert({ parent, slot, index? }, node)`, `replace(path, node)`,
|
|
1408
|
+
`remove(path)`, `setNodeProps(path, props)` and `setText(path, text)`. A
|
|
1409
|
+
path is a list of steps from the root, each `{ slot, index }`: the slot a
|
|
1410
|
+
node is in and its index there. The empty path `[]` addresses the root, and
|
|
1411
|
+
a parent of `[]` inserts into the root's own slots.
|
|
1412
|
+
- `setNodeProps(path, props)` replaces the node's props with `props`; it
|
|
1413
|
+
does not merge, so pass the props to keep along with the one that changed.
|
|
1414
|
+
For the root, `[]`, it does what `actions.setProps(props)` does, except
|
|
1415
|
+
that it leaves the root's content, and so `children`, as it is: the tree's
|
|
1416
|
+
root props never hold `children`, so a root prop control passes the root's
|
|
1417
|
+
`props` from the tree.
|
|
1418
|
+
- `move(from, to)` moves a node to a drop target, its index counted before
|
|
1419
|
+
the node is taken out, as a drop reports it. `moveBy(path, offset)` moves
|
|
1420
|
+
it within its slot, `-1` up and `1` down, stopping at either end, for up
|
|
1421
|
+
and down buttons. `resolveDrop(drop)` turns what a sortable list reports
|
|
1422
|
+
into the `from` and `to` of a move.
|
|
1423
|
+
- `nodeSlots(catalog, node)`, from the shell entry, lists the slots a node
|
|
1424
|
+
holds content in, the default first: a component's as the catalog lists
|
|
1425
|
+
them, an HTML tag's default slot, none for text. The tree itself stands for
|
|
1426
|
+
its root. `childPath(parent, slot, index)` gives a child's path,
|
|
1427
|
+
`childPath([], 'default', 0)` the root's first child, and `nodeAt(slots,
|
|
1428
|
+
path)` the node at a path, or `null` where there is none (the root is the
|
|
1429
|
+
tree itself, so `nodeAt` of `[]` is `null` too).
|
|
1430
|
+
|
|
1431
|
+
`controller.capabilities.regeneration` is `false` in the static build, which cannot
|
|
1432
|
+
regenerate a story; a shell hides its editor then.
|
|
1433
|
+
|
|
1434
|
+
### A story's code
|
|
1435
|
+
|
|
1436
|
+
`state.code` holds the code of what the selection's panes show, for a shell
|
|
1437
|
+
to put beside them. It follows the story, its scenario, `props` and, while the
|
|
1438
|
+
editor holds a tree, the tree:
|
|
1439
|
+
|
|
1440
|
+
- `targets` - the code a consumer writes, by target, for each target other
|
|
1441
|
+
than html the story renders in (`targets.supported`): a React component, a
|
|
1442
|
+
Vue single-file component, a Svelte component. It imports each component
|
|
1443
|
+
from the kit's package entry for the target, writes the props the panes show,
|
|
1444
|
+
leaving out a prop that holds the component's declared default or a value
|
|
1445
|
+
the preview starts from on its own, and the slot content, leaving out the
|
|
1446
|
+
component's name the panes show in an unfilled default slot. A two-way prop
|
|
1447
|
+
the story gives is bound to state of the code's own, starting at the
|
|
1448
|
+
story's value, as a consumer controls it:
|
|
1449
|
+
|
|
1450
|
+
| Target | The code holds the value in | and binds it with |
|
|
1451
|
+
|---|---|---|
|
|
1452
|
+
| React | `const [open, setOpen] = useState(true);` | `open={open} onOpenChange={setOpen}` |
|
|
1453
|
+
| Vue | `const open = ref(true);` | `v-model:open="open"`, or `v-model="open"` for the prop the component names its model |
|
|
1454
|
+
| Svelte | `let open = $state(true);` in runes mode, `let open = true;` in legacy mode, as the kit builds Svelte | `bind:open` |
|
|
1455
|
+
|
|
1456
|
+
The state takes the prop's declared type where the value alone would
|
|
1457
|
+
read narrower, such as a union of strings. Where the declared type names
|
|
1458
|
+
types the kit's package entry exports, such as an interface a template
|
|
1459
|
+
declares with `export` in its `imports`, the state takes it as declared and
|
|
1460
|
+
the code imports them from the entry (`useState<DateInputValue>` beside
|
|
1461
|
+
`import { DateInput, type DateInputValue }`). Where it names other types, or
|
|
1462
|
+
where nothing is declared and the value is an array, an object or `null`,
|
|
1463
|
+
the state takes the component's own prop type, `ComponentProps<typeof
|
|
1464
|
+
Disclosure>['open']`; for a component with type parameters the value infers
|
|
1465
|
+
it. React binds a prop only when the component has a change prop to report
|
|
1466
|
+
it through; without one it prints the value. A story that leaves the prop
|
|
1467
|
+
out prints none of this, and neither does one whose value equals the prop's
|
|
1468
|
+
declared default, which the code leaves out as it does any default. A story
|
|
1469
|
+
that gives the prop leaves out the prop the component's own value starts
|
|
1470
|
+
from (`defaultOpen` beside `open`), which does nothing once the prop is
|
|
1471
|
+
given. A prop the story gives `null` is left out, unless it is a two-way
|
|
1472
|
+
prop the code binds; its state then starts at `null`, which type-checks
|
|
1473
|
+
where the template declares the prop with `null` (`type: 'string | null'`).
|
|
1474
|
+
Props print in the order the component declares them. The html is the markup
|
|
1475
|
+
at the story's value either way. It is printed in the page as the state
|
|
1476
|
+
changes, so it is always there for a story.
|
|
1477
|
+
- `html` - the markup the story renders to, at rest, as `storySource` prints
|
|
1478
|
+
it (see Using it from code): `{ status: 'ready', source, stylesheet?,
|
|
1479
|
+
warnings? }`, where `stylesheet` holds the styles the kit's build wrote for
|
|
1480
|
+
the components the markup renders, which the html pane shows it under, and
|
|
1481
|
+
`warnings` holds the evaluation warnings for the expressions the markup
|
|
1482
|
+
left out because they cannot be evaluated at rest, those one component
|
|
1483
|
+
gives for one reason as a single warning counting them, which the html
|
|
1484
|
+
pane counts in its legend and the html code pane beside its note, each
|
|
1485
|
+
count a button opening the warnings in a popover,
|
|
1486
|
+
`{ status: 'loading' }` while it is being printed, or
|
|
1487
|
+
`{ status: 'unavailable', reason }`, such as for a story holding a part
|
|
1488
|
+
that has no markup. The markup needs the kit's templates, so the page cannot
|
|
1489
|
+
print it: the development server prints it on request, once a change
|
|
1490
|
+
pauses, and drops a request the next change replaces. The static build
|
|
1491
|
+
prints the html of each scenario when it builds; for changed props or
|
|
1492
|
+
edited content it has none, and `html` says so.
|
|
1493
|
+
|
|
1494
|
+
`printStorySource(catalog, story)`, from the shell entry, prints any story's
|
|
1495
|
+
source from the catalog, for the story reference
|
|
1496
|
+
`frames.mountStory` takes, `{ component, scenario?, props? }`, so a shell can
|
|
1497
|
+
show the code beside a detached pane. It returns the source by target, for
|
|
1498
|
+
the targets other than html the component renders in, and throws for an unknown component
|
|
1499
|
+
or scenario. The html of a detached pane's story is not printed.
|
|
1500
|
+
|
|
1501
|
+
<!-- example: preview-print-story-source -->
|
|
1502
|
+
```ts
|
|
1503
|
+
import { printStorySource } from '@scaffold-ui-kit/preview/shell';
|
|
1504
|
+
|
|
1505
|
+
const { react } = printStorySource(controller.catalog, { component: 'components-button', scenario: 'ghost' });
|
|
1506
|
+
```
|
|
1507
|
+
|
|
1508
|
+
The catalog carries what the printing reads: `imports`, how a consumer
|
|
1509
|
+
imports each of the kit's components (`{ exportName, modules }`, the module by
|
|
1510
|
+
target), `svelteMode`, the mode the kit builds its Svelte target in
|
|
1511
|
+
(`legacy` or `runes`, the kit's `svelte.mode`), which the Svelte code holds a
|
|
1512
|
+
bound prop's state in, and on each component `previewDefaults`, the values
|
|
1513
|
+
the preview starts a prop from that the component does not declare, which
|
|
1514
|
+
the code leaves out, and `bindings`, its two-way props.
|
|
1515
|
+
|
|
1516
|
+
### Frameworks
|
|
1517
|
+
|
|
1518
|
+
A shell may be written in React, Vue, Svelte or plain DOM. The preview's dev
|
|
1519
|
+
server compiles all of them, and a shell renders with the preview's own copy
|
|
1520
|
+
of each framework. While the dev server runs, an edit to the shell mounts the
|
|
1521
|
+
new shell over the same controller, keeping the selection and the editor's
|
|
1522
|
+
tree.
|
|
1523
|
+
|
|
1524
|
+
- **React.** `usePreview(controller, selector)` from
|
|
1525
|
+
`@scaffold-ui-kit/preview/react` reads a slice of the state and re-renders
|
|
1526
|
+
when it changes. Return a field of the state (or a primitive derived from
|
|
1527
|
+
one) from `selector`, not a new object. The preview compiles the shell
|
|
1528
|
+
without type-checking it; to type-check a React shell with `tsc`, the kit
|
|
1529
|
+
installs `@types/react` and `@types/react-dom`, and declares a module for
|
|
1530
|
+
stylesheets if the shell imports one:
|
|
1531
|
+
|
|
1532
|
+
```ts
|
|
1533
|
+
// css.d.ts
|
|
1534
|
+
declare module '*.css';
|
|
1535
|
+
```
|
|
1536
|
+
|
|
1537
|
+
- **Svelte.** `previewStore(controller, selector?)` from the shell entry is
|
|
1538
|
+
the state, or a slice of it, as a Svelte store, so a component reads it as
|
|
1539
|
+
`$preview`. Pass the controller through the `context` option of Svelte's
|
|
1540
|
+
`mount` and read it with `getContext`: it lives as long as the shell, and
|
|
1541
|
+
Svelte warns about a prop read once at the top of a component.
|
|
1542
|
+
|
|
1543
|
+
<!-- example: preview-svelte-shell -->
|
|
1544
|
+
```ts
|
|
1545
|
+
import { mount, unmount } from 'svelte';
|
|
1546
|
+
import { defineShell } from '@scaffold-ui-kit/preview/shell';
|
|
1547
|
+
import Shell from './shell.svelte';
|
|
1548
|
+
|
|
1549
|
+
export default defineShell({
|
|
1550
|
+
apiVersion: 1,
|
|
1551
|
+
mount(root, controller) {
|
|
1552
|
+
const shell = mount(Shell, { target: root, context: new Map([['controller', controller]]) });
|
|
1553
|
+
return () => void unmount(shell);
|
|
1554
|
+
},
|
|
1555
|
+
});
|
|
1556
|
+
```
|
|
1557
|
+
|
|
1558
|
+
Mount a pane in an attachment, or in an effect that reads nothing but the
|
|
1559
|
+
element and the framework. An effect that also reads the state re-runs on
|
|
1560
|
+
every change, unmounting the pane and mounting it afresh each time.
|
|
1561
|
+
|
|
1562
|
+
```svelte
|
|
1563
|
+
<script lang="ts">
|
|
1564
|
+
import { getContext } from 'svelte';
|
|
1565
|
+
import { previewStore, type PreviewController } from '@scaffold-ui-kit/preview/shell';
|
|
1566
|
+
|
|
1567
|
+
const controller = getContext<PreviewController>('controller');
|
|
1568
|
+
const panes = previewStore(controller, (state) => state.targets.panes);
|
|
1569
|
+
</script>
|
|
1570
|
+
|
|
1571
|
+
{#each $panes as target (target)}
|
|
1572
|
+
<div class="pane" {@attach (element) => controller.frames.mount(element, target)}></div>
|
|
1573
|
+
{/each}
|
|
1574
|
+
|
|
1575
|
+
<style>
|
|
1576
|
+
.pane :global(iframe) { width: 100%; height: 20rem; border: 0; }
|
|
1577
|
+
</style>
|
|
1578
|
+
```
|
|
1579
|
+
|
|
1580
|
+
A documentation page lists every scenario of the story in a detached pane
|
|
1581
|
+
of its own, with a field that sets one of its props:
|
|
1582
|
+
|
|
1583
|
+
```svelte
|
|
1584
|
+
<script lang="ts">
|
|
1585
|
+
import { getContext } from 'svelte';
|
|
1586
|
+
import { previewStore, type PreviewController } from '@scaffold-ui-kit/preview/shell';
|
|
1587
|
+
|
|
1588
|
+
const controller = getContext<PreviewController>('controller');
|
|
1589
|
+
const preview = previewStore(controller);
|
|
1590
|
+
const component = $derived(controller.catalog.components.find((entry) => entry.id === $preview.selection.id));
|
|
1591
|
+
</script>
|
|
1592
|
+
|
|
1593
|
+
{#if component}
|
|
1594
|
+
<input
|
|
1595
|
+
value={String($preview.props.label ?? '')}
|
|
1596
|
+
oninput={(event) => controller.actions.setProps({ ...$preview.props, label: event.currentTarget.value })}
|
|
1597
|
+
/>
|
|
1598
|
+
{#each component.scenarios.filter((scenario) => !scenario.empty) as scenario (`${component.id}:${scenario.id}`)}
|
|
1599
|
+
<figure>
|
|
1600
|
+
<figcaption>{scenario.label}</figcaption>
|
|
1601
|
+
<div
|
|
1602
|
+
class="pane"
|
|
1603
|
+
{@attach (element) => controller.frames.mountStory(element, component.targets[0], { component: component.id, scenario: scenario.id })}
|
|
1604
|
+
></div>
|
|
1605
|
+
</figure>
|
|
1606
|
+
{/each}
|
|
1607
|
+
{/if}
|
|
1608
|
+
```
|
|
1609
|
+
|
|
1610
|
+
- **Vue and plain DOM** subscribe to the controller directly, Vue into a
|
|
1611
|
+
`shallowRef` of the state.
|
|
1612
|
+
|
|
1613
|
+
### A complete shell
|
|
1614
|
+
|
|
1615
|
+
A shell in plain DOM, with a select of stories, a checkbox per boolean option
|
|
1616
|
+
and the panes side by side:
|
|
1617
|
+
|
|
1618
|
+
<!-- example: preview-plain-shell -->
|
|
1619
|
+
```ts
|
|
1620
|
+
import { defineShell, optionKind } from '@scaffold-ui-kit/preview/shell';
|
|
1621
|
+
|
|
1622
|
+
export default defineShell({
|
|
1623
|
+
apiVersion: 1,
|
|
1624
|
+
mount(root, controller) {
|
|
1625
|
+
const navigation = document.createElement('select');
|
|
1626
|
+
for (const component of controller.catalog.components.filter((component) => component.standalone && !component.unlisted)) {
|
|
1627
|
+
navigation.add(new Option(component.shortTitle, component.id));
|
|
1628
|
+
}
|
|
1629
|
+
navigation.onchange = () => controller.actions.selectComponent(navigation.value);
|
|
1630
|
+
|
|
1631
|
+
const toggles = controller.options.filter((option) => optionKind(option) === 'boolean').map((option) => {
|
|
1632
|
+
const box = Object.assign(document.createElement('input'), { type: 'checkbox', title: option.label });
|
|
1633
|
+
box.onchange = () => controller.actions.setOption(option.id, box.checked);
|
|
1634
|
+
return { option, box };
|
|
1635
|
+
});
|
|
1636
|
+
|
|
1637
|
+
const stage = document.createElement('div');
|
|
1638
|
+
stage.style.cssText = 'display: flex; height: 80vh';
|
|
1639
|
+
root.append(navigation, ...toggles.map(({ box }) => box), stage);
|
|
1640
|
+
|
|
1641
|
+
let panes = '';
|
|
1642
|
+
let unmountPanes: (() => void)[] = [];
|
|
1643
|
+
const render = (state = controller.getState()) => {
|
|
1644
|
+
navigation.value = state.selection.id;
|
|
1645
|
+
for (const { option, box } of toggles) box.checked = state.options[option.id] === true;
|
|
1646
|
+
if (state.targets.panes.join() === panes) return;
|
|
1647
|
+
panes = state.targets.panes.join();
|
|
1648
|
+
unmountPanes.forEach((unmount) => unmount());
|
|
1649
|
+
stage.replaceChildren();
|
|
1650
|
+
unmountPanes = state.targets.panes.map((target) => {
|
|
1651
|
+
const pane = stage.appendChild(document.createElement('div'));
|
|
1652
|
+
pane.style.flex = '1';
|
|
1653
|
+
const unmount = controller.frames.mount(pane, target);
|
|
1654
|
+
// The pane is a bare iframe: the shell sizes it.
|
|
1655
|
+
pane.querySelector('iframe')!.style.cssText = 'width: 100%; height: 100%; border: 0';
|
|
1656
|
+
return unmount;
|
|
1657
|
+
});
|
|
1658
|
+
};
|
|
1659
|
+
render();
|
|
1660
|
+
const stop = controller.subscribe(render);
|
|
1661
|
+
return () => {
|
|
1662
|
+
stop();
|
|
1663
|
+
unmountPanes.forEach((unmount) => unmount());
|
|
1664
|
+
};
|
|
1665
|
+
},
|
|
1666
|
+
});
|
|
1667
|
+
```
|
|
1668
|
+
|
|
1669
|
+
`preview.shell` then names its module, such as `'preview/plain-shell.ts'`.
|
|
1670
|
+
|
|
1671
|
+
To set a story's props from such a shell, read them from `state.props` and
|
|
1672
|
+
hand back the whole set, here a checkbox per boolean prop:
|
|
1673
|
+
|
|
1674
|
+
<!-- example: preview-shell-props -->
|
|
1675
|
+
```ts
|
|
1676
|
+
const component = controller.catalog.components.find((entry) => entry.id === state.selection.id);
|
|
1677
|
+
for (const propType of component?.propTypes.filter((entry) => entry.control === 'boolean' && entry.include) ?? []) {
|
|
1678
|
+
const box = Object.assign(document.createElement('input'), { type: 'checkbox', title: propType.declaredType });
|
|
1679
|
+
box.checked = state.props[propType.name] === true;
|
|
1680
|
+
box.onchange = () => controller.actions.setProps({ ...controller.getState().props, [propType.name]: box.checked });
|
|
1681
|
+
form.append(box);
|
|
1682
|
+
}
|
|
1683
|
+
```
|
|
1684
|
+
|
|
1685
|
+
## Commands
|
|
1686
|
+
|
|
1687
|
+
```sh
|
|
1688
|
+
scaffold-ui-kit-preview dev # generate, then serve the preview
|
|
1689
|
+
scaffold-ui-kit-preview build # check, then build the static preview
|
|
1690
|
+
scaffold-ui-kit-preview generate # write the registries and manifest into .preview-generated/
|
|
1691
|
+
scaffold-ui-kit-preview check # check the story files, the manual specs and the build against the components
|
|
1692
|
+
```
|
|
1693
|
+
|
|
1694
|
+
Every command takes `--kit <path>` for a kit root other than the current
|
|
1695
|
+
directory and `--configuration <path>`, relative to the directory the
|
|
1696
|
+
command runs in, for a kit configuration other than the one the kit gives
|
|
1697
|
+
(see Setting it up), `dev` takes `--port <number>` and `--host [address]` (every
|
|
1698
|
+
address when none is given, so another device on the network can open the
|
|
1699
|
+
preview), and `check --strict` fails on every warning the check itself
|
|
1700
|
+
raises too, such as a select value or boolean no scenario sets, a scenario
|
|
1701
|
+
prop set to `undefined` or a `standalone: false` story no compound lists
|
|
1702
|
+
(what reading the kit configuration and the kit's own build warn of stay
|
|
1703
|
+
warnings). `build` runs the check
|
|
1704
|
+
first, printing its warnings and errors as `check` does, and stops on
|
|
1705
|
+
an error, exiting non-zero. `build` warns of a bundled file larger than
|
|
1706
|
+
500 kB after minification, naming it, except the host page's under the
|
|
1707
|
+
charcell shell, named as it is or configured by a module of the kit's,
|
|
1708
|
+
which holds the preview's interface and is that large whatever the kit
|
|
1709
|
+
holds. `.preview-generated/` (which also holds Vite's
|
|
1710
|
+
cache) and `.preview-static/` are generated and belong in `.gitignore`.
|
|
1711
|
+
|
|
1712
|
+
The static build serves from any file server, at a domain's root or under a
|
|
1713
|
+
subpath, such as a project site at `https://example.github.io/kit/`: its
|
|
1714
|
+
pages load everything by relative paths. The scenario editor and the Empty
|
|
1715
|
+
scenario need the dev server: each edit regenerates the story there. So does
|
|
1716
|
+
the html of a story whose props differ from its scenario's: the build prints
|
|
1717
|
+
the html of every scenario, one `html-source/<story id>.json` beside its
|
|
1718
|
+
pages, and the dev server prints any story's html on request. In the static
|
|
1719
|
+
build the html pane shows a scenario's printed html, and for changed props
|
|
1720
|
+
says it has none.
|
|
1721
|
+
|
|
1722
|
+
The dev server keeps the preview in step with the kit while it runs. Saving
|
|
1723
|
+
a story file, a template, a manual components module, a shared module, or a
|
|
1724
|
+
theme's stylesheet or assets regenerates the preview. Adding or deleting a
|
|
1725
|
+
template adds or removes its component, and a story file added or deleted is
|
|
1726
|
+
picked up the same way. Saves that arrive
|
|
1727
|
+
together regenerate once. Only the generated files whose content changed are
|
|
1728
|
+
written, and the pages that import them reload. If a module fails to load,
|
|
1729
|
+
for example because of a syntax error halfway through an edit, the server
|
|
1730
|
+
logs the error and keeps the last good preview until a save loads again. The
|
|
1731
|
+
views module, the setup modules and the kit's built components are served by
|
|
1732
|
+
Vite, which reloads them on its own. An edit to the kit configuration
|
|
1733
|
+
restarts the server with the new configuration. An edit to the shell mounts
|
|
1734
|
+
the new shell in place (see Frameworks).
|
|
1735
|
+
|
|
1736
|
+
A reload keeps whatever the address holds: the selected story or view, the
|
|
1737
|
+
scenario, the story's props, the visible targets and the option values.
|
|
1738
|
+
The scenario editor's tree is not: a reload seeds it afresh from the
|
|
1739
|
+
scenario and the tree built in it is lost, so a composition worth keeping
|
|
1740
|
+
belongs in a story file's scenario.
|
|
1741
|
+
|
|
1742
|
+
## Using it from code
|
|
1743
|
+
|
|
1744
|
+
The package exports the pieces the commands are built from, for a kit's own
|
|
1745
|
+
scripts and tests. One is `storySource`, which prints a story as the code a
|
|
1746
|
+
consumer would write in each target, importing from the kit's package
|
|
1747
|
+
entries. A docs site can show it next to the component, and the snippets
|
|
1748
|
+
follow the story files. A default slot the story leaves unfilled is empty in
|
|
1749
|
+
every target, without the component's name the panes show there. A two-way prop the story gives is bound as the code
|
|
1750
|
+
view binds it (see A story's source):
|
|
1751
|
+
|
|
1752
|
+
<!-- example: preview-story-source -->
|
|
1753
|
+
```ts
|
|
1754
|
+
import { storySource } from '@scaffold-ui-kit/preview';
|
|
1755
|
+
|
|
1756
|
+
const { react, vue, svelte, html } = await storySource(kitRoot, 'Button', { scenario: 'destructive' });
|
|
1757
|
+
```
|
|
1758
|
+
|
|
1759
|
+
`html` is the markup the story renders to, for a reader who works without a
|
|
1760
|
+
framework: the template rendered once for the story's props and slot content,
|
|
1761
|
+
with the components it references rendered inline, and the exports of the
|
|
1762
|
+
kit's shared modules (the `add.shared` code modules its `package.json`
|
|
1763
|
+
exports), of `@scaffold-ui-kit/helpers/runtime` and `/zag` when the kit or
|
|
1764
|
+
one of its packages has that package installed, and the declarations on
|
|
1765
|
+
each template's own `imports` lines in scope for its expressions. A kit's
|
|
1766
|
+
own module wins a name it shares with the helpers. It is the markup at rest, with no behavior, and it assumes
|
|
1767
|
+
the kit's stylesheet. A component written by hand renders as
|
|
1768
|
+
`handWrittenComponents` maps it - its content, closed, its placeholder
|
|
1769
|
+
showing, or the template it renders - inside one
|
|
1770
|
+
`<!-- component: Name (behavior not included) -->` comment, and slot content
|
|
1771
|
+
written per framework renders its `html`. A story holding any other
|
|
1772
|
+
component without a template, or such slot content without `html`, gets no
|
|
1773
|
+
`html`, and a warning names what has no markup. An expression that cannot be
|
|
1774
|
+
evaluated without running the component is left out, so the part it decides
|
|
1775
|
+
shows at a default: a class it would add is missing, and a panel it would
|
|
1776
|
+
reveal stays out. These evaluation warnings, one per expression, are
|
|
1777
|
+
returned in `htmlWarnings` and printed once per story here and by
|
|
1778
|
+
`preview build`, where the expressions one component cannot evaluate for one
|
|
1779
|
+
reason print as one line counting them, such as
|
|
1780
|
+
`TabsTrigger: could not evaluate 3 expressions (isActive is not defined)`;
|
|
1781
|
+
a styling extension's notices are not among them. A call into a value a
|
|
1782
|
+
hand-written component exposes to its content, such as a driver's
|
|
1783
|
+
`api.getTriggerProps()`, has no result at rest by design and is left out
|
|
1784
|
+
without a warning. A name the component's
|
|
1785
|
+
script defines, such as its state, is one the markup cannot read; the
|
|
1786
|
+
helpers the template imports from a shared module (an `add.shared` code
|
|
1787
|
+
module the kit's `package.json` exports) or from `@scaffold-ui-kit/helpers`,
|
|
1788
|
+
or declares on its `imports` lines, are in scope, so an
|
|
1789
|
+
expression that should evaluate at rest calls one of those instead. Pass
|
|
1790
|
+
`targets` to print only some of them.
|
|
1791
|
+
|
|
1792
|
+
Each call prints one story on its own, so two examples on one page carry the
|
|
1793
|
+
same ids and radio `name`s: a label then points at the other example's
|
|
1794
|
+
field, and the radios of both form one group. Show each example in an iframe
|
|
1795
|
+
of its own.
|
|
1796
|
+
|
|
1797
|
+
## Testing a kit's components
|
|
1798
|
+
|
|
1799
|
+
`@scaffold-ui-kit/preview/testing` drives the preview from a browser test;
|
|
1800
|
+
[`@scaffold-ui-kit/testing`](https://www.npmjs.com/package/@scaffold-ui-kit/testing)
|
|
1801
|
+
is built on it and sweeps every story with axe and the keyboard. It
|
|
1802
|
+
loads one target's preview page on its own, at the top level of the page,
|
|
1803
|
+
and mounts a story in it with the props and options you give, so locators and
|
|
1804
|
+
keyboard input reach the component directly:
|
|
1805
|
+
|
|
1806
|
+
<!-- example: preview-open-story -->
|
|
1807
|
+
```ts
|
|
1808
|
+
import { openStory } from '@scaffold-ui-kit/preview/testing';
|
|
1809
|
+
|
|
1810
|
+
const story = await openStory(page, { target: 'vue', id: 'components-button', props: { variant: 'solid' } });
|
|
1811
|
+
await story.setProps({ variant: 'ghost' });
|
|
1812
|
+
await story.setOptions({ mode: 'dark' });
|
|
1813
|
+
await expect(story.page.getByRole('button')).toBeVisible();
|
|
1814
|
+
```
|
|
1815
|
+
|
|
1816
|
+
A story's id is the one the catalog and the address use
|
|
1817
|
+
(`PreviewComponent.id`, `?component=` in a link): its title in lower case,
|
|
1818
|
+
each run of other characters a hyphen. A `Button` under the default
|
|
1819
|
+
`Components` category is `components-button`, and one a story file places in
|
|
1820
|
+
`Inputs` is `inputs-button`. An `unlisted` story opens this way too, so a
|
|
1821
|
+
story that exists for the tests can stay out of the navigation.
|
|
1822
|
+
|
|
1823
|
+
The same steps are available as functions of the page, for a suite that loads
|
|
1824
|
+
the page once per test and sets stories as it goes:
|
|
1825
|
+
|
|
1826
|
+
- `gotoPreview(page, target)` loads the target's preview page and waits
|
|
1827
|
+
until it listens. A page that never does, such as after an import fails to
|
|
1828
|
+
load, throws within seconds, naming the page and where to look.
|
|
1829
|
+
- `setStory(page, id, props)` shows a story, and `mountStory(page, id, props)`
|
|
1830
|
+
shows it from a fresh mount, with no focus or component state carried over
|
|
1831
|
+
from the story before. An id the page has no story for throws, listing
|
|
1832
|
+
those it has, as `openStory` does.
|
|
1833
|
+
- `waitForStory(page, id)` follows `mountStory` and waits until the story has
|
|
1834
|
+
finished mounting, whatever it rendered, including nothing, and throws after
|
|
1835
|
+
15 seconds, naming the story. After `setStory` it can return on the page's
|
|
1836
|
+
earlier report of the same story.
|
|
1837
|
+
- `setProps(page, props)` replaces the story's props in place, keeping its
|
|
1838
|
+
component instance.
|
|
1839
|
+
- `setOptions(page, options)` applies option values, such as a color mode.
|
|
1840
|
+
An id the kit does not define throws, listing those it does, as does a
|
|
1841
|
+
value the option does not list.
|
|
1842
|
+
- `settle(page)` waits until the frame after the next paint, for a component
|
|
1843
|
+
whose event handling attaches just after it renders.
|
|
1844
|
+
- `readStoryIds(page)` lists the ids of the stories the page can mount.
|
|
1845
|
+
- `readRenderError(page)` is the message of what the mounted story threw as it
|
|
1846
|
+
rendered, as the pane reports it, or `undefined` while it renders.
|
|
1847
|
+
- `PREVIEW_MOUNT_ELEMENT_ID` is the id of the element the story mounts in,
|
|
1848
|
+
for a test that scopes its queries to the story.
|
|
1849
|
+
- `readPreviewManifest(kitRoot)` reads the components of
|
|
1850
|
+
`.preview-generated/manifest.json`, which the preview writes when it
|
|
1851
|
+
starts, typed `PreviewManifestComponent`.
|
|
1852
|
+
|
|
1853
|
+
`props` are the story's whole prop set: the story file's `defaultProps` and
|
|
1854
|
+
scenarios are not applied, so pass every prop the story needs, such as a
|
|
1855
|
+
required label.
|
|
1856
|
+
|
|
1857
|
+
`page` is a Playwright `Page`, or any object with `goto`, `waitForFunction`
|
|
1858
|
+
and `evaluate` of the same shape; the package does not depend on Playwright.
|
|
1859
|
+
Paths are relative, resolved against the test runner's base URL, or pass
|
|
1860
|
+
`baseUrl` to name the server. Serve the preview with `dev` or the static
|
|
1861
|
+
build.
|
|
1862
|
+
|
|
1863
|
+
## Requirements
|
|
1864
|
+
|
|
1865
|
+
Node 22 or later.
|
|
1866
|
+
|
|
1867
|
+
## License
|
|
1868
|
+
|
|
1869
|
+
MIT
|