@dfosco/storyboard-react 4.2.0-beta.17 → 4.2.0-beta.18
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/BranchBar/BranchBar.jsx +3 -1
- package/src/BranchBar/BranchBar.module.css +2 -2
- package/src/BranchBar/useBranches.js +20 -6
- package/src/BranchBar/useBranches.test.js +68 -0
- package/src/CommandPalette/CommandPalette.jsx +250 -61
- package/src/CommandPalette/command-palette.css +12 -0
- package/src/Icon.jsx +46 -11
- package/src/Viewfinder.jsx +53 -133
- package/src/Viewfinder.module.css +20 -91
- package/src/Workspace.jsx +7 -0
- package/src/canvas/CanvasPage.jsx +601 -62
- package/src/canvas/CanvasPage.module.css +15 -2
- package/src/canvas/CanvasPage.multiselect.test.jsx +7 -0
- package/src/canvas/ConnectorLayer.jsx +120 -152
- package/src/canvas/ConnectorLayer.module.css +69 -0
- package/src/canvas/canvasApi.js +68 -2
- package/src/canvas/connectorGeometry.js +132 -0
- package/src/canvas/hotPoolDevLogs.js +25 -0
- 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 +472 -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 +49 -102
- package/src/canvas/widgets/ImageWidget.jsx +129 -8
- package/src/canvas/widgets/ImageWidget.module.css +30 -0
- package/src/canvas/widgets/LinkPreview.jsx +93 -44
- package/src/canvas/widgets/MarkdownBlock.jsx +141 -16
- package/src/canvas/widgets/MarkdownBlock.module.css +25 -0
- package/src/canvas/widgets/PromptWidget.jsx +414 -0
- package/src/canvas/widgets/PromptWidget.module.css +273 -0
- package/src/canvas/widgets/PrototypeEmbed.jsx +46 -170
- package/src/canvas/widgets/ResizeHandle.jsx +17 -6
- package/src/canvas/widgets/StoryWidget.jsx +65 -11
- package/src/canvas/widgets/TerminalReadWidget.jsx +11 -5
- package/src/canvas/widgets/TerminalReadWidget.module.css +3 -1
- package/src/canvas/widgets/TerminalWidget.jsx +301 -124
- package/src/canvas/widgets/TerminalWidget.module.css +121 -12
- package/src/canvas/widgets/TilesWidget.jsx +302 -0
- package/src/canvas/widgets/TilesWidget.module.css +133 -0
- package/src/canvas/widgets/WidgetChrome.jsx +67 -152
- package/src/canvas/widgets/WidgetChrome.module.css +20 -1
- package/src/canvas/widgets/expandUtils.js +385 -16
- package/src/canvas/widgets/expandUtils.test.js +155 -0
- package/src/canvas/widgets/index.js +6 -2
- 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 +37 -4
- package/src/canvas/widgets/widgetIcons.jsx +190 -0
- package/src/canvas/widgets/widgetProps.js +1 -0
- package/src/context.jsx +47 -19
- package/src/hooks/usePrototypeReloadGuard.js +64 -0
- package/src/index.js +4 -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 +79 -35
- package/src/canvas/widgets/ActionWidget.jsx +0 -200
- package/src/canvas/widgets/ActionWidget.module.css +0 -122
- package/src/canvas/widgets/SplitExpandModal.jsx +0 -234
- package/src/canvas/widgets/SplitExpandModal.module.css +0 -335
- package/src/canvas/widgets/SplitScreenTopBar.jsx +0 -30
- package/src/canvas/widgets/SplitScreenTopBar.module.css +0 -58
|
@@ -1,13 +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'
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
8
|
+
import { findAllConnectedSplitTargets, getSplitPaneLabel, buildPaneForWidget, buildSplitLayout } from './expandUtils.js'
|
|
9
|
+
import ExpandedPane from './ExpandedPane.jsx'
|
|
11
10
|
import styles from './PrototypeEmbed.module.css'
|
|
12
11
|
import overlayStyles from './embedOverlay.module.css'
|
|
13
12
|
|
|
@@ -66,7 +65,8 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
|
|
|
66
65
|
|
|
67
66
|
const [editing, setEditing] = useState(false)
|
|
68
67
|
const [interactive, setInteractive] = useState(false)
|
|
69
|
-
const [
|
|
68
|
+
const [expandMode, setExpandMode] = useState(null)
|
|
69
|
+
const expanded = expandMode !== null
|
|
70
70
|
const [filter, setFilter] = useState('')
|
|
71
71
|
const [canvasTheme, setCanvasTheme] = useState(() => resolveCanvasThemeFromStorage())
|
|
72
72
|
const inputRef = useRef(null)
|
|
@@ -193,19 +193,6 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
|
|
|
193
193
|
return () => document.removeEventListener('storyboard:theme:changed', readToolbarTheme)
|
|
194
194
|
}, [])
|
|
195
195
|
|
|
196
|
-
// Close expanded modal on Escape
|
|
197
|
-
useEffect(() => {
|
|
198
|
-
if (!expanded) return
|
|
199
|
-
function handleKeyDown(e) {
|
|
200
|
-
if (e.key === 'Escape') {
|
|
201
|
-
e.stopPropagation()
|
|
202
|
-
setExpanded(false)
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
document.addEventListener('keydown', handleKeyDown, true)
|
|
206
|
-
return () => document.removeEventListener('keydown', handleKeyDown, true)
|
|
207
|
-
}, [expanded])
|
|
208
|
-
|
|
209
196
|
// Reparent iframe between inline and modal
|
|
210
197
|
useEffect(() => {
|
|
211
198
|
const iframe = iframeRef.current
|
|
@@ -262,8 +249,10 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
|
|
|
262
249
|
handleAction(actionId) {
|
|
263
250
|
if (actionId === 'edit') {
|
|
264
251
|
setEditing(true)
|
|
265
|
-
} else if (actionId === 'expand' || actionId === '
|
|
266
|
-
|
|
252
|
+
} else if (actionId === 'expand' || actionId === 'expand-single') {
|
|
253
|
+
setExpandMode('single')
|
|
254
|
+
} else if (actionId === 'split-screen') {
|
|
255
|
+
setExpandMode('split')
|
|
267
256
|
} else if (actionId === 'open-external') {
|
|
268
257
|
if (rawSrc) window.open(rawSrc, '_blank', 'noopener')
|
|
269
258
|
} else if (actionId === 'zoom-in') {
|
|
@@ -384,6 +373,7 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
|
|
|
384
373
|
}}
|
|
385
374
|
title={`${prototypeTitle} prototype`}
|
|
386
375
|
sandbox="allow-same-origin allow-scripts allow-forms allow-popups"
|
|
376
|
+
onLoad={(e) => e.target.blur()}
|
|
387
377
|
/>
|
|
388
378
|
</div>
|
|
389
379
|
{!interactive && !expanded && (
|
|
@@ -417,172 +407,58 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
|
|
|
417
407
|
</div>
|
|
418
408
|
{resizable && <ResizeHandle targetRef={embedRef} width={width} height={height} onResize={handleResize} />}
|
|
419
409
|
</WidgetWrapper>
|
|
420
|
-
{
|
|
421
|
-
<
|
|
422
|
-
expanded={expanded}
|
|
423
|
-
onClose={() => setExpanded(false)}
|
|
424
|
-
modalContainerRef={modalContainerRef}
|
|
410
|
+
{expanded && (
|
|
411
|
+
<PrototypeExpandPane
|
|
425
412
|
widgetId={widgetId}
|
|
426
|
-
|
|
427
|
-
|
|
413
|
+
modalContainerRef={modalContainerRef}
|
|
414
|
+
splitMode={expandMode === 'split'}
|
|
415
|
+
onClose={() => setExpandMode(null)}
|
|
416
|
+
/>
|
|
428
417
|
)}
|
|
429
418
|
</>
|
|
430
419
|
)
|
|
431
420
|
})
|
|
432
421
|
|
|
433
422
|
/**
|
|
434
|
-
*
|
|
435
|
-
*
|
|
423
|
+
* Builds pane configs and renders ExpandedPane for an expanded prototype widget.
|
|
424
|
+
* The primary pane is an external pane that receives the iframe via reparenting.
|
|
436
425
|
*/
|
|
437
|
-
function
|
|
438
|
-
const
|
|
439
|
-
() =>
|
|
440
|
-
[
|
|
441
|
-
)
|
|
442
|
-
const hasSplit = Boolean(connectedWidget)
|
|
443
|
-
const paneOrder = useMemo(
|
|
444
|
-
() => (hasSplit ? getPaneOrder(widgetId, connectedWidget) : { primaryIsLeft: true }),
|
|
445
|
-
[hasSplit, widgetId, connectedWidget],
|
|
426
|
+
function PrototypeExpandPane({ widgetId, modalContainerRef, splitMode, onClose }) {
|
|
427
|
+
const connectedWidgets = useMemo(
|
|
428
|
+
() => splitMode ? findAllConnectedSplitTargets(widgetId) : [],
|
|
429
|
+
[widgetId, splitMode],
|
|
446
430
|
)
|
|
447
|
-
const secondaryUrl = useMemo(() => buildSecondaryIframeUrl(connectedWidget), [connectedWidget])
|
|
448
|
-
const isTerminalSecondary = connectedWidget?.type === 'terminal' || connectedWidget?.type === 'terminal-read' || connectedWidget?.type === 'agent'
|
|
449
|
-
const terminalRef = useRef(null)
|
|
450
|
-
const cleanupRef = useRef(null)
|
|
451
|
-
const [activePane, setActivePane] = useState('left')
|
|
452
|
-
|
|
453
|
-
// Build labels for the top bar
|
|
454
431
|
const primaryWidget = useMemo(() => {
|
|
455
432
|
const bridge = window.__storyboardCanvasBridgeState
|
|
456
|
-
return bridge?.widgets?.find((w) => w.id === widgetId) || { type: 'prototype', props: {} }
|
|
457
|
-
}, [widgetId
|
|
458
|
-
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
}
|
|
472
|
-
}, [isTerminalSecondary, expanded, connectedWidget?.id])
|
|
473
|
-
|
|
474
|
-
const primaryPane = (
|
|
475
|
-
<div
|
|
476
|
-
ref={modalContainerRef}
|
|
477
|
-
className={hasSplit ? styles.expandContainerSplit : styles.expandContainer}
|
|
478
|
-
onClick={(e) => e.stopPropagation()}
|
|
479
|
-
onPointerDown={() => setActivePane(paneOrder.primaryIsLeft ? 'left' : 'right')}
|
|
480
|
-
>
|
|
481
|
-
{!hasSplit && <button className={styles.expandClose} onClick={onClose} aria-label="Close expanded view" autoFocus>✕</button>}
|
|
482
|
-
</div>
|
|
483
|
-
)
|
|
484
|
-
|
|
485
|
-
let secondaryPane = null
|
|
486
|
-
const secondarySide = paneOrder.primaryIsLeft ? 'right' : 'left'
|
|
487
|
-
if (hasSplit) {
|
|
488
|
-
if (secondaryUrl) {
|
|
489
|
-
secondaryPane = (
|
|
490
|
-
<div className={styles.expandSecondary} onClick={(e) => e.stopPropagation()} onPointerDown={() => setActivePane(secondarySide)}>
|
|
491
|
-
<iframe src={secondaryUrl} className={styles.expandIframe} title="Connected widget" />
|
|
492
|
-
</div>
|
|
493
|
-
)
|
|
494
|
-
} else if (isTerminalSecondary) {
|
|
495
|
-
secondaryPane = (
|
|
496
|
-
<div className={styles.expandSecondary} onClick={(e) => e.stopPropagation()} onPointerDown={() => setActivePane(secondarySide)}>
|
|
497
|
-
<div ref={terminalRef} className={styles.expandTerminal} />
|
|
498
|
-
</div>
|
|
499
|
-
)
|
|
500
|
-
} else if (connectedWidget?.type === 'markdown') {
|
|
501
|
-
secondaryPane = (
|
|
502
|
-
<div className={styles.expandSecondary} onClick={(e) => e.stopPropagation()} onPointerDown={() => setActivePane(secondarySide)}>
|
|
503
|
-
<MarkdownSecondaryPane content={connectedWidget.props?.content} />
|
|
504
|
-
</div>
|
|
505
|
-
)
|
|
506
|
-
} else if (connectedWidget?.type === 'link-preview') {
|
|
507
|
-
secondaryPane = (
|
|
508
|
-
<div className={styles.expandSecondary} onClick={(e) => e.stopPropagation()} onPointerDown={() => setActivePane(secondarySide)}>
|
|
509
|
-
<LinkPreviewSecondaryPane widget={connectedWidget} />
|
|
510
|
-
</div>
|
|
511
|
-
)
|
|
433
|
+
return bridge?.widgets?.find((w) => w.id === widgetId) || { id: widgetId, type: 'prototype', position: { x: 0, y: 0 }, props: {} }
|
|
434
|
+
}, [widgetId])
|
|
435
|
+
|
|
436
|
+
const buildPaneFn = useCallback((widget) => {
|
|
437
|
+
if (widget.id === widgetId) {
|
|
438
|
+
return {
|
|
439
|
+
id: widgetId,
|
|
440
|
+
label: getSplitPaneLabel(primaryWidget),
|
|
441
|
+
widgetType: 'prototype',
|
|
442
|
+
kind: 'external',
|
|
443
|
+
attach: (container) => {
|
|
444
|
+
modalContainerRef.current = container
|
|
445
|
+
return () => { modalContainerRef.current = null }
|
|
446
|
+
},
|
|
447
|
+
}
|
|
512
448
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
const leftPane = paneOrder.primaryIsLeft ? primaryPane : secondaryPane
|
|
516
|
-
const rightPane = paneOrder.primaryIsLeft ? secondaryPane : primaryPane
|
|
449
|
+
return buildPaneForWidget(widget)
|
|
450
|
+
}, [widgetId, primaryWidget, modalContainerRef])
|
|
517
451
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
style={expanded ? undefined : { display: 'none' }}
|
|
522
|
-
onClick={onClose}
|
|
523
|
-
onPointerDown={(e) => e.stopPropagation()}
|
|
524
|
-
onKeyDown={(e) => e.stopPropagation()}
|
|
525
|
-
onWheel={(e) => e.stopPropagation()}
|
|
526
|
-
>
|
|
527
|
-
{hasSplit ? (
|
|
528
|
-
<div className={styles.expandSplitBody}>
|
|
529
|
-
<SplitScreenTopBar
|
|
530
|
-
leftLabel={leftLabel}
|
|
531
|
-
rightLabel={rightLabel}
|
|
532
|
-
activePane={activePane}
|
|
533
|
-
onClose={onClose}
|
|
534
|
-
/>
|
|
535
|
-
<div className={styles.expandSplitPanes}>
|
|
536
|
-
<div className={styles.expandSplitLeft}>{leftPane}</div>
|
|
537
|
-
<div className={styles.expandSplitRight}>{rightPane}</div>
|
|
538
|
-
</div>
|
|
539
|
-
</div>
|
|
540
|
-
) : (
|
|
541
|
-
primaryPane
|
|
542
|
-
)}
|
|
543
|
-
</div>
|
|
452
|
+
const layout = useMemo(
|
|
453
|
+
() => buildSplitLayout(primaryWidget, connectedWidgets, buildPaneFn),
|
|
454
|
+
[primaryWidget, connectedWidgets, buildPaneFn],
|
|
544
455
|
)
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
/** Minimal markdown renderer for secondary pane */
|
|
548
|
-
function MarkdownSecondaryPane({ content }) {
|
|
549
|
-
const ref = useRef(null)
|
|
550
|
-
useEffect(() => {
|
|
551
|
-
if (!ref.current || !content) return
|
|
552
|
-
let cancelled = false
|
|
553
|
-
;(async () => {
|
|
554
|
-
const { remark } = await import('remark')
|
|
555
|
-
const remarkGfm = (await import('remark-gfm')).default
|
|
556
|
-
const remarkHtml = (await import('remark-html')).default
|
|
557
|
-
if (cancelled) return
|
|
558
|
-
const result = remark().use(remarkGfm).use(remarkHtml, { sanitize: false }).processSync(content)
|
|
559
|
-
if (ref.current) ref.current.innerHTML = String(result).replace(/<a\s/g, '<a target="_blank" rel="noopener noreferrer" ')
|
|
560
|
-
})()
|
|
561
|
-
return () => { cancelled = true }
|
|
562
|
-
}, [content])
|
|
563
|
-
return <div ref={ref} className={styles.expandMarkdownContent} />
|
|
564
|
-
}
|
|
565
456
|
|
|
566
|
-
/** Link preview secondary pane (GitHub card or plain) */
|
|
567
|
-
function LinkPreviewSecondaryPane({ widget }) {
|
|
568
|
-
const { url, title, github } = widget.props || {}
|
|
569
|
-
const bodyRef = useRef(null)
|
|
570
|
-
useEffect(() => {
|
|
571
|
-
if (bodyRef.current && github?.bodyHtml) bodyRef.current.innerHTML = github.bodyHtml
|
|
572
|
-
}, [github?.bodyHtml])
|
|
573
|
-
|
|
574
|
-
if (github) {
|
|
575
|
-
return (
|
|
576
|
-
<div className={styles.expandGithubCard}>
|
|
577
|
-
<div className={styles.expandGithubHeader}>{title || url || 'GitHub'}</div>
|
|
578
|
-
{github.bodyHtml && <div ref={bodyRef} className={styles.expandGithubBody} />}
|
|
579
|
-
</div>
|
|
580
|
-
)
|
|
581
|
-
}
|
|
582
457
|
return (
|
|
583
|
-
<
|
|
584
|
-
|
|
585
|
-
{
|
|
586
|
-
|
|
458
|
+
<ExpandedPane
|
|
459
|
+
initialLayout={layout}
|
|
460
|
+
variant={layout.flat().length <= 1 ? 'modal' : 'full'}
|
|
461
|
+
onClose={onClose}
|
|
462
|
+
/>
|
|
587
463
|
)
|
|
588
464
|
}
|
|
@@ -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)
|
|
@@ -156,6 +155,8 @@ export default forwardRef(function StoryWidget({ id: widgetId, props, onUpdate,
|
|
|
156
155
|
handleAction(actionId) {
|
|
157
156
|
if (actionId === 'show-code') toggleShowCode()
|
|
158
157
|
else if (actionId === 'copy-code') copyCode()
|
|
158
|
+
else if (actionId === 'expand' || actionId === 'expand-single') setExpandMode('single')
|
|
159
|
+
else if (actionId === 'split-screen') setExpandMode('split')
|
|
159
160
|
else if (actionId === 'open-external') {
|
|
160
161
|
const story = getStoryData(storyId)
|
|
161
162
|
if (story?._route) {
|
|
@@ -213,6 +214,7 @@ export default forwardRef(function StoryWidget({ id: widgetId, props, onUpdate,
|
|
|
213
214
|
if (typeof height === 'number') sizeStyle.height = `${height}px`
|
|
214
215
|
|
|
215
216
|
return (
|
|
217
|
+
<>
|
|
216
218
|
<WidgetWrapper>
|
|
217
219
|
<div ref={containerRef} className={styles.container} style={sizeStyle}>
|
|
218
220
|
<div className={styles.header}>
|
|
@@ -247,6 +249,7 @@ export default forwardRef(function StoryWidget({ id: widgetId, props, onUpdate,
|
|
|
247
249
|
src={effectiveSrc}
|
|
248
250
|
className={styles.iframe}
|
|
249
251
|
title={displayName}
|
|
252
|
+
onLoad={(e) => e.target.blur()}
|
|
250
253
|
/>
|
|
251
254
|
</div>
|
|
252
255
|
|
|
@@ -276,5 +279,56 @@ export default forwardRef(function StoryWidget({ id: widgetId, props, onUpdate,
|
|
|
276
279
|
</div>
|
|
277
280
|
{resizable && <ResizeHandle targetRef={containerRef} width={width} height={height} onResize={handleResize} />}
|
|
278
281
|
</WidgetWrapper>
|
|
282
|
+
{expanded && (
|
|
283
|
+
<StoryExpandPane
|
|
284
|
+
widgetId={widgetId}
|
|
285
|
+
storyId={storyId}
|
|
286
|
+
exportName={exportName}
|
|
287
|
+
splitMode={expandMode === 'split'}
|
|
288
|
+
onClose={() => setExpandMode(null)}
|
|
289
|
+
/>
|
|
290
|
+
)}
|
|
291
|
+
</>
|
|
279
292
|
)
|
|
280
293
|
})
|
|
294
|
+
|
|
295
|
+
function StoryExpandPane({ widgetId, storyId, exportName, splitMode, onClose }) {
|
|
296
|
+
const connectedWidgets = useMemo(
|
|
297
|
+
() => splitMode ? findAllConnectedSplitTargets(widgetId) : [],
|
|
298
|
+
[widgetId, splitMode],
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
const primaryWidget = useMemo(() => {
|
|
302
|
+
const bridge = window.__storyboardCanvasBridgeState
|
|
303
|
+
return bridge?.widgets?.find((w) => w.id === widgetId) || { id: widgetId, type: 'story', position: { x: 0, y: 0 }, props: { storyId, exportName } }
|
|
304
|
+
}, [widgetId, storyId, exportName])
|
|
305
|
+
|
|
306
|
+
const buildPaneFn = useCallback((widget) => {
|
|
307
|
+
if (widget.id === widgetId) {
|
|
308
|
+
const url = buildSecondaryIframeUrl({ type: 'story', props: { storyId, exportName } })
|
|
309
|
+
return {
|
|
310
|
+
id: widgetId,
|
|
311
|
+
label: getSplitPaneLabel({ type: 'story', props: { storyId, exportName } }),
|
|
312
|
+
widgetType: 'story',
|
|
313
|
+
kind: 'react',
|
|
314
|
+
render: () => url
|
|
315
|
+
? <iframe src={url} style={{ border: 'none', width: '100%', height: '100%', display: 'block' }} title={storyId} onLoad={(e) => e.target.blur()} />
|
|
316
|
+
: <div style={{ padding: 32, color: 'var(--fgColor-muted)' }}>Story "{storyId}" not found</div>,
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return buildPaneForWidget(widget)
|
|
320
|
+
}, [widgetId, storyId, exportName])
|
|
321
|
+
|
|
322
|
+
const layout = useMemo(
|
|
323
|
+
() => buildSplitLayout(primaryWidget, connectedWidgets, buildPaneFn),
|
|
324
|
+
[primaryWidget, connectedWidgets, buildPaneFn],
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
return (
|
|
328
|
+
<ExpandedPane
|
|
329
|
+
initialLayout={layout}
|
|
330
|
+
variant={layout.flat().length <= 1 ? 'modal' : 'full'}
|
|
331
|
+
onClose={onClose}
|
|
332
|
+
/>
|
|
333
|
+
)
|
|
334
|
+
}
|
|
@@ -56,10 +56,16 @@ export default function TerminalReadWidget({ id, props }) {
|
|
|
56
56
|
if (!canvasId) { setFailed(true); return }
|
|
57
57
|
|
|
58
58
|
const urls = isProduction()
|
|
59
|
-
? [
|
|
59
|
+
? [
|
|
60
|
+
// New flat format: <widgetId>.snapshot.json
|
|
61
|
+
`${baseUrl}_storyboard/terminal-snapshots/${id}.snapshot.json`,
|
|
62
|
+
// Legacy nested format: <canvasDir>/<widgetId>.json
|
|
63
|
+
`${baseUrl}_storyboard/terminal-snapshots/${canvasId.replace(/\//g, '--')}/${id}.json`,
|
|
64
|
+
]
|
|
60
65
|
: [
|
|
61
|
-
`${baseUrl}_storyboard/canvas
|
|
62
|
-
`${baseUrl}_storyboard/terminal-snapshots/${
|
|
66
|
+
`${baseUrl}_storyboard/canvas/terminal-snapshot/${id}`,
|
|
67
|
+
`${baseUrl}_storyboard/terminal-snapshots/${id}.snapshot.json`,
|
|
68
|
+
`${baseUrl}_storyboard/terminal-snapshots/${canvasId.replace(/\//g, '--')}/${id}.json`,
|
|
63
69
|
]
|
|
64
70
|
|
|
65
71
|
for (const url of urls) {
|
|
@@ -68,7 +74,7 @@ export default function TerminalReadWidget({ id, props }) {
|
|
|
68
74
|
if (!res.ok) continue
|
|
69
75
|
const data = await res.json()
|
|
70
76
|
if (cancelled) return
|
|
71
|
-
const text = data.content || data.output || ''
|
|
77
|
+
const text = data.paneContent || data.content || data.output || ''
|
|
72
78
|
setContent(text)
|
|
73
79
|
|
|
74
80
|
const converter = await getConverter()
|
|
@@ -100,7 +106,7 @@ export default function TerminalReadWidget({ id, props }) {
|
|
|
100
106
|
|
|
101
107
|
return (
|
|
102
108
|
<div className={styles.container}>
|
|
103
|
-
<div className={styles.titleBar}>
|
|
109
|
+
<div className={`tc-drag-handle ${styles.titleBar}`}>
|
|
104
110
|
<span>{titleLabel}</span>
|
|
105
111
|
<span className={styles.readOnlyBadge}>read only</span>
|
|
106
112
|
</div>
|
|
@@ -19,10 +19,12 @@
|
|
|
19
19
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
20
20
|
font-size: 11px;
|
|
21
21
|
color: #8b949e;
|
|
22
|
-
pointer-events: none;
|
|
23
22
|
user-select: none;
|
|
24
23
|
white-space: nowrap;
|
|
25
24
|
z-index: 2;
|
|
25
|
+
cursor: grab;
|
|
26
|
+
padding: 2px 6px;
|
|
27
|
+
border-radius: 4px;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
[data-widget-selected] .titleBar {
|