@dfosco/storyboard-react 3.11.0-beta.2 → 3.11.0-beta.3
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
|
+
"version": "3.11.0-beta.3",
|
|
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.3",
|
|
7
|
+
"@dfosco/tiny-canvas": "3.11.0-beta.3",
|
|
8
8
|
"@neodrag/react": "^2.3.1",
|
|
9
9
|
"glob": "^11.0.0",
|
|
10
10
|
"jsonc-parser": "^3.3.1"
|
|
@@ -446,29 +446,49 @@ export default function CanvasPage({ name }) {
|
|
|
446
446
|
const targetId = params.get('widget')
|
|
447
447
|
if (!targetId || loading) return
|
|
448
448
|
|
|
449
|
-
const widgets = localWidgets ?? []
|
|
450
|
-
const widget = widgets.find((w) => w.id === targetId)
|
|
451
|
-
if (!widget) return
|
|
452
|
-
|
|
453
449
|
const el = scrollRef.current
|
|
454
450
|
if (!el) return
|
|
455
451
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
const
|
|
460
|
-
const
|
|
461
|
-
|
|
452
|
+
let x, y, w, h
|
|
453
|
+
|
|
454
|
+
// Check JSON widgets first
|
|
455
|
+
const widgets = localWidgets ?? []
|
|
456
|
+
const widget = widgets.find((wgt) => wgt.id === targetId)
|
|
457
|
+
if (widget) {
|
|
458
|
+
const fallback = WIDGET_FALLBACK_SIZES[widget.type] || { width: 200, height: 150 }
|
|
459
|
+
x = widget.position?.x ?? 0
|
|
460
|
+
y = widget.position?.y ?? 0
|
|
461
|
+
w = widget.props?.width ?? fallback.width
|
|
462
|
+
h = widget.props?.height ?? fallback.height
|
|
463
|
+
}
|
|
462
464
|
|
|
463
|
-
//
|
|
464
|
-
|
|
465
|
-
|
|
465
|
+
// Check JSX sources (jsx-ExportName)
|
|
466
|
+
if (!widget && targetId.startsWith('jsx-')) {
|
|
467
|
+
const exportName = targetId.slice(4)
|
|
468
|
+
const sourceMap = Object.fromEntries(
|
|
469
|
+
(localSources || []).filter((s) => s?.export).map((s) => [s.export, s])
|
|
470
|
+
)
|
|
471
|
+
const sourceData = sourceMap[exportName]
|
|
472
|
+
if (sourceData || (jsxExports && exportName in jsxExports)) {
|
|
473
|
+
const fallback = WIDGET_FALLBACK_SIZES['component']
|
|
474
|
+
x = sourceData?.position?.x ?? 0
|
|
475
|
+
y = sourceData?.position?.y ?? 0
|
|
476
|
+
w = sourceData?.width ?? fallback.width
|
|
477
|
+
h = sourceData?.height ?? fallback.height
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (x == null) return
|
|
482
|
+
|
|
483
|
+
const scale = zoomRef.current / 100
|
|
484
|
+
el.scrollLeft = (x + w / 2) * scale - el.clientWidth / 2
|
|
485
|
+
el.scrollTop = (y + h / 2) * scale - el.clientHeight / 2
|
|
466
486
|
|
|
467
487
|
// Clean the URL param without triggering navigation
|
|
468
488
|
const url = new URL(window.location.href)
|
|
469
489
|
url.searchParams.delete('widget')
|
|
470
490
|
window.history.replaceState({}, '', url.toString())
|
|
471
|
-
}, [loading, localWidgets])
|
|
491
|
+
}, [loading, localWidgets, localSources, jsxExports])
|
|
472
492
|
|
|
473
493
|
// Persist viewport state (zoom + scroll) to localStorage on changes
|
|
474
494
|
useEffect(() => {
|
|
@@ -1065,6 +1085,7 @@ export default function CanvasPage({ name }) {
|
|
|
1065
1085
|
}}
|
|
1066
1086
|
>
|
|
1067
1087
|
<WidgetChrome
|
|
1088
|
+
widgetId={`jsx-${exportName}`}
|
|
1068
1089
|
features={componentFeatures}
|
|
1069
1090
|
selected={selectedWidgetId === `jsx-${exportName}`}
|
|
1070
1091
|
onSelect={() => setSelectedWidgetId(`jsx-${exportName}`)}
|
|
@@ -51,6 +51,28 @@ const ImageWidget = forwardRef(function ImageWidget({ props, onUpdate }, ref) {
|
|
|
51
51
|
}).catch((err) => {
|
|
52
52
|
console.error('[canvas] Failed to toggle image privacy:', err)
|
|
53
53
|
})
|
|
54
|
+
} else if (actionId === 'download-image') {
|
|
55
|
+
if (!src) return
|
|
56
|
+
const url = getImageUrl(src)
|
|
57
|
+
const a = document.createElement('a')
|
|
58
|
+
a.href = url
|
|
59
|
+
a.download = src.replace(/^_/, '')
|
|
60
|
+
document.body.appendChild(a)
|
|
61
|
+
a.click()
|
|
62
|
+
document.body.removeChild(a)
|
|
63
|
+
} else if (actionId === 'copy-as-png') {
|
|
64
|
+
if (!src) return
|
|
65
|
+
const url = getImageUrl(src)
|
|
66
|
+
fetch(url)
|
|
67
|
+
.then((r) => r.blob())
|
|
68
|
+
.then((blob) => {
|
|
69
|
+
const pngBlob = blob.type === 'image/png' ? blob : blob
|
|
70
|
+
navigator.clipboard.write([new ClipboardItem({ 'image/png': pngBlob })]).catch(() => {})
|
|
71
|
+
})
|
|
72
|
+
.catch((err) => console.error('[canvas] Failed to copy image:', err))
|
|
73
|
+
} else if (actionId === 'copy-file-path') {
|
|
74
|
+
if (!src) return
|
|
75
|
+
navigator.clipboard.writeText(`src/canvas/images/${src}`).catch(() => {})
|
|
54
76
|
}
|
|
55
77
|
}
|
|
56
78
|
}), [src, onUpdate])
|
|
@@ -85,6 +85,23 @@ function LinkIcon() {
|
|
|
85
85
|
)
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
function ChevronDownIcon() {
|
|
89
|
+
return (
|
|
90
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
|
91
|
+
<path d="M12.78 5.22a.749.749 0 0 1 0 1.06l-4.25 4.25a.749.749 0 0 1-1.06 0L3.22 6.28a.749.749 0 1 1 1.06-1.06L8 8.939l3.72-3.719a.749.749 0 0 1 1.06 0Z" />
|
|
92
|
+
</svg>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function DownloadIcon() {
|
|
97
|
+
return (
|
|
98
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
|
99
|
+
<path d="M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 13.25 14Z" />
|
|
100
|
+
<path d="M7.25 7.689V2a.75.75 0 0 1 1.5 0v5.689l1.97-1.969a.749.749 0 1 1 1.06 1.06l-3.25 3.25a.749.749 0 0 1-1.06 0L4.22 6.78a.749.749 0 1 1 1.06-1.06Z" />
|
|
101
|
+
</svg>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
88
105
|
/** Icon registry — maps icon name strings from config to React components. */
|
|
89
106
|
const ICON_REGISTRY = {
|
|
90
107
|
'trash': DeleteIcon,
|
|
@@ -97,6 +114,8 @@ const ICON_REGISTRY = {
|
|
|
97
114
|
'copy': CopyIcon,
|
|
98
115
|
'link': LinkIcon,
|
|
99
116
|
'more': MoreIcon,
|
|
117
|
+
'chevron-down': ChevronDownIcon,
|
|
118
|
+
'download': DownloadIcon,
|
|
100
119
|
}
|
|
101
120
|
|
|
102
121
|
/** Danger-styled actions in the overflow menu. */
|
|
@@ -167,6 +186,64 @@ function WidgetOverflowMenu({ widgetId, menuFeatures, onAction }) {
|
|
|
167
186
|
)
|
|
168
187
|
}
|
|
169
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Dropdown feature — a chevron button that opens a menu of actions.
|
|
191
|
+
* Items and their icons/labels come from config.
|
|
192
|
+
*/
|
|
193
|
+
function DropdownFeature({ feature, onAction }) {
|
|
194
|
+
const [open, setOpen] = useState(false)
|
|
195
|
+
const menuRef = useRef(null)
|
|
196
|
+
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
if (!open) return
|
|
199
|
+
function handlePointerDown(e) {
|
|
200
|
+
if (menuRef.current && !menuRef.current.contains(e.target)) {
|
|
201
|
+
setOpen(false)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
document.addEventListener('pointerdown', handlePointerDown)
|
|
205
|
+
return () => document.removeEventListener('pointerdown', handlePointerDown)
|
|
206
|
+
}, [open])
|
|
207
|
+
|
|
208
|
+
const TriggerIcon = ICON_REGISTRY[feature.icon] || ChevronDownIcon
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<div ref={menuRef} className={styles.overflowWrapper}>
|
|
212
|
+
<Tooltip text={feature.label || 'Actions'} direction="n">
|
|
213
|
+
<button
|
|
214
|
+
className={styles.featureBtn}
|
|
215
|
+
onClick={(e) => { e.stopPropagation(); setOpen((v) => !v) }}
|
|
216
|
+
aria-label={feature.label || 'Actions'}
|
|
217
|
+
aria-expanded={open}
|
|
218
|
+
>
|
|
219
|
+
<TriggerIcon />
|
|
220
|
+
</button>
|
|
221
|
+
</Tooltip>
|
|
222
|
+
{open && (
|
|
223
|
+
<div className={styles.overflowMenu}>
|
|
224
|
+
{(feature.items || []).map((item) => {
|
|
225
|
+
const Icon = ICON_REGISTRY[item.icon]
|
|
226
|
+
return (
|
|
227
|
+
<button
|
|
228
|
+
key={item.action}
|
|
229
|
+
className={styles.overflowItem}
|
|
230
|
+
onClick={(e) => {
|
|
231
|
+
e.stopPropagation()
|
|
232
|
+
onAction?.(item.action)
|
|
233
|
+
setOpen(false)
|
|
234
|
+
}}
|
|
235
|
+
>
|
|
236
|
+
{Icon && <Icon />}
|
|
237
|
+
<span>{item.label || item.action}</span>
|
|
238
|
+
</button>
|
|
239
|
+
)
|
|
240
|
+
})}
|
|
241
|
+
</div>
|
|
242
|
+
)}
|
|
243
|
+
</div>
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
|
|
170
247
|
/**
|
|
171
248
|
* ColorPicker feature button — shows a dot that reveals color options on hover.
|
|
172
249
|
*/
|
|
@@ -347,6 +424,22 @@ export default function WidgetChrome({
|
|
|
347
424
|
)
|
|
348
425
|
}
|
|
349
426
|
|
|
427
|
+
if (feature.type === 'dropdown') {
|
|
428
|
+
return (
|
|
429
|
+
<DropdownFeature
|
|
430
|
+
key={feature.id}
|
|
431
|
+
feature={feature}
|
|
432
|
+
onAction={(actionId) => {
|
|
433
|
+
if (widgetRef?.current?.handleAction) {
|
|
434
|
+
widgetRef.current.handleAction(actionId)
|
|
435
|
+
} else {
|
|
436
|
+
onAction?.(actionId)
|
|
437
|
+
}
|
|
438
|
+
}}
|
|
439
|
+
/>
|
|
440
|
+
)
|
|
441
|
+
}
|
|
442
|
+
|
|
350
443
|
return null
|
|
351
444
|
})}
|
|
352
445
|
<WidgetOverflowMenu
|
|
@@ -26,12 +26,20 @@ function resolveVar(value) {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
|
-
* Resolve all string values in a feature object.
|
|
29
|
+
* Resolve all string values in a feature object, including nested items.
|
|
30
30
|
*/
|
|
31
31
|
function resolveFeature(feature) {
|
|
32
32
|
const resolved = {}
|
|
33
33
|
for (const [key, val] of Object.entries(feature)) {
|
|
34
|
-
|
|
34
|
+
if (key === 'items' && Array.isArray(val)) {
|
|
35
|
+
resolved[key] = val.map((item) => {
|
|
36
|
+
const r = {}
|
|
37
|
+
for (const [k, v] of Object.entries(item)) r[k] = resolveVar(v)
|
|
38
|
+
return r
|
|
39
|
+
})
|
|
40
|
+
} else {
|
|
41
|
+
resolved[key] = resolveVar(val)
|
|
42
|
+
}
|
|
35
43
|
}
|
|
36
44
|
return resolved
|
|
37
45
|
}
|