@dfosco/storyboard-react 4.0.0-beta.23 → 4.0.0-beta.25
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/CanvasPage.bridge.test.jsx +8 -8
- package/src/canvas/CanvasPage.dragdrop.test.jsx +8 -8
- package/src/canvas/CanvasPage.jsx +77 -77
- package/src/canvas/CanvasPage.multiselect.test.jsx +11 -11
- package/src/canvas/CanvasToolbar.jsx +2 -2
- package/src/canvas/canvasApi.js +12 -10
- package/src/canvas/useCanvas.js +9 -8
- package/src/canvas/widgets/ComponentWidget.jsx +21 -6
- package/src/canvas/widgets/ComponentWidget.module.css +5 -0
- package/src/canvas/widgets/FigmaEmbed.jsx +44 -15
- package/src/canvas/widgets/PrototypeEmbed.jsx +217 -208
- package/src/canvas/widgets/PrototypeEmbed.module.css +61 -19
- package/src/canvas/widgets/StoryWidget.jsx +142 -171
- package/src/canvas/widgets/StoryWidget.module.css +38 -28
- package/src/canvas/widgets/WidgetChrome.jsx +3 -2
- package/src/canvas/widgets/embedInteraction.test.jsx +86 -4
- package/src/canvas/widgets/embedTheme.js +20 -0
- package/src/canvas/widgets/iframeDevLogs.js +49 -0
- package/src/canvas/widgets/iframeDevLogs.test.jsx +81 -0
- package/src/canvas/widgets/snapshotDisplay.test.jsx +289 -0
- package/src/canvas/widgets/useSnapshotCapture.js +221 -0
- package/src/canvas/widgets/useSnapshotCapture.test.jsx +149 -0
- package/src/context.jsx +14 -14
- package/src/story/StoryPage.jsx +7 -7
- package/src/vite/data-plugin.js +25 -20
- package/src/vite/data-plugin.test.js +4 -4
- package/src/canvas/widgets/useViewportEntry.js +0 -93
|
@@ -123,7 +123,7 @@ describe('CanvasPage multi-select', () => {
|
|
|
123
123
|
})
|
|
124
124
|
|
|
125
125
|
it('shift+click on select handle adds widget to selection', async () => {
|
|
126
|
-
render(<CanvasPage
|
|
126
|
+
render(<CanvasPage canvasId="test-canvas" />)
|
|
127
127
|
|
|
128
128
|
// Select first widget
|
|
129
129
|
fireEvent.click(screen.getByTestId('select-w1'))
|
|
@@ -138,7 +138,7 @@ describe('CanvasPage multi-select', () => {
|
|
|
138
138
|
})
|
|
139
139
|
|
|
140
140
|
it('shift+click on already selected widget removes it from selection', async () => {
|
|
141
|
-
render(<CanvasPage
|
|
141
|
+
render(<CanvasPage canvasId="test-canvas" />)
|
|
142
142
|
|
|
143
143
|
// Select both
|
|
144
144
|
fireEvent.click(screen.getByTestId('select-w1'))
|
|
@@ -155,7 +155,7 @@ describe('CanvasPage multi-select', () => {
|
|
|
155
155
|
})
|
|
156
156
|
|
|
157
157
|
it('normal click replaces multi-selection with single', async () => {
|
|
158
|
-
render(<CanvasPage
|
|
158
|
+
render(<CanvasPage canvasId="test-canvas" />)
|
|
159
159
|
|
|
160
160
|
// Multi-select
|
|
161
161
|
fireEvent.click(screen.getByTestId('select-w1'))
|
|
@@ -172,7 +172,7 @@ describe('CanvasPage multi-select', () => {
|
|
|
172
172
|
})
|
|
173
173
|
|
|
174
174
|
it('sets multiSelected on all selected widgets when multiple are selected', async () => {
|
|
175
|
-
render(<CanvasPage
|
|
175
|
+
render(<CanvasPage canvasId="test-canvas" />)
|
|
176
176
|
|
|
177
177
|
fireEvent.click(screen.getByTestId('select-w1'))
|
|
178
178
|
fireEvent.click(screen.getByTestId('shift-select-w2'))
|
|
@@ -185,7 +185,7 @@ describe('CanvasPage multi-select', () => {
|
|
|
185
185
|
})
|
|
186
186
|
|
|
187
187
|
it('Escape clears all selection', async () => {
|
|
188
|
-
render(<CanvasPage
|
|
188
|
+
render(<CanvasPage canvasId="test-canvas" />)
|
|
189
189
|
|
|
190
190
|
fireEvent.click(screen.getByTestId('select-w1'))
|
|
191
191
|
fireEvent.click(screen.getByTestId('shift-select-w2'))
|
|
@@ -199,7 +199,7 @@ describe('CanvasPage multi-select', () => {
|
|
|
199
199
|
})
|
|
200
200
|
|
|
201
201
|
it('Delete removes all selected widgets and calls updateCanvas', async () => {
|
|
202
|
-
render(<CanvasPage
|
|
202
|
+
render(<CanvasPage canvasId="test-canvas" />)
|
|
203
203
|
|
|
204
204
|
// Multi-select w1 and w2
|
|
205
205
|
fireEvent.click(screen.getByTestId('select-w1'))
|
|
@@ -224,7 +224,7 @@ describe('CanvasPage multi-select', () => {
|
|
|
224
224
|
})
|
|
225
225
|
|
|
226
226
|
it('single-select Delete uses removeWidget API', async () => {
|
|
227
|
-
render(<CanvasPage
|
|
227
|
+
render(<CanvasPage canvasId="test-canvas" />)
|
|
228
228
|
|
|
229
229
|
fireEvent.click(screen.getByTestId('select-w1'))
|
|
230
230
|
fireEvent.keyDown(document, { key: 'Backspace' })
|
|
@@ -234,7 +234,7 @@ describe('CanvasPage multi-select', () => {
|
|
|
234
234
|
})
|
|
235
235
|
|
|
236
236
|
it('multi-select move applies delta to all selected widgets', async () => {
|
|
237
|
-
render(<CanvasPage
|
|
237
|
+
render(<CanvasPage canvasId="test-canvas" />)
|
|
238
238
|
|
|
239
239
|
// Multi-select w1 (100,100) and w2 (300,100)
|
|
240
240
|
fireEvent.click(screen.getByTestId('select-w1'))
|
|
@@ -265,7 +265,7 @@ describe('CanvasPage multi-select', () => {
|
|
|
265
265
|
})
|
|
266
266
|
|
|
267
267
|
it('multi-select drag captures peer articles on drag start', async () => {
|
|
268
|
-
render(<CanvasPage
|
|
268
|
+
render(<CanvasPage canvasId="test-canvas" />)
|
|
269
269
|
|
|
270
270
|
// Multi-select w1 and w2
|
|
271
271
|
fireEvent.click(screen.getByTestId('select-w1'))
|
|
@@ -288,7 +288,7 @@ describe('CanvasPage multi-select', () => {
|
|
|
288
288
|
})
|
|
289
289
|
|
|
290
290
|
it('multi-select drag preserves selection after drag end', async () => {
|
|
291
|
-
render(<CanvasPage
|
|
291
|
+
render(<CanvasPage canvasId="test-canvas" />)
|
|
292
292
|
|
|
293
293
|
// Multi-select w1 and w2
|
|
294
294
|
fireEvent.click(screen.getByTestId('select-w1'))
|
|
@@ -313,7 +313,7 @@ describe('CanvasPage multi-select', () => {
|
|
|
313
313
|
})
|
|
314
314
|
|
|
315
315
|
it('any selected widget can serve as drag handler for the group', async () => {
|
|
316
|
-
render(<CanvasPage
|
|
316
|
+
render(<CanvasPage canvasId="test-canvas" />)
|
|
317
317
|
|
|
318
318
|
// Multi-select w1 (100,100), w2 (300,100), w3 (500,200)
|
|
319
319
|
fireEvent.click(screen.getByTestId('select-w1'))
|
|
@@ -9,7 +9,7 @@ const WIDGET_TYPES = getMenuWidgetTypes()
|
|
|
9
9
|
/**
|
|
10
10
|
* Floating toolbar for adding widgets to a canvas.
|
|
11
11
|
*/
|
|
12
|
-
export default function CanvasToolbar({
|
|
12
|
+
export default function CanvasToolbar({ canvasId, onWidgetAdded }) {
|
|
13
13
|
const [open, setOpen] = useState(false)
|
|
14
14
|
const [adding, setAdding] = useState(false)
|
|
15
15
|
|
|
@@ -18,7 +18,7 @@ export default function CanvasToolbar({ canvasName, onWidgetAdded }) {
|
|
|
18
18
|
setAdding(true)
|
|
19
19
|
try {
|
|
20
20
|
const defaultProps = schemas[type] ? getDefaults(schemas[type]) : {}
|
|
21
|
-
const result = await addWidgetApi(
|
|
21
|
+
const result = await addWidgetApi(canvasId, {
|
|
22
22
|
type,
|
|
23
23
|
props: defaultProps,
|
|
24
24
|
position: { x: 0, y: 0 },
|
package/src/canvas/canvasApi.js
CHANGED
|
@@ -28,26 +28,28 @@ export function createCanvas(data) {
|
|
|
28
28
|
return request('/create', 'POST', data)
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
export function updateCanvas(
|
|
32
|
-
return request('/update', 'PUT', { name, widgets, sources, settings })
|
|
31
|
+
export function updateCanvas(canvasId, { widgets, sources, settings }) {
|
|
32
|
+
return request('/update', 'PUT', { name: canvasId, widgets, sources, settings })
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
export function addWidget(
|
|
36
|
-
return request('/widget', 'POST', { name, type, props, position })
|
|
35
|
+
export function addWidget(canvasId, { type, props, position }) {
|
|
36
|
+
return request('/widget', 'POST', { name: canvasId, type, props, position })
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
export function removeWidget(
|
|
40
|
-
return request('/widget', 'DELETE', { name, widgetId })
|
|
39
|
+
export function removeWidget(canvasId, widgetId) {
|
|
40
|
+
return request('/widget', 'DELETE', { name: canvasId, widgetId })
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
export function uploadImage(dataUrl,
|
|
44
|
-
|
|
43
|
+
export function uploadImage(dataUrl, canvasId, filename) {
|
|
44
|
+
const body = { dataUrl, canvasName: canvasId }
|
|
45
|
+
if (filename) body.filename = filename
|
|
46
|
+
return request('/image', 'POST', body)
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
export function toggleImagePrivacy(filename) {
|
|
48
50
|
return request('/image/toggle-private', 'POST', { filename })
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
export function getCanvas(
|
|
52
|
-
return request(`/read?name=${encodeURIComponent(
|
|
53
|
+
export function getCanvas(canvasId) {
|
|
54
|
+
return request(`/read?name=${encodeURIComponent(canvasId)}`, 'GET')
|
|
53
55
|
}
|
package/src/canvas/useCanvas.js
CHANGED
|
@@ -31,11 +31,11 @@ export function resolveCanvasModuleImport(modulePath, baseUrl = import.meta.env?
|
|
|
31
31
|
* Uses build-time data for static config (routes, JSX path), but fetches
|
|
32
32
|
* fresh widget data from the server to pick up persisted edits.
|
|
33
33
|
*
|
|
34
|
-
* @param {string}
|
|
34
|
+
* @param {string} canvasId - Canonical canvas ID as indexed by the data plugin
|
|
35
35
|
* @returns {{ canvas: object|null, jsxExports: object|null, jsxError: boolean, loading: boolean }}
|
|
36
36
|
*/
|
|
37
|
-
export function useCanvas(
|
|
38
|
-
const buildTimeCanvas = useMemo(() => getCanvasData(
|
|
37
|
+
export function useCanvas(canvasId) {
|
|
38
|
+
const buildTimeCanvas = useMemo(() => getCanvasData(canvasId), [canvasId])
|
|
39
39
|
const [canvas, setCanvas] = useState(buildTimeCanvas)
|
|
40
40
|
const [jsxExports, setJsxExports] = useState(null)
|
|
41
41
|
const [jsxError, setJsxError] = useState(false)
|
|
@@ -50,7 +50,7 @@ export function useCanvas(name) {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
setLoading(true)
|
|
53
|
-
fetchCanvasFromServer(
|
|
53
|
+
fetchCanvasFromServer(canvasId).then((fresh) => {
|
|
54
54
|
if (fresh) {
|
|
55
55
|
// Merge: use server data for widgets/sources, keep build-time for _route/_jsxModule
|
|
56
56
|
setCanvas({ ...buildTimeCanvas, ...fresh })
|
|
@@ -59,7 +59,7 @@ export function useCanvas(name) {
|
|
|
59
59
|
}
|
|
60
60
|
setLoading(false)
|
|
61
61
|
})
|
|
62
|
-
}, [
|
|
62
|
+
}, [canvasId, buildTimeCanvas])
|
|
63
63
|
|
|
64
64
|
const jsxModule = canvas?._jsxModule
|
|
65
65
|
const jsxImport = canvas?._jsxImport
|
|
@@ -99,8 +99,9 @@ export function useCanvas(name) {
|
|
|
99
99
|
if (!import.meta.hot || !buildTimeCanvas) return
|
|
100
100
|
|
|
101
101
|
const handleCanvasFileChanged = ({ data }) => {
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
const eventId = data?.canvasId || data?.name
|
|
103
|
+
if (!data || eventId !== canvasId) return
|
|
104
|
+
fetchCanvasFromServer(canvasId).then((fresh) => {
|
|
104
105
|
if (fresh) {
|
|
105
106
|
setCanvas((prev) => ({ ...(prev || buildTimeCanvas), ...fresh }))
|
|
106
107
|
}
|
|
@@ -111,7 +112,7 @@ export function useCanvas(name) {
|
|
|
111
112
|
return () => {
|
|
112
113
|
import.meta.hot.off('storyboard:canvas-file-changed', handleCanvasFileChanged)
|
|
113
114
|
}
|
|
114
|
-
}, [
|
|
115
|
+
}, [canvasId, buildTimeCanvas])
|
|
115
116
|
|
|
116
117
|
return { canvas, jsxExports, jsxError, loading }
|
|
117
118
|
}
|
|
@@ -2,6 +2,7 @@ import { useRef, useCallback, useState, useEffect, useMemo } from 'react'
|
|
|
2
2
|
import WidgetWrapper from './WidgetWrapper.jsx'
|
|
3
3
|
import ResizeHandle from './ResizeHandle.jsx'
|
|
4
4
|
import ComponentErrorBoundary from '../ComponentErrorBoundary.jsx'
|
|
5
|
+
import { useIframeDevLogs } from './iframeDevLogs.js'
|
|
5
6
|
import styles from './ComponentWidget.module.css'
|
|
6
7
|
import overlayStyles from './embedOverlay.module.css'
|
|
7
8
|
|
|
@@ -31,6 +32,7 @@ export default function ComponentWidget({
|
|
|
31
32
|
}) {
|
|
32
33
|
const containerRef = useRef(null)
|
|
33
34
|
const [interactive, setInteractive] = useState(false)
|
|
35
|
+
const [showIframe, setShowIframe] = useState(false)
|
|
34
36
|
|
|
35
37
|
const handleResize = useCallback((w, h) => {
|
|
36
38
|
onUpdate?.({ width: w, height: h })
|
|
@@ -44,6 +46,7 @@ export default function ComponentWidget({
|
|
|
44
46
|
function handlePointerDown(e) {
|
|
45
47
|
if (containerRef.current && !containerRef.current.contains(e.target)) {
|
|
46
48
|
setInteractive(false)
|
|
49
|
+
setShowIframe(false)
|
|
47
50
|
}
|
|
48
51
|
}
|
|
49
52
|
document.addEventListener('pointerdown', handlePointerDown)
|
|
@@ -64,6 +67,12 @@ export default function ComponentWidget({
|
|
|
64
67
|
|
|
65
68
|
const useIframe = isLocalDev && iframeSrc
|
|
66
69
|
|
|
70
|
+
useIframeDevLogs({
|
|
71
|
+
widget: 'ComponentWidget',
|
|
72
|
+
loaded: Boolean(useIframe && showIframe),
|
|
73
|
+
src: iframeSrc,
|
|
74
|
+
})
|
|
75
|
+
|
|
67
76
|
if (!useIframe && !Component) return null
|
|
68
77
|
|
|
69
78
|
const sizeStyle = {}
|
|
@@ -75,12 +84,16 @@ export default function ComponentWidget({
|
|
|
75
84
|
<div ref={containerRef} className={styles.container} style={sizeStyle}>
|
|
76
85
|
<div className={styles.content}>
|
|
77
86
|
{useIframe ? (
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
showIframe ? (
|
|
88
|
+
<iframe
|
|
89
|
+
src={iframeSrc}
|
|
90
|
+
className={styles.iframe}
|
|
91
|
+
title={exportName || 'Component widget'}
|
|
92
|
+
sandbox="allow-same-origin allow-scripts"
|
|
93
|
+
/>
|
|
94
|
+
) : (
|
|
95
|
+
<div className={styles.placeholder} />
|
|
96
|
+
)
|
|
84
97
|
) : Component ? (
|
|
85
98
|
<ComponentErrorBoundary name={exportName}>
|
|
86
99
|
<Component />
|
|
@@ -93,6 +106,7 @@ export default function ComponentWidget({
|
|
|
93
106
|
onClick={(e) => {
|
|
94
107
|
// Don't enter interactive mode for modifier clicks (shift/meta/ctrl for multi-select)
|
|
95
108
|
if (e.shiftKey || e.metaKey || e.ctrlKey || e.altKey) return
|
|
109
|
+
if (useIframe) setShowIframe(true)
|
|
96
110
|
enterInteractive()
|
|
97
111
|
}}
|
|
98
112
|
role="button"
|
|
@@ -101,6 +115,7 @@ export default function ComponentWidget({
|
|
|
101
115
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
102
116
|
e.preventDefault()
|
|
103
117
|
e.stopPropagation()
|
|
118
|
+
if (useIframe) setShowIframe(true)
|
|
104
119
|
enterInteractive()
|
|
105
120
|
}
|
|
106
121
|
}}
|
|
@@ -4,6 +4,7 @@ import WidgetWrapper from './WidgetWrapper.jsx'
|
|
|
4
4
|
import { readProp } from './widgetProps.js'
|
|
5
5
|
import { schemas } from './widgetConfig.js'
|
|
6
6
|
import { toFigmaEmbedUrl, getFigmaTitle, getFigmaType, isFigmaUrl } from './figmaUrl.js'
|
|
7
|
+
import { useIframeDevLogs } from './iframeDevLogs.js'
|
|
7
8
|
import styles from './FigmaEmbed.module.css'
|
|
8
9
|
import overlayStyles from './embedOverlay.module.css'
|
|
9
10
|
|
|
@@ -30,9 +31,11 @@ export default forwardRef(function FigmaEmbed({ props, onUpdate, resizable }, re
|
|
|
30
31
|
const height = readProp(props, 'height', figmaEmbedSchema)
|
|
31
32
|
|
|
32
33
|
const [interactive, setInteractive] = useState(false)
|
|
34
|
+
const [showIframe, setShowIframe] = useState(false)
|
|
33
35
|
const [expanded, setExpanded] = useState(false)
|
|
34
36
|
|
|
35
37
|
const iframeRef = useRef(null)
|
|
38
|
+
const embedRef = useRef(null)
|
|
36
39
|
const inlineContainerRef = useRef(null)
|
|
37
40
|
const modalContainerRef = useRef(null)
|
|
38
41
|
|
|
@@ -43,7 +46,28 @@ export default forwardRef(function FigmaEmbed({ props, onUpdate, resizable }, re
|
|
|
43
46
|
const figmaType = useMemo(() => getFigmaType(url), [url])
|
|
44
47
|
const typeLabel = figmaType ? TYPE_LABELS[figmaType] : 'Figma'
|
|
45
48
|
|
|
46
|
-
|
|
49
|
+
useIframeDevLogs({
|
|
50
|
+
widget: 'FigmaEmbed',
|
|
51
|
+
loaded: showIframe && Boolean(embedUrl),
|
|
52
|
+
src: embedUrl,
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const enterInteractive = useCallback(() => {
|
|
56
|
+
setShowIframe(true)
|
|
57
|
+
setInteractive(true)
|
|
58
|
+
}, [])
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (!interactive || expanded) return
|
|
62
|
+
function handlePointerDown(e) {
|
|
63
|
+
if (embedRef.current && !embedRef.current.contains(e.target)) {
|
|
64
|
+
setInteractive(false)
|
|
65
|
+
setShowIframe(false)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
document.addEventListener('pointerdown', handlePointerDown)
|
|
69
|
+
return () => document.removeEventListener('pointerdown', handlePointerDown)
|
|
70
|
+
}, [interactive, expanded])
|
|
47
71
|
|
|
48
72
|
// Close expanded modal on Escape
|
|
49
73
|
useEffect(() => {
|
|
@@ -97,6 +121,7 @@ export default forwardRef(function FigmaEmbed({ props, onUpdate, resizable }, re
|
|
|
97
121
|
if (actionId === 'open-external') {
|
|
98
122
|
if (url) window.open(url, '_blank', 'noopener')
|
|
99
123
|
} else if (actionId === 'expand') {
|
|
124
|
+
setShowIframe(true)
|
|
100
125
|
setExpanded(true)
|
|
101
126
|
}
|
|
102
127
|
},
|
|
@@ -105,26 +130,30 @@ export default forwardRef(function FigmaEmbed({ props, onUpdate, resizable }, re
|
|
|
105
130
|
return (
|
|
106
131
|
<>
|
|
107
132
|
<WidgetWrapper>
|
|
108
|
-
<div className={styles.embed} style={{ width, height }}>
|
|
133
|
+
<div ref={embedRef} className={styles.embed} style={{ width, height }}>
|
|
109
134
|
<div className={styles.header}>
|
|
110
135
|
<FigmaLogo />
|
|
111
136
|
<span className={styles.headerTitle}>{title}</span>
|
|
112
137
|
</div>
|
|
113
138
|
{embedUrl ? (
|
|
114
139
|
<>
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
140
|
+
{showIframe ? (
|
|
141
|
+
<div
|
|
142
|
+
ref={inlineContainerRef}
|
|
143
|
+
className={styles.iframeContainer}
|
|
144
|
+
style={expanded ? { visibility: 'hidden' } : undefined}
|
|
145
|
+
>
|
|
146
|
+
<iframe
|
|
147
|
+
ref={iframeRef}
|
|
148
|
+
src={embedUrl}
|
|
149
|
+
className={styles.iframe}
|
|
150
|
+
title={`Figma ${typeLabel}: ${title}`}
|
|
151
|
+
allowFullScreen
|
|
152
|
+
/>
|
|
153
|
+
</div>
|
|
154
|
+
) : (
|
|
155
|
+
<div className={styles.iframeContainer} />
|
|
156
|
+
)}
|
|
128
157
|
{!interactive && !expanded && (
|
|
129
158
|
<div
|
|
130
159
|
className={overlayStyles.interactOverlay}
|