stimeo-ui 0.12.0 → 0.14.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +71 -0
  3. data/dist/cable/index.js +438 -152
  4. data/dist/controllers/calendar_controller.js +34 -2
  5. data/dist/controllers/carousel_controller.js +9 -7
  6. data/dist/controllers/command_palette_controller.js +2 -2
  7. data/dist/controllers/conditional_fields_controller.js +7 -1
  8. data/dist/controllers/currency_input_controller.js +68 -23
  9. data/dist/controllers/date_range_picker_controller.js +34 -10
  10. data/dist/controllers/direct_upload_controller.js +21 -10
  11. data/dist/controllers/drawer_controller.js +2 -2
  12. data/dist/controllers/form_field_controller.js +7 -1
  13. data/dist/controllers/input_mask_controller.js +33 -12
  14. data/dist/controllers/intersection_controller.js +10 -5
  15. data/dist/controllers/lazy_frame_controller.js +10 -5
  16. data/dist/controllers/local_time_controller.js +34 -7
  17. data/dist/controllers/masonry_controller.js +1 -1
  18. data/dist/controllers/menu_controller.js +10 -2
  19. data/dist/controllers/menubar_controller.js +10 -2
  20. data/dist/controllers/navigation_menu_controller.js +8 -2
  21. data/dist/controllers/optimistic_controller.js +141 -20
  22. data/dist/controllers/otp_controller.js +23 -10
  23. data/dist/controllers/overflow_menu_controller.js +10 -1
  24. data/dist/controllers/pointer_drag_controller.js +10 -3
  25. data/dist/controllers/portal_controller.js +21 -10
  26. data/dist/controllers/radio_group_controller.js +11 -3
  27. data/dist/controllers/relative_time_controller.js +31 -5
  28. data/dist/controllers/reset_before_cache_controller.js +23 -10
  29. data/dist/controllers/resizable_controller.js +7 -2
  30. data/dist/controllers/roving_controller.js +7 -3
  31. data/dist/controllers/scroll_visibility_controller.js +22 -13
  32. data/dist/controllers/scrollspy_controller.js +11 -6
  33. data/dist/controllers/smart_sticky_header_controller.js +23 -12
  34. data/dist/controllers/sticky_observer_controller.js +10 -5
  35. data/dist/controllers/theme_controller.js +23 -13
  36. data/dist/controllers/toast_controller.js +1 -1
  37. data/dist/controllers/toggle_group_controller.js +11 -3
  38. data/dist/controllers/toolbar_controller.js +7 -3
  39. data/dist/index.js +259 -202
  40. data/lib/stimeo/ui/version.rb +1 -1
  41. metadata +2 -2
@@ -18,6 +18,35 @@ function isReservedArrowChord(event, allow = []) {
18
18
  return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
19
19
  }
20
20
 
21
+ // src/utils/intl_format.ts
22
+ var formatters = /* @__PURE__ */ new WeakMap();
23
+ function intlFormatter(factory, locale, options, fallbackLocale) {
24
+ const cache = cacheFor(factory);
25
+ const key = cacheKey(locale, options, fallbackLocale);
26
+ if (cache.has(key)) return cache.get(key);
27
+ const formatter = construct(factory, locale, options, fallbackLocale);
28
+ cache.set(key, formatter);
29
+ return formatter;
30
+ }
31
+ function cacheFor(factory) {
32
+ const existing = formatters.get(factory);
33
+ if (existing) return existing;
34
+ const created = /* @__PURE__ */ new Map();
35
+ formatters.set(factory, created);
36
+ return created;
37
+ }
38
+ function cacheKey(locale, options, fallbackLocale) {
39
+ return JSON.stringify([locale, options, fallbackLocale]);
40
+ }
41
+ function construct(factory, locale, options, fallbackLocale) {
42
+ try {
43
+ return new factory(locale, options);
44
+ } catch (error) {
45
+ if (!(error instanceof RangeError)) throw error;
46
+ return new factory(fallbackLocale, options);
47
+ }
48
+ }
49
+
21
50
  // src/utils/dates.ts
22
51
  function toISODateString(date) {
23
52
  const year = date.getFullYear();
@@ -51,6 +80,10 @@ function parseISOMonthString(monthStr) {
51
80
  function toISOMonthString(date) {
52
81
  return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
53
82
  }
83
+ var MONTH_LABEL_OPTIONS = { month: "long", year: "numeric" };
84
+ function monthLabelFormatter(locale) {
85
+ return intlFormatter(Intl.DateTimeFormat, locale, MONTH_LABEL_OPTIONS, "en");
86
+ }
54
87
 
55
88
  // src/utils/safe_timeout.ts
56
89
  var TimerRegistry = class {
@@ -288,8 +321,7 @@ var CalendarController = class extends Controller {
288
321
  const month = monthStart.getMonth() + 1;
289
322
  if (this.hasLabelTarget) {
290
323
  const lang = document.documentElement.lang || "en";
291
- const formatter = new Intl.DateTimeFormat(lang, { month: "long", year: "numeric" });
292
- this.labelTarget.textContent = formatter.format(monthStart);
324
+ this.labelTarget.textContent = monthLabelFormatter(lang).format(monthStart);
293
325
  }
294
326
  const days = this.#calculateGridDays(year, month);
295
327
  const dayElements = this.dayTargets;
@@ -90,6 +90,12 @@ var BeforeCacheReset = class _BeforeCacheReset {
90
90
  }
91
91
  };
92
92
 
93
+ // src/utils/event_owner.ts
94
+ function ownerIndex(candidates, node) {
95
+ if (!(node instanceof Node)) return -1;
96
+ return candidates.findIndex((candidate) => candidate.contains(node));
97
+ }
98
+
93
99
  // src/utils/microtask_coalescer.ts
94
100
  var MicrotaskCoalescer = class {
95
101
  #run;
@@ -252,7 +258,7 @@ var CarouselController = class extends Controller {
252
258
  #handledEvents = /* @__PURE__ */ new WeakSet();
253
259
  /**
254
260
  * Whether `connect()` has run. Scheduling is already inert outside that window
255
- * ({@link MicrotaskCoalescer}), so this gates the Value callbacks Stimulus
261
+ * (`MicrotaskCoalescer`), so this gates the Value callbacks Stimulus
256
262
  * delivers ahead of `connect()`.
257
263
  */
258
264
  #connected = false;
@@ -510,9 +516,7 @@ var CarouselController = class extends Controller {
510
516
  }
511
517
  /** Position of the picker that is or contains `node`, or `-1` when none does. */
512
518
  #pickerIndexFor(node) {
513
- return this.pickerTargets.findIndex(
514
- (picker) => node instanceof Node && (picker === node || picker.contains(node))
515
- );
519
+ return ownerIndex(this.pickerTargets, node);
516
520
  }
517
521
  /** Whether a focus transition lands on another control of this same carousel. */
518
522
  #focusStaysInside(event) {
@@ -731,9 +735,7 @@ function hasModifier(event) {
731
735
  return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
732
736
  }
733
737
  function hits(elements, node) {
734
- return elements.some(
735
- (element) => node instanceof Node && (element === node || element.contains(node))
736
- );
738
+ return ownerIndex(elements, node) !== -1;
737
739
  }
738
740
  function setAttributeIfChanged(element, name, value) {
739
741
  if (element.getAttribute(name) !== value) element.setAttribute(name, value);
@@ -512,7 +512,7 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
512
512
  * re-deriving from the declarative `open` Value (which would slam a user-opened
513
513
  * palette shut). The `open` Value only seeds the initial state of a genuinely
514
514
  * fresh render. We normalize to a clean closed baseline first so {@link open}
515
- * runs its full setup — the {@link FocusTrap} is a fresh instance after a
515
+ * runs its full setup — the `FocusTrap` is a fresh instance after a
516
516
  * reconnect and must be re-activated.
517
517
  */
518
518
  connect() {
@@ -617,7 +617,7 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
617
617
  }
618
618
  /**
619
619
  * Combobox navigation keys (arrows / Home / End / Enter), bound to the input.
620
- * `Tab` (focus trap) and `Escape` (close) are owned by the {@link FocusTrap} at
620
+ * `Tab` (focus trap) and `Escape` (close) are owned by the shared `FocusTrap` at
621
621
  * the document level, so they work no matter which element inside the dialog has
622
622
  * focus — not only the input.
623
623
  */
@@ -2,6 +2,12 @@ import { Controller } from '@hotwired/stimulus';
2
2
 
3
3
  // src/controllers/conditional_fields_controller.ts
4
4
 
5
+ // src/utils/event_owner.ts
6
+ function ownerIndex(candidates, node) {
7
+ if (!(node instanceof Node)) return -1;
8
+ return candidates.findIndex((candidate) => candidate.contains(node));
9
+ }
10
+
5
11
  // src/utils/focus_candidate.ts
6
12
  function inheritsFieldsetDisabled(control) {
7
13
  let fieldset = control.closest("fieldset[disabled]");
@@ -393,7 +399,7 @@ var ConditionalFieldsController = class extends Controller {
393
399
  }
394
400
  /** Whether an authored mutation target belongs to a trigger, region, or managed control. */
395
401
  #elementAffectsState(target, triggers, regions) {
396
- return triggers.some((trigger) => trigger === target || trigger.contains(target)) || regions.some((region) => region === target || region.contains(target));
402
+ return ownerIndex(triggers, target) !== -1 || ownerIndex(regions, target) !== -1;
397
403
  }
398
404
  /** Whether a form owns at least one current trigger, including `form=` associations. */
399
405
  #hasTriggerOwnedBy(form) {
@@ -58,6 +58,36 @@ function halfWidthChar(char) {
58
58
  return char === "\u3000" ? " " : char;
59
59
  }
60
60
 
61
+ // src/utils/intl_format.ts
62
+ var formatters = /* @__PURE__ */ new WeakMap();
63
+ function intlFormatter(factory, locale, options, fallbackLocale) {
64
+ const cache = cacheFor(factory);
65
+ const key = cacheKey(locale, options, fallbackLocale);
66
+ if (cache.has(key)) return cache.get(key);
67
+ const formatter = construct(factory, locale, options, fallbackLocale);
68
+ cache.set(key, formatter);
69
+ return formatter;
70
+ }
71
+ function cacheFor(factory) {
72
+ const existing = formatters.get(factory);
73
+ if (existing) return existing;
74
+ const created = /* @__PURE__ */ new Map();
75
+ formatters.set(factory, created);
76
+ return created;
77
+ }
78
+ function cacheKey(locale, options, fallbackLocale) {
79
+ return JSON.stringify([locale, options, fallbackLocale]);
80
+ }
81
+ function construct(factory, locale, options, fallbackLocale) {
82
+ try {
83
+ return new factory(locale, options);
84
+ } catch (error) {
85
+ if (!(error instanceof RangeError)) throw error;
86
+ if (fallbackLocale === void 0) return null;
87
+ return new factory(fallbackLocale, options);
88
+ }
89
+ }
90
+
61
91
  // src/controllers/currency_input_controller.ts
62
92
  var CurrencyInputController = class extends Controller {
63
93
  static targets = ["display", "field", "srValue"];
@@ -309,37 +339,52 @@ var CurrencyInputController = class extends Controller {
309
339
  * formatters from the validated set.
310
340
  */
311
341
  #revalidate() {
312
- this.#locale = "en-US";
313
- try {
314
- new Intl.NumberFormat(this.localeValue);
315
- this.#locale = this.localeValue;
316
- } catch {
317
- }
342
+ const declaredLocale = intlFormatter(Intl.NumberFormat, this.localeValue, {});
343
+ this.#locale = declaredLocale === null ? "en-US" : this.localeValue;
318
344
  const precision = this.precisionValue;
319
345
  this.#precision = Number.isInteger(precision) && precision >= 0 && precision <= 100 ? precision : 2;
320
346
  this.#currency = "";
321
347
  if (this.currencyValue !== "") {
322
- try {
323
- new Intl.NumberFormat(this.#locale, { style: "currency", currency: this.currencyValue });
324
- this.#currency = this.currencyValue;
325
- } catch {
326
- }
348
+ const declaredCurrency = intlFormatter(Intl.NumberFormat, this.#locale, {
349
+ style: "currency",
350
+ currency: this.currencyValue
351
+ });
352
+ if (declaredCurrency !== null) this.#currency = this.currencyValue;
327
353
  }
328
- this.#grouping = new Intl.NumberFormat(this.#locale, {
329
- useGrouping: true,
330
- maximumFractionDigits: 0
331
- });
332
- this.#fixed = new Intl.NumberFormat(this.#locale, {
333
- useGrouping: true,
334
- minimumFractionDigits: this.#precision,
335
- maximumFractionDigits: this.#precision
336
- });
337
- this.#accessible = this.#currency ? new Intl.NumberFormat(this.#locale, { style: "currency", currency: this.#currency }) : this.#fixed;
338
- const parts = new Intl.NumberFormat(this.#locale).formatToParts(11111.1);
354
+ this.#grouping = intlFormatter(
355
+ Intl.NumberFormat,
356
+ this.#locale,
357
+ { useGrouping: true, maximumFractionDigits: 0 },
358
+ this.#locale
359
+ );
360
+ this.#fixed = intlFormatter(
361
+ Intl.NumberFormat,
362
+ this.#locale,
363
+ {
364
+ useGrouping: true,
365
+ minimumFractionDigits: this.#precision,
366
+ maximumFractionDigits: this.#precision
367
+ },
368
+ this.#locale
369
+ );
370
+ this.#accessible = this.#currency ? intlFormatter(
371
+ Intl.NumberFormat,
372
+ this.#locale,
373
+ { style: "currency", currency: this.#currency },
374
+ this.#locale
375
+ ) : this.#fixed;
376
+ const parts = intlFormatter(Intl.NumberFormat, this.#locale, {}, this.#locale).formatToParts(
377
+ 11111.1
378
+ );
339
379
  this.#group = parts.find((p) => p.type === "group")?.value ?? ",";
340
380
  this.#decimal = parts.find((p) => p.type === "decimal")?.value ?? ".";
341
381
  this.#digits.clear();
342
- const digitFormatter = new Intl.NumberFormat(this.#locale, { useGrouping: false });
382
+ const digitFormatter = intlFormatter(
383
+ Intl.NumberFormat,
384
+ this.#locale,
385
+ { useGrouping: false },
386
+ this.#locale
387
+ );
343
388
  for (let i = 0; i <= 9; i++) {
344
389
  const digit = digitFormatter.format(i);
345
390
  if (digit !== String(i)) this.#digits.set(digit, String(i));
@@ -47,6 +47,35 @@ var BeforeCacheReset = class _BeforeCacheReset {
47
47
  }
48
48
  };
49
49
 
50
+ // src/utils/intl_format.ts
51
+ var formatters = /* @__PURE__ */ new WeakMap();
52
+ function intlFormatter(factory, locale, options, fallbackLocale) {
53
+ const cache = cacheFor(factory);
54
+ const key = cacheKey(locale, options, fallbackLocale);
55
+ if (cache.has(key)) return cache.get(key);
56
+ const formatter = construct(factory, locale, options, fallbackLocale);
57
+ cache.set(key, formatter);
58
+ return formatter;
59
+ }
60
+ function cacheFor(factory) {
61
+ const existing = formatters.get(factory);
62
+ if (existing) return existing;
63
+ const created = /* @__PURE__ */ new Map();
64
+ formatters.set(factory, created);
65
+ return created;
66
+ }
67
+ function cacheKey(locale, options, fallbackLocale) {
68
+ return JSON.stringify([locale, options, fallbackLocale]);
69
+ }
70
+ function construct(factory, locale, options, fallbackLocale) {
71
+ try {
72
+ return new factory(locale, options);
73
+ } catch (error) {
74
+ if (!(error instanceof RangeError)) throw error;
75
+ return new factory(fallbackLocale, options);
76
+ }
77
+ }
78
+
50
79
  // src/utils/dates.ts
51
80
  function toISODateString(date) {
52
81
  const year = date.getFullYear();
@@ -80,6 +109,10 @@ function parseISOMonthString(monthStr) {
80
109
  function toISOMonthString(date) {
81
110
  return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
82
111
  }
112
+ var MONTH_LABEL_OPTIONS = { month: "long", year: "numeric" };
113
+ function monthLabelFormatter(locale) {
114
+ return intlFormatter(Intl.DateTimeFormat, locale, MONTH_LABEL_OPTIONS, "en");
115
+ }
83
116
 
84
117
  // src/utils/microtask_coalescer.ts
85
118
  var MicrotaskCoalescer = class {
@@ -433,7 +466,7 @@ var DateRangePickerController = class extends Controller {
433
466
  if (this.#previewDate && !this.#isSelectable(this.#previewDate)) this.#previewDate = "";
434
467
  if (this.hasMonthLabelTarget) {
435
468
  const lang = document.documentElement.lang || "en";
436
- const formatter = monthFormatter(lang);
469
+ const formatter = monthLabelFormatter(lang);
437
470
  this.monthLabelTarget.textContent = formatter.format(new Date(year, month - 1, 1));
438
471
  }
439
472
  const [rangeStart, rangeEnd] = this.#visualRange();
@@ -588,15 +621,6 @@ function normalizeISO(value) {
588
621
  function orderRange(start, end) {
589
622
  return start && end && end < start ? [end, start] : [start, end];
590
623
  }
591
- function monthFormatter(locale) {
592
- const options = { month: "long", year: "numeric" };
593
- try {
594
- return new Intl.DateTimeFormat(locale, options);
595
- } catch (error) {
596
- if (!(error instanceof RangeError)) throw error;
597
- return new Intl.DateTimeFormat("en", options);
598
- }
599
- }
600
624
  function computePreset(name) {
601
625
  const today = /* @__PURE__ */ new Date();
602
626
  const todayStr = toISODateString(today);
@@ -48,6 +48,26 @@ var BeforeCacheReset = class _BeforeCacheReset {
48
48
  }
49
49
  };
50
50
 
51
+ // src/utils/declared_value.ts
52
+ function parseDeclared(raw, parse, fallback) {
53
+ try {
54
+ return parse(raw);
55
+ } catch {
56
+ return fallback;
57
+ }
58
+ }
59
+ function validSelector(element, raw, fallback) {
60
+ if (raw.length === 0) return fallback;
61
+ return parseDeclared(
62
+ raw,
63
+ (selector) => {
64
+ element.matches(selector);
65
+ return selector;
66
+ },
67
+ fallback
68
+ );
69
+ }
70
+
51
71
  // src/utils/safe_timeout.ts
52
72
  var TimerRegistry = class {
53
73
  /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
@@ -156,16 +176,7 @@ var DirectUploadController = class extends Controller {
156
176
  }
157
177
  /** Validates `scope` once so the per-event path never parses or throws. */
158
178
  scopeValueChanged() {
159
- const selector = this.scopeValue;
160
- if (selector.length > 0) {
161
- try {
162
- this.element.matches(selector);
163
- this.#scopeSelector = selector;
164
- return;
165
- } catch {
166
- }
167
- }
168
- this.#scopeSelector = "";
179
+ this.#scopeSelector = validSelector(this.element, this.scopeValue, "");
169
180
  }
170
181
  /** Updates a row's progress and the aggregate, emitting `progress`. */
171
182
  #updateProgress(id, percent, name) {
@@ -586,7 +586,7 @@ var DrawerController = class extends Controller {
586
586
  * rather than being re-derived from the declarative `open` Value (which would
587
587
  * close a user-opened drawer). The `open` Value only seeds a genuinely fresh
588
588
  * render. We normalize to a clean closed baseline first so {@link open} runs its
589
- * full reveal + trap activation — the {@link FocusTrap} is inactive after a
589
+ * full reveal + trap activation — the `FocusTrap` is inactive after a
590
590
  * disconnect and must be re-activated.
591
591
  */
592
592
  connect() {
@@ -719,7 +719,7 @@ var DrawerController = class extends Controller {
719
719
  * Runs once the close transition has finished: applies `hidden` to the panel
720
720
  * and overlay, then reverts the modal side effects (scroll lock, background
721
721
  * `inert`, keydown listener) and restores focus to the opener. Deferring the
722
- * {@link FocusTrap} teardown to here — rather than at {@link close} time — keeps
722
+ * `FocusTrap` teardown to here — rather than at {@link close} time — keeps
723
723
  * the background unreachable and focus trapped for the whole exit animation.
724
724
  */
725
725
  #applyHidden(panel) {
@@ -100,6 +100,12 @@ var BeforeCacheReset = class _BeforeCacheReset {
100
100
  }
101
101
  };
102
102
 
103
+ // src/utils/event_owner.ts
104
+ function ownerIndex(candidates, node) {
105
+ if (!(node instanceof Node)) return -1;
106
+ return candidates.findIndex((candidate) => candidate.contains(node));
107
+ }
108
+
103
109
  // src/utils/microtask_coalescer.ts
104
110
  var MicrotaskCoalescer = class {
105
111
  #run;
@@ -357,7 +363,7 @@ var FormFieldController = class _FormFieldController extends Controller {
357
363
  return this.errorTargets.includes(target);
358
364
  return record.attributeName === "id" && [...this.descriptionTargets, ...this.errorTargets].includes(target);
359
365
  }
360
- return this.errorTargets.some((error) => error === target || error.contains(target));
366
+ return ownerIndex(this.errorTargets, target) !== -1;
361
367
  }
362
368
  /** Returns borrowed control ARIA before Turbo snapshots the page. */
363
369
  #rewindForCache() {
@@ -49,6 +49,30 @@ var CompositionTracker = class {
49
49
  };
50
50
  };
51
51
 
52
+ // src/utils/declared_value.ts
53
+ function parseDeclared(raw, parse, fallback) {
54
+ try {
55
+ return parse(raw);
56
+ } catch {
57
+ return fallback;
58
+ }
59
+ }
60
+ function compileRegExp(source, anchor = "none") {
61
+ return parseDeclared(
62
+ source,
63
+ (text) => {
64
+ const bare = new RegExp(text);
65
+ return anchor === "exact" ? new RegExp(`^(?:${text})$`) : bare;
66
+ },
67
+ null
68
+ );
69
+ }
70
+ function parseJsonObject(raw) {
71
+ const parsed = parseDeclared(raw, (text) => JSON.parse(text), null);
72
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
73
+ return parsed;
74
+ }
75
+
52
76
  // src/utils/focus_candidate.ts
53
77
  function inheritsFieldsetDisabled(control) {
54
78
  let fieldset = control.closest("fieldset[disabled]");
@@ -118,22 +142,19 @@ function applyMask(value, pattern, tokens) {
118
142
  }
119
143
  function compileTokens(declaration) {
120
144
  const map = /* @__PURE__ */ new Map();
121
- for (const [key, source] of Object.entries({ ...DEFAULT_TOKENS, ...parseTokens(declaration) })) {
122
- try {
123
- map.set(key, new RegExp(`^(?:${source})$`));
124
- } catch {
125
- }
145
+ for (const [key, source] of Object.entries(DEFAULT_TOKENS)) {
146
+ const compiled = compileRegExp(source, "exact");
147
+ if (compiled) map.set(key, compiled);
148
+ }
149
+ for (const [key, source] of Object.entries(parseTokens(declaration))) {
150
+ const compiled = compileRegExp(source, "exact");
151
+ if (compiled) map.set(key, compiled);
126
152
  }
127
153
  return map;
128
154
  }
129
155
  function parseTokens(declaration) {
130
- let parsed;
131
- try {
132
- parsed = JSON.parse(declaration);
133
- } catch {
134
- return {};
135
- }
136
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return {};
156
+ const parsed = parseJsonObject(declaration);
157
+ if (parsed === null) return {};
137
158
  const tokens = {};
138
159
  for (const [key, source] of Object.entries(parsed)) {
139
160
  if (typeof source === "string") tokens[key] = source;
@@ -2,6 +2,15 @@ import { Controller } from '@hotwired/stimulus';
2
2
 
3
3
  // src/controllers/intersection_controller.ts
4
4
 
5
+ // src/utils/declared_value.ts
6
+ function parseDeclared(raw, parse, fallback) {
7
+ try {
8
+ return parse(raw);
9
+ } catch {
10
+ return fallback;
11
+ }
12
+ }
13
+
5
14
  // src/utils/intersection_watcher.ts
6
15
  function isBeforeRootStart(entry) {
7
16
  const rect = entry.boundingClientRect;
@@ -11,11 +20,7 @@ function isBeforeRootStart(entry) {
11
20
  }
12
21
  function queryRoot(selector) {
13
22
  if (!selector) return null;
14
- try {
15
- return document.querySelector(selector);
16
- } catch {
17
- }
18
- return null;
23
+ return parseDeclared(selector, (raw) => document.querySelector(raw), null);
19
24
  }
20
25
  var IntersectionWatcher = class {
21
26
  #onEntries;
@@ -2,14 +2,19 @@ import { Controller } from '@hotwired/stimulus';
2
2
 
3
3
  // src/controllers/lazy_frame_controller.ts
4
4
 
5
- // src/utils/intersection_watcher.ts
6
- function queryRoot(selector) {
7
- if (!selector) return null;
5
+ // src/utils/declared_value.ts
6
+ function parseDeclared(raw, parse, fallback) {
8
7
  try {
9
- return document.querySelector(selector);
8
+ return parse(raw);
10
9
  } catch {
10
+ return fallback;
11
11
  }
12
- return null;
12
+ }
13
+
14
+ // src/utils/intersection_watcher.ts
15
+ function queryRoot(selector) {
16
+ if (!selector) return null;
17
+ return parseDeclared(selector, (raw) => document.querySelector(raw), null);
13
18
  }
14
19
  var IntersectionWatcher = class {
15
20
  #onEntries;
@@ -2,6 +2,35 @@ import { Controller } from '@hotwired/stimulus';
2
2
 
3
3
  // src/controllers/local_time_controller.ts
4
4
 
5
+ // src/utils/intl_format.ts
6
+ var formatters = /* @__PURE__ */ new WeakMap();
7
+ function intlFormatter(factory, locale, options, fallbackLocale) {
8
+ const cache = cacheFor(factory);
9
+ const key = cacheKey(locale, options, fallbackLocale);
10
+ if (cache.has(key)) return cache.get(key);
11
+ const formatter = construct(factory, locale, options);
12
+ cache.set(key, formatter);
13
+ return formatter;
14
+ }
15
+ function cacheFor(factory) {
16
+ const existing = formatters.get(factory);
17
+ if (existing) return existing;
18
+ const created = /* @__PURE__ */ new Map();
19
+ formatters.set(factory, created);
20
+ return created;
21
+ }
22
+ function cacheKey(locale, options, fallbackLocale) {
23
+ return JSON.stringify([locale, options, fallbackLocale]);
24
+ }
25
+ function construct(factory, locale, options, fallbackLocale) {
26
+ try {
27
+ return new factory(locale, options);
28
+ } catch (error) {
29
+ if (!(error instanceof RangeError)) throw error;
30
+ return null;
31
+ }
32
+ }
33
+
5
34
  // src/utils/microtask_coalescer.ts
6
35
  var MicrotaskCoalescer = class {
7
36
  #run;
@@ -150,8 +179,9 @@ var LocalTimeController = class extends Controller {
150
179
  /**
151
180
  * Formats `date` with `Intl.DateTimeFormat`, including each style only when it
152
181
  * is a valid keyword (so a consumer can show date-only or time-only by clearing
153
- * the other). Returns `null` when neither style is usable or `Intl` throws, so
154
- * the caller can leave the authored text untouched.
182
+ * the other). Returns `null` when neither style is usable or the declaration
183
+ * names a locale or time zone `Intl` rejects, so the caller can leave the
184
+ * authored text untouched.
155
185
  */
156
186
  #applyFormat(date, dateStyle, timeStyle) {
157
187
  const options = {
@@ -160,11 +190,8 @@ var LocalTimeController = class extends Controller {
160
190
  };
161
191
  if (options.dateStyle === void 0 && options.timeStyle === void 0) return null;
162
192
  if (this.timeZoneValue.length > 0) options.timeZone = this.timeZoneValue;
163
- try {
164
- return new Intl.DateTimeFormat(this.#locale, options).format(date);
165
- } catch {
166
- return null;
167
- }
193
+ const formatter = intlFormatter(Intl.DateTimeFormat, this.#locale, options);
194
+ return formatter === null ? null : formatter.format(date);
168
195
  }
169
196
  /** Locale precedence: the value, then the nearest `lang` up the ancestor chain. */
170
197
  get #locale() {
@@ -153,7 +153,7 @@ var MasonryController = class extends Controller {
153
153
  *
154
154
  * The removal is queued rather than immediate because teardown reports every
155
155
  * target as disconnected: doing it here would strip the whole grid just before
156
- * a Turbo snapshot is taken. {@link MicrotaskCoalescer.cancel} drops the queue
156
+ * a Turbo snapshot is taken. The coalescer's `cancel` drops the queue
157
157
  * with the pass, so only a genuine target change reaches it.
158
158
  */
159
159
  itemTargetDisconnected(item) {
@@ -98,6 +98,15 @@ var EscapeLayer = class _EscapeLayer {
98
98
  }
99
99
  };
100
100
 
101
+ // src/utils/event_owner.ts
102
+ function ownerIndex(candidates, node) {
103
+ if (!(node instanceof Node)) return -1;
104
+ return candidates.findIndex((candidate) => candidate.contains(node));
105
+ }
106
+ function ownerOf(candidates, node) {
107
+ return candidates[ownerIndex(candidates, node)] ?? null;
108
+ }
109
+
101
110
  // src/utils/safe_timeout.ts
102
111
  var TimerRegistry = class {
103
112
  /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
@@ -338,8 +347,7 @@ var MenuController = class extends Controller {
338
347
  };
339
348
  /** The item target that is, or contains, `node`; `null` when it is neither. */
340
349
  #itemFrom(node) {
341
- if (!(node instanceof Node)) return null;
342
- return this.itemTargets.find((item) => item === node || item.contains(node)) ?? null;
350
+ return ownerOf(this.itemTargets, node);
343
351
  }
344
352
  /**
345
353
  * First look at every click inside the controller, and the only point at which
@@ -103,6 +103,15 @@ var EscapeLayer = class _EscapeLayer {
103
103
  }
104
104
  };
105
105
 
106
+ // src/utils/event_owner.ts
107
+ function ownerIndex(candidates, node) {
108
+ if (!(node instanceof Node)) return -1;
109
+ return candidates.findIndex((candidate) => candidate.contains(node));
110
+ }
111
+ function ownerOf(candidates, node) {
112
+ return candidates[ownerIndex(candidates, node)] ?? null;
113
+ }
114
+
106
115
  // src/utils/roving_tabindex.ts
107
116
  var RovingTabindex = class {
108
117
  /** Returns the current ordered item elements; called on every operation. */
@@ -577,8 +586,7 @@ var MenubarController = class extends Controller {
577
586
  * menu without moving focus.
578
587
  */
579
588
  #dismissOpenMenu() {
580
- const active = document.activeElement;
581
- const menu = this.menuTargets.find((candidate) => candidate.contains(active));
589
+ const menu = ownerOf(this.menuTargets, document.activeElement);
582
590
  if (menu) {
583
591
  const top = this.#topFor(menu);
584
592
  this.#closeMenu(top);