@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,496 @@
|
|
|
1
|
+
// The scenario editor: compose a component's content as an editable node tree and
|
|
2
|
+
// see it rendered live across React, Vue and Svelte. The tree and every operation
|
|
3
|
+
// on it are the controller's (`controller.editor`): a structural edit regenerates
|
|
4
|
+
// the scratch story through it and reloads the panes. State is in-session only;
|
|
5
|
+
// nothing writes back to the story files.
|
|
6
|
+
//
|
|
7
|
+
// The root is the story's component, picked in the stage bar: named on its
|
|
8
|
+
// first row, with no picker and no grip, its props and content start
|
|
9
|
+
// directly in the scenario row's Frame, with no ring of their own. Each nested node is a charcell Frame with a picker and a remove control
|
|
10
|
+
// on its first row. A node's props and each of its slots, the default
|
|
11
|
+
// included, are windows under it: a quiet Frame titled `props` or
|
|
12
|
+
// `slot: <name>`, its one action in the legend - `expand` or `collapse` - the
|
|
13
|
+
// props starting collapsed and the slots open. The add dialog closes the
|
|
14
|
+
// node. Reordering is charcell's SortableGroup, resolved through the
|
|
15
|
+
// controller's `resolveDrop`. Styled in its index.css.
|
|
16
|
+
import { createContext, useContext, useEffect, useRef, useState } from 'react';
|
|
17
|
+
import {
|
|
18
|
+
Button,
|
|
19
|
+
Frame,
|
|
20
|
+
SortableGroup,
|
|
21
|
+
SortableList,
|
|
22
|
+
TextInput,
|
|
23
|
+
type SortableMove,
|
|
24
|
+
} from 'charcell/react/unlinked';
|
|
25
|
+
import {
|
|
26
|
+
type AroundFill,
|
|
27
|
+
childPath,
|
|
28
|
+
nodeAt,
|
|
29
|
+
nodeAddSlots,
|
|
30
|
+
nodeSlots,
|
|
31
|
+
type ContainerNode,
|
|
32
|
+
type DropTarget,
|
|
33
|
+
type EditorNode,
|
|
34
|
+
type NodePath,
|
|
35
|
+
type OpaqueFill,
|
|
36
|
+
type PreviewPropType,
|
|
37
|
+
type SortableListAddress,
|
|
38
|
+
type Step,
|
|
39
|
+
} from '../../../../../src/shell';
|
|
40
|
+
import { usePreviewController, usePreviewState } from '../../controller-context';
|
|
41
|
+
import { Control } from '../Control';
|
|
42
|
+
import { EditorAddDialog } from '../EditorAddDialog';
|
|
43
|
+
import { Hint } from '../Hint';
|
|
44
|
+
import { Select } from '../Select';
|
|
45
|
+
import './index.css';
|
|
46
|
+
|
|
47
|
+
type Json = unknown;
|
|
48
|
+
|
|
49
|
+
/** A path as a string, for keys and the collapsed sets: `root`, or
|
|
50
|
+
* `default:0/header:1`. */
|
|
51
|
+
function pathKey(path: NodePath): string {
|
|
52
|
+
return path.length === 0 ? 'root' : path.map((step) => `${step.slot}:${step.index}`).join('/');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// --- the panel --------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
export function EditorPanel() {
|
|
58
|
+
const controller = usePreviewController();
|
|
59
|
+
const { components, htmlTags } = controller.catalog;
|
|
60
|
+
const tree = usePreviewState((state) => state.editor.tree);
|
|
61
|
+
// The root's props are the story's: the state holds them beside the tree.
|
|
62
|
+
const rootProps = usePreviewState((state) => state.props);
|
|
63
|
+
// The content the story renders beside the root: the story's own, shown
|
|
64
|
+
// locked outside the tree.
|
|
65
|
+
const selection = usePreviewState((state) => state.selection);
|
|
66
|
+
const around =
|
|
67
|
+
selection.kind === 'component'
|
|
68
|
+
? components.find((candidate) => candidate.id === selection.id)?.around
|
|
69
|
+
: undefined;
|
|
70
|
+
const byName = (name: string) => components.find((candidate) => candidate.name === name);
|
|
71
|
+
// The name to show in the tree/picker: a component's display leaf (its title's last
|
|
72
|
+
// segment), which differs from the preview `name` only for another package's aliased export
|
|
73
|
+
// (`NoticeToast` → "Toast"). Core components' leaf equals their name.
|
|
74
|
+
const displayName = (name: string) => byName(name)?.title.split('/').pop() ?? name;
|
|
75
|
+
|
|
76
|
+
// Path-keyed: which nodes have their props panel expanded.
|
|
77
|
+
const [expanded, setExpanded] = useState<Set<string>>(() => new Set());
|
|
78
|
+
|
|
79
|
+
// Reordering is charcell's SortableGroup: every slot's children are one
|
|
80
|
+
// SortableList, named by its parent's path and the slot, and a move between
|
|
81
|
+
// or within them resolves through the controller. Root is never draggable.
|
|
82
|
+
const lists = useRef(new Map<string, SortableListAddress>());
|
|
83
|
+
function onMove({ from, to }: SortableMove) {
|
|
84
|
+
const source = lists.current.get(from.list);
|
|
85
|
+
const target = lists.current.get(to.list);
|
|
86
|
+
if (!source || !target) return;
|
|
87
|
+
const move = controller.editor.resolveDrop({
|
|
88
|
+
source,
|
|
89
|
+
target,
|
|
90
|
+
sameList: from.list === to.list,
|
|
91
|
+
fromIndex: from.index,
|
|
92
|
+
toIndex: to.index,
|
|
93
|
+
});
|
|
94
|
+
controller.editor.move(move.from, move.to);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Test hook, on the dev server only: drive a move programmatically (the same
|
|
98
|
+
// resolver a drop uses) so an end-to-end spec can assert how a move resolves
|
|
99
|
+
// and regenerates without simulating a drag.
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (!import.meta.env.DEV) return;
|
|
102
|
+
(window as unknown as { __previewEditorMove?: (from: Step[], target: DropTarget) => void }).__previewEditorMove = (
|
|
103
|
+
from,
|
|
104
|
+
target,
|
|
105
|
+
) => controller.editor.move(from, target);
|
|
106
|
+
return () => {
|
|
107
|
+
delete (window as unknown as { __previewEditorMove?: unknown }).__previewEditorMove;
|
|
108
|
+
};
|
|
109
|
+
}, [controller]);
|
|
110
|
+
|
|
111
|
+
function toggleExpanded(path: NodePath) {
|
|
112
|
+
const key = pathKey(path);
|
|
113
|
+
setExpanded((previous) => {
|
|
114
|
+
const next = new Set(previous);
|
|
115
|
+
if (next.has(key)) next.delete(key);
|
|
116
|
+
else next.add(key);
|
|
117
|
+
return next;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Collapsed slot content, keyed by `${pathKey(path)}#${slot}` - hides a slot's
|
|
122
|
+
// children (a named slot's legend toggle, or a default-only node's header toggle).
|
|
123
|
+
const [collapsedContent, setCollapsedContent] = useState<Set<string>>(() => new Set());
|
|
124
|
+
const contentKey = (path: NodePath, slot: string) => `${pathKey(path)}#${slot}`;
|
|
125
|
+
const isContentCollapsed = (path: NodePath, slot: string) => collapsedContent.has(contentKey(path, slot));
|
|
126
|
+
function toggleContent(path: NodePath, slot: string) {
|
|
127
|
+
const key = contentKey(path, slot);
|
|
128
|
+
setCollapsedContent((previous) => {
|
|
129
|
+
const next = new Set(previous);
|
|
130
|
+
if (next.has(key)) next.delete(key);
|
|
131
|
+
else next.add(key);
|
|
132
|
+
return next;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!tree) return null;
|
|
137
|
+
const state = tree;
|
|
138
|
+
|
|
139
|
+
// One prop of a node changes: the node's props are set whole, the root's
|
|
140
|
+
// as the story's props (live, no regeneration, its content left as it is),
|
|
141
|
+
// a nested node's baked into the generated template (a regeneration).
|
|
142
|
+
function updateProp(path: Step[], current: Record<string, Json>, name: string, value: Json) {
|
|
143
|
+
const props = { ...current };
|
|
144
|
+
if (value === undefined) delete props[name];
|
|
145
|
+
else props[name] = value;
|
|
146
|
+
controller.editor.setNodeProps(path, props);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function addChild(path: NodePath, slot: string, child: EditorNode) {
|
|
150
|
+
controller.editor.insert({ parent: path, slot }, child);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function swapComponent(path: Step[], name: string) {
|
|
154
|
+
if (!byName(name)) return;
|
|
155
|
+
controller.editor.replace(path, controller.editor.createNode({ kind: 'component', name, importAs: 'default' }));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Render one element, or the root's props and content unwrapped, recursing
|
|
159
|
+
// into its content. `gripNode` is the node's grip, placed by its slot's
|
|
160
|
+
// SortableList (absent for the root).
|
|
161
|
+
function renderElement(
|
|
162
|
+
path: NodePath,
|
|
163
|
+
component: string,
|
|
164
|
+
propValues: Record<string, Json>,
|
|
165
|
+
gripNode?: React.ReactNode,
|
|
166
|
+
): React.ReactNode {
|
|
167
|
+
const isRoot = path.length === 0;
|
|
168
|
+
const node = isRoot ? null : nodeAt(state.slots, path);
|
|
169
|
+
// The slots the node holds, the default first.
|
|
170
|
+
const options = nodeSlots(controller.catalog, node ?? state);
|
|
171
|
+
// The slots plain content can go in: not one whose content must take its scope.
|
|
172
|
+
const addOptions = nodeAddSlots(controller.catalog, node ?? state);
|
|
173
|
+
// No props ⇒ no props window.
|
|
174
|
+
const propTypes = byName(component)?.propTypes ?? [];
|
|
175
|
+
const hasProps = propTypes.some((propType) => propType.include);
|
|
176
|
+
|
|
177
|
+
const body = (
|
|
178
|
+
<>
|
|
179
|
+
{hasProps && renderProps(path, propTypes, propValues)}
|
|
180
|
+
{renderSlots(path, options)}
|
|
181
|
+
{addOptions.length > 0 && (
|
|
182
|
+
<div className="EditorPanel__add">
|
|
183
|
+
<EditorAddDialog slots={addOptions} onAdd={(slot, child) => addChild(path, slot, child)} />
|
|
184
|
+
</div>
|
|
185
|
+
)}
|
|
186
|
+
</>
|
|
187
|
+
);
|
|
188
|
+
// The root is the stage bar's pick: named on a row of its own, no picker
|
|
189
|
+
// and no grip.
|
|
190
|
+
if (isRoot)
|
|
191
|
+
return (
|
|
192
|
+
<>
|
|
193
|
+
<div className="EditorPanel__row">
|
|
194
|
+
<span className="EditorPanel__label">component</span>
|
|
195
|
+
<span>{displayName(component)}</span>
|
|
196
|
+
</div>
|
|
197
|
+
{body}
|
|
198
|
+
</>
|
|
199
|
+
);
|
|
200
|
+
return (
|
|
201
|
+
<NodeFrame>
|
|
202
|
+
<div className="EditorPanel__row">
|
|
203
|
+
{gripNode}
|
|
204
|
+
<span className="EditorPanel__label">component</span>
|
|
205
|
+
<Select
|
|
206
|
+
value={component}
|
|
207
|
+
onChange={(name) => swapComponent(path, name)}
|
|
208
|
+
options={components
|
|
209
|
+
// An unlisted story is not offered, unless it is the node's own.
|
|
210
|
+
.filter((candidate) => !candidate.unlisted || candidate.name === component)
|
|
211
|
+
.map((candidate) => ({ value: candidate.name, label: displayName(candidate.name) }))}
|
|
212
|
+
/>
|
|
213
|
+
{removeButton(path)}
|
|
214
|
+
</div>
|
|
215
|
+
{body}
|
|
216
|
+
</NodeFrame>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// A node's props window: collapsed to its ring until expanded.
|
|
221
|
+
function renderProps(path: NodePath, propTypes: PreviewPropType[], propValues: Record<string, Json>): React.ReactNode {
|
|
222
|
+
const open = expanded.has(pathKey(path));
|
|
223
|
+
return (
|
|
224
|
+
<Frame
|
|
225
|
+
as="div"
|
|
226
|
+
quiet
|
|
227
|
+
title="props"
|
|
228
|
+
legend={
|
|
229
|
+
<Button
|
|
230
|
+
variant="dense"
|
|
231
|
+
data-editor-window="props"
|
|
232
|
+
aria-label={`${open ? 'collapse' : 'expand'} the props`}
|
|
233
|
+
label={open ? 'collapse' : 'expand'}
|
|
234
|
+
onClick={() => toggleExpanded(path)}
|
|
235
|
+
/>
|
|
236
|
+
}
|
|
237
|
+
>
|
|
238
|
+
{open && (
|
|
239
|
+
<NodeControls
|
|
240
|
+
propTypes={propTypes}
|
|
241
|
+
values={propValues}
|
|
242
|
+
onChange={(name, value) => updateProp(path, propValues, name, value)}
|
|
243
|
+
/>
|
|
244
|
+
)}
|
|
245
|
+
</Frame>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// A node's slots, the default first, each a window of its children; a
|
|
250
|
+
// slot with children collapses to its ring.
|
|
251
|
+
function renderSlots(path: NodePath, slotNames: string[]): React.ReactNode {
|
|
252
|
+
const isRoot = path.length === 0;
|
|
253
|
+
const node = isRoot ? null : (nodeAt(state.slots, path) as ContainerNode | null);
|
|
254
|
+
const slots = isRoot ? state.slots : node?.slots ?? {};
|
|
255
|
+
return slotNames.map((slot) => {
|
|
256
|
+
const collapsed = isContentCollapsed(path, slot);
|
|
257
|
+
const hasChildren = (slots[slot] ?? []).length > 0;
|
|
258
|
+
return (
|
|
259
|
+
<Frame
|
|
260
|
+
key={slot}
|
|
261
|
+
as="div"
|
|
262
|
+
quiet
|
|
263
|
+
title={`slot: ${slot}`}
|
|
264
|
+
legend={
|
|
265
|
+
hasChildren ? (
|
|
266
|
+
<Button
|
|
267
|
+
variant="dense"
|
|
268
|
+
data-editor-window={`slot: ${slot}`}
|
|
269
|
+
aria-label={`${collapsed ? 'expand' : 'collapse'} the ${slot} slot`}
|
|
270
|
+
label={collapsed ? 'expand' : 'collapse'}
|
|
271
|
+
onClick={() => toggleContent(path, slot)}
|
|
272
|
+
/>
|
|
273
|
+
) : undefined
|
|
274
|
+
}
|
|
275
|
+
>
|
|
276
|
+
{!collapsed && renderNodes(path, slot, slots[slot] ?? [])}
|
|
277
|
+
</Frame>
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function removeButton(path: Step[]): React.ReactNode {
|
|
283
|
+
return (
|
|
284
|
+
<span className="EditorPanel__remove">
|
|
285
|
+
<Button variant="dense" data-editor-remove label="remove" onClick={() => controller.editor.remove(path)} />
|
|
286
|
+
</span>
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Render a slot's children as one of the group's sortable lists. Each node
|
|
291
|
+
// places its own grip in its header row, and lifts (a double right and
|
|
292
|
+
// bottom border) while dragged; the hole it leaves is where it lands. An
|
|
293
|
+
// empty slot offers charcell's own place and drop rows while a node is
|
|
294
|
+
// picked up or dragged.
|
|
295
|
+
function renderNodes(parent: NodePath, slot: string, nodes: EditorNode[]): React.ReactNode {
|
|
296
|
+
const listId = `${pathKey(parent)}#${slot}`;
|
|
297
|
+
lists.current.set(listId, { parent, slot });
|
|
298
|
+
return (
|
|
299
|
+
<SortableList
|
|
300
|
+
id={listId}
|
|
301
|
+
items={nodes}
|
|
302
|
+
getKey={(node) => node.id!}
|
|
303
|
+
customGrip
|
|
304
|
+
>
|
|
305
|
+
{({ item: node, index, dragging, gripProps }) => {
|
|
306
|
+
const path = childPath(parent, slot, index);
|
|
307
|
+
const grip = (
|
|
308
|
+
<Hint text="drag to move" triggerProps={{ ...gripProps }}>
|
|
309
|
+
{(props) => <button {...props} />}
|
|
310
|
+
</Hint>
|
|
311
|
+
);
|
|
312
|
+
return (
|
|
313
|
+
<LiftContext.Provider value={dragging}>
|
|
314
|
+
{node.kind === 'text'
|
|
315
|
+
? renderText(path, node.value, grip)
|
|
316
|
+
: node.kind === 'html'
|
|
317
|
+
? renderHtmlNode(path, node, grip)
|
|
318
|
+
: node.kind === 'opaque'
|
|
319
|
+
? renderOpaque(path, node.fill, grip)
|
|
320
|
+
: renderElement(path, node.component, node.props, grip)}
|
|
321
|
+
</LiftContext.Provider>
|
|
322
|
+
);
|
|
323
|
+
}}
|
|
324
|
+
</SortableList>
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// A plain HTML tag (e.g. `<p>`/`<section>` wrappers): a tag picker and no
|
|
329
|
+
// props, its default slot's window, and the add dialog.
|
|
330
|
+
function renderHtmlNode(path: Step[], node: Extract<EditorNode, { kind: 'html' }>, gripNode?: React.ReactNode): React.ReactNode {
|
|
331
|
+
const options = nodeSlots(controller.catalog, node);
|
|
332
|
+
return (
|
|
333
|
+
<NodeFrame>
|
|
334
|
+
<div className="EditorPanel__row">
|
|
335
|
+
{gripNode}
|
|
336
|
+
<span className="EditorPanel__label">html tag</span>
|
|
337
|
+
<Select
|
|
338
|
+
value={node.tag}
|
|
339
|
+
onChange={(tag) => controller.editor.replace(path, { ...node, tag })}
|
|
340
|
+
// A story file may author a tag outside the picker's list - keep the
|
|
341
|
+
// select truthful by prepending the node's actual tag.
|
|
342
|
+
options={(htmlTags.includes(node.tag) ? htmlTags : [node.tag, ...htmlTags]).map(
|
|
343
|
+
(tag) => ({ value: tag, label: tag }),
|
|
344
|
+
)}
|
|
345
|
+
/>
|
|
346
|
+
{removeButton(path)}
|
|
347
|
+
</div>
|
|
348
|
+
{renderSlots(path, options)}
|
|
349
|
+
<div className="EditorPanel__add">
|
|
350
|
+
<EditorAddDialog slots={options} onAdd={(slot, child) => addChild(path, slot, child)} />
|
|
351
|
+
</div>
|
|
352
|
+
</NodeFrame>
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// A `raw` or `scoped` fill or a call to a part from the story file: per-target
|
|
357
|
+
// source the editor cannot author, so it is locked - labeled with its kind,
|
|
358
|
+
// moved or removed like any node, and saved back verbatim. The React body
|
|
359
|
+
// stands in for all three targets.
|
|
360
|
+
function renderOpaque(path: Step[], fill: OpaqueFill, gripNode?: React.ReactNode): React.ReactNode {
|
|
361
|
+
const summary =
|
|
362
|
+
fill.kind === 'scoped'
|
|
363
|
+
? `(${fill.parameters.join(', ')}) => ${fill.react}`
|
|
364
|
+
: fill.kind === 'part'
|
|
365
|
+
? `${fill.name}({ ${fill.parameters.join(', ')} }) => ${fill.react}`
|
|
366
|
+
: fill.react;
|
|
367
|
+
return (
|
|
368
|
+
<NodeFrame>
|
|
369
|
+
<div className="EditorPanel__row">
|
|
370
|
+
{gripNode}
|
|
371
|
+
<span className="EditorPanel__label">{fill.kind}</span>
|
|
372
|
+
<Hint text={summary}>
|
|
373
|
+
{(hint) => (
|
|
374
|
+
<code {...hint} className="EditorPanel__opaque">
|
|
375
|
+
{summary}
|
|
376
|
+
</code>
|
|
377
|
+
)}
|
|
378
|
+
</Hint>
|
|
379
|
+
{removeButton(path)}
|
|
380
|
+
</div>
|
|
381
|
+
</NodeFrame>
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// What the story renders beside the root, before or after it: locked, as
|
|
386
|
+
// the story gives it, outside the root's content, so no edit reaches it.
|
|
387
|
+
function renderBeside(where: 'before' | 'after', fill: AroundFill): React.ReactNode {
|
|
388
|
+
const summary = besideSummary(fill);
|
|
389
|
+
return (
|
|
390
|
+
<NodeFrame>
|
|
391
|
+
<div className="EditorPanel__row" data-preview-editor-beside={where}>
|
|
392
|
+
<span className="EditorPanel__label">{where}</span>
|
|
393
|
+
<Hint text={summary}>
|
|
394
|
+
{(hint) => (
|
|
395
|
+
<code {...hint} className="EditorPanel__opaque">
|
|
396
|
+
{summary}
|
|
397
|
+
</code>
|
|
398
|
+
)}
|
|
399
|
+
</Hint>
|
|
400
|
+
</div>
|
|
401
|
+
</NodeFrame>
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function renderText(path: Step[], value: string, gripNode?: React.ReactNode): React.ReactNode {
|
|
406
|
+
return (
|
|
407
|
+
<NodeFrame key={pathKey(path)}>
|
|
408
|
+
<div className="EditorPanel__row">
|
|
409
|
+
{gripNode}
|
|
410
|
+
<span className="EditorPanel__label">text</span>
|
|
411
|
+
<TextInput
|
|
412
|
+
variant="dense"
|
|
413
|
+
type="text"
|
|
414
|
+
aria-label="text"
|
|
415
|
+
value={value}
|
|
416
|
+
onChange={(event) => controller.editor.setText(path, event.target.value)}
|
|
417
|
+
/>
|
|
418
|
+
{removeButton(path)}
|
|
419
|
+
</div>
|
|
420
|
+
</NodeFrame>
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return (
|
|
425
|
+
<SortableGroup onMove={onMove}>
|
|
426
|
+
<div className="EditorPanel" data-preview-editor>
|
|
427
|
+
{around?.before && renderBeside('before', around.before)}
|
|
428
|
+
{renderElement([], state.root, rootProps)}
|
|
429
|
+
{around?.after && renderBeside('after', around.after)}
|
|
430
|
+
</div>
|
|
431
|
+
</SortableGroup>
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** Content beside the root on one line: its text, its tags and components
|
|
436
|
+
* as `<name>`, and a raw fill's or a part's React body. */
|
|
437
|
+
function besideSummary(fill: AroundFill): string {
|
|
438
|
+
switch (fill.kind) {
|
|
439
|
+
case 'text':
|
|
440
|
+
return fill.value;
|
|
441
|
+
case 'html':
|
|
442
|
+
return `<${fill.tag}>${fill.children && fill.children.kind !== 'scoped' ? besideSummary(fill.children) : ''}`;
|
|
443
|
+
case 'element':
|
|
444
|
+
return `<${fill.component}>${fill.children && fill.children.kind !== 'scoped' ? besideSummary(fill.children) : ''}`;
|
|
445
|
+
case 'fragment':
|
|
446
|
+
return fill.children
|
|
447
|
+
.filter((child): child is AroundFill => child.kind !== 'scoped')
|
|
448
|
+
.map(besideSummary)
|
|
449
|
+
.join(' ');
|
|
450
|
+
case 'raw':
|
|
451
|
+
return fill.react;
|
|
452
|
+
case 'part':
|
|
453
|
+
return `${fill.name}({ ${fill.parameters.join(', ')} })`;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// One editor node: a quiet charcell Frame, as its props and slots windows
|
|
458
|
+
// are, so no level inside the scenario row's Frame outshines what groups it.
|
|
459
|
+
// The node being dragged is raised: its outermost Frame takes a double end
|
|
460
|
+
// and bottom border, and nothing nested inside it.
|
|
461
|
+
const LiftContext = createContext(false);
|
|
462
|
+
|
|
463
|
+
function NodeFrame(props: { children: React.ReactNode }) {
|
|
464
|
+
const lifted = useContext(LiftContext);
|
|
465
|
+
return (
|
|
466
|
+
<Frame
|
|
467
|
+
as="div"
|
|
468
|
+
quiet
|
|
469
|
+
borderEnd={lifted ? 'double' : undefined}
|
|
470
|
+
borderBottom={lifted ? 'double' : undefined}
|
|
471
|
+
>
|
|
472
|
+
<LiftContext.Provider value={false}>{props.children}</LiftContext.Provider>
|
|
473
|
+
</Frame>
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function NodeControls(props: {
|
|
478
|
+
propTypes: PreviewPropType[];
|
|
479
|
+
values: Record<string, Json>;
|
|
480
|
+
onChange: (name: string, value: Json) => void;
|
|
481
|
+
}) {
|
|
482
|
+
const controls = props.propTypes.filter((propType) => propType.include);
|
|
483
|
+
if (controls.length === 0) return null; // no props ⇒ render nothing (the toggle is hidden too)
|
|
484
|
+
return (
|
|
485
|
+
<div className="EditorPanel__controls">
|
|
486
|
+
{controls.map((propType) => (
|
|
487
|
+
<Control
|
|
488
|
+
key={propType.name}
|
|
489
|
+
propType={propType}
|
|
490
|
+
value={props.values[propType.name]}
|
|
491
|
+
onChange={(value) => props.onChange(propType.name, value)}
|
|
492
|
+
/>
|
|
493
|
+
))}
|
|
494
|
+
</div>
|
|
495
|
+
);
|
|
496
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
@layer charcell.components {
|
|
2
|
+
/* The events row fills its panel of the stage's split. */
|
|
3
|
+
.EventsRow {
|
|
4
|
+
flex: 1;
|
|
5
|
+
display: flex;
|
|
6
|
+
flex-direction: column;
|
|
7
|
+
min-width: 0;
|
|
8
|
+
min-height: 0;
|
|
9
|
+
}
|
|
10
|
+
/* The log: when, which pane, which event and what it was called with, in
|
|
11
|
+
four columns, a row each. The Frame is flush, so the log keeps a cell
|
|
12
|
+
from its ring. */
|
|
13
|
+
.EventsRow__log {
|
|
14
|
+
margin: 0;
|
|
15
|
+
padding: 0 1ch;
|
|
16
|
+
list-style: none;
|
|
17
|
+
display: grid;
|
|
18
|
+
grid-template-columns: max-content max-content max-content minmax(0, 1fr);
|
|
19
|
+
column-gap: 2ch;
|
|
20
|
+
}
|
|
21
|
+
.EventsRow__event {
|
|
22
|
+
display: contents;
|
|
23
|
+
}
|
|
24
|
+
.EventsRow__time,
|
|
25
|
+
.EventsRow__target {
|
|
26
|
+
color: var(--charcell-mid);
|
|
27
|
+
}
|
|
28
|
+
.EventsRow__name {
|
|
29
|
+
font-weight: 700;
|
|
30
|
+
}
|
|
31
|
+
.EventsRow__payload {
|
|
32
|
+
min-width: 0;
|
|
33
|
+
overflow: hidden;
|
|
34
|
+
white-space: nowrap;
|
|
35
|
+
text-overflow: ellipsis;
|
|
36
|
+
}
|
|
37
|
+
.EventsRow__note {
|
|
38
|
+
margin: 0;
|
|
39
|
+
padding: 0 1ch;
|
|
40
|
+
color: var(--charcell-mid);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// The events row, the stage's last row while the events view shows: the log
|
|
2
|
+
// of the events the story's panes recorded (`state.events`), each as it
|
|
3
|
+
// fires, newest last - when, in which pane, which event, and a summary of
|
|
4
|
+
// what it was called with - in a quiet charcell Frame, scrolling in a
|
|
5
|
+
// charcell ScrollArea, a whole row at a time, and following the newest, as a
|
|
6
|
+
// terminal does. A story that declares no events
|
|
7
|
+
// says so, and one that has fired none yet says how one shows. Its heading,
|
|
8
|
+
// with `clear` (EventsClear), sits on the row above. Styled in its index.css.
|
|
9
|
+
import { Button, Frame, ScrollArea } from 'charcell/react/unlinked';
|
|
10
|
+
import { useLayoutEffect, useRef } from 'react';
|
|
11
|
+
import { usePreviewController, usePreviewState } from '../../controller-context';
|
|
12
|
+
import './index.css';
|
|
13
|
+
|
|
14
|
+
/** A time of day as the log shows it, `hh:mm:ss`. */
|
|
15
|
+
function clock(time: number): string {
|
|
16
|
+
const date = new Date(time);
|
|
17
|
+
return [date.getHours(), date.getMinutes(), date.getSeconds()]
|
|
18
|
+
.map((part) => String(part).padStart(2, '0'))
|
|
19
|
+
.join(':');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function EventsRow() {
|
|
23
|
+
const controller = usePreviewController();
|
|
24
|
+
const events = usePreviewState((state) => state.events);
|
|
25
|
+
const selection = usePreviewState((state) => state.selection);
|
|
26
|
+
const declared =
|
|
27
|
+
controller.catalog.components.find((component) => component.id === selection.id)?.events ?? [];
|
|
28
|
+
// Each new event scrolls the log to its end: the scrolling region (the
|
|
29
|
+
// ScrollArea's, by its role) as far down as it goes.
|
|
30
|
+
const log = useRef<HTMLOListElement>(null);
|
|
31
|
+
useLayoutEffect(() => {
|
|
32
|
+
const region = log.current?.closest('[role="region"]');
|
|
33
|
+
if (region instanceof HTMLElement) region.scrollTop = region.scrollHeight;
|
|
34
|
+
}, [events]);
|
|
35
|
+
return (
|
|
36
|
+
<div className="EventsRow" data-preview-events>
|
|
37
|
+
<Frame quiet flush fill>
|
|
38
|
+
{events.length > 0 ? (
|
|
39
|
+
<ScrollArea label="events" overscroll="none" fill>
|
|
40
|
+
<ol ref={log} className="EventsRow__log">
|
|
41
|
+
{events.map((event) => (
|
|
42
|
+
<li key={event.id} className="EventsRow__event">
|
|
43
|
+
<span className="EventsRow__time">{clock(event.time)}</span>
|
|
44
|
+
<span className="EventsRow__target">{event.target}</span>
|
|
45
|
+
<span className="EventsRow__name">{event.name}</span>
|
|
46
|
+
<span className="EventsRow__payload">{event.payload}</span>
|
|
47
|
+
</li>
|
|
48
|
+
))}
|
|
49
|
+
</ol>
|
|
50
|
+
</ScrollArea>
|
|
51
|
+
) : (
|
|
52
|
+
<p className="EventsRow__note">
|
|
53
|
+
{declared.length === 0
|
|
54
|
+
? 'This story declares no events.'
|
|
55
|
+
: `No events yet: ${declared.join(', ')} show here as the story fires them.`}
|
|
56
|
+
</p>
|
|
57
|
+
)}
|
|
58
|
+
</Frame>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The events row's `clear`, at the end of its heading row. */
|
|
64
|
+
export function EventsClear() {
|
|
65
|
+
const controller = usePreviewController();
|
|
66
|
+
const empty = usePreviewState((state) => state.events.length === 0);
|
|
67
|
+
return <Button variant="dense" label="clear" disabled={empty} onClick={() => controller.actions.clearEvents()} />;
|
|
68
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
@layer charcell.components {
|
|
2
|
+
/* A row tall, the heading a cell in and the controls at the end. */
|
|
3
|
+
.HeadingRow {
|
|
4
|
+
padding: 0 1ch;
|
|
5
|
+
display: flex;
|
|
6
|
+
align-items: center;
|
|
7
|
+
justify-content: space-between;
|
|
8
|
+
gap: 2ch;
|
|
9
|
+
min-height: var(--charcell-row);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// A view's heading on a row of its own, where no split's handle row carries
|
|
2
|
+
// it: the heading a cell in from the start and the view's controls at the
|
|
3
|
+
// end, as a handle row lays them (the scenario row's, a custom view's).
|
|
4
|
+
// Styled in its index.css.
|
|
5
|
+
import type { ReactNode } from 'react';
|
|
6
|
+
import { ViewHeading } from '../ViewHeading';
|
|
7
|
+
import './index.css';
|
|
8
|
+
|
|
9
|
+
export function HeadingRow({ heading, end }: { heading: string; end?: ReactNode }) {
|
|
10
|
+
return (
|
|
11
|
+
<div className="HeadingRow">
|
|
12
|
+
<ViewHeading>{heading}</ViewHeading>
|
|
13
|
+
{end}
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
@layer charcell.components {
|
|
2
|
+
/* The span a hint wraps around its control: the control's own box, so it
|
|
3
|
+
sits in a bar or a field as the control alone would. */
|
|
4
|
+
.Hint {
|
|
5
|
+
display: inline-flex;
|
|
6
|
+
max-width: 100%;
|
|
7
|
+
min-width: 0;
|
|
8
|
+
}
|
|
9
|
+
/* Standing in for a disabled control, the span shows where focus is. */
|
|
10
|
+
.Hint--focusable {
|
|
11
|
+
outline: none;
|
|
12
|
+
}
|
|
13
|
+
.Hint--focusable:focus-visible {
|
|
14
|
+
outline: 1px solid var(--charcell-foreground);
|
|
15
|
+
}
|
|
16
|
+
}
|