@dfosco/storyboard 0.9.4 → 0.9.6
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/dist/storyboard-ui.css +1 -1
- package/dist/storyboard-ui.js.map +1 -1
- package/dist/tailwind.css +1 -1
- package/package.json +1 -1
- package/scaffold/AGENTS.md +0 -79
- package/scaffold/src/_prototype.jsx +9 -1
- package/src/core/cli/workleaf.js +3 -3
- package/src/core/mountStoryboardCore.js +1 -0
- package/src/core/stores/canvasInteractionStore.js +29 -9
- package/src/core/stores/canvasInteractionStore.test.js +37 -11
- package/src/core/stores/configSchema.js +1 -0
- package/src/core/stores/configSchema.test.js +1 -0
- package/src/core/styles/tailwind.css +2 -0
- package/src/core/workleaf/create.js +62 -12
- package/src/core/workleaf/nameGenerator.js +133 -0
- package/src/core/workleaf/nameGenerator.test.js +99 -0
- package/src/internals/BaseUiForm/BaseUiForm.module.css +2 -2
- package/src/internals/canvas/CanvasPage.bridge.test.jsx +1 -0
- package/src/internals/canvas/CanvasPage.dragdrop.test.jsx +1 -0
- package/src/internals/canvas/CanvasPage.jsx +54 -12
- package/src/internals/canvas/CanvasPage.multiselect.test.jsx +1 -0
- package/src/internals/canvas/widgets/StorySetWidget.jsx +8 -3
- package/src/internals/canvas/widgets/expandUtils.js +6 -3
- package/src/internals/canvas/widgets/widgetConfig.js +33 -1
- package/src/internals/canvas/widgets/widgetConfig.merged.test.js +27 -0
- package/src/internals/story/ComponentSetPage.jsx +21 -9
|
@@ -518,11 +518,14 @@ export function buildSecondaryIframeUrl(widget) {
|
|
|
518
518
|
params.set('_sb_component_set', '')
|
|
519
519
|
const layout = widget.props?.layout
|
|
520
520
|
if (layout) params.set('layout', layout)
|
|
521
|
+
// `selected` lives in the URL hash, not the query string — see
|
|
522
|
+
// StorySetWidget.resolveStorySetUrl. Keeps variant-change navigation
|
|
523
|
+
// as a same-document fragment update (no iframe reload).
|
|
521
524
|
const selected = widget.props?.selected
|
|
522
|
-
|
|
525
|
+
const hash = selected ? `#selected=${encodeURIComponent(selected)}` : ''
|
|
523
526
|
const route = storyData._route || `/components/${storyId}`
|
|
524
|
-
if (import.meta.env.DEV) return `${baseClean}/stories.html${route}?${params}`
|
|
525
|
-
return `${baseClean}${route}?${params}`
|
|
527
|
+
if (import.meta.env.DEV) return `${baseClean}/stories.html${route}?${params}${hash}`
|
|
528
|
+
return `${baseClean}${route}?${params}${hash}`
|
|
526
529
|
}
|
|
527
530
|
|
|
528
531
|
return null
|
|
@@ -216,9 +216,17 @@ export const widgetTypes = new Proxy({}, {
|
|
|
216
216
|
function readInteraction(def) {
|
|
217
217
|
if (!def) return null
|
|
218
218
|
const i = def.interaction || {}
|
|
219
|
+
// Preserve shape of `movable` so consumers can opt into prod via the
|
|
220
|
+
// structured form `{ enabled, prod }` (mirroring `resize`). Boolean stays
|
|
221
|
+
// boolean for back-compat — readers consult `isMovable()` for the
|
|
222
|
+
// env-aware verdict.
|
|
223
|
+
let movable
|
|
224
|
+
if (i.movable === false) movable = false
|
|
225
|
+
else if (typeof i.movable === 'object' && i.movable !== null) movable = i.movable
|
|
226
|
+
else movable = true
|
|
219
227
|
return {
|
|
220
228
|
selectable: i.selectable !== false, // default true
|
|
221
|
-
movable
|
|
229
|
+
movable, // default true
|
|
222
230
|
resize: i.resize ?? def.resize ?? null,
|
|
223
231
|
expandable: i.expandable ?? def.expandable ?? false,
|
|
224
232
|
splitScreen: i.splitScreen ?? def.splitScreen ?? false,
|
|
@@ -323,6 +331,30 @@ export function isResizable(type) {
|
|
|
323
331
|
return true
|
|
324
332
|
}
|
|
325
333
|
|
|
334
|
+
/**
|
|
335
|
+
* Check if a widget type supports drag-to-move in the current environment.
|
|
336
|
+
*
|
|
337
|
+
* `interaction.movable` accepts either:
|
|
338
|
+
* - boolean (default true) — movable in dev only; locked in production
|
|
339
|
+
* - `{ enabled: boolean, prod?: boolean }` — opt into production drag via
|
|
340
|
+
* `prod: true` (mirrors the `resize` shape)
|
|
341
|
+
*
|
|
342
|
+
* @param {string} type — widget type string
|
|
343
|
+
* @returns {boolean}
|
|
344
|
+
*/
|
|
345
|
+
export function isMovable(type) {
|
|
346
|
+
const movable = getInteractionOptions(type).movable
|
|
347
|
+
if (movable === false) return false
|
|
348
|
+
if (typeof movable === 'object' && movable !== null) {
|
|
349
|
+
if (movable.enabled === false) return false
|
|
350
|
+
if (import.meta.env?.PROD && !movable.prod) return false
|
|
351
|
+
return true
|
|
352
|
+
}
|
|
353
|
+
// Boolean true (or undefined): historical "movable in dev only" semantic.
|
|
354
|
+
if (import.meta.env?.PROD) return false
|
|
355
|
+
return true
|
|
356
|
+
}
|
|
357
|
+
|
|
326
358
|
/**
|
|
327
359
|
* Get the display metadata (label, icon) for a widget type.
|
|
328
360
|
* @param {string} type — widget type string
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
getWidgetMeta,
|
|
8
8
|
isResizable,
|
|
9
9
|
isExpandable,
|
|
10
|
+
isMovable,
|
|
10
11
|
isSplitScreenCapable,
|
|
11
12
|
getInteractGate,
|
|
12
13
|
getMenuWidgetTypes,
|
|
@@ -53,6 +54,32 @@ describe('widgetConfig — merged consumer + built-in registry', () => {
|
|
|
53
54
|
expect(getInteractionOptions('decorative').movable).toBe(true)
|
|
54
55
|
})
|
|
55
56
|
|
|
57
|
+
it('isMovable() returns dev-only for the boolean shape (back-compat)', () => {
|
|
58
|
+
// Default sticky-note has no interaction.movable → boolean true semantic.
|
|
59
|
+
// Vitest runs in dev mode by default (import.meta.env.PROD = false),
|
|
60
|
+
// so isMovable returns true.
|
|
61
|
+
expect(isMovable('sticky-note')).toBe(true)
|
|
62
|
+
registerWidget('locked', { label: 'Locked', interaction: { movable: false } })
|
|
63
|
+
expect(isMovable('locked')).toBe(false)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('isMovable() honors the { enabled, prod } object shape', () => {
|
|
67
|
+
registerWidget('alwaysDraggable', {
|
|
68
|
+
label: 'Always',
|
|
69
|
+
interaction: { movable: { enabled: true, prod: true } },
|
|
70
|
+
})
|
|
71
|
+
// enabled: true → movable in dev (PROD branch is irrelevant under vitest)
|
|
72
|
+
expect(isMovable('alwaysDraggable')).toBe(true)
|
|
73
|
+
// Object form is preserved on getInteractionOptions for downstream readers
|
|
74
|
+
expect(getInteractionOptions('alwaysDraggable').movable).toEqual({ enabled: true, prod: true })
|
|
75
|
+
|
|
76
|
+
registerWidget('explicitlyOff', {
|
|
77
|
+
label: 'Off',
|
|
78
|
+
interaction: { movable: { enabled: false } },
|
|
79
|
+
})
|
|
80
|
+
expect(isMovable('explicitlyOff')).toBe(false)
|
|
81
|
+
})
|
|
82
|
+
|
|
56
83
|
it('interaction.resize/expandable/splitScreen flow through to legacy getters', () => {
|
|
57
84
|
registerWidget('big', {
|
|
58
85
|
label: 'Big',
|
|
@@ -21,6 +21,18 @@ import styles from './ComponentSetPage.module.css'
|
|
|
21
21
|
|
|
22
22
|
const StoryWrapper = getStoryWrapper()
|
|
23
23
|
|
|
24
|
+
function parseHashSelected(hash) {
|
|
25
|
+
if (!hash) return ''
|
|
26
|
+
const trimmed = hash.startsWith('#') ? hash.slice(1) : hash
|
|
27
|
+
if (!trimmed) return ''
|
|
28
|
+
try {
|
|
29
|
+
const params = new URLSearchParams(trimmed)
|
|
30
|
+
return params.get('selected') || ''
|
|
31
|
+
} catch {
|
|
32
|
+
return ''
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
24
36
|
export default function ComponentSetPage({ name }) {
|
|
25
37
|
const location = useLocation()
|
|
26
38
|
const navigate = useNavigate()
|
|
@@ -28,7 +40,7 @@ export default function ComponentSetPage({ name }) {
|
|
|
28
40
|
|
|
29
41
|
const layout = searchParams.get('layout') || 'auto'
|
|
30
42
|
const density = searchParams.get('density') || 'comfy'
|
|
31
|
-
const selected =
|
|
43
|
+
const selected = parseHashSelected(location.hash)
|
|
32
44
|
const isEmbed = searchParams.has('_sb_embed')
|
|
33
45
|
|
|
34
46
|
// Same reload-guard policy as StoryPage / prototype embeds.
|
|
@@ -157,20 +169,20 @@ export default function ComponentSetPage({ name }) {
|
|
|
157
169
|
}, [exports, layout, density, isEmbed])
|
|
158
170
|
|
|
159
171
|
const handleSelect = useCallback((exportName) => {
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
navigate(`${location.pathname}
|
|
172
|
+
const isToggleOff = exportName === selected
|
|
173
|
+
const newSelected = isToggleOff ? '' : exportName
|
|
174
|
+
const hash = newSelected ? `#selected=${encodeURIComponent(newSelected)}` : ''
|
|
175
|
+
// Preserve current search params; only flip the fragment. The parent
|
|
176
|
+
// widget keeps `selected` in the URL hash specifically so this update
|
|
177
|
+
// round-trips as a same-document fragment change — no iframe reload.
|
|
178
|
+
navigate(`${location.pathname}${location.search}${hash}`, { replace: true })
|
|
167
179
|
|
|
168
180
|
// Notify parent widget
|
|
169
181
|
if (window.parent !== window) {
|
|
170
182
|
window.parent.postMessage({
|
|
171
183
|
type: 'storyboard:component-set:select',
|
|
172
184
|
storyId: name,
|
|
173
|
-
exportName:
|
|
185
|
+
exportName: newSelected || null,
|
|
174
186
|
}, '*')
|
|
175
187
|
}
|
|
176
188
|
}, [location, navigate, name, selected])
|