@zag-js/slider 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.
@@ -3,8 +3,8 @@ import {
3
3
  } from "./chunk-IJAAAKZQ.mjs";
4
4
 
5
5
  // src/slider.dom.ts
6
+ import { getRelativePoint } from "@zag-js/dom-event";
6
7
  import { createScope } from "@zag-js/dom-query";
7
- import { getRelativePointValue } from "@zag-js/dom-event";
8
8
  import { dispatchInputValueEvent } from "@zag-js/form-utils";
9
9
  import { getPercentValue } from "@zag-js/numeric-range";
10
10
  var dom = createScope({
@@ -20,21 +20,15 @@ var dom = createScope({
20
20
  getMarkerId: (ctx, value) => `slider:${ctx.id}:marker:${value}`,
21
21
  getRootEl: (ctx) => dom.getById(ctx, dom.getRootId(ctx)),
22
22
  getThumbEl: (ctx) => dom.getById(ctx, dom.getThumbId(ctx)),
23
- getControlEl: (ctx) => dom.getById(ctx, dom.getControlId(ctx)),
23
+ getControlEl: (ctx) => dom.queryById(ctx, dom.getControlId(ctx)),
24
24
  getHiddenInputEl: (ctx) => dom.getById(ctx, dom.getHiddenInputId(ctx)),
25
25
  getValueFromPoint(ctx, point) {
26
- const el = dom.getControlEl(ctx);
27
- if (!el)
28
- return;
29
- const relativePoint = getRelativePointValue(point, el);
30
- const percentX = relativePoint.x / el.offsetWidth;
31
- const percentY = relativePoint.y / el.offsetHeight;
32
- let percent;
33
- if (ctx.isHorizontal) {
34
- percent = ctx.isRtl ? 1 - percentX : percentX;
35
- } else {
36
- percent = 1 - percentY;
37
- }
26
+ const relativePoint = getRelativePoint(point, dom.getControlEl(ctx));
27
+ const percent = relativePoint.getPercentValue({
28
+ orientation: ctx.orientation,
29
+ dir: ctx.dir,
30
+ inverted: { y: true }
31
+ });
38
32
  return getPercentValue(percent, ctx.min, ctx.max, ctx.step);
39
33
  },
40
34
  dispatchChangeEvent(ctx) {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  dom
3
- } from "./chunk-JG4OWFJP.mjs";
3
+ } from "./chunk-NYN3VIY4.mjs";
4
4
  import {
5
5
  constrainValue,
6
6
  decrement,
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-3Y7IIPR5.mjs";
4
4
  import {
5
5
  dom
6
- } from "./chunk-JG4OWFJP.mjs";
6
+ } from "./chunk-NYN3VIY4.mjs";
7
7
 
8
8
  // src/slider.connect.ts
9
9
  import {
package/dist/index.js CHANGED
@@ -49,8 +49,8 @@ var import_dom_query2 = require("@zag-js/dom-query");
49
49
  var import_numeric_range3 = require("@zag-js/numeric-range");
50
50
 
51
51
  // src/slider.dom.ts
52
- var import_dom_query = require("@zag-js/dom-query");
53
52
  var import_dom_event = require("@zag-js/dom-event");
53
+ var import_dom_query = require("@zag-js/dom-query");
54
54
  var import_form_utils = require("@zag-js/form-utils");
55
55
  var import_numeric_range2 = require("@zag-js/numeric-range");
56
56
 
@@ -180,21 +180,15 @@ var dom = (0, import_dom_query.createScope)({
180
180
  getMarkerId: (ctx, value) => `slider:${ctx.id}:marker:${value}`,
181
181
  getRootEl: (ctx) => dom.getById(ctx, dom.getRootId(ctx)),
182
182
  getThumbEl: (ctx) => dom.getById(ctx, dom.getThumbId(ctx)),
183
- getControlEl: (ctx) => dom.getById(ctx, dom.getControlId(ctx)),
183
+ getControlEl: (ctx) => dom.queryById(ctx, dom.getControlId(ctx)),
184
184
  getHiddenInputEl: (ctx) => dom.getById(ctx, dom.getHiddenInputId(ctx)),
185
185
  getValueFromPoint(ctx, point) {
186
- const el = dom.getControlEl(ctx);
187
- if (!el)
188
- return;
189
- const relativePoint = (0, import_dom_event.getRelativePointValue)(point, el);
190
- const percentX = relativePoint.x / el.offsetWidth;
191
- const percentY = relativePoint.y / el.offsetHeight;
192
- let percent;
193
- if (ctx.isHorizontal) {
194
- percent = ctx.isRtl ? 1 - percentX : percentX;
195
- } else {
196
- percent = 1 - percentY;
197
- }
186
+ const relativePoint = (0, import_dom_event.getRelativePoint)(point, dom.getControlEl(ctx));
187
+ const percent = relativePoint.getPercentValue({
188
+ orientation: ctx.orientation,
189
+ dir: ctx.dir,
190
+ inverted: { y: true }
191
+ });
198
192
  return (0, import_numeric_range2.getPercentValue)(percent, ctx.min, ctx.max, ctx.step);
199
193
  },
200
194
  dispatchChangeEvent(ctx) {
package/dist/index.mjs CHANGED
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  connect
3
- } from "./chunk-TMLAO23S.mjs";
3
+ } from "./chunk-T6GXNUP7.mjs";
4
4
  import {
5
5
  anatomy
6
6
  } from "./chunk-3Y7IIPR5.mjs";
7
7
  import {
8
8
  machine
9
- } from "./chunk-CNR2557J.mjs";
9
+ } from "./chunk-OCT2YBMN.mjs";
10
10
  import {
11
11
  dom
12
- } from "./chunk-JG4OWFJP.mjs";
12
+ } from "./chunk-NYN3VIY4.mjs";
13
13
  import "./chunk-IJAAAKZQ.mjs";
14
14
  import "./chunk-YGFSMEUO.mjs";
15
15
  export {
@@ -44,8 +44,8 @@ var anatomy = (0, import_anatomy.createAnatomy)("slider").parts(
44
44
  var parts = anatomy.build();
45
45
 
46
46
  // src/slider.dom.ts
47
- var import_dom_query = require("@zag-js/dom-query");
48
47
  var import_dom_event = require("@zag-js/dom-event");
48
+ var import_dom_query = require("@zag-js/dom-query");
49
49
  var import_form_utils = require("@zag-js/form-utils");
50
50
  var import_numeric_range2 = require("@zag-js/numeric-range");
51
51
 
@@ -175,21 +175,15 @@ var dom = (0, import_dom_query.createScope)({
175
175
  getMarkerId: (ctx, value) => `slider:${ctx.id}:marker:${value}`,
176
176
  getRootEl: (ctx) => dom.getById(ctx, dom.getRootId(ctx)),
177
177
  getThumbEl: (ctx) => dom.getById(ctx, dom.getThumbId(ctx)),
178
- getControlEl: (ctx) => dom.getById(ctx, dom.getControlId(ctx)),
178
+ getControlEl: (ctx) => dom.queryById(ctx, dom.getControlId(ctx)),
179
179
  getHiddenInputEl: (ctx) => dom.getById(ctx, dom.getHiddenInputId(ctx)),
180
180
  getValueFromPoint(ctx, point) {
181
- const el = dom.getControlEl(ctx);
182
- if (!el)
183
- return;
184
- const relativePoint = (0, import_dom_event.getRelativePointValue)(point, el);
185
- const percentX = relativePoint.x / el.offsetWidth;
186
- const percentY = relativePoint.y / el.offsetHeight;
187
- let percent;
188
- if (ctx.isHorizontal) {
189
- percent = ctx.isRtl ? 1 - percentX : percentX;
190
- } else {
191
- percent = 1 - percentY;
192
- }
181
+ const relativePoint = (0, import_dom_event.getRelativePoint)(point, dom.getControlEl(ctx));
182
+ const percent = relativePoint.getPercentValue({
183
+ orientation: ctx.orientation,
184
+ dir: ctx.dir,
185
+ inverted: { y: true }
186
+ });
193
187
  return (0, import_numeric_range2.getPercentValue)(percent, ctx.min, ctx.max, ctx.step);
194
188
  },
195
189
  dispatchChangeEvent(ctx) {
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  connect
3
- } from "./chunk-TMLAO23S.mjs";
3
+ } from "./chunk-T6GXNUP7.mjs";
4
4
  import "./chunk-3Y7IIPR5.mjs";
5
- import "./chunk-JG4OWFJP.mjs";
5
+ import "./chunk-NYN3VIY4.mjs";
6
6
  import "./chunk-IJAAAKZQ.mjs";
7
7
  export {
8
8
  connect
@@ -33,7 +33,7 @@ declare const dom: {
33
33
  getMarkerId: (ctx: MachineContext, value: number) => string;
34
34
  getRootEl: (ctx: MachineContext) => HTMLElement | null;
35
35
  getThumbEl: (ctx: MachineContext) => HTMLElement | null;
36
- getControlEl: (ctx: MachineContext) => HTMLElement | null;
36
+ getControlEl: (ctx: MachineContext) => HTMLElement;
37
37
  getHiddenInputEl: (ctx: MachineContext) => HTMLInputElement | null;
38
38
  getValueFromPoint(ctx: MachineContext, point: Point): number | undefined;
39
39
  dispatchChangeEvent(ctx: MachineContext): void;
@@ -23,8 +23,8 @@ __export(slider_dom_exports, {
23
23
  dom: () => dom
24
24
  });
25
25
  module.exports = __toCommonJS(slider_dom_exports);
26
- var import_dom_query = require("@zag-js/dom-query");
27
26
  var import_dom_event = require("@zag-js/dom-event");
27
+ var import_dom_query = require("@zag-js/dom-query");
28
28
  var import_form_utils = require("@zag-js/form-utils");
29
29
  var import_numeric_range2 = require("@zag-js/numeric-range");
30
30
 
@@ -154,21 +154,15 @@ var dom = (0, import_dom_query.createScope)({
154
154
  getMarkerId: (ctx, value) => `slider:${ctx.id}:marker:${value}`,
155
155
  getRootEl: (ctx) => dom.getById(ctx, dom.getRootId(ctx)),
156
156
  getThumbEl: (ctx) => dom.getById(ctx, dom.getThumbId(ctx)),
157
- getControlEl: (ctx) => dom.getById(ctx, dom.getControlId(ctx)),
157
+ getControlEl: (ctx) => dom.queryById(ctx, dom.getControlId(ctx)),
158
158
  getHiddenInputEl: (ctx) => dom.getById(ctx, dom.getHiddenInputId(ctx)),
159
159
  getValueFromPoint(ctx, point) {
160
- const el = dom.getControlEl(ctx);
161
- if (!el)
162
- return;
163
- const relativePoint = (0, import_dom_event.getRelativePointValue)(point, el);
164
- const percentX = relativePoint.x / el.offsetWidth;
165
- const percentY = relativePoint.y / el.offsetHeight;
166
- let percent;
167
- if (ctx.isHorizontal) {
168
- percent = ctx.isRtl ? 1 - percentX : percentX;
169
- } else {
170
- percent = 1 - percentY;
171
- }
160
+ const relativePoint = (0, import_dom_event.getRelativePoint)(point, dom.getControlEl(ctx));
161
+ const percent = relativePoint.getPercentValue({
162
+ orientation: ctx.orientation,
163
+ dir: ctx.dir,
164
+ inverted: { y: true }
165
+ });
172
166
  return (0, import_numeric_range2.getPercentValue)(percent, ctx.min, ctx.max, ctx.step);
173
167
  },
174
168
  dispatchChangeEvent(ctx) {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  dom
3
- } from "./chunk-JG4OWFJP.mjs";
3
+ } from "./chunk-NYN3VIY4.mjs";
4
4
  import "./chunk-IJAAAKZQ.mjs";
5
5
  export {
6
6
  dom
@@ -32,8 +32,8 @@ var import_numeric_range4 = require("@zag-js/numeric-range");
32
32
  var import_utils = require("@zag-js/utils");
33
33
 
34
34
  // src/slider.dom.ts
35
- var import_dom_query = require("@zag-js/dom-query");
36
35
  var import_dom_event = require("@zag-js/dom-event");
36
+ var import_dom_query = require("@zag-js/dom-query");
37
37
  var import_form_utils = require("@zag-js/form-utils");
38
38
  var import_numeric_range2 = require("@zag-js/numeric-range");
39
39
 
@@ -163,21 +163,15 @@ var dom = (0, import_dom_query.createScope)({
163
163
  getMarkerId: (ctx, value) => `slider:${ctx.id}:marker:${value}`,
164
164
  getRootEl: (ctx) => dom.getById(ctx, dom.getRootId(ctx)),
165
165
  getThumbEl: (ctx) => dom.getById(ctx, dom.getThumbId(ctx)),
166
- getControlEl: (ctx) => dom.getById(ctx, dom.getControlId(ctx)),
166
+ getControlEl: (ctx) => dom.queryById(ctx, dom.getControlId(ctx)),
167
167
  getHiddenInputEl: (ctx) => dom.getById(ctx, dom.getHiddenInputId(ctx)),
168
168
  getValueFromPoint(ctx, point) {
169
- const el = dom.getControlEl(ctx);
170
- if (!el)
171
- return;
172
- const relativePoint = (0, import_dom_event.getRelativePointValue)(point, el);
173
- const percentX = relativePoint.x / el.offsetWidth;
174
- const percentY = relativePoint.y / el.offsetHeight;
175
- let percent;
176
- if (ctx.isHorizontal) {
177
- percent = ctx.isRtl ? 1 - percentX : percentX;
178
- } else {
179
- percent = 1 - percentY;
180
- }
169
+ const relativePoint = (0, import_dom_event.getRelativePoint)(point, dom.getControlEl(ctx));
170
+ const percent = relativePoint.getPercentValue({
171
+ orientation: ctx.orientation,
172
+ dir: ctx.dir,
173
+ inverted: { y: true }
174
+ });
181
175
  return (0, import_numeric_range2.getPercentValue)(percent, ctx.min, ctx.max, ctx.step);
182
176
  },
183
177
  dispatchChangeEvent(ctx) {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  machine
3
- } from "./chunk-CNR2557J.mjs";
4
- import "./chunk-JG4OWFJP.mjs";
3
+ } from "./chunk-OCT2YBMN.mjs";
4
+ import "./chunk-NYN3VIY4.mjs";
5
5
  import "./chunk-IJAAAKZQ.mjs";
6
6
  import "./chunk-YGFSMEUO.mjs";
7
7
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zag-js/slider",
3
- "version": "0.9.2",
3
+ "version": "0.10.1",
4
4
  "description": "Core logic for the slider widget implemented as a state machine",
5
5
  "keywords": [
6
6
  "js",
@@ -17,7 +17,8 @@
17
17
  "repository": "https://github.com/chakra-ui/zag/tree/main/packages/slider",
18
18
  "sideEffects": false,
19
19
  "files": [
20
- "dist/**/*"
20
+ "dist",
21
+ "src"
21
22
  ],
22
23
  "publishConfig": {
23
24
  "access": "public"
@@ -26,15 +27,15 @@
26
27
  "url": "https://github.com/chakra-ui/zag/issues"
27
28
  },
28
29
  "dependencies": {
29
- "@zag-js/anatomy": "0.9.2",
30
- "@zag-js/core": "0.9.2",
31
- "@zag-js/dom-query": "0.9.2",
32
- "@zag-js/dom-event": "0.9.2",
33
- "@zag-js/form-utils": "0.9.2",
34
- "@zag-js/utils": "0.9.2",
35
- "@zag-js/element-size": "0.9.2",
36
- "@zag-js/numeric-range": "0.9.2",
37
- "@zag-js/types": "0.9.2"
30
+ "@zag-js/anatomy": "0.10.1",
31
+ "@zag-js/core": "0.10.1",
32
+ "@zag-js/dom-query": "0.10.1",
33
+ "@zag-js/dom-event": "0.10.1",
34
+ "@zag-js/form-utils": "0.10.1",
35
+ "@zag-js/utils": "0.10.1",
36
+ "@zag-js/element-size": "0.10.1",
37
+ "@zag-js/numeric-range": "0.10.1",
38
+ "@zag-js/types": "0.10.1"
38
39
  },
39
40
  "devDependencies": {
40
41
  "clean-package": "2.2.0"
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export { anatomy } from "./slider.anatomy"
2
+ export { connect } from "./slider.connect"
3
+ export { dom as unstable__dom } from "./slider.dom"
4
+ export { machine } from "./slider.machine"
5
+ export type { UserDefinedContext as Context } from "./slider.types"
@@ -0,0 +1,15 @@
1
+ import { createAnatomy } from "@zag-js/anatomy"
2
+
3
+ export const anatomy = createAnatomy("slider").parts(
4
+ "root",
5
+ "label",
6
+ "thumb",
7
+ "hiddenInput",
8
+ "output",
9
+ "track",
10
+ "range",
11
+ "control",
12
+ "markerGroup",
13
+ "marker",
14
+ )
15
+ export const parts = anatomy.build()
@@ -0,0 +1,276 @@
1
+ import {
2
+ EventKeyMap,
3
+ getEventKey,
4
+ getEventPoint,
5
+ getEventStep,
6
+ getNativeEvent,
7
+ isLeftClick,
8
+ isModifiedEvent,
9
+ } from "@zag-js/dom-event"
10
+ import { ariaAttr, dataAttr } from "@zag-js/dom-query"
11
+ import { getPercentValue, getValuePercent } from "@zag-js/numeric-range"
12
+ import type { NormalizeProps, PropTypes } from "@zag-js/types"
13
+ import { parts } from "./slider.anatomy"
14
+ import { dom } from "./slider.dom"
15
+ import type { Send, State } from "./slider.types"
16
+
17
+ export function connect<T extends PropTypes>(state: State, send: Send, normalize: NormalizeProps<T>) {
18
+ const ariaLabel = state.context["aria-label"]
19
+ const ariaLabelledBy = state.context["aria-labelledby"]
20
+ const ariaValueText = state.context.getAriaValueText?.(state.context.value)
21
+
22
+ const isFocused = state.matches("focus")
23
+ const isDragging = state.matches("dragging")
24
+ const isDisabled = state.context.disabled
25
+ const isInteractive = state.context.isInteractive
26
+ const isInvalid = state.context.invalid
27
+
28
+ function getPercentValueFn(percent: number) {
29
+ return getPercentValue(percent, state.context.min, state.context.max, state.context.step)
30
+ }
31
+
32
+ function getValuePercentFn(value: number) {
33
+ return getValuePercent(value, state.context.min, state.context.max)
34
+ }
35
+
36
+ // TODO - getMarkerState
37
+
38
+ return {
39
+ /**
40
+ * Whether the slider is focused.
41
+ */
42
+ isFocused,
43
+ /**
44
+ * Whether the slider is being dragged.
45
+ */
46
+ isDragging,
47
+ /**
48
+ * The value of the slider.
49
+ */
50
+ value: state.context.value,
51
+ /**
52
+ * The value of the slider as a percent.
53
+ */
54
+ percent: getValuePercent(state.context.value, state.context.min, state.context.max),
55
+ /**
56
+ * Function to set the value of the slider.
57
+ */
58
+ setValue(value: number) {
59
+ send({ type: "SET_VALUE", value })
60
+ },
61
+ /**
62
+ * Returns the value of the slider at the given percent.
63
+ */
64
+ getPercentValue: getPercentValueFn,
65
+ /**
66
+ * Returns the percent of the slider at the given value.
67
+ */
68
+ getValuePercent: getValuePercentFn,
69
+ /**
70
+ * Function to focus the slider.
71
+ */
72
+ focus() {
73
+ dom.getThumbEl(state.context)?.focus()
74
+ },
75
+ /**
76
+ * Function to increment the value of the slider by the step.
77
+ */
78
+ increment() {
79
+ send("INCREMENT")
80
+ },
81
+ /**
82
+ * Function to decrement the value of the slider by the step.
83
+ */
84
+ decrement() {
85
+ send("DECREMENT")
86
+ },
87
+
88
+ rootProps: normalize.element({
89
+ ...parts.root.attrs,
90
+ "data-disabled": dataAttr(isDisabled),
91
+ "data-focus": dataAttr(isFocused),
92
+ "data-orientation": state.context.orientation,
93
+ "data-invalid": dataAttr(isInvalid),
94
+ id: dom.getRootId(state.context),
95
+ dir: state.context.dir,
96
+ style: dom.getRootStyle(state.context),
97
+ }),
98
+
99
+ labelProps: normalize.label({
100
+ ...parts.label.attrs,
101
+ "data-disabled": dataAttr(isDisabled),
102
+ "data-invalid": dataAttr(isInvalid),
103
+ "data-focus": dataAttr(isFocused),
104
+ id: dom.getLabelId(state.context),
105
+ htmlFor: dom.getHiddenInputId(state.context),
106
+ onClick(event) {
107
+ if (!isInteractive) return
108
+ event.preventDefault()
109
+ dom.getThumbEl(state.context)?.focus()
110
+ },
111
+ style: dom.getLabelStyle(),
112
+ }),
113
+
114
+ thumbProps: normalize.element({
115
+ ...parts.thumb.attrs,
116
+ id: dom.getThumbId(state.context),
117
+ "data-disabled": dataAttr(isDisabled),
118
+ "data-orientation": state.context.orientation,
119
+ "data-focus": dataAttr(isFocused),
120
+ draggable: false,
121
+ "aria-invalid": ariaAttr(isInvalid),
122
+ "data-invalid": dataAttr(isInvalid),
123
+ "aria-disabled": ariaAttr(isDisabled),
124
+ "aria-label": ariaLabel,
125
+ "aria-labelledby": ariaLabel ? undefined : ariaLabelledBy ?? dom.getLabelId(state.context),
126
+ "aria-orientation": state.context.orientation,
127
+ "aria-valuemax": state.context.max,
128
+ "aria-valuemin": state.context.min,
129
+ "aria-valuenow": state.context.value,
130
+ "aria-valuetext": ariaValueText,
131
+ role: "slider",
132
+ tabIndex: isDisabled ? undefined : 0,
133
+ onBlur() {
134
+ if (!isInteractive) return
135
+ send("BLUR")
136
+ },
137
+ onFocus() {
138
+ if (!isInteractive) return
139
+ send("FOCUS")
140
+ },
141
+ onKeyDown(event) {
142
+ if (!isInteractive) return
143
+ const step = getEventStep(event) * state.context.step
144
+ let prevent = true
145
+ const keyMap: EventKeyMap = {
146
+ ArrowUp() {
147
+ send({ type: "ARROW_UP", step })
148
+ prevent = state.context.isVertical
149
+ },
150
+ ArrowDown() {
151
+ send({ type: "ARROW_DOWN", step })
152
+ prevent = state.context.isVertical
153
+ },
154
+ ArrowLeft() {
155
+ send({ type: "ARROW_LEFT", step })
156
+ prevent = state.context.isHorizontal
157
+ },
158
+ ArrowRight() {
159
+ send({ type: "ARROW_RIGHT", step })
160
+ prevent = state.context.isHorizontal
161
+ },
162
+ PageUp() {
163
+ send({ type: "PAGE_UP", step })
164
+ },
165
+ PageDown() {
166
+ send({ type: "PAGE_DOWN", step })
167
+ },
168
+ Home() {
169
+ send("HOME")
170
+ },
171
+ End() {
172
+ send("END")
173
+ },
174
+ }
175
+
176
+ const key = getEventKey(event, state.context)
177
+ const exec = keyMap[key]
178
+
179
+ if (!exec) return
180
+ exec(event)
181
+
182
+ if (prevent) {
183
+ event.preventDefault()
184
+ }
185
+ },
186
+ style: dom.getThumbStyle(state.context),
187
+ }),
188
+
189
+ hiddenInputProps: normalize.input({
190
+ ...parts.hiddenInput.attrs,
191
+ type: "text",
192
+ defaultValue: state.context.value,
193
+ name: state.context.name,
194
+ form: state.context.form,
195
+ id: dom.getHiddenInputId(state.context),
196
+ hidden: true,
197
+ }),
198
+
199
+ outputProps: normalize.output({
200
+ ...parts.output.attrs,
201
+ "data-disabled": dataAttr(isDisabled),
202
+ "data-invalid": dataAttr(isInvalid),
203
+ id: dom.getOutputId(state.context),
204
+ htmlFor: dom.getHiddenInputId(state.context),
205
+ "aria-live": "off",
206
+ }),
207
+
208
+ trackProps: normalize.element({
209
+ ...parts.track.attrs,
210
+ id: dom.getTrackId(state.context),
211
+ "data-disabled": dataAttr(isDisabled),
212
+ "data-focus": dataAttr(isFocused),
213
+ "data-invalid": dataAttr(isInvalid),
214
+ "data-orientation": state.context.orientation,
215
+ style: dom.getTrackStyle(),
216
+ }),
217
+
218
+ rangeProps: normalize.element({
219
+ ...parts.range.attrs,
220
+ id: dom.getRangeId(state.context),
221
+ "data-focus": dataAttr(isFocused),
222
+ "data-invalid": dataAttr(isInvalid),
223
+ "data-disabled": dataAttr(isDisabled),
224
+ "data-orientation": state.context.orientation,
225
+ style: dom.getRangeStyle(state.context),
226
+ }),
227
+
228
+ controlProps: normalize.element({
229
+ ...parts.control.attrs,
230
+ id: dom.getControlId(state.context),
231
+ "data-disabled": dataAttr(isDisabled),
232
+ "data-invalid": dataAttr(isInvalid),
233
+ "data-orientation": state.context.orientation,
234
+ "data-focus": dataAttr(isFocused),
235
+ onPointerDown(event) {
236
+ if (!isInteractive) return
237
+
238
+ const evt = getNativeEvent(event)
239
+ if (!isLeftClick(evt) || isModifiedEvent(evt)) return
240
+
241
+ const point = getEventPoint(evt)
242
+ send({ type: "POINTER_DOWN", point })
243
+
244
+ event.preventDefault()
245
+ event.stopPropagation()
246
+ },
247
+ style: dom.getControlStyle(),
248
+ }),
249
+
250
+ markerGroupProps: normalize.element({
251
+ ...parts.markerGroup.attrs,
252
+ role: "presentation",
253
+ "aria-hidden": true,
254
+ "data-orientation": state.context.orientation,
255
+ style: dom.getMarkerGroupStyle(),
256
+ }),
257
+
258
+ getMarkerProps({ value }: { value: number }) {
259
+ const percent = getValuePercentFn(value)
260
+ const style = dom.getMarkerStyle(state.context, percent)
261
+ const markerState =
262
+ value > state.context.value ? "over-value" : value < state.context.value ? "under-value" : "at-value"
263
+
264
+ return normalize.element({
265
+ ...parts.marker.attrs,
266
+ id: dom.getMarkerId(state.context, value),
267
+ role: "presentation",
268
+ "data-orientation": state.context.orientation,
269
+ "data-value": value,
270
+ "data-disabled": dataAttr(isDisabled),
271
+ "data-state": markerState,
272
+ style,
273
+ })
274
+ },
275
+ }
276
+ }
@@ -0,0 +1,41 @@
1
+ import { getRelativePoint } from "@zag-js/dom-event"
2
+ import { createScope } from "@zag-js/dom-query"
3
+ import { dispatchInputValueEvent } from "@zag-js/form-utils"
4
+ import { getPercentValue } from "@zag-js/numeric-range"
5
+ import { styles } from "./slider.style"
6
+ import type { MachineContext as Ctx, Point } from "./slider.types"
7
+
8
+ export const dom = createScope({
9
+ ...styles,
10
+
11
+ getRootId: (ctx: Ctx) => ctx.ids?.root ?? `slider:${ctx.id}`,
12
+ getThumbId: (ctx: Ctx) => ctx.ids?.thumb ?? `slider:${ctx.id}:thumb`,
13
+ getControlId: (ctx: Ctx) => ctx.ids?.control ?? `slider:${ctx.id}:control`,
14
+ getHiddenInputId: (ctx: Ctx) => ctx.ids?.hiddenInput ?? `slider:${ctx.id}:input`,
15
+ getOutputId: (ctx: Ctx) => ctx.ids?.output ?? `slider:${ctx.id}:output`,
16
+ getTrackId: (ctx: Ctx) => ctx.ids?.track ?? `slider:${ctx.id}track`,
17
+ getRangeId: (ctx: Ctx) => ctx.ids?.track ?? `slider:${ctx.id}:range`,
18
+ getLabelId: (ctx: Ctx) => ctx.ids?.label ?? `slider:${ctx.id}:label`,
19
+ getMarkerId: (ctx: Ctx, value: number) => `slider:${ctx.id}:marker:${value}`,
20
+
21
+ getRootEl: (ctx: Ctx) => dom.getById(ctx, dom.getRootId(ctx)),
22
+ getThumbEl: (ctx: Ctx) => dom.getById(ctx, dom.getThumbId(ctx)),
23
+ getControlEl: (ctx: Ctx) => dom.queryById(ctx, dom.getControlId(ctx)),
24
+ getHiddenInputEl: (ctx: Ctx) => dom.getById<HTMLInputElement>(ctx, dom.getHiddenInputId(ctx)),
25
+
26
+ getValueFromPoint(ctx: Ctx, point: Point): number | undefined {
27
+ const relativePoint = getRelativePoint(point, dom.getControlEl(ctx))
28
+ const percent = relativePoint.getPercentValue({
29
+ orientation: ctx.orientation,
30
+ dir: ctx.dir,
31
+ inverted: { y: true },
32
+ })
33
+ return getPercentValue(percent, ctx.min, ctx.max, ctx.step)
34
+ },
35
+
36
+ dispatchChangeEvent(ctx: Ctx) {
37
+ const input = dom.getHiddenInputEl(ctx)
38
+ if (!input) return
39
+ dispatchInputValueEvent(input, { value: ctx.value })
40
+ },
41
+ })
@@ -0,0 +1,210 @@
1
+ import { createMachine } from "@zag-js/core"
2
+ import { trackPointerMove } from "@zag-js/dom-event"
3
+ import { raf } from "@zag-js/dom-query"
4
+ import { trackElementSize } from "@zag-js/element-size"
5
+ import { trackFormControl } from "@zag-js/form-utils"
6
+ import { clampValue, getValuePercent } from "@zag-js/numeric-range"
7
+ import { compact } from "@zag-js/utils"
8
+ import { dom } from "./slider.dom"
9
+ import type { MachineContext, MachineState, UserDefinedContext } from "./slider.types"
10
+ import { constrainValue, decrement, increment } from "./slider.utils"
11
+
12
+ export function machine(userContext: UserDefinedContext) {
13
+ const ctx = compact(userContext)
14
+ return createMachine<MachineContext, MachineState>(
15
+ {
16
+ id: "slider",
17
+ initial: "idle",
18
+ context: {
19
+ thumbSize: null,
20
+ thumbAlignment: "contain",
21
+ disabled: false,
22
+ threshold: 5,
23
+ dir: "ltr",
24
+ origin: "start",
25
+ orientation: "horizontal",
26
+ initialValue: null,
27
+ value: 0,
28
+ step: 1,
29
+ min: 0,
30
+ max: 100,
31
+ ...ctx,
32
+ },
33
+
34
+ computed: {
35
+ isHorizontal: (ctx) => ctx.orientation === "horizontal",
36
+ isVertical: (ctx) => ctx.orientation === "vertical",
37
+ isRtl: (ctx) => ctx.orientation === "horizontal" && ctx.dir === "rtl",
38
+ isInteractive: (ctx) => !(ctx.disabled || ctx.readOnly),
39
+ hasMeasuredThumbSize: (ctx) => ctx.thumbSize !== null,
40
+ valuePercent: (ctx) => 100 * getValuePercent(ctx.value, ctx.min, ctx.max),
41
+ },
42
+
43
+ watch: {
44
+ value: ["invokeOnChange", "dispatchChangeEvent"],
45
+ },
46
+
47
+ activities: ["trackFormControlState", "trackThumbSize"],
48
+
49
+ on: {
50
+ SET_VALUE: {
51
+ actions: "setValue",
52
+ },
53
+ INCREMENT: {
54
+ actions: "increment",
55
+ },
56
+ DECREMENT: {
57
+ actions: "decrement",
58
+ },
59
+ },
60
+
61
+ entry: ["checkValue"],
62
+
63
+ states: {
64
+ idle: {
65
+ on: {
66
+ POINTER_DOWN: {
67
+ target: "dragging",
68
+ actions: ["setPointerValue", "invokeOnChangeStart", "focusThumb"],
69
+ },
70
+ FOCUS: "focus",
71
+ },
72
+ },
73
+
74
+ focus: {
75
+ entry: "focusThumb",
76
+ on: {
77
+ POINTER_DOWN: {
78
+ target: "dragging",
79
+ actions: ["setPointerValue", "invokeOnChangeStart", "focusThumb"],
80
+ },
81
+ ARROW_LEFT: {
82
+ guard: "isHorizontal",
83
+ actions: "decrement",
84
+ },
85
+ ARROW_RIGHT: {
86
+ guard: "isHorizontal",
87
+ actions: "increment",
88
+ },
89
+ ARROW_UP: {
90
+ guard: "isVertical",
91
+ actions: "increment",
92
+ },
93
+ ARROW_DOWN: {
94
+ guard: "isVertical",
95
+ actions: "decrement",
96
+ },
97
+ PAGE_UP: {
98
+ actions: "increment",
99
+ },
100
+ PAGE_DOWN: {
101
+ actions: "decrement",
102
+ },
103
+ HOME: {
104
+ actions: "setToMin",
105
+ },
106
+ END: {
107
+ actions: "setToMax",
108
+ },
109
+ BLUR: "idle",
110
+ },
111
+ },
112
+
113
+ dragging: {
114
+ entry: "focusThumb",
115
+ activities: "trackPointerMove",
116
+ on: {
117
+ POINTER_UP: {
118
+ target: "focus",
119
+ actions: "invokeOnChangeEnd",
120
+ },
121
+ POINTER_MOVE: {
122
+ actions: "setPointerValue",
123
+ },
124
+ },
125
+ },
126
+ },
127
+ },
128
+ {
129
+ guards: {
130
+ isHorizontal: (ctx) => ctx.isHorizontal,
131
+ isVertical: (ctx) => ctx.isVertical,
132
+ },
133
+
134
+ activities: {
135
+ trackFormControlState(ctx) {
136
+ return trackFormControl(dom.getHiddenInputEl(ctx), {
137
+ onFieldsetDisabled() {
138
+ ctx.disabled = true
139
+ },
140
+ onFormReset() {
141
+ if (ctx.initialValue != null) {
142
+ ctx.value = ctx.initialValue
143
+ }
144
+ },
145
+ })
146
+ },
147
+
148
+ trackPointerMove(ctx, _evt, { send }) {
149
+ return trackPointerMove(dom.getDoc(ctx), {
150
+ onPointerMove(info) {
151
+ send({ type: "POINTER_MOVE", point: info.point })
152
+ },
153
+ onPointerUp() {
154
+ send("POINTER_UP")
155
+ },
156
+ })
157
+ },
158
+ trackThumbSize(ctx, _evt) {
159
+ if (ctx.thumbAlignment !== "contain") return
160
+ return trackElementSize(dom.getThumbEl(ctx), (size) => {
161
+ if (size) ctx.thumbSize = size
162
+ })
163
+ },
164
+ },
165
+
166
+ actions: {
167
+ checkValue(ctx) {
168
+ const value = constrainValue(ctx, ctx.value)
169
+ ctx.value = value
170
+ ctx.initialValue = value
171
+ },
172
+ invokeOnChangeStart(ctx) {
173
+ ctx.onChangeStart?.({ value: ctx.value })
174
+ },
175
+ invokeOnChangeEnd(ctx) {
176
+ ctx.onChangeEnd?.({ value: ctx.value })
177
+ },
178
+ invokeOnChange(ctx) {
179
+ ctx.onChange?.({ value: ctx.value })
180
+ },
181
+ dispatchChangeEvent(ctx) {
182
+ dom.dispatchChangeEvent(ctx)
183
+ },
184
+ setPointerValue(ctx, evt) {
185
+ const value = dom.getValueFromPoint(ctx, evt.point)
186
+ if (value == null) return
187
+ ctx.value = clampValue(value, ctx.min, ctx.max)
188
+ },
189
+ focusThumb(ctx) {
190
+ raf(() => dom.getThumbEl(ctx)?.focus())
191
+ },
192
+ decrement(ctx, evt) {
193
+ ctx.value = decrement(ctx, evt.step)
194
+ },
195
+ increment(ctx, evt) {
196
+ ctx.value = increment(ctx, evt.step)
197
+ },
198
+ setToMin(ctx) {
199
+ ctx.value = ctx.min
200
+ },
201
+ setToMax(ctx) {
202
+ ctx.value = ctx.max
203
+ },
204
+ setValue(ctx, evt) {
205
+ ctx.value = constrainValue(ctx, evt.value)
206
+ },
207
+ },
208
+ },
209
+ )
210
+ }
@@ -0,0 +1,165 @@
1
+ import { getValuePercent, getValueTransformer } from "@zag-js/numeric-range"
2
+ import type { Style } from "@zag-js/types"
3
+ import type { MachineContext as Ctx, SharedContext } from "./slider.types"
4
+
5
+ /* -----------------------------------------------------------------------------
6
+ * Thumb style calculations
7
+ * -----------------------------------------------------------------------------*/
8
+
9
+ function getVerticalThumbOffset(ctx: SharedContext) {
10
+ const { height = 0 } = ctx.thumbSize ?? {}
11
+ const getValue = getValueTransformer([ctx.min, ctx.max], [-height / 2, height / 2])
12
+ return parseFloat(getValue(ctx.value).toFixed(2))
13
+ }
14
+
15
+ function getHorizontalThumbOffset(ctx: SharedContext) {
16
+ const { width = 0 } = ctx.thumbSize ?? {}
17
+
18
+ if (ctx.isRtl) {
19
+ const getValue = getValueTransformer([ctx.max, ctx.min], [-width * 1.5, -width / 2])
20
+ return -1 * parseFloat(getValue(ctx.value).toFixed(2))
21
+ }
22
+
23
+ const getValue = getValueTransformer([ctx.min, ctx.max], [-width / 2, width / 2])
24
+ return parseFloat(getValue(ctx.value).toFixed(2))
25
+ }
26
+
27
+ function getThumbOffset(ctx: SharedContext) {
28
+ const percent = getValuePercent(ctx.value, ctx.min, ctx.max) * 100
29
+
30
+ if (ctx.thumbAlignment === "center") {
31
+ return `${percent}%`
32
+ }
33
+
34
+ const offset = ctx.isVertical ? getVerticalThumbOffset(ctx) : getHorizontalThumbOffset(ctx)
35
+ return `calc(${percent}% - ${offset}px)`
36
+ }
37
+
38
+ function getVisibility(ctx: SharedContext) {
39
+ let visibility: "visible" | "hidden" = "visible"
40
+ if (ctx.thumbAlignment === "contain" && !ctx.hasMeasuredThumbSize) {
41
+ visibility = "hidden"
42
+ }
43
+ return visibility
44
+ }
45
+
46
+ function getThumbStyle(ctx: SharedContext): Style {
47
+ const placementProp = ctx.isVertical ? "bottom" : ctx.isRtl ? "right" : "left"
48
+ return {
49
+ visibility: getVisibility(ctx),
50
+ position: "absolute",
51
+ transform: "var(--slider-thumb-transform)",
52
+ [placementProp]: "var(--slider-thumb-offset)",
53
+ }
54
+ }
55
+
56
+ /* -----------------------------------------------------------------------------
57
+ * Range style calculations
58
+ * -----------------------------------------------------------------------------*/
59
+
60
+ function getRangeOffsets(ctx: Ctx) {
61
+ let start = "0%"
62
+ let end = `${100 - ctx.valuePercent}%`
63
+
64
+ if (ctx.origin === "center") {
65
+ const isNegative = ctx.valuePercent < 50
66
+ start = isNegative ? `${ctx.valuePercent}%` : "50%"
67
+ end = isNegative ? "50%" : end
68
+ }
69
+
70
+ return { start, end }
71
+ }
72
+
73
+ function getRangeStyle(ctx: Pick<SharedContext, "isVertical" | "isRtl">): Style {
74
+ if (ctx.isVertical) {
75
+ return {
76
+ position: "absolute",
77
+ bottom: "var(--slider-range-start)",
78
+ top: "var(--slider-range-end)",
79
+ }
80
+ }
81
+
82
+ return {
83
+ position: "absolute",
84
+ [ctx.isRtl ? "right" : "left"]: "var(--slider-range-start)",
85
+ [ctx.isRtl ? "left" : "right"]: "var(--slider-range-end)",
86
+ }
87
+ }
88
+
89
+ /* -----------------------------------------------------------------------------
90
+ * Control style calculations
91
+ * -----------------------------------------------------------------------------*/
92
+
93
+ function getControlStyle(): Style {
94
+ return {
95
+ touchAction: "none",
96
+ userSelect: "none",
97
+ position: "relative",
98
+ }
99
+ }
100
+
101
+ /* -----------------------------------------------------------------------------
102
+ * Root style calculations
103
+ * -----------------------------------------------------------------------------*/
104
+
105
+ function getRootStyle(ctx: Ctx): Style {
106
+ const range = getRangeOffsets(ctx)
107
+ return {
108
+ "--slider-thumb-transform": ctx.isVertical ? "translateY(50%)" : "translateX(-50%)",
109
+ "--slider-thumb-offset": getThumbOffset(ctx),
110
+ "--slider-range-start": range.start,
111
+ "--slider-range-end": range.end,
112
+ }
113
+ }
114
+
115
+ /* -----------------------------------------------------------------------------
116
+ * Marker style calculations
117
+ * -----------------------------------------------------------------------------*/
118
+
119
+ function getMarkerStyle(ctx: Pick<SharedContext, "isHorizontal" | "isRtl">, percent: number): Style {
120
+ return {
121
+ position: "absolute",
122
+ pointerEvents: "none",
123
+ [ctx.isHorizontal ? "left" : "bottom"]: `${(ctx.isRtl ? 1 - percent : percent) * 100}%`,
124
+ }
125
+ }
126
+
127
+ /* -----------------------------------------------------------------------------
128
+ * Label style calculations
129
+ * -----------------------------------------------------------------------------*/
130
+
131
+ function getLabelStyle(): Style {
132
+ return { userSelect: "none" }
133
+ }
134
+
135
+ /* -----------------------------------------------------------------------------
136
+ * Label style calculations
137
+ * -----------------------------------------------------------------------------*/
138
+
139
+ function getTrackStyle(): Style {
140
+ return { position: "relative" }
141
+ }
142
+
143
+ /* -----------------------------------------------------------------------------
144
+ * Label style calculations
145
+ * -----------------------------------------------------------------------------*/
146
+
147
+ function getMarkerGroupStyle(): Style {
148
+ return {
149
+ userSelect: "none",
150
+ pointerEvents: "none",
151
+ position: "relative",
152
+ }
153
+ }
154
+
155
+ export const styles = {
156
+ getThumbOffset,
157
+ getControlStyle,
158
+ getThumbStyle,
159
+ getRangeStyle,
160
+ getRootStyle,
161
+ getMarkerStyle,
162
+ getLabelStyle,
163
+ getTrackStyle,
164
+ getMarkerGroupStyle,
165
+ }
@@ -0,0 +1,182 @@
1
+ import type { StateMachine as S } from "@zag-js/core"
2
+ import type { CommonProperties, Context, DirectionProperty, RequiredBy } from "@zag-js/types"
3
+
4
+ type ElementIds = Partial<{
5
+ root: string
6
+ thumb: string
7
+ control: string
8
+ track: string
9
+ range: string
10
+ label: string
11
+ output: string
12
+ hiddenInput: string
13
+ }>
14
+
15
+ type PublicContext = DirectionProperty &
16
+ CommonProperties & {
17
+ /**
18
+ * The ids of the elements in the slider. Useful for composition.
19
+ */
20
+ ids?: ElementIds
21
+ /**
22
+ * The value of the slider
23
+ */
24
+ value: number
25
+ /**
26
+ * The name associated with the slider (when used in a form)
27
+ */
28
+ name?: string
29
+ /**
30
+ * The associate form of the underlying input element.
31
+ */
32
+ form?: string
33
+ /**
34
+ * Whether the slider is disabled
35
+ */
36
+ disabled?: boolean
37
+ /**
38
+ * Whether the slider is read-only
39
+ */
40
+ readOnly?: boolean
41
+ /**
42
+ * Whether the slider value is invalid
43
+ */
44
+ invalid?: boolean
45
+ /**
46
+ * The minimum value of the slider
47
+ */
48
+ min: number
49
+ /**
50
+ * The maximum value of the slider
51
+ */
52
+ max: number
53
+ /**
54
+ * The step value of the slider
55
+ */
56
+ step: number
57
+ /**
58
+ * The orientation of the slider
59
+ */
60
+ orientation?: "vertical" | "horizontal"
61
+ /**
62
+ * - "start": Useful when the value represents an absolute value
63
+ * - "center": Useful when the value represents an offset (relative)
64
+ */
65
+ origin?: "start" | "center"
66
+ /**
67
+ * The aria-label of the slider. Useful for providing an accessible name to the slider
68
+ */
69
+ "aria-label"?: string
70
+ /**
71
+ * The `id` of the element that labels the slider. Useful for providing an accessible name to the slider
72
+ */
73
+ "aria-labelledby"?: string
74
+ /**
75
+ * Whether to focus the slider thumb after interaction (scrub and keyboard)
76
+ */
77
+ focusThumbOnChange?: boolean
78
+ /**
79
+ * Function that returns a human readable value for the slider
80
+ */
81
+ getAriaValueText?(value: number): string
82
+ /**
83
+ * Function invoked when the value of the slider changes
84
+ */
85
+ onChange?(details: { value: number }): void
86
+ /**
87
+ * Function invoked when the slider value change is done
88
+ */
89
+ onChangeEnd?(details: { value: number }): void
90
+ /**
91
+ * Function invoked when the slider value change is started
92
+ */
93
+ onChangeStart?(details: { value: number }): void
94
+ /**
95
+ * The alignment of the slider thumb relative to the track
96
+ * - `center`: the thumb will extend beyond the bounds of the slider track.
97
+ * - `contain`: the thumb will be contained within the bounds of the track.
98
+ */
99
+ thumbAlignment?: "contain" | "center"
100
+ }
101
+
102
+ export type UserDefinedContext = RequiredBy<PublicContext, "id">
103
+
104
+ type ComputedContext = Readonly<{
105
+ /**
106
+ * @computed
107
+ * Whether the slider is interactive
108
+ */
109
+ readonly isInteractive: boolean
110
+ /**
111
+ * @computed
112
+ * Whether the thumb size has been measured
113
+ */
114
+ readonly hasMeasuredThumbSize: boolean
115
+ /**
116
+ * @computed
117
+ * Whether the slider is horizontal
118
+ */
119
+ readonly isHorizontal: boolean
120
+ /**
121
+ * @computed
122
+ * Whether the slider is vertical
123
+ */
124
+ readonly isVertical: boolean
125
+ /**
126
+ * @computed
127
+ * Whether the slider is in RTL mode
128
+ */
129
+ readonly isRtl: boolean
130
+ /**
131
+ * @computed
132
+ * The value of the slider as a percentage
133
+ */
134
+ readonly valuePercent: number
135
+ }>
136
+
137
+ type PrivateContext = Context<{
138
+ /**
139
+ * @internal The move threshold of the slider thumb before it is considered to be moved
140
+ */
141
+ threshold: number
142
+ /**
143
+ * @internal The slider thumb dimensions
144
+ */
145
+ thumbSize: { width: number; height: number } | null
146
+ /**
147
+ * @internal
148
+ * The value of the slider when it was initially rendered.
149
+ * Used when the `form.reset(...)` is called.
150
+ */
151
+ initialValue: number | null
152
+ }>
153
+
154
+ export type MachineContext = PublicContext & ComputedContext & PrivateContext
155
+
156
+ export type MachineState = {
157
+ value: "idle" | "dragging" | "focus"
158
+ }
159
+
160
+ export type State = S.State<MachineContext, MachineState>
161
+
162
+ export type Send = S.Send<S.AnyEventObject>
163
+
164
+ export type SharedContext = {
165
+ min: number
166
+ max: number
167
+ step: number
168
+ dir?: "ltr" | "rtl"
169
+ isRtl: boolean
170
+ isVertical: boolean
171
+ isHorizontal: boolean
172
+ value: number
173
+ thumbSize: { width: number; height: number } | null
174
+ thumbAlignment?: "contain" | "center"
175
+ orientation?: "horizontal" | "vertical"
176
+ readonly hasMeasuredThumbSize: boolean
177
+ }
178
+
179
+ export type Point = {
180
+ x: number
181
+ y: number
182
+ }
@@ -0,0 +1,31 @@
1
+ import { clampValue, getNextStepValue, getPreviousStepValue, snapValueToStep } from "@zag-js/numeric-range"
2
+ import type { MachineContext as Ctx } from "./slider.types"
3
+
4
+ export function clampPercent(percent: number) {
5
+ return clampValue(percent, 0, 1)
6
+ }
7
+
8
+ export function constrainValue(ctx: Ctx, value: number) {
9
+ const snapValue = snapValueToStep(value, ctx.min, ctx.max, ctx.step)
10
+ return clampValue(snapValue, ctx.min, ctx.max)
11
+ }
12
+
13
+ export function decrement(ctx: Ctx, step?: number) {
14
+ const index = 0
15
+ const values = getPreviousStepValue(index, {
16
+ ...ctx,
17
+ step: step ?? ctx.step,
18
+ values: [ctx.value],
19
+ })
20
+ return values[index]
21
+ }
22
+
23
+ export function increment(ctx: Ctx, step?: number) {
24
+ const index = 0
25
+ const values = getNextStepValue(index, {
26
+ ...ctx,
27
+ step: step ?? ctx.step,
28
+ values: [ctx.value],
29
+ })
30
+ return values[index]
31
+ }