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