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,84 +1,157 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/number_input_controller.ts
4
-
5
- // src/utils/arrow_step.ts
6
3
  function isReservedArrowChord(event, allow = []) {
7
4
  if (!event.key.startsWith("Arrow")) return false;
8
5
  return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
9
6
  }
10
7
 
11
- // src/utils/attribute_lease.ts
12
- var AttributeLease = class {
8
+ function sharedRegistry(key, create) {
9
+ const symbol = Symbol.for(key);
10
+ const scope = globalThis;
11
+ const existing = scope[symbol];
12
+ if (existing !== void 0) return existing;
13
+ const registry = create();
14
+ Object.defineProperty(globalThis, symbol, { value: registry });
15
+ return registry;
16
+ }
17
+
18
+ function isAttributeValue(value) {
19
+ return value === null || typeof value === "string";
20
+ }
21
+ function readLeaseRecord(element, record) {
22
+ try {
23
+ const parsed = JSON.parse(element.getAttribute(record) ?? "null");
24
+ if (!Array.isArray(parsed)) return void 0;
25
+ const [original, written, ...rest] = parsed;
26
+ const linked = rest[0] !== false;
27
+ const beneath = linked ? rest : rest.slice(1);
28
+ if (!beneath.every((under) => typeof under === "string")) return void 0;
29
+ return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
30
+ } catch {
31
+ return void 0;
32
+ }
33
+ }
34
+ var AttributeLease = class _AttributeLease {
35
+ static #registry = sharedRegistry(
36
+ "stimeo-ui.attribute-lease.registry.v1",
37
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
38
+ );
13
39
  #attribute;
14
- #records = /* @__PURE__ */ new Map();
15
- /** @param attribute - The attribute whose temporary values this lease owns. */
16
- constructor(attribute) {
40
+ #record;
41
+ #base;
42
+ #held = /* @__PURE__ */ new Set();
43
+ constructor(attribute, owner, options = {}) {
17
44
  this.#attribute = attribute;
45
+ this.#record = `data-${owner}-${attribute}-lease`;
46
+ this.#base = options.base === true;
47
+ const names = _AttributeLease.#registry.names;
48
+ names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
18
49
  }
19
- /** Writes or removes the leased attribute while preserving its authored value. */
20
50
  write(element, value) {
21
- const existing = this.#records.get(element);
22
- if (existing) {
23
- existing.written = value;
24
- } else {
25
- this.#records.set(element, {
26
- original: element.getAttribute(this.#attribute),
27
- written: value
28
- });
29
- }
30
- this.#reflect(element, value);
51
+ const stack = this.#stack(element);
52
+ const hold = this.#take(stack) ?? this.#acquire(element, stack);
53
+ this.#held.add(element);
54
+ const shows = !this.#covered(element, stack, hold);
55
+ hold.written = value;
56
+ this.#mark(element, stack);
57
+ if (shows) this.#reflect(element, value);
31
58
  }
32
- /** Returns one lease without overwriting a value subsequently authored by a consumer. */
33
59
  return(element) {
34
- const record = this.#records.get(element);
35
- if (!record) return;
36
- this.#records.delete(element);
37
- const stillOwned = element.getAttribute(this.#attribute) === record.written;
38
- if (stillOwned) this.#reflect(element, record.original);
60
+ const held = this.#held.delete(element);
61
+ const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
62
+ const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
63
+ const hold = this.#take(stack);
64
+ if (!hold) return false;
65
+ const carries = element.getAttribute(this.#attribute) === hold.written;
66
+ const owns = carries && !this.#covered(element, stack, hold);
67
+ const index = stack.indexOf(hold);
68
+ stack.splice(index, 1);
69
+ const above = stack[index];
70
+ if (above) above.linked &&= hold.linked;
71
+ this.#mark(element, stack);
72
+ if (!owns) return true;
73
+ const below = hold.linked ? stack[index - 1] : void 0;
74
+ this.#reflect(element, below ? below.written : hold.original);
75
+ return true;
76
+ }
77
+ returnAll() {
78
+ for (const element of Array.from(this.#held)) this.return(element);
79
+ }
80
+ #stack(element) {
81
+ const stacks = _AttributeLease.#registry.stacks;
82
+ const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
83
+ stacks.set(element, byAttribute);
84
+ const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
85
+ byAttribute.set(this.#attribute, stack);
86
+ return stack;
87
+ }
88
+ #seed(element) {
89
+ const found = [];
90
+ for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
91
+ const read = readLeaseRecord(element, record);
92
+ if (read) found.push({ ...read, record });
93
+ }
94
+ const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
95
+ return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
96
+ lease: null,
97
+ record,
98
+ written,
99
+ original,
100
+ linked: linked && index > 0
101
+ }));
102
+ }
103
+ #take(stack) {
104
+ const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
105
+ if (hold) hold.lease = this;
106
+ return hold;
107
+ }
108
+ #acquire(element, stack) {
109
+ const current = element.getAttribute(this.#attribute);
110
+ const top = stack.at(-1);
111
+ const bottom = stack[0];
112
+ const over = top?.written === current ? top : void 0;
113
+ const from = this.#base ? bottom : over;
114
+ const hold = {
115
+ lease: this,
116
+ record: this.#record,
117
+ written: current,
118
+ original: from ? from.original : current,
119
+ linked: !this.#base && over !== void 0
120
+ };
121
+ if (this.#base && bottom) {
122
+ bottom.linked = true;
123
+ stack.unshift(hold);
124
+ } else stack.push(hold);
125
+ return hold;
126
+ }
127
+ #covered(element, stack, hold) {
128
+ const current = element.getAttribute(this.#attribute);
129
+ return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
130
+ }
131
+ #mark(element, stack) {
132
+ const records = /* @__PURE__ */ new Map([[this.#record, null]]);
133
+ stack.forEach((hold, index) => {
134
+ const names = stack.slice(0, index).map((under) => under.record);
135
+ const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
136
+ const marker = hold.linked || beneath.length === 0 ? [] : [false];
137
+ records.set(
138
+ hold.record,
139
+ hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
140
+ );
141
+ });
142
+ for (const [name, recorded] of records) {
143
+ if (element.getAttribute(name) === recorded) continue;
144
+ if (recorded === null) element.removeAttribute(name);
145
+ else element.setAttribute(name, recorded);
146
+ }
39
147
  }
40
- /** Reflects only a real value transition, avoiding self-triggered mutation work. */
41
148
  #reflect(element, value) {
42
149
  if (element.getAttribute(this.#attribute) === value) return;
43
150
  if (value === null) element.removeAttribute(this.#attribute);
44
151
  else element.setAttribute(this.#attribute, value);
45
152
  }
46
- /** Returns every outstanding lease using the same ownership check as {@link return}. */
47
- returnAll() {
48
- for (const element of Array.from(this.#records.keys())) this.return(element);
49
- }
50
153
  };
51
154
 
52
- // src/utils/before_cache_reset.ts
53
- var BeforeCacheReset = class _BeforeCacheReset {
54
- /** Every subscribed instance, iterated by the one shared document listener. */
55
- static #subscribers = /* @__PURE__ */ new Set();
56
- /** The shared listener; installed while at least one instance is subscribed. */
57
- static #onBeforeCache = () => {
58
- for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
59
- };
60
- #rewind;
61
- /** @param rewind - the pass that returns this controller's state to its initial form. */
62
- constructor(rewind) {
63
- this.#rewind = rewind;
64
- }
65
- /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
66
- activate() {
67
- const first = _BeforeCacheReset.#subscribers.size === 0;
68
- _BeforeCacheReset.#subscribers.add(this);
69
- if (first) {
70
- document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
71
- }
72
- }
73
- /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
74
- deactivate() {
75
- _BeforeCacheReset.#subscribers.delete(this);
76
- if (_BeforeCacheReset.#subscribers.size > 0) return;
77
- document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
78
- }
79
- };
80
-
81
- // src/utils/composition_tracker.ts
82
155
  var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
83
156
  "insertCompositionText",
84
157
  "insertFromComposition",
@@ -90,13 +163,11 @@ var CompositionTracker = class {
90
163
  #activeTargets = /* @__PURE__ */ new Set();
91
164
  #onStart;
92
165
  #onEnd;
93
- /** The field whose confirming `input` is still owed, while the window is open. */
94
166
  #confirmedTarget = null;
95
167
  constructor(options = {}) {
96
168
  this.#onStart = options.onStart;
97
169
  this.#onEnd = options.onEnd;
98
170
  }
99
- /** Starts lifecycle tracking for `target`; repeated calls are idempotent. */
100
171
  observe(target) {
101
172
  if (this.#observedTargets.has(target)) return;
102
173
  target.addEventListener("compositionstart", this.#handleStart);
@@ -104,7 +175,6 @@ var CompositionTracker = class {
104
175
  target.addEventListener("keydown", this.#handleKeydown);
105
176
  this.#observedTargets.add(target);
106
177
  }
107
- /** Stops tracking one target and clears any active composition it owned. */
108
178
  unobserve(target) {
109
179
  if (!this.#observedTargets.delete(target)) return;
110
180
  target.removeEventListener("compositionstart", this.#handleStart);
@@ -113,7 +183,6 @@ var CompositionTracker = class {
113
183
  this.#activeTargets.delete(target);
114
184
  if (this.#confirmedTarget === target) this.#confirmedTarget = null;
115
185
  }
116
- /** Releases every listener and clears state so reconnect starts cleanly. */
117
186
  disconnect() {
118
187
  for (const target of this.#observedTargets) {
119
188
  target.removeEventListener("compositionstart", this.#handleStart);
@@ -124,16 +193,9 @@ var CompositionTracker = class {
124
193
  this.#activeTargets.clear();
125
194
  this.#confirmedTarget = null;
126
195
  }
127
- /** True when lifecycle tracking or the current event reports composition. */
128
196
  isComposing(event) {
129
197
  return this.#activeTargets.size > 0 || event?.isComposing === true;
130
198
  }
131
- /**
132
- * Whether `event` is the `input` echoing the composition just confirmed.
133
- *
134
- * Asking closes the window either way, so one confirmation is folded at most
135
- * once and a consumer asks once per `input`.
136
- */
137
199
  consumesConfirmedInput(event) {
138
200
  const confirmed = this.#confirmedTarget;
139
201
  this.#confirmedTarget = null;
@@ -151,19 +213,15 @@ var CompositionTracker = class {
151
213
  this.#confirmedTarget = event.target;
152
214
  this.#onEnd?.(event);
153
215
  };
154
- /** A key on an observed field opens an edit of its own, so no echo is owed. */
155
216
  #handleKeydown = () => {
156
217
  this.#confirmedTarget = null;
157
218
  };
158
219
  };
159
220
 
160
- // src/utils/field_mirror.ts
161
- function commitEdit(control) {
162
- control.dispatchEvent(new Event("input", { bubbles: true }));
163
- control.dispatchEvent(new Event("change", { bubbles: true }));
221
+ function commitField(target) {
222
+ target.dispatchEvent(new Event("change", { bubbles: true }));
164
223
  }
165
224
 
166
- // src/utils/half_width.ts
167
225
  var FULL_WIDTH_SHIFT = 65248;
168
226
  function halfWidthChar(char) {
169
227
  if (char >= "\uFF01" && char <= "\uFF5E") {
@@ -177,27 +235,33 @@ function toHalfWidth(text) {
177
235
  return out;
178
236
  }
179
237
 
180
- // src/utils/microtask_coalescer.ts
238
+ var ListenerSet = class {
239
+ #abort = new AbortController();
240
+ add(target, type, handler, options) {
241
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
242
+ }
243
+ dispose() {
244
+ this.#abort.abort();
245
+ this.#abort = new AbortController();
246
+ }
247
+ };
248
+
181
249
  var MicrotaskCoalescer = class {
182
250
  #run;
183
251
  #queued = false;
184
252
  #active = false;
185
253
  #generation = 0;
186
- /** @param run - the single reconciliation pass, invoked at most once per batch. */
187
254
  constructor(run) {
188
255
  this.#run = run;
189
256
  }
190
- /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
191
257
  activate() {
192
258
  this.#active = true;
193
259
  }
194
- /** Closes the window and drops any pending pass; call from `disconnect()`. */
195
260
  cancel() {
196
261
  this.#active = false;
197
262
  this.#queued = false;
198
263
  this.#generation += 1;
199
264
  }
200
- /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
201
265
  schedule() {
202
266
  if (!this.#active || this.#queued) return;
203
267
  this.#queued = true;
@@ -210,43 +274,107 @@ var MicrotaskCoalescer = class {
210
274
  }
211
275
  };
212
276
 
213
- // src/utils/safe_timeout.ts
277
+ var MorphRenderWatcher = class {
278
+ #listeners = new ListenerSet();
279
+ #pass;
280
+ #element = null;
281
+ #onMorph = () => this.#pass.schedule();
282
+ constructor(run) {
283
+ this.#pass = new MicrotaskCoalescer(run);
284
+ }
285
+ observe(element) {
286
+ if (this.#element === element) return;
287
+ this.disconnect();
288
+ this.#element = element;
289
+ this.#pass.activate();
290
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
291
+ }
292
+ schedule() {
293
+ this.#pass.schedule();
294
+ }
295
+ disconnect() {
296
+ this.#listeners.dispose();
297
+ this.#pass.cancel();
298
+ this.#element = null;
299
+ }
300
+ };
301
+
302
+ var MoveCounter = class {
303
+ #sequence = 0;
304
+ record() {
305
+ this.#sequence += 1;
306
+ return this.#sequence;
307
+ }
308
+ isLatest(token) {
309
+ return token === this.#sequence;
310
+ }
311
+ };
312
+
313
+ var NUMBER_BOUNDS = {
314
+ nonNegative: { finite: true, min: 0 },
315
+ positive: { finite: true, exclusiveMin: 0 },
316
+ lowerBound: { finite: true, allowInfinity: "negative" },
317
+ upperBound: { finite: true, allowInfinity: "positive" }};
318
+ function matchesNumberBounds(value, bounds) {
319
+ if (!Number.isFinite(value)) {
320
+ const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
321
+ if (direction === null) return false;
322
+ if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
323
+ }
324
+ if (bounds.min !== void 0 && value < bounds.min) return false;
325
+ if (bounds.max !== void 0 && value > bounds.max) return false;
326
+ if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
327
+ if (bounds.integer && !Number.isInteger(value)) return false;
328
+ if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
329
+ return true;
330
+ }
331
+
332
+ function readNumber(raw, fallback, bounds) {
333
+ return matchesNumberBounds(raw, bounds) ? raw : fallback;
334
+ }
335
+
336
+ var NumberValueReader = class {
337
+ #lastRejected = /* @__PURE__ */ new Map();
338
+ read(owner, name, raw, fallback, bounds) {
339
+ const resolved = readNumber(raw, fallback, bounds);
340
+ if (matchesNumberBounds(raw, bounds)) {
341
+ this.#lastRejected.delete(name);
342
+ return resolved;
343
+ }
344
+ const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
345
+ const literal = owner.element.getAttribute(attribute);
346
+ if (literal === null) {
347
+ this.#lastRejected.delete(name);
348
+ return resolved;
349
+ }
350
+ if (this.#lastRejected.get(name) !== literal) {
351
+ this.#lastRejected.set(name, literal);
352
+ console.warn(
353
+ `Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
354
+ );
355
+ }
356
+ return resolved;
357
+ }
358
+ };
359
+
214
360
  var TimerRegistry = class {
215
- /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
216
361
  ids = /* @__PURE__ */ new Set();
217
- /**
218
- * Cancels a single tracked timer.
219
- *
220
- * No-ops if the id is unknown (already cleared, fired, or never owned by this
221
- * registry), so callers can clear defensively without guarding.
222
- */
223
362
  clear(id) {
224
363
  if (this.ids.delete(id)) {
225
364
  this.cancel(id);
226
365
  }
227
366
  }
228
- /**
229
- * Cancels every tracked timer. Call this from a controller's `disconnect()`
230
- * to guarantee no timer outlives the element.
231
- */
232
367
  clearAll() {
233
368
  for (const id of this.ids) {
234
369
  this.cancel(id);
235
370
  }
236
371
  this.ids.clear();
237
372
  }
238
- /** Number of timers currently tracked (pending). */
239
373
  get size() {
240
374
  return this.ids.size;
241
375
  }
242
376
  };
243
377
  var SafeTimeout = class extends TimerRegistry {
244
- /**
245
- * Schedules `callback` after `delay` ms and returns the timer id.
246
- *
247
- * The id is removed from the registry automatically when the timeout fires,
248
- * so {@link TimerRegistry.size | size} reflects only still-pending timers.
249
- */
250
378
  set(callback, delay) {
251
379
  const id = this.schedule(() => {
252
380
  this.ids.delete(id);
@@ -263,7 +391,6 @@ var SafeTimeout = class extends TimerRegistry {
263
391
  }
264
392
  };
265
393
  var SafeInterval = class extends TimerRegistry {
266
- /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */
267
394
  set(callback, delay) {
268
395
  const id = this.schedule(callback, delay);
269
396
  this.ids.add(id);
@@ -277,7 +404,6 @@ var SafeInterval = class extends TimerRegistry {
277
404
  }
278
405
  };
279
406
 
280
- // src/utils/stepped_value.ts
281
407
  function effectiveStep(step) {
282
408
  return Number.isFinite(step) && step > 0 ? step : 1;
283
409
  }
@@ -383,9 +509,11 @@ function finiteFallback(min, max) {
383
509
  return 0;
384
510
  }
385
511
 
386
- // src/controllers/number_input_controller.ts
387
512
  var OWNED_DISABLED = "data-number-input-disabled";
388
513
  var NumberInputController = class _NumberInputController extends Controller {
514
+ #numbers = new NumberValueReader();
515
+ #moves = new MoveCounter();
516
+ #move = 0;
389
517
  static targets = ["input", "increment", "decrement"];
390
518
  static values = {
391
519
  min: { type: Number, default: Number.NEGATIVE_INFINITY },
@@ -393,70 +521,41 @@ var NumberInputController = class _NumberInputController extends Controller {
393
521
  step: { type: Number, default: 1 },
394
522
  pageStep: { type: Number, default: 0 }
395
523
  };
524
+ static valueConstraints = {
525
+ min: NUMBER_BOUNDS.lowerBound,
526
+ max: NUMBER_BOUNDS.upperBound,
527
+ step: NUMBER_BOUNDS.positive,
528
+ pageStep: NUMBER_BOUNDS.nonNegative
529
+ };
396
530
  static actions = ["decrement", "increment", "onInput", "onKeydown"];
397
531
  static events = ["change", "reconcile"];
398
- /** Delay (ms) a button must be held before auto-repeat starts. */
399
532
  static #HOLD_DELAY_MS = 400;
400
- /** Interval (ms) between auto-repeat steps once a hold has started. */
401
533
  static #HOLD_REPEAT_MS = 80;
402
- /**
403
- * Window (ms) after a hold ends during which the trailing synthetic `click` is
404
- * swallowed. A safety net for the (rare) case where that click never arrives
405
- * (e.g. the pointer was released off the button): the flag self-clears instead
406
- * of poisoning the next legitimate click.
407
- */
408
534
  static #SUPPRESS_RESET_MS = 250;
409
- /** Aborts global pointer guards on disconnect so none outlive the element. */
410
535
  #globalGuards = null;
411
- /** Per-target guards allow an old button to be released without touching its replacement. */
412
536
  #buttonGuards = /* @__PURE__ */ new Map();
413
- /**
414
- * Tracks IME lifecycle on the current input, including confirming keys without a
415
- * signal, and runs a reconciliation the composition held back once it ends.
416
- */
417
537
  #composition = new CompositionTracker({
418
538
  onEnd: () => {
419
539
  if (this.#reconcileHeld) this.#repaint.schedule();
420
540
  }
421
541
  });
422
- /** A reconciliation that arrived while the input was composing. */
423
542
  #reconcileHeld = false;
424
- /** Restores authored custom-spinbutton ARIA when a target leaves or the controller stops. */
425
- #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
426
- #ariaValueNow = new AttributeLease("aria-valuenow");
427
- #ariaValueMin = new AttributeLease("aria-valuemin");
428
- #ariaValueMax = new AttributeLease("aria-valuemax");
429
- /**
430
- * The number shown last — reconciled or committed by the user — which the next
431
- * move is measured from; `null` while the field is blank, and before this
432
- * instance has shown one. A disconnect keeps it for the same instance
433
- * connecting again.
434
- */
543
+ #ariaValueNow = new AttributeLease("aria-valuenow", this.identifier);
544
+ #ariaValueMin = new AttributeLease("aria-valuemin", this.identifier);
545
+ #ariaValueMax = new AttributeLease("aria-valuemax", this.identifier);
435
546
  #lastValue = null;
436
- /**
437
- * Whether `connect()` has run and `disconnect()` has not since. The pass
438
- * `connect()` runs takes the baseline without reporting it.
439
- */
440
547
  #connected = false;
441
- /** Timers for the hold delay and the suppress-reset safety net. */
442
548
  #holdTimeouts = new SafeTimeout();
443
- /** The running auto-repeat interval (one at a time). */
444
549
  #holdIntervals = new SafeInterval();
445
- /** True while a hold is armed/running, making `#stopHold` idempotent. */
446
550
  #holdActive = false;
447
- /** True once a hold actually produced a repeated step (vs. a quick click). */
551
+ #holdGeneration = 0;
448
552
  #repeatedDuringHold = false;
449
- /** True when the next `click` is the trailing one after a hold and must be ignored. */
450
553
  #suppressNextClick = false;
451
- /** Button and pointer that own the current hold, so other instances/pointers cannot end it. */
452
554
  #holdButton = null;
453
555
  #holdPointerId = null;
454
- /** Collapses runtime range/step changes into one input reconciliation. */
455
- #repaint = new MicrotaskCoalescer(() => this.#reconcile());
456
- /** Normalizes any initial value and wires the focus/hold pointer guards. */
556
+ #repaint = new MorphRenderWatcher(() => this.#reconcile());
457
557
  connect() {
458
- this.#repaint.activate();
459
- this.#beforeCache.activate();
558
+ this.#repaint.observe(this.element);
460
559
  this.#globalGuards = new AbortController();
461
560
  const { signal } = this.#globalGuards;
462
561
  for (const button of this.incrementTargets) this.#wireButton(button, 1);
@@ -467,17 +566,10 @@ var NumberInputController = class _NumberInputController extends Controller {
467
566
  this.#reconcile();
468
567
  this.#connected = true;
469
568
  }
470
- /**
471
- * Releases listeners, derived attributes, timers, and the hold state. The number
472
- * shown last stays: the pass the next `connect()` runs takes a new one without a
473
- * report from an input that is present, and with none it stays the number an
474
- * input arriving later is compared with.
475
- */
476
569
  disconnect() {
477
570
  this.#connected = false;
478
- this.#repaint.cancel();
571
+ this.#repaint.disconnect();
479
572
  this.#composition.disconnect();
480
- this.#beforeCache.deactivate();
481
573
  this.#globalGuards?.abort();
482
574
  this.#globalGuards = null;
483
575
  const buttons = /* @__PURE__ */ new Set([
@@ -492,74 +584,55 @@ var NumberInputController = class _NumberInputController extends Controller {
492
584
  this.#repeatedDuringHold = false;
493
585
  this.#suppressNextClick = false;
494
586
  this.#holdTimeouts.clearAll();
495
- this.#holdIntervals.clearAll();
496
587
  this.#ariaValueNow.returnAll();
497
588
  this.#ariaValueMin.returnAll();
498
589
  this.#ariaValueMax.returnAll();
499
590
  }
500
- /** Reconciles the value against a minimum changed by application code or a Turbo morph. */
501
591
  minValueChanged() {
502
592
  this.#repaint.schedule();
503
593
  }
504
- /** Reconciles the value against a maximum changed by application code or a Turbo morph. */
505
594
  maxValueChanged() {
506
595
  this.#repaint.schedule();
507
596
  }
508
- /** Reconciles the value against a step changed by application code or a Turbo morph. */
509
597
  stepValueChanged() {
510
598
  this.#repaint.schedule();
511
599
  }
512
- /** Tracks and reconciles an input inserted or replaced at runtime. */
513
600
  inputTargetConnected(input) {
514
601
  this.#composition.observe(input);
515
602
  this.#repaint.schedule();
516
603
  }
517
- /**
518
- * Releases ARIA and composition state owned by an input that left the controller.
519
- * The number it showed stays the baseline, so an input swapped in holding another
520
- * number is reported as `reconcile`.
521
- */
522
604
  inputTargetDisconnected(input) {
523
605
  this.#composition.unobserve(input);
524
606
  this.#releaseInputAria(input);
525
607
  this.#stopHold(false);
526
608
  this.#repaint.schedule();
527
609
  }
528
- /** Wires focus preservation and hold behavior on a runtime increment target. */
529
610
  incrementTargetConnected(button) {
530
611
  this.#wireButton(button, 1);
531
612
  this.#repaint.schedule();
532
613
  }
533
- /** Releases only the increment target that actually disconnected. */
534
614
  incrementTargetDisconnected(button) {
535
615
  this.#unwireButton(button);
536
- this.#repaint.schedule();
537
616
  }
538
- /** Wires focus preservation and hold behavior on a runtime decrement target. */
539
617
  decrementTargetConnected(button) {
540
618
  this.#wireButton(button, -1);
541
619
  this.#repaint.schedule();
542
620
  }
543
- /** Releases only the decrement target that actually disconnected. */
544
621
  decrementTargetDisconnected(button) {
545
622
  this.#unwireButton(button);
546
- this.#repaint.schedule();
547
623
  }
548
- /** Increases by one step. Bound via `data-action` (click). */
549
624
  increment() {
550
625
  if (this.#consumeSuppressedClick()) return;
551
626
  if (!this.hasInputTarget) return;
552
627
  this.#commitStep(1);
553
628
  this.inputTarget.focus();
554
629
  }
555
- /** Decreases by one step. Bound via `data-action` (click). */
556
630
  decrement() {
557
631
  if (this.#consumeSuppressedClick()) return;
558
632
  if (!this.hasInputTarget) return;
559
633
  this.#commitStep(-1);
560
634
  this.inputTarget.focus();
561
635
  }
562
- /** Clamps and snaps a typed value. Bound via `data-action` (change). */
563
636
  onInput() {
564
637
  if (!this.hasInputTarget) return;
565
638
  if (this.inputTarget.value.trim() === "") {
@@ -569,7 +642,6 @@ var NumberInputController = class _NumberInputController extends Controller {
569
642
  }
570
643
  this.#commit(this.#currentValue());
571
644
  }
572
- /** Keyboard stepping per the APG spinbutton model. */
573
645
  onKeydown(event) {
574
646
  if (!this.hasInputTarget) return;
575
647
  if (this.#composition.isComposing(event)) return;
@@ -583,18 +655,18 @@ var NumberInputController = class _NumberInputController extends Controller {
583
655
  next = stepSteppedValue(this.#currentValue(), -1, this.#steppedRange);
584
656
  break;
585
657
  case "PageUp":
586
- next = this.pageStepValue > 0 ? this.#currentValue() + this.pageStepValue : stepSteppedValue(this.#currentValue(), 10, this.#steppedRange);
658
+ next = this.#safePageStep > 0 ? this.#currentValue() + this.#safePageStep : stepSteppedValue(this.#currentValue(), 10, this.#steppedRange);
587
659
  break;
588
660
  case "PageDown":
589
- next = this.pageStepValue > 0 ? this.#currentValue() - this.pageStepValue : stepSteppedValue(this.#currentValue(), -10, this.#steppedRange);
661
+ next = this.#safePageStep > 0 ? this.#currentValue() - this.#safePageStep : stepSteppedValue(this.#currentValue(), -10, this.#steppedRange);
590
662
  break;
591
663
  case "Home":
592
- if (!Number.isFinite(this.minValue)) return;
593
- next = this.minValue;
664
+ if (!Number.isFinite(this.#safeMin)) return;
665
+ next = this.#safeMin;
594
666
  break;
595
667
  case "End":
596
- if (!Number.isFinite(this.maxValue)) return;
597
- next = this.maxValue;
668
+ if (!Number.isFinite(this.#safeMax)) return;
669
+ next = this.#safeMax;
598
670
  break;
599
671
  default:
600
672
  return;
@@ -602,11 +674,6 @@ var NumberInputController = class _NumberInputController extends Controller {
602
674
  event.preventDefault();
603
675
  this.#commit(next, true);
604
676
  }
605
- /**
606
- * Wires a step button: `pointerdown` keeps focus on the input and arms the
607
- * hold; leaving the button while held stops it (the global listeners cover
608
- * release/cancel/blur).
609
- */
610
677
  #wireButton(button, direction) {
611
678
  if (this.#buttonGuards.has(button)) return;
612
679
  const guard = new AbortController();
@@ -622,14 +689,12 @@ var NumberInputController = class _NumberInputController extends Controller {
622
689
  { signal: guard.signal }
623
690
  );
624
691
  }
625
- /** Aborts one target's listeners and returns any state derived onto that button. */
626
692
  #unwireButton(button) {
627
693
  this.#buttonGuards.get(button)?.abort();
628
694
  this.#buttonGuards.delete(button);
629
695
  if (this.#holdButton === button) this.#stopHold(false);
630
696
  this.#releaseButtonState(button);
631
697
  }
632
- /** Starts a hold: focus the input, then schedule the first repeat after a delay. */
633
698
  #armHold(event, button, direction) {
634
699
  const pointerButton = event.button;
635
700
  if (typeof pointerButton === "number" && pointerButton !== 0) return;
@@ -642,25 +707,19 @@ var NumberInputController = class _NumberInputController extends Controller {
642
707
  this.#holdPointerId = this.#pointerId(event);
643
708
  this.#repeatedDuringHold = false;
644
709
  this.#suppressNextClick = false;
710
+ const generation = ++this.#holdGeneration;
645
711
  this.#holdTimeouts.set(() => {
646
- if (!this.#commitStep(direction)) {
647
- this.#stopHold();
648
- return;
649
- }
712
+ if (!this.#commitStep(direction) || generation !== this.#holdGeneration) return;
650
713
  this.#repeatedDuringHold = true;
651
714
  this.#holdIntervals.set(() => {
652
715
  if (!this.#commitStep(direction)) this.#stopHold();
653
716
  }, _NumberInputController.#HOLD_REPEAT_MS);
654
717
  }, _NumberInputController.#HOLD_DELAY_MS);
655
718
  }
656
- /**
657
- * Stops the current hold (idempotent). When the hold actually repeated, the
658
- * trailing synthetic `click` must be ignored, so it is suppressed until the
659
- * click consumes it or a short safety-net timeout clears it.
660
- */
661
719
  #stopHold(suppressTrailingClick = true) {
662
720
  if (!this.#holdActive) return;
663
721
  this.#holdActive = false;
722
+ this.#holdGeneration += 1;
664
723
  this.#holdButton = null;
665
724
  this.#holdPointerId = null;
666
725
  this.#holdTimeouts.clearAll();
@@ -670,64 +729,50 @@ var NumberInputController = class _NumberInputController extends Controller {
670
729
  this.#holdTimeouts.set(() => {
671
730
  this.#suppressNextClick = false;
672
731
  }, _NumberInputController.#SUPPRESS_RESET_MS);
673
- } else if (!suppressTrailingClick) {
674
- this.#suppressNextClick = false;
675
732
  }
676
733
  }
677
- /** Ends a hold only for its initiating pointer (legacy/synthetic events remain accepted). */
678
734
  #onPointerEnd = (event) => {
679
735
  if (this.#ownsPointer(event)) this.#stopHold();
680
736
  };
681
- /** Window deactivation is global and always ends the active hold. */
682
737
  #onWindowBlur = () => {
683
738
  this.#stopHold();
684
739
  };
685
- /** Extracts pointer identity when the runtime event exposes it. */
686
740
  #pointerId(event) {
687
741
  return event.pointerId ?? null;
688
742
  }
689
- /** Whether `event` belongs to the active pointer, with a synthetic-event fallback. */
690
743
  #ownsPointer(event) {
691
744
  const pointerId = this.#pointerId(event);
692
745
  return this.#holdActive && (this.#holdPointerId === null || pointerId === null || pointerId === this.#holdPointerId);
693
746
  }
694
- /** Consumes a pending trailing-click suppression; returns true if the click was swallowed. */
695
747
  #consumeSuppressedClick() {
696
748
  if (!this.#suppressNextClick) return false;
697
749
  this.#suppressNextClick = false;
698
750
  this.#holdTimeouts.clearAll();
699
751
  return true;
700
752
  }
701
- /**
702
- * Normalizes `raw`, reflects it, and dispatches `{ value: number }` only when
703
- * the committed numeric value differs from the last reconciled value.
704
- *
705
- * @returns Whether the value changed (drives the auto-repeat's bound stop).
706
- */
707
753
  #commit(raw, report = false) {
708
754
  const value = this.#normalize(raw);
709
755
  const changed = this.#lastValue === null || value !== this.#lastValue;
710
756
  const shown = this.inputTarget.value;
711
- this.#write(value);
712
757
  this.#lastValue = value;
713
- if (report && shown !== this.inputTarget.value) commitEdit(this.inputTarget);
758
+ if (changed) this.#move = this.#moves.record();
759
+ const move = this.#move;
760
+ this.#write(value);
761
+ if (!this.#moves.isLatest(move)) return changed;
762
+ if (report && shown !== this.inputTarget.value) {
763
+ const input = this.inputTarget;
764
+ input.dispatchEvent(new Event("input", { bubbles: true }));
765
+ if (!this.#moves.isLatest(move)) return changed;
766
+ commitField(input);
767
+ }
768
+ if (!this.#moves.isLatest(move)) return changed;
714
769
  if (changed) this.dispatch("change", { detail: { value } });
715
770
  return changed;
716
771
  }
717
- /** Commits an adjacent endpoint/grid value and reports whether it moved. */
718
772
  #commitStep(count) {
719
773
  if (!this.hasInputTarget) return false;
720
774
  return this.#commit(stepSteppedValue(this.#currentValue(), count, this.#steppedRange), true);
721
775
  }
722
- /**
723
- * Reflects the current value after a range, step or input change, reporting a
724
- * number that moved from the one shown last as `reconcile`. While the input is
725
- * composing, its text is the IME's: the pass waits for the composition to end
726
- * instead of reading and rewriting it. With no input there is nothing to show,
727
- * and the number shown last stays the baseline for the input that comes next.
728
- *
729
- * @stimeoRenderRoot
730
- */
731
776
  #reconcile() {
732
777
  if (!this.hasInputTarget) return;
733
778
  if (this.#composition.isComposing()) {
@@ -738,65 +783,57 @@ var NumberInputController = class _NumberInputController extends Controller {
738
783
  const previous = this.#lastValue;
739
784
  if (this.inputTarget.value.trim() !== "") {
740
785
  const value = this.#normalize(this.#currentValue());
741
- this.#write(value);
742
786
  this.#lastValue = value;
787
+ const move = this.#move;
788
+ this.#write(value);
789
+ if (!this.#moves.isLatest(move)) return;
743
790
  } else {
744
791
  this.#lastValue = null;
745
792
  this.#reflectEmpty();
746
793
  }
747
794
  this.#reportMove(previous);
748
795
  }
749
- /**
750
- * Reports the number shown as `reconcile` when a change the page made moved it
751
- * from `previous`. The page moved it, so it is reported apart from `change`,
752
- * which stays reserved for user edits. A blank field reports nothing, the way a
753
- * user blanking it does: both events carry a number.
754
- */
755
796
  #reportMove(previous) {
756
797
  const value = this.#lastValue;
757
798
  if (!this.#connected || value === null || value === previous) return;
758
799
  this.dispatch("reconcile", { detail: { value } });
759
800
  }
760
- /** Reflects `value` on the input (and ARIA for non-native hosts) and the buttons. */
761
801
  #write(value) {
762
802
  const input = this.inputTarget;
763
- input.value = String(value);
803
+ if (input.value !== String(value)) input.value = String(value);
764
804
  this.#syncInputAria(input, value);
765
805
  this.#updateButtons(value);
766
806
  }
767
- /** Reflects a blank value without inventing a numeric `aria-valuenow`. */
768
807
  #reflectEmpty() {
769
808
  this.#syncInputAria(this.inputTarget, null);
770
809
  this.#updateButtons(this.#currentValue());
771
810
  }
772
- /** Synchronizes controller-derived ARIA while preserving authored teardown values. */
773
811
  #syncInputAria(input, value) {
774
812
  if (input.getAttribute("role") !== "spinbutton") {
775
813
  this.#releaseInputAria(input);
776
814
  return;
777
815
  }
778
816
  this.#ariaValueNow.write(input, value === null ? null : String(value));
779
- this.#ariaValueMin.write(input, Number.isFinite(this.minValue) ? String(this.minValue) : null);
780
- this.#ariaValueMax.write(input, Number.isFinite(this.maxValue) ? String(this.maxValue) : null);
817
+ this.#ariaValueMin.write(input, Number.isFinite(this.#safeMin) ? String(this.#safeMin) : null);
818
+ this.#ariaValueMax.write(input, Number.isFinite(this.#safeMax) ? String(this.#safeMax) : null);
781
819
  }
782
- /** Returns all custom-spinbutton ARIA leased on `input`. */
783
820
  #releaseInputAria(input) {
784
821
  this.#ariaValueNow.return(input);
785
822
  this.#ariaValueMin.return(input);
786
823
  this.#ariaValueMax.return(input);
787
824
  }
788
- /** Disables a step button at its bound, returning focus to the input first. */
789
825
  #updateButtons(value) {
790
- for (const button of this.incrementTargets) this.#toggleButton(button, value < this.maxValue);
791
- for (const button of this.decrementTargets) this.#toggleButton(button, value > this.minValue);
792
- }
793
- /**
794
- * Enables or disables a step button at its bound, never disabling one while it
795
- * holds focus. Owns only the `disabled` it sets itself via a marker
796
- * (`data-number-input-disabled`, like `conditional-fields`/`submit-once`), so an
797
- * author-disabled button (e.g. the whole control disabled) is never re-enabled.
798
- */
799
- #toggleButton(button, enabled) {
826
+ const move = this.#move;
827
+ for (const button of this.incrementTargets) {
828
+ this.#toggleButton(button, value < this.#safeMax, move);
829
+ if (!this.#moves.isLatest(move)) return;
830
+ }
831
+ for (const button of this.decrementTargets) {
832
+ this.#toggleButton(button, value > this.#safeMin, move);
833
+ if (!this.#moves.isLatest(move)) return;
834
+ }
835
+ }
836
+ #toggleButton(button, enabled, move) {
800
837
  if (enabled) {
801
838
  if (button.hasAttribute(OWNED_DISABLED)) {
802
839
  button.disabled = false;
@@ -806,44 +843,64 @@ var NumberInputController = class _NumberInputController extends Controller {
806
843
  }
807
844
  if (button.disabled) return;
808
845
  if (document.activeElement === button && this.hasInputTarget) this.inputTarget.focus();
846
+ if (!this.#moves.isLatest(move)) return;
809
847
  button.disabled = true;
810
848
  button.setAttribute(OWNED_DISABLED, "");
811
849
  }
812
- /** Returns `disabled` only when its explicit marker still identifies controller ownership. */
813
850
  #releaseButtonState(button) {
814
851
  if (!button.hasAttribute(OWNED_DISABLED)) return;
815
852
  button.disabled = false;
816
853
  button.removeAttribute(OWNED_DISABLED);
817
854
  }
818
- /**
819
- * The current numeric value, falling back to a finite min (else 0) when blank.
820
- * Every read of the field's number comes through here, and the text is read
821
- * through the shared half-width mapping: a text-type spinbutton holds whatever an
822
- * IME confirmed, full-width digits and signs included (`34`, `-5`), while a
823
- * native number field's value is already half-width or empty.
824
- */
825
855
  #currentValue() {
826
856
  const text = toHalfWidth(this.inputTarget.value);
827
857
  const parsed = Number(text);
828
858
  if (Number.isFinite(parsed) && text.trim() !== "") return parsed;
829
- return Number.isFinite(this.minValue) ? this.minValue : 0;
859
+ return Number.isFinite(this.#safeMin) ? this.#safeMin : 0;
830
860
  }
831
- /** Clamps to `[min, max]` and snaps to the step grid anchored at a finite min (else 0). */
832
861
  #normalize(raw) {
833
862
  return snapSteppedValue(raw, this.#steppedRange);
834
863
  }
835
- /** Shared range configuration; finite endpoints remain allowed off the grid. */
836
864
  get #steppedRange() {
837
- return { min: this.minValue, max: this.maxValue, step: this.stepValue };
865
+ return { min: this.#safeMin, max: this.#safeMax, step: this.#safeStep };
866
+ }
867
+ get #safeMin() {
868
+ return this.#numbers.read(
869
+ this,
870
+ "min",
871
+ this.minValue,
872
+ _NumberInputController.values.min.default,
873
+ _NumberInputController.valueConstraints.min
874
+ );
838
875
  }
839
- /** Returns borrowed range ARIA before Turbo snapshots the page. */
840
- #rewindForCache() {
841
- this.#ariaValueNow.returnAll();
842
- this.#ariaValueMin.returnAll();
843
- this.#ariaValueMax.returnAll();
876
+ get #safeMax() {
877
+ return this.#numbers.read(
878
+ this,
879
+ "max",
880
+ this.maxValue,
881
+ _NumberInputController.values.max.default,
882
+ _NumberInputController.valueConstraints.max
883
+ );
884
+ }
885
+ get #safeStep() {
886
+ return this.#numbers.read(
887
+ this,
888
+ "step",
889
+ this.stepValue,
890
+ _NumberInputController.values.step.default,
891
+ _NumberInputController.valueConstraints.step
892
+ );
893
+ }
894
+ get #safePageStep() {
895
+ return this.#numbers.read(
896
+ this,
897
+ "pageStep",
898
+ this.pageStepValue,
899
+ _NumberInputController.values.pageStep.default,
900
+ _NumberInputController.valueConstraints.pageStep
901
+ );
844
902
  }
845
903
  };
846
904
 
847
905
  export { NumberInputController };
848
- //# sourceMappingURL=number_input_controller.js.map
849
906
  //# sourceMappingURL=number_input_controller.js.map