@dfosco/storyboard-react 3.11.0-beta.3 → 3.11.0-beta.4

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": "3.11.0-beta.3",
3
+ "version": "3.11.0-beta.4",
4
4
  "type": "module",
5
5
  "dependencies": {
6
- "@dfosco/storyboard-core": "3.11.0-beta.3",
7
- "@dfosco/tiny-canvas": "3.11.0-beta.3",
6
+ "@dfosco/storyboard-core": "3.11.0-beta.4",
7
+ "@dfosco/tiny-canvas": "3.11.0-beta.4",
8
8
  "@neodrag/react": "^2.3.1",
9
9
  "glob": "^11.0.0",
10
10
  "jsonc-parser": "^3.3.1"
@@ -130,6 +130,26 @@ function roundPosition(value) {
130
130
  return Math.round(value)
131
131
  }
132
132
 
133
+ /** Snap a value to the nearest grid line. */
134
+ function snapValue(value, gridSize) {
135
+ return Math.round(value / gridSize) * gridSize
136
+ }
137
+
138
+ /** Snap a position to the grid if snapping is enabled. */
139
+ function snapPosition(pos, gridSize, enabled) {
140
+ if (!enabled || !gridSize) return pos
141
+ return {
142
+ x: Math.max(0, snapValue(pos.x, gridSize)),
143
+ y: Math.max(0, snapValue(pos.y, gridSize)),
144
+ }
145
+ }
146
+
147
+ /** Snap a dimension to the grid if snapping is enabled. */
148
+ function snapDimension(value, gridSize, enabled, min = 0) {
149
+ if (!enabled || !gridSize) return value
150
+ return Math.max(min, snapValue(value, gridSize))
151
+ }
152
+
133
153
  /** Padding (canvas-space pixels) around bounding box for zoom-to-fit. */
134
154
  const FIT_PADDING = 48
135
155
 
@@ -264,6 +284,8 @@ export default function CanvasPage({ name }) {
264
284
  const titleInputRef = useRef(null)
265
285
  const [localSources, setLocalSources] = useState(canvas?.sources ?? [])
266
286
  const [canvasTheme, setCanvasTheme] = useState(() => resolveCanvasThemeFromStorage())
287
+ const [snapEnabled, setSnapEnabled] = useState(canvas?.snapToGrid ?? false)
288
+ const snapGridSize = canvas?.gridSize || 40
267
289
 
268
290
  // Undo/redo history — tracks both widgets and sources as a combined snapshot
269
291
  const undoRedo = useUndoRedo()
@@ -321,15 +343,21 @@ export default function CanvasPage({ name }) {
321
343
 
322
344
  const handleWidgetUpdate = useCallback((widgetId, updates) => {
323
345
  undoRedo.snapshot(stateRef.current, 'edit', widgetId)
346
+ // Snap width/height to grid when snap is enabled
347
+ const snapped = { ...updates }
348
+ if (snapEnabled && snapGridSize) {
349
+ if (snapped.width != null) snapped.width = snapDimension(snapped.width, snapGridSize, true, 60)
350
+ if (snapped.height != null) snapped.height = snapDimension(snapped.height, snapGridSize, true, 60)
351
+ }
324
352
  setLocalWidgets((prev) => {
325
353
  if (!prev) return prev
326
354
  const next = prev.map((w) =>
327
- w.id === widgetId ? { ...w, props: { ...w.props, ...updates } } : w
355
+ w.id === widgetId ? { ...w, props: { ...w.props, ...snapped } } : w
328
356
  )
329
357
  debouncedSave(name, next)
330
358
  return next
331
359
  })
332
- }, [name, debouncedSave, undoRedo])
360
+ }, [name, debouncedSave, undoRedo, snapEnabled, snapGridSize])
333
361
 
334
362
  const handleWidgetRemove = useCallback((widgetId) => {
335
363
  undoRedo.snapshot(stateRef.current, 'remove', widgetId)
@@ -390,7 +418,8 @@ export default function CanvasPage({ name }) {
390
418
 
391
419
  const handleItemDragEnd = useCallback((dragId, position) => {
392
420
  if (!dragId || !position) return
393
- const rounded = { x: Math.max(0, roundPosition(position.x)), y: Math.max(0, roundPosition(position.y)) }
421
+ const raw = { x: Math.max(0, roundPosition(position.x)), y: Math.max(0, roundPosition(position.y)) }
422
+ const rounded = snapPosition(raw, snapGridSize, snapEnabled)
394
423
 
395
424
  if (dragId.startsWith('jsx-')) {
396
425
  undoRedo.snapshot(stateRef.current, 'move', dragId)
@@ -423,7 +452,7 @@ export default function CanvasPage({ name }) {
423
452
  )
424
453
  return next
425
454
  })
426
- }, [name, undoRedo])
455
+ }, [name, undoRedo, snapEnabled, snapGridSize])
427
456
 
428
457
  useEffect(() => {
429
458
  zoomRef.current = zoom
@@ -647,6 +676,28 @@ export default function CanvasPage({ name }) {
647
676
  return () => document.removeEventListener('storyboard:canvas:set-zoom', handleZoom)
648
677
  }, [])
649
678
 
679
+ // Listen for snap-to-grid toggle from CoreUIBar
680
+ useEffect(() => {
681
+ function handleSnapToggle() {
682
+ setSnapEnabled((prev) => {
683
+ const next = !prev
684
+ updateCanvas(name, { snapToGrid: next }).catch((err) =>
685
+ console.error('[canvas] Failed to persist snap setting:', err)
686
+ )
687
+ return next
688
+ })
689
+ }
690
+ document.addEventListener('storyboard:canvas:toggle-snap', handleSnapToggle)
691
+ return () => document.removeEventListener('storyboard:canvas:toggle-snap', handleSnapToggle)
692
+ }, [name])
693
+
694
+ // Broadcast snap state to Svelte toolbar
695
+ useEffect(() => {
696
+ document.dispatchEvent(new CustomEvent('storyboard:canvas:snap-state', {
697
+ detail: { snapEnabled }
698
+ }))
699
+ }, [snapEnabled])
700
+
650
701
  // Listen for zoom-to-fit from CoreUIBar
651
702
  useEffect(() => {
652
703
  function handleZoomToFit() {