stimeo-ui 0.16.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 +31 -0
  3. data/README.md +8 -1
  4. data/dist/cable/index.js +363 -436
  5. data/dist/controllers/accordion_controller.js +166 -55
  6. data/dist/controllers/alert_dialog_controller.js +862 -221
  7. data/dist/controllers/announcer_controller.js +135 -198
  8. data/dist/controllers/aspect_ratio_controller.js +0 -10
  9. data/dist/controllers/auto_submit_controller.js +271 -127
  10. data/dist/controllers/avatar_controller.js +171 -105
  11. data/dist/controllers/breadcrumb_controller.js +0 -130
  12. data/dist/controllers/bulk_select_controller.js +262 -52
  13. data/dist/controllers/calendar_controller.js +175 -209
  14. data/dist/controllers/carousel_controller.js +461 -399
  15. data/dist/controllers/character_counter_controller.js +246 -156
  16. data/dist/controllers/checkbox_controller.js +32 -74
  17. data/dist/controllers/clipboard_controller.js +168 -147
  18. data/dist/controllers/collapsible_controller.js +301 -120
  19. data/dist/controllers/color_picker_controller.js +149 -139
  20. data/dist/controllers/combobox_controller.js +360 -133
  21. data/dist/controllers/command_palette_controller.js +970 -358
  22. data/dist/controllers/conditional_fields_controller.js +166 -124
  23. data/dist/controllers/confirm_controller.js +939 -248
  24. data/dist/controllers/context_menu_controller.js +256 -128
  25. data/dist/controllers/count_up_controller.js +73 -21
  26. data/dist/controllers/countdown_controller.js +183 -148
  27. data/dist/controllers/currency_input_controller.js +134 -158
  28. data/dist/controllers/data_grid_controller.js +114 -186
  29. data/dist/controllers/date_range_picker_controller.js +288 -206
  30. data/dist/controllers/dialog_controller.js +858 -223
  31. data/dist/controllers/direct_upload_controller.js +73 -153
  32. data/dist/controllers/dirty_form_controller.js +14 -48
  33. data/dist/controllers/dismissible_controller.js +175 -21
  34. data/dist/controllers/drawer_controller.js +933 -341
  35. data/dist/controllers/dropdown_controller.js +312 -82
  36. data/dist/controllers/editable_controller.js +158 -68
  37. data/dist/controllers/empty_state_controller.js +67 -53
  38. data/dist/controllers/file_dropzone_controller.js +311 -207
  39. data/dist/controllers/filter_controller.js +67 -39
  40. data/dist/controllers/flash_controller.js +172 -314
  41. data/dist/controllers/focus_controller.js +654 -240
  42. data/dist/controllers/form_field_controller.js +147 -132
  43. data/dist/controllers/form_validation_controller.js +13 -86
  44. data/dist/controllers/frame_loading_controller.js +264 -213
  45. data/dist/controllers/highlight_controller.js +107 -75
  46. data/dist/controllers/hover_card_controller.js +309 -118
  47. data/dist/controllers/idle_controller.js +267 -101
  48. data/dist/controllers/input_mask_controller.js +71 -72
  49. data/dist/controllers/intersection_controller.js +147 -106
  50. data/dist/controllers/lazy_frame_controller.js +9 -65
  51. data/dist/controllers/listbox_controller.js +306 -204
  52. data/dist/controllers/local_time_controller.js +54 -64
  53. data/dist/controllers/masonry_controller.js +70 -93
  54. data/dist/controllers/menu_controller.js +214 -156
  55. data/dist/controllers/menubar_controller.js +170 -329
  56. data/dist/controllers/meter_controller.js +151 -67
  57. data/dist/controllers/multi_select_controller.js +593 -358
  58. data/dist/controllers/navigation_menu_controller.js +124 -232
  59. data/dist/controllers/nested_form_controller.js +352 -182
  60. data/dist/controllers/network_status_controller.js +242 -54
  61. data/dist/controllers/number_input_controller.js +320 -263
  62. data/dist/controllers/optimistic_controller.js +16 -76
  63. data/dist/controllers/otp_controller.js +198 -193
  64. data/dist/controllers/overflow_indicator_controller.js +116 -113
  65. data/dist/controllers/overflow_menu_controller.js +343 -283
  66. data/dist/controllers/pagination_controller.js +295 -119
  67. data/dist/controllers/password_reveal_controller.js +389 -121
  68. data/dist/controllers/password_strength_controller.js +99 -153
  69. data/dist/controllers/persist_controller.js +237 -103
  70. data/dist/controllers/pointer_drag_controller.js +185 -165
  71. data/dist/controllers/popover_controller.js +236 -97
  72. data/dist/controllers/portal_controller.js +209 -76
  73. data/dist/controllers/preview_guard_controller.js +232 -116
  74. data/dist/controllers/progress_controller.js +125 -56
  75. data/dist/controllers/radio_group_controller.js +117 -137
  76. data/dist/controllers/range_slider_controller.js +192 -105
  77. data/dist/controllers/rating_controller.js +418 -207
  78. data/dist/controllers/read_more_controller.js +134 -63
  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 +255 -139
  83. data/dist/controllers/roving_controller.js +39 -84
  84. data/dist/controllers/scroll_area_controller.js +428 -225
  85. data/dist/controllers/scroll_restore_controller.js +0 -53
  86. data/dist/controllers/scroll_visibility_controller.js +395 -145
  87. data/dist/controllers/scrollspy_controller.js +181 -242
  88. data/dist/controllers/separator_controller.js +267 -140
  89. data/dist/controllers/sidebar_controller.js +1023 -386
  90. data/dist/controllers/skeleton_controller.js +248 -97
  91. data/dist/controllers/slider_controller.js +159 -79
  92. data/dist/controllers/smart_sticky_header_controller.js +162 -63
  93. data/dist/controllers/sortable_controller.js +0 -112
  94. data/dist/controllers/spinner_controller.js +277 -169
  95. data/dist/controllers/step_indicator_controller.js +99 -46
  96. data/dist/controllers/stepper_controller.js +114 -73
  97. data/dist/controllers/stick_to_bottom_controller.js +262 -138
  98. data/dist/controllers/sticky_observer_controller.js +214 -49
  99. data/dist/controllers/submit_once_controller.js +294 -205
  100. data/dist/controllers/switch_controller.js +33 -60
  101. data/dist/controllers/tabs_controller.js +35 -29
  102. data/dist/controllers/tags_input_controller.js +136 -145
  103. data/dist/controllers/textarea_autosize_controller.js +71 -58
  104. data/dist/controllers/theme_controller.js +130 -112
  105. data/dist/controllers/time_picker_controller.js +146 -80
  106. data/dist/controllers/toast_controller.js +186 -297
  107. data/dist/controllers/toggle_group_controller.js +95 -136
  108. data/dist/controllers/toolbar_controller.js +68 -98
  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 +150 -345
  112. data/dist/index.js +6370 -9346
  113. data/dist/positioning/index.js +148 -47
  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,7 +14,6 @@ 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/field_mirror.ts
22
17
  function writeField(field, value) {
23
18
  if (field.value === value) return false;
24
19
  field.value = value;
@@ -28,27 +23,33 @@ function commitField(target) {
28
23
  target.dispatchEvent(new Event("change", { bubbles: true }));
29
24
  }
30
25
 
31
- // src/utils/microtask_coalescer.ts
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
+
32
37
  var MicrotaskCoalescer = class {
33
38
  #run;
34
39
  #queued = false;
35
40
  #active = false;
36
41
  #generation = 0;
37
- /** @param run - the single reconciliation pass, invoked at most once per batch. */
38
42
  constructor(run) {
39
43
  this.#run = run;
40
44
  }
41
- /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
42
45
  activate() {
43
46
  this.#active = true;
44
47
  }
45
- /** Closes the window and drops any pending pass; call from `disconnect()`. */
46
48
  cancel() {
47
49
  this.#active = false;
48
50
  this.#queued = false;
49
51
  this.#generation += 1;
50
52
  }
51
- /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
52
53
  schedule() {
53
54
  if (!this.#active || this.#queued) return;
54
55
  this.#queued = true;
@@ -61,7 +62,87 @@ var MicrotaskCoalescer = class {
61
62
  }
62
63
  };
63
64
 
64
- // 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
+
65
146
  var OwnedPointerSession = class {
66
147
  #pointerId;
67
148
  #owner;
@@ -81,7 +162,6 @@ var OwnedPointerSession = class {
81
162
  } catch {
82
163
  }
83
164
  }
84
- /** Releases capture and listeners, then reports the end exactly once. */
85
165
  end() {
86
166
  this.#finish("teardown");
87
167
  }
@@ -106,7 +186,6 @@ var OwnedPointerSession = class {
106
186
  };
107
187
  };
108
188
 
109
- // src/utils/range.ts
110
189
  function rangeFraction(value, min, max) {
111
190
  const span = max - min;
112
191
  if (!(span > 0)) return 0;
@@ -122,7 +201,6 @@ function rangeFraction(value, min, max) {
122
201
  return Math.min(1, Math.max(0, fraction));
123
202
  }
124
203
 
125
- // src/utils/stepped_value.ts
126
204
  function effectiveStep(step) {
127
205
  return Number.isFinite(step) && step > 0 ? step : 1;
128
206
  }
@@ -228,12 +306,15 @@ function finiteFallback(min, max) {
228
306
  return 0;
229
307
  }
230
308
 
231
- // src/controllers/range_slider_controller.ts
232
309
  var START_PROPERTY = "--stimeo--range-slider-start";
233
310
  var END_PROPERTY = "--stimeo--range-slider-end";
234
311
  var DEFAULT_MIN = 0;
235
312
  var DEFAULT_MAX = 100;
236
- var RangeSliderController = class extends Controller {
313
+ var RangeSliderController = class _RangeSliderController extends Controller {
314
+ #numbers = new NumberValueReader();
315
+ #fieldWrites = /* @__PURE__ */ new WeakMap();
316
+ #moves = new MoveCounter();
317
+ #move = 0;
237
318
  static targets = ["track", "startThumb", "endThumb", "startField", "endField"];
238
319
  static values = {
239
320
  min: { type: Number, default: DEFAULT_MIN },
@@ -243,59 +324,45 @@ var RangeSliderController = class extends Controller {
243
324
  end: { type: Number, default: 100 },
244
325
  logicalTrack: { type: Boolean, default: false }
245
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
+ };
246
334
  static actions = ["onKeydown", "onPointerDown"];
247
335
  static events = ["change", "reconcile"];
248
- /** One initiating pointer owns each live drag and its stable target snapshot. */
249
336
  #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
337
  #settled = { start: DEFAULT_MIN, end: DEFAULT_MAX };
255
- /** Whether the consumer declared a mirroring track and the direction mirrors it. */
256
338
  get #mirrored() {
257
339
  return this.logicalTrackValue && isRtl(this.element);
258
340
  }
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
- */
341
+ #repaint = new MorphRenderWatcher(() => this.#render());
268
342
  connect() {
269
- this.#repaint.activate();
343
+ this.#repaint.observe(this.element);
270
344
  this.#settled = this.#currentPair(this.#effectiveRange);
271
345
  this.#render();
272
346
  }
273
- /** Cancels any active pointer drag so document listeners never leak. */
274
347
  disconnect() {
275
- this.#repaint.cancel();
348
+ this.#repaint.disconnect();
276
349
  this.#endDrag();
277
350
  }
278
- /** Repaints when application code (or a Turbo morph) changes `min` at runtime. */
279
351
  minValueChanged() {
280
352
  this.#repaint.schedule();
281
353
  }
282
- /** Repaints when application code (or a Turbo morph) changes `max` at runtime. */
283
354
  maxValueChanged() {
284
355
  this.#repaint.schedule();
285
356
  }
286
- /** Repaints when application code (or a Turbo morph) changes `step` at runtime. */
287
357
  stepValueChanged() {
288
358
  this.#repaint.schedule();
289
359
  }
290
- /** Repaints when application code (or a Turbo morph) changes `start` at runtime. */
291
360
  startValueChanged() {
292
361
  this.#repaint.schedule();
293
362
  }
294
- /** Repaints when application code (or a Turbo morph) changes `end` at runtime. */
295
363
  endValueChanged() {
296
364
  this.#repaint.schedule();
297
365
  }
298
- /** Hydrates a replacement start thumb and restores live-drag focus ownership. */
299
366
  startThumbTargetConnected(thumb) {
300
367
  const range = this.#effectiveRange;
301
368
  const pair = this.#currentPair(range);
@@ -307,11 +374,10 @@ var RangeSliderController = class extends Controller {
307
374
  }
308
375
  this.#repaint.schedule();
309
376
  }
310
- /** Drops a stale start-thumb reference without orphaning the track gesture. */
311
377
  startThumbTargetDisconnected(thumb) {
312
378
  if (this.#drag?.kind === "start" && this.#drag.thumb === thumb) this.#drag.thumb = null;
379
+ this.#repaint.schedule();
313
380
  }
314
- /** Hydrates a replacement end thumb and restores live-drag focus ownership. */
315
381
  endThumbTargetConnected(thumb) {
316
382
  const range = this.#effectiveRange;
317
383
  const pair = this.#currentPair(range);
@@ -323,28 +389,30 @@ var RangeSliderController = class extends Controller {
323
389
  }
324
390
  this.#repaint.schedule();
325
391
  }
326
- /** Fills a start field inserted or replaced at runtime on the next repaint. */
327
392
  startFieldTargetConnected() {
328
393
  this.#repaint.schedule();
329
394
  }
330
- /** Fills an end field inserted or replaced at runtime on the next repaint. */
395
+ startFieldTargetDisconnected() {
396
+ this.#repaint.schedule();
397
+ }
331
398
  endFieldTargetConnected() {
332
399
  this.#repaint.schedule();
333
400
  }
334
- /** Drops a stale end-thumb reference without orphaning the track gesture. */
401
+ endFieldTargetDisconnected() {
402
+ this.#repaint.schedule();
403
+ }
335
404
  endThumbTargetDisconnected(thumb) {
336
405
  if (this.#drag?.kind === "end" && this.#drag.thumb === thumb) this.#drag.thumb = null;
406
+ this.#repaint.schedule();
337
407
  }
338
- /** Ends a gesture whose geometry target disappeared or ceased being a target. */
339
408
  trackTargetDisconnected(track) {
340
409
  if (this.#drag?.track === track) this.#endDrag();
341
410
  }
342
- /** Keyboard stepping for whichever thumb is focused (the action's element). */
343
411
  onKeydown(event) {
344
412
  if (isReservedArrowChord(event)) return;
345
413
  const thumb = event.currentTarget;
346
- const isStart = this.hasStartThumbTarget && thumb === this.startThumbTarget;
347
- const isEnd = this.hasEndThumbTarget && thumb === this.endThumbTarget;
414
+ const isStart = this.startThumbTargets.includes(thumb);
415
+ const isEnd = this.endThumbTargets.includes(thumb);
348
416
  const effectiveRange = this.#effectiveRange;
349
417
  const pair = this.#currentPair(effectiveRange);
350
418
  let kind;
@@ -398,7 +466,6 @@ var RangeSliderController = class extends Controller {
398
466
  event.preventDefault();
399
467
  this.#moveThumb(kind, next);
400
468
  }
401
- /** Begins a pointer drag on the track, moving the thumb nearest the press. */
402
469
  onPointerDown(event) {
403
470
  if (event.button !== 0 || this.#drag || !this.hasTrackTarget || !this.hasStartThumbTarget || !this.hasEndThumbTarget) {
404
471
  return;
@@ -431,7 +498,6 @@ var RangeSliderController = class extends Controller {
431
498
  });
432
499
  this.#drag = drag;
433
500
  }
434
- /** Maps a pointer X coordinate to a raw value using the track geometry. */
435
501
  #valueFromClientX(clientX, mirrored, track) {
436
502
  const rect = track.getBoundingClientRect();
437
503
  if (rect.width === 0) return null;
@@ -440,12 +506,6 @@ var RangeSliderController = class extends Controller {
440
506
  const range = this.#effectiveRange;
441
507
  return range.min + fraction * (range.max - range.min);
442
508
  }
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
509
  #nearestThumb(value, pair) {
450
510
  const startDistance = Math.abs(value - pair.start);
451
511
  const endDistance = Math.abs(value - pair.end);
@@ -453,7 +513,6 @@ var RangeSliderController = class extends Controller {
453
513
  if (pair.start === pair.end && value > pair.start) return "end";
454
514
  return "start";
455
515
  }
456
- /** Moves one thumb to a new raw value, keeping the pair ordered. */
457
516
  #moveThumb(kind, raw) {
458
517
  const pair = this.#currentPair(this.#effectiveRange);
459
518
  if (kind === "start") {
@@ -462,17 +521,6 @@ var RangeSliderController = class extends Controller {
462
521
  this.#commit(pair.start, raw, "end");
463
522
  }
464
523
  }
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
524
  #commit(start, end, moving) {
477
525
  const range = this.#effectiveRange;
478
526
  let nextStart = snapSteppedValue(start, range);
@@ -484,47 +532,53 @@ var RangeSliderController = class extends Controller {
484
532
  const pair = { start: nextStart, end: nextEnd };
485
533
  const reported = this.#settled;
486
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;
487
538
  if (!Object.is(this.startValue, nextStart)) this.startValue = nextStart;
488
539
  if (!Object.is(this.endValue, nextEnd)) this.endValue = nextEnd;
489
540
  this.#renderPair(pair, range);
490
- this.#mirrorFields(pair, true);
491
- if (nextStart !== reported.start || nextEnd !== reported.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) {
492
547
  this.dispatch("change", { detail: { start: nextStart, end: nextEnd } });
493
548
  }
494
549
  }
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
550
  #render() {
503
551
  const range = this.#effectiveRange;
504
552
  const pair = this.#currentPair(range);
505
553
  this.#renderPair(pair, range);
506
- this.#mirrorFields(pair, false);
554
+ this.#mirrorFields(pair);
507
555
  const settled = this.#settled;
508
556
  if (pair.start === settled.start && pair.end === settled.end) return;
509
557
  this.#settled = pair;
558
+ this.#move = this.#moves.record();
510
559
  this.dispatch("reconcile", { detail: { start: pair.start, end: pair.end } });
511
560
  }
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) {
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 = [];
518
568
  if (this.hasStartFieldTarget) {
519
569
  const field = this.startFieldTarget;
520
- if (writeField(field, String(pair.start)) && notify) commitField(field);
570
+ const value = String(pair.start);
571
+ if (this.#writeField(field, value))
572
+ fields.push([field, value, this.#fieldWrites.get(field) ?? 0]);
521
573
  }
522
574
  if (this.hasEndFieldTarget) {
523
575
  const field = this.endFieldTarget;
524
- if (writeField(field, String(pair.end)) && notify) commitField(field);
576
+ const value = String(pair.end);
577
+ if (this.#writeField(field, value))
578
+ fields.push([field, value, this.#fieldWrites.get(field) ?? 0]);
525
579
  }
580
+ return fields;
526
581
  }
527
- /** Reflects one normalized pair onto both thumbs and CSS properties. */
528
582
  #renderPair(pair, range) {
529
583
  if (this.hasStartThumbTarget) {
530
584
  this.#renderStartThumb(this.startThumbTarget, pair, range);
@@ -534,24 +588,20 @@ var RangeSliderController = class extends Controller {
534
588
  }
535
589
  this.#renderFractions(pair, range);
536
590
  }
537
- /** Writes only changed ARIA for the lower thumb. */
538
591
  #renderStartThumb(thumb, pair, range) {
539
592
  this.#setAria(thumb, "aria-valuemin", range.min);
540
593
  this.#setAria(thumb, "aria-valuemax", pair.end);
541
594
  this.#setAria(thumb, "aria-valuenow", pair.start);
542
595
  }
543
- /** Writes only changed ARIA for the upper thumb. */
544
596
  #renderEndThumb(thumb, pair, range) {
545
597
  this.#setAria(thumb, "aria-valuemin", pair.start);
546
598
  this.#setAria(thumb, "aria-valuemax", range.max);
547
599
  this.#setAria(thumb, "aria-valuenow", pair.end);
548
600
  }
549
- /** Writes one numeric ARIA attribute only when its serialized value changed. */
550
601
  #setAria(thumb, name, value) {
551
602
  const next = String(value);
552
603
  if (thumb.getAttribute(name) !== next) thumb.setAttribute(name, next);
553
604
  }
554
- /** Writes the two behavior-only fraction hooks only when they changed. */
555
605
  #renderFractions(pair, range) {
556
606
  const start = String(rangeFraction(pair.start, range.min, range.max));
557
607
  const end = String(rangeFraction(pair.end, range.min, range.max));
@@ -562,33 +612,70 @@ var RangeSliderController = class extends Controller {
562
612
  this.element.style.setProperty(END_PROPERTY, end);
563
613
  }
564
614
  }
565
- /** Current normalized and ordered pair derived from live declarative Values. */
566
615
  #currentPair(range) {
567
- const rawStart = Number.isFinite(this.startValue) ? this.startValue : range.min;
568
- const rawEnd = Number.isFinite(this.endValue) ? this.endValue : range.max;
616
+ const rawStart = this.#safeStart;
617
+ const rawEnd = this.#safeEnd;
569
618
  const start = snapSteppedValue(rawStart, range);
570
619
  const end = snapSteppedValue(rawEnd, range);
571
620
  if (start <= end) return { start, end };
572
621
  return { start: end, end: start };
573
622
  }
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
623
  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 };
624
+ const min = this.#safeMin;
625
+ const authoredMax = this.#safeMax;
626
+ return { min, max: Math.max(min, authoredMax), step: this.#safeStep, base: min };
583
627
  }
584
- /** Ends the current pointer session without dispatching another change. */
585
628
  #endDrag() {
586
629
  const drag = this.#drag;
587
630
  this.#drag = null;
588
631
  drag?.pointer?.end();
589
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
+ }
590
678
  };
591
679
 
592
680
  export { RangeSliderController };
593
- //# sourceMappingURL=range_slider_controller.js.map
594
681
  //# sourceMappingURL=range_slider_controller.js.map