@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.
Files changed (79) hide show
  1. package/package.json +3 -3
  2. package/src/BranchBar/BranchBar.jsx +3 -1
  3. package/src/BranchBar/BranchBar.module.css +2 -2
  4. package/src/BranchBar/useBranches.js +20 -6
  5. package/src/BranchBar/useBranches.test.js +68 -0
  6. package/src/CommandPalette/CommandPalette.jsx +250 -61
  7. package/src/CommandPalette/command-palette.css +12 -0
  8. package/src/Icon.jsx +46 -11
  9. package/src/Viewfinder.jsx +53 -133
  10. package/src/Viewfinder.module.css +20 -91
  11. package/src/Workspace.jsx +7 -0
  12. package/src/canvas/CanvasPage.jsx +601 -62
  13. package/src/canvas/CanvasPage.module.css +15 -2
  14. package/src/canvas/CanvasPage.multiselect.test.jsx +7 -0
  15. package/src/canvas/ConnectorLayer.jsx +120 -152
  16. package/src/canvas/ConnectorLayer.module.css +69 -0
  17. package/src/canvas/canvasApi.js +68 -2
  18. package/src/canvas/connectorGeometry.js +132 -0
  19. package/src/canvas/hotPoolDevLogs.js +25 -0
  20. package/src/canvas/useMarqueeSelect.js +30 -4
  21. package/src/canvas/widgets/CodePenEmbed.jsx +1 -0
  22. package/src/canvas/widgets/ComponentSetWidget.jsx +199 -0
  23. package/src/canvas/widgets/ComponentSetWidget.module.css +89 -0
  24. package/src/canvas/widgets/ComponentWidget.jsx +1 -0
  25. package/src/canvas/widgets/CropOverlay.jsx +219 -0
  26. package/src/canvas/widgets/CropOverlay.module.css +118 -0
  27. package/src/canvas/widgets/ExpandedPane.jsx +472 -0
  28. package/src/canvas/widgets/ExpandedPane.module.css +179 -0
  29. package/src/canvas/widgets/ExpandedPane.test.jsx +240 -0
  30. package/src/canvas/widgets/ExpandedPaneTopBar.jsx +111 -0
  31. package/src/canvas/widgets/ExpandedPaneTopBar.module.css +59 -0
  32. package/src/canvas/widgets/ExpandedPaneTopBar.test.jsx +45 -0
  33. package/src/canvas/widgets/FigmaEmbed.jsx +49 -102
  34. package/src/canvas/widgets/ImageWidget.jsx +129 -8
  35. package/src/canvas/widgets/ImageWidget.module.css +30 -0
  36. package/src/canvas/widgets/LinkPreview.jsx +93 -44
  37. package/src/canvas/widgets/MarkdownBlock.jsx +141 -16
  38. package/src/canvas/widgets/MarkdownBlock.module.css +25 -0
  39. package/src/canvas/widgets/PromptWidget.jsx +414 -0
  40. package/src/canvas/widgets/PromptWidget.module.css +273 -0
  41. package/src/canvas/widgets/PrototypeEmbed.jsx +46 -170
  42. package/src/canvas/widgets/ResizeHandle.jsx +17 -6
  43. package/src/canvas/widgets/StoryWidget.jsx +65 -11
  44. package/src/canvas/widgets/TerminalReadWidget.jsx +11 -5
  45. package/src/canvas/widgets/TerminalReadWidget.module.css +3 -1
  46. package/src/canvas/widgets/TerminalWidget.jsx +301 -124
  47. package/src/canvas/widgets/TerminalWidget.module.css +121 -12
  48. package/src/canvas/widgets/TilesWidget.jsx +302 -0
  49. package/src/canvas/widgets/TilesWidget.module.css +133 -0
  50. package/src/canvas/widgets/WidgetChrome.jsx +67 -152
  51. package/src/canvas/widgets/WidgetChrome.module.css +20 -1
  52. package/src/canvas/widgets/expandUtils.js +385 -16
  53. package/src/canvas/widgets/expandUtils.test.js +155 -0
  54. package/src/canvas/widgets/index.js +6 -2
  55. package/src/canvas/widgets/tilePool.js +23 -0
  56. package/src/canvas/widgets/tiles/diagonal-bl.png +0 -0
  57. package/src/canvas/widgets/tiles/diagonal-br.png +0 -0
  58. package/src/canvas/widgets/tiles/diagonal-tl.png +0 -0
  59. package/src/canvas/widgets/tiles/leaf.png +0 -0
  60. package/src/canvas/widgets/tiles/quarter-tl.png +0 -0
  61. package/src/canvas/widgets/tiles/quarter-tr.png +0 -0
  62. package/src/canvas/widgets/tiles/solid-a.png +0 -0
  63. package/src/canvas/widgets/tiles/solid-b.png +0 -0
  64. package/src/canvas/widgets/widgetConfig.js +37 -4
  65. package/src/canvas/widgets/widgetIcons.jsx +190 -0
  66. package/src/canvas/widgets/widgetProps.js +1 -0
  67. package/src/context.jsx +47 -19
  68. package/src/hooks/usePrototypeReloadGuard.js +64 -0
  69. package/src/index.js +4 -2
  70. package/src/story/ComponentSetPage.jsx +186 -0
  71. package/src/story/ComponentSetPage.module.css +121 -0
  72. package/src/story/StoryPage.jsx +32 -2
  73. package/src/vite/data-plugin.js +79 -35
  74. package/src/canvas/widgets/ActionWidget.jsx +0 -200
  75. package/src/canvas/widgets/ActionWidget.module.css +0 -122
  76. package/src/canvas/widgets/SplitExpandModal.jsx +0 -234
  77. package/src/canvas/widgets/SplitExpandModal.module.css +0 -335
  78. package/src/canvas/widgets/SplitScreenTopBar.jsx +0 -30
  79. package/src/canvas/widgets/SplitScreenTopBar.module.css +0 -58
@@ -0,0 +1,472 @@
1
+ /**
2
+ * ExpandedPane — unified full-screen expand/split-screen portal for canvas widgets.
3
+ *
4
+ * Supports two display variants for single-pane mode:
5
+ * - "modal" — 90vw × 90vh centered card (prototype, figma, markdown, link-preview)
6
+ * - "full" — fixed inset 0, no border-radius (terminal, agent)
7
+ *
8
+ * Multi-pane (split-screen) always uses "full" layout with CSS grid columns.
9
+ * Each column can contain 1 or 2 panes stacked vertically (row split).
10
+ *
11
+ * Layout is a 2D array: PaneConfig[][] where outer = columns, inner = rows.
12
+ * Supports up to 2 columns × 2 rows (4 panes max).
13
+ *
14
+ * Each pane provides either:
15
+ * - kind: 'react' + render prop (for normal React content)
16
+ * - kind: 'external' + attach/detach (for imperative DOM like terminals/iframes)
17
+ *
18
+ * ExpandedPane owns container measurement and ResizeObserver. It notifies external
19
+ * panes via onResize(rect) when their container dimensions change, so they don't
20
+ * have to guess layout timing.
21
+ */
22
+ import { useState, useEffect, useLayoutEffect, useRef, useCallback, useMemo } from 'react'
23
+ import { createPortal } from 'react-dom'
24
+ import ExpandedPaneTopBar from './ExpandedPaneTopBar.jsx'
25
+ import styles from './ExpandedPane.module.css'
26
+
27
+ const MIN_PANE_WIDTH_PX = 120
28
+ const MIN_PANE_HEIGHT_PX = 80
29
+
30
+ /**
31
+ * @typedef {Object} ReactPane
32
+ * @property {string} id — stable identifier (widgetId)
33
+ * @property {string} label — display label for top bar
34
+ * @property {'react'} kind
35
+ * @property {() => React.ReactNode} render — returns React content for the pane
36
+ */
37
+
38
+ /**
39
+ * @typedef {Object} ExternalPane
40
+ * @property {string} id — stable identifier (widgetId)
41
+ * @property {string} label — display label for top bar
42
+ * @property {'external'} kind
43
+ * @property {(container: HTMLElement) => (() => void)} attach — mount into container, return detach
44
+ * @property {(rect: DOMRect) => void} [onResize] — called when container resizes
45
+ */
46
+
47
+ /**
48
+ * @typedef {ReactPane | ExternalPane} PaneConfig
49
+ */
50
+
51
+ /**
52
+ * @param {Object} props
53
+ * @param {PaneConfig[]} [props.initialPanes] — flat pane list (backward compat, each becomes a single-row column)
54
+ * @param {PaneConfig[][]} [props.initialLayout] — 2D layout: outer = columns, inner = rows within column
55
+ * @param {'modal' | 'full'} [props.variant='modal'] — single-pane display variant
56
+ * @param {() => void} props.onClose — close callback
57
+ * @param {((panes: PaneConfig[]) => void)} [props.onPanesChange] — notify parent of pane changes
58
+ */
59
+ export default function ExpandedPane({ initialPanes, initialLayout, variant = 'modal', onClose, onPanesChange }) {
60
+ // Normalize to 2D layout: outer = columns, inner = rows
61
+ const [layout, setLayout] = useState(() => {
62
+ if (initialLayout) return initialLayout
63
+ if (initialPanes) return initialPanes.map((p) => [p])
64
+ return []
65
+ })
66
+
67
+ // Sync layout when initialLayout changes (preserves column/row sizes)
68
+ useEffect(() => {
69
+ if (initialLayout) setLayout(initialLayout)
70
+ }, [initialLayout])
71
+
72
+ // Force re-render when pane actions change (e.g. markdown edit toggle)
73
+ const [, forceUpdate] = useState(0)
74
+ useEffect(() => {
75
+ const handler = () => forceUpdate((n) => n + 1)
76
+ document.addEventListener('storyboard:expanded-pane:refresh', handler)
77
+ return () => document.removeEventListener('storyboard:expanded-pane:refresh', handler)
78
+ }, [])
79
+
80
+ const allPanes = useMemo(() => layout.flat(), [layout])
81
+
82
+ const [columnSizes, setColumnSizes] = useState(() => layout.map(() => '1fr'))
83
+ // Row ratios per column: rowRatios[colIdx] = [ratio, ratio, ...] (flex values)
84
+ const [rowRatios, setRowRatios] = useState(() =>
85
+ layout.map((col) => col.map(() => 1)),
86
+ )
87
+ const [activePaneId, setActivePaneId] = useState(null)
88
+
89
+ // Ref map: paneId → container DOM element (callback refs)
90
+ const containerRefs = useRef(new Map())
91
+ // Ref map: paneId → detach cleanup function
92
+ const detachRefs = useRef(new Map())
93
+ // Ref map: paneId → ResizeObserver
94
+ const observerRefs = useRef(new Map())
95
+
96
+ const totalPanes = allPanes.length
97
+ const isSplit = totalPanes >= 2
98
+ const useFullLayout = isSplit || variant === 'full'
99
+
100
+ // ── External pane attach/detach via useLayoutEffect ──
101
+ useLayoutEffect(() => {
102
+ for (const pane of allPanes) {
103
+ if (pane.kind !== 'external') continue
104
+ const container = containerRefs.current.get(pane.id)
105
+ if (!container) continue
106
+ if (detachRefs.current.has(pane.id)) continue
107
+ const detach = pane.attach(container)
108
+ detachRefs.current.set(pane.id, detach)
109
+ }
110
+ return () => {
111
+ for (const [id, detach] of detachRefs.current) {
112
+ detach?.()
113
+ }
114
+ detachRefs.current.clear()
115
+ }
116
+ }, []) // eslint-disable-line react-hooks/exhaustive-deps — intentional mount-only
117
+
118
+ // Handle pane list changes: attach new external panes, detach removed ones
119
+ useLayoutEffect(() => {
120
+ const currentIds = new Set(allPanes.map((p) => p.id))
121
+
122
+ for (const [id, detach] of detachRefs.current) {
123
+ if (!currentIds.has(id)) {
124
+ detach?.()
125
+ detachRefs.current.delete(id)
126
+ observerRefs.current.get(id)?.disconnect()
127
+ observerRefs.current.delete(id)
128
+ }
129
+ }
130
+
131
+ for (const pane of allPanes) {
132
+ if (pane.kind !== 'external') continue
133
+ if (detachRefs.current.has(pane.id)) continue
134
+ const container = containerRefs.current.get(pane.id)
135
+ if (!container) continue
136
+ const detach = pane.attach(container)
137
+ detachRefs.current.set(pane.id, detach)
138
+ }
139
+ }) // runs every render to catch pane changes
140
+
141
+ // ── ResizeObserver per external pane ──
142
+ useEffect(() => {
143
+ for (const pane of allPanes) {
144
+ if (pane.kind !== 'external' || !pane.onResize) continue
145
+ if (observerRefs.current.has(pane.id)) continue
146
+ const container = containerRefs.current.get(pane.id)
147
+ if (!container) continue
148
+ const ro = new ResizeObserver((entries) => {
149
+ for (const entry of entries) {
150
+ pane.onResize(entry.contentRect)
151
+ }
152
+ })
153
+ ro.observe(container)
154
+ observerRefs.current.set(pane.id, ro)
155
+ }
156
+ return () => {
157
+ for (const ro of observerRefs.current.values()) {
158
+ ro.disconnect()
159
+ }
160
+ observerRefs.current.clear()
161
+ }
162
+ }, [allPanes])
163
+
164
+ // ── Callback ref factory: stable per pane id ──
165
+ const getContainerRef = useCallback((paneId) => (el) => {
166
+ if (el) {
167
+ containerRefs.current.set(paneId, el)
168
+ } else {
169
+ containerRefs.current.delete(paneId)
170
+ }
171
+ }, [])
172
+
173
+ // ── Escape to close ──
174
+ useEffect(() => {
175
+ function handleKeyDown(e) {
176
+ if (e.key === 'Escape') {
177
+ e.stopPropagation()
178
+ onClose()
179
+ }
180
+ }
181
+ document.addEventListener('keydown', handleKeyDown, true)
182
+ return () => document.removeEventListener('keydown', handleKeyDown, true)
183
+ }, [onClose])
184
+
185
+ // ── Column drag-to-resize dividers ──
186
+ const dragState = useRef(null)
187
+
188
+ const handleColumnDividerPointerDown = useCallback((e, dividerIndex) => {
189
+ e.preventDefault()
190
+ const gridEl = e.target.closest(`.${styles.grid}`)
191
+ if (!gridEl) return
192
+
193
+ const currentCols = Array.from(gridEl.children)
194
+ .filter((el) => !el.classList.contains(styles.divider))
195
+ .map((el) => el.getBoundingClientRect().width)
196
+
197
+ dragState.current = {
198
+ kind: 'column',
199
+ dividerIndex,
200
+ startX: e.clientX,
201
+ startWidths: currentCols,
202
+ }
203
+
204
+ function handleMove(ev) {
205
+ if (!dragState.current || dragState.current.kind !== 'column') return
206
+ const { dividerIndex: di, startX, startWidths } = dragState.current
207
+ const dx = ev.clientX - startX
208
+ const leftW = Math.max(MIN_PANE_WIDTH_PX, startWidths[di] + dx)
209
+ const rightW = Math.max(MIN_PANE_WIDTH_PX, startWidths[di + 1] - dx)
210
+ const newWidths = [...startWidths]
211
+ newWidths[di] = leftW
212
+ newWidths[di + 1] = rightW
213
+ const total = newWidths.reduce((a, b) => a + b, 0)
214
+ setColumnSizes(newWidths.map((w) => `${((w / total) * layout.length).toFixed(3)}fr`))
215
+ }
216
+
217
+ function handleUp() {
218
+ dragState.current = null
219
+ document.removeEventListener('pointermove', handleMove)
220
+ document.removeEventListener('pointerup', handleUp)
221
+ }
222
+
223
+ document.addEventListener('pointermove', handleMove)
224
+ document.addEventListener('pointerup', handleUp)
225
+ }, [layout.length])
226
+
227
+ // ── Row drag-to-resize dividers (vertical splits within a column) ──
228
+ const handleRowDividerPointerDown = useCallback((e, colIndex) => {
229
+ e.preventDefault()
230
+ const columnEl = e.target.closest(`.${styles.column}`)
231
+ if (!columnEl) return
232
+
233
+ const paneEls = Array.from(columnEl.children).filter(
234
+ (el) => !el.classList.contains(styles.rowDivider),
235
+ )
236
+ const startHeights = paneEls.map((el) => el.getBoundingClientRect().height)
237
+
238
+ dragState.current = {
239
+ kind: 'row',
240
+ colIndex,
241
+ startY: e.clientY,
242
+ startHeights,
243
+ }
244
+
245
+ function handleMove(ev) {
246
+ if (!dragState.current || dragState.current.kind !== 'row') return
247
+ const { colIndex: ci, startY, startHeights: sh } = dragState.current
248
+ const dy = ev.clientY - startY
249
+ const topH = Math.max(MIN_PANE_HEIGHT_PX, sh[0] + dy)
250
+ const bottomH = Math.max(MIN_PANE_HEIGHT_PX, sh[1] - dy)
251
+ const total = topH + bottomH
252
+ setRowRatios((prev) => {
253
+ const next = [...prev]
254
+ next[ci] = [topH / total, bottomH / total]
255
+ return next
256
+ })
257
+ }
258
+
259
+ function handleUp() {
260
+ dragState.current = null
261
+ document.removeEventListener('pointermove', handleMove)
262
+ document.removeEventListener('pointerup', handleUp)
263
+ }
264
+
265
+ document.addEventListener('pointermove', handleMove)
266
+ document.addEventListener('pointerup', handleUp)
267
+ }, [])
268
+
269
+ // ── Build grid-template-columns ──
270
+ const gridTemplateColumns = useMemo(() => {
271
+ if (layout.length < 2) return undefined
272
+ const parts = []
273
+ for (let i = 0; i < columnSizes.length; i++) {
274
+ if (i > 0) parts.push('0px')
275
+ parts.push(columnSizes[i])
276
+ }
277
+ return parts.join(' ')
278
+ }, [layout.length, columnSizes])
279
+
280
+ // ── Render pane content ──
281
+ function renderPaneContent(pane) {
282
+ if (pane.kind === 'react') {
283
+ return (
284
+ <div
285
+ ref={getContainerRef(pane.id)}
286
+ className={styles.paneContent}
287
+ onPointerDown={() => setActivePaneId(pane.id)}
288
+ >
289
+ {pane.render()}
290
+ </div>
291
+ )
292
+ }
293
+ return (
294
+ <div
295
+ ref={getContainerRef(pane.id)}
296
+ className={styles.paneContent}
297
+ onPointerDown={() => setActivePaneId(pane.id)}
298
+ />
299
+ )
300
+ }
301
+
302
+ // ── Render a single column (1 or 2 panes stacked) ──
303
+ function renderColumn(column, colIndex, isLastCol) {
304
+ if (column.length === 1) {
305
+ const pane = column[0]
306
+ return (
307
+ <div className={styles.pane} key={pane.id}>
308
+ <ExpandedPaneTopBar
309
+ label={pane.label}
310
+ widgetType={pane.widgetType}
311
+ actions={pane.actions}
312
+ features={pane.features}
313
+ getState={pane.getState}
314
+ onAction={pane.onAction}
315
+ showClose={isLastCol}
316
+ onClose={onClose}
317
+ />
318
+ {renderPaneContent(pane)}
319
+ </div>
320
+ )
321
+ }
322
+
323
+ // Multi-row column
324
+ const ratios = rowRatios[colIndex] || column.map(() => 1)
325
+ return (
326
+ <div className={styles.column} key={`col-${colIndex}`}>
327
+ {column.map((pane, rowIdx) => (
328
+ <PaneWithRowDivider
329
+ key={pane.id}
330
+ pane={pane}
331
+ flex={ratios[rowIdx] ?? 1}
332
+ isLast={rowIdx === column.length - 1}
333
+ colIndex={colIndex}
334
+ onRowDividerPointerDown={handleRowDividerPointerDown}
335
+ >
336
+ <ExpandedPaneTopBar
337
+ label={pane.label}
338
+ widgetType={pane.widgetType}
339
+ actions={pane.actions}
340
+ features={pane.features}
341
+ getState={pane.getState}
342
+ onAction={pane.onAction}
343
+ showClose={isLastCol && rowIdx === 0}
344
+ onClose={onClose}
345
+ />
346
+ {renderPaneContent(pane)}
347
+ </PaneWithRowDivider>
348
+ ))}
349
+ </div>
350
+ )
351
+ }
352
+
353
+ // ── Single-pane modal variant ──
354
+ if (!isSplit && variant === 'modal') {
355
+ const pane = allPanes[0]
356
+ if (!pane) return null
357
+ return createPortal(
358
+ <div
359
+ className={styles.backdrop}
360
+ onClick={onClose}
361
+ onPointerDown={(e) => e.stopPropagation()}
362
+ onKeyDown={(e) => e.stopPropagation()}
363
+ onWheel={(e) => e.stopPropagation()}
364
+ >
365
+ <div className={styles.modalContainer} onClick={(e) => e.stopPropagation()}>
366
+ <ExpandedPaneTopBar
367
+ label={pane.label}
368
+ widgetType={pane.widgetType}
369
+ actions={pane.actions}
370
+ features={pane.features}
371
+ getState={pane.getState}
372
+ onAction={pane.onAction}
373
+ showClose
374
+ onClose={onClose}
375
+ />
376
+ <div className={styles.modalBody}>
377
+ {renderPaneContent(pane)}
378
+ </div>
379
+ </div>
380
+ </div>,
381
+ document.body,
382
+ )
383
+ }
384
+
385
+ // ── Full layout (single-pane full or multi-pane split) ──
386
+ return createPortal(
387
+ <div
388
+ className={styles.fullContainer}
389
+ onPointerDown={(e) => e.stopPropagation()}
390
+ onKeyDown={(e) => e.stopPropagation()}
391
+ onWheel={(e) => e.stopPropagation()}
392
+ >
393
+ {isSplit ? (
394
+ <div
395
+ className={styles.grid}
396
+ style={{ gridTemplateColumns }}
397
+ >
398
+ {layout.map((column, colIdx) => {
399
+ const isLastCol = colIdx === layout.length - 1
400
+ return (
401
+ <ColumnWithDivider
402
+ key={`col-${colIdx}`}
403
+ colIndex={colIdx}
404
+ isLast={isLastCol}
405
+ onDividerPointerDown={handleColumnDividerPointerDown}
406
+ >
407
+ {renderColumn(column, colIdx, isLastCol)}
408
+ </ColumnWithDivider>
409
+ )
410
+ })}
411
+ </div>
412
+ ) : (
413
+ <>
414
+ <ExpandedPaneTopBar
415
+ label={allPanes[0]?.label}
416
+ widgetType={allPanes[0]?.widgetType}
417
+ actions={allPanes[0]?.actions}
418
+ features={allPanes[0]?.features}
419
+ getState={allPanes[0]?.getState}
420
+ onAction={allPanes[0]?.onAction}
421
+ showClose
422
+ onClose={onClose}
423
+ />
424
+ <div className={styles.singleFull}>
425
+ {renderPaneContent(allPanes[0])}
426
+ </div>
427
+ </>
428
+ )}
429
+ </div>,
430
+ document.body,
431
+ )
432
+ }
433
+
434
+ /**
435
+ * Wraps a column cell in the grid, optionally followed by a vertical divider.
436
+ */
437
+ function ColumnWithDivider({ colIndex, isLast, onDividerPointerDown, children }) {
438
+ return (
439
+ <>
440
+ {children}
441
+ {!isLast && (
442
+ <div
443
+ className={styles.divider}
444
+ onPointerDown={(e) => onDividerPointerDown(e, colIndex)}
445
+ role="separator"
446
+ aria-orientation="vertical"
447
+ />
448
+ )}
449
+ </>
450
+ )
451
+ }
452
+
453
+ /**
454
+ * Renders a pane within a row-split column, optionally followed by a horizontal row divider.
455
+ */
456
+ function PaneWithRowDivider({ pane, flex, isLast, colIndex, onRowDividerPointerDown, children }) {
457
+ return (
458
+ <>
459
+ <div className={styles.pane} style={{ flex }}>
460
+ {children}
461
+ </div>
462
+ {!isLast && (
463
+ <div
464
+ className={styles.rowDivider}
465
+ onPointerDown={(e) => onRowDividerPointerDown(e, colIndex)}
466
+ role="separator"
467
+ aria-orientation="horizontal"
468
+ />
469
+ )}
470
+ </>
471
+ )
472
+ }
@@ -0,0 +1,179 @@
1
+ /* ── ExpandedPane styles ──────────────────────────────────────────── */
2
+
3
+ /* ── Backdrop (modal variant only) ───────────────────────────────── */
4
+
5
+ .backdrop {
6
+ position: fixed;
7
+ inset: 0;
8
+ z-index: 100000;
9
+ background: rgba(0, 0, 0, 0.8);
10
+ display: flex;
11
+ align-items: center;
12
+ justify-content: center;
13
+ animation: expandedPaneFadeIn 0.15s ease;
14
+ }
15
+
16
+ @keyframes expandedPaneFadeIn {
17
+ from { opacity: 0; }
18
+ to { opacity: 1; }
19
+ }
20
+
21
+ /* ── Modal container (single-pane modal variant) ─────────────────── */
22
+
23
+ .modalContainer {
24
+ width: 90vw;
25
+ height: 90vh;
26
+ position: relative;
27
+ border-radius: 12px;
28
+ overflow: hidden;
29
+ background: var(--bgColor-default, #ffffff);
30
+ box-shadow: 0 24px 64px rgba(0, 0, 0, 0.4);
31
+ display: flex;
32
+ flex-direction: column;
33
+ animation: expandedPaneScaleIn 0.2s ease;
34
+ }
35
+
36
+ @keyframes expandedPaneScaleIn {
37
+ from { transform: scale(0.95); opacity: 0; }
38
+ to { transform: scale(1); opacity: 1; }
39
+ }
40
+
41
+ .modalBody {
42
+ flex: 1;
43
+ min-height: 0;
44
+ display: flex;
45
+ }
46
+
47
+ /* ── Full container (single-pane full + multi-pane split) ────────── */
48
+
49
+ .fullContainer {
50
+ position: fixed;
51
+ inset: 0;
52
+ z-index: 100000;
53
+ display: flex;
54
+ flex-direction: column;
55
+ overflow: hidden;
56
+ background: var(--bgColor-default, #ffffff);
57
+ animation: expandedPaneScaleIn 0.2s ease;
58
+ }
59
+
60
+ /* ── Single-pane full body ───────────────────────────────────────── */
61
+
62
+ .singleFull {
63
+ flex: 1;
64
+ min-height: 0;
65
+ display: flex;
66
+ }
67
+
68
+ /* ── CSS Grid (multi-pane split) ─────────────────────────────────── */
69
+
70
+ .grid {
71
+ flex: 1;
72
+ min-height: 0;
73
+ display: grid;
74
+ /* grid-template-columns set via inline style */
75
+ }
76
+
77
+ /* ── Individual pane cell ────────────────────────────────────────── */
78
+
79
+ .pane {
80
+ overflow: hidden;
81
+ min-width: 0;
82
+ min-height: 0;
83
+ position: relative;
84
+ display: flex;
85
+ flex-direction: column;
86
+ }
87
+
88
+ /* ── Column container (wraps 1-2 panes vertically) ──────────────── */
89
+
90
+ .column {
91
+ overflow: hidden;
92
+ min-width: 0;
93
+ min-height: 0;
94
+ display: flex;
95
+ flex-direction: column;
96
+ }
97
+
98
+ .column > .pane {
99
+ flex: 1;
100
+ min-height: 0;
101
+ }
102
+
103
+ /* ── Pane content fills remaining space ──────────────────────────── */
104
+
105
+ .paneContent {
106
+ flex: 1;
107
+ min-height: 0;
108
+ width: 100%;
109
+ overflow: auto;
110
+ }
111
+
112
+ /* ── Draggable divider between panes ─────────────────────────────── */
113
+
114
+ .divider {
115
+ width: 8px;
116
+ margin: 0 -4px;
117
+ cursor: col-resize;
118
+ z-index: 1;
119
+ position: relative;
120
+ background: transparent;
121
+ transition: background 100ms;
122
+ }
123
+
124
+ .divider:hover,
125
+ .divider:active {
126
+ background: var(--bgColor-accent-muted, rgba(9, 105, 218, 0.15));
127
+ }
128
+
129
+ .divider::after {
130
+ content: '';
131
+ position: absolute;
132
+ top: 0;
133
+ bottom: 0;
134
+ left: 50%;
135
+ width: 1px;
136
+ background: var(--borderColor-muted, #d8dee4);
137
+ transform: translateX(-50%);
138
+ }
139
+
140
+ .divider:hover::after,
141
+ .divider:active::after {
142
+ background: var(--fgColor-accent, #0969da);
143
+ width: 2px;
144
+ }
145
+
146
+ /* ── Horizontal row divider (between stacked panes in a column) ── */
147
+
148
+ .rowDivider {
149
+ height: 8px;
150
+ margin: -4px 0;
151
+ cursor: row-resize;
152
+ z-index: 1;
153
+ position: relative;
154
+ background: transparent;
155
+ transition: background 100ms;
156
+ flex-shrink: 0;
157
+ }
158
+
159
+ .rowDivider:hover,
160
+ .rowDivider:active {
161
+ background: var(--bgColor-accent-muted, rgba(9, 105, 218, 0.15));
162
+ }
163
+
164
+ .rowDivider::after {
165
+ content: '';
166
+ position: absolute;
167
+ left: 0;
168
+ right: 0;
169
+ top: 50%;
170
+ height: 1px;
171
+ background: var(--borderColor-muted, #d8dee4);
172
+ transform: translateY(-50%);
173
+ }
174
+
175
+ .rowDivider:hover::after,
176
+ .rowDivider:active::after {
177
+ background: var(--fgColor-accent, #0969da);
178
+ height: 2px;
179
+ }