@dfosco/storyboard-react 4.2.0-beta.4 → 4.2.0

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 (89) hide show
  1. package/package.json +10 -11
  2. package/src/AuthModal/AuthModal.jsx +6 -8
  3. package/src/BranchBar/BranchBar.jsx +20 -6
  4. package/src/BranchBar/BranchBar.module.css +13 -4
  5. package/src/BranchBar/useBranches.js +20 -6
  6. package/src/BranchBar/useBranches.test.js +68 -0
  7. package/src/CommandPalette/CommandPalette.jsx +480 -187
  8. package/src/CommandPalette/command-palette.css +142 -78
  9. package/src/Icon.jsx +157 -58
  10. package/src/Viewfinder.jsx +562 -207
  11. package/src/Viewfinder.module.css +434 -93
  12. package/src/Workspace.jsx +7 -0
  13. package/src/canvas/CanvasPage.bridge.test.jsx +14 -6
  14. package/src/canvas/CanvasPage.dragdrop.test.jsx +11 -7
  15. package/src/canvas/CanvasPage.jsx +739 -219
  16. package/src/canvas/CanvasPage.module.css +13 -15
  17. package/src/canvas/CanvasPage.multiselect.test.jsx +17 -6
  18. package/src/canvas/ConnectorLayer.jsx +121 -165
  19. package/src/canvas/ConnectorLayer.module.css +69 -0
  20. package/src/canvas/PageSelector.test.jsx +15 -6
  21. package/src/canvas/canvasApi.js +68 -2
  22. package/src/canvas/canvasReloadGuard.test.js +1 -1
  23. package/src/canvas/connectorGeometry.js +132 -0
  24. package/src/canvas/hotPoolDevLogs.js +25 -0
  25. package/src/canvas/useCanvas.js +1 -1
  26. package/src/canvas/useMarqueeSelect.js +30 -4
  27. package/src/canvas/widgets/CodePenEmbed.jsx +1 -0
  28. package/src/canvas/widgets/ComponentSetWidget.jsx +199 -0
  29. package/src/canvas/widgets/ComponentSetWidget.module.css +89 -0
  30. package/src/canvas/widgets/ComponentWidget.jsx +1 -0
  31. package/src/canvas/widgets/CropOverlay.jsx +219 -0
  32. package/src/canvas/widgets/CropOverlay.module.css +118 -0
  33. package/src/canvas/widgets/ExpandedPane.jsx +474 -0
  34. package/src/canvas/widgets/ExpandedPane.module.css +179 -0
  35. package/src/canvas/widgets/ExpandedPane.test.jsx +240 -0
  36. package/src/canvas/widgets/ExpandedPaneTopBar.jsx +111 -0
  37. package/src/canvas/widgets/ExpandedPaneTopBar.module.css +59 -0
  38. package/src/canvas/widgets/ExpandedPaneTopBar.test.jsx +45 -0
  39. package/src/canvas/widgets/FigmaEmbed.jsx +62 -47
  40. package/src/canvas/widgets/FigmaEmbed.module.css +61 -0
  41. package/src/canvas/widgets/ImageWidget.jsx +130 -9
  42. package/src/canvas/widgets/ImageWidget.module.css +30 -0
  43. package/src/canvas/widgets/LinkPreview.jsx +113 -5
  44. package/src/canvas/widgets/LinkPreview.module.css +127 -0
  45. package/src/canvas/widgets/MarkdownBlock.jsx +167 -17
  46. package/src/canvas/widgets/MarkdownBlock.module.css +148 -0
  47. package/src/canvas/widgets/PromptWidget.jsx +414 -0
  48. package/src/canvas/widgets/PromptWidget.module.css +273 -0
  49. package/src/canvas/widgets/PrototypeEmbed.jsx +77 -39
  50. package/src/canvas/widgets/PrototypeEmbed.module.css +117 -0
  51. package/src/canvas/widgets/PrototypeEmbed.test.jsx +2 -2
  52. package/src/canvas/widgets/ResizeHandle.jsx +17 -6
  53. package/src/canvas/widgets/StoryWidget.jsx +73 -15
  54. package/src/canvas/widgets/TerminalReadWidget.jsx +146 -0
  55. package/src/canvas/widgets/TerminalReadWidget.module.css +94 -0
  56. package/src/canvas/widgets/TerminalWidget.jsx +445 -67
  57. package/src/canvas/widgets/TerminalWidget.module.css +271 -8
  58. package/src/canvas/widgets/TilesWidget.jsx +300 -0
  59. package/src/canvas/widgets/TilesWidget.module.css +133 -0
  60. package/src/canvas/widgets/WidgetChrome.jsx +74 -153
  61. package/src/canvas/widgets/WidgetChrome.module.css +30 -1
  62. package/src/canvas/widgets/embedInteraction.test.jsx +24 -26
  63. package/src/canvas/widgets/expandUtils.js +560 -0
  64. package/src/canvas/widgets/expandUtils.test.js +155 -0
  65. package/src/canvas/widgets/index.js +9 -0
  66. package/src/canvas/widgets/snapshotDisplay.test.jsx +23 -71
  67. package/src/canvas/widgets/tilePool.js +23 -0
  68. package/src/canvas/widgets/tiles/diagonal-bl.png +0 -0
  69. package/src/canvas/widgets/tiles/diagonal-br.png +0 -0
  70. package/src/canvas/widgets/tiles/diagonal-tl.png +0 -0
  71. package/src/canvas/widgets/tiles/leaf.png +0 -0
  72. package/src/canvas/widgets/tiles/quarter-tl.png +0 -0
  73. package/src/canvas/widgets/tiles/quarter-tr.png +0 -0
  74. package/src/canvas/widgets/tiles/solid-a.png +0 -0
  75. package/src/canvas/widgets/tiles/solid-b.png +0 -0
  76. package/src/canvas/widgets/widgetConfig.js +55 -4
  77. package/src/canvas/widgets/widgetIcons.jsx +190 -0
  78. package/src/canvas/widgets/widgetProps.js +1 -0
  79. package/src/context.jsx +48 -20
  80. package/src/hooks/useConfig.js +14 -0
  81. package/src/hooks/usePrototypeReloadGuard.js +64 -0
  82. package/src/hooks/useSceneData.js +1 -0
  83. package/src/hooks/useThemeState.test.js +1 -1
  84. package/src/index.js +8 -2
  85. package/src/story/ComponentSetPage.jsx +186 -0
  86. package/src/story/ComponentSetPage.module.css +121 -0
  87. package/src/story/StoryPage.jsx +32 -2
  88. package/src/vite/data-plugin.js +363 -67
  89. package/src/vite/data-plugin.test.js +1 -1
@@ -0,0 +1,474 @@
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 }) {
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
+ // eslint-disable-next-line react-hooks/set-state-in-effect
70
+ if (initialLayout) setLayout(initialLayout)
71
+ }, [initialLayout])
72
+
73
+ // Force re-render when pane actions change (e.g. markdown edit toggle)
74
+ const [, forceUpdate] = useState(0)
75
+ useEffect(() => {
76
+ const handler = () => forceUpdate((n) => n + 1)
77
+ document.addEventListener('storyboard:expanded-pane:refresh', handler)
78
+ return () => document.removeEventListener('storyboard:expanded-pane:refresh', handler)
79
+ }, [])
80
+
81
+ const allPanes = useMemo(() => layout.flat(), [layout])
82
+
83
+ const [columnSizes, setColumnSizes] = useState(() => layout.map(() => '1fr'))
84
+ // Row ratios per column: rowRatios[colIdx] = [ratio, ratio, ...] (flex values)
85
+ const [rowRatios, setRowRatios] = useState(() =>
86
+ layout.map((col) => col.map(() => 1)),
87
+ )
88
+ const [, setActivePaneId] = useState(null)
89
+
90
+ // Ref map: paneId → container DOM element (callback refs)
91
+ const containerRefs = useRef(new Map())
92
+ // Ref map: paneId → detach cleanup function
93
+ const detachRefs = useRef(new Map())
94
+ // Ref map: paneId → ResizeObserver
95
+ const observerRefs = useRef(new Map())
96
+
97
+ const totalPanes = allPanes.length
98
+ const isSplit = totalPanes >= 2
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 [, detach] of detachRefs.current) {
112
+ detach?.()
113
+ }
114
+ detachRefs.current.clear()
115
+ }
116
+ // intentional mount-only
117
+ // eslint-disable-next-line react-hooks/exhaustive-deps
118
+ }, [])
119
+
120
+ // Handle pane list changes: attach new external panes, detach removed ones
121
+ useLayoutEffect(() => {
122
+ const currentIds = new Set(allPanes.map((p) => p.id))
123
+
124
+ for (const [id, detach] of detachRefs.current) {
125
+ if (!currentIds.has(id)) {
126
+ detach?.()
127
+ detachRefs.current.delete(id)
128
+ observerRefs.current.get(id)?.disconnect()
129
+ observerRefs.current.delete(id)
130
+ }
131
+ }
132
+
133
+ for (const pane of allPanes) {
134
+ if (pane.kind !== 'external') continue
135
+ if (detachRefs.current.has(pane.id)) continue
136
+ const container = containerRefs.current.get(pane.id)
137
+ if (!container) continue
138
+ const detach = pane.attach(container)
139
+ detachRefs.current.set(pane.id, detach)
140
+ }
141
+ }) // runs every render to catch pane changes
142
+
143
+ // ── ResizeObserver per external pane ──
144
+ useEffect(() => {
145
+ for (const pane of allPanes) {
146
+ if (pane.kind !== 'external' || !pane.onResize) continue
147
+ if (observerRefs.current.has(pane.id)) continue
148
+ const container = containerRefs.current.get(pane.id)
149
+ if (!container) continue
150
+ const ro = new ResizeObserver((entries) => {
151
+ for (const entry of entries) {
152
+ pane.onResize(entry.contentRect)
153
+ }
154
+ })
155
+ ro.observe(container)
156
+ observerRefs.current.set(pane.id, ro)
157
+ }
158
+ return () => {
159
+ for (const ro of observerRefs.current.values()) {
160
+ ro.disconnect()
161
+ }
162
+ observerRefs.current.clear()
163
+ }
164
+ }, [allPanes])
165
+
166
+ // ── Callback ref factory: stable per pane id ──
167
+ const getContainerRef = useCallback((paneId) => (el) => {
168
+ if (el) {
169
+ containerRefs.current.set(paneId, el)
170
+ } else {
171
+ containerRefs.current.delete(paneId)
172
+ }
173
+ }, [])
174
+
175
+ // ── Escape to close ──
176
+ useEffect(() => {
177
+ function handleKeyDown(e) {
178
+ if (e.key === 'Escape') {
179
+ e.stopPropagation()
180
+ onClose()
181
+ }
182
+ }
183
+ document.addEventListener('keydown', handleKeyDown, true)
184
+ return () => document.removeEventListener('keydown', handleKeyDown, true)
185
+ }, [onClose])
186
+
187
+ // ── Column drag-to-resize dividers ──
188
+ const dragState = useRef(null)
189
+
190
+ const handleColumnDividerPointerDown = useCallback((e, dividerIndex) => {
191
+ e.preventDefault()
192
+ const gridEl = e.target.closest(`.${styles.grid}`)
193
+ if (!gridEl) return
194
+
195
+ const currentCols = Array.from(gridEl.children)
196
+ .filter((el) => !el.classList.contains(styles.divider))
197
+ .map((el) => el.getBoundingClientRect().width)
198
+
199
+ dragState.current = {
200
+ kind: 'column',
201
+ dividerIndex,
202
+ startX: e.clientX,
203
+ startWidths: currentCols,
204
+ }
205
+
206
+ function handleMove(ev) {
207
+ if (!dragState.current || dragState.current.kind !== 'column') return
208
+ const { dividerIndex: di, startX, startWidths } = dragState.current
209
+ const dx = ev.clientX - startX
210
+ const leftW = Math.max(MIN_PANE_WIDTH_PX, startWidths[di] + dx)
211
+ const rightW = Math.max(MIN_PANE_WIDTH_PX, startWidths[di + 1] - dx)
212
+ const newWidths = [...startWidths]
213
+ newWidths[di] = leftW
214
+ newWidths[di + 1] = rightW
215
+ const total = newWidths.reduce((a, b) => a + b, 0)
216
+ setColumnSizes(newWidths.map((w) => `${((w / total) * layout.length).toFixed(3)}fr`))
217
+ }
218
+
219
+ function handleUp() {
220
+ dragState.current = null
221
+ document.removeEventListener('pointermove', handleMove)
222
+ document.removeEventListener('pointerup', handleUp)
223
+ }
224
+
225
+ document.addEventListener('pointermove', handleMove)
226
+ document.addEventListener('pointerup', handleUp)
227
+ }, [layout.length])
228
+
229
+ // ── Row drag-to-resize dividers (vertical splits within a column) ──
230
+ const handleRowDividerPointerDown = useCallback((e, colIndex) => {
231
+ e.preventDefault()
232
+ const columnEl = e.target.closest(`.${styles.column}`)
233
+ if (!columnEl) return
234
+
235
+ const paneEls = Array.from(columnEl.children).filter(
236
+ (el) => !el.classList.contains(styles.rowDivider),
237
+ )
238
+ const startHeights = paneEls.map((el) => el.getBoundingClientRect().height)
239
+
240
+ dragState.current = {
241
+ kind: 'row',
242
+ colIndex,
243
+ startY: e.clientY,
244
+ startHeights,
245
+ }
246
+
247
+ function handleMove(ev) {
248
+ if (!dragState.current || dragState.current.kind !== 'row') return
249
+ const { colIndex: ci, startY, startHeights: sh } = dragState.current
250
+ const dy = ev.clientY - startY
251
+ const topH = Math.max(MIN_PANE_HEIGHT_PX, sh[0] + dy)
252
+ const bottomH = Math.max(MIN_PANE_HEIGHT_PX, sh[1] - dy)
253
+ const total = topH + bottomH
254
+ setRowRatios((prev) => {
255
+ const next = [...prev]
256
+ next[ci] = [topH / total, bottomH / total]
257
+ return next
258
+ })
259
+ }
260
+
261
+ function handleUp() {
262
+ dragState.current = null
263
+ document.removeEventListener('pointermove', handleMove)
264
+ document.removeEventListener('pointerup', handleUp)
265
+ }
266
+
267
+ document.addEventListener('pointermove', handleMove)
268
+ document.addEventListener('pointerup', handleUp)
269
+ }, [])
270
+
271
+ // ── Build grid-template-columns ──
272
+ const gridTemplateColumns = useMemo(() => {
273
+ if (layout.length < 2) return undefined
274
+ const parts = []
275
+ for (let i = 0; i < columnSizes.length; i++) {
276
+ if (i > 0) parts.push('0px')
277
+ parts.push(columnSizes[i])
278
+ }
279
+ return parts.join(' ')
280
+ }, [layout.length, columnSizes])
281
+
282
+ // ── Render pane content ──
283
+ function renderPaneContent(pane) {
284
+ if (pane.kind === 'react') {
285
+ return (
286
+ <div
287
+ ref={getContainerRef(pane.id)}
288
+ className={styles.paneContent}
289
+ onPointerDown={() => setActivePaneId(pane.id)}
290
+ >
291
+ {pane.render()}
292
+ </div>
293
+ )
294
+ }
295
+ return (
296
+ <div
297
+ ref={getContainerRef(pane.id)}
298
+ className={styles.paneContent}
299
+ onPointerDown={() => setActivePaneId(pane.id)}
300
+ />
301
+ )
302
+ }
303
+
304
+ // ── Render a single column (1 or 2 panes stacked) ──
305
+ function renderColumn(column, colIndex, isLastCol) {
306
+ if (column.length === 1) {
307
+ const pane = column[0]
308
+ return (
309
+ <div className={styles.pane} key={pane.id}>
310
+ <ExpandedPaneTopBar
311
+ label={pane.label}
312
+ widgetType={pane.widgetType}
313
+ actions={pane.actions}
314
+ features={pane.features}
315
+ getState={pane.getState}
316
+ onAction={pane.onAction}
317
+ showClose={isLastCol}
318
+ onClose={onClose}
319
+ />
320
+ {renderPaneContent(pane)}
321
+ </div>
322
+ )
323
+ }
324
+
325
+ // Multi-row column
326
+ const ratios = rowRatios[colIndex] || column.map(() => 1)
327
+ return (
328
+ <div className={styles.column} key={`col-${colIndex}`}>
329
+ {column.map((pane, rowIdx) => (
330
+ <PaneWithRowDivider
331
+ key={pane.id}
332
+ pane={pane}
333
+ flex={ratios[rowIdx] ?? 1}
334
+ isLast={rowIdx === column.length - 1}
335
+ colIndex={colIndex}
336
+ onRowDividerPointerDown={handleRowDividerPointerDown}
337
+ >
338
+ <ExpandedPaneTopBar
339
+ label={pane.label}
340
+ widgetType={pane.widgetType}
341
+ actions={pane.actions}
342
+ features={pane.features}
343
+ getState={pane.getState}
344
+ onAction={pane.onAction}
345
+ showClose={isLastCol && rowIdx === 0}
346
+ onClose={onClose}
347
+ />
348
+ {renderPaneContent(pane)}
349
+ </PaneWithRowDivider>
350
+ ))}
351
+ </div>
352
+ )
353
+ }
354
+
355
+ // ── Single-pane modal variant ──
356
+ if (!isSplit && variant === 'modal') {
357
+ const pane = allPanes[0]
358
+ if (!pane) return null
359
+ return createPortal(
360
+ <div
361
+ className={styles.backdrop}
362
+ onClick={onClose}
363
+ onPointerDown={(e) => e.stopPropagation()}
364
+ onKeyDown={(e) => e.stopPropagation()}
365
+ onWheel={(e) => e.stopPropagation()}
366
+ >
367
+ <div className={styles.modalContainer} onClick={(e) => e.stopPropagation()}>
368
+ <ExpandedPaneTopBar
369
+ label={pane.label}
370
+ widgetType={pane.widgetType}
371
+ actions={pane.actions}
372
+ features={pane.features}
373
+ getState={pane.getState}
374
+ onAction={pane.onAction}
375
+ showClose
376
+ onClose={onClose}
377
+ />
378
+ <div className={styles.modalBody}>
379
+ {renderPaneContent(pane)}
380
+ </div>
381
+ </div>
382
+ </div>,
383
+ document.body,
384
+ )
385
+ }
386
+
387
+ // ── Full layout (single-pane full or multi-pane split) ──
388
+ return createPortal(
389
+ <div
390
+ className={styles.fullContainer}
391
+ onPointerDown={(e) => e.stopPropagation()}
392
+ onKeyDown={(e) => e.stopPropagation()}
393
+ onWheel={(e) => e.stopPropagation()}
394
+ >
395
+ {isSplit ? (
396
+ <div
397
+ className={styles.grid}
398
+ style={{ gridTemplateColumns }}
399
+ >
400
+ {layout.map((column, colIdx) => {
401
+ const isLastCol = colIdx === layout.length - 1
402
+ return (
403
+ <ColumnWithDivider
404
+ key={`col-${colIdx}`}
405
+ colIndex={colIdx}
406
+ isLast={isLastCol}
407
+ onDividerPointerDown={handleColumnDividerPointerDown}
408
+ >
409
+ {renderColumn(column, colIdx, isLastCol)}
410
+ </ColumnWithDivider>
411
+ )
412
+ })}
413
+ </div>
414
+ ) : (
415
+ <>
416
+ <ExpandedPaneTopBar
417
+ label={allPanes[0]?.label}
418
+ widgetType={allPanes[0]?.widgetType}
419
+ actions={allPanes[0]?.actions}
420
+ features={allPanes[0]?.features}
421
+ getState={allPanes[0]?.getState}
422
+ onAction={allPanes[0]?.onAction}
423
+ showClose
424
+ onClose={onClose}
425
+ />
426
+ <div className={styles.singleFull}>
427
+ {renderPaneContent(allPanes[0])}
428
+ </div>
429
+ </>
430
+ )}
431
+ </div>,
432
+ document.body,
433
+ )
434
+ }
435
+
436
+ /**
437
+ * Wraps a column cell in the grid, optionally followed by a vertical divider.
438
+ */
439
+ function ColumnWithDivider({ colIndex, isLast, onDividerPointerDown, children }) {
440
+ return (
441
+ <>
442
+ {children}
443
+ {!isLast && (
444
+ <div
445
+ className={styles.divider}
446
+ onPointerDown={(e) => onDividerPointerDown(e, colIndex)}
447
+ role="separator"
448
+ aria-orientation="vertical"
449
+ />
450
+ )}
451
+ </>
452
+ )
453
+ }
454
+
455
+ /**
456
+ * Renders a pane within a row-split column, optionally followed by a horizontal row divider.
457
+ */
458
+ function PaneWithRowDivider({ flex, isLast, colIndex, onRowDividerPointerDown, children }) {
459
+ return (
460
+ <>
461
+ <div className={styles.pane} style={{ flex }}>
462
+ {children}
463
+ </div>
464
+ {!isLast && (
465
+ <div
466
+ className={styles.rowDivider}
467
+ onPointerDown={(e) => onRowDividerPointerDown(e, colIndex)}
468
+ role="separator"
469
+ aria-orientation="horizontal"
470
+ />
471
+ )}
472
+ </>
473
+ )
474
+ }
@@ -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
+ }