@dfosco/storyboard-react 4.2.0-beta.2 → 4.2.0-beta.21
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 +9 -4
- package/src/AuthModal/AuthModal.jsx +6 -2
- 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 +478 -186
- package/src/CommandPalette/command-palette.css +142 -78
- package/src/Icon.jsx +157 -58
- package/src/Viewfinder.jsx +561 -191
- 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 +10 -6
- package/src/canvas/CanvasPage.jsx +738 -216
- package/src/canvas/CanvasPage.module.css +13 -15
- package/src/canvas/CanvasPage.multiselect.test.jsx +17 -6
- package/src/canvas/ConnectorLayer.jsx +121 -153
- 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/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 +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 +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 +112 -4
- package/src/canvas/widgets/LinkPreview.module.css +127 -0
- package/src/canvas/widgets/MarkdownBlock.jsx +164 -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 -38
- 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 +72 -15
- package/src/canvas/widgets/TerminalReadWidget.jsx +146 -0
- package/src/canvas/widgets/TerminalReadWidget.module.css +94 -0
- package/src/canvas/widgets/TerminalWidget.jsx +496 -69
- package/src/canvas/widgets/TerminalWidget.module.css +271 -8
- package/src/canvas/widgets/TilesWidget.jsx +302 -0
- package/src/canvas/widgets/TilesWidget.module.css +133 -0
- package/src/canvas/widgets/WidgetChrome.jsx +73 -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 +557 -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 +47 -19
- package/src/hooks/useConfig.js +14 -0
- package/src/hooks/usePrototypeReloadGuard.js +64 -0
- 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 +324 -30
|
@@ -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
|
|
|
@@ -64,7 +65,8 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
|
|
|
64
65
|
|
|
65
66
|
const [editing, setEditing] = useState(false)
|
|
66
67
|
const [interactive, setInteractive] = useState(false)
|
|
67
|
-
const [
|
|
68
|
+
const [expandMode, setExpandMode] = useState(null)
|
|
69
|
+
const expanded = expandMode !== null
|
|
68
70
|
const [filter, setFilter] = useState('')
|
|
69
71
|
const [canvasTheme, setCanvasTheme] = useState(() => resolveCanvasThemeFromStorage())
|
|
70
72
|
const inputRef = useRef(null)
|
|
@@ -84,6 +86,8 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
|
|
|
84
86
|
return `${base}${sep}_sb_embed&_sb_hide_branch_bar&_sb_theme_target=prototype&_sb_canvas_theme=${canvasTheme}${hash}`
|
|
85
87
|
}, [rawSrc, canvasTheme])
|
|
86
88
|
|
|
89
|
+
const effectiveSrc = iframeSrc
|
|
90
|
+
|
|
87
91
|
const prototypeIndex = useMemo(() => {
|
|
88
92
|
try { return buildPrototypeIndex() }
|
|
89
93
|
catch { return { folders: [], prototypes: [], globalFlows: [], sorted: { title: { prototypes: [], folders: [] } } } }
|
|
@@ -153,8 +157,8 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
|
|
|
153
157
|
|
|
154
158
|
useIframeDevLogs({
|
|
155
159
|
widget: 'PrototypeEmbed',
|
|
156
|
-
loaded: Boolean(
|
|
157
|
-
src:
|
|
160
|
+
loaded: Boolean(effectiveSrc && interactive),
|
|
161
|
+
src: effectiveSrc,
|
|
158
162
|
})
|
|
159
163
|
|
|
160
164
|
useEffect(() => {
|
|
@@ -189,19 +193,6 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
|
|
|
189
193
|
return () => document.removeEventListener('storyboard:theme:changed', readToolbarTheme)
|
|
190
194
|
}, [])
|
|
191
195
|
|
|
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
196
|
// Reparent iframe between inline and modal
|
|
206
197
|
useEffect(() => {
|
|
207
198
|
const iframe = iframeRef.current
|
|
@@ -212,8 +203,12 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
|
|
|
212
203
|
iframe.className = styles.expandIframe
|
|
213
204
|
iframe.removeAttribute('style')
|
|
214
205
|
const target = modalContainerRef.current
|
|
215
|
-
|
|
216
|
-
|
|
206
|
+
try {
|
|
207
|
+
if (target.moveBefore) target.moveBefore(iframe, target.firstChild)
|
|
208
|
+
else target.prepend(iframe)
|
|
209
|
+
} catch {
|
|
210
|
+
target.prepend(iframe)
|
|
211
|
+
}
|
|
217
212
|
} else if (!expanded && inlineContainerRef.current) {
|
|
218
213
|
if (iframe._savedClassName !== undefined) {
|
|
219
214
|
iframe.className = iframe._savedClassName
|
|
@@ -222,8 +217,12 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
|
|
|
222
217
|
delete iframe._savedStyle
|
|
223
218
|
}
|
|
224
219
|
const target = inlineContainerRef.current
|
|
225
|
-
|
|
226
|
-
|
|
220
|
+
try {
|
|
221
|
+
if (target.moveBefore) target.moveBefore(iframe, null)
|
|
222
|
+
else target.appendChild(iframe)
|
|
223
|
+
} catch {
|
|
224
|
+
target.appendChild(iframe)
|
|
225
|
+
}
|
|
227
226
|
}
|
|
228
227
|
}, [expanded])
|
|
229
228
|
|
|
@@ -250,8 +249,10 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
|
|
|
250
249
|
handleAction(actionId) {
|
|
251
250
|
if (actionId === 'edit') {
|
|
252
251
|
setEditing(true)
|
|
253
|
-
} else if (actionId === 'expand') {
|
|
254
|
-
|
|
252
|
+
} else if (actionId === 'expand' || actionId === 'expand-single') {
|
|
253
|
+
setExpandMode('single')
|
|
254
|
+
} else if (actionId === 'split-screen') {
|
|
255
|
+
setExpandMode('split')
|
|
255
256
|
} else if (actionId === 'open-external') {
|
|
256
257
|
if (rawSrc) window.open(rawSrc, '_blank', 'noopener')
|
|
257
258
|
} else if (actionId === 'zoom-in') {
|
|
@@ -362,7 +363,7 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
|
|
|
362
363
|
>
|
|
363
364
|
<iframe
|
|
364
365
|
ref={iframeRef}
|
|
365
|
-
src={
|
|
366
|
+
src={effectiveSrc}
|
|
366
367
|
className={styles.iframe}
|
|
367
368
|
style={{
|
|
368
369
|
width: width / scale,
|
|
@@ -372,6 +373,7 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
|
|
|
372
373
|
}}
|
|
373
374
|
title={`${prototypeTitle} prototype`}
|
|
374
375
|
sandbox="allow-same-origin allow-scripts allow-forms allow-popups"
|
|
376
|
+
onLoad={(e) => e.target.blur()}
|
|
375
377
|
/>
|
|
376
378
|
</div>
|
|
377
379
|
{!interactive && !expanded && (
|
|
@@ -405,21 +407,58 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
|
|
|
405
407
|
</div>
|
|
406
408
|
{resizable && <ResizeHandle targetRef={embedRef} width={width} height={height} onResize={handleResize} />}
|
|
407
409
|
</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
|
|
410
|
+
{expanded && (
|
|
411
|
+
<PrototypeExpandPane
|
|
412
|
+
widgetId={widgetId}
|
|
413
|
+
modalContainerRef={modalContainerRef}
|
|
414
|
+
splitMode={expandMode === 'split'}
|
|
415
|
+
onClose={() => setExpandMode(null)}
|
|
416
|
+
/>
|
|
422
417
|
)}
|
|
423
418
|
</>
|
|
424
419
|
)
|
|
425
420
|
})
|
|
421
|
+
|
|
422
|
+
/**
|
|
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.
|
|
425
|
+
*/
|
|
426
|
+
function PrototypeExpandPane({ widgetId, modalContainerRef, splitMode, onClose }) {
|
|
427
|
+
const connectedWidgets = useMemo(
|
|
428
|
+
() => splitMode ? findAllConnectedSplitTargets(widgetId) : [],
|
|
429
|
+
[widgetId, splitMode],
|
|
430
|
+
)
|
|
431
|
+
const primaryWidget = useMemo(() => {
|
|
432
|
+
const bridge = window.__storyboardCanvasBridgeState
|
|
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
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return buildPaneForWidget(widget)
|
|
450
|
+
}, [widgetId, primaryWidget, modalContainerRef])
|
|
451
|
+
|
|
452
|
+
const layout = useMemo(
|
|
453
|
+
() => buildSplitLayout(primaryWidget, connectedWidgets, buildPaneFn),
|
|
454
|
+
[primaryWidget, connectedWidgets, buildPaneFn],
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
return (
|
|
458
|
+
<ExpandedPane
|
|
459
|
+
initialLayout={layout}
|
|
460
|
+
variant={layout.flat().length <= 1 ? 'modal' : 'full'}
|
|
461
|
+
onClose={onClose}
|
|
462
|
+
/>
|
|
463
|
+
)
|
|
464
|
+
}
|
|
@@ -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)
|
|
@@ -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) {
|
|
@@ -171,10 +172,13 @@ export default forwardRef(function StoryWidget({ id: widgetId, props, onUpdate,
|
|
|
171
172
|
[storyId, exportName, storyIndexKey],
|
|
172
173
|
)
|
|
173
174
|
|
|
175
|
+
// When paused and not interactive, freeze the iframe src to prevent reloads
|
|
176
|
+
const effectiveSrc = iframeSrc
|
|
177
|
+
|
|
174
178
|
useIframeDevLogs({
|
|
175
179
|
widget: 'StoryWidget',
|
|
176
|
-
loaded: interactive && !showCode && Boolean(
|
|
177
|
-
src:
|
|
180
|
+
loaded: interactive && !showCode && Boolean(effectiveSrc),
|
|
181
|
+
src: effectiveSrc,
|
|
178
182
|
})
|
|
179
183
|
|
|
180
184
|
const displayName = exportName ? `${storyId} / ${exportName}` : storyId
|
|
@@ -192,7 +196,7 @@ export default forwardRef(function StoryWidget({ id: widgetId, props, onUpdate,
|
|
|
192
196
|
)
|
|
193
197
|
}
|
|
194
198
|
|
|
195
|
-
if (!
|
|
199
|
+
if (!effectiveSrc) {
|
|
196
200
|
return (
|
|
197
201
|
<WidgetWrapper>
|
|
198
202
|
<div className={styles.container} ref={containerRef}>
|
|
@@ -210,6 +214,7 @@ export default forwardRef(function StoryWidget({ id: widgetId, props, onUpdate,
|
|
|
210
214
|
if (typeof height === 'number') sizeStyle.height = `${height}px`
|
|
211
215
|
|
|
212
216
|
return (
|
|
217
|
+
<>
|
|
213
218
|
<WidgetWrapper>
|
|
214
219
|
<div ref={containerRef} className={styles.container} style={sizeStyle}>
|
|
215
220
|
<div className={styles.header}>
|
|
@@ -241,9 +246,10 @@ export default forwardRef(function StoryWidget({ id: widgetId, props, onUpdate,
|
|
|
241
246
|
<div className={styles.content}>
|
|
242
247
|
<iframe
|
|
243
248
|
ref={iframeRef}
|
|
244
|
-
src={
|
|
249
|
+
src={effectiveSrc}
|
|
245
250
|
className={styles.iframe}
|
|
246
251
|
title={displayName}
|
|
252
|
+
onLoad={(e) => e.target.blur()}
|
|
247
253
|
/>
|
|
248
254
|
</div>
|
|
249
255
|
|
|
@@ -273,5 +279,56 @@ export default forwardRef(function StoryWidget({ id: widgetId, props, onUpdate,
|
|
|
273
279
|
</div>
|
|
274
280
|
{resizable && <ResizeHandle targetRef={containerRef} width={width} height={height} onResize={handleResize} />}
|
|
275
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
|
+
</>
|
|
276
292
|
)
|
|
277
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
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { useRef, useEffect, useState } from 'react'
|
|
2
|
+
import { readProp, schemas } from './widgetProps.js'
|
|
3
|
+
import styles from './TerminalReadWidget.module.css'
|
|
4
|
+
|
|
5
|
+
const terminalSchema = schemas['terminal']
|
|
6
|
+
|
|
7
|
+
let Convert = null
|
|
8
|
+
let ansiLoadAttempted = false
|
|
9
|
+
|
|
10
|
+
async function getConverter() {
|
|
11
|
+
if (Convert) return new Convert({ fg: '#e6edf3', bg: '#0d1117', newline: true })
|
|
12
|
+
if (ansiLoadAttempted) return null
|
|
13
|
+
ansiLoadAttempted = true
|
|
14
|
+
try {
|
|
15
|
+
const mod = await import(/* @vite-ignore */ 'ansi-to-html')
|
|
16
|
+
Convert = mod.default || mod
|
|
17
|
+
return new Convert({ fg: '#e6edf3', bg: '#0d1117', newline: true })
|
|
18
|
+
} catch {
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function stripAnsi(text) {
|
|
24
|
+
// eslint-disable-next-line no-control-regex
|
|
25
|
+
return text.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getBaseUrl() {
|
|
29
|
+
const base = (typeof import.meta !== 'undefined' && import.meta.env?.BASE_URL) || '/'
|
|
30
|
+
return base.endsWith('/') ? base : base + '/'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getCanvasId() {
|
|
34
|
+
return window.__storyboardCanvasBridgeState?.canvasId || null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isProduction() {
|
|
38
|
+
return typeof import.meta !== 'undefined' && import.meta.env?.PROD
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default function TerminalReadWidget({ id, props }) {
|
|
42
|
+
const width = readProp(props, 'width', terminalSchema)
|
|
43
|
+
const height = readProp(props, 'height', terminalSchema)
|
|
44
|
+
const prettyName = props?.prettyName || '...'
|
|
45
|
+
|
|
46
|
+
const [content, setContent] = useState(null)
|
|
47
|
+
const [html, setHtml] = useState(null)
|
|
48
|
+
const [failed, setFailed] = useState(false)
|
|
49
|
+
const contentRef = useRef(null)
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
let cancelled = false
|
|
53
|
+
async function fetchSnapshot() {
|
|
54
|
+
const baseUrl = getBaseUrl()
|
|
55
|
+
const canvasId = getCanvasId()
|
|
56
|
+
if (!canvasId) { setFailed(true); return }
|
|
57
|
+
|
|
58
|
+
const urls = isProduction()
|
|
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
|
+
]
|
|
65
|
+
: [
|
|
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`,
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
for (const url of urls) {
|
|
72
|
+
try {
|
|
73
|
+
const res = await fetch(url)
|
|
74
|
+
if (!res.ok) continue
|
|
75
|
+
const data = await res.json()
|
|
76
|
+
if (cancelled) return
|
|
77
|
+
const text = data.paneContent || data.content || data.output || ''
|
|
78
|
+
setContent(text)
|
|
79
|
+
|
|
80
|
+
const converter = await getConverter()
|
|
81
|
+
if (cancelled) return
|
|
82
|
+
if (converter) {
|
|
83
|
+
setHtml(converter.toHtml(text))
|
|
84
|
+
} else {
|
|
85
|
+
setContent(stripAnsi(text))
|
|
86
|
+
}
|
|
87
|
+
return
|
|
88
|
+
} catch {
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (!cancelled) setFailed(true)
|
|
93
|
+
}
|
|
94
|
+
fetchSnapshot()
|
|
95
|
+
return () => { cancelled = true }
|
|
96
|
+
}, [id])
|
|
97
|
+
|
|
98
|
+
// Auto-scroll to bottom
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (contentRef.current) {
|
|
101
|
+
contentRef.current.scrollTop = contentRef.current.scrollHeight
|
|
102
|
+
}
|
|
103
|
+
}, [html, content])
|
|
104
|
+
|
|
105
|
+
const titleLabel = `terminal · ${prettyName}`
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div className={styles.container}>
|
|
109
|
+
<div className={`tc-drag-handle ${styles.titleBar}`}>
|
|
110
|
+
<span>{titleLabel}</span>
|
|
111
|
+
<span className={styles.readOnlyBadge}>read only</span>
|
|
112
|
+
</div>
|
|
113
|
+
<div
|
|
114
|
+
ref={contentRef}
|
|
115
|
+
className={styles.content}
|
|
116
|
+
style={{
|
|
117
|
+
...(typeof width === 'number' ? { width: `${width}px` } : undefined),
|
|
118
|
+
...(typeof height === 'number' ? { height: `${height}px` } : undefined),
|
|
119
|
+
}}
|
|
120
|
+
>
|
|
121
|
+
{failed && (
|
|
122
|
+
<div className={styles.placeholder}>
|
|
123
|
+
<span className={styles.placeholderTitle}>Terminal session · {prettyName}</span>
|
|
124
|
+
<span className={styles.placeholderSub}>No captured output available</span>
|
|
125
|
+
</div>
|
|
126
|
+
)}
|
|
127
|
+
{!failed && content === null && (
|
|
128
|
+
<div className={styles.placeholder}>
|
|
129
|
+
<span className={styles.placeholderSub}>Loading…</span>
|
|
130
|
+
</div>
|
|
131
|
+
)}
|
|
132
|
+
{!failed && html && (
|
|
133
|
+
<pre
|
|
134
|
+
style={{ margin: 0, whiteSpace: 'pre', fontFamily: 'inherit', fontSize: 'inherit', lineHeight: 'inherit' }}
|
|
135
|
+
dangerouslySetInnerHTML={{ __html: html }}
|
|
136
|
+
/>
|
|
137
|
+
)}
|
|
138
|
+
{!failed && content !== null && !html && (
|
|
139
|
+
<pre style={{ margin: 0, whiteSpace: 'pre', fontFamily: 'inherit', fontSize: 'inherit', lineHeight: 'inherit' }}>
|
|
140
|
+
{content}
|
|
141
|
+
</pre>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
)
|
|
146
|
+
}
|