@proyecto-viviana/solid-stately 0.6.0-rc.0 → 0.6.0-rc.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.
Files changed (127) hide show
  1. package/README.md +59 -29
  2. package/dist/calendar/createCalendarState.d.ts +3 -1
  3. package/dist/calendar/createCalendarState.d.ts.map +1 -1
  4. package/dist/calendar/createDatePickerState.d.ts +2 -0
  5. package/dist/calendar/createDatePickerState.d.ts.map +1 -1
  6. package/dist/calendar/createRangeCalendarState.d.ts +2 -0
  7. package/dist/calendar/createRangeCalendarState.d.ts.map +1 -1
  8. package/dist/calendar/createTimeFieldState.d.ts +1 -1
  9. package/dist/calendar/createTimeFieldState.d.ts.map +1 -1
  10. package/dist/checkbox/createCheckboxGroupState.d.ts +2 -0
  11. package/dist/checkbox/createCheckboxGroupState.d.ts.map +1 -1
  12. package/dist/collections/createListState.d.ts.map +1 -1
  13. package/dist/collections/createMenuState.d.ts +46 -2
  14. package/dist/collections/createMenuState.d.ts.map +1 -1
  15. package/dist/collections/createSelectionState.d.ts.map +1 -1
  16. package/dist/collections/index.d.ts +1 -1
  17. package/dist/collections/index.d.ts.map +1 -1
  18. package/dist/color/createColorAreaState.d.ts +4 -0
  19. package/dist/color/createColorAreaState.d.ts.map +1 -1
  20. package/dist/color/createColorFieldState.d.ts +2 -0
  21. package/dist/color/createColorFieldState.d.ts.map +1 -1
  22. package/dist/color/createColorSliderState.d.ts +2 -0
  23. package/dist/color/createColorSliderState.d.ts.map +1 -1
  24. package/dist/color/createColorWheelState.d.ts.map +1 -1
  25. package/dist/combobox/createComboBoxState.d.ts +90 -23
  26. package/dist/combobox/createComboBoxState.d.ts.map +1 -1
  27. package/dist/combobox/index.d.ts +1 -1
  28. package/dist/combobox/index.d.ts.map +1 -1
  29. package/dist/data/createAsyncList.d.ts.map +1 -1
  30. package/dist/data/createListData.d.ts.map +1 -1
  31. package/dist/data/createTreeData.d.ts.map +1 -1
  32. package/dist/dnd/createDraggableCollectionState.d.ts +5 -1
  33. package/dist/dnd/createDraggableCollectionState.d.ts.map +1 -1
  34. package/dist/dnd/createDroppableCollectionState.d.ts +6 -0
  35. package/dist/dnd/createDroppableCollectionState.d.ts.map +1 -1
  36. package/dist/form/createFormValidationState.d.ts.map +1 -1
  37. package/dist/grid/createGridState.d.ts +2 -2
  38. package/dist/grid/createGridState.d.ts.map +1 -1
  39. package/dist/index.d.ts +5 -5
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +1012 -471
  42. package/dist/index.js.map +1 -1
  43. package/dist/menu/index.d.ts +2 -1
  44. package/dist/menu/index.d.ts.map +1 -1
  45. package/dist/numberfield/createNumberFieldState.d.ts +6 -3
  46. package/dist/numberfield/createNumberFieldState.d.ts.map +1 -1
  47. package/dist/private/flags/flags.d.ts.map +1 -0
  48. package/dist/private/flags/flags.js +1 -1
  49. package/dist/private/flags/flags.js.map +1 -1
  50. package/dist/radio/createRadioGroupState.d.ts.map +1 -1
  51. package/dist/select/createSelectState.d.ts +85 -29
  52. package/dist/select/createSelectState.d.ts.map +1 -1
  53. package/dist/select/index.d.ts +1 -1
  54. package/dist/select/index.d.ts.map +1 -1
  55. package/dist/selection/SelectionManager.d.ts.map +1 -1
  56. package/dist/selection/createMultipleSelectionState.d.ts.map +1 -1
  57. package/dist/slider/createSliderState.d.ts +35 -9
  58. package/dist/slider/createSliderState.d.ts.map +1 -1
  59. package/dist/ssr/index.d.ts +0 -6
  60. package/dist/ssr/index.d.ts.map +1 -1
  61. package/dist/steplist/createStepListState.d.ts +8 -13
  62. package/dist/steplist/createStepListState.d.ts.map +1 -1
  63. package/dist/table/createTableColumnResizeState.d.ts.map +1 -1
  64. package/dist/table/createTableState.d.ts +2 -2
  65. package/dist/table/createTableState.d.ts.map +1 -1
  66. package/dist/table/createTreeGridState.d.ts +2 -2
  67. package/dist/table/createTreeGridState.d.ts.map +1 -1
  68. package/dist/table/types.d.ts +2 -2
  69. package/dist/table/types.d.ts.map +1 -1
  70. package/dist/tabs/createTabListState.d.ts.map +1 -1
  71. package/dist/tokenfield/TokenFieldValue.d.ts +28 -4
  72. package/dist/tokenfield/TokenFieldValue.d.ts.map +1 -1
  73. package/dist/tokenfield/index.d.ts +1 -1
  74. package/dist/tokenfield/index.d.ts.map +1 -1
  75. package/dist/tree/createTreeState.d.ts +2 -2
  76. package/dist/tree/createTreeState.d.ts.map +1 -1
  77. package/dist/utils/index.d.ts +1 -1
  78. package/dist/utils/index.d.ts.map +1 -1
  79. package/dist/utils/reactivity.d.ts +5 -0
  80. package/dist/utils/reactivity.d.ts.map +1 -1
  81. package/package.json +2 -2
  82. package/src/calendar/createCalendarState.ts +139 -58
  83. package/src/calendar/createDatePickerState.ts +14 -11
  84. package/src/calendar/createRangeCalendarState.ts +43 -17
  85. package/src/calendar/createTimeFieldState.ts +1 -1
  86. package/src/checkbox/createCheckboxGroupState.ts +16 -0
  87. package/src/collections/createListState.ts +4 -4
  88. package/src/collections/createMenuState.ts +134 -4
  89. package/src/collections/createSelectionState.ts +2 -2
  90. package/src/collections/index.ts +4 -0
  91. package/src/color/createColorAreaState.ts +19 -0
  92. package/src/color/createColorFieldState.ts +76 -26
  93. package/src/color/createColorSliderState.ts +25 -2
  94. package/src/color/createColorWheelState.ts +9 -5
  95. package/src/combobox/createComboBoxState.ts +371 -121
  96. package/src/combobox/index.ts +4 -0
  97. package/src/data/createAsyncList.ts +19 -4
  98. package/src/data/createListData.ts +8 -2
  99. package/src/data/createTreeData.ts +11 -3
  100. package/src/dnd/createDraggableCollectionState.ts +8 -1
  101. package/src/dnd/createDroppableCollectionState.ts +10 -0
  102. package/src/form/createFormValidationState.ts +17 -3
  103. package/src/grid/createGridState.ts +15 -8
  104. package/src/index.ts +18 -2
  105. package/src/menu/index.ts +5 -0
  106. package/src/numberfield/createNumberFieldState.ts +97 -35
  107. package/src/radio/createRadioGroupState.ts +4 -1
  108. package/src/select/createSelectState.ts +253 -131
  109. package/src/select/index.ts +8 -1
  110. package/src/selection/SelectionManager.ts +3 -1
  111. package/src/selection/createMultipleSelectionState.ts +2 -2
  112. package/src/slider/createSliderState.ts +227 -103
  113. package/src/ssr/index.ts +11 -4
  114. package/src/steplist/createStepListState.ts +156 -123
  115. package/src/table/createTableColumnResizeState.ts +51 -11
  116. package/src/table/createTableState.ts +5 -4
  117. package/src/table/createTreeGridState.ts +3 -4
  118. package/src/table/types.ts +2 -2
  119. package/src/tabs/createTabListState.ts +8 -31
  120. package/src/tokenfield/TokenFieldValue.ts +75 -16
  121. package/src/tokenfield/index.ts +1 -0
  122. package/src/tree/createTreeState.ts +15 -8
  123. package/src/utils/index.ts +1 -0
  124. package/src/utils/reactivity.ts +8 -0
  125. package/dist/flags/flags.d.ts.map +0 -1
  126. /package/dist/{flags → private/flags}/flags.d.ts +0 -0
  127. /package/src/{flags → private/flags}/flags.ts +0 -0
@@ -17,21 +17,21 @@
17
17
  * Based on @react-stately/slider useSliderState.
18
18
  */
19
19
 
20
- import { createMemo } from "solid-js";
20
+ import { createMemo, createEffect } from "solid-js";
21
21
  import type { Accessor } from "solid-js";
22
22
  import { createInternalSignal, access, type MaybeAccessor } from "../utils";
23
23
 
24
24
  export type SliderOrientation = "horizontal" | "vertical";
25
25
 
26
- export interface SliderStateProps {
26
+ export interface SliderStateProps<T extends number | number[] = number> {
27
27
  /** The current value (controlled). */
28
- value?: number;
28
+ value?: T;
29
29
  /** The default value (uncontrolled). */
30
- defaultValue?: number;
30
+ defaultValue?: T;
31
31
  /** Handler called when the value changes. */
32
- onChange?: (value: number) => void;
32
+ onChange?: (value: T) => void;
33
33
  /** Handler called when the user stops dragging. */
34
- onChangeEnd?: (value: number) => void;
34
+ onChangeEnd?: (value: T) => void;
35
35
  /** The minimum value. */
36
36
  minValue?: number;
37
37
  /** The maximum value. */
@@ -58,14 +58,35 @@ export interface SliderState {
58
58
  /** Gets the value as a percent (0-1). */
59
59
  getValuePercent: Accessor<number>;
60
60
  /** Gets the formatted value string. */
61
- getFormattedValue: Accessor<string>;
62
- /** Whether the thumb is being dragged. */
61
+ getFormattedValue: {
62
+ (): string;
63
+ (value: number | readonly number[]): string;
64
+ };
65
+ /** Canonical values; legacy numeric members address thumb zero. */
66
+ values: Accessor<readonly number[]>;
67
+ /** Current reset defaults, normalized to the domain. */
68
+ defaultValues: Accessor<readonly number[]>;
69
+ getThumbValue: (index: number) => number;
70
+ setThumbValue: (index: number, value: number) => void;
71
+ setThumbPercent: (index: number, percent: number) => void;
72
+ getThumbPercent: (index: number) => number;
73
+ getThumbMinValue: (index: number) => number;
74
+ getThumbMaxValue: (index: number) => number;
75
+ getThumbValueLabel: (index: number) => string;
76
+ isThumbDragging: (index: number) => boolean;
77
+ setThumbDragging: (index: number, dragging: boolean) => void;
78
+ focusedThumb: Accessor<number | undefined>;
79
+ setFocusedThumb: (index: number | undefined) => void;
80
+ /** Absolute value distance, at least state.step. */
81
+ incrementThumb: (index: number, stepSize?: number) => void;
82
+ decrementThumb: (index: number, stepSize?: number) => void;
83
+ /** Whether any thumb is being dragged. */
63
84
  isDragging: Accessor<boolean>;
64
- /** Sets the dragging state. */
85
+ /** Sets thumb zero dragging state. */
65
86
  setDragging: (dragging: boolean) => void;
66
87
  /** Whether the slider is focused. */
67
88
  isFocused: Accessor<boolean>;
68
- /** Sets the focused state. */
89
+ /** Focuses thumb zero; false clears only thumb zero. */
69
90
  setFocused: (focused: boolean) => void;
70
91
  /** Increments the value by step. */
71
92
  increment: (stepMultiplier?: number) => void;
@@ -110,17 +131,40 @@ function snapToStep(value: number, min: number, max: number, step: number): numb
110
131
  /**
111
132
  * Provides state management for a slider component.
112
133
  */
113
- export function createSliderState(props: MaybeAccessor<SliderStateProps>): SliderState {
134
+ export function createSliderState(
135
+ props: MaybeAccessor<
136
+ SliderStateProps<number[]> & ({ value: number[] } | { defaultValue: number[] })
137
+ >,
138
+ ): SliderState;
139
+ export function createSliderState(props: MaybeAccessor<SliderStateProps>): SliderState;
140
+ export function createSliderState(
141
+ props: MaybeAccessor<SliderStateProps | SliderStateProps<number[]>>,
142
+ ): SliderState {
114
143
  const getProps = () => access(props);
115
-
116
144
  const initialProps = getProps();
117
- const initialMinValue = initialProps.minValue ?? DEFAULT_MIN;
118
- const initialMaxValue = initialProps.maxValue ?? DEFAULT_MAX;
119
- const initialStep = initialProps.step ?? DEFAULT_STEP;
120
-
121
- const minValue = createMemo(() => getProps().minValue ?? DEFAULT_MIN);
122
- const maxValue = createMemo(() => getProps().maxValue ?? DEFAULT_MAX);
123
- const step = createMemo(() => getProps().step ?? DEFAULT_STEP);
145
+ // Mode is established by explicit input shape, never by callback arity.
146
+ const arrayMode = Array.isArray(initialProps.value) || Array.isArray(initialProps.defaultValue);
147
+ const initialArray = Array.isArray(initialProps.value)
148
+ ? initialProps.value
149
+ : initialProps.defaultValue;
150
+ const initialCount = Array.isArray(initialArray) ? initialArray.length : 1;
151
+ const finite = (value: number | undefined, fallback: number) =>
152
+ typeof value === "number" && Number.isFinite(value) ? value : fallback;
153
+ const minValue = createMemo(() =>
154
+ arrayMode ? finite(getProps().minValue, DEFAULT_MIN) : (getProps().minValue ?? DEFAULT_MIN),
155
+ );
156
+ const step = createMemo(() => {
157
+ const next = arrayMode
158
+ ? finite(getProps().step, DEFAULT_STEP)
159
+ : (getProps().step ?? DEFAULT_STEP);
160
+ return arrayMode && next <= 0 ? DEFAULT_STEP : next;
161
+ });
162
+ const maxValue = createMemo(() => {
163
+ const next = arrayMode
164
+ ? finite(getProps().maxValue, DEFAULT_MAX)
165
+ : (getProps().maxValue ?? DEFAULT_MAX);
166
+ return arrayMode && next <= minValue() ? minValue() + step() : next;
167
+ });
124
168
  const orientation = createMemo(() => getProps().orientation ?? "horizontal");
125
169
  const isDisabled = createMemo(() => getProps().isDisabled ?? false);
126
170
  const pageStep = createMemo(() =>
@@ -129,103 +173,183 @@ export function createSliderState(props: MaybeAccessor<SliderStateProps>): Slide
129
173
  snapToStep((maxValue() - minValue()) / 10, 0, maxValue() - minValue(), step()),
130
174
  ),
131
175
  );
132
-
133
- // Controlled vs uncontrolled
134
176
  const isControlled = () => getProps().value !== undefined;
135
-
136
- // Internal signal for uncontrolled mode
137
- const [internalValue, setInternalValue] = createInternalSignal(
138
- snapToStep(
139
- initialProps.defaultValue ?? initialMinValue,
140
- initialMinValue,
141
- initialMaxValue,
142
- initialStep,
143
- ),
177
+ const fallbackValues = () =>
178
+ Array.from({ length: initialCount }, (_, index) => (index === 0 ? minValue() : maxValue()));
179
+ const normalize = (raw: number | readonly number[] | undefined): number[] => {
180
+ const items = raw === undefined ? fallbackValues() : typeof raw === "number" ? [raw] : raw;
181
+ const snapped = items.map((item, index) =>
182
+ snapToStep(
183
+ arrayMode ? finite(item, index === 0 ? minValue() : maxValue()) : item,
184
+ minValue(),
185
+ maxValue(),
186
+ step(),
187
+ ),
188
+ );
189
+ return arrayMode ? snapped.sort((a, b) => a - b) : snapped;
190
+ };
191
+ const [internalValues, setInternalValues] = createInternalSignal(
192
+ normalize(initialProps.defaultValue),
144
193
  );
145
-
146
- // Dragging and focus state
147
- const [isDragging, setIsDragging] = createInternalSignal(false);
148
- const [isFocused, setIsFocused] = createInternalSignal(false);
149
-
150
- // Current value accessor
151
- const value = createMemo(() => {
152
- const p = getProps();
153
- const rawValue = isControlled() ? (p.value ?? minValue()) : internalValue();
154
- return snapToStep(rawValue, minValue(), maxValue(), step());
155
- });
156
-
157
- // Value as percent (0-1)
158
- const getValuePercent = createMemo(() => {
159
- return (value() - minValue()) / (maxValue() - minValue());
160
- });
161
-
162
- // Formatted value
163
- const getFormattedValue = createMemo(() => {
164
- const p = getProps();
165
- const formatter = new Intl.NumberFormat(p.locale, p.formatOptions);
166
- return formatter.format(value());
167
- });
168
-
169
- // Set value function
170
- const setValue = (newValue: number) => {
171
- if (isDisabled()) return;
172
-
194
+ const values: Accessor<readonly number[]> = () =>
195
+ normalize(isControlled() ? getProps().value : internalValues());
196
+ const initialValues = [...values()];
197
+ const initialDefaultValues = normalize(initialProps.defaultValue ?? initialValues);
198
+ const defaultValues: Accessor<readonly number[]> = () =>
199
+ // Scalar defaults are captured at mount; RangeSlider resets to current domain defaults.
200
+ normalize(arrayMode ? getProps().defaultValue : initialDefaultValues);
201
+ if (arrayMode) {
202
+ // Preserve RangeSlider's reactive uncontrolled default/domain reset policy.
203
+ // The compute phase reads only props/bounds, never the mutable current values.
204
+ const resetSnapshot = () =>
205
+ isControlled()
206
+ ? undefined
207
+ : {
208
+ defaults: normalize(getProps().defaultValue),
209
+ min: minValue(),
210
+ max: maxValue(),
211
+ step: step(),
212
+ };
213
+ // Seed before effects drain: an initial equal default must not erase setters
214
+ // called during setup. Only a changed default/domain/control mode resets.
215
+ let previousReset = resetSnapshot();
216
+ createEffect(resetSnapshot, (next) => {
217
+ const previous = previousReset;
218
+ previousReset = next;
219
+ if (
220
+ next &&
221
+ (!previous ||
222
+ next.min !== previous.min ||
223
+ next.max !== previous.max ||
224
+ next.step !== previous.step ||
225
+ next.defaults.length !== previous.defaults.length ||
226
+ next.defaults.some((value, index) => value !== previous.defaults[index]))
227
+ ) {
228
+ setInternalValues(next.defaults);
229
+ }
230
+ });
231
+ }
232
+ const [draggings, setDraggings] = createInternalSignal<boolean[]>(initialValues.map(() => false));
233
+ const [focusedThumb, setFocusedThumbValue] = createInternalSignal<number | undefined>(undefined);
234
+ const isDragging = () => draggings().some(Boolean);
235
+ let pendingValues = [...initialValues];
236
+ let hasPendingUpdate = false;
237
+ // Upstream retains each setter proposal outside drag too. Rebase when rendered values change.
238
+ let nonDraggingRenderedSnapshot: readonly number[] | undefined;
239
+ const validIndex = (index: number) =>
240
+ Number.isInteger(index) && index >= 0 && index < values().length;
241
+ const getThumbValue = (index: number) => values()[index];
242
+ const getThumbMinValue = (index: number) => (index === 0 ? minValue() : values()[index - 1]);
243
+ const getThumbMaxValue = (index: number) =>
244
+ index === values().length - 1 ? maxValue() : values()[index + 1];
245
+ const emit = (kind: "onChange" | "onChangeEnd", next: readonly number[]) => {
173
246
  const p = getProps();
174
- const snappedValue = snapToStep(newValue, minValue(), maxValue(), step());
175
-
176
- if (!isControlled()) {
177
- setInternalValue(snappedValue);
178
- }
179
-
180
- p.onChange?.(snappedValue);
247
+ // The public overloads guarantee this callback shape for the established mode.
248
+ if (arrayMode) (p as SliderStateProps<number[]>)[kind]?.([...next]);
249
+ else (p as SliderStateProps)[kind]?.(next[0]);
181
250
  };
182
-
183
- // Set value by percent
184
- const setValuePercent = (percent: number) => {
185
- const clampedPercent = clamp(percent, 0, 1);
186
- const newValue = clampedPercent * (maxValue() - minValue()) + minValue();
187
- setValue(newValue);
251
+ const setThumbValue = (index: number, nextValue: number) => {
252
+ if (isDisabled() || !validIndex(index)) return;
253
+ const rendered = values();
254
+ const retainNonDraggingProposal =
255
+ arrayMode &&
256
+ isControlled() &&
257
+ nonDraggingRenderedSnapshot !== undefined &&
258
+ rendered.length === nonDraggingRenderedSnapshot.length &&
259
+ rendered.every((item, i) => item === nonDraggingRenderedSnapshot![i]);
260
+ const base = normalize(isDragging() || retainNonDraggingProposal ? pendingValues : rendered);
261
+ const lower = index === 0 ? minValue() : base[index - 1];
262
+ const upper = index === base.length - 1 ? maxValue() : base[index + 1];
263
+ // Current RangeSlider anchors snapping to the whole domain, then clamps to neighbors.
264
+ const snapped = clamp(snapToStep(nextValue, minValue(), maxValue(), step()), lower, upper);
265
+ const next = [...base];
266
+ next[index] = snapped;
267
+ pendingValues = next;
268
+ hasPendingUpdate = true;
269
+ nonDraggingRenderedSnapshot =
270
+ !isDragging() && arrayMode && isControlled() ? [...rendered] : undefined;
271
+ const changed = next.some((item, i) => item !== rendered[i]);
272
+ if (!isControlled() && (!arrayMode || changed)) setInternalValues(next);
273
+ // Scalar's duplicate callbacks and RangeSlider's equal-pair suppression are intentional.
274
+ if (!arrayMode || changed) emit("onChange", next);
188
275
  };
189
-
190
- // Dragging state management
191
- const setDragging = (dragging: boolean) => {
192
- const wasDragging = isDragging();
193
- setIsDragging(dragging);
194
-
195
- // Call onChangeEnd when dragging stops
196
- if (wasDragging && !dragging) {
197
- getProps().onChangeEnd?.(value());
276
+ const setThumbPercent = (index: number, percent: number) =>
277
+ setThumbValue(index, clamp(percent, 0, 1) * (maxValue() - minValue()) + minValue());
278
+ const getThumbPercent = (index: number) =>
279
+ (getThumbValue(index) - minValue()) / (maxValue() - minValue());
280
+ const isThumbDragging = (index: number) => draggings()[index] ?? false;
281
+ const setThumbDragging = (index: number, dragging: boolean) => {
282
+ if (dragging && (!validIndex(index) || (arrayMode && isDisabled()))) return;
283
+ const wasDragging = isThumbDragging(index);
284
+ if (wasDragging === dragging) return;
285
+ // One snapshot per drag session: a second handle cannot discard unechoed updates.
286
+ if (dragging && !isDragging()) {
287
+ pendingValues = [...values()];
288
+ hasPendingUpdate = false;
289
+ nonDraggingRenderedSnapshot = undefined;
290
+ }
291
+ const next = [...draggings()];
292
+ next[index] = dragging;
293
+ setDraggings(next);
294
+ // Allow disabled endings to release ownership; completion occurs only at the final end.
295
+ if (wasDragging && !isDragging()) {
296
+ emit("onChangeEnd", normalize(hasPendingUpdate ? pendingValues : values()));
198
297
  }
199
298
  };
200
-
201
- // Increment/decrement
202
- const increment = (stepMultiplier = 1) => {
203
- if (isDisabled()) return;
204
- setValue(value() + step() * stepMultiplier);
299
+ const setFocusedThumb = (index: number | undefined) => {
300
+ if (index === undefined || validIndex(index)) setFocusedThumbValue(index);
205
301
  };
206
-
207
- const decrement = (stepMultiplier = 1) => {
208
- if (isDisabled()) return;
209
- setValue(value() - step() * stepMultiplier);
210
- };
211
-
212
- // Set focused state
213
- const setFocused = (focused: boolean) => {
214
- setIsFocused(focused);
215
- };
216
-
302
+ const incrementThumb = (index: number, stepSize = 1) =>
303
+ setThumbValue(index, getThumbValue(index) + Math.max(stepSize, step()));
304
+ const decrementThumb = (index: number, stepSize = 1) =>
305
+ setThumbValue(index, getThumbValue(index) - Math.max(stepSize, step()));
306
+ const formatter = createMemo(
307
+ () => new Intl.NumberFormat(getProps().locale, getProps().formatOptions),
308
+ );
309
+ function getFormattedValue(): string;
310
+ function getFormattedValue(value: number | readonly number[]): string;
311
+ function getFormattedValue(value: number | readonly number[] = values()): string {
312
+ const items = typeof value === "number" ? [value] : value;
313
+ if (items.length === 0) return "";
314
+ if (items.length === 1) return formatter().format(items[0]);
315
+ if (items.length === 2) return formatter().formatRange(items[0], items[1]);
316
+ return new Intl.ListFormat(formatter().resolvedOptions().locale, { type: "unit" }).format(
317
+ items.map((item) => formatter().format(item)),
318
+ );
319
+ }
320
+ const value = () => getThumbValue(0) ?? minValue();
321
+ const setValue = (next: number) => setThumbValue(0, next);
217
322
  return {
323
+ values,
324
+ defaultValues,
325
+ getThumbValue,
326
+ setThumbValue,
327
+ setThumbPercent,
328
+ getThumbPercent,
329
+ getThumbMinValue,
330
+ getThumbMaxValue,
331
+ getThumbValueLabel: (index) => getFormattedValue(getThumbValue(index)),
332
+ isThumbDragging,
333
+ setThumbDragging,
334
+ focusedThumb,
335
+ setFocusedThumb,
336
+ incrementThumb,
337
+ decrementThumb,
218
338
  value,
219
339
  setValue,
220
- setValuePercent,
221
- getValuePercent,
340
+ setValuePercent: (percent) => setThumbPercent(0, percent),
341
+ getValuePercent: () => getThumbPercent(0),
222
342
  getFormattedValue,
223
343
  isDragging,
224
- setDragging,
225
- isFocused,
226
- setFocused,
227
- increment,
228
- decrement,
344
+ setDragging: (dragging) => setThumbDragging(0, dragging),
345
+ isFocused: () => focusedThumb() !== undefined,
346
+ setFocused: (focused) => {
347
+ if (focused) setFocusedThumb(0);
348
+ else if (focusedThumb() === 0) setFocusedThumb(undefined);
349
+ },
350
+ // Preserve even zero/negative scalar multiplier semantics, rather than using indexed delta.
351
+ increment: (multiplier = 1) => setValue(value() + step() * multiplier),
352
+ decrement: (multiplier = 1) => setValue(value() - step() * multiplier),
229
353
  get minValue() {
230
354
  return minValue();
231
355
  },
package/src/ssr/index.ts CHANGED
@@ -46,14 +46,21 @@ export function createIsSSR(): boolean {
46
46
  *
47
47
  * @param defaultId - Optional default ID to use instead of generating one.
48
48
  */
49
+ let fallbackIdCounter = 0;
50
+
49
51
  export function createId(defaultId?: string): string {
50
- // Generate first, choose second, like upstream `useId`: `createUniqueId` is
51
- // order-dependent in both of its branches, so returning early on `defaultId`
52
- // would shift every later id in the same render or hydration pass.
53
- const uniqueId = createUniqueId();
54
52
  if (defaultId) {
55
53
  return defaultId;
56
54
  }
55
+ let uniqueId: string | undefined;
56
+ try {
57
+ uniqueId = createUniqueId();
58
+ } catch {
59
+ // createUniqueId throws outside of a reactive context in server/hydration passes
60
+ }
61
+ if (!uniqueId) {
62
+ uniqueId = `nh-${++fallbackIdCounter}`;
63
+ }
57
64
  return `solid-stately-${uniqueId}`;
58
65
  }
59
66