@dfosco/storyboard-react 3.10.0-beta.1 → 3.10.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 +3 -3
- package/src/canvas/CanvasPage.jsx +77 -8
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dfosco/storyboard-react",
|
|
3
|
-
"version": "3.10.0
|
|
3
|
+
"version": "3.10.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@dfosco/storyboard-core": "3.10.0
|
|
7
|
-
"@dfosco/tiny-canvas": "3.10.0
|
|
6
|
+
"@dfosco/storyboard-core": "3.10.0",
|
|
7
|
+
"@dfosco/tiny-canvas": "3.10.0",
|
|
8
8
|
"@neodrag/react": "^2.3.1",
|
|
9
9
|
"glob": "^11.0.0",
|
|
10
10
|
"jsonc-parser": "^3.3.1"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createElement, useCallback, useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { flushSync } from 'react-dom'
|
|
2
3
|
import { Canvas } from '@dfosco/tiny-canvas'
|
|
3
4
|
import '@dfosco/tiny-canvas/style.css'
|
|
4
5
|
import { useCanvas } from './useCanvas.js'
|
|
@@ -56,12 +57,41 @@ function debounce(fn, ms) {
|
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
/**
|
|
59
|
-
* Get viewport-center coordinates for placing a new widget.
|
|
60
|
+
* Get viewport-center coordinates in canvas space for placing a new widget.
|
|
61
|
+
* Converts the visible center of the scroll container to unscaled canvas coordinates.
|
|
60
62
|
*/
|
|
61
|
-
function getViewportCenter() {
|
|
63
|
+
function getViewportCenter(scrollEl, scale) {
|
|
64
|
+
if (!scrollEl) {
|
|
65
|
+
return { x: 0, y: 0 }
|
|
66
|
+
}
|
|
67
|
+
const cx = scrollEl.scrollLeft + scrollEl.clientWidth / 2
|
|
68
|
+
const cy = scrollEl.scrollTop + scrollEl.clientHeight / 2
|
|
69
|
+
return {
|
|
70
|
+
x: Math.round(cx / scale),
|
|
71
|
+
y: Math.round(cy / scale),
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Fallback sizes for widget types without explicit width/height defaults. */
|
|
76
|
+
const WIDGET_FALLBACK_SIZES = {
|
|
77
|
+
'sticky-note': { width: 180, height: 60 },
|
|
78
|
+
'markdown': { width: 360, height: 200 },
|
|
79
|
+
'prototype': { width: 800, height: 600 },
|
|
80
|
+
'link-preview': { width: 320, height: 120 },
|
|
81
|
+
'component': { width: 200, height: 150 },
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Offset a position so the widget's center (not its top-left corner)
|
|
86
|
+
* lands on the given point.
|
|
87
|
+
*/
|
|
88
|
+
function centerPositionForWidget(pos, type, props) {
|
|
89
|
+
const fallback = WIDGET_FALLBACK_SIZES[type] || { width: 200, height: 150 }
|
|
90
|
+
const w = props?.width ?? fallback.width
|
|
91
|
+
const h = props?.height ?? fallback.height
|
|
62
92
|
return {
|
|
63
|
-
x: Math.round(
|
|
64
|
-
y: Math.round(
|
|
93
|
+
x: Math.round(pos.x - w / 2),
|
|
94
|
+
y: Math.round(pos.y - h / 2),
|
|
65
95
|
}
|
|
66
96
|
}
|
|
67
97
|
|
|
@@ -257,6 +287,43 @@ export default function CanvasPage({ name }) {
|
|
|
257
287
|
zoomRef.current = zoom
|
|
258
288
|
}, [zoom])
|
|
259
289
|
|
|
290
|
+
/**
|
|
291
|
+
* Zoom to a new level, anchoring on an optional client-space point.
|
|
292
|
+
* When a cursor position is provided (e.g. from a wheel event), the
|
|
293
|
+
* canvas point under the cursor stays fixed. Otherwise falls back to
|
|
294
|
+
* the viewport center.
|
|
295
|
+
*/
|
|
296
|
+
function applyZoom(newZoom, clientX, clientY) {
|
|
297
|
+
const el = scrollRef.current
|
|
298
|
+
const clampedZoom = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, newZoom))
|
|
299
|
+
|
|
300
|
+
if (!el) {
|
|
301
|
+
setZoom(clampedZoom)
|
|
302
|
+
return
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const oldScale = zoomRef.current / 100
|
|
306
|
+
const newScale = clampedZoom / 100
|
|
307
|
+
|
|
308
|
+
// Anchor point in scroll-container space
|
|
309
|
+
const rect = el.getBoundingClientRect()
|
|
310
|
+
const useViewportCenter = clientX == null || clientY == null
|
|
311
|
+
const anchorX = useViewportCenter ? el.clientWidth / 2 : clientX - rect.left
|
|
312
|
+
const anchorY = useViewportCenter ? el.clientHeight / 2 : clientY - rect.top
|
|
313
|
+
|
|
314
|
+
// Anchor → canvas coordinate
|
|
315
|
+
const canvasX = (el.scrollLeft + anchorX) / oldScale
|
|
316
|
+
const canvasY = (el.scrollTop + anchorY) / oldScale
|
|
317
|
+
|
|
318
|
+
// Synchronous render so the DOM has the new transform before we adjust scroll
|
|
319
|
+
zoomRef.current = clampedZoom
|
|
320
|
+
flushSync(() => setZoom(clampedZoom))
|
|
321
|
+
|
|
322
|
+
// Scroll so the same canvas point stays under the anchor
|
|
323
|
+
el.scrollLeft = canvasX * newScale - anchorX
|
|
324
|
+
el.scrollTop = canvasY * newScale - anchorY
|
|
325
|
+
}
|
|
326
|
+
|
|
260
327
|
// Signal canvas mount/unmount to CoreUIBar
|
|
261
328
|
useEffect(() => {
|
|
262
329
|
window[CANVAS_BRIDGE_STATE_KEY] = { active: true, name, zoom: zoomRef.current }
|
|
@@ -281,7 +348,8 @@ export default function CanvasPage({ name }) {
|
|
|
281
348
|
// Add a widget by type — used by CanvasControls and CoreUIBar event
|
|
282
349
|
const addWidget = useCallback(async (type) => {
|
|
283
350
|
const defaultProps = schemas[type] ? getDefaults(schemas[type]) : {}
|
|
284
|
-
const
|
|
351
|
+
const center = getViewportCenter(scrollRef.current, zoomRef.current / 100)
|
|
352
|
+
const pos = centerPositionForWidget(center, type, defaultProps)
|
|
285
353
|
try {
|
|
286
354
|
const result = await addWidgetApi(name, {
|
|
287
355
|
type,
|
|
@@ -310,7 +378,7 @@ export default function CanvasPage({ name }) {
|
|
|
310
378
|
function handleZoom(e) {
|
|
311
379
|
const { zoom: newZoom } = e.detail
|
|
312
380
|
if (typeof newZoom === 'number') {
|
|
313
|
-
|
|
381
|
+
applyZoom(newZoom)
|
|
314
382
|
}
|
|
315
383
|
}
|
|
316
384
|
document.addEventListener('storyboard:canvas:set-zoom', handleZoom)
|
|
@@ -419,7 +487,8 @@ export default function CanvasPage({ name }) {
|
|
|
419
487
|
props = { content: text }
|
|
420
488
|
}
|
|
421
489
|
|
|
422
|
-
const
|
|
490
|
+
const center = getViewportCenter(scrollRef.current, zoomRef.current / 100)
|
|
491
|
+
const pos = centerPositionForWidget(center, type, props)
|
|
423
492
|
try {
|
|
424
493
|
const result = await addWidgetApi(name, {
|
|
425
494
|
type,
|
|
@@ -449,7 +518,7 @@ export default function CanvasPage({ name }) {
|
|
|
449
518
|
const step = Math.trunc(zoomAccum.current)
|
|
450
519
|
if (step === 0) return
|
|
451
520
|
zoomAccum.current -= step
|
|
452
|
-
|
|
521
|
+
applyZoom(zoomRef.current + step, e.clientX, e.clientY)
|
|
453
522
|
}
|
|
454
523
|
document.addEventListener('wheel', handleWheel, { passive: false })
|
|
455
524
|
return () => document.removeEventListener('wheel', handleWheel)
|