@dfosco/storyboard 0.5.0-beta.38 → 0.5.0-beta.40

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@dfosco/storyboard",
3
- "version": "0.5.0-beta.38",
3
+ "version": "0.5.0-beta.40",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Storyboard prototyping framework — core engine, React integration, and canvas",
@@ -194,19 +194,23 @@ export default function PageSelector({ currentName, pages: initialPages, isLocal
194
194
  return
195
195
  }
196
196
  const route = result?.route
197
+ const newName = result?.name || oldName
197
198
  if (route) {
199
+ // Optimistic UI: replace the renamed entry in our local list so the
200
+ // selector reflects the change immediately (don't wait for HMR).
201
+ setPages(prev => prev.map(p => p.name === oldName
202
+ ? { ...p, name: newName, route, title: trimmed }
203
+ : p
204
+ ))
205
+ try { sessionStorage.setItem('sb-open-page-selector', '1') } catch { /* ignore */ }
206
+
207
+ // Navigate to the new URL immediately. The user is currently on the
208
+ // OLD route, which no longer resolves on the server — refreshing
209
+ // there would 404. Do a hard navigate so the SPA reloads and picks
210
+ // up the renamed canvas from the fresh virtual module.
198
211
  const base = (import.meta.env?.BASE_URL || '/').replace(/\/$/, '')
199
212
  const targetUrl = base + route
200
- try { sessionStorage.setItem('sb-open-page-selector', '1') } catch { /* ignore */ }
201
- if (import.meta.hot) {
202
- const timer = setTimeout(() => { window.location.href = targetUrl }, 3000)
203
- import.meta.hot.on('vite:beforeFullReload', () => {
204
- clearTimeout(timer)
205
- sessionStorage.setItem('sb-pending-navigate', targetUrl)
206
- })
207
- } else {
208
- setTimeout(() => { window.location.href = targetUrl }, 1000)
209
- }
213
+ window.location.href = targetUrl
210
214
  }
211
215
  } catch (err) {
212
216
  console.error('Failed to rename page:', err)
@@ -259,15 +263,11 @@ export default function PageSelector({ currentName, pages: initialPages, isLocal
259
263
 
260
264
  try { sessionStorage.setItem('sb-open-page-selector', '1') } catch { /* ignore */ }
261
265
 
262
- if (import.meta.hot) {
263
- const timer = setTimeout(() => { window.location.href = targetUrl }, 3000)
264
- import.meta.hot.on('vite:beforeFullReload', () => {
265
- clearTimeout(timer)
266
- sessionStorage.setItem('sb-pending-navigate', targetUrl)
267
- })
268
- } else {
269
- setTimeout(() => { window.location.href = targetUrl }, 1000)
270
- }
266
+ // Hard navigate immediately. With the route map now reading `canvases`
267
+ // live, an SPA navigation would also work for the new canvas — but
268
+ // some refs (e.g. `_jsxModule`) only get filled in by a fresh module
269
+ // build, so a hard reload is the safer default.
270
+ window.location.href = targetUrl
271
271
  } catch (err) {
272
272
  console.error('Failed to duplicate page:', err)
273
273
  }
@@ -363,18 +363,9 @@ export default function PageSelector({ currentName, pages: initialPages, isLocal
363
363
  // Stash a flag so the page selector opens automatically on the new page
364
364
  try { sessionStorage.setItem('sb-open-page-selector', '1') } catch { /* ignore */ }
365
365
 
366
- // Navigate to the new page after Vite picks up the new file
367
- if (import.meta.hot) {
368
- const timer = setTimeout(() => {
369
- window.location.href = targetUrl
370
- }, 3000)
371
- import.meta.hot.on('vite:beforeFullReload', () => {
372
- clearTimeout(timer)
373
- sessionStorage.setItem('sb-pending-navigate', targetUrl)
374
- })
375
- } else {
376
- setTimeout(() => { window.location.href = targetUrl }, 1000)
377
- }
366
+ // Navigate immediately. The server has already written the file; the
367
+ // route map will be rebuilt on hard reload.
368
+ window.location.href = targetUrl
378
369
  } catch (err) {
379
370
  console.error('Failed to create canvas page:', err)
380
371
  setCreating(false)
@@ -216,6 +216,7 @@ const PromptWidget = forwardRef(function PromptWidget({ id, props, onUpdate, res
216
216
  e.stopPropagation()
217
217
  }, [handleSubmit])
218
218
 
219
+ // eslint-disable-next-line react-hooks/preserve-manual-memoization
219
220
  const handleReset = useCallback(() => {
220
221
  setExecStatus('idle')
221
222
  setExecError('')
@@ -293,7 +293,7 @@ export default forwardRef(function TerminalWidget({ id, props, onUpdate, multiSe
293
293
  // letters). Especially visible on consumer installs where fonts come
294
294
  // over the network instead of from cache.
295
295
  if (typeof document !== 'undefined' && document.fonts?.ready) {
296
- try { await document.fonts.ready } catch {}
296
+ try { await document.fonts.ready } catch { /* font load failures are non-fatal */ }
297
297
  if (disposed) return
298
298
  }
299
299
 
@@ -52,43 +52,53 @@ class SectionErrorBoundary extends Component {
52
52
  }
53
53
  }
54
54
 
55
- // Build a map from canvas route paths canvas names at module load time
56
- const canvasRouteMap = new Map()
57
- // Build a map from group name → array of { name, route, title } for page selector
58
- const canvasGroupMap = new Map()
59
- for (const [name, data] of Object.entries(canvases || {})) {
60
- const route = (data?._route || `/canvas/${name}`).replace(/\/+$/, '')
61
- canvasRouteMap.set(route, name)
62
- const group = data?._group
63
- if (group) {
64
- if (!canvasGroupMap.has(group)) canvasGroupMap.set(group, [])
65
- canvasGroupMap.get(group).push({
55
+ // Canvas route resolution and group lookup happen live below
56
+ // (see matchCanvasRoute / getCanvasGroupMap) so that HMR rename/duplicate
57
+ // take effect without a page reload.
58
+
59
+ // Build a map from group name array of { name, route, title } for page selector.
60
+ // Read live from `canvases` (HMR mutates it in place) so rename/duplicate are
61
+ // reflected immediately. Sorted by pageOrder from .meta.json when available.
62
+ function getCanvasGroupMap() {
63
+ const map = new Map()
64
+ for (const [name, data] of Object.entries(canvases || {})) {
65
+ const route = (data?._route || `/canvas/${name}`).replace(/\/+$/, '')
66
+ const group = data?._group
67
+ if (!group) continue
68
+ if (!map.has(group)) map.set(group, [])
69
+ map.get(group).push({
66
70
  name,
67
71
  route,
68
72
  title: data?.title || name.split('/').pop(),
69
73
  _canvasMeta: data?._canvasMeta || null,
70
74
  })
71
75
  }
72
- }
73
- // Sort each group's pages by pageOrder from .meta.json (if available)
74
- for (const [, pages] of canvasGroupMap) {
75
- const pageOrder = pages[0]?._canvasMeta?.pageOrder
76
- if (Array.isArray(pageOrder)) {
77
- const orderMap = new Map()
78
- pageOrder.forEach((entry, idx) => {
79
- if (typeof entry === 'string' && !entry.startsWith('sep-')) orderMap.set(entry, idx)
80
- })
81
- pages.sort((a, b) => {
82
- const ai = orderMap.has(a.name) ? orderMap.get(a.name) : Infinity
83
- const bi = orderMap.has(b.name) ? orderMap.get(b.name) : Infinity
84
- return ai - bi
85
- })
76
+ for (const [, pages] of map) {
77
+ const pageOrder = pages[0]?._canvasMeta?.pageOrder
78
+ if (Array.isArray(pageOrder)) {
79
+ const orderMap = new Map()
80
+ pageOrder.forEach((entry, idx) => {
81
+ if (typeof entry === 'string' && !entry.startsWith('sep-')) orderMap.set(entry, idx)
82
+ })
83
+ pages.sort((a, b) => {
84
+ const ai = orderMap.has(a.name) ? orderMap.get(a.name) : Infinity
85
+ const bi = orderMap.has(b.name) ? orderMap.get(b.name) : Infinity
86
+ return ai - bi
87
+ })
88
+ }
86
89
  }
90
+ return map
87
91
  }
88
92
 
89
93
  function matchCanvasRoute(pathname) {
90
94
  const normalized = stripBasePath(pathname)
91
- return canvasRouteMap.get(normalized) || null
95
+ // Iterate `canvases` live so HMR rename/duplicate are reflected without
96
+ // a page reload (virtual-module HMR mutates the object in place).
97
+ for (const [name, data] of Object.entries(canvases || {})) {
98
+ const route = (data?._route || `/canvas/${name}`).replace(/\/+$/, '')
99
+ if (route === normalized) return name
100
+ }
101
+ return null
92
102
  }
93
103
 
94
104
  /**
@@ -219,6 +229,17 @@ function StoryboardProviderInner({ flowName, sceneName, recordName, recordParam,
219
229
  return () => document.removeEventListener('storyboard:story-index-changed', handler)
220
230
  }, [])
221
231
 
232
+ // Same pattern for canvases: HMR mutates the in-memory `canvases` object
233
+ // when files are added/renamed/removed. Re-run canvas route matching and
234
+ // sibling-page derivation when that happens so the SPA reflects the new
235
+ // state without requiring a hard reload (which would 404 on old URLs).
236
+ const [canvasIndexKey, setCanvasIndexKey] = useState(0)
237
+ useEffect(() => {
238
+ const handler = () => setCanvasIndexKey((k) => k + 1)
239
+ document.addEventListener('storyboard:canvas-index-changed', handler)
240
+ return () => document.removeEventListener('storyboard:canvas-index-changed', handler)
241
+ }, [])
242
+
222
243
  // Story route detection — matches current URL against registered story routes
223
244
  // storyIndexKey forces re-evaluation when HMR mutates the stories object in place
224
245
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -228,8 +249,10 @@ function StoryboardProviderInner({ flowName, sceneName, recordName, recordParam,
228
249
  [location.pathname, storyName],
229
250
  )
230
251
 
231
- // Canvas route detection — matches current URL against registered canvas routes
232
- const canvasId = useMemo(() => matchCanvasRoute(location.pathname), [location.pathname])
252
+ // Canvas route detection — matches current URL against registered canvas routes.
253
+ // canvasIndexKey forces re-evaluation when HMR mutates the canvases object in place
254
+ // eslint-disable-next-line react-hooks/exhaustive-deps
255
+ const canvasId = useMemo(() => matchCanvasRoute(location.pathname), [location.pathname, canvasIndexKey])
233
256
  const isMissingCanvasRoute = useMemo(
234
257
  () => isCanvasPath(location.pathname) && !canvasId && !storyName,
235
258
  [location.pathname, canvasId, storyName],
@@ -400,8 +423,11 @@ function StoryboardProviderInner({ flowName, sceneName, recordName, recordParam,
400
423
  const group = canvasData?._group
401
424
  // Include the current canvas as a sibling even if it's the only page in its group,
402
425
  // so the PageSelector can render and allow adding new pages.
426
+ // canvasIndexKey ensures siblingPages are re-derived on HMR
427
+ // eslint-disable-next-line no-unused-vars
428
+ const _hmrTick = canvasIndexKey
403
429
  const siblingPages = group
404
- ? canvasGroupMap.get(group) || []
430
+ ? getCanvasGroupMap().get(group) || []
405
431
  : [{ name: canvasId, route: canvasData?._route || `/canvas/${canvasId}`, title: canvasData?.title || canvasId.split('/').pop() }]
406
432
  const canvasMeta = canvasData?._canvasMeta || null
407
433
  const canvasValue = {