react-zeugma 6.5.0 → 6.5.2

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
@@ -1,84 +1,59 @@
1
1
  # react-zeugma
2
2
 
3
- **A recursive, drag-and-drop dashboard layout engine for React.**
4
-
5
- `react-zeugma` combines the tree-based, arbitrary splitting capabilities of `react-mosaic` with the declarative, state-driven API model of `react-grid-layout`. Built with React 18+, TypeScript, and [`@dnd-kit`](https://dndkit.com).
3
+ A recursive, drag-and-drop dashboard layout engine for React. It combines the tree-based, arbitrary splitting capabilities of `react-mosaic` with the declarative, state-driven API model of `react-grid-layout`, powered by `@dnd-kit/core`.
6
4
 
7
5
  [![npm version](https://img.shields.io/npm/v/react-zeugma?color=brightgreen&style=flat-square)](https://www.npmjs.com/package/react-zeugma)
8
6
  [![bundle size](https://img.shields.io/bundlephobia/minzip/react-zeugma?color=blue&style=flat-square)](https://bundlephobia.com/package/react-zeugma)
9
7
  [![license](https://img.shields.io/npm/l/react-zeugma?color=yellow&style=flat-square)](./LICENSE)
10
8
  [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue?style=flat-square&logo=typescript)](https://www.typescriptlang.org)
11
9
 
12
- > [!TIP]
13
- > **Headless Design System** — react-zeugma is entirely style-agnostic and relies on your class name configurations for styling visual states. You bring your own CSS/Tailwind rules, and we handle the complex drag-and-drop mechanics, resize handle math, and layout tree calculations.
14
-
15
- ### Core Features
16
-
17
- - **Recursive Split Trees**: Nest rows and columns to any depth using a simple serialized JSON node structure.
18
- - **4-Zone Docking Previews**: Drag panels on the top, bottom, left, or right edges of another pane to split it.
19
- - **Native Flexbox Resizers**: Fluid, non-blocking split handles built on pointer events.
20
- - **Accessible Drag-and-Drop**: Built on top of the performant and accessible [`@dnd-kit`](https://dndkit.com) toolkit.
21
- - **Fullscreen Zoom Toggle**: Programmatically expand any pane to cover the entire viewport and snap it back instantly.
22
-
23
- ---
10
+ It is completely style-agnostic (headless), meaning you style all container states, resizers, and drop previews with your own class names.
24
11
 
25
12
  ## Installation
26
13
 
27
- Install the package into your React project using your preferred package manager.
28
-
29
14
  ```bash
30
15
  npm install react-zeugma
31
16
  ```
32
17
 
33
- > [!NOTE]
34
- > **Peer Dependencies** — react-zeugma is compatible with both **React 18** and **React 19** (along with matching `react-dom`).
35
-
36
- ---
37
-
38
18
  ## Quick Start
39
19
 
40
- Import the core components and configure the layout state inside your React application using the `useZeugma` hook.
20
+ Initialize your layout tree with `useZeugma` and render the dashboard using `<Zeugma>`.
41
21
 
42
22
  ```tsx
43
- import { useZeugma, Zeugma, PaneTree, Pane, TreeNode } from 'react-zeugma'
23
+ import { useZeugma, Zeugma, Pane, TreeNode } from 'react-zeugma'
44
24
 
25
+ // 1. Define the initial layout tree structure
45
26
  const initialLayout: TreeNode = {
46
27
  type: 'split',
47
28
  direction: 'row',
48
- splitPercentage: 20,
49
- first: { type: 'pane', id: 'explorer', tabs: ['explorer'], activeTabId: 'explorer' },
50
- second: {
51
- type: 'split',
52
- direction: 'row',
53
- splitPercentage: 50,
54
- first: { type: 'pane', id: 'editor', tabs: ['editor'], activeTabId: 'editor' },
55
- second: { type: 'pane', id: 'preview', tabs: ['preview'], activeTabId: 'preview' },
56
- },
29
+ splitPercentage: 30,
30
+ first: { type: 'pane', id: 'left-panel', tabs: ['left-panel'], activeTabId: 'left-panel' },
31
+ second: { type: 'pane', id: 'right-panel', tabs: ['right-panel'], activeTabId: 'right-panel' },
57
32
  }
58
33
 
59
- function MyPane({ id }: { id: string }) {
34
+ // 2. Build your custom pane wrapper
35
+ function DashboardPane({ id }: { id: string }) {
60
36
  return (
61
37
  <Pane id={id}>
62
- <div className="h-full flex flex-col bg-[#18181b]">
63
- <Pane.DragHandle>
64
- <div className="px-3 py-2 bg-[#27272a] border-b border-[#3f3f46] flex items-center justify-between cursor-grab">
65
- <span className="text-xs uppercase text-zinc-300 font-bold">{id}</span>
66
- </div>
38
+ <div className="flex flex-col h-full bg-zinc-900 border border-zinc-700">
39
+ <Pane.DragHandle className="p-2 bg-zinc-800 cursor-grab text-zinc-300 font-semibold">
40
+ {id}
67
41
  </Pane.DragHandle>
68
- <Pane.Content className="flex-1 p-4 text-sm text-zinc-400">
69
- {(tab) => <div>Content for {tab.id}</div>}
42
+ <Pane.Content className="flex-1 p-4 text-zinc-400">
43
+ {(tab) => <div>Active Tab Content: {tab.id}</div>}
70
44
  </Pane.Content>
71
45
  </div>
72
46
  </Pane>
73
47
  )
74
48
  }
75
49
 
76
- export default function Dashboard() {
50
+ // 3. Mount the layout controller and dashboard renderer
51
+ export default function DashboardApp() {
77
52
  const controller = useZeugma({ initialLayout })
78
53
 
79
54
  return (
80
55
  <div className="w-screen h-screen">
81
- <Zeugma controller={controller} renderPane={(id) => <MyPane id={id} />} />
56
+ <Zeugma controller={controller} renderPane={(paneId) => <DashboardPane id={paneId} />} />
82
57
  </div>
83
58
  )
84
59
  }
@@ -88,427 +63,398 @@ export default function Dashboard() {
88
63
 
89
64
  ## API Reference
90
65
 
91
- ### `<Zeugma>`
92
-
93
- The main visual dashboard grid renderer and context provider.
94
-
95
- - **With Children (Provider Mode)**: If `<Zeugma>` is rendered with children, it acts as the context provider. You must pass the `controller` prop to it, and the children will have access to the Zeugma context functions.
96
- - **Without Children (Renderer/Standalone Mode)**: If `<Zeugma>` is rendered without children, it acts as both the provider and the visual layout renderer. If it is nested within a parent `<Zeugma>` provider, it automatically reads the state, configuration, and callbacks directly from the parent context.
97
-
98
- | Prop | Type | Required | Description |
99
- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | --------------------------------------------------------------------------------------------- |
100
- | `controller` | `ZeugmaController` | Standalone only | The Zeugma controller object returned by `useZeugma(options)`. |
101
- | `renderPane` | `(paneId: string) => ReactNode` | Standalone only | Renderer function lookup that returns a `<Pane>` structure. |
102
- | `classNames` | `ZeugmaClassNames` | No | Custom classes for overriding pane, resizer, and drop preview overlays. |
103
- | `renderDragOverlay` | `(active: DragOverlayActiveItem) => ReactNode` | No | Renders a custom cursor-following drag preview overlay. |
104
- | `resizerSize` | `number` | No | Thickness of the split resizer bars in pixels. Defaults to `4`. |
105
- | `dragActivationDistance` | `number` | No | Minimum pointer drag distance (in pixels) required to activate dragging (defaults to `8`). |
106
- | `snapThreshold` | `number` | No | Threshold in pixels to snap layout resizers to adjacent edges (defaults to `8`). |
107
- | `minSplitPercentage` | `number` | No | Minimum resizing limit percentage (defaults to `5`). |
108
- | `maxSplitPercentage` | `number` | No | Maximum resizing limit percentage (defaults to `95`). |
109
- | `enableDragToDismiss` | `boolean` | No | If true, enables the drag-out-to-dismiss gesture to close widgets (defaults to `false`). |
110
- | `dismissThreshold` | `number` | No | Distance in pixels outside container bounds required to trigger dismissal (defaults to `60`). |
111
- | `onRemove` | `(paneId: string) => void` | No | Callback triggered when a pane is removed. |
112
- | `onDragStart` | `(activeId: string) => void` | No | Callback triggered when dragging starts. |
113
- | `onDragEnd` | `(activeId: string, overId: string \| null, dropAction: { type: 'split' \| 'move'; direction?: SplitDirection; position?: string } \| null) => void` | No | Callback triggered when dragging ends. |
114
- | `onResizeStart` | `(currentNode: SplitNode) => void` | No | Callback triggered when resizing starts. |
115
- | `onResize` | `(currentNode: SplitNode, percentage: number) => void` | No | Callback triggered during resizing. |
116
- | `onResizeEnd` | `(currentNode: SplitNode, percentage: number) => void` | No | Callback triggered when resizing ends. |
117
- | `onDismissIntentChange` | `(paneId: string \| null) => void` | No | Callback triggered when drag-out intent changes. |
118
-
119
- ### `useZeugma(options)`
120
-
121
- A custom state hook that initializes and manages the layout tree, locked state, and fullscreen mode.
122
-
123
- | Option | Type | Default | Description |
124
- | -------------------- | --------------------------------------- | ------- | --------------------------------------------------------------------------------------------------------- |
125
- | `initialLayout` | `TreeNode \| null` | — | Initial layout tree structure for uncontrolled mode. Only used on initial mount. |
126
- | `layout` | `TreeNode \| null` | — | Controlled layout tree structure. If provided, the hook runs in controlled mode and synchronizes with it. |
127
- | `onChange` | `(newLayout: TreeNode \| null) => void` | — | Callback triggered when the layout changes. |
128
- | `fullscreenPaneId` | `string \| null` | — | Controlled fullscreen pane ID. Pass `null` for no fullscreen pane. |
129
- | `onFullscreenChange` | `(paneId: string \| null) => void` | — | Callback triggered when a pane is toggled to/from fullscreen mode. |
130
- | `locked` | `boolean` | `false` | If true, layout resizes and drags are disabled. |
131
-
132
- ### `useZeugmaContext()`
133
-
134
- A custom React context hook that returns the unified layout controller properties and state actions. Must be used within a `<Zeugma>` provider component.
135
-
136
- Provides direct access to the current layout state (e.g., `layout`, `locked`), state setters (e.g., `setLocked`), queries (e.g., `findTabById`, `findPaneContainingTab`, `findPaneById`), and mutation actions (e.g., `addTab`, `removePane`, `selectTab`, etc.).
66
+ ### Components
137
67
 
138
- ```ts
139
- const { layout, locked, findTabById, setLocked, removePane } = useZeugmaContext()
140
- ```
68
+ #### `<Zeugma>`
141
69
 
142
- ### `<PaneTree>`
70
+ The root provider and layout renderer. It configures the drag-and-drop context, calculates panel positions, and renders resize splitters.
143
71
 
144
- An internal component that recursively renders the split nodes and pane nodes. It is automatically managed and rendered internally by `<Zeugma>`, so consumers do not need to import or render it manually. Configuration options like `resizerSize` and `snapThreshold` are passed directly as props to `<Zeugma>` instead.
72
+ ##### Usage
145
73
 
146
- ### `<Pane id>`
147
-
148
- Wraps the individual pane components inside the renderer. It acts as a context provider and container. Any pane state or handlers should be accessed via `usePaneContext()` or compound sub-components.
149
-
150
- | Prop | Type | Required | Description |
151
- | ---------- | --------------------- | -------- | ----------------------------------------------------------------------- |
152
- | `id` | `string` | Yes | The unique ID corresponding to a `PaneNode`'s `paneId`. |
153
- | `children` | `React.ReactNode` | Yes | Children components inside the pane (e.g. tabs, drag handles, content). |
154
- | `style` | `React.CSSProperties` | No | Optional inline CSS styles applied to the pane outer container. |
155
- | `locked` | `boolean` | No | Optional override to lock this specific pane (disables drag and drop). |
156
-
157
- #### Compound Sub-components
158
-
159
- - **`<Pane.Content>`**: Renders the portal target for the active tab content.
160
- - `children` can be a callback function `(tab: TabDetails) => React.ReactNode` or static `React.ReactNode`.
161
- - Accepts `className` and `style` props.
162
- - **`<Pane.DragHandle>`**: Defines the interactive drag region.
163
- - **`<Pane.Tabs>`**: Renders the list of tab items for the pane.
164
- - **`<Pane.Tab>`**: Renders an individual tab item.
165
- - **`<Pane.Controls>`**: A headless wrapper for pane control buttons (fullscreen, close, etc.).
166
-
167
- #### Hook: `usePaneContext()`
168
-
169
- Provides direct access to the pane's state and action handlers from within any child component of `<Pane>`. Returns `PaneContextValue` which extends `PaneRenderProps`:
170
-
171
- ```ts
172
- const { id, isDragging, isFullscreen, toggleFullscreen, remove, tabs, activeTabId } =
173
- usePaneContext()
74
+ ```tsx
75
+ import { Zeugma } from 'react-zeugma'
76
+
77
+ ;<Zeugma
78
+ controller={controller}
79
+ renderPane={(paneId) => <MyPane id={paneId} />}
80
+ resizerSize={4}
81
+ dragActivationDistance={8}
82
+ snapThreshold={8}
83
+ minSplitPercentage={5}
84
+ maxSplitPercentage={95}
85
+ enableDragToDismiss={false}
86
+ dismissThreshold={60}
87
+ classNames={{
88
+ dashboard: 'bg-zinc-950',
89
+ pane: 'rounded-lg overflow-hidden',
90
+ resizer: 'bg-zinc-800 hover:bg-indigo-500 transition-colors',
91
+ dropPreview: 'bg-indigo-500/20 border border-indigo-500',
92
+ }}
93
+ onRemove={(paneId) => console.log(`Pane ${paneId} closed`)}
94
+ onResizeEnd={(currentNode, percentage) => console.log('Resized to', percentage)}
95
+ />
174
96
  ```
175
97
 
176
- #### Pane Context Value: `PaneRenderProps`
177
-
178
- | Parameter | Type | Description |
179
- | ------------------- | --------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
180
- | `isDragging` | `boolean` | `true` if the pane is actively being dragged. |
181
- | `isFullscreen` | `boolean` | `true` if the pane currently occupies the fullscreen view. |
182
- | `toggleFullscreen` | `() => void` | Toggles the pane to and from fullscreen/zoomed mode. |
183
- | `remove` | `() => void` | Removes this pane (and its active tab) from the layout tree. |
184
- | `metadata` | `Record<string, unknown> \| undefined` | The metadata values associated with the active tab. |
185
- | `updateMetadata` | `(updater: (current: Record<string, unknown> \| undefined) => Record<string, unknown> \| undefined) => void` | Updates metadata of the active tab via an updater function. |
186
- | `locked` | `boolean` | `true` if this specific pane or the dashboard globally is locked. |
187
- | `tabs` | `string[]` | The array of tab IDs in this pane. |
188
- | `activeTabId` | `string` | The currently active tab ID. |
189
- | `selectTab` | `(tabId: string) => void` | Selects a specific tab to make it active. |
190
- | `removeTab` | `(tabId: string) => void` | Removes/closes a specific tab. |
191
- | `tabsMetadata` | `Record<string, Record<string, unknown>> \| undefined` | Metadata values associated with all tabs in this pane. |
192
- | `updateTabMetadata` | `(tabId: string, updater: (current: Record<string, unknown> \| undefined) => Record<string, unknown> \| undefined) => void` | Updates the metadata of a specific tab. |
193
-
194
- ### `<Tabs>`
195
-
196
- A helper component that renders a list of tab items for a pane, wrapping the internal drag-and-drop tab logic.
197
-
198
- | Prop | Type | Required | Description |
199
- | :------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------- | :------------------------------------------------------------------- |
200
- | `tabs` | `string[]` | Yes | The list of tab IDs in this pane. |
201
- | `activeTabId` | `string` | Yes | The currently active tab ID. |
202
- | `locked` | `boolean` | No | Whether dragging/reordering tabs is disabled (defaults to `false`). |
203
- | `tabsMetadata` | `Record<string, Record<string, any>>` | No | Metadata associated with each tab. |
204
- | `selectTab` | `(id: string) => void` | Yes | Callback when a tab is selected. |
205
- | `removeTab` | `(id: string) => void` | Yes | Callback when a tab is closed/removed. |
206
- | `classNames` | `{ container?: string; tab?: string \| ((tabId: string) => string) }` | No | Custom class names for the container and individual tab items. |
207
- | `styles` | `{ container?: CSSProperties; tab?: CSSProperties \| ((tabId: string) => CSSProperties) }` | No | Custom inline CSS styles for the container and individual tab items. |
208
- | `renderTab` | `(props: { tabId: string; activeTabId: string; isDragging: boolean; isOver: boolean; metadata?: Record<string, unknown>; onSelect: () => void; onRemove: () => void }) => ReactNode` | Yes | Render prop function called for each tab item. |
209
-
210
- ### `<DragHandle>`
98
+ ##### Props
99
+
100
+ | Property | Description | Type | Default |
101
+ | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- |
102
+ | `controller` | The layout state controller returned by `useZeugma(options)`. | `ZeugmaController` | - |
103
+ | `children` | Children components rendered inside the context provider. | `ReactNode` | - |
104
+ | `renderPane` | Callback function to map active pane IDs to custom pane structures. Required in standalone mode (without children) and must not be passed in provider mode. | `(paneId: string) => ReactNode` | - |
105
+ | `renderDragOverlay` | Custom overlay renderer function for the drag-under-cursor preview. | `(active: DragOverlayActiveItem) => ReactNode` | - |
106
+ | `classNames` | CSS class name mapping overrides for custom dashboard and overlay styling. | `ZeugmaClassNames` | - |
107
+ | `resizerSize` | Thickness of the split resizer bars in pixels. | `number` | `4` |
108
+ | `dragActivationDistance` | Minimum pointer drag distance (in pixels) required to activate dragging. | `number` | `8` |
109
+ | `snapThreshold` | Threshold in pixels to snap layout resizers to adjacent edges. | `number` | `8` |
110
+ | `minSplitPercentage` | Minimum split limit percentage allowed for resized panes. | `number` | `5` |
111
+ | `maxSplitPercentage` | Maximum split limit percentage allowed for resized panes. | `number` | `95` |
112
+ | `enableDragToDismiss` | Enables drag-out-to-dismiss gesture for widgets. | `boolean` | `false` |
113
+ | `dismissThreshold` | Distance in pixels outside container bounds required to trigger dismissal. | `number` | `60` |
114
+ | `onRemove` | Callback triggered when a pane is removed. | `(paneId: string) => void` | - |
115
+ | `onDragStart` | Callback triggered when a drag gesture begins. | `(activeId: string) => void` | - |
116
+ | `onDragEnd` | Callback triggered when a drag gesture ends, containing active pane, target pane, and action metadata. | `(activeId: string, overId: string \| null, dropAction: { type: 'split' \| 'move'; direction?: SplitDirection; position?: 'top' \| 'bottom' \| 'left' \| 'right' \| 'center' } \| null) => void` | - |
117
+ | `onResizeStart` | Callback triggered when resizing begins. | `(currentNode: SplitNode) => void` | - |
118
+ | `onResize` | Callback triggered during pane resizing. | `(currentNode: SplitNode, percentage: number) => void` | - |
119
+ | `onResizeEnd` | Callback triggered when pane resizing completes. | `(currentNode: SplitNode, percentage: number) => void` | - |
120
+ | `onDismissIntentChange` | Callback triggered when drag-out dismiss intent changes. | `(paneId: string \| null) => void` | - |
211
121
 
212
- Defines the interactive drag region inside a `<Pane>`. **Must be placed inside a `<Pane>` component.**
122
+ ---
213
123
 
214
- | Prop | Type | Required | Description |
215
- | ----------- | --------------------- | -------- | ---------------------------------------------------------------- |
216
- | `children` | `ReactNode` | Yes | Element(s) that function as the drag handle (e.g., pane header). |
217
- | `className` | `string` | No | Custom CSS class for the drag handle wrapper. |
218
- | `style` | `React.CSSProperties` | No | Inline styles for the drag handle wrapper. |
124
+ #### `<PaneTree>`
219
125
 
220
- ## Tree Utilities
126
+ Recursively renders the dashboard grid hierarchy (resizers, split panels, and active pane contents). **Must be rendered when using `<Zeugma>` as a context provider.**
221
127
 
222
- Import these serializable tree utility functions from `react-zeugma/utils` for programmatically mutating or querying layout schemas.
128
+ ##### Usage
223
129
 
224
- #### `generateUniqueId(): string`
130
+ ```tsx
131
+ import { Zeugma, PaneTree } from 'react-zeugma'
225
132
 
226
- Generates a unique pane ID string (e.g., `'pane-abc123xyz'`). Useful when creating new pane nodes programmatically.
133
+ ;<Zeugma controller={controller}>
134
+ <div className="workspace">
135
+ <PaneTree renderPane={(paneId) => <MyPane id={paneId} />} />
136
+ </div>
137
+ </Zeugma>
138
+ ```
227
139
 
228
- #### `removePane(tree: TreeNode | null, paneId: string): TreeNode | null`
140
+ ##### Props
229
141
 
230
- Recursively scans the layout tree, removes the targeted pane node, and collapses redundant split boundaries.
142
+ | Property | Description | Type | Default |
143
+ | --------------- | ----------------------------------------------------------------------------------------- | ------------------------------- | ------- |
144
+ | `renderPane` | **Required**. Callback function mapping unique pane IDs to custom `<Pane>` components. | `(paneId: string) => ReactNode` | - |
145
+ | `tree` | Optional layout subtree to render (defaults to the root layout tree from the controller). | `TreeNode \| null` | - |
146
+ | `resizerSize` | Optional override for the thickness of split resizer handles in pixels. | `number` | `4` |
147
+ | `snapThreshold` | Optional override for the snapping threshold of resizer handles in pixels. | `number` | `8` |
231
148
 
232
- #### `removeTab(tree: TreeNode | null, tabId: string): TreeNode | null`
149
+ ---
233
150
 
234
- 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.
151
+ #### `<Pane>`
235
152
 
236
- #### `addPane(tree: TreeNode | null, paneToAdd: string, metadata?: Record<string, unknown>): TreeNode`
153
+ Wraps each individual pane/widget within the dashboard, establishing drag-and-drop boundaries.
237
154
 
238
- 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.
155
+ - **`<Pane.DragHandle>`**: Defines the interactive header or area used to drag the pane.
156
+ - **`<Pane.Content>`**: Renders the active tab's content. Accepts a child render function `(tab) => React.ReactNode` or static ReactNode.
157
+ - **`<Pane.Controls>`**: Renders standard control buttons for closing or maximizing the pane.
239
158
 
240
- #### `addTab(tree: TreeNode | null, targetPaneId: string, tabId: string, metadata?: Record<string, unknown>): TreeNode | null`
159
+ ##### Usage
241
160
 
242
- 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.
161
+ ```tsx
162
+ import { Pane } from 'react-zeugma'
163
+
164
+ ;<Pane id="pane-1" locked={false}>
165
+ <Pane.DragHandle className="p-2 cursor-grab bg-zinc-800">
166
+ <span>Pane Title</span>
167
+ </Pane.DragHandle>
168
+ <Pane.Controls />
169
+ <Pane.Content className="p-4">
170
+ {(tab) => <div>Rendered content for tab: {tab.id}</div>}
171
+ </Pane.Content>
172
+ </Pane>
173
+ ```
243
174
 
244
- #### `splitPane(tree, targetId, direction, splitType, paneToAdd)`
175
+ ##### Props
245
176
 
246
- 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.
177
+ | Property | Description | Type | Default |
178
+ | ---------- | ------------------------------------------------------------------ | --------------------- | ------- |
179
+ | `id` | The unique ID corresponding to the layout node. | `string` | - |
180
+ | `children` | Children components rendered inside the pane. | `React.ReactNode` | - |
181
+ | `style` | Optional inline CSS styles applied to the outer pane container. | `React.CSSProperties` | - |
182
+ | `locked` | Optional override to lock this specific pane and disable dragging. | `boolean` | `false` |
247
183
 
248
- #### `updateSplitPercentage(tree: TreeNode | null, target: SplitNode, newPercentage: number): TreeNode | null`
184
+ ---
249
185
 
250
- Finds the target `SplitNode` by reference in the tree and updates its `splitPercentage`.
186
+ #### `<Tabs>`
251
187
 
252
- #### `selectTab(tree: TreeNode | null, paneId: string, tabId: string): TreeNode | null`
188
+ A helper component to render and reorder a list of tabs inside a pane.
253
189
 
254
- 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.
190
+ ##### Usage
255
191
 
256
- #### `mergeTab(tree: TreeNode | null, draggedTabId: string, targetPaneId: string): TreeNode | null`
192
+ ```tsx
193
+ import { Tabs } from 'react-zeugma'
194
+
195
+ ;<Tabs
196
+ tabs={['tab1', 'tab2']}
197
+ activeTabId="tab1"
198
+ selectTab={(tabId) => console.log('Select tab:', tabId)}
199
+ removeTab={(tabId) => console.log('Close tab:', tabId)}
200
+ renderTab={({ tabId, activeTabId, onSelect, onRemove }) => (
201
+ <button
202
+ onClick={onSelect}
203
+ className={`px-3 py-1 ${tabId === activeTabId ? 'bg-zinc-800 text-white' : 'text-zinc-400'}`}
204
+ >
205
+ {tabId}
206
+ <span
207
+ className="ml-2 cursor-pointer"
208
+ onClick={(e) => {
209
+ e.stopPropagation()
210
+ onRemove()
211
+ }}
212
+ >
213
+ ×
214
+ </span>
215
+ </button>
216
+ )}
217
+ />
218
+ ```
257
219
 
258
- 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.
220
+ ##### Props
259
221
 
260
- #### `moveTab(tree: TreeNode | null, draggedTabId: string, targetTabId: string, position?: 'before' | 'after'): TreeNode | null`
222
+ | Property | Description | Type | Default |
223
+ | -------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- |
224
+ | `tabs` | Array of tab IDs. | `string[]` | - |
225
+ | `activeTabId` | The currently active tab ID. | `string` | - |
226
+ | `locked` | Whether tab dragging/reordering is disabled. | `boolean` | `false` |
227
+ | `tabsMetadata` | Metadata mapping associated with each tab in the pane. | `Record<string, Record<string, unknown>>` | - |
228
+ | `selectTab` | Callback when a tab is selected. | `(id: string) => void` | - |
229
+ | `removeTab` | Callback when a tab is closed. | `(id: string) => void` | - |
230
+ | `classNames` | Custom class names for the container and tabs. | `{ container?: string; tab?: string \| ((tabId: string) => string) }` | - |
231
+ | `styles` | Custom CSS style overrides for the container and tabs. | `{ container?: CSSProperties; tab?: CSSProperties \| ((tabId: string) => CSSProperties) }` | - |
232
+ | `renderTab` | Render prop function called for each tab item. | `(props: { tabId: string; activeTabId: string; isDragging: boolean; isOver: boolean; metadata?: Record<string, unknown>; onSelect: () => void; onRemove: () => void }) => ReactNode` | - |
261
233
 
262
- Reorders `draggedTabId` adjacent to `targetTabId` within the same pane (or moves it cross-pane). `position` defaults to `'before'`.
234
+ ---
263
235
 
264
- #### `updateTabMetadata(tree: TreeNode | null, tabId: string, updater: (current: Record<string, unknown> | undefined) => Record<string, unknown> | undefined): TreeNode | null`
236
+ ### Hooks
265
237
 
266
- Updates the metadata of a specific tab using an updater function. Returning `undefined` from the updater removes the entry.
238
+ #### `useZeugma(options)`
267
239
 
268
- #### `updatePaneLock(tree: TreeNode | null, paneId: string, locked: boolean): TreeNode | null`
240
+ A custom state hook that instantiates the dashboard layout engine and returns the controller.
269
241
 
270
- 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.
242
+ ##### Usage
271
243
 
272
- #### `findPaneById(tree: TreeNode | null, paneId: string): PaneNode | null`
244
+ ```tsx
245
+ import { useZeugma } from 'react-zeugma'
246
+
247
+ const controller = useZeugma({
248
+ initialLayout: myInitialLayoutTree, // used on mount
249
+ layout: myControlledLayout, // used for controlled mode
250
+ onChange: (nextLayout) => {}, // layout update callback
251
+ locked: false, // lock all dragging and resizing
252
+ fullscreenPaneId: null, // ID of pane to zoom fullscreen
253
+ onFullscreenChange: (paneId) => {}, // callback when pane zoom toggled
254
+ })
255
+ ```
273
256
 
274
- Recursively searches the layout tree and returns the target `PaneNode` if found, or `null` otherwise.
257
+ ##### Options
275
258
 
276
- #### `findPaneContainingTab(tree: TreeNode | null, tabId: string): PaneNode | null`
259
+ | Parameter | Description | Type | Default |
260
+ | -------------------- | --------------------------------------------------------------------------- | --------------------------------------- | ------- |
261
+ | `initialLayout` | Initial layout tree structure. Only used on mount. | `TreeNode \| null` | `null` |
262
+ | `layout` | Controlled layout tree structure. Hook runs in controlled mode if provided. | `TreeNode \| null` | `null` |
263
+ | `onChange` | Callback triggered when the layout tree updates. | `(newLayout: TreeNode \| null) => void` | - |
264
+ | `fullscreenPaneId` | Controlled fullscreen pane ID. | `string \| null` | `null` |
265
+ | `onFullscreenChange` | Callback triggered when fullscreen state toggles. | `(paneId: string \| null) => void` | - |
266
+ | `locked` | Global lock status to disable resizing and drag-and-drop operations. | `boolean` | `false` |
277
267
 
278
- Recursively searches the layout tree and returns the `PaneNode` containing the specified `tabId`.
268
+ ---
279
269
 
280
- #### `findTabById(tree: TreeNode | null, tabId: string): TabDetails | null`
270
+ #### `useZeugmaContext()`
281
271
 
282
- Searches the layout tree for the given `tabId` and returns computed details (parent `paneId`, `isActive`, `index`, and custom `metadata`).
272
+ Context hook to retrieve layout state, queries, and mutation actions from anywhere under the `<Zeugma>` tree.
283
273
 
284
- #### `computeLayout(node: TreeNode | null, left?, top?, width?, height?): { panes: ComputedPane[]; splitters: ComputedSplitter[] }`
274
+ ##### Usage
285
275
 
286
- 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.
276
+ ```tsx
277
+ import { useZeugmaContext } from 'react-zeugma'
287
278
 
288
- #### `calculateTabDropIndex(tabs: string[], activeType: string | null, overTabId: string | null, overTabPosition: 'before' | 'after' | null): number`
279
+ const { layout, locked, setLocked, addTab, removePane, selectTab, findPaneById } =
280
+ useZeugmaContext()
281
+ ```
289
282
 
290
- 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.
283
+ ##### Context Values
284
+
285
+ | Property / Method | Description | Type |
286
+ | ----------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
287
+ | `layout` | The current active layout tree structure. | `TreeNode \| null` |
288
+ | `fullscreenPaneId` | The ID of the maximized fullscreen pane. | `string \| null` |
289
+ | `locked` | Whether the dashboard layout is globally locked. | `boolean` |
290
+ | `setLayout` | React state setter to update the layout tree. | `Dispatch<SetStateAction<TreeNode \| null>>` |
291
+ | `setFullscreenPaneId` | Updates the active fullscreen pane ID. | `(paneId: string \| null) => void` |
292
+ | `setLocked` | Updates the global layout lock state. | `Dispatch<SetStateAction<boolean>>` |
293
+ | `removePane` | Removes a pane and collapses the split. | `(paneId: string) => void` |
294
+ | `addTab` | Adds a tab to a pane, or splits/creates one if target is omitted. | `(tabId: string, targetPaneId?: string, metadata?: Record<string, unknown>) => void` |
295
+ | `updateMetadata` | Mutates a specific tab's metadata. | `(id: string, updater: (current: Record<string, unknown> \| undefined) => Record<string, unknown> \| undefined) => void` |
296
+ | `updatePaneLock` | Toggles the lock status of a specific pane. | `(paneId: string, locked: boolean) => void` |
297
+ | `selectTab` | Focuses/activates a tab within a pane. | `(paneId: string, tabId: string) => void` |
298
+ | `mergeTab` | Programmatically drags and drops a tab from one pane to another. | `(draggedTabId: string, targetPaneId: string) => void` |
299
+ | `removeTab` | Programmatically closes a tab. | `(tabId: string) => void` |
300
+ | `splitPane` | Programmatically splits a pane node and adds a new one. | `(targetId: string, direction: SplitDirection, splitType: 'left' \| 'right' \| 'top' \| 'bottom', paneToAdd: string) => void` |
301
+ | `updateSplitPercentage` | Updates a SplitNode percentage. | `(currentNode: SplitNode, percentage: number) => void` |
302
+ | `moveTab` | Reorders a tab next to another. | `(draggedTabId: string, targetTabId: string, position?: 'before' \| 'after') => void` |
303
+ | `findPaneById` | Queries a PaneNode by its unique ID. | `(paneId: string) => PaneNode \| null` |
304
+ | `findPaneContainingTab` | Queries the parent PaneNode of a tab ID. | `(tabId: string) => PaneNode \| null` |
305
+ | `findTabById` | Queries detailed tab location and state metadata. | `(tabId: string) => TabDetails \| null` |
306
+ | `getTabMetadata` | Gets metadata for a tab ID. | `(tabId: string) => Record<string, unknown> \| undefined` |
307
+ | `getActiveTabMetadata` | Gets metadata for the active tab in a pane. | `(paneId: string) => Record<string, unknown> \| undefined` |
291
308
 
292
309
  ---
293
310
 
294
- ## Custom Styling
311
+ #### `usePaneContext()`
312
+
313
+ Context hook to access the state and actions of a specific pane. Must be used inside a `<Pane>` component.
295
314
 
296
- Use custom CSS or styling rules to style resizers, dragging states, drop previews, or active nodes by overriding `classNames` in the provider.
315
+ ##### Usage
297
316
 
298
317
  ```tsx
299
- <Zeugma
300
- {...zeugma}
301
- classNames={{
302
- // resizer handles
303
- resizer:
304
- 'bg-transparent hover:bg-indigo-500/50 active:bg-indigo-500 transition-colors duration-150',
305
- // split previews
306
- dropPreview: 'bg-indigo-500/10 border-2 border-dashed border-indigo-500/50 backdrop-blur-xs',
307
- // tab separator line
308
- tabSeparator: 'w-px h-4 bg-zinc-700',
309
- }}
310
- >
311
- <PaneTree />
312
- </Zeugma>
318
+ import { usePaneContext } from 'react-zeugma'
319
+
320
+ const {
321
+ id,
322
+ tabs,
323
+ activeTabId,
324
+ isDragging,
325
+ isFullscreen,
326
+ toggleFullscreen,
327
+ remove,
328
+ selectTab,
329
+ removeTab,
330
+ updateMetadata,
331
+ } = usePaneContext()
313
332
  ```
314
333
 
315
- ### `ZeugmaClassNames` reference
316
-
317
- | Key | Applied to |
318
- | ------------------------ | --------------------------------------------------------------------------------- |
319
- | `dashboard` | Root dashboard container. |
320
- | `dashboardDismissActive` | Root container when a drag-out dismiss is active. |
321
- | `dashboardLocked` | Root container when the dashboard is globally locked. |
322
- | `pane` | Outer wrapper `<div>` of each `<Pane>`. |
323
- | `paneLocked` | Pane container when locked. |
324
- | `dropPreview` | Drop zone preview box when hovering over a split-edge drop zone. |
325
- | `dragOverlay` | Cursor-following drag preview portal wrapper. |
326
- | `resizer` | Drag-to-resize split bar handles. |
327
- | `dismissPreview` | Background dismiss zone indicator during a drag-out dismiss gesture. |
328
- | `lockedPreview` | Drop zone indicator when hovering over a locked pane. |
329
- | `tabDropPreview` | Placeholder line element rendered at the target insertion point during tab drags. |
330
- | `tabSeparator` | Separator line rendered between non-active adjacent tabs in `<Tabs>`. |
334
+ ##### Context Values
335
+
336
+ | Property / Method | Description | Type |
337
+ | ------------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
338
+ | `id` | The ID of the current pane. | `string` |
339
+ | `tabs` | List of tab IDs inside the pane. | `string[]` |
340
+ | `activeTabId` | Currently active tab ID. | `string` |
341
+ | `isDragging` | `true` if this pane is being dragged. | `boolean` |
342
+ | `isFullscreen` | `true` if this pane is maximized. | `boolean` |
343
+ | `toggleFullscreen` | Toggles maximized state for this pane. | `() => void` |
344
+ | `remove` | Removes this pane from the layout tree. | `() => void` |
345
+ | `selectTab` | Activates a tab within this pane. | `(tabId: string) => void` |
346
+ | `removeTab` | Closes a tab from this pane. | `(tabId: string) => void` |
347
+ | `metadata` | Active tab's custom metadata. | `Record<string, unknown> \| undefined` |
348
+ | `updateMetadata` | Updates active tab's metadata. | `(updater: (current: Record<string, unknown> \| undefined) => Record<string, unknown> \| undefined) => void` |
349
+ | `locked` | Whether the pane or the dashboard is locked. | `boolean` |
350
+ | `tabsMetadata` | Tab metadata mapping for all tabs inside this pane. | `Record<string, Record<string, unknown>> \| undefined` |
351
+ | `updateTabMetadata` | Updates metadata for a specific tab in the pane. | `(tabId: string, updater: (current: Record<string, unknown> \| undefined) => Record<string, unknown> \| undefined) => void` |
331
352
 
332
353
  ---
333
354
 
334
- ## Types Reference
355
+ #### `useResizer(props)`
335
356
 
336
- Full TypeScript type definitions exported from `react-zeugma`.
357
+ Low-level hook for implementing custom pane resizing handles.
337
358
 
338
- ### Layout Tree
339
-
340
- ```ts
341
- export type SplitDirection = 'row' | 'column'
342
-
343
- export interface SplitNode {
344
- type: 'split'
345
- direction: SplitDirection
346
- first: TreeNode
347
- second: TreeNode
348
- splitPercentage: number // 0–100
349
- }
350
-
351
- export interface PaneNode {
352
- type: 'pane'
353
- id: string
354
- tabs: string[]
355
- activeTabId: string
356
- locked?: boolean
357
- tabsMetadata?: Record<string, Record<string, unknown>>
358
- }
359
+ ##### Usage
359
360
 
360
- export type TreeNode = SplitNode | PaneNode
361
-
362
- export interface TabDetails {
363
- id: string
364
- paneId: string
365
- isActive: boolean
366
- index: number
367
- metadata: Record<string, unknown> | undefined
368
- }
361
+ ```tsx
362
+ import { useResizer } from 'react-zeugma'
363
+
364
+ const handlePointerDown = useResizer({
365
+ containerRef,
366
+ isRow,
367
+ direction,
368
+ splitPercentage,
369
+ resizerSize,
370
+ snapThreshold,
371
+ layout,
372
+ currentNode,
373
+ onLayoutChange: (nextTree) => {},
374
+ })
369
375
  ```
370
376
 
371
- ### Component Props
372
-
373
- ```ts
374
- export interface PaneProps {
375
- id: string
376
- children: React.ReactNode
377
- style?: React.CSSProperties
378
- locked?: boolean
379
- }
377
+ ---
380
378
 
381
- export interface DragHandleProps {
382
- children?: React.ReactNode
383
- className?: string
384
- style?: React.CSSProperties
385
- }
379
+ ## Layout Utilities
386
380
 
387
- export interface TabsProps {
388
- tabs: string[]
389
- activeTabId: string
390
- locked?: boolean
391
- tabsMetadata?: Record<string, Record<string, unknown>>
392
- selectTab: (id: string) => void
393
- removeTab: (id: string) => void
394
- renderTab: (props: {
395
- tabId: string
396
- activeTabId: string
397
- isDragging: boolean
398
- isOver: boolean
399
- metadata?: Record<string, unknown>
400
- onSelect: () => void
401
- onRemove: () => void
402
- }) => React.ReactNode
403
- classNames?: {
404
- container?: string
405
- tab?: string | ((tabId: string) => string)
406
- }
407
- styles?: {
408
- container?: React.CSSProperties
409
- tab?: React.CSSProperties | ((tabId: string) => React.CSSProperties)
410
- }
411
- }
412
- ```
381
+ Import utility functions from `react-zeugma/utils` to programmatically query or update the serialized layout tree.
413
382
 
414
- ### Render Props
383
+ ##### Usage
415
384
 
416
385
  ```ts
417
- export interface PaneRenderProps {
418
- isDragging: boolean
419
- isFullscreen: boolean
420
- toggleFullscreen: () => void
421
- remove: () => void
422
- metadata: Record<string, unknown> | undefined
423
- updateMetadata: (
424
- updater: (current: Record<string, unknown> | undefined) => Record<string, unknown> | undefined,
425
- ) => void
426
- locked: boolean
427
- tabs: string[]
428
- activeTabId: string
429
- selectTab: (tabId: string) => void
430
- removeTab: (tabId: string) => void
431
- tabsMetadata: Record<string, Record<string, unknown>> | undefined
432
- updateTabMetadata: (
433
- tabId: string,
434
- updater: (current: Record<string, unknown> | undefined) => Record<string, unknown> | undefined,
435
- ) => void
436
- }
437
-
438
- }
386
+ import {
387
+ generateUniqueId,
388
+ splitPane,
389
+ removePane,
390
+ addTab,
391
+ removeTab,
392
+ selectTab,
393
+ mergeTab,
394
+ moveTab,
395
+ findPaneById,
396
+ findPaneContainingTab,
397
+ findTabById,
398
+ computeLayout,
399
+ } from 'react-zeugma/utils'
400
+
401
+ // 1. Generate a random unique pane ID
402
+ const newPaneId = generateUniqueId()
403
+
404
+ // 2. Programmatically split a target pane in the tree
405
+ const updatedTree = splitPane(currentTree, 'explorer', 'row', 'right', 'terminal')
406
+
407
+ // 3. Programmatically add a tab to a pane
408
+ const updatedTree = addTab(currentTree, 'editor', 'new-file.js', { status: 'unsaved' })
409
+
410
+ // 4. Find which pane contains a specific tab
411
+ const parentPane = findPaneContainingTab(currentTree, 'new-file.js')
439
412
  ```
440
413
 
441
- ### Controller & Context
442
-
443
- ```ts
444
- export interface ZeugmaState {
445
- layout: TreeNode | null
446
- fullscreenPaneId: string | null
447
- locked: boolean
448
- }
414
+ ---
449
415
 
450
- export interface ZeugmaStateSetters {
451
- setLayout: Dispatch<SetStateAction<TreeNode | null>>
452
- setFullscreenPaneId: (paneId: string | null) => void
453
- setLocked: Dispatch<SetStateAction<boolean>>
454
- }
416
+ ## Styling & Class Names
455
417
 
456
- export interface ZeugmaActions {
457
- removePane: (paneId: string) => void
458
- addTab: (tabId: string, targetPaneId?: string, metadata?: Record<string, unknown>) => void
459
- updateMetadata: (
460
- id: string,
461
- updater: (current: Record<string, unknown> | undefined) => Record<string, unknown> | undefined,
462
- ) => void
463
- updatePaneLock: (paneId: string, locked: boolean) => void
464
- selectTab: (paneId: string, tabId: string) => void
465
- mergeTab: (draggedTabId: string, targetPaneId: string) => void
466
- removeTab: (tabId: string) => void
467
- splitPane: (
468
- targetId: string,
469
- direction: SplitDirection,
470
- splitType: 'left' | 'right' | 'top' | 'bottom',
471
- paneToAdd: string,
472
- ) => void
473
- updateSplitPercentage: (currentNode: SplitNode, percentage: number) => void
474
- moveTab: (draggedTabId: string, targetTabId: string, position?: 'before' | 'after') => void
475
- }
418
+ `react-zeugma` is 100% headless. You must style resizers, previews, and containers by providing custom class names.
476
419
 
477
- export interface ZeugmaQueries {
478
- findPaneById: (paneId: string) => PaneNode | null
479
- findPaneContainingTab: (tabId: string) => PaneNode | null
480
- findTabById: (tabId: string) => TabDetails | null
481
- getTabMetadata: (tabId: string) => Record<string, unknown> | undefined
482
- getActiveTabMetadata: (paneId: string) => Record<string, unknown> | undefined
483
- }
420
+ ##### Usage
484
421
 
485
- export interface ZeugmaController
486
- extends ZeugmaState, ZeugmaStateSetters, ZeugmaActions, ZeugmaQueries {}
422
+ ```tsx
423
+ <Zeugma
424
+ controller={controller}
425
+ classNames={{
426
+ dashboard: 'dashboard-root',
427
+ pane: 'pane-wrapper',
428
+ resizer: 'custom-resizer-line',
429
+ dropPreview: 'drop-preview-box',
430
+ tabDropPreview: 'tab-line-preview',
431
+ }}
432
+ />
487
433
  ```
488
434
 
489
- ### Computed Layout Types (from `react-zeugma/utils`)
490
-
491
- ```ts
492
- export interface ComputedPane {
493
- paneId: string
494
- left: number // percentage
495
- top: number // percentage
496
- width: number // percentage
497
- height: number // percentage
498
- node: PaneNode
499
- }
500
-
501
- export interface ComputedSplitter {
502
- id: string
503
- currentNode: SplitNode
504
- direction: SplitDirection
505
- left: number
506
- top: number
507
- width: number
508
- height: number
509
- parentLeft: number
510
- parentTop: number
511
- parentWidth: number
512
- parentHeight: number
513
- }
514
- ```
435
+ ##### Class Names Mapping
436
+
437
+ | Class Key | Description |
438
+ | ------------------------ | ------------------------------------------------------------------- |
439
+ | `dashboard` | Root dashboard grid container. |
440
+ | `dashboardDismissActive` | Dashboard container when active item is dragged outside to dismiss. |
441
+ | `dashboardLocked` | Dashboard container when layout is globally locked. |
442
+ | `pane` | Outer container div of each `<Pane>`. |
443
+ | `paneLocked` | Pane container wrapper when locked. |
444
+ | `paneContainer` | Pane inner content container wrapper. |
445
+ | `paneHeader` | Drag header wrapper inside the pane. |
446
+ | `paneControls` | Controls wrapper container (maximizing, close, lock). |
447
+ | `paneButton` | Maximize/Close control buttons. |
448
+ | `dropPreview` | Preview indicator box for edge layout splits. |
449
+ | `rootDropPreview` | Preview indicator for full layout splits. |
450
+ | `dragOverlay` | Absolute portal wrapper following the dragging cursor. |
451
+ | `resizer` | Pointer-drag splitter handle bars. |
452
+ | `dismissPreview` | Background indicator showing visual drag-to-dismiss zones. |
453
+ | `lockedPreview` | Hover visual feedback indicator for locked pane zones. |
454
+ | `tabDropPreview` | Tab list insertion indicator line. |
455
+ | `tabSeparator` | Line separator between static tabs. |
456
+ | `tabContentWrapper` | Custom tab content element wrapper. |
457
+ | `tabsContainer` | Layout tabs container header bar. |
458
+ | `tab` | Individual tab list items. |
459
+ | `tabCloseButton` | Close button inside a tab item. |
460
+ | `dragHandle` | Drag target region. |