@dfosco/storyboard-react 3.11.0-beta.1 → 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"
|
|
@@ -440,6 +440,56 @@ export default function CanvasPage({ name }) {
|
|
|
440
440
|
}
|
|
441
441
|
}, [name, loading])
|
|
442
442
|
|
|
443
|
+
// Center on a specific widget if `?widget=<id>` is in the URL
|
|
444
|
+
useEffect(() => {
|
|
445
|
+
const params = new URLSearchParams(window.location.search)
|
|
446
|
+
const targetId = params.get('widget')
|
|
447
|
+
if (!targetId || loading) return
|
|
448
|
+
|
|
449
|
+
const el = scrollRef.current
|
|
450
|
+
if (!el) return
|
|
451
|
+
|
|
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
|
+
}
|
|
464
|
+
|
|
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
|
|
486
|
+
|
|
487
|
+
// Clean the URL param without triggering navigation
|
|
488
|
+
const url = new URL(window.location.href)
|
|
489
|
+
url.searchParams.delete('widget')
|
|
490
|
+
window.history.replaceState({}, '', url.toString())
|
|
491
|
+
}, [loading, localWidgets, localSources, jsxExports])
|
|
492
|
+
|
|
443
493
|
// Persist viewport state (zoom + scroll) to localStorage on changes
|
|
444
494
|
useEffect(() => {
|
|
445
495
|
const el = scrollRef.current
|
|
@@ -1035,6 +1085,7 @@ export default function CanvasPage({ name }) {
|
|
|
1035
1085
|
}}
|
|
1036
1086
|
>
|
|
1037
1087
|
<WidgetChrome
|
|
1088
|
+
widgetId={`jsx-${exportName}`}
|
|
1038
1089
|
features={componentFeatures}
|
|
1039
1090
|
selected={selectedWidgetId === `jsx-${exportName}`}
|
|
1040
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])
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useCallback, useRef } from 'react'
|
|
1
|
+
import { useState, useCallback, useRef, useEffect } from 'react'
|
|
2
2
|
import { Tooltip } from '@primer/react'
|
|
3
3
|
import { EyeIcon as OcticonEye, EyeClosedIcon as OcticonEyeClosed } from '@primer/octicons-react'
|
|
4
4
|
import styles from './WidgetChrome.module.css'
|
|
@@ -69,24 +69,179 @@ function CopyIcon() {
|
|
|
69
69
|
)
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
function MoreIcon() {
|
|
73
|
+
return (
|
|
74
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
|
75
|
+
<path d="M8 9a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM1.5 9a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Zm13 0a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" />
|
|
76
|
+
</svg>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function LinkIcon() {
|
|
81
|
+
return (
|
|
82
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
|
83
|
+
<path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z" />
|
|
84
|
+
</svg>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
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
|
+
|
|
105
|
+
/** Icon registry — maps icon name strings from config to React components. */
|
|
106
|
+
const ICON_REGISTRY = {
|
|
107
|
+
'trash': DeleteIcon,
|
|
74
108
|
'zoom-in': ZoomInIcon,
|
|
75
109
|
'zoom-out': ZoomOutIcon,
|
|
76
110
|
'edit': EditIcon,
|
|
77
111
|
'open-external': OpenExternalIcon,
|
|
78
|
-
'
|
|
112
|
+
'eye': EyeIcon,
|
|
113
|
+
'eye-closed': EyeClosedIcon,
|
|
79
114
|
'copy': CopyIcon,
|
|
115
|
+
'link': LinkIcon,
|
|
116
|
+
'more': MoreIcon,
|
|
117
|
+
'chevron-down': ChevronDownIcon,
|
|
118
|
+
'download': DownloadIcon,
|
|
80
119
|
}
|
|
81
120
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
121
|
+
/** Danger-styled actions in the overflow menu. */
|
|
122
|
+
const DANGER_ACTIONS = new Set(['delete'])
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Overflow menu — `...` button that opens a dropdown with menu-only actions.
|
|
126
|
+
*/
|
|
127
|
+
function WidgetOverflowMenu({ widgetId, menuFeatures, onAction }) {
|
|
128
|
+
const [open, setOpen] = useState(false)
|
|
129
|
+
const menuRef = useRef(null)
|
|
130
|
+
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
if (!open) return
|
|
133
|
+
function handlePointerDown(e) {
|
|
134
|
+
if (menuRef.current && !menuRef.current.contains(e.target)) {
|
|
135
|
+
setOpen(false)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
document.addEventListener('pointerdown', handlePointerDown)
|
|
139
|
+
return () => document.removeEventListener('pointerdown', handlePointerDown)
|
|
140
|
+
}, [open])
|
|
141
|
+
|
|
142
|
+
const handleItemClick = useCallback((action, e) => {
|
|
143
|
+
e.stopPropagation()
|
|
144
|
+
if (action === 'copy-link') {
|
|
145
|
+
const url = new URL(window.location.href)
|
|
146
|
+
url.searchParams.set('widget', widgetId)
|
|
147
|
+
navigator.clipboard.writeText(url.toString()).catch(() => {})
|
|
148
|
+
} else {
|
|
149
|
+
onAction?.(action)
|
|
150
|
+
}
|
|
151
|
+
setOpen(false)
|
|
152
|
+
}, [widgetId, onAction])
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<div ref={menuRef} className={styles.overflowWrapper}>
|
|
156
|
+
<Tooltip text="More actions" direction="n">
|
|
157
|
+
<button
|
|
158
|
+
className={styles.featureBtn}
|
|
159
|
+
onClick={(e) => { e.stopPropagation(); setOpen((v) => !v) }}
|
|
160
|
+
aria-label="More actions"
|
|
161
|
+
aria-expanded={open}
|
|
162
|
+
>
|
|
163
|
+
<MoreIcon />
|
|
164
|
+
</button>
|
|
165
|
+
</Tooltip>
|
|
166
|
+
{open && (
|
|
167
|
+
<div className={styles.overflowMenu}>
|
|
168
|
+
{menuFeatures.map((feature) => {
|
|
169
|
+
const Icon = ICON_REGISTRY[feature.icon]
|
|
170
|
+
const label = feature.label || feature.action
|
|
171
|
+
const isDanger = DANGER_ACTIONS.has(feature.action)
|
|
172
|
+
return (
|
|
173
|
+
<button
|
|
174
|
+
key={feature.id}
|
|
175
|
+
className={`${styles.overflowItem} ${isDanger ? styles.overflowItemDanger : ''}`}
|
|
176
|
+
onClick={(e) => handleItemClick(feature.action, e)}
|
|
177
|
+
>
|
|
178
|
+
{Icon && <Icon />}
|
|
179
|
+
<span>{label}</span>
|
|
180
|
+
</button>
|
|
181
|
+
)
|
|
182
|
+
})}
|
|
183
|
+
</div>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
)
|
|
187
|
+
}
|
|
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
|
+
)
|
|
90
245
|
}
|
|
91
246
|
|
|
92
247
|
/**
|
|
@@ -146,6 +301,7 @@ function ColorPickerFeature({ currentColor, options, onColorChange }) {
|
|
|
146
301
|
* non-standard actions (anything other than 'delete').
|
|
147
302
|
*/
|
|
148
303
|
export default function WidgetChrome({
|
|
304
|
+
widgetId,
|
|
149
305
|
features = [],
|
|
150
306
|
selected = false,
|
|
151
307
|
widgetProps,
|
|
@@ -227,6 +383,9 @@ export default function WidgetChrome({
|
|
|
227
383
|
<div className={`${styles.toolbarContent} ${showToolbar ? styles.toolbarContentVisible : ''}`}>
|
|
228
384
|
<div className={styles.featureButtons}>
|
|
229
385
|
{features.map((feature) => {
|
|
386
|
+
// Menu features are rendered in WidgetOverflowMenu
|
|
387
|
+
if (feature.menu) return null
|
|
388
|
+
|
|
230
389
|
if (feature.type === 'color-picker') {
|
|
231
390
|
return (
|
|
232
391
|
<ColorPickerFeature
|
|
@@ -239,13 +398,13 @@ export default function WidgetChrome({
|
|
|
239
398
|
}
|
|
240
399
|
|
|
241
400
|
if (feature.type === 'action') {
|
|
242
|
-
let Icon =
|
|
243
|
-
let label =
|
|
401
|
+
let Icon = ICON_REGISTRY[feature.icon]
|
|
402
|
+
let label = feature.label || feature.action
|
|
244
403
|
|
|
245
404
|
// Toggle-private: swap icon/label based on current state
|
|
246
405
|
if (feature.action === 'toggle-private') {
|
|
247
406
|
if (widgetProps?.private) {
|
|
248
|
-
Icon =
|
|
407
|
+
Icon = ICON_REGISTRY['eye-closed']
|
|
249
408
|
label = 'Private image — only visible locally'
|
|
250
409
|
} else {
|
|
251
410
|
label = 'Published image — deployed with canvas'
|
|
@@ -265,8 +424,29 @@ export default function WidgetChrome({
|
|
|
265
424
|
)
|
|
266
425
|
}
|
|
267
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
|
+
|
|
268
443
|
return null
|
|
269
444
|
})}
|
|
445
|
+
<WidgetOverflowMenu
|
|
446
|
+
widgetId={widgetId}
|
|
447
|
+
menuFeatures={features.filter((f) => f.menu)}
|
|
448
|
+
onAction={onAction}
|
|
449
|
+
/>
|
|
270
450
|
</div>
|
|
271
451
|
|
|
272
452
|
<Tooltip text="Select" direction="n">
|
|
@@ -217,3 +217,67 @@
|
|
|
217
217
|
border-color: currentColor;
|
|
218
218
|
box-shadow: 0 0 0 1px currentColor;
|
|
219
219
|
}
|
|
220
|
+
|
|
221
|
+
/* Overflow menu */
|
|
222
|
+
.overflowWrapper {
|
|
223
|
+
position: relative;
|
|
224
|
+
display: flex;
|
|
225
|
+
align-items: center;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.overflowMenu {
|
|
229
|
+
position: absolute;
|
|
230
|
+
top: calc(100% + 4px);
|
|
231
|
+
right: 0;
|
|
232
|
+
min-width: 180px;
|
|
233
|
+
padding: 4px;
|
|
234
|
+
background: var(--bgColor-default, #ffffff);
|
|
235
|
+
border-radius: 10px;
|
|
236
|
+
box-shadow:
|
|
237
|
+
0 0 0 1px rgba(0, 0, 0, 0.08),
|
|
238
|
+
0 4px 12px rgba(0, 0, 0, 0.12);
|
|
239
|
+
z-index: 10;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
:global([data-sb-canvas-theme^='dark']) .overflowMenu {
|
|
243
|
+
background: var(--bgColor-muted, #161b22);
|
|
244
|
+
box-shadow:
|
|
245
|
+
0 0 0 1px rgba(255, 255, 255, 0.08),
|
|
246
|
+
0 4px 12px rgba(0, 0, 0, 0.45);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.overflowItem {
|
|
250
|
+
all: unset;
|
|
251
|
+
cursor: pointer;
|
|
252
|
+
display: flex;
|
|
253
|
+
align-items: center;
|
|
254
|
+
gap: 8px;
|
|
255
|
+
width: 100%;
|
|
256
|
+
padding: 6px 10px;
|
|
257
|
+
font-size: 12px;
|
|
258
|
+
color: var(--fgColor-default, #1f2328);
|
|
259
|
+
border-radius: 6px;
|
|
260
|
+
box-sizing: border-box;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
:global([data-sb-canvas-theme^='dark']) .overflowItem {
|
|
264
|
+
color: var(--fgColor-default, #e6edf3);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.overflowItem:hover {
|
|
268
|
+
background: var(--bgColor-neutral-muted, #eaeef2);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
:global([data-sb-canvas-theme^='dark']) .overflowItem:hover {
|
|
272
|
+
background: var(--bgColor-neutral-muted, #272c33);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.overflowItemDanger:hover {
|
|
276
|
+
background: var(--bgColor-danger-muted, #ffebe9);
|
|
277
|
+
color: var(--fgColor-danger, #d1242f);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
:global([data-sb-canvas-theme^='dark']) .overflowItemDanger:hover {
|
|
281
|
+
background: var(--bgColor-danger-muted, #490202);
|
|
282
|
+
color: var(--fgColor-danger, #f85149);
|
|
283
|
+
}
|
|
@@ -6,9 +6,44 @@
|
|
|
6
6
|
*
|
|
7
7
|
* The config is the single source of truth for widget definitions —
|
|
8
8
|
* prop schemas, feature lists, labels, and icons all come from here.
|
|
9
|
+
*
|
|
10
|
+
* Supports `$variable` references in string values, resolved from
|
|
11
|
+
* the top-level `variables` object in widgets.config.json.
|
|
9
12
|
*/
|
|
10
13
|
import widgetsConfig from '@dfosco/storyboard-core/widgets.config.json'
|
|
11
14
|
|
|
15
|
+
/** Variables defined in config — used to resolve `$key` references. */
|
|
16
|
+
const variables = widgetsConfig.variables || {}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Resolve `$variable` references in a string value.
|
|
20
|
+
* Returns the original value if it's not a string or doesn't start with `$`.
|
|
21
|
+
*/
|
|
22
|
+
function resolveVar(value) {
|
|
23
|
+
if (typeof value !== 'string' || !value.startsWith('$')) return value
|
|
24
|
+
const key = value.slice(1)
|
|
25
|
+
return variables[key] ?? value
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolve all string values in a feature object, including nested items.
|
|
30
|
+
*/
|
|
31
|
+
function resolveFeature(feature) {
|
|
32
|
+
const resolved = {}
|
|
33
|
+
for (const [key, val] of Object.entries(feature)) {
|
|
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
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return resolved
|
|
45
|
+
}
|
|
46
|
+
|
|
12
47
|
/**
|
|
13
48
|
* Convert a config prop definition to the schema shape used by widgetProps.js.
|
|
14
49
|
* Config uses `"default"`, schema uses `"defaultValue"`.
|
|
@@ -42,16 +77,30 @@ function buildSchemas() {
|
|
|
42
77
|
return result
|
|
43
78
|
}
|
|
44
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Build resolved widget type entries with variables expanded in features.
|
|
82
|
+
*/
|
|
83
|
+
function buildWidgetTypes() {
|
|
84
|
+
const result = {}
|
|
85
|
+
for (const [type, def] of Object.entries(widgetsConfig.widgets)) {
|
|
86
|
+
result[type] = {
|
|
87
|
+
...def,
|
|
88
|
+
features: (def.features || []).map(resolveFeature),
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return result
|
|
92
|
+
}
|
|
93
|
+
|
|
45
94
|
/** All widget schemas, keyed by type string. */
|
|
46
95
|
export const schemas = buildSchemas()
|
|
47
96
|
|
|
48
|
-
/** Full widget config entries, keyed by type string. */
|
|
49
|
-
export const widgetTypes =
|
|
97
|
+
/** Full widget config entries (with resolved variables), keyed by type string. */
|
|
98
|
+
export const widgetTypes = buildWidgetTypes()
|
|
50
99
|
|
|
51
100
|
/**
|
|
52
101
|
* Get the feature list for a widget type.
|
|
53
102
|
* @param {string} type — widget type string
|
|
54
|
-
* @returns {Array} features array from config, or empty array
|
|
103
|
+
* @returns {Array} features array from config (variables resolved), or empty array
|
|
55
104
|
*/
|
|
56
105
|
export function getFeatures(type) {
|
|
57
106
|
return widgetTypes[type]?.features ?? []
|