@dfosco/storyboard-react 4.2.0-beta.4 → 4.2.1
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 +407 -67
- package/src/vite/data-plugin.test.js +1 -1
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { useState, useRef, useEffect, useCallback, useMemo, forwardRef, useImperativeHandle } from 'react'
|
|
2
|
-
import { createPortal } from 'react-dom'
|
|
3
2
|
import { buildPrototypeIndex } from '@dfosco/storyboard-core'
|
|
4
3
|
import WidgetWrapper from './WidgetWrapper.jsx'
|
|
5
4
|
import ResizeHandle from './ResizeHandle.jsx'
|
|
6
5
|
import { readProp, prototypeEmbedSchema } from './widgetProps.js'
|
|
7
6
|
import { getEmbedChromeVars } from './embedTheme.js'
|
|
8
7
|
import { useIframeDevLogs } from './iframeDevLogs.js'
|
|
8
|
+
import { findAllConnectedSplitTargets, getSplitPaneLabel, buildPaneForWidget, buildSplitLayout } from './expandUtils.js'
|
|
9
|
+
import ExpandedPane from './ExpandedPane.jsx'
|
|
9
10
|
import styles from './PrototypeEmbed.module.css'
|
|
10
11
|
import overlayStyles from './embedOverlay.module.css'
|
|
11
12
|
|
|
@@ -60,11 +61,11 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
|
|
|
60
61
|
}, [src, basePath, baseSegment])
|
|
61
62
|
|
|
62
63
|
const scale = zoom / 100
|
|
63
|
-
const isExternal = /^https?:\/\//.test(src || '')
|
|
64
64
|
|
|
65
65
|
const [editing, setEditing] = useState(false)
|
|
66
66
|
const [interactive, setInteractive] = useState(false)
|
|
67
|
-
const [
|
|
67
|
+
const [expandMode, setExpandMode] = useState(null)
|
|
68
|
+
const expanded = expandMode !== null
|
|
68
69
|
const [filter, setFilter] = useState('')
|
|
69
70
|
const [canvasTheme, setCanvasTheme] = useState(() => resolveCanvasThemeFromStorage())
|
|
70
71
|
const inputRef = useRef(null)
|
|
@@ -84,6 +85,8 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
|
|
|
84
85
|
return `${base}${sep}_sb_embed&_sb_hide_branch_bar&_sb_theme_target=prototype&_sb_canvas_theme=${canvasTheme}${hash}`
|
|
85
86
|
}, [rawSrc, canvasTheme])
|
|
86
87
|
|
|
88
|
+
const effectiveSrc = iframeSrc
|
|
89
|
+
|
|
87
90
|
const prototypeIndex = useMemo(() => {
|
|
88
91
|
try { return buildPrototypeIndex() }
|
|
89
92
|
catch { return { folders: [], prototypes: [], globalFlows: [], sorted: { title: { prototypes: [], folders: [] } } } }
|
|
@@ -153,8 +156,8 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
|
|
|
153
156
|
|
|
154
157
|
useIframeDevLogs({
|
|
155
158
|
widget: 'PrototypeEmbed',
|
|
156
|
-
loaded: Boolean(
|
|
157
|
-
src:
|
|
159
|
+
loaded: Boolean(effectiveSrc && interactive),
|
|
160
|
+
src: effectiveSrc,
|
|
158
161
|
})
|
|
159
162
|
|
|
160
163
|
useEffect(() => {
|
|
@@ -189,19 +192,6 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
|
|
|
189
192
|
return () => document.removeEventListener('storyboard:theme:changed', readToolbarTheme)
|
|
190
193
|
}, [])
|
|
191
194
|
|
|
192
|
-
// Close expanded modal on Escape
|
|
193
|
-
useEffect(() => {
|
|
194
|
-
if (!expanded) return
|
|
195
|
-
function handleKeyDown(e) {
|
|
196
|
-
if (e.key === 'Escape') {
|
|
197
|
-
e.stopPropagation()
|
|
198
|
-
setExpanded(false)
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
document.addEventListener('keydown', handleKeyDown, true)
|
|
202
|
-
return () => document.removeEventListener('keydown', handleKeyDown, true)
|
|
203
|
-
}, [expanded])
|
|
204
|
-
|
|
205
195
|
// Reparent iframe between inline and modal
|
|
206
196
|
useEffect(() => {
|
|
207
197
|
const iframe = iframeRef.current
|
|
@@ -212,8 +202,12 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
|
|
|
212
202
|
iframe.className = styles.expandIframe
|
|
213
203
|
iframe.removeAttribute('style')
|
|
214
204
|
const target = modalContainerRef.current
|
|
215
|
-
|
|
216
|
-
|
|
205
|
+
try {
|
|
206
|
+
if (target.moveBefore) target.moveBefore(iframe, target.firstChild)
|
|
207
|
+
else target.prepend(iframe)
|
|
208
|
+
} catch {
|
|
209
|
+
target.prepend(iframe)
|
|
210
|
+
}
|
|
217
211
|
} else if (!expanded && inlineContainerRef.current) {
|
|
218
212
|
if (iframe._savedClassName !== undefined) {
|
|
219
213
|
iframe.className = iframe._savedClassName
|
|
@@ -222,8 +216,12 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
|
|
|
222
216
|
delete iframe._savedStyle
|
|
223
217
|
}
|
|
224
218
|
const target = inlineContainerRef.current
|
|
225
|
-
|
|
226
|
-
|
|
219
|
+
try {
|
|
220
|
+
if (target.moveBefore) target.moveBefore(iframe, null)
|
|
221
|
+
else target.appendChild(iframe)
|
|
222
|
+
} catch {
|
|
223
|
+
target.appendChild(iframe)
|
|
224
|
+
}
|
|
227
225
|
}
|
|
228
226
|
}, [expanded])
|
|
229
227
|
|
|
@@ -250,8 +248,10 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
|
|
|
250
248
|
handleAction(actionId) {
|
|
251
249
|
if (actionId === 'edit') {
|
|
252
250
|
setEditing(true)
|
|
253
|
-
} else if (actionId === 'expand') {
|
|
254
|
-
|
|
251
|
+
} else if (actionId === 'expand' || actionId === 'expand-single') {
|
|
252
|
+
setExpandMode('single')
|
|
253
|
+
} else if (actionId === 'split-screen') {
|
|
254
|
+
setExpandMode('split')
|
|
255
255
|
} else if (actionId === 'open-external') {
|
|
256
256
|
if (rawSrc) window.open(rawSrc, '_blank', 'noopener')
|
|
257
257
|
} else if (actionId === 'zoom-in') {
|
|
@@ -362,7 +362,7 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
|
|
|
362
362
|
>
|
|
363
363
|
<iframe
|
|
364
364
|
ref={iframeRef}
|
|
365
|
-
src={
|
|
365
|
+
src={effectiveSrc}
|
|
366
366
|
className={styles.iframe}
|
|
367
367
|
style={{
|
|
368
368
|
width: width / scale,
|
|
@@ -372,6 +372,7 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
|
|
|
372
372
|
}}
|
|
373
373
|
title={`${prototypeTitle} prototype`}
|
|
374
374
|
sandbox="allow-same-origin allow-scripts allow-forms allow-popups"
|
|
375
|
+
onLoad={(e) => e.target.blur()}
|
|
375
376
|
/>
|
|
376
377
|
</div>
|
|
377
378
|
{!interactive && !expanded && (
|
|
@@ -405,21 +406,58 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
|
|
|
405
406
|
</div>
|
|
406
407
|
{resizable && <ResizeHandle targetRef={embedRef} width={width} height={height} onResize={handleResize} />}
|
|
407
408
|
</WidgetWrapper>
|
|
408
|
-
{
|
|
409
|
-
<
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
onWheel={(e) => e.stopPropagation()}
|
|
416
|
-
>
|
|
417
|
-
<div ref={modalContainerRef} className={styles.expandContainer} onClick={(e) => e.stopPropagation()}>
|
|
418
|
-
<button className={styles.expandClose} onClick={() => setExpanded(false)} aria-label="Close expanded view" autoFocus>✕</button>
|
|
419
|
-
</div>
|
|
420
|
-
</div>,
|
|
421
|
-
document.body
|
|
409
|
+
{expanded && (
|
|
410
|
+
<PrototypeExpandPane
|
|
411
|
+
widgetId={widgetId}
|
|
412
|
+
modalContainerRef={modalContainerRef}
|
|
413
|
+
splitMode={expandMode === 'split'}
|
|
414
|
+
onClose={() => setExpandMode(null)}
|
|
415
|
+
/>
|
|
422
416
|
)}
|
|
423
417
|
</>
|
|
424
418
|
)
|
|
425
419
|
})
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Builds pane configs and renders ExpandedPane for an expanded prototype widget.
|
|
423
|
+
* The primary pane is an external pane that receives the iframe via reparenting.
|
|
424
|
+
*/
|
|
425
|
+
function PrototypeExpandPane({ widgetId, modalContainerRef, splitMode, onClose }) {
|
|
426
|
+
const connectedWidgets = useMemo(
|
|
427
|
+
() => splitMode ? findAllConnectedSplitTargets(widgetId) : [],
|
|
428
|
+
[widgetId, splitMode],
|
|
429
|
+
)
|
|
430
|
+
const primaryWidget = useMemo(() => {
|
|
431
|
+
const bridge = window.__storyboardCanvasBridgeState
|
|
432
|
+
return bridge?.widgets?.find((w) => w.id === widgetId) || { id: widgetId, type: 'prototype', position: { x: 0, y: 0 }, props: {} }
|
|
433
|
+
}, [widgetId])
|
|
434
|
+
|
|
435
|
+
const buildPaneFn = useCallback((widget) => {
|
|
436
|
+
if (widget.id === widgetId) {
|
|
437
|
+
return {
|
|
438
|
+
id: widgetId,
|
|
439
|
+
label: getSplitPaneLabel(primaryWidget),
|
|
440
|
+
widgetType: 'prototype',
|
|
441
|
+
kind: 'external',
|
|
442
|
+
attach: (container) => {
|
|
443
|
+
modalContainerRef.current = container
|
|
444
|
+
return () => { modalContainerRef.current = null }
|
|
445
|
+
},
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return buildPaneForWidget(widget)
|
|
449
|
+
}, [widgetId, primaryWidget, modalContainerRef])
|
|
450
|
+
|
|
451
|
+
const layout = useMemo(
|
|
452
|
+
() => buildSplitLayout(primaryWidget, connectedWidgets, buildPaneFn),
|
|
453
|
+
[primaryWidget, connectedWidgets, buildPaneFn],
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
return (
|
|
457
|
+
<ExpandedPane
|
|
458
|
+
initialLayout={layout}
|
|
459
|
+
variant={layout.flat().length <= 1 ? 'modal' : 'full'}
|
|
460
|
+
onClose={onClose}
|
|
461
|
+
/>
|
|
462
|
+
)
|
|
463
|
+
}
|
|
@@ -460,3 +460,120 @@
|
|
|
460
460
|
.expandClose:hover {
|
|
461
461
|
background: rgba(0, 0, 0, 0.7);
|
|
462
462
|
}
|
|
463
|
+
|
|
464
|
+
/* ── Split-screen layout ──────────────────────────────────────────── */
|
|
465
|
+
|
|
466
|
+
.expandContainerSplit {
|
|
467
|
+
flex: 1;
|
|
468
|
+
min-width: 0;
|
|
469
|
+
height: 100%;
|
|
470
|
+
position: relative;
|
|
471
|
+
overflow: hidden;
|
|
472
|
+
background: var(--bgColor-default, #ffffff);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
.expandContainerSplit .expandIframe {
|
|
476
|
+
border: none;
|
|
477
|
+
display: block;
|
|
478
|
+
width: 100%;
|
|
479
|
+
height: 100%;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
.expandSplitBody {
|
|
483
|
+
position: fixed;
|
|
484
|
+
inset: 0;
|
|
485
|
+
display: flex;
|
|
486
|
+
flex-direction: column;
|
|
487
|
+
overflow: hidden;
|
|
488
|
+
animation: expandScaleIn 0.2s ease;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
.expandSplitPanes {
|
|
492
|
+
flex: 1;
|
|
493
|
+
min-height: 0;
|
|
494
|
+
display: flex;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
.expandSplitLeft {
|
|
498
|
+
flex: 1;
|
|
499
|
+
min-width: 0;
|
|
500
|
+
height: 100%;
|
|
501
|
+
overflow: hidden;
|
|
502
|
+
border-right: 1px solid var(--borderColor-muted, #d8dee4);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
.expandSplitRight {
|
|
506
|
+
flex: 1;
|
|
507
|
+
min-width: 0;
|
|
508
|
+
height: 100%;
|
|
509
|
+
overflow: hidden;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
.expandSecondary {
|
|
513
|
+
width: 100%;
|
|
514
|
+
height: 100%;
|
|
515
|
+
overflow: auto;
|
|
516
|
+
background: var(--bgColor-default, #ffffff);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
.expandSecondary iframe {
|
|
520
|
+
border: none;
|
|
521
|
+
width: 100%;
|
|
522
|
+
height: 100%;
|
|
523
|
+
display: block;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
.expandTerminal {
|
|
527
|
+
width: 100%;
|
|
528
|
+
height: 100%;
|
|
529
|
+
background: #0d1117;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
.expandMarkdownContent {
|
|
533
|
+
padding: 32px 40px;
|
|
534
|
+
font-size: 15px;
|
|
535
|
+
line-height: 1.7;
|
|
536
|
+
color: var(--fgColor-default, #1f2328);
|
|
537
|
+
max-width: 800px;
|
|
538
|
+
margin: 0 auto;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
.expandMarkdownContent * { pointer-events: auto; }
|
|
542
|
+
.expandMarkdownContent a { color: var(--fgColor-accent, #0969da); text-decoration: none; }
|
|
543
|
+
.expandMarkdownContent a:hover { text-decoration: underline; }
|
|
544
|
+
.expandMarkdownContent img { max-width: 100%; height: auto; border-radius: 6px; margin: 8px 0; display: block; }
|
|
545
|
+
|
|
546
|
+
.expandGithubCard {
|
|
547
|
+
height: 100%;
|
|
548
|
+
display: flex;
|
|
549
|
+
flex-direction: column;
|
|
550
|
+
background: var(--bgColor-default, #ffffff);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
.expandGithubHeader {
|
|
554
|
+
padding: 16px 24px;
|
|
555
|
+
font-size: 18px;
|
|
556
|
+
font-weight: 600;
|
|
557
|
+
border-bottom: 1px solid var(--borderColor-muted, #d8dee4);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
.expandGithubBody {
|
|
561
|
+
flex: 1;
|
|
562
|
+
overflow: auto;
|
|
563
|
+
padding: 24px;
|
|
564
|
+
font-size: 15px;
|
|
565
|
+
line-height: 1.7;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
.expandGithubBody * { pointer-events: auto; }
|
|
569
|
+
.expandGithubBody a { color: var(--fgColor-accent, #0969da); }
|
|
570
|
+
|
|
571
|
+
.expandLinkCard {
|
|
572
|
+
padding: 32px 40px;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
.expandLinkTitle {
|
|
576
|
+
font-size: 18px;
|
|
577
|
+
font-weight: 600;
|
|
578
|
+
margin: 0 0 8px;
|
|
579
|
+
}
|
|
@@ -4,7 +4,7 @@ import { getEmbedChromeVars } from './embedTheme.js'
|
|
|
4
4
|
describe('getEmbedChromeVars', () => {
|
|
5
5
|
it('follows toolbar theme variants for embed edit chrome', () => {
|
|
6
6
|
expect(getEmbedChromeVars('light')['--bgColor-default']).toBe('#ffffff')
|
|
7
|
-
expect(getEmbedChromeVars('dark')['--bgColor-default']).toBe('#
|
|
8
|
-
expect(getEmbedChromeVars('dark_dimmed')['--bgColor-default']).toBe('#
|
|
7
|
+
expect(getEmbedChromeVars('dark')['--bgColor-default']).toBe('#0d1117')
|
|
8
|
+
expect(getEmbedChromeVars('dark_dimmed')['--bgColor-default']).toBe('#212830')
|
|
9
9
|
})
|
|
10
10
|
})
|
|
@@ -13,9 +13,12 @@ import styles from './ResizeHandle.module.css'
|
|
|
13
13
|
* @param {React.RefObject} props.targetRef - ref to the element being resized (reads offsetWidth/Height)
|
|
14
14
|
* @param {number} [props.minWidth=180] - minimum allowed width
|
|
15
15
|
* @param {number} [props.minHeight=60] - minimum allowed height
|
|
16
|
+
* @param {'both'|'vertical'|'horizontal'} [props.axis='both'] - constrain resize to a single axis
|
|
16
17
|
* @param {Function} props.onResize - callback: (width, height) => void
|
|
18
|
+
* @param {Function} [props.onResizeStart] - called when drag begins
|
|
19
|
+
* @param {Function} [props.onResizeEnd] - called with final (width, height) on drag end
|
|
17
20
|
*/
|
|
18
|
-
export default function ResizeHandle({ targetRef, minWidth = 180, minHeight = 60, onResize }) {
|
|
21
|
+
export default function ResizeHandle({ targetRef, minWidth = 180, minHeight = 60, axis = 'both', onResize, onResizeStart, onResizeEnd }) {
|
|
19
22
|
const handleMouseDown = useCallback((e) => {
|
|
20
23
|
e.stopPropagation()
|
|
21
24
|
e.preventDefault()
|
|
@@ -27,29 +30,37 @@ export default function ResizeHandle({ targetRef, minWidth = 180, minHeight = 60
|
|
|
27
30
|
const startY = e.clientY
|
|
28
31
|
const startW = el.offsetWidth
|
|
29
32
|
const startH = el.offsetHeight
|
|
33
|
+
let lastW = startW
|
|
34
|
+
let lastH = startH
|
|
35
|
+
|
|
36
|
+
onResizeStart?.()
|
|
30
37
|
|
|
31
38
|
function onMove(ev) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
onResize?.(
|
|
39
|
+
lastW = axis === 'vertical' ? startW : Math.max(minWidth, startW + ev.clientX - startX)
|
|
40
|
+
lastH = axis === 'horizontal' ? startH : Math.max(minHeight, startH + ev.clientY - startY)
|
|
41
|
+
onResize?.(lastW, lastH)
|
|
35
42
|
}
|
|
36
43
|
|
|
37
44
|
function onUp() {
|
|
38
45
|
document.removeEventListener('mousemove', onMove)
|
|
39
46
|
document.removeEventListener('mouseup', onUp)
|
|
47
|
+
onResizeEnd?.(lastW, lastH)
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
document.addEventListener('mousemove', onMove)
|
|
43
51
|
document.addEventListener('mouseup', onUp)
|
|
44
|
-
}, [targetRef, minWidth, minHeight, onResize])
|
|
52
|
+
}, [targetRef, minWidth, minHeight, axis, onResize, onResizeStart, onResizeEnd])
|
|
53
|
+
|
|
54
|
+
const cursor = axis === 'vertical' ? 'ns-resize' : axis === 'horizontal' ? 'ew-resize' : 'nwse-resize'
|
|
45
55
|
|
|
46
56
|
return (
|
|
47
57
|
<div
|
|
48
58
|
className={styles.handle}
|
|
59
|
+
style={axis !== 'both' ? { cursor } : undefined}
|
|
49
60
|
onMouseDown={handleMouseDown}
|
|
50
61
|
onPointerDown={(e) => e.stopPropagation()}
|
|
51
62
|
role="separator"
|
|
52
|
-
aria-orientation=
|
|
63
|
+
aria-orientation={axis === 'vertical' ? 'vertical' : 'horizontal'}
|
|
53
64
|
aria-label="Resize"
|
|
54
65
|
/>
|
|
55
66
|
)
|
|
@@ -11,21 +11,17 @@
|
|
|
11
11
|
import { forwardRef, useImperativeHandle, useRef, useCallback, useState, useEffect, useMemo } from 'react'
|
|
12
12
|
import { getStoryData } from '@dfosco/storyboard-core'
|
|
13
13
|
import { createInspectorHighlighter } from '@dfosco/storyboard-core/inspector/highlighter'
|
|
14
|
+
import Icon from '../../Icon.jsx'
|
|
14
15
|
import WidgetWrapper from './WidgetWrapper.jsx'
|
|
15
16
|
import ResizeHandle from './ResizeHandle.jsx'
|
|
16
17
|
import { useIframeDevLogs } from './iframeDevLogs.js'
|
|
18
|
+
import { findAllConnectedSplitTargets, getSplitPaneLabel, buildPaneForWidget, buildSplitLayout, buildSecondaryIframeUrl } from './expandUtils.js'
|
|
19
|
+
import ExpandedPane from './ExpandedPane.jsx'
|
|
17
20
|
import styles from './StoryWidget.module.css'
|
|
18
21
|
import overlayStyles from './embedOverlay.module.css'
|
|
19
22
|
|
|
20
23
|
function ComponentIcon({ size = 36 }) {
|
|
21
|
-
return
|
|
22
|
-
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden="true">
|
|
23
|
-
<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" />
|
|
24
|
-
<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" />
|
|
25
|
-
<path d="M17.9395 15.1113L15.2525 12.4243C15.0182 12.1899 15.0182 11.8101 15.2525 11.5757L17.9395 8.88873C18.1738 8.65442 18.5537 8.65442 18.788 8.88873L21.475 11.5757C21.7094 11.8101 21.7094 12.1899 21.475 12.4243L18.788 15.1113C18.5537 15.3456 18.1738 15.3456 17.9395 15.1113Z" />
|
|
26
|
-
<path d="M11.5757 8.74727L8.88874 6.06026C8.65443 5.82595 8.65443 5.44605 8.88874 5.21173L11.5757 2.52473C11.8101 2.29041 12.19 2.29041 12.4243 2.52473L15.1113 5.21173C15.3456 5.44605 15.3456 5.82595 15.1113 6.06026L12.4243 8.74727C12.19 8.98158 11.8101 8.98158 11.5757 8.74727Z" />
|
|
27
|
-
</svg>
|
|
28
|
-
)
|
|
24
|
+
return <Icon name="iconoir/keyframe" size={size} />
|
|
29
25
|
}
|
|
30
26
|
|
|
31
27
|
function resolveStoryUrl(storyId, exportName) {
|
|
@@ -43,9 +39,10 @@ const _storySourcesCache = {}
|
|
|
43
39
|
|
|
44
40
|
async function fetchStorySource(modulePath) {
|
|
45
41
|
if (modulePath in _storySourcesCache) return _storySourcesCache[modulePath]
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
|
|
42
|
+
const base = (import.meta.env.BASE_URL || '/').replace(/\/$/, '')
|
|
43
|
+
const path = modulePath.startsWith('/') ? modulePath : `/${modulePath}`
|
|
44
|
+
const res = await fetch(`${base}${path}?raw`)
|
|
45
|
+
if (!res.ok) throw new Error(`Failed to fetch ${base}${path}`)
|
|
49
46
|
const code = await res.text()
|
|
50
47
|
_storySourcesCache[modulePath] = code
|
|
51
48
|
return code
|
|
@@ -58,6 +55,8 @@ export default forwardRef(function StoryWidget({ id: widgetId, props, onUpdate,
|
|
|
58
55
|
const height = props?.height
|
|
59
56
|
|
|
60
57
|
const containerRef = useRef(null)
|
|
58
|
+
const [expandMode, setExpandMode] = useState(null)
|
|
59
|
+
const expanded = expandMode !== null
|
|
61
60
|
const iframeRef = useRef(null)
|
|
62
61
|
const [interactive, setInteractive] = useState(false)
|
|
63
62
|
const [showCode, setShowCode] = useState(!!props?.showCode)
|
|
@@ -106,6 +105,7 @@ export default forwardRef(function StoryWidget({ id: widgetId, props, onUpdate,
|
|
|
106
105
|
if (!showCode || sourceCode !== null) return
|
|
107
106
|
const story = getStoryData(storyId)
|
|
108
107
|
if (!story?._storyModule) {
|
|
108
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
109
109
|
setSourceCode('// Source not available')
|
|
110
110
|
return
|
|
111
111
|
}
|
|
@@ -156,6 +156,8 @@ export default forwardRef(function StoryWidget({ id: widgetId, props, onUpdate,
|
|
|
156
156
|
handleAction(actionId) {
|
|
157
157
|
if (actionId === 'show-code') toggleShowCode()
|
|
158
158
|
else if (actionId === 'copy-code') copyCode()
|
|
159
|
+
else if (actionId === 'expand' || actionId === 'expand-single') setExpandMode('single')
|
|
160
|
+
else if (actionId === 'split-screen') setExpandMode('split')
|
|
159
161
|
else if (actionId === 'open-external') {
|
|
160
162
|
const story = getStoryData(storyId)
|
|
161
163
|
if (story?._route) {
|
|
@@ -171,10 +173,13 @@ export default forwardRef(function StoryWidget({ id: widgetId, props, onUpdate,
|
|
|
171
173
|
[storyId, exportName, storyIndexKey],
|
|
172
174
|
)
|
|
173
175
|
|
|
176
|
+
// When paused and not interactive, freeze the iframe src to prevent reloads
|
|
177
|
+
const effectiveSrc = iframeSrc
|
|
178
|
+
|
|
174
179
|
useIframeDevLogs({
|
|
175
180
|
widget: 'StoryWidget',
|
|
176
|
-
loaded: interactive && !showCode && Boolean(
|
|
177
|
-
src:
|
|
181
|
+
loaded: interactive && !showCode && Boolean(effectiveSrc),
|
|
182
|
+
src: effectiveSrc,
|
|
178
183
|
})
|
|
179
184
|
|
|
180
185
|
const displayName = exportName ? `${storyId} / ${exportName}` : storyId
|
|
@@ -192,7 +197,7 @@ export default forwardRef(function StoryWidget({ id: widgetId, props, onUpdate,
|
|
|
192
197
|
)
|
|
193
198
|
}
|
|
194
199
|
|
|
195
|
-
if (!
|
|
200
|
+
if (!effectiveSrc) {
|
|
196
201
|
return (
|
|
197
202
|
<WidgetWrapper>
|
|
198
203
|
<div className={styles.container} ref={containerRef}>
|
|
@@ -210,6 +215,7 @@ export default forwardRef(function StoryWidget({ id: widgetId, props, onUpdate,
|
|
|
210
215
|
if (typeof height === 'number') sizeStyle.height = `${height}px`
|
|
211
216
|
|
|
212
217
|
return (
|
|
218
|
+
<>
|
|
213
219
|
<WidgetWrapper>
|
|
214
220
|
<div ref={containerRef} className={styles.container} style={sizeStyle}>
|
|
215
221
|
<div className={styles.header}>
|
|
@@ -241,9 +247,10 @@ export default forwardRef(function StoryWidget({ id: widgetId, props, onUpdate,
|
|
|
241
247
|
<div className={styles.content}>
|
|
242
248
|
<iframe
|
|
243
249
|
ref={iframeRef}
|
|
244
|
-
src={
|
|
250
|
+
src={effectiveSrc}
|
|
245
251
|
className={styles.iframe}
|
|
246
252
|
title={displayName}
|
|
253
|
+
onLoad={(e) => e.target.blur()}
|
|
247
254
|
/>
|
|
248
255
|
</div>
|
|
249
256
|
|
|
@@ -273,5 +280,56 @@ export default forwardRef(function StoryWidget({ id: widgetId, props, onUpdate,
|
|
|
273
280
|
</div>
|
|
274
281
|
{resizable && <ResizeHandle targetRef={containerRef} width={width} height={height} onResize={handleResize} />}
|
|
275
282
|
</WidgetWrapper>
|
|
283
|
+
{expanded && (
|
|
284
|
+
<StoryExpandPane
|
|
285
|
+
widgetId={widgetId}
|
|
286
|
+
storyId={storyId}
|
|
287
|
+
exportName={exportName}
|
|
288
|
+
splitMode={expandMode === 'split'}
|
|
289
|
+
onClose={() => setExpandMode(null)}
|
|
290
|
+
/>
|
|
291
|
+
)}
|
|
292
|
+
</>
|
|
276
293
|
)
|
|
277
294
|
})
|
|
295
|
+
|
|
296
|
+
function StoryExpandPane({ widgetId, storyId, exportName, splitMode, onClose }) {
|
|
297
|
+
const connectedWidgets = useMemo(
|
|
298
|
+
() => splitMode ? findAllConnectedSplitTargets(widgetId) : [],
|
|
299
|
+
[widgetId, splitMode],
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
const primaryWidget = useMemo(() => {
|
|
303
|
+
const bridge = window.__storyboardCanvasBridgeState
|
|
304
|
+
return bridge?.widgets?.find((w) => w.id === widgetId) || { id: widgetId, type: 'story', position: { x: 0, y: 0 }, props: { storyId, exportName } }
|
|
305
|
+
}, [widgetId, storyId, exportName])
|
|
306
|
+
|
|
307
|
+
const buildPaneFn = useCallback((widget) => {
|
|
308
|
+
if (widget.id === widgetId) {
|
|
309
|
+
const url = buildSecondaryIframeUrl({ type: 'story', props: { storyId, exportName } })
|
|
310
|
+
return {
|
|
311
|
+
id: widgetId,
|
|
312
|
+
label: getSplitPaneLabel({ type: 'story', props: { storyId, exportName } }),
|
|
313
|
+
widgetType: 'story',
|
|
314
|
+
kind: 'react',
|
|
315
|
+
render: () => url
|
|
316
|
+
? <iframe src={url} style={{ border: 'none', width: '100%', height: '100%', display: 'block' }} title={storyId} onLoad={(e) => e.target.blur()} />
|
|
317
|
+
: <div style={{ padding: 32, color: 'var(--fgColor-muted)' }}>Story "{storyId}" not found</div>,
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return buildPaneForWidget(widget)
|
|
321
|
+
}, [widgetId, storyId, exportName])
|
|
322
|
+
|
|
323
|
+
const layout = useMemo(
|
|
324
|
+
() => buildSplitLayout(primaryWidget, connectedWidgets, buildPaneFn),
|
|
325
|
+
[primaryWidget, connectedWidgets, buildPaneFn],
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
return (
|
|
329
|
+
<ExpandedPane
|
|
330
|
+
initialLayout={layout}
|
|
331
|
+
variant={layout.flat().length <= 1 ? 'modal' : 'full'}
|
|
332
|
+
onClose={onClose}
|
|
333
|
+
/>
|
|
334
|
+
)
|
|
335
|
+
}
|