@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-beta.2",
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-beta.2",
8
- "@dfosco/tiny-canvas": "4.1.0-beta.2",
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
- () => filteredItems.filter(list => !list.id?.startsWith('cfg:sep')),
847
- [filteredItems]
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
- {filteredItems.length ? (
869
- filteredItems.map((list) => (
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
- if (Date.now() - timestamp > VIEWPORT_TTL_MS) {
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
- // Block saves until the new canvas's viewport is fully restored.
570
- viewportInitName.current = null
571
- const newViewport = loadViewportState(canvasId)
572
- pendingScrollRestore.current = newViewport
573
- // Restore zoom from the new canvas's saved state
574
- const newZoom = newViewport?.zoom ?? 100
575
- zoomRef.current = newZoom
576
- setZoom(newZoom)
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, {
@@ -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`