@dfosco/storyboard-react 4.0.0-beta.24 → 4.0.0-beta.26
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 +3 -3
- package/src/canvas/CanvasPage.bridge.test.jsx +8 -8
- package/src/canvas/CanvasPage.dragdrop.test.jsx +8 -8
- package/src/canvas/CanvasPage.jsx +170 -103
- package/src/canvas/CanvasPage.module.css +7 -0
- package/src/canvas/CanvasPage.multiselect.test.jsx +11 -11
- package/src/canvas/CanvasToolbar.jsx +2 -2
- package/src/canvas/canvasApi.js +12 -10
- package/src/canvas/useCanvas.js +9 -8
- package/src/canvas/widgets/ComponentWidget.jsx +21 -6
- package/src/canvas/widgets/ComponentWidget.module.css +5 -0
- package/src/canvas/widgets/FigmaEmbed.jsx +44 -15
- package/src/canvas/widgets/PrototypeEmbed.jsx +210 -208
- package/src/canvas/widgets/PrototypeEmbed.module.css +61 -19
- package/src/canvas/widgets/StoryWidget.jsx +135 -171
- package/src/canvas/widgets/StoryWidget.module.css +38 -28
- package/src/canvas/widgets/WidgetChrome.jsx +3 -2
- package/src/canvas/widgets/embedInteraction.test.jsx +86 -4
- package/src/canvas/widgets/embedTheme.js +20 -0
- package/src/canvas/widgets/iframeDevLogs.js +49 -0
- package/src/canvas/widgets/iframeDevLogs.test.jsx +81 -0
- package/src/canvas/widgets/snapshotDisplay.test.jsx +289 -0
- package/src/canvas/widgets/useSnapshotCapture.js +258 -0
- package/src/canvas/widgets/useSnapshotCapture.test.jsx +225 -0
- package/src/context.jsx +14 -14
- package/src/story/StoryPage.jsx +7 -7
- package/src/vite/data-plugin.js +25 -20
- package/src/vite/data-plugin.test.js +4 -4
- package/src/canvas/widgets/useViewportEntry.js +0 -93
package/src/vite/data-plugin.js
CHANGED
|
@@ -78,7 +78,9 @@ function parseDataFile(filePath) {
|
|
|
78
78
|
// Derive group: canvases sharing a directory form a group
|
|
79
79
|
const slashIdx = name.lastIndexOf('/')
|
|
80
80
|
const group = canvasFolderName || (slashIdx > 0 ? name.substring(0, slashIdx) : null)
|
|
81
|
-
|
|
81
|
+
// Extract a relative path for toCanvasId (it expects src/canvas/... or src/prototypes/...)
|
|
82
|
+
const canvasIdInput = normalized.replace(/^.*?(src\/(?:canvas|prototypes)\/)/, '$1')
|
|
83
|
+
return { name, suffix: 'canvas', ext: 'jsonl', folder: canvasFolderName || folderName, inferredRoute, id: toCanvasId(canvasIdInput), group }
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
// Handle canvas .meta.json files
|
|
@@ -404,8 +406,10 @@ function buildIndex(root) {
|
|
|
404
406
|
}
|
|
405
407
|
|
|
406
408
|
// Track canvas groups (canvases sharing a folder prefix)
|
|
409
|
+
// Use canonical ID as key to match the canvas index
|
|
407
410
|
if (parsed.suffix === 'canvas' && parsed.group) {
|
|
408
|
-
|
|
411
|
+
const groupKey = parsed.id || parsed.name
|
|
412
|
+
canvasGroups[groupKey] = parsed.group
|
|
409
413
|
}
|
|
410
414
|
|
|
411
415
|
// Track inferred routes for stories
|
|
@@ -781,12 +785,13 @@ function generateModule({ index, protoFolders, flowRoutes, canvasRoutes, canvasA
|
|
|
781
785
|
'if (import.meta.hot) {',
|
|
782
786
|
' import.meta.hot.on("storyboard:canvas-file-changed", (data) => {',
|
|
783
787
|
' if (!data) return',
|
|
788
|
+
' const id = data.canvasId || data.name',
|
|
784
789
|
' if (data.removed) {',
|
|
785
|
-
' delete canvases[
|
|
790
|
+
' delete canvases[id]',
|
|
786
791
|
' } else if (data.metadata) {',
|
|
787
792
|
' // Merge into existing entry to preserve build-time fields (_jsxModule, _jsxImport, etc.)',
|
|
788
|
-
' canvases[
|
|
789
|
-
' ? Object.assign({}, canvases[
|
|
793
|
+
' canvases[id] = canvases[id]',
|
|
794
|
+
' ? Object.assign({}, canvases[id], data.metadata)',
|
|
790
795
|
' : data.metadata',
|
|
791
796
|
' }',
|
|
792
797
|
' init({ flows, objects, records, prototypes, folders, canvases, stories })',
|
|
@@ -911,7 +916,7 @@ export default function storyboardDataPlugin() {
|
|
|
911
916
|
// Watch for data file changes in dev mode
|
|
912
917
|
const watcher = server.watcher
|
|
913
918
|
if (!buildResult) buildResult = buildIndex(root)
|
|
914
|
-
const
|
|
919
|
+
const knownCanvasIds = new Set(Object.keys(buildResult.index.canvas || {}))
|
|
915
920
|
const pendingCanvasUnlinks = new Map()
|
|
916
921
|
|
|
917
922
|
const triggerFullReload = () => {
|
|
@@ -958,12 +963,12 @@ export default function storyboardDataPlugin() {
|
|
|
958
963
|
// viewfinder can react in place.
|
|
959
964
|
if (/\.canvas\.jsonl$/.test(normalized)) {
|
|
960
965
|
const parsed = parseDataFile(filePath)
|
|
961
|
-
if (parsed?.suffix === 'canvas' && parsed?.
|
|
966
|
+
if (parsed?.suffix === 'canvas' && parsed?.id) {
|
|
962
967
|
const metadata = readCanvasMetadata(filePath, parsed)
|
|
963
968
|
server.ws.send({
|
|
964
969
|
type: 'custom',
|
|
965
970
|
event: 'storyboard:canvas-file-changed',
|
|
966
|
-
data: { name: parsed.
|
|
971
|
+
data: { canvasId: parsed.id, name: parsed.id, ...(metadata ? { metadata } : {}) },
|
|
967
972
|
})
|
|
968
973
|
}
|
|
969
974
|
softInvalidate()
|
|
@@ -1003,53 +1008,53 @@ export default function storyboardDataPlugin() {
|
|
|
1003
1008
|
// Treat canvas add/unlink as runtime data updates and never full-reload
|
|
1004
1009
|
// from watcher events. Canvas pages sync from disk via custom WS events.
|
|
1005
1010
|
if (parsed?.suffix === 'canvas') {
|
|
1006
|
-
const
|
|
1011
|
+
const canvasId = parsed.id || parsed.name
|
|
1007
1012
|
if (eventType === 'unlink') {
|
|
1008
1013
|
const timer = setTimeout(() => {
|
|
1009
|
-
pendingCanvasUnlinks.delete(
|
|
1010
|
-
|
|
1014
|
+
pendingCanvasUnlinks.delete(canvasId)
|
|
1015
|
+
knownCanvasIds.delete(canvasId)
|
|
1011
1016
|
server.ws.send({
|
|
1012
1017
|
type: 'custom',
|
|
1013
1018
|
event: 'storyboard:canvas-file-changed',
|
|
1014
|
-
data: { name, removed: true },
|
|
1019
|
+
data: { canvasId, name: canvasId, removed: true },
|
|
1015
1020
|
})
|
|
1016
1021
|
softInvalidate()
|
|
1017
1022
|
}, 1500)
|
|
1018
|
-
pendingCanvasUnlinks.set(
|
|
1023
|
+
pendingCanvasUnlinks.set(canvasId, timer)
|
|
1019
1024
|
return
|
|
1020
1025
|
}
|
|
1021
1026
|
|
|
1022
1027
|
if (eventType === 'add') {
|
|
1023
1028
|
const metadata = readCanvasMetadata(filePath, parsed)
|
|
1024
|
-
const pending = pendingCanvasUnlinks.get(
|
|
1029
|
+
const pending = pendingCanvasUnlinks.get(canvasId)
|
|
1025
1030
|
if (pending) {
|
|
1026
1031
|
// unlink+add pair = in-place save (atomic write), not a real remove
|
|
1027
1032
|
clearTimeout(pending)
|
|
1028
|
-
pendingCanvasUnlinks.delete(
|
|
1033
|
+
pendingCanvasUnlinks.delete(canvasId)
|
|
1029
1034
|
server.ws.send({
|
|
1030
1035
|
type: 'custom',
|
|
1031
1036
|
event: 'storyboard:canvas-file-changed',
|
|
1032
|
-
data: { name, ...(metadata ? { metadata } : {}) },
|
|
1037
|
+
data: { canvasId, name: canvasId, ...(metadata ? { metadata } : {}) },
|
|
1033
1038
|
})
|
|
1034
1039
|
softInvalidate()
|
|
1035
1040
|
return
|
|
1036
1041
|
}
|
|
1037
1042
|
|
|
1038
|
-
if (
|
|
1043
|
+
if (knownCanvasIds.has(canvasId)) {
|
|
1039
1044
|
server.ws.send({
|
|
1040
1045
|
type: 'custom',
|
|
1041
1046
|
event: 'storyboard:canvas-file-changed',
|
|
1042
|
-
data: { name, ...(metadata ? { metadata } : {}) },
|
|
1047
|
+
data: { canvasId, name: canvasId, ...(metadata ? { metadata } : {}) },
|
|
1043
1048
|
})
|
|
1044
1049
|
softInvalidate()
|
|
1045
1050
|
return
|
|
1046
1051
|
}
|
|
1047
1052
|
|
|
1048
|
-
|
|
1053
|
+
knownCanvasIds.add(canvasId)
|
|
1049
1054
|
server.ws.send({
|
|
1050
1055
|
type: 'custom',
|
|
1051
1056
|
event: 'storyboard:canvas-file-changed',
|
|
1052
|
-
data: { name, ...(metadata ? { metadata } : {}) },
|
|
1057
|
+
data: { canvasId, name: canvasId, ...(metadata ? { metadata } : {}) },
|
|
1053
1058
|
})
|
|
1054
1059
|
softInvalidate()
|
|
1055
1060
|
return
|
|
@@ -902,7 +902,7 @@ describe('canvas watcher behavior', () => {
|
|
|
902
902
|
|
|
903
903
|
expect(customEvents.length).toBe(1)
|
|
904
904
|
expect(customEvents[0].event).toBe('storyboard:canvas-file-changed')
|
|
905
|
-
expect(customEvents[0].data.
|
|
905
|
+
expect(customEvents[0].data.canvasId).toBe('test-canvas')
|
|
906
906
|
expect(fullReloads.length).toBe(0)
|
|
907
907
|
|
|
908
908
|
// Should have invalidated the virtual module
|
|
@@ -939,7 +939,7 @@ describe('canvas watcher behavior', () => {
|
|
|
939
939
|
const fullReloads = server.wsSent.filter(m => m.type === 'full-reload')
|
|
940
940
|
|
|
941
941
|
expect(customEvents.length).toBe(1)
|
|
942
|
-
expect(customEvents[0].data.
|
|
942
|
+
expect(customEvents[0].data.canvasId).toBe('new-canvas')
|
|
943
943
|
expect(customEvents[0].data.metadata).toBeDefined()
|
|
944
944
|
expect(fullReloads.length).toBe(0)
|
|
945
945
|
expect(server.invalidatedModules).toContain(RESOLVED_ID)
|
|
@@ -963,7 +963,7 @@ describe('canvas watcher behavior', () => {
|
|
|
963
963
|
|
|
964
964
|
const customEvents = server.wsSent.filter(m => m.type === 'custom')
|
|
965
965
|
expect(customEvents.length).toBe(1)
|
|
966
|
-
expect(customEvents[0].data.
|
|
966
|
+
expect(customEvents[0].data.canvasId).toBe('doomed-canvas')
|
|
967
967
|
expect(customEvents[0].data.removed).toBe(true)
|
|
968
968
|
expect(server.invalidatedModules).toContain(RESOLVED_ID)
|
|
969
969
|
})
|
|
@@ -985,7 +985,7 @@ describe('canvas watcher behavior', () => {
|
|
|
985
985
|
// Should have sent one event immediately (the add cancelling the unlink)
|
|
986
986
|
const customEvents = server.wsSent.filter(m => m.type === 'custom')
|
|
987
987
|
expect(customEvents.length).toBe(1)
|
|
988
|
-
expect(customEvents[0].data.
|
|
988
|
+
expect(customEvents[0].data.canvasId).toBe('saved-canvas')
|
|
989
989
|
expect(customEvents[0].data.removed).toBeUndefined()
|
|
990
990
|
expect(server.invalidatedModules).toContain(RESOLVED_ID)
|
|
991
991
|
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback, useRef } from 'react'
|
|
2
|
-
import { getFlag } from '@dfosco/storyboard-core'
|
|
3
|
-
|
|
4
|
-
function devLog(...args) {
|
|
5
|
-
try { if (getFlag('dev-logs')) console.log('[canvas:iframe-queue]', ...args) } catch { /* flag system not initialized */ }
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Sequential iframe loading queue.
|
|
10
|
-
*
|
|
11
|
-
* Embed widgets (prototype, story) that lack snapshots call `requestSlot()`
|
|
12
|
-
* which returns a promise that resolves when it's their turn to load.
|
|
13
|
-
* Only one iframe loads at a time — the next starts when the previous
|
|
14
|
-
* calls the returned `release()` function (or on timeout).
|
|
15
|
-
*
|
|
16
|
-
* This prevents the "iframe stampede" when many embeds lack snapshots
|
|
17
|
-
* and would otherwise all try to load simultaneously.
|
|
18
|
-
*/
|
|
19
|
-
const SLOT_TIMEOUT = 15_000
|
|
20
|
-
|
|
21
|
-
let _queue = []
|
|
22
|
-
let _active = false
|
|
23
|
-
|
|
24
|
-
function processQueue() {
|
|
25
|
-
if (_active || _queue.length === 0) return
|
|
26
|
-
_active = true
|
|
27
|
-
const { resolve, label } = _queue.shift()
|
|
28
|
-
devLog(`slot granted → ${label} (${_queue.length} queued)`)
|
|
29
|
-
|
|
30
|
-
let released = false
|
|
31
|
-
const release = () => {
|
|
32
|
-
if (released) return
|
|
33
|
-
released = true
|
|
34
|
-
_active = false
|
|
35
|
-
devLog(`slot released ← ${label}`)
|
|
36
|
-
processQueue()
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Safety timeout — if the iframe never signals load, release after 15s
|
|
40
|
-
setTimeout(release, SLOT_TIMEOUT)
|
|
41
|
-
resolve(release)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function requestSlot(label = '?') {
|
|
45
|
-
return new Promise((resolve) => {
|
|
46
|
-
_queue.push({ resolve, label })
|
|
47
|
-
devLog(`queued ${label} (position ${_queue.length})`)
|
|
48
|
-
processQueue()
|
|
49
|
-
})
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Hook for embed widgets that need sequential iframe loading.
|
|
54
|
-
*
|
|
55
|
-
* When the widget has a usable snapshot, this is a no-op (returns ready immediately).
|
|
56
|
-
* When no snapshot exists, it queues the widget and returns `ready: true` when
|
|
57
|
-
* it's this widget's turn.
|
|
58
|
-
*
|
|
59
|
-
* @param {boolean} hasUsableSnapshot - whether the widget has a working snapshot
|
|
60
|
-
* @param {string} [label] - debug label for dev logs
|
|
61
|
-
* @returns {{ ready: boolean, releaseSlot: Function }}
|
|
62
|
-
*/
|
|
63
|
-
export function useIframeQueue(hasUsableSnapshot, label = '?') {
|
|
64
|
-
const [ready, setReady] = useState(hasUsableSnapshot)
|
|
65
|
-
const releaseRef = useRef(null)
|
|
66
|
-
|
|
67
|
-
useEffect(() => {
|
|
68
|
-
if (hasUsableSnapshot || ready) return
|
|
69
|
-
|
|
70
|
-
let cancelled = false
|
|
71
|
-
requestSlot(label).then((release) => {
|
|
72
|
-
if (cancelled) {
|
|
73
|
-
release()
|
|
74
|
-
return
|
|
75
|
-
}
|
|
76
|
-
releaseRef.current = release
|
|
77
|
-
setReady(true)
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
return () => {
|
|
81
|
-
cancelled = true
|
|
82
|
-
releaseRef.current?.()
|
|
83
|
-
}
|
|
84
|
-
}, [hasUsableSnapshot]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
85
|
-
|
|
86
|
-
// Release the slot when the component unmounts
|
|
87
|
-
const releaseSlot = useCallback(() => {
|
|
88
|
-
releaseRef.current?.()
|
|
89
|
-
releaseRef.current = null
|
|
90
|
-
}, [])
|
|
91
|
-
|
|
92
|
-
return { ready, releaseSlot }
|
|
93
|
-
}
|