react-zeugma 6.0.4 → 6.1.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.
package/README.md CHANGED
@@ -113,24 +113,26 @@ The context provider that sets up the drag-and-drop state machine, monitors acti
113
113
 
114
114
  A custom state hook that initializes and manages the recursive layout tree and handles drag-and-drop actions.
115
115
 
116
- | Option | Type | Default | Description |
117
- | ------------------------ | --------------------------------------------------------------------- | ------- | --------------------------------------------------------------------------------------------------------- |
118
- | `initialLayout` | `TreeNode \| null` | — | Initial layout tree structure for uncontrolled mode. Only used on initial mount. |
119
- | `layout` | `TreeNode \| null` | — | Controlled layout tree structure. If provided, the hook runs in controlled mode and synchronizes with it. |
120
- | `locked` | `boolean` | `false` | If true, layout resizes and drags are disabled. |
121
- | `dragActivationDistance` | `number` | `8` | Minimum pointer drag distance (in pixels) required to activate dragging. |
122
- | `snapThreshold` | `number` | `8` | Threshold in pixels to snap layout resizers to adjacent edges. |
123
- | `minSplitPercentage` | `number` | `5` | Minimum resizing limit percentage. |
124
- | `maxSplitPercentage` | `number` | `95` | Maximum resizing limit percentage. |
125
- | `enableDragToDismiss` | `boolean` | `false` | If true, enables the drag-out-to-dismiss gesture to close widgets. |
126
- | `dismissThreshold` | `number` | `60` | Distance in pixels outside container bounds required to trigger dismissal. |
127
- | `onRemove` | `(paneId: string) => void` | | Callback triggered when a pane is removed. |
128
- | `onDragStart` | `(activeId: string) => void` | | Callback triggered when dragging starts. |
129
- | `onDragEnd` | `(activeId: string, overId: string \| null, dropAction: any) => void` | — | Callback triggered when dragging ends. |
130
- | `onResizeStart` | `(currentNode: SplitNode) => void` | — | Callback triggered when resizing starts. |
131
- | `onResize` | `(currentNode: SplitNode, percentage: number) => void` | — | Callback triggered during resizing. |
132
- | `onResizeEnd` | `(currentNode: SplitNode, percentage: number) => void` | — | Callback triggered when resizing ends. |
133
- | `onDismissIntentChange` | `(paneId: string \| null) => void` | — | Callback triggered when drag-out intent changes. |
116
+ | Option | Type | Default | Description |
117
+ | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | --------------------------------------------------------------------------------------------------------- |
118
+ | `initialLayout` | `TreeNode \| null` | — | Initial layout tree structure for uncontrolled mode. Only used on initial mount. |
119
+ | `layout` | `TreeNode \| null` | — | Controlled layout tree structure. If provided, the hook runs in controlled mode and synchronizes with it. |
120
+ | `fullscreenPaneId` | `string \| null` | | Controlled fullscreen pane ID. Pass `null` for no fullscreen pane. |
121
+ | `onFullscreenChange` | `(paneId: string \| null) => void` | | Callback triggered when a pane is toggled to/from fullscreen mode. |
122
+ | `locked` | `boolean` | `false` | If true, layout resizes and drags are disabled. |
123
+ | `dragActivationDistance` | `number` | `8` | Minimum pointer drag distance (in pixels) required to activate dragging. |
124
+ | `snapThreshold` | `number` | `8` | Threshold in pixels to snap layout resizers to adjacent edges. |
125
+ | `minSplitPercentage` | `number` | `5` | Minimum resizing limit percentage. |
126
+ | `maxSplitPercentage` | `number` | `95` | Maximum resizing limit percentage. |
127
+ | `enableDragToDismiss` | `boolean` | `false` | If true, enables the drag-out-to-dismiss gesture to close widgets. |
128
+ | `dismissThreshold` | `number` | `60` | Distance in pixels outside container bounds required to trigger dismissal. |
129
+ | `onRemove` | `(paneId: string) => void` | — | Callback triggered when a pane is removed. |
130
+ | `onDragStart` | `(activeId: string) => void` | — | Callback triggered when dragging starts. |
131
+ | `onDragEnd` | `(activeId: string, overId: string \| null, dropAction: { type: 'split' \| 'move'; direction?: SplitDirection; position?: string } \| null) => void` | — | Callback triggered when dragging ends, with drop target and action details. |
132
+ | `onResizeStart` | `(currentNode: SplitNode) => void` | — | Callback triggered when resizing starts. |
133
+ | `onResize` | `(currentNode: SplitNode, percentage: number) => void` | — | Callback triggered during resizing. |
134
+ | `onResizeEnd` | `(currentNode: SplitNode, percentage: number) => void` | — | Callback triggered when resizing ends. |
135
+ | `onDismissIntentChange` | `(paneId: string \| null) => void` | — | Callback triggered when drag-out intent changes. |
134
136
 
135
137
  ### `useZeugmaContext()`
136
138
 
@@ -155,36 +157,67 @@ Recursively renders the split nodes and pane nodes. Must be placed inside `<Zeug
155
157
 
156
158
  Wraps the individual pane components inside the renderer. Utilizes a render prop passing active layout attributes.
157
159
 
158
- | Prop | Type | Required | Description |
159
- | ---------- | --------------------------------------- | -------- | ------------------------------------------------------- |
160
- | `id` | `string` | Yes | The unique ID corresponding to a `PaneNode`'s `paneId`. |
161
- | `children` | `(props: PaneRenderProps) => ReactNode` | Yes | Render prop function. |
160
+ | Prop | Type | Required | Description |
161
+ | ---------- | --------------------------------------- | -------- | ---------------------------------------------------------------------- |
162
+ | `id` | `string` | Yes | The unique ID corresponding to a `PaneNode`'s `paneId`. |
163
+ | `children` | `(props: PaneRenderProps) => ReactNode` | Yes | Render prop function. |
164
+ | `style` | `React.CSSProperties` | No | Optional inline CSS styles applied to the pane outer container. |
165
+ | `locked` | `boolean` | No | Optional override to lock this specific pane (disables drag and drop). |
162
166
 
163
167
  #### Render Props: `PaneRenderProps`
164
168
 
165
- | Parameter | Type | Description |
166
- | ------------------ | ----------------- | -------------------------------------------------------------------------------- |
167
- | `isDragging` | `boolean` | Returns `true` if the node wrapper is actively being dragged. |
168
- | `isFullscreen` | `boolean` | Returns `true` if the pane is zoomed/fullscreen. |
169
- | `toggleFullscreen` | `() => void` | Callback to toggle fullscreen viewport coverage. |
170
- | `remove` | `() => void` | Triggers removal of this pane from the layout tree. |
171
- | `renderActiveTab` | `() => ReactNode` | Renders the target placeholder element for the currently active tab in the pane. |
169
+ | Parameter | Type | Description |
170
+ | ------------------- | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
171
+ | `isDragging` | `boolean` | `true` if the pane is actively being dragged. |
172
+ | `isFullscreen` | `boolean` | `true` if the pane currently occupies the fullscreen view. |
173
+ | `toggleFullscreen` | `() => void` | Toggles the pane to and from fullscreen/zoomed mode. |
174
+ | `remove` | `() => void` | Removes this pane (and its active tab) from the layout tree. |
175
+ | `metadata` | `Record<string, unknown> \| undefined` | The metadata values associated with the active tab. |
176
+ | `updateMetadata` | `(updater: (current: Record<string, unknown> \| undefined) => Record<string, unknown> \| undefined) => void` | Updates metadata of the active tab via an updater function. |
177
+ | `locked` | `boolean` | `true` if this specific pane or the dashboard globally is locked. |
178
+ | `tabs` | `string[]` | The array of tab IDs in this pane. |
179
+ | `activeTabId` | `string` | The currently active tab ID. |
180
+ | `selectTab` | `(tabId: string) => void` | Selects a specific tab to make it active. |
181
+ | `removeTab` | `(tabId: string) => void` | Removes/closes a specific tab. |
182
+ | `tabsMetadata` | `Record<string, Record<string, unknown>> \| undefined` | Metadata values associated with all tabs in this pane. |
183
+ | `updateTabMetadata` | `(tabId: string, updater: (current: Record<string, unknown> \| undefined) => Record<string, unknown> \| undefined) => void` | Updates the metadata of a specific tab. |
184
+ | `renderActiveTab` | `() => ReactNode` | Renders the portal placeholder for the currently active tab in the pane. |
172
185
 
173
186
  ### `<Tabs>`
174
187
 
175
188
  A helper component that renders a list of tab items for a pane, wrapping the internal drag-and-drop tab logic.
176
189
 
177
- | Prop | Type | Required | Description |
178
- | :------------- | :----------------------------------------------------------------------------------------------------------------------------------- | :------- | :------------------------------------------------------------------- |
179
- | `tabs` | `string[]` | Yes | The list of tab IDs in this pane. |
180
- | `activeTabId` | `string` | Yes | The currently active tab ID. |
181
- | `locked` | `boolean` | No | Whether dragging/reordering tabs is disabled (defaults to `false`). |
182
- | `tabsMetadata` | `Record<string, Record<string, any>>` | No | Metadata associated with each tab. |
183
- | `selectTab` | `(id: string) => void` | Yes | Callback when a tab is selected. |
184
- | `removeTab` | `(id: string) => void` | Yes | Callback when a tab is closed/removed. |
185
- | `classNames` | `{ container?: string; tab?: string \| ((tabId: string) => string) }` | No | Custom class names for the container and individual tab items. |
186
- | `styles` | `{ container?: CSSProperties; tab?: CSSProperties \| ((tabId: string) => CSSProperties) }` | No | Custom inline CSS styles for the container and individual tab items. |
187
- | `renderTab` | `(props: { tabId: string; activeTabId: string; isDragging: boolean; isOver: boolean; metadata?: Record<string, any> }) => ReactNode` | Yes | Render prop function called for each tab item. |
190
+ | Prop | Type | Required | Description |
191
+ | :------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------- | :------------------------------------------------------------------- |
192
+ | `tabs` | `string[]` | Yes | The list of tab IDs in this pane. |
193
+ | `activeTabId` | `string` | Yes | The currently active tab ID. |
194
+ | `locked` | `boolean` | No | Whether dragging/reordering tabs is disabled (defaults to `false`). |
195
+ | `tabsMetadata` | `Record<string, Record<string, any>>` | No | Metadata associated with each tab. |
196
+ | `selectTab` | `(id: string) => void` | Yes | Callback when a tab is selected. |
197
+ | `removeTab` | `(id: string) => void` | Yes | Callback when a tab is closed/removed. |
198
+ | `classNames` | `{ container?: string; tab?: string \| ((tabId: string) => string) }` | No | Custom class names for the container and individual tab items. |
199
+ | `styles` | `{ container?: CSSProperties; tab?: CSSProperties \| ((tabId: string) => CSSProperties) }` | No | Custom inline CSS styles for the container and individual tab items. |
200
+ | `renderTab` | `(props: { tabId: string; activeTabId: string; isDragging: boolean; isOver: boolean; metadata?: Record<string, unknown>; selectTab: (id: string) => void; removeTab: (id: string) => void }) => ReactNode` | Yes | Render prop function called for each tab item. |
201
+
202
+ ### `useTabContext()`
203
+
204
+ A context hook that provides per-tab state from within a `<Tab>` component rendered inside `<Tabs>`. Must be used within a rendered tab child.
205
+
206
+ ```ts
207
+ const { tabId, isActive, isDragging, isOver, metadata, locked, selectTab, removeTab } =
208
+ useTabContext()
209
+ ```
210
+
211
+ | Property | Type | Description |
212
+ | ------------ | -------------------------------------- | -------------------------------------------------- |
213
+ | `tabId` | `string` | The ID of the tab this context belongs to. |
214
+ | `isActive` | `boolean` | Whether this tab is the currently active tab. |
215
+ | `isDragging` | `boolean` | Whether this tab is currently being dragged. |
216
+ | `isOver` | `boolean` | Whether a dragged item is currently over this tab. |
217
+ | `metadata` | `Record<string, unknown> \| undefined` | Custom metadata associated with this tab. |
218
+ | `locked` | `boolean` | Whether this tab (or the dashboard) is locked. |
219
+ | `selectTab` | `() => void` | Selects this tab (no argument needed). |
220
+ | `removeTab` | `() => void` | Removes/closes this tab (no argument needed). |
188
221
 
189
222
  ### `<DragHandle>`
190
223
 
@@ -196,39 +229,57 @@ Defines the interactive drag region inside a `<Pane>`. **Must be placed inside a
196
229
  | `className` | `string` | No | Custom CSS class for the drag handle wrapper. |
197
230
  | `style` | `React.CSSProperties` | No | Inline styles for the drag handle wrapper. |
198
231
 
199
- ### `<ResizableContainer>`
232
+ ## Tree Utilities
200
233
 
201
- A vertical-resize container wrapper that wraps any node (typically `<PaneTree />` or a dashboard component) and allows the user to resize its height by dragging a handle at the bottom edge. Includes smooth scroll parent propagation and drag-to-scroll infinite scrolling behavior.
234
+ Import these serializable tree utility functions from `react-zeugma/utils` for programmatically mutating or querying layout schemas.
202
235
 
203
- | Prop | Type | Required | Default | Description |
204
- | :----------------- | :------------------------- | :------- | :--------------- | :--------------------------------------------------------------- |
205
- | `height` | `number` | No | `400` | Controlled height in pixels (or initial height if uncontrolled). |
206
- | `onHeightChange` | `(height: number) => void` | No | — | Callback function triggered during or after dragging to resize. |
207
- | `minHeight` | `number` | No | `100` | Minimum allowed height in pixels. |
208
- | `maxHeight` | `number` | No | `Infinity` | Maximum allowed height in pixels. |
209
- | `persist` | `boolean` | No | — | Whether to persist height changes in localStorage. |
210
- | `localStorageKey` | `string` | No | `'default-pane'` | Custom localStorage key name (prefixed by `zeugma-height:`). |
211
- | `resizerHeight` | `number` | No | `6` | Height of the resizer drag handle in pixels. |
212
- | `className` | `string` | No | — | Custom CSS class applied to the outer container. |
213
- | `resizerClassName` | `string` | No | — | Custom CSS class applied to the drag handle. |
236
+ #### `generateUniqueId(): string`
214
237
 
215
- ---
238
+ Generates a unique pane ID string (e.g., `'pane-abc123xyz'`). Useful when creating new pane nodes programmatically.
216
239
 
217
- ## Tree Utilities
240
+ #### `removePane(tree: TreeNode | null, paneId: string): TreeNode | null`
218
241
 
219
- Import these serializable tree utility functions from `react-zeugma/utils` for programmatically mutating or querying layout schemas.
242
+ Recursively scans the layout tree, removes the targeted pane node, and collapses redundant split boundaries.
220
243
 
221
- #### `removePane(tree: TreeNode | null, id: string): TreeNode | null`
244
+ #### `removeTab(tree: TreeNode | null, tabId: string): TreeNode | null`
222
245
 
223
- Recursively scans the layout tree, removes the targeted pane node, and collapses redundant split boundaries.
246
+ Removes a single tab from its parent pane. If the pane has no remaining tabs after removal, the pane itself is collapsed out of the tree.
247
+
248
+ #### `addPane(tree: TreeNode | null, paneToAdd: string, metadata?: Record<string, unknown>): TreeNode`
249
+
250
+ Recursively matches the bottommost/rightmost pane leaf in the tree, splits it, and inserts the target `paneToAdd`. Optionally sets initial metadata for the new pane's tab.
224
251
 
225
- #### `addPane(tree: TreeNode | null, paneToAdd: string): TreeNode`
252
+ #### `addTab(tree: TreeNode | null, targetPaneId: string, tabId: string, metadata?: Record<string, unknown>): TreeNode | null`
226
253
 
227
- Recursively matches the bottommost/rightmost pane leaf in the tree, splits it, and inserts the target `paneToAdd`.
254
+ Appends a tab directly into a specific target pane node by its ID. Sets the new tab as the active tab. Does nothing if `targetPaneId` is not found.
228
255
 
229
256
  #### `splitPane(tree, targetId, direction, splitType, paneToAdd)`
230
257
 
231
- Splits the targeted `targetId` pane inside the tree with `direction` (_row_ / _column_) and type (_left_, _right_, _top_, _bottom_) to insert `paneToAdd`.
258
+ Splits the targeted `targetId` pane inside the tree with `direction` (_row_ / _column_) and type (_left_, _right_, _top_, _bottom_) to insert `paneToAdd`. `paneToAdd` may be a tab ID string or a full `PaneNode` object.
259
+
260
+ #### `updateSplitPercentage(tree: TreeNode | null, target: SplitNode, newPercentage: number): TreeNode | null`
261
+
262
+ Finds the target `SplitNode` by reference in the tree and updates its `splitPercentage`.
263
+
264
+ #### `selectTab(tree: TreeNode | null, paneId: string, tabId: string): TreeNode | null`
265
+
266
+ Activates the specified `tabId` within the `paneId` pane node. Returns the tree unchanged if the tab is already active or the pane is not found.
267
+
268
+ #### `mergeTab(tree: TreeNode | null, draggedTabId: string, targetPaneId: string): TreeNode | null`
269
+
270
+ Moves `draggedTabId` from its current pane into `targetPaneId`, preserving tab metadata. Collapses the source pane if it becomes empty. Sets the moved tab as the active tab in the target pane.
271
+
272
+ #### `moveTab(tree: TreeNode | null, draggedTabId: string, targetTabId: string, position?: 'before' | 'after'): TreeNode | null`
273
+
274
+ Reorders `draggedTabId` adjacent to `targetTabId` within the same pane (or moves it cross-pane). `position` defaults to `'before'`.
275
+
276
+ #### `updateTabMetadata(tree: TreeNode | null, tabId: string, updater: (current: Record<string, unknown> | undefined) => Record<string, unknown> | undefined): TreeNode | null`
277
+
278
+ Updates the metadata of a specific tab using an updater function. Returning `undefined` from the updater removes the entry.
279
+
280
+ #### `updatePaneLock(tree: TreeNode | null, paneId: string, locked: boolean): TreeNode | null`
281
+
282
+ Sets the `locked` flag on the specified pane node. When `locked` is `false`, the field is removed from the node entirely to keep the tree clean.
232
283
 
233
284
  #### `findPaneById(tree: TreeNode | null, paneId: string): PaneNode | null`
234
285
 
@@ -242,10 +293,81 @@ Recursively searches the layout tree and returns the `PaneNode` containing the s
242
293
 
243
294
  Searches the layout tree for the given `tabId` and returns computed details (parent `paneId`, `isActive`, `index`, and custom `metadata`).
244
295
 
296
+ #### `computeLayout(node: TreeNode | null, left?, top?, width?, height?): { panes: ComputedPane[]; splitters: ComputedSplitter[] }`
297
+
298
+ Recursively computes the absolute position and dimensions (as percentages relative to the container) for every pane and splitter in the tree. Useful for building custom render layers or analytics on top of the layout engine.
299
+
245
300
  #### `calculateTabDropIndex(tabs: string[], activeType: string | null, overTabId: string | null, overTabPosition: 'before' | 'after' | null): number`
246
301
 
247
302
  Calculates the target insertion index for a dragged tab within a list of tabs. Returns `-1` if the drop target is not in the list.
248
303
 
304
+ #### `getOrCreateHiddenContainer(id: string): HTMLElement`
305
+
306
+ Retrieves or initializes a hidden container `div` appended to `document.body` for sheltering portal elements while they are inactive or popped out.
307
+
308
+ ---
309
+
310
+ ## Popping Out Tabs (Open in a New Window)
311
+
312
+ `react-zeugma` includes built-in support for popping out tab panels into separate browser windows (popouts) with **zero React component unmounting or state loss**.
313
+
314
+ ### How it Works
315
+
316
+ 1. **State-Driven**: The popped-out state is controlled via the tab's metadata under the `openInNewWindow` boolean key.
317
+ 2. **Stable Portal target**: The React portal container reference is kept stable, and `document.adoptNode` is used to migrate the DOM container directly between parent and popup documents, keeping internal component state (like inputs, terminal histories, or connections) fully intact.
318
+ 3. **Style Syncing**: Active stylesheets and link tags are automatically copied from the parent workspace into the popup window on creation.
319
+ 4. **Auto-Restoration**: Close actions on the popup window automatically reset the metadata state to `openInNewWindow: false` and restore the tab wrapper back to the main document layout.
320
+
321
+ ### Integration Recipe
322
+
323
+ To enable popping out tabs in your dashboard:
324
+
325
+ #### 1. Add a Popout Toggle Button
326
+
327
+ Inside your pane header or controls, add a button to toggle the popout state of the active tab:
328
+
329
+ ```tsx
330
+ <button
331
+ onClick={() => {
332
+ const activeTabId = paneProps.activeTabId
333
+ const isPopout = !!paneProps.tabsMetadata?.[activeTabId]?.openInNewWindow
334
+
335
+ paneProps.updateTabMetadata(activeTabId, (current) => ({
336
+ ...current,
337
+ openInNewWindow: !isPopout,
338
+ }))
339
+ }}
340
+ >
341
+ {paneProps.tabsMetadata?.[paneProps.activeTabId]?.openInNewWindow ? 'Restore' : 'Popout'}
342
+ </button>
343
+ ```
344
+
345
+ #### 2. Display a Placeholder in the Workspace Layout
346
+
347
+ When a tab is open in a separate window, render a custom placeholder inside the main workspace pane instead of rendering the tab's active content:
348
+
349
+ ```tsx
350
+ <div className="pane-body">
351
+ {paneProps.tabsMetadata?.[paneProps.activeTabId]?.openInNewWindow ? (
352
+ <div className="popout-placeholder">
353
+ <p>This tab is open in a separate window.</p>
354
+ <button
355
+ onClick={() => {
356
+ paneProps.updateTabMetadata(paneProps.activeTabId, (current) => ({
357
+ ...current,
358
+ openInNewWindow: false,
359
+ }))
360
+ }}
361
+ >
362
+ Restore Tab
363
+ </button>
364
+ </div>
365
+ ) : (
366
+ paneProps.renderActiveTab()
367
+ )}
368
+ </div>
369
+ ```
370
+
249
371
  ---
250
372
 
251
373
  ## Custom Styling
@@ -261,17 +383,38 @@ Use custom CSS or styling rules to style resizers, dragging states, drop preview
261
383
  'bg-transparent hover:bg-indigo-500/50 active:bg-indigo-500 transition-colors duration-150',
262
384
  // split previews
263
385
  dropPreview: 'bg-indigo-500/10 border-2 border-dashed border-indigo-500/50 backdrop-blur-xs',
386
+ // tab separator line
387
+ tabSeparator: 'w-px h-4 bg-zinc-700',
264
388
  }}
265
389
  >
266
390
  <PaneTree />
267
391
  </Zeugma>
268
392
  ```
269
393
 
394
+ ### `ZeugmaClassNames` reference
395
+
396
+ | Key | Applied to |
397
+ | ------------------------ | --------------------------------------------------------------------------------- |
398
+ | `dashboard` | Root dashboard container. |
399
+ | `dashboardDismissActive` | Root container when a drag-out dismiss is active. |
400
+ | `dashboardLocked` | Root container when the dashboard is globally locked. |
401
+ | `pane` | Outer wrapper `<div>` of each `<Pane>`. |
402
+ | `paneLocked` | Pane container when locked. |
403
+ | `dropPreview` | Drop zone preview box when hovering over a split-edge drop zone. |
404
+ | `dragOverlay` | Cursor-following drag preview portal wrapper. |
405
+ | `resizer` | Drag-to-resize split bar handles. |
406
+ | `dismissPreview` | Background dismiss zone indicator during a drag-out dismiss gesture. |
407
+ | `lockedPreview` | Drop zone indicator when hovering over a locked pane. |
408
+ | `tabDropPreview` | Placeholder line element rendered at the target insertion point during tab drags. |
409
+ | `tabSeparator` | Separator line rendered between non-active adjacent tabs in `<Tabs>`. |
410
+
270
411
  ---
271
412
 
272
413
  ## Types Reference
273
414
 
274
- Full TypeScript type definitions utilized in the dashboard layout configuration.
415
+ Full TypeScript type definitions exported from `react-zeugma`.
416
+
417
+ ### Layout Tree
275
418
 
276
419
  ```ts
277
420
  export type SplitDirection = 'row' | 'column'
@@ -281,7 +424,7 @@ export interface SplitNode {
281
424
  direction: SplitDirection
282
425
  first: TreeNode
283
426
  second: TreeNode
284
- splitPercentage: number
427
+ splitPercentage: number // 0–100
285
428
  }
286
429
 
287
430
  export interface PaneNode {
@@ -304,6 +447,164 @@ export interface TabDetails {
304
447
  }
305
448
  ```
306
449
 
450
+ ### Component Props
451
+
452
+ ````ts
453
+ export interface PaneProps {
454
+ id: string
455
+ children: (props: PaneRenderProps) => React.ReactNode
456
+ style?: React.CSSProperties
457
+ locked?: boolean
458
+ }
459
+
460
+ export interface DragHandleProps {
461
+ children?: React.ReactNode
462
+ className?: string
463
+ style?: React.CSSProperties
464
+ }
465
+
466
+ export interface TabsProps {
467
+ tabs: string[]
468
+ activeTabId: string
469
+ locked?: boolean
470
+ tabsMetadata?: Record<string, Record<string, unknown>>
471
+ selectTab: (id: string) => void
472
+ removeTab: (id: string) => void
473
+ renderTab: (props: {
474
+ tabId: string
475
+ activeTabId: string
476
+ isDragging: boolean
477
+ isOver: boolean
478
+ metadata?: Record<string, unknown>
479
+ selectTab: (id: string) => void
480
+ removeTab: (id: string) => void
481
+ }) => React.ReactNode
482
+ classNames?: {
483
+ container?: string
484
+ tab?: string | ((tabId: string) => string)
485
+ }
486
+ styles?: {
487
+ container?: React.CSSProperties
488
+ tab?: React.CSSProperties | ((tabId: string) => React.CSSProperties)
489
+ }
490
+ }
491
+
492
+ ### Render Props
493
+
494
+ ```ts
495
+ export interface PaneRenderProps {
496
+ isDragging: boolean
497
+ isFullscreen: boolean
498
+ toggleFullscreen: () => void
499
+ remove: () => void
500
+ metadata: Record<string, unknown> | undefined
501
+ updateMetadata: (
502
+ updater: (current: Record<string, unknown> | undefined) => Record<string, unknown> | undefined,
503
+ ) => void
504
+ locked: boolean
505
+ tabs: string[]
506
+ activeTabId: string
507
+ selectTab: (tabId: string) => void
508
+ removeTab: (tabId: string) => void
509
+ tabsMetadata: Record<string, Record<string, unknown>> | undefined
510
+ updateTabMetadata: (
511
+ tabId: string,
512
+ updater: (current: Record<string, unknown> | undefined) => Record<string, unknown> | undefined,
513
+ ) => void
514
+ renderActiveTab: () => ReactNode
515
+ }
516
+
517
+ export interface TabContextValue {
518
+ tabId: string
519
+ isActive: boolean
520
+ isDragging: boolean
521
+ isOver: boolean
522
+ metadata?: Record<string, unknown>
523
+ locked: boolean
524
+ selectTab: () => void
525
+ removeTab: () => void
526
+ }
527
+ ````
528
+
529
+ ### Controller & Context
530
+
531
+ ```ts
532
+ export interface UseZeugmaOptions { /* see useZeugma() section above */ }
533
+
534
+ export interface ZeugmaController {
535
+ // State
536
+ layout: TreeNode | null
537
+ setLayout: Dispatch<SetStateAction<TreeNode | null>>
538
+ fullscreenPaneId: string | null
539
+ setFullscreenPaneId: (paneId: string | null) => void
540
+ locked: boolean
541
+ setLocked: Dispatch<SetStateAction<boolean>>
542
+
543
+ // DnD state
544
+ activeId: string | null
545
+ activeType: 'pane' | 'tab' | null
546
+ dismissIntentId: string | null
547
+
548
+ // Config
549
+ dragActivationDistance: number
550
+ snapThreshold: number
551
+ minSplitPercentage: number
552
+ maxSplitPercentage: number
553
+ enableDragToDismiss: boolean
554
+ dismissThreshold: number
555
+
556
+ // Public actions
557
+ removePane: (paneId: string) => void
558
+ addPane: (paneId: string, metadata?: Record<string, unknown>) => void
559
+ addTab: (paneId: string, tabId: string, metadata?: Record<string, unknown>) => void
560
+ updateTabMetadata: (tabId: string, updater: ...) => void
561
+ updatePaneLock: (paneId: string, locked: boolean) => void
562
+ selectTab: (paneId: string, tabId: string) => void
563
+ mergeTab: (draggedTabId: string, targetPaneId: string) => void
564
+ removeTab: (tabId: string) => void
565
+ splitPane: (targetId: string, direction: SplitDirection, splitType: 'left' | 'right' | 'top' | 'bottom', paneToAdd: string) => void
566
+ updateSplitPercentage: (currentNode: SplitNode, percentage: number) => void
567
+ moveTab: (draggedTabId: string, targetTabId: string, position?: 'before' | 'after') => void
568
+
569
+ // Public queries
570
+ findPaneById: (paneId: string) => PaneNode | null
571
+ findPaneContainingTab: (tabId: string) => PaneNode | null
572
+ findTabById: (tabId: string) => TabDetails | null
573
+ }
574
+
575
+ // ZeugmaContextValue is the combined state + actions interface
576
+ // exposed by useZeugmaContext(). It includes all of ZeugmaController
577
+ // plus the renderPane and classNames configuration.
578
+ export interface ZeugmaContextValue extends ZeugmaStateValue, ZeugmaActionsValue {}
579
+ ```
580
+
581
+ ### Computed Layout Types (from `react-zeugma/utils`)
582
+
583
+ ```ts
584
+ export interface ComputedPane {
585
+ paneId: string
586
+ left: number // percentage
587
+ top: number // percentage
588
+ width: number // percentage
589
+ height: number // percentage
590
+ node: PaneNode
591
+ }
592
+
593
+ export interface ComputedSplitter {
594
+ id: string
595
+ currentNode: SplitNode
596
+ direction: SplitDirection
597
+ left: number
598
+ top: number
599
+ width: number
600
+ height: number
601
+ parentLeft: number
602
+ parentTop: number
603
+ parentWidth: number
604
+ parentHeight: number
605
+ }
606
+ ```
607
+
307
608
  ---
308
609
 
309
610
  ## SKILL.md
@@ -481,24 +782,6 @@ Defines the interactive drag region inside a `<Pane>`. **Must be placed inside a
481
782
  - `className?: string`
482
783
  - `style?: React.CSSProperties`
483
784
 
484
- ---
485
-
486
- ### `<ResizableContainer>`
487
-
488
- A vertical-resize container wrapper that wraps any node (typically `<PaneTree />` or a dashboard component) and allows the user to resize its height by dragging a handle at the bottom edge. Includes smooth scroll parent propagation and drag-to-scroll infinite scrolling behavior.
489
-
490
- #### Props
491
-
492
- - `height?: number` — Controlled height in pixels (or initial height if uncontrolled). Defaults to `400`.
493
- - `onHeightChange?: (height: number) => void` — Callback function triggered during or after dragging to resize.
494
- - `minHeight?: number` — Minimum allowed height in pixels (defaults to `100`).
495
- - `maxHeight?: number` — Maximum allowed height in pixels (defaults to `Infinity`).
496
- - `persist?: boolean` — Whether to persist height changes in localStorage.
497
- - `localStorageKey?: string` — Custom localStorage key name (defaults to `'default-pane'`).
498
- - `resizerHeight?: number` — Height of the resizer drag handle in pixels (defaults to `6`).
499
- - `className?: string` — Custom CSS class applied to the outer container.
500
- - `resizerClassName?: string` — Custom CSS class applied to the drag handle.
501
-
502
785
  ## 3. Programmatic State Utilities
503
786
 
504
787
  Import these helpers from `react-zeugma/utils` to manipulate or query the tree layout programmatically in your state handlers:
@@ -591,6 +874,7 @@ interface ZeugmaClassNames {
591
874
  dismissPreview?: string // Applied to the background dismiss zone indicator during a drag-out dismiss gesture
592
875
  lockedPreview?: string // Applied to drop zone indicator when hovering over a locked pane
593
876
  tabDropPreview?: string // Applied to the drop placeholder line element during tab drags
877
+ tabSeparator?: string // Applied to the separator line between non-active adjacent tabs
594
878
  }
595
879
  ```
596
880
 
@@ -263,4 +263,13 @@ interface PortalRegistryValue {
263
263
  registerPortalTarget: (tabId: string, el: HTMLDivElement | null) => void;
264
264
  }
265
265
 
266
- export type { PortalRegistryValue as P, SplitDirection as S, TreeNode as T, UseZeugmaOptions as U, ZeugmaActionsValue as Z, ZeugmaDragStateValue as a, ZeugmaStateValue as b, ZeugmaController as c, ZeugmaContextValue as d, ZeugmaProps as e, SplitNode as f, PaneNode as g, TabDetails as h, ZeugmaClassNames as i };
266
+ /**
267
+ * Utility to copy stylesheets from source document to destination document.
268
+ */
269
+ declare function copyStyles(srcDoc: Document, destDoc: Document): void;
270
+ /**
271
+ * Utility to get or create a hidden container element on the document body.
272
+ */
273
+ declare function getOrCreateHiddenContainer(id: string): HTMLElement;
274
+
275
+ export { type PortalRegistryValue as P, type SplitDirection as S, type TreeNode as T, type UseZeugmaOptions as U, type ZeugmaActionsValue as Z, type ZeugmaDragStateValue as a, type ZeugmaStateValue as b, type ZeugmaController as c, type ZeugmaContextValue as d, type ZeugmaProps as e, type SplitNode as f, type PaneNode as g, type TabDetails as h, type ZeugmaClassNames as i, copyStyles as j, getOrCreateHiddenContainer as k };
@@ -263,4 +263,13 @@ interface PortalRegistryValue {
263
263
  registerPortalTarget: (tabId: string, el: HTMLDivElement | null) => void;
264
264
  }
265
265
 
266
- export type { PortalRegistryValue as P, SplitDirection as S, TreeNode as T, UseZeugmaOptions as U, ZeugmaActionsValue as Z, ZeugmaDragStateValue as a, ZeugmaStateValue as b, ZeugmaController as c, ZeugmaContextValue as d, ZeugmaProps as e, SplitNode as f, PaneNode as g, TabDetails as h, ZeugmaClassNames as i };
266
+ /**
267
+ * Utility to copy stylesheets from source document to destination document.
268
+ */
269
+ declare function copyStyles(srcDoc: Document, destDoc: Document): void;
270
+ /**
271
+ * Utility to get or create a hidden container element on the document body.
272
+ */
273
+ declare function getOrCreateHiddenContainer(id: string): HTMLElement;
274
+
275
+ export { type PortalRegistryValue as P, type SplitDirection as S, type TreeNode as T, type UseZeugmaOptions as U, type ZeugmaActionsValue as Z, type ZeugmaDragStateValue as a, type ZeugmaStateValue as b, type ZeugmaController as c, type ZeugmaContextValue as d, type ZeugmaProps as e, type SplitNode as f, type PaneNode as g, type TabDetails as h, type ZeugmaClassNames as i, copyStyles as j, getOrCreateHiddenContainer as k };