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