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,151 +1,267 @@
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
- };
51
-
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
153
  };
80
154
 
81
- // src/utils/composition_tracker.ts
155
+ var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
156
+ "insertCompositionText",
157
+ "insertFromComposition",
158
+ "deleteCompositionText",
159
+ "deleteByComposition"
160
+ ]);
82
161
  var CompositionTracker = class {
83
162
  #observedTargets = /* @__PURE__ */ new Set();
84
163
  #activeTargets = /* @__PURE__ */ new Set();
85
164
  #onStart;
86
165
  #onEnd;
166
+ #confirmedTarget = null;
87
167
  constructor(options = {}) {
88
168
  this.#onStart = options.onStart;
89
169
  this.#onEnd = options.onEnd;
90
170
  }
91
- /** Starts lifecycle tracking for `target`; repeated calls are idempotent. */
92
171
  observe(target) {
93
172
  if (this.#observedTargets.has(target)) return;
94
173
  target.addEventListener("compositionstart", this.#handleStart);
95
174
  target.addEventListener("compositionend", this.#handleEnd);
175
+ target.addEventListener("keydown", this.#handleKeydown);
96
176
  this.#observedTargets.add(target);
97
177
  }
98
- /** Stops tracking one target and clears any active composition it owned. */
99
178
  unobserve(target) {
100
179
  if (!this.#observedTargets.delete(target)) return;
101
180
  target.removeEventListener("compositionstart", this.#handleStart);
102
181
  target.removeEventListener("compositionend", this.#handleEnd);
182
+ target.removeEventListener("keydown", this.#handleKeydown);
103
183
  this.#activeTargets.delete(target);
184
+ if (this.#confirmedTarget === target) this.#confirmedTarget = null;
104
185
  }
105
- /** Releases every listener and clears state so reconnect starts cleanly. */
106
186
  disconnect() {
107
187
  for (const target of this.#observedTargets) {
108
188
  target.removeEventListener("compositionstart", this.#handleStart);
109
189
  target.removeEventListener("compositionend", this.#handleEnd);
190
+ target.removeEventListener("keydown", this.#handleKeydown);
110
191
  }
111
192
  this.#observedTargets.clear();
112
193
  this.#activeTargets.clear();
194
+ this.#confirmedTarget = null;
113
195
  }
114
- /** True when lifecycle tracking or the current event reports composition. */
115
196
  isComposing(event) {
116
197
  return this.#activeTargets.size > 0 || event?.isComposing === true;
117
198
  }
199
+ consumesConfirmedInput(event) {
200
+ const confirmed = this.#confirmedTarget;
201
+ this.#confirmedTarget = null;
202
+ if (confirmed === null || confirmed !== event.target) return false;
203
+ const inputType = event.inputType;
204
+ return !inputType || COMPOSITION_INPUT_TYPES.has(inputType);
205
+ }
118
206
  #handleStart = (event) => {
207
+ this.#confirmedTarget = null;
119
208
  if (event.currentTarget) this.#activeTargets.add(event.currentTarget);
120
209
  this.#onStart?.(event);
121
210
  };
122
211
  #handleEnd = (event) => {
123
212
  if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);
213
+ this.#confirmedTarget = event.target;
124
214
  this.#onEnd?.(event);
125
215
  };
216
+ #handleKeydown = () => {
217
+ this.#confirmedTarget = null;
218
+ };
219
+ };
220
+
221
+ function commitField(target) {
222
+ target.dispatchEvent(new Event("change", { bubbles: true }));
223
+ }
224
+
225
+ var FULL_WIDTH_SHIFT = 65248;
226
+ function halfWidthChar(char) {
227
+ if (char >= "\uFF01" && char <= "\uFF5E") {
228
+ return String.fromCharCode(char.charCodeAt(0) - FULL_WIDTH_SHIFT);
229
+ }
230
+ return char === "\u3000" ? " " : char;
231
+ }
232
+ function toHalfWidth(text) {
233
+ let out = "";
234
+ for (const char of text) out += halfWidthChar(char);
235
+ return out;
236
+ }
237
+
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
+ }
126
247
  };
127
248
 
128
- // src/utils/microtask_coalescer.ts
129
249
  var MicrotaskCoalescer = class {
130
250
  #run;
131
251
  #queued = false;
132
252
  #active = false;
133
253
  #generation = 0;
134
- /** @param run - the single reconciliation pass, invoked at most once per batch. */
135
254
  constructor(run) {
136
255
  this.#run = run;
137
256
  }
138
- /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
139
257
  activate() {
140
258
  this.#active = true;
141
259
  }
142
- /** Closes the window and drops any pending pass; call from `disconnect()`. */
143
260
  cancel() {
144
261
  this.#active = false;
145
262
  this.#queued = false;
146
263
  this.#generation += 1;
147
264
  }
148
- /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
149
265
  schedule() {
150
266
  if (!this.#active || this.#queued) return;
151
267
  this.#queued = true;
@@ -158,43 +274,107 @@ var MicrotaskCoalescer = class {
158
274
  }
159
275
  };
160
276
 
161
- // 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
+
162
360
  var TimerRegistry = class {
163
- /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
164
361
  ids = /* @__PURE__ */ new Set();
165
- /**
166
- * Cancels a single tracked timer.
167
- *
168
- * No-ops if the id is unknown (already cleared, fired, or never owned by this
169
- * registry), so callers can clear defensively without guarding.
170
- */
171
362
  clear(id) {
172
363
  if (this.ids.delete(id)) {
173
364
  this.cancel(id);
174
365
  }
175
366
  }
176
- /**
177
- * Cancels every tracked timer. Call this from a controller's `disconnect()`
178
- * to guarantee no timer outlives the element.
179
- */
180
367
  clearAll() {
181
368
  for (const id of this.ids) {
182
369
  this.cancel(id);
183
370
  }
184
371
  this.ids.clear();
185
372
  }
186
- /** Number of timers currently tracked (pending). */
187
373
  get size() {
188
374
  return this.ids.size;
189
375
  }
190
376
  };
191
377
  var SafeTimeout = class extends TimerRegistry {
192
- /**
193
- * Schedules `callback` after `delay` ms and returns the timer id.
194
- *
195
- * The id is removed from the registry automatically when the timeout fires,
196
- * so {@link TimerRegistry.size | size} reflects only still-pending timers.
197
- */
198
378
  set(callback, delay) {
199
379
  const id = this.schedule(() => {
200
380
  this.ids.delete(id);
@@ -211,7 +391,6 @@ var SafeTimeout = class extends TimerRegistry {
211
391
  }
212
392
  };
213
393
  var SafeInterval = class extends TimerRegistry {
214
- /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */
215
394
  set(callback, delay) {
216
395
  const id = this.schedule(callback, delay);
217
396
  this.ids.add(id);
@@ -225,7 +404,6 @@ var SafeInterval = class extends TimerRegistry {
225
404
  }
226
405
  };
227
406
 
228
- // src/utils/stepped_value.ts
229
407
  function effectiveStep(step) {
230
408
  return Number.isFinite(step) && step > 0 ? step : 1;
231
409
  }
@@ -331,9 +509,11 @@ function finiteFallback(min, max) {
331
509
  return 0;
332
510
  }
333
511
 
334
- // src/controllers/number_input_controller.ts
335
512
  var OWNED_DISABLED = "data-number-input-disabled";
336
513
  var NumberInputController = class _NumberInputController extends Controller {
514
+ #numbers = new NumberValueReader();
515
+ #moves = new MoveCounter();
516
+ #move = 0;
337
517
  static targets = ["input", "increment", "decrement"];
338
518
  static values = {
339
519
  min: { type: Number, default: Number.NEGATIVE_INFINITY },
@@ -341,51 +521,41 @@ var NumberInputController = class _NumberInputController extends Controller {
341
521
  step: { type: Number, default: 1 },
342
522
  pageStep: { type: Number, default: 0 }
343
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
+ };
344
530
  static actions = ["decrement", "increment", "onInput", "onKeydown"];
345
531
  static events = ["change", "reconcile"];
346
- /** Delay (ms) a button must be held before auto-repeat starts. */
347
532
  static #HOLD_DELAY_MS = 400;
348
- /** Interval (ms) between auto-repeat steps once a hold has started. */
349
533
  static #HOLD_REPEAT_MS = 80;
350
- /**
351
- * Window (ms) after a hold ends during which the trailing synthetic `click` is
352
- * swallowed. A safety net for the (rare) case where that click never arrives
353
- * (e.g. the pointer was released off the button): the flag self-clears instead
354
- * of poisoning the next legitimate click.
355
- */
356
534
  static #SUPPRESS_RESET_MS = 250;
357
- /** Aborts global pointer guards on disconnect so none outlive the element. */
358
535
  #globalGuards = null;
359
- /** Per-target guards allow an old button to be released without touching its replacement. */
360
536
  #buttonGuards = /* @__PURE__ */ new Map();
361
- /** Tracks IME lifecycle on the current input, including confirming keys without a signal. */
362
- #composition = new CompositionTracker();
363
- /** Restores authored custom-spinbutton ARIA when a target leaves or the controller stops. */
364
- #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
365
- #ariaValueNow = new AttributeLease("aria-valuenow");
366
- #ariaValueMin = new AttributeLease("aria-valuemin");
367
- #ariaValueMax = new AttributeLease("aria-valuemax");
368
- /** Last reconciled or user-committed numeric value. */
537
+ #composition = new CompositionTracker({
538
+ onEnd: () => {
539
+ if (this.#reconcileHeld) this.#repaint.schedule();
540
+ }
541
+ });
542
+ #reconcileHeld = false;
543
+ #ariaValueNow = new AttributeLease("aria-valuenow", this.identifier);
544
+ #ariaValueMin = new AttributeLease("aria-valuemin", this.identifier);
545
+ #ariaValueMax = new AttributeLease("aria-valuemax", this.identifier);
369
546
  #lastValue = null;
370
- /** Timers for the hold delay and the suppress-reset safety net. */
547
+ #connected = false;
371
548
  #holdTimeouts = new SafeTimeout();
372
- /** The running auto-repeat interval (one at a time). */
373
549
  #holdIntervals = new SafeInterval();
374
- /** True while a hold is armed/running, making `#stopHold` idempotent. */
375
550
  #holdActive = false;
376
- /** True once a hold actually produced a repeated step (vs. a quick click). */
551
+ #holdGeneration = 0;
377
552
  #repeatedDuringHold = false;
378
- /** True when the next `click` is the trailing one after a hold and must be ignored. */
379
553
  #suppressNextClick = false;
380
- /** Button and pointer that own the current hold, so other instances/pointers cannot end it. */
381
554
  #holdButton = null;
382
555
  #holdPointerId = null;
383
- /** Collapses runtime range/step changes into one input reconciliation. */
384
- #repaint = new MicrotaskCoalescer(() => this.#reconcile());
385
- /** Normalizes any initial value and wires the focus/hold pointer guards. */
556
+ #repaint = new MorphRenderWatcher(() => this.#reconcile());
386
557
  connect() {
387
- this.#repaint.activate();
388
- this.#beforeCache.activate();
558
+ this.#repaint.observe(this.element);
389
559
  this.#globalGuards = new AbortController();
390
560
  const { signal } = this.#globalGuards;
391
561
  for (const button of this.incrementTargets) this.#wireButton(button, 1);
@@ -394,12 +564,12 @@ var NumberInputController = class _NumberInputController extends Controller {
394
564
  window.addEventListener("pointercancel", this.#onPointerEnd, { signal });
395
565
  window.addEventListener("blur", this.#onWindowBlur, { signal });
396
566
  this.#reconcile();
567
+ this.#connected = true;
397
568
  }
398
- /** Releases listeners, derived attributes, timers, and transient value state. */
399
569
  disconnect() {
400
- this.#repaint.cancel();
570
+ this.#connected = false;
571
+ this.#repaint.disconnect();
401
572
  this.#composition.disconnect();
402
- this.#beforeCache.deactivate();
403
573
  this.#globalGuards?.abort();
404
574
  this.#globalGuards = null;
405
575
  const buttons = /* @__PURE__ */ new Set([
@@ -414,72 +584,55 @@ var NumberInputController = class _NumberInputController extends Controller {
414
584
  this.#repeatedDuringHold = false;
415
585
  this.#suppressNextClick = false;
416
586
  this.#holdTimeouts.clearAll();
417
- this.#holdIntervals.clearAll();
418
587
  this.#ariaValueNow.returnAll();
419
588
  this.#ariaValueMin.returnAll();
420
589
  this.#ariaValueMax.returnAll();
421
- this.#lastValue = null;
422
590
  }
423
- /** Reconciles the value against a minimum changed by application code or a Turbo morph. */
424
591
  minValueChanged() {
425
592
  this.#repaint.schedule();
426
593
  }
427
- /** Reconciles the value against a maximum changed by application code or a Turbo morph. */
428
594
  maxValueChanged() {
429
595
  this.#repaint.schedule();
430
596
  }
431
- /** Reconciles the value against a step changed by application code or a Turbo morph. */
432
597
  stepValueChanged() {
433
598
  this.#repaint.schedule();
434
599
  }
435
- /** Tracks and reconciles an input inserted or replaced at runtime. */
436
600
  inputTargetConnected(input) {
437
601
  this.#composition.observe(input);
438
602
  this.#repaint.schedule();
439
603
  }
440
- /** Releases ARIA and composition state owned by an input that left the controller. */
441
604
  inputTargetDisconnected(input) {
442
605
  this.#composition.unobserve(input);
443
606
  this.#releaseInputAria(input);
444
607
  this.#stopHold(false);
445
- this.#lastValue = null;
446
608
  this.#repaint.schedule();
447
609
  }
448
- /** Wires focus preservation and hold behavior on a runtime increment target. */
449
610
  incrementTargetConnected(button) {
450
611
  this.#wireButton(button, 1);
451
612
  this.#repaint.schedule();
452
613
  }
453
- /** Releases only the increment target that actually disconnected. */
454
614
  incrementTargetDisconnected(button) {
455
615
  this.#unwireButton(button);
456
- this.#repaint.schedule();
457
616
  }
458
- /** Wires focus preservation and hold behavior on a runtime decrement target. */
459
617
  decrementTargetConnected(button) {
460
618
  this.#wireButton(button, -1);
461
619
  this.#repaint.schedule();
462
620
  }
463
- /** Releases only the decrement target that actually disconnected. */
464
621
  decrementTargetDisconnected(button) {
465
622
  this.#unwireButton(button);
466
- this.#repaint.schedule();
467
623
  }
468
- /** Increases by one step. Bound via `data-action` (click). */
469
624
  increment() {
470
625
  if (this.#consumeSuppressedClick()) return;
471
626
  if (!this.hasInputTarget) return;
472
627
  this.#commitStep(1);
473
628
  this.inputTarget.focus();
474
629
  }
475
- /** Decreases by one step. Bound via `data-action` (click). */
476
630
  decrement() {
477
631
  if (this.#consumeSuppressedClick()) return;
478
632
  if (!this.hasInputTarget) return;
479
633
  this.#commitStep(-1);
480
634
  this.inputTarget.focus();
481
635
  }
482
- /** Clamps and snaps a typed value. Bound via `data-action` (change). */
483
636
  onInput() {
484
637
  if (!this.hasInputTarget) return;
485
638
  if (this.inputTarget.value.trim() === "") {
@@ -489,7 +642,6 @@ var NumberInputController = class _NumberInputController extends Controller {
489
642
  }
490
643
  this.#commit(this.#currentValue());
491
644
  }
492
- /** Keyboard stepping per the APG spinbutton model. */
493
645
  onKeydown(event) {
494
646
  if (!this.hasInputTarget) return;
495
647
  if (this.#composition.isComposing(event)) return;
@@ -503,30 +655,25 @@ var NumberInputController = class _NumberInputController extends Controller {
503
655
  next = stepSteppedValue(this.#currentValue(), -1, this.#steppedRange);
504
656
  break;
505
657
  case "PageUp":
506
- 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);
507
659
  break;
508
660
  case "PageDown":
509
- 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);
510
662
  break;
511
663
  case "Home":
512
- if (!Number.isFinite(this.minValue)) return;
513
- next = this.minValue;
664
+ if (!Number.isFinite(this.#safeMin)) return;
665
+ next = this.#safeMin;
514
666
  break;
515
667
  case "End":
516
- if (!Number.isFinite(this.maxValue)) return;
517
- next = this.maxValue;
668
+ if (!Number.isFinite(this.#safeMax)) return;
669
+ next = this.#safeMax;
518
670
  break;
519
671
  default:
520
672
  return;
521
673
  }
522
674
  event.preventDefault();
523
- this.#commit(next);
675
+ this.#commit(next, true);
524
676
  }
525
- /**
526
- * Wires a step button: `pointerdown` keeps focus on the input and arms the
527
- * hold; leaving the button while held stops it (the global listeners cover
528
- * release/cancel/blur).
529
- */
530
677
  #wireButton(button, direction) {
531
678
  if (this.#buttonGuards.has(button)) return;
532
679
  const guard = new AbortController();
@@ -542,14 +689,12 @@ var NumberInputController = class _NumberInputController extends Controller {
542
689
  { signal: guard.signal }
543
690
  );
544
691
  }
545
- /** Aborts one target's listeners and returns any state derived onto that button. */
546
692
  #unwireButton(button) {
547
693
  this.#buttonGuards.get(button)?.abort();
548
694
  this.#buttonGuards.delete(button);
549
695
  if (this.#holdButton === button) this.#stopHold(false);
550
696
  this.#releaseButtonState(button);
551
697
  }
552
- /** Starts a hold: focus the input, then schedule the first repeat after a delay. */
553
698
  #armHold(event, button, direction) {
554
699
  const pointerButton = event.button;
555
700
  if (typeof pointerButton === "number" && pointerButton !== 0) return;
@@ -562,25 +707,19 @@ var NumberInputController = class _NumberInputController extends Controller {
562
707
  this.#holdPointerId = this.#pointerId(event);
563
708
  this.#repeatedDuringHold = false;
564
709
  this.#suppressNextClick = false;
710
+ const generation = ++this.#holdGeneration;
565
711
  this.#holdTimeouts.set(() => {
566
- if (!this.#commitStep(direction)) {
567
- this.#stopHold();
568
- return;
569
- }
712
+ if (!this.#commitStep(direction) || generation !== this.#holdGeneration) return;
570
713
  this.#repeatedDuringHold = true;
571
714
  this.#holdIntervals.set(() => {
572
715
  if (!this.#commitStep(direction)) this.#stopHold();
573
716
  }, _NumberInputController.#HOLD_REPEAT_MS);
574
717
  }, _NumberInputController.#HOLD_DELAY_MS);
575
718
  }
576
- /**
577
- * Stops the current hold (idempotent). When the hold actually repeated, the
578
- * trailing synthetic `click` must be ignored, so it is suppressed until the
579
- * click consumes it or a short safety-net timeout clears it.
580
- */
581
719
  #stopHold(suppressTrailingClick = true) {
582
720
  if (!this.#holdActive) return;
583
721
  this.#holdActive = false;
722
+ this.#holdGeneration += 1;
584
723
  this.#holdButton = null;
585
724
  this.#holdPointerId = null;
586
725
  this.#holdTimeouts.clearAll();
@@ -590,117 +729,111 @@ var NumberInputController = class _NumberInputController extends Controller {
590
729
  this.#holdTimeouts.set(() => {
591
730
  this.#suppressNextClick = false;
592
731
  }, _NumberInputController.#SUPPRESS_RESET_MS);
593
- } else if (!suppressTrailingClick) {
594
- this.#suppressNextClick = false;
595
732
  }
596
733
  }
597
- /** Ends a hold only for its initiating pointer (legacy/synthetic events remain accepted). */
598
734
  #onPointerEnd = (event) => {
599
735
  if (this.#ownsPointer(event)) this.#stopHold();
600
736
  };
601
- /** Window deactivation is global and always ends the active hold. */
602
737
  #onWindowBlur = () => {
603
738
  this.#stopHold();
604
739
  };
605
- /** Extracts pointer identity when the runtime event exposes it. */
606
740
  #pointerId(event) {
607
741
  return event.pointerId ?? null;
608
742
  }
609
- /** Whether `event` belongs to the active pointer, with a synthetic-event fallback. */
610
743
  #ownsPointer(event) {
611
744
  const pointerId = this.#pointerId(event);
612
745
  return this.#holdActive && (this.#holdPointerId === null || pointerId === null || pointerId === this.#holdPointerId);
613
746
  }
614
- /** Consumes a pending trailing-click suppression; returns true if the click was swallowed. */
615
747
  #consumeSuppressedClick() {
616
748
  if (!this.#suppressNextClick) return false;
617
749
  this.#suppressNextClick = false;
618
750
  this.#holdTimeouts.clearAll();
619
751
  return true;
620
752
  }
621
- /**
622
- * Normalizes `raw`, reflects it, and dispatches `{ value: number }` only when
623
- * the committed numeric value differs from the last reconciled value.
624
- *
625
- * @returns Whether the value changed (drives the auto-repeat's bound stop).
626
- */
627
- #commit(raw) {
753
+ #commit(raw, report = false) {
628
754
  const value = this.#normalize(raw);
629
755
  const changed = this.#lastValue === null || value !== this.#lastValue;
630
- this.#write(value);
756
+ const shown = this.inputTarget.value;
631
757
  this.#lastValue = value;
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;
632
769
  if (changed) this.dispatch("change", { detail: { value } });
633
770
  return changed;
634
771
  }
635
- /** Commits an adjacent endpoint/grid value and reports whether it moved. */
636
772
  #commitStep(count) {
637
773
  if (!this.hasInputTarget) return false;
638
- return this.#commit(stepSteppedValue(this.#currentValue(), count, this.#steppedRange));
639
- }
640
- /**
641
- * Reflects the current value after a range or step morph, reporting a moved
642
- * value as `reconcile`.
643
- *
644
- * @stimeoRenderRoot
645
- */
774
+ return this.#commit(stepSteppedValue(this.#currentValue(), count, this.#steppedRange), true);
775
+ }
646
776
  #reconcile() {
647
- if (!this.hasInputTarget) {
648
- this.#lastValue = null;
777
+ if (!this.hasInputTarget) return;
778
+ if (this.#composition.isComposing()) {
779
+ this.#reconcileHeld = true;
649
780
  return;
650
781
  }
782
+ this.#reconcileHeld = false;
783
+ const previous = this.#lastValue;
651
784
  if (this.inputTarget.value.trim() !== "") {
652
- const previous = this.#lastValue;
653
785
  const value = this.#normalize(this.#currentValue());
654
- this.#write(value);
655
786
  this.#lastValue = value;
656
- if (previous !== null && value !== previous) {
657
- this.dispatch("reconcile", { detail: { value } });
658
- }
787
+ const move = this.#move;
788
+ this.#write(value);
789
+ if (!this.#moves.isLatest(move)) return;
659
790
  } else {
660
791
  this.#lastValue = null;
661
792
  this.#reflectEmpty();
662
793
  }
794
+ this.#reportMove(previous);
795
+ }
796
+ #reportMove(previous) {
797
+ const value = this.#lastValue;
798
+ if (!this.#connected || value === null || value === previous) return;
799
+ this.dispatch("reconcile", { detail: { value } });
663
800
  }
664
- /** Reflects `value` on the input (and ARIA for non-native hosts) and the buttons. */
665
801
  #write(value) {
666
802
  const input = this.inputTarget;
667
- input.value = String(value);
803
+ if (input.value !== String(value)) input.value = String(value);
668
804
  this.#syncInputAria(input, value);
669
805
  this.#updateButtons(value);
670
806
  }
671
- /** Reflects a blank value without inventing a numeric `aria-valuenow`. */
672
807
  #reflectEmpty() {
673
808
  this.#syncInputAria(this.inputTarget, null);
674
809
  this.#updateButtons(this.#currentValue());
675
810
  }
676
- /** Synchronizes controller-derived ARIA while preserving authored teardown values. */
677
811
  #syncInputAria(input, value) {
678
812
  if (input.getAttribute("role") !== "spinbutton") {
679
813
  this.#releaseInputAria(input);
680
814
  return;
681
815
  }
682
816
  this.#ariaValueNow.write(input, value === null ? null : String(value));
683
- this.#ariaValueMin.write(input, Number.isFinite(this.minValue) ? String(this.minValue) : null);
684
- 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);
685
819
  }
686
- /** Returns all custom-spinbutton ARIA leased on `input`. */
687
820
  #releaseInputAria(input) {
688
821
  this.#ariaValueNow.return(input);
689
822
  this.#ariaValueMin.return(input);
690
823
  this.#ariaValueMax.return(input);
691
824
  }
692
- /** Disables a step button at its bound, returning focus to the input first. */
693
825
  #updateButtons(value) {
694
- for (const button of this.incrementTargets) this.#toggleButton(button, value < this.maxValue);
695
- for (const button of this.decrementTargets) this.#toggleButton(button, value > this.minValue);
696
- }
697
- /**
698
- * Enables or disables a step button at its bound, never disabling one while it
699
- * holds focus. Owns only the `disabled` it sets itself via a marker
700
- * (`data-number-input-disabled`, like `conditional-fields`/`submit-once`), so an
701
- * author-disabled button (e.g. the whole control disabled) is never re-enabled.
702
- */
703
- #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) {
704
837
  if (enabled) {
705
838
  if (button.hasAttribute(OWNED_DISABLED)) {
706
839
  button.disabled = false;
@@ -710,37 +843,64 @@ var NumberInputController = class _NumberInputController extends Controller {
710
843
  }
711
844
  if (button.disabled) return;
712
845
  if (document.activeElement === button && this.hasInputTarget) this.inputTarget.focus();
846
+ if (!this.#moves.isLatest(move)) return;
713
847
  button.disabled = true;
714
848
  button.setAttribute(OWNED_DISABLED, "");
715
849
  }
716
- /** Returns `disabled` only when its explicit marker still identifies controller ownership. */
717
850
  #releaseButtonState(button) {
718
851
  if (!button.hasAttribute(OWNED_DISABLED)) return;
719
852
  button.disabled = false;
720
853
  button.removeAttribute(OWNED_DISABLED);
721
854
  }
722
- /** The current numeric value, falling back to a finite min (else 0) when blank. */
723
855
  #currentValue() {
724
- const parsed = Number(this.inputTarget.value);
725
- if (Number.isFinite(parsed) && this.inputTarget.value.trim() !== "") return parsed;
726
- return Number.isFinite(this.minValue) ? this.minValue : 0;
856
+ const text = toHalfWidth(this.inputTarget.value);
857
+ const parsed = Number(text);
858
+ if (Number.isFinite(parsed) && text.trim() !== "") return parsed;
859
+ return Number.isFinite(this.#safeMin) ? this.#safeMin : 0;
727
860
  }
728
- /** Clamps to `[min, max]` and snaps to the step grid anchored at a finite min (else 0). */
729
861
  #normalize(raw) {
730
862
  return snapSteppedValue(raw, this.#steppedRange);
731
863
  }
732
- /** Shared range configuration; finite endpoints remain allowed off the grid. */
733
864
  get #steppedRange() {
734
- 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
+ );
735
875
  }
736
- /** Returns borrowed range ARIA before Turbo snapshots the page. */
737
- #rewindForCache() {
738
- this.#ariaValueNow.returnAll();
739
- this.#ariaValueMin.returnAll();
740
- 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
+ );
741
902
  }
742
903
  };
743
904
 
744
905
  export { NumberInputController };
745
- //# sourceMappingURL=number_input_controller.js.map
746
906
  //# sourceMappingURL=number_input_controller.js.map