@zag-js/splitter 0.9.1 → 0.10.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.
@@ -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) {
@@ -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) {
@@ -268,10 +268,15 @@ function machine(userContext) {
268
268
  setPointerValue(ctx2, evt) {
269
269
  const panels = getHandlePanels(ctx2);
270
270
  const bounds = getHandleBounds(ctx2);
271
- const rootEl = dom.getRootEl(ctx2);
272
- if (!panels || !rootEl || !bounds)
271
+ if (!panels || !bounds)
273
272
  return;
274
- let pointValue = getRelativePointPercent(evt.point, rootEl).normalize(ctx2) * 100;
273
+ const rootEl = dom.getRootEl(ctx2);
274
+ const relativePoint = getRelativePoint(evt.point, rootEl);
275
+ const percentValue = relativePoint.getPercentValue({
276
+ dir: ctx2.dir,
277
+ orientation: ctx2.orientation
278
+ });
279
+ let pointValue = percentValue * 100;
275
280
  ctx2.activeResizeState = {
276
281
  isAtMin: pointValue < bounds.min,
277
282
  isAtMax: pointValue > bounds.max
@@ -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";
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) {
@@ -659,10 +659,15 @@ function machine(userContext) {
659
659
  setPointerValue(ctx2, evt) {
660
660
  const panels = getHandlePanels(ctx2);
661
661
  const bounds = getHandleBounds(ctx2);
662
- const rootEl = dom.getRootEl(ctx2);
663
- if (!panels || !rootEl || !bounds)
662
+ if (!panels || !bounds)
664
663
  return;
665
- let pointValue = (0, import_dom_event2.getRelativePointPercent)(evt.point, rootEl).normalize(ctx2) * 100;
664
+ const rootEl = dom.getRootEl(ctx2);
665
+ const relativePoint = (0, import_dom_event2.getRelativePoint)(evt.point, rootEl);
666
+ const percentValue = relativePoint.getPercentValue({
667
+ dir: ctx2.dir,
668
+ orientation: ctx2.orientation
669
+ });
670
+ let pointValue = percentValue * 100;
666
671
  ctx2.activeResizeState = {
667
672
  isAtMin: pointValue < bounds.min,
668
673
  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-FCVFZHPC.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-64C2WGI5.mjs";
10
+ import "./chunk-3GDOPT5W.mjs";
11
11
  import "./chunk-MV44GBQY.mjs";
12
12
  export {
13
13
  anatomy,
@@ -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) {
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  connect
3
- } from "./chunk-ECKRJG7O.mjs";
3
+ } from "./chunk-FCVFZHPC.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) {
@@ -462,10 +462,15 @@ function machine(userContext) {
462
462
  setPointerValue(ctx2, evt) {
463
463
  const panels = getHandlePanels(ctx2);
464
464
  const bounds = getHandleBounds(ctx2);
465
- const rootEl = dom.getRootEl(ctx2);
466
- if (!panels || !rootEl || !bounds)
465
+ if (!panels || !bounds)
467
466
  return;
468
- let pointValue = (0, import_dom_event.getRelativePointPercent)(evt.point, rootEl).normalize(ctx2) * 100;
467
+ const rootEl = dom.getRootEl(ctx2);
468
+ const relativePoint = (0, import_dom_event.getRelativePoint)(evt.point, rootEl);
469
+ const percentValue = relativePoint.getPercentValue({
470
+ dir: ctx2.dir,
471
+ orientation: ctx2.orientation
472
+ });
473
+ let pointValue = percentValue * 100;
469
474
  ctx2.activeResizeState = {
470
475
  isAtMin: pointValue < bounds.min,
471
476
  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-64C2WGI5.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.1",
3
+ "version": "0.10.0",
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.1",
31
- "@zag-js/core": "0.9.1",
32
- "@zag-js/types": "0.9.1",
33
- "@zag-js/dom-query": "0.9.1",
34
- "@zag-js/dom-event": "0.9.1",
35
- "@zag-js/number-utils": "0.9.1",
36
- "@zag-js/utils": "0.9.1"
31
+ "@zag-js/anatomy": "0.10.0",
32
+ "@zag-js/core": "0.10.0",
33
+ "@zag-js/types": "0.10.0",
34
+ "@zag-js/dom-query": "0.10.0",
35
+ "@zag-js/dom-event": "0.10.0",
36
+ "@zag-js/number-utils": "0.10.0",
37
+ "@zag-js/utils": "0.10.0"
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,198 @@
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
+
14
+ const api = {
15
+ /**
16
+ * Whether the splitter is focused.
17
+ */
18
+ isFocused,
19
+ /**
20
+ * Whether the splitter is being dragged.
21
+ */
22
+ isDragging,
23
+ /**
24
+ * The bounds of the currently dragged splitter handle.
25
+ */
26
+ bounds: getHandleBounds(state.context),
27
+ /**
28
+ * Function to collapse a panel.
29
+ */
30
+ collapse(id: PanelId) {
31
+ send({ type: "COLLAPSE", id })
32
+ },
33
+ /**
34
+ * Function to expand a panel.
35
+ */
36
+ expand(id: PanelId) {
37
+ send({ type: "EXPAND", id })
38
+ },
39
+ /**
40
+ * Function to toggle a panel between collapsed and expanded.
41
+ */
42
+ toggle(id: PanelId) {
43
+ send({ type: "TOGGLE", id })
44
+ },
45
+ /**
46
+ * Function to set the size of a panel.
47
+ */
48
+ setSize(id: PanelId, size: number) {
49
+ send({ type: "SET_SIZE", id, size })
50
+ },
51
+ /**
52
+ * Returns the state details for a resize trigger.
53
+ */
54
+ getResizeTriggerState(props: ResizeTriggerProps) {
55
+ const { id, disabled } = props
56
+ const ids = id.split(":")
57
+ const panelIds = ids.map((id) => dom.getPanelId(state.context, id))
58
+ const panels = getHandleBounds(state.context, id)
59
+
60
+ return {
61
+ isDisabled: !!disabled,
62
+ isFocused: state.context.activeResizeId === id && isFocused,
63
+ panelIds,
64
+ min: panels?.min,
65
+ max: panels?.max,
66
+ value: 0,
67
+ }
68
+ },
69
+
70
+ rootProps: normalize.element({
71
+ ...parts.root.attrs,
72
+ "data-orientation": state.context.orientation,
73
+ id: dom.getRootId(state.context),
74
+ dir: state.context.dir,
75
+ style: {
76
+ display: "flex",
77
+ flexDirection: isHorizontal ? "row" : "column",
78
+ height: "100%",
79
+ width: "100%",
80
+ overflow: "hidden",
81
+ },
82
+ }),
83
+
84
+ getPanelProps(props: PanelProps) {
85
+ const { id } = props
86
+ return normalize.element({
87
+ ...parts.panel.attrs,
88
+ dir: state.context.dir,
89
+ id: dom.getPanelId(state.context, id),
90
+ "data-ownedby": dom.getRootId(state.context),
91
+ style: dom.getPanelStyle(state.context, id),
92
+ })
93
+ },
94
+
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
+
104
+ getResizeTriggerProps(props: ResizeTriggerProps) {
105
+ const { id, disabled, step = 1 } = props
106
+ const triggerState = api.getResizeTriggerState(props)
107
+
108
+ return normalize.element({
109
+ ...parts.resizeTrigger.attrs,
110
+ dir: state.context.dir,
111
+ id: dom.getResizeTriggerId(state.context, id),
112
+ role: "separator",
113
+ "data-ownedby": dom.getRootId(state.context),
114
+ tabIndex: disabled ? undefined : 0,
115
+ "aria-valuenow": triggerState.value,
116
+ "aria-valuemin": triggerState.min,
117
+ "aria-valuemax": triggerState.max,
118
+ "data-orientation": state.context.orientation,
119
+ "aria-orientation": state.context.orientation,
120
+ "aria-controls": triggerState.panelIds.join(" "),
121
+ "data-focus": dataAttr(triggerState.isFocused),
122
+ "data-disabled": dataAttr(disabled),
123
+ style: {
124
+ touchAction: "none",
125
+ userSelect: "none",
126
+ flex: "0 0 auto",
127
+ pointerEvents: isDragging && !triggerState.isFocused ? "none" : undefined,
128
+ cursor: isHorizontal ? "col-resize" : "row-resize",
129
+ [isHorizontal ? "minHeight" : "minWidth"]: "0",
130
+ },
131
+ onPointerDown(event) {
132
+ if (disabled) {
133
+ event.preventDefault()
134
+ return
135
+ }
136
+ send({ type: "POINTER_DOWN", id })
137
+ event.preventDefault()
138
+ event.stopPropagation()
139
+ },
140
+ onPointerOver() {
141
+ if (disabled) return
142
+ send({ type: "POINTER_OVER", id })
143
+ },
144
+ onPointerLeave() {
145
+ if (disabled) return
146
+ send({ type: "POINTER_LEAVE", id })
147
+ },
148
+ onBlur() {
149
+ send("BLUR")
150
+ },
151
+ onFocus() {
152
+ send({ type: "FOCUS", id })
153
+ },
154
+ onDoubleClick() {
155
+ if (disabled) return
156
+ send({ type: "DOUBLE_CLICK", id })
157
+ },
158
+ onKeyDown(event) {
159
+ if (disabled) return
160
+ const moveStep = getEventStep(event) * step
161
+ const keyMap: EventKeyMap = {
162
+ Enter() {
163
+ send("ENTER")
164
+ },
165
+ ArrowUp() {
166
+ send({ type: "ARROW_UP", step: moveStep })
167
+ },
168
+ ArrowDown() {
169
+ send({ type: "ARROW_DOWN", step: moveStep })
170
+ },
171
+ ArrowLeft() {
172
+ send({ type: "ARROW_LEFT", step: moveStep })
173
+ },
174
+ ArrowRight() {
175
+ send({ type: "ARROW_RIGHT", step: moveStep })
176
+ },
177
+ Home() {
178
+ send("HOME")
179
+ },
180
+ End() {
181
+ send("END")
182
+ },
183
+ }
184
+
185
+ const key = getEventKey(event, state.context)
186
+ const exec = keyMap[key]
187
+
188
+ if (exec) {
189
+ exec(event)
190
+ event.preventDefault()
191
+ }
192
+ },
193
+ })
194
+ },
195
+ }
196
+
197
+ return api
198
+ }
@@ -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,294 @@
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
+ },
56
+ states: {
57
+ idle: {
58
+ entry: ["clearActiveHandleId"],
59
+ on: {
60
+ POINTER_OVER: {
61
+ target: "hover:temp",
62
+ actions: ["setActiveHandleId"],
63
+ },
64
+ FOCUS: {
65
+ target: "focused",
66
+ actions: ["setActiveHandleId"],
67
+ },
68
+ DOUBLE_CLICK: {
69
+ actions: ["resetStartPanel", "setPreviousPanels"],
70
+ },
71
+ },
72
+ },
73
+
74
+ "hover:temp": {
75
+ after: {
76
+ HOVER_DELAY: "hover",
77
+ },
78
+ on: {
79
+ POINTER_DOWN: {
80
+ target: "dragging",
81
+ actions: ["setActiveHandleId", "invokeOnResizeStart"],
82
+ },
83
+ POINTER_LEAVE: "idle",
84
+ },
85
+ },
86
+
87
+ hover: {
88
+ tags: ["focus"],
89
+ on: {
90
+ POINTER_DOWN: {
91
+ target: "dragging",
92
+ actions: ["invokeOnResizeStart"],
93
+ },
94
+ POINTER_LEAVE: "idle",
95
+ },
96
+ },
97
+
98
+ focused: {
99
+ tags: ["focus"],
100
+ on: {
101
+ BLUR: "idle",
102
+ POINTER_DOWN: {
103
+ target: "dragging",
104
+ actions: ["setActiveHandleId", "invokeOnResizeStart"],
105
+ },
106
+ ARROW_LEFT: {
107
+ guard: "isHorizontal",
108
+ actions: ["shrinkStartPanel", "setPreviousPanels"],
109
+ },
110
+ ARROW_RIGHT: {
111
+ guard: "isHorizontal",
112
+ actions: ["expandStartPanel", "setPreviousPanels"],
113
+ },
114
+ ARROW_UP: {
115
+ guard: "isVertical",
116
+ actions: ["shrinkStartPanel", "setPreviousPanels"],
117
+ },
118
+ ARROW_DOWN: {
119
+ guard: "isVertical",
120
+ actions: ["expandStartPanel", "setPreviousPanels"],
121
+ },
122
+ ENTER: [
123
+ {
124
+ guard: "isStartPanelAtMax",
125
+ actions: ["setStartPanelToMin", "setPreviousPanels"],
126
+ },
127
+ { actions: ["setStartPanelToMax", "setPreviousPanels"] },
128
+ ],
129
+ HOME: {
130
+ actions: ["setStartPanelToMin", "setPreviousPanels"],
131
+ },
132
+ END: {
133
+ actions: ["setStartPanelToMax", "setPreviousPanels"],
134
+ },
135
+ },
136
+ },
137
+
138
+ dragging: {
139
+ tags: ["focus"],
140
+ entry: "focusResizeHandle",
141
+ activities: ["trackPointerMove"],
142
+ on: {
143
+ POINTER_MOVE: {
144
+ actions: ["setPointerValue", "setGlobalCursor"],
145
+ },
146
+ POINTER_UP: {
147
+ target: "focused",
148
+ actions: ["invokeOnResizeEnd", "setPreviousPanels", "clearGlobalCursor", "blurResizeHandle"],
149
+ },
150
+ },
151
+ },
152
+ },
153
+ },
154
+ {
155
+ activities: {
156
+ trackPointerMove: (ctx, _evt, { send }) => {
157
+ const doc = dom.getDoc(ctx)
158
+ return trackPointerMove(doc, {
159
+ onPointerMove(info) {
160
+ send({ type: "POINTER_MOVE", point: info.point })
161
+ },
162
+ onPointerUp() {
163
+ send("POINTER_UP")
164
+ },
165
+ })
166
+ },
167
+ },
168
+ guards: {
169
+ isStartPanelAtMin: (ctx) => ctx.activeResizeState.isAtMin,
170
+ isStartPanelAtMax: (ctx) => ctx.activeResizeState.isAtMax,
171
+ isHorizontal: (ctx) => ctx.isHorizontal,
172
+ isVertical: (ctx) => !ctx.isHorizontal,
173
+ },
174
+ delays: {
175
+ HOVER_DELAY: 250,
176
+ },
177
+ actions: {
178
+ setGlobalCursor(ctx) {
179
+ dom.setupGlobalCursor(ctx)
180
+ },
181
+ clearGlobalCursor(ctx) {
182
+ dom.removeGlobalCursor(ctx)
183
+ },
184
+ invokeOnResize(ctx) {
185
+ ctx.onResize?.({ size: ctx.size, activeHandleId: ctx.activeResizeId })
186
+ },
187
+ invokeOnResizeStart(ctx) {
188
+ ctx.onResizeStart?.({ size: ctx.size, activeHandleId: ctx.activeResizeId })
189
+ },
190
+ invokeOnResizeEnd(ctx) {
191
+ ctx.onResizeEnd?.({ size: ctx.size, activeHandleId: ctx.activeResizeId })
192
+ },
193
+ setActiveHandleId(ctx, evt) {
194
+ ctx.activeResizeId = evt.id
195
+ },
196
+ clearActiveHandleId(ctx) {
197
+ ctx.activeResizeId = null
198
+ },
199
+ setInitialSize(ctx) {
200
+ ctx.initialSize = ctx.panels.slice().map((panel) => ({
201
+ id: panel.id,
202
+ size: panel.size,
203
+ }))
204
+ },
205
+ setStartPanelToMin(ctx) {
206
+ const bounds = getPanelBounds(ctx)
207
+ if (!bounds) return
208
+ const { before, after } = bounds
209
+ ctx.size[before.index].size = before.min
210
+ ctx.size[after.index].size = after.min
211
+ },
212
+ setStartPanelToMax(ctx) {
213
+ const bounds = getPanelBounds(ctx)
214
+ if (!bounds) return
215
+ const { before, after } = bounds
216
+ ctx.size[before.index].size = before.max
217
+ ctx.size[after.index].size = after.max
218
+ },
219
+ expandStartPanel(ctx, evt) {
220
+ const bounds = getPanelBounds(ctx)
221
+ if (!bounds) return
222
+ const { before, after } = bounds
223
+ ctx.size[before.index].size = before.up(evt.step)
224
+ ctx.size[after.index].size = after.down(evt.step)
225
+ },
226
+ shrinkStartPanel(ctx, evt) {
227
+ const bounds = getPanelBounds(ctx)
228
+ if (!bounds) return
229
+ const { before, after } = bounds
230
+ ctx.size[before.index].size = before.down(evt.step)
231
+ ctx.size[after.index].size = after.up(evt.step)
232
+ },
233
+ resetStartPanel(ctx, evt) {
234
+ const bounds = getPanelBounds(ctx, evt.id)
235
+ if (!bounds) return
236
+ const { before, after } = bounds
237
+ ctx.size[before.index].size = ctx.initialSize[before.index].size
238
+ ctx.size[after.index].size = ctx.initialSize[after.index].size
239
+ },
240
+ focusResizeHandle(ctx) {
241
+ raf(() => {
242
+ dom.getActiveHandleEl(ctx)?.focus({ preventScroll: true })
243
+ })
244
+ },
245
+ blurResizeHandle(ctx) {
246
+ raf(() => {
247
+ dom.getActiveHandleEl(ctx)?.blur()
248
+ })
249
+ },
250
+ setPreviousPanels(ctx) {
251
+ ctx.previousPanels = ctx.panels.slice()
252
+ },
253
+ setActiveResizeState(ctx) {
254
+ const panels = getPanelBounds(ctx)
255
+ if (!panels) return
256
+ const { before } = panels
257
+ ctx.activeResizeState = {
258
+ isAtMin: before.isAtMin,
259
+ isAtMax: before.isAtMax,
260
+ }
261
+ },
262
+ setPointerValue(ctx, evt) {
263
+ const panels = getHandlePanels(ctx)
264
+ const bounds = getHandleBounds(ctx)
265
+
266
+ if (!panels || !bounds) return
267
+
268
+ const rootEl = dom.getRootEl(ctx)
269
+ const relativePoint = getRelativePoint(evt.point, rootEl)
270
+ const percentValue = relativePoint.getPercentValue({
271
+ dir: ctx.dir,
272
+ orientation: ctx.orientation,
273
+ })
274
+
275
+ let pointValue = percentValue * 100
276
+
277
+ // update active resize state here because we use `previousPanels` in the calculations
278
+ ctx.activeResizeState = {
279
+ isAtMin: pointValue < bounds.min,
280
+ isAtMax: pointValue > bounds.max,
281
+ }
282
+
283
+ pointValue = clamp(pointValue, bounds.min, bounds.max)
284
+
285
+ const { before, after } = panels
286
+
287
+ const offset = pointValue - before.end
288
+ ctx.size[before.index].size = before.size + offset
289
+ ctx.size[after.index].size = after.size - offset
290
+ },
291
+ },
292
+ },
293
+ )
294
+ }
@@ -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
+ }