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,55 +1,71 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/currency_input_controller.ts
4
-
5
- // src/utils/composition_tracker.ts
3
+ var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
4
+ "insertCompositionText",
5
+ "insertFromComposition",
6
+ "deleteCompositionText",
7
+ "deleteByComposition"
8
+ ]);
6
9
  var CompositionTracker = class {
7
10
  #observedTargets = /* @__PURE__ */ new Set();
8
11
  #activeTargets = /* @__PURE__ */ new Set();
9
12
  #onStart;
10
13
  #onEnd;
14
+ #confirmedTarget = null;
11
15
  constructor(options = {}) {
12
16
  this.#onStart = options.onStart;
13
17
  this.#onEnd = options.onEnd;
14
18
  }
15
- /** Starts lifecycle tracking for `target`; repeated calls are idempotent. */
16
19
  observe(target) {
17
20
  if (this.#observedTargets.has(target)) return;
18
21
  target.addEventListener("compositionstart", this.#handleStart);
19
22
  target.addEventListener("compositionend", this.#handleEnd);
23
+ target.addEventListener("keydown", this.#handleKeydown);
20
24
  this.#observedTargets.add(target);
21
25
  }
22
- /** Stops tracking one target and clears any active composition it owned. */
23
26
  unobserve(target) {
24
27
  if (!this.#observedTargets.delete(target)) return;
25
28
  target.removeEventListener("compositionstart", this.#handleStart);
26
29
  target.removeEventListener("compositionend", this.#handleEnd);
30
+ target.removeEventListener("keydown", this.#handleKeydown);
27
31
  this.#activeTargets.delete(target);
32
+ if (this.#confirmedTarget === target) this.#confirmedTarget = null;
28
33
  }
29
- /** Releases every listener and clears state so reconnect starts cleanly. */
30
34
  disconnect() {
31
35
  for (const target of this.#observedTargets) {
32
36
  target.removeEventListener("compositionstart", this.#handleStart);
33
37
  target.removeEventListener("compositionend", this.#handleEnd);
38
+ target.removeEventListener("keydown", this.#handleKeydown);
34
39
  }
35
40
  this.#observedTargets.clear();
36
41
  this.#activeTargets.clear();
42
+ this.#confirmedTarget = null;
37
43
  }
38
- /** True when lifecycle tracking or the current event reports composition. */
39
44
  isComposing(event) {
40
45
  return this.#activeTargets.size > 0 || event?.isComposing === true;
41
46
  }
47
+ consumesConfirmedInput(event) {
48
+ const confirmed = this.#confirmedTarget;
49
+ this.#confirmedTarget = null;
50
+ if (confirmed === null || confirmed !== event.target) return false;
51
+ const inputType = event.inputType;
52
+ return !inputType || COMPOSITION_INPUT_TYPES.has(inputType);
53
+ }
42
54
  #handleStart = (event) => {
55
+ this.#confirmedTarget = null;
43
56
  if (event.currentTarget) this.#activeTargets.add(event.currentTarget);
44
57
  this.#onStart?.(event);
45
58
  };
46
59
  #handleEnd = (event) => {
47
60
  if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);
61
+ this.#confirmedTarget = event.target;
48
62
  this.#onEnd?.(event);
49
63
  };
64
+ #handleKeydown = () => {
65
+ this.#confirmedTarget = null;
66
+ };
50
67
  };
51
68
 
52
- // src/utils/half_width.ts
53
69
  var FULL_WIDTH_SHIFT = 65248;
54
70
  function halfWidthChar(char) {
55
71
  if (char >= "\uFF01" && char <= "\uFF5E") {
@@ -58,7 +74,6 @@ function halfWidthChar(char) {
58
74
  return char === "\u3000" ? " " : char;
59
75
  }
60
76
 
61
- // src/utils/intl_format.ts
62
77
  var formatters = /* @__PURE__ */ new WeakMap();
63
78
  function intlFormatter(factory, locale, options, fallbackLocale) {
64
79
  const cache = cacheFor(factory);
@@ -88,42 +103,154 @@ function construct(factory, locale, options, fallbackLocale) {
88
103
  }
89
104
  }
90
105
 
91
- // src/utils/locale.ts
92
106
  function resolveLocale(element, declared = "") {
93
107
  return declared || element.closest("[lang]")?.getAttribute("lang") || void 0;
94
108
  }
95
109
 
96
- // src/controllers/currency_input_controller.ts
110
+ var ListenerSet = class {
111
+ #abort = new AbortController();
112
+ add(target, type, handler, options) {
113
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
114
+ }
115
+ dispose() {
116
+ this.#abort.abort();
117
+ this.#abort = new AbortController();
118
+ }
119
+ };
120
+
121
+ var MicrotaskCoalescer = class {
122
+ #run;
123
+ #queued = false;
124
+ #active = false;
125
+ #generation = 0;
126
+ constructor(run) {
127
+ this.#run = run;
128
+ }
129
+ activate() {
130
+ this.#active = true;
131
+ }
132
+ cancel() {
133
+ this.#active = false;
134
+ this.#queued = false;
135
+ this.#generation += 1;
136
+ }
137
+ schedule() {
138
+ if (!this.#active || this.#queued) return;
139
+ this.#queued = true;
140
+ const generation = this.#generation;
141
+ queueMicrotask(() => {
142
+ if (generation !== this.#generation || !this.#queued || !this.#active) return;
143
+ this.#queued = false;
144
+ this.#run();
145
+ });
146
+ }
147
+ };
148
+
149
+ var MorphRenderWatcher = class {
150
+ #listeners = new ListenerSet();
151
+ #pass;
152
+ #element = null;
153
+ #onMorph = () => this.#pass.schedule();
154
+ constructor(run) {
155
+ this.#pass = new MicrotaskCoalescer(run);
156
+ }
157
+ observe(element) {
158
+ if (this.#element === element) return;
159
+ this.disconnect();
160
+ this.#element = element;
161
+ this.#pass.activate();
162
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
163
+ }
164
+ schedule() {
165
+ this.#pass.schedule();
166
+ }
167
+ disconnect() {
168
+ this.#listeners.dispose();
169
+ this.#pass.cancel();
170
+ this.#element = null;
171
+ }
172
+ };
173
+
174
+ function matchesNumberBounds(value, bounds) {
175
+ if (!Number.isFinite(value)) {
176
+ const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
177
+ if (direction === null) return false;
178
+ if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
179
+ }
180
+ if (bounds.min !== void 0 && value < bounds.min) return false;
181
+ if (bounds.max !== void 0 && value > bounds.max) return false;
182
+ if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
183
+ if (bounds.integer && !Number.isInteger(value)) return false;
184
+ if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
185
+ return true;
186
+ }
187
+
188
+ function readNumber(raw, fallback, bounds) {
189
+ return matchesNumberBounds(raw, bounds) ? raw : fallback;
190
+ }
191
+
192
+ var NumberValueReader = class {
193
+ #lastRejected = /* @__PURE__ */ new Map();
194
+ read(owner, name, raw, fallback, bounds) {
195
+ const resolved = readNumber(raw, fallback, bounds);
196
+ if (matchesNumberBounds(raw, bounds)) {
197
+ this.#lastRejected.delete(name);
198
+ return resolved;
199
+ }
200
+ const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
201
+ const literal = owner.element.getAttribute(attribute);
202
+ if (literal === null) {
203
+ this.#lastRejected.delete(name);
204
+ return resolved;
205
+ }
206
+ if (this.#lastRejected.get(name) !== literal) {
207
+ this.#lastRejected.set(name, literal);
208
+ console.warn(
209
+ `Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
210
+ );
211
+ }
212
+ return resolved;
213
+ }
214
+ };
215
+
97
216
  var FALLBACK_LOCALE = "en-US";
98
- var CurrencyInputController = class extends Controller {
217
+ var CurrencyInputController = class _CurrencyInputController extends Controller {
218
+ #numbers = new NumberValueReader();
99
219
  static targets = ["display", "field", "srValue"];
100
220
  static values = {
101
221
  locale: { type: String, default: "" },
102
222
  currency: { type: String, default: "" },
103
223
  precision: { type: Number, default: 2 }
104
224
  };
225
+ static valueConstraints = {
226
+ precision: { finite: true, integer: true, min: 0, max: 100 }
227
+ };
105
228
  static actions = ["format", "onInput"];
106
- static events = ["change"];
107
- /** Last committed numeric value, to suppress duplicate `change` dispatches. */
229
+ static events = ["change", "reconcile"];
108
230
  #lastValue = null;
109
231
  #started = false;
110
- /** Validated mirrors of the Values; the hot path never reads a raw Value. */
232
+ #changeHeld = false;
233
+ #composing = null;
234
+ #rendered = null;
111
235
  #locale;
112
236
  #currency = "";
113
237
  #precision = 2;
114
- /** Formatters rebuilt only when a Value changes, never per keystroke. */
115
238
  #grouping;
116
239
  #fixed;
117
240
  #accessible;
118
241
  #group = ",";
119
242
  #decimal = ".";
120
- /** The locale's non-Latin digits mapped back to ASCII (empty for Latin locales). */
121
243
  #digits = /* @__PURE__ */ new Map();
122
- /** Holds mid-composition input so the IME's uncommitted text is never rewritten. */
123
244
  #composition = new CompositionTracker({
124
- onEnd: () => this.#reformat(false)
245
+ onStart: (event) => {
246
+ this.#composing = event.currentTarget;
247
+ },
248
+ onEnd: () => {
249
+ this.#composing = null;
250
+ this.#reformat(false, "edit");
251
+ this.#applyHeldChange();
252
+ }
125
253
  });
126
- /** Re-validates on declaration changes and re-renders the committed display. */
127
254
  localeValueChanged() {
128
255
  this.#applyValueChange();
129
256
  }
@@ -133,92 +260,98 @@ var CurrencyInputController = class extends Controller {
133
260
  precisionValueChanged() {
134
261
  this.#applyValueChange();
135
262
  }
136
- /**
137
- * Scans the display under the *outgoing* configuration (its separators wrote
138
- * that text), then revalidates and re-renders under the new one — so a locale
139
- * switch re-interprets the value, never the old text with new separators.
140
- */
141
263
  #applyValueChange() {
142
- const scan = this.#started && this.hasDisplayTarget ? this.#scan(this.displayTarget.value) : null;
143
- this.#revalidate();
144
- if (!scan || !this.hasDisplayTarget) return;
264
+ if (this.#composition.isComposing()) {
265
+ this.#changeHeld = true;
266
+ return;
267
+ }
268
+ if (!this.#started || !this.hasDisplayTarget) return;
269
+ const scan = this.#readDisplay();
145
270
  if (document.activeElement === this.displayTarget) {
146
271
  const formatted = this.#render(scan.parts);
147
- this.displayTarget.value = formatted;
148
- this.#reflect(scan.value, formatted);
149
- } else if (scan.value === null) {
150
- this.displayTarget.value = "";
151
- this.#reflect(null, "");
272
+ this.#show(formatted);
273
+ this.#reflect(scan.value, formatted, "reconcile");
152
274
  } else {
153
- const rounded = round(scan.value, this.#precision);
154
- const formatted = this.#fixed.format(rounded);
155
- this.displayTarget.value = formatted;
156
- this.#reflect(rounded, formatted);
275
+ this.#renderFixed(scan.value, "reconcile");
157
276
  }
158
277
  }
159
- /** Normalizes any pre-filled display value to its fixed-precision form. */
278
+ #applyHeldChange() {
279
+ if (!this.#changeHeld) return;
280
+ this.#changeHeld = false;
281
+ this.#applyValueChange();
282
+ }
283
+ #readDisplay() {
284
+ const text = this.displayTarget.value;
285
+ const own = text === this.#rendered ? this.#scan(text) : null;
286
+ this.#takeInDeclarations();
287
+ return own ?? this.#scan(text);
288
+ }
289
+ #takeInDeclarations() {
290
+ this.#changeHeld = false;
291
+ this.#revalidate();
292
+ }
293
+ #morph = new MorphRenderWatcher(() => this.#applyValueChange());
160
294
  connect() {
295
+ this.#morph.observe(this.element);
161
296
  this.#started = true;
297
+ this.#takeInDeclarations();
162
298
  if (!this.hasDisplayTarget) return;
299
+ if (this.displayTarget.value === this.#rendered) {
300
+ this.#renderFixed(this.#lastValue, "reconcile");
301
+ return;
302
+ }
163
303
  const { value } = this.#scan(this.displayTarget.value);
164
304
  this.#lastValue = value === null ? null : round(value, this.#precision);
165
- if (value === null) {
166
- this.displayTarget.value = "";
167
- this.#reflect(null, "");
168
- } else {
169
- this.#reformat(true);
170
- }
305
+ this.#renderFixed(value, "reconcile");
171
306
  }
172
307
  disconnect() {
173
- this.#started = false;
308
+ this.#morph.disconnect();
309
+ this.#endUntrackedComposition();
174
310
  this.#composition.disconnect();
311
+ this.#started = false;
175
312
  }
176
- /** Tracks composition on an arriving (or swapped-in) display and normalizes it. */
177
313
  displayTargetConnected(target) {
178
314
  this.#composition.observe(target);
179
- if (this.#started) this.#reformat(true);
315
+ if (!this.#started || !this.hasDisplayTarget || this.displayTarget !== target) return;
316
+ this.#renderFixed(this.#readDisplay().value, "reconcile");
180
317
  }
181
318
  displayTargetDisconnected(target) {
182
319
  this.#composition.unobserve(target);
320
+ if (target === this.#composing) this.#endUntrackedComposition();
321
+ this.#applyValueChange();
322
+ }
323
+ #endUntrackedComposition() {
324
+ const display = this.#composing;
325
+ this.#composing = null;
326
+ if (this.hasDisplayTarget && this.displayTarget === display) {
327
+ this.#reformat(false, "reconcile");
328
+ }
183
329
  }
184
- /** Syncs a late-arriving hidden field without touching the display or events. */
185
330
  fieldTargetConnected() {
186
331
  if (this.#started) this.#resync();
187
332
  }
188
- /** Syncs a late-arriving screen-reader span the same way. */
333
+ fieldTargetDisconnected() {
334
+ if (this.#started) this.#resync();
335
+ }
189
336
  srValueTargetConnected() {
190
337
  if (this.#started) this.#resync();
191
338
  }
192
- /** Re-groups digits as the user types, preserving the caret position. */
339
+ srValueTargetDisconnected() {
340
+ if (this.#started) this.#resync();
341
+ }
193
342
  onInput(event) {
194
343
  if (this.#composition.isComposing(event)) return;
195
- this.#reformat(false);
344
+ this.#reformat(false, "edit");
196
345
  }
197
- /** Applies the fixed-precision rounding on blur. */
198
346
  format() {
199
- this.#reformat(true);
200
- }
201
- /**
202
- * Parses the display value, rewrites it grouped (optionally at fixed
203
- * precision), keeps the caret stable by significant characters, and syncs the
204
- * field, the screen-reader span, and the `change` event.
205
- *
206
- * @stimeoRenderRoot
207
- */
208
- #reformat(fixedPrecision) {
347
+ this.#reformat(true, "edit");
348
+ }
349
+ #reformat(fixedPrecision, cause) {
209
350
  if (!this.hasDisplayTarget) return;
210
351
  const raw = this.displayTarget.value;
211
352
  const { parts, value } = this.#scan(raw);
212
353
  if (fixedPrecision) {
213
- if (value === null) {
214
- this.displayTarget.value = "";
215
- this.#reflect(null, "");
216
- return;
217
- }
218
- const rounded = round(value, this.#precision);
219
- const formatted2 = this.#fixed.format(rounded);
220
- this.displayTarget.value = formatted2;
221
- this.#reflect(rounded, formatted2);
354
+ this.#renderFixed(value, cause);
222
355
  return;
223
356
  }
224
357
  const formatted = this.#render(parts);
@@ -228,19 +361,29 @@ var CurrencyInputController = class extends Controller {
228
361
  this.displayTarget.value = formatted;
229
362
  if (anchor !== null) this.#restoreCaret(formatted, anchor);
230
363
  }
231
- this.#reflect(value, value === null ? "" : formatted);
364
+ this.#rendered = formatted;
365
+ this.#reflect(value, formatted, cause);
366
+ }
367
+ #renderFixed(value, cause) {
368
+ if (value === null) {
369
+ this.#show("");
370
+ this.#reflect(null, "", cause);
371
+ return;
372
+ }
373
+ const rounded = round(value, this.#precision);
374
+ const formatted = this.#fixed.format(rounded);
375
+ this.#show(formatted);
376
+ this.#reflect(rounded, formatted, cause);
377
+ }
378
+ #show(text) {
379
+ if (this.displayTarget.value !== text) this.displayTarget.value = text;
380
+ this.#rendered = text;
232
381
  }
233
- /**
234
- * The in-progress rendering: grouped integer, sign and fraction as typed.
235
- *
236
- * @stimeoRenderRoot
237
- */
238
382
  #render(parts) {
239
383
  const int = parts.int === "" ? "" : this.#grouping.format(BigInt(parts.int));
240
384
  const frac = parts.hasDot ? this.#decimal + parts.frac : "";
241
385
  return parts.sign + int + frac;
242
386
  }
243
- /** Restores the caret to sit just after the n-th significant character. */
244
387
  #restoreCaret(formatted, significantBefore) {
245
388
  let seen = 0;
246
389
  let position = formatted.length;
@@ -256,11 +399,6 @@ var CurrencyInputController = class extends Controller {
256
399
  } catch {
257
400
  }
258
401
  }
259
- /**
260
- * Counts the characters before `caret` that survive into the rendering,
261
- * applying the same acceptance rules as {@link #scan} — a rejected keystroke
262
- * (a mid-string sign, a second decimal mark) must not shift the anchor.
263
- */
264
402
  #significantBefore(text, caret) {
265
403
  let count = 0;
266
404
  let sawSign = false;
@@ -280,36 +418,27 @@ var CurrencyInputController = class extends Controller {
280
418
  }
281
419
  return count;
282
420
  }
283
- /** Digits, signs, and the decimal mark anchor the caret; grouping does not. */
284
421
  #isSignificant(ch) {
285
422
  if (ch >= "0" && ch <= "9") return true;
286
423
  if (ch === "-" || ch === "+") return true;
287
424
  return ch === this.#decimal || ch === "." && this.#group !== ".";
288
425
  }
289
- /** Writes the normalized value to the field, the SR span, and the empty hook. */
290
426
  #write(value) {
291
427
  const isEmpty = value === null;
292
428
  if (this.hasFieldTarget) this.fieldTarget.value = isEmpty ? "" : String(value);
293
429
  if (this.hasSrValueTarget) {
294
430
  this.srValueTarget.textContent = isEmpty ? "" : this.#accessible.format(value);
295
431
  }
296
- this.element.toggleAttribute("data-stimeo--currency-input-empty", isEmpty);
432
+ this.element.toggleAttribute(`data-${this.identifier}-empty`, isEmpty);
297
433
  }
298
- /** {@link #write}, then reports a moved value as `change` (`""` rides with `null`). */
299
- #reflect(value, formatted) {
434
+ #reflect(value, formatted, cause) {
300
435
  this.#write(value);
301
- if (value !== this.#lastValue) {
302
- this.#lastValue = value;
303
- this.dispatch("change", { detail: { value, formatted } });
304
- }
436
+ if (value === this.#lastValue) return;
437
+ this.#lastValue = value;
438
+ const detail = { value, formatted: value === null ? "" : formatted };
439
+ if (cause === "edit") this.dispatch("change", { detail });
440
+ else this.dispatch("reconcile", { detail });
305
441
  }
306
- /**
307
- * Scans arbitrary input text into its number-shaped parts and value. Keeps
308
- * digits, one leading sign, one decimal mark, and the fraction verbatim;
309
- * everything else (grouping, symbols, words) is dropped. An ASCII "." is only
310
- * a decimal mark where it is not the locale's grouping separator. Precision
311
- * plays no part here — rounding belongs to the blur-time format.
312
- */
313
442
  #scan(text) {
314
443
  let sign = "";
315
444
  let int = "";
@@ -329,27 +458,19 @@ var CurrencyInputController = class extends Controller {
329
458
  const value = Number(`${sign}${int}.${frac}`);
330
459
  return { parts, value: Number.isFinite(value) ? value : null };
331
460
  }
332
- /** Whether `ch` reads as this locale's decimal mark. */
333
461
  #isDecimalMark(ch) {
334
462
  return ch === this.#decimal || ch === "." && this.#group !== ".";
335
463
  }
336
- /** Re-syncs field / srValue / hook from the current display without dispatching. */
337
464
  #resync() {
338
465
  if (!this.hasDisplayTarget) return;
339
- this.#write(this.#scan(this.displayTarget.value).value);
340
- }
341
- /**
342
- * Validates the declared Values, falling back to each Value's default when a
343
- * declaration cannot be interpreted (a malformed locale or currency tag, a
344
- * precision outside `Intl`'s 0–100 integer range), and rebuilds the cached
345
- * formatters from the validated set.
346
- */
466
+ const value = this.#composition.isComposing() ? this.#lastValue : this.#scan(this.displayTarget.value).value;
467
+ this.#write(value);
468
+ }
347
469
  #revalidate() {
348
470
  const resolved = resolveLocale(this.element, this.localeValue);
349
471
  const usable = intlFormatter(Intl.NumberFormat, resolved, {});
350
472
  this.#locale = usable === null ? FALLBACK_LOCALE : resolved;
351
- const precision = this.precisionValue;
352
- this.#precision = Number.isInteger(precision) && precision >= 0 && precision <= 100 ? precision : 2;
473
+ this.#precision = this.#safePrecision;
353
474
  this.#currency = "";
354
475
  if (this.currencyValue !== "") {
355
476
  const declaredCurrency = intlFormatter(Intl.NumberFormat, this.#locale, {
@@ -397,12 +518,6 @@ var CurrencyInputController = class extends Controller {
397
518
  if (digit !== String(i)) this.#digits.set(digit, String(i));
398
519
  }
399
520
  }
400
- /**
401
- * Maps the locale's own digits to ASCII, folds full-width forms (the digits,
402
- * signs, and marks an IME confirms as 0-9+-.,) through the shared
403
- * half-width mapping, and reads U+2212 MINUS SIGN as "-", which some locales'
404
- * formatted output uses for negatives.
405
- */
406
521
  #normalize(text) {
407
522
  let out = "";
408
523
  for (const ch of text) {
@@ -417,6 +532,15 @@ var CurrencyInputController = class extends Controller {
417
532
  }
418
533
  return out;
419
534
  }
535
+ get #safePrecision() {
536
+ return this.#numbers.read(
537
+ this,
538
+ "precision",
539
+ this.precisionValue,
540
+ _CurrencyInputController.values.precision.default,
541
+ _CurrencyInputController.valueConstraints.precision
542
+ );
543
+ }
420
544
  };
421
545
  function round(value, precision) {
422
546
  const factor = 10 ** Math.max(0, precision);
@@ -426,5 +550,4 @@ function round(value, precision) {
426
550
  }
427
551
 
428
552
  export { CurrencyInputController };
429
- //# sourceMappingURL=currency_input_controller.js.map
430
553
  //# sourceMappingURL=currency_input_controller.js.map