@dfosco/storyboard-react 4.2.0-beta.2 → 4.2.0-beta.20
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
|
@@ -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
|
+
}
|