stimeo-ui 0.15.0 → 0.17.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 (115) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +209 -0
  3. data/README.md +128 -1
  4. data/dist/cable/index.js +392 -417
  5. data/dist/controllers/accordion_controller.js +228 -28
  6. data/dist/controllers/alert_dialog_controller.js +862 -221
  7. data/dist/controllers/announcer_controller.js +135 -191
  8. data/dist/controllers/aspect_ratio_controller.js +0 -10
  9. data/dist/controllers/auto_submit_controller.js +295 -110
  10. data/dist/controllers/avatar_controller.js +172 -106
  11. data/dist/controllers/breadcrumb_controller.js +0 -129
  12. data/dist/controllers/bulk_select_controller.js +264 -53
  13. data/dist/controllers/calendar_controller.js +346 -180
  14. data/dist/controllers/carousel_controller.js +587 -300
  15. data/dist/controllers/character_counter_controller.js +271 -143
  16. data/dist/controllers/checkbox_controller.js +77 -75
  17. data/dist/controllers/clipboard_controller.js +187 -108
  18. data/dist/controllers/collapsible_controller.js +374 -108
  19. data/dist/controllers/color_picker_controller.js +183 -127
  20. data/dist/controllers/combobox_controller.js +426 -109
  21. data/dist/controllers/command_palette_controller.js +994 -350
  22. data/dist/controllers/conditional_fields_controller.js +212 -125
  23. data/dist/controllers/confirm_controller.js +939 -245
  24. data/dist/controllers/context_menu_controller.js +275 -127
  25. data/dist/controllers/count_up_controller.js +73 -21
  26. data/dist/controllers/countdown_controller.js +235 -97
  27. data/dist/controllers/currency_input_controller.js +241 -118
  28. data/dist/controllers/data_grid_controller.js +229 -135
  29. data/dist/controllers/date_range_picker_controller.js +359 -165
  30. data/dist/controllers/dialog_controller.js +887 -225
  31. data/dist/controllers/direct_upload_controller.js +80 -142
  32. data/dist/controllers/dirty_form_controller.js +26 -47
  33. data/dist/controllers/dismissible_controller.js +175 -20
  34. data/dist/controllers/drawer_controller.js +963 -336
  35. data/dist/controllers/dropdown_controller.js +342 -85
  36. data/dist/controllers/editable_controller.js +182 -58
  37. data/dist/controllers/empty_state_controller.js +67 -53
  38. data/dist/controllers/file_dropzone_controller.js +426 -229
  39. data/dist/controllers/filter_controller.js +78 -36
  40. data/dist/controllers/flash_controller.js +328 -211
  41. data/dist/controllers/focus_controller.js +654 -239
  42. data/dist/controllers/form_field_controller.js +153 -136
  43. data/dist/controllers/form_validation_controller.js +29 -96
  44. data/dist/controllers/frame_loading_controller.js +263 -198
  45. data/dist/controllers/highlight_controller.js +119 -70
  46. data/dist/controllers/hover_card_controller.js +335 -118
  47. data/dist/controllers/idle_controller.js +316 -65
  48. data/dist/controllers/input_mask_controller.js +116 -61
  49. data/dist/controllers/intersection_controller.js +147 -103
  50. data/dist/controllers/lazy_frame_controller.js +9 -56
  51. data/dist/controllers/listbox_controller.js +413 -153
  52. data/dist/controllers/local_time_controller.js +54 -64
  53. data/dist/controllers/masonry_controller.js +70 -95
  54. data/dist/controllers/menu_controller.js +254 -167
  55. data/dist/controllers/menubar_controller.js +206 -331
  56. data/dist/controllers/meter_controller.js +159 -71
  57. data/dist/controllers/multi_select_controller.js +797 -388
  58. data/dist/controllers/navigation_menu_controller.js +156 -231
  59. data/dist/controllers/nested_form_controller.js +373 -174
  60. data/dist/controllers/network_status_controller.js +241 -45
  61. data/dist/controllers/number_input_controller.js +387 -227
  62. data/dist/controllers/optimistic_controller.js +28 -75
  63. data/dist/controllers/otp_controller.js +344 -196
  64. data/dist/controllers/overflow_indicator_controller.js +167 -93
  65. data/dist/controllers/overflow_menu_controller.js +408 -277
  66. data/dist/controllers/pagination_controller.js +342 -120
  67. data/dist/controllers/password_reveal_controller.js +424 -99
  68. data/dist/controllers/password_strength_controller.js +99 -153
  69. data/dist/controllers/persist_controller.js +252 -96
  70. data/dist/controllers/pointer_drag_controller.js +263 -164
  71. data/dist/controllers/popover_controller.js +267 -94
  72. data/dist/controllers/portal_controller.js +209 -72
  73. data/dist/controllers/preview_guard_controller.js +243 -112
  74. data/dist/controllers/progress_controller.js +132 -59
  75. data/dist/controllers/radio_group_controller.js +138 -133
  76. data/dist/controllers/range_slider_controller.js +245 -112
  77. data/dist/controllers/rating_controller.js +429 -194
  78. data/dist/controllers/read_more_controller.js +185 -47
  79. data/dist/controllers/reading_progress_controller.js +147 -135
  80. data/dist/controllers/relative_time_controller.js +145 -110
  81. data/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +97 -38
  82. data/dist/controllers/resizable_controller.js +294 -118
  83. data/dist/controllers/roving_controller.js +39 -84
  84. data/dist/controllers/scroll_area_controller.js +428 -221
  85. data/dist/controllers/scroll_restore_controller.js +0 -53
  86. data/dist/controllers/scroll_visibility_controller.js +395 -146
  87. data/dist/controllers/scrollspy_controller.js +181 -236
  88. data/dist/controllers/separator_controller.js +288 -132
  89. data/dist/controllers/sidebar_controller.js +1071 -375
  90. data/dist/controllers/skeleton_controller.js +248 -92
  91. data/dist/controllers/slider_controller.js +211 -96
  92. data/dist/controllers/smart_sticky_header_controller.js +163 -54
  93. data/dist/controllers/sortable_controller.js +13 -110
  94. data/dist/controllers/spinner_controller.js +277 -161
  95. data/dist/controllers/step_indicator_controller.js +107 -53
  96. data/dist/controllers/stepper_controller.js +171 -48
  97. data/dist/controllers/stick_to_bottom_controller.js +310 -116
  98. data/dist/controllers/sticky_observer_controller.js +214 -49
  99. data/dist/controllers/submit_once_controller.js +294 -192
  100. data/dist/controllers/switch_controller.js +107 -43
  101. data/dist/controllers/tabs_controller.js +50 -25
  102. data/dist/controllers/tags_input_controller.js +290 -149
  103. data/dist/controllers/textarea_autosize_controller.js +71 -58
  104. data/dist/controllers/theme_controller.js +148 -80
  105. data/dist/controllers/time_picker_controller.js +162 -81
  106. data/dist/controllers/toast_controller.js +365 -193
  107. data/dist/controllers/toggle_group_controller.js +194 -99
  108. data/dist/controllers/toolbar_controller.js +92 -90
  109. data/dist/controllers/tooltip_controller.js +185 -125
  110. data/dist/controllers/transition_controller.js +123 -115
  111. data/dist/controllers/tree_view_controller.js +248 -290
  112. data/dist/index.js +8649 -8572
  113. data/dist/positioning/index.js +148 -45
  114. data/lib/stimeo/ui/version.rb +1 -1
  115. metadata +3 -3
@@ -1,13 +1,9 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/range_slider_controller.ts
4
-
5
- // src/utils/logical_scroll.ts
6
3
  function isRtl(element) {
7
4
  return window.getComputedStyle(element).direction === "rtl";
8
5
  }
9
6
 
10
- // src/utils/arrow_step.ts
11
7
  function logicalArrowKey(key, element) {
12
8
  if (key !== "ArrowRight" && key !== "ArrowLeft") return key;
13
9
  if (!isRtl(element)) return key;
@@ -18,27 +14,42 @@ function isReservedArrowChord(event, allow = []) {
18
14
  return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
19
15
  }
20
16
 
21
- // src/utils/microtask_coalescer.ts
17
+ function writeField(field, value) {
18
+ if (field.value === value) return false;
19
+ field.value = value;
20
+ return true;
21
+ }
22
+ function commitField(target) {
23
+ target.dispatchEvent(new Event("change", { bubbles: true }));
24
+ }
25
+
26
+ var ListenerSet = class {
27
+ #abort = new AbortController();
28
+ add(target, type, handler, options) {
29
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
30
+ }
31
+ dispose() {
32
+ this.#abort.abort();
33
+ this.#abort = new AbortController();
34
+ }
35
+ };
36
+
22
37
  var MicrotaskCoalescer = class {
23
38
  #run;
24
39
  #queued = false;
25
40
  #active = false;
26
41
  #generation = 0;
27
- /** @param run - the single reconciliation pass, invoked at most once per batch. */
28
42
  constructor(run) {
29
43
  this.#run = run;
30
44
  }
31
- /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
32
45
  activate() {
33
46
  this.#active = true;
34
47
  }
35
- /** Closes the window and drops any pending pass; call from `disconnect()`. */
36
48
  cancel() {
37
49
  this.#active = false;
38
50
  this.#queued = false;
39
51
  this.#generation += 1;
40
52
  }
41
- /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
42
53
  schedule() {
43
54
  if (!this.#active || this.#queued) return;
44
55
  this.#queued = true;
@@ -51,60 +62,130 @@ var MicrotaskCoalescer = class {
51
62
  }
52
63
  };
53
64
 
54
- // src/utils/owned_pointer_session.ts
65
+ var MorphRenderWatcher = class {
66
+ #listeners = new ListenerSet();
67
+ #pass;
68
+ #element = null;
69
+ #onMorph = () => this.#pass.schedule();
70
+ constructor(run) {
71
+ this.#pass = new MicrotaskCoalescer(run);
72
+ }
73
+ observe(element) {
74
+ if (this.#element === element) return;
75
+ this.disconnect();
76
+ this.#element = element;
77
+ this.#pass.activate();
78
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
79
+ }
80
+ schedule() {
81
+ this.#pass.schedule();
82
+ }
83
+ disconnect() {
84
+ this.#listeners.dispose();
85
+ this.#pass.cancel();
86
+ this.#element = null;
87
+ }
88
+ };
89
+
90
+ var MoveCounter = class {
91
+ #sequence = 0;
92
+ record() {
93
+ this.#sequence += 1;
94
+ return this.#sequence;
95
+ }
96
+ isLatest(token) {
97
+ return token === this.#sequence;
98
+ }
99
+ };
100
+
101
+ var NUMBER_BOUNDS = {
102
+ finite: { finite: true },
103
+ positive: { finite: true, exclusiveMin: 0 }};
104
+ function matchesNumberBounds(value, bounds) {
105
+ if (!Number.isFinite(value)) {
106
+ const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
107
+ if (direction === null) return false;
108
+ if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
109
+ }
110
+ if (bounds.min !== void 0 && value < bounds.min) return false;
111
+ if (bounds.max !== void 0 && value > bounds.max) return false;
112
+ if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
113
+ if (bounds.integer && !Number.isInteger(value)) return false;
114
+ if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
115
+ return true;
116
+ }
117
+
118
+ function readNumber(raw, fallback, bounds) {
119
+ return matchesNumberBounds(raw, bounds) ? raw : fallback;
120
+ }
121
+
122
+ var NumberValueReader = class {
123
+ #lastRejected = /* @__PURE__ */ new Map();
124
+ read(owner, name, raw, fallback, bounds) {
125
+ const resolved = readNumber(raw, fallback, bounds);
126
+ if (matchesNumberBounds(raw, bounds)) {
127
+ this.#lastRejected.delete(name);
128
+ return resolved;
129
+ }
130
+ const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
131
+ const literal = owner.element.getAttribute(attribute);
132
+ if (literal === null) {
133
+ this.#lastRejected.delete(name);
134
+ return resolved;
135
+ }
136
+ if (this.#lastRejected.get(name) !== literal) {
137
+ this.#lastRejected.set(name, literal);
138
+ console.warn(
139
+ `Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
140
+ );
141
+ }
142
+ return resolved;
143
+ }
144
+ };
145
+
55
146
  var OwnedPointerSession = class {
56
- pointerId;
147
+ #pointerId;
57
148
  #owner;
58
149
  #handlers;
59
150
  #abort = new AbortController();
60
151
  #active = true;
61
152
  constructor(start, owner, handlers) {
62
- this.pointerId = start.pointerId;
153
+ this.#pointerId = start.pointerId;
63
154
  this.#owner = owner;
64
155
  this.#handlers = handlers;
65
156
  const { signal } = this.#abort;
66
157
  owner.ownerDocument.addEventListener("pointermove", this.#onMove, { signal });
67
158
  owner.ownerDocument.addEventListener("pointerup", this.#onEndEvent, { signal });
68
159
  owner.ownerDocument.addEventListener("pointercancel", this.#onEndEvent, { signal });
69
- owner.addEventListener("lostpointercapture", this.#onLostCapture, { signal });
70
160
  try {
71
- owner.setPointerCapture?.(this.pointerId);
161
+ owner.setPointerCapture?.(this.#pointerId);
72
162
  } catch {
73
163
  }
74
164
  }
75
- /** Whether this session still owns its pointer and listeners. */
76
- get active() {
77
- return this.#active;
78
- }
79
- /** Whether `event` belongs to the initiating pointer of the live session. */
80
- owns(event) {
81
- return this.#active && event.pointerId === this.pointerId;
82
- }
83
- /** Releases capture/listeners and invokes the end callback exactly once. */
84
165
  end() {
166
+ this.#finish("teardown");
167
+ }
168
+ #finish(kind) {
85
169
  if (!this.#active) return;
86
170
  this.#active = false;
87
171
  this.#abort.abort();
88
172
  try {
89
- this.#owner.releasePointerCapture?.(this.pointerId);
173
+ this.#owner.releasePointerCapture?.(this.#pointerId);
90
174
  } catch {
91
175
  }
92
- this.#handlers.end?.();
176
+ this.#handlers.end?.(kind);
177
+ }
178
+ #owns(event) {
179
+ return this.#active && event.pointerId === this.#pointerId;
93
180
  }
94
181
  #onMove = (event) => {
95
- if (this.owns(event)) this.#handlers.move(event);
182
+ if (this.#owns(event)) this.#handlers.move(event);
96
183
  };
97
184
  #onEndEvent = (event) => {
98
- if (this.owns(event)) this.end();
99
- };
100
- #onLostCapture = (event) => {
101
- const pointerId = event.pointerId;
102
- if (typeof pointerId === "number" && pointerId !== this.pointerId) return;
103
- this.end();
185
+ if (this.#owns(event)) this.#finish(event.type === "pointerup" ? "up" : "cancel");
104
186
  };
105
187
  };
106
188
 
107
- // src/utils/range.ts
108
189
  function rangeFraction(value, min, max) {
109
190
  const span = max - min;
110
191
  if (!(span > 0)) return 0;
@@ -120,7 +201,6 @@ function rangeFraction(value, min, max) {
120
201
  return Math.min(1, Math.max(0, fraction));
121
202
  }
122
203
 
123
- // src/utils/stepped_value.ts
124
204
  function effectiveStep(step) {
125
205
  return Number.isFinite(step) && step > 0 ? step : 1;
126
206
  }
@@ -226,13 +306,16 @@ function finiteFallback(min, max) {
226
306
  return 0;
227
307
  }
228
308
 
229
- // src/controllers/range_slider_controller.ts
230
309
  var START_PROPERTY = "--stimeo--range-slider-start";
231
310
  var END_PROPERTY = "--stimeo--range-slider-end";
232
311
  var DEFAULT_MIN = 0;
233
312
  var DEFAULT_MAX = 100;
234
- var RangeSliderController = class extends Controller {
235
- static targets = ["track", "startThumb", "endThumb"];
313
+ var RangeSliderController = class _RangeSliderController extends Controller {
314
+ #numbers = new NumberValueReader();
315
+ #fieldWrites = /* @__PURE__ */ new WeakMap();
316
+ #moves = new MoveCounter();
317
+ #move = 0;
318
+ static targets = ["track", "startThumb", "endThumb", "startField", "endField"];
236
319
  static values = {
237
320
  min: { type: Number, default: DEFAULT_MIN },
238
321
  max: { type: Number, default: DEFAULT_MAX },
@@ -241,51 +324,45 @@ var RangeSliderController = class extends Controller {
241
324
  end: { type: Number, default: 100 },
242
325
  logicalTrack: { type: Boolean, default: false }
243
326
  };
327
+ static valueConstraints = {
328
+ min: NUMBER_BOUNDS.finite,
329
+ max: NUMBER_BOUNDS.finite,
330
+ step: NUMBER_BOUNDS.positive,
331
+ start: NUMBER_BOUNDS.finite,
332
+ end: NUMBER_BOUNDS.finite
333
+ };
244
334
  static actions = ["onKeydown", "onPointerDown"];
245
- static events = ["change"];
246
- /** One initiating pointer owns each live drag and its stable target snapshot. */
335
+ static events = ["change", "reconcile"];
247
336
  #drag = null;
248
- /** Whether the consumer declared a mirroring track and the direction mirrors it. */
337
+ #settled = { start: DEFAULT_MIN, end: DEFAULT_MAX };
249
338
  get #mirrored() {
250
339
  return this.logicalTrackValue && isRtl(this.element);
251
340
  }
252
- /**
253
- * Collapses a morph that swaps render inputs into one repaint, and refuses the
254
- * pass Stimulus delivers before `connect()`.
255
- */
256
- #repaint = new MicrotaskCoalescer(() => this.#render());
341
+ #repaint = new MorphRenderWatcher(() => this.#render());
257
342
  connect() {
258
- this.#repaint.activate();
259
- const range = this.#effectiveRange;
260
- const pair = this.#currentPair(range);
261
- this.#commit(pair.start, pair.end, null, false);
343
+ this.#repaint.observe(this.element);
344
+ this.#settled = this.#currentPair(this.#effectiveRange);
345
+ this.#render();
262
346
  }
263
- /** Cancels any active pointer drag so document listeners never leak. */
264
347
  disconnect() {
265
- this.#repaint.cancel();
348
+ this.#repaint.disconnect();
266
349
  this.#endDrag();
267
350
  }
268
- /** Repaints when application code (or a Turbo morph) changes `min` at runtime. */
269
351
  minValueChanged() {
270
352
  this.#repaint.schedule();
271
353
  }
272
- /** Repaints when application code (or a Turbo morph) changes `max` at runtime. */
273
354
  maxValueChanged() {
274
355
  this.#repaint.schedule();
275
356
  }
276
- /** Repaints when application code (or a Turbo morph) changes `step` at runtime. */
277
357
  stepValueChanged() {
278
358
  this.#repaint.schedule();
279
359
  }
280
- /** Repaints when application code (or a Turbo morph) changes `start` at runtime. */
281
360
  startValueChanged() {
282
361
  this.#repaint.schedule();
283
362
  }
284
- /** Repaints when application code (or a Turbo morph) changes `end` at runtime. */
285
363
  endValueChanged() {
286
364
  this.#repaint.schedule();
287
365
  }
288
- /** Hydrates a replacement start thumb and restores live-drag focus ownership. */
289
366
  startThumbTargetConnected(thumb) {
290
367
  const range = this.#effectiveRange;
291
368
  const pair = this.#currentPair(range);
@@ -297,11 +374,10 @@ var RangeSliderController = class extends Controller {
297
374
  }
298
375
  this.#repaint.schedule();
299
376
  }
300
- /** Drops a stale start-thumb reference without orphaning the track gesture. */
301
377
  startThumbTargetDisconnected(thumb) {
302
378
  if (this.#drag?.kind === "start" && this.#drag.thumb === thumb) this.#drag.thumb = null;
379
+ this.#repaint.schedule();
303
380
  }
304
- /** Hydrates a replacement end thumb and restores live-drag focus ownership. */
305
381
  endThumbTargetConnected(thumb) {
306
382
  const range = this.#effectiveRange;
307
383
  const pair = this.#currentPair(range);
@@ -313,20 +389,30 @@ var RangeSliderController = class extends Controller {
313
389
  }
314
390
  this.#repaint.schedule();
315
391
  }
316
- /** Drops a stale end-thumb reference without orphaning the track gesture. */
392
+ startFieldTargetConnected() {
393
+ this.#repaint.schedule();
394
+ }
395
+ startFieldTargetDisconnected() {
396
+ this.#repaint.schedule();
397
+ }
398
+ endFieldTargetConnected() {
399
+ this.#repaint.schedule();
400
+ }
401
+ endFieldTargetDisconnected() {
402
+ this.#repaint.schedule();
403
+ }
317
404
  endThumbTargetDisconnected(thumb) {
318
405
  if (this.#drag?.kind === "end" && this.#drag.thumb === thumb) this.#drag.thumb = null;
406
+ this.#repaint.schedule();
319
407
  }
320
- /** Ends a gesture whose geometry target disappeared or ceased being a target. */
321
408
  trackTargetDisconnected(track) {
322
409
  if (this.#drag?.track === track) this.#endDrag();
323
410
  }
324
- /** Keyboard stepping for whichever thumb is focused (the action's element). */
325
411
  onKeydown(event) {
326
412
  if (isReservedArrowChord(event)) return;
327
413
  const thumb = event.currentTarget;
328
- const isStart = this.hasStartThumbTarget && thumb === this.startThumbTarget;
329
- const isEnd = this.hasEndThumbTarget && thumb === this.endThumbTarget;
414
+ const isStart = this.startThumbTargets.includes(thumb);
415
+ const isEnd = this.endThumbTargets.includes(thumb);
330
416
  const effectiveRange = this.#effectiveRange;
331
417
  const pair = this.#currentPair(effectiveRange);
332
418
  let kind;
@@ -380,7 +466,6 @@ var RangeSliderController = class extends Controller {
380
466
  event.preventDefault();
381
467
  this.#moveThumb(kind, next);
382
468
  }
383
- /** Begins a pointer drag on the track, moving the thumb nearest the press. */
384
469
  onPointerDown(event) {
385
470
  if (event.button !== 0 || this.#drag || !this.hasTrackTarget || !this.hasStartThumbTarget || !this.hasEndThumbTarget) {
386
471
  return;
@@ -413,7 +498,6 @@ var RangeSliderController = class extends Controller {
413
498
  });
414
499
  this.#drag = drag;
415
500
  }
416
- /** Maps a pointer X coordinate to a raw value using the track geometry. */
417
501
  #valueFromClientX(clientX, mirrored, track) {
418
502
  const rect = track.getBoundingClientRect();
419
503
  if (rect.width === 0) return null;
@@ -422,12 +506,6 @@ var RangeSliderController = class extends Controller {
422
506
  const range = this.#effectiveRange;
423
507
  return range.min + fraction * (range.max - range.min);
424
508
  }
425
- /**
426
- * Chooses the closest thumb while keeping an overlapped pair expandable.
427
- * Ordinary midpoint ties stay deterministic on `start`; at an overlap, a
428
- * press above the shared value selects `end` and a press below selects
429
- * `start`.
430
- */
431
509
  #nearestThumb(value, pair) {
432
510
  const startDistance = Math.abs(value - pair.start);
433
511
  const endDistance = Math.abs(value - pair.end);
@@ -435,50 +513,72 @@ var RangeSliderController = class extends Controller {
435
513
  if (pair.start === pair.end && value > pair.start) return "end";
436
514
  return "start";
437
515
  }
438
- /** Moves one thumb to a new raw value, keeping the pair ordered. */
439
516
  #moveThumb(kind, raw) {
440
517
  const pair = this.#currentPair(this.#effectiveRange);
441
518
  if (kind === "start") {
442
- this.#commit(raw, pair.end, "start", true);
519
+ this.#commit(raw, pair.end, "start");
443
520
  } else {
444
- this.#commit(pair.start, raw, "end", true);
521
+ this.#commit(pair.start, raw, "end");
445
522
  }
446
523
  }
447
- /**
448
- * Clamps and snaps `start`/`end`, enforces `start ≤ end`, stores the pair, and
449
- * reflects it onto the thumbs' ARIA attributes and the range custom
450
- * properties. Dispatches `stimeo--range-slider:change` only when a
451
- * user-driven update changes the normalized pair, with
452
- * `{ start: number, end: number }` in `detail`.
453
- */
454
- #commit(start, end, moving, notify) {
524
+ #commit(start, end, moving) {
455
525
  const range = this.#effectiveRange;
456
- const previous = this.#currentPair(range);
457
526
  let nextStart = snapSteppedValue(start, range);
458
527
  let nextEnd = snapSteppedValue(end, range);
459
528
  if (nextStart > nextEnd) {
460
529
  if (moving === "start") nextStart = nextEnd;
461
- else if (moving === "end") nextEnd = nextStart;
462
- else [nextStart, nextEnd] = [nextEnd, nextStart];
530
+ else nextEnd = nextStart;
463
531
  }
532
+ const pair = { start: nextStart, end: nextEnd };
533
+ const reported = this.#settled;
534
+ this.#settled = pair;
535
+ const changed = nextStart !== reported.start || nextEnd !== reported.end;
536
+ if (changed) this.#move = this.#moves.record();
537
+ const move = this.#move;
464
538
  if (!Object.is(this.startValue, nextStart)) this.startValue = nextStart;
465
539
  if (!Object.is(this.endValue, nextEnd)) this.endValue = nextEnd;
466
- const pair = { start: nextStart, end: nextEnd };
467
540
  this.#renderPair(pair, range);
468
- if (notify && (nextStart !== previous.start || nextEnd !== previous.end)) {
541
+ const fields = this.#mirrorFields(pair);
542
+ for (const [field, value, write] of fields) {
543
+ if (field.value === value && this.#fieldWrites.get(field) === write) commitField(field);
544
+ }
545
+ if (!this.#moves.isLatest(move)) return;
546
+ if (changed) {
469
547
  this.dispatch("change", { detail: { start: nextStart, end: nextEnd } });
470
548
  }
471
549
  }
472
- /**
473
- * Reflects morph-supplied Values without writing them back or dispatching.
474
- *
475
- * @stimeoRenderRoot
476
- */
477
550
  #render() {
478
551
  const range = this.#effectiveRange;
479
- this.#renderPair(this.#currentPair(range), range);
552
+ const pair = this.#currentPair(range);
553
+ this.#renderPair(pair, range);
554
+ this.#mirrorFields(pair);
555
+ const settled = this.#settled;
556
+ if (pair.start === settled.start && pair.end === settled.end) return;
557
+ this.#settled = pair;
558
+ this.#move = this.#moves.record();
559
+ this.dispatch("reconcile", { detail: { start: pair.start, end: pair.end } });
560
+ }
561
+ #writeField(field, value) {
562
+ if (!writeField(field, value)) return false;
563
+ this.#fieldWrites.set(field, (this.#fieldWrites.get(field) ?? 0) + 1);
564
+ return true;
565
+ }
566
+ #mirrorFields(pair) {
567
+ const fields = [];
568
+ if (this.hasStartFieldTarget) {
569
+ const field = this.startFieldTarget;
570
+ const value = String(pair.start);
571
+ if (this.#writeField(field, value))
572
+ fields.push([field, value, this.#fieldWrites.get(field) ?? 0]);
573
+ }
574
+ if (this.hasEndFieldTarget) {
575
+ const field = this.endFieldTarget;
576
+ const value = String(pair.end);
577
+ if (this.#writeField(field, value))
578
+ fields.push([field, value, this.#fieldWrites.get(field) ?? 0]);
579
+ }
580
+ return fields;
480
581
  }
481
- /** Reflects one normalized pair onto both thumbs and CSS properties. */
482
582
  #renderPair(pair, range) {
483
583
  if (this.hasStartThumbTarget) {
484
584
  this.#renderStartThumb(this.startThumbTarget, pair, range);
@@ -488,24 +588,20 @@ var RangeSliderController = class extends Controller {
488
588
  }
489
589
  this.#renderFractions(pair, range);
490
590
  }
491
- /** Writes only changed ARIA for the lower thumb. */
492
591
  #renderStartThumb(thumb, pair, range) {
493
592
  this.#setAria(thumb, "aria-valuemin", range.min);
494
593
  this.#setAria(thumb, "aria-valuemax", pair.end);
495
594
  this.#setAria(thumb, "aria-valuenow", pair.start);
496
595
  }
497
- /** Writes only changed ARIA for the upper thumb. */
498
596
  #renderEndThumb(thumb, pair, range) {
499
597
  this.#setAria(thumb, "aria-valuemin", pair.start);
500
598
  this.#setAria(thumb, "aria-valuemax", range.max);
501
599
  this.#setAria(thumb, "aria-valuenow", pair.end);
502
600
  }
503
- /** Writes one numeric ARIA attribute only when its serialized value changed. */
504
601
  #setAria(thumb, name, value) {
505
602
  const next = String(value);
506
603
  if (thumb.getAttribute(name) !== next) thumb.setAttribute(name, next);
507
604
  }
508
- /** Writes the two behavior-only fraction hooks only when they changed. */
509
605
  #renderFractions(pair, range) {
510
606
  const start = String(rangeFraction(pair.start, range.min, range.max));
511
607
  const end = String(rangeFraction(pair.end, range.min, range.max));
@@ -516,33 +612,70 @@ var RangeSliderController = class extends Controller {
516
612
  this.element.style.setProperty(END_PROPERTY, end);
517
613
  }
518
614
  }
519
- /** Current normalized and ordered pair derived from live declarative Values. */
520
615
  #currentPair(range) {
521
- const rawStart = Number.isFinite(this.startValue) ? this.startValue : range.min;
522
- const rawEnd = Number.isFinite(this.endValue) ? this.endValue : range.max;
616
+ const rawStart = this.#safeStart;
617
+ const rawEnd = this.#safeEnd;
523
618
  const start = snapSteppedValue(rawStart, range);
524
619
  const end = snapSteppedValue(rawEnd, range);
525
620
  if (start <= end) return { start, end };
526
621
  return { start: end, end: start };
527
622
  }
528
- /**
529
- * Finite ordered range used by ARIA, keyboard, pointer, and CSS reflection.
530
- * Invalid endpoints fall back to the public defaults; an authored `max`
531
- * below `min` collapses to the finite minimum.
532
- */
533
623
  get #effectiveRange() {
534
- const min = Number.isFinite(this.minValue) ? this.minValue : DEFAULT_MIN;
535
- const authoredMax = Number.isFinite(this.maxValue) ? this.maxValue : DEFAULT_MAX;
536
- return { min, max: Math.max(min, authoredMax), step: this.stepValue, base: min };
624
+ const min = this.#safeMin;
625
+ const authoredMax = this.#safeMax;
626
+ return { min, max: Math.max(min, authoredMax), step: this.#safeStep, base: min };
537
627
  }
538
- /** Ends the current pointer session without dispatching another change. */
539
628
  #endDrag() {
540
629
  const drag = this.#drag;
541
630
  this.#drag = null;
542
631
  drag?.pointer?.end();
543
632
  }
633
+ get #safeMin() {
634
+ return this.#numbers.read(
635
+ this,
636
+ "min",
637
+ this.minValue,
638
+ _RangeSliderController.values.min.default,
639
+ _RangeSliderController.valueConstraints.min
640
+ );
641
+ }
642
+ get #safeMax() {
643
+ return this.#numbers.read(
644
+ this,
645
+ "max",
646
+ this.maxValue,
647
+ _RangeSliderController.values.max.default,
648
+ _RangeSliderController.valueConstraints.max
649
+ );
650
+ }
651
+ get #safeStep() {
652
+ return this.#numbers.read(
653
+ this,
654
+ "step",
655
+ this.stepValue,
656
+ _RangeSliderController.values.step.default,
657
+ _RangeSliderController.valueConstraints.step
658
+ );
659
+ }
660
+ get #safeStart() {
661
+ return this.#numbers.read(
662
+ this,
663
+ "start",
664
+ this.startValue,
665
+ this.#effectiveRange.min,
666
+ _RangeSliderController.valueConstraints.start
667
+ );
668
+ }
669
+ get #safeEnd() {
670
+ return this.#numbers.read(
671
+ this,
672
+ "end",
673
+ this.endValue,
674
+ this.#effectiveRange.max,
675
+ _RangeSliderController.valueConstraints.end
676
+ );
677
+ }
544
678
  };
545
679
 
546
680
  export { RangeSliderController };
547
- //# sourceMappingURL=range_slider_controller.js.map
548
681
  //# sourceMappingURL=range_slider_controller.js.map