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/time_picker_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,14 +62,100 @@ var MicrotaskCoalescer = class {
51
62
  }
52
63
  };
53
64
 
54
- // src/controllers/time_picker_controller.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
+ positive: { finite: true, exclusiveMin: 0 }};
103
+ function matchesNumberBounds(value, bounds) {
104
+ if (!Number.isFinite(value)) {
105
+ const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
106
+ if (direction === null) return false;
107
+ if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
108
+ }
109
+ if (bounds.min !== void 0 && value < bounds.min) return false;
110
+ if (bounds.max !== void 0 && value > bounds.max) return false;
111
+ if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
112
+ if (bounds.integer && !Number.isInteger(value)) return false;
113
+ if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
114
+ return true;
115
+ }
116
+
117
+ function readNumber(raw, fallback, bounds) {
118
+ return matchesNumberBounds(raw, bounds) ? raw : fallback;
119
+ }
120
+
121
+ var NumberValueReader = class {
122
+ #lastRejected = /* @__PURE__ */ new Map();
123
+ read(owner, name, raw, fallback, bounds) {
124
+ const resolved = readNumber(raw, fallback, bounds);
125
+ if (matchesNumberBounds(raw, bounds)) {
126
+ this.#lastRejected.delete(name);
127
+ return resolved;
128
+ }
129
+ const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
130
+ const literal = owner.element.getAttribute(attribute);
131
+ if (literal === null) {
132
+ this.#lastRejected.delete(name);
133
+ return resolved;
134
+ }
135
+ if (this.#lastRejected.get(name) !== literal) {
136
+ this.#lastRejected.set(name, literal);
137
+ console.warn(
138
+ `Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
139
+ );
140
+ }
141
+ return resolved;
142
+ }
143
+ };
144
+
145
+ function targetSelector(identifier, name) {
146
+ return `[data-${identifier}-target~="${name}"]`;
147
+ }
148
+
55
149
  var AM = 0;
56
150
  var PM = 1;
57
151
  var SECONDS_PER_MINUTE = 60;
58
152
  var SECONDS_PER_HOUR = 60 * SECONDS_PER_MINUTE;
59
153
  var SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR;
60
154
  var hasModifier = (event) => event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
61
- var TimePickerController = class extends Controller {
155
+ var TimePickerController = class _TimePickerController extends Controller {
156
+ #numbers = new NumberValueReader();
157
+ #moves = new MoveCounter();
158
+ #move = 0;
62
159
  static targets = ["segment", "field"];
63
160
  static values = {
64
161
  hourCycle: { type: Number, default: 24 },
@@ -66,34 +163,32 @@ var TimePickerController = class extends Controller {
66
163
  seconds: { type: Boolean, default: false },
67
164
  wrap: { type: Boolean, default: true }
68
165
  };
166
+ static valueConstraints = {
167
+ hourCycle: { finite: true, allowedValues: [12, 24] },
168
+ step: NUMBER_BOUNDS.positive
169
+ };
69
170
  static actions = ["onKeydown"];
70
171
  static events = ["change", "reconcile"];
71
- /** Collapses target, Value, and retained-attribute morphs into one silent render. */
72
- #reconcile = new MicrotaskCoalescer(() => this.#reconcileDom());
73
- /** Canonical state; the displayed hour and meridiem are derived from this value. */
172
+ #reconcile = new MorphRenderWatcher(() => this.#reconcileDom());
74
173
  #state = { hour: 0, minute: 0, second: 0 };
75
- /** Direct-entry digit buffer and the segment it belongs to. */
76
174
  #typeBuffer = "";
77
175
  #typeSegment = null;
78
- /** Last composed value, used to suppress duplicate user notifications. */
79
176
  #lastValue = "";
80
177
  #observer = null;
81
178
  #connected = false;
82
179
  #domDirty = false;
83
- /** Seeds state from the DOM, renders canonical ARIA, and starts morph observation. */
84
180
  connect() {
85
181
  this.#connected = true;
86
- this.#reconcile.activate();
182
+ this.#reconcile.observe(this.element);
87
183
  this.element.addEventListener("focusout", this.#onFocusOut);
88
184
  this.#domDirty = false;
89
185
  this.#adoptDomState();
90
186
  this.#render();
91
187
  this.#observeMutations();
92
188
  }
93
- /** Releases the observer, listener, queued reconciliation, and transient input state. */
94
189
  disconnect() {
95
190
  this.#connected = false;
96
- this.#reconcile.cancel();
191
+ this.#reconcile.disconnect();
97
192
  this.element.removeEventListener("focusout", this.#onFocusOut);
98
193
  this.#observer?.disconnect();
99
194
  this.#observer = null;
@@ -101,45 +196,37 @@ var TimePickerController = class extends Controller {
101
196
  this.#clearTypeBuffer();
102
197
  this.#lastValue = "";
103
198
  }
104
- /** Re-renders the canonical instant when 12/24-hour presentation changes at runtime. */
105
199
  hourCycleValueChanged() {
106
200
  this.#reconcile.schedule();
107
201
  }
108
- /** Recomposes the optional seconds portion after a runtime Value change. */
109
202
  secondsValueChanged() {
110
203
  this.#reconcile.schedule();
111
204
  }
112
- /** Adopts a segment inserted or replaced by a Turbo morph. */
113
205
  segmentTargetConnected() {
114
206
  this.#domDirty = true;
115
- this.#clearTypeBuffer();
116
207
  this.#reconcile.schedule();
117
208
  }
118
- /** Rebuilds state after a segment leaves the retained controller element. */
119
209
  segmentTargetDisconnected() {
120
210
  this.#domDirty = true;
121
- this.#clearTypeBuffer();
122
211
  this.#reconcile.schedule();
123
212
  }
124
- /** Reflects the current composed value into a field added or replaced at runtime. */
125
213
  fieldTargetConnected() {
126
214
  this.#reconcile.schedule();
127
215
  }
128
- /** Reconciles silently when the optional field leaves at runtime. */
129
216
  fieldTargetDisconnected() {
130
217
  this.#reconcile.schedule();
131
218
  }
132
- /** Handles APG stepping, inter-segment focus moves, jumps, and direct entry. */
133
219
  onKeydown(event) {
134
220
  if (isReservedArrowChord(event)) return;
135
221
  const segment = event.target?.closest(
136
- "[data-stimeo--time-picker-target~='segment']"
222
+ targetSelector(this.identifier, "segment")
137
223
  );
138
224
  if (!segment || !this.segmentTargets.includes(segment)) return;
139
225
  const kind = this.#kindOf(segment);
140
226
  if (!kind) return;
141
227
  const key = logicalArrowKey(event.key, this.element);
142
228
  if ((key === "Home" || key === "End") && hasModifier(event)) return;
229
+ if (this.#adoptPendingDom()) this.#reconcile.schedule();
143
230
  switch (key) {
144
231
  case "ArrowUp":
145
232
  event.preventDefault();
@@ -177,21 +264,18 @@ var TimePickerController = class extends Controller {
177
264
  }
178
265
  this.#clearTypeBuffer();
179
266
  }
180
- /** Clears direct-entry state when Tab, Shift+Tab, or pointer focus leaves a segment. */
181
267
  #onFocusOut = (event) => {
182
268
  const segment = event.target?.closest(
183
- "[data-stimeo--time-picker-target~='segment']"
269
+ targetSelector(this.identifier, "segment")
184
270
  );
185
271
  if (!segment || !this.segmentTargets.includes(segment)) return;
186
272
  this.#clearTypeBuffer();
187
273
  };
188
- /** Uses the normalized positive-integer minute step; other segments step by one. */
189
274
  #delta(kind) {
190
275
  if (kind !== "minute") return 1;
191
- const step = Math.trunc(this.stepValue);
192
- return Number.isFinite(step) && step > 0 ? step : 1;
276
+ const step = Math.trunc(this.#safeStep);
277
+ return step > 0 ? step : 1;
193
278
  }
194
- /** Mutates one complete action and then renders/notifies its final value once. */
195
279
  #step(kind, delta) {
196
280
  if (kind === "meridiem" && this.wrapValue) {
197
281
  this.#state.hour = modulo(this.#state.hour + 12, 24);
@@ -207,7 +291,6 @@ var TimePickerController = class extends Controller {
207
291
  }
208
292
  this.#commitRender();
209
293
  }
210
- /** Accumulates a typed digit, rendering once and advancing after completion. */
211
294
  #typeDigit(segment, kind, digit) {
212
295
  if (this.#typeSegment !== kind) this.#typeBuffer = "";
213
296
  this.#typeSegment = kind;
@@ -221,27 +304,30 @@ var TimePickerController = class extends Controller {
221
304
  this.#focusSibling(segment, 1);
222
305
  }
223
306
  }
224
- /** Moves focus to the previous/next valid segment, if one exists. */
225
307
  #focusSibling(segment, direction) {
226
308
  const segments = this.segmentTargets.filter((candidate) => this.#kindOf(candidate) !== null);
227
309
  const index = segments.indexOf(segment);
228
310
  segments[index + direction]?.focus();
229
311
  }
230
- /** Clears the direct-entry buffer and its owning segment together. */
231
312
  #clearTypeBuffer() {
232
313
  this.#typeBuffer = "";
233
314
  this.#typeSegment = null;
234
315
  }
235
- /** Reconciles one coalesced target, Value, or retained-attribute mutation batch. */
236
- #reconcileDom() {
237
- if (this.#domDirty) {
238
- this.#domDirty = false;
239
- this.#clearTypeBuffer();
240
- this.#adoptDomState();
316
+ #adoptPendingDom() {
317
+ const pending = this.#observer?.takeRecords() ?? [];
318
+ if (pending.some(({ target }) => this.segmentTargets.includes(target))) {
319
+ this.#domDirty = true;
241
320
  }
321
+ if (!this.#domDirty) return false;
322
+ this.#domDirty = false;
323
+ this.#clearTypeBuffer();
324
+ this.#adoptDomState();
325
+ return true;
326
+ }
327
+ #reconcileDom() {
328
+ this.#adoptPendingDom();
242
329
  this.#render();
243
330
  }
244
- /** Rebuilds canonical state from the targets; an absent hour represents midnight. */
245
331
  #adoptDomState() {
246
332
  let displayedHour = 0;
247
333
  let meridiem = AM;
@@ -257,51 +343,39 @@ var TimePickerController = class extends Controller {
257
343
  else meridiem = value;
258
344
  }
259
345
  this.#state = {
260
- hour: this.hourCycleValue === 12 ? displayedHour % 12 + (meridiem === PM ? 12 : 0) : displayedHour,
346
+ hour: this.#safeHourCycle === 12 ? displayedHour % 12 + (meridiem === PM ? 12 : 0) : displayedHour,
261
347
  minute,
262
348
  second
263
349
  };
264
350
  }
265
- /** Reads and integer-clamps one authored `aria-valuenow`. */
266
351
  #authoredValue(segment, kind) {
267
352
  const raw = Number(segment.getAttribute("aria-valuenow"));
268
353
  const { min, max } = this.#bounds(kind);
269
354
  if (!Number.isFinite(raw)) return min;
270
355
  return Math.min(max, Math.max(min, Math.trunc(raw)));
271
356
  }
272
- /** Writes a displayed segment value back into canonical state without rendering. */
273
357
  #setDisplayed(kind, raw) {
274
358
  const { min, max } = this.#bounds(kind);
275
359
  const value = Math.min(max, Math.max(min, Math.trunc(raw)));
276
360
  if (kind === "hour") {
277
- this.#state.hour = this.hourCycleValue === 12 ? value % 12 + (this.#state.hour >= 12 ? 12 : 0) : value;
361
+ this.#state.hour = this.#safeHourCycle === 12 ? value % 12 + (this.#state.hour >= 12 ? 12 : 0) : value;
278
362
  } else if (kind === "minute" || kind === "second") {
279
363
  this.#state[kind] = value;
280
364
  } else {
281
365
  this.#state.hour = this.#state.hour % 12 + (value === PM ? 12 : 0);
282
366
  }
283
367
  }
284
- /** Converts seconds with day wrapping into the canonical three-unit state. */
285
368
  #setFromSeconds(raw) {
286
369
  const seconds = modulo(raw, SECONDS_PER_DAY);
287
370
  this.#state.hour = Math.floor(seconds / SECONDS_PER_HOUR);
288
371
  this.#state.minute = Math.floor(seconds % SECONDS_PER_HOUR / SECONDS_PER_MINUTE);
289
372
  this.#state.second = seconds % SECONDS_PER_MINUTE;
290
373
  }
291
- /** Reflects canonical state to every segment without dispatching user events. */
292
374
  #renderSegments() {
293
375
  this.#withoutObservation(() => {
294
376
  for (const segment of this.segmentTargets) this.#renderSegment(segment);
295
377
  });
296
378
  }
297
- /**
298
- * Reflects canonical state during connection or DOM reconciliation.
299
- *
300
- * No user edit reaches here, so `change` never fires from this path. A pass
301
- * that moves the committed value reports `reconcile` instead.
302
- *
303
- * @stimeoRenderRoot
304
- */
305
379
  #render() {
306
380
  this.#renderSegments();
307
381
  const value = this.#composedValue;
@@ -309,19 +383,21 @@ var TimePickerController = class extends Controller {
309
383
  this.#writeField(value);
310
384
  this.#lastValue = value;
311
385
  if (previous !== "" && value !== previous) {
386
+ this.#move = this.#moves.record();
312
387
  this.dispatch("reconcile", { detail: { value } });
313
388
  }
314
389
  }
315
- /** Reflects one completed user action and dispatches its final change exactly once. */
316
390
  #commitRender() {
317
391
  this.#renderSegments();
318
392
  const value = this.#composedValue;
319
- const fieldChanged = this.#writeField(value);
320
- if (fieldChanged) this.fieldTarget.dispatchEvent(new Event("change", { bubbles: true }));
321
- if (value !== this.#lastValue) this.dispatch("change", { detail: { value } });
393
+ const changed = value !== this.#lastValue;
322
394
  this.#lastValue = value;
395
+ if (changed) this.#move = this.#moves.record();
396
+ const move = this.#move;
397
+ if (this.#writeField(value)) commitField(this.fieldTarget);
398
+ if (!this.#moves.isLatest(move)) return;
399
+ if (changed) this.dispatch("change", { detail: { value } });
323
400
  }
324
- /** Reflects one segment's value and controller-owned ARIA bounds/text. */
325
401
  #renderSegment(segment) {
326
402
  const kind = this.#kindOf(segment);
327
403
  if (!kind) return;
@@ -334,31 +410,25 @@ var TimePickerController = class extends Controller {
334
410
  segment.setAttribute("aria-valuemax", String(max));
335
411
  segment.textContent = text;
336
412
  }
337
- /** Writes a composed value to the optional form field and reports whether it changed. */
338
413
  #writeField(value) {
339
- if (!this.hasFieldTarget || this.fieldTarget.value === value) return false;
340
- this.fieldTarget.value = value;
341
- return true;
414
+ return this.hasFieldTarget && writeField(this.fieldTarget, value);
342
415
  }
343
- /** The canonical form value composed as `HH:MM[:SS]`. */
344
416
  get #composedValue() {
345
417
  const parts = [pad(this.#state.hour), pad(this.#state.minute)];
346
418
  if (this.secondsValue) parts.push(pad(this.#state.second));
347
419
  return parts.join(":");
348
420
  }
349
- /** Returns a canonical unit in the presentation form exposed by its segment. */
350
421
  #displayValue(kind) {
351
422
  if (kind === "hour") {
352
- return this.hourCycleValue === 12 ? this.#state.hour % 12 || 12 : this.#state.hour;
423
+ return this.#safeHourCycle === 12 ? this.#state.hour % 12 || 12 : this.#state.hour;
353
424
  }
354
425
  if (kind === "meridiem") return this.#state.hour >= 12 ? PM : AM;
355
426
  return this.#state[kind];
356
427
  }
357
- /** The inclusive controller-owned bounds for a segment kind. */
358
428
  #bounds(kind) {
359
429
  switch (kind) {
360
430
  case "hour":
361
- return this.hourCycleValue === 12 ? { min: 1, max: 12 } : { min: 0, max: 23 };
431
+ return this.#safeHourCycle === 12 ? { min: 1, max: 12 } : { min: 0, max: 23 };
362
432
  case "minute":
363
433
  case "second":
364
434
  return { min: 0, max: 59 };
@@ -366,7 +436,6 @@ var TimePickerController = class extends Controller {
366
436
  return { min: AM, max: PM };
367
437
  }
368
438
  }
369
- /** Watches retained segment state while excluding this controller's own reflections. */
370
439
  #observeMutations() {
371
440
  const observer = new MutationObserver((records) => {
372
441
  const changed = records.some(
@@ -374,13 +443,11 @@ var TimePickerController = class extends Controller {
374
443
  );
375
444
  if (!changed) return;
376
445
  this.#domDirty = true;
377
- this.#clearTypeBuffer();
378
446
  this.#reconcile.schedule();
379
447
  });
380
448
  this.#observer = observer;
381
449
  this.#observeWith(observer);
382
450
  }
383
- /** Registers retained attribute observation on the controller subtree. */
384
451
  #observeWith(observer) {
385
452
  observer.observe(this.element, {
386
453
  subtree: true,
@@ -388,7 +455,6 @@ var TimePickerController = class extends Controller {
388
455
  attributeFilter: ["aria-valuenow", "data-segment"]
389
456
  });
390
457
  }
391
- /** Temporarily pauses observation so derived ARIA writes cannot become DOM input. */
392
458
  #withoutObservation(run) {
393
459
  const observer = this.#observer;
394
460
  observer?.disconnect();
@@ -398,7 +464,6 @@ var TimePickerController = class extends Controller {
398
464
  if (observer && this.#connected) this.#observeWith(observer);
399
465
  }
400
466
  }
401
- /** Reads a segment's declared kind, or null when absent/invalid. */
402
467
  #kindOf(segment) {
403
468
  const kind = segment.getAttribute("data-segment");
404
469
  if (kind === "hour" || kind === "minute" || kind === "second" || kind === "meridiem") {
@@ -406,10 +471,27 @@ var TimePickerController = class extends Controller {
406
471
  }
407
472
  return null;
408
473
  }
409
- /** Canonical state as seconds since the start of its nominal day. */
410
474
  get #secondsOfDay() {
411
475
  return this.#state.hour * SECONDS_PER_HOUR + this.#state.minute * SECONDS_PER_MINUTE + this.#state.second;
412
476
  }
477
+ get #safeHourCycle() {
478
+ return this.#numbers.read(
479
+ this,
480
+ "hourCycle",
481
+ this.hourCycleValue,
482
+ _TimePickerController.values.hourCycle.default,
483
+ _TimePickerController.valueConstraints.hourCycle
484
+ );
485
+ }
486
+ get #safeStep() {
487
+ return this.#numbers.read(
488
+ this,
489
+ "step",
490
+ this.stepValue,
491
+ _TimePickerController.values.step.default,
492
+ _TimePickerController.valueConstraints.step
493
+ );
494
+ }
413
495
  };
414
496
  function modulo(value, modulus) {
415
497
  return (value % modulus + modulus) % modulus;
@@ -419,5 +501,4 @@ function pad(value) {
419
501
  }
420
502
 
421
503
  export { TimePickerController };
422
- //# sourceMappingURL=time_picker_controller.js.map
423
504
  //# sourceMappingURL=time_picker_controller.js.map