@dfosco/storyboard-react 3.11.0-beta.5 → 3.11.1-beta.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 +3 -3
- package/src/canvas/CanvasControls.jsx +59 -2
- package/src/canvas/CanvasControls.module.css +29 -0
- package/src/canvas/CanvasPage.bridge.test.jsx +42 -67
- package/src/canvas/CanvasPage.jsx +50 -572
- package/src/canvas/CanvasPage.module.css +2 -40
- package/src/canvas/canvasApi.js +0 -8
- package/src/canvas/widgets/PrototypeEmbed.jsx +1 -20
- package/src/canvas/widgets/WidgetChrome.jsx +36 -258
- package/src/canvas/widgets/WidgetChrome.module.css +5 -79
- package/src/canvas/widgets/index.js +0 -4
- package/src/canvas/widgets/widgetConfig.js +5 -54
- package/src/canvas/widgets/widgetProps.js +0 -2
- package/src/canvas/computeCanvasBounds.test.js +0 -121
- package/src/canvas/useUndoRedo.js +0 -86
- package/src/canvas/useUndoRedo.test.js +0 -231
- package/src/canvas/widgets/FigmaEmbed.jsx +0 -106
- package/src/canvas/widgets/FigmaEmbed.module.css +0 -83
- package/src/canvas/widgets/ImageWidget.jsx +0 -113
- package/src/canvas/widgets/ImageWidget.module.css +0 -39
- package/src/canvas/widgets/figmaUrl.js +0 -118
- package/src/canvas/widgets/figmaUrl.test.js +0 -139
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dfosco/storyboard-react",
|
|
3
|
-
"version": "3.11.
|
|
3
|
+
"version": "3.11.1-beta.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@dfosco/storyboard-core": "3.11.
|
|
7
|
-
"@dfosco/tiny-canvas": "3.11.
|
|
6
|
+
"@dfosco/storyboard-core": "3.11.1-beta.0",
|
|
7
|
+
"@dfosco/tiny-canvas": "3.11.1-beta.0",
|
|
8
8
|
"@neodrag/react": "^2.3.1",
|
|
9
9
|
"glob": "^11.0.0",
|
|
10
10
|
"jsonc-parser": "^3.3.1"
|
|
@@ -2,12 +2,16 @@ import { useState, useRef, useEffect, useCallback } from 'react'
|
|
|
2
2
|
import { getMenuWidgetTypes } from './widgets/widgetConfig.js'
|
|
3
3
|
import styles from './CanvasControls.module.css'
|
|
4
4
|
|
|
5
|
+
const ZOOM_STEPS = [25, 50, 75, 100, 125, 150, 200]
|
|
6
|
+
export const ZOOM_MIN = ZOOM_STEPS[0]
|
|
7
|
+
export const ZOOM_MAX = ZOOM_STEPS[ZOOM_STEPS.length - 1]
|
|
8
|
+
|
|
5
9
|
const WIDGET_TYPES = getMenuWidgetTypes()
|
|
6
10
|
|
|
7
11
|
/**
|
|
8
|
-
* Focused canvas toolbar — bottom-left
|
|
12
|
+
* Focused canvas toolbar — bottom-left controls for zoom and widget creation.
|
|
9
13
|
*/
|
|
10
|
-
export default function CanvasControls({ onAddWidget }) {
|
|
14
|
+
export default function CanvasControls({ zoom, onZoomChange, onAddWidget }) {
|
|
11
15
|
const [menuOpen, setMenuOpen] = useState(false)
|
|
12
16
|
const menuRef = useRef(null)
|
|
13
17
|
|
|
@@ -23,6 +27,24 @@ export default function CanvasControls({ onAddWidget }) {
|
|
|
23
27
|
return () => document.removeEventListener('pointerdown', handlePointerDown)
|
|
24
28
|
}, [menuOpen])
|
|
25
29
|
|
|
30
|
+
const zoomIn = useCallback(() => {
|
|
31
|
+
onZoomChange((z) => {
|
|
32
|
+
const next = ZOOM_STEPS.find((s) => s > z)
|
|
33
|
+
return next ?? ZOOM_MAX
|
|
34
|
+
})
|
|
35
|
+
}, [onZoomChange])
|
|
36
|
+
|
|
37
|
+
const zoomOut = useCallback(() => {
|
|
38
|
+
onZoomChange((z) => {
|
|
39
|
+
const next = [...ZOOM_STEPS].reverse().find((s) => s < z)
|
|
40
|
+
return next ?? ZOOM_MIN
|
|
41
|
+
})
|
|
42
|
+
}, [onZoomChange])
|
|
43
|
+
|
|
44
|
+
const resetZoom = useCallback(() => {
|
|
45
|
+
onZoomChange(100)
|
|
46
|
+
}, [onZoomChange])
|
|
47
|
+
|
|
26
48
|
const handleAddWidget = useCallback((type) => {
|
|
27
49
|
onAddWidget(type)
|
|
28
50
|
setMenuOpen(false)
|
|
@@ -30,6 +52,7 @@ export default function CanvasControls({ onAddWidget }) {
|
|
|
30
52
|
|
|
31
53
|
return (
|
|
32
54
|
<div className={styles.toolbar} role="toolbar" aria-label="Canvas controls">
|
|
55
|
+
{/* Create widget */}
|
|
33
56
|
<div ref={menuRef} className={styles.createGroup}>
|
|
34
57
|
<button
|
|
35
58
|
className={styles.btn}
|
|
@@ -58,6 +81,40 @@ export default function CanvasControls({ onAddWidget }) {
|
|
|
58
81
|
</div>
|
|
59
82
|
)}
|
|
60
83
|
</div>
|
|
84
|
+
|
|
85
|
+
<div className={styles.divider} />
|
|
86
|
+
|
|
87
|
+
{/* Zoom controls */}
|
|
88
|
+
<button
|
|
89
|
+
className={styles.btn}
|
|
90
|
+
onClick={zoomOut}
|
|
91
|
+
disabled={zoom <= ZOOM_MIN}
|
|
92
|
+
aria-label="Zoom out"
|
|
93
|
+
title="Zoom out"
|
|
94
|
+
>
|
|
95
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
|
96
|
+
<path d="M2.75 7.25h10.5a.75.75 0 0 1 0 1.5H2.75a.75.75 0 0 1 0-1.5Z" />
|
|
97
|
+
</svg>
|
|
98
|
+
</button>
|
|
99
|
+
<button
|
|
100
|
+
className={styles.zoomLevel}
|
|
101
|
+
onClick={resetZoom}
|
|
102
|
+
title="Reset to 100%"
|
|
103
|
+
aria-label={`Zoom ${zoom}%, click to reset`}
|
|
104
|
+
>
|
|
105
|
+
{zoom}%
|
|
106
|
+
</button>
|
|
107
|
+
<button
|
|
108
|
+
className={styles.btn}
|
|
109
|
+
onClick={zoomIn}
|
|
110
|
+
disabled={zoom >= ZOOM_MAX}
|
|
111
|
+
aria-label="Zoom in"
|
|
112
|
+
title="Zoom in"
|
|
113
|
+
>
|
|
114
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
|
115
|
+
<path d="M7.75 2a.75.75 0 0 1 .75.75V7h4.25a.75.75 0 0 1 0 1.5H8.5v4.25a.75.75 0 0 1-1.5 0V8.5H2.75a.75.75 0 0 1 0-1.5H7V2.75A.75.75 0 0 1 7.75 2Z" />
|
|
116
|
+
</svg>
|
|
117
|
+
</button>
|
|
61
118
|
</div>
|
|
62
119
|
)
|
|
63
120
|
}
|
|
@@ -50,6 +50,35 @@
|
|
|
50
50
|
cursor: default;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
.zoomLevel {
|
|
54
|
+
all: unset;
|
|
55
|
+
cursor: pointer;
|
|
56
|
+
display: flex;
|
|
57
|
+
align-items: center;
|
|
58
|
+
justify-content: center;
|
|
59
|
+
min-width: 44px;
|
|
60
|
+
height: 32px;
|
|
61
|
+
padding: 0 4px;
|
|
62
|
+
border-radius: 8px;
|
|
63
|
+
font-size: 12px;
|
|
64
|
+
font-weight: 500;
|
|
65
|
+
font-variant-numeric: tabular-nums;
|
|
66
|
+
color: var(--fgColor-muted, #656d76);
|
|
67
|
+
transition: background 120ms;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.zoomLevel:hover {
|
|
71
|
+
background: var(--bgColor-muted, #f6f8fa);
|
|
72
|
+
color: var(--fgColor-default, #1f2328);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.divider {
|
|
76
|
+
width: 1px;
|
|
77
|
+
height: 20px;
|
|
78
|
+
margin: 0 2px;
|
|
79
|
+
background: var(--borderColor-muted, #d8dee4);
|
|
80
|
+
}
|
|
81
|
+
|
|
53
82
|
/* Create widget menu */
|
|
54
83
|
.createGroup {
|
|
55
84
|
position: relative;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fireEvent, render, screen, waitFor
|
|
1
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
2
2
|
import CanvasPage from './CanvasPage.jsx'
|
|
3
3
|
import { getCanvasPrimerAttrs, getCanvasThemeVars } from './canvasTheme.js'
|
|
4
4
|
import { updateCanvas } from './canvasApi.js'
|
|
@@ -54,42 +54,15 @@ vi.mock('./widgets/index.js', () => ({
|
|
|
54
54
|
getWidgetComponent: () => function MockWidget() { return <div>mock widget</div> },
|
|
55
55
|
}))
|
|
56
56
|
|
|
57
|
-
vi.mock('./widgets/WidgetChrome.jsx', () => ({
|
|
58
|
-
default: ({ children }) => <div data-testid="widget-chrome">{children}</div>,
|
|
59
|
-
}))
|
|
60
|
-
|
|
61
57
|
vi.mock('./widgets/widgetProps.js', () => ({
|
|
62
58
|
schemas: {},
|
|
63
59
|
getDefaults: () => ({}),
|
|
64
60
|
}))
|
|
65
61
|
|
|
66
|
-
vi.mock('./widgets/widgetConfig.js', () => ({
|
|
67
|
-
getFeatures: () => [],
|
|
68
|
-
schemas: {},
|
|
69
|
-
getMenuWidgetTypes: () => [],
|
|
70
|
-
}))
|
|
71
|
-
|
|
72
|
-
vi.mock('./widgets/figmaUrl.js', () => ({
|
|
73
|
-
isFigmaUrl: () => false,
|
|
74
|
-
sanitizeFigmaUrl: (url) => url,
|
|
75
|
-
}))
|
|
76
|
-
|
|
77
62
|
vi.mock('./canvasApi.js', () => ({
|
|
78
63
|
addWidget: vi.fn(),
|
|
79
64
|
updateCanvas: vi.fn(() => Promise.resolve({ success: true })),
|
|
80
65
|
removeWidget: vi.fn(),
|
|
81
|
-
uploadImage: vi.fn(),
|
|
82
|
-
}))
|
|
83
|
-
|
|
84
|
-
vi.mock('./useUndoRedo.js', () => ({
|
|
85
|
-
default: () => ({
|
|
86
|
-
snapshot: vi.fn(),
|
|
87
|
-
undo: vi.fn(),
|
|
88
|
-
redo: vi.fn(),
|
|
89
|
-
reset: vi.fn(),
|
|
90
|
-
canUndo: false,
|
|
91
|
-
canRedo: false,
|
|
92
|
-
}),
|
|
93
66
|
}))
|
|
94
67
|
|
|
95
68
|
describe('CanvasPage canvas bridge', () => {
|
|
@@ -144,55 +117,57 @@ describe('CanvasPage canvas bridge', () => {
|
|
|
144
117
|
document.removeEventListener('storyboard:canvas:unmounted', unmountedHandler)
|
|
145
118
|
})
|
|
146
119
|
|
|
147
|
-
it
|
|
120
|
+
it('persists dragged JSON widgets and JSX sources to canvas JSONL via update API', async () => {
|
|
148
121
|
render(<CanvasPage name="design-overview" />)
|
|
149
122
|
|
|
150
123
|
fireEvent.click(screen.getByTestId('drag-widget'))
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
)
|
|
124
|
+
await waitFor(() => {
|
|
125
|
+
expect(updateCanvas).toHaveBeenCalledWith(
|
|
126
|
+
'design-overview',
|
|
127
|
+
expect.objectContaining({
|
|
128
|
+
widgets: expect.arrayContaining([
|
|
129
|
+
expect.objectContaining({
|
|
130
|
+
id: 'widget-1',
|
|
131
|
+
position: { x: 111, y: 223 },
|
|
132
|
+
}),
|
|
133
|
+
]),
|
|
134
|
+
})
|
|
135
|
+
)
|
|
136
|
+
})
|
|
164
137
|
|
|
165
138
|
fireEvent.click(screen.getByTestId('drag-source'))
|
|
166
|
-
await
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
139
|
+
await waitFor(() => {
|
|
140
|
+
expect(updateCanvas).toHaveBeenCalledWith(
|
|
141
|
+
'design-overview',
|
|
142
|
+
expect.objectContaining({
|
|
143
|
+
sources: expect.arrayContaining([
|
|
144
|
+
expect.objectContaining({
|
|
145
|
+
export: 'PrimaryButtons',
|
|
146
|
+
position: { x: 333, y: 445 },
|
|
147
|
+
}),
|
|
148
|
+
]),
|
|
149
|
+
})
|
|
150
|
+
)
|
|
151
|
+
})
|
|
178
152
|
})
|
|
179
153
|
|
|
180
|
-
it
|
|
154
|
+
it('clamps negative drag positions to zero', async () => {
|
|
181
155
|
render(<CanvasPage name="design-overview" />)
|
|
182
156
|
|
|
183
157
|
fireEvent.click(screen.getByTestId('drag-widget-negative'))
|
|
184
|
-
await
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
158
|
+
await waitFor(() => {
|
|
159
|
+
expect(updateCanvas).toHaveBeenCalledWith(
|
|
160
|
+
'design-overview',
|
|
161
|
+
expect.objectContaining({
|
|
162
|
+
widgets: expect.arrayContaining([
|
|
163
|
+
expect.objectContaining({
|
|
164
|
+
id: 'widget-1',
|
|
165
|
+
position: { x: 0, y: 0 },
|
|
166
|
+
}),
|
|
167
|
+
]),
|
|
168
|
+
})
|
|
169
|
+
)
|
|
170
|
+
})
|
|
196
171
|
})
|
|
197
172
|
})
|
|
198
173
|
|