stimeo-ui 0.13.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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +39 -5
  3. data/dist/controllers/calendar_controller.js +31 -7
  4. data/dist/controllers/carousel_controller.js +8 -6
  5. data/dist/controllers/conditional_fields_controller.js +7 -1
  6. data/dist/controllers/currency_input_controller.js +68 -23
  7. data/dist/controllers/date_range_picker_controller.js +31 -7
  8. data/dist/controllers/direct_upload_controller.js +21 -10
  9. data/dist/controllers/form_field_controller.js +7 -1
  10. data/dist/controllers/input_mask_controller.js +33 -12
  11. data/dist/controllers/intersection_controller.js +10 -5
  12. data/dist/controllers/lazy_frame_controller.js +10 -5
  13. data/dist/controllers/local_time_controller.js +34 -7
  14. data/dist/controllers/masonry_controller.js +1 -1
  15. data/dist/controllers/menu_controller.js +10 -2
  16. data/dist/controllers/menubar_controller.js +10 -2
  17. data/dist/controllers/navigation_menu_controller.js +8 -2
  18. data/dist/controllers/otp_controller.js +23 -10
  19. data/dist/controllers/overflow_menu_controller.js +10 -1
  20. data/dist/controllers/pointer_drag_controller.js +10 -3
  21. data/dist/controllers/portal_controller.js +21 -10
  22. data/dist/controllers/radio_group_controller.js +11 -3
  23. data/dist/controllers/relative_time_controller.js +31 -5
  24. data/dist/controllers/reset_before_cache_controller.js +23 -10
  25. data/dist/controllers/resizable_controller.js +7 -2
  26. data/dist/controllers/roving_controller.js +7 -3
  27. data/dist/controllers/scroll_visibility_controller.js +22 -13
  28. data/dist/controllers/scrollspy_controller.js +11 -6
  29. data/dist/controllers/smart_sticky_header_controller.js +23 -12
  30. data/dist/controllers/sticky_observer_controller.js +10 -5
  31. data/dist/controllers/theme_controller.js +23 -13
  32. data/dist/controllers/toggle_group_controller.js +11 -3
  33. data/dist/controllers/toolbar_controller.js +7 -3
  34. data/dist/index.js +165 -171
  35. data/lib/stimeo/ui/version.rb +1 -1
  36. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9270fc3c010e18a652ab837ecec7b712bbdd78e9859b0690409fff8b5623f640
4
- data.tar.gz: 8920d26834c8c2444f35c0f6d04cc2c21835163414a62f7339e95608130dc29a
3
+ metadata.gz: 8220bc5fa861ec2aa75c7abeb2fcb6758a4ba6b56606b77d5bc6d948c9550a09
4
+ data.tar.gz: 95b3643b1a9bbdfdf7d556b5952c1431e6adc1ae557e5d0cf5f40b102276593a
5
5
  SHA512:
6
- metadata.gz: 8da3b269a404ca927d73e19a3be176b8fe7ce094a3d2742055bdb0675c2a1862fda1b20667aed527a2a7848dedaf568617b01824d7bd90819171a7d3bbd49e2e
7
- data.tar.gz: 0141d8fec6cc63786a1e0dc348350670236c54129969dfebbc93538c23dea48945bb2080d5e692e7219d092304cf07725a93a7539e3185bbf56198e544f0fd46
6
+ metadata.gz: 837c894e5a64b823b2afc662dc4cb6e74daeedb04bee3d348bbd56127ee544cb31247f0534ffca2c60a1f676a2421ec2e3dbe0bef6ecbaeed85eac17f41ac1e1
7
+ data.tar.gz: 22e7cfc49254e48c27915cb9b83f632ea1348db396046a5a41be5513c8a8e4f01f00feee422c582c2423569d1cdf73228201ffc848554bb54cd1d927e18d9550
data/CHANGELOG.md CHANGED
@@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
  While the version is `0.x`, the public API (the `stimeo--*` data attributes) may
8
8
  change between releases.
9
9
 
10
+ ## [0.14.0] - 2026-09-19
11
+
12
+ Minor release with no new components. input-mask changes what a broken `tokens`
13
+ source falls back to, and currency-input, local-time, and relative-time narrow
14
+ which failures they absorb, so read Changed before upgrading. The Inspector
15
+ manifest stays on schema v12.
16
+
17
+ ### Added
18
+
19
+ - Inspector: `missing-action-event` (error) — a `data-action` that omits the
20
+ event on an element with no default one binds nothing, while the markup still
21
+ reads as wired. The elements that have a default are `<a>`, `<button>`,
22
+ `<details>`, `<form>`, `<input>`, `<select>`, and `<textarea>`.
23
+ - Inspector: `valueSyntaxConstraints` reports a `stimeo--otp` `pattern` that is
24
+ not a regular expression, and a `stimeo--input-mask` `tokens` that is not a
25
+ JSON object of them, before the page runs.
26
+
27
+ ### Changed
28
+
29
+ - input-mask: a `tokens` source that does not compile leaves that key on its
30
+ default (`9`, `a`, `*`) instead of dropping the key, and a source that
31
+ compiles only against the anchors the controller adds — `0)|(1` — is refused.
32
+ - currency-input, local-time, and relative-time absorb only what a declaration
33
+ can get wrong: a locale, time zone, currency code, or digit count `Intl`
34
+ rejects. Any other failure surfaces.
35
+ - Inspector: a `data-action` that omits the event resolves to the one Stimulus
36
+ binds for that element, so a shorthand on a `<form>` reports
37
+ `missing-action-completion` where it passed in silence.
38
+ - The markup examples in the published types name the event
39
+ (`click->stimeo--dialog#open`).
40
+
10
41
  ## [0.13.0] - 2026-09-13
11
42
 
12
43
  Minor release with no new components. The three server-bound components are
@@ -21,11 +52,13 @@ so read Changed before upgrading. The Inspector manifest stays on schema v12.
21
52
  A delta carrying your own `id` is applied unless an optimistic add of the same
22
53
  size is still outstanding. Runtime changes to `channel` / `params`
23
54
  re-subscribe.
24
- - presence: a leave notice is sent by the last element claiming that `id` rather
25
- than by every element, a `channel` key in `params` no longer overrides the
26
- declared `channel`, `heartbeat` / `timeout` fall back to their defaults unless
27
- finite, positive and at most 2147483647 — typing-indicator's `timeout` takes
28
- the same bound and `template` clones only its root element.
55
+ - presence: on `disconnect()` a leave notice is sent by the last element claiming
56
+ that `id` rather than by every element; `pagehide` still sends from each one,
57
+ because the whole page is going away. A `channel` key in `params` no longer
58
+ overrides the declared `channel`, `heartbeat` / `timeout` fall back to their
59
+ defaults unless finite, positive and within the browser timer ceiling
60
+ (2147483647 ms) — typing-indicator's `timeout` takes the same bound — and
61
+ `template` clones only its root element.
29
62
  - optimistic: `turbo:submit-end` is matched to the submission it names instead of
30
63
  to any ending that reaches the element, so a submission whose ending never
31
64
  arrives no longer strands the element. The `data-optimistic-toggled` marker
@@ -1035,6 +1068,7 @@ Initial public alpha: 101 behavior-only, accessible Stimulus controllers driven
1035
1068
  by `data-*` attributes, shipping no CSS. Published to npm (with provenance) and
1036
1069
  RubyGems.
1037
1070
 
1071
+ [0.14.0]: https://github.com/taiyaky/stimeo-ui/releases/tag/v0.14.0
1038
1072
  [0.13.0]: https://github.com/taiyaky/stimeo-ui/releases/tag/v0.13.0
1039
1073
  [0.12.0]: https://github.com/taiyaky/stimeo-ui/releases/tag/v0.12.0
1040
1074
  [0.11.0]: https://github.com/taiyaky/stimeo-ui/releases/tag/v0.11.0
@@ -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,14 +80,9 @@ 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" };
54
84
  function monthLabelFormatter(locale) {
55
- const options = { month: "long", year: "numeric" };
56
- try {
57
- return new Intl.DateTimeFormat(locale, options);
58
- } catch (error) {
59
- if (!(error instanceof RangeError)) throw error;
60
- return new Intl.DateTimeFormat("en", options);
61
- }
85
+ return intlFormatter(Intl.DateTimeFormat, locale, MONTH_LABEL_OPTIONS, "en");
62
86
  }
63
87
 
64
88
  // src/utils/safe_timeout.ts
@@ -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;
@@ -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);
@@ -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,14 +109,9 @@ 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" };
83
113
  function monthLabelFormatter(locale) {
84
- const options = { month: "long", year: "numeric" };
85
- try {
86
- return new Intl.DateTimeFormat(locale, options);
87
- } catch (error) {
88
- if (!(error instanceof RangeError)) throw error;
89
- return new Intl.DateTimeFormat("en", options);
90
- }
114
+ return intlFormatter(Intl.DateTimeFormat, locale, MONTH_LABEL_OPTIONS, "en");
91
115
  }
92
116
 
93
117
  // src/utils/microtask_coalescer.ts
@@ -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) {
@@ -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);