@dannysir/floating-components 0.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/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
6
+
7
+ ## [0.1.0] - 2026-04-14
8
+
9
+ ### Added
10
+
11
+ - `TreeLayout` component and `useLayoutTree` hook
12
+ - Path-based border resize (`resizeBorder`)
13
+ - `splitPanel` / `removePanel` / `movePanel` API
14
+ - Drag-and-drop panel reordering (HTML5 Drag & Drop API, depth-aware drop target)
15
+ - `resizerClassName` prop for custom resizer styling
16
+ - ESM + CJS dual-format bundle with TypeScript declarations
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 dannysir
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any purpose
6
+ with or without fee is hereby granted, provided that the above copyright notice
7
+ and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
11
+ FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
13
+ OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
14
+ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
15
+ THIS SOFTWARE.
package/README.ko.md ADDED
@@ -0,0 +1,235 @@
1
+ # react-tree-layout
2
+
3
+ [English README](./README.md)
4
+
5
+ Tree 기반으로 크기 조절과 패널 이동이 가능한 React 레이아웃 라이브러리입니다. VS Code나 IDE처럼 패널을 수평/수직으로 분할하고, 경계선 드래그로 크기를 조절하고, 드래그 앤 드롭으로 패널을 이동할 수 있습니다.
6
+
7
+ <img src="doc/assets/layout-overview.png" alt="기본 레이아웃" width="640" />
8
+
9
+ ---
10
+
11
+ ## 특징
12
+
13
+ - **N-ary 트리 구조** — SplitNode가 2개 이상의 자식을 가질 수 있어 불필요한 중첩 없이 flat한 트리 유지
14
+ - **경계선 드래그 리사이즈** — 패널 사이 경계선을 드래그해서 크기 조절 (requestAnimationFrame 최적화)
15
+ - **드래그 앤 드롭 패널 이동** — HTML5 Drag & Drop API로 패널을 다른 위치로 이동
16
+ - **다단계 드롭 타겟 감지** — 패널 가장자리, 부모 split 가장자리, 루트 가장자리를 구분하여 depth 기반 배치
17
+ - **불변 상태 관리** — 모든 트리 업데이트가 immutable하게 처리
18
+ - **View / State 분리** — `TreeLayout` (렌더링)과 `useLayoutTree` (상태 관리)를 독립적으로 사용 가능
19
+ - **TypeScript 지원** — 모든 타입 선언 포함
20
+ - **ESM + CJS** — 듀얼 포맷 번들 출력
21
+
22
+ ---
23
+
24
+ ## 설치
25
+
26
+ ```bash
27
+ npm install react-tree-layout
28
+ ```
29
+
30
+ > **Peer dependencies**: `react >= 17`, `react-dom >= 17`
31
+
32
+ ---
33
+
34
+ ## 빠른 시작
35
+
36
+ ```tsx
37
+ import { TreeLayout, useLayoutTree, type LayoutNode } from "react-tree-layout";
38
+
39
+ const initialTree: LayoutNode = {
40
+ type: "split",
41
+ direction: "horizontal",
42
+ size: 1,
43
+ children: [
44
+ {
45
+ type: "panel",
46
+ id: "panel-a",
47
+ size: 1,
48
+ component: <div style={{ padding: 16, background: "#dbeafe", height: "100%" }}>Panel A</div>,
49
+ },
50
+ {
51
+ type: "split",
52
+ direction: "vertical",
53
+ size: 1,
54
+ children: [
55
+ {
56
+ type: "panel",
57
+ id: "panel-b",
58
+ size: 1,
59
+ component: <div style={{ padding: 16, background: "#dcfce7", height: "100%" }}>Panel B</div>,
60
+ },
61
+ {
62
+ type: "panel",
63
+ id: "panel-c",
64
+ size: 1,
65
+ component: <div style={{ padding: 16, background: "#ffedd5", height: "100%" }}>Panel C</div>,
66
+ },
67
+ ],
68
+ },
69
+ ],
70
+ };
71
+
72
+ const App = () => {
73
+ const { tree, resizeBorder, movePanel } = useLayoutTree(initialTree);
74
+
75
+ return (
76
+ <div style={{ width: "100vw", height: "100vh" }}>
77
+ <TreeLayout tree={tree} onResizeBorder={resizeBorder} onMovePanel={movePanel} />
78
+ </div>
79
+ );
80
+ };
81
+ ```
82
+
83
+ ---
84
+
85
+ ## 설계 개요
86
+
87
+ 레이아웃 상태를 **N-ary 트리**로 표현합니다.
88
+
89
+ - **Leaf 노드 (`PanelNode`)** — 실제 콘텐츠가 렌더링되는 패널. `id`와 `component`를 가짐
90
+ - **Branch 노드 (`SplitNode`)** — 자식 노드들을 수평 또는 수직으로 분할하는 컨테이너. `id` 없음
91
+
92
+ ```
93
+ root (SplitNode, horizontal)
94
+ ├── panel-a (PanelNode, size: 1)
95
+ ├── panel-b (PanelNode, size: 1)
96
+ └── (SplitNode, vertical)
97
+ ├── panel-c (PanelNode, size: 1)
98
+ └── panel-d (PanelNode, size: 1)
99
+ ```
100
+
101
+ ### 아키텍처
102
+
103
+ ```
104
+ src/
105
+ ├── types.ts # LayoutNode, PanelNode, SplitNode 타입
106
+ ├── hooks/
107
+ │ └── useLayoutTree.ts # 트리 상태 관리 훅
108
+ ├── renderers/
109
+ │ ├── TreeLayout.tsx # 루트 레이아웃 컴포넌트
110
+ │ ├── LayoutNodeRenderer.tsx # 재귀 노드 렌더러 + 드래그 앤 드롭
111
+ │ └── Resizer.tsx # 경계선 리사이즈 핸들
112
+ └── index.ts # public API export
113
+ ```
114
+
115
+ ---
116
+
117
+ ## API
118
+
119
+ ### `<TreeLayout />`
120
+
121
+ 레이아웃 트리를 재귀적으로 렌더링하는 컴포넌트입니다. flexbox 기반으로 패널을 배치합니다.
122
+
123
+ | Prop | Type | 필수 | 설명 |
124
+ |------|------|:----:|------|
125
+ | `tree` | `LayoutNode` | O | 렌더링할 트리 루트 노드 |
126
+ | `onResizeBorder` | `(path, borderIndex, delta) => void` | | 경계선 리사이즈 콜백 |
127
+ | `onMovePanel` | `(sourceId, anchorId, position, depth) => void` | | 드래그 앤 드롭 이동 콜백 |
128
+ | `className` | `string` | | 최상위 div의 className |
129
+ | `style` | `CSSProperties` | | 최상위 div의 인라인 스타일 |
130
+ | `resizerClassName` | `string` | | 경계선 div에 적용할 className |
131
+
132
+ ### `useLayoutTree(initialTree)`
133
+
134
+ 레이아웃 트리 상태를 관리하는 훅입니다.
135
+
136
+ ```ts
137
+ const { tree, setTree, resizeBorder, splitPanel, removePanel, movePanel } = useLayoutTree(initialTree);
138
+ ```
139
+
140
+ | 반환값 | 타입 | 설명 |
141
+ |--------|------|------|
142
+ | `tree` | `LayoutNode` | 현재 레이아웃 트리 상태 |
143
+ | `setTree` | `(tree: LayoutNode) => void` | 트리 직접 설정 |
144
+ | `resizeBorder` | `(path, borderIndex, delta) => void` | 경계선 기반 리사이즈 |
145
+ | `splitPanel` | `(panelId, direction) => void` | 패널 분할 |
146
+ | `removePanel` | `(panelId) => void` | 패널 제거 |
147
+ | `movePanel` | `(sourceId, anchorId, position, depth?) => void` | 패널 이동 |
148
+
149
+ ---
150
+
151
+ ## 타입
152
+
153
+ ```ts
154
+ type SplitDirection = "horizontal" | "vertical";
155
+
156
+ interface PanelNode {
157
+ type: "panel";
158
+ id: string;
159
+ size: number; // flex 비율
160
+ component: ReactNode; // 렌더링할 콘텐츠
161
+ }
162
+
163
+ interface SplitNode {
164
+ type: "split";
165
+ direction: SplitDirection;
166
+ size: number; // flex 비율
167
+ children: LayoutNode[]; // 2개 이상의 자식
168
+ }
169
+
170
+ type LayoutNode = PanelNode | SplitNode;
171
+
172
+ type DropPosition = "top" | "bottom" | "left" | "right";
173
+ ```
174
+
175
+ ---
176
+
177
+ ## 커스터마이징
178
+
179
+ ### 리사이저 스타일
180
+
181
+ `resizerClassName`으로 경계선 스타일을 CSS로 완전히 제어할 수 있습니다.
182
+
183
+ ```tsx
184
+ // styles.css
185
+ // .my-resizer { background: #6366f1; width: 2px; }
186
+ // .my-resizer:hover { background: #4f46e5; }
187
+
188
+ <TreeLayout
189
+ tree={tree}
190
+ onResizeBorder={resizeBorder}
191
+ resizerClassName="my-resizer"
192
+ />
193
+ ```
194
+
195
+ 기본 인라인 스타일(너비 4px, `#e0e0e0` 배경)은 베이스로 유지되며, `className`으로 CSS 우선순위 또는 원하는 스타일링 방식을 통해 덮어쓸 수 있습니다.
196
+
197
+ <img src="doc/assets/resize-demo.png" alt="경계선 리사이즈" width="640" />
198
+
199
+ ### 드래그 앤 드롭
200
+
201
+ <img src="doc/assets/drag-drop-demo.png" alt="드래그 앤 드롭" width="640" />
202
+
203
+ `movePanel(sourceId, anchorId, position, depth)` — 패널을 다른 위치로 이동합니다.
204
+
205
+ - `position`: 앵커 패널 기준 드롭 위치 (`top` / `bottom` / `left` / `right`)
206
+ - `depth`: 드롭 깊이 (0 = 패널 레벨, 1 = 부모 split 레벨, ...)
207
+
208
+ **드롭 타겟 감지 우선순위:**
209
+ 1. 루트 가장자리 (외곽 5%) — 최상위 레벨에 배치
210
+ 2. 부모 split 가장자리 (외곽 15%) — 상위 split에 배치
211
+ 3. 패널 중앙 — 패널 레벨에서 분할
212
+
213
+ ---
214
+
215
+ ## 빌드
216
+
217
+ ```bash
218
+ npm run build # dist/ 생성 (ESM + CJS + .d.ts)
219
+ npm run dev # Vite 개발 서버
220
+ npm run type-check # 타입 검사
221
+ ```
222
+
223
+ ### 출력물
224
+
225
+ | 파일 | 용도 |
226
+ |------|------|
227
+ | `dist/index.js` | ESM 번들 |
228
+ | `dist/index.cjs` | CommonJS 번들 |
229
+ | `dist/index.d.ts` | TypeScript 타입 선언 |
230
+
231
+ ---
232
+
233
+ ## 라이선스
234
+
235
+ ISC
package/README.md ADDED
@@ -0,0 +1,185 @@
1
+ # react-tree-layout
2
+
3
+ [한국어 README](./README.ko.md)
4
+
5
+ Tree-based resizable and reorderable panel layout for React. Split panels horizontally or vertically, resize borders by dragging, and reorder panels via drag and drop — just like VS Code or any modern IDE.
6
+
7
+ <img src="doc/assets/layout-overview.png" alt="Layout overview" width="640" />
8
+
9
+ ---
10
+
11
+ ## Features
12
+
13
+ - **N-ary tree structure** — `SplitNode` can hold two or more children, keeping the tree flat without unnecessary nesting
14
+ - **Border drag resize** — drag panel borders to resize (requestAnimationFrame optimized)
15
+ - **Drag-and-drop panel move** — reorder panels via HTML5 Drag & Drop API
16
+ - **Multi-level drop target** — distinguishes panel edge, parent split edge, and root edge for depth-aware placement
17
+ - **Immutable state** — all tree updates produce new objects via spread
18
+ - **View / State separation** — `TreeLayout` (rendering) and `useLayoutTree` (state) can be used independently
19
+ - **TypeScript** — full type declarations included
20
+ - **ESM + CJS** — dual-format bundle output
21
+
22
+ ---
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ npm install react-tree-layout
28
+ ```
29
+
30
+ > **Peer dependencies**: `react >= 17`, `react-dom >= 17`
31
+
32
+ ---
33
+
34
+ ## Quick Start
35
+
36
+ ```tsx
37
+ import { TreeLayout, useLayoutTree, type LayoutNode } from "react-tree-layout";
38
+
39
+ const initialTree: LayoutNode = {
40
+ type: "split",
41
+ direction: "horizontal",
42
+ size: 1,
43
+ children: [
44
+ {
45
+ type: "panel",
46
+ id: "panel-a",
47
+ size: 1,
48
+ component: <div style={{ padding: 16, background: "#dbeafe", height: "100%" }}>Panel A</div>,
49
+ },
50
+ {
51
+ type: "split",
52
+ direction: "vertical",
53
+ size: 1,
54
+ children: [
55
+ {
56
+ type: "panel",
57
+ id: "panel-b",
58
+ size: 1,
59
+ component: <div style={{ padding: 16, background: "#dcfce7", height: "100%" }}>Panel B</div>,
60
+ },
61
+ {
62
+ type: "panel",
63
+ id: "panel-c",
64
+ size: 1,
65
+ component: <div style={{ padding: 16, background: "#ffedd5", height: "100%" }}>Panel C</div>,
66
+ },
67
+ ],
68
+ },
69
+ ],
70
+ };
71
+
72
+ const App = () => {
73
+ const { tree, resizeBorder, movePanel } = useLayoutTree(initialTree);
74
+
75
+ return (
76
+ <div style={{ width: "100vw", height: "100vh" }}>
77
+ <TreeLayout tree={tree} onResizeBorder={resizeBorder} onMovePanel={movePanel} />
78
+ </div>
79
+ );
80
+ };
81
+ ```
82
+
83
+ ---
84
+
85
+ ## API
86
+
87
+ ### `<TreeLayout />`
88
+
89
+ Recursively renders the layout tree using flexbox.
90
+
91
+ | Prop | Type | Required | Description |
92
+ |------|------|:--------:|-------------|
93
+ | `tree` | `LayoutNode` | Yes | Root node of the layout tree |
94
+ | `onResizeBorder` | `(path, borderIndex, delta) => void` | | Border resize callback |
95
+ | `onMovePanel` | `(sourceId, anchorId, position, depth) => void` | | Drag-and-drop move callback |
96
+ | `className` | `string` | | className for the root div |
97
+ | `style` | `CSSProperties` | | Inline style for the root div |
98
+ | `resizerClassName` | `string` | | className applied to every resizer border div |
99
+
100
+ ### `useLayoutTree(initialTree)`
101
+
102
+ Hook for managing layout tree state.
103
+
104
+ ```ts
105
+ const { tree, setTree, resizeBorder, splitPanel, removePanel, movePanel } = useLayoutTree(initialTree);
106
+ ```
107
+
108
+ | Return | Type | Description |
109
+ |--------|------|-------------|
110
+ | `tree` | `LayoutNode` | Current layout tree state |
111
+ | `setTree` | `(tree: LayoutNode) => void` | Directly set the tree |
112
+ | `resizeBorder` | `(path, borderIndex, delta) => void` | Resize by border index |
113
+ | `splitPanel` | `(panelId, direction) => void` | Split a panel |
114
+ | `removePanel` | `(panelId) => void` | Remove a panel |
115
+ | `movePanel` | `(sourceId, anchorId, position, depth?) => void` | Move a panel |
116
+
117
+ ---
118
+
119
+ ## Types
120
+
121
+ ```ts
122
+ type SplitDirection = "horizontal" | "vertical";
123
+
124
+ interface PanelNode {
125
+ type: "panel";
126
+ id: string;
127
+ size: number; // flex ratio
128
+ component: ReactNode; // content to render
129
+ }
130
+
131
+ interface SplitNode {
132
+ type: "split";
133
+ direction: SplitDirection;
134
+ size: number; // flex ratio
135
+ children: LayoutNode[]; // two or more children
136
+ }
137
+
138
+ type LayoutNode = PanelNode | SplitNode;
139
+
140
+ type DropPosition = "top" | "bottom" | "left" | "right";
141
+ ```
142
+
143
+ ---
144
+
145
+ ## Customization
146
+
147
+ ### Resizer style
148
+
149
+ Use `resizerClassName` to fully control the border appearance via CSS:
150
+
151
+ ```tsx
152
+ // styles.css
153
+ // .my-resizer { background: #6366f1; width: 2px; }
154
+ // .my-resizer:hover { background: #4f46e5; }
155
+
156
+ <TreeLayout
157
+ tree={tree}
158
+ onResizeBorder={resizeBorder}
159
+ resizerClassName="my-resizer"
160
+ />
161
+ ```
162
+
163
+ The default inline styles (4 px wide, `#e0e0e0` background) are still applied as a base; `className` lets you override them via CSS specificity or your preferred styling solution.
164
+
165
+ <img src="doc/assets/resize-demo.png" alt="Border resize" width="640" />
166
+
167
+ ### Drag-and-drop
168
+
169
+ <img src="doc/assets/drag-drop-demo.png" alt="Drag and drop" width="640" />
170
+
171
+ `movePanel(sourceId, anchorId, position, depth)` — moves a panel to another location.
172
+
173
+ - `position`: drop side relative to the anchor panel (`top` / `bottom` / `left` / `right`)
174
+ - `depth`: 0 = panel level, 1 = parent split level, higher = ancestor split level
175
+
176
+ **Drop target priority:**
177
+ 1. Root edge (outer 5%) — places at the top level
178
+ 2. Parent split edge (outer 15%) — places at the enclosing split level
179
+ 3. Panel center — splits at the panel level
180
+
181
+ ---
182
+
183
+ ## License
184
+
185
+ ISC
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const M=require("react/jsx-runtime"),E=require("react"),w=t=>{process.env.NODE_ENV!=="production"&&console.warn(`[react-tree-layout] ${t}`)};let q=0;const F=()=>typeof crypto<"u"&&typeof crypto.randomUUID=="function"?crypto.randomUUID():`panel-${++q}-${Math.random().toString(36).slice(2,9)}`,G=(t,u)=>{let i=t;for(const s of u){if(i.type!=="split"||s>=i.children.length)return null;i=i.children[s]}return i},j=(t,u,i)=>{if(u.length===0)return i(t);if(t.type!=="split")return t;const[s,...o]=u;return{...t,children:t.children.map((n,l)=>l===s?j(n,o,i):n)}},L=(t,u)=>{const i=[],s=n=>{if(n.type==="panel")return n.id===u?n:null;for(let l=0;l<n.children.length;l++){i.push({split:n,childIndex:l});const e=s(n.children[l]);if(e)return e;i.pop()}return null},o=s(t);return o?{panel:o,ancestors:[...i]}:null},k=(t,u,i,s=null,o=0)=>{if(t.type==="panel"&&t.id===u)return i(t,s,o);if(t.type==="split"){const n=t.children.map((l,e)=>k(l,u,i,t,e)).filter(l=>l!==null);return n.length===0?null:n.length===1?n[0]:{...t,children:n}}return t},O="__move_ghost__",X=(t,u,i,s,o)=>{const n=s==="left"||s==="right"?"horizontal":"vertical",l=s==="left"||s==="top",e={...u,size:1},f=L(t,i);if(!f)return t;const{ancestors:d}=f,p=Math.min(o,d.length),h=a=>{if(a.type==="split"&&a.direction===n){const y=l?[e,...a.children]:[...a.children,e];return{...a,children:y}}const g=l?[e,{...a,size:1}]:[{...a,size:1},e];return{type:"split",direction:n,size:1,children:g}};if(p>=d.length)return h(t);if(p===0){const a=(g,y=null)=>{if(g.type==="panel"&&g.id===i){if(y&&y.direction===n)return g;const x=l?[e,{...g,size:1}]:[{...g,size:1},e];return{type:"split",direction:n,size:g.size,children:x}}if(g.type==="split"){const x=g.children.findIndex(c=>c.type==="panel"&&c.id===i);if(x!==-1&&g.direction===n){const c=[...g.children],r=l?x:x+1;return c.splice(r,0,e),{...g,children:c}}return{...g,children:g.children.map(c=>a(c,g))}}return g};return a(t)}const z=d.length-p,m=z>0?d[z-1]:null;if(!m)return h(t);const T=m.childIndex,P=d.slice(0,z-1).map(a=>a.childIndex);return j(t,P,a=>{if(a.type!=="split")return a;if(a.direction===n){const x=[...a.children],c=l?T:T+1;return x.splice(c,0,e),{...a,children:x}}const g=a.children[T],y=l?[e,{...g,size:1}]:[{...g,size:1},e];return{...a,children:a.children.map((x,c)=>c===T?{type:"split",direction:n,size:g.size,children:y}:x)}})},v=(t,u,i,s,o)=>{if(u===i)return null;let n=null;const l=h=>{h.type==="panel"&&h.id===u?n=h:h.type==="split"&&h.children.forEach(l)};if(l(t),!n)return null;const f=X(t,{type:"panel",id:O,size:1,component:null},i,s,o),d=k(f,u,()=>null);return d?k(d,O,()=>({...n,size:1}))??null:null},Y=t=>{const[u,i]=E.useState(t),s=E.useCallback((e,f,d,p)=>{i(h=>{const z=G(h,e);if(!z||z.type!=="split"||f<0||f>=z.children.length-1)return h;const m=z.children[f],T=z.children[f+1],P=m.size+T.size,a=D=>p&&p>0?D/p*P:0,g=m.minSize!==void 0?a(m.minSize):.05,y=m.maxSize!==void 0?a(m.maxSize):P-.05,x=T.minSize!==void 0?a(T.minSize):.05,c=T.maxSize!==void 0?a(T.maxSize):P-.05,r=Math.max(g,P-c),R=Math.min(y,P-x),I=Math.max(r,Math.min(R,m.size+d)),b=P-I;return j(h,e,D=>D.type!=="split"?D:{...D,children:D.children.map((S,A)=>A===f?{...S,size:I}:A===f+1?{...S,size:b}:S)})})},[]),o=E.useCallback((e,f,d)=>{var h;const p=((h=d==null?void 0:d.newPanel)==null?void 0:h.id)??F();return i(z=>{var a,g;if(!L(z,e))return w(`splitPanel: panel "${e}" not found`),z;const m={type:"panel",id:p,size:((a=d==null?void 0:d.newPanel)==null?void 0:a.size)??.5,component:((g=d==null?void 0:d.newPanel)==null?void 0:g.component)??null},T=k(z,e,(y,x)=>x&&x.direction===f?y:{type:"split",direction:f,size:y.size,children:[{...y,size:1},{...m,size:1}]}),P=y=>{if(y.type==="split"){const x=y.children.findIndex(c=>c.type==="panel"&&c.id===e);if(x!==-1&&y.direction===f){const c=[...y.children];return c.splice(x+1,0,m),{...y,children:c}}return{...y,children:y.children.map(P)}}return y};return T===z?z:T?P(T):z}),p},[]),n=E.useCallback(e=>{i(f=>L(f,e)?k(f,e,()=>null)??f:(w(`removePanel: panel "${e}" not found`),f))},[]),l=E.useCallback((e,f,d,p=0)=>{i(h=>L(h,e)?L(h,f)?v(h,e,f,d,p)??h:(w(`movePanel: anchor panel "${f}" not found`),h):(w(`movePanel: source panel "${e}" not found`),h))},[]);return{tree:u,setTree:i,resizeBorder:s,splitPanel:o,removePanel:n,movePanel:l}},B=({direction:t,onResize:u,className:i,style:s})=>{const o=t==="horizontal",n=E.useRef(0),l=E.useRef(0),e=E.useRef(0),f=E.useCallback(p=>{p.preventDefault(),n.current=o?p.clientX:p.clientY,e.current=0;const h=m=>{const T=o?m.clientX:m.clientY,P=T-n.current;P!==0&&(n.current=T,e.current+=P,l.current||(l.current=requestAnimationFrame(()=>{u(e.current),e.current=0,l.current=0})))},z=()=>{l.current&&(cancelAnimationFrame(l.current),e.current!==0&&u(e.current),l.current=0,e.current=0),document.removeEventListener("mousemove",h),document.removeEventListener("mouseup",z)};document.addEventListener("mousemove",h),document.addEventListener("mouseup",z)},[o,u]),d=i?{}:{width:o?4:"100%",height:o?"100%":4,background:"#e0e0e0",cursor:o?"col-resize":"row-resize"};return M.jsx("div",{onMouseDown:f,className:i,style:{flexShrink:0,...d,...s}})},H=.05,V=.15,_=(t,u,i)=>{const s=i.getBoundingClientRect(),o={left:(t-s.left)/s.width,right:(s.right-t)/s.width,top:(u-s.top)/s.height,bottom:(s.bottom-u)/s.height},n=Object.keys(o).reduce((l,e)=>o[l]<o[e]?l:e);return{position:n,dist:o[n]}},$=(t,u,i)=>{const s=[];let o=null,n=i.parentElement;for(;n;){if(n.hasAttribute("data-tree-root")){o=n;break}n.hasAttribute("data-layout-split")&&s.push(n),n=n.parentElement}if(o){const{position:e,dist:f}=_(t,u,o);if(f<H)return{position:e,depth:s.length+1}}for(let e=s.length-1;e>=0;e--){const{position:f,dist:d}=_(t,u,s[e]);if(d<V)return{position:f,depth:e+1}}const{position:l}=_(t,u,i);return{position:l,depth:0}},C=({node:t,path:u=[],onResizeBorder:i,onMovePanel:s,onDropPreviewChange:o,shadowPanelId:n,isPreviewActive:l,resizerClassName:e,resizerStyle:f,dragHandleSelector:d,shadowClassName:p,shadowStyle:h,panelClassName:z,splitClassName:m})=>{const T=E.useRef(null),P=E.useRef(0),a=E.useRef(null),g=E.useCallback((c,r)=>{if(!i||!T.current)return;const R=T.current.getBoundingClientRect(),I=t.type==="split"?t.direction==="horizontal"?R.width:R.height:0;if(I===0||t.type!=="split")return;const b=t.children.reduce((S,A)=>S+A.size,0),D=r/I*b;i(u,c,D,I)},[i,u,t]),y=E.useCallback(c=>{if(!d)return;const R=!!c.target.closest(d);a.current&&(a.current.draggable=R)},[d]);if(t.type==="panel"){const c=t.id===n;return M.jsx("div",{ref:a,draggable:!d,onMouseDown:y,className:c?p??z:z,onDragStart:r=>{r.dataTransfer.setData("text/panel-id",t.id),r.dataTransfer.effectAllowed="move";const R=r.currentTarget.closest("[data-tree-root]");R&&(R.dataset.draggingPanelId=t.id)},onDragOver:r=>{r.preventDefault(),r.dataTransfer.dropEffect="move";const R=r.clientX,I=r.clientY,b=r.currentTarget,D=b.closest("[data-tree-root]"),S=D==null?void 0:D.dataset.draggingPanelId;!S||S===t.id||P.current||(P.current=requestAnimationFrame(()=>{P.current=0;const{position:A,depth:W}=$(R,I,b);o==null||o({sourcePanelId:S,anchorPanelId:t.id,position:A,depth:W})}))},onDrop:r=>{if(r.preventDefault(),l)return;r.stopPropagation(),o==null||o(null);const R=r.dataTransfer.getData("text/panel-id");if(!R||R===t.id||!s)return;const{position:I,depth:b}=$(r.clientX,r.clientY,r.currentTarget);s(R,t.id,I,b)},style:{flex:t.size,minWidth:0,minHeight:0,overflow:"hidden",...c?p?h:{opacity:.5,outline:"2px dashed rgba(59, 130, 246, 0.6)",outlineOffset:-2,...h}:void 0},children:t.component})}const x=[];return t.children.forEach((c,r)=>{x.push(M.jsx(C,{node:c,path:[...u,r],onResizeBorder:i,onMovePanel:s,onDropPreviewChange:o,shadowPanelId:n,isPreviewActive:l,resizerClassName:e,resizerStyle:f,dragHandleSelector:d,shadowClassName:p,shadowStyle:h,panelClassName:z,splitClassName:m},c.type==="panel"?c.id:`split-${r}`)),r<t.children.length-1&&x.push(M.jsx(B,{direction:t.direction,onResize:R=>g(r,R),className:e,style:f},`resizer-${r}`))}),M.jsx("div",{ref:T,"data-layout-split":!0,className:m,style:{display:"flex",flexDirection:t.direction==="horizontal"?"row":"column",flex:t.size,minWidth:0,minHeight:0},children:x})},U=(t,u=[])=>(t.type==="panel"?u.push(t.id):t.children.forEach(i=>U(i,u)),u),J=({tree:t,onResizeBorder:u,onMovePanel:i,className:s,style:o,resizerClassName:n,resizerStyle:l,dragHandleSelector:e,shadowClassName:f,shadowStyle:d,classNames:p})=>{const h=E.useRef(null),z=E.useId();E.useEffect(()=>{const c=U(t),r=new Set;c.forEach(R=>{r.has(R)&&w(`Duplicate panel id detected: "${R}". Panel ids must be unique.`),r.add(R)})},[t]);const[m,T]=E.useState(null),P=E.useRef(null),a=E.useRef(null),g=E.useCallback(c=>{const r=P.current;!c&&!r||c&&r&&r.sourcePanelId===c.sourcePanelId&&r.anchorPanelId===c.anchorPanelId&&r.position===c.position&&r.depth===c.depth||(P.current=c,a.current=c,T(c))},[]),y=E.useCallback(()=>{P.current=null,a.current=null,T(null),h.current&&delete h.current.dataset.draggingPanelId},[z]),x=E.useMemo(()=>m?v(t,m.sourcePanelId,m.anchorPanelId,m.position,m.depth):null,[t,m]);return M.jsx("div",{ref:h,"data-tree-root":z,className:s,style:{display:"flex",width:"100%",height:"100%",...o},onDrop:c=>{c.preventDefault();const r=a.current;r&&i&&i(r.sourcePanelId,r.anchorPanelId,r.position,r.depth),y()},onDragEnd:()=>y(),onDragLeave:c=>{const r=h.current;if(!r)return;const R=c.relatedTarget;(!R||!r.contains(R))&&y()},children:M.jsx(C,{node:x??t,onResizeBorder:u,onMovePanel:i,onDropPreviewChange:g,shadowPanelId:m==null?void 0:m.sourcePanelId,isPreviewActive:!!x,resizerClassName:(p==null?void 0:p.resizer)??n,resizerStyle:l,dragHandleSelector:e,shadowClassName:f,shadowStyle:d,panelClassName:p==null?void 0:p.panel,splitClassName:p==null?void 0:p.split})})};exports.TreeLayout=J;exports.useLayoutTree=Y;
@@ -0,0 +1,64 @@
1
+ import { CSSProperties } from 'react';
2
+ import { Dispatch } from 'react';
3
+ import { JSX } from 'react/jsx-runtime';
4
+ import { ReactNode } from 'react';
5
+ import { SetStateAction } from 'react';
6
+
7
+ export declare type DropPosition = "top" | "bottom" | "left" | "right";
8
+
9
+ export declare interface LayoutClassNames {
10
+ panel?: string;
11
+ split?: string;
12
+ resizer?: string;
13
+ }
14
+
15
+ export declare type LayoutNode = PanelNode | SplitNode;
16
+
17
+ export declare interface PanelNode {
18
+ type: "panel";
19
+ id: string;
20
+ size: number;
21
+ component: ReactNode;
22
+ minSize?: number;
23
+ maxSize?: number;
24
+ }
25
+
26
+ export declare type SplitDirection = "horizontal" | "vertical";
27
+
28
+ export declare interface SplitNode {
29
+ type: "split";
30
+ direction: SplitDirection;
31
+ size: number;
32
+ children: LayoutNode[];
33
+ minSize?: number;
34
+ maxSize?: number;
35
+ }
36
+
37
+ export declare const TreeLayout: ({ tree, onResizeBorder, onMovePanel, className, style, resizerClassName, resizerStyle, dragHandleSelector, shadowClassName, shadowStyle, classNames }: TreeLayoutProps) => JSX.Element;
38
+
39
+ declare interface TreeLayoutProps {
40
+ tree: LayoutNode;
41
+ onResizeBorder?: (path: number[], borderIndex: number, delta: number, totalPixels?: number) => void;
42
+ onMovePanel?: (sourcePanelId: string, anchorPanelId: string, position: DropPosition, depth: number) => void;
43
+ className?: string;
44
+ style?: CSSProperties;
45
+ resizerClassName?: string;
46
+ resizerStyle?: CSSProperties;
47
+ dragHandleSelector?: string;
48
+ shadowClassName?: string;
49
+ shadowStyle?: CSSProperties;
50
+ classNames?: LayoutClassNames;
51
+ }
52
+
53
+ export declare const useLayoutTree: (initialTree: LayoutNode) => {
54
+ tree: LayoutNode;
55
+ setTree: Dispatch<SetStateAction<LayoutNode>>;
56
+ resizeBorder: (path: number[], borderIndex: number, delta: number, totalPixels?: number) => void;
57
+ splitPanel: (panelId: string, direction: SplitDirection, options?: {
58
+ newPanel?: Partial<Omit<PanelNode, "type">>;
59
+ }) => string;
60
+ removePanel: (panelId: string) => void;
61
+ movePanel: (sourcePanelId: string, anchorPanelId: string, position: DropPosition, depth?: number) => void;
62
+ };
63
+
64
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,425 @@
1
+ import { jsx as R } from "react/jsx-runtime";
2
+ import { useState as k, useCallback as M, useRef as A, useId as X, useEffect as Y, useMemo as B } from "react";
3
+ const _ = (t) => {
4
+ process.env.NODE_ENV !== "production" && console.warn(`[react-tree-layout] ${t}`);
5
+ };
6
+ let H = 0;
7
+ const V = () => typeof crypto < "u" && typeof crypto.randomUUID == "function" ? crypto.randomUUID() : `panel-${++H}-${Math.random().toString(36).slice(2, 9)}`, C = (t, u) => {
8
+ let i = t;
9
+ for (const l of u) {
10
+ if (i.type !== "split" || l >= i.children.length) return null;
11
+ i = i.children[l];
12
+ }
13
+ return i;
14
+ }, b = (t, u, i) => {
15
+ if (u.length === 0) return i(t);
16
+ if (t.type !== "split") return t;
17
+ const [l, ...o] = u;
18
+ return {
19
+ ...t,
20
+ children: t.children.map(
21
+ (n, s) => s === l ? b(n, o, i) : n
22
+ )
23
+ };
24
+ }, $ = (t, u) => {
25
+ const i = [], l = (n) => {
26
+ if (n.type === "panel") return n.id === u ? n : null;
27
+ for (let s = 0; s < n.children.length; s++) {
28
+ i.push({ split: n, childIndex: s });
29
+ const e = l(n.children[s]);
30
+ if (e) return e;
31
+ i.pop();
32
+ }
33
+ return null;
34
+ }, o = l(t);
35
+ return o ? { panel: o, ancestors: [...i] } : null;
36
+ }, O = (t, u, i, l = null, o = 0) => {
37
+ if (t.type === "panel" && t.id === u)
38
+ return i(t, l, o);
39
+ if (t.type === "split") {
40
+ const n = t.children.map((s, e) => O(s, u, i, t, e)).filter((s) => s !== null);
41
+ return n.length === 0 ? null : n.length === 1 ? n[0] : { ...t, children: n };
42
+ }
43
+ return t;
44
+ }, U = "__move_ghost__", J = (t, u, i, l, o) => {
45
+ const n = l === "left" || l === "right" ? "horizontal" : "vertical", s = l === "left" || l === "top", e = { ...u, size: 1 }, f = $(t, i);
46
+ if (!f) return t;
47
+ const { ancestors: d } = f, p = Math.min(o, d.length), h = (a) => {
48
+ if (a.type === "split" && a.direction === n) {
49
+ const z = s ? [e, ...a.children] : [...a.children, e];
50
+ return { ...a, children: z };
51
+ }
52
+ const g = s ? [e, { ...a, size: 1 }] : [{ ...a, size: 1 }, e];
53
+ return { type: "split", direction: n, size: 1, children: g };
54
+ };
55
+ if (p >= d.length)
56
+ return h(t);
57
+ if (p === 0) {
58
+ const a = (g, z = null) => {
59
+ if (g.type === "panel" && g.id === i) {
60
+ if (z && z.direction === n) return g;
61
+ const x = s ? [e, { ...g, size: 1 }] : [{ ...g, size: 1 }, e];
62
+ return { type: "split", direction: n, size: g.size, children: x };
63
+ }
64
+ if (g.type === "split") {
65
+ const x = g.children.findIndex(
66
+ (c) => c.type === "panel" && c.id === i
67
+ );
68
+ if (x !== -1 && g.direction === n) {
69
+ const c = [...g.children], r = s ? x : x + 1;
70
+ return c.splice(r, 0, e), { ...g, children: c };
71
+ }
72
+ return {
73
+ ...g,
74
+ children: g.children.map((c) => a(c, g))
75
+ };
76
+ }
77
+ return g;
78
+ };
79
+ return a(t);
80
+ }
81
+ const y = d.length - p, m = y > 0 ? d[y - 1] : null;
82
+ if (!m)
83
+ return h(t);
84
+ const E = m.childIndex, I = d.slice(0, y - 1).map((a) => a.childIndex);
85
+ return b(t, I, (a) => {
86
+ if (a.type !== "split") return a;
87
+ if (a.direction === n) {
88
+ const x = [...a.children], c = s ? E : E + 1;
89
+ return x.splice(c, 0, e), { ...a, children: x };
90
+ }
91
+ const g = a.children[E], z = s ? [e, { ...g, size: 1 }] : [{ ...g, size: 1 }, e];
92
+ return {
93
+ ...a,
94
+ children: a.children.map(
95
+ (x, c) => c === E ? { type: "split", direction: n, size: g.size, children: z } : x
96
+ )
97
+ };
98
+ });
99
+ }, F = (t, u, i, l, o) => {
100
+ if (u === i) return null;
101
+ let n = null;
102
+ const s = (h) => {
103
+ h.type === "panel" && h.id === u ? n = h : h.type === "split" && h.children.forEach(s);
104
+ };
105
+ if (s(t), !n) return null;
106
+ const f = J(t, { type: "panel", id: U, size: 1, component: null }, i, l, o), d = O(f, u, () => null);
107
+ return d ? O(d, U, () => ({ ...n, size: 1 })) ?? null : null;
108
+ }, et = (t) => {
109
+ const [u, i] = k(t), l = M(
110
+ (e, f, d, p) => {
111
+ i((h) => {
112
+ const y = C(h, e);
113
+ if (!y || y.type !== "split" || f < 0 || f >= y.children.length - 1) return h;
114
+ const m = y.children[f], E = y.children[f + 1], I = m.size + E.size, a = (D) => p && p > 0 ? D / p * I : 0, g = m.minSize !== void 0 ? a(m.minSize) : 0.05, z = m.maxSize !== void 0 ? a(m.maxSize) : I - 0.05, x = E.minSize !== void 0 ? a(E.minSize) : 0.05, c = E.maxSize !== void 0 ? a(E.maxSize) : I - 0.05, r = Math.max(g, I - c), T = Math.min(z, I - x), P = Math.max(r, Math.min(T, m.size + d)), w = I - P;
115
+ return b(h, e, (D) => D.type !== "split" ? D : {
116
+ ...D,
117
+ children: D.children.map((S, L) => L === f ? { ...S, size: P } : L === f + 1 ? { ...S, size: w } : S)
118
+ });
119
+ });
120
+ },
121
+ []
122
+ ), o = M(
123
+ (e, f, d) => {
124
+ var h;
125
+ const p = ((h = d == null ? void 0 : d.newPanel) == null ? void 0 : h.id) ?? V();
126
+ return i((y) => {
127
+ var a, g;
128
+ if (!$(y, e))
129
+ return _(`splitPanel: panel "${e}" not found`), y;
130
+ const m = {
131
+ type: "panel",
132
+ id: p,
133
+ size: ((a = d == null ? void 0 : d.newPanel) == null ? void 0 : a.size) ?? 0.5,
134
+ component: ((g = d == null ? void 0 : d.newPanel) == null ? void 0 : g.component) ?? null
135
+ }, E = O(y, e, (z, x) => x && x.direction === f ? z : {
136
+ type: "split",
137
+ direction: f,
138
+ size: z.size,
139
+ children: [{ ...z, size: 1 }, { ...m, size: 1 }]
140
+ }), I = (z) => {
141
+ if (z.type === "split") {
142
+ const x = z.children.findIndex(
143
+ (c) => c.type === "panel" && c.id === e
144
+ );
145
+ if (x !== -1 && z.direction === f) {
146
+ const c = [...z.children];
147
+ return c.splice(x + 1, 0, m), { ...z, children: c };
148
+ }
149
+ return { ...z, children: z.children.map(I) };
150
+ }
151
+ return z;
152
+ };
153
+ return E === y ? y : E ? I(E) : y;
154
+ }), p;
155
+ },
156
+ []
157
+ ), n = M((e) => {
158
+ i((f) => $(f, e) ? O(f, e, () => null) ?? f : (_(`removePanel: panel "${e}" not found`), f));
159
+ }, []), s = M(
160
+ (e, f, d, p = 0) => {
161
+ i((h) => $(h, e) ? $(h, f) ? F(h, e, f, d, p) ?? h : (_(`movePanel: anchor panel "${f}" not found`), h) : (_(`movePanel: source panel "${e}" not found`), h));
162
+ },
163
+ []
164
+ );
165
+ return { tree: u, setTree: i, resizeBorder: l, splitPanel: o, removePanel: n, movePanel: s };
166
+ }, K = ({ direction: t, onResize: u, className: i, style: l }) => {
167
+ const o = t === "horizontal", n = A(0), s = A(0), e = A(0), f = M(
168
+ (p) => {
169
+ p.preventDefault(), n.current = o ? p.clientX : p.clientY, e.current = 0;
170
+ const h = (m) => {
171
+ const E = o ? m.clientX : m.clientY, I = E - n.current;
172
+ I !== 0 && (n.current = E, e.current += I, s.current || (s.current = requestAnimationFrame(() => {
173
+ u(e.current), e.current = 0, s.current = 0;
174
+ })));
175
+ }, y = () => {
176
+ s.current && (cancelAnimationFrame(s.current), e.current !== 0 && u(e.current), s.current = 0, e.current = 0), document.removeEventListener("mousemove", h), document.removeEventListener("mouseup", y);
177
+ };
178
+ document.addEventListener("mousemove", h), document.addEventListener("mouseup", y);
179
+ },
180
+ [o, u]
181
+ );
182
+ return /* @__PURE__ */ R(
183
+ "div",
184
+ {
185
+ onMouseDown: f,
186
+ className: i,
187
+ style: {
188
+ flexShrink: 0,
189
+ ...i ? {} : {
190
+ width: o ? 4 : "100%",
191
+ height: o ? "100%" : 4,
192
+ background: "#e0e0e0",
193
+ cursor: o ? "col-resize" : "row-resize"
194
+ },
195
+ ...l
196
+ }
197
+ }
198
+ );
199
+ }, Q = 0.05, Z = 0.15, v = (t, u, i) => {
200
+ const l = i.getBoundingClientRect(), o = {
201
+ left: (t - l.left) / l.width,
202
+ right: (l.right - t) / l.width,
203
+ top: (u - l.top) / l.height,
204
+ bottom: (l.bottom - u) / l.height
205
+ }, n = Object.keys(o).reduce(
206
+ (s, e) => o[s] < o[e] ? s : e
207
+ );
208
+ return { position: n, dist: o[n] };
209
+ }, W = (t, u, i) => {
210
+ const l = [];
211
+ let o = null, n = i.parentElement;
212
+ for (; n; ) {
213
+ if (n.hasAttribute("data-tree-root")) {
214
+ o = n;
215
+ break;
216
+ }
217
+ n.hasAttribute("data-layout-split") && l.push(n), n = n.parentElement;
218
+ }
219
+ if (o) {
220
+ const { position: e, dist: f } = v(t, u, o);
221
+ if (f < Q)
222
+ return { position: e, depth: l.length + 1 };
223
+ }
224
+ for (let e = l.length - 1; e >= 0; e--) {
225
+ const { position: f, dist: d } = v(t, u, l[e]);
226
+ if (d < Z)
227
+ return { position: f, depth: e + 1 };
228
+ }
229
+ const { position: s } = v(t, u, i);
230
+ return { position: s, depth: 0 };
231
+ }, G = ({
232
+ node: t,
233
+ path: u = [],
234
+ onResizeBorder: i,
235
+ onMovePanel: l,
236
+ onDropPreviewChange: o,
237
+ shadowPanelId: n,
238
+ isPreviewActive: s,
239
+ resizerClassName: e,
240
+ resizerStyle: f,
241
+ dragHandleSelector: d,
242
+ shadowClassName: p,
243
+ shadowStyle: h,
244
+ panelClassName: y,
245
+ splitClassName: m
246
+ }) => {
247
+ const E = A(null), I = A(0), a = A(null), g = M(
248
+ (c, r) => {
249
+ if (!i || !E.current) return;
250
+ const T = E.current.getBoundingClientRect(), P = t.type === "split" ? t.direction === "horizontal" ? T.width : T.height : 0;
251
+ if (P === 0 || t.type !== "split") return;
252
+ const w = t.children.reduce((S, L) => S + L.size, 0), D = r / P * w;
253
+ i(u, c, D, P);
254
+ },
255
+ [i, u, t]
256
+ ), z = M(
257
+ (c) => {
258
+ if (!d) return;
259
+ const T = !!c.target.closest(d);
260
+ a.current && (a.current.draggable = T);
261
+ },
262
+ [d]
263
+ );
264
+ if (t.type === "panel") {
265
+ const c = t.id === n;
266
+ return /* @__PURE__ */ R(
267
+ "div",
268
+ {
269
+ ref: a,
270
+ draggable: !d,
271
+ onMouseDown: z,
272
+ className: c ? p ?? y : y,
273
+ onDragStart: (r) => {
274
+ r.dataTransfer.setData("text/panel-id", t.id), r.dataTransfer.effectAllowed = "move";
275
+ const T = r.currentTarget.closest("[data-tree-root]");
276
+ T && (T.dataset.draggingPanelId = t.id);
277
+ },
278
+ onDragOver: (r) => {
279
+ r.preventDefault(), r.dataTransfer.dropEffect = "move";
280
+ const T = r.clientX, P = r.clientY, w = r.currentTarget, D = w.closest("[data-tree-root]"), S = D == null ? void 0 : D.dataset.draggingPanelId;
281
+ !S || S === t.id || I.current || (I.current = requestAnimationFrame(() => {
282
+ I.current = 0;
283
+ const { position: L, depth: j } = W(T, P, w);
284
+ o == null || o({ sourcePanelId: S, anchorPanelId: t.id, position: L, depth: j });
285
+ }));
286
+ },
287
+ onDrop: (r) => {
288
+ if (r.preventDefault(), s) return;
289
+ r.stopPropagation(), o == null || o(null);
290
+ const T = r.dataTransfer.getData("text/panel-id");
291
+ if (!T || T === t.id || !l) return;
292
+ const { position: P, depth: w } = W(r.clientX, r.clientY, r.currentTarget);
293
+ l(T, t.id, P, w);
294
+ },
295
+ style: {
296
+ flex: t.size,
297
+ minWidth: 0,
298
+ minHeight: 0,
299
+ overflow: "hidden",
300
+ ...c ? p ? h : {
301
+ opacity: 0.5,
302
+ outline: "2px dashed rgba(59, 130, 246, 0.6)",
303
+ outlineOffset: -2,
304
+ ...h
305
+ } : void 0
306
+ },
307
+ children: t.component
308
+ }
309
+ );
310
+ }
311
+ const x = [];
312
+ return t.children.forEach((c, r) => {
313
+ x.push(
314
+ /* @__PURE__ */ R(
315
+ G,
316
+ {
317
+ node: c,
318
+ path: [...u, r],
319
+ onResizeBorder: i,
320
+ onMovePanel: l,
321
+ onDropPreviewChange: o,
322
+ shadowPanelId: n,
323
+ isPreviewActive: s,
324
+ resizerClassName: e,
325
+ resizerStyle: f,
326
+ dragHandleSelector: d,
327
+ shadowClassName: p,
328
+ shadowStyle: h,
329
+ panelClassName: y,
330
+ splitClassName: m
331
+ },
332
+ c.type === "panel" ? c.id : `split-${r}`
333
+ )
334
+ ), r < t.children.length - 1 && x.push(
335
+ /* @__PURE__ */ R(
336
+ K,
337
+ {
338
+ direction: t.direction,
339
+ onResize: (T) => g(r, T),
340
+ className: e,
341
+ style: f
342
+ },
343
+ `resizer-${r}`
344
+ )
345
+ );
346
+ }), /* @__PURE__ */ R(
347
+ "div",
348
+ {
349
+ ref: E,
350
+ "data-layout-split": !0,
351
+ className: m,
352
+ style: {
353
+ display: "flex",
354
+ flexDirection: t.direction === "horizontal" ? "row" : "column",
355
+ flex: t.size,
356
+ minWidth: 0,
357
+ minHeight: 0
358
+ },
359
+ children: x
360
+ }
361
+ );
362
+ }, q = (t, u = []) => (t.type === "panel" ? u.push(t.id) : t.children.forEach((i) => q(i, u)), u), nt = ({ tree: t, onResizeBorder: u, onMovePanel: i, className: l, style: o, resizerClassName: n, resizerStyle: s, dragHandleSelector: e, shadowClassName: f, shadowStyle: d, classNames: p }) => {
363
+ const h = A(null), y = X();
364
+ Y(() => {
365
+ const c = q(t), r = /* @__PURE__ */ new Set();
366
+ c.forEach((T) => {
367
+ r.has(T) && _(`Duplicate panel id detected: "${T}". Panel ids must be unique.`), r.add(T);
368
+ });
369
+ }, [t]);
370
+ const [m, E] = k(null), I = A(null), a = A(null), g = M((c) => {
371
+ const r = I.current;
372
+ !c && !r || c && r && r.sourcePanelId === c.sourcePanelId && r.anchorPanelId === c.anchorPanelId && r.position === c.position && r.depth === c.depth || (I.current = c, a.current = c, E(c));
373
+ }, []), z = M(() => {
374
+ I.current = null, a.current = null, E(null), h.current && delete h.current.dataset.draggingPanelId;
375
+ }, [y]), x = B(() => m ? F(
376
+ t,
377
+ m.sourcePanelId,
378
+ m.anchorPanelId,
379
+ m.position,
380
+ m.depth
381
+ ) : null, [t, m]);
382
+ return /* @__PURE__ */ R(
383
+ "div",
384
+ {
385
+ ref: h,
386
+ "data-tree-root": y,
387
+ className: l,
388
+ style: { display: "flex", width: "100%", height: "100%", ...o },
389
+ onDrop: (c) => {
390
+ c.preventDefault();
391
+ const r = a.current;
392
+ r && i && i(r.sourcePanelId, r.anchorPanelId, r.position, r.depth), z();
393
+ },
394
+ onDragEnd: () => z(),
395
+ onDragLeave: (c) => {
396
+ const r = h.current;
397
+ if (!r) return;
398
+ const T = c.relatedTarget;
399
+ (!T || !r.contains(T)) && z();
400
+ },
401
+ children: /* @__PURE__ */ R(
402
+ G,
403
+ {
404
+ node: x ?? t,
405
+ onResizeBorder: u,
406
+ onMovePanel: i,
407
+ onDropPreviewChange: g,
408
+ shadowPanelId: m == null ? void 0 : m.sourcePanelId,
409
+ isPreviewActive: !!x,
410
+ resizerClassName: (p == null ? void 0 : p.resizer) ?? n,
411
+ resizerStyle: s,
412
+ dragHandleSelector: e,
413
+ shadowClassName: f,
414
+ shadowStyle: d,
415
+ panelClassName: p == null ? void 0 : p.panel,
416
+ splitClassName: p == null ? void 0 : p.split
417
+ }
418
+ )
419
+ }
420
+ );
421
+ };
422
+ export {
423
+ nt as TreeLayout,
424
+ et as useLayoutTree
425
+ };
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@dannysir/floating-components",
3
+ "version": "0.1.0",
4
+ "description": "VS Code-style tree-based panel layout for React",
5
+ "license": "ISC",
6
+ "author": "dannysir",
7
+ "keywords": [
8
+ "react",
9
+ "layout",
10
+ "panel",
11
+ "split",
12
+ "resize",
13
+ "drag-drop",
14
+ "tree"
15
+ ],
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/dannysir/floating-component.git"
19
+ },
20
+ "bugs": {
21
+ "url": "https://github.com/dannysir/floating-component/issues"
22
+ },
23
+ "homepage": "https://github.com/dannysir/floating-component#readme",
24
+ "type": "module",
25
+ "main": "./dist/index.cjs",
26
+ "module": "./dist/index.js",
27
+ "types": "./dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.js",
32
+ "require": "./dist/index.cjs"
33
+ }
34
+ },
35
+ "sideEffects": false,
36
+ "files": [
37
+ "dist",
38
+ "LICENSE",
39
+ "README.md",
40
+ "README.ko.md",
41
+ "CHANGELOG.md"
42
+ ],
43
+ "scripts": {
44
+ "dev": "vite",
45
+ "build": "tsc && vite build",
46
+ "preview": "vite preview",
47
+ "type-check": "tsc --noEmit"
48
+ },
49
+ "engines": {
50
+ "node": ">=18"
51
+ },
52
+ "publishConfig": {
53
+ "access": "public"
54
+ },
55
+ "peerDependencies": {
56
+ "react": ">=18.0.0"
57
+ },
58
+ "devDependencies": {
59
+ "@types/node": "^25.5.2",
60
+ "@types/react": "^19.0.0",
61
+ "@types/react-dom": "^19.0.0",
62
+ "@vitejs/plugin-react": "^4.3.4",
63
+ "react": "^19.0.0",
64
+ "react-dom": "^19.0.0",
65
+ "typescript": "^5.7.2",
66
+ "vite": "^6.3.0",
67
+ "vite-plugin-dts": "^4.5.0"
68
+ }
69
+ }