@dfosco/storyboard-react 4.2.0-beta.3 → 4.2.0
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 +10 -11
- package/src/AuthModal/AuthModal.jsx +6 -8
- package/src/BranchBar/BranchBar.jsx +20 -6
- package/src/BranchBar/BranchBar.module.css +13 -4
- package/src/BranchBar/useBranches.js +20 -6
- package/src/BranchBar/useBranches.test.js +68 -0
- package/src/CommandPalette/CommandPalette.jsx +480 -187
- package/src/CommandPalette/command-palette.css +142 -78
- package/src/Icon.jsx +157 -58
- package/src/Viewfinder.jsx +562 -207
- package/src/Viewfinder.module.css +434 -93
- package/src/Workspace.jsx +7 -0
- package/src/canvas/CanvasPage.bridge.test.jsx +14 -6
- package/src/canvas/CanvasPage.dragdrop.test.jsx +11 -7
- package/src/canvas/CanvasPage.jsx +739 -219
- package/src/canvas/CanvasPage.module.css +13 -15
- package/src/canvas/CanvasPage.multiselect.test.jsx +17 -6
- package/src/canvas/ConnectorLayer.jsx +121 -165
- package/src/canvas/ConnectorLayer.module.css +69 -0
- package/src/canvas/PageSelector.test.jsx +15 -6
- package/src/canvas/canvasApi.js +68 -2
- package/src/canvas/canvasReloadGuard.test.js +1 -1
- package/src/canvas/connectorGeometry.js +132 -0
- package/src/canvas/hotPoolDevLogs.js +25 -0
- package/src/canvas/useCanvas.js +1 -1
- package/src/canvas/useMarqueeSelect.js +30 -4
- package/src/canvas/widgets/CodePenEmbed.jsx +1 -0
- package/src/canvas/widgets/ComponentSetWidget.jsx +199 -0
- package/src/canvas/widgets/ComponentSetWidget.module.css +89 -0
- package/src/canvas/widgets/ComponentWidget.jsx +1 -0
- package/src/canvas/widgets/CropOverlay.jsx +219 -0
- package/src/canvas/widgets/CropOverlay.module.css +118 -0
- package/src/canvas/widgets/ExpandedPane.jsx +474 -0
- package/src/canvas/widgets/ExpandedPane.module.css +179 -0
- package/src/canvas/widgets/ExpandedPane.test.jsx +240 -0
- package/src/canvas/widgets/ExpandedPaneTopBar.jsx +111 -0
- package/src/canvas/widgets/ExpandedPaneTopBar.module.css +59 -0
- package/src/canvas/widgets/ExpandedPaneTopBar.test.jsx +45 -0
- package/src/canvas/widgets/FigmaEmbed.jsx +62 -47
- package/src/canvas/widgets/FigmaEmbed.module.css +61 -0
- package/src/canvas/widgets/ImageWidget.jsx +130 -9
- package/src/canvas/widgets/ImageWidget.module.css +30 -0
- package/src/canvas/widgets/LinkPreview.jsx +113 -5
- package/src/canvas/widgets/LinkPreview.module.css +127 -0
- package/src/canvas/widgets/MarkdownBlock.jsx +167 -17
- package/src/canvas/widgets/MarkdownBlock.module.css +148 -0
- package/src/canvas/widgets/PromptWidget.jsx +414 -0
- package/src/canvas/widgets/PromptWidget.module.css +273 -0
- package/src/canvas/widgets/PrototypeEmbed.jsx +77 -39
- package/src/canvas/widgets/PrototypeEmbed.module.css +117 -0
- package/src/canvas/widgets/PrototypeEmbed.test.jsx +2 -2
- package/src/canvas/widgets/ResizeHandle.jsx +17 -6
- package/src/canvas/widgets/StoryWidget.jsx +73 -15
- package/src/canvas/widgets/TerminalReadWidget.jsx +146 -0
- package/src/canvas/widgets/TerminalReadWidget.module.css +94 -0
- package/src/canvas/widgets/TerminalWidget.jsx +445 -67
- package/src/canvas/widgets/TerminalWidget.module.css +271 -8
- package/src/canvas/widgets/TilesWidget.jsx +300 -0
- package/src/canvas/widgets/TilesWidget.module.css +133 -0
- package/src/canvas/widgets/WidgetChrome.jsx +74 -153
- package/src/canvas/widgets/WidgetChrome.module.css +30 -1
- package/src/canvas/widgets/embedInteraction.test.jsx +24 -26
- package/src/canvas/widgets/expandUtils.js +560 -0
- package/src/canvas/widgets/expandUtils.test.js +155 -0
- package/src/canvas/widgets/index.js +9 -0
- package/src/canvas/widgets/snapshotDisplay.test.jsx +23 -71
- package/src/canvas/widgets/tilePool.js +23 -0
- package/src/canvas/widgets/tiles/diagonal-bl.png +0 -0
- package/src/canvas/widgets/tiles/diagonal-br.png +0 -0
- package/src/canvas/widgets/tiles/diagonal-tl.png +0 -0
- package/src/canvas/widgets/tiles/leaf.png +0 -0
- package/src/canvas/widgets/tiles/quarter-tl.png +0 -0
- package/src/canvas/widgets/tiles/quarter-tr.png +0 -0
- package/src/canvas/widgets/tiles/solid-a.png +0 -0
- package/src/canvas/widgets/tiles/solid-b.png +0 -0
- package/src/canvas/widgets/widgetConfig.js +55 -4
- package/src/canvas/widgets/widgetIcons.jsx +190 -0
- package/src/canvas/widgets/widgetProps.js +1 -0
- package/src/context.jsx +48 -20
- package/src/hooks/useConfig.js +14 -0
- package/src/hooks/usePrototypeReloadGuard.js +64 -0
- package/src/hooks/useSceneData.js +1 -0
- package/src/hooks/useThemeState.test.js +1 -1
- package/src/index.js +8 -2
- package/src/story/ComponentSetPage.jsx +186 -0
- package/src/story/ComponentSetPage.module.css +121 -0
- package/src/story/StoryPage.jsx +32 -2
- package/src/vite/data-plugin.js +363 -67
- package/src/vite/data-plugin.test.js +1 -1
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { assignToQuadrants, buildSplitLayout } from './expandUtils.js'
|
|
3
|
+
|
|
4
|
+
describe('assignToQuadrants', () => {
|
|
5
|
+
it('assigns 2 items to left/right columns', () => {
|
|
6
|
+
const result = assignToQuadrants([
|
|
7
|
+
{ x: 100, y: 200, data: 'A' },
|
|
8
|
+
{ x: 500, y: 200, data: 'B' },
|
|
9
|
+
])
|
|
10
|
+
// centroid.y = 200 = both y values, so both get 'b' row (>= centroid)
|
|
11
|
+
expect(result.bl).toBe('A')
|
|
12
|
+
expect(result.br).toBe('B')
|
|
13
|
+
expect(result.tl).toBeNull()
|
|
14
|
+
expect(result.tr).toBeNull()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('assigns 2 items stacked vertically to top/bottom in same column', () => {
|
|
18
|
+
const result = assignToQuadrants([
|
|
19
|
+
{ x: 100, y: 100, data: 'A' },
|
|
20
|
+
{ x: 100, y: 500, data: 'B' },
|
|
21
|
+
])
|
|
22
|
+
// Both same x → degenerate x, but different y
|
|
23
|
+
// centroid = (100, 300). A.y=100 < 300 → top, B.y=500 >= 300 → bottom
|
|
24
|
+
// A.x=100 is NOT < centroid.x=100, so both go to 'r'
|
|
25
|
+
// → tr='A', br='B'
|
|
26
|
+
expect(result.tr).toBe('A')
|
|
27
|
+
expect(result.br).toBe('B')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('assigns 4 items to all quadrants', () => {
|
|
31
|
+
const result = assignToQuadrants([
|
|
32
|
+
{ x: 100, y: 100, data: 'TL' },
|
|
33
|
+
{ x: 500, y: 100, data: 'TR' },
|
|
34
|
+
{ x: 100, y: 500, data: 'BL' },
|
|
35
|
+
{ x: 500, y: 500, data: 'BR' },
|
|
36
|
+
])
|
|
37
|
+
expect(result.tl).toBe('TL')
|
|
38
|
+
expect(result.tr).toBe('TR')
|
|
39
|
+
expect(result.bl).toBe('BL')
|
|
40
|
+
expect(result.br).toBe('BR')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('assigns 3 items: 2 in left column, 1 in right', () => {
|
|
44
|
+
const result = assignToQuadrants([
|
|
45
|
+
{ x: 100, y: 100, data: 'TL' },
|
|
46
|
+
{ x: 100, y: 500, data: 'BL' },
|
|
47
|
+
{ x: 500, y: 300, data: 'R' },
|
|
48
|
+
])
|
|
49
|
+
// centroid x = (100+100+500)/3 ≈ 233, y = (100+500+300)/3 = 300
|
|
50
|
+
// TL: x=100 < 233 → l, y=100 < 300 → t → tl
|
|
51
|
+
// BL: x=100 < 233 → l, y=500 >= 300 → b → bl
|
|
52
|
+
// R: x=500 >= 233 → r, y=300 >= 300 → b → br
|
|
53
|
+
expect(result.tl).toBe('TL')
|
|
54
|
+
expect(result.bl).toBe('BL')
|
|
55
|
+
expect(result.br).toBe('R')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('cycles TL→TR→BL→BR when all positions are identical', () => {
|
|
59
|
+
const result = assignToQuadrants([
|
|
60
|
+
{ x: 0, y: 0, data: 'A' },
|
|
61
|
+
{ x: 0, y: 0, data: 'B' },
|
|
62
|
+
{ x: 0, y: 0, data: 'C' },
|
|
63
|
+
])
|
|
64
|
+
expect(result.tl).toBe('A')
|
|
65
|
+
expect(result.tr).toBe('B')
|
|
66
|
+
expect(result.bl).toBe('C')
|
|
67
|
+
expect(result.br).toBeNull()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('handles overflow: 2 items in same quadrant', () => {
|
|
71
|
+
// Two items very close, both land in same quadrant → overflow redistributes
|
|
72
|
+
const result = assignToQuadrants([
|
|
73
|
+
{ x: 100, y: 100, data: 'A' },
|
|
74
|
+
{ x: 110, y: 110, data: 'B' },
|
|
75
|
+
{ x: 500, y: 500, data: 'C' },
|
|
76
|
+
])
|
|
77
|
+
// centroid = (236, 236). A: x<236 y<236 → tl. B: x<236 y<236 → tl (overflow!)
|
|
78
|
+
// C: x>=236 y>=236 → br
|
|
79
|
+
// A wins tl, B overflows to first empty slot (tr)
|
|
80
|
+
expect(result.tl).toBe('A')
|
|
81
|
+
expect(result.br).toBe('C')
|
|
82
|
+
// B goes to overflow → first empty slot (tr)
|
|
83
|
+
expect(result.tr).toBe('B')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('returns all nulls for empty input', () => {
|
|
87
|
+
const result = assignToQuadrants([])
|
|
88
|
+
expect(result.tl).toBeNull()
|
|
89
|
+
expect(result.tr).toBeNull()
|
|
90
|
+
expect(result.bl).toBeNull()
|
|
91
|
+
expect(result.br).toBeNull()
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
describe('buildSplitLayout', () => {
|
|
96
|
+
function mockPaneFn(widget) {
|
|
97
|
+
return { id: widget.id, label: widget.id, kind: 'react', render: () => null }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
it('returns single column for 1 widget (no connected)', () => {
|
|
101
|
+
const primary = { id: 'a', type: 'terminal', position: { x: 0, y: 0 }, props: {} }
|
|
102
|
+
const layout = buildSplitLayout(primary, [], mockPaneFn)
|
|
103
|
+
expect(layout.length).toBe(1)
|
|
104
|
+
expect(layout[0].length).toBe(1)
|
|
105
|
+
expect(layout[0][0].id).toBe('a')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('returns 2 columns for 2 horizontally-spaced widgets', () => {
|
|
109
|
+
const primary = { id: 'a', type: 'terminal', position: { x: 0, y: 100 }, props: {} }
|
|
110
|
+
const connected = [
|
|
111
|
+
{ id: 'b', type: 'prototype', position: { x: 500, y: 100 }, props: {} },
|
|
112
|
+
]
|
|
113
|
+
const layout = buildSplitLayout(primary, connected, mockPaneFn)
|
|
114
|
+
expect(layout.length).toBe(2)
|
|
115
|
+
expect(layout[0].length).toBe(1) // left column
|
|
116
|
+
expect(layout[1].length).toBe(1) // right column
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('returns 2 columns with row split for 3 widgets', () => {
|
|
120
|
+
const primary = { id: 'a', type: 'terminal', position: { x: 0, y: 0 }, props: {} }
|
|
121
|
+
const connected = [
|
|
122
|
+
{ id: 'b', type: 'prototype', position: { x: 500, y: 0 }, props: {} },
|
|
123
|
+
{ id: 'c', type: 'markdown', position: { x: 500, y: 500 }, props: {} },
|
|
124
|
+
]
|
|
125
|
+
const layout = buildSplitLayout(primary, connected, mockPaneFn)
|
|
126
|
+
const totalPanes = layout.flat().length
|
|
127
|
+
expect(totalPanes).toBe(3)
|
|
128
|
+
// One column should have 2 panes (row split)
|
|
129
|
+
const hasRowSplit = layout.some((col) => col.length === 2)
|
|
130
|
+
expect(hasRowSplit).toBe(true)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('returns 2×2 grid for 4 widgets', () => {
|
|
134
|
+
const primary = { id: 'a', type: 'terminal', position: { x: 0, y: 0 }, props: {} }
|
|
135
|
+
const connected = [
|
|
136
|
+
{ id: 'b', type: 'prototype', position: { x: 500, y: 0 }, props: {} },
|
|
137
|
+
{ id: 'c', type: 'markdown', position: { x: 0, y: 500 }, props: {} },
|
|
138
|
+
{ id: 'd', type: 'agent', position: { x: 500, y: 500 }, props: {} },
|
|
139
|
+
]
|
|
140
|
+
const layout = buildSplitLayout(primary, connected, mockPaneFn)
|
|
141
|
+
expect(layout.length).toBe(2)
|
|
142
|
+
expect(layout[0].length).toBe(2)
|
|
143
|
+
expect(layout[1].length).toBe(2)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('skips widgets where buildPaneFn returns null', () => {
|
|
147
|
+
const primary = { id: 'a', type: 'terminal', position: { x: 0, y: 0 }, props: {} }
|
|
148
|
+
const connected = [
|
|
149
|
+
{ id: 'b', type: 'unknown', position: { x: 500, y: 0 }, props: {} },
|
|
150
|
+
]
|
|
151
|
+
const paneFn = (w) => w.id === 'a' ? mockPaneFn(w) : null
|
|
152
|
+
const layout = buildSplitLayout(primary, connected, paneFn)
|
|
153
|
+
expect(layout.flat().length).toBe(1)
|
|
154
|
+
})
|
|
155
|
+
})
|
|
@@ -6,7 +6,11 @@ import ImageWidget from './ImageWidget.jsx'
|
|
|
6
6
|
import FigmaEmbed from './FigmaEmbed.jsx'
|
|
7
7
|
import CodePenEmbed from './CodePenEmbed.jsx'
|
|
8
8
|
import StoryWidget from './StoryWidget.jsx'
|
|
9
|
+
import ComponentSetWidget from './ComponentSetWidget.jsx'
|
|
9
10
|
import TerminalWidget from './TerminalWidget.jsx'
|
|
11
|
+
import TerminalReadWidget from './TerminalReadWidget.jsx'
|
|
12
|
+
import PromptWidget from './PromptWidget.jsx'
|
|
13
|
+
import TilesWidget from './TilesWidget.jsx'
|
|
10
14
|
|
|
11
15
|
/**
|
|
12
16
|
* Maps widget type strings to their React components.
|
|
@@ -21,7 +25,12 @@ export const widgetRegistry = {
|
|
|
21
25
|
'figma-embed': FigmaEmbed,
|
|
22
26
|
'codepen-embed': CodePenEmbed,
|
|
23
27
|
'story': StoryWidget,
|
|
28
|
+
'component-set': ComponentSetWidget,
|
|
24
29
|
'terminal': TerminalWidget,
|
|
30
|
+
'terminal-read': TerminalReadWidget,
|
|
31
|
+
'agent': TerminalWidget,
|
|
32
|
+
'prompt': PromptWidget,
|
|
33
|
+
'tiles': TilesWidget,
|
|
25
34
|
}
|
|
26
35
|
|
|
27
36
|
/**
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tests for iframe snapshot display — single snapshot prop.
|
|
3
3
|
*/
|
|
4
|
-
import { describe, it, expect, vi,
|
|
5
|
-
import { render
|
|
4
|
+
import { describe, it, expect, vi, afterEach } from 'vitest'
|
|
5
|
+
import { render } from '@testing-library/react'
|
|
6
6
|
import PrototypeEmbed from './PrototypeEmbed.jsx'
|
|
7
7
|
import StoryWidget from './StoryWidget.jsx'
|
|
8
8
|
|
|
@@ -54,9 +54,9 @@ afterEach(() => {
|
|
|
54
54
|
document.querySelectorAll('[data-sb-canvas-theme]').forEach(el => el.remove())
|
|
55
55
|
})
|
|
56
56
|
|
|
57
|
-
describe('Snapshot display', () => {
|
|
57
|
+
describe('Snapshot display (snapshots removed — iframes always render)', () => {
|
|
58
58
|
describe('PrototypeEmbed', () => {
|
|
59
|
-
it('
|
|
59
|
+
it('renders iframe even when snapshot prop is provided', () => {
|
|
60
60
|
const { wrapper } = renderInCanvas(
|
|
61
61
|
<PrototypeEmbed
|
|
62
62
|
id="proto-abc123"
|
|
@@ -72,13 +72,11 @@ describe('Snapshot display', () => {
|
|
|
72
72
|
/>
|
|
73
73
|
)
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
expect(
|
|
77
|
-
expect(img.src).toContain('snapshot-proto-abc123.webp')
|
|
78
|
-
expect(wrapper.querySelector('iframe')).not.toBeInTheDocument()
|
|
75
|
+
expect(wrapper.querySelector('img')).not.toBeInTheDocument()
|
|
76
|
+
expect(wrapper.querySelector('iframe')).toBeInTheDocument()
|
|
79
77
|
})
|
|
80
78
|
|
|
81
|
-
it('
|
|
79
|
+
it('renders iframe when snapshotLight prop is provided', () => {
|
|
82
80
|
const { wrapper } = renderInCanvas(
|
|
83
81
|
<PrototypeEmbed
|
|
84
82
|
id="proto-abc123"
|
|
@@ -94,12 +92,11 @@ describe('Snapshot display', () => {
|
|
|
94
92
|
/>
|
|
95
93
|
)
|
|
96
94
|
|
|
97
|
-
|
|
98
|
-
expect(
|
|
99
|
-
expect(img.src).toContain('snapshot-proto-abc123--light.webp')
|
|
95
|
+
expect(wrapper.querySelector('img')).not.toBeInTheDocument()
|
|
96
|
+
expect(wrapper.querySelector('iframe')).toBeInTheDocument()
|
|
100
97
|
})
|
|
101
98
|
|
|
102
|
-
it('
|
|
99
|
+
it('renders iframe when no snapshot exists', () => {
|
|
103
100
|
const { wrapper } = renderInCanvas(
|
|
104
101
|
<PrototypeEmbed
|
|
105
102
|
id="proto-xyz"
|
|
@@ -110,32 +107,10 @@ describe('Snapshot display', () => {
|
|
|
110
107
|
)
|
|
111
108
|
|
|
112
109
|
expect(wrapper.querySelector('img')).not.toBeInTheDocument()
|
|
113
|
-
expect(wrapper.querySelector('iframe')).
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
it('falls back to placeholder when snapshot image fails to load', () => {
|
|
117
|
-
const { wrapper } = renderInCanvas(
|
|
118
|
-
<PrototypeEmbed
|
|
119
|
-
id="proto-abc123"
|
|
120
|
-
props={{
|
|
121
|
-
src: '/test',
|
|
122
|
-
width: 400,
|
|
123
|
-
height: 300,
|
|
124
|
-
zoom: 100,
|
|
125
|
-
snapshot: '/_storyboard/canvas/images/snapshot-proto-abc123.webp?v=123',
|
|
126
|
-
}}
|
|
127
|
-
onUpdate={vi.fn()}
|
|
128
|
-
resizable={false}
|
|
129
|
-
/>
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
const img = wrapper.querySelector('img')
|
|
133
|
-
expect(img).toBeInTheDocument()
|
|
134
|
-
fireEvent.error(img)
|
|
135
|
-
expect(wrapper.querySelector('img')).not.toBeInTheDocument()
|
|
110
|
+
expect(wrapper.querySelector('iframe')).toBeInTheDocument()
|
|
136
111
|
})
|
|
137
112
|
|
|
138
|
-
it('ignores snapshot that does not match widget ID', () => {
|
|
113
|
+
it('ignores snapshot prop that does not match widget ID', () => {
|
|
139
114
|
const { wrapper } = renderInCanvas(
|
|
140
115
|
<PrototypeEmbed
|
|
141
116
|
id="proto-abc123"
|
|
@@ -152,9 +127,10 @@ describe('Snapshot display', () => {
|
|
|
152
127
|
)
|
|
153
128
|
|
|
154
129
|
expect(wrapper.querySelector('img')).not.toBeInTheDocument()
|
|
130
|
+
expect(wrapper.querySelector('iframe')).toBeInTheDocument()
|
|
155
131
|
})
|
|
156
132
|
|
|
157
|
-
it('
|
|
133
|
+
it('renders iframe for external URLs regardless of snapshot', () => {
|
|
158
134
|
const { wrapper } = renderInCanvas(
|
|
159
135
|
<PrototypeEmbed
|
|
160
136
|
id="proto-ext"
|
|
@@ -171,11 +147,12 @@ describe('Snapshot display', () => {
|
|
|
171
147
|
)
|
|
172
148
|
|
|
173
149
|
expect(wrapper.querySelector('img')).not.toBeInTheDocument()
|
|
150
|
+
expect(wrapper.querySelector('iframe')).toBeInTheDocument()
|
|
174
151
|
})
|
|
175
152
|
})
|
|
176
153
|
|
|
177
154
|
describe('StoryWidget', () => {
|
|
178
|
-
it('
|
|
155
|
+
it('renders iframe even when snapshot prop is provided', () => {
|
|
179
156
|
const { wrapper } = renderInCanvas(
|
|
180
157
|
<StoryWidget
|
|
181
158
|
id="story-abc123"
|
|
@@ -191,13 +168,11 @@ describe('Snapshot display', () => {
|
|
|
191
168
|
/>
|
|
192
169
|
)
|
|
193
170
|
|
|
194
|
-
|
|
195
|
-
expect(
|
|
196
|
-
expect(img.src).toContain('snapshot-story-abc123.webp')
|
|
197
|
-
expect(wrapper.querySelector('iframe')).not.toBeInTheDocument()
|
|
171
|
+
expect(wrapper.querySelector('img')).not.toBeInTheDocument()
|
|
172
|
+
expect(wrapper.querySelector('iframe')).toBeInTheDocument()
|
|
198
173
|
})
|
|
199
174
|
|
|
200
|
-
it('
|
|
175
|
+
it('renders iframe when snapshotDark prop is provided', () => {
|
|
201
176
|
const { wrapper } = renderInCanvas(
|
|
202
177
|
<StoryWidget
|
|
203
178
|
id="story-abc123"
|
|
@@ -210,50 +185,27 @@ describe('Snapshot display', () => {
|
|
|
210
185
|
/>
|
|
211
186
|
)
|
|
212
187
|
|
|
213
|
-
const img = wrapper.querySelector('img')
|
|
214
|
-
expect(img).toBeInTheDocument()
|
|
215
|
-
expect(img.src).toContain('snapshot-story-abc123--dark.webp')
|
|
216
|
-
})
|
|
217
|
-
|
|
218
|
-
it('shows placeholder when no snapshot exists', () => {
|
|
219
|
-
const { wrapper } = renderInCanvas(
|
|
220
|
-
<StoryWidget
|
|
221
|
-
id="story-xyz"
|
|
222
|
-
props={{
|
|
223
|
-
storyId: 'button-patterns',
|
|
224
|
-
exportName: 'Primary',
|
|
225
|
-
width: 400,
|
|
226
|
-
height: 300,
|
|
227
|
-
}}
|
|
228
|
-
onUpdate={vi.fn()}
|
|
229
|
-
resizable={false}
|
|
230
|
-
/>
|
|
231
|
-
)
|
|
232
|
-
|
|
233
188
|
expect(wrapper.querySelector('img')).not.toBeInTheDocument()
|
|
234
|
-
expect(wrapper.querySelector('iframe')).
|
|
189
|
+
expect(wrapper.querySelector('iframe')).toBeInTheDocument()
|
|
235
190
|
})
|
|
236
191
|
|
|
237
|
-
it('
|
|
192
|
+
it('renders iframe when no snapshot exists', () => {
|
|
238
193
|
const { wrapper } = renderInCanvas(
|
|
239
194
|
<StoryWidget
|
|
240
|
-
id="story-
|
|
195
|
+
id="story-xyz"
|
|
241
196
|
props={{
|
|
242
197
|
storyId: 'button-patterns',
|
|
243
198
|
exportName: 'Primary',
|
|
244
199
|
width: 400,
|
|
245
200
|
height: 300,
|
|
246
|
-
snapshot: '/_storyboard/canvas/images/snapshot-story-abc123.webp?v=456',
|
|
247
201
|
}}
|
|
248
202
|
onUpdate={vi.fn()}
|
|
249
203
|
resizable={false}
|
|
250
204
|
/>
|
|
251
205
|
)
|
|
252
206
|
|
|
253
|
-
const img = wrapper.querySelector('img')
|
|
254
|
-
expect(img).toBeInTheDocument()
|
|
255
|
-
fireEvent.error(img)
|
|
256
207
|
expect(wrapper.querySelector('img')).not.toBeInTheDocument()
|
|
208
|
+
expect(wrapper.querySelector('iframe')).toBeInTheDocument()
|
|
257
209
|
})
|
|
258
210
|
})
|
|
259
211
|
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static tile image pool.
|
|
3
|
+
* Each import resolves to a Vite asset URL at build time.
|
|
4
|
+
*/
|
|
5
|
+
import solidA from './tiles/solid-a.png'
|
|
6
|
+
import solidB from './tiles/solid-b.png'
|
|
7
|
+
import quarterTL from './tiles/quarter-tl.png'
|
|
8
|
+
import quarterTR from './tiles/quarter-tr.png'
|
|
9
|
+
import diagonalBR from './tiles/diagonal-br.png'
|
|
10
|
+
import diagonalBL from './tiles/diagonal-bl.png'
|
|
11
|
+
import diagonalTL from './tiles/diagonal-tl.png'
|
|
12
|
+
import leaf from './tiles/leaf.png'
|
|
13
|
+
|
|
14
|
+
export const TILE_POOL = [
|
|
15
|
+
solidA,
|
|
16
|
+
solidB,
|
|
17
|
+
quarterTL,
|
|
18
|
+
quarterTR,
|
|
19
|
+
diagonalBR,
|
|
20
|
+
diagonalBL,
|
|
21
|
+
diagonalTL,
|
|
22
|
+
leaf,
|
|
23
|
+
]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -50,6 +50,11 @@ function resolveFeature(feature) {
|
|
|
50
50
|
const r = {}
|
|
51
51
|
for (const [k, v] of Object.entries(val)) r[k] = resolveVar(v)
|
|
52
52
|
resolved[key] = r
|
|
53
|
+
} else if (key === 'toggle' && val && typeof val === 'object') {
|
|
54
|
+
// Pass toggle config through as-is (stateKey, activeIcon, activeLabel)
|
|
55
|
+
resolved[key] = { ...val }
|
|
56
|
+
} else if (key === 'surfaces' && Array.isArray(val)) {
|
|
57
|
+
resolved[key] = val
|
|
53
58
|
} else {
|
|
54
59
|
resolved[key] = resolveVar(val)
|
|
55
60
|
}
|
|
@@ -115,16 +120,44 @@ export const widgetTypes = buildWidgetTypes()
|
|
|
115
120
|
* In production (or when isLocalDev is false, e.g. ?prodMode simulation),
|
|
116
121
|
* only features with `prod: true` are returned.
|
|
117
122
|
* In dev, all features are returned.
|
|
123
|
+
*
|
|
124
|
+
* Features with an explicit `surfaces` array that does NOT include `"toolbar"`
|
|
125
|
+
* are excluded — they only render on their declared surfaces (fullbar/splitbar).
|
|
126
|
+
* Features without a `surfaces` array default to toolbar-only.
|
|
127
|
+
*
|
|
118
128
|
* @param {string} type — widget type string
|
|
119
129
|
* @param {{ isLocalDev?: boolean }} [options]
|
|
120
130
|
* @returns {Array} features array from config (variables resolved), or empty array
|
|
121
131
|
*/
|
|
122
132
|
export function getFeatures(type, { isLocalDev = true } = {}) {
|
|
123
133
|
const features = widgetTypes[type]?.features ?? []
|
|
134
|
+
let filtered = features.filter(f => {
|
|
135
|
+
const surfaces = f.surfaces || ['toolbar']
|
|
136
|
+
return surfaces.includes('toolbar')
|
|
137
|
+
})
|
|
124
138
|
if (import.meta.env?.PROD || !isLocalDev) {
|
|
125
|
-
|
|
139
|
+
filtered = filtered.filter(f => f.prod)
|
|
126
140
|
}
|
|
127
|
-
return
|
|
141
|
+
return filtered
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get features for a specific rendering surface.
|
|
146
|
+
* Features without a `surfaces` array default to `["toolbar"]`.
|
|
147
|
+
* @param {string} type — widget type string
|
|
148
|
+
* @param {'toolbar' | 'fullbar' | 'splitbar'} surface — target surface
|
|
149
|
+
* @param {{ isLocalDev?: boolean }} [options]
|
|
150
|
+
* @returns {Array} filtered features for the given surface
|
|
151
|
+
*/
|
|
152
|
+
export function getFeaturesForSurface(type, surface, { isLocalDev = true } = {}) {
|
|
153
|
+
let features = widgetTypes[type]?.features ?? []
|
|
154
|
+
if (import.meta.env?.PROD || !isLocalDev) {
|
|
155
|
+
features = features.filter(f => f.prod)
|
|
156
|
+
}
|
|
157
|
+
return features.filter(f => {
|
|
158
|
+
const surfaces = f.surfaces || ['toolbar']
|
|
159
|
+
return surfaces.includes(surface)
|
|
160
|
+
})
|
|
128
161
|
}
|
|
129
162
|
|
|
130
163
|
/**
|
|
@@ -151,6 +184,24 @@ export function getWidgetMeta(type) {
|
|
|
151
184
|
return { label: def.label, icon: def.icon }
|
|
152
185
|
}
|
|
153
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Check if a widget type supports expanding to a full-screen modal.
|
|
189
|
+
* @param {string} type — widget type string
|
|
190
|
+
* @returns {boolean}
|
|
191
|
+
*/
|
|
192
|
+
export function isExpandable(type) {
|
|
193
|
+
return widgetTypes[type]?.expandable === true
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Check if a widget type can appear in a split-screen pane.
|
|
198
|
+
* @param {string} type — widget type string
|
|
199
|
+
* @returns {boolean}
|
|
200
|
+
*/
|
|
201
|
+
export function isSplitScreenCapable(type) {
|
|
202
|
+
return widgetTypes[type]?.splitScreen === true
|
|
203
|
+
}
|
|
204
|
+
|
|
154
205
|
/**
|
|
155
206
|
* Get the interact gate config for a widget type.
|
|
156
207
|
* @returns {{ enabled: boolean, label: string }}
|
|
@@ -170,7 +221,7 @@ export function getInteractGate(type) {
|
|
|
170
221
|
*/
|
|
171
222
|
export function getMenuWidgetTypes() {
|
|
172
223
|
return Object.entries(widgetTypes)
|
|
173
|
-
.filter(([type]) => type !== 'link-preview' && type !== 'image' && type !== 'figma-embed' && type !== 'codepen-embed' && type !== 'story')
|
|
224
|
+
.filter(([type, def]) => type !== 'link-preview' && type !== 'image' && type !== 'figma-embed' && type !== 'codepen-embed' && type !== 'story' && type !== 'terminal-read' && !def.unlisted)
|
|
174
225
|
.map(([type, def]) => ({ type, label: def.label, icon: def.icon }))
|
|
175
226
|
}
|
|
176
227
|
|
|
@@ -216,7 +267,7 @@ export function getConnectorDefaults() {
|
|
|
216
267
|
endpointFill: defaults.endpointFill ?? 'var(--fgColor-accent, #0969da)',
|
|
217
268
|
endpointStroke: defaults.endpointStroke ?? 'var(--bgColor-default, #ffffff)',
|
|
218
269
|
endpointStrokeWidth: defaults.endpointStrokeWidth ?? 3,
|
|
219
|
-
hitAreaStrokeWidth: defaults.hitAreaStrokeWidth ??
|
|
270
|
+
hitAreaStrokeWidth: defaults.hitAreaStrokeWidth ?? 24,
|
|
220
271
|
dragStroke: defaults.dragStroke ?? 'var(--fgColor-accent, #0969da)',
|
|
221
272
|
dragStrokeWidth: defaults.dragStrokeWidth ?? 2,
|
|
222
273
|
dragDasharray: defaults.dragDasharray ?? '6 4',
|