@dfosco/storyboard 0.9.3 → 0.9.5
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 +1 -10
- package/dist/storyboard-ui.js.map +1 -1
- package/dist/tailwind.css +1 -1
- package/package.json +1 -4
- package/scaffold/AGENTS.md +2 -79
- package/scaffold/skills/storyboard-core/SKILL.md +63 -171
- package/scaffold/skills/storyboard-widget/SKILL.md +172 -0
- package/scaffold/skills/tools/SKILL.md +12 -12
- package/src/core/canvas/collision.js +41 -19
- package/src/core/canvas/customWidgets.js +92 -0
- package/src/core/canvas/customWidgets.test.js +69 -0
- package/src/core/canvas/server.js +4 -3
- package/src/core/index.js +13 -0
- package/src/core/mountStoryboardCore.js +10 -0
- package/src/core/stores/configSchema.js +2 -1
- package/src/core/stores/widgetRegistry.js +184 -0
- package/src/core/stores/widgetRegistry.test.js +109 -0
- package/src/core/ui/CoreUIBar.jsx +1 -1
- package/src/core/ui-entry.js +0 -6
- package/src/core/vite/server-plugin.js +12 -0
- package/src/internals/canvas/CanvasPage.bridge.test.jsx +2 -0
- package/src/internals/canvas/CanvasPage.dragdrop.test.jsx +2 -0
- package/src/internals/canvas/CanvasPage.jsx +19 -3
- package/src/internals/canvas/CanvasPage.multiselect.test.jsx +2 -0
- package/src/internals/canvas/widgets/WidgetChrome.jsx +50 -5
- package/src/internals/canvas/widgets/WidgetChrome.test.jsx +65 -0
- package/src/internals/canvas/widgets/index.js +18 -4
- package/src/internals/canvas/widgets/widgetConfig.js +202 -45
- package/src/internals/canvas/widgets/widgetConfig.merged.test.js +144 -0
- package/src/internals/context.jsx +1 -17
- package/src/core/ui/design-modes.ts +0 -7
- package/src/core/ui/viewfinder.ts +0 -7
- package/src/core/workshop/ui/mount.ts +0 -6
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
registerWidget,
|
|
4
|
+
_resetWidgetRegistry,
|
|
5
|
+
} from '../../../core/stores/widgetRegistry.js'
|
|
6
|
+
import {
|
|
7
|
+
getWidgetMeta,
|
|
8
|
+
isResizable,
|
|
9
|
+
isExpandable,
|
|
10
|
+
isSplitScreenCapable,
|
|
11
|
+
getInteractGate,
|
|
12
|
+
getMenuWidgetTypes,
|
|
13
|
+
getChromeOptions,
|
|
14
|
+
getInteractionOptions,
|
|
15
|
+
getFeatures,
|
|
16
|
+
getConnectorConfig,
|
|
17
|
+
schemas,
|
|
18
|
+
} from './widgetConfig.js'
|
|
19
|
+
|
|
20
|
+
describe('widgetConfig — merged consumer + built-in registry', () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
_resetWidgetRegistry()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('falls back to built-in widgets when nothing registered', () => {
|
|
26
|
+
expect(getWidgetMeta('sticky-note')?.label).toBeTruthy()
|
|
27
|
+
expect(getChromeOptions('sticky-note').enabled).toBe(true)
|
|
28
|
+
expect(getInteractionOptions('sticky-note').selectable).toBe(true)
|
|
29
|
+
expect(getInteractionOptions('sticky-note').movable).toBe(true)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('consumer-registered widget exposes label/icon via getWidgetMeta', () => {
|
|
33
|
+
registerWidget('my-thing', { label: 'My Thing', icon: 'sparkle-fill' })
|
|
34
|
+
expect(getWidgetMeta('my-thing')).toEqual({ label: 'My Thing', icon: 'sparkle-fill' })
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('chrome.enabled defaults to true, overrideable to false', () => {
|
|
38
|
+
registerWidget('invisible', { label: 'Invisible', chrome: { enabled: false } })
|
|
39
|
+
expect(getChromeOptions('invisible').enabled).toBe(false)
|
|
40
|
+
registerWidget('visible', { label: 'Visible', chrome: { enabled: true } })
|
|
41
|
+
expect(getChromeOptions('visible').enabled).toBe(true)
|
|
42
|
+
registerWidget('default', { label: 'Default' })
|
|
43
|
+
expect(getChromeOptions('default').enabled).toBe(true)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('interaction.selectable/movable default to true, overrideable to false', () => {
|
|
47
|
+
registerWidget('pinned', { label: 'Pinned', interaction: { movable: false } })
|
|
48
|
+
expect(getInteractionOptions('pinned').movable).toBe(false)
|
|
49
|
+
expect(getInteractionOptions('pinned').selectable).toBe(true)
|
|
50
|
+
|
|
51
|
+
registerWidget('decorative', { label: 'Deco', interaction: { selectable: false } })
|
|
52
|
+
expect(getInteractionOptions('decorative').selectable).toBe(false)
|
|
53
|
+
expect(getInteractionOptions('decorative').movable).toBe(true)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('interaction.resize/expandable/splitScreen flow through to legacy getters', () => {
|
|
57
|
+
registerWidget('big', {
|
|
58
|
+
label: 'Big',
|
|
59
|
+
interaction: {
|
|
60
|
+
resize: { enabled: true, prod: true },
|
|
61
|
+
expandable: true,
|
|
62
|
+
splitScreen: true,
|
|
63
|
+
interactGate: true,
|
|
64
|
+
interactGateLabel: 'Tap to interact',
|
|
65
|
+
},
|
|
66
|
+
})
|
|
67
|
+
expect(isResizable('big')).toBe(true)
|
|
68
|
+
expect(isExpandable('big')).toBe(true)
|
|
69
|
+
expect(isSplitScreenCapable('big')).toBe(true)
|
|
70
|
+
expect(getInteractGate('big')).toEqual({ enabled: true, label: 'Tap to interact' })
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('legacy top-level keys still work as back-compat (widgets.config.json shape)', () => {
|
|
74
|
+
// No nested `interaction` block — the widgets.config.json baseline
|
|
75
|
+
// should keep working when consumer omits the block.
|
|
76
|
+
registerWidget('legacy', {
|
|
77
|
+
label: 'Legacy',
|
|
78
|
+
resize: { enabled: true, prod: false },
|
|
79
|
+
expandable: true,
|
|
80
|
+
splitScreen: true,
|
|
81
|
+
interactGate: true,
|
|
82
|
+
interactGateLabel: 'Old gate',
|
|
83
|
+
})
|
|
84
|
+
expect(isResizable('legacy')).toBe(true)
|
|
85
|
+
expect(isExpandable('legacy')).toBe(true)
|
|
86
|
+
expect(isSplitScreenCapable('legacy')).toBe(true)
|
|
87
|
+
expect(getInteractGate('legacy').enabled).toBe(true)
|
|
88
|
+
expect(getInteractGate('legacy').label).toBe('Old gate')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('getMenuWidgetTypes includes consumer entries and excludes unlisted', () => {
|
|
92
|
+
registerWidget('shown', { label: 'Shown', icon: 'star' })
|
|
93
|
+
registerWidget('hidden', { label: 'Hidden', unlisted: true })
|
|
94
|
+
const types = getMenuWidgetTypes()
|
|
95
|
+
expect(types.some((t) => t.type === 'shown')).toBe(true)
|
|
96
|
+
expect(types.some((t) => t.type === 'hidden')).toBe(false)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('consumer features flow through getFeatures', () => {
|
|
100
|
+
registerWidget('withFeat', {
|
|
101
|
+
label: 'With',
|
|
102
|
+
features: [
|
|
103
|
+
{ id: 'foo', type: 'action', action: 'foo', label: 'Foo', icon: 'star', prod: true },
|
|
104
|
+
],
|
|
105
|
+
})
|
|
106
|
+
const feats = getFeatures('withFeat', { isLocalDev: true })
|
|
107
|
+
expect(feats.length).toBe(1)
|
|
108
|
+
expect(feats[0].action).toBe('foo')
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('consumer connector config flows through getConnectorConfig', () => {
|
|
112
|
+
registerWidget('lockedConn', {
|
|
113
|
+
label: 'Locked',
|
|
114
|
+
connectors: { anchors: { top: 'unavailable', bottom: 'disabled', left: 'available', right: 'available' }, accept: ['sticky-note'] },
|
|
115
|
+
})
|
|
116
|
+
const cfg = getConnectorConfig('lockedConn')
|
|
117
|
+
expect(cfg.anchors.top).toBe('unavailable')
|
|
118
|
+
expect(cfg.anchors.bottom).toBe('disabled')
|
|
119
|
+
expect(cfg.accept).toEqual(['sticky-note'])
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('consumer schemas are exposed via the schemas proxy', () => {
|
|
123
|
+
registerWidget('withProps', {
|
|
124
|
+
label: 'WithProps',
|
|
125
|
+
props: {
|
|
126
|
+
title: { type: 'text', label: 'Title', default: 'Hi' },
|
|
127
|
+
width: { type: 'number', label: 'Width', default: 320 },
|
|
128
|
+
},
|
|
129
|
+
})
|
|
130
|
+
expect(schemas['withProps']).toEqual({
|
|
131
|
+
title: { type: 'text', label: 'Title', category: undefined, defaultValue: 'Hi' },
|
|
132
|
+
width: { type: 'number', label: 'Width', category: undefined, defaultValue: 320 },
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('consumer registration overrides core widget for same type', () => {
|
|
137
|
+
// sticky-note is movable by default
|
|
138
|
+
expect(getInteractionOptions('sticky-note').movable).toBe(true)
|
|
139
|
+
registerWidget('sticky-note', { label: 'Sticky', interaction: { movable: false } })
|
|
140
|
+
expect(getInteractionOptions('sticky-note').movable).toBe(false)
|
|
141
|
+
// After unregister via re-init the core entry returns
|
|
142
|
+
vi.clearAllMocks()
|
|
143
|
+
})
|
|
144
|
+
})
|
|
@@ -2,7 +2,7 @@ import { useState, useEffect, useMemo, useRef, Suspense, lazy, Component } from
|
|
|
2
2
|
import { useParams, useLocation } from 'react-router-dom'
|
|
3
3
|
// Named import seeds the core data index via init() AND provides canvas/story route data
|
|
4
4
|
import { canvases, stories } from 'virtual:storyboard-data-index'
|
|
5
|
-
import { loadFlow, flowExists, findRecord, deepMerge, setFlowClass, installBodyClassSync, resolveFlowName, resolveRecordName,
|
|
5
|
+
import { loadFlow, flowExists, findRecord, deepMerge, setFlowClass, installBodyClassSync, resolveFlowName, resolveRecordName, getPrototypeMetadata } from '../core/index.js'
|
|
6
6
|
import { StoryboardContext } from './StoryboardContext.js'
|
|
7
7
|
import usePrototypeReloadGuard from './hooks/usePrototypeReloadGuard.js'
|
|
8
8
|
import styles from './FlowError.module.css'
|
|
@@ -336,22 +336,6 @@ function StoryboardProviderInner({ flowName, sceneName, recordName, recordParam,
|
|
|
336
336
|
document.title = title + branchSuffix
|
|
337
337
|
}, [canvasId, prototypeName])
|
|
338
338
|
|
|
339
|
-
// Mount design modes UI when enabled in storyboard.config.json
|
|
340
|
-
useEffect(() => {
|
|
341
|
-
if (!isModesEnabled()) return
|
|
342
|
-
|
|
343
|
-
let cleanup
|
|
344
|
-
import('@dfosco/storyboard/ui-runtime')
|
|
345
|
-
.then(({ mountDesignModes }) => {
|
|
346
|
-
cleanup = mountDesignModes()
|
|
347
|
-
})
|
|
348
|
-
.catch(() => {
|
|
349
|
-
// UI not available — degrade gracefully
|
|
350
|
-
})
|
|
351
|
-
|
|
352
|
-
return () => cleanup?.()
|
|
353
|
-
}, [])
|
|
354
|
-
|
|
355
339
|
// Skip flow loading for canvas/story pages and flow-less pages
|
|
356
340
|
const { data, error, flowTokens } = useMemo(() => {
|
|
357
341
|
if (canvasId || isMissingCanvasRoute || storyName || isMissingStoryRoute) return { data: null, error: null, flowTokens: null }
|