@dfosco/storyboard-react 3.2.0 → 3.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/package.json +2 -2
- package/src/Viewfinder.jsx +1 -1
- package/src/canvas/CanvasControls.jsx +1 -1
- package/src/canvas/CanvasPage.jsx +1 -0
- package/src/canvas/CanvasPage.module.css +5 -0
- package/src/canvas/CanvasToolbar.jsx +1 -1
- package/src/canvas/widgets/PrototypeEmbed.jsx +184 -20
- package/src/canvas/widgets/PrototypeEmbed.module.css +98 -0
- package/src/context.jsx +50 -10
- package/src/vite/data-plugin.js +3 -3
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dfosco/storyboard-react",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@dfosco/storyboard-core": "3.
|
|
6
|
+
"@dfosco/storyboard-core": "3.3.0",
|
|
7
7
|
"@dfosco/tiny-canvas": "^1.1.0",
|
|
8
8
|
"@neodrag/react": "^2.3.1",
|
|
9
9
|
"glob": "^11.0.0",
|
package/src/Viewfinder.jsx
CHANGED
|
@@ -33,7 +33,7 @@ export default function Viewfinder({ pageModules = {}, basePath, title = 'Storyb
|
|
|
33
33
|
|
|
34
34
|
let cancelled = false
|
|
35
35
|
|
|
36
|
-
import('@dfosco/storyboard-core/ui
|
|
36
|
+
import('@dfosco/storyboard-core/ui-runtime').then(({ mountViewfinder, unmountViewfinder }) => {
|
|
37
37
|
if (cancelled) return
|
|
38
38
|
// Ensure clean state for re-mounts
|
|
39
39
|
unmountViewfinder()
|
|
@@ -8,7 +8,7 @@ export const ZOOM_MAX = ZOOM_STEPS[ZOOM_STEPS.length - 1]
|
|
|
8
8
|
const WIDGET_TYPES = [
|
|
9
9
|
{ type: 'sticky-note', label: 'Sticky Note' },
|
|
10
10
|
{ type: 'markdown', label: 'Markdown' },
|
|
11
|
-
{ type: 'prototype', label: 'Prototype' },
|
|
11
|
+
{ type: 'prototype', label: 'Prototype embed' },
|
|
12
12
|
]
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createElement, useCallback, useEffect, useRef, useState } from 'react'
|
|
2
2
|
import { Canvas } from '@dfosco/tiny-canvas'
|
|
3
|
+
import '@dfosco/tiny-canvas/style.css'
|
|
3
4
|
import { useCanvas } from './useCanvas.js'
|
|
4
5
|
import { getWidgetComponent } from './widgets/index.js'
|
|
5
6
|
import { schemas, getDefaults } from './widgets/widgetProps.js'
|
|
@@ -6,7 +6,7 @@ import styles from './CanvasToolbar.module.css'
|
|
|
6
6
|
const WIDGET_TYPES = [
|
|
7
7
|
{ type: 'sticky-note', label: 'Sticky Note', icon: '📝' },
|
|
8
8
|
{ type: 'markdown', label: 'Markdown', icon: '📄' },
|
|
9
|
-
{ type: 'prototype', label: 'Prototype', icon: '🖥️' },
|
|
9
|
+
{ type: 'prototype', label: 'Prototype embed', icon: '🖥️' },
|
|
10
10
|
]
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import { useState, useRef, useEffect, useCallback } from 'react'
|
|
1
|
+
import { useState, useRef, useEffect, useCallback, useMemo } from 'react'
|
|
2
|
+
import { buildPrototypeIndex } from '@dfosco/storyboard-core'
|
|
2
3
|
import WidgetWrapper from './WidgetWrapper.jsx'
|
|
3
4
|
import { readProp, prototypeEmbedSchema } from './widgetProps.js'
|
|
4
5
|
import styles from './PrototypeEmbed.module.css'
|
|
5
6
|
|
|
7
|
+
function formatName(name) {
|
|
8
|
+
return name
|
|
9
|
+
.replace(/[-_]/g, ' ')
|
|
10
|
+
.replace(/\b\w/g, (c) => c.toUpperCase())
|
|
11
|
+
}
|
|
12
|
+
|
|
6
13
|
export default function PrototypeEmbed({ props, onUpdate }) {
|
|
7
14
|
const src = readProp(props, 'src', prototypeEmbedSchema)
|
|
8
15
|
const width = readProp(props, 'width', prototypeEmbedSchema)
|
|
@@ -18,15 +25,100 @@ export default function PrototypeEmbed({ props, onUpdate }) {
|
|
|
18
25
|
|
|
19
26
|
const [editing, setEditing] = useState(false)
|
|
20
27
|
const [interactive, setInteractive] = useState(false)
|
|
28
|
+
const [filter, setFilter] = useState('')
|
|
21
29
|
const inputRef = useRef(null)
|
|
30
|
+
const filterRef = useRef(null)
|
|
22
31
|
const embedRef = useRef(null)
|
|
23
32
|
|
|
33
|
+
// Build prototype index for the picker
|
|
34
|
+
const prototypeIndex = useMemo(() => {
|
|
35
|
+
try {
|
|
36
|
+
return buildPrototypeIndex()
|
|
37
|
+
} catch {
|
|
38
|
+
return { folders: [], prototypes: [], globalFlows: [], sorted: { title: { prototypes: [], folders: [] } } }
|
|
39
|
+
}
|
|
40
|
+
}, [])
|
|
41
|
+
|
|
42
|
+
// Build grouped picker entries from the prototype index
|
|
43
|
+
const pickerGroups = useMemo(() => {
|
|
44
|
+
const groups = []
|
|
45
|
+
const idx = prototypeIndex
|
|
46
|
+
|
|
47
|
+
// Collect all prototypes (from folders first, then ungrouped)
|
|
48
|
+
const allProtos = []
|
|
49
|
+
for (const folder of (idx.sorted?.title?.folders || idx.folders || [])) {
|
|
50
|
+
for (const proto of folder.prototypes || []) {
|
|
51
|
+
if (!proto.isExternal) allProtos.push(proto)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
for (const proto of (idx.sorted?.title?.prototypes || idx.prototypes || [])) {
|
|
55
|
+
if (!proto.isExternal) allProtos.push(proto)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const proto of allProtos) {
|
|
59
|
+
if (proto.hideFlows && proto.flows.length === 1) {
|
|
60
|
+
groups.push({
|
|
61
|
+
label: proto.name,
|
|
62
|
+
items: [{ name: proto.name, route: proto.flows[0].route }],
|
|
63
|
+
})
|
|
64
|
+
} else if (proto.flows.length > 0) {
|
|
65
|
+
groups.push({
|
|
66
|
+
label: proto.name,
|
|
67
|
+
items: proto.flows.map((f) => ({
|
|
68
|
+
name: f.meta?.title || formatName(f.name),
|
|
69
|
+
route: f.route,
|
|
70
|
+
})),
|
|
71
|
+
})
|
|
72
|
+
} else {
|
|
73
|
+
groups.push({
|
|
74
|
+
label: proto.name,
|
|
75
|
+
items: [{ name: proto.name, route: `/${proto.dirName}` }],
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Global flows
|
|
81
|
+
const gf = idx.globalFlows || []
|
|
82
|
+
if (gf.length > 0) {
|
|
83
|
+
groups.push({
|
|
84
|
+
label: 'Other flows',
|
|
85
|
+
items: gf.map((f) => ({
|
|
86
|
+
name: f.meta?.title || formatName(f.name),
|
|
87
|
+
route: f.route,
|
|
88
|
+
})),
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return groups
|
|
93
|
+
}, [prototypeIndex])
|
|
94
|
+
|
|
95
|
+
// Filter groups by search text
|
|
96
|
+
const filteredGroups = useMemo(() => {
|
|
97
|
+
if (!filter) return pickerGroups
|
|
98
|
+
const q = filter.toLowerCase()
|
|
99
|
+
return pickerGroups
|
|
100
|
+
.map((group) => {
|
|
101
|
+
const labelMatch = group.label.toLowerCase().includes(q)
|
|
102
|
+
if (labelMatch) return group
|
|
103
|
+
const matchedItems = group.items.filter((item) =>
|
|
104
|
+
item.name.toLowerCase().includes(q) || item.route.toLowerCase().includes(q)
|
|
105
|
+
)
|
|
106
|
+
if (matchedItems.length === 0) return null
|
|
107
|
+
return { ...group, items: matchedItems }
|
|
108
|
+
})
|
|
109
|
+
.filter(Boolean)
|
|
110
|
+
}, [pickerGroups, filter])
|
|
111
|
+
|
|
112
|
+
const hasPicker = pickerGroups.length > 0
|
|
113
|
+
|
|
24
114
|
useEffect(() => {
|
|
25
|
-
if (editing &&
|
|
115
|
+
if (editing && hasPicker && filterRef.current) {
|
|
116
|
+
filterRef.current.focus()
|
|
117
|
+
} else if (editing && !hasPicker && inputRef.current) {
|
|
26
118
|
inputRef.current.focus()
|
|
27
119
|
inputRef.current.select()
|
|
28
120
|
}
|
|
29
|
-
}, [editing])
|
|
121
|
+
}, [editing, hasPicker])
|
|
30
122
|
|
|
31
123
|
// Exit interactive mode when clicking outside the embed
|
|
32
124
|
useEffect(() => {
|
|
@@ -42,11 +134,23 @@ export default function PrototypeEmbed({ props, onUpdate }) {
|
|
|
42
134
|
|
|
43
135
|
const enterInteractive = useCallback(() => setInteractive(true), [])
|
|
44
136
|
|
|
137
|
+
function handlePickRoute(route) {
|
|
138
|
+
onUpdate?.({ src: route })
|
|
139
|
+
setEditing(false)
|
|
140
|
+
setFilter('')
|
|
141
|
+
}
|
|
142
|
+
|
|
45
143
|
function handleSubmit(e) {
|
|
46
144
|
e.preventDefault()
|
|
47
145
|
const value = inputRef.current?.value?.trim() || ''
|
|
48
146
|
onUpdate?.({ src: value })
|
|
49
147
|
setEditing(false)
|
|
148
|
+
setFilter('')
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function handleCancelEdit() {
|
|
152
|
+
setEditing(false)
|
|
153
|
+
setFilter('')
|
|
50
154
|
}
|
|
51
155
|
|
|
52
156
|
return (
|
|
@@ -57,26 +161,86 @@ export default function PrototypeEmbed({ props, onUpdate }) {
|
|
|
57
161
|
style={{ width, height }}
|
|
58
162
|
>
|
|
59
163
|
{editing ? (
|
|
60
|
-
<
|
|
61
|
-
className={styles.
|
|
62
|
-
onSubmit={handleSubmit}
|
|
164
|
+
<div
|
|
165
|
+
className={styles.pickerPanel}
|
|
63
166
|
onMouseDown={(e) => e.stopPropagation()}
|
|
64
167
|
onPointerDown={(e) => e.stopPropagation()}
|
|
65
168
|
>
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
169
|
+
{hasPicker && (
|
|
170
|
+
<>
|
|
171
|
+
<div className={styles.pickerHeader}>
|
|
172
|
+
<span className={styles.urlLabel}>Pick a prototype</span>
|
|
173
|
+
<button
|
|
174
|
+
type="button"
|
|
175
|
+
className={styles.urlCancel}
|
|
176
|
+
onClick={handleCancelEdit}
|
|
177
|
+
aria-label="Cancel"
|
|
178
|
+
>✕</button>
|
|
179
|
+
</div>
|
|
180
|
+
<input
|
|
181
|
+
ref={filterRef}
|
|
182
|
+
className={styles.filterInput}
|
|
183
|
+
type="text"
|
|
184
|
+
value={filter}
|
|
185
|
+
onChange={(e) => setFilter(e.target.value)}
|
|
186
|
+
placeholder="Filter…"
|
|
187
|
+
onKeyDown={(e) => { if (e.key === 'Escape') handleCancelEdit() }}
|
|
188
|
+
/>
|
|
189
|
+
<div className={styles.pickerList} role="listbox">
|
|
190
|
+
{filteredGroups.map((group) => (
|
|
191
|
+
<div key={group.label} className={styles.pickerGroup}>
|
|
192
|
+
{group.items.length === 1 && group.items[0].name === group.label ? (
|
|
193
|
+
<button
|
|
194
|
+
className={styles.pickerItem}
|
|
195
|
+
role="option"
|
|
196
|
+
onClick={() => handlePickRoute(group.items[0].route)}
|
|
197
|
+
>
|
|
198
|
+
{group.label}
|
|
199
|
+
</button>
|
|
200
|
+
) : (
|
|
201
|
+
<>
|
|
202
|
+
<div className={styles.pickerGroupLabel}>{group.label}</div>
|
|
203
|
+
{group.items.map((item) => (
|
|
204
|
+
<button
|
|
205
|
+
key={item.route}
|
|
206
|
+
className={styles.pickerItem}
|
|
207
|
+
role="option"
|
|
208
|
+
onClick={() => handlePickRoute(item.route)}
|
|
209
|
+
>
|
|
210
|
+
{item.name}
|
|
211
|
+
</button>
|
|
212
|
+
))}
|
|
213
|
+
</>
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
))}
|
|
217
|
+
{filteredGroups.length === 0 && (
|
|
218
|
+
<div className={styles.pickerEmpty}>No matches</div>
|
|
219
|
+
)}
|
|
220
|
+
</div>
|
|
221
|
+
<div className={styles.pickerDivider} />
|
|
222
|
+
</>
|
|
223
|
+
)}
|
|
224
|
+
<form className={styles.customUrlSection} onSubmit={handleSubmit}>
|
|
225
|
+
<label className={styles.urlLabel}>
|
|
226
|
+
{hasPicker ? 'Or enter a custom URL' : 'Prototype URL path'}
|
|
227
|
+
</label>
|
|
228
|
+
<input
|
|
229
|
+
ref={inputRef}
|
|
230
|
+
className={styles.urlInput}
|
|
231
|
+
type="text"
|
|
232
|
+
defaultValue={src}
|
|
233
|
+
placeholder="/MyPrototype/page"
|
|
234
|
+
onKeyDown={(e) => { if (e.key === 'Escape') handleCancelEdit() }}
|
|
235
|
+
/>
|
|
236
|
+
<div className={styles.urlActions}>
|
|
237
|
+
<button type="submit" className={styles.urlSave}>Save</button>
|
|
238
|
+
{!hasPicker && (
|
|
239
|
+
<button type="button" className={styles.urlCancel} onClick={handleCancelEdit}>Cancel</button>
|
|
240
|
+
)}
|
|
241
|
+
</div>
|
|
242
|
+
</form>
|
|
243
|
+
</div>
|
|
80
244
|
) : iframeSrc ? (
|
|
81
245
|
<>
|
|
82
246
|
<div className={styles.iframeContainer}>
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
position: relative;
|
|
3
3
|
overflow: hidden;
|
|
4
4
|
background: var(--bgColor-default, #ffffff);
|
|
5
|
+
border: 3px solid var(--borderColor-default, #d0d7de);
|
|
6
|
+
border-radius: 8px;
|
|
7
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
|
5
8
|
}
|
|
6
9
|
|
|
7
10
|
.iframeContainer {
|
|
@@ -89,6 +92,101 @@
|
|
|
89
92
|
justify-content: center;
|
|
90
93
|
}
|
|
91
94
|
|
|
95
|
+
.pickerPanel {
|
|
96
|
+
display: flex;
|
|
97
|
+
flex-direction: column;
|
|
98
|
+
height: 100%;
|
|
99
|
+
box-sizing: border-box;
|
|
100
|
+
overflow: hidden;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.pickerHeader {
|
|
104
|
+
display: flex;
|
|
105
|
+
align-items: center;
|
|
106
|
+
justify-content: space-between;
|
|
107
|
+
padding: 12px 16px 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.filterInput {
|
|
111
|
+
all: unset;
|
|
112
|
+
margin: 8px 16px;
|
|
113
|
+
padding: 6px 10px;
|
|
114
|
+
font-size: 13px;
|
|
115
|
+
border: 1px solid var(--borderColor-default, #d0d7de);
|
|
116
|
+
border-radius: 6px;
|
|
117
|
+
background: var(--bgColor-default, #ffffff);
|
|
118
|
+
color: var(--fgColor-default, #1f2328);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.filterInput:focus {
|
|
122
|
+
border-color: var(--bgColor-accent-emphasis, #2f81f7);
|
|
123
|
+
box-shadow: 0 0 0 2px rgba(47, 129, 247, 0.3);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.pickerList {
|
|
127
|
+
flex: 1;
|
|
128
|
+
overflow-y: auto;
|
|
129
|
+
padding: 4px 8px;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.pickerGroup {
|
|
133
|
+
margin-bottom: 2px;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.pickerGroupLabel {
|
|
137
|
+
padding: 6px 8px 2px;
|
|
138
|
+
font-size: 11px;
|
|
139
|
+
font-weight: 600;
|
|
140
|
+
color: var(--fgColor-muted, #656d76);
|
|
141
|
+
text-transform: uppercase;
|
|
142
|
+
letter-spacing: 0.5px;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.pickerItem {
|
|
146
|
+
all: unset;
|
|
147
|
+
display: block;
|
|
148
|
+
width: 100%;
|
|
149
|
+
box-sizing: border-box;
|
|
150
|
+
padding: 6px 12px;
|
|
151
|
+
font-size: 13px;
|
|
152
|
+
color: var(--fgColor-default, #1f2328);
|
|
153
|
+
border-radius: 6px;
|
|
154
|
+
cursor: pointer;
|
|
155
|
+
white-space: nowrap;
|
|
156
|
+
overflow: hidden;
|
|
157
|
+
text-overflow: ellipsis;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.pickerItem:hover {
|
|
161
|
+
background: var(--bgColor-neutral-muted, #eaeef2);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.pickerItem:focus-visible {
|
|
165
|
+
outline: 2px solid var(--bgColor-accent-emphasis, #2f81f7);
|
|
166
|
+
outline-offset: -2px;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.pickerEmpty {
|
|
170
|
+
padding: 12px;
|
|
171
|
+
font-size: 13px;
|
|
172
|
+
color: var(--fgColor-muted, #656d76);
|
|
173
|
+
font-style: italic;
|
|
174
|
+
text-align: center;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.pickerDivider {
|
|
178
|
+
height: 1px;
|
|
179
|
+
margin: 4px 16px;
|
|
180
|
+
background: var(--borderColor-muted, #d8dee4);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.customUrlSection {
|
|
184
|
+
display: flex;
|
|
185
|
+
flex-direction: column;
|
|
186
|
+
gap: 6px;
|
|
187
|
+
padding: 8px 16px 12px;
|
|
188
|
+
}
|
|
189
|
+
|
|
92
190
|
.urlLabel {
|
|
93
191
|
font-size: 12px;
|
|
94
192
|
font-weight: 600;
|
package/src/context.jsx
CHANGED
|
@@ -1,13 +1,27 @@
|
|
|
1
|
-
import { useEffect, useMemo } from 'react'
|
|
1
|
+
import { useEffect, useMemo, Suspense, lazy } from 'react'
|
|
2
2
|
import { useParams, useLocation } from 'react-router-dom'
|
|
3
|
-
//
|
|
4
|
-
import 'virtual:storyboard-data-index'
|
|
3
|
+
// Named import seeds the core data index via init() AND provides canvas route data
|
|
4
|
+
import { canvases } 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
7
|
import styles from './FlowError.module.css'
|
|
8
8
|
|
|
9
9
|
export { StoryboardContext }
|
|
10
10
|
|
|
11
|
+
const CanvasPageLazy = lazy(() => import('./canvas/CanvasPage.jsx'))
|
|
12
|
+
|
|
13
|
+
// Build a map from canvas route paths → canvas names at module load time
|
|
14
|
+
const canvasRouteMap = new Map()
|
|
15
|
+
for (const [name, data] of Object.entries(canvases || {})) {
|
|
16
|
+
const route = (data?._route || `/${name}`).replace(/\/+$/, '')
|
|
17
|
+
canvasRouteMap.set(route, name)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function matchCanvasRoute(pathname) {
|
|
21
|
+
const normalized = pathname.replace(/\/+$/, '') || '/'
|
|
22
|
+
return canvasRouteMap.get(normalized) || null
|
|
23
|
+
}
|
|
24
|
+
|
|
11
25
|
/**
|
|
12
26
|
* Derives the top-level prototype name from a pathname.
|
|
13
27
|
* "/Dashboard" → "Dashboard", "/Dashboard/sub" → "Dashboard"
|
|
@@ -44,14 +58,19 @@ function getPageFlowName(pathname) {
|
|
|
44
58
|
*/
|
|
45
59
|
export default function StoryboardProvider({ flowName, sceneName, recordName, recordParam, children }) {
|
|
46
60
|
const location = useLocation()
|
|
61
|
+
const params = useParams()
|
|
62
|
+
|
|
63
|
+
// Canvas route detection — matches current URL against registered canvas routes
|
|
64
|
+
const canvasName = useMemo(() => matchCanvasRoute(location.pathname), [location.pathname])
|
|
65
|
+
|
|
47
66
|
const searchParams = new URLSearchParams(location.search)
|
|
48
67
|
const sceneParam = searchParams.get('flow') || searchParams.get('scene')
|
|
49
68
|
const prototypeName = getPrototypeName(location.pathname)
|
|
50
69
|
const pageFlow = getPageFlowName(location.pathname)
|
|
51
|
-
const params = useParams()
|
|
52
70
|
|
|
53
|
-
// Resolve flow name with prototype scoping
|
|
71
|
+
// Resolve flow name with prototype scoping (skip for canvas pages)
|
|
54
72
|
const activeFlowName = useMemo(() => {
|
|
73
|
+
if (canvasName) return null
|
|
55
74
|
const requested = sceneParam || flowName || sceneName
|
|
56
75
|
if (requested) {
|
|
57
76
|
return resolveFlowName(prototypeName, requested)
|
|
@@ -66,7 +85,7 @@ export default function StoryboardProvider({ flowName, sceneName, recordName, re
|
|
|
66
85
|
}
|
|
67
86
|
// 3. Global default
|
|
68
87
|
return 'default'
|
|
69
|
-
}, [sceneParam, flowName, sceneName, prototypeName, pageFlow])
|
|
88
|
+
}, [canvasName, sceneParam, flowName, sceneName, prototypeName, pageFlow])
|
|
70
89
|
|
|
71
90
|
// Auto-install body class sync (sb-key--value classes on <body>)
|
|
72
91
|
useEffect(() => installBodyClassSync(), [])
|
|
@@ -76,9 +95,9 @@ export default function StoryboardProvider({ flowName, sceneName, recordName, re
|
|
|
76
95
|
if (!isModesEnabled()) return
|
|
77
96
|
|
|
78
97
|
let cleanup
|
|
79
|
-
import('@dfosco/storyboard-core/ui
|
|
80
|
-
.then(({
|
|
81
|
-
cleanup =
|
|
98
|
+
import('@dfosco/storyboard-core/ui-runtime')
|
|
99
|
+
.then(({ mountDesignModes }) => {
|
|
100
|
+
cleanup = mountDesignModes()
|
|
82
101
|
})
|
|
83
102
|
.catch(() => {
|
|
84
103
|
// Svelte UI not available — degrade gracefully
|
|
@@ -87,7 +106,9 @@ export default function StoryboardProvider({ flowName, sceneName, recordName, re
|
|
|
87
106
|
return () => cleanup?.()
|
|
88
107
|
}, [])
|
|
89
108
|
|
|
109
|
+
// Skip flow loading for canvas pages
|
|
90
110
|
const { data, error } = useMemo(() => {
|
|
111
|
+
if (canvasName) return { data: null, error: null }
|
|
91
112
|
try {
|
|
92
113
|
let flowData = loadFlow(activeFlowName)
|
|
93
114
|
|
|
@@ -105,7 +126,26 @@ export default function StoryboardProvider({ flowName, sceneName, recordName, re
|
|
|
105
126
|
} catch (err) {
|
|
106
127
|
return { data: null, error: err.message }
|
|
107
128
|
}
|
|
108
|
-
}, [activeFlowName, recordName, recordParam, params, prototypeName])
|
|
129
|
+
}, [canvasName, activeFlowName, recordName, recordParam, params, prototypeName])
|
|
130
|
+
|
|
131
|
+
// Canvas pages get their own rendering path — no flow data needed
|
|
132
|
+
if (canvasName) {
|
|
133
|
+
const canvasValue = {
|
|
134
|
+
data: null,
|
|
135
|
+
error: null,
|
|
136
|
+
loading: false,
|
|
137
|
+
flowName: null,
|
|
138
|
+
sceneName: null,
|
|
139
|
+
prototypeName: null,
|
|
140
|
+
}
|
|
141
|
+
return (
|
|
142
|
+
<StoryboardContext.Provider value={canvasValue}>
|
|
143
|
+
<Suspense fallback={null}>
|
|
144
|
+
<CanvasPageLazy name={canvasName} />
|
|
145
|
+
</Suspense>
|
|
146
|
+
</StoryboardContext.Provider>
|
|
147
|
+
)
|
|
148
|
+
}
|
|
109
149
|
|
|
110
150
|
const value = {
|
|
111
151
|
data,
|
package/src/vite/data-plugin.js
CHANGED
|
@@ -295,7 +295,7 @@ function readConfig(root) {
|
|
|
295
295
|
}
|
|
296
296
|
|
|
297
297
|
/**
|
|
298
|
-
* Read
|
|
298
|
+
* Read toolbar.config.json from @dfosco/storyboard-core.
|
|
299
299
|
* Returns the full config object with modes array.
|
|
300
300
|
* Falls back to hardcoded defaults if not found.
|
|
301
301
|
*/
|
|
@@ -311,9 +311,9 @@ function readModesConfig(root) {
|
|
|
311
311
|
|
|
312
312
|
// Try local workspace path first (monorepo), then node_modules
|
|
313
313
|
const candidates = [
|
|
314
|
-
path.resolve(root, 'packages/core/
|
|
314
|
+
path.resolve(root, 'packages/core/toolbar.config.json'),
|
|
315
315
|
path.resolve(root, 'packages/core/configs/modes.config.json'),
|
|
316
|
-
path.resolve(root, 'node_modules/@dfosco/storyboard-core/
|
|
316
|
+
path.resolve(root, 'node_modules/@dfosco/storyboard-core/toolbar.config.json'),
|
|
317
317
|
path.resolve(root, 'node_modules/@dfosco/storyboard-core/configs/modes.config.json'),
|
|
318
318
|
]
|
|
319
319
|
|