react-zeugma 0.5.0 → 0.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 +309 -57
- package/dist/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,34 +1,26 @@
|
|
|
1
|
-
|
|
1
|
+
{/_ @meta title="Introduction" _/}
|
|
2
2
|
|
|
3
3
|
# react-zeugma
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**A recursive, drag-and-drop dashboard layout engine for React.**
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
`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).
|
|
8
8
|
|
|
9
9
|
[](https://www.npmjs.com/package/react-zeugma)
|
|
10
10
|
[](https://bundlephobia.com/package/react-zeugma)
|
|
11
11
|
[](./LICENSE)
|
|
12
12
|
[](https://www.typescriptlang.org)
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
---
|
|
17
|
-
|
|
18
|
-
## Introduction
|
|
19
|
-
|
|
20
|
-
**react-zeugma** is a recursive drag-and-drop dashboard layout engine for React. It combines the tree-based, arbitrary splitting of _react-mosaic_ with the declarative, state-driven API of _react-grid-layout_, built on top of [`@dnd-kit`](https://dndkit.com).
|
|
21
|
-
|
|
14
|
+
> [!TIP]
|
|
22
15
|
> **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.
|
|
23
16
|
|
|
24
17
|
### Core Features
|
|
25
18
|
|
|
26
|
-
- **Recursive Split Trees
|
|
27
|
-
- **5-Zone Docking Previews
|
|
28
|
-
- **Native Flexbox Resizers
|
|
29
|
-
- **Accessible Drag-and-Drop
|
|
30
|
-
- **Fullscreen Zoom Toggle
|
|
31
|
-
- **Tree-shakeable & Tiny** — ESM-first with zero runtime CSS. Bring your own styles.
|
|
19
|
+
- **Recursive Split Trees**: Nest rows and columns to any depth using a simple serialized JSON node structure.
|
|
20
|
+
- **5-Zone Docking Previews**: Drag panels on the top, bottom, left, or right edges of another pane to split it, or onto the center to swap their positions.
|
|
21
|
+
- **Native Flexbox Resizers**: Fluid, non-blocking split handles built on pointer events.
|
|
22
|
+
- **Accessible Drag-and-Drop**: Built on top of the performant and accessible [`@dnd-kit`](https://dndkit.com) toolkit.
|
|
23
|
+
- **Fullscreen Zoom Toggle**: Programmatically expand any pane to cover the entire viewport and snap it back instantly.
|
|
32
24
|
|
|
33
25
|
---
|
|
34
26
|
|
|
@@ -40,7 +32,8 @@ Install the package into your React project using your preferred package manager
|
|
|
40
32
|
npm install react-zeugma
|
|
41
33
|
```
|
|
42
34
|
|
|
43
|
-
>
|
|
35
|
+
> [!NOTE]
|
|
36
|
+
> **Peer Dependencies** — react-zeugma is compatible with both **React 18** and **React 19** (along with matching `react-dom`).
|
|
44
37
|
|
|
45
38
|
---
|
|
46
39
|
|
|
@@ -51,7 +44,6 @@ Import the core components and configure the layout state inside your React appl
|
|
|
51
44
|
```tsx
|
|
52
45
|
import { useState } from 'react'
|
|
53
46
|
import { DashboardProvider, PaneTree, Pane, DragHandle, TreeNode } from 'react-zeugma'
|
|
54
|
-
import './Dashboard.css' // Import your custom styles here
|
|
55
47
|
|
|
56
48
|
const initialLayout: TreeNode = {
|
|
57
49
|
type: 'split',
|
|
@@ -71,16 +63,16 @@ function MyPane({ id }: { id: string }) {
|
|
|
71
63
|
return (
|
|
72
64
|
<Pane id={id}>
|
|
73
65
|
{({ isDragging, remove }) => (
|
|
74
|
-
<div className={`
|
|
66
|
+
<div className={`h-full flex flex-col bg-[#18181b] ${isDragging ? 'opacity-30' : ''}`}>
|
|
75
67
|
<DragHandle>
|
|
76
|
-
<div className="
|
|
77
|
-
<span className="
|
|
78
|
-
<button onClick={remove} className="
|
|
68
|
+
<div className="px-3 py-2 bg-[#27272a] border-b border-[#3f3f46] flex items-center justify-between cursor-grab">
|
|
69
|
+
<span className="text-xs uppercase text-zinc-300 font-bold">{id}</span>
|
|
70
|
+
<button onClick={remove} className="text-zinc-500 hover:text-rose-400 text-xs">
|
|
79
71
|
×
|
|
80
72
|
</button>
|
|
81
73
|
</div>
|
|
82
74
|
</DragHandle>
|
|
83
|
-
<div className="
|
|
75
|
+
<div className="flex-1 p-4 text-sm text-zinc-400">Content for {id}</div>
|
|
84
76
|
</div>
|
|
85
77
|
)}
|
|
86
78
|
</Pane>
|
|
@@ -92,7 +84,7 @@ export default function Dashboard() {
|
|
|
92
84
|
|
|
93
85
|
return (
|
|
94
86
|
<DashboardProvider layout={layout} onChange={setLayout} renderPane={(id) => <MyPane id={id} />}>
|
|
95
|
-
<div className="
|
|
87
|
+
<div className="w-screen h-screen">
|
|
96
88
|
<PaneTree />
|
|
97
89
|
</div>
|
|
98
90
|
</DashboardProvider>
|
|
@@ -108,17 +100,25 @@ export default function Dashboard() {
|
|
|
108
100
|
|
|
109
101
|
The context provider that sets up the drag-and-drop state machine, monitors active drags, and registers layout change notifications.
|
|
110
102
|
|
|
111
|
-
| Prop | Type
|
|
112
|
-
| ------------------------ |
|
|
113
|
-
| `layout` | `TreeNode \| null`
|
|
114
|
-
| `onChange` | `(layout: TreeNode \| null) => void`
|
|
115
|
-
| `renderPane` | `(paneId: string) => ReactNode`
|
|
116
|
-
| `
|
|
117
|
-
| `
|
|
118
|
-
| `
|
|
119
|
-
| `onFullscreenChange` | `(paneId: string \| null) => void`
|
|
120
|
-
| `onRemove` | `(paneId: string) => void`
|
|
121
|
-
| `dragActivationDistance` | `number`
|
|
103
|
+
| Prop | Type | Required | Description |
|
|
104
|
+
| ------------------------ | --------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
105
|
+
| `layout` | `TreeNode \| null` | Yes | The serializable tree layout schema. |
|
|
106
|
+
| `onChange` | `(layout: TreeNode \| null) => void` | Yes | Fires when resizes, splits, swaps, or removes modify the tree. |
|
|
107
|
+
| `renderPane` | `(paneId: string) => ReactNode` | Yes | Renderer function lookup that returns a `<Pane>` structure. |
|
|
108
|
+
| `classNames` | `ZeugmaClassNames` | No | Custom classes for overriding pane, resizer, and drop preview overlays. |
|
|
109
|
+
| `fullscreenPaneId` | `string \| null` | No | Active ID of the pane taking full viewport coverage. |
|
|
110
|
+
| `renderDragOverlay` | `(activeId: string) => ReactNode` | No | Renders a custom cursor-following drag preview overlay. |
|
|
111
|
+
| `onFullscreenChange` | `(paneId: string \| null) => void` | No | Callback triggered when a pane enters or leaves fullscreen. |
|
|
112
|
+
| `onRemove` | `(paneId: string) => void` | No | Callback triggered when a pane is closed/removed from the layout tree. |
|
|
113
|
+
| `dragActivationDistance` | `number` | No | Minimum pointer drag distance (in pixels) required to activate dragging. Defaults to `8`. |
|
|
114
|
+
| `onDragStart` | `(activeId: string) => void` | No | Callback triggered when dragging starts on a pane. |
|
|
115
|
+
| `onDragEnd` | `(activeId: string, overId: string \| null, dropAction: any) => void` | No | Callback triggered when dragging ends, providing swap or split details. The `overId` is set to `'root'` if dropped onto outer boundaries to split the entire dashboard root. |
|
|
116
|
+
| `onResizeStart` | `(currentNode: SplitNode) => void` | No | Callback triggered when resizing starts on a split node. |
|
|
117
|
+
| `onResize` | `(currentNode: SplitNode, percentage: number) => void` | No | Callback triggered continuously while resizing a split node. |
|
|
118
|
+
| `onResizeEnd` | `(currentNode: SplitNode, percentage: number) => void` | No | Callback triggered when resizing ends on a split node. |
|
|
119
|
+
| `renderResizer` | `(props: ResizerRenderProps) => ReactNode` | No | Custom renderer function for rendering custom-styled resizer bars. |
|
|
120
|
+
| `minSplitPercentage` | `number` | No | Minimum resizing limit percentage. Defaults to `5`. |
|
|
121
|
+
| `maxSplitPercentage` | `number` | No | Maximum resizing limit percentage. Defaults to `95`. |
|
|
122
122
|
|
|
123
123
|
### `<PaneTree>`
|
|
124
124
|
|
|
@@ -153,7 +153,7 @@ Defines the interactive drag region inside a `<Pane>`. **Must be placed inside a
|
|
|
153
153
|
|
|
154
154
|
| Prop | Type | Required | Description |
|
|
155
155
|
| ----------- | --------------------- | -------- | ---------------------------------------------------------------- |
|
|
156
|
-
| `children` | `
|
|
156
|
+
| `children` | `ReactNode` | Yes | Element(s) that function as the drag handle (e.g., pane header). |
|
|
157
157
|
| `className` | `string` | No | Custom CSS class for the drag handle wrapper. |
|
|
158
158
|
| `style` | `React.CSSProperties` | No | Inline styles for the drag handle wrapper. |
|
|
159
159
|
|
|
@@ -179,6 +179,10 @@ Swaps the positions of `idA` and `idB` nodes directly inside the tree structure.
|
|
|
179
179
|
|
|
180
180
|
Splits the targeted `targetId` pane inside the tree with `direction` (_row_ / _column_) and type (_left_, _right_, _top_, _bottom_) to insert `paneToAdd`.
|
|
181
181
|
|
|
182
|
+
#### `splitRoot(tree, draggingId, splitType)`
|
|
183
|
+
|
|
184
|
+
Splits the entire dashboard tree at the root, placing the dragged `draggingId` pane on one half and the rest of the layout tree on the other.
|
|
185
|
+
|
|
182
186
|
---
|
|
183
187
|
|
|
184
188
|
## Custom Styling
|
|
@@ -192,11 +196,12 @@ Use custom CSS or styling rules to style resizers, dragging states, drop preview
|
|
|
192
196
|
renderPane={renderPane}
|
|
193
197
|
classNames={{
|
|
194
198
|
// resizer handles
|
|
195
|
-
resizer:
|
|
199
|
+
resizer:
|
|
200
|
+
'bg-transparent hover:bg-indigo-500/50 active:bg-indigo-500 transition-colors duration-150',
|
|
196
201
|
// split previews
|
|
197
|
-
dropPreview: '
|
|
202
|
+
dropPreview: 'bg-indigo-500/10 border-2 border-dashed border-indigo-500/50 backdrop-blur-xs',
|
|
198
203
|
// swap previews
|
|
199
|
-
swapPreview: '
|
|
204
|
+
swapPreview: 'bg-amber-500/10 border-2 border-dashed border-amber-500/50 backdrop-blur-xs',
|
|
200
205
|
}}
|
|
201
206
|
>
|
|
202
207
|
<PaneTree />
|
|
@@ -241,45 +246,284 @@ export interface PaneRenderProps {
|
|
|
241
246
|
toggleFullscreen: () => void
|
|
242
247
|
remove: () => void
|
|
243
248
|
}
|
|
249
|
+
|
|
250
|
+
export interface ResizerRenderProps {
|
|
251
|
+
direction: SplitDirection
|
|
252
|
+
splitPercentage: number
|
|
253
|
+
resizerSize: number
|
|
254
|
+
isResizing: boolean
|
|
255
|
+
onPointerDown: (e: React.PointerEvent<HTMLDivElement>) => void
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export interface DashboardContextValue {
|
|
259
|
+
layout: TreeNode | null
|
|
260
|
+
onLayoutChange: (newLayout: TreeNode | null) => void
|
|
261
|
+
renderPane: (paneId: string) => ReactNode
|
|
262
|
+
activeId: string | null
|
|
263
|
+
fullscreenPaneId: string | null
|
|
264
|
+
classNames: ZeugmaClassNames
|
|
265
|
+
onRemove?: (paneId: string) => void
|
|
266
|
+
onFullscreenChange?: (paneId: string | null) => void
|
|
267
|
+
snapThreshold?: number
|
|
268
|
+
onResizeStart?: (currentNode: SplitNode) => void
|
|
269
|
+
onResize?: (currentNode: SplitNode, percentage: number) => void
|
|
270
|
+
onResizeEnd?: (currentNode: SplitNode, percentage: number) => void
|
|
271
|
+
renderResizer?: (props: ResizerRenderProps) => ReactNode
|
|
272
|
+
minSplitPercentage?: number
|
|
273
|
+
maxSplitPercentage?: number
|
|
274
|
+
removePane: (paneId: string) => void
|
|
275
|
+
addPane: (paneId: string) => void
|
|
276
|
+
swapPanes: (paneIdA: string, paneIdB: string) => void
|
|
277
|
+
splitPane: (
|
|
278
|
+
targetId: string,
|
|
279
|
+
direction: SplitDirection,
|
|
280
|
+
splitType: 'left' | 'right' | 'top' | 'bottom',
|
|
281
|
+
paneToAdd: string,
|
|
282
|
+
) => void
|
|
283
|
+
updateSplitPercentage: (currentNode: SplitNode, percentage: number) => void
|
|
284
|
+
}
|
|
244
285
|
```
|
|
245
286
|
|
|
246
287
|
---
|
|
247
288
|
|
|
248
289
|
## SKILL.md
|
|
249
290
|
|
|
250
|
-
|
|
291
|
+
Below is the comprehensive developer skill configuration for integrations, tree manipulation, and styling patterns within `react-zeugma`. Copy or download it for AI agents or reference.
|
|
251
292
|
|
|
293
|
+
````markdown
|
|
294
|
+
---
|
|
295
|
+
name: use-react-zeugma
|
|
296
|
+
description: Integrate, configure, style, and programmatically manipulate dashboard layouts using the react-zeugma package.
|
|
252
297
|
---
|
|
253
298
|
|
|
254
|
-
|
|
299
|
+
# Skill: Using react-zeugma
|
|
255
300
|
|
|
256
|
-
|
|
257
|
-
# Clone & install
|
|
258
|
-
git clone [https://github.com/yusufarsln98/react-zeugma.git](https://github.com/yusufarsln98/react-zeugma.git)
|
|
259
|
-
cd react-zeugma
|
|
260
|
-
npm install
|
|
301
|
+
`react-zeugma` is a recursive drag-and-drop dashboard layout engine for React. It combines tree-based pane splitting (similar to `react-mosaic`) with a declarative, state-driven API (similar to `react-grid-layout`), built using `@dnd-kit/core`.
|
|
261
302
|
|
|
262
|
-
|
|
263
|
-
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
## 1. Data Model (Tree Nodes)
|
|
306
|
+
|
|
307
|
+
The entire dashboard layout is represented as a serializable recursive tree structure.
|
|
308
|
+
|
|
309
|
+
### Types & Interface
|
|
264
310
|
|
|
265
|
-
|
|
266
|
-
|
|
311
|
+
```ts
|
|
312
|
+
export type SplitDirection = 'row' | 'column'
|
|
267
313
|
|
|
268
|
-
|
|
269
|
-
|
|
314
|
+
export interface SplitNode {
|
|
315
|
+
type: 'split'
|
|
316
|
+
direction: SplitDirection
|
|
317
|
+
first: TreeNode
|
|
318
|
+
second: TreeNode
|
|
319
|
+
splitPercentage: number // 0 to 100
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export interface PaneNode {
|
|
323
|
+
type: 'pane'
|
|
324
|
+
paneId: string
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export type TreeNode = SplitNode | PaneNode
|
|
270
328
|
```
|
|
271
329
|
|
|
330
|
+
- **`PaneNode` (Leaf):** Represents a single content pane. It must have a unique `paneId`.
|
|
331
|
+
- **`SplitNode` (Branch):** Splits its area horizontally (`column`) or vertically (`row`) into two child `TreeNode` nodes (`first` and `second`), based on `splitPercentage`.
|
|
332
|
+
|
|
272
333
|
---
|
|
273
334
|
|
|
274
|
-
##
|
|
335
|
+
## 2. Core Components
|
|
336
|
+
|
|
337
|
+
### `<DashboardProvider>`
|
|
338
|
+
|
|
339
|
+
The root context provider. It handles the drag-and-drop event loop and coordinates the layout state.
|
|
340
|
+
|
|
341
|
+
#### Props
|
|
342
|
+
|
|
343
|
+
- `layout: TreeNode | null` — The current dashboard layout tree.
|
|
344
|
+
- `onChange: (newLayout: TreeNode | null) => void` — Callback triggered when the layout tree changes (resizing, dragging to split, dragging to swap).
|
|
345
|
+
- `renderPane: (paneId: string) => ReactNode` — Callback to render the contents of a pane given its ID.
|
|
346
|
+
- `renderDragOverlay?: (activeId: string) => ReactNode` — (Optional) Renders a custom cursor-following drag preview.
|
|
347
|
+
- `classNames?: ZeugmaClassNames` — (Optional) CSS class overrides for styling various layout elements.
|
|
348
|
+
- `fullscreenPaneId?: string | null` — (Optional) ID of the pane currently in fullscreen mode.
|
|
349
|
+
- `onFullscreenChange?: (paneId: string | null) => void` — (Optional) Callback triggered when a pane enters/leaves fullscreen.
|
|
350
|
+
- `onRemove?: (paneId: string) => void` — (Optional) Callback triggered when a pane is closed/removed.
|
|
351
|
+
- `dragActivationDistance?: number` — (Optional) Minimum pointer drag distance (in pixels) required to activate dragging. Defaults to `8`.
|
|
352
|
+
- `onDragStart?: (activeId: string) => void` — (Optional) Callback triggered when dragging starts on a pane.
|
|
353
|
+
- `onDragEnd?: (activeId: string, overId: string | null, dropAction: any) => void` — (Optional) Callback triggered when dragging ends. The `overId` will be `'root'` if the pane was dropped onto the outer dashboard boundaries to split the root layout.
|
|
354
|
+
- `onResizeStart?: (currentNode: SplitNode) => void` — (Optional) Callback triggered when resizing starts.
|
|
355
|
+
- `onResize?: (currentNode: SplitNode, percentage: number) => void` — (Optional) Callback triggered during resizing.
|
|
356
|
+
- `onResizeEnd?: (currentNode: SplitNode, percentage: number) => void` — (Optional) Callback triggered when resizing ends.
|
|
357
|
+
- `renderResizer?: (props: ResizerRenderProps) => ReactNode` — (Optional) Custom resizer bar component renderer.
|
|
358
|
+
- `minSplitPercentage?: number` — (Optional) Minimum resizing limit percentage (defaults to `5`).
|
|
359
|
+
- `maxSplitPercentage?: number` — (Optional) Maximum resizing limit percentage (defaults to `95`).
|
|
360
|
+
|
|
361
|
+
### `<PaneTree>`
|
|
362
|
+
|
|
363
|
+
Recursively renders the split nodes and pane nodes. Must be placed inside `<DashboardProvider>`.
|
|
364
|
+
|
|
365
|
+
#### Props
|
|
366
|
+
|
|
367
|
+
- `tree?: TreeNode | null` — (Optional) Custom subtree to render. Defaults to the provider's root `layout`.
|
|
368
|
+
- `resizerSize?: number` — (Optional) Thickness of the split resizer bars in pixels. Defaults to `4`.
|
|
369
|
+
|
|
370
|
+
### `<Pane>`
|
|
275
371
|
|
|
276
|
-
|
|
372
|
+
Wraps the contents of an individual pane. It sets up draggable and droppable zones.
|
|
373
|
+
|
|
374
|
+
#### Props
|
|
375
|
+
|
|
376
|
+
- `id: string` — The unique ID corresponding to a `PaneNode`'s `paneId`.
|
|
377
|
+
- `children: (props: PaneRenderProps) => ReactNode` — Render prop function.
|
|
378
|
+
|
|
379
|
+
#### `PaneRenderProps`
|
|
380
|
+
|
|
381
|
+
```ts
|
|
382
|
+
interface PaneRenderProps {
|
|
383
|
+
isDragging: boolean
|
|
384
|
+
isFullscreen: boolean
|
|
385
|
+
toggleFullscreen: () => void
|
|
386
|
+
remove: () => void
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### `<DragHandle>`
|
|
391
|
+
|
|
392
|
+
Defines the interactive drag region inside a `<Pane>`. **Must be placed inside a `<Pane>` component.**
|
|
393
|
+
|
|
394
|
+
#### Props
|
|
395
|
+
|
|
396
|
+
- `children: React.ReactNode` — Element(s) that function as the drag handle (e.g., pane header).
|
|
397
|
+
- `className?: string`
|
|
398
|
+
- `style?: React.CSSProperties`
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
## 3. Programmatic State Utilities
|
|
403
|
+
|
|
404
|
+
Import these helpers from `react-zeugma` to manipulate the tree layout programmatically in your state handlers:
|
|
405
|
+
|
|
406
|
+
- **`removePane(tree: TreeNode | null, idToRemove: string): TreeNode | null`**
|
|
407
|
+
Removes a pane from the tree and collapses the leftover sibling split node.
|
|
408
|
+
- **`splitPane(tree: TreeNode | null, targetId: string, direction: SplitDirection, splitType: 'left' | 'right' | 'top' | 'bottom', paneToAdd: string): TreeNode | null`**
|
|
409
|
+
Splits a specific target pane by nesting it under a new `SplitNode` along with a new pane.
|
|
410
|
+
- **`splitRoot(tree: TreeNode | null, draggingId: string, splitType: 'left' | 'right' | 'top' | 'bottom'): TreeNode | null`**
|
|
411
|
+
Splits the entire dashboard tree at the root, placing the dragged pane on one half and the remaining layout tree on the other.
|
|
412
|
+
- **`swapPanes(tree: TreeNode | null, idA: string, idB: string): TreeNode | null`**
|
|
413
|
+
Swaps the positions of two panes in the tree.
|
|
414
|
+
|
|
415
|
+
Alternatively, you can consume the convenient mutation helpers directly from the **`useDashboard()`** context hook inside pane components without importing utilities:
|
|
416
|
+
|
|
417
|
+
- **`removePane(paneId: string) => void`**
|
|
418
|
+
- **`addPane(paneId: string) => void`**
|
|
419
|
+
- **`swapPanes(paneIdA: string, paneIdB: string) => void`**
|
|
420
|
+
- **`splitPane(targetId: string, direction: SplitDirection, splitType: string, paneToAdd: string) => void`**
|
|
421
|
+
- **`updateSplitPercentage(currentNode: SplitNode, percentage: number) => void`**
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
|
|
425
|
+
## 4. Basic Integration Recipe
|
|
426
|
+
|
|
427
|
+
```tsx
|
|
428
|
+
import { useState } from 'react'
|
|
429
|
+
import { DashboardProvider, PaneTree, Pane, DragHandle, TreeNode } from 'react-zeugma'
|
|
430
|
+
|
|
431
|
+
const initialLayout: TreeNode = {
|
|
432
|
+
type: 'split',
|
|
433
|
+
direction: 'row',
|
|
434
|
+
splitPercentage: 50,
|
|
435
|
+
first: { type: 'pane', paneId: 'sidebar' },
|
|
436
|
+
second: { type: 'pane', paneId: 'main' },
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function CustomPane({ id }: { id: string }) {
|
|
440
|
+
return (
|
|
441
|
+
<Pane id={id}>
|
|
442
|
+
{({ isDragging, isFullscreen, toggleFullscreen, remove }) => (
|
|
443
|
+
<div style={{ height: '100%', border: '1px solid #ccc', opacity: isDragging ? 0.5 : 1 }}>
|
|
444
|
+
<div style={{ display: 'flex', background: '#eee', padding: 8 }}>
|
|
445
|
+
<DragHandle style={{ flex: 1 }}>
|
|
446
|
+
<strong>Header: {id}</strong>
|
|
447
|
+
</DragHandle>
|
|
448
|
+
<button onClick={toggleFullscreen}>
|
|
449
|
+
{isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'}
|
|
450
|
+
</button>
|
|
451
|
+
<button onClick={remove}>Close</button>
|
|
452
|
+
</div>
|
|
453
|
+
<div style={{ padding: 16 }}>Content for {id}</div>
|
|
454
|
+
</div>
|
|
455
|
+
)}
|
|
456
|
+
</Pane>
|
|
457
|
+
)
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export default function App() {
|
|
461
|
+
const [layout, setLayout] = useState<TreeNode | null>(initialLayout)
|
|
462
|
+
const [fullscreenId, setFullscreenId] = useState<string | null>(null)
|
|
463
|
+
|
|
464
|
+
const handleRemove = (paneId: string) => {
|
|
465
|
+
// Remove the pane and update layout
|
|
466
|
+
setLayout((prev) => removePane(prev, paneId))
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return (
|
|
470
|
+
<DashboardProvider
|
|
471
|
+
layout={layout}
|
|
472
|
+
onChange={setLayout}
|
|
473
|
+
renderPane={(id) => <CustomPane id={id} />}
|
|
474
|
+
fullscreenPaneId={fullscreenId}
|
|
475
|
+
onFullscreenChange={setFullscreenId}
|
|
476
|
+
onRemove={handleRemove}
|
|
477
|
+
>
|
|
478
|
+
<div style={{ width: '100vw', height: '100vh' }}>
|
|
479
|
+
<PaneTree />
|
|
480
|
+
</div>
|
|
481
|
+
</DashboardProvider>
|
|
482
|
+
)
|
|
483
|
+
}
|
|
484
|
+
```
|
|
277
485
|
|
|
278
486
|
---
|
|
279
487
|
|
|
280
|
-
##
|
|
488
|
+
## 5. Styling Customization
|
|
281
489
|
|
|
282
|
-
|
|
490
|
+
`react-zeugma` is style-agnostic and relies on class name configuration for visual states. Define classes in your styling framework and pass them via the `classNames` prop on `<DashboardProvider>`:
|
|
491
|
+
|
|
492
|
+
```ts
|
|
493
|
+
interface ZeugmaClassNames {
|
|
494
|
+
pane?: string // Applied to the outer wrapper of <Pane>
|
|
495
|
+
dropPreview?: string // Applied to the preview box when hovering over edge dropzones
|
|
496
|
+
swapPreview?: string // Applied to the preview box when hovering over center dropzone
|
|
497
|
+
dragOverlay?: string // Applied to the cursor-following drag preview portal
|
|
498
|
+
resizer?: string // Applied to the drag-to-resize split bar
|
|
499
|
+
}
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
### CSS Example:
|
|
503
|
+
|
|
504
|
+
```css
|
|
505
|
+
/* Custom resizer style */
|
|
506
|
+
.my-resizer {
|
|
507
|
+
background-color: #e2e8f0;
|
|
508
|
+
transition: background-color 0.2s;
|
|
509
|
+
}
|
|
510
|
+
.my-resizer:hover {
|
|
511
|
+
background-color: #3b82f6;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/* Edge drop previews */
|
|
515
|
+
.my-drop-preview {
|
|
516
|
+
background-color: rgba(59, 130, 246, 0.2);
|
|
517
|
+
border: 2px dashed #3b82f6;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/* Center swap preview */
|
|
521
|
+
.my-swap-preview {
|
|
522
|
+
background-color: rgba(16, 185, 129, 0.25);
|
|
523
|
+
border: 2px solid #10b981;
|
|
524
|
+
}
|
|
525
|
+
```
|
|
526
|
+
````
|
|
283
527
|
|
|
284
528
|
---
|
|
285
529
|
|
|
@@ -290,3 +534,11 @@ _Zeugma_ is an ancient city of Commagene, located in modern-day **Gaziantep, Tur
|
|
|
290
534
|
During modern excavation efforts, archeologists discovered some of the most breathtaking Greco-Roman mosaic panels in history, now housed inside the **Zeugma Mosaic Museum** in Gaziantep. The famous _"Gypsy Girl" (Çingene Kızı)_ mosaic, with her hauntingly detailed eyes, has become a global icon of the city.
|
|
291
535
|
|
|
292
536
|
> _"We chose the name Zeugma because of this ancient craftsmanship. Mosaics are assembled from hundreds of tiny, individual tesserae tiles to form a magnificent, cohesive picture. In the same spirit, react-zeugma lets you build beautiful, customized application workspaces from simple, individual components. Many tiles, one masterpiece."_
|
|
537
|
+
|
|
538
|
+
---
|
|
539
|
+
|
|
540
|
+
## Links
|
|
541
|
+
|
|
542
|
+
- [GitHub Repository](https://github.com/yusufarsln98/react-zeugma)
|
|
543
|
+
- [npm Package](https://www.npmjs.com/package/react-zeugma)
|
|
544
|
+
- [Contributing Guide](https://github.com/yusufarsln98/react-zeugma/blob/master/CONTRIBUTING.md)
|
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
'use strict';var react=require('react'),core=require('@dnd-kit/core'),jsxRuntime=require('react/jsx-runtime');var G=react.createContext(void 0);var y=()=>{let e=react.useContext(G);if(!e)throw new Error("useDashboard must be used within a DashboardProvider");return e};function
|
|
1
|
+
'use strict';var react=require('react'),core=require('@dnd-kit/core'),jsxRuntime=require('react/jsx-runtime');var G=react.createContext(void 0);var y=()=>{let e=react.useContext(G);if(!e)throw new Error("useDashboard must be used within a DashboardProvider");return e};function E(e,t){if(e===null)return null;if(e.type==="pane")return e.paneId===t?null:e;let o=E(e.first,t),n=E(e.second,t);return o===null?n:n===null?o:{...e,first:o,second:n}}function O(e,t,o,n,r){if(e===null)return {type:"pane",paneId:r};if(e.type==="pane"){if(e.paneId===t){let s={type:"pane",paneId:r},a={type:"pane",paneId:t},d=n==="left"||n==="top";return {type:"split",direction:o,first:d?s:a,second:d?a:s,splitPercentage:50}}return e}return {...e,first:O(e.first,t,o,n,r)||e.first,second:O(e.second,t,o,n,r)||e.second}}function _(e,t,o){return e===null?null:e.type==="pane"?e.paneId===t?{...e,paneId:o}:e.paneId===o?{...e,paneId:t}:e:{...e,first:_(e.first,t,o)||e.first,second:_(e.second,t,o)||e.second}}function ne(e,t){if(e===null)return {type:"pane",paneId:t};function o(n,r){return n.type==="pane"?{type:"split",direction:r==="row"?"column":"row",splitPercentage:50,first:n,second:{type:"pane",paneId:t}}:{...n,second:o(n.second,n.direction)}}return o(e,null)}function F(e,t,o){return e===null?null:e===t?{...e,splitPercentage:o}:e.type==="split"?{...e,first:F(e.first,t,o)||e.first,second:F(e.second,t,o)||e.second}:e}function re(e,t,o){let n=E(e,t);if(n===null)return {type:"pane",paneId:t};let r=o==="left"||o==="right"?"row":"column",s=o==="left"||o==="top",a={type:"pane",paneId:t};return {type:"split",direction:r,first:s?a:n,second:s?n:a,splitPercentage:50}}var me=8,fe=8,nt=4;var De=({activeId:e,render:t,className:o})=>{let n=react.useRef(null);return react.useEffect(()=>{let r=s=>{n.current&&(n.current.style.transform=`translate(${s.clientX+12}px, ${s.clientY+12}px)`);};return document.addEventListener("pointermove",r),()=>document.removeEventListener("pointermove",r)},[]),jsxRuntime.jsx("div",{ref:n,className:o,style:{position:"fixed",top:0,left:0,zIndex:9999,pointerEvents:"none"},children:t(e)})},ze=({layout:e,onChange:t,renderPane:o,renderDragOverlay:n,classNames:r={},fullscreenPaneId:s=null,onFullscreenChange:a,onRemove:d,dragActivationDistance:R=8,snapThreshold:h=8,onDragStart:v,onDragEnd:i,onResizeStart:p,onResize:b,onResizeEnd:D,renderResizer:m,minSplitPercentage:f=5,maxSplitPercentage:x=95,children:z})=>{let[g,k]=react.useState(null),J=core.useSensors(core.useSensor(core.PointerSensor,{activationConstraint:{distance:R}})),X=l=>{let c=l.active.id.toString();k(c),v&&v(c);},A=l=>{k(null);let{active:c,over:N}=l,u=c.id.toString();if(!N){i&&i(u,null,null);return}let S=N.id.toString(),$=S.match(/^drop-root-(left|right|top|bottom)$/);if($){let[,w]=$,pe=re(e,u,w);t(pe),i&&i(u,"root",{type:"split",direction:w==="left"||w==="right"?"row":"column",position:w});return}let L=S.match(/^drop-center-(.+)$/);if(L){let[,w]=L;u!==w&&t(_(e,u,w)),i&&i(u,w,{type:"swap",position:"center"});return}let W=S.match(/^drop-(left|right|top|bottom)-(.+)$/);if(!W){i&&i(u,null,null);return}let[,C,T]=W;if(u===T){i&&i(u,null,null);return}let oe=C==="left"||C==="right"?"row":"column",le=E(e,u),de=O(le,T,oe,C,u);t(de),i&&i(u,T,{type:"split",direction:oe,position:C});},K=react.useCallback(l=>{let c=E(e,l);t(c);},[e,t]),Y=react.useCallback(l=>{let c=ne(e,l);t(c);},[e,t]),Z=react.useCallback((l,c)=>{let N=_(e,l,c);t(N);},[e,t]),M=react.useCallback((l,c,N,u)=>{let S=O(e,l,c,N,u);t(S);},[e,t]),H=react.useCallback((l,c)=>{let N=F(e,l,c);t(N);},[e,t]),P=react.useMemo(()=>({layout:e,onLayoutChange:t,renderPane:o,activeId:g,fullscreenPaneId:s,classNames:r,onRemove:d,onFullscreenChange:a,snapThreshold:h,onResizeStart:p,onResize:b,onResizeEnd:D,renderResizer:m,minSplitPercentage:f,maxSplitPercentage:x,removePane:K,addPane:Y,swapPanes:Z,splitPane:M,updateSplitPercentage:H}),[e,t,o,g,s,r,d,a,h,p,b,D,m,f,x,K,Y,Z,M,H]);return jsxRuntime.jsxs(G.Provider,{value:P,children:[jsxRuntime.jsx(core.DndContext,{id:"zeugma-dnd-context",sensors:J,collisionDetection:core.pointerWithin,onDragStart:X,onDragEnd:A,children:z}),g&&n&&jsxRuntime.jsx(De,{activeId:g,render:n,className:r.dragOverlay})]})};var ye={top:{position:"absolute",top:0,left:0,right:0,height:"32px",zIndex:30,pointerEvents:"auto"},bottom:{position:"absolute",bottom:0,left:0,right:0,height:"32px",zIndex:30,pointerEvents:"auto"},left:{position:"absolute",top:0,bottom:0,left:0,width:"32px",zIndex:30,pointerEvents:"auto"},right:{position:"absolute",top:0,bottom:0,right:0,width:"32px",zIndex:30,pointerEvents:"auto"}},Ee={top:{position:"absolute",top:0,left:0,right:0,height:"50%",zIndex:31,pointerEvents:"none",boxSizing:"border-box"},bottom:{position:"absolute",bottom:0,left:0,right:0,height:"50%",zIndex:31,pointerEvents:"none",boxSizing:"border-box"},left:{position:"absolute",top:0,bottom:0,left:0,width:"50%",zIndex:31,pointerEvents:"none",boxSizing:"border-box"},right:{position:"absolute",top:0,bottom:0,right:0,width:"50%",zIndex:31,pointerEvents:"none",boxSizing:"border-box"}},Ce=({id:e,position:t,activeClassName:o})=>{let{setNodeRef:n,isOver:r}=core.useDroppable({id:e});return jsxRuntime.jsxs(jsxRuntime.Fragment,{children:[jsxRuntime.jsx("div",{ref:n,style:ye[t]}),r&&jsxRuntime.jsx("div",{className:o,style:Ee[t]})]})},ie=({activeId:e,hasOtherPanes:t,dropPreviewClassName:o})=>!e||!t?null:jsxRuntime.jsx("div",{style:{position:"absolute",top:0,left:0,right:0,bottom:0,zIndex:30,pointerEvents:"none"},children:["top","bottom","left","right"].map(n=>jsxRuntime.jsx(Ce,{id:`drop-root-${n}`,position:n,activeClassName:o},n))});function j({containerRef:e,isRow:t,direction:o,splitPercentage:n,resizerSize:r,snapThreshold:s,layout:a,currentNode:d,onLayoutChange:R,onResizeStart:h,onResizeEnd:v}){let{onResizeStart:i,onResize:p,onResizeEnd:b,minSplitPercentage:D=5,maxSplitPercentage:m=95}=y();return react.useCallback(f=>{f.preventDefault();let x=e.current;if(!x)return;document.body.classList.add("zeugma-resizing");let z=document.createElement("style");z.id="zeugma-global-cursor-style",z.textContent=`
|
|
2
2
|
* {
|
|
3
3
|
cursor: ${t?"col-resize":"row-resize"} !important;
|
|
4
4
|
user-select: none !important;
|
|
5
5
|
}
|
|
6
|
-
`,document.head.appendChild(
|
|
6
|
+
`,document.head.appendChild(z),h&&h(),i&&i(d);let g=x.getBoundingClientRect(),k=f.clientX,J=f.clientY,X=n,A=f.currentTarget;A.setAttribute("data-resizing","true");let Y=Array.from(document.querySelectorAll('div[role="separator"][data-direction]')).filter(P=>P!==A&&P.getAttribute("data-direction")===o).map(P=>{let l=P.getBoundingClientRect();return t?l.left+l.width/2:l.top+l.height/2}),Z=X,M=P=>{let l=t?(P.clientX-k)/g.width*100:(P.clientY-J)/g.height*100,c=X+l,N=t?g.left+(g.width-r)*(c/100)+r/2:g.top+(g.height-r)*(c/100)+r/2,u=1/0,S=null;for(let C of Y){let T=Math.abs(N-C);T<s&&T<u&&(u=T,S=C);}let $=c;S!==null&&($=t?(S-r/2-g.left)/(g.width-r)*100:(S-r/2-g.top)/(g.height-r)*100);let L=Math.max(D,Math.min(m,$));Z=L;let W=F(a,d,L);R(W),p&&p(d,L);},H=()=>{document.body.classList.remove("zeugma-resizing"),A.removeAttribute("data-resizing");let P=document.getElementById("zeugma-global-cursor-style");P&&P.remove(),document.removeEventListener("pointermove",M),document.removeEventListener("pointerup",H),v&&v(),b&&b(d,Z);};document.addEventListener("pointermove",M),document.addEventListener("pointerup",H);},[e,t,o,n,r,s,a,d,R,h,v,i,p,b,D,m])}var He=({currentNode:e,resizerSize:t,snapThreshold:o,renderResizer:n})=>{let{layout:r,onLayoutChange:s,classNames:a,renderResizer:d}=y(),[R,h]=react.useState(false),v=n||d,i=react.useRef(null),{direction:p,first:b,second:D,splitPercentage:m}=e,f=p==="row",x=j({containerRef:i,isRow:f,direction:p,splitPercentage:m,resizerSize:t,snapThreshold:o??8,layout:r,currentNode:e,onLayoutChange:s,onResizeStart:()=>h(true),onResizeEnd:()=>h(false)});return jsxRuntime.jsxs("div",{ref:i,style:{display:"flex",flexDirection:f?"row":"column",width:"100%",height:"100%",overflow:"hidden"},children:[jsxRuntime.jsx("div",{style:{flex:`${m} 1 0%`,overflow:"hidden"},children:jsxRuntime.jsx(ee,{tree:b,resizerSize:t,snapThreshold:o,renderResizer:n})}),v?v({direction:p,splitPercentage:m,resizerSize:t,isResizing:R,onPointerDown:x}):jsxRuntime.jsx("div",{className:a.resizer,"data-direction":p,style:{width:f?`${t}px`:"100%",height:f?"100%":`${t}px`,cursor:f?"col-resize":"row-resize",position:"relative",zIndex:10,userSelect:"none",boxSizing:"border-box",flexShrink:0},onPointerDown:x,role:"separator","aria-valuenow":m,"aria-valuemin":5,"aria-valuemax":95}),jsxRuntime.jsx("div",{style:{flex:`${100-m} 1 0%`,overflow:"hidden"},children:jsxRuntime.jsx(ee,{tree:D,resizerSize:t,snapThreshold:o,renderResizer:n})})]})},ee=({tree:e,resizerSize:t=4,snapThreshold:o,renderResizer:n})=>{let{layout:r,renderPane:s,activeId:a,classNames:d,fullscreenPaneId:R,snapThreshold:h}=y(),v=o!==void 0?o:h,i=react.useMemo(()=>a?E(r,a)!==null:false,[r,a]);if(R&&!e)return jsxRuntime.jsx("div",{style:{width:"100%",height:"100%",position:"relative"},children:s(R)});let p=e!==void 0?e:r;if(!p)return null;let b=()=>p.type==="pane"?jsxRuntime.jsx("div",{style:{width:"100%",height:"100%",position:"relative"},children:s(p.paneId)}):jsxRuntime.jsx(He,{currentNode:p,resizerSize:t,snapThreshold:v,renderResizer:n});return e===void 0?jsxRuntime.jsxs("div",{className:"zeugma-dashboard-root",style:{position:"relative",width:"100%",height:"100%",overflow:"hidden"},children:[b(),jsxRuntime.jsx(ie,{activeId:a,hasOtherPanes:i,dropPreviewClassName:d.dropPreview})]}):b()};var B=react.createContext(null);var Ve={top:{position:"absolute",top:0,left:"25%",width:"50%",height:"25%",zIndex:20,pointerEvents:"auto"},bottom:{position:"absolute",bottom:0,left:"25%",width:"50%",height:"25%",zIndex:20,pointerEvents:"auto"},left:{position:"absolute",top:0,bottom:0,left:0,width:"25%",height:"100%",zIndex:20,pointerEvents:"auto"},right:{position:"absolute",top:0,bottom:0,right:0,width:"25%",height:"100%",zIndex:20,pointerEvents:"auto"},center:{position:"absolute",top:"25%",left:"25%",width:"50%",height:"50%",zIndex:20,pointerEvents:"auto"}},ke={top:{position:"absolute",top:0,left:0,right:0,height:"50%",zIndex:21,pointerEvents:"none",boxSizing:"border-box"},bottom:{position:"absolute",bottom:0,left:0,right:0,height:"50%",zIndex:21,pointerEvents:"none",boxSizing:"border-box"},left:{position:"absolute",top:0,bottom:0,left:0,width:"50%",zIndex:21,pointerEvents:"none",boxSizing:"border-box"},right:{position:"absolute",top:0,bottom:0,right:0,width:"50%",zIndex:21,pointerEvents:"none",boxSizing:"border-box"},center:{position:"absolute",top:0,left:0,right:0,bottom:0,zIndex:21,pointerEvents:"none",boxSizing:"border-box"}},ae=({id:e,position:t,activeClassName:o})=>{let{setNodeRef:n,isOver:r}=core.useDroppable({id:e});return jsxRuntime.jsxs(jsxRuntime.Fragment,{children:[jsxRuntime.jsx("div",{ref:n,style:Ve[t]}),r&&jsxRuntime.jsx("div",{className:o,style:ke[t]})]})},Xe=({id:e,children:t,style:o})=>{let{activeId:n,classNames:r,fullscreenPaneId:s,onRemove:a,onFullscreenChange:d,removePane:R}=y(),h=n!==null&&n!==e,{attributes:v,listeners:i,setNodeRef:p,isDragging:b}=core.useDraggable({id:e}),D=n===e||b,m=s===e,f={isDragging:D,isFullscreen:m,toggleFullscreen:()=>d?.(m?null:e),remove:()=>{m&&d?.(null),a?a(e):R(e);}},x=react.useMemo(()=>({...i,...v}),[i,v]);return jsxRuntime.jsx(B.Provider,{value:x,children:jsxRuntime.jsxs("div",{ref:p,className:r.pane,style:{position:"relative",width:"100%",height:"100%",...o},children:[t(f),h&&jsxRuntime.jsxs("div",{style:{position:"absolute",top:0,left:0,right:0,bottom:0,zIndex:15,pointerEvents:"none"},children:[["top","bottom","left","right"].map(z=>jsxRuntime.jsx(ae,{id:`drop-${z}-${e}`,position:z,activeClassName:r.dropPreview},z)),jsxRuntime.jsx(ae,{id:`drop-center-${e}`,position:"center",activeClassName:r.swapPreview})]})]})})};var Ge=({children:e,className:t,style:o})=>{let n=react.useContext(B);if(!n)throw new Error("<DragHandle> must be used inside a <Pane>");return jsxRuntime.jsx("div",{className:t,style:{cursor:"grab",userSelect:"none",...o},...n,children:e})};exports.DEFAULT_DRAG_ACTIVATION_DISTANCE=fe;exports.DEFAULT_RESIZER_SIZE=nt;exports.DEFAULT_SNAP_THRESHOLD=me;exports.DashboardProvider=ze;exports.DragHandle=Ge;exports.Pane=Xe;exports.PaneTree=ee;exports.addPane=ne;exports.removePane=E;exports.splitPane=O;exports.splitRoot=re;exports.swapPanes=_;exports.updateSplitPercentage=F;exports.useDashboard=y;exports.useResizer=j;//# sourceMappingURL=index.cjs.map
|
|
7
7
|
//# sourceMappingURL=index.cjs.map
|