@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
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// One preview window: a quiet charcell Frame titled by what it shows, filling its
|
|
2
|
+
// container, around the element the controller mounts the iframe in - a
|
|
3
|
+
// target's story, the story's html, or a custom view. The pane only provides that element;
|
|
4
|
+
// the iframe and what drives it are the controller's. A target's pane
|
|
5
|
+
// takes the height its story reports as its content's, so a row that starts
|
|
6
|
+
// at its content's height fits the story; it still fills the row it is in.
|
|
7
|
+
// While the controller lays the story panes out at a width (`paneWidth`), a
|
|
8
|
+
// target's or the html pane is that wide and as tall as its story, in a
|
|
9
|
+
// charcell ScrollArea: centered in a wider pane, and scrolled across, its
|
|
10
|
+
// scrollbar drawn in cells on a row of its own, in a narrower one, or down
|
|
11
|
+
// in a shorter one. A view's pane always fills its own.
|
|
12
|
+
// The Frame's top row holds, at its end, `reload`, which loads the pane again,
|
|
13
|
+
// and, on a target's pane, `open`, which opens its story as it is now in a
|
|
14
|
+
// window of its own. Where they do not fit beside the title they give way a
|
|
15
|
+
// whole cell at a time, `open` first, and go once too little of a label is
|
|
16
|
+
// left (fit-labels.ts), so a label is never cut through a cell. While the iframe loads, hidden until it shows its content, the
|
|
17
|
+
// pane says `loading` once a moment has passed (index.css). While the html
|
|
18
|
+
// pane has no html for the story, such as the static build's for changed
|
|
19
|
+
// props, the pane says why over it, as the code pane does (index.css). While
|
|
20
|
+
// the story's html leaves out what it cannot evaluate at rest, such as a
|
|
21
|
+
// part shown on a condition its script holds, the html pane's legend says how
|
|
22
|
+
// many warnings, the count opening them in a popover
|
|
23
|
+
// (HtmlWarnings), as the code pane's row does.
|
|
24
|
+
import { useEffect, useLayoutEffect, useRef, useState, type CSSProperties, type RefObject } from 'react';
|
|
25
|
+
import { Button, Frame, ScrollArea } from 'charcell/react/unlinked';
|
|
26
|
+
import { PREVIEW_SCRIPTED_TARGETS, type PreviewScriptedTarget, type PreviewTarget } from '../../../../../src/shell';
|
|
27
|
+
import { usePreviewController, usePreviewState } from '../../controller-context';
|
|
28
|
+
import { Hint } from '../Hint';
|
|
29
|
+
import { HtmlWarnings } from '../HtmlWarnings';
|
|
30
|
+
import { fitLabels } from '../../fit-labels';
|
|
31
|
+
import { htmlWarningsLabel } from '../../html-warnings';
|
|
32
|
+
import './index.css';
|
|
33
|
+
|
|
34
|
+
/** The cells of the top row beside its title's text and its actions: the
|
|
35
|
+
* ring's corner and a cell of its stroke at each side, the cell at each side
|
|
36
|
+
* of the title, and at least one of the stroke between the two. */
|
|
37
|
+
const TOP_ROW_CELLS = 7;
|
|
38
|
+
|
|
39
|
+
/** How far under a whole cell a width may fall and still be that cell's. */
|
|
40
|
+
const CELL_TOLERANCE = 0.01;
|
|
41
|
+
|
|
42
|
+
/** The pane's whole width in cells, as it changes; `undefined` until measured. */
|
|
43
|
+
function useColumns(frame: RefObject<HTMLDivElement | null>, cell: RefObject<HTMLSpanElement | null>) {
|
|
44
|
+
const [columns, setColumns] = useState<number | undefined>(undefined);
|
|
45
|
+
useLayoutEffect(() => {
|
|
46
|
+
const element = frame.current;
|
|
47
|
+
if (!element) return;
|
|
48
|
+
const measure = () => {
|
|
49
|
+
const width = cell.current?.getBoundingClientRect().width;
|
|
50
|
+
if (width) setColumns(Math.floor(element.getBoundingClientRect().width / width + CELL_TOLERANCE));
|
|
51
|
+
};
|
|
52
|
+
measure();
|
|
53
|
+
const observer = new ResizeObserver(measure);
|
|
54
|
+
observer.observe(element);
|
|
55
|
+
return () => observer.disconnect();
|
|
56
|
+
}, [frame, cell]);
|
|
57
|
+
return columns;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function PreviewPane(props: { title: string } & ({ target: PreviewTarget } | { viewId: string })) {
|
|
61
|
+
const controller = usePreviewController();
|
|
62
|
+
const element = useRef<HTMLDivElement>(null);
|
|
63
|
+
const frame = useRef<HTMLDivElement>(null);
|
|
64
|
+
const cell = useRef<HTMLSpanElement>(null);
|
|
65
|
+
const columns = useColumns(frame, cell);
|
|
66
|
+
const target = 'target' in props ? props.target : undefined;
|
|
67
|
+
const viewId = 'viewId' in props ? props.viewId : undefined;
|
|
68
|
+
const height = usePreviewState((state) => (target === undefined ? undefined : state.frameSizes[target]?.height));
|
|
69
|
+
const width = usePreviewState((state) => (target === undefined ? undefined : state.paneWidth));
|
|
70
|
+
const fixed = width !== undefined;
|
|
71
|
+
const unavailable = usePreviewState((state) =>
|
|
72
|
+
target === 'html' && state.code?.html.status === 'unavailable' ? state.code.html.reason : undefined,
|
|
73
|
+
);
|
|
74
|
+
const warnings = usePreviewState((state) =>
|
|
75
|
+
target === 'html' && state.code?.html.status === 'ready' ? state.code.html.warnings : undefined,
|
|
76
|
+
);
|
|
77
|
+
// A fixed pane mounts in a scroll area, a fitted one in the Frame itself:
|
|
78
|
+
// the element changes between the two, so the iframe mounts again.
|
|
79
|
+
useEffect(
|
|
80
|
+
() =>
|
|
81
|
+
target !== undefined
|
|
82
|
+
? controller.frames.mount(element.current!, target)
|
|
83
|
+
: controller.frames.mountView(element.current!, viewId!),
|
|
84
|
+
[controller, target, viewId, fixed],
|
|
85
|
+
);
|
|
86
|
+
const style = {
|
|
87
|
+
...(height === undefined ? {} : { '--preview-pane-content': `${Math.ceil(height)}px` }),
|
|
88
|
+
...(width === undefined ? {} : { '--preview-pane-width': `${width}px` }),
|
|
89
|
+
} as CSSProperties;
|
|
90
|
+
const actions = [
|
|
91
|
+
{
|
|
92
|
+
label: 'reload',
|
|
93
|
+
hint: `load the ${props.title} pane again`,
|
|
94
|
+
press: () => controller.frames.reload(target ?? 'view'),
|
|
95
|
+
},
|
|
96
|
+
...(target !== undefined && isScriptedTarget(target)
|
|
97
|
+
? [
|
|
98
|
+
{
|
|
99
|
+
label: 'open',
|
|
100
|
+
hint: `open the ${props.title} story in a window of its own`,
|
|
101
|
+
press: () => window.open(controller.frames.standaloneUrl(target), '_blank'),
|
|
102
|
+
},
|
|
103
|
+
]
|
|
104
|
+
: []),
|
|
105
|
+
];
|
|
106
|
+
const warningsLabel = warnings && warnings.length > 0 ? htmlWarningsLabel(warnings) : undefined;
|
|
107
|
+
const titleCells = [...props.title].length + (warningsLabel === undefined ? 0 : warningsLabel.length + 1);
|
|
108
|
+
const fitted = fitLabels(
|
|
109
|
+
actions.map((action) => action.label),
|
|
110
|
+
columns === undefined ? undefined : columns - titleCells - TOP_ROW_CELLS,
|
|
111
|
+
);
|
|
112
|
+
return (
|
|
113
|
+
<div ref={frame} className="PreviewPane__frame">
|
|
114
|
+
<span ref={cell} className="PreviewPane__cell" aria-hidden="true" />
|
|
115
|
+
<Frame
|
|
116
|
+
title={props.title}
|
|
117
|
+
quiet
|
|
118
|
+
flush
|
|
119
|
+
fill
|
|
120
|
+
legend={
|
|
121
|
+
warnings && warnings.length > 0 ? (
|
|
122
|
+
<HtmlWarnings
|
|
123
|
+
warnings={warnings}
|
|
124
|
+
className="PreviewPane__warnings"
|
|
125
|
+
attributes={{ 'data-preview-html-warnings': '' }}
|
|
126
|
+
/>
|
|
127
|
+
) : undefined
|
|
128
|
+
}
|
|
129
|
+
legendEnd={
|
|
130
|
+
fitted.length > 0 ? (
|
|
131
|
+
<span className="PreviewPane__actions">
|
|
132
|
+
{fitted.map((text, index) => {
|
|
133
|
+
const action = actions[index]!;
|
|
134
|
+
return (
|
|
135
|
+
<Hint key={action.label} text={action.hint}>
|
|
136
|
+
{(hint) => (
|
|
137
|
+
<Button
|
|
138
|
+
{...hint}
|
|
139
|
+
variant="dense"
|
|
140
|
+
label={text}
|
|
141
|
+
aria-label={action.label}
|
|
142
|
+
onClick={action.press}
|
|
143
|
+
/>
|
|
144
|
+
)}
|
|
145
|
+
</Hint>
|
|
146
|
+
);
|
|
147
|
+
})}
|
|
148
|
+
</span>
|
|
149
|
+
) : undefined
|
|
150
|
+
}
|
|
151
|
+
>
|
|
152
|
+
{fixed ? (
|
|
153
|
+
<ScrollArea label={`${props.title} story`} orientation="both" overscroll="none" fill>
|
|
154
|
+
<div ref={element} key="fixed" className="PreviewPane PreviewPane--fixed" style={style}>
|
|
155
|
+
{note(unavailable)}
|
|
156
|
+
</div>
|
|
157
|
+
</ScrollArea>
|
|
158
|
+
) : (
|
|
159
|
+
<div ref={element} key="fitted" className="PreviewPane" style={style}>
|
|
160
|
+
{note(unavailable)}
|
|
161
|
+
</div>
|
|
162
|
+
)}
|
|
163
|
+
</Frame>
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** The note over the html pane saying why it has no html, beside the
|
|
169
|
+
* iframe the controller mounts in the same element. */
|
|
170
|
+
function note(reason: string | undefined) {
|
|
171
|
+
return reason === undefined ? null : (
|
|
172
|
+
<p className="PreviewPane__note" role="status" data-preview-html-note="">
|
|
173
|
+
{reason}
|
|
174
|
+
</p>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Whether a target is a scripted one, whose pane can open on its own (the
|
|
179
|
+
* html pane shows the markup its host gives it). */
|
|
180
|
+
function isScriptedTarget(target: PreviewTarget): target is PreviewScriptedTarget {
|
|
181
|
+
return (PREVIEW_SCRIPTED_TARGETS as readonly string[]).includes(target);
|
|
182
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// The shell's top bar: the tool sigil, the kit and its
|
|
2
|
+
// version, and the options placed in the top bar (the kit's own settings, such
|
|
3
|
+
// as its theme) at the start; at the end, two groups of toggles at one level,
|
|
4
|
+
// the views (the navigation, then the stage's rows: scenario, output, code,
|
|
5
|
+
// events) and the targets (its columns), then the kit's `top-bar-end` slot. One build of one kit is served,
|
|
6
|
+
// so the kit and its version are plain labels. A target the kit does not
|
|
7
|
+
// build gets no target toggle; html gets one, after the targets', where the
|
|
8
|
+
// kit builds it. Each toggle is on while the stage shows its view or target;
|
|
9
|
+
// in a narrow window each group shows and toggles one at a time
|
|
10
|
+
// (stage-toggles.ts), the navigation apart from that, being no row of the
|
|
11
|
+
// stage.
|
|
12
|
+
import { Field as CharcellField, Toggle } from 'charcell/react/unlinked';
|
|
13
|
+
import { useId } from 'react';
|
|
14
|
+
import type { PreviewTarget } from '../../../../../src/shell';
|
|
15
|
+
import { Bar } from '../Bar';
|
|
16
|
+
import { Hint } from '../Hint';
|
|
17
|
+
import { usePreviewController, usePreviewState } from '../../controller-context';
|
|
18
|
+
import { OptionControls } from '../OptionControls';
|
|
19
|
+
import {
|
|
20
|
+
CODE_VIEW_OPTION,
|
|
21
|
+
EVENTS_VIEW_OPTION,
|
|
22
|
+
NAVIGATION_OPTION,
|
|
23
|
+
OUTPUT_VIEW_OPTION,
|
|
24
|
+
SCENARIO_VIEW_OPTION,
|
|
25
|
+
} from '../../shell-options';
|
|
26
|
+
import { SlotOutlet } from '../SlotOutlet';
|
|
27
|
+
import { toggleTarget, toggleView, useStage } from '../../stage-toggles';
|
|
28
|
+
|
|
29
|
+
/** The stage's views, top to bottom: each a shell option, so the address keeps it. */
|
|
30
|
+
const VIEWS = [
|
|
31
|
+
{ option: SCENARIO_VIEW_OPTION, label: 'scenario', title: 'the props and content editor' },
|
|
32
|
+
{ option: OUTPUT_VIEW_OPTION, label: 'output', title: 'the story rendered in each target' },
|
|
33
|
+
{ option: CODE_VIEW_OPTION, label: 'code', title: 'the story’s source in each target' },
|
|
34
|
+
{ option: EVENTS_VIEW_OPTION, label: 'events', title: 'the events the story’s panes fire' },
|
|
35
|
+
] as const;
|
|
36
|
+
|
|
37
|
+
export function PromptBar() {
|
|
38
|
+
const controller = usePreviewController();
|
|
39
|
+
const { kitName, kitVersion, targets: kitTargets } = controller.catalog;
|
|
40
|
+
const targets = usePreviewState((state) => state.targets);
|
|
41
|
+
const viewsLabel = useId();
|
|
42
|
+
const targetsLabel = useId();
|
|
43
|
+
const navigationShown = usePreviewState((state) => state.options[NAVIGATION_OPTION] !== false);
|
|
44
|
+
// Narrow, the stage shows one view and one target at a time.
|
|
45
|
+
const { narrow, views, panes } = useStage();
|
|
46
|
+
return (
|
|
47
|
+
<Bar
|
|
48
|
+
joined
|
|
49
|
+
lead="preview$"
|
|
50
|
+
end={
|
|
51
|
+
<>
|
|
52
|
+
<CharcellField label="views" labelId={viewsLabel} role="group" aria-labelledby={viewsLabel}>
|
|
53
|
+
<Hint text="the components and views" around>
|
|
54
|
+
{(hint) => (
|
|
55
|
+
<Toggle
|
|
56
|
+
variant="dense"
|
|
57
|
+
{...hint}
|
|
58
|
+
label="navigation"
|
|
59
|
+
pressed={navigationShown}
|
|
60
|
+
onPressedChange={() => controller.actions.setOption(NAVIGATION_OPTION, !navigationShown)}
|
|
61
|
+
/>
|
|
62
|
+
)}
|
|
63
|
+
</Hint>
|
|
64
|
+
{VIEWS.map((view) => {
|
|
65
|
+
const shown = views.has(view.option);
|
|
66
|
+
return (
|
|
67
|
+
<Hint key={view.option} text={view.title} around>
|
|
68
|
+
{(hint) => (
|
|
69
|
+
<Toggle
|
|
70
|
+
variant="dense"
|
|
71
|
+
{...hint}
|
|
72
|
+
label={view.label}
|
|
73
|
+
pressed={shown}
|
|
74
|
+
onPressedChange={() => toggleView(controller, view.option, narrow)}
|
|
75
|
+
/>
|
|
76
|
+
)}
|
|
77
|
+
</Hint>
|
|
78
|
+
);
|
|
79
|
+
})}
|
|
80
|
+
</CharcellField>
|
|
81
|
+
<CharcellField label="targets" labelId={targetsLabel} role="group" aria-labelledby={targetsLabel}>
|
|
82
|
+
{kitTargets.map((target) => {
|
|
83
|
+
// The selected component may not render into this target (a React-only
|
|
84
|
+
// fixture of another package's components) - then the toggle is disabled and forced off,
|
|
85
|
+
// its hint saying why. A hint either way keeps the toggle where it is as the selection
|
|
86
|
+
// changes, so a focused toggle keeps the focus.
|
|
87
|
+
const supported = targets.supported.includes(target);
|
|
88
|
+
const on = supported && panes.includes(target);
|
|
89
|
+
return (
|
|
90
|
+
<Hint
|
|
91
|
+
key={target}
|
|
92
|
+
text={supported ? targetHint(target) : 'not supported by this component'}
|
|
93
|
+
around
|
|
94
|
+
focusable={!supported}
|
|
95
|
+
label={target}
|
|
96
|
+
>
|
|
97
|
+
{(hint) => (
|
|
98
|
+
<Toggle
|
|
99
|
+
variant="dense"
|
|
100
|
+
{...hint}
|
|
101
|
+
label={target}
|
|
102
|
+
pressed={on}
|
|
103
|
+
disabled={!supported}
|
|
104
|
+
onPressedChange={() => toggleTarget(controller, target, narrow)}
|
|
105
|
+
/>
|
|
106
|
+
)}
|
|
107
|
+
</Hint>
|
|
108
|
+
);
|
|
109
|
+
})}
|
|
110
|
+
</CharcellField>
|
|
111
|
+
<SlotOutlet name="top-bar-end" />
|
|
112
|
+
</>
|
|
113
|
+
}
|
|
114
|
+
>
|
|
115
|
+
{/* Which kit, and which version of it, is being previewed. A kit with
|
|
116
|
+
no package version shows none. */}
|
|
117
|
+
<CharcellField label="kit" data-testid="kit-name">
|
|
118
|
+
<span>{kitName}</span>
|
|
119
|
+
</CharcellField>
|
|
120
|
+
{kitVersion && (
|
|
121
|
+
<CharcellField label="version" data-testid="kit-version">
|
|
122
|
+
<span>{kitVersion}</span>
|
|
123
|
+
</CharcellField>
|
|
124
|
+
)}
|
|
125
|
+
<OptionControls placement="top-bar" />
|
|
126
|
+
</Bar>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** What a target's toggle shows or hides. */
|
|
131
|
+
function targetHint(target: PreviewTarget): string {
|
|
132
|
+
return target === 'html' ? 'the story’s markup, with no script' : `the story rendered in ${target}`;
|
|
133
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
@layer charcell.components {
|
|
2
|
+
/* The scenario row fills its panel of the split (StageSplit), which
|
|
3
|
+
holds its height in whole rows: the view's heading, then a charcell
|
|
4
|
+
Frame taking the rest, whose body the editor's ScrollArea fills. A
|
|
5
|
+
column, so the Frame takes the rest of the row where the split sets its
|
|
6
|
+
height and its content's where the split measures the content to start
|
|
7
|
+
the row at. */
|
|
8
|
+
.ScenarioRow {
|
|
9
|
+
flex: 1;
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-direction: column;
|
|
12
|
+
min-width: 0;
|
|
13
|
+
min-height: 0;
|
|
14
|
+
}
|
|
15
|
+
/* The picker and its info button, side by side with no cell between. */
|
|
16
|
+
.ScenarioRow__pick {
|
|
17
|
+
display: flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
}
|
|
20
|
+
.ScenarioRow__frame {
|
|
21
|
+
flex: 1 1 auto;
|
|
22
|
+
display: flex;
|
|
23
|
+
flex-direction: column;
|
|
24
|
+
min-height: 0;
|
|
25
|
+
}
|
|
26
|
+
/* The Frame is flush, so the scrollbar runs beside its ring: the note and
|
|
27
|
+
the tree each keep a cell from it. */
|
|
28
|
+
.ScenarioRow__note {
|
|
29
|
+
margin: 0;
|
|
30
|
+
padding: 0 1ch;
|
|
31
|
+
color: var(--charcell-mid);
|
|
32
|
+
}
|
|
33
|
+
.ScenarioRow__tree {
|
|
34
|
+
padding: 0 1ch;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// The scenario row, the stage's top row while the scenario view shows: the
|
|
2
|
+
// view's heading, with `pick`, the scenario picker, at the heading row's end
|
|
3
|
+
// and an info button for the picked scenario's description; then the props
|
|
4
|
+
// and content editor (EditorPanel) of the picked scenario in a quiet charcell
|
|
5
|
+
// Frame, as the target panes are, scrolling in a charcell ScrollArea, a whole
|
|
6
|
+
// row at a time. The picker shows only when there is a real choice beyond
|
|
7
|
+
// the default (an injected Empty, where the preview can regenerate, and/or
|
|
8
|
+
// named scenarios); its baseline option is "(default)", or
|
|
9
|
+
// "<label> (default)" when a story file names it. The story's context sits in
|
|
10
|
+
// the stage bar (StageBar). Where the preview cannot regenerate there is no
|
|
11
|
+
// editor, and the row says so. The Frame is flush, so the editor's scrollbar
|
|
12
|
+
// runs beside its ring; the note and the tree keep their own cell from it.
|
|
13
|
+
// Styled in its index.css.
|
|
14
|
+
import { Frame, ScrollArea } from 'charcell/react/unlinked';
|
|
15
|
+
import { scenarioChoices } from '../../../../../src/shell';
|
|
16
|
+
import { usePreviewController, usePreviewState } from '../../controller-context';
|
|
17
|
+
import { Description } from '../Description';
|
|
18
|
+
import { EditorPanel } from '../EditorPanel';
|
|
19
|
+
import { Select } from '../Select';
|
|
20
|
+
import { HeadingRow } from '../HeadingRow';
|
|
21
|
+
import { ViewControls } from '../ViewControls';
|
|
22
|
+
import './index.css';
|
|
23
|
+
|
|
24
|
+
export function ScenarioRow() {
|
|
25
|
+
const controller = usePreviewController();
|
|
26
|
+
const selection = usePreviewState((state) => state.selection);
|
|
27
|
+
const scenario = usePreviewState((state) => state.scenario);
|
|
28
|
+
const editing = usePreviewState((state) => state.editor.open && state.editor.tree !== undefined);
|
|
29
|
+
const selected = controller.catalog.components.find((component) => component.id === selection.id);
|
|
30
|
+
const choices = scenarioChoices(selected, controller.capabilities);
|
|
31
|
+
const picked = selected?.scenarios.find((candidate) => candidate.id === scenario);
|
|
32
|
+
return (
|
|
33
|
+
<div className="ScenarioRow" data-preview-scenario>
|
|
34
|
+
<HeadingRow
|
|
35
|
+
heading="scenario"
|
|
36
|
+
end={
|
|
37
|
+
<ViewControls view="scenario">
|
|
38
|
+
{selected && choices.length > 0 && (
|
|
39
|
+
<span className="ScenarioRow__pick">
|
|
40
|
+
<Select
|
|
41
|
+
label="pick"
|
|
42
|
+
testId="select-scenario"
|
|
43
|
+
value={scenario}
|
|
44
|
+
onChange={controller.actions.pickScenario}
|
|
45
|
+
options={choices}
|
|
46
|
+
/>
|
|
47
|
+
<Description text={picked?.description} label={`about ${picked?.label ?? 'the scenario'}`} />
|
|
48
|
+
</span>
|
|
49
|
+
)}
|
|
50
|
+
</ViewControls>
|
|
51
|
+
}
|
|
52
|
+
/>
|
|
53
|
+
<div className="ScenarioRow__frame">
|
|
54
|
+
<Frame quiet flush fill>
|
|
55
|
+
{selected && editing ? (
|
|
56
|
+
<ScrollArea label="scenario" overscroll="none" fill>
|
|
57
|
+
<div className="ScenarioRow__tree">
|
|
58
|
+
{/* Reseed the panel's own view state when the story or scenario changes. */}
|
|
59
|
+
<EditorPanel key={`${selected.id}:${scenario}`} />
|
|
60
|
+
</div>
|
|
61
|
+
</ScrollArea>
|
|
62
|
+
) : (
|
|
63
|
+
!controller.capabilities.regeneration && (
|
|
64
|
+
<p className="ScenarioRow__note">Editing a scenario needs the preview's development server.</p>
|
|
65
|
+
)
|
|
66
|
+
)}
|
|
67
|
+
</Frame>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// The preview's labeled dropdown - charcell's Select (its own monochrome
|
|
2
|
+
// listbox; a native <select>'s popup is the operating system's own popup).
|
|
3
|
+
// This wrapper keeps the preview-wide call-site API (string value in/out,
|
|
4
|
+
// `label`, `testId`). charcell's Select is bare, so a label puts a charcell
|
|
5
|
+
// Field around it, pointed at the trigger by id; the test id is stamped on
|
|
6
|
+
// the trigger. charcell's layer tokens stack the listbox above a popover panel,
|
|
7
|
+
// so a select opened inside the add-content dialog needs no variant of its own.
|
|
8
|
+
import { useId } from 'react';
|
|
9
|
+
import { Field, Select as CharcellSelect } from 'charcell/react/unlinked';
|
|
10
|
+
|
|
11
|
+
export interface SelectOption {
|
|
12
|
+
label: string;
|
|
13
|
+
value: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function Select(props: {
|
|
17
|
+
/** Omit for a bare trigger (no inline label), e.g. the editor's node pickers. */
|
|
18
|
+
label?: string;
|
|
19
|
+
/** A bare trigger's id, for a label of the caller's own to point at. */
|
|
20
|
+
id?: string;
|
|
21
|
+
value: string;
|
|
22
|
+
options: SelectOption[];
|
|
23
|
+
/** Omit for a display-only select. */
|
|
24
|
+
onChange?: (value: string) => void;
|
|
25
|
+
/** Stamped on the trigger for tests to open the widget. */
|
|
26
|
+
testId?: string;
|
|
27
|
+
}) {
|
|
28
|
+
const { label, value, options, onChange, testId } = props;
|
|
29
|
+
const ownId = useId();
|
|
30
|
+
const id = label === undefined ? props.id : ownId;
|
|
31
|
+
const select = (
|
|
32
|
+
<CharcellSelect
|
|
33
|
+
variant="dense"
|
|
34
|
+
id={id}
|
|
35
|
+
items={options.map((option) => ({ value: option.value, label: option.label }))}
|
|
36
|
+
value={value}
|
|
37
|
+
onValueChange={onChange === undefined ? undefined : (next) => onChange(String(next))}
|
|
38
|
+
triggerProps={testId === undefined ? undefined : { 'data-testid': testId }}
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
return label === undefined ? (
|
|
42
|
+
select
|
|
43
|
+
) : (
|
|
44
|
+
<Field label={label} htmlFor={id}>
|
|
45
|
+
{select}
|
|
46
|
+
</Field>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// A named slot of the shell: where the kit's fill for it mounts. The outlet
|
|
2
|
+
// renders one element and hands it to the fill with the controller; the fill
|
|
3
|
+
// renders whatever it likes inside, in its own framework, and is unmounted
|
|
4
|
+
// with the outlet. A slot with no fill renders nothing.
|
|
5
|
+
import { useEffect, useRef } from 'react';
|
|
6
|
+
import type { CharcellSlotName } from '../../configuration';
|
|
7
|
+
import { usePreviewController, useShellConfiguration } from '../../controller-context';
|
|
8
|
+
import './index.css';
|
|
9
|
+
|
|
10
|
+
export function SlotOutlet({ name }: { name: CharcellSlotName }) {
|
|
11
|
+
const controller = usePreviewController();
|
|
12
|
+
const fill = useShellConfiguration().slots?.[name];
|
|
13
|
+
const element = useRef<HTMLDivElement>(null);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (!fill || !element.current) return;
|
|
16
|
+
const unmount = fill(element.current, controller);
|
|
17
|
+
return () => {
|
|
18
|
+
unmount?.();
|
|
19
|
+
};
|
|
20
|
+
}, [fill, controller]);
|
|
21
|
+
if (!fill) return null;
|
|
22
|
+
// `display: contents` (index.css): the fill's children sit in the
|
|
23
|
+
// bar's zone as the option controls do, with no box of the outlet's own.
|
|
24
|
+
return <div ref={element} className="SlotOutlet" data-preview-slot={name} />;
|
|
25
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// The stage bar, over a component story or a custom view: what is on the
|
|
2
|
+
// stage and how it is set up. At its start `component` (`view` on a custom view), a combobox of everything the
|
|
3
|
+
// navigation lists (the selected entry too, when it is unlisted), narrowed
|
|
4
|
+
// as the reader types, as wide as its longest entry and titled with the
|
|
5
|
+
// selected one's label, with an info button for the component's
|
|
6
|
+
// description; at its end, the options a kit places in the stage bar and its
|
|
7
|
+
// `stage-bar-end` slot, both empty by default. The scenario picker is the
|
|
8
|
+
// scenario row's (ScenarioRow), and the context options the output view's
|
|
9
|
+
// (ViewControls). The bar's rule row is left empty, so no line crosses the
|
|
10
|
+
// navigation's. Styled in its index.css.
|
|
11
|
+
import { Combobox, Field } from 'charcell/react/unlinked';
|
|
12
|
+
import { useId } from 'react';
|
|
13
|
+
import type { PreviewCatalog, PreviewNavigationNode } from '../../../../../src/shell';
|
|
14
|
+
import { usePreviewController, usePreviewState } from '../../controller-context';
|
|
15
|
+
import { Bar } from '../Bar';
|
|
16
|
+
import { Description } from '../Description';
|
|
17
|
+
import { OptionControls } from '../OptionControls';
|
|
18
|
+
import type { SelectOption } from '../Select';
|
|
19
|
+
import { SlotOutlet } from '../SlotOutlet';
|
|
20
|
+
import './index.css';
|
|
21
|
+
|
|
22
|
+
export function StageBar() {
|
|
23
|
+
const controller = usePreviewController();
|
|
24
|
+
const selection = usePreviewState((state) => state.selection);
|
|
25
|
+
const { catalog } = controller;
|
|
26
|
+
const selected =
|
|
27
|
+
selection.kind === 'view' ? undefined : catalog.components.find((component) => component.id === selection.id);
|
|
28
|
+
const entries = stageEntries(catalog, selection);
|
|
29
|
+
const current = entries.find((entry) => entry.value === entryValue(selection))?.label;
|
|
30
|
+
const findId = useId();
|
|
31
|
+
return (
|
|
32
|
+
<Bar
|
|
33
|
+
unruled
|
|
34
|
+
flush
|
|
35
|
+
end={
|
|
36
|
+
<>
|
|
37
|
+
<OptionControls placement="stage-bar" />
|
|
38
|
+
<SlotOutlet name="stage-bar-end" />
|
|
39
|
+
</>
|
|
40
|
+
}
|
|
41
|
+
>
|
|
42
|
+
<span className="StageBar__pick">
|
|
43
|
+
<Field label={selection.kind === 'view' ? 'view' : 'component'} htmlFor={findId}>
|
|
44
|
+
<Combobox
|
|
45
|
+
variant="dense"
|
|
46
|
+
id={findId}
|
|
47
|
+
items={entries}
|
|
48
|
+
value={entryValue(selection)}
|
|
49
|
+
onValueChange={(value) => controller.actions.select(entryOf(value))}
|
|
50
|
+
inputProps={{ 'data-testid': 'select-component', title: current }}
|
|
51
|
+
style={{ width: pickerWidth(entries) }}
|
|
52
|
+
/>
|
|
53
|
+
</Field>
|
|
54
|
+
<Description text={selected?.description} label={`about ${selected?.shortTitle ?? 'the component'}`} />
|
|
55
|
+
</span>
|
|
56
|
+
</Bar>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
type Entry = { kind: 'component' | 'view'; id: string };
|
|
61
|
+
|
|
62
|
+
/** The cells a picker's field takes beside its text: a cell of padding at
|
|
63
|
+
* each end, the cell between the text and its `▾`, and the `▾`. */
|
|
64
|
+
const CELLS_BESIDE_TEXT = 4;
|
|
65
|
+
|
|
66
|
+
/** The width the picker's field takes: its text as wide as the longest
|
|
67
|
+
* entry, and never narrower than charcell's own 20 cells, so no name is
|
|
68
|
+
* cut short and the field keeps its width as the reader moves between
|
|
69
|
+
* entries. */
|
|
70
|
+
function pickerWidth(entries: SelectOption[]): string {
|
|
71
|
+
const longest = Math.max(20, ...entries.map((entry) => [...entry.label].length));
|
|
72
|
+
return `${longest + CELLS_BESIDE_TEXT}ch`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A select value for a navigation entry: a view's id and a component's may coincide. */
|
|
76
|
+
function entryValue(entry: Entry): string {
|
|
77
|
+
return `${entry.kind}:${entry.id}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function entryOf(value: string): Entry {
|
|
81
|
+
const separator = value.indexOf(':');
|
|
82
|
+
return { kind: value.slice(0, separator) as Entry['kind'], id: value.slice(separator + 1) };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Every entry the navigation lists, in its order, components by their short
|
|
86
|
+
* title; the selection too, where the navigation leaves it out. */
|
|
87
|
+
function stageEntries(catalog: PreviewCatalog, selection: Entry): SelectOption[] {
|
|
88
|
+
const entries: SelectOption[] = [];
|
|
89
|
+
const shortTitle = (id: string) => catalog.components.find((component) => component.id === id)?.shortTitle ?? id;
|
|
90
|
+
const walk = (nodes: PreviewNavigationNode[]) => {
|
|
91
|
+
for (const node of nodes) {
|
|
92
|
+
if (node.kind === 'group') walk(node.children);
|
|
93
|
+
else
|
|
94
|
+
entries.push({
|
|
95
|
+
value: entryValue(node),
|
|
96
|
+
label: node.kind === 'component' ? shortTitle(node.id) : node.label,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
walk(catalog.navigation);
|
|
101
|
+
if (!entries.some((entry) => entry.value === entryValue(selection))) {
|
|
102
|
+
const view = catalog.views.find((candidate) => candidate.id === selection.id);
|
|
103
|
+
entries.push({
|
|
104
|
+
value: entryValue(selection),
|
|
105
|
+
label: selection.kind === 'view' ? (view?.label ?? selection.id) : shortTitle(selection.id),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return entries;
|
|
109
|
+
}
|