@dfosco/storyboard-react 4.0.0-beta.24 → 4.0.0-beta.26
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 +170 -103
- package/src/canvas/CanvasPage.module.css +7 -0
- 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 +210 -208
- package/src/canvas/widgets/PrototypeEmbed.module.css +61 -19
- package/src/canvas/widgets/StoryWidget.jsx +135 -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 +258 -0
- package/src/canvas/widgets/useSnapshotCapture.test.jsx +225 -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
|
@@ -12,17 +12,25 @@
|
|
|
12
12
|
* Props: { storyId, exportName, width, height }
|
|
13
13
|
*/
|
|
14
14
|
import { forwardRef, useImperativeHandle, useRef, useCallback, useState, useEffect, useMemo } from 'react'
|
|
15
|
-
import { getStoryData
|
|
15
|
+
import { getStoryData } from '@dfosco/storyboard-core'
|
|
16
16
|
import { createInspectorHighlighter } from '@dfosco/storyboard-core/inspector/highlighter'
|
|
17
17
|
import WidgetWrapper from './WidgetWrapper.jsx'
|
|
18
18
|
import ResizeHandle from './ResizeHandle.jsx'
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
19
|
+
import { useIframeDevLogs } from './iframeDevLogs.js'
|
|
20
|
+
import { useSnapshotCapture } from './useSnapshotCapture.js'
|
|
21
|
+
import { resolveCanvasTheme } from './embedTheme.js'
|
|
21
22
|
import styles from './StoryWidget.module.css'
|
|
22
23
|
import overlayStyles from './embedOverlay.module.css'
|
|
23
24
|
|
|
24
|
-
function
|
|
25
|
-
|
|
25
|
+
function ComponentIcon({ size = 36 }) {
|
|
26
|
+
return (
|
|
27
|
+
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden="true">
|
|
28
|
+
<path d="M5.21173 15.1113L2.52473 12.4243C2.29041 12.1899 2.29041 11.8101 2.52473 11.5757L5.21173 8.88873C5.44605 8.65442 5.82595 8.65442 6.06026 8.88873L8.74727 11.5757C8.98158 11.8101 8.98158 12.1899 8.74727 12.4243L6.06026 15.1113C5.82595 15.3456 5.44605 15.3456 5.21173 15.1113Z" />
|
|
29
|
+
<path d="M11.5757 21.475L8.88874 18.788C8.65443 18.5537 8.65443 18.1738 8.88874 17.9395L11.5757 15.2525C11.8101 15.0182 12.19 15.0182 12.4243 15.2525L15.1113 17.9395C15.3456 18.1738 15.3456 18.5537 15.1113 18.788L12.4243 21.475C12.19 21.7094 11.8101 21.7094 11.5757 21.475Z" />
|
|
30
|
+
<path d="M11.5757 8.7475L8.88874 6.06049C8.65443 5.82618 8.65443 5.44628 8.88874 5.21197L11.5757 2.52496C11.8101 2.29065 12.19 2.29065 12.4243 2.52496L15.1113 5.21197C15.3456 5.44628 15.3456 5.82618 15.1113 6.06049L12.4243 8.7475C12.19 8.98181 11.8101 8.98181 11.5757 8.7475Z" />
|
|
31
|
+
<path d="M17.9396 15.1113L15.2526 12.4243C15.0183 12.1899 15.0183 11.8101 15.2526 11.5757L17.9396 8.88873C18.174 8.65442 18.5539 8.65442 18.7882 8.88873L21.4752 11.5757C21.7095 11.8101 21.7095 12.1899 21.4752 12.4243L18.7882 15.1113C18.5539 15.3456 18.174 15.3456 17.9396 15.1113Z" />
|
|
32
|
+
</svg>
|
|
33
|
+
)
|
|
26
34
|
}
|
|
27
35
|
|
|
28
36
|
function resolveStoryUrl(storyId, exportName) {
|
|
@@ -78,89 +86,46 @@ export default forwardRef(function StoryWidget({ id: widgetId, props, onUpdate,
|
|
|
78
86
|
const exportName = props?.exportName || ''
|
|
79
87
|
const width = props?.width
|
|
80
88
|
const height = props?.height
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const snapshotLight = props?.snapshotLight || null
|
|
84
|
-
const snapshotDark = props?.snapshotDark || null
|
|
89
|
+
const snapshotLight = props?.snapshotLight || ''
|
|
90
|
+
const snapshotDark = props?.snapshotDark || ''
|
|
85
91
|
|
|
86
92
|
const containerRef = useRef(null)
|
|
87
93
|
const iframeRef = useRef(null)
|
|
94
|
+
const resizeTimerRef = useRef(null)
|
|
95
|
+
const captureOnReadyRef = useRef(false)
|
|
96
|
+
const exitSessionRef = useRef(0)
|
|
88
97
|
const [interactive, setInteractive] = useState(false)
|
|
98
|
+
const [showIframe, setShowIframe] = useState(false)
|
|
99
|
+
const [iframeLoaded, setIframeLoaded] = useState(false)
|
|
89
100
|
const [showCode, setShowCode] = useState(!!props?.showCode)
|
|
90
101
|
const [sourceCode, setSourceCode] = useState(null)
|
|
91
102
|
const [highlightedHtml, setHighlightedHtml] = useState(null)
|
|
92
103
|
const [sourceLoading, setSourceLoading] = useState(false)
|
|
104
|
+
const [storyIndexKey, setStoryIndexKey] = useState(0)
|
|
105
|
+
const [brokenSnaps, setBrokenSnaps] = useState({})
|
|
93
106
|
|
|
94
|
-
//
|
|
95
|
-
const [canvasTheme, setCanvasTheme] = useState(() =>
|
|
96
|
-
if (typeof localStorage === 'undefined') return 'light'
|
|
97
|
-
const stored = localStorage.getItem('sb-color-scheme') || 'system'
|
|
98
|
-
if (stored !== 'system') return stored
|
|
99
|
-
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
|
100
|
-
})
|
|
107
|
+
// Resolve canvas theme — reactive to theme changes
|
|
108
|
+
const [canvasTheme, setCanvasTheme] = useState(() => resolveCanvasTheme())
|
|
101
109
|
|
|
102
110
|
useEffect(() => {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
setCanvasTheme(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
|
|
107
|
-
}
|
|
108
|
-
document.addEventListener('storyboard:theme:changed', onThemeChanged)
|
|
109
|
-
return () => document.removeEventListener('storyboard:theme:changed', onThemeChanged)
|
|
111
|
+
const readTheme = () => setCanvasTheme(resolveCanvasTheme())
|
|
112
|
+
document.addEventListener('storyboard:theme:changed', readTheme)
|
|
113
|
+
return () => document.removeEventListener('storyboard:theme:changed', readTheme)
|
|
110
114
|
}, [])
|
|
111
115
|
|
|
112
|
-
//
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
// Sequential iframe queue — prevents stampede when many embeds lack snapshots.
|
|
121
|
-
const { ready: queueReady, releaseSlot } = useIframeQueue(hasSnapshot, widgetId)
|
|
122
|
-
const [preloadIframe, setPreloadIframe] = useState(hasSnapshot)
|
|
123
|
-
const [iframeLoaded, setIframeLoaded] = useState(false)
|
|
124
|
-
const [showIframe, setShowIframe] = useState(hasSnapshot)
|
|
125
|
-
const [showSpinner, setShowSpinner] = useState(false)
|
|
126
|
-
const capturingRef = useRef(false)
|
|
127
|
-
|
|
128
|
-
devLog(widgetId, { hasSnapshot, queueReady, preloadIframe, showIframe, iframeLoaded, storyId })
|
|
129
|
-
|
|
130
|
-
// Start loading when the queue grants this widget a slot
|
|
131
|
-
useEffect(() => {
|
|
132
|
-
if (queueReady && !preloadIframe) {
|
|
133
|
-
devLog(widgetId, 'queue ready → loading iframe')
|
|
134
|
-
setPreloadIframe(true)
|
|
135
|
-
setShowIframe(true)
|
|
136
|
-
}
|
|
137
|
-
}, [queueReady, preloadIframe])
|
|
138
|
-
|
|
139
|
-
// Release the queue slot once the iframe has loaded or user clicked to interact
|
|
140
|
-
useEffect(() => {
|
|
141
|
-
if (iframeLoaded) {
|
|
142
|
-
devLog(widgetId, 'iframe loaded')
|
|
143
|
-
releaseSlot()
|
|
144
|
-
}
|
|
145
|
-
}, [iframeLoaded, releaseSlot])
|
|
146
|
-
|
|
147
|
-
// Click-to-interact: immediately start iframe and release queue slot for others
|
|
148
|
-
const activateIframe = useCallback(() => {
|
|
149
|
-
devLog(widgetId, 'user activated → jumping queue')
|
|
150
|
-
setShowIframe(true)
|
|
151
|
-
setPreloadIframe(true)
|
|
152
|
-
releaseSlot()
|
|
153
|
-
}, [releaseSlot, widgetId])
|
|
116
|
+
// Snapshot capture hook
|
|
117
|
+
const { iframeReady, requestCapture } = useSnapshotCapture({
|
|
118
|
+
iframeRef,
|
|
119
|
+
widgetId,
|
|
120
|
+
onUpdate,
|
|
121
|
+
canvasTheme,
|
|
122
|
+
})
|
|
154
123
|
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
setShowSpinner(false)
|
|
162
|
-
}, [showIframe, iframeLoaded, hasSnapshot])
|
|
163
|
-
const [storyIndexKey, setStoryIndexKey] = useState(0)
|
|
124
|
+
// Determine available snapshots for layered rendering
|
|
125
|
+
const isDark = canvasTheme?.startsWith('dark')
|
|
126
|
+
const hasLightSnap = !!(snapshotLight && snapshotLight.includes(widgetId) && !brokenSnaps[snapshotLight])
|
|
127
|
+
const hasDarkSnap = !!(snapshotDark && snapshotDark.includes(widgetId) && !brokenSnaps[snapshotDark])
|
|
128
|
+
const hasAnySnap = hasLightSnap || hasDarkSnap
|
|
164
129
|
|
|
165
130
|
// Re-resolve story URL when the story index is live-patched (new story added)
|
|
166
131
|
useEffect(() => {
|
|
@@ -180,90 +145,69 @@ export default forwardRef(function StoryWidget({ id: widgetId, props, onUpdate,
|
|
|
180
145
|
})
|
|
181
146
|
}, [onUpdate])
|
|
182
147
|
|
|
183
|
-
const enterInteractive = useCallback(() =>
|
|
148
|
+
const enterInteractive = useCallback(() => {
|
|
149
|
+
exitSessionRef.current++
|
|
150
|
+
setShowIframe(true)
|
|
151
|
+
setInteractive(true)
|
|
152
|
+
}, [])
|
|
153
|
+
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
if (!showIframe) setIframeLoaded(false)
|
|
156
|
+
}, [showIframe])
|
|
184
157
|
|
|
158
|
+
// Exit interactive mode when clicking outside.
|
|
159
|
+
// Hides iframe immediately for a responsive feel, then captures
|
|
160
|
+
// snapshots in the background with the iframe hidden but still mounted.
|
|
185
161
|
useEffect(() => {
|
|
186
162
|
if (!interactive) return
|
|
187
163
|
function handlePointerDown(e) {
|
|
188
164
|
if (containerRef.current && !containerRef.current.contains(e.target)) {
|
|
165
|
+
const chromeEl = e.target.closest(`[data-widget-id="${widgetId}"]`)
|
|
166
|
+
if (chromeEl) return
|
|
167
|
+
|
|
189
168
|
setInteractive(false)
|
|
169
|
+
if (onUpdate && iframeLoaded && iframeRef.current?.contentWindow) {
|
|
170
|
+
// Keep iframe mounted but hidden for background capture
|
|
171
|
+
if (iframeRef.current) iframeRef.current.style.visibility = 'hidden'
|
|
172
|
+
const session = ++exitSessionRef.current
|
|
173
|
+
requestCapture({ force: true }).then(() => {
|
|
174
|
+
if (exitSessionRef.current !== session) return
|
|
175
|
+
setShowIframe(false)
|
|
176
|
+
})
|
|
177
|
+
} else {
|
|
178
|
+
setShowIframe(false)
|
|
179
|
+
}
|
|
190
180
|
}
|
|
191
181
|
}
|
|
192
182
|
document.addEventListener('pointerdown', handlePointerDown)
|
|
193
183
|
return () => document.removeEventListener('pointerdown', handlePointerDown)
|
|
194
|
-
}, [interactive])
|
|
195
|
-
|
|
196
|
-
// Listen for snapshot messages from the iframe
|
|
197
|
-
useEffect(() => {
|
|
198
|
-
function handleMessage(e) {
|
|
199
|
-
if (!iframeRef.current?.contentWindow) return
|
|
200
|
-
if (e.source !== iframeRef.current.contentWindow) return
|
|
201
|
-
|
|
202
|
-
if (e.data?.type === 'storyboard:embed:snapshot') {
|
|
203
|
-
if (e.data.error) {
|
|
204
|
-
console.warn('[canvas] Story snapshot capture failed:', e.data.error)
|
|
205
|
-
return
|
|
206
|
-
}
|
|
207
|
-
handleSnapshotResult(e.data.dataUrl)
|
|
208
|
-
return
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// snapshot-ready means the iframe content has fully rendered
|
|
212
|
-
if (e.data?.type === 'storyboard:embed:snapshot-ready') {
|
|
213
|
-
setIframeLoaded(true)
|
|
214
|
-
if (onUpdate) requestSnapshotCapture()
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
window.addEventListener('message', handleMessage)
|
|
218
|
-
return () => window.removeEventListener('message', handleMessage)
|
|
219
|
-
}, [onUpdate, canvasTheme])
|
|
220
|
-
|
|
221
|
-
const requestSnapshotCapture = useCallback(() => {
|
|
222
|
-
if (!iframeRef.current?.contentWindow || capturingRef.current) return
|
|
223
|
-
capturingRef.current = true
|
|
224
|
-
iframeRef.current.contentWindow.postMessage({
|
|
225
|
-
type: 'storyboard:embed:capture',
|
|
226
|
-
requestId: `story-snap-${Date.now()}`,
|
|
227
|
-
}, '*')
|
|
228
|
-
}, [])
|
|
229
|
-
|
|
230
|
-
const handleSnapshotResult = useCallback(async (dataUrl) => {
|
|
231
|
-
if (!dataUrl || !onUpdate || !widgetId) return
|
|
232
|
-
capturingRef.current = false
|
|
233
|
-
try {
|
|
234
|
-
const result = await uploadImage(dataUrl, `snapshot-${widgetId}`)
|
|
235
|
-
if (!result?.success || !result?.filename) return
|
|
236
|
-
const imageUrl = `/_storyboard/canvas/images/${result.filename}`
|
|
237
|
-
const themeKey = isDark ? 'snapshotDark' : 'snapshotLight'
|
|
238
|
-
onUpdate?.({ [themeKey]: imageUrl })
|
|
239
|
-
} catch (err) {
|
|
240
|
-
console.warn('[canvas] Failed to upload story snapshot:', err)
|
|
241
|
-
}
|
|
242
|
-
}, [onUpdate, isDark, widgetId])
|
|
243
|
-
|
|
244
|
-
// Re-capture after resize
|
|
245
|
-
const resizeCaptureTimer = useRef(null)
|
|
246
|
-
const triggerResizeCapture = useCallback(() => {
|
|
247
|
-
if (!onUpdate) return
|
|
248
|
-
clearTimeout(resizeCaptureTimer.current)
|
|
249
|
-
resizeCaptureTimer.current = setTimeout(() => requestSnapshotCapture(), 2000)
|
|
250
|
-
}, [requestSnapshotCapture, onUpdate])
|
|
184
|
+
}, [interactive, onUpdate, iframeLoaded, requestCapture])
|
|
251
185
|
|
|
252
186
|
const handleResize = useCallback((w, h) => {
|
|
253
187
|
onUpdate?.({ width: w, height: h })
|
|
254
|
-
|
|
255
|
-
|
|
188
|
+
// Recapture snapshot after resize (debounced)
|
|
189
|
+
clearTimeout(resizeTimerRef.current)
|
|
190
|
+
resizeTimerRef.current = setTimeout(() => requestCapture(), 1500)
|
|
191
|
+
}, [onUpdate, requestCapture])
|
|
256
192
|
|
|
257
|
-
//
|
|
258
|
-
const prevThemeRef = useRef(canvasTheme)
|
|
193
|
+
// Capture snapshot on first iframe ready (when no existing snapshot)
|
|
259
194
|
useEffect(() => {
|
|
260
|
-
if (
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
return () => clearTimeout(timer)
|
|
195
|
+
if (!iframeReady || !onUpdate) return
|
|
196
|
+
if (!hasAnySnap) {
|
|
197
|
+
requestCapture()
|
|
264
198
|
}
|
|
265
|
-
|
|
266
|
-
|
|
199
|
+
}, [iframeReady]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
200
|
+
|
|
201
|
+
// Capture when iframe becomes ready after refresh-thumbnail requested it
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
if (iframeReady && captureOnReadyRef.current) {
|
|
204
|
+
captureOnReadyRef.current = false
|
|
205
|
+
requestCapture()
|
|
206
|
+
}
|
|
207
|
+
}, [iframeReady, requestCapture])
|
|
208
|
+
|
|
209
|
+
// Cleanup resize timer on unmount
|
|
210
|
+
useEffect(() => () => clearTimeout(resizeTimerRef.current), [])
|
|
267
211
|
|
|
268
212
|
// Load source code when show-code is toggled on
|
|
269
213
|
useEffect(() => {
|
|
@@ -350,15 +294,28 @@ export default forwardRef(function StoryWidget({ id: widgetId, props, onUpdate,
|
|
|
350
294
|
const base = (import.meta.env.BASE_URL || '/').replace(/\/$/, '')
|
|
351
295
|
window.open(`${base}${story._route}`, '_blank', 'noopener')
|
|
352
296
|
}
|
|
297
|
+
} else if (actionId === 'refresh-thumbnail') {
|
|
298
|
+
if (iframeReady && iframeRef.current?.contentWindow) {
|
|
299
|
+
requestCapture()
|
|
300
|
+
} else {
|
|
301
|
+
captureOnReadyRef.current = true
|
|
302
|
+
setShowIframe(true)
|
|
303
|
+
}
|
|
353
304
|
}
|
|
354
305
|
},
|
|
355
|
-
}), [storyId, showCode, toggleShowCode, copyCode])
|
|
306
|
+
}), [storyId, showCode, toggleShowCode, copyCode, iframeReady, requestCapture])
|
|
356
307
|
|
|
357
308
|
const iframeSrc = useMemo(
|
|
358
309
|
() => resolveStoryUrl(storyId, exportName),
|
|
359
310
|
[storyId, exportName, storyIndexKey],
|
|
360
311
|
)
|
|
361
312
|
|
|
313
|
+
useIframeDevLogs({
|
|
314
|
+
widget: 'StoryWidget',
|
|
315
|
+
loaded: showIframe && !showCode && Boolean(iframeSrc),
|
|
316
|
+
src: iframeSrc,
|
|
317
|
+
})
|
|
318
|
+
|
|
362
319
|
const displayName = exportName ? `${storyId} / ${exportName}` : storyId
|
|
363
320
|
|
|
364
321
|
// Error state — missing story or no route
|
|
@@ -367,7 +324,7 @@ export default forwardRef(function StoryWidget({ id: widgetId, props, onUpdate,
|
|
|
367
324
|
<WidgetWrapper>
|
|
368
325
|
<div className={styles.container} ref={containerRef}>
|
|
369
326
|
<div className={styles.error}>
|
|
370
|
-
<span className={styles.errorIcon}
|
|
327
|
+
<span className={styles.errorIcon}><ComponentIcon size={20} /></span>
|
|
371
328
|
<span className={styles.errorText}>Missing story ID</span>
|
|
372
329
|
</div>
|
|
373
330
|
</div>
|
|
@@ -380,7 +337,7 @@ export default forwardRef(function StoryWidget({ id: widgetId, props, onUpdate,
|
|
|
380
337
|
<WidgetWrapper>
|
|
381
338
|
<div className={styles.container} ref={containerRef}>
|
|
382
339
|
<div className={styles.error}>
|
|
383
|
-
<span className={styles.errorIcon}
|
|
340
|
+
<span className={styles.errorIcon}><ComponentIcon size={20} /></span>
|
|
384
341
|
<span className={styles.errorText}>Story “{storyId}” not found or has no route</span>
|
|
385
342
|
</div>
|
|
386
343
|
</div>
|
|
@@ -396,7 +353,7 @@ export default forwardRef(function StoryWidget({ id: widgetId, props, onUpdate,
|
|
|
396
353
|
<WidgetWrapper>
|
|
397
354
|
<div ref={containerRef} className={styles.container} style={sizeStyle}>
|
|
398
355
|
<div className={styles.header}>
|
|
399
|
-
<span className={styles.headerIcon}
|
|
356
|
+
<span className={styles.headerIcon}><ComponentIcon size={16} /></span>
|
|
400
357
|
<span className={styles.headerTitle}>{displayName}</span>
|
|
401
358
|
</div>
|
|
402
359
|
{showCode ? (
|
|
@@ -430,47 +387,55 @@ export default forwardRef(function StoryWidget({ id: widgetId, props, onUpdate,
|
|
|
430
387
|
</div>
|
|
431
388
|
) : (
|
|
432
389
|
<>
|
|
433
|
-
{
|
|
434
|
-
|
|
435
|
-
|
|
390
|
+
<div className={styles.content}>
|
|
391
|
+
{/* Snapshot layer — both themes always in DOM for instant swap */}
|
|
392
|
+
{hasLightSnap && (
|
|
436
393
|
<img
|
|
437
|
-
src={
|
|
438
|
-
alt={displayName}
|
|
394
|
+
src={snapshotLight}
|
|
439
395
|
className={styles.snapshotImage}
|
|
396
|
+
style={(isDark && hasDarkSnap) ? { visibility: 'hidden' } : undefined}
|
|
397
|
+
alt={`${displayName} snapshot`}
|
|
440
398
|
draggable={false}
|
|
399
|
+
onError={() => setBrokenSnaps(prev => ({ ...prev, [snapshotLight]: true }))}
|
|
441
400
|
/>
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
401
|
+
)}
|
|
402
|
+
{hasDarkSnap && (
|
|
403
|
+
<img
|
|
404
|
+
src={snapshotDark}
|
|
405
|
+
className={styles.snapshotImage}
|
|
406
|
+
style={(!isDark && hasLightSnap) ? { visibility: 'hidden' } : undefined}
|
|
407
|
+
alt={`${displayName} snapshot`}
|
|
408
|
+
draggable={false}
|
|
409
|
+
onError={() => setBrokenSnaps(prev => ({ ...prev, [snapshotDark]: true }))}
|
|
410
|
+
/>
|
|
411
|
+
)}
|
|
449
412
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
<div
|
|
453
|
-
className={styles.content}
|
|
454
|
-
style={hasSnapshot && !(showIframe && iframeLoaded) ? { position: 'absolute', top: 31, left: 0, right: 0, bottom: 0, opacity: 0, pointerEvents: 'none' } : undefined}
|
|
455
|
-
>
|
|
413
|
+
{/* Iframe layer — on top, transparent until loaded */}
|
|
414
|
+
{showIframe && (
|
|
456
415
|
<iframe
|
|
457
416
|
ref={iframeRef}
|
|
458
417
|
src={iframeSrc}
|
|
459
418
|
className={styles.iframe}
|
|
419
|
+
style={iframeLoaded ? undefined : { opacity: 0 }}
|
|
420
|
+
onLoad={() => setIframeLoaded(true)}
|
|
460
421
|
title={displayName}
|
|
461
422
|
/>
|
|
462
|
-
|
|
463
|
-
|
|
423
|
+
)}
|
|
424
|
+
|
|
425
|
+
{/* Placeholder — only when no snapshots and no iframe */}
|
|
426
|
+
{!hasAnySnap && !showIframe && (
|
|
427
|
+
<div className={styles.placeholder}>
|
|
428
|
+
<ComponentIcon size={36} />
|
|
429
|
+
<span className={styles.placeholderLabel}>{displayName}</span>
|
|
430
|
+
</div>
|
|
431
|
+
)}
|
|
432
|
+
</div>
|
|
464
433
|
|
|
465
434
|
{!interactive && (
|
|
466
435
|
<div
|
|
467
436
|
className={overlayStyles.interactOverlay}
|
|
468
|
-
onPointerEnter={() => {
|
|
469
|
-
if (!preloadIframe) setPreloadIframe(true)
|
|
470
|
-
}}
|
|
471
437
|
onClick={(e) => {
|
|
472
438
|
if (e.shiftKey || e.metaKey || e.ctrlKey || e.altKey) return
|
|
473
|
-
activateIframe()
|
|
474
439
|
enterInteractive()
|
|
475
440
|
}}
|
|
476
441
|
role="button"
|
|
@@ -479,7 +444,6 @@ export default forwardRef(function StoryWidget({ id: widgetId, props, onUpdate,
|
|
|
479
444
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
480
445
|
e.preventDefault()
|
|
481
446
|
e.stopPropagation()
|
|
482
|
-
activateIframe()
|
|
483
447
|
enterInteractive()
|
|
484
448
|
}
|
|
485
449
|
}}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
display: flex;
|
|
16
16
|
align-items: center;
|
|
17
17
|
gap: 6px;
|
|
18
|
-
padding:
|
|
18
|
+
padding: 10px 10px;
|
|
19
19
|
font-size: 12px;
|
|
20
20
|
font-weight: 500;
|
|
21
21
|
color: var(--fgColor-muted, #656d76);
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
.headerIcon {
|
|
31
|
-
|
|
31
|
+
display: inline-flex;
|
|
32
32
|
flex-shrink: 0;
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -38,57 +38,67 @@
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
.content {
|
|
41
|
+
position: relative;
|
|
41
42
|
width: 100%;
|
|
42
|
-
height: calc(100% -
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
.iframe {
|
|
46
|
-
display: block;
|
|
47
|
-
width: 100%;
|
|
48
|
-
height: 100%;
|
|
49
|
-
border: none;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
.snapshotImage {
|
|
53
|
-
display: block;
|
|
54
|
-
width: 100%;
|
|
55
|
-
height: 100%;
|
|
56
|
-
object-fit: cover;
|
|
57
|
-
object-position: top left;
|
|
43
|
+
height: calc(100% - 37px);
|
|
58
44
|
}
|
|
59
45
|
|
|
60
|
-
.
|
|
46
|
+
.placeholder {
|
|
61
47
|
position: absolute;
|
|
62
48
|
inset: 0;
|
|
63
49
|
display: flex;
|
|
50
|
+
flex-direction: column;
|
|
64
51
|
align-items: center;
|
|
65
52
|
justify-content: center;
|
|
66
|
-
|
|
67
|
-
|
|
53
|
+
gap: 8px;
|
|
54
|
+
color: var(--fgColor-muted, #656d76);
|
|
55
|
+
text-align: center;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.placeholderLabel {
|
|
59
|
+
font-size: 13px;
|
|
60
|
+
font-weight: 500;
|
|
68
61
|
}
|
|
69
62
|
|
|
70
63
|
.spinner {
|
|
71
64
|
width: 24px;
|
|
72
65
|
height: 24px;
|
|
73
|
-
border:
|
|
74
|
-
border-top-color: var(--fgColor-accent, #
|
|
66
|
+
border: 3px solid var(--borderColor-muted, #d0d7de);
|
|
67
|
+
border-top-color: var(--fgColor-accent, #2f81f7);
|
|
75
68
|
border-radius: 50%;
|
|
76
|
-
animation: spin 0.
|
|
69
|
+
animation: spin 0.8s linear infinite;
|
|
77
70
|
}
|
|
78
71
|
|
|
79
72
|
@keyframes spin {
|
|
73
|
+
from { transform: rotate(0deg); }
|
|
80
74
|
to { transform: rotate(360deg); }
|
|
81
75
|
}
|
|
82
76
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
77
|
+
.iframe {
|
|
78
|
+
position: absolute;
|
|
79
|
+
inset: 0;
|
|
80
|
+
display: block;
|
|
81
|
+
width: 100%;
|
|
82
|
+
height: 100%;
|
|
83
|
+
border: none;
|
|
84
|
+
z-index: 1;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.snapshotImage {
|
|
88
|
+
position: absolute;
|
|
89
|
+
inset: 0;
|
|
90
|
+
width: 100%;
|
|
91
|
+
height: 100%;
|
|
92
|
+
object-fit: cover;
|
|
93
|
+
object-position: top left;
|
|
94
|
+
display: block;
|
|
95
|
+
pointer-events: none;
|
|
86
96
|
}
|
|
87
97
|
|
|
88
98
|
.codeView {
|
|
89
99
|
display: flex;
|
|
90
100
|
flex-direction: column;
|
|
91
|
-
height: calc(100% -
|
|
101
|
+
height: calc(100% - 37px);
|
|
92
102
|
overflow: hidden;
|
|
93
103
|
}
|
|
94
104
|
|
|
@@ -204,8 +204,8 @@ function WidgetOverflowMenu({ widgetId, menuFeatures, onAction }) {
|
|
|
204
204
|
url.searchParams.set('widget', widgetId)
|
|
205
205
|
navigator.clipboard.writeText(url.toString()).catch(() => {})
|
|
206
206
|
} else if (action === 'copy-widget-id') {
|
|
207
|
-
const
|
|
208
|
-
navigator.clipboard.writeText(`${
|
|
207
|
+
const canvasId = window.__storyboardCanvasBridgeState?.canvasId || ''
|
|
208
|
+
navigator.clipboard.writeText(`${canvasId}::${widgetId}`).catch(() => {})
|
|
209
209
|
} else {
|
|
210
210
|
onAction?.(action)
|
|
211
211
|
}
|
|
@@ -436,6 +436,7 @@ export default function WidgetChrome({
|
|
|
436
436
|
return (
|
|
437
437
|
<div
|
|
438
438
|
className={styles.chromeContainer}
|
|
439
|
+
data-widget-id={widgetId}
|
|
439
440
|
data-tc-elevated={(hovered || selected) || undefined}
|
|
440
441
|
onMouseEnter={(readOnly && !hasFeatures) ? undefined : handleMouseEnter}
|
|
441
442
|
onMouseLeave={(readOnly && !hasFeatures) ? undefined : handleMouseLeave}
|