aeon-ui-engine 1.3.1 → 1.4.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.
Files changed (153) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/package.json +1 -1
  3. package/packages/cli/README.md +13 -1
  4. package/packages/cli/dist/bin.js +23 -5
  5. package/packages/cli/dist/chunk-5L47SLNV.js +247 -0
  6. package/packages/cli/dist/index.d.ts +17 -1
  7. package/packages/cli/dist/index.js +5 -3
  8. package/packages/cli/src/bin.ts +21 -3
  9. package/packages/cli/src/index.ts +1 -0
  10. package/packages/cli/src/migrate.ts +170 -0
  11. package/packages/core/dist/index.d.ts +35 -7
  12. package/packages/core/dist/index.js +102 -2
  13. package/packages/core/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  14. package/packages/core/src/anatomy.ts +39 -3
  15. package/packages/core/src/identicon.test.ts +39 -0
  16. package/packages/core/src/identicon.ts +79 -0
  17. package/packages/core/src/index.ts +1 -0
  18. package/packages/panda/panda.config.ts +4 -0
  19. package/packages/panda/src/recipes/composer.ts +58 -4
  20. package/packages/panda/src/recipes/file-upload.ts +100 -0
  21. package/packages/panda/src/recipes/index.ts +2 -0
  22. package/packages/panda/src/recipes/range-slider.ts +59 -0
  23. package/packages/panda/src/recipes/thread.ts +77 -2
  24. package/packages/panda/styled-system/recipes/aeon-composer.d.ts +1 -1
  25. package/packages/panda/styled-system/recipes/aeon-composer.mjs +12 -0
  26. package/packages/panda/styled-system/recipes/aeon-file-upload.d.ts +31 -0
  27. package/packages/panda/styled-system/recipes/aeon-file-upload.mjs +61 -0
  28. package/packages/panda/styled-system/recipes/aeon-range-slider.d.ts +31 -0
  29. package/packages/panda/styled-system/recipes/aeon-range-slider.mjs +49 -0
  30. package/packages/panda/styled-system/recipes/aeon-thread.d.ts +1 -1
  31. package/packages/panda/styled-system/recipes/aeon-thread.mjs +20 -0
  32. package/packages/panda/styled-system/recipes/index.d.ts +2 -0
  33. package/packages/panda/styled-system/recipes/index.mjs +2 -0
  34. package/packages/panda/styled-system/styles.css +292 -0
  35. package/packages/primitives/dist/chunk-CV4UXXZF.js +44 -0
  36. package/packages/primitives/dist/chunk-IZKQSCYU.js +164 -0
  37. package/packages/primitives/dist/index.d.ts +2 -0
  38. package/packages/primitives/dist/index.js +22 -6
  39. package/packages/primitives/dist/machines/file-upload.d.ts +39 -0
  40. package/packages/primitives/dist/machines/file-upload.js +8 -0
  41. package/packages/primitives/dist/machines/range-slider.d.ts +129 -0
  42. package/packages/primitives/dist/machines/range-slider.js +12 -0
  43. package/packages/primitives/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  44. package/packages/primitives/src/__tests__/file-upload.test.ts +25 -0
  45. package/packages/primitives/src/__tests__/range-slider.test.ts +39 -0
  46. package/packages/primitives/src/index.ts +2 -0
  47. package/packages/primitives/src/machines/file-upload.ts +55 -0
  48. package/packages/primitives/src/machines/range-slider.ts +209 -0
  49. package/packages/react/dist/index.d.ts +117 -25
  50. package/packages/react/dist/index.js +1731 -1006
  51. package/packages/react/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  52. package/packages/react/src/button/index.tsx +50 -15
  53. package/packages/react/src/combobox/index.tsx +13 -8
  54. package/packages/react/src/composer/index.tsx +58 -3
  55. package/packages/react/src/file-upload/index.tsx +322 -0
  56. package/packages/react/src/index.ts +3 -0
  57. package/packages/react/src/prompt/index.tsx +45 -0
  58. package/packages/react/src/range-slider/index.tsx +322 -0
  59. package/packages/react/src/thread/index.tsx +74 -3
  60. package/packages/react/src/tooltip/index.tsx +48 -22
  61. package/packages/react/src/utils/slot.tsx +47 -0
  62. package/packages/schemas/dist/index.d.ts +73 -24
  63. package/packages/schemas/dist/index.js +514 -50
  64. package/packages/schemas/json/appNav.json +146 -0
  65. package/packages/schemas/json/appShell.json +151 -0
  66. package/packages/schemas/json/avatar.json +130 -0
  67. package/packages/schemas/json/bar.json +126 -0
  68. package/packages/schemas/json/button.json +6 -0
  69. package/packages/schemas/json/catalog.json +3496 -15
  70. package/packages/schemas/json/combobox.json +19 -7
  71. package/packages/schemas/json/composer.json +125 -0
  72. package/packages/schemas/json/content.json +167 -0
  73. package/packages/schemas/json/conversation.json +166 -0
  74. package/packages/schemas/json/entry.json +182 -0
  75. package/packages/schemas/json/fileUpload.json +168 -0
  76. package/packages/schemas/json/identity.json +147 -0
  77. package/packages/schemas/json/listRow.json +131 -0
  78. package/packages/schemas/json/metricStrip.json +132 -0
  79. package/packages/schemas/json/nav.json +136 -0
  80. package/packages/schemas/json/panel.json +143 -0
  81. package/packages/schemas/json/pinInput.json +112 -0
  82. package/packages/schemas/json/profileHeader.json +138 -0
  83. package/packages/schemas/json/prompt.json +155 -0
  84. package/packages/schemas/json/radioGroup.json +130 -0
  85. package/packages/schemas/json/rangeSlider.json +157 -0
  86. package/packages/schemas/json/separator.json +105 -0
  87. package/packages/schemas/json/slider.json +147 -0
  88. package/packages/schemas/json/statusBanner.json +149 -0
  89. package/packages/schemas/json/stickyBar.json +153 -0
  90. package/packages/schemas/json/thread.json +129 -0
  91. package/packages/schemas/json/tooltip.json +21 -7
  92. package/packages/schemas/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  93. package/packages/schemas/prompts/cursorrules.md +6 -4
  94. package/packages/schemas/prompts/statecharts.md +23 -2
  95. package/packages/schemas/prompts/system.md +2 -2
  96. package/packages/schemas/registry/catalog.json +313 -4
  97. package/packages/schemas/registry/components/appNav.json +146 -0
  98. package/packages/schemas/registry/components/appShell.json +151 -0
  99. package/packages/schemas/registry/components/avatar.json +130 -0
  100. package/packages/schemas/registry/components/bar.json +126 -0
  101. package/packages/schemas/registry/components/button.json +6 -0
  102. package/packages/schemas/registry/components/combobox.json +19 -7
  103. package/packages/schemas/registry/components/composer.json +125 -0
  104. package/packages/schemas/registry/components/content.json +167 -0
  105. package/packages/schemas/registry/components/conversation.json +166 -0
  106. package/packages/schemas/registry/components/entry.json +182 -0
  107. package/packages/schemas/registry/components/fileUpload.json +168 -0
  108. package/packages/schemas/registry/components/identity.json +147 -0
  109. package/packages/schemas/registry/components/listRow.json +131 -0
  110. package/packages/schemas/registry/components/metricStrip.json +132 -0
  111. package/packages/schemas/registry/components/nav.json +136 -0
  112. package/packages/schemas/registry/components/panel.json +143 -0
  113. package/packages/schemas/registry/components/pinInput.json +112 -0
  114. package/packages/schemas/registry/components/profileHeader.json +138 -0
  115. package/packages/schemas/registry/components/prompt.json +155 -0
  116. package/packages/schemas/registry/components/radioGroup.json +130 -0
  117. package/packages/schemas/registry/components/rangeSlider.json +157 -0
  118. package/packages/schemas/registry/components/separator.json +105 -0
  119. package/packages/schemas/registry/components/slider.json +147 -0
  120. package/packages/schemas/registry/components/statusBanner.json +149 -0
  121. package/packages/schemas/registry/components/stickyBar.json +153 -0
  122. package/packages/schemas/registry/components/thread.json +129 -0
  123. package/packages/schemas/registry/components/tooltip.json +21 -7
  124. package/packages/schemas/registry/machines/accordion.json +58 -0
  125. package/packages/schemas/registry/machines/appNav.json +108 -0
  126. package/packages/schemas/registry/machines/appShell.json +128 -0
  127. package/packages/schemas/registry/machines/contentRegion.json +314 -0
  128. package/packages/schemas/registry/machines/fileUpload.json +182 -0
  129. package/packages/schemas/registry/machines/panel.json +131 -0
  130. package/packages/schemas/registry/machines/popover.json +143 -0
  131. package/packages/schemas/registry/machines/rangeSlider.json +201 -0
  132. package/packages/schemas/registry/machines/slider.json +151 -0
  133. package/packages/schemas/registry/machines/stickyBar.json +261 -0
  134. package/packages/schemas/registry/machines/tabs.json +58 -0
  135. package/packages/schemas/registry/machines/toast.json +89 -0
  136. package/packages/schemas/registry/machines/toggle.json +134 -0
  137. package/packages/schemas/scripts/emit.ts +27 -0
  138. package/packages/schemas/src/components.ts +499 -6
  139. package/packages/schemas/src/index.test.ts +83 -0
  140. package/packages/schemas/src/index.ts +1 -0
  141. package/packages/schemas/src/machine-id.ts +7 -0
  142. package/packages/schemas/src/prompts.ts +25 -4
  143. package/packages/schemas/src/registry.ts +28 -39
  144. package/packages/schemas/tsconfig.json +1 -1
  145. package/packages/ui/dist/index.d.ts +100 -46
  146. package/packages/ui/dist/index.js +463 -341
  147. package/packages/ui/src/composer.tsx +37 -11
  148. package/packages/ui/src/file-upload.tsx +36 -0
  149. package/packages/ui/src/index.ts +2 -0
  150. package/packages/ui/src/prompt.tsx +3 -0
  151. package/packages/ui/src/range-slider.tsx +24 -0
  152. package/packages/ui/src/thread.tsx +40 -0
  153. package/packages/cli/dist/chunk-IW5NT2Z2.js +0 -103
@@ -0,0 +1,129 @@
1
+ import * as xstate from 'xstate';
2
+
3
+ interface RangeSliderContext {
4
+ values: [number, number];
5
+ min: number;
6
+ max: number;
7
+ step: number;
8
+ minStepsBetween: number;
9
+ disabled: boolean;
10
+ activeThumb: 0 | 1 | null;
11
+ }
12
+ type RangeSliderEvent = {
13
+ type: 'SET_VALUES';
14
+ values: [number, number];
15
+ } | {
16
+ type: 'SET_BOUNDS';
17
+ min: number;
18
+ max: number;
19
+ step: number;
20
+ minStepsBetween: number;
21
+ } | {
22
+ type: 'SET_DISABLED';
23
+ disabled: boolean;
24
+ } | {
25
+ type: 'POINTER_DOWN';
26
+ thumb: 0 | 1;
27
+ } | {
28
+ type: 'POINTER_MOVE';
29
+ value: number;
30
+ } | {
31
+ type: 'POINTER_UP';
32
+ } | {
33
+ type: 'STEP';
34
+ thumb: 0 | 1;
35
+ delta: number;
36
+ } | {
37
+ type: 'HOME';
38
+ thumb: 0 | 1;
39
+ } | {
40
+ type: 'END';
41
+ thumb: 0 | 1;
42
+ };
43
+ declare function clamp(value: number, min: number, max: number): number;
44
+ declare function snap(value: number, min: number, max: number, step: number): number;
45
+ declare function normalizePair(raw: [number, number], min: number, max: number, step: number, minStepsBetween: number): [number, number];
46
+ type RangeSliderInput = {
47
+ values: [number, number];
48
+ min: number;
49
+ max: number;
50
+ step: number;
51
+ minStepsBetween?: number;
52
+ disabled?: boolean;
53
+ };
54
+ /** Two-thumb range control — idle vs dragging with activeThumb in context. */
55
+ declare const rangeSliderMachine: xstate.StateMachine<RangeSliderContext, {
56
+ type: "SET_VALUES";
57
+ values: [number, number];
58
+ } | {
59
+ type: "SET_BOUNDS";
60
+ min: number;
61
+ max: number;
62
+ step: number;
63
+ minStepsBetween: number;
64
+ } | {
65
+ type: "SET_DISABLED";
66
+ disabled: boolean;
67
+ } | {
68
+ type: "POINTER_DOWN";
69
+ thumb: 0 | 1;
70
+ } | {
71
+ type: "POINTER_MOVE";
72
+ value: number;
73
+ } | {
74
+ type: "POINTER_UP";
75
+ } | {
76
+ type: "STEP";
77
+ thumb: 0 | 1;
78
+ delta: number;
79
+ } | {
80
+ type: "HOME";
81
+ thumb: 0 | 1;
82
+ } | {
83
+ type: "END";
84
+ thumb: 0 | 1;
85
+ }, {}, never, {
86
+ type: "setValues";
87
+ params: xstate.NonReducibleUnknown;
88
+ } | {
89
+ type: "setBounds";
90
+ params: xstate.NonReducibleUnknown;
91
+ } | {
92
+ type: "setDisabled";
93
+ params: xstate.NonReducibleUnknown;
94
+ } | {
95
+ type: "pointerDown";
96
+ params: xstate.NonReducibleUnknown;
97
+ } | {
98
+ type: "pointerMove";
99
+ params: xstate.NonReducibleUnknown;
100
+ } | {
101
+ type: "pointerUp";
102
+ params: xstate.NonReducibleUnknown;
103
+ } | {
104
+ type: "stepThumb";
105
+ params: xstate.NonReducibleUnknown;
106
+ } | {
107
+ type: "homeThumb";
108
+ params: xstate.NonReducibleUnknown;
109
+ } | {
110
+ type: "endThumb";
111
+ params: xstate.NonReducibleUnknown;
112
+ }, {
113
+ type: "notDisabled";
114
+ params: unknown;
115
+ }, never, {
116
+ interaction: "idle" | "dragging";
117
+ }, string, RangeSliderInput, xstate.NonReducibleUnknown, xstate.EventObject, xstate.MetaObject, {
118
+ id: "rangeSlider";
119
+ states: {
120
+ readonly interaction: {
121
+ states: {
122
+ readonly idle: {};
123
+ readonly dragging: {};
124
+ };
125
+ };
126
+ };
127
+ }>;
128
+
129
+ export { type RangeSliderContext, type RangeSliderEvent, type RangeSliderInput, clamp as clampRangeSliderValue, normalizePair as normalizeRangeSliderValues, rangeSliderMachine, snap as snapRangeSliderValue };
@@ -0,0 +1,12 @@
1
+ import {
2
+ clamp,
3
+ normalizePair,
4
+ rangeSliderMachine,
5
+ snap
6
+ } from "../chunk-IZKQSCYU.js";
7
+ export {
8
+ clamp as clampRangeSliderValue,
9
+ normalizePair as normalizeRangeSliderValues,
10
+ rangeSliderMachine,
11
+ snap as snapRangeSliderValue
12
+ };
@@ -1 +1 @@
1
- {"version":"3.2.4","results":[[":src/machines/dialog.test.ts",{"duration":5.771292000000017,"failed":false}],[":src/machines/select.test.ts",{"duration":6.394416000000035,"failed":false}],[":src/__tests__/dialog.test.ts",{"duration":3.686000000000007,"failed":false}],[":src/__tests__/select.test.ts",{"duration":2.8540000000000134,"failed":false}],[":src/__tests__/floating-surface.test.ts",{"duration":6.529208000000011,"failed":false}],[":src/__tests__/app-nav.test.ts",{"duration":7.017959000000019,"failed":false}]]}
1
+ {"version":"3.2.4","results":[[":src/machines/dialog.test.ts",{"duration":5.771292000000017,"failed":false}],[":src/machines/select.test.ts",{"duration":6.394416000000035,"failed":false}],[":src/__tests__/dialog.test.ts",{"duration":3.686000000000007,"failed":false}],[":src/__tests__/select.test.ts",{"duration":2.8540000000000134,"failed":false}],[":src/__tests__/floating-surface.test.ts",{"duration":5.357416999999998,"failed":false}],[":src/__tests__/app-nav.test.ts",{"duration":7.012541999999996,"failed":false}],[":src/__tests__/file-upload.test.ts",{"duration":5.016666000000015,"failed":false}],[":src/__tests__/range-slider.test.ts",{"duration":6.245291999999978,"failed":false}]]}
@@ -0,0 +1,25 @@
1
+ import { createActor } from 'xstate'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { fileUploadMachine, getFileUploadPhase } from '../machines/file-upload.js'
4
+
5
+ describe('fileUploadMachine', () => {
6
+ it('tracks drag → accept → idle', () => {
7
+ const actor = createActor(fileUploadMachine).start()
8
+ expect(getFileUploadPhase(actor.getSnapshot().value)).toBe('idle')
9
+
10
+ actor.send({ type: 'DRAG_OVER' })
11
+ expect(getFileUploadPhase(actor.getSnapshot().value)).toBe('dragging')
12
+
13
+ actor.send({ type: 'DROP_ACCEPT' })
14
+ expect(getFileUploadPhase(actor.getSnapshot().value)).toBe('idle')
15
+ })
16
+
17
+ it('rejects then resets', () => {
18
+ const actor = createActor(fileUploadMachine).start()
19
+ actor.send({ type: 'DROP_REJECT' })
20
+ expect(getFileUploadPhase(actor.getSnapshot().value)).toBe('rejected')
21
+
22
+ actor.send({ type: 'RESET' })
23
+ expect(getFileUploadPhase(actor.getSnapshot().value)).toBe('idle')
24
+ })
25
+ })
@@ -0,0 +1,39 @@
1
+ import { createActor } from 'xstate'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { rangeSliderMachine } from '../machines/range-slider.js'
4
+
5
+ describe('rangeSliderMachine', () => {
6
+ it('normalizes and enforces minStepsBetween on SET_VALUES', () => {
7
+ const actor = createActor(rangeSliderMachine, {
8
+ input: { values: [10, 90], min: 0, max: 100, step: 1, minStepsBetween: 10 },
9
+ }).start()
10
+
11
+ actor.send({ type: 'SET_VALUES', values: [50, 55] })
12
+ expect(actor.getSnapshot().context.values).toEqual([50, 60])
13
+ })
14
+
15
+ it('moves start thumb with gap constraint while dragging', () => {
16
+ const actor = createActor(rangeSliderMachine, {
17
+ input: { values: [20, 80], min: 0, max: 100, step: 1, minStepsBetween: 5 },
18
+ }).start()
19
+
20
+ actor.send({ type: 'POINTER_DOWN', thumb: 0 })
21
+ actor.send({ type: 'POINTER_MOVE', value: 90 })
22
+ expect(actor.getSnapshot().context.values).toEqual([75, 80])
23
+ expect(actor.getSnapshot().matches({ interaction: 'dragging' })).toBe(true)
24
+
25
+ actor.send({ type: 'POINTER_UP' })
26
+ expect(actor.getSnapshot().matches({ interaction: 'idle' })).toBe(true)
27
+ expect(actor.getSnapshot().context.activeThumb).toBeNull()
28
+ })
29
+
30
+ it('ignores interaction when disabled', () => {
31
+ const actor = createActor(rangeSliderMachine, {
32
+ input: { values: [10, 40], min: 0, max: 100, step: 1, disabled: true },
33
+ }).start()
34
+
35
+ actor.send({ type: 'POINTER_DOWN', thumb: 1 })
36
+ expect(actor.getSnapshot().matches({ interaction: 'idle' })).toBe(true)
37
+ expect(actor.getSnapshot().context.values).toEqual([10, 40])
38
+ })
39
+ })
@@ -9,6 +9,8 @@ export * from './machines/field.js'
9
9
  export * from './machines/popover.js'
10
10
  export * from './machines/toast.js'
11
11
  export * from './machines/slider.js'
12
+ export * from './machines/range-slider.js'
13
+ export * from './machines/file-upload.js'
12
14
  export * from './machines/app-shell.js'
13
15
  export * from './machines/app-nav.js'
14
16
  export * from './machines/sticky-bar.js'
@@ -0,0 +1,55 @@
1
+ import { setup } from 'xstate'
2
+
3
+ export type FileUploadPhase = 'idle' | 'dragging' | 'rejected'
4
+
5
+ export type FileUploadEvent =
6
+ | { type: 'DRAG_OVER' }
7
+ | { type: 'DRAG_LEAVE' }
8
+ | { type: 'DROP_ACCEPT' }
9
+ | { type: 'DROP_REJECT' }
10
+ | { type: 'RESET' }
11
+
12
+ /**
13
+ * Drag-and-drop surface lifecycle. Selected file bytes stay in React
14
+ * (or parent callbacks); idle / dragging / rejected are chart-owned.
15
+ */
16
+ export const fileUploadMachine = setup({
17
+ types: {} as {
18
+ events: FileUploadEvent
19
+ },
20
+ }).createMachine({
21
+ id: 'fileUpload',
22
+ initial: 'idle',
23
+ states: {
24
+ idle: {
25
+ on: {
26
+ DRAG_OVER: 'dragging',
27
+ DROP_REJECT: 'rejected',
28
+ RESET: 'idle',
29
+ },
30
+ },
31
+ dragging: {
32
+ on: {
33
+ DRAG_OVER: 'dragging',
34
+ DRAG_LEAVE: 'idle',
35
+ DROP_ACCEPT: 'idle',
36
+ DROP_REJECT: 'rejected',
37
+ },
38
+ },
39
+ rejected: {
40
+ on: {
41
+ DRAG_OVER: 'dragging',
42
+ DROP_ACCEPT: 'idle',
43
+ DROP_REJECT: 'rejected',
44
+ RESET: 'idle',
45
+ },
46
+ },
47
+ },
48
+ })
49
+
50
+ export function getFileUploadPhase(value: unknown): FileUploadPhase {
51
+ if (value === 'idle' || value === 'dragging' || value === 'rejected') {
52
+ return value
53
+ }
54
+ return 'idle'
55
+ }
@@ -0,0 +1,209 @@
1
+ import { assign, setup } from 'xstate'
2
+
3
+ export interface RangeSliderContext {
4
+ values: [number, number]
5
+ min: number
6
+ max: number
7
+ step: number
8
+ minStepsBetween: number
9
+ disabled: boolean
10
+ activeThumb: 0 | 1 | null
11
+ }
12
+
13
+ export type RangeSliderEvent =
14
+ | { type: 'SET_VALUES'; values: [number, number] }
15
+ | { type: 'SET_BOUNDS'; min: number; max: number; step: number; minStepsBetween: number }
16
+ | { type: 'SET_DISABLED'; disabled: boolean }
17
+ | { type: 'POINTER_DOWN'; thumb: 0 | 1 }
18
+ | { type: 'POINTER_MOVE'; value: number }
19
+ | { type: 'POINTER_UP' }
20
+ | { type: 'STEP'; thumb: 0 | 1; delta: number }
21
+ | { type: 'HOME'; thumb: 0 | 1 }
22
+ | { type: 'END'; thumb: 0 | 1 }
23
+
24
+ function clamp(value: number, min: number, max: number) {
25
+ return Math.min(max, Math.max(min, value))
26
+ }
27
+
28
+ function snap(value: number, min: number, max: number, step: number) {
29
+ if (step <= 0) return clamp(value, min, max)
30
+ const snapped = min + Math.round((value - min) / step) * step
31
+ return clamp(snapped, min, max)
32
+ }
33
+
34
+ function normalizePair(
35
+ raw: [number, number],
36
+ min: number,
37
+ max: number,
38
+ step: number,
39
+ minStepsBetween: number,
40
+ ): [number, number] {
41
+ const gap = Math.max(0, minStepsBetween) * (step > 0 ? step : 1)
42
+ let a = snap(raw[0], min, max, step)
43
+ let b = snap(raw[1], min, max, step)
44
+ if (a > b) [a, b] = [b, a]
45
+ if (b - a < gap) {
46
+ if (a + gap <= max) b = snap(a + gap, min, max, step)
47
+ else a = snap(b - gap, min, max, step)
48
+ if (a > b) [a, b] = [b, a]
49
+ }
50
+ return [a, b]
51
+ }
52
+
53
+ function moveThumb(
54
+ context: RangeSliderContext,
55
+ thumb: 0 | 1,
56
+ nextRaw: number,
57
+ ): [number, number] {
58
+ const { min, max, step, minStepsBetween, values } = context
59
+ const gap = Math.max(0, minStepsBetween) * (step > 0 ? step : 1)
60
+ const next = snap(nextRaw, min, max, step)
61
+ if (thumb === 0) {
62
+ const maxStart = values[1] - gap
63
+ return [clamp(next, min, maxStart), values[1]]
64
+ }
65
+ const minEnd = values[0] + gap
66
+ return [values[0], clamp(next, minEnd, max)]
67
+ }
68
+
69
+ export type RangeSliderInput = {
70
+ values: [number, number]
71
+ min: number
72
+ max: number
73
+ step: number
74
+ minStepsBetween?: number
75
+ disabled?: boolean
76
+ }
77
+
78
+ /** Two-thumb range control — idle vs dragging with activeThumb in context. */
79
+ export const rangeSliderMachine = setup({
80
+ types: {
81
+ context: {} as RangeSliderContext,
82
+ events: {} as RangeSliderEvent,
83
+ input: {} as RangeSliderInput,
84
+ },
85
+ guards: {
86
+ notDisabled: ({ context }) => !context.disabled,
87
+ },
88
+ actions: {
89
+ setValues: assign({
90
+ values: ({ context, event }) => {
91
+ if (event.type !== 'SET_VALUES') return context.values
92
+ return normalizePair(
93
+ event.values,
94
+ context.min,
95
+ context.max,
96
+ context.step,
97
+ context.minStepsBetween,
98
+ )
99
+ },
100
+ }),
101
+ setBounds: assign(({ context, event }) => {
102
+ if (event.type !== 'SET_BOUNDS') return {}
103
+ const next = {
104
+ min: event.min,
105
+ max: event.max,
106
+ step: event.step,
107
+ minStepsBetween: event.minStepsBetween,
108
+ }
109
+ return {
110
+ ...next,
111
+ values: normalizePair(
112
+ context.values,
113
+ next.min,
114
+ next.max,
115
+ next.step,
116
+ next.minStepsBetween,
117
+ ),
118
+ }
119
+ }),
120
+ setDisabled: assign({
121
+ disabled: ({ context, event }) =>
122
+ event.type === 'SET_DISABLED' ? event.disabled : context.disabled,
123
+ }),
124
+ pointerDown: assign({
125
+ activeThumb: ({ event }) => (event.type === 'POINTER_DOWN' ? event.thumb : null),
126
+ }),
127
+ pointerMove: assign({
128
+ values: ({ context, event }) => {
129
+ if (event.type !== 'POINTER_MOVE' || context.activeThumb == null) {
130
+ return context.values
131
+ }
132
+ return moveThumb(context, context.activeThumb, event.value)
133
+ },
134
+ }),
135
+ pointerUp: assign({
136
+ activeThumb: null,
137
+ }),
138
+ stepThumb: assign({
139
+ values: ({ context, event }) => {
140
+ if (event.type !== 'STEP') return context.values
141
+ const current = context.values[event.thumb]
142
+ return moveThumb(context, event.thumb, current + event.delta)
143
+ },
144
+ }),
145
+ homeThumb: assign({
146
+ values: ({ context, event }) => {
147
+ if (event.type !== 'HOME') return context.values
148
+ return moveThumb(context, event.thumb, context.min)
149
+ },
150
+ }),
151
+ endThumb: assign({
152
+ values: ({ context, event }) => {
153
+ if (event.type !== 'END') return context.values
154
+ return moveThumb(context, event.thumb, context.max)
155
+ },
156
+ }),
157
+ },
158
+ }).createMachine({
159
+ id: 'rangeSlider',
160
+ context: ({ input }) => {
161
+ const min = input.min
162
+ const max = input.max
163
+ const step = input.step
164
+ const minStepsBetween = input.minStepsBetween ?? 0
165
+ return {
166
+ values: normalizePair(input.values, min, max, step, minStepsBetween),
167
+ min,
168
+ max,
169
+ step,
170
+ minStepsBetween,
171
+ disabled: input.disabled ?? false,
172
+ activeThumb: null,
173
+ }
174
+ },
175
+ initial: 'interaction',
176
+ states: {
177
+ interaction: {
178
+ initial: 'idle',
179
+ states: {
180
+ idle: {
181
+ on: {
182
+ SET_VALUES: { actions: 'setValues', guard: 'notDisabled' },
183
+ SET_BOUNDS: { actions: 'setBounds' },
184
+ SET_DISABLED: { actions: 'setDisabled' },
185
+ POINTER_DOWN: { target: 'dragging', actions: 'pointerDown', guard: 'notDisabled' },
186
+ STEP: { actions: 'stepThumb', guard: 'notDisabled' },
187
+ HOME: { actions: 'homeThumb', guard: 'notDisabled' },
188
+ END: { actions: 'endThumb', guard: 'notDisabled' },
189
+ },
190
+ },
191
+ dragging: {
192
+ on: {
193
+ POINTER_MOVE: { actions: 'pointerMove' },
194
+ POINTER_UP: { target: 'idle', actions: 'pointerUp' },
195
+ SET_VALUES: { actions: 'setValues' },
196
+ SET_BOUNDS: { actions: 'setBounds' },
197
+ SET_DISABLED: { actions: 'setDisabled' },
198
+ },
199
+ },
200
+ },
201
+ },
202
+ },
203
+ })
204
+
205
+ export {
206
+ clamp as clampRangeSliderValue,
207
+ snap as snapRangeSliderValue,
208
+ normalizePair as normalizeRangeSliderValues,
209
+ }