@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.
Files changed (34) hide show
  1. package/dist/storyboard-ui.css +1 -1
  2. package/dist/storyboard-ui.js +1 -10
  3. package/dist/storyboard-ui.js.map +1 -1
  4. package/dist/tailwind.css +1 -1
  5. package/package.json +1 -4
  6. package/scaffold/AGENTS.md +2 -79
  7. package/scaffold/skills/storyboard-core/SKILL.md +63 -171
  8. package/scaffold/skills/storyboard-widget/SKILL.md +172 -0
  9. package/scaffold/skills/tools/SKILL.md +12 -12
  10. package/src/core/canvas/collision.js +41 -19
  11. package/src/core/canvas/customWidgets.js +92 -0
  12. package/src/core/canvas/customWidgets.test.js +69 -0
  13. package/src/core/canvas/server.js +4 -3
  14. package/src/core/index.js +13 -0
  15. package/src/core/mountStoryboardCore.js +10 -0
  16. package/src/core/stores/configSchema.js +2 -1
  17. package/src/core/stores/widgetRegistry.js +184 -0
  18. package/src/core/stores/widgetRegistry.test.js +109 -0
  19. package/src/core/ui/CoreUIBar.jsx +1 -1
  20. package/src/core/ui-entry.js +0 -6
  21. package/src/core/vite/server-plugin.js +12 -0
  22. package/src/internals/canvas/CanvasPage.bridge.test.jsx +2 -0
  23. package/src/internals/canvas/CanvasPage.dragdrop.test.jsx +2 -0
  24. package/src/internals/canvas/CanvasPage.jsx +19 -3
  25. package/src/internals/canvas/CanvasPage.multiselect.test.jsx +2 -0
  26. package/src/internals/canvas/widgets/WidgetChrome.jsx +50 -5
  27. package/src/internals/canvas/widgets/WidgetChrome.test.jsx +65 -0
  28. package/src/internals/canvas/widgets/index.js +18 -4
  29. package/src/internals/canvas/widgets/widgetConfig.js +202 -45
  30. package/src/internals/canvas/widgets/widgetConfig.merged.test.js +144 -0
  31. package/src/internals/context.jsx +1 -17
  32. package/src/core/ui/design-modes.ts +0 -7
  33. package/src/core/ui/viewfinder.ts +0 -7
  34. 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, isModesEnabled, getPrototypeMetadata } from '../core/index.js'
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 }
@@ -1,7 +0,0 @@
1
- /**
2
- * Design modes UI mount — stub (Svelte UI removed).
3
- * These functions are no-ops. Mode switching is handled by React components.
4
- */
5
-
6
- export function mountDesignModesUI() {}
7
- export function unmountDesignModesUI() {}
@@ -1,7 +0,0 @@
1
- /**
2
- * Viewfinder mount — stub (Svelte UI removed).
3
- * These functions are no-ops. The viewfinder is rendered by React.
4
- */
5
-
6
- export function mountViewfinder() {}
7
- export function unmountViewfinder() {}
@@ -1,6 +0,0 @@
1
- /**
2
- * Workshop panel mount — stub (Svelte UI removed).
3
- */
4
-
5
- export function mountWorkshopPanel() {}
6
- export function unmountWorkshopPanel() {}