@dfosco/storyboard-react 4.0.0-beta.12 → 4.0.0-beta.13
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": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.13",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@dfosco/storyboard-core": "4.0.0-beta.
|
|
7
|
-
"@dfosco/tiny-canvas": "4.0.0-beta.
|
|
6
|
+
"@dfosco/storyboard-core": "4.0.0-beta.13",
|
|
7
|
+
"@dfosco/tiny-canvas": "4.0.0-beta.13",
|
|
8
8
|
"@neodrag/react": "^2.3.1",
|
|
9
9
|
"glob": "^11.0.0",
|
|
10
10
|
"jsonc-parser": "^3.3.1",
|
|
@@ -12,7 +12,7 @@ import { isFigmaUrl, sanitizeFigmaUrl } from './widgets/figmaUrl.js'
|
|
|
12
12
|
import WidgetChrome from './widgets/WidgetChrome.jsx'
|
|
13
13
|
import ComponentWidget from './widgets/ComponentWidget.jsx'
|
|
14
14
|
import useUndoRedo from './useUndoRedo.js'
|
|
15
|
-
import { addWidget as addWidgetApi, updateCanvas, removeWidget as removeWidgetApi, uploadImage } from './canvasApi.js'
|
|
15
|
+
import { addWidget as addWidgetApi, updateCanvas, removeWidget as removeWidgetApi, uploadImage, getCanvas as getCanvasApi } from './canvasApi.js'
|
|
16
16
|
import styles from './CanvasPage.module.css'
|
|
17
17
|
|
|
18
18
|
const ZOOM_MIN = 25
|
|
@@ -51,6 +51,7 @@ function resolveCanvasThemeFromStorage() {
|
|
|
51
51
|
* Get the copyable URL for a widget based on its type.
|
|
52
52
|
* Returns the most relevant URL/path for the widget content.
|
|
53
53
|
*/
|
|
54
|
+
// eslint-disable-next-line no-unused-vars
|
|
54
55
|
function getWidgetCopyableUrl(widget) {
|
|
55
56
|
const { type, props = {} } = widget
|
|
56
57
|
const base = (typeof import.meta !== 'undefined' && import.meta.env?.BASE_URL) || '/'
|
|
@@ -403,13 +404,13 @@ export default function CanvasPage({ name }) {
|
|
|
403
404
|
// Flag to suppress the click-based selection reset that fires after a drag
|
|
404
405
|
const justDraggedRef = useRef(false)
|
|
405
406
|
|
|
406
|
-
const handleItemDragStart = useCallback((dragId
|
|
407
|
+
const handleItemDragStart = useCallback((dragId) => {
|
|
407
408
|
const ids = selectedIdsRef.current
|
|
408
409
|
peerArticlesRef.current.clear()
|
|
409
410
|
if (ids.size <= 1 || !ids.has(dragId)) return
|
|
410
411
|
|
|
411
412
|
// Suppress selection changes for the duration of the drag
|
|
412
|
-
justDraggedRef.current = true
|
|
413
|
+
justDraggedRef.current = true // eslint-disable-line react-hooks/immutability
|
|
413
414
|
|
|
414
415
|
// Collect peer article elements for transition on drag end
|
|
415
416
|
for (const id of ids) {
|
|
@@ -449,6 +450,8 @@ export default function CanvasPage({ name }) {
|
|
|
449
450
|
setLocalWidgets(canvas?.widgets ?? null)
|
|
450
451
|
setLocalSources(canvas?.sources ?? [])
|
|
451
452
|
setCanvasTitle(canvas?.title || name)
|
|
453
|
+
setSnapEnabled(canvas?.snapToGrid ?? false)
|
|
454
|
+
setSnapGridSize(canvas?.gridSize || 40)
|
|
452
455
|
undoRedo.reset()
|
|
453
456
|
}
|
|
454
457
|
|
|
@@ -575,7 +578,7 @@ export default function CanvasPage({ name }) {
|
|
|
575
578
|
if (ids.size > 1 && ids.has(dragId)) {
|
|
576
579
|
transitionPeers()
|
|
577
580
|
// Suppress the click-based selection reset that fires after pointerup
|
|
578
|
-
justDraggedRef.current = true
|
|
581
|
+
justDraggedRef.current = true // eslint-disable-line react-hooks/immutability
|
|
579
582
|
requestAnimationFrame(() => { justDraggedRef.current = false })
|
|
580
583
|
undoRedo.snapshot(stateRef.current, 'multi-move')
|
|
581
584
|
|
|
@@ -911,7 +914,7 @@ export default function CanvasPage({ name }) {
|
|
|
911
914
|
function handleSnapToggle() {
|
|
912
915
|
setSnapEnabled((prev) => {
|
|
913
916
|
const next = !prev
|
|
914
|
-
updateCanvas(name, { snapToGrid: next }).catch((err) =>
|
|
917
|
+
updateCanvas(name, { settings: { snapToGrid: next } }).catch((err) =>
|
|
915
918
|
console.error('[canvas] Failed to persist snap setting:', err)
|
|
916
919
|
)
|
|
917
920
|
return next
|
|
@@ -929,6 +932,17 @@ export default function CanvasPage({ name }) {
|
|
|
929
932
|
snapEnabledRef.current = snapEnabled
|
|
930
933
|
}, [snapEnabled])
|
|
931
934
|
|
|
935
|
+
// Respond to snap-state requests from Svelte toolbar (handles mount-order race)
|
|
936
|
+
useEffect(() => {
|
|
937
|
+
function handleRequest() {
|
|
938
|
+
document.dispatchEvent(new CustomEvent('storyboard:canvas:snap-state', {
|
|
939
|
+
detail: { snapEnabled: snapEnabledRef.current }
|
|
940
|
+
}))
|
|
941
|
+
}
|
|
942
|
+
document.addEventListener('storyboard:canvas:snap-state-request', handleRequest)
|
|
943
|
+
return () => document.removeEventListener('storyboard:canvas:snap-state-request', handleRequest)
|
|
944
|
+
}, [])
|
|
945
|
+
|
|
932
946
|
// Listen for gridSize from Svelte toolbar config
|
|
933
947
|
useEffect(() => {
|
|
934
948
|
function handleGridSize(e) {
|
|
@@ -1015,33 +1029,13 @@ export default function CanvasPage({ name }) {
|
|
|
1015
1029
|
e.preventDefault()
|
|
1016
1030
|
setSelectedWidgetIds(new Set())
|
|
1017
1031
|
}
|
|
1018
|
-
// Copy
|
|
1019
|
-
//
|
|
1020
|
-
// - Shift+C (no cmd) → copy widget ID (or file path for images)
|
|
1032
|
+
// Copy shortcut (single widget selected):
|
|
1033
|
+
// cmd+c → copy canvasName/widgetId (for cross-canvas paste-duplicate)
|
|
1021
1034
|
const mod = e.metaKey || e.ctrlKey
|
|
1022
1035
|
if (mod && e.key === 'c' && !e.shiftKey && selectedWidgetIds.size === 1) {
|
|
1023
1036
|
const widgetId = [...selectedWidgetIds][0]
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
e.preventDefault()
|
|
1027
|
-
const url = getWidgetCopyableUrl(widget)
|
|
1028
|
-
if (url) {
|
|
1029
|
-
navigator.clipboard.writeText(url).catch(() => {})
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
// Shift+C (uppercase C, no cmd) → copy ID or file path
|
|
1034
|
-
if (e.key === 'C' && e.shiftKey && !mod && selectedWidgetIds.size === 1) {
|
|
1035
|
-
const widgetId = [...selectedWidgetIds][0]
|
|
1036
|
-
const widget = localWidgets?.find(w => w.id === widgetId)
|
|
1037
|
-
if (widget) {
|
|
1038
|
-
e.preventDefault()
|
|
1039
|
-
if (widget.type === 'image' && widget.props?.src) {
|
|
1040
|
-
navigator.clipboard.writeText(`src/canvas/images/${widget.props.src}`).catch(() => {})
|
|
1041
|
-
} else {
|
|
1042
|
-
navigator.clipboard.writeText(widgetId).catch(() => {})
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1037
|
+
e.preventDefault()
|
|
1038
|
+
navigator.clipboard.writeText(`${name}/${widgetId}`).catch(() => {})
|
|
1045
1039
|
}
|
|
1046
1040
|
if (e.key === 'Delete' || e.key === 'Backspace') {
|
|
1047
1041
|
e.preventDefault()
|
|
@@ -1217,6 +1211,40 @@ export default function CanvasPage({ name }) {
|
|
|
1217
1211
|
|
|
1218
1212
|
e.preventDefault()
|
|
1219
1213
|
|
|
1214
|
+
// Detect canvasName/widgetId format for widget duplication (cross-canvas copy-paste)
|
|
1215
|
+
const widgetRefMatch = text.match(/^([^/]+)\/([^/]+)$/)
|
|
1216
|
+
if (widgetRefMatch) {
|
|
1217
|
+
const [, sourceCanvas, sourceWidgetId] = widgetRefMatch
|
|
1218
|
+
// Component widgets are code, not duplicable data — silently consume the ref
|
|
1219
|
+
if (sourceWidgetId.startsWith('jsx-')) return
|
|
1220
|
+
try {
|
|
1221
|
+
let sourceWidget = null
|
|
1222
|
+
if (sourceCanvas === name) {
|
|
1223
|
+
sourceWidget = (localWidgets ?? []).find(w => w.id === sourceWidgetId)
|
|
1224
|
+
} else {
|
|
1225
|
+
const canvasData = await getCanvasApi(sourceCanvas)
|
|
1226
|
+
sourceWidget = (canvasData?.widgets ?? []).find(w => w.id === sourceWidgetId)
|
|
1227
|
+
}
|
|
1228
|
+
if (sourceWidget) {
|
|
1229
|
+
const center = getViewportCenter(scrollRef.current, zoomRef.current / 100)
|
|
1230
|
+
const pos = centerPositionForWidget(center, sourceWidget.type, sourceWidget.props)
|
|
1231
|
+
undoRedo.snapshot(stateRef.current, 'add')
|
|
1232
|
+
const result = await addWidgetApi(name, {
|
|
1233
|
+
type: sourceWidget.type,
|
|
1234
|
+
props: { ...sourceWidget.props },
|
|
1235
|
+
position: pos,
|
|
1236
|
+
})
|
|
1237
|
+
if (result.success && result.widget) {
|
|
1238
|
+
setLocalWidgets((prev) => [...(prev || []), result.widget])
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
} catch (err) {
|
|
1242
|
+
console.error('[canvas] Failed to paste widget reference:', err)
|
|
1243
|
+
}
|
|
1244
|
+
// Always consume the ref — never fall through to markdown creation
|
|
1245
|
+
return
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1220
1248
|
let type, props
|
|
1221
1249
|
const url = looksLikeWebUrl(text)
|
|
1222
1250
|
if (url) {
|
|
@@ -1256,7 +1284,7 @@ export default function CanvasPage({ name }) {
|
|
|
1256
1284
|
|
|
1257
1285
|
document.addEventListener('paste', handlePaste)
|
|
1258
1286
|
return () => document.removeEventListener('paste', handlePaste)
|
|
1259
|
-
}, [name, undoRedo])
|
|
1287
|
+
}, [name, undoRedo, localWidgets])
|
|
1260
1288
|
|
|
1261
1289
|
// --- Drag and drop handlers for images from Finder/file manager ---
|
|
1262
1290
|
// Separate effect to ensure listeners attach after scroll container mounts (loading=false)
|
|
@@ -103,6 +103,12 @@
|
|
|
103
103
|
overflow: visible;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
/* Elevate stacking context for hovered/selected widgets so their chrome
|
|
107
|
+
(toolbar, menus, selection outline) renders above sibling widgets. */
|
|
108
|
+
:global(.tc-drag:has([data-tc-elevated])) {
|
|
109
|
+
z-index: 1;
|
|
110
|
+
}
|
|
111
|
+
|
|
106
112
|
.localEditingLabel {
|
|
107
113
|
display: inline-flex;
|
|
108
114
|
align-items: center;
|
package/src/canvas/canvasApi.js
CHANGED
|
@@ -47,3 +47,7 @@ export function uploadImage(dataUrl, canvasName) {
|
|
|
47
47
|
export function toggleImagePrivacy(filename) {
|
|
48
48
|
return request('/image/toggle-private', 'POST', { filename })
|
|
49
49
|
}
|
|
50
|
+
|
|
51
|
+
export function getCanvas(name) {
|
|
52
|
+
return request(`/read?name=${encodeURIComponent(name)}`, 'GET')
|
|
53
|
+
}
|
|
@@ -421,6 +421,7 @@ export default function WidgetChrome({
|
|
|
421
421
|
return (
|
|
422
422
|
<div
|
|
423
423
|
className={styles.chromeContainer}
|
|
424
|
+
data-tc-elevated={(hovered || selected) || undefined}
|
|
424
425
|
onMouseEnter={(readOnly && !hasFeatures) ? undefined : handleMouseEnter}
|
|
425
426
|
onMouseLeave={(readOnly && !hasFeatures) ? undefined : handleMouseLeave}
|
|
426
427
|
>
|
|
@@ -17,10 +17,12 @@ import { subscribeToStorage, getStorageSnapshot } from '@dfosco/storyboard-core'
|
|
|
17
17
|
*
|
|
18
18
|
* @param {string} [path] - Dot-notation path (e.g. 'user.profile.name').
|
|
19
19
|
* Omit to get the entire flow object.
|
|
20
|
+
* @param {{ optional?: boolean }} [opts] - Pass { optional: true } to suppress
|
|
21
|
+
* the "path not found" warning for optional data.
|
|
20
22
|
* @returns {*} The resolved value. Returns {} if path is missing after loading.
|
|
21
23
|
* @throws If used outside a StoryboardProvider.
|
|
22
24
|
*/
|
|
23
|
-
export function useFlowData(path) {
|
|
25
|
+
export function useFlowData(path, opts) {
|
|
24
26
|
const context = useContext(StoryboardContext)
|
|
25
27
|
|
|
26
28
|
if (context === null) {
|
|
@@ -73,7 +75,7 @@ export function useFlowData(path) {
|
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
if (sceneValue === undefined) {
|
|
76
|
-
if (data != null && Object.keys(data).length > 0) {
|
|
78
|
+
if (!opts?.optional && data != null && Object.keys(data).length > 0) {
|
|
77
79
|
console.warn(`[useFlowData] Path "${path}" not found in flow data.`)
|
|
78
80
|
}
|
|
79
81
|
return {}
|