@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
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { createElement, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
-
import { flushSync } from 'react-dom'
|
|
1
|
+
import { createElement, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
3
2
|
import { Canvas } from '@dfosco/tiny-canvas'
|
|
4
3
|
import '@dfosco/tiny-canvas/style.css'
|
|
5
4
|
import { useCanvas } from './useCanvas.js'
|
|
@@ -106,18 +105,18 @@ function debounce(fn, ms) {
|
|
|
106
105
|
}
|
|
107
106
|
|
|
108
107
|
/** Per-canvas viewport state persistence (zoom + scroll position). */
|
|
109
|
-
function getViewportStorageKey(
|
|
110
|
-
return `sb-canvas-viewport:${
|
|
108
|
+
function getViewportStorageKey(canvasId) {
|
|
109
|
+
return `sb-canvas-viewport:${canvasId}`
|
|
111
110
|
}
|
|
112
111
|
|
|
113
|
-
function loadViewportState(
|
|
112
|
+
function loadViewportState(canvasId) {
|
|
114
113
|
try {
|
|
115
|
-
const raw = localStorage.getItem(getViewportStorageKey(
|
|
114
|
+
const raw = localStorage.getItem(getViewportStorageKey(canvasId))
|
|
116
115
|
if (!raw) return null
|
|
117
116
|
const state = JSON.parse(raw)
|
|
118
117
|
const timestamp = typeof state.timestamp === 'number' ? state.timestamp : 0
|
|
119
118
|
if (Date.now() - timestamp > VIEWPORT_TTL_MS) {
|
|
120
|
-
localStorage.removeItem(getViewportStorageKey(
|
|
119
|
+
localStorage.removeItem(getViewportStorageKey(canvasId))
|
|
121
120
|
return null
|
|
122
121
|
}
|
|
123
122
|
return {
|
|
@@ -128,9 +127,9 @@ function loadViewportState(canvasName) {
|
|
|
128
127
|
} catch { return null }
|
|
129
128
|
}
|
|
130
129
|
|
|
131
|
-
function saveViewportState(
|
|
130
|
+
function saveViewportState(canvasId, state) {
|
|
132
131
|
try {
|
|
133
|
-
localStorage.setItem(getViewportStorageKey(
|
|
132
|
+
localStorage.setItem(getViewportStorageKey(canvasId), JSON.stringify({
|
|
134
133
|
...state,
|
|
135
134
|
timestamp: Date.now(),
|
|
136
135
|
}))
|
|
@@ -267,8 +266,10 @@ function WidgetRenderer({ widget, onUpdate, widgetRef }) {
|
|
|
267
266
|
/**
|
|
268
267
|
* Wrapper for each JSON widget that holds its own ref for imperative actions.
|
|
269
268
|
* This allows WidgetChrome to dispatch actions to the widget via ref.
|
|
269
|
+
*
|
|
270
|
+
* Memoized to prevent re-renders during zoom and unrelated state changes.
|
|
270
271
|
*/
|
|
271
|
-
function ChromeWrappedWidget({
|
|
272
|
+
const ChromeWrappedWidget = memo(function ChromeWrappedWidget({
|
|
272
273
|
widget,
|
|
273
274
|
selected,
|
|
274
275
|
multiSelected,
|
|
@@ -293,6 +294,10 @@ function ChromeWrappedWidget({
|
|
|
293
294
|
}
|
|
294
295
|
}, [widget, onRemove, onCopy])
|
|
295
296
|
|
|
297
|
+
const handleWidgetFieldUpdate = useCallback((updates) => {
|
|
298
|
+
onUpdate?.(widget.id, updates)
|
|
299
|
+
}, [onUpdate, widget.id])
|
|
300
|
+
|
|
296
301
|
return (
|
|
297
302
|
<WidgetChrome
|
|
298
303
|
widgetId={widget.id}
|
|
@@ -305,40 +310,55 @@ function ChromeWrappedWidget({
|
|
|
305
310
|
onSelect={onSelect}
|
|
306
311
|
onDeselect={onDeselect}
|
|
307
312
|
onAction={handleAction}
|
|
308
|
-
onUpdate={onUpdate ?
|
|
313
|
+
onUpdate={onUpdate ? handleWidgetFieldUpdate : undefined}
|
|
309
314
|
readOnly={readOnly}
|
|
310
315
|
>
|
|
311
316
|
<WidgetRenderer
|
|
312
317
|
widget={widget}
|
|
313
|
-
onUpdate={onUpdate ?
|
|
318
|
+
onUpdate={onUpdate ? handleWidgetFieldUpdate : undefined}
|
|
314
319
|
widgetRef={widgetRef}
|
|
315
320
|
/>
|
|
316
321
|
</WidgetChrome>
|
|
317
322
|
)
|
|
318
|
-
}
|
|
323
|
+
}, function chromeWidgetAreEqual(prev, next) {
|
|
324
|
+
return (
|
|
325
|
+
prev.widget === next.widget &&
|
|
326
|
+
prev.selected === next.selected &&
|
|
327
|
+
prev.multiSelected === next.multiSelected &&
|
|
328
|
+
prev.readOnly === next.readOnly &&
|
|
329
|
+
prev.onSelect === next.onSelect &&
|
|
330
|
+
prev.onDeselect === next.onDeselect &&
|
|
331
|
+
prev.onUpdate === next.onUpdate &&
|
|
332
|
+
prev.onRemove === next.onRemove &&
|
|
333
|
+
prev.onCopy === next.onCopy
|
|
334
|
+
)
|
|
335
|
+
})
|
|
319
336
|
|
|
320
337
|
/**
|
|
321
338
|
* Generic canvas page component.
|
|
322
339
|
* Reads canvas data from the index and renders all widgets on a draggable surface.
|
|
323
340
|
*
|
|
324
|
-
* @param {{
|
|
341
|
+
* @param {{ canvasId: string }} props - Canvas name as indexed by the data plugin
|
|
325
342
|
*/
|
|
326
|
-
export default function CanvasPage({
|
|
327
|
-
const { canvas, jsxExports, jsxError, loading } = useCanvas(
|
|
343
|
+
export default function CanvasPage({ canvasId, siblingPages = [], canvasMeta = null }) {
|
|
344
|
+
const { canvas, jsxExports, jsxError, loading } = useCanvas(canvasId)
|
|
328
345
|
const isLocalDev = typeof window !== 'undefined' && window.__SB_LOCAL_DEV__ === true && !new URLSearchParams(window.location.search).has('prodMode')
|
|
329
346
|
|
|
330
347
|
// Local mutable copy of widgets for instant UI updates
|
|
331
348
|
const [localWidgets, setLocalWidgets] = useState(canvas?.widgets ?? null)
|
|
332
349
|
const [trackedCanvas, setTrackedCanvas] = useState(canvas)
|
|
333
350
|
const [selectedWidgetIds, setSelectedWidgetIds] = useState(() => new Set())
|
|
334
|
-
const initialViewport = loadViewportState(
|
|
351
|
+
const initialViewport = loadViewportState(canvasId)
|
|
335
352
|
const [zoom, setZoom] = useState(initialViewport?.zoom ?? 100)
|
|
336
353
|
const zoomRef = useRef(initialViewport?.zoom ?? 100)
|
|
337
354
|
const scrollRef = useRef(null)
|
|
355
|
+
const zoomElRef = useRef(null)
|
|
356
|
+
const zoomCommitTimer = useRef(null)
|
|
357
|
+
const zoomEventTimer = useRef(null)
|
|
338
358
|
const pendingScrollRestore = useRef(initialViewport)
|
|
339
359
|
// Gate viewport persistence until initial positioning is complete.
|
|
340
|
-
// Tracks which
|
|
341
|
-
// write when this matches `
|
|
360
|
+
// Tracks which canvasId was last initialized — save effects only
|
|
361
|
+
// write when this matches `canvasId`, preventing cross-canvas corruption.
|
|
342
362
|
const viewportInitName = useRef(null)
|
|
343
363
|
const [localSources, setLocalSources] = useState(canvas?.sources ?? [])
|
|
344
364
|
const [canvasTheme, setCanvasTheme] = useState(() => resolveCanvasThemeFromStorage())
|
|
@@ -482,7 +502,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
482
502
|
undoRedo.reset()
|
|
483
503
|
// Block saves until the new canvas's viewport is fully restored.
|
|
484
504
|
viewportInitName.current = null
|
|
485
|
-
const newViewport = loadViewportState(
|
|
505
|
+
const newViewport = loadViewportState(canvasId)
|
|
486
506
|
pendingScrollRestore.current = newViewport
|
|
487
507
|
// Restore zoom from the new canvas's saved state
|
|
488
508
|
const newZoom = newViewport?.zoom ?? 100
|
|
@@ -492,8 +512,8 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
492
512
|
|
|
493
513
|
// Debounced save to server
|
|
494
514
|
const debouncedSave = useRef(
|
|
495
|
-
debounce((
|
|
496
|
-
updateCanvas(
|
|
515
|
+
debounce((canvasId, widgets) => {
|
|
516
|
+
updateCanvas(canvasId, { widgets }).catch((err) =>
|
|
497
517
|
console.error('[canvas] Failed to save:', err)
|
|
498
518
|
)
|
|
499
519
|
}, 2000)
|
|
@@ -512,20 +532,20 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
512
532
|
const next = prev.map((w) =>
|
|
513
533
|
w.id === widgetId ? { ...w, props: { ...w.props, ...snapped } } : w
|
|
514
534
|
)
|
|
515
|
-
debouncedSave(
|
|
535
|
+
debouncedSave(canvasId, next)
|
|
516
536
|
return next
|
|
517
537
|
})
|
|
518
|
-
}, [
|
|
538
|
+
}, [canvasId, debouncedSave, undoRedo, snapEnabled, snapGridSize])
|
|
519
539
|
|
|
520
540
|
const handleWidgetRemove = useCallback((widgetId) => {
|
|
521
541
|
undoRedo.snapshot(stateRef.current, 'remove', widgetId)
|
|
522
542
|
setLocalWidgets((prev) => prev ? prev.filter((w) => w.id !== widgetId) : prev)
|
|
523
543
|
queueWrite(() =>
|
|
524
|
-
removeWidgetApi(
|
|
544
|
+
removeWidgetApi(canvasId, widgetId).catch((err) =>
|
|
525
545
|
console.error('[canvas] Failed to remove widget:', err)
|
|
526
546
|
)
|
|
527
547
|
)
|
|
528
|
-
}, [
|
|
548
|
+
}, [canvasId, undoRedo])
|
|
529
549
|
|
|
530
550
|
const handleWidgetCopy = useCallback(async (widget) => {
|
|
531
551
|
// Find the next free offset — check how many copies already exist at +n*40
|
|
@@ -541,7 +561,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
541
561
|
const position = { x: baseX + n * 40, y: baseY + n * 40 }
|
|
542
562
|
try {
|
|
543
563
|
undoRedo.snapshot(stateRef.current, 'add')
|
|
544
|
-
const result = await addWidgetApi(
|
|
564
|
+
const result = await addWidgetApi(canvasId, {
|
|
545
565
|
type: widget.type,
|
|
546
566
|
props: { ...widget.props },
|
|
547
567
|
position,
|
|
@@ -552,11 +572,11 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
552
572
|
} catch (err) {
|
|
553
573
|
console.error('[canvas] Failed to copy widget:', err)
|
|
554
574
|
}
|
|
555
|
-
}, [
|
|
575
|
+
}, [canvasId, localWidgets, undoRedo])
|
|
556
576
|
|
|
557
577
|
const debouncedSourceSave = useRef(
|
|
558
|
-
debounce((
|
|
559
|
-
updateCanvas(
|
|
578
|
+
debounce((canvasId, sources) => {
|
|
579
|
+
updateCanvas(canvasId, { sources }).catch((err) =>
|
|
560
580
|
console.error('[canvas] Failed to save sources:', err)
|
|
561
581
|
)
|
|
562
582
|
}, 2000)
|
|
@@ -574,10 +594,10 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
574
594
|
const next = current.some((s) => s?.export === exportName)
|
|
575
595
|
? current.map((s) => (s?.export === exportName ? { ...s, ...snapped } : s))
|
|
576
596
|
: [...current, { export: exportName, ...snapped }]
|
|
577
|
-
debouncedSourceSave(
|
|
597
|
+
debouncedSourceSave(canvasId, next)
|
|
578
598
|
return next
|
|
579
599
|
})
|
|
580
|
-
}, [
|
|
600
|
+
}, [canvasId, debouncedSourceSave, undoRedo, snapEnabled, snapGridSize])
|
|
581
601
|
|
|
582
602
|
const handleItemDragEnd = useCallback((dragId, position) => {
|
|
583
603
|
if (!dragId || !position) {
|
|
@@ -629,7 +649,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
629
649
|
return w
|
|
630
650
|
})
|
|
631
651
|
queueWrite(() =>
|
|
632
|
-
updateCanvas(
|
|
652
|
+
updateCanvas(canvasId, { widgets: next }).catch((err) =>
|
|
633
653
|
console.error('[canvas] Failed to save multi-move:', err)
|
|
634
654
|
)
|
|
635
655
|
)
|
|
@@ -661,7 +681,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
661
681
|
})
|
|
662
682
|
if (changed) {
|
|
663
683
|
queueWrite(() =>
|
|
664
|
-
updateCanvas(
|
|
684
|
+
updateCanvas(canvasId, { sources: next }).catch((err) =>
|
|
665
685
|
console.error('[canvas] Failed to save multi-move sources:', err)
|
|
666
686
|
)
|
|
667
687
|
)
|
|
@@ -680,7 +700,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
680
700
|
? current.map((s) => (s?.export === sourceExport ? { ...s, position: rounded } : s))
|
|
681
701
|
: [...current, { export: sourceExport, position: rounded }]
|
|
682
702
|
queueWrite(() =>
|
|
683
|
-
updateCanvas(
|
|
703
|
+
updateCanvas(canvasId, { sources: next }).catch((err) =>
|
|
684
704
|
console.error('[canvas] Failed to save source position:', err)
|
|
685
705
|
)
|
|
686
706
|
)
|
|
@@ -696,18 +716,25 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
696
716
|
w.id === dragId ? { ...w, position: rounded } : w
|
|
697
717
|
)
|
|
698
718
|
queueWrite(() =>
|
|
699
|
-
updateCanvas(
|
|
719
|
+
updateCanvas(canvasId, { widgets: next }).catch((err) =>
|
|
700
720
|
console.error('[canvas] Failed to save widget position:', err)
|
|
701
721
|
)
|
|
702
722
|
)
|
|
703
723
|
return next
|
|
704
724
|
})
|
|
705
|
-
}, [
|
|
725
|
+
}, [canvasId, undoRedo, debouncedSave, transitionPeers, clearDragPreview])
|
|
706
726
|
|
|
727
|
+
// Keep zoomRef in sync when React state is set (e.g. by toolbar or zoom-to-fit)
|
|
707
728
|
useEffect(() => {
|
|
708
729
|
zoomRef.current = zoom
|
|
709
730
|
}, [zoom])
|
|
710
731
|
|
|
732
|
+
// Cleanup zoom timers on unmount
|
|
733
|
+
useEffect(() => () => {
|
|
734
|
+
clearTimeout(zoomCommitTimer.current)
|
|
735
|
+
clearTimeout(zoomEventTimer.current)
|
|
736
|
+
}, [])
|
|
737
|
+
|
|
711
738
|
// Restore scroll position from localStorage after first render.
|
|
712
739
|
// When saved state is fresh (< 15 min), restore it. Otherwise zoom-to-fit
|
|
713
740
|
// all objects so the user sees a useful overview instead of stale coordinates.
|
|
@@ -730,7 +757,14 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
730
757
|
const fitZoom = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, Math.round(fitScale * 100)))
|
|
731
758
|
const newScale = fitZoom / 100
|
|
732
759
|
zoomRef.current = fitZoom
|
|
733
|
-
|
|
760
|
+
// Imperative DOM update for initial zoom-to-fit — same path as applyZoom
|
|
761
|
+
const zoomEl = zoomElRef.current
|
|
762
|
+
if (zoomEl) {
|
|
763
|
+
zoomEl.style.transform = `scale(${newScale})`
|
|
764
|
+
zoomEl.style.width = `${Math.max(10000, 100 / newScale)}vw`
|
|
765
|
+
zoomEl.style.height = `${Math.max(10000, 100 / newScale)}vh`
|
|
766
|
+
}
|
|
767
|
+
setZoom(fitZoom)
|
|
734
768
|
el.scrollLeft = (bounds.minX - FIT_PADDING) * newScale
|
|
735
769
|
el.scrollTop = (bounds.minY - FIT_PADDING) * newScale
|
|
736
770
|
} else {
|
|
@@ -739,8 +773,8 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
739
773
|
}
|
|
740
774
|
}
|
|
741
775
|
// Allow save effects for this canvas now that positioning is settled.
|
|
742
|
-
viewportInitName.current =
|
|
743
|
-
}, [
|
|
776
|
+
viewportInitName.current = canvasId
|
|
777
|
+
}, [canvasId, loading])
|
|
744
778
|
|
|
745
779
|
// Center on a specific widget if `?widget=<id>` is in the URL
|
|
746
780
|
useEffect(() => {
|
|
@@ -795,23 +829,23 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
795
829
|
// operations (applyZoom, zoom-to-fit) adjust scroll AFTER setZoom, so the
|
|
796
830
|
// scroll values would be stale at this point.
|
|
797
831
|
useEffect(() => {
|
|
798
|
-
if (viewportInitName.current !==
|
|
832
|
+
if (viewportInitName.current !== canvasId) return
|
|
799
833
|
const el = scrollRef.current
|
|
800
834
|
// Read current scroll so the zoom entry doesn't zero-out position,
|
|
801
835
|
// but the authoritative scroll save comes from the scroll handler.
|
|
802
|
-
saveViewportState(
|
|
836
|
+
saveViewportState(canvasId, {
|
|
803
837
|
zoom,
|
|
804
838
|
scrollLeft: el?.scrollLeft ?? 0,
|
|
805
839
|
scrollTop: el?.scrollTop ?? 0,
|
|
806
840
|
})
|
|
807
|
-
}, [
|
|
841
|
+
}, [canvasId, zoom])
|
|
808
842
|
|
|
809
843
|
useEffect(() => {
|
|
810
844
|
const el = scrollRef.current
|
|
811
845
|
if (!el) return
|
|
812
846
|
const saveNow = () => {
|
|
813
|
-
if (viewportInitName.current !==
|
|
814
|
-
saveViewportState(
|
|
847
|
+
if (viewportInitName.current !== canvasId) return
|
|
848
|
+
saveViewportState(canvasId, {
|
|
815
849
|
zoom: zoomRef.current,
|
|
816
850
|
scrollLeft: el.scrollLeft,
|
|
817
851
|
scrollTop: el.scrollTop,
|
|
@@ -819,7 +853,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
819
853
|
}
|
|
820
854
|
const debouncedScrollSave = debounce(saveNow, 150)
|
|
821
855
|
function handleScroll() {
|
|
822
|
-
if (viewportInitName.current !==
|
|
856
|
+
if (viewportInitName.current !== canvasId) return
|
|
823
857
|
debouncedScrollSave()
|
|
824
858
|
}
|
|
825
859
|
el.addEventListener('scroll', handleScroll, { passive: true })
|
|
@@ -839,19 +873,26 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
839
873
|
// beforeunload doesn't fire).
|
|
840
874
|
saveNow()
|
|
841
875
|
}
|
|
842
|
-
}, [
|
|
876
|
+
}, [canvasId, loading])
|
|
843
877
|
|
|
844
878
|
/**
|
|
845
879
|
* Zoom to a new level, anchoring on an optional client-space point.
|
|
846
880
|
* When a cursor position is provided (e.g. from a wheel event), the
|
|
847
881
|
* canvas point under the cursor stays fixed. Otherwise falls back to
|
|
848
882
|
* the viewport center.
|
|
883
|
+
*
|
|
884
|
+
* Performs an imperative DOM mutation instead of a React state update
|
|
885
|
+
* to avoid triggering a full re-render of the widget tree on every
|
|
886
|
+
* zoom tick. React state is committed after a debounce for toolbar
|
|
887
|
+
* display updates.
|
|
849
888
|
*/
|
|
850
889
|
function applyZoom(newZoom, clientX, clientY) {
|
|
851
890
|
const el = scrollRef.current
|
|
891
|
+
const zoomEl = zoomElRef.current
|
|
852
892
|
const clampedZoom = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, newZoom))
|
|
853
893
|
|
|
854
|
-
if (!el) {
|
|
894
|
+
if (!el || !zoomEl) {
|
|
895
|
+
zoomRef.current = clampedZoom
|
|
855
896
|
setZoom(clampedZoom)
|
|
856
897
|
return
|
|
857
898
|
}
|
|
@@ -869,35 +910,48 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
869
910
|
const canvasX = (el.scrollLeft + anchorX) / oldScale
|
|
870
911
|
const canvasY = (el.scrollTop + anchorY) / oldScale
|
|
871
912
|
|
|
872
|
-
//
|
|
913
|
+
// Imperative DOM update — no React re-render
|
|
873
914
|
zoomRef.current = clampedZoom
|
|
874
|
-
|
|
915
|
+
zoomEl.style.transform = `scale(${newScale})`
|
|
916
|
+
zoomEl.style.width = `${Math.max(10000, 100 / newScale)}vw`
|
|
917
|
+
zoomEl.style.height = `${Math.max(10000, 100 / newScale)}vh`
|
|
918
|
+
|
|
919
|
+
// Hint GPU compositing during active zoom
|
|
920
|
+
zoomEl.dataset.zooming = ''
|
|
875
921
|
|
|
876
922
|
// Scroll so the same canvas point stays under the anchor
|
|
877
923
|
el.scrollLeft = canvasX * newScale - anchorX
|
|
878
924
|
el.scrollTop = canvasY * newScale - anchorY
|
|
879
925
|
|
|
880
|
-
//
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
926
|
+
// Debounced commit: update React state for toolbar display + persistence
|
|
927
|
+
clearTimeout(zoomCommitTimer.current)
|
|
928
|
+
zoomCommitTimer.current = setTimeout(() => {
|
|
929
|
+
// Remove GPU compositing hint
|
|
930
|
+
delete zoomEl.dataset.zooming
|
|
931
|
+
setZoom(clampedZoom)
|
|
932
|
+
}, 150)
|
|
933
|
+
|
|
934
|
+
// Throttled zoom-changed event for external consumers (toolbar)
|
|
935
|
+
if (!zoomEventTimer.current) {
|
|
936
|
+
zoomEventTimer.current = setTimeout(() => {
|
|
937
|
+
zoomEventTimer.current = null
|
|
938
|
+
window[CANVAS_BRIDGE_STATE_KEY] = { active: true, canvasId, zoom: zoomRef.current }
|
|
939
|
+
document.dispatchEvent(new CustomEvent('storyboard:canvas:zoom-changed', {
|
|
940
|
+
detail: { zoom: zoomRef.current }
|
|
941
|
+
}))
|
|
942
|
+
}, 100)
|
|
889
943
|
}
|
|
890
944
|
}
|
|
891
945
|
|
|
892
946
|
// Signal canvas mount/unmount to CoreUIBar
|
|
893
947
|
useEffect(() => {
|
|
894
|
-
window[CANVAS_BRIDGE_STATE_KEY] = { active: true,
|
|
948
|
+
window[CANVAS_BRIDGE_STATE_KEY] = { active: true, canvasId, zoom: zoomRef.current }
|
|
895
949
|
document.dispatchEvent(new CustomEvent('storyboard:canvas:mounted', {
|
|
896
|
-
detail: {
|
|
950
|
+
detail: { canvasId, zoom: zoomRef.current }
|
|
897
951
|
}))
|
|
898
952
|
|
|
899
953
|
function handleStatusRequest() {
|
|
900
|
-
const state = window[CANVAS_BRIDGE_STATE_KEY] || { active: true,
|
|
954
|
+
const state = window[CANVAS_BRIDGE_STATE_KEY] || { active: true, canvasId, zoom: zoomRef.current }
|
|
901
955
|
document.dispatchEvent(new CustomEvent('storyboard:canvas:status', { detail: state }))
|
|
902
956
|
}
|
|
903
957
|
|
|
@@ -905,10 +959,10 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
905
959
|
|
|
906
960
|
return () => {
|
|
907
961
|
document.removeEventListener('storyboard:canvas:status-request', handleStatusRequest)
|
|
908
|
-
window[CANVAS_BRIDGE_STATE_KEY] = { active: false,
|
|
962
|
+
window[CANVAS_BRIDGE_STATE_KEY] = { active: false, canvasId: '', zoom: 100 }
|
|
909
963
|
document.dispatchEvent(new CustomEvent('storyboard:canvas:unmounted'))
|
|
910
964
|
}
|
|
911
|
-
}, [
|
|
965
|
+
}, [canvasId])
|
|
912
966
|
|
|
913
967
|
// Tell the Vite dev server to suppress full-reloads while this canvas is active.
|
|
914
968
|
// The ?canvas-hmr URL param opts out of the guard for canvas UI development.
|
|
@@ -928,7 +982,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
928
982
|
clearInterval(interval)
|
|
929
983
|
import.meta.hot.send('storyboard:canvas-hmr-guard', { active: false, hmrEnabled: true })
|
|
930
984
|
}
|
|
931
|
-
}, [
|
|
985
|
+
}, [canvasId])
|
|
932
986
|
|
|
933
987
|
// Add a widget by type — used by CanvasControls and CoreUIBar event
|
|
934
988
|
const addWidget = useCallback(async (type) => {
|
|
@@ -936,7 +990,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
936
990
|
const center = getViewportCenter(scrollRef.current, zoomRef.current / 100)
|
|
937
991
|
const pos = centerPositionForWidget(center, type, defaultProps)
|
|
938
992
|
try {
|
|
939
|
-
const result = await addWidgetApi(
|
|
993
|
+
const result = await addWidgetApi(canvasId, {
|
|
940
994
|
type,
|
|
941
995
|
props: defaultProps,
|
|
942
996
|
position: pos,
|
|
@@ -948,7 +1002,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
948
1002
|
} catch (err) {
|
|
949
1003
|
console.error('[canvas] Failed to add widget:', err)
|
|
950
1004
|
}
|
|
951
|
-
}, [
|
|
1005
|
+
}, [canvasId, undoRedo])
|
|
952
1006
|
|
|
953
1007
|
// Add a story widget by storyId — used by CanvasControls story picker
|
|
954
1008
|
const addStoryWidget = useCallback(async (storyId) => {
|
|
@@ -956,7 +1010,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
956
1010
|
const center = getViewportCenter(scrollRef.current, zoomRef.current / 100)
|
|
957
1011
|
const pos = centerPositionForWidget(center, 'story', storyProps)
|
|
958
1012
|
try {
|
|
959
|
-
const result = await addWidgetApi(
|
|
1013
|
+
const result = await addWidgetApi(canvasId, {
|
|
960
1014
|
type: 'story',
|
|
961
1015
|
props: storyProps,
|
|
962
1016
|
position: pos,
|
|
@@ -968,7 +1022,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
968
1022
|
} catch (err) {
|
|
969
1023
|
console.error('[canvas] Failed to add story widget:', err)
|
|
970
1024
|
}
|
|
971
|
-
}, [
|
|
1025
|
+
}, [canvasId, undoRedo])
|
|
972
1026
|
|
|
973
1027
|
// Listen for CoreUIBar add-widget events
|
|
974
1028
|
useEffect(() => {
|
|
@@ -1003,7 +1057,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
1003
1057
|
function handleSnapToggle() {
|
|
1004
1058
|
setSnapEnabled((prev) => {
|
|
1005
1059
|
const next = !prev
|
|
1006
|
-
updateCanvas(
|
|
1060
|
+
updateCanvas(canvasId, { settings: { snapToGrid: next } }).catch((err) =>
|
|
1007
1061
|
console.error('[canvas] Failed to persist snap setting:', err)
|
|
1008
1062
|
)
|
|
1009
1063
|
return next
|
|
@@ -1011,7 +1065,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
1011
1065
|
}
|
|
1012
1066
|
document.addEventListener('storyboard:canvas:toggle-snap', handleSnapToggle)
|
|
1013
1067
|
return () => document.removeEventListener('storyboard:canvas:toggle-snap', handleSnapToggle)
|
|
1014
|
-
}, [
|
|
1068
|
+
}, [canvasId])
|
|
1015
1069
|
|
|
1016
1070
|
// Broadcast snap state to Svelte toolbar
|
|
1017
1071
|
useEffect(() => {
|
|
@@ -1067,17 +1121,23 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
1067
1121
|
const fitZoom = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, Math.round(fitScale * 100)))
|
|
1068
1122
|
const newScale = fitZoom / 100
|
|
1069
1123
|
|
|
1070
|
-
//
|
|
1124
|
+
// Imperative DOM update — same path as applyZoom
|
|
1071
1125
|
zoomRef.current = fitZoom
|
|
1072
|
-
|
|
1126
|
+
const zoomEl = zoomElRef.current
|
|
1127
|
+
if (zoomEl) {
|
|
1128
|
+
zoomEl.style.transform = `scale(${newScale})`
|
|
1129
|
+
zoomEl.style.width = `${Math.max(10000, 100 / newScale)}vw`
|
|
1130
|
+
zoomEl.style.height = `${Math.max(10000, 100 / newScale)}vh`
|
|
1131
|
+
}
|
|
1132
|
+
setZoom(fitZoom)
|
|
1073
1133
|
|
|
1074
1134
|
// Scroll so the bounding box top-left (with padding) is at viewport top-left
|
|
1075
1135
|
el.scrollLeft = (bounds.minX - FIT_PADDING) * newScale
|
|
1076
1136
|
el.scrollTop = (bounds.minY - FIT_PADDING) * newScale
|
|
1077
1137
|
|
|
1078
1138
|
// Persist after both zoom and scroll are settled
|
|
1079
|
-
if (viewportInitName.current ===
|
|
1080
|
-
saveViewportState(
|
|
1139
|
+
if (viewportInitName.current === canvasId) {
|
|
1140
|
+
saveViewportState(canvasId, {
|
|
1081
1141
|
zoom: fitZoom,
|
|
1082
1142
|
scrollLeft: el.scrollLeft,
|
|
1083
1143
|
scrollTop: el.scrollTop,
|
|
@@ -1101,11 +1161,11 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
1101
1161
|
|
|
1102
1162
|
// Broadcast zoom level to CoreUIBar whenever it changes
|
|
1103
1163
|
useEffect(() => {
|
|
1104
|
-
window[CANVAS_BRIDGE_STATE_KEY] = { active: true,
|
|
1164
|
+
window[CANVAS_BRIDGE_STATE_KEY] = { active: true, canvasId, zoom }
|
|
1105
1165
|
document.dispatchEvent(new CustomEvent('storyboard:canvas:zoom-changed', {
|
|
1106
1166
|
detail: { zoom }
|
|
1107
1167
|
}))
|
|
1108
|
-
}, [
|
|
1168
|
+
}, [canvasId, zoom])
|
|
1109
1169
|
|
|
1110
1170
|
// Delete selected widget on Delete/Backspace key
|
|
1111
1171
|
useEffect(() => {
|
|
@@ -1128,12 +1188,12 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
1128
1188
|
setSelectedWidgetIds(new Set())
|
|
1129
1189
|
}
|
|
1130
1190
|
// Copy shortcut (single widget selected):
|
|
1131
|
-
// cmd+c → copy
|
|
1191
|
+
// cmd+c → copy canvasId::widgetId (for cross-canvas paste-duplicate)
|
|
1132
1192
|
const mod = e.metaKey || e.ctrlKey
|
|
1133
1193
|
if (mod && e.key === 'c' && !e.shiftKey && selectedWidgetIds.size === 1) {
|
|
1134
1194
|
const widgetId = [...selectedWidgetIds][0]
|
|
1135
1195
|
e.preventDefault()
|
|
1136
|
-
navigator.clipboard.writeText(`${
|
|
1196
|
+
navigator.clipboard.writeText(`${canvasId}::${widgetId}`).catch(() => {})
|
|
1137
1197
|
}
|
|
1138
1198
|
if (e.key === 'Delete' || e.key === 'Backspace') {
|
|
1139
1199
|
e.preventDefault()
|
|
@@ -1145,7 +1205,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
1145
1205
|
if (!prev) return prev
|
|
1146
1206
|
const next = prev.filter(w => !selectedWidgetIds.has(w.id))
|
|
1147
1207
|
queueWrite(() =>
|
|
1148
|
-
updateCanvas(
|
|
1208
|
+
updateCanvas(canvasId, { widgets: next }).catch(err =>
|
|
1149
1209
|
console.error('[canvas] Failed to save multi-delete:', err)
|
|
1150
1210
|
)
|
|
1151
1211
|
)
|
|
@@ -1160,7 +1220,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
1160
1220
|
}
|
|
1161
1221
|
document.addEventListener('keydown', handleKeyDown)
|
|
1162
1222
|
return () => document.removeEventListener('keydown', handleKeyDown)
|
|
1163
|
-
}, [selectedWidgetIds, localWidgets, handleWidgetRemove, undoRedo,
|
|
1223
|
+
}, [selectedWidgetIds, localWidgets, handleWidgetRemove, undoRedo, canvasId, debouncedSave])
|
|
1164
1224
|
|
|
1165
1225
|
// Ref to store processImageFile for use by drop effect
|
|
1166
1226
|
const processImageFileRef = useRef(null)
|
|
@@ -1209,7 +1269,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
1209
1269
|
displayW = maxWidth
|
|
1210
1270
|
}
|
|
1211
1271
|
|
|
1212
|
-
const uploadResult = await uploadImage(dataUrl,
|
|
1272
|
+
const uploadResult = await uploadImage(dataUrl, canvasId)
|
|
1213
1273
|
if (!uploadResult.success) {
|
|
1214
1274
|
console.error('[canvas] Image upload failed:', uploadResult.error)
|
|
1215
1275
|
return false
|
|
@@ -1224,7 +1284,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
1224
1284
|
pos = centerPositionForWidget(center, 'image', { width: displayW, height: displayH })
|
|
1225
1285
|
}
|
|
1226
1286
|
|
|
1227
|
-
const result = await addWidgetApi(
|
|
1287
|
+
const result = await addWidgetApi(canvasId, {
|
|
1228
1288
|
type: 'image',
|
|
1229
1289
|
props: { src: uploadResult.filename, private: false, width: displayW, height: displayH },
|
|
1230
1290
|
position: pos,
|
|
@@ -1271,8 +1331,8 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
1271
1331
|
const text = e.clipboardData?.getData('text/plain')?.trim()
|
|
1272
1332
|
if (!text) return
|
|
1273
1333
|
|
|
1274
|
-
// Detect
|
|
1275
|
-
// Also supports legacy
|
|
1334
|
+
// Detect canvasId::widgetId format for widget duplication (cross-canvas copy-paste)
|
|
1335
|
+
// Also supports legacy canvasId/widgetId for basenames without slashes,
|
|
1276
1336
|
// but only when the second segment looks like a widget ID (type-hash).
|
|
1277
1337
|
const widgetRefMatch = text.match(/^(.+)::([^:]+)$/) || (text.indexOf('::') === -1 && text.match(/^([^/]+)\/((?:sticky-note|markdown|prototype|link-preview|figma-embed|component|image)-[a-z0-9]+)$/))
|
|
1278
1338
|
if (widgetRefMatch) {
|
|
@@ -1282,7 +1342,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
1282
1342
|
if (sourceWidgetId.startsWith('jsx-')) return
|
|
1283
1343
|
try {
|
|
1284
1344
|
let sourceWidget = null
|
|
1285
|
-
if (sourceCanvas ===
|
|
1345
|
+
if (sourceCanvas === canvasId) {
|
|
1286
1346
|
sourceWidget = (localWidgets ?? []).find(w => w.id === sourceWidgetId)
|
|
1287
1347
|
} else {
|
|
1288
1348
|
const canvasData = await getCanvasApi(sourceCanvas)
|
|
@@ -1292,7 +1352,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
1292
1352
|
const center = getViewportCenter(scrollRef.current, zoomRef.current / 100)
|
|
1293
1353
|
const pos = centerPositionForWidget(center, sourceWidget.type, sourceWidget.props)
|
|
1294
1354
|
undoRedo.snapshot(stateRef.current, 'add')
|
|
1295
|
-
const result = await addWidgetApi(
|
|
1355
|
+
const result = await addWidgetApi(canvasId, {
|
|
1296
1356
|
type: sourceWidget.type,
|
|
1297
1357
|
props: { ...sourceWidget.props },
|
|
1298
1358
|
position: pos,
|
|
@@ -1316,7 +1376,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
1316
1376
|
const center = getViewportCenter(scrollRef.current, zoomRef.current / 100)
|
|
1317
1377
|
const pos = centerPositionForWidget(center, type, props)
|
|
1318
1378
|
try {
|
|
1319
|
-
const result = await addWidgetApi(
|
|
1379
|
+
const result = await addWidgetApi(canvasId, {
|
|
1320
1380
|
type,
|
|
1321
1381
|
props,
|
|
1322
1382
|
position: pos,
|
|
@@ -1332,7 +1392,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
1332
1392
|
|
|
1333
1393
|
document.addEventListener('paste', handlePaste)
|
|
1334
1394
|
return () => document.removeEventListener('paste', handlePaste)
|
|
1335
|
-
}, [
|
|
1395
|
+
}, [canvasId, undoRedo, localWidgets])
|
|
1336
1396
|
|
|
1337
1397
|
// --- Drag and drop handlers for images from Finder/file manager ---
|
|
1338
1398
|
// Separate effect to ensure listeners attach after scroll container mounts (loading=false)
|
|
@@ -1407,11 +1467,11 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
1407
1467
|
setLocalWidgets(previous.widgets)
|
|
1408
1468
|
setLocalSources(previous.sources)
|
|
1409
1469
|
queueWrite(() =>
|
|
1410
|
-
updateCanvas(
|
|
1470
|
+
updateCanvas(canvasId, { widgets: previous.widgets, sources: previous.sources }).catch((err) =>
|
|
1411
1471
|
console.error('[canvas] Failed to persist undo:', err)
|
|
1412
1472
|
)
|
|
1413
1473
|
)
|
|
1414
|
-
}, [
|
|
1474
|
+
}, [canvasId, debouncedSave, debouncedSourceSave, undoRedo])
|
|
1415
1475
|
|
|
1416
1476
|
const handleRedo = useCallback(() => {
|
|
1417
1477
|
const next = undoRedo.redo(stateRef.current)
|
|
@@ -1421,11 +1481,11 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
1421
1481
|
setLocalWidgets(next.widgets)
|
|
1422
1482
|
setLocalSources(next.sources)
|
|
1423
1483
|
queueWrite(() =>
|
|
1424
|
-
updateCanvas(
|
|
1484
|
+
updateCanvas(canvasId, { widgets: next.widgets, sources: next.sources }).catch((err) =>
|
|
1425
1485
|
console.error('[canvas] Failed to persist redo:', err)
|
|
1426
1486
|
)
|
|
1427
1487
|
)
|
|
1428
|
-
}, [
|
|
1488
|
+
}, [canvasId, debouncedSave, debouncedSourceSave, undoRedo])
|
|
1429
1489
|
|
|
1430
1490
|
// Keyboard shortcuts — dev-only (Cmd+Z / Cmd+Shift+Z)
|
|
1431
1491
|
useEffect(() => {
|
|
@@ -1595,7 +1655,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
1595
1655
|
if (!canvas) {
|
|
1596
1656
|
return (
|
|
1597
1657
|
<div className={styles.empty}>
|
|
1598
|
-
<p>Canvas “{
|
|
1658
|
+
<p>Canvas “{canvasId}” not found</p>
|
|
1599
1659
|
</div>
|
|
1600
1660
|
)
|
|
1601
1661
|
}
|
|
@@ -1623,6 +1683,15 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
1623
1683
|
const canvasThemeVars = getCanvasThemeVars(canvasTheme)
|
|
1624
1684
|
const canvasPrimerAttrs = getCanvasPrimerAttrs(canvasTheme)
|
|
1625
1685
|
|
|
1686
|
+
// Stable callback for deselecting all widgets
|
|
1687
|
+
const handleDeselectAll = useCallback(() => setSelectedWidgetIds(new Set()), [])
|
|
1688
|
+
|
|
1689
|
+
// Stable callback for widget removal + deselect
|
|
1690
|
+
const handleWidgetRemoveAndDeselect = useCallback((id) => {
|
|
1691
|
+
handleWidgetRemove(id)
|
|
1692
|
+
setSelectedWidgetIds(new Set())
|
|
1693
|
+
}, [handleWidgetRemove])
|
|
1694
|
+
|
|
1626
1695
|
// Merge JSX-sourced widgets (from .canvas.jsx) and JSON widgets
|
|
1627
1696
|
const allChildren = []
|
|
1628
1697
|
|
|
@@ -1653,7 +1722,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
1653
1722
|
selected={selectedWidgetIds.has(`jsx-${exportName}`)}
|
|
1654
1723
|
multiSelected={isMultiSelected && selectedWidgetIds.has(`jsx-${exportName}`)}
|
|
1655
1724
|
onSelect={(shiftKey) => handleWidgetSelect(`jsx-${exportName}`, shiftKey)}
|
|
1656
|
-
onDeselect={
|
|
1725
|
+
onDeselect={handleDeselectAll}
|
|
1657
1726
|
readOnly={!isLocalDev}
|
|
1658
1727
|
>
|
|
1659
1728
|
<ComponentWidget
|
|
@@ -1695,13 +1764,10 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
1695
1764
|
selected={selectedWidgetIds.has(widget.id)}
|
|
1696
1765
|
multiSelected={isMultiSelected && selectedWidgetIds.has(widget.id)}
|
|
1697
1766
|
onSelect={(shiftKey) => handleWidgetSelect(widget.id, shiftKey)}
|
|
1698
|
-
onDeselect={
|
|
1767
|
+
onDeselect={handleDeselectAll}
|
|
1699
1768
|
onUpdate={isLocalDev ? handleWidgetUpdate : undefined}
|
|
1700
1769
|
onCopy={isLocalDev ? handleWidgetCopy : undefined}
|
|
1701
|
-
onRemove={isLocalDev ?
|
|
1702
|
-
handleWidgetRemove(id)
|
|
1703
|
-
setSelectedWidgetIds(new Set())
|
|
1704
|
-
} : undefined}
|
|
1770
|
+
onRemove={isLocalDev ? handleWidgetRemoveAndDeselect : undefined}
|
|
1705
1771
|
readOnly={!isLocalDev}
|
|
1706
1772
|
/>
|
|
1707
1773
|
</div>
|
|
@@ -1713,8 +1779,8 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
1713
1779
|
return (
|
|
1714
1780
|
<>
|
|
1715
1781
|
<div className={styles.canvasTitle}>
|
|
1716
|
-
<h1 className={styles.canvasTitleStatic}>{canvasMeta?.title || canvas?.title ||
|
|
1717
|
-
<PageSelector currentName={
|
|
1782
|
+
<h1 className={styles.canvasTitleStatic}>{canvasMeta?.title || canvas?.title || canvasId.split('/').pop()}</h1>
|
|
1783
|
+
<PageSelector currentName={canvasId} pages={siblingPages} />
|
|
1718
1784
|
{isLocalDev && (
|
|
1719
1785
|
<span className={styles.localEditingLabel}>Local editing</span>
|
|
1720
1786
|
)}
|
|
@@ -1729,10 +1795,11 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
1729
1795
|
...canvasThemeVars,
|
|
1730
1796
|
...(spaceHeld ? { cursor: panningActive ? 'grabbing' : 'grab' } : {}),
|
|
1731
1797
|
}}
|
|
1732
|
-
onClick={
|
|
1798
|
+
onClick={handleDeselectAll}
|
|
1733
1799
|
onMouseDown={handlePanStart}
|
|
1734
1800
|
>
|
|
1735
1801
|
<div
|
|
1802
|
+
ref={zoomElRef}
|
|
1736
1803
|
data-storyboard-canvas-zoom
|
|
1737
1804
|
data-sb-canvas-theme={canvasTheme}
|
|
1738
1805
|
className={styles.canvasZoom}
|