@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 +3 -3
- package/src/canvas/CanvasPage.jsx +55 -4
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dfosco/storyboard-react",
|
|
3
|
-
"version": "3.11.0-beta.
|
|
3
|
+
"version": "3.11.0-beta.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@dfosco/storyboard-core": "3.11.0-beta.
|
|
7
|
-
"@dfosco/tiny-canvas": "3.11.0-beta.
|
|
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, ...
|
|
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
|
|
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() {
|