@zag-js/splitter 0.9.2 → 0.10.1

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.
@@ -7,7 +7,7 @@ var dom = createScope({
7
7
  getLabelId: (ctx) => ctx.ids?.label ?? `splitter:${ctx.id}:label`,
8
8
  getPanelId: (ctx, id) => ctx.ids?.panel?.(id) ?? `splitter:${ctx.id}:panel:${id}`,
9
9
  globalCursorId: (ctx) => `splitter:${ctx.id}:global-cursor`,
10
- getRootEl: (ctx) => dom.getById(ctx, dom.getRootId(ctx)),
10
+ getRootEl: (ctx) => dom.queryById(ctx, dom.getRootId(ctx)),
11
11
  getResizeTriggerEl: (ctx, id) => dom.getById(ctx, dom.getResizeTriggerId(ctx, id)),
12
12
  getPanelEl: (ctx, id) => dom.getById(ctx, dom.getPanelId(ctx, id)),
13
13
  getCursor(ctx) {
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-HPRMFGOY.mjs";
4
4
  import {
5
5
  dom
6
- } from "./chunk-G4OIUPPJ.mjs";
6
+ } from "./chunk-3GDOPT5W.mjs";
7
7
  import {
8
8
  getHandleBounds
9
9
  } from "./chunk-MV44GBQY.mjs";
@@ -15,6 +15,7 @@ function connect(state, send, normalize) {
15
15
  const isHorizontal = state.context.isHorizontal;
16
16
  const isFocused = state.hasTag("focus");
17
17
  const isDragging = state.matches("dragging");
18
+ const panels = state.context.panels;
18
19
  const api = {
19
20
  /**
20
21
  * Whether the splitter is focused.
@@ -29,22 +30,18 @@ function connect(state, send, normalize) {
29
30
  */
30
31
  bounds: getHandleBounds(state.context),
31
32
  /**
32
- * Function to collapse a panel.
33
+ * Function to set a panel to its minimum size.
33
34
  */
34
- collapse(id) {
35
- send({ type: "COLLAPSE", id });
35
+ setToMinSize(id) {
36
+ const panel = panels.find((panel2) => panel2.id === id);
37
+ send({ type: "SET_SIZE", id, size: panel?.minSize, src: "collapse" });
36
38
  },
37
39
  /**
38
- * Function to expand a panel.
40
+ * Function to set a panel to its maximum size.
39
41
  */
40
- expand(id) {
41
- send({ type: "EXPAND", id });
42
- },
43
- /**
44
- * Function to toggle a panel between collapsed and expanded.
45
- */
46
- toggle(id) {
47
- send({ type: "TOGGLE", id });
42
+ setToMaxSize(id) {
43
+ const panel = panels.find((panel2) => panel2.id === id);
44
+ send({ type: "SET_SIZE", id, size: panel?.maxSize, src: "expand" });
48
45
  },
49
46
  /**
50
47
  * Function to set the size of a panel.
@@ -59,13 +56,13 @@ function connect(state, send, normalize) {
59
56
  const { id, disabled } = props;
60
57
  const ids = id.split(":");
61
58
  const panelIds = ids.map((id2) => dom.getPanelId(state.context, id2));
62
- const panels = getHandleBounds(state.context, id);
59
+ const panels2 = getHandleBounds(state.context, id);
63
60
  return {
64
61
  isDisabled: !!disabled,
65
62
  isFocused: state.context.activeResizeId === id && isFocused,
66
63
  panelIds,
67
- min: panels?.min,
68
- max: panels?.max,
64
+ min: panels2?.min,
65
+ max: panels2?.max,
69
66
  value: 0
70
67
  };
71
68
  },
@@ -92,14 +89,6 @@ function connect(state, send, normalize) {
92
89
  style: dom.getPanelStyle(state.context, id)
93
90
  });
94
91
  },
95
- // toggleTriggerProps: normalize.element({
96
- // ...parts.toggleButton.attrs,
97
- // id: dom.getToggleButtonId(state.context),
98
- // "aria-label": state.context.isAtMin ? "Expand Primary Pane" : "Collapse Primary Pane",
99
- // onClick() {
100
- // send("TOGGLE")
101
- // },
102
- // }),
103
92
  getResizeTriggerProps(props) {
104
93
  const { id, disabled, step = 1 } = props;
105
94
  const triggerState = api.getResizeTriggerState(props);
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  dom
3
- } from "./chunk-G4OIUPPJ.mjs";
3
+ } from "./chunk-3GDOPT5W.mjs";
4
4
  import {
5
5
  clamp,
6
6
  getHandleBounds,
@@ -11,7 +11,7 @@ import {
11
11
 
12
12
  // src/splitter.machine.ts
13
13
  import { createMachine } from "@zag-js/core";
14
- import { getRelativePointPercent, trackPointerMove } from "@zag-js/dom-event";
14
+ import { getRelativePoint, trackPointerMove } from "@zag-js/dom-event";
15
15
  import { raf } from "@zag-js/dom-query";
16
16
  import { compact } from "@zag-js/utils";
17
17
  function machine(userContext) {
@@ -55,7 +55,10 @@ function machine(userContext) {
55
55
  {
56
56
  actions: "setStartPanelToMin"
57
57
  }
58
- ]
58
+ ],
59
+ SET_SIZE: {
60
+ actions: "setPanelSize"
61
+ }
59
62
  },
60
63
  states: {
61
64
  idle: {
@@ -202,6 +205,13 @@ function machine(userContext) {
202
205
  size: panel.size
203
206
  }));
204
207
  },
208
+ setPanelSize(ctx2, evt) {
209
+ const { id, size } = evt;
210
+ ctx2.size = ctx2.size.map((panel) => {
211
+ const panelSize = clamp(size, panel.minSize ?? 0, panel.maxSize ?? 100);
212
+ return panel.id === id ? { ...panel, size: panelSize } : panel;
213
+ });
214
+ },
205
215
  setStartPanelToMin(ctx2) {
206
216
  const bounds = getPanelBounds(ctx2);
207
217
  if (!bounds)
@@ -268,10 +278,15 @@ function machine(userContext) {
268
278
  setPointerValue(ctx2, evt) {
269
279
  const panels = getHandlePanels(ctx2);
270
280
  const bounds = getHandleBounds(ctx2);
271
- const rootEl = dom.getRootEl(ctx2);
272
- if (!panels || !rootEl || !bounds)
281
+ if (!panels || !bounds)
273
282
  return;
274
- let pointValue = getRelativePointPercent(evt.point, rootEl).normalize(ctx2) * 100;
283
+ const rootEl = dom.getRootEl(ctx2);
284
+ const relativePoint = getRelativePoint(evt.point, rootEl);
285
+ const percentValue = relativePoint.getPercentValue({
286
+ dir: ctx2.dir,
287
+ orientation: ctx2.orientation
288
+ });
289
+ let pointValue = percentValue * 100;
275
290
  ctx2.activeResizeState = {
276
291
  isAtMin: pointValue < bounds.min,
277
292
  isAtMax: pointValue > bounds.max
package/dist/index.js CHANGED
@@ -44,7 +44,7 @@ var dom = (0, import_dom_query.createScope)({
44
44
  getLabelId: (ctx) => ctx.ids?.label ?? `splitter:${ctx.id}:label`,
45
45
  getPanelId: (ctx, id) => ctx.ids?.panel?.(id) ?? `splitter:${ctx.id}:panel:${id}`,
46
46
  globalCursorId: (ctx) => `splitter:${ctx.id}:global-cursor`,
47
- getRootEl: (ctx) => dom.getById(ctx, dom.getRootId(ctx)),
47
+ getRootEl: (ctx) => dom.queryById(ctx, dom.getRootId(ctx)),
48
48
  getResizeTriggerEl: (ctx, id) => dom.getById(ctx, dom.getResizeTriggerId(ctx, id)),
49
49
  getPanelEl: (ctx, id) => dom.getById(ctx, dom.getPanelId(ctx, id)),
50
50
  getCursor(ctx) {
@@ -219,6 +219,7 @@ function connect(state, send, normalize) {
219
219
  const isHorizontal = state.context.isHorizontal;
220
220
  const isFocused = state.hasTag("focus");
221
221
  const isDragging = state.matches("dragging");
222
+ const panels = state.context.panels;
222
223
  const api = {
223
224
  /**
224
225
  * Whether the splitter is focused.
@@ -233,22 +234,18 @@ function connect(state, send, normalize) {
233
234
  */
234
235
  bounds: getHandleBounds(state.context),
235
236
  /**
236
- * Function to collapse a panel.
237
+ * Function to set a panel to its minimum size.
237
238
  */
238
- collapse(id) {
239
- send({ type: "COLLAPSE", id });
239
+ setToMinSize(id) {
240
+ const panel = panels.find((panel2) => panel2.id === id);
241
+ send({ type: "SET_SIZE", id, size: panel?.minSize, src: "collapse" });
240
242
  },
241
243
  /**
242
- * Function to expand a panel.
244
+ * Function to set a panel to its maximum size.
243
245
  */
244
- expand(id) {
245
- send({ type: "EXPAND", id });
246
- },
247
- /**
248
- * Function to toggle a panel between collapsed and expanded.
249
- */
250
- toggle(id) {
251
- send({ type: "TOGGLE", id });
246
+ setToMaxSize(id) {
247
+ const panel = panels.find((panel2) => panel2.id === id);
248
+ send({ type: "SET_SIZE", id, size: panel?.maxSize, src: "expand" });
252
249
  },
253
250
  /**
254
251
  * Function to set the size of a panel.
@@ -263,13 +260,13 @@ function connect(state, send, normalize) {
263
260
  const { id, disabled } = props;
264
261
  const ids = id.split(":");
265
262
  const panelIds = ids.map((id2) => dom.getPanelId(state.context, id2));
266
- const panels = getHandleBounds(state.context, id);
263
+ const panels2 = getHandleBounds(state.context, id);
267
264
  return {
268
265
  isDisabled: !!disabled,
269
266
  isFocused: state.context.activeResizeId === id && isFocused,
270
267
  panelIds,
271
- min: panels?.min,
272
- max: panels?.max,
268
+ min: panels2?.min,
269
+ max: panels2?.max,
273
270
  value: 0
274
271
  };
275
272
  },
@@ -296,14 +293,6 @@ function connect(state, send, normalize) {
296
293
  style: dom.getPanelStyle(state.context, id)
297
294
  });
298
295
  },
299
- // toggleTriggerProps: normalize.element({
300
- // ...parts.toggleButton.attrs,
301
- // id: dom.getToggleButtonId(state.context),
302
- // "aria-label": state.context.isAtMin ? "Expand Primary Pane" : "Collapse Primary Pane",
303
- // onClick() {
304
- // send("TOGGLE")
305
- // },
306
- // }),
307
296
  getResizeTriggerProps(props) {
308
297
  const { id, disabled, step = 1 } = props;
309
298
  const triggerState = api.getResizeTriggerState(props);
@@ -446,7 +435,10 @@ function machine(userContext) {
446
435
  {
447
436
  actions: "setStartPanelToMin"
448
437
  }
449
- ]
438
+ ],
439
+ SET_SIZE: {
440
+ actions: "setPanelSize"
441
+ }
450
442
  },
451
443
  states: {
452
444
  idle: {
@@ -593,6 +585,13 @@ function machine(userContext) {
593
585
  size: panel.size
594
586
  }));
595
587
  },
588
+ setPanelSize(ctx2, evt) {
589
+ const { id, size } = evt;
590
+ ctx2.size = ctx2.size.map((panel) => {
591
+ const panelSize = clamp(size, panel.minSize ?? 0, panel.maxSize ?? 100);
592
+ return panel.id === id ? { ...panel, size: panelSize } : panel;
593
+ });
594
+ },
596
595
  setStartPanelToMin(ctx2) {
597
596
  const bounds = getPanelBounds(ctx2);
598
597
  if (!bounds)
@@ -659,10 +658,15 @@ function machine(userContext) {
659
658
  setPointerValue(ctx2, evt) {
660
659
  const panels = getHandlePanels(ctx2);
661
660
  const bounds = getHandleBounds(ctx2);
662
- const rootEl = dom.getRootEl(ctx2);
663
- if (!panels || !rootEl || !bounds)
661
+ if (!panels || !bounds)
664
662
  return;
665
- let pointValue = (0, import_dom_event2.getRelativePointPercent)(evt.point, rootEl).normalize(ctx2) * 100;
663
+ const rootEl = dom.getRootEl(ctx2);
664
+ const relativePoint = (0, import_dom_event2.getRelativePoint)(evt.point, rootEl);
665
+ const percentValue = relativePoint.getPercentValue({
666
+ dir: ctx2.dir,
667
+ orientation: ctx2.orientation
668
+ });
669
+ let pointValue = percentValue * 100;
666
670
  ctx2.activeResizeState = {
667
671
  isAtMin: pointValue < bounds.min,
668
672
  isAtMax: pointValue > bounds.max
package/dist/index.mjs CHANGED
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  connect
3
- } from "./chunk-ECKRJG7O.mjs";
3
+ } from "./chunk-53T2VZ2R.mjs";
4
4
  import {
5
5
  anatomy
6
6
  } from "./chunk-HPRMFGOY.mjs";
7
7
  import {
8
8
  machine
9
- } from "./chunk-PI3URGYH.mjs";
10
- import "./chunk-G4OIUPPJ.mjs";
9
+ } from "./chunk-X2E2LAC5.mjs";
10
+ import "./chunk-3GDOPT5W.mjs";
11
11
  import "./chunk-MV44GBQY.mjs";
12
12
  export {
13
13
  anatomy,
@@ -19,17 +19,13 @@ declare function connect<T extends PropTypes>(state: State, send: Send, normaliz
19
19
  max: number;
20
20
  } | undefined;
21
21
  /**
22
- * Function to collapse a panel.
22
+ * Function to set a panel to its minimum size.
23
23
  */
24
- collapse(id: PanelId): void;
24
+ setToMinSize(id: PanelId): void;
25
25
  /**
26
- * Function to expand a panel.
26
+ * Function to set a panel to its maximum size.
27
27
  */
28
- expand(id: PanelId): void;
29
- /**
30
- * Function to toggle a panel between collapsed and expanded.
31
- */
32
- toggle(id: PanelId): void;
28
+ setToMaxSize(id: PanelId): void;
33
29
  /**
34
30
  * Function to set the size of a panel.
35
31
  */
@@ -40,7 +40,7 @@ var dom = (0, import_dom_query.createScope)({
40
40
  getLabelId: (ctx) => ctx.ids?.label ?? `splitter:${ctx.id}:label`,
41
41
  getPanelId: (ctx, id) => ctx.ids?.panel?.(id) ?? `splitter:${ctx.id}:panel:${id}`,
42
42
  globalCursorId: (ctx) => `splitter:${ctx.id}:global-cursor`,
43
- getRootEl: (ctx) => dom.getById(ctx, dom.getRootId(ctx)),
43
+ getRootEl: (ctx) => dom.queryById(ctx, dom.getRootId(ctx)),
44
44
  getResizeTriggerEl: (ctx, id) => dom.getById(ctx, dom.getResizeTriggerId(ctx, id)),
45
45
  getPanelEl: (ctx, id) => dom.getById(ctx, dom.getPanelId(ctx, id)),
46
46
  getCursor(ctx) {
@@ -126,6 +126,7 @@ function connect(state, send, normalize) {
126
126
  const isHorizontal = state.context.isHorizontal;
127
127
  const isFocused = state.hasTag("focus");
128
128
  const isDragging = state.matches("dragging");
129
+ const panels = state.context.panels;
129
130
  const api = {
130
131
  /**
131
132
  * Whether the splitter is focused.
@@ -140,22 +141,18 @@ function connect(state, send, normalize) {
140
141
  */
141
142
  bounds: getHandleBounds(state.context),
142
143
  /**
143
- * Function to collapse a panel.
144
+ * Function to set a panel to its minimum size.
144
145
  */
145
- collapse(id) {
146
- send({ type: "COLLAPSE", id });
146
+ setToMinSize(id) {
147
+ const panel = panels.find((panel2) => panel2.id === id);
148
+ send({ type: "SET_SIZE", id, size: panel?.minSize, src: "collapse" });
147
149
  },
148
150
  /**
149
- * Function to expand a panel.
151
+ * Function to set a panel to its maximum size.
150
152
  */
151
- expand(id) {
152
- send({ type: "EXPAND", id });
153
- },
154
- /**
155
- * Function to toggle a panel between collapsed and expanded.
156
- */
157
- toggle(id) {
158
- send({ type: "TOGGLE", id });
153
+ setToMaxSize(id) {
154
+ const panel = panels.find((panel2) => panel2.id === id);
155
+ send({ type: "SET_SIZE", id, size: panel?.maxSize, src: "expand" });
159
156
  },
160
157
  /**
161
158
  * Function to set the size of a panel.
@@ -170,13 +167,13 @@ function connect(state, send, normalize) {
170
167
  const { id, disabled } = props;
171
168
  const ids = id.split(":");
172
169
  const panelIds = ids.map((id2) => dom.getPanelId(state.context, id2));
173
- const panels = getHandleBounds(state.context, id);
170
+ const panels2 = getHandleBounds(state.context, id);
174
171
  return {
175
172
  isDisabled: !!disabled,
176
173
  isFocused: state.context.activeResizeId === id && isFocused,
177
174
  panelIds,
178
- min: panels?.min,
179
- max: panels?.max,
175
+ min: panels2?.min,
176
+ max: panels2?.max,
180
177
  value: 0
181
178
  };
182
179
  },
@@ -203,14 +200,6 @@ function connect(state, send, normalize) {
203
200
  style: dom.getPanelStyle(state.context, id)
204
201
  });
205
202
  },
206
- // toggleTriggerProps: normalize.element({
207
- // ...parts.toggleButton.attrs,
208
- // id: dom.getToggleButtonId(state.context),
209
- // "aria-label": state.context.isAtMin ? "Expand Primary Pane" : "Collapse Primary Pane",
210
- // onClick() {
211
- // send("TOGGLE")
212
- // },
213
- // }),
214
203
  getResizeTriggerProps(props) {
215
204
  const { id, disabled, step = 1 } = props;
216
205
  const triggerState = api.getResizeTriggerState(props);
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  connect
3
- } from "./chunk-ECKRJG7O.mjs";
3
+ } from "./chunk-53T2VZ2R.mjs";
4
4
  import "./chunk-HPRMFGOY.mjs";
5
- import "./chunk-G4OIUPPJ.mjs";
5
+ import "./chunk-3GDOPT5W.mjs";
6
6
  import "./chunk-MV44GBQY.mjs";
7
7
  export {
8
8
  connect
@@ -28,7 +28,7 @@ declare const dom: {
28
28
  getLabelId: (ctx: MachineContext) => string | ((id: string) => string);
29
29
  getPanelId: (ctx: MachineContext, id: string | number) => string;
30
30
  globalCursorId: (ctx: MachineContext) => string;
31
- getRootEl: (ctx: MachineContext) => HTMLElement | null;
31
+ getRootEl: (ctx: MachineContext) => HTMLElement;
32
32
  getResizeTriggerEl: (ctx: MachineContext, id: string) => HTMLElement | null;
33
33
  getPanelEl: (ctx: MachineContext, id: string | number) => HTMLElement | null;
34
34
  getCursor(ctx: MachineContext): (string & {}) | "col-resize" | "e-resize" | "n-resize" | "row-resize" | "s-resize" | "w-resize";
@@ -31,7 +31,7 @@ var dom = (0, import_dom_query.createScope)({
31
31
  getLabelId: (ctx) => ctx.ids?.label ?? `splitter:${ctx.id}:label`,
32
32
  getPanelId: (ctx, id) => ctx.ids?.panel?.(id) ?? `splitter:${ctx.id}:panel:${id}`,
33
33
  globalCursorId: (ctx) => `splitter:${ctx.id}:global-cursor`,
34
- getRootEl: (ctx) => dom.getById(ctx, dom.getRootId(ctx)),
34
+ getRootEl: (ctx) => dom.queryById(ctx, dom.getRootId(ctx)),
35
35
  getResizeTriggerEl: (ctx, id) => dom.getById(ctx, dom.getResizeTriggerId(ctx, id)),
36
36
  getPanelEl: (ctx, id) => dom.getById(ctx, dom.getPanelId(ctx, id)),
37
37
  getCursor(ctx) {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  dom
3
- } from "./chunk-G4OIUPPJ.mjs";
3
+ } from "./chunk-3GDOPT5W.mjs";
4
4
  export {
5
5
  dom
6
6
  };
@@ -37,7 +37,7 @@ var dom = (0, import_dom_query.createScope)({
37
37
  getLabelId: (ctx) => ctx.ids?.label ?? `splitter:${ctx.id}:label`,
38
38
  getPanelId: (ctx, id) => ctx.ids?.panel?.(id) ?? `splitter:${ctx.id}:panel:${id}`,
39
39
  globalCursorId: (ctx) => `splitter:${ctx.id}:global-cursor`,
40
- getRootEl: (ctx) => dom.getById(ctx, dom.getRootId(ctx)),
40
+ getRootEl: (ctx) => dom.queryById(ctx, dom.getRootId(ctx)),
41
41
  getResizeTriggerEl: (ctx, id) => dom.getById(ctx, dom.getResizeTriggerId(ctx, id)),
42
42
  getPanelEl: (ctx, id) => dom.getById(ctx, dom.getPanelId(ctx, id)),
43
43
  getCursor(ctx) {
@@ -249,7 +249,10 @@ function machine(userContext) {
249
249
  {
250
250
  actions: "setStartPanelToMin"
251
251
  }
252
- ]
252
+ ],
253
+ SET_SIZE: {
254
+ actions: "setPanelSize"
255
+ }
253
256
  },
254
257
  states: {
255
258
  idle: {
@@ -396,6 +399,13 @@ function machine(userContext) {
396
399
  size: panel.size
397
400
  }));
398
401
  },
402
+ setPanelSize(ctx2, evt) {
403
+ const { id, size } = evt;
404
+ ctx2.size = ctx2.size.map((panel) => {
405
+ const panelSize = clamp(size, panel.minSize ?? 0, panel.maxSize ?? 100);
406
+ return panel.id === id ? { ...panel, size: panelSize } : panel;
407
+ });
408
+ },
399
409
  setStartPanelToMin(ctx2) {
400
410
  const bounds = getPanelBounds(ctx2);
401
411
  if (!bounds)
@@ -462,10 +472,15 @@ function machine(userContext) {
462
472
  setPointerValue(ctx2, evt) {
463
473
  const panels = getHandlePanels(ctx2);
464
474
  const bounds = getHandleBounds(ctx2);
465
- const rootEl = dom.getRootEl(ctx2);
466
- if (!panels || !rootEl || !bounds)
475
+ if (!panels || !bounds)
467
476
  return;
468
- let pointValue = (0, import_dom_event.getRelativePointPercent)(evt.point, rootEl).normalize(ctx2) * 100;
477
+ const rootEl = dom.getRootEl(ctx2);
478
+ const relativePoint = (0, import_dom_event.getRelativePoint)(evt.point, rootEl);
479
+ const percentValue = relativePoint.getPercentValue({
480
+ dir: ctx2.dir,
481
+ orientation: ctx2.orientation
482
+ });
483
+ let pointValue = percentValue * 100;
469
484
  ctx2.activeResizeState = {
470
485
  isAtMin: pointValue < bounds.min,
471
486
  isAtMax: pointValue > bounds.max
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  machine
3
- } from "./chunk-PI3URGYH.mjs";
4
- import "./chunk-G4OIUPPJ.mjs";
3
+ } from "./chunk-X2E2LAC5.mjs";
4
+ import "./chunk-3GDOPT5W.mjs";
5
5
  import "./chunk-MV44GBQY.mjs";
6
6
  export {
7
7
  machine
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zag-js/splitter",
3
- "version": "0.9.2",
3
+ "version": "0.10.1",
4
4
  "description": "Core logic for the splitter widget implemented as a state machine",
5
5
  "keywords": [
6
6
  "js",
@@ -18,7 +18,8 @@
18
18
  "repository": "https://github.com/chakra-ui/zag/tree/main/packages/splitter",
19
19
  "sideEffects": false,
20
20
  "files": [
21
- "dist/**/*"
21
+ "dist",
22
+ "src"
22
23
  ],
23
24
  "publishConfig": {
24
25
  "access": "public"
@@ -27,13 +28,13 @@
27
28
  "url": "https://github.com/chakra-ui/zag/issues"
28
29
  },
29
30
  "dependencies": {
30
- "@zag-js/anatomy": "0.9.2",
31
- "@zag-js/core": "0.9.2",
32
- "@zag-js/types": "0.9.2",
33
- "@zag-js/dom-query": "0.9.2",
34
- "@zag-js/dom-event": "0.9.2",
35
- "@zag-js/number-utils": "0.9.2",
36
- "@zag-js/utils": "0.9.2"
31
+ "@zag-js/anatomy": "0.10.1",
32
+ "@zag-js/core": "0.10.1",
33
+ "@zag-js/types": "0.10.1",
34
+ "@zag-js/dom-query": "0.10.1",
35
+ "@zag-js/dom-event": "0.10.1",
36
+ "@zag-js/number-utils": "0.10.1",
37
+ "@zag-js/utils": "0.10.1"
37
38
  },
38
39
  "devDependencies": {
39
40
  "clean-package": "2.2.0"
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export { anatomy } from "./splitter.anatomy"
2
+ export { connect } from "./splitter.connect"
3
+ export { machine } from "./splitter.machine"
4
+ export type { MachineState, UserDefinedContext as Context, PanelProps, ResizeTriggerProps } from "./splitter.types"
@@ -0,0 +1,5 @@
1
+ import { createAnatomy } from "@zag-js/anatomy"
2
+
3
+ export const anatomy = createAnatomy("splitter").parts("root", "panel", "toggleTrigger", "resizeTrigger")
4
+
5
+ export const parts = anatomy.build()
@@ -0,0 +1,186 @@
1
+ import { EventKeyMap, getEventKey, getEventStep } from "@zag-js/dom-event"
2
+ import { dataAttr } from "@zag-js/dom-query"
3
+ import type { NormalizeProps, PropTypes } from "@zag-js/types"
4
+ import { parts } from "./splitter.anatomy"
5
+ import { dom } from "./splitter.dom"
6
+ import type { PanelId, PanelProps, ResizeTriggerProps, Send, State } from "./splitter.types"
7
+ import { getHandleBounds } from "./splitter.utils"
8
+
9
+ export function connect<T extends PropTypes>(state: State, send: Send, normalize: NormalizeProps<T>) {
10
+ const isHorizontal = state.context.isHorizontal
11
+ const isFocused = state.hasTag("focus")
12
+ const isDragging = state.matches("dragging")
13
+ const panels = state.context.panels
14
+
15
+ const api = {
16
+ /**
17
+ * Whether the splitter is focused.
18
+ */
19
+ isFocused,
20
+ /**
21
+ * Whether the splitter is being dragged.
22
+ */
23
+ isDragging,
24
+ /**
25
+ * The bounds of the currently dragged splitter handle.
26
+ */
27
+ bounds: getHandleBounds(state.context),
28
+ /**
29
+ * Function to set a panel to its minimum size.
30
+ */
31
+ setToMinSize(id: PanelId) {
32
+ const panel = panels.find((panel) => panel.id === id)
33
+ send({ type: "SET_SIZE", id, size: panel?.minSize, src: "collapse" })
34
+ },
35
+ /**
36
+ * Function to set a panel to its maximum size.
37
+ */
38
+ setToMaxSize(id: PanelId) {
39
+ const panel = panels.find((panel) => panel.id === id)
40
+ send({ type: "SET_SIZE", id, size: panel?.maxSize, src: "expand" })
41
+ },
42
+ /**
43
+ * Function to set the size of a panel.
44
+ */
45
+ setSize(id: PanelId, size: number) {
46
+ send({ type: "SET_SIZE", id, size })
47
+ },
48
+ /**
49
+ * Returns the state details for a resize trigger.
50
+ */
51
+ getResizeTriggerState(props: ResizeTriggerProps) {
52
+ const { id, disabled } = props
53
+ const ids = id.split(":")
54
+ const panelIds = ids.map((id) => dom.getPanelId(state.context, id))
55
+ const panels = getHandleBounds(state.context, id)
56
+
57
+ return {
58
+ isDisabled: !!disabled,
59
+ isFocused: state.context.activeResizeId === id && isFocused,
60
+ panelIds,
61
+ min: panels?.min,
62
+ max: panels?.max,
63
+ value: 0,
64
+ }
65
+ },
66
+
67
+ rootProps: normalize.element({
68
+ ...parts.root.attrs,
69
+ "data-orientation": state.context.orientation,
70
+ id: dom.getRootId(state.context),
71
+ dir: state.context.dir,
72
+ style: {
73
+ display: "flex",
74
+ flexDirection: isHorizontal ? "row" : "column",
75
+ height: "100%",
76
+ width: "100%",
77
+ overflow: "hidden",
78
+ },
79
+ }),
80
+
81
+ getPanelProps(props: PanelProps) {
82
+ const { id } = props
83
+ return normalize.element({
84
+ ...parts.panel.attrs,
85
+ dir: state.context.dir,
86
+ id: dom.getPanelId(state.context, id),
87
+ "data-ownedby": dom.getRootId(state.context),
88
+ style: dom.getPanelStyle(state.context, id),
89
+ })
90
+ },
91
+
92
+ getResizeTriggerProps(props: ResizeTriggerProps) {
93
+ const { id, disabled, step = 1 } = props
94
+ const triggerState = api.getResizeTriggerState(props)
95
+
96
+ return normalize.element({
97
+ ...parts.resizeTrigger.attrs,
98
+ dir: state.context.dir,
99
+ id: dom.getResizeTriggerId(state.context, id),
100
+ role: "separator",
101
+ "data-ownedby": dom.getRootId(state.context),
102
+ tabIndex: disabled ? undefined : 0,
103
+ "aria-valuenow": triggerState.value,
104
+ "aria-valuemin": triggerState.min,
105
+ "aria-valuemax": triggerState.max,
106
+ "data-orientation": state.context.orientation,
107
+ "aria-orientation": state.context.orientation,
108
+ "aria-controls": triggerState.panelIds.join(" "),
109
+ "data-focus": dataAttr(triggerState.isFocused),
110
+ "data-disabled": dataAttr(disabled),
111
+ style: {
112
+ touchAction: "none",
113
+ userSelect: "none",
114
+ flex: "0 0 auto",
115
+ pointerEvents: isDragging && !triggerState.isFocused ? "none" : undefined,
116
+ cursor: isHorizontal ? "col-resize" : "row-resize",
117
+ [isHorizontal ? "minHeight" : "minWidth"]: "0",
118
+ },
119
+ onPointerDown(event) {
120
+ if (disabled) {
121
+ event.preventDefault()
122
+ return
123
+ }
124
+ send({ type: "POINTER_DOWN", id })
125
+ event.preventDefault()
126
+ event.stopPropagation()
127
+ },
128
+ onPointerOver() {
129
+ if (disabled) return
130
+ send({ type: "POINTER_OVER", id })
131
+ },
132
+ onPointerLeave() {
133
+ if (disabled) return
134
+ send({ type: "POINTER_LEAVE", id })
135
+ },
136
+ onBlur() {
137
+ send("BLUR")
138
+ },
139
+ onFocus() {
140
+ send({ type: "FOCUS", id })
141
+ },
142
+ onDoubleClick() {
143
+ if (disabled) return
144
+ send({ type: "DOUBLE_CLICK", id })
145
+ },
146
+ onKeyDown(event) {
147
+ if (disabled) return
148
+ const moveStep = getEventStep(event) * step
149
+ const keyMap: EventKeyMap = {
150
+ Enter() {
151
+ send("ENTER")
152
+ },
153
+ ArrowUp() {
154
+ send({ type: "ARROW_UP", step: moveStep })
155
+ },
156
+ ArrowDown() {
157
+ send({ type: "ARROW_DOWN", step: moveStep })
158
+ },
159
+ ArrowLeft() {
160
+ send({ type: "ARROW_LEFT", step: moveStep })
161
+ },
162
+ ArrowRight() {
163
+ send({ type: "ARROW_RIGHT", step: moveStep })
164
+ },
165
+ Home() {
166
+ send("HOME")
167
+ },
168
+ End() {
169
+ send("END")
170
+ },
171
+ }
172
+
173
+ const key = getEventKey(event, state.context)
174
+ const exec = keyMap[key]
175
+
176
+ if (exec) {
177
+ exec(event)
178
+ event.preventDefault()
179
+ }
180
+ },
181
+ })
182
+ },
183
+ }
184
+
185
+ return api
186
+ }
@@ -0,0 +1,62 @@
1
+ import { createScope, queryAll } from "@zag-js/dom-query"
2
+ import type { JSX, Style } from "@zag-js/types"
3
+ import type { MachineContext as Ctx, PanelId } from "./splitter.types"
4
+
5
+ export const dom = createScope({
6
+ getRootId: (ctx: Ctx) => ctx.ids?.root ?? `splitter:${ctx.id}`,
7
+ getResizeTriggerId: (ctx: Ctx, id: string) => ctx.ids?.resizeTrigger?.(id) ?? `splitter:${ctx.id}:splitter:${id}`,
8
+ getToggleTriggerId: (ctx: Ctx) => ctx.ids?.toggleTrigger?.(ctx.id) ?? `splitter:${ctx.id}:toggle-btn`,
9
+ getLabelId: (ctx: Ctx) => ctx.ids?.label ?? `splitter:${ctx.id}:label`,
10
+ getPanelId: (ctx: Ctx, id: string | number) => ctx.ids?.panel?.(id) ?? `splitter:${ctx.id}:panel:${id}`,
11
+ globalCursorId: (ctx: Ctx) => `splitter:${ctx.id}:global-cursor`,
12
+
13
+ getRootEl: (ctx: Ctx) => dom.queryById(ctx, dom.getRootId(ctx)),
14
+ getResizeTriggerEl: (ctx: Ctx, id: string) => dom.getById(ctx, dom.getResizeTriggerId(ctx, id)),
15
+ getPanelEl: (ctx: Ctx, id: string | number) => dom.getById(ctx, dom.getPanelId(ctx, id)),
16
+
17
+ getCursor(ctx: Ctx) {
18
+ const x = ctx.isHorizontal
19
+ let cursor: Style["cursor"] = x ? "col-resize" : "row-resize"
20
+ if (ctx.activeResizeState.isAtMin) cursor = x ? "e-resize" : "s-resize"
21
+ if (ctx.activeResizeState.isAtMax) cursor = x ? "w-resize" : "n-resize"
22
+ return cursor
23
+ },
24
+
25
+ getPanelStyle(ctx: Ctx, id: PanelId): JSX.CSSProperties {
26
+ const flexGrow = ctx.panels.find((panel) => panel.id === id)?.size ?? "0"
27
+ return {
28
+ flexBasis: 0,
29
+ flexGrow,
30
+ flexShrink: 1,
31
+ overflow: "hidden",
32
+ }
33
+ },
34
+
35
+ getActiveHandleEl(ctx: Ctx) {
36
+ const activeId = ctx.activeResizeId
37
+ if (activeId == null) return
38
+ return dom.getById(ctx, dom.getResizeTriggerId(ctx, activeId))
39
+ },
40
+
41
+ getResizeTriggerEls(ctx: Ctx) {
42
+ const ownerId = CSS.escape(dom.getRootId(ctx))
43
+ return queryAll(dom.getRootEl(ctx), `[role=separator][data-ownedby='${ownerId}']`)
44
+ },
45
+
46
+ setupGlobalCursor(ctx: Ctx) {
47
+ const styleEl = dom.getById(ctx, dom.globalCursorId(ctx))
48
+ const textContent = `* { cursor: ${dom.getCursor(ctx)} !important; }`
49
+ if (styleEl) {
50
+ styleEl.textContent = textContent
51
+ } else {
52
+ const style = dom.getDoc(ctx).createElement("style")
53
+ style.id = dom.globalCursorId(ctx)
54
+ style.textContent = textContent
55
+ dom.getDoc(ctx).head.appendChild(style)
56
+ }
57
+ },
58
+
59
+ removeGlobalCursor(ctx: Ctx) {
60
+ dom.getById(ctx, dom.globalCursorId(ctx))?.remove()
61
+ },
62
+ })
@@ -0,0 +1,304 @@
1
+ import { createMachine } from "@zag-js/core"
2
+ import { getRelativePoint, trackPointerMove } from "@zag-js/dom-event"
3
+ import { raf } from "@zag-js/dom-query"
4
+ import { compact } from "@zag-js/utils"
5
+ import { dom } from "./splitter.dom"
6
+ import type { MachineContext, MachineState, UserDefinedContext } from "./splitter.types"
7
+ import { clamp, getHandleBounds, getHandlePanels, getNormalizedPanels, getPanelBounds } from "./splitter.utils"
8
+
9
+ export function machine(userContext: UserDefinedContext) {
10
+ const ctx = compact(userContext)
11
+ return createMachine<MachineContext, MachineState>(
12
+ {
13
+ id: "splitter",
14
+ initial: "idle",
15
+ context: {
16
+ orientation: "horizontal",
17
+ activeResizeId: null,
18
+ previousPanels: [],
19
+ size: [],
20
+ initialSize: [],
21
+ activeResizeState: {
22
+ isAtMin: false,
23
+ isAtMax: false,
24
+ },
25
+ ...ctx,
26
+ },
27
+
28
+ created: ["setPreviousPanels", "setInitialSize"],
29
+
30
+ watch: {
31
+ size: ["setActiveResizeState"],
32
+ },
33
+
34
+ computed: {
35
+ isHorizontal: (ctx) => ctx.orientation === "horizontal",
36
+ panels: (ctx) => getNormalizedPanels(ctx),
37
+ },
38
+
39
+ on: {
40
+ COLLAPSE: {
41
+ actions: "setStartPanelToMin",
42
+ },
43
+ EXPAND: {
44
+ actions: "setStartPanelToMax",
45
+ },
46
+ TOGGLE: [
47
+ {
48
+ guard: "isStartPanelAtMin",
49
+ actions: "setStartPanelToMax",
50
+ },
51
+ {
52
+ actions: "setStartPanelToMin",
53
+ },
54
+ ],
55
+ SET_SIZE: {
56
+ actions: "setPanelSize",
57
+ },
58
+ },
59
+ states: {
60
+ idle: {
61
+ entry: ["clearActiveHandleId"],
62
+ on: {
63
+ POINTER_OVER: {
64
+ target: "hover:temp",
65
+ actions: ["setActiveHandleId"],
66
+ },
67
+ FOCUS: {
68
+ target: "focused",
69
+ actions: ["setActiveHandleId"],
70
+ },
71
+ DOUBLE_CLICK: {
72
+ actions: ["resetStartPanel", "setPreviousPanels"],
73
+ },
74
+ },
75
+ },
76
+
77
+ "hover:temp": {
78
+ after: {
79
+ HOVER_DELAY: "hover",
80
+ },
81
+ on: {
82
+ POINTER_DOWN: {
83
+ target: "dragging",
84
+ actions: ["setActiveHandleId", "invokeOnResizeStart"],
85
+ },
86
+ POINTER_LEAVE: "idle",
87
+ },
88
+ },
89
+
90
+ hover: {
91
+ tags: ["focus"],
92
+ on: {
93
+ POINTER_DOWN: {
94
+ target: "dragging",
95
+ actions: ["invokeOnResizeStart"],
96
+ },
97
+ POINTER_LEAVE: "idle",
98
+ },
99
+ },
100
+
101
+ focused: {
102
+ tags: ["focus"],
103
+ on: {
104
+ BLUR: "idle",
105
+ POINTER_DOWN: {
106
+ target: "dragging",
107
+ actions: ["setActiveHandleId", "invokeOnResizeStart"],
108
+ },
109
+ ARROW_LEFT: {
110
+ guard: "isHorizontal",
111
+ actions: ["shrinkStartPanel", "setPreviousPanels"],
112
+ },
113
+ ARROW_RIGHT: {
114
+ guard: "isHorizontal",
115
+ actions: ["expandStartPanel", "setPreviousPanels"],
116
+ },
117
+ ARROW_UP: {
118
+ guard: "isVertical",
119
+ actions: ["shrinkStartPanel", "setPreviousPanels"],
120
+ },
121
+ ARROW_DOWN: {
122
+ guard: "isVertical",
123
+ actions: ["expandStartPanel", "setPreviousPanels"],
124
+ },
125
+ ENTER: [
126
+ {
127
+ guard: "isStartPanelAtMax",
128
+ actions: ["setStartPanelToMin", "setPreviousPanels"],
129
+ },
130
+ { actions: ["setStartPanelToMax", "setPreviousPanels"] },
131
+ ],
132
+ HOME: {
133
+ actions: ["setStartPanelToMin", "setPreviousPanels"],
134
+ },
135
+ END: {
136
+ actions: ["setStartPanelToMax", "setPreviousPanels"],
137
+ },
138
+ },
139
+ },
140
+
141
+ dragging: {
142
+ tags: ["focus"],
143
+ entry: "focusResizeHandle",
144
+ activities: ["trackPointerMove"],
145
+ on: {
146
+ POINTER_MOVE: {
147
+ actions: ["setPointerValue", "setGlobalCursor"],
148
+ },
149
+ POINTER_UP: {
150
+ target: "focused",
151
+ actions: ["invokeOnResizeEnd", "setPreviousPanels", "clearGlobalCursor", "blurResizeHandle"],
152
+ },
153
+ },
154
+ },
155
+ },
156
+ },
157
+ {
158
+ activities: {
159
+ trackPointerMove: (ctx, _evt, { send }) => {
160
+ const doc = dom.getDoc(ctx)
161
+ return trackPointerMove(doc, {
162
+ onPointerMove(info) {
163
+ send({ type: "POINTER_MOVE", point: info.point })
164
+ },
165
+ onPointerUp() {
166
+ send("POINTER_UP")
167
+ },
168
+ })
169
+ },
170
+ },
171
+ guards: {
172
+ isStartPanelAtMin: (ctx) => ctx.activeResizeState.isAtMin,
173
+ isStartPanelAtMax: (ctx) => ctx.activeResizeState.isAtMax,
174
+ isHorizontal: (ctx) => ctx.isHorizontal,
175
+ isVertical: (ctx) => !ctx.isHorizontal,
176
+ },
177
+ delays: {
178
+ HOVER_DELAY: 250,
179
+ },
180
+ actions: {
181
+ setGlobalCursor(ctx) {
182
+ dom.setupGlobalCursor(ctx)
183
+ },
184
+ clearGlobalCursor(ctx) {
185
+ dom.removeGlobalCursor(ctx)
186
+ },
187
+ invokeOnResize(ctx) {
188
+ ctx.onResize?.({ size: ctx.size, activeHandleId: ctx.activeResizeId })
189
+ },
190
+ invokeOnResizeStart(ctx) {
191
+ ctx.onResizeStart?.({ size: ctx.size, activeHandleId: ctx.activeResizeId })
192
+ },
193
+ invokeOnResizeEnd(ctx) {
194
+ ctx.onResizeEnd?.({ size: ctx.size, activeHandleId: ctx.activeResizeId })
195
+ },
196
+ setActiveHandleId(ctx, evt) {
197
+ ctx.activeResizeId = evt.id
198
+ },
199
+ clearActiveHandleId(ctx) {
200
+ ctx.activeResizeId = null
201
+ },
202
+ setInitialSize(ctx) {
203
+ ctx.initialSize = ctx.panels.slice().map((panel) => ({
204
+ id: panel.id,
205
+ size: panel.size,
206
+ }))
207
+ },
208
+ setPanelSize(ctx, evt) {
209
+ const { id, size } = evt
210
+ ctx.size = ctx.size.map((panel) => {
211
+ const panelSize = clamp(size, panel.minSize ?? 0, panel.maxSize ?? 100)
212
+ return panel.id === id ? { ...panel, size: panelSize } : panel
213
+ })
214
+ },
215
+ setStartPanelToMin(ctx) {
216
+ const bounds = getPanelBounds(ctx)
217
+ if (!bounds) return
218
+ const { before, after } = bounds
219
+ ctx.size[before.index].size = before.min
220
+ ctx.size[after.index].size = after.min
221
+ },
222
+ setStartPanelToMax(ctx) {
223
+ const bounds = getPanelBounds(ctx)
224
+ if (!bounds) return
225
+ const { before, after } = bounds
226
+ ctx.size[before.index].size = before.max
227
+ ctx.size[after.index].size = after.max
228
+ },
229
+ expandStartPanel(ctx, evt) {
230
+ const bounds = getPanelBounds(ctx)
231
+ if (!bounds) return
232
+ const { before, after } = bounds
233
+ ctx.size[before.index].size = before.up(evt.step)
234
+ ctx.size[after.index].size = after.down(evt.step)
235
+ },
236
+ shrinkStartPanel(ctx, evt) {
237
+ const bounds = getPanelBounds(ctx)
238
+ if (!bounds) return
239
+ const { before, after } = bounds
240
+ ctx.size[before.index].size = before.down(evt.step)
241
+ ctx.size[after.index].size = after.up(evt.step)
242
+ },
243
+ resetStartPanel(ctx, evt) {
244
+ const bounds = getPanelBounds(ctx, evt.id)
245
+ if (!bounds) return
246
+ const { before, after } = bounds
247
+ ctx.size[before.index].size = ctx.initialSize[before.index].size
248
+ ctx.size[after.index].size = ctx.initialSize[after.index].size
249
+ },
250
+ focusResizeHandle(ctx) {
251
+ raf(() => {
252
+ dom.getActiveHandleEl(ctx)?.focus({ preventScroll: true })
253
+ })
254
+ },
255
+ blurResizeHandle(ctx) {
256
+ raf(() => {
257
+ dom.getActiveHandleEl(ctx)?.blur()
258
+ })
259
+ },
260
+ setPreviousPanels(ctx) {
261
+ ctx.previousPanels = ctx.panels.slice()
262
+ },
263
+ setActiveResizeState(ctx) {
264
+ const panels = getPanelBounds(ctx)
265
+ if (!panels) return
266
+ const { before } = panels
267
+ ctx.activeResizeState = {
268
+ isAtMin: before.isAtMin,
269
+ isAtMax: before.isAtMax,
270
+ }
271
+ },
272
+ setPointerValue(ctx, evt) {
273
+ const panels = getHandlePanels(ctx)
274
+ const bounds = getHandleBounds(ctx)
275
+
276
+ if (!panels || !bounds) return
277
+
278
+ const rootEl = dom.getRootEl(ctx)
279
+ const relativePoint = getRelativePoint(evt.point, rootEl)
280
+ const percentValue = relativePoint.getPercentValue({
281
+ dir: ctx.dir,
282
+ orientation: ctx.orientation,
283
+ })
284
+
285
+ let pointValue = percentValue * 100
286
+
287
+ // update active resize state here because we use `previousPanels` in the calculations
288
+ ctx.activeResizeState = {
289
+ isAtMin: pointValue < bounds.min,
290
+ isAtMax: pointValue > bounds.max,
291
+ }
292
+
293
+ pointValue = clamp(pointValue, bounds.min, bounds.max)
294
+
295
+ const { before, after } = panels
296
+
297
+ const offset = pointValue - before.end
298
+ ctx.size[before.index].size = before.size + offset
299
+ ctx.size[after.index].size = after.size - offset
300
+ },
301
+ },
302
+ },
303
+ )
304
+ }
@@ -0,0 +1,100 @@
1
+ import type { StateMachine as S } from "@zag-js/core"
2
+ import type { CommonProperties, Context, DirectionProperty, RequiredBy } from "@zag-js/types"
3
+
4
+ export type PanelId = string | number
5
+
6
+ type PanelSizeData = {
7
+ id: PanelId
8
+ size?: number
9
+ minSize?: number
10
+ maxSize?: number
11
+ }
12
+
13
+ type ResizeDetails = {
14
+ size: PanelSizeData[]
15
+ activeHandleId: string | null
16
+ }
17
+
18
+ type ElementIds = Partial<{
19
+ root: string
20
+ resizeTrigger(id: string): string
21
+ toggleTrigger(id: string): string
22
+ label(id: string): string
23
+ panel(id: string | number): string
24
+ }>
25
+
26
+ type PublicContext = DirectionProperty &
27
+ CommonProperties & {
28
+ /**
29
+ * The orientation of the splitter. Can be `horizontal` or `vertical`
30
+ */
31
+ orientation: "horizontal" | "vertical"
32
+ /**
33
+ * The size data of the panels
34
+ */
35
+ size: PanelSizeData[]
36
+ /**
37
+ * Function called when the splitter is resized.
38
+ */
39
+ onResize?: (details: ResizeDetails) => void
40
+ /**
41
+ * Function called when the splitter resize starts.
42
+ */
43
+ onResizeStart?: (details: ResizeDetails) => void
44
+ /**
45
+ * Function called when the splitter resize ends.
46
+ */
47
+ onResizeEnd?: (details: ResizeDetails) => void
48
+ /**
49
+ * The ids of the elements in the splitter. Useful for composition.
50
+ */
51
+ ids?: ElementIds
52
+ }
53
+
54
+ export type UserDefinedContext = RequiredBy<PublicContext, "id">
55
+
56
+ export type NormalizedPanelData = Array<
57
+ Required<PanelSizeData> & {
58
+ remainingSize: number
59
+ minSize: number
60
+ maxSize: number
61
+ start: number
62
+ end: number
63
+ }
64
+ >
65
+
66
+ type ComputedContext = Readonly<{
67
+ isHorizontal: boolean
68
+ panels: NormalizedPanelData
69
+ activeResizeBounds?: { min: number; max: number }
70
+ activeResizePanels?: { before: PanelSizeData; after: PanelSizeData }
71
+ }>
72
+
73
+ type PrivateContext = Context<{
74
+ activeResizeId: string | null
75
+ previousPanels: NormalizedPanelData
76
+ activeResizeState: { isAtMin: boolean; isAtMax: boolean }
77
+ initialSize: Array<Required<Pick<PanelSizeData, "id" | "size">>>
78
+ }>
79
+
80
+ export type MachineContext = PublicContext & ComputedContext & PrivateContext
81
+
82
+ export type MachineState = {
83
+ value: "idle" | "hover:temp" | "hover" | "dragging" | "focused"
84
+ tags: "focus"
85
+ }
86
+
87
+ export type State = S.State<MachineContext, MachineState>
88
+
89
+ export type Send = S.Send<S.AnyEventObject>
90
+
91
+ export type PanelProps = {
92
+ id: PanelId
93
+ snapSize?: number
94
+ }
95
+
96
+ export type ResizeTriggerProps = {
97
+ id: `${PanelId}:${PanelId}`
98
+ step?: number
99
+ disabled?: boolean
100
+ }
@@ -0,0 +1,143 @@
1
+ import type { MachineContext as Ctx, NormalizedPanelData } from "./splitter.types"
2
+
3
+ function validateSize(key: string, size: number) {
4
+ if (Math.floor(size) > 100) {
5
+ throw new Error(`Total ${key} of panels cannot be greater than 100`)
6
+ }
7
+ }
8
+
9
+ export function getNormalizedPanels(ctx: Ctx): NormalizedPanelData {
10
+ let numOfPanelsWithoutSize = 0
11
+ let totalSize = 0
12
+ let totalMinSize = 0
13
+
14
+ const panels = ctx.size.map((panel) => {
15
+ const minSize = panel.minSize ?? 10
16
+ const maxSize = panel.maxSize ?? 100
17
+
18
+ totalMinSize += minSize
19
+
20
+ if (panel.size == null) {
21
+ numOfPanelsWithoutSize++
22
+ } else {
23
+ totalSize += panel.size
24
+ }
25
+
26
+ return {
27
+ ...panel,
28
+ minSize,
29
+ maxSize,
30
+ }
31
+ })
32
+
33
+ validateSize("minSize", totalMinSize)
34
+ validateSize("size", totalSize)
35
+
36
+ let end = 0
37
+ let remainingSize = 0
38
+
39
+ const result = panels.map((panel) => {
40
+ let start = end
41
+
42
+ if (panel.size != null) {
43
+ end += panel.size
44
+ remainingSize = panel.size - panel.minSize
45
+ return {
46
+ ...panel,
47
+ start,
48
+ end,
49
+ remainingSize,
50
+ }
51
+ }
52
+
53
+ const size = (100 - totalSize) / numOfPanelsWithoutSize
54
+ end += size
55
+ remainingSize = size - panel.minSize
56
+
57
+ return { ...panel, size, start, end, remainingSize }
58
+ })
59
+
60
+ return result as NormalizedPanelData
61
+ }
62
+
63
+ export function getHandlePanels(ctx: Ctx, id = ctx.activeResizeId) {
64
+ const [beforeId, afterId] = id?.split(":") ?? []
65
+ if (!beforeId || !afterId) return
66
+
67
+ const beforeIndex = ctx.previousPanels.findIndex((panel) => panel.id === beforeId)
68
+ const afterIndex = ctx.previousPanels.findIndex((panel) => panel.id === afterId)
69
+ if (beforeIndex === -1 || afterIndex === -1) return
70
+
71
+ const before = ctx.previousPanels[beforeIndex]
72
+ const after = ctx.previousPanels[afterIndex]
73
+
74
+ return {
75
+ before: {
76
+ ...before,
77
+ index: beforeIndex,
78
+ },
79
+ after: {
80
+ ...after,
81
+ index: afterIndex,
82
+ },
83
+ }
84
+ }
85
+
86
+ export function getHandleBounds(ctx: Ctx, id = ctx.activeResizeId) {
87
+ const panels = getHandlePanels(ctx, id)
88
+ if (!panels) return
89
+
90
+ const { before, after } = panels
91
+
92
+ return {
93
+ min: Math.max(before.start + before.minSize, after.end - after.maxSize),
94
+ max: Math.min(after.end - after.minSize, before.maxSize + before.start),
95
+ }
96
+ }
97
+
98
+ export function getPanelBounds(ctx: Ctx, id?: string | null) {
99
+ const bounds = getHandleBounds(ctx, id)
100
+ const panels = getHandlePanels(ctx, id)
101
+
102
+ if (!bounds || !panels) return
103
+ const { before, after } = panels
104
+
105
+ const beforeMin = Math.abs(before.start - bounds.min)
106
+ const afterMin = after.size + (before.size - beforeMin)
107
+
108
+ const beforeMax = Math.abs(before.start - bounds.max)
109
+ const afterMax = after.size - (beforeMax - before.size)
110
+
111
+ return {
112
+ before: {
113
+ index: before.index,
114
+ min: beforeMin,
115
+ max: beforeMax,
116
+ isAtMin: beforeMin === before.size,
117
+ isAtMax: beforeMax === before.size,
118
+ up(step: number) {
119
+ return Math.min(before.size + step, beforeMax)
120
+ },
121
+ down(step: number) {
122
+ return Math.max(before.size - step, beforeMin)
123
+ },
124
+ },
125
+ after: {
126
+ index: after.index,
127
+ min: afterMin,
128
+ max: afterMax,
129
+ isAtMin: afterMin === after.size,
130
+ isAtMax: afterMax === after.size,
131
+ up(step: number) {
132
+ return Math.min(after.size + step, afterMin)
133
+ },
134
+ down(step: number) {
135
+ return Math.max(after.size - step, afterMax)
136
+ },
137
+ },
138
+ }
139
+ }
140
+
141
+ export function clamp(value: number, min: number, max: number) {
142
+ return Math.min(Math.max(value, min), max)
143
+ }