@dfosco/storyboard-react 4.0.0-beta.24 → 4.0.0-beta.25

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 CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@dfosco/storyboard-react",
3
- "version": "4.0.0-beta.24",
3
+ "version": "4.0.0-beta.25",
4
4
  "type": "module",
5
5
  "dependencies": {
6
- "@dfosco/storyboard-core": "4.0.0-beta.24",
7
- "@dfosco/tiny-canvas": "4.0.0-beta.24",
6
+ "@dfosco/storyboard-core": "4.0.0-beta.25",
7
+ "@dfosco/tiny-canvas": "4.0.0-beta.25",
8
8
  "@neodrag/react": "^2.3.1",
9
9
  "glob": "^11.0.0",
10
10
  "jsonc-parser": "^3.3.1",
@@ -105,11 +105,11 @@ describe('CanvasPage canvas bridge', () => {
105
105
  document.addEventListener('storyboard:canvas:mounted', mountedHandler)
106
106
  document.addEventListener('storyboard:canvas:status', statusHandler)
107
107
 
108
- const { unmount } = render(<CanvasPage name="design-overview" />)
108
+ const { unmount } = render(<CanvasPage canvasId="design-overview" />)
109
109
 
110
110
  expect(window.__storyboardCanvasBridgeState).toEqual({
111
111
  active: true,
112
- name: 'design-overview',
112
+ canvasId: 'design-overview',
113
113
  zoom: 100,
114
114
  })
115
115
  expect(mountedHandler).toHaveBeenCalled()
@@ -118,7 +118,7 @@ describe('CanvasPage canvas bridge', () => {
118
118
  expect(statusHandler).toHaveBeenCalled()
119
119
  expect(statusHandler.mock.calls.at(-1)?.[0]?.detail).toEqual({
120
120
  active: true,
121
- name: 'design-overview',
121
+ canvasId: 'design-overview',
122
122
  zoom: 100,
123
123
  })
124
124
 
@@ -132,13 +132,13 @@ describe('CanvasPage canvas bridge', () => {
132
132
  const unmountedHandler = vi.fn()
133
133
  document.addEventListener('storyboard:canvas:unmounted', unmountedHandler)
134
134
 
135
- const { unmount } = render(<CanvasPage name="design-overview" />)
135
+ const { unmount } = render(<CanvasPage canvasId="design-overview" />)
136
136
  unmount()
137
137
 
138
138
  expect(unmountedHandler).toHaveBeenCalled()
139
139
  expect(window.__storyboardCanvasBridgeState).toEqual({
140
140
  active: false,
141
- name: '',
141
+ canvasId: '',
142
142
  zoom: 100,
143
143
  })
144
144
 
@@ -146,7 +146,7 @@ describe('CanvasPage canvas bridge', () => {
146
146
  })
147
147
 
148
148
  it.skip('persists dragged JSON widgets and JSX sources to canvas JSONL via update API', async () => {
149
- render(<CanvasPage name="design-overview" />)
149
+ render(<CanvasPage canvasId="design-overview" />)
150
150
 
151
151
  fireEvent.click(screen.getByTestId('drag-widget'))
152
152
  // Flush the promise-based write queue
@@ -179,7 +179,7 @@ describe('CanvasPage canvas bridge', () => {
179
179
  })
180
180
 
181
181
  it.skip('clamps negative drag positions to zero', async () => {
182
- render(<CanvasPage name="design-overview" />)
182
+ render(<CanvasPage canvasId="design-overview" />)
183
183
 
184
184
  fireEvent.click(screen.getByTestId('drag-widget-negative'))
185
185
  await act(async () => { await new Promise((r) => setTimeout(r, 0)) })
@@ -243,7 +243,7 @@ describe('canvas target fallback', () => {
243
243
  localStorage.setItem('sb-color-scheme', 'dark')
244
244
  document.documentElement.setAttribute('data-sb-canvas-theme', 'dark')
245
245
 
246
- render(<CanvasPage name="design-overview" />)
246
+ render(<CanvasPage canvasId="design-overview" />)
247
247
 
248
248
  const scroll = document.querySelector('[data-storyboard-canvas-scroll]')
249
249
  const jsxWidget = document.getElementById('jsx-PrimaryButtons')
@@ -123,7 +123,7 @@ describe('CanvasPage image drag-and-drop', () => {
123
123
  })
124
124
 
125
125
  it('allows drop by preventing default on dragover with Files', () => {
126
- render(<CanvasPage name="test-canvas" />)
126
+ render(<CanvasPage canvasId="test-canvas" />)
127
127
  const scrollContainer = document.querySelector('[data-storyboard-canvas-scroll]')
128
128
 
129
129
  const dragOverEvent = new Event('dragover', { bubbles: true, cancelable: true })
@@ -137,7 +137,7 @@ describe('CanvasPage image drag-and-drop', () => {
137
137
  })
138
138
 
139
139
  it('ignores dragover without Files type (internal widget drag)', () => {
140
- render(<CanvasPage name="test-canvas" />)
140
+ render(<CanvasPage canvasId="test-canvas" />)
141
141
  const scrollContainer = document.querySelector('[data-storyboard-canvas-scroll]')
142
142
 
143
143
  const dragOverEvent = new Event('dragover', { bubbles: true, cancelable: true })
@@ -150,7 +150,7 @@ describe('CanvasPage image drag-and-drop', () => {
150
150
  })
151
151
 
152
152
  it('uploads image and creates widget on drop', async () => {
153
- render(<CanvasPage name="test-canvas" />)
153
+ render(<CanvasPage canvasId="test-canvas" />)
154
154
  const scrollContainer = document.querySelector('[data-storyboard-canvas-scroll]')
155
155
 
156
156
  const imageFile = createMockImageFile('photo.png', 'image/png')
@@ -199,7 +199,7 @@ describe('CanvasPage image drag-and-drop', () => {
199
199
  })
200
200
 
201
201
  it('ignores non-image files but prevents browser default', async () => {
202
- render(<CanvasPage name="test-canvas" />)
202
+ render(<CanvasPage canvasId="test-canvas" />)
203
203
  const scrollContainer = document.querySelector('[data-storyboard-canvas-scroll]')
204
204
 
205
205
  const textFile = new File(['text content'], 'readme.txt', { type: 'text/plain' })
@@ -225,7 +225,7 @@ describe('CanvasPage image drag-and-drop', () => {
225
225
  })
226
226
 
227
227
  it('processes multiple image files on drop', async () => {
228
- render(<CanvasPage name="test-canvas" />)
228
+ render(<CanvasPage canvasId="test-canvas" />)
229
229
  const scrollContainer = document.querySelector('[data-storyboard-canvas-scroll]')
230
230
 
231
231
  const image1 = createMockImageFile('photo1.png', 'image/png')
@@ -253,7 +253,7 @@ describe('CanvasPage image drag-and-drop', () => {
253
253
  })
254
254
 
255
255
  it('ignores drop without Files type', async () => {
256
- render(<CanvasPage name="test-canvas" />)
256
+ render(<CanvasPage canvasId="test-canvas" />)
257
257
  const scrollContainer = document.querySelector('[data-storyboard-canvas-scroll]')
258
258
 
259
259
  const dropEvent = new Event('drop', { bubbles: true, cancelable: true })
@@ -273,7 +273,7 @@ describe('CanvasPage image drag-and-drop', () => {
273
273
  const originalSnapToGrid = mockCanvas.snapToGrid
274
274
  mockCanvas.snapToGrid = true
275
275
 
276
- render(<CanvasPage name="test-canvas" />)
276
+ render(<CanvasPage canvasId="test-canvas" />)
277
277
  const scrollContainer = document.querySelector('[data-storyboard-canvas-scroll]')
278
278
 
279
279
  const imageFile = createMockImageFile()
@@ -311,7 +311,7 @@ describe('CanvasPage image drag-and-drop', () => {
311
311
  const originalSnapToGrid = mockCanvas.snapToGrid
312
312
  mockCanvas.snapToGrid = false
313
313
 
314
- render(<CanvasPage name="test-canvas" />)
314
+ render(<CanvasPage canvasId="test-canvas" />)
315
315
  const scrollContainer = document.querySelector('[data-storyboard-canvas-scroll]')
316
316
 
317
317
  const imageFile = createMockImageFile()
@@ -106,18 +106,18 @@ function debounce(fn, ms) {
106
106
  }
107
107
 
108
108
  /** Per-canvas viewport state persistence (zoom + scroll position). */
109
- function getViewportStorageKey(canvasName) {
110
- return `sb-canvas-viewport:${canvasName}`
109
+ function getViewportStorageKey(canvasId) {
110
+ return `sb-canvas-viewport:${canvasId}`
111
111
  }
112
112
 
113
- function loadViewportState(canvasName) {
113
+ function loadViewportState(canvasId) {
114
114
  try {
115
- const raw = localStorage.getItem(getViewportStorageKey(canvasName))
115
+ const raw = localStorage.getItem(getViewportStorageKey(canvasId))
116
116
  if (!raw) return null
117
117
  const state = JSON.parse(raw)
118
118
  const timestamp = typeof state.timestamp === 'number' ? state.timestamp : 0
119
119
  if (Date.now() - timestamp > VIEWPORT_TTL_MS) {
120
- localStorage.removeItem(getViewportStorageKey(canvasName))
120
+ localStorage.removeItem(getViewportStorageKey(canvasId))
121
121
  return null
122
122
  }
123
123
  return {
@@ -128,9 +128,9 @@ function loadViewportState(canvasName) {
128
128
  } catch { return null }
129
129
  }
130
130
 
131
- function saveViewportState(canvasName, state) {
131
+ function saveViewportState(canvasId, state) {
132
132
  try {
133
- localStorage.setItem(getViewportStorageKey(canvasName), JSON.stringify({
133
+ localStorage.setItem(getViewportStorageKey(canvasId), JSON.stringify({
134
134
  ...state,
135
135
  timestamp: Date.now(),
136
136
  }))
@@ -321,24 +321,24 @@ function ChromeWrappedWidget({
321
321
  * Generic canvas page component.
322
322
  * Reads canvas data from the index and renders all widgets on a draggable surface.
323
323
  *
324
- * @param {{ name: string }} props - Canvas name as indexed by the data plugin
324
+ * @param {{ canvasId: string }} props - Canvas name as indexed by the data plugin
325
325
  */
326
- export default function CanvasPage({ name, siblingPages = [], canvasMeta = null }) {
327
- const { canvas, jsxExports, jsxError, loading } = useCanvas(name)
326
+ export default function CanvasPage({ canvasId, siblingPages = [], canvasMeta = null }) {
327
+ const { canvas, jsxExports, jsxError, loading } = useCanvas(canvasId)
328
328
  const isLocalDev = typeof window !== 'undefined' && window.__SB_LOCAL_DEV__ === true && !new URLSearchParams(window.location.search).has('prodMode')
329
329
 
330
330
  // Local mutable copy of widgets for instant UI updates
331
331
  const [localWidgets, setLocalWidgets] = useState(canvas?.widgets ?? null)
332
332
  const [trackedCanvas, setTrackedCanvas] = useState(canvas)
333
333
  const [selectedWidgetIds, setSelectedWidgetIds] = useState(() => new Set())
334
- const initialViewport = loadViewportState(name)
334
+ const initialViewport = loadViewportState(canvasId)
335
335
  const [zoom, setZoom] = useState(initialViewport?.zoom ?? 100)
336
336
  const zoomRef = useRef(initialViewport?.zoom ?? 100)
337
337
  const scrollRef = useRef(null)
338
338
  const pendingScrollRestore = useRef(initialViewport)
339
339
  // Gate viewport persistence until initial positioning is complete.
340
- // Tracks which canvas name was last initialized — save effects only
341
- // write when this matches `name`, preventing cross-canvas corruption.
340
+ // Tracks which canvasId was last initialized — save effects only
341
+ // write when this matches `canvasId`, preventing cross-canvas corruption.
342
342
  const viewportInitName = useRef(null)
343
343
  const [localSources, setLocalSources] = useState(canvas?.sources ?? [])
344
344
  const [canvasTheme, setCanvasTheme] = useState(() => resolveCanvasThemeFromStorage())
@@ -482,7 +482,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
482
482
  undoRedo.reset()
483
483
  // Block saves until the new canvas's viewport is fully restored.
484
484
  viewportInitName.current = null
485
- const newViewport = loadViewportState(name)
485
+ const newViewport = loadViewportState(canvasId)
486
486
  pendingScrollRestore.current = newViewport
487
487
  // Restore zoom from the new canvas's saved state
488
488
  const newZoom = newViewport?.zoom ?? 100
@@ -492,8 +492,8 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
492
492
 
493
493
  // Debounced save to server
494
494
  const debouncedSave = useRef(
495
- debounce((canvasName, widgets) => {
496
- updateCanvas(canvasName, { widgets }).catch((err) =>
495
+ debounce((canvasId, widgets) => {
496
+ updateCanvas(canvasId, { widgets }).catch((err) =>
497
497
  console.error('[canvas] Failed to save:', err)
498
498
  )
499
499
  }, 2000)
@@ -512,20 +512,20 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
512
512
  const next = prev.map((w) =>
513
513
  w.id === widgetId ? { ...w, props: { ...w.props, ...snapped } } : w
514
514
  )
515
- debouncedSave(name, next)
515
+ debouncedSave(canvasId, next)
516
516
  return next
517
517
  })
518
- }, [name, debouncedSave, undoRedo, snapEnabled, snapGridSize])
518
+ }, [canvasId, debouncedSave, undoRedo, snapEnabled, snapGridSize])
519
519
 
520
520
  const handleWidgetRemove = useCallback((widgetId) => {
521
521
  undoRedo.snapshot(stateRef.current, 'remove', widgetId)
522
522
  setLocalWidgets((prev) => prev ? prev.filter((w) => w.id !== widgetId) : prev)
523
523
  queueWrite(() =>
524
- removeWidgetApi(name, widgetId).catch((err) =>
524
+ removeWidgetApi(canvasId, widgetId).catch((err) =>
525
525
  console.error('[canvas] Failed to remove widget:', err)
526
526
  )
527
527
  )
528
- }, [name, undoRedo])
528
+ }, [canvasId, undoRedo])
529
529
 
530
530
  const handleWidgetCopy = useCallback(async (widget) => {
531
531
  // Find the next free offset — check how many copies already exist at +n*40
@@ -541,7 +541,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
541
541
  const position = { x: baseX + n * 40, y: baseY + n * 40 }
542
542
  try {
543
543
  undoRedo.snapshot(stateRef.current, 'add')
544
- const result = await addWidgetApi(name, {
544
+ const result = await addWidgetApi(canvasId, {
545
545
  type: widget.type,
546
546
  props: { ...widget.props },
547
547
  position,
@@ -552,11 +552,11 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
552
552
  } catch (err) {
553
553
  console.error('[canvas] Failed to copy widget:', err)
554
554
  }
555
- }, [name, localWidgets, undoRedo])
555
+ }, [canvasId, localWidgets, undoRedo])
556
556
 
557
557
  const debouncedSourceSave = useRef(
558
- debounce((canvasName, sources) => {
559
- updateCanvas(canvasName, { sources }).catch((err) =>
558
+ debounce((canvasId, sources) => {
559
+ updateCanvas(canvasId, { sources }).catch((err) =>
560
560
  console.error('[canvas] Failed to save sources:', err)
561
561
  )
562
562
  }, 2000)
@@ -574,10 +574,10 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
574
574
  const next = current.some((s) => s?.export === exportName)
575
575
  ? current.map((s) => (s?.export === exportName ? { ...s, ...snapped } : s))
576
576
  : [...current, { export: exportName, ...snapped }]
577
- debouncedSourceSave(name, next)
577
+ debouncedSourceSave(canvasId, next)
578
578
  return next
579
579
  })
580
- }, [name, debouncedSourceSave, undoRedo, snapEnabled, snapGridSize])
580
+ }, [canvasId, debouncedSourceSave, undoRedo, snapEnabled, snapGridSize])
581
581
 
582
582
  const handleItemDragEnd = useCallback((dragId, position) => {
583
583
  if (!dragId || !position) {
@@ -629,7 +629,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
629
629
  return w
630
630
  })
631
631
  queueWrite(() =>
632
- updateCanvas(name, { widgets: next }).catch((err) =>
632
+ updateCanvas(canvasId, { widgets: next }).catch((err) =>
633
633
  console.error('[canvas] Failed to save multi-move:', err)
634
634
  )
635
635
  )
@@ -661,7 +661,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
661
661
  })
662
662
  if (changed) {
663
663
  queueWrite(() =>
664
- updateCanvas(name, { sources: next }).catch((err) =>
664
+ updateCanvas(canvasId, { sources: next }).catch((err) =>
665
665
  console.error('[canvas] Failed to save multi-move sources:', err)
666
666
  )
667
667
  )
@@ -680,7 +680,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
680
680
  ? current.map((s) => (s?.export === sourceExport ? { ...s, position: rounded } : s))
681
681
  : [...current, { export: sourceExport, position: rounded }]
682
682
  queueWrite(() =>
683
- updateCanvas(name, { sources: next }).catch((err) =>
683
+ updateCanvas(canvasId, { sources: next }).catch((err) =>
684
684
  console.error('[canvas] Failed to save source position:', err)
685
685
  )
686
686
  )
@@ -696,13 +696,13 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
696
696
  w.id === dragId ? { ...w, position: rounded } : w
697
697
  )
698
698
  queueWrite(() =>
699
- updateCanvas(name, { widgets: next }).catch((err) =>
699
+ updateCanvas(canvasId, { widgets: next }).catch((err) =>
700
700
  console.error('[canvas] Failed to save widget position:', err)
701
701
  )
702
702
  )
703
703
  return next
704
704
  })
705
- }, [name, undoRedo, debouncedSave, transitionPeers, clearDragPreview])
705
+ }, [canvasId, undoRedo, debouncedSave, transitionPeers, clearDragPreview])
706
706
 
707
707
  useEffect(() => {
708
708
  zoomRef.current = zoom
@@ -739,8 +739,8 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
739
739
  }
740
740
  }
741
741
  // Allow save effects for this canvas now that positioning is settled.
742
- viewportInitName.current = name
743
- }, [name, loading])
742
+ viewportInitName.current = canvasId
743
+ }, [canvasId, loading])
744
744
 
745
745
  // Center on a specific widget if `?widget=<id>` is in the URL
746
746
  useEffect(() => {
@@ -795,23 +795,23 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
795
795
  // operations (applyZoom, zoom-to-fit) adjust scroll AFTER setZoom, so the
796
796
  // scroll values would be stale at this point.
797
797
  useEffect(() => {
798
- if (viewportInitName.current !== name) return
798
+ if (viewportInitName.current !== canvasId) return
799
799
  const el = scrollRef.current
800
800
  // Read current scroll so the zoom entry doesn't zero-out position,
801
801
  // but the authoritative scroll save comes from the scroll handler.
802
- saveViewportState(name, {
802
+ saveViewportState(canvasId, {
803
803
  zoom,
804
804
  scrollLeft: el?.scrollLeft ?? 0,
805
805
  scrollTop: el?.scrollTop ?? 0,
806
806
  })
807
- }, [name, zoom])
807
+ }, [canvasId, zoom])
808
808
 
809
809
  useEffect(() => {
810
810
  const el = scrollRef.current
811
811
  if (!el) return
812
812
  const saveNow = () => {
813
- if (viewportInitName.current !== name) return
814
- saveViewportState(name, {
813
+ if (viewportInitName.current !== canvasId) return
814
+ saveViewportState(canvasId, {
815
815
  zoom: zoomRef.current,
816
816
  scrollLeft: el.scrollLeft,
817
817
  scrollTop: el.scrollTop,
@@ -819,7 +819,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
819
819
  }
820
820
  const debouncedScrollSave = debounce(saveNow, 150)
821
821
  function handleScroll() {
822
- if (viewportInitName.current !== name) return
822
+ if (viewportInitName.current !== canvasId) return
823
823
  debouncedScrollSave()
824
824
  }
825
825
  el.addEventListener('scroll', handleScroll, { passive: true })
@@ -839,7 +839,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
839
839
  // beforeunload doesn't fire).
840
840
  saveNow()
841
841
  }
842
- }, [name, loading])
842
+ }, [canvasId, loading])
843
843
 
844
844
  /**
845
845
  * Zoom to a new level, anchoring on an optional client-space point.
@@ -880,8 +880,8 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
880
880
  // Persist after both zoom and scroll are settled (the zoom effect
881
881
  // fires inside flushSync before the scroll adjustment above, so it
882
882
  // would capture stale scroll values).
883
- if (viewportInitName.current === name) {
884
- saveViewportState(name, {
883
+ if (viewportInitName.current === canvasId) {
884
+ saveViewportState(canvasId, {
885
885
  zoom: clampedZoom,
886
886
  scrollLeft: el.scrollLeft,
887
887
  scrollTop: el.scrollTop,
@@ -891,13 +891,13 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
891
891
 
892
892
  // Signal canvas mount/unmount to CoreUIBar
893
893
  useEffect(() => {
894
- window[CANVAS_BRIDGE_STATE_KEY] = { active: true, name, zoom: zoomRef.current }
894
+ window[CANVAS_BRIDGE_STATE_KEY] = { active: true, canvasId, zoom: zoomRef.current }
895
895
  document.dispatchEvent(new CustomEvent('storyboard:canvas:mounted', {
896
- detail: { name, zoom: zoomRef.current }
896
+ detail: { canvasId, zoom: zoomRef.current }
897
897
  }))
898
898
 
899
899
  function handleStatusRequest() {
900
- const state = window[CANVAS_BRIDGE_STATE_KEY] || { active: true, name, zoom: zoomRef.current }
900
+ const state = window[CANVAS_BRIDGE_STATE_KEY] || { active: true, canvasId, zoom: zoomRef.current }
901
901
  document.dispatchEvent(new CustomEvent('storyboard:canvas:status', { detail: state }))
902
902
  }
903
903
 
@@ -905,10 +905,10 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
905
905
 
906
906
  return () => {
907
907
  document.removeEventListener('storyboard:canvas:status-request', handleStatusRequest)
908
- window[CANVAS_BRIDGE_STATE_KEY] = { active: false, name: '', zoom: 100 }
908
+ window[CANVAS_BRIDGE_STATE_KEY] = { active: false, canvasId: '', zoom: 100 }
909
909
  document.dispatchEvent(new CustomEvent('storyboard:canvas:unmounted'))
910
910
  }
911
- }, [name])
911
+ }, [canvasId])
912
912
 
913
913
  // Tell the Vite dev server to suppress full-reloads while this canvas is active.
914
914
  // The ?canvas-hmr URL param opts out of the guard for canvas UI development.
@@ -928,7 +928,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
928
928
  clearInterval(interval)
929
929
  import.meta.hot.send('storyboard:canvas-hmr-guard', { active: false, hmrEnabled: true })
930
930
  }
931
- }, [name])
931
+ }, [canvasId])
932
932
 
933
933
  // Add a widget by type — used by CanvasControls and CoreUIBar event
934
934
  const addWidget = useCallback(async (type) => {
@@ -936,7 +936,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
936
936
  const center = getViewportCenter(scrollRef.current, zoomRef.current / 100)
937
937
  const pos = centerPositionForWidget(center, type, defaultProps)
938
938
  try {
939
- const result = await addWidgetApi(name, {
939
+ const result = await addWidgetApi(canvasId, {
940
940
  type,
941
941
  props: defaultProps,
942
942
  position: pos,
@@ -948,7 +948,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
948
948
  } catch (err) {
949
949
  console.error('[canvas] Failed to add widget:', err)
950
950
  }
951
- }, [name, undoRedo])
951
+ }, [canvasId, undoRedo])
952
952
 
953
953
  // Add a story widget by storyId — used by CanvasControls story picker
954
954
  const addStoryWidget = useCallback(async (storyId) => {
@@ -956,7 +956,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
956
956
  const center = getViewportCenter(scrollRef.current, zoomRef.current / 100)
957
957
  const pos = centerPositionForWidget(center, 'story', storyProps)
958
958
  try {
959
- const result = await addWidgetApi(name, {
959
+ const result = await addWidgetApi(canvasId, {
960
960
  type: 'story',
961
961
  props: storyProps,
962
962
  position: pos,
@@ -968,7 +968,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
968
968
  } catch (err) {
969
969
  console.error('[canvas] Failed to add story widget:', err)
970
970
  }
971
- }, [name, undoRedo])
971
+ }, [canvasId, undoRedo])
972
972
 
973
973
  // Listen for CoreUIBar add-widget events
974
974
  useEffect(() => {
@@ -1003,7 +1003,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
1003
1003
  function handleSnapToggle() {
1004
1004
  setSnapEnabled((prev) => {
1005
1005
  const next = !prev
1006
- updateCanvas(name, { settings: { snapToGrid: next } }).catch((err) =>
1006
+ updateCanvas(canvasId, { settings: { snapToGrid: next } }).catch((err) =>
1007
1007
  console.error('[canvas] Failed to persist snap setting:', err)
1008
1008
  )
1009
1009
  return next
@@ -1011,7 +1011,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
1011
1011
  }
1012
1012
  document.addEventListener('storyboard:canvas:toggle-snap', handleSnapToggle)
1013
1013
  return () => document.removeEventListener('storyboard:canvas:toggle-snap', handleSnapToggle)
1014
- }, [name])
1014
+ }, [canvasId])
1015
1015
 
1016
1016
  // Broadcast snap state to Svelte toolbar
1017
1017
  useEffect(() => {
@@ -1076,8 +1076,8 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
1076
1076
  el.scrollTop = (bounds.minY - FIT_PADDING) * newScale
1077
1077
 
1078
1078
  // Persist after both zoom and scroll are settled
1079
- if (viewportInitName.current === name) {
1080
- saveViewportState(name, {
1079
+ if (viewportInitName.current === canvasId) {
1080
+ saveViewportState(canvasId, {
1081
1081
  zoom: fitZoom,
1082
1082
  scrollLeft: el.scrollLeft,
1083
1083
  scrollTop: el.scrollTop,
@@ -1101,11 +1101,11 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
1101
1101
 
1102
1102
  // Broadcast zoom level to CoreUIBar whenever it changes
1103
1103
  useEffect(() => {
1104
- window[CANVAS_BRIDGE_STATE_KEY] = { active: true, name, zoom }
1104
+ window[CANVAS_BRIDGE_STATE_KEY] = { active: true, canvasId, zoom }
1105
1105
  document.dispatchEvent(new CustomEvent('storyboard:canvas:zoom-changed', {
1106
1106
  detail: { zoom }
1107
1107
  }))
1108
- }, [name, zoom])
1108
+ }, [canvasId, zoom])
1109
1109
 
1110
1110
  // Delete selected widget on Delete/Backspace key
1111
1111
  useEffect(() => {
@@ -1128,12 +1128,12 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
1128
1128
  setSelectedWidgetIds(new Set())
1129
1129
  }
1130
1130
  // Copy shortcut (single widget selected):
1131
- // cmd+c → copy canvasName::widgetId (for cross-canvas paste-duplicate)
1131
+ // cmd+c → copy canvasId::widgetId (for cross-canvas paste-duplicate)
1132
1132
  const mod = e.metaKey || e.ctrlKey
1133
1133
  if (mod && e.key === 'c' && !e.shiftKey && selectedWidgetIds.size === 1) {
1134
1134
  const widgetId = [...selectedWidgetIds][0]
1135
1135
  e.preventDefault()
1136
- navigator.clipboard.writeText(`${name}::${widgetId}`).catch(() => {})
1136
+ navigator.clipboard.writeText(`${canvasId}::${widgetId}`).catch(() => {})
1137
1137
  }
1138
1138
  if (e.key === 'Delete' || e.key === 'Backspace') {
1139
1139
  e.preventDefault()
@@ -1145,7 +1145,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
1145
1145
  if (!prev) return prev
1146
1146
  const next = prev.filter(w => !selectedWidgetIds.has(w.id))
1147
1147
  queueWrite(() =>
1148
- updateCanvas(name, { widgets: next }).catch(err =>
1148
+ updateCanvas(canvasId, { widgets: next }).catch(err =>
1149
1149
  console.error('[canvas] Failed to save multi-delete:', err)
1150
1150
  )
1151
1151
  )
@@ -1160,7 +1160,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
1160
1160
  }
1161
1161
  document.addEventListener('keydown', handleKeyDown)
1162
1162
  return () => document.removeEventListener('keydown', handleKeyDown)
1163
- }, [selectedWidgetIds, localWidgets, handleWidgetRemove, undoRedo, name, debouncedSave])
1163
+ }, [selectedWidgetIds, localWidgets, handleWidgetRemove, undoRedo, canvasId, debouncedSave])
1164
1164
 
1165
1165
  // Ref to store processImageFile for use by drop effect
1166
1166
  const processImageFileRef = useRef(null)
@@ -1209,7 +1209,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
1209
1209
  displayW = maxWidth
1210
1210
  }
1211
1211
 
1212
- const uploadResult = await uploadImage(dataUrl, name)
1212
+ const uploadResult = await uploadImage(dataUrl, canvasId)
1213
1213
  if (!uploadResult.success) {
1214
1214
  console.error('[canvas] Image upload failed:', uploadResult.error)
1215
1215
  return false
@@ -1224,7 +1224,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
1224
1224
  pos = centerPositionForWidget(center, 'image', { width: displayW, height: displayH })
1225
1225
  }
1226
1226
 
1227
- const result = await addWidgetApi(name, {
1227
+ const result = await addWidgetApi(canvasId, {
1228
1228
  type: 'image',
1229
1229
  props: { src: uploadResult.filename, private: false, width: displayW, height: displayH },
1230
1230
  position: pos,
@@ -1271,8 +1271,8 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
1271
1271
  const text = e.clipboardData?.getData('text/plain')?.trim()
1272
1272
  if (!text) return
1273
1273
 
1274
- // Detect canvasName::widgetId format for widget duplication (cross-canvas copy-paste)
1275
- // Also supports legacy canvasName/widgetId for basenames without slashes,
1274
+ // Detect canvasId::widgetId format for widget duplication (cross-canvas copy-paste)
1275
+ // Also supports legacy canvasId/widgetId for basenames without slashes,
1276
1276
  // but only when the second segment looks like a widget ID (type-hash).
1277
1277
  const widgetRefMatch = text.match(/^(.+)::([^:]+)$/) || (text.indexOf('::') === -1 && text.match(/^([^/]+)\/((?:sticky-note|markdown|prototype|link-preview|figma-embed|component|image)-[a-z0-9]+)$/))
1278
1278
  if (widgetRefMatch) {
@@ -1282,7 +1282,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
1282
1282
  if (sourceWidgetId.startsWith('jsx-')) return
1283
1283
  try {
1284
1284
  let sourceWidget = null
1285
- if (sourceCanvas === name) {
1285
+ if (sourceCanvas === canvasId) {
1286
1286
  sourceWidget = (localWidgets ?? []).find(w => w.id === sourceWidgetId)
1287
1287
  } else {
1288
1288
  const canvasData = await getCanvasApi(sourceCanvas)
@@ -1292,7 +1292,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
1292
1292
  const center = getViewportCenter(scrollRef.current, zoomRef.current / 100)
1293
1293
  const pos = centerPositionForWidget(center, sourceWidget.type, sourceWidget.props)
1294
1294
  undoRedo.snapshot(stateRef.current, 'add')
1295
- const result = await addWidgetApi(name, {
1295
+ const result = await addWidgetApi(canvasId, {
1296
1296
  type: sourceWidget.type,
1297
1297
  props: { ...sourceWidget.props },
1298
1298
  position: pos,
@@ -1316,7 +1316,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
1316
1316
  const center = getViewportCenter(scrollRef.current, zoomRef.current / 100)
1317
1317
  const pos = centerPositionForWidget(center, type, props)
1318
1318
  try {
1319
- const result = await addWidgetApi(name, {
1319
+ const result = await addWidgetApi(canvasId, {
1320
1320
  type,
1321
1321
  props,
1322
1322
  position: pos,
@@ -1332,7 +1332,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
1332
1332
 
1333
1333
  document.addEventListener('paste', handlePaste)
1334
1334
  return () => document.removeEventListener('paste', handlePaste)
1335
- }, [name, undoRedo, localWidgets])
1335
+ }, [canvasId, undoRedo, localWidgets])
1336
1336
 
1337
1337
  // --- Drag and drop handlers for images from Finder/file manager ---
1338
1338
  // Separate effect to ensure listeners attach after scroll container mounts (loading=false)
@@ -1407,11 +1407,11 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
1407
1407
  setLocalWidgets(previous.widgets)
1408
1408
  setLocalSources(previous.sources)
1409
1409
  queueWrite(() =>
1410
- updateCanvas(name, { widgets: previous.widgets, sources: previous.sources }).catch((err) =>
1410
+ updateCanvas(canvasId, { widgets: previous.widgets, sources: previous.sources }).catch((err) =>
1411
1411
  console.error('[canvas] Failed to persist undo:', err)
1412
1412
  )
1413
1413
  )
1414
- }, [name, debouncedSave, debouncedSourceSave, undoRedo])
1414
+ }, [canvasId, debouncedSave, debouncedSourceSave, undoRedo])
1415
1415
 
1416
1416
  const handleRedo = useCallback(() => {
1417
1417
  const next = undoRedo.redo(stateRef.current)
@@ -1421,11 +1421,11 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
1421
1421
  setLocalWidgets(next.widgets)
1422
1422
  setLocalSources(next.sources)
1423
1423
  queueWrite(() =>
1424
- updateCanvas(name, { widgets: next.widgets, sources: next.sources }).catch((err) =>
1424
+ updateCanvas(canvasId, { widgets: next.widgets, sources: next.sources }).catch((err) =>
1425
1425
  console.error('[canvas] Failed to persist redo:', err)
1426
1426
  )
1427
1427
  )
1428
- }, [name, debouncedSave, debouncedSourceSave, undoRedo])
1428
+ }, [canvasId, debouncedSave, debouncedSourceSave, undoRedo])
1429
1429
 
1430
1430
  // Keyboard shortcuts — dev-only (Cmd+Z / Cmd+Shift+Z)
1431
1431
  useEffect(() => {
@@ -1595,7 +1595,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
1595
1595
  if (!canvas) {
1596
1596
  return (
1597
1597
  <div className={styles.empty}>
1598
- <p>Canvas &ldquo;{name}&rdquo; not found</p>
1598
+ <p>Canvas &ldquo;{canvasId}&rdquo; not found</p>
1599
1599
  </div>
1600
1600
  )
1601
1601
  }
@@ -1713,8 +1713,8 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
1713
1713
  return (
1714
1714
  <>
1715
1715
  <div className={styles.canvasTitle}>
1716
- <h1 className={styles.canvasTitleStatic}>{canvasMeta?.title || canvas?.title || name.split('/').pop()}</h1>
1717
- <PageSelector currentName={name} pages={siblingPages} />
1716
+ <h1 className={styles.canvasTitleStatic}>{canvasMeta?.title || canvas?.title || canvasId.split('/').pop()}</h1>
1717
+ <PageSelector currentName={canvasId} pages={siblingPages} />
1718
1718
  {isLocalDev && (
1719
1719
  <span className={styles.localEditingLabel}>Local editing</span>
1720
1720
  )}