@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.
@@ -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
- return { name, suffix: 'canvas', ext: 'jsonl', folder: canvasFolderName || folderName, inferredRoute, id: toCanvasId(filePath), group }
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
- canvasGroups[parsed.name] = parsed.group
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[data.name]',
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[data.name] = canvases[data.name]',
789
- ' ? Object.assign({}, canvases[data.name], data.metadata)',
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 knownCanvasNames = new Set(Object.keys(buildResult.index.canvas || {}))
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?.name) {
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.name, ...(metadata ? { metadata } : {}) },
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 name = parsed.name
1011
+ const canvasId = parsed.id || parsed.name
1007
1012
  if (eventType === 'unlink') {
1008
1013
  const timer = setTimeout(() => {
1009
- pendingCanvasUnlinks.delete(name)
1010
- knownCanvasNames.delete(name)
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(name, timer)
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(name)
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(name)
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 (knownCanvasNames.has(name)) {
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
- knownCanvasNames.add(name)
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.name).toBe('test-canvas')
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.name).toBe('new-canvas')
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.name).toBe('doomed-canvas')
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.name).toBe('saved-canvas')
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
- }