@dfosco/storyboard-react 3.11.0-beta.1 → 3.11.0-beta.2
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.2",
|
|
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.2",
|
|
7
|
+
"@dfosco/tiny-canvas": "3.11.0-beta.2",
|
|
8
8
|
"@neodrag/react": "^2.3.1",
|
|
9
9
|
"glob": "^11.0.0",
|
|
10
10
|
"jsonc-parser": "^3.3.1"
|
|
@@ -440,6 +440,36 @@ 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 widgets = localWidgets ?? []
|
|
450
|
+
const widget = widgets.find((w) => w.id === targetId)
|
|
451
|
+
if (!widget) return
|
|
452
|
+
|
|
453
|
+
const el = scrollRef.current
|
|
454
|
+
if (!el) return
|
|
455
|
+
|
|
456
|
+
const scale = zoomRef.current / 100
|
|
457
|
+
const fallback = WIDGET_FALLBACK_SIZES[widget.type] || { width: 200, height: 150 }
|
|
458
|
+
const wW = (widget.props?.width ?? fallback.width) * scale
|
|
459
|
+
const wH = (widget.props?.height ?? fallback.height) * scale
|
|
460
|
+
const wX = (widget.position?.x ?? 0) * scale
|
|
461
|
+
const wY = (widget.position?.y ?? 0) * scale
|
|
462
|
+
|
|
463
|
+
// Center the widget in the viewport
|
|
464
|
+
el.scrollLeft = wX + wW / 2 - el.clientWidth / 2
|
|
465
|
+
el.scrollTop = wY + wH / 2 - el.clientHeight / 2
|
|
466
|
+
|
|
467
|
+
// Clean the URL param without triggering navigation
|
|
468
|
+
const url = new URL(window.location.href)
|
|
469
|
+
url.searchParams.delete('widget')
|
|
470
|
+
window.history.replaceState({}, '', url.toString())
|
|
471
|
+
}, [loading, localWidgets])
|
|
472
|
+
|
|
443
473
|
// Persist viewport state (zoom + scroll) to localStorage on changes
|
|
444
474
|
useEffect(() => {
|
|
445
475
|
const el = scrollRef.current
|
|
@@ -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,102 @@ 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
|
+
/** Icon registry — maps icon name strings from config to React components. */
|
|
89
|
+
const ICON_REGISTRY = {
|
|
90
|
+
'trash': DeleteIcon,
|
|
74
91
|
'zoom-in': ZoomInIcon,
|
|
75
92
|
'zoom-out': ZoomOutIcon,
|
|
76
93
|
'edit': EditIcon,
|
|
77
94
|
'open-external': OpenExternalIcon,
|
|
78
|
-
'
|
|
95
|
+
'eye': EyeIcon,
|
|
96
|
+
'eye-closed': EyeClosedIcon,
|
|
79
97
|
'copy': CopyIcon,
|
|
98
|
+
'link': LinkIcon,
|
|
99
|
+
'more': MoreIcon,
|
|
80
100
|
}
|
|
81
101
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
102
|
+
/** Danger-styled actions in the overflow menu. */
|
|
103
|
+
const DANGER_ACTIONS = new Set(['delete'])
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Overflow menu — `...` button that opens a dropdown with menu-only actions.
|
|
107
|
+
*/
|
|
108
|
+
function WidgetOverflowMenu({ widgetId, menuFeatures, onAction }) {
|
|
109
|
+
const [open, setOpen] = useState(false)
|
|
110
|
+
const menuRef = useRef(null)
|
|
111
|
+
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
if (!open) return
|
|
114
|
+
function handlePointerDown(e) {
|
|
115
|
+
if (menuRef.current && !menuRef.current.contains(e.target)) {
|
|
116
|
+
setOpen(false)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
document.addEventListener('pointerdown', handlePointerDown)
|
|
120
|
+
return () => document.removeEventListener('pointerdown', handlePointerDown)
|
|
121
|
+
}, [open])
|
|
122
|
+
|
|
123
|
+
const handleItemClick = useCallback((action, e) => {
|
|
124
|
+
e.stopPropagation()
|
|
125
|
+
if (action === 'copy-link') {
|
|
126
|
+
const url = new URL(window.location.href)
|
|
127
|
+
url.searchParams.set('widget', widgetId)
|
|
128
|
+
navigator.clipboard.writeText(url.toString()).catch(() => {})
|
|
129
|
+
} else {
|
|
130
|
+
onAction?.(action)
|
|
131
|
+
}
|
|
132
|
+
setOpen(false)
|
|
133
|
+
}, [widgetId, onAction])
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<div ref={menuRef} className={styles.overflowWrapper}>
|
|
137
|
+
<Tooltip text="More actions" direction="n">
|
|
138
|
+
<button
|
|
139
|
+
className={styles.featureBtn}
|
|
140
|
+
onClick={(e) => { e.stopPropagation(); setOpen((v) => !v) }}
|
|
141
|
+
aria-label="More actions"
|
|
142
|
+
aria-expanded={open}
|
|
143
|
+
>
|
|
144
|
+
<MoreIcon />
|
|
145
|
+
</button>
|
|
146
|
+
</Tooltip>
|
|
147
|
+
{open && (
|
|
148
|
+
<div className={styles.overflowMenu}>
|
|
149
|
+
{menuFeatures.map((feature) => {
|
|
150
|
+
const Icon = ICON_REGISTRY[feature.icon]
|
|
151
|
+
const label = feature.label || feature.action
|
|
152
|
+
const isDanger = DANGER_ACTIONS.has(feature.action)
|
|
153
|
+
return (
|
|
154
|
+
<button
|
|
155
|
+
key={feature.id}
|
|
156
|
+
className={`${styles.overflowItem} ${isDanger ? styles.overflowItemDanger : ''}`}
|
|
157
|
+
onClick={(e) => handleItemClick(feature.action, e)}
|
|
158
|
+
>
|
|
159
|
+
{Icon && <Icon />}
|
|
160
|
+
<span>{label}</span>
|
|
161
|
+
</button>
|
|
162
|
+
)
|
|
163
|
+
})}
|
|
164
|
+
</div>
|
|
165
|
+
)}
|
|
166
|
+
</div>
|
|
167
|
+
)
|
|
90
168
|
}
|
|
91
169
|
|
|
92
170
|
/**
|
|
@@ -146,6 +224,7 @@ function ColorPickerFeature({ currentColor, options, onColorChange }) {
|
|
|
146
224
|
* non-standard actions (anything other than 'delete').
|
|
147
225
|
*/
|
|
148
226
|
export default function WidgetChrome({
|
|
227
|
+
widgetId,
|
|
149
228
|
features = [],
|
|
150
229
|
selected = false,
|
|
151
230
|
widgetProps,
|
|
@@ -227,6 +306,9 @@ export default function WidgetChrome({
|
|
|
227
306
|
<div className={`${styles.toolbarContent} ${showToolbar ? styles.toolbarContentVisible : ''}`}>
|
|
228
307
|
<div className={styles.featureButtons}>
|
|
229
308
|
{features.map((feature) => {
|
|
309
|
+
// Menu features are rendered in WidgetOverflowMenu
|
|
310
|
+
if (feature.menu) return null
|
|
311
|
+
|
|
230
312
|
if (feature.type === 'color-picker') {
|
|
231
313
|
return (
|
|
232
314
|
<ColorPickerFeature
|
|
@@ -239,13 +321,13 @@ export default function WidgetChrome({
|
|
|
239
321
|
}
|
|
240
322
|
|
|
241
323
|
if (feature.type === 'action') {
|
|
242
|
-
let Icon =
|
|
243
|
-
let label =
|
|
324
|
+
let Icon = ICON_REGISTRY[feature.icon]
|
|
325
|
+
let label = feature.label || feature.action
|
|
244
326
|
|
|
245
327
|
// Toggle-private: swap icon/label based on current state
|
|
246
328
|
if (feature.action === 'toggle-private') {
|
|
247
329
|
if (widgetProps?.private) {
|
|
248
|
-
Icon =
|
|
330
|
+
Icon = ICON_REGISTRY['eye-closed']
|
|
249
331
|
label = 'Private image — only visible locally'
|
|
250
332
|
} else {
|
|
251
333
|
label = 'Published image — deployed with canvas'
|
|
@@ -267,6 +349,11 @@ export default function WidgetChrome({
|
|
|
267
349
|
|
|
268
350
|
return null
|
|
269
351
|
})}
|
|
352
|
+
<WidgetOverflowMenu
|
|
353
|
+
widgetId={widgetId}
|
|
354
|
+
menuFeatures={features.filter((f) => f.menu)}
|
|
355
|
+
onAction={onAction}
|
|
356
|
+
/>
|
|
270
357
|
</div>
|
|
271
358
|
|
|
272
359
|
<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,36 @@
|
|
|
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.
|
|
30
|
+
*/
|
|
31
|
+
function resolveFeature(feature) {
|
|
32
|
+
const resolved = {}
|
|
33
|
+
for (const [key, val] of Object.entries(feature)) {
|
|
34
|
+
resolved[key] = resolveVar(val)
|
|
35
|
+
}
|
|
36
|
+
return resolved
|
|
37
|
+
}
|
|
38
|
+
|
|
12
39
|
/**
|
|
13
40
|
* Convert a config prop definition to the schema shape used by widgetProps.js.
|
|
14
41
|
* Config uses `"default"`, schema uses `"defaultValue"`.
|
|
@@ -42,16 +69,30 @@ function buildSchemas() {
|
|
|
42
69
|
return result
|
|
43
70
|
}
|
|
44
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Build resolved widget type entries with variables expanded in features.
|
|
74
|
+
*/
|
|
75
|
+
function buildWidgetTypes() {
|
|
76
|
+
const result = {}
|
|
77
|
+
for (const [type, def] of Object.entries(widgetsConfig.widgets)) {
|
|
78
|
+
result[type] = {
|
|
79
|
+
...def,
|
|
80
|
+
features: (def.features || []).map(resolveFeature),
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return result
|
|
84
|
+
}
|
|
85
|
+
|
|
45
86
|
/** All widget schemas, keyed by type string. */
|
|
46
87
|
export const schemas = buildSchemas()
|
|
47
88
|
|
|
48
|
-
/** Full widget config entries, keyed by type string. */
|
|
49
|
-
export const widgetTypes =
|
|
89
|
+
/** Full widget config entries (with resolved variables), keyed by type string. */
|
|
90
|
+
export const widgetTypes = buildWidgetTypes()
|
|
50
91
|
|
|
51
92
|
/**
|
|
52
93
|
* Get the feature list for a widget type.
|
|
53
94
|
* @param {string} type — widget type string
|
|
54
|
-
* @returns {Array} features array from config, or empty array
|
|
95
|
+
* @returns {Array} features array from config (variables resolved), or empty array
|
|
55
96
|
*/
|
|
56
97
|
export function getFeatures(type) {
|
|
57
98
|
return widgetTypes[type]?.features ?? []
|