stimeo-ui 0.14.0 → 0.16.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 (107) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +212 -0
  3. data/README.md +120 -0
  4. data/dist/cable/index.js +123 -29
  5. data/dist/controllers/accordion_controller.js +98 -9
  6. data/dist/controllers/announcer_controller.js +96 -62
  7. data/dist/controllers/auto_submit_controller.js +83 -7
  8. data/dist/controllers/avatar_controller.js +1 -1
  9. data/dist/controllers/breadcrumb_controller.js +38 -11
  10. data/dist/controllers/bulk_select_controller.js +7 -6
  11. data/dist/controllers/calendar_controller.js +340 -123
  12. data/dist/controllers/carousel_controller.js +263 -38
  13. data/dist/controllers/character_counter_controller.js +40 -2
  14. data/dist/controllers/checkbox_controller.js +81 -12
  15. data/dist/controllers/clipboard_controller.js +63 -5
  16. data/dist/controllers/collapsible_controller.js +99 -14
  17. data/dist/controllers/color_picker_controller.js +80 -34
  18. data/dist/controllers/combobox_controller.js +106 -16
  19. data/dist/controllers/command_palette_controller.js +35 -3
  20. data/dist/controllers/conditional_fields_controller.js +85 -17
  21. data/dist/controllers/confirm_controller.js +3 -0
  22. data/dist/controllers/context_menu_controller.js +32 -12
  23. data/dist/controllers/countdown_controller.js +129 -26
  24. data/dist/controllers/currency_input_controller.js +221 -67
  25. data/dist/controllers/data_grid_controller.js +195 -29
  26. data/dist/controllers/date_range_picker_controller.js +151 -30
  27. data/dist/controllers/dialog_controller.js +35 -8
  28. data/dist/controllers/direct_upload_controller.js +22 -4
  29. data/dist/controllers/dirty_form_controller.js +46 -13
  30. data/dist/controllers/dismissible_controller.js +1 -0
  31. data/dist/controllers/drawer_controller.js +54 -19
  32. data/dist/controllers/dropdown_controller.js +36 -9
  33. data/dist/controllers/editable_controller.js +34 -0
  34. data/dist/controllers/file_dropzone_controller.js +144 -51
  35. data/dist/controllers/filter_controller.js +20 -6
  36. data/dist/controllers/flash_controller.js +432 -71
  37. data/dist/controllers/focus_controller.js +1 -0
  38. data/dist/controllers/form_field_controller.js +7 -5
  39. data/dist/controllers/form_validation_controller.js +19 -13
  40. data/dist/controllers/frame_loading_controller.js +45 -8
  41. data/dist/controllers/highlight_controller.js +82 -25
  42. data/dist/controllers/hover_card_controller.js +40 -14
  43. data/dist/controllers/idle_controller.js +90 -5
  44. data/dist/controllers/input_mask_controller.js +65 -9
  45. data/dist/controllers/intersection_controller.js +3 -0
  46. data/dist/controllers/lazy_frame_controller.js +11 -2
  47. data/dist/controllers/listbox_controller.js +203 -45
  48. data/dist/controllers/local_time_controller.js +10 -5
  49. data/dist/controllers/masonry_controller.js +31 -15
  50. data/dist/controllers/menu_controller.js +45 -16
  51. data/dist/controllers/menubar_controller.js +58 -24
  52. data/dist/controllers/meter_controller.js +9 -5
  53. data/dist/controllers/multi_select_controller.js +278 -104
  54. data/dist/controllers/navigation_menu_controller.js +48 -15
  55. data/dist/controllers/nested_form_controller.js +37 -8
  56. data/dist/controllers/network_status_controller.js +9 -1
  57. data/dist/controllers/number_input_controller.js +124 -21
  58. data/dist/controllers/optimistic_controller.js +42 -5
  59. data/dist/controllers/otp_controller.js +198 -55
  60. data/dist/controllers/overflow_indicator_controller.js +115 -21
  61. data/dist/controllers/overflow_menu_controller.js +141 -44
  62. data/dist/controllers/pagination_controller.js +74 -28
  63. data/dist/controllers/password_reveal_controller.js +59 -2
  64. data/dist/controllers/persist_controller.js +30 -8
  65. data/dist/controllers/pointer_drag_controller.js +131 -52
  66. data/dist/controllers/popover_controller.js +45 -11
  67. data/dist/controllers/portal_controller.js +6 -2
  68. data/dist/controllers/preview_guard_controller.js +16 -1
  69. data/dist/controllers/progress_controller.js +8 -4
  70. data/dist/controllers/radio_group_controller.js +42 -17
  71. data/dist/controllers/range_slider_controller.js +88 -42
  72. data/dist/controllers/rating_controller.js +39 -15
  73. data/dist/controllers/read_more_controller.js +100 -7
  74. data/dist/controllers/reading_progress_controller.js +65 -19
  75. data/dist/controllers/relative_time_controller.js +10 -5
  76. data/dist/controllers/resizable_controller.js +82 -22
  77. data/dist/controllers/scroll_area_controller.js +75 -27
  78. data/dist/controllers/scroll_restore_controller.js +37 -16
  79. data/dist/controllers/scroll_visibility_controller.js +49 -30
  80. data/dist/controllers/scrollspy_controller.js +71 -26
  81. data/dist/controllers/separator_controller.js +66 -37
  82. data/dist/controllers/sidebar_controller.js +77 -18
  83. data/dist/controllers/skeleton_controller.js +6 -1
  84. data/dist/controllers/slider_controller.js +82 -47
  85. data/dist/controllers/smart_sticky_header_controller.js +60 -26
  86. data/dist/controllers/sortable_controller.js +17 -2
  87. data/dist/controllers/spinner_controller.js +10 -2
  88. data/dist/controllers/step_indicator_controller.js +18 -17
  89. data/dist/controllers/stepper_controller.js +101 -19
  90. data/dist/controllers/stick_to_bottom_controller.js +104 -8
  91. data/dist/controllers/submit_once_controller.js +45 -9
  92. data/dist/controllers/switch_controller.js +101 -10
  93. data/dist/controllers/tabs_controller.js +21 -2
  94. data/dist/controllers/tags_input_controller.js +209 -59
  95. data/dist/controllers/textarea_autosize_controller.js +29 -3
  96. data/dist/controllers/theme_controller.js +64 -14
  97. data/dist/controllers/time_picker_controller.js +23 -8
  98. data/dist/controllers/toast_controller.js +451 -105
  99. data/dist/controllers/toggle_group_controller.js +159 -23
  100. data/dist/controllers/toolbar_controller.js +32 -0
  101. data/dist/controllers/tooltip_controller.js +39 -13
  102. data/dist/controllers/transition_controller.js +4 -0
  103. data/dist/controllers/tree_view_controller.js +169 -16
  104. data/dist/index.js +5002 -1911
  105. data/dist/positioning/index.js +2 -0
  106. data/lib/stimeo/ui/version.rb +1 -1
  107. metadata +2 -2
@@ -3,11 +3,19 @@ import { Controller } from '@hotwired/stimulus';
3
3
  // src/controllers/currency_input_controller.ts
4
4
 
5
5
  // src/utils/composition_tracker.ts
6
+ var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
7
+ "insertCompositionText",
8
+ "insertFromComposition",
9
+ "deleteCompositionText",
10
+ "deleteByComposition"
11
+ ]);
6
12
  var CompositionTracker = class {
7
13
  #observedTargets = /* @__PURE__ */ new Set();
8
14
  #activeTargets = /* @__PURE__ */ new Set();
9
15
  #onStart;
10
16
  #onEnd;
17
+ /** The field whose confirming `input` is still owed, while the window is open. */
18
+ #confirmedTarget = null;
11
19
  constructor(options = {}) {
12
20
  this.#onStart = options.onStart;
13
21
  this.#onEnd = options.onEnd;
@@ -17,6 +25,7 @@ var CompositionTracker = class {
17
25
  if (this.#observedTargets.has(target)) return;
18
26
  target.addEventListener("compositionstart", this.#handleStart);
19
27
  target.addEventListener("compositionend", this.#handleEnd);
28
+ target.addEventListener("keydown", this.#handleKeydown);
20
29
  this.#observedTargets.add(target);
21
30
  }
22
31
  /** Stops tracking one target and clears any active composition it owned. */
@@ -24,29 +33,52 @@ var CompositionTracker = class {
24
33
  if (!this.#observedTargets.delete(target)) return;
25
34
  target.removeEventListener("compositionstart", this.#handleStart);
26
35
  target.removeEventListener("compositionend", this.#handleEnd);
36
+ target.removeEventListener("keydown", this.#handleKeydown);
27
37
  this.#activeTargets.delete(target);
38
+ if (this.#confirmedTarget === target) this.#confirmedTarget = null;
28
39
  }
29
40
  /** Releases every listener and clears state so reconnect starts cleanly. */
30
41
  disconnect() {
31
42
  for (const target of this.#observedTargets) {
32
43
  target.removeEventListener("compositionstart", this.#handleStart);
33
44
  target.removeEventListener("compositionend", this.#handleEnd);
45
+ target.removeEventListener("keydown", this.#handleKeydown);
34
46
  }
35
47
  this.#observedTargets.clear();
36
48
  this.#activeTargets.clear();
49
+ this.#confirmedTarget = null;
37
50
  }
38
51
  /** True when lifecycle tracking or the current event reports composition. */
39
52
  isComposing(event) {
40
53
  return this.#activeTargets.size > 0 || event?.isComposing === true;
41
54
  }
55
+ /**
56
+ * Whether `event` is the `input` echoing the composition just confirmed.
57
+ *
58
+ * Asking closes the window either way, so one confirmation is folded at most
59
+ * once and a consumer asks once per `input`.
60
+ */
61
+ consumesConfirmedInput(event) {
62
+ const confirmed = this.#confirmedTarget;
63
+ this.#confirmedTarget = null;
64
+ if (confirmed === null || confirmed !== event.target) return false;
65
+ const inputType = event.inputType;
66
+ return !inputType || COMPOSITION_INPUT_TYPES.has(inputType);
67
+ }
42
68
  #handleStart = (event) => {
69
+ this.#confirmedTarget = null;
43
70
  if (event.currentTarget) this.#activeTargets.add(event.currentTarget);
44
71
  this.#onStart?.(event);
45
72
  };
46
73
  #handleEnd = (event) => {
47
74
  if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);
75
+ this.#confirmedTarget = event.target;
48
76
  this.#onEnd?.(event);
49
77
  };
78
+ /** A key on an observed field opens an edit of its own, so no echo is owed. */
79
+ #handleKeydown = () => {
80
+ this.#confirmedTarget = null;
81
+ };
50
82
  };
51
83
 
52
84
  // src/utils/half_width.ts
@@ -88,24 +120,39 @@ function construct(factory, locale, options, fallbackLocale) {
88
120
  }
89
121
  }
90
122
 
123
+ // src/utils/locale.ts
124
+ function resolveLocale(element, declared = "") {
125
+ return declared || element.closest("[lang]")?.getAttribute("lang") || void 0;
126
+ }
127
+
91
128
  // src/controllers/currency_input_controller.ts
129
+ var FALLBACK_LOCALE = "en-US";
92
130
  var CurrencyInputController = class extends Controller {
93
131
  static targets = ["display", "field", "srValue"];
94
132
  static values = {
95
- locale: { type: String, default: "en-US" },
133
+ locale: { type: String, default: "" },
96
134
  currency: { type: String, default: "" },
97
135
  precision: { type: Number, default: 2 }
98
136
  };
99
137
  static actions = ["format", "onInput"];
100
- static events = ["change"];
101
- /** Last committed numeric value, to suppress duplicate `change` dispatches. */
138
+ static events = ["change", "reconcile"];
139
+ /** The numeric value last committed; both events report only a move away from it. */
102
140
  #lastValue = null;
103
141
  #started = false;
142
+ /** A declaration change that arrived mid-composition, applied once nothing composes. */
143
+ #changeHeld = false;
144
+ /** The display a composition is running on, until that composition ends. */
145
+ #composing = null;
146
+ /**
147
+ * The display text this controller last rendered. It tells this controller's own
148
+ * text, written with the separators in force, from text the page wrote.
149
+ */
150
+ #rendered = null;
104
151
  /** Validated mirrors of the Values; the hot path never reads a raw Value. */
105
- #locale = "en-US";
152
+ #locale;
106
153
  #currency = "";
107
154
  #precision = 2;
108
- /** Formatters rebuilt only when a Value changes, never per keystroke. */
155
+ /** Formatters rebuilt when the declarations are taken in, never per keystroke. */
109
156
  #grouping;
110
157
  #fixed;
111
158
  #accessible;
@@ -113,11 +160,25 @@ var CurrencyInputController = class extends Controller {
113
160
  #decimal = ".";
114
161
  /** The locale's non-Latin digits mapped back to ASCII (empty for Latin locales). */
115
162
  #digits = /* @__PURE__ */ new Map();
116
- /** Holds mid-composition input so the IME's uncommitted text is never rewritten. */
163
+ /**
164
+ * Holds mid-composition input so the IME's uncommitted text is never rewritten.
165
+ * The confirmed text is formatted once, and a declaration change the composition
166
+ * held back is applied after it.
167
+ */
117
168
  #composition = new CompositionTracker({
118
- onEnd: () => this.#reformat(false)
169
+ onStart: (event) => {
170
+ this.#composing = event.currentTarget;
171
+ },
172
+ onEnd: () => {
173
+ this.#composing = null;
174
+ this.#reformat(false, "edit");
175
+ this.#applyHeldChange();
176
+ }
119
177
  });
120
- /** Re-validates on declaration changes and re-renders the committed display. */
178
+ /**
179
+ * Re-validates on declaration changes and re-renders the committed display,
180
+ * reporting a value the re-render moved as `reconcile`.
181
+ */
121
182
  localeValueChanged() {
122
183
  this.#applyValueChange();
123
184
  }
@@ -128,52 +189,123 @@ var CurrencyInputController = class extends Controller {
128
189
  this.#applyValueChange();
129
190
  }
130
191
  /**
131
- * Scans the display under the *outgoing* configuration (its separators wrote
132
- * that text), then revalidates and re-renders under the new one — so a locale
133
- * switch re-interprets the value, never the old text with new separators.
192
+ * Reads the display with the declarations its text was written for, then
193
+ * re-renders it under the new ones — so a locale switch re-interprets the
194
+ * value, never the old text with new separators, and text the page wrote for
195
+ * the declarations it changes is never read with the old ones.
196
+ *
197
+ * While the display is composing, its text is the IME's: the change is held,
198
+ * configuration and all, until the composition ends, so the confirmed text is
199
+ * read with the separators it was typed against before the change applies.
200
+ *
201
+ * @stimeoRenderRoot
134
202
  */
135
203
  #applyValueChange() {
136
- const scan = this.#started && this.hasDisplayTarget ? this.#scan(this.displayTarget.value) : null;
137
- this.#revalidate();
138
- if (!scan || !this.hasDisplayTarget) return;
204
+ if (this.#composition.isComposing()) {
205
+ this.#changeHeld = true;
206
+ return;
207
+ }
208
+ if (!this.#started || !this.hasDisplayTarget) return;
209
+ const scan = this.#readDisplay();
139
210
  if (document.activeElement === this.displayTarget) {
140
211
  const formatted = this.#render(scan.parts);
141
- this.displayTarget.value = formatted;
142
- this.#reflect(scan.value, formatted);
143
- } else if (scan.value === null) {
144
- this.displayTarget.value = "";
145
- this.#reflect(null, "");
212
+ this.#show(formatted);
213
+ this.#reflect(scan.value, formatted, "reconcile");
146
214
  } else {
147
- const rounded = round(scan.value, this.#precision);
148
- const formatted = this.#fixed.format(rounded);
149
- this.displayTarget.value = formatted;
150
- this.#reflect(rounded, formatted);
215
+ this.#renderFixed(scan.value, "reconcile");
151
216
  }
152
217
  }
153
- /** Normalizes any pre-filled display value to its fixed-precision form. */
218
+ /** Applies a declaration change a composition held back. */
219
+ #applyHeldChange() {
220
+ if (!this.#changeHeld) return;
221
+ this.#changeHeld = false;
222
+ this.#applyValueChange();
223
+ }
224
+ /**
225
+ * Reads the display and takes in the declarations the page carries now. This
226
+ * controller's own rendering was written with the separators in force, so it
227
+ * is read before they change; any other text is the page's, written for the
228
+ * declarations it carries, so it is read after.
229
+ */
230
+ #readDisplay() {
231
+ const text = this.displayTarget.value;
232
+ const own = text === this.#rendered ? this.#scan(text) : null;
233
+ this.#takeInDeclarations();
234
+ return own ?? this.#scan(text);
235
+ }
236
+ /** Takes in the declarations the page carries now, a change a composition held among them. */
237
+ #takeInDeclarations() {
238
+ this.#changeHeld = false;
239
+ this.#revalidate();
240
+ }
241
+ /**
242
+ * Takes in the declarations the page carries now and normalizes the display
243
+ * to its fixed-precision form.
244
+ *
245
+ * A display still showing this controller's own last rendering is not read
246
+ * back: its separators may belong to declarations that changed while the
247
+ * controller was away, and the committed value is known anyway. That value is
248
+ * re-rendered at the current fixed precision, which rounds it whether
249
+ * `precision` changed meanwhile or the entry was left unrounded mid-typing, and
250
+ * a move the rounding causes is reported as `reconcile`. Any other text is the
251
+ * page's, written for the declarations it carries, so it is read with them.
252
+ */
154
253
  connect() {
155
254
  this.#started = true;
255
+ this.#takeInDeclarations();
156
256
  if (!this.hasDisplayTarget) return;
257
+ if (this.displayTarget.value === this.#rendered) {
258
+ this.#renderFixed(this.#lastValue, "reconcile");
259
+ return;
260
+ }
157
261
  const { value } = this.#scan(this.displayTarget.value);
158
262
  this.#lastValue = value === null ? null : round(value, this.#precision);
159
- if (value === null) {
160
- this.displayTarget.value = "";
161
- this.#reflect(null, "");
162
- } else {
163
- this.#reformat(true);
164
- }
263
+ this.#renderFixed(value, "reconcile");
165
264
  }
265
+ /**
266
+ * Releases the composition listeners. A composition still running hears no
267
+ * `compositionend` once they are gone, so it ends here; a change it held is
268
+ * taken in on the next `connect()`.
269
+ */
166
270
  disconnect() {
167
- this.#started = false;
271
+ this.#endUntrackedComposition();
168
272
  this.#composition.disconnect();
273
+ this.#started = false;
169
274
  }
170
- /** Tracks composition on an arriving (or swapped-in) display and normalizes it. */
275
+ /**
276
+ * Tracks composition on an arriving (or swapped-in) display and normalizes it
277
+ * under the declarations the page carries now, reading this controller's own
278
+ * rendering with the separators it was written under; an amount that differs
279
+ * from the committed one is the page's, so it is reported as `reconcile`.
280
+ */
171
281
  displayTargetConnected(target) {
172
282
  this.#composition.observe(target);
173
- if (this.#started) this.#reformat(true);
283
+ if (!this.#started) return;
284
+ this.#renderFixed(this.#readDisplay().value, "reconcile");
174
285
  }
286
+ /**
287
+ * Stops tracking a display that leaves. A composition running on it hears no
288
+ * `compositionend` any more, so it ends here; a change it held waits for the
289
+ * next display to be read.
290
+ */
175
291
  displayTargetDisconnected(target) {
176
292
  this.#composition.unobserve(target);
293
+ this.#endUntrackedComposition();
294
+ }
295
+ /**
296
+ * Ends a composition no `compositionend` will reach. A display still in place —
297
+ * one a move re-inserted, or the display of a controller that moves — holds the
298
+ * text the composition left, typed against the declarations in force, so it is
299
+ * read as `compositionend` would read it and becomes this controller's own
300
+ * rendering. The page's move ended the composition, so a value it moves is
301
+ * reported as `reconcile`. A display that left takes its composition along.
302
+ */
303
+ #endUntrackedComposition() {
304
+ const display = this.#composing;
305
+ this.#composing = null;
306
+ if (this.hasDisplayTarget && this.displayTarget === display) {
307
+ this.#reformat(false, "reconcile");
308
+ }
177
309
  }
178
310
  /** Syncs a late-arriving hidden field without touching the display or events. */
179
311
  fieldTargetConnected() {
@@ -186,33 +318,23 @@ var CurrencyInputController = class extends Controller {
186
318
  /** Re-groups digits as the user types, preserving the caret position. */
187
319
  onInput(event) {
188
320
  if (this.#composition.isComposing(event)) return;
189
- this.#reformat(false);
321
+ this.#reformat(false, "edit");
190
322
  }
191
323
  /** Applies the fixed-precision rounding on blur. */
192
324
  format() {
193
- this.#reformat(true);
325
+ this.#reformat(true, "edit");
194
326
  }
195
327
  /**
196
328
  * Parses the display value, rewrites it grouped (optionally at fixed
197
329
  * precision), keeps the caret stable by significant characters, and syncs the
198
- * field, the screen-reader span, and the `change` event.
199
- *
200
- * @stimeoRenderRoot
330
+ * field, the screen-reader span, and the event `cause` selects.
201
331
  */
202
- #reformat(fixedPrecision) {
332
+ #reformat(fixedPrecision, cause) {
203
333
  if (!this.hasDisplayTarget) return;
204
334
  const raw = this.displayTarget.value;
205
335
  const { parts, value } = this.#scan(raw);
206
336
  if (fixedPrecision) {
207
- if (value === null) {
208
- this.displayTarget.value = "";
209
- this.#reflect(null, "");
210
- return;
211
- }
212
- const rounded = round(value, this.#precision);
213
- const formatted2 = this.#fixed.format(rounded);
214
- this.displayTarget.value = formatted2;
215
- this.#reflect(rounded, formatted2);
337
+ this.#renderFixed(value, cause);
216
338
  return;
217
339
  }
218
340
  const formatted = this.#render(parts);
@@ -222,12 +344,31 @@ var CurrencyInputController = class extends Controller {
222
344
  this.displayTarget.value = formatted;
223
345
  if (anchor !== null) this.#restoreCaret(formatted, anchor);
224
346
  }
225
- this.#reflect(value, value === null ? "" : formatted);
347
+ this.#rendered = formatted;
348
+ this.#reflect(value, formatted, cause);
349
+ }
350
+ /**
351
+ * Shows `value` at the fixed precision — an empty display for no value — and
352
+ * reports a move under the event `cause` selects.
353
+ */
354
+ #renderFixed(value, cause) {
355
+ if (value === null) {
356
+ this.#show("");
357
+ this.#reflect(null, "", cause);
358
+ return;
359
+ }
360
+ const rounded = round(value, this.#precision);
361
+ const formatted = this.#fixed.format(rounded);
362
+ this.#show(formatted);
363
+ this.#reflect(rounded, formatted, cause);
364
+ }
365
+ /** Writes `text` to the display as this controller's own rendering. */
366
+ #show(text) {
367
+ this.displayTarget.value = text;
368
+ this.#rendered = text;
226
369
  }
227
370
  /**
228
371
  * The in-progress rendering: grouped integer, sign and fraction as typed.
229
- *
230
- * @stimeoRenderRoot
231
372
  */
232
373
  #render(parts) {
233
374
  const int = parts.int === "" ? "" : this.#grouping.format(BigInt(parts.int));
@@ -287,15 +428,22 @@ var CurrencyInputController = class extends Controller {
287
428
  if (this.hasSrValueTarget) {
288
429
  this.srValueTarget.textContent = isEmpty ? "" : this.#accessible.format(value);
289
430
  }
290
- this.element.toggleAttribute("data-stimeo--currency-input-empty", isEmpty);
431
+ this.element.toggleAttribute(`data-${this.identifier}-empty`, isEmpty);
291
432
  }
292
- /** {@link #write}, then reports a moved value as `change` (`""` rides with `null`). */
293
- #reflect(value, formatted) {
433
+ /**
434
+ * {@link #write}, then reports a moved value under the event `cause` selects:
435
+ * `change` for the user's edit, `reconcile` for a move the page caused. The
436
+ * display may hold an in-progress `-`, but a `null` value always rides with an
437
+ * empty `formatted`, whichever event reports it, so consumers can treat the
438
+ * pair as cleared.
439
+ */
440
+ #reflect(value, formatted, cause) {
294
441
  this.#write(value);
295
- if (value !== this.#lastValue) {
296
- this.#lastValue = value;
297
- this.dispatch("change", { detail: { value, formatted } });
298
- }
442
+ if (value === this.#lastValue) return;
443
+ this.#lastValue = value;
444
+ const detail = { value, formatted: value === null ? "" : formatted };
445
+ if (cause === "edit") this.dispatch("change", { detail });
446
+ else this.dispatch("reconcile", { detail });
299
447
  }
300
448
  /**
301
449
  * Scans arbitrary input text into its number-shaped parts and value. Keeps
@@ -327,10 +475,15 @@ var CurrencyInputController = class extends Controller {
327
475
  #isDecimalMark(ch) {
328
476
  return ch === this.#decimal || ch === "." && this.#group !== ".";
329
477
  }
330
- /** Re-syncs field / srValue / hook from the current display without dispatching. */
478
+ /**
479
+ * Re-syncs field / srValue / hook without dispatching: from the display, or —
480
+ * while it is composing, when its text is not committed yet — from the value
481
+ * last committed.
482
+ */
331
483
  #resync() {
332
484
  if (!this.hasDisplayTarget) return;
333
- this.#write(this.#scan(this.displayTarget.value).value);
485
+ const value = this.#composition.isComposing() ? this.#lastValue : this.#scan(this.displayTarget.value).value;
486
+ this.#write(value);
334
487
  }
335
488
  /**
336
489
  * Validates the declared Values, falling back to each Value's default when a
@@ -339,8 +492,9 @@ var CurrencyInputController = class extends Controller {
339
492
  * formatters from the validated set.
340
493
  */
341
494
  #revalidate() {
342
- const declaredLocale = intlFormatter(Intl.NumberFormat, this.localeValue, {});
343
- this.#locale = declaredLocale === null ? "en-US" : this.localeValue;
495
+ const resolved = resolveLocale(this.element, this.localeValue);
496
+ const usable = intlFormatter(Intl.NumberFormat, resolved, {});
497
+ this.#locale = usable === null ? FALLBACK_LOCALE : resolved;
344
498
  const precision = this.precisionValue;
345
499
  this.#precision = Number.isInteger(precision) && precision >= 0 && precision <= 100 ? precision : 2;
346
500
  this.#currency = "";
@@ -355,7 +509,7 @@ var CurrencyInputController = class extends Controller {
355
509
  Intl.NumberFormat,
356
510
  this.#locale,
357
511
  { useGrouping: true, maximumFractionDigits: 0 },
358
- this.#locale
512
+ FALLBACK_LOCALE
359
513
  );
360
514
  this.#fixed = intlFormatter(
361
515
  Intl.NumberFormat,
@@ -365,15 +519,15 @@ var CurrencyInputController = class extends Controller {
365
519
  minimumFractionDigits: this.#precision,
366
520
  maximumFractionDigits: this.#precision
367
521
  },
368
- this.#locale
522
+ FALLBACK_LOCALE
369
523
  );
370
524
  this.#accessible = this.#currency ? intlFormatter(
371
525
  Intl.NumberFormat,
372
526
  this.#locale,
373
527
  { style: "currency", currency: this.#currency },
374
- this.#locale
528
+ FALLBACK_LOCALE
375
529
  ) : this.#fixed;
376
- const parts = intlFormatter(Intl.NumberFormat, this.#locale, {}, this.#locale).formatToParts(
530
+ const parts = intlFormatter(Intl.NumberFormat, this.#locale, {}, FALLBACK_LOCALE).formatToParts(
377
531
  11111.1
378
532
  );
379
533
  this.#group = parts.find((p) => p.type === "group")?.value ?? ",";
@@ -383,7 +537,7 @@ var CurrencyInputController = class extends Controller {
383
537
  Intl.NumberFormat,
384
538
  this.#locale,
385
539
  { useGrouping: false },
386
- this.#locale
540
+ FALLBACK_LOCALE
387
541
  );
388
542
  for (let i = 0; i <= 9; i++) {
389
543
  const digit = digitFormatter.format(i);