@dfosco/storyboard-react 4.2.0-beta.4 → 4.2.1
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/package.json +10 -11
- package/src/AuthModal/AuthModal.jsx +6 -8
- package/src/BranchBar/BranchBar.jsx +20 -6
- package/src/BranchBar/BranchBar.module.css +13 -4
- package/src/BranchBar/useBranches.js +20 -6
- package/src/BranchBar/useBranches.test.js +68 -0
- package/src/CommandPalette/CommandPalette.jsx +480 -187
- package/src/CommandPalette/command-palette.css +142 -78
- package/src/Icon.jsx +157 -58
- package/src/Viewfinder.jsx +562 -207
- package/src/Viewfinder.module.css +434 -93
- package/src/Workspace.jsx +7 -0
- package/src/canvas/CanvasPage.bridge.test.jsx +14 -6
- package/src/canvas/CanvasPage.dragdrop.test.jsx +11 -7
- package/src/canvas/CanvasPage.jsx +739 -219
- package/src/canvas/CanvasPage.module.css +13 -15
- package/src/canvas/CanvasPage.multiselect.test.jsx +17 -6
- package/src/canvas/ConnectorLayer.jsx +121 -165
- package/src/canvas/ConnectorLayer.module.css +69 -0
- package/src/canvas/PageSelector.test.jsx +15 -6
- package/src/canvas/canvasApi.js +68 -2
- package/src/canvas/canvasReloadGuard.test.js +1 -1
- package/src/canvas/connectorGeometry.js +132 -0
- package/src/canvas/hotPoolDevLogs.js +25 -0
- package/src/canvas/useCanvas.js +1 -1
- package/src/canvas/useMarqueeSelect.js +30 -4
- package/src/canvas/widgets/CodePenEmbed.jsx +1 -0
- package/src/canvas/widgets/ComponentSetWidget.jsx +199 -0
- package/src/canvas/widgets/ComponentSetWidget.module.css +89 -0
- package/src/canvas/widgets/ComponentWidget.jsx +1 -0
- package/src/canvas/widgets/CropOverlay.jsx +219 -0
- package/src/canvas/widgets/CropOverlay.module.css +118 -0
- package/src/canvas/widgets/ExpandedPane.jsx +474 -0
- package/src/canvas/widgets/ExpandedPane.module.css +179 -0
- package/src/canvas/widgets/ExpandedPane.test.jsx +240 -0
- package/src/canvas/widgets/ExpandedPaneTopBar.jsx +111 -0
- package/src/canvas/widgets/ExpandedPaneTopBar.module.css +59 -0
- package/src/canvas/widgets/ExpandedPaneTopBar.test.jsx +45 -0
- package/src/canvas/widgets/FigmaEmbed.jsx +62 -47
- package/src/canvas/widgets/FigmaEmbed.module.css +61 -0
- package/src/canvas/widgets/ImageWidget.jsx +130 -9
- package/src/canvas/widgets/ImageWidget.module.css +30 -0
- package/src/canvas/widgets/LinkPreview.jsx +113 -5
- package/src/canvas/widgets/LinkPreview.module.css +127 -0
- package/src/canvas/widgets/MarkdownBlock.jsx +167 -17
- package/src/canvas/widgets/MarkdownBlock.module.css +148 -0
- package/src/canvas/widgets/PromptWidget.jsx +414 -0
- package/src/canvas/widgets/PromptWidget.module.css +273 -0
- package/src/canvas/widgets/PrototypeEmbed.jsx +77 -39
- package/src/canvas/widgets/PrototypeEmbed.module.css +117 -0
- package/src/canvas/widgets/PrototypeEmbed.test.jsx +2 -2
- package/src/canvas/widgets/ResizeHandle.jsx +17 -6
- package/src/canvas/widgets/StoryWidget.jsx +73 -15
- package/src/canvas/widgets/TerminalReadWidget.jsx +146 -0
- package/src/canvas/widgets/TerminalReadWidget.module.css +94 -0
- package/src/canvas/widgets/TerminalWidget.jsx +445 -67
- package/src/canvas/widgets/TerminalWidget.module.css +271 -8
- package/src/canvas/widgets/TilesWidget.jsx +300 -0
- package/src/canvas/widgets/TilesWidget.module.css +133 -0
- package/src/canvas/widgets/WidgetChrome.jsx +74 -153
- package/src/canvas/widgets/WidgetChrome.module.css +30 -1
- package/src/canvas/widgets/embedInteraction.test.jsx +24 -26
- package/src/canvas/widgets/expandUtils.js +560 -0
- package/src/canvas/widgets/expandUtils.test.js +155 -0
- package/src/canvas/widgets/index.js +9 -0
- package/src/canvas/widgets/snapshotDisplay.test.jsx +23 -71
- package/src/canvas/widgets/tilePool.js +23 -0
- package/src/canvas/widgets/tiles/diagonal-bl.png +0 -0
- package/src/canvas/widgets/tiles/diagonal-br.png +0 -0
- package/src/canvas/widgets/tiles/diagonal-tl.png +0 -0
- package/src/canvas/widgets/tiles/leaf.png +0 -0
- package/src/canvas/widgets/tiles/quarter-tl.png +0 -0
- package/src/canvas/widgets/tiles/quarter-tr.png +0 -0
- package/src/canvas/widgets/tiles/solid-a.png +0 -0
- package/src/canvas/widgets/tiles/solid-b.png +0 -0
- package/src/canvas/widgets/widgetConfig.js +55 -4
- package/src/canvas/widgets/widgetIcons.jsx +190 -0
- package/src/canvas/widgets/widgetProps.js +1 -0
- package/src/context.jsx +48 -20
- package/src/hooks/useConfig.js +14 -0
- package/src/hooks/usePrototypeReloadGuard.js +64 -0
- package/src/hooks/useSceneData.js +1 -0
- package/src/hooks/useThemeState.test.js +1 -1
- package/src/index.js +8 -2
- package/src/story/ComponentSetPage.jsx +186 -0
- package/src/story/ComponentSetPage.module.css +121 -0
- package/src/story/StoryPage.jsx +32 -2
- package/src/vite/data-plugin.js +407 -67
- package/src/vite/data-plugin.test.js +1 -1
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared icon registry for widget toolbars and titlebar actions.
|
|
3
|
+
*
|
|
4
|
+
* Maps string icon names from widgets.config.json to React components.
|
|
5
|
+
* Used by both WidgetChrome (canvas toolbar) and ExpandedPaneTopBar (fullscreen/split titlebars).
|
|
6
|
+
*/
|
|
7
|
+
import { EyeIcon as OcticonEye, EyeClosedIcon as OcticonEyeClosed, CodeIcon as OcticonCode, UnwrapIcon as OcticonUnwrap, ImageIcon as OcticonImage, UnfoldIcon as OcticonUnfold, FoldIcon as OcticonFold, ScreenFullIcon as OcticonScreenFull, ScreenNormalIcon as OcticonScreenNormal, BroadcastIcon as OcticonBroadcast, CheckIcon as OcticonCheck, MirrorIcon as OcticonMirror } from '@primer/octicons-react'
|
|
8
|
+
|
|
9
|
+
function DeleteIcon() {
|
|
10
|
+
return (
|
|
11
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
|
12
|
+
<path d="M11 1.75V3h2.25a.75.75 0 0 1 0 1.5H2.75a.75.75 0 0 1 0-1.5H5V1.75C5 .784 5.784 0 6.75 0h2.5C10.216 0 11 .784 11 1.75ZM4.496 6.675l.66 6.6a.25.25 0 0 0 .249.225h5.19a.25.25 0 0 0 .249-.225l.66-6.6a.75.75 0 0 1 1.492.15l-.66 6.6A1.748 1.748 0 0 1 10.595 15h-5.19a1.75 1.75 0 0 1-1.741-1.575l-.66-6.6a.75.75 0 1 1 1.492-.15ZM6.5 1.75V3h3V1.75a.25.25 0 0 0-.25-.25h-2.5a.25.25 0 0 0-.25.25Z" />
|
|
13
|
+
</svg>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function ZoomInIcon() {
|
|
18
|
+
return (
|
|
19
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
|
20
|
+
<path d="M7.75 2a.75.75 0 0 1 .75.75V7h4.25a.75.75 0 0 1 0 1.5H8.5v4.25a.75.75 0 0 1-1.5 0V8.5H2.75a.75.75 0 0 1 0-1.5H7V2.75A.75.75 0 0 1 7.75 2Z" />
|
|
21
|
+
</svg>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function ZoomOutIcon() {
|
|
26
|
+
return (
|
|
27
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
|
28
|
+
<path d="M2.75 7.25h10.5a.75.75 0 0 1 0 1.5H2.75a.75.75 0 0 1 0-1.5Z" />
|
|
29
|
+
</svg>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function EditIcon() {
|
|
34
|
+
return (
|
|
35
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
|
36
|
+
<path d="M11.013 1.427a1.75 1.75 0 0 1 2.474 0l1.086 1.086a1.75 1.75 0 0 1 0 2.474l-8.61 8.61c-.21.21-.47.364-.756.445l-3.251.93a.75.75 0 0 1-.927-.928l.929-3.25c.081-.286.235-.547.445-.758l8.61-8.61Zm.176 4.823L9.75 4.81l-6.286 6.287a.253.253 0 0 0-.064.108l-.558 1.953 1.953-.558a.253.253 0 0 0 .108-.064Zm1.238-3.763a.25.25 0 0 0-.354 0L10.811 3.75l1.439 1.44 1.263-1.263a.25.25 0 0 0 0-.354Z" />
|
|
37
|
+
</svg>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function OpenExternalIcon() {
|
|
42
|
+
return (
|
|
43
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
|
44
|
+
<path d="M3.75 2h3.5a.75.75 0 0 1 0 1.5h-3.5a.25.25 0 0 0-.25.25v8.5c0 .138.112.25.25.25h8.5a.25.25 0 0 0 .25-.25v-3.5a.75.75 0 0 1 1.5 0v3.5A1.75 1.75 0 0 1 12.25 14h-8.5A1.75 1.75 0 0 1 2 12.25v-8.5C2 2.784 2.784 2 3.75 2Zm6.854-1h4.146a.25.25 0 0 1 .25.25v4.146a.25.25 0 0 1-.427.177L13.03 4.03 9.28 7.78a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042l3.75-3.75-1.543-1.543A.25.25 0 0 1 10.604 1Z" />
|
|
45
|
+
</svg>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function EyeIcon() {
|
|
50
|
+
return <OcticonEye size={12} />
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function EyeClosedIcon() {
|
|
54
|
+
return <OcticonEyeClosed size={12} />
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function CodeIcon() {
|
|
58
|
+
return <OcticonCode size={12} />
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function UnwrapIcon() {
|
|
62
|
+
return <OcticonUnwrap size={12} />
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function ImageIcon() {
|
|
66
|
+
return <OcticonImage size={12} />
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function CopyIcon() {
|
|
70
|
+
return (
|
|
71
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
|
72
|
+
<path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z" />
|
|
73
|
+
<path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z" />
|
|
74
|
+
</svg>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function MoreIcon() {
|
|
79
|
+
return (
|
|
80
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
|
81
|
+
<path d="M8 9a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM1.5 9a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Zm13 0a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" />
|
|
82
|
+
</svg>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function LinkIcon() {
|
|
87
|
+
return (
|
|
88
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
|
89
|
+
<path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z" />
|
|
90
|
+
</svg>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function ChevronDownIcon() {
|
|
95
|
+
return (
|
|
96
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
|
97
|
+
<path d="M12.78 5.22a.749.749 0 0 1 0 1.06l-4.25 4.25a.749.749 0 0 1-1.06 0L3.22 6.28a.749.749 0 1 1 1.06-1.06L8 8.939l3.72-3.719a.749.749 0 0 1 1.06 0Z" />
|
|
98
|
+
</svg>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function DownloadIcon() {
|
|
103
|
+
return (
|
|
104
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
|
105
|
+
<path d="M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 13.25 14Z" />
|
|
106
|
+
<path d="M7.25 7.689V2a.75.75 0 0 1 1.5 0v5.689l1.97-1.969a.749.749 0 1 1 1.06 1.06l-3.25 3.25a.749.749 0 0 1-1.06 0L4.22 6.78a.749.749 0 1 1 1.06-1.06Z" />
|
|
107
|
+
</svg>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function ExpandIcon() {
|
|
112
|
+
return <OcticonScreenFull size={12} />
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function CollapseIcon() {
|
|
116
|
+
return <OcticonScreenNormal size={12} />
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function SyncIcon() {
|
|
120
|
+
return (
|
|
121
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
|
122
|
+
<path d="M1.705 8.005a.75.75 0 0 1 .834.656 5.5 5.5 0 0 0 9.592 2.97l-1.204-1.204a.25.25 0 0 1 .177-.427h3.646a.25.25 0 0 1 .25.25v3.646a.25.25 0 0 1-.427.177l-1.38-1.38A7.002 7.002 0 0 1 1.05 8.84a.75.75 0 0 1 .656-.834ZM8 2.5a5.487 5.487 0 0 0-4.131 1.869l1.204 1.204A.25.25 0 0 1 4.896 6H1.25A.25.25 0 0 1 1 5.75V2.104a.25.25 0 0 1 .427-.177l1.38 1.38A7.002 7.002 0 0 1 14.95 7.16a.75.75 0 0 1-1.49.178A5.5 5.5 0 0 0 8 2.5Z" />
|
|
123
|
+
</svg>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function UnfoldIcon() {
|
|
128
|
+
return <OcticonUnfold size={12} />
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function FoldIcon() {
|
|
132
|
+
return <OcticonFold size={12} />
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function ColumnsIcon() {
|
|
136
|
+
return (
|
|
137
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
138
|
+
<path d="M12 3v18" /><rect x="3" y="3" width="18" height="18" rx="2" />
|
|
139
|
+
</svg>
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function CheckMarkIcon() {
|
|
144
|
+
return <OcticonCheck size={12} />
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function BroadcastIcon() {
|
|
148
|
+
return <OcticonBroadcast size={12} />
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function MirrorIcon() {
|
|
152
|
+
return <OcticonMirror size={12} />
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function CropIcon() {
|
|
156
|
+
return (
|
|
157
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
|
158
|
+
<path d="M3.75 1a.75.75 0 0 1 .75.75V3.5h7a1.75 1.75 0 0 1 1.75 1.75v7h1.75a.75.75 0 0 1 0 1.5H13.25v1.5a.75.75 0 0 1-1.5 0v-1.5h-7A1.75 1.75 0 0 1 3 12.5v-7H1.25a.75.75 0 0 1 0-1.5H3V1.75A.75.75 0 0 1 3.75 1ZM4.5 5.25v7c0 .138.112.25.25.25h7V5.25a.25.25 0 0 0-.25-.25h-7Z" />
|
|
159
|
+
</svg>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Maps icon name strings from config to React components. */
|
|
164
|
+
export const ICON_REGISTRY = {
|
|
165
|
+
'trash': DeleteIcon,
|
|
166
|
+
'zoom-in': ZoomInIcon,
|
|
167
|
+
'zoom-out': ZoomOutIcon,
|
|
168
|
+
'edit': EditIcon,
|
|
169
|
+
'open-external': OpenExternalIcon,
|
|
170
|
+
'eye': EyeIcon,
|
|
171
|
+
'eye-closed': EyeClosedIcon,
|
|
172
|
+
'code': CodeIcon,
|
|
173
|
+
'unwrap': UnwrapIcon,
|
|
174
|
+
'image': ImageIcon,
|
|
175
|
+
'copy': CopyIcon,
|
|
176
|
+
'link': LinkIcon,
|
|
177
|
+
'more': MoreIcon,
|
|
178
|
+
'chevron-down': ChevronDownIcon,
|
|
179
|
+
'download': DownloadIcon,
|
|
180
|
+
'expand': ExpandIcon,
|
|
181
|
+
'collapse': CollapseIcon,
|
|
182
|
+
'sync': SyncIcon,
|
|
183
|
+
'unfold': UnfoldIcon,
|
|
184
|
+
'fold': FoldIcon,
|
|
185
|
+
'columns': ColumnsIcon,
|
|
186
|
+
'broadcast': BroadcastIcon,
|
|
187
|
+
'check': CheckMarkIcon,
|
|
188
|
+
'crop': CropIcon,
|
|
189
|
+
'mirror': MirrorIcon,
|
|
190
|
+
}
|
|
@@ -130,3 +130,4 @@ export const linkPreviewSchema = schemas['link-preview']
|
|
|
130
130
|
export const imageSchema = schemas['image']
|
|
131
131
|
export const figmaEmbedSchema = schemas['figma-embed']
|
|
132
132
|
export const terminalSchema = schemas['terminal']
|
|
133
|
+
export const promptSchema = schemas['prompt']
|
package/src/context.jsx
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { useEffect, useMemo, Suspense, lazy } from 'react'
|
|
1
|
+
import { useState, useEffect, useMemo, Suspense, lazy } from 'react'
|
|
2
2
|
import { useParams, useLocation } from 'react-router-dom'
|
|
3
3
|
// Named import seeds the core data index via init() AND provides canvas/story route data
|
|
4
|
-
import { canvases,
|
|
4
|
+
import { canvases, stories } from 'virtual:storyboard-data-index'
|
|
5
5
|
import { loadFlow, flowExists, findRecord, deepMerge, setFlowClass, installBodyClassSync, resolveFlowName, resolveRecordName, isModesEnabled } from '@dfosco/storyboard-core'
|
|
6
6
|
import { StoryboardContext } from './StoryboardContext.js'
|
|
7
|
+
import usePrototypeReloadGuard from './hooks/usePrototypeReloadGuard.js'
|
|
7
8
|
import styles from './FlowError.module.css'
|
|
8
9
|
|
|
9
10
|
export { StoryboardContext }
|
|
@@ -46,23 +47,35 @@ for (const [, pages] of canvasGroupMap) {
|
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
// Build a map from story route paths → story names at module load time
|
|
50
|
-
const storyRouteMap = new Map()
|
|
51
|
-
for (const [name, data] of Object.entries(stories || {})) {
|
|
52
|
-
if (data?._route) {
|
|
53
|
-
const route = data._route.replace(/\/+$/, '')
|
|
54
|
-
storyRouteMap.set(route, name)
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
50
|
function matchCanvasRoute(pathname) {
|
|
59
51
|
const normalized = stripBasePath(pathname)
|
|
60
52
|
return canvasRouteMap.get(normalized) || null
|
|
61
53
|
}
|
|
62
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Live-lookup a story route against the current `stories` object.
|
|
57
|
+
*
|
|
58
|
+
* Unlike the canvas route map (built once at module scope), this iterates
|
|
59
|
+
* the `stories` object on every call so it always reflects HMR mutations
|
|
60
|
+
* (the virtual-module HMR handler mutates `stories` in place).
|
|
61
|
+
*
|
|
62
|
+
* Also strips encoded query strings (%3F / %3f) that can leak into the
|
|
63
|
+
* pathname when an iframe src is percent-encoded incorrectly.
|
|
64
|
+
*/
|
|
63
65
|
function matchStoryRoute(pathname) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
let normalized = stripBasePath(pathname)
|
|
67
|
+
// Strip encoded query strings that leaked into the path (%3F / %3f = ?)
|
|
68
|
+
const encodedIdx = normalized.search(/%3f/i)
|
|
69
|
+
if (encodedIdx !== -1) normalized = normalized.substring(0, encodedIdx)
|
|
70
|
+
const literalIdx = normalized.indexOf('?')
|
|
71
|
+
if (literalIdx !== -1) normalized = normalized.substring(0, literalIdx)
|
|
72
|
+
|
|
73
|
+
for (const [name, data] of Object.entries(stories || {})) {
|
|
74
|
+
if (data?._route && data._route.replace(/\/+$/, '') === normalized) {
|
|
75
|
+
return name
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null
|
|
66
79
|
}
|
|
67
80
|
|
|
68
81
|
/**
|
|
@@ -133,6 +146,9 @@ function getPageFlowName(pathname) {
|
|
|
133
146
|
export default function StoryboardProvider({ flowName, sceneName, recordName, recordParam, children }) {
|
|
134
147
|
const basePath = import.meta.env?.BASE_URL || '/'
|
|
135
148
|
|
|
149
|
+
// Suppress HMR full-reloads when prototype-auto-reload flag is off
|
|
150
|
+
usePrototypeReloadGuard()
|
|
151
|
+
|
|
136
152
|
return (
|
|
137
153
|
<>
|
|
138
154
|
<StoryboardProviderInner
|
|
@@ -154,20 +170,32 @@ function StoryboardProviderInner({ flowName, sceneName, recordName, recordParam,
|
|
|
154
170
|
const location = useLocation()
|
|
155
171
|
const params = useParams()
|
|
156
172
|
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
173
|
+
// Re-evaluate story route detection when the data index changes via HMR.
|
|
174
|
+
// The virtual-module HMR handler mutates `stories` in place and dispatches
|
|
175
|
+
// this event; bumping the key forces useMemo deps to re-fire.
|
|
176
|
+
const [storyIndexKey, setStoryIndexKey] = useState(0)
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
const handler = () => setStoryIndexKey((k) => k + 1)
|
|
179
|
+
document.addEventListener('storyboard:story-index-changed', handler)
|
|
180
|
+
return () => document.removeEventListener('storyboard:story-index-changed', handler)
|
|
181
|
+
}, [])
|
|
163
182
|
|
|
164
183
|
// Story route detection — matches current URL against registered story routes
|
|
165
|
-
|
|
184
|
+
// storyIndexKey forces re-evaluation when HMR mutates the stories object in place
|
|
185
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
186
|
+
const storyName = useMemo(() => matchStoryRoute(location.pathname), [location.pathname, storyIndexKey])
|
|
166
187
|
const isMissingStoryRoute = useMemo(
|
|
167
188
|
() => isStoryPath(location.pathname) && !storyName,
|
|
168
189
|
[location.pathname, storyName],
|
|
169
190
|
)
|
|
170
191
|
|
|
192
|
+
// Canvas route detection — matches current URL against registered canvas routes
|
|
193
|
+
const canvasId = useMemo(() => matchCanvasRoute(location.pathname), [location.pathname])
|
|
194
|
+
const isMissingCanvasRoute = useMemo(
|
|
195
|
+
() => isCanvasPath(location.pathname) && !canvasId && !storyName,
|
|
196
|
+
[location.pathname, canvasId, storyName],
|
|
197
|
+
)
|
|
198
|
+
|
|
171
199
|
const searchParams = new URLSearchParams(location.search)
|
|
172
200
|
const sceneParam = searchParams.get('flow') || searchParams.get('scene')
|
|
173
201
|
const prototypeName = getPrototypeName(location.pathname)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useSyncExternalStore, useCallback } from 'react'
|
|
2
|
+
import { getConfig, subscribeToConfig, getConfigSnapshot } from '@dfosco/storyboard-core'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* React hook for reading from the unified config store.
|
|
6
|
+
*
|
|
7
|
+
* @param {string} [domain] - Optional domain key (e.g. 'toolbar', 'canvas')
|
|
8
|
+
* @returns {object} The config object (full or domain slice)
|
|
9
|
+
*/
|
|
10
|
+
export function useConfig(domain) {
|
|
11
|
+
const snapshot = useSyncExternalStore(subscribeToConfig, getConfigSnapshot)
|
|
12
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
13
|
+
return useCallback(() => getConfig(domain), [snapshot, domain])()
|
|
14
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prototype reload guard — suppresses Vite HMR full-reloads for non-canvas pages.
|
|
3
|
+
*
|
|
4
|
+
* Controlled by the "prototype-auto-reload" feature flag (default: true).
|
|
5
|
+
* When the flag is false (user opted out), sends heartbeat messages to the
|
|
6
|
+
* Vite dev server which suppresses full-reload and update payloads for this
|
|
7
|
+
* client. Custom storyboard events (canvas file changes, story changes, etc.)
|
|
8
|
+
* always pass through.
|
|
9
|
+
*
|
|
10
|
+
* Heartbeats are sent every 3s and auto-expire server-side after 5s, so
|
|
11
|
+
* closed tabs never leave the guard stuck.
|
|
12
|
+
*/
|
|
13
|
+
import { useEffect } from 'react'
|
|
14
|
+
import { getFlag, subscribeToStorage } from '@dfosco/storyboard-core'
|
|
15
|
+
|
|
16
|
+
const FLAG_KEY = 'prototype-auto-reload'
|
|
17
|
+
const HEARTBEAT_MS = 3000
|
|
18
|
+
|
|
19
|
+
export default function usePrototypeReloadGuard() {
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (!import.meta.hot) return
|
|
22
|
+
|
|
23
|
+
let interval = null
|
|
24
|
+
|
|
25
|
+
function start() {
|
|
26
|
+
if (interval) return
|
|
27
|
+
const msg = { active: true }
|
|
28
|
+
import.meta.hot.send('storyboard:prototype-reload-guard', msg)
|
|
29
|
+
interval = setInterval(() => {
|
|
30
|
+
import.meta.hot.send('storyboard:prototype-reload-guard', msg)
|
|
31
|
+
}, HEARTBEAT_MS)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function stop() {
|
|
35
|
+
if (interval) {
|
|
36
|
+
clearInterval(interval)
|
|
37
|
+
interval = null
|
|
38
|
+
}
|
|
39
|
+
import.meta.hot.send('storyboard:prototype-reload-guard', { active: false })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function sync() {
|
|
43
|
+
const autoReload = getFlag(FLAG_KEY)
|
|
44
|
+
if (autoReload) {
|
|
45
|
+
stop()
|
|
46
|
+
} else {
|
|
47
|
+
start()
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Initial sync
|
|
52
|
+
sync()
|
|
53
|
+
|
|
54
|
+
// Re-sync when the flag changes in localStorage (e.g. toggled from devtools)
|
|
55
|
+
const unsub = subscribeToStorage((key) => {
|
|
56
|
+
if (key === 'flag.' + FLAG_KEY) sync()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
return () => {
|
|
60
|
+
stop()
|
|
61
|
+
unsub()
|
|
62
|
+
}
|
|
63
|
+
}, [])
|
|
64
|
+
}
|
|
@@ -36,6 +36,7 @@ export function useFlowData(path, opts) {
|
|
|
36
36
|
const storageString = useSyncExternalStore(subscribeToStorage, getStorageSnapshot)
|
|
37
37
|
|
|
38
38
|
// Collect overrides relevant to this path
|
|
39
|
+
// eslint-disable-next-line react-hooks/preserve-manual-memoization
|
|
39
40
|
const result = useMemo(() => {
|
|
40
41
|
if (loading || error || data == null) return undefined
|
|
41
42
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { renderHook, act } from '@testing-library/react'
|
|
2
|
-
import { describe, it, expect, beforeEach
|
|
2
|
+
import { describe, it, expect, beforeEach } from 'vitest'
|
|
3
3
|
import { setTheme, setThemeSyncTarget } from '@dfosco/storyboard-core'
|
|
4
4
|
import { useThemeState, useThemeSyncTargets } from './useThemeState.js'
|
|
5
5
|
|
package/src/index.js
CHANGED
|
@@ -25,6 +25,7 @@ export { useUndoRedo } from './hooks/useUndoRedo.js'
|
|
|
25
25
|
export { useFeatureFlag } from './hooks/useFeatureFlag.js'
|
|
26
26
|
export { useMode } from './hooks/useMode.js'
|
|
27
27
|
export { useThemeState, useThemeSyncTargets } from './hooks/useThemeState.js'
|
|
28
|
+
export { useConfig } from './hooks/useConfig.js'
|
|
28
29
|
|
|
29
30
|
// React Router integration
|
|
30
31
|
export { installHashPreserver } from './hashPreserver.js'
|
|
@@ -35,8 +36,10 @@ export { FormContext } from './context/FormContext.js'
|
|
|
35
36
|
// Design mode hook (keep — React apps may still read mode state)
|
|
36
37
|
// ModeSwitch and ToolbarShell UI moved to @dfosco/storyboard-svelte-ui
|
|
37
38
|
|
|
38
|
-
//
|
|
39
|
-
export { default as
|
|
39
|
+
// Workspace dashboard
|
|
40
|
+
export { default as Workspace } from './Workspace.jsx'
|
|
41
|
+
// Deprecated alias — use Workspace instead
|
|
42
|
+
export { default as Viewfinder } from './Workspace.jsx'
|
|
40
43
|
|
|
41
44
|
// Command Palette (includes BranchBar automatically)
|
|
42
45
|
export { default as StoryboardCommandPalette } from './CommandPalette/CommandPalette.jsx'
|
|
@@ -50,3 +53,6 @@ export { default as AuthModal } from './AuthModal/AuthModal.jsx'
|
|
|
50
53
|
// Canvas
|
|
51
54
|
export { default as CanvasPage } from './canvas/CanvasPage.jsx'
|
|
52
55
|
export { useCanvas } from './canvas/useCanvas.js'
|
|
56
|
+
|
|
57
|
+
// Icon
|
|
58
|
+
export { default as Icon } from './Icon.jsx'
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ComponentSetPage — renders all exports from a .story.jsx in a grid.
|
|
3
|
+
*
|
|
4
|
+
* Mounted when StoryPage detects `?_sb_component_set` in the URL.
|
|
5
|
+
* Each export gets its own grid cell with a selectable label header.
|
|
6
|
+
*
|
|
7
|
+
* URL params:
|
|
8
|
+
* layout — "horizontal" (default) | "vertical"
|
|
9
|
+
* selected — export name of the currently selected cell
|
|
10
|
+
*
|
|
11
|
+
* Selection: clicking a cell label updates `?selected=` and posts
|
|
12
|
+
* `storyboard:component-set:select` to the parent window so the
|
|
13
|
+
* canvas widget can track which export the user picked.
|
|
14
|
+
*/
|
|
15
|
+
import { useState, useEffect, useLayoutEffect, useMemo, useCallback, useRef } from 'react'
|
|
16
|
+
import { useLocation, useNavigate } from 'react-router-dom'
|
|
17
|
+
import { getStoryData } from '@dfosco/storyboard-core'
|
|
18
|
+
import { ThemeProvider, BaseStyles } from '@primer/react'
|
|
19
|
+
import styles from './ComponentSetPage.module.css'
|
|
20
|
+
|
|
21
|
+
export default function ComponentSetPage({ name }) {
|
|
22
|
+
const location = useLocation()
|
|
23
|
+
const navigate = useNavigate()
|
|
24
|
+
const searchParams = new URLSearchParams(location.search)
|
|
25
|
+
|
|
26
|
+
const layout = searchParams.get('layout') || 'horizontal'
|
|
27
|
+
const selected = searchParams.get('selected') || ''
|
|
28
|
+
const isEmbed = searchParams.has('_sb_embed')
|
|
29
|
+
|
|
30
|
+
// Suppress HMR full-reloads when embedded as a canvas iframe
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!isEmbed || !import.meta.hot) return
|
|
33
|
+
const msg = { active: true }
|
|
34
|
+
import.meta.hot.send('storyboard:canvas-hmr-guard', msg)
|
|
35
|
+
const interval = setInterval(() => {
|
|
36
|
+
import.meta.hot.send('storyboard:canvas-hmr-guard', msg)
|
|
37
|
+
}, 3000)
|
|
38
|
+
return () => {
|
|
39
|
+
clearInterval(interval)
|
|
40
|
+
import.meta.hot.send('storyboard:canvas-hmr-guard', { active: false })
|
|
41
|
+
}
|
|
42
|
+
}, [isEmbed])
|
|
43
|
+
|
|
44
|
+
const story = useMemo(() => getStoryData(name), [name])
|
|
45
|
+
const [exports, setExports] = useState(null)
|
|
46
|
+
const [error, setError] = useState(null)
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!story?._storyImport) {
|
|
50
|
+
Promise.resolve().then(() => setError(`Story "${name}" not found or missing import`))
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let cancelled = false
|
|
55
|
+
story._storyImport()
|
|
56
|
+
.then((mod) => {
|
|
57
|
+
if (cancelled) return
|
|
58
|
+
const namedExports = {}
|
|
59
|
+
for (const [key, value] of Object.entries(mod)) {
|
|
60
|
+
if (key !== 'default' && typeof value === 'function') {
|
|
61
|
+
namedExports[key] = value
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
setExports(namedExports)
|
|
65
|
+
setError(null)
|
|
66
|
+
})
|
|
67
|
+
.catch((err) => {
|
|
68
|
+
if (cancelled) return
|
|
69
|
+
setError(`Failed to load story "${name}": ${err.message || err}`)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
return () => { cancelled = true }
|
|
73
|
+
}, [name, story])
|
|
74
|
+
|
|
75
|
+
// Signal snapshot-ready after grid renders in embed mode
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (!isEmbed || !exports || window.parent === window) return
|
|
78
|
+
document.fonts.ready.then(() => {
|
|
79
|
+
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
80
|
+
window.__sbSnapshotReady?.()
|
|
81
|
+
}))
|
|
82
|
+
})
|
|
83
|
+
}, [isEmbed, exports])
|
|
84
|
+
|
|
85
|
+
const gridRef = useRef(null)
|
|
86
|
+
|
|
87
|
+
// Measure all cell content elements and snap cells to the largest
|
|
88
|
+
useLayoutEffect(() => {
|
|
89
|
+
const grid = gridRef.current
|
|
90
|
+
if (!grid || !exports) return
|
|
91
|
+
|
|
92
|
+
const cells = grid.querySelectorAll('[data-cell-content]')
|
|
93
|
+
if (cells.length === 0) return
|
|
94
|
+
|
|
95
|
+
function measure() {
|
|
96
|
+
let maxW = 0
|
|
97
|
+
let maxH = 0
|
|
98
|
+
for (const el of cells) {
|
|
99
|
+
maxW = Math.max(maxW, el.scrollWidth)
|
|
100
|
+
maxH = Math.max(maxH, el.scrollHeight)
|
|
101
|
+
}
|
|
102
|
+
grid.style.setProperty('--cell-snap-w', `${maxW}px`)
|
|
103
|
+
grid.style.setProperty('--cell-snap-h', `${maxH}px`)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Measure after fonts load and initial paint
|
|
107
|
+
measure()
|
|
108
|
+
document.fonts.ready.then(() => requestAnimationFrame(measure))
|
|
109
|
+
|
|
110
|
+
const ro = new ResizeObserver(measure)
|
|
111
|
+
for (const el of cells) ro.observe(el)
|
|
112
|
+
return () => ro.disconnect()
|
|
113
|
+
}, [exports, layout])
|
|
114
|
+
|
|
115
|
+
const handleSelect = useCallback((exportName) => {
|
|
116
|
+
const params = new URLSearchParams(location.search)
|
|
117
|
+
if (exportName === params.get('selected')) {
|
|
118
|
+
params.delete('selected')
|
|
119
|
+
} else {
|
|
120
|
+
params.set('selected', exportName)
|
|
121
|
+
}
|
|
122
|
+
navigate(`${location.pathname}?${params}`, { replace: true })
|
|
123
|
+
|
|
124
|
+
// Notify parent widget
|
|
125
|
+
if (window.parent !== window) {
|
|
126
|
+
window.parent.postMessage({
|
|
127
|
+
type: 'storyboard:component-set:select',
|
|
128
|
+
storyId: name,
|
|
129
|
+
exportName: exportName === selected ? null : exportName,
|
|
130
|
+
}, '*')
|
|
131
|
+
}
|
|
132
|
+
}, [location, navigate, name, selected])
|
|
133
|
+
|
|
134
|
+
if (error) {
|
|
135
|
+
return (
|
|
136
|
+
<div className={styles.error}>
|
|
137
|
+
<strong>Component Set Error</strong>
|
|
138
|
+
<span>{error}</span>
|
|
139
|
+
</div>
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!exports) {
|
|
144
|
+
if (isEmbed) return null
|
|
145
|
+
return <div className={styles.loading}>Loading component set…</div>
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const exportNames = Object.keys(exports)
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<ThemeProvider colorMode="day">
|
|
152
|
+
<BaseStyles>
|
|
153
|
+
<div
|
|
154
|
+
ref={gridRef}
|
|
155
|
+
className={styles.grid}
|
|
156
|
+
data-layout={layout}
|
|
157
|
+
>
|
|
158
|
+
{exportNames.map((exportName) => {
|
|
159
|
+
const Component = exports[exportName]
|
|
160
|
+
const isSelected = exportName === selected
|
|
161
|
+
return (
|
|
162
|
+
<div
|
|
163
|
+
key={exportName}
|
|
164
|
+
className={styles.cell}
|
|
165
|
+
data-selected={isSelected || undefined}
|
|
166
|
+
>
|
|
167
|
+
<button
|
|
168
|
+
className={styles.cellLabel}
|
|
169
|
+
onClick={() => handleSelect(exportName)}
|
|
170
|
+
data-selected={isSelected || undefined}
|
|
171
|
+
aria-pressed={isSelected}
|
|
172
|
+
>
|
|
173
|
+
<span className={styles.cellRadio} data-selected={isSelected || undefined} />
|
|
174
|
+
<span className={styles.cellName}>{exportName}</span>
|
|
175
|
+
</button>
|
|
176
|
+
<div className={styles.cellContent} data-cell-content>
|
|
177
|
+
<Component />
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
)
|
|
181
|
+
})}
|
|
182
|
+
</div>
|
|
183
|
+
</BaseStyles>
|
|
184
|
+
</ThemeProvider>
|
|
185
|
+
)
|
|
186
|
+
}
|