@dfosco/storyboard-react 4.1.0-beta.2 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dfosco/storyboard-react",
|
|
3
|
-
"version": "4.1.0
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@base-ui/react": "^1.4.0",
|
|
7
|
-
"@dfosco/storyboard-core": "4.1.0
|
|
8
|
-
"@dfosco/tiny-canvas": "4.1.0
|
|
7
|
+
"@dfosco/storyboard-core": "4.1.0",
|
|
8
|
+
"@dfosco/tiny-canvas": "4.1.0",
|
|
9
9
|
"@neodrag/react": "^2.3.1",
|
|
10
10
|
"glob": "^11.0.0",
|
|
11
11
|
"jsonc-parser": "^3.3.1",
|
|
@@ -841,10 +841,23 @@ export default function StoryboardCommandPalette({ basePath }) {
|
|
|
841
841
|
return result
|
|
842
842
|
}, [items, search, subPageGroups, authorIndex])
|
|
843
843
|
|
|
844
|
+
// Remove consecutive separators and leading/trailing separators
|
|
845
|
+
const deduplicatedItems = useMemo(() => {
|
|
846
|
+
const result = []
|
|
847
|
+
for (const item of filteredItems) {
|
|
848
|
+
const isSep = item.id?.startsWith('cfg:sep')
|
|
849
|
+
if (isSep && (result.length === 0 || result[result.length - 1].id?.startsWith('cfg:sep'))) continue
|
|
850
|
+
result.push(item)
|
|
851
|
+
}
|
|
852
|
+
// Remove trailing separator
|
|
853
|
+
while (result.length > 0 && result[result.length - 1].id?.startsWith('cfg:sep')) result.pop()
|
|
854
|
+
return result
|
|
855
|
+
}, [filteredItems])
|
|
856
|
+
|
|
844
857
|
// Items without separators — used for keyboard navigation indexing
|
|
845
858
|
const navigableItems = useMemo(
|
|
846
|
-
() =>
|
|
847
|
-
[
|
|
859
|
+
() => deduplicatedItems.filter(list => !list.id?.startsWith('cfg:sep')),
|
|
860
|
+
[deduplicatedItems]
|
|
848
861
|
)
|
|
849
862
|
|
|
850
863
|
const handleChangeSearch = useCallback((value) => {
|
|
@@ -865,8 +878,8 @@ export default function StoryboardCommandPalette({ basePath }) {
|
|
|
865
878
|
}
|
|
866
879
|
>
|
|
867
880
|
<CommandPalette.Page id="root">
|
|
868
|
-
{
|
|
869
|
-
|
|
881
|
+
{deduplicatedItems.length ? (
|
|
882
|
+
deduplicatedItems.map((list) => (
|
|
870
883
|
list.id?.startsWith('cfg:sep') ? (
|
|
871
884
|
!search && <hr key={list.id} style={{ border: 'none', borderTop: '1px solid var(--borderColor-muted, #e5e5e5)', margin: '4px 14px' }} />
|
|
872
885
|
) : (
|
|
@@ -128,13 +128,16 @@ function getViewportStorageKey(canvasId) {
|
|
|
128
128
|
function loadViewportState(canvasId) {
|
|
129
129
|
try {
|
|
130
130
|
const raw = localStorage.getItem(getViewportStorageKey(canvasId))
|
|
131
|
-
if (!raw) return null
|
|
131
|
+
if (!raw) { console.log('[viewport] no saved state for', canvasId); return null }
|
|
132
132
|
const state = JSON.parse(raw)
|
|
133
133
|
const timestamp = typeof state.timestamp === 'number' ? state.timestamp : 0
|
|
134
|
-
|
|
134
|
+
const age = Date.now() - timestamp
|
|
135
|
+
if (age > VIEWPORT_TTL_MS) {
|
|
136
|
+
console.log('[viewport] stale state for', canvasId, '— age:', Math.round(age / 1000), 's')
|
|
135
137
|
localStorage.removeItem(getViewportStorageKey(canvasId))
|
|
136
138
|
return null
|
|
137
139
|
}
|
|
140
|
+
console.log('[viewport] loaded state for', canvasId, '— age:', Math.round(age / 1000), 's, zoom:', state.zoom, 'scroll:', state.scrollLeft, state.scrollTop)
|
|
138
141
|
return {
|
|
139
142
|
zoom: typeof state.zoom === 'number' ? Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, state.zoom)) : null,
|
|
140
143
|
scrollLeft: typeof state.scrollLeft === 'number' ? state.scrollLeft : null,
|
|
@@ -560,20 +563,24 @@ export default function CanvasPage({ canvasId: canvasIdProp, name, siblingPages
|
|
|
560
563
|
}, [])
|
|
561
564
|
|
|
562
565
|
if (canvas !== trackedCanvas) {
|
|
566
|
+
const isCanvasSwitch = trackedCanvas && canvas && trackedCanvas._route !== canvas._route
|
|
567
|
+
console.log('[viewport] canvas changed —', isCanvasSwitch ? 'new canvas, resetting viewport' : 'same canvas, updating widgets only')
|
|
563
568
|
setTrackedCanvas(canvas)
|
|
564
569
|
setLocalWidgets(canvas?.widgets ?? null)
|
|
565
570
|
setLocalSources(canvas?.sources ?? [])
|
|
566
571
|
setSnapEnabled(canvas?.snapToGrid ?? false)
|
|
567
572
|
setSnapGridSize(canvas?.gridSize || 40)
|
|
568
573
|
undoRedo.reset()
|
|
569
|
-
//
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
574
|
+
// Only reset viewport state when switching to a different canvas,
|
|
575
|
+
// not when the same canvas refreshes with server data.
|
|
576
|
+
if (isCanvasSwitch) {
|
|
577
|
+
viewportInitName.current = null
|
|
578
|
+
const newViewport = loadViewportState(canvasId)
|
|
579
|
+
pendingScrollRestore.current = newViewport
|
|
580
|
+
const newZoom = newViewport?.zoom ?? 100
|
|
581
|
+
zoomRef.current = newZoom
|
|
582
|
+
setZoom(newZoom)
|
|
583
|
+
}
|
|
577
584
|
}
|
|
578
585
|
|
|
579
586
|
// Debounced save to server
|
|
@@ -861,11 +868,13 @@ export default function CanvasPage({ canvasId: canvasIdProp, name, siblingPages
|
|
|
861
868
|
if (!el || loading) return
|
|
862
869
|
const saved = pendingScrollRestore.current
|
|
863
870
|
if (saved) {
|
|
871
|
+
console.log('[viewport] restoring saved viewport — zoom:', saved.zoom, 'scroll:', saved.scrollLeft, saved.scrollTop)
|
|
864
872
|
// Fresh saved viewport — restore exactly
|
|
865
873
|
if (saved.scrollLeft != null) el.scrollLeft = saved.scrollLeft
|
|
866
874
|
if (saved.scrollTop != null) el.scrollTop = saved.scrollTop
|
|
867
875
|
pendingScrollRestore.current = null
|
|
868
876
|
} else {
|
|
877
|
+
console.log('[viewport] no saved viewport — fitting to objects')
|
|
869
878
|
// No saved state or stale — zoom-to-fit all objects
|
|
870
879
|
const bounds = computeCanvasBounds(localWidgets, componentEntries)
|
|
871
880
|
if (bounds && el.clientWidth > 0 && el.clientHeight > 0) {
|
|
@@ -949,6 +958,7 @@ export default function CanvasPage({ canvasId: canvasIdProp, name, siblingPages
|
|
|
949
958
|
useEffect(() => {
|
|
950
959
|
if (viewportInitName.current !== canvasId) return
|
|
951
960
|
const el = scrollRef.current
|
|
961
|
+
console.log('[viewport] saving — zoom:', zoom, 'scroll:', el?.scrollLeft, el?.scrollTop)
|
|
952
962
|
// Read current scroll so the zoom entry doesn't zero-out position,
|
|
953
963
|
// but the authoritative scroll save comes from the scroll handler.
|
|
954
964
|
saveViewportState(canvasId, {
|
package/src/canvas/useCanvas.js
CHANGED
|
@@ -6,6 +6,8 @@ import { getCanvasData } from '@dfosco/storyboard-core'
|
|
|
6
6
|
* Falls back to build-time data if the server is unavailable.
|
|
7
7
|
*/
|
|
8
8
|
async function fetchCanvasFromServer(name) {
|
|
9
|
+
// Canvas server API is only available during local dev
|
|
10
|
+
if (import.meta.env?.PROD) return null
|
|
9
11
|
try {
|
|
10
12
|
const base = (import.meta.env?.BASE_URL || '/').replace(/\/$/, '')
|
|
11
13
|
const res = await fetch(`${base}/_storyboard/canvas/read?name=${encodeURIComponent(name)}`)
|
|
@@ -101,6 +103,12 @@ export function useCanvas(canvasId) {
|
|
|
101
103
|
const handleCanvasFileChanged = ({ data }) => {
|
|
102
104
|
const eventId = data?.canvasId || data?.name
|
|
103
105
|
if (!data || eventId !== canvasId) return
|
|
106
|
+
// Use metadata from the HMR event directly if available (faster)
|
|
107
|
+
if (data.metadata?.widgets) {
|
|
108
|
+
setCanvas((prev) => ({ ...(prev || buildTimeCanvas), ...data.metadata }))
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
// Fallback: re-fetch from server
|
|
104
112
|
fetchCanvasFromServer(canvasId).then((fresh) => {
|
|
105
113
|
if (fresh) {
|
|
106
114
|
setCanvas((prev) => ({ ...(prev || buildTimeCanvas), ...fresh }))
|
|
@@ -24,6 +24,9 @@ const ImageWidget = forwardRef(function ImageWidget({ props, onUpdate, resizable
|
|
|
24
24
|
|
|
25
25
|
const src = readProp(props, 'src', imageSchema)
|
|
26
26
|
const isPrivate = readProp(props, 'private', imageSchema)
|
|
27
|
+
|
|
28
|
+
// Private images are not included in production builds
|
|
29
|
+
const isHiddenInProd = isPrivate && import.meta.env?.PROD
|
|
27
30
|
const width = readProp(props, 'width', imageSchema)
|
|
28
31
|
const height = readProp(props, 'height', imageSchema)
|
|
29
32
|
|
|
@@ -77,7 +80,7 @@ const ImageWidget = forwardRef(function ImageWidget({ props, onUpdate, resizable
|
|
|
77
80
|
}
|
|
78
81
|
}), [src, onUpdate])
|
|
79
82
|
|
|
80
|
-
if (!src) return null
|
|
83
|
+
if (!src || isHiddenInProd) return null
|
|
81
84
|
|
|
82
85
|
const sizeStyle = {}
|
|
83
86
|
if (typeof width === 'number') sizeStyle.width = `${width}px`
|