@proyecto-viviana/solid-stately 0.2.4 → 0.3.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 (243) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +30 -283
  3. package/dist/autocomplete/createAutocompleteState.d.ts +3 -2
  4. package/dist/autocomplete/index.d.ts +1 -1
  5. package/dist/calendar/createCalendarState.d.ts +22 -7
  6. package/dist/calendar/createDateFieldState.d.ts +30 -7
  7. package/dist/calendar/createDatePickerState.d.ts +60 -0
  8. package/dist/calendar/createRangeCalendarState.d.ts +27 -8
  9. package/dist/calendar/createTimeFieldState.d.ts +34 -7
  10. package/dist/calendar/index.d.ts +7 -6
  11. package/dist/checkbox/createCheckboxGroupState.d.ts +12 -3
  12. package/dist/checkbox/index.d.ts +1 -1
  13. package/dist/collections/ListCollection.d.ts +1 -1
  14. package/dist/collections/createListState.d.ts +8 -8
  15. package/dist/collections/createMenuState.d.ts +7 -7
  16. package/dist/collections/createSelectionState.d.ts +5 -5
  17. package/dist/collections/index.d.ts +5 -5
  18. package/dist/collections/types.d.ts +23 -12
  19. package/dist/color/Color.d.ts +1 -1
  20. package/dist/color/createColorAreaState.d.ts +4 -2
  21. package/dist/color/createColorFieldState.d.ts +38 -12
  22. package/dist/color/createColorSliderState.d.ts +9 -2
  23. package/dist/color/createColorWheelState.d.ts +13 -2
  24. package/dist/color/getColorChannels.d.ts +20 -0
  25. package/dist/color/index.d.ts +6 -6
  26. package/dist/color/types.d.ts +4 -4
  27. package/dist/combobox/createComboBoxState.d.ts +27 -13
  28. package/dist/combobox/index.d.ts +1 -1
  29. package/dist/data/createAsyncList.d.ts +111 -0
  30. package/dist/data/createListData.d.ts +65 -0
  31. package/dist/data/createTreeData.d.ts +65 -0
  32. package/dist/data/index.d.ts +3 -0
  33. package/dist/datepicker/index.d.ts +8 -0
  34. package/dist/disclosure/createDisclosureState.d.ts +2 -2
  35. package/dist/disclosure/index.d.ts +1 -1
  36. package/dist/dnd/createDragState.d.ts +2 -2
  37. package/dist/dnd/createDraggableCollectionState.d.ts +2 -2
  38. package/dist/dnd/createDropState.d.ts +2 -2
  39. package/dist/dnd/createDroppableCollectionState.d.ts +3 -3
  40. package/dist/dnd/index.d.ts +5 -5
  41. package/dist/dnd/types.d.ts +16 -16
  42. package/dist/form/createFormValidationState.d.ts +3 -3
  43. package/dist/form/index.d.ts +1 -1
  44. package/dist/grid/createGridState.d.ts +2 -2
  45. package/dist/grid/index.d.ts +2 -2
  46. package/dist/grid/types.d.ts +17 -11
  47. package/dist/index.d.ts +31 -25
  48. package/dist/index.js +7990 -6264
  49. package/dist/index.js.map +1 -7
  50. package/dist/menu/index.d.ts +8 -0
  51. package/dist/numberfield/createNumberFieldState.d.ts +2 -2
  52. package/dist/numberfield/index.d.ts +1 -1
  53. package/dist/overlays/createOverlayTriggerState.d.ts +2 -2
  54. package/dist/overlays/index.d.ts +1 -1
  55. package/dist/radio/createRadioGroupState.d.ts +13 -4
  56. package/dist/radio/index.d.ts +1 -1
  57. package/dist/searchfield/createSearchFieldState.d.ts +2 -2
  58. package/dist/searchfield/index.d.ts +2 -2
  59. package/dist/select/createSelectState.d.ts +20 -3
  60. package/dist/select/index.d.ts +1 -1
  61. package/dist/selection/index.d.ts +11 -0
  62. package/dist/slider/createSliderState.d.ts +3 -3
  63. package/dist/slider/index.d.ts +2 -2
  64. package/dist/steplist/createStepListState.d.ts +48 -0
  65. package/dist/steplist/index.d.ts +1 -0
  66. package/dist/table/TableCollection.d.ts +3 -3
  67. package/dist/table/createTableColumnResizeState.d.ts +54 -0
  68. package/dist/table/createTableState.d.ts +2 -2
  69. package/dist/table/index.d.ts +5 -3
  70. package/dist/table/types.d.ts +10 -10
  71. package/dist/tabs/createTabListState.d.ts +5 -5
  72. package/dist/tabs/index.d.ts +1 -1
  73. package/dist/textfield/createTextFieldState.d.ts +2 -2
  74. package/dist/textfield/index.d.ts +1 -1
  75. package/dist/toast/createToastState.d.ts +15 -3
  76. package/dist/toast/index.d.ts +1 -1
  77. package/dist/toggle/createToggleGroupState.d.ts +45 -0
  78. package/dist/toggle/createToggleState.d.ts +2 -2
  79. package/dist/toggle/index.d.ts +2 -1
  80. package/dist/tooltip/createTooltipTriggerState.d.ts +2 -2
  81. package/dist/tooltip/index.d.ts +1 -1
  82. package/dist/tree/TreeCollection.d.ts +5 -4
  83. package/dist/tree/createTreeState.d.ts +2 -2
  84. package/dist/tree/index.d.ts +3 -3
  85. package/dist/tree/types.d.ts +19 -13
  86. package/dist/utils/index.d.ts +1 -1
  87. package/dist/utils/reactivity.d.ts +1 -1
  88. package/package.json +30 -30
  89. package/src/autocomplete/createAutocompleteState.d.ts +19 -17
  90. package/src/autocomplete/createAutocompleteState.ts +25 -32
  91. package/src/autocomplete/index.d.ts +6 -2
  92. package/src/autocomplete/index.ts +1 -1
  93. package/src/calendar/createCalendarState.d.ts +139 -117
  94. package/src/calendar/createCalendarState.ts +221 -74
  95. package/src/calendar/createDateFieldState.d.ts +130 -95
  96. package/src/calendar/createDateFieldState.ts +286 -151
  97. package/src/calendar/createDatePickerState.ts +347 -0
  98. package/src/calendar/createRangeCalendarState.d.ts +154 -129
  99. package/src/calendar/createRangeCalendarState.ts +293 -79
  100. package/src/calendar/createTimeFieldState.d.ts +81 -79
  101. package/src/calendar/createTimeFieldState.ts +187 -108
  102. package/src/calendar/index.d.ts +80 -7
  103. package/src/calendar/index.ts +21 -12
  104. package/src/checkbox/createCheckboxGroupState.d.ts +57 -55
  105. package/src/checkbox/createCheckboxGroupState.ts +57 -30
  106. package/src/checkbox/index.d.ts +6 -2
  107. package/src/checkbox/index.ts +1 -1
  108. package/src/collections/ListCollection.d.ts +25 -22
  109. package/src/collections/ListCollection.ts +152 -146
  110. package/src/collections/createListState.d.ts +73 -58
  111. package/src/collections/createListState.ts +267 -264
  112. package/src/collections/createMenuState.d.ts +36 -31
  113. package/src/collections/createMenuState.ts +112 -106
  114. package/src/collections/createSelectionState.d.ts +78 -65
  115. package/src/collections/createSelectionState.ts +340 -336
  116. package/src/collections/index.d.ts +38 -6
  117. package/src/collections/index.ts +41 -46
  118. package/src/collections/types.d.ts +101 -101
  119. package/src/collections/types.ts +181 -169
  120. package/src/color/Color.d.ts +20 -5
  121. package/src/color/Color.ts +1054 -951
  122. package/src/color/createColorAreaState.d.ts +67 -65
  123. package/src/color/createColorAreaState.ts +301 -293
  124. package/src/color/createColorFieldState.d.ts +46 -44
  125. package/src/color/createColorFieldState.ts +428 -292
  126. package/src/color/createColorSliderState.d.ts +58 -56
  127. package/src/color/createColorSliderState.ts +272 -241
  128. package/src/color/createColorWheelState.d.ts +42 -40
  129. package/src/color/createColorWheelState.ts +248 -211
  130. package/src/color/getColorChannels.ts +36 -0
  131. package/src/color/index.d.ts +36 -7
  132. package/src/color/index.ts +44 -47
  133. package/src/color/types.d.ts +68 -63
  134. package/src/color/types.ts +119 -127
  135. package/src/combobox/createComboBoxState.d.ts +107 -105
  136. package/src/combobox/createComboBoxState.ts +790 -703
  137. package/src/combobox/index.d.ts +10 -2
  138. package/src/combobox/index.ts +13 -13
  139. package/src/data/createAsyncList.ts +403 -0
  140. package/src/data/createListData.ts +298 -0
  141. package/src/data/createTreeData.ts +646 -0
  142. package/src/data/index.ts +25 -0
  143. package/src/datepicker/index.ts +32 -0
  144. package/src/disclosure/createDisclosureState.d.ts +47 -43
  145. package/src/disclosure/createDisclosureState.ts +9 -23
  146. package/src/disclosure/index.d.ts +10 -2
  147. package/src/disclosure/index.ts +1 -1
  148. package/src/dnd/createDragState.d.ts +51 -42
  149. package/src/dnd/createDragState.ts +151 -153
  150. package/src/dnd/createDraggableCollectionState.d.ts +52 -41
  151. package/src/dnd/createDraggableCollectionState.ts +158 -165
  152. package/src/dnd/createDropState.d.ts +64 -45
  153. package/src/dnd/createDropState.ts +202 -212
  154. package/src/dnd/createDroppableCollectionState.d.ts +88 -59
  155. package/src/dnd/createDroppableCollectionState.ts +354 -357
  156. package/src/dnd/index.d.ts +54 -6
  157. package/src/dnd/index.ts +63 -76
  158. package/src/dnd/types.d.ts +167 -156
  159. package/src/dnd/types.ts +316 -317
  160. package/src/form/createFormValidationState.d.ts +47 -45
  161. package/src/form/createFormValidationState.ts +356 -389
  162. package/src/form/index.d.ts +16 -2
  163. package/src/form/index.ts +15 -15
  164. package/src/grid/createGridState.d.ts +7 -4
  165. package/src/grid/createGridState.ts +44 -51
  166. package/src/grid/index.d.ts +3 -3
  167. package/src/grid/index.ts +2 -8
  168. package/src/grid/types.d.ts +136 -129
  169. package/src/grid/types.ts +24 -17
  170. package/src/index.d.ts +310 -26
  171. package/src/index.ts +87 -79
  172. package/src/menu/index.ts +17 -0
  173. package/src/numberfield/createNumberFieldState.d.ts +56 -54
  174. package/src/numberfield/createNumberFieldState.ts +422 -383
  175. package/src/numberfield/index.d.ts +6 -2
  176. package/src/numberfield/index.ts +5 -5
  177. package/src/overlays/createOverlayTriggerState.d.ts +22 -20
  178. package/src/overlays/createOverlayTriggerState.ts +66 -67
  179. package/src/overlays/index.d.ts +6 -2
  180. package/src/overlays/index.ts +5 -5
  181. package/src/radio/createRadioGroupState.d.ts +54 -52
  182. package/src/radio/createRadioGroupState.ts +66 -30
  183. package/src/radio/index.d.ts +7 -2
  184. package/src/radio/index.ts +1 -1
  185. package/src/searchfield/createSearchFieldState.d.ts +16 -14
  186. package/src/searchfield/createSearchFieldState.ts +56 -62
  187. package/src/searchfield/index.d.ts +3 -3
  188. package/src/searchfield/index.ts +2 -5
  189. package/src/select/createSelectState.d.ts +63 -61
  190. package/src/select/createSelectState.ts +290 -181
  191. package/src/select/index.d.ts +2 -2
  192. package/src/select/index.ts +1 -5
  193. package/src/selection/index.ts +27 -0
  194. package/src/slider/createSliderState.d.ts +60 -60
  195. package/src/slider/createSliderState.ts +233 -211
  196. package/src/slider/index.d.ts +3 -3
  197. package/src/slider/index.ts +2 -6
  198. package/src/ssr/index.d.ts +1 -1
  199. package/src/ssr/index.ts +2 -2
  200. package/src/steplist/createStepListState.ts +191 -0
  201. package/src/steplist/index.ts +5 -0
  202. package/src/table/TableCollection.d.ts +42 -40
  203. package/src/table/TableCollection.ts +61 -64
  204. package/src/table/createTableColumnResizeState.ts +242 -0
  205. package/src/table/createTableState.d.ts +7 -4
  206. package/src/table/createTableState.ts +10 -7
  207. package/src/table/index.d.ts +14 -4
  208. package/src/table/index.ts +10 -3
  209. package/src/table/types.d.ts +98 -92
  210. package/src/table/types.ts +17 -13
  211. package/src/tabs/createTabListState.d.ts +59 -57
  212. package/src/tabs/createTabListState.ts +58 -39
  213. package/src/tabs/index.d.ts +8 -2
  214. package/src/tabs/index.ts +1 -1
  215. package/src/textfield/createTextFieldState.d.ts +16 -14
  216. package/src/textfield/createTextFieldState.ts +7 -18
  217. package/src/textfield/index.d.ts +6 -2
  218. package/src/textfield/index.ts +1 -1
  219. package/src/toast/createToastState.d.ts +92 -81
  220. package/src/toast/createToastState.ts +56 -44
  221. package/src/toast/index.d.ts +12 -2
  222. package/src/toast/index.ts +1 -1
  223. package/src/toggle/createToggleGroupState.ts +117 -0
  224. package/src/toggle/createToggleState.d.ts +19 -19
  225. package/src/toggle/createToggleState.ts +2 -16
  226. package/src/toggle/index.d.ts +2 -2
  227. package/src/toggle/index.ts +6 -4
  228. package/src/tooltip/createTooltipTriggerState.d.ts +17 -17
  229. package/src/tooltip/createTooltipTriggerState.ts +184 -183
  230. package/src/tooltip/index.d.ts +7 -2
  231. package/src/tooltip/index.ts +6 -6
  232. package/src/tree/TreeCollection.d.ts +26 -23
  233. package/src/tree/TreeCollection.ts +215 -175
  234. package/src/tree/createTreeState.d.ts +7 -4
  235. package/src/tree/createTreeState.ts +390 -392
  236. package/src/tree/index.d.ts +10 -4
  237. package/src/tree/index.ts +13 -13
  238. package/src/tree/types.d.ts +131 -126
  239. package/src/tree/types.ts +183 -174
  240. package/src/utils/index.d.ts +2 -2
  241. package/src/utils/index.ts +1 -1
  242. package/src/utils/reactivity.d.ts +2 -2
  243. package/src/utils/reactivity.ts +3 -3
@@ -1,383 +1,422 @@
1
- /**
2
- * State management for NumberField.
3
- * Based on @react-stately/numberfield useNumberFieldState.
4
- */
5
-
6
- import { createSignal, createMemo, type Accessor } from 'solid-js';
7
- import { access, type MaybeAccessor } from '../utils';
8
-
9
- export interface NumberFieldStateProps {
10
- /** The current value (controlled). */
11
- value?: number;
12
- /** The default value (uncontrolled). */
13
- defaultValue?: number;
14
- /** Handler called when the value changes. */
15
- onChange?: (value: number) => void;
16
- /** The minimum value. */
17
- minValue?: number;
18
- /** The maximum value. */
19
- maxValue?: number;
20
- /** The step value for increment/decrement. */
21
- step?: number;
22
- /** Whether the field is disabled. */
23
- isDisabled?: boolean;
24
- /** Whether the field is read-only. */
25
- isReadOnly?: boolean;
26
- /** The locale for number formatting. */
27
- locale?: string;
28
- /** Number format options. */
29
- formatOptions?: Intl.NumberFormatOptions;
30
- }
31
-
32
- export interface NumberFieldState {
33
- /** The current input value as a string. */
34
- inputValue: Accessor<string>;
35
- /** The current numeric value. */
36
- numberValue: Accessor<number>;
37
- /** Whether the value can be incremented. */
38
- canIncrement: Accessor<boolean>;
39
- /** Whether the value can be decremented. */
40
- canDecrement: Accessor<boolean>;
41
- /** Whether the field is disabled. */
42
- isDisabled: Accessor<boolean>;
43
- /** Whether the field is read-only. */
44
- isReadOnly: Accessor<boolean>;
45
- /** The minimum value. */
46
- minValue: Accessor<number | undefined>;
47
- /** The maximum value. */
48
- maxValue: Accessor<number | undefined>;
49
- /** Set the input value. */
50
- setInputValue: (value: string) => void;
51
- /** Validate a partial input value. */
52
- validate: (value: string) => boolean;
53
- /** Commit the current input value. */
54
- commit: () => void;
55
- /** Increment the value by step. */
56
- increment: () => void;
57
- /** Decrement the value by step. */
58
- decrement: () => void;
59
- /** Set to maximum value. */
60
- incrementToMax: () => void;
61
- /** Set to minimum value. */
62
- decrementToMin: () => void;
63
- }
64
-
65
- /**
66
- * Handles decimal operations to avoid floating point errors.
67
- */
68
- function handleDecimalOperation(
69
- operator: '+' | '-',
70
- value1: number,
71
- value2: number
72
- ): number {
73
- // Find the number of decimal places
74
- const getDecimals = (n: number) => {
75
- const str = String(n);
76
- const idx = str.indexOf('.');
77
- return idx === -1 ? 0 : str.length - idx - 1;
78
- };
79
-
80
- const decimals = Math.max(getDecimals(value1), getDecimals(value2));
81
- const multiplier = Math.pow(10, decimals);
82
-
83
- const int1 = Math.round(value1 * multiplier);
84
- const int2 = Math.round(value2 * multiplier);
85
-
86
- const result = operator === '+' ? int1 + int2 : int1 - int2;
87
- return result / multiplier;
88
- }
89
-
90
- /**
91
- * Clamps a value between min and max.
92
- */
93
- function clamp(value: number, min?: number, max?: number): number {
94
- let result = value;
95
- if (min != null && result < min) result = min;
96
- if (max != null && result > max) result = max;
97
- return result;
98
- }
99
-
100
- /**
101
- * Snaps a value to the nearest step.
102
- */
103
- function snapToStep(value: number, step: number, min?: number): number {
104
- const base = min ?? 0;
105
- const diff = value - base;
106
- const steps = Math.round(diff / step);
107
- return handleDecimalOperation('+', base, steps * step);
108
- }
109
-
110
- /**
111
- * Creates state for a number field.
112
- */
113
- export function createNumberFieldState(
114
- props: MaybeAccessor<NumberFieldStateProps>
115
- ): NumberFieldState {
116
- const getProps = () => access(props);
117
-
118
- // Get locale and formatter
119
- const locale = () => getProps().locale ?? 'en-US';
120
- const formatOptions = () => getProps().formatOptions ?? {};
121
-
122
- // Create number formatter
123
- const formatter = createMemo(() => {
124
- return new Intl.NumberFormat(locale(), formatOptions());
125
- });
126
-
127
- // Create number parser (simplified - real implementation would be more robust)
128
- const parseNumber = (value: string): number => {
129
- if (!value || value === '' || value === '-') return NaN;
130
-
131
- // Handle locale-specific decimal separators
132
- const opts = formatOptions();
133
- const testNumber = formatter().format(1.1);
134
- const decimalSeparator = testNumber.charAt(1);
135
-
136
- // Normalize the input
137
- let normalized = value;
138
- if (decimalSeparator !== '.') {
139
- normalized = normalized.replace(decimalSeparator, '.');
140
- }
141
-
142
- // Remove grouping separators and currency symbols
143
- normalized = normalized.replace(/[^\d.\-]/g, '');
144
-
145
- const parsed = parseFloat(normalized);
146
- return parsed;
147
- };
148
-
149
- // Format a number to string
150
- const formatNumber = (value: number): string => {
151
- if (isNaN(value)) return '';
152
- return formatter().format(value);
153
- };
154
-
155
- // Determine step value
156
- const step = createMemo(() => {
157
- const p = getProps();
158
- if (p.step != null) return p.step;
159
- // Default step for percent is 0.01
160
- if (p.formatOptions?.style === 'percent') return 0.01;
161
- return 1;
162
- });
163
-
164
- // Internal signals
165
- const [inputValue, setInputValueInternal] = createSignal<string>('');
166
- const [numberValue, setNumberValue] = createSignal<number>(NaN);
167
-
168
- // Initialize from props
169
- const initValue = () => {
170
- const p = getProps();
171
- const initial = p.value ?? p.defaultValue;
172
- if (initial != null) {
173
- setNumberValue(initial);
174
- setInputValueInternal(formatNumber(initial));
175
- }
176
- };
177
-
178
- // Call init on first access
179
- let initialized = false;
180
- const ensureInitialized = () => {
181
- if (!initialized) {
182
- initialized = true;
183
- initValue();
184
- }
185
- };
186
-
187
- // Controlled mode: sync with props.value
188
- const actualNumberValue = createMemo(() => {
189
- ensureInitialized();
190
- const p = getProps();
191
- if (p.value !== undefined) {
192
- return p.value;
193
- }
194
- return numberValue();
195
- });
196
-
197
- // Validate partial input
198
- const validate = (value: string): boolean => {
199
- if (value === '' || value === '-') return true;
200
-
201
- // Allow partial decimal input like "1."
202
- const opts = formatOptions();
203
- const testNumber = formatter().format(1.1);
204
- const decimalSeparator = testNumber.charAt(1);
205
-
206
- // Check if it's a valid partial number
207
- const pattern = new RegExp(
208
- `^-?\\d*${decimalSeparator === '.' ? '\\.' : decimalSeparator}?\\d*$`
209
- );
210
- return pattern.test(value);
211
- };
212
-
213
- // Set input value with validation
214
- const setInputValue = (value: string) => {
215
- ensureInitialized();
216
- setInputValueInternal(value);
217
- };
218
-
219
- // Commit the current input value
220
- const commit = () => {
221
- ensureInitialized();
222
- const p = getProps();
223
- const input = inputValue();
224
-
225
- if (input === '' || input === '-') {
226
- // Clear value
227
- setNumberValue(NaN);
228
- setInputValueInternal('');
229
- return;
230
- }
231
-
232
- let parsed = parseNumber(input);
233
-
234
- if (isNaN(parsed)) {
235
- // Invalid input - revert to current value
236
- setInputValueInternal(formatNumber(actualNumberValue()));
237
- return;
238
- }
239
-
240
- // Clamp and snap
241
- parsed = clamp(parsed, p.minValue, p.maxValue);
242
- parsed = snapToStep(parsed, step(), p.minValue);
243
-
244
- // Update state
245
- setNumberValue(parsed);
246
- setInputValueInternal(formatNumber(parsed));
247
-
248
- // Notify change
249
- if (p.value === undefined) {
250
- p.onChange?.(parsed);
251
- } else {
252
- p.onChange?.(parsed);
253
- }
254
- };
255
-
256
- // Check if can increment
257
- const canIncrement = createMemo(() => {
258
- ensureInitialized();
259
- const p = getProps();
260
- if (p.isDisabled || p.isReadOnly) return false;
261
-
262
- const current = actualNumberValue();
263
- if (isNaN(current)) return true; // Can start from min
264
-
265
- if (p.maxValue == null) return true;
266
- return handleDecimalOperation('+', current, step()) <= p.maxValue;
267
- });
268
-
269
- // Check if can decrement
270
- const canDecrement = createMemo(() => {
271
- ensureInitialized();
272
- const p = getProps();
273
- if (p.isDisabled || p.isReadOnly) return false;
274
-
275
- const current = actualNumberValue();
276
- if (isNaN(current)) return true; // Can start from max
277
-
278
- if (p.minValue == null) return true;
279
- return handleDecimalOperation('-', current, step()) >= p.minValue;
280
- });
281
-
282
- // Increment by step
283
- const increment = () => {
284
- ensureInitialized();
285
- const p = getProps();
286
- if (p.isDisabled || p.isReadOnly) return;
287
-
288
- let current = actualNumberValue();
289
-
290
- if (isNaN(current)) {
291
- // Start from min or 0
292
- current = p.minValue ?? 0;
293
- } else {
294
- // Snap and increment
295
- current = snapToStep(current, step(), p.minValue);
296
- current = handleDecimalOperation('+', current, step());
297
- }
298
-
299
- // Clamp
300
- current = clamp(current, p.minValue, p.maxValue);
301
-
302
- // Update
303
- setNumberValue(current);
304
- setInputValueInternal(formatNumber(current));
305
- p.onChange?.(current);
306
- };
307
-
308
- // Decrement by step
309
- const decrement = () => {
310
- ensureInitialized();
311
- const p = getProps();
312
- if (p.isDisabled || p.isReadOnly) return;
313
-
314
- let current = actualNumberValue();
315
-
316
- if (isNaN(current)) {
317
- // Start from max or 0
318
- current = p.maxValue ?? 0;
319
- } else {
320
- // Snap and decrement
321
- current = snapToStep(current, step(), p.minValue);
322
- current = handleDecimalOperation('-', current, step());
323
- }
324
-
325
- // Clamp
326
- current = clamp(current, p.minValue, p.maxValue);
327
-
328
- // Update
329
- setNumberValue(current);
330
- setInputValueInternal(formatNumber(current));
331
- p.onChange?.(current);
332
- };
333
-
334
- // Set to max
335
- const incrementToMax = () => {
336
- ensureInitialized();
337
- const p = getProps();
338
- if (p.isDisabled || p.isReadOnly) return;
339
-
340
- if (p.maxValue == null) return;
341
-
342
- const snapped = snapToStep(p.maxValue, step(), p.minValue);
343
- setNumberValue(snapped);
344
- setInputValueInternal(formatNumber(snapped));
345
- p.onChange?.(snapped);
346
- };
347
-
348
- // Set to min
349
- const decrementToMin = () => {
350
- ensureInitialized();
351
- const p = getProps();
352
- if (p.isDisabled || p.isReadOnly) return;
353
-
354
- if (p.minValue == null) return;
355
-
356
- setNumberValue(p.minValue);
357
- setInputValueInternal(formatNumber(p.minValue));
358
- p.onChange?.(p.minValue);
359
- };
360
-
361
- return {
362
- get inputValue() {
363
- ensureInitialized();
364
- return inputValue;
365
- },
366
- get numberValue() {
367
- return actualNumberValue;
368
- },
369
- canIncrement,
370
- canDecrement,
371
- isDisabled: () => getProps().isDisabled ?? false,
372
- isReadOnly: () => getProps().isReadOnly ?? false,
373
- minValue: () => getProps().minValue,
374
- maxValue: () => getProps().maxValue,
375
- setInputValue,
376
- validate,
377
- commit,
378
- increment,
379
- decrement,
380
- incrementToMax,
381
- decrementToMin,
382
- };
383
- }
1
+ /**
2
+ * State management for NumberField.
3
+ * Based on @react-stately/numberfield useNumberFieldState.
4
+ */
5
+
6
+ import { createSignal, createMemo, type Accessor } from "solid-js";
7
+ import { access, type MaybeAccessor } from "../utils";
8
+
9
+ export interface NumberFieldStateProps {
10
+ /** The current value (controlled). */
11
+ value?: number;
12
+ /** The default value (uncontrolled). */
13
+ defaultValue?: number;
14
+ /** Handler called when the value changes. */
15
+ onChange?: (value: number) => void;
16
+ /** The minimum value. */
17
+ minValue?: number;
18
+ /** The maximum value. */
19
+ maxValue?: number;
20
+ /** The step value for increment/decrement. */
21
+ step?: number;
22
+ /** Whether the field is disabled. */
23
+ isDisabled?: boolean;
24
+ /** Whether the field is read-only. */
25
+ isReadOnly?: boolean;
26
+ /** The locale for number formatting. */
27
+ locale?: string;
28
+ /** Number format options. */
29
+ formatOptions?: Intl.NumberFormatOptions;
30
+ }
31
+
32
+ export interface NumberFieldState {
33
+ /** The current input value as a string. */
34
+ inputValue: Accessor<string>;
35
+ /** The current numeric value. */
36
+ numberValue: Accessor<number>;
37
+ /** Whether the value can be incremented. */
38
+ canIncrement: Accessor<boolean>;
39
+ /** Whether the value can be decremented. */
40
+ canDecrement: Accessor<boolean>;
41
+ /** Whether the field is disabled. */
42
+ isDisabled: Accessor<boolean>;
43
+ /** Whether the field is read-only. */
44
+ isReadOnly: Accessor<boolean>;
45
+ /** The minimum value. */
46
+ minValue: Accessor<number | undefined>;
47
+ /** The maximum value. */
48
+ maxValue: Accessor<number | undefined>;
49
+ /** Set the input value. */
50
+ setInputValue: (value: string) => void;
51
+ /** Validate a partial input value. */
52
+ validate: (value: string) => boolean;
53
+ /** Commit the current input value. */
54
+ commit: () => void;
55
+ /** Increment the value by step. */
56
+ increment: () => void;
57
+ /** Decrement the value by step. */
58
+ decrement: () => void;
59
+ /** Set to maximum value. */
60
+ incrementToMax: () => void;
61
+ /** Set to minimum value. */
62
+ decrementToMin: () => void;
63
+ }
64
+
65
+ /**
66
+ * Handles decimal operations to avoid floating point errors.
67
+ */
68
+ function handleDecimalOperation(operator: "+" | "-", value1: number, value2: number): number {
69
+ // Find the number of decimal places
70
+ const getDecimals = (n: number) => {
71
+ const str = String(n);
72
+ const idx = str.indexOf(".");
73
+ return idx === -1 ? 0 : str.length - idx - 1;
74
+ };
75
+
76
+ const decimals = Math.max(getDecimals(value1), getDecimals(value2));
77
+ const multiplier = Math.pow(10, decimals);
78
+
79
+ const int1 = Math.round(value1 * multiplier);
80
+ const int2 = Math.round(value2 * multiplier);
81
+
82
+ const result = operator === "+" ? int1 + int2 : int1 - int2;
83
+ return result / multiplier;
84
+ }
85
+
86
+ /**
87
+ * Clamps a value between min and max.
88
+ */
89
+ function clamp(value: number, min?: number, max?: number): number {
90
+ let result = value;
91
+ if (min != null && result < min) result = min;
92
+ if (max != null && result > max) result = max;
93
+ return result;
94
+ }
95
+
96
+ /**
97
+ * Snaps a value to the nearest step.
98
+ */
99
+ function snapToStep(value: number, step: number, min?: number): number {
100
+ const base = min ?? 0;
101
+ const diff = value - base;
102
+ const steps = Math.round(diff / step);
103
+ return handleDecimalOperation("+", base, steps * step);
104
+ }
105
+
106
+ function isValidStep(step: number | undefined): step is number {
107
+ return step != null && !isNaN(step) && step > 0;
108
+ }
109
+
110
+ /**
111
+ * Creates state for a number field.
112
+ */
113
+ export function createNumberFieldState(
114
+ props: MaybeAccessor<NumberFieldStateProps>,
115
+ ): NumberFieldState {
116
+ const getProps = () => access(props);
117
+
118
+ // Get locale and formatter
119
+ const locale = () => getProps().locale ?? "en-US";
120
+ const formatOptions = () => getProps().formatOptions ?? {};
121
+
122
+ // Create number formatter
123
+ const formatter = createMemo(() => {
124
+ return new Intl.NumberFormat(locale(), formatOptions());
125
+ });
126
+
127
+ // Create number parser (simplified - real implementation would be more robust)
128
+ const parseNumber = (value: string): number => {
129
+ if (!value || value === "" || value === "-") return NaN;
130
+
131
+ // Handle locale-specific decimal separators
132
+ const opts = formatOptions();
133
+ const testNumber = formatter().format(1.1);
134
+ const decimalSeparator = testNumber.charAt(1);
135
+
136
+ // Normalize the input
137
+ let normalized = value;
138
+ if (decimalSeparator !== ".") {
139
+ normalized = normalized.replace(decimalSeparator, ".");
140
+ }
141
+
142
+ // Remove grouping separators and currency symbols
143
+ normalized = normalized.replace(/[^\d.-]/g, "");
144
+
145
+ const parsed = parseFloat(normalized);
146
+ if (isNaN(parsed)) return parsed;
147
+
148
+ if (opts.style === "percent") {
149
+ return parsed / 100;
150
+ }
151
+
152
+ return parsed;
153
+ };
154
+
155
+ // Format a number to string
156
+ const formatNumber = (value: number): string => {
157
+ if (isNaN(value)) return "";
158
+ return formatter().format(value);
159
+ };
160
+
161
+ // Determine step value
162
+ const hasCustomStep = createMemo(() => isValidStep(getProps().step));
163
+
164
+ const step = createMemo(() => {
165
+ const p = getProps();
166
+ if (hasCustomStep()) return p.step as number;
167
+ // Default step for percent is 0.01
168
+ if (p.formatOptions?.style === "percent") return 0.01;
169
+ return 1;
170
+ });
171
+
172
+ // Internal signals
173
+ const [inputValue, setInputValueInternal] = createSignal<string>("");
174
+ const [numberValue, setNumberValue] = createSignal<number>(NaN);
175
+
176
+ const applyConstraints = (value: number): number => {
177
+ const p = getProps();
178
+ if (isNaN(value)) return NaN;
179
+
180
+ if (hasCustomStep()) {
181
+ return clamp(snapToStep(value, step(), p.minValue), p.minValue, p.maxValue);
182
+ }
183
+
184
+ return clamp(value, p.minValue, p.maxValue);
185
+ };
186
+
187
+ // Initialize from props
188
+ const initValue = () => {
189
+ const p = getProps();
190
+ const initial = p.value ?? p.defaultValue;
191
+ if (initial != null) {
192
+ const constrained = applyConstraints(initial);
193
+ setNumberValue(constrained);
194
+ setInputValueInternal(formatNumber(constrained));
195
+ }
196
+ };
197
+
198
+ // Call init on first access
199
+ let initialized = false;
200
+ const ensureInitialized = () => {
201
+ if (!initialized) {
202
+ initialized = true;
203
+ initValue();
204
+ }
205
+ };
206
+
207
+ // Controlled mode: sync with props.value
208
+ const actualNumberValue = createMemo(() => {
209
+ ensureInitialized();
210
+ const p = getProps();
211
+ if (p.value !== undefined) {
212
+ return applyConstraints(p.value);
213
+ }
214
+ return numberValue();
215
+ });
216
+
217
+ let lastControlledValue: number | undefined = undefined;
218
+ const syncControlledValue = () => {
219
+ const p = getProps();
220
+ if (p.value === undefined) {
221
+ lastControlledValue = undefined;
222
+ return;
223
+ }
224
+
225
+ const constrained = applyConstraints(p.value);
226
+ if (lastControlledValue === undefined || !Object.is(lastControlledValue, constrained)) {
227
+ lastControlledValue = constrained;
228
+ setNumberValue(constrained);
229
+ setInputValueInternal(formatNumber(constrained));
230
+ }
231
+ };
232
+
233
+ const parsedInputValue = () => {
234
+ ensureInitialized();
235
+ syncControlledValue();
236
+ return parseNumber(inputValue());
237
+ };
238
+
239
+ // Validate partial input
240
+ const validate = (value: string): boolean => {
241
+ if (value === "" || value === "-") return true;
242
+
243
+ // Allow partial decimal input like "1."
244
+ const opts = formatOptions();
245
+ const testNumber = formatter().format(1.1);
246
+ const decimalSeparator = testNumber.charAt(1);
247
+
248
+ // Check if it's a valid partial number
249
+ const pattern = new RegExp(
250
+ `^-?\\d*${decimalSeparator === "." ? "\\." : decimalSeparator}?\\d*$`,
251
+ );
252
+ return pattern.test(value);
253
+ };
254
+
255
+ // Set input value with validation
256
+ const setInputValue = (value: string) => {
257
+ ensureInitialized();
258
+ syncControlledValue();
259
+ setInputValueInternal(value);
260
+ };
261
+
262
+ // Commit the current input value
263
+ const commit = () => {
264
+ ensureInitialized();
265
+ const p = getProps();
266
+ const input = inputValue();
267
+
268
+ if (input === "" || input === "-") {
269
+ // Clear value
270
+ setNumberValue(NaN);
271
+ setInputValueInternal(p.value === undefined ? "" : formatNumber(actualNumberValue()));
272
+ p.onChange?.(NaN);
273
+ return;
274
+ }
275
+
276
+ let parsed = parseNumber(input);
277
+
278
+ if (isNaN(parsed)) {
279
+ // Invalid input - revert to current value
280
+ setInputValueInternal(formatNumber(actualNumberValue()));
281
+ return;
282
+ }
283
+
284
+ // Clamp and optionally snap to custom step.
285
+ parsed = applyConstraints(parsed);
286
+
287
+ // Update state
288
+ setNumberValue(parsed);
289
+ setInputValueInternal(formatNumber(parsed));
290
+
291
+ p.onChange?.(parsed);
292
+ };
293
+
294
+ // Check if can increment
295
+ const canIncrement = createMemo(() => {
296
+ ensureInitialized();
297
+ const p = getProps();
298
+ if (p.isDisabled || p.isReadOnly) return false;
299
+
300
+ const current = parsedInputValue();
301
+ if (isNaN(current)) return true; // Can start from min
302
+
303
+ if (p.maxValue == null) return true;
304
+ return (
305
+ snapToStep(current, step(), p.minValue) > current ||
306
+ handleDecimalOperation("+", current, step()) <= p.maxValue
307
+ );
308
+ });
309
+
310
+ // Check if can decrement
311
+ const canDecrement = createMemo(() => {
312
+ ensureInitialized();
313
+ const p = getProps();
314
+ if (p.isDisabled || p.isReadOnly) return false;
315
+
316
+ const current = parsedInputValue();
317
+ if (isNaN(current)) return true; // Can start from max
318
+
319
+ if (p.minValue == null) return true;
320
+ return (
321
+ snapToStep(current, step(), p.minValue) < current ||
322
+ handleDecimalOperation("-", current, step()) >= p.minValue
323
+ );
324
+ });
325
+
326
+ const safeNextStep = (operation: "+" | "-", minOrMaxValue: number = 0): number => {
327
+ const p = getProps();
328
+ const parsed = parsedInputValue();
329
+
330
+ if (isNaN(parsed)) {
331
+ const base = isNaN(minOrMaxValue) ? 0 : minOrMaxValue;
332
+ return clamp(snapToStep(base, step(), p.minValue), p.minValue, p.maxValue);
333
+ }
334
+
335
+ const snapped = snapToStep(parsed, step(), p.minValue);
336
+ if ((operation === "+" && snapped > parsed) || (operation === "-" && snapped < parsed)) {
337
+ return clamp(snapped, p.minValue, p.maxValue);
338
+ }
339
+
340
+ const next = handleDecimalOperation(operation, parsed, step());
341
+ return clamp(snapToStep(next, step(), p.minValue), p.minValue, p.maxValue);
342
+ };
343
+
344
+ // Increment by step
345
+ const increment = () => {
346
+ ensureInitialized();
347
+ syncControlledValue();
348
+ const p = getProps();
349
+ if (p.isDisabled || p.isReadOnly) return;
350
+
351
+ const current = safeNextStep("+", p.minValue);
352
+ setNumberValue(current);
353
+ setInputValueInternal(formatNumber(current));
354
+ p.onChange?.(current);
355
+ };
356
+
357
+ // Decrement by step
358
+ const decrement = () => {
359
+ ensureInitialized();
360
+ syncControlledValue();
361
+ const p = getProps();
362
+ if (p.isDisabled || p.isReadOnly) return;
363
+
364
+ const current = safeNextStep("-", p.maxValue);
365
+ setNumberValue(current);
366
+ setInputValueInternal(formatNumber(current));
367
+ p.onChange?.(current);
368
+ };
369
+
370
+ // Set to max
371
+ const incrementToMax = () => {
372
+ ensureInitialized();
373
+ syncControlledValue();
374
+ const p = getProps();
375
+ if (p.isDisabled || p.isReadOnly) return;
376
+
377
+ if (p.maxValue == null) return;
378
+
379
+ const snapped = snapToStep(p.maxValue, step(), p.minValue);
380
+ setNumberValue(snapped);
381
+ setInputValueInternal(formatNumber(snapped));
382
+ p.onChange?.(snapped);
383
+ };
384
+
385
+ // Set to min
386
+ const decrementToMin = () => {
387
+ ensureInitialized();
388
+ syncControlledValue();
389
+ const p = getProps();
390
+ if (p.isDisabled || p.isReadOnly) return;
391
+
392
+ if (p.minValue == null) return;
393
+
394
+ setNumberValue(p.minValue);
395
+ setInputValueInternal(formatNumber(p.minValue));
396
+ p.onChange?.(p.minValue);
397
+ };
398
+
399
+ return {
400
+ get inputValue() {
401
+ ensureInitialized();
402
+ syncControlledValue();
403
+ return inputValue;
404
+ },
405
+ get numberValue() {
406
+ return actualNumberValue;
407
+ },
408
+ canIncrement,
409
+ canDecrement,
410
+ isDisabled: () => getProps().isDisabled ?? false,
411
+ isReadOnly: () => getProps().isReadOnly ?? false,
412
+ minValue: () => getProps().minValue,
413
+ maxValue: () => getProps().maxValue,
414
+ setInputValue,
415
+ validate,
416
+ commit,
417
+ increment,
418
+ decrement,
419
+ incrementToMax,
420
+ decrementToMin,
421
+ };
422
+ }