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
data/dist/index.js CHANGED
@@ -1853,6 +1853,36 @@ var BulkSelectController = class extends Controller {
1853
1853
  }
1854
1854
  };
1855
1855
 
1856
+ // src/utils/intl_format.ts
1857
+ var formatters = /* @__PURE__ */ new WeakMap();
1858
+ function intlFormatter(factory, locale, options, fallbackLocale) {
1859
+ const cache = cacheFor(factory);
1860
+ const key = cacheKey(locale, options, fallbackLocale);
1861
+ if (cache.has(key)) return cache.get(key);
1862
+ const formatter = construct(factory, locale, options, fallbackLocale);
1863
+ cache.set(key, formatter);
1864
+ return formatter;
1865
+ }
1866
+ function cacheFor(factory) {
1867
+ const existing = formatters.get(factory);
1868
+ if (existing) return existing;
1869
+ const created = /* @__PURE__ */ new Map();
1870
+ formatters.set(factory, created);
1871
+ return created;
1872
+ }
1873
+ function cacheKey(locale, options, fallbackLocale) {
1874
+ return JSON.stringify([locale, options, fallbackLocale]);
1875
+ }
1876
+ function construct(factory, locale, options, fallbackLocale) {
1877
+ try {
1878
+ return new factory(locale, options);
1879
+ } catch (error) {
1880
+ if (!(error instanceof RangeError)) throw error;
1881
+ if (fallbackLocale === void 0) return null;
1882
+ return new factory(fallbackLocale, options);
1883
+ }
1884
+ }
1885
+
1856
1886
  // src/utils/dates.ts
1857
1887
  function toISODateString(date) {
1858
1888
  const year = date.getFullYear();
@@ -1886,14 +1916,9 @@ function parseISOMonthString(monthStr) {
1886
1916
  function toISOMonthString(date) {
1887
1917
  return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
1888
1918
  }
1919
+ var MONTH_LABEL_OPTIONS = { month: "long", year: "numeric" };
1889
1920
  function monthLabelFormatter(locale) {
1890
- const options = { month: "long", year: "numeric" };
1891
- try {
1892
- return new Intl.DateTimeFormat(locale, options);
1893
- } catch (error) {
1894
- if (!(error instanceof RangeError)) throw error;
1895
- return new Intl.DateTimeFormat("en", options);
1896
- }
1921
+ return intlFormatter(Intl.DateTimeFormat, locale, MONTH_LABEL_OPTIONS, "en");
1897
1922
  }
1898
1923
 
1899
1924
  // src/controllers/calendar_controller.ts
@@ -2246,6 +2271,15 @@ var CalendarController = class extends Controller {
2246
2271
  }
2247
2272
  };
2248
2273
 
2274
+ // src/utils/event_owner.ts
2275
+ function ownerIndex(candidates, node) {
2276
+ if (!(node instanceof Node)) return -1;
2277
+ return candidates.findIndex((candidate) => candidate.contains(node));
2278
+ }
2279
+ function ownerOf(candidates, node) {
2280
+ return candidates[ownerIndex(candidates, node)] ?? null;
2281
+ }
2282
+
2249
2283
  // src/utils/reduced_motion.ts
2250
2284
  function prefersReducedMotion() {
2251
2285
  return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
@@ -2589,9 +2623,7 @@ var CarouselController = class extends Controller {
2589
2623
  }
2590
2624
  /** Position of the picker that is or contains `node`, or `-1` when none does. */
2591
2625
  #pickerIndexFor(node) {
2592
- return this.pickerTargets.findIndex(
2593
- (picker) => node instanceof Node && (picker === node || picker.contains(node))
2594
- );
2626
+ return ownerIndex(this.pickerTargets, node);
2595
2627
  }
2596
2628
  /** Whether a focus transition lands on another control of this same carousel. */
2597
2629
  #focusStaysInside(event) {
@@ -2810,9 +2842,7 @@ function hasModifier(event) {
2810
2842
  return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
2811
2843
  }
2812
2844
  function hits(elements, node) {
2813
- return elements.some(
2814
- (element) => node instanceof Node && (element === node || element.contains(node))
2815
- );
2845
+ return ownerIndex(elements, node) !== -1;
2816
2846
  }
2817
2847
  function setAttributeIfChanged(element, name, value) {
2818
2848
  if (element.getAttribute(name) !== value) element.setAttribute(name, value);
@@ -5068,7 +5098,7 @@ var ConditionalFieldsController = class extends Controller {
5068
5098
  }
5069
5099
  /** Whether an authored mutation target belongs to a trigger, region, or managed control. */
5070
5100
  #elementAffectsState(target, triggers, regions) {
5071
- return triggers.some((trigger) => trigger === target || trigger.contains(target)) || regions.some((region) => region === target || region.contains(target));
5101
+ return ownerIndex(triggers, target) !== -1 || ownerIndex(regions, target) !== -1;
5072
5102
  }
5073
5103
  /** Whether a form owns at least one current trigger, including `form=` associations. */
5074
5104
  #hasTriggerOwnedBy(form) {
@@ -5928,37 +5958,52 @@ var CurrencyInputController = class extends Controller {
5928
5958
  * formatters from the validated set.
5929
5959
  */
5930
5960
  #revalidate() {
5931
- this.#locale = "en-US";
5932
- try {
5933
- new Intl.NumberFormat(this.localeValue);
5934
- this.#locale = this.localeValue;
5935
- } catch {
5936
- }
5961
+ const declaredLocale = intlFormatter(Intl.NumberFormat, this.localeValue, {});
5962
+ this.#locale = declaredLocale === null ? "en-US" : this.localeValue;
5937
5963
  const precision = this.precisionValue;
5938
5964
  this.#precision = Number.isInteger(precision) && precision >= 0 && precision <= 100 ? precision : 2;
5939
5965
  this.#currency = "";
5940
5966
  if (this.currencyValue !== "") {
5941
- try {
5942
- new Intl.NumberFormat(this.#locale, { style: "currency", currency: this.currencyValue });
5943
- this.#currency = this.currencyValue;
5944
- } catch {
5945
- }
5967
+ const declaredCurrency = intlFormatter(Intl.NumberFormat, this.#locale, {
5968
+ style: "currency",
5969
+ currency: this.currencyValue
5970
+ });
5971
+ if (declaredCurrency !== null) this.#currency = this.currencyValue;
5946
5972
  }
5947
- this.#grouping = new Intl.NumberFormat(this.#locale, {
5948
- useGrouping: true,
5949
- maximumFractionDigits: 0
5950
- });
5951
- this.#fixed = new Intl.NumberFormat(this.#locale, {
5952
- useGrouping: true,
5953
- minimumFractionDigits: this.#precision,
5954
- maximumFractionDigits: this.#precision
5955
- });
5956
- this.#accessible = this.#currency ? new Intl.NumberFormat(this.#locale, { style: "currency", currency: this.#currency }) : this.#fixed;
5957
- const parts = new Intl.NumberFormat(this.#locale).formatToParts(11111.1);
5973
+ this.#grouping = intlFormatter(
5974
+ Intl.NumberFormat,
5975
+ this.#locale,
5976
+ { useGrouping: true, maximumFractionDigits: 0 },
5977
+ this.#locale
5978
+ );
5979
+ this.#fixed = intlFormatter(
5980
+ Intl.NumberFormat,
5981
+ this.#locale,
5982
+ {
5983
+ useGrouping: true,
5984
+ minimumFractionDigits: this.#precision,
5985
+ maximumFractionDigits: this.#precision
5986
+ },
5987
+ this.#locale
5988
+ );
5989
+ this.#accessible = this.#currency ? intlFormatter(
5990
+ Intl.NumberFormat,
5991
+ this.#locale,
5992
+ { style: "currency", currency: this.#currency },
5993
+ this.#locale
5994
+ ) : this.#fixed;
5995
+ const parts = intlFormatter(Intl.NumberFormat, this.#locale, {}, this.#locale).formatToParts(
5996
+ 11111.1
5997
+ );
5958
5998
  this.#group = parts.find((p) => p.type === "group")?.value ?? ",";
5959
5999
  this.#decimal = parts.find((p) => p.type === "decimal")?.value ?? ".";
5960
6000
  this.#digits.clear();
5961
- const digitFormatter = new Intl.NumberFormat(this.#locale, { useGrouping: false });
6001
+ const digitFormatter = intlFormatter(
6002
+ Intl.NumberFormat,
6003
+ this.#locale,
6004
+ { useGrouping: false },
6005
+ this.#locale
6006
+ );
5962
6007
  for (let i = 0; i <= 9; i++) {
5963
6008
  const digit = digitFormatter.format(i);
5964
6009
  if (digit !== String(i)) this.#digits.set(digit, String(i));
@@ -6790,6 +6835,43 @@ var DialogController = class extends Controller {
6790
6835
  return this.hasDialogTarget && !this.dialogTarget.hidden;
6791
6836
  }
6792
6837
  };
6838
+
6839
+ // src/utils/declared_value.ts
6840
+ function parseDeclared(raw, parse, fallback) {
6841
+ try {
6842
+ return parse(raw);
6843
+ } catch {
6844
+ return fallback;
6845
+ }
6846
+ }
6847
+ function validSelector(element, raw, fallback) {
6848
+ if (raw.length === 0) return fallback;
6849
+ return parseDeclared(
6850
+ raw,
6851
+ (selector) => {
6852
+ element.matches(selector);
6853
+ return selector;
6854
+ },
6855
+ fallback
6856
+ );
6857
+ }
6858
+ function compileRegExp(source, anchor = "none") {
6859
+ return parseDeclared(
6860
+ source,
6861
+ (text) => {
6862
+ const bare = new RegExp(text);
6863
+ return anchor === "exact" ? new RegExp(`^(?:${text})$`) : bare;
6864
+ },
6865
+ null
6866
+ );
6867
+ }
6868
+ function parseJsonObject(raw) {
6869
+ const parsed = parseDeclared(raw, (text) => JSON.parse(text), null);
6870
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
6871
+ return parsed;
6872
+ }
6873
+
6874
+ // src/controllers/direct_upload_controller.ts
6793
6875
  var REMOVE_DELAY = 4e3;
6794
6876
  var DirectUploadController = class extends Controller {
6795
6877
  static targets = ["list", "row"];
@@ -6844,16 +6926,7 @@ var DirectUploadController = class extends Controller {
6844
6926
  }
6845
6927
  /** Validates `scope` once so the per-event path never parses or throws. */
6846
6928
  scopeValueChanged() {
6847
- const selector = this.scopeValue;
6848
- if (selector.length > 0) {
6849
- try {
6850
- this.element.matches(selector);
6851
- this.#scopeSelector = selector;
6852
- return;
6853
- } catch {
6854
- }
6855
- }
6856
- this.#scopeSelector = "";
6929
+ this.#scopeSelector = validSelector(this.element, this.scopeValue, "");
6857
6930
  }
6858
6931
  /** Updates a row's progress and the aggregate, emitting `progress`. */
6859
6932
  #updateProgress(id, percent, name) {
@@ -9088,7 +9161,7 @@ var FormFieldController = class _FormFieldController extends Controller {
9088
9161
  return this.errorTargets.includes(target);
9089
9162
  return record.attributeName === "id" && [...this.descriptionTargets, ...this.errorTargets].includes(target);
9090
9163
  }
9091
- return this.errorTargets.some((error) => error === target || error.contains(target));
9164
+ return ownerIndex(this.errorTargets, target) !== -1;
9092
9165
  }
9093
9166
  /** Returns borrowed control ARIA before Turbo snapshots the page. */
9094
9167
  #rewindForCache() {
@@ -10008,22 +10081,19 @@ function applyMask(value, pattern, tokens) {
10008
10081
  }
10009
10082
  function compileTokens(declaration) {
10010
10083
  const map = /* @__PURE__ */ new Map();
10011
- for (const [key, source] of Object.entries({ ...DEFAULT_TOKENS, ...parseTokens(declaration) })) {
10012
- try {
10013
- map.set(key, new RegExp(`^(?:${source})$`));
10014
- } catch {
10015
- }
10084
+ for (const [key, source] of Object.entries(DEFAULT_TOKENS)) {
10085
+ const compiled = compileRegExp(source, "exact");
10086
+ if (compiled) map.set(key, compiled);
10087
+ }
10088
+ for (const [key, source] of Object.entries(parseTokens(declaration))) {
10089
+ const compiled = compileRegExp(source, "exact");
10090
+ if (compiled) map.set(key, compiled);
10016
10091
  }
10017
10092
  return map;
10018
10093
  }
10019
10094
  function parseTokens(declaration) {
10020
- let parsed;
10021
- try {
10022
- parsed = JSON.parse(declaration);
10023
- } catch {
10024
- return {};
10025
- }
10026
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return {};
10095
+ const parsed = parseJsonObject(declaration);
10096
+ if (parsed === null) return {};
10027
10097
  const tokens = {};
10028
10098
  for (const [key, source] of Object.entries(parsed)) {
10029
10099
  if (typeof source === "string") tokens[key] = source;
@@ -10235,11 +10305,7 @@ function isBeforeRootStart(entry) {
10235
10305
  }
10236
10306
  function queryRoot(selector) {
10237
10307
  if (!selector) return null;
10238
- try {
10239
- return document.querySelector(selector);
10240
- } catch {
10241
- }
10242
- return null;
10308
+ return parseDeclared(selector, (raw) => document.querySelector(raw), null);
10243
10309
  }
10244
10310
  var IntersectionWatcher = class {
10245
10311
  #onEntries;
@@ -11070,8 +11136,9 @@ var LocalTimeController = class extends Controller {
11070
11136
  /**
11071
11137
  * Formats `date` with `Intl.DateTimeFormat`, including each style only when it
11072
11138
  * is a valid keyword (so a consumer can show date-only or time-only by clearing
11073
- * the other). Returns `null` when neither style is usable or `Intl` throws, so
11074
- * the caller can leave the authored text untouched.
11139
+ * the other). Returns `null` when neither style is usable or the declaration
11140
+ * names a locale or time zone `Intl` rejects, so the caller can leave the
11141
+ * authored text untouched.
11075
11142
  */
11076
11143
  #applyFormat(date, dateStyle, timeStyle) {
11077
11144
  const options = {
@@ -11080,11 +11147,8 @@ var LocalTimeController = class extends Controller {
11080
11147
  };
11081
11148
  if (options.dateStyle === void 0 && options.timeStyle === void 0) return null;
11082
11149
  if (this.timeZoneValue.length > 0) options.timeZone = this.timeZoneValue;
11083
- try {
11084
- return new Intl.DateTimeFormat(this.#locale, options).format(date);
11085
- } catch {
11086
- return null;
11087
- }
11150
+ const formatter = intlFormatter(Intl.DateTimeFormat, this.#locale, options);
11151
+ return formatter === null ? null : formatter.format(date);
11088
11152
  }
11089
11153
  /** Locale precedence: the value, then the nearest `lang` up the ancestor chain. */
11090
11154
  get #locale() {
@@ -11152,7 +11216,7 @@ var MasonryController = class extends Controller {
11152
11216
  *
11153
11217
  * The removal is queued rather than immediate because teardown reports every
11154
11218
  * target as disconnected: doing it here would strip the whole grid just before
11155
- * a Turbo snapshot is taken. {@link MicrotaskCoalescer.cancel} drops the queue
11219
+ * a Turbo snapshot is taken. The coalescer's `cancel` drops the queue
11156
11220
  * with the pass, so only a genuine target change reaches it.
11157
11221
  */
11158
11222
  itemTargetDisconnected(item) {
@@ -11433,8 +11497,7 @@ var MenuController = class extends Controller {
11433
11497
  };
11434
11498
  /** The item target that is, or contains, `node`; `null` when it is neither. */
11435
11499
  #itemFrom(node) {
11436
- if (!(node instanceof Node)) return null;
11437
- return this.itemTargets.find((item) => item === node || item.contains(node)) ?? null;
11500
+ return ownerOf(this.itemTargets, node);
11438
11501
  }
11439
11502
  /**
11440
11503
  * First look at every click inside the controller, and the only point at which
@@ -11810,8 +11873,7 @@ var MenubarController = class extends Controller {
11810
11873
  * menu without moving focus.
11811
11874
  */
11812
11875
  #dismissOpenMenu() {
11813
- const active = document.activeElement;
11814
- const menu = this.menuTargets.find((candidate) => candidate.contains(active));
11876
+ const menu = ownerOf(this.menuTargets, document.activeElement);
11815
11877
  if (menu) {
11816
11878
  const top = this.#topFor(menu);
11817
11879
  this.#closeMenu(top);
@@ -13225,7 +13287,7 @@ var NavigationMenuController = class extends Controller {
13225
13287
  */
13226
13288
  #onPointerLeave = (event) => {
13227
13289
  const next = event instanceof MouseEvent ? event.relatedTarget : null;
13228
- if (next instanceof Node && this.#hoverElements.some((el) => el.contains(next))) return;
13290
+ if (ownerIndex(this.#hoverElements, next) !== -1) return;
13229
13291
  this.#hoveredTrigger = null;
13230
13292
  this.#hoverTimers.clearAll();
13231
13293
  this.#hoverTimers.set(() => this.#closeFromHover(), this.hoverDelayValue);
@@ -13295,7 +13357,7 @@ var NavigationMenuController = class extends Controller {
13295
13357
  */
13296
13358
  get #hoverElements() {
13297
13359
  const areas = this.hoverAreaTargets;
13298
- const covered = (element) => areas.some((area) => area.contains(element));
13360
+ const covered = (element) => ownerIndex(areas, element) !== -1;
13299
13361
  return [
13300
13362
  ...areas,
13301
13363
  ...this.triggerTargets.filter((trigger) => !covered(trigger)),
@@ -14336,13 +14398,7 @@ var OptimisticController = class extends Controller {
14336
14398
  }
14337
14399
  };
14338
14400
  var DEFAULT_PATTERN = "[0-9]";
14339
- function compilePattern(source) {
14340
- try {
14341
- return new RegExp(`^${source}$`);
14342
- } catch {
14343
- return null;
14344
- }
14345
- }
14401
+ var DEFAULT_MATCHER = new RegExp(`^(?:${DEFAULT_PATTERN})$`);
14346
14402
  function hasModifier2(event) {
14347
14403
  return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
14348
14404
  }
@@ -14357,7 +14413,7 @@ var OtpController = class extends Controller {
14357
14413
  static actions = ["onInput", "onKeydown", "onPaste", "onPointerDown", "clear"];
14358
14414
  static events = ["change", "complete", "invalid", "reconcile"];
14359
14415
  /** Validated matcher; the hot path never compiles a raw declaration. */
14360
- #pattern = new RegExp(`^${DEFAULT_PATTERN}$`);
14416
+ #pattern = DEFAULT_MATCHER;
14361
14417
  /** Source of {@link #pattern}, reported in `invalid` so consumers can word it. */
14362
14418
  #patternSource = DEFAULT_PATTERN;
14363
14419
  /** Public state carried by the last dispatch; keeps a no-op sync silent. */
@@ -14430,9 +14486,9 @@ var OtpController = class extends Controller {
14430
14486
  * the new pattern no longer accepts, so the combined value stays interpretable.
14431
14487
  */
14432
14488
  patternValueChanged() {
14433
- const compiled = compilePattern(this.patternValue);
14489
+ const compiled = compileRegExp(this.patternValue, "exact");
14434
14490
  this.#patternSource = compiled ? this.patternValue : DEFAULT_PATTERN;
14435
- this.#pattern = compiled ?? new RegExp(`^${DEFAULT_PATTERN}$`);
14491
+ this.#pattern = compiled ?? DEFAULT_MATCHER;
14436
14492
  if (!this.#connected) return;
14437
14493
  let dropped = false;
14438
14494
  for (const field of this.fieldTargets) {
@@ -15316,7 +15372,7 @@ var OverflowMenuController = class extends Controller {
15316
15372
  }
15317
15373
  /** The managed item that is, or contains, `el` — focus rescue works on either. */
15318
15374
  #ownerOf(el) {
15319
- return this.#items.find((item) => item === el || item.contains(el)) ?? null;
15375
+ return ownerOf(this.#items, el);
15320
15376
  }
15321
15377
  /** Whether the item takes up room in the bar; an authored `hidden` one does not. */
15322
15378
  #rendered(item) {
@@ -16833,9 +16889,7 @@ var PointerDragController = class _PointerDragController extends Controller {
16833
16889
  }
16834
16890
  /** Resolves the handle owning an event target (the handle or a descendant). */
16835
16891
  #handleFor(target) {
16836
- const node = target;
16837
- if (!node) return null;
16838
- return this.#handles().find((handle) => handle === node || handle.contains(node)) ?? null;
16892
+ return ownerOf(this.#handles(), target);
16839
16893
  }
16840
16894
  #pointerTypeOf(event) {
16841
16895
  const type = event.pointerType;
@@ -17016,16 +17070,7 @@ var PortalController = class extends Controller {
17016
17070
  * instead, which reads as a mistake and can be traced back to the declaration.
17017
17071
  */
17018
17072
  toValueChanged() {
17019
- const selector = this.toValue;
17020
- if (selector.length > 0) {
17021
- try {
17022
- this.element.matches(selector);
17023
- this.#toSelector = selector;
17024
- return;
17025
- } catch {
17026
- }
17027
- }
17028
- this.#toSelector = "body";
17073
+ this.#toSelector = validSelector(this.element, this.toValue, "body");
17029
17074
  }
17030
17075
  connect() {
17031
17076
  this.#gate.cancel();
@@ -17751,9 +17796,8 @@ var RadioGroupController = class extends Controller {
17751
17796
  }
17752
17797
  /** Finds this group's radio containing an event target, excluding nested groups. */
17753
17798
  #radioForEventTarget(target) {
17754
- if (!this.#ownsEventTarget(target)) return void 0;
17755
- const node = target;
17756
- return this.radioTargets.find((radio) => radio === node || radio.contains(node));
17799
+ if (!this.#ownsEventTarget(target)) return null;
17800
+ return ownerOf(this.radioTargets, target);
17757
17801
  }
17758
17802
  /** Whether the closest Radio Group scope around a target is this instance. */
17759
17803
  #ownsEventTarget(target) {
@@ -18660,6 +18704,7 @@ var UNITS = [
18660
18704
  { limit: 31557600, unit: "month", ms: 26298e5 },
18661
18705
  YEAR_SCALE
18662
18706
  ];
18707
+ var RELATIVE_OPTIONS = { numeric: "auto" };
18663
18708
  var RelativeTimeController = class extends Controller {
18664
18709
  static values = {
18665
18710
  locale: { type: String, default: "" },
@@ -18767,11 +18812,7 @@ var RelativeTimeController = class extends Controller {
18767
18812
  * when the runtime rejects that locale.
18768
18813
  */
18769
18814
  get #formatter() {
18770
- try {
18771
- return new Intl.RelativeTimeFormat(this.#locale, { numeric: "auto" });
18772
- } catch {
18773
- return null;
18774
- }
18815
+ return intlFormatter(Intl.RelativeTimeFormat, this.#locale, RELATIVE_OPTIONS);
18775
18816
  }
18776
18817
  /** Locale precedence: the value, then the nearest `lang` up the ancestor chain. */
18777
18818
  get #locale() {
@@ -18820,16 +18861,7 @@ var ResetBeforeCacheController = class extends Controller {
18820
18861
  * silently taking the sweep down.
18821
18862
  */
18822
18863
  scopeValueChanged() {
18823
- const selector = this.scopeValue;
18824
- if (selector.length > 0) {
18825
- try {
18826
- this.element.matches(selector);
18827
- this.#scopeSelector = selector;
18828
- return;
18829
- } catch {
18830
- }
18831
- }
18832
- this.#scopeSelector = "";
18864
+ this.#scopeSelector = validSelector(this.element, this.scopeValue, "");
18833
18865
  }
18834
18866
  connect() {
18835
18867
  document.addEventListener("turbo:before-cache", this.#onBeforeCache);
@@ -18931,8 +18963,7 @@ var ResizableController = class extends Controller {
18931
18963
  const panes = [];
18932
18964
  if (this.hasPrimaryTarget) panes.push(this.primaryTarget);
18933
18965
  if (this.hasSecondaryTarget) panes.push(this.secondaryTarget);
18934
- const target = event.target;
18935
- const current = panes.findIndex((pane) => target instanceof Node && pane.contains(target));
18966
+ const current = ownerIndex(panes, event.target);
18936
18967
  const next = panes[(current + 1) % panes.length];
18937
18968
  if (!next) return;
18938
18969
  event.preventDefault();
@@ -19184,9 +19215,7 @@ var RovingController = class extends Controller {
19184
19215
  };
19185
19216
  /** Resolves the item index owning an event target (the item or a descendant). */
19186
19217
  #indexOf(target) {
19187
- const node = target;
19188
- if (!node) return -1;
19189
- return this.itemTargets.findIndex((item) => item === node || item.contains(node));
19218
+ return ownerIndex(this.itemTargets, target);
19190
19219
  }
19191
19220
  /** Makes `index` the tab stop (optionally focusing it), emitting `change` once. */
19192
19221
  #activate(index, focus) {
@@ -19941,11 +19970,11 @@ var ScrollVisibilityController = class extends Controller {
19941
19970
  }
19942
19971
  /** Validates `root` once so connect never parses a selector that throws. */
19943
19972
  rootValueChanged() {
19944
- this.#rootSelector = this.#validSelector(this.rootValue);
19973
+ this.#rootSelector = validSelector(this.element, this.rootValue, "");
19945
19974
  }
19946
19975
  /** Validates `focusSelector` once so `toTop` never parses a selector that throws. */
19947
19976
  focusSelectorValueChanged() {
19948
- this.#focusSelector = this.#validSelector(this.focusSelectorValue);
19977
+ this.#focusSelector = validSelector(this.element, this.focusSelectorValue, "");
19949
19978
  }
19950
19979
  /** Scrolls the source to the top and, optionally, moves focus to a safe target. */
19951
19980
  toTop() {
@@ -20017,17 +20046,6 @@ var ScrollVisibilityController = class extends Controller {
20017
20046
  if (focused instanceof HTMLElement && this.elementTarget.contains(focused)) return focused;
20018
20047
  return null;
20019
20048
  }
20020
- /** Returns `declared` when it parses as a selector, and `""` when it does not. */
20021
- #validSelector(declared) {
20022
- if (declared.length > 0) {
20023
- try {
20024
- this.element.matches(declared);
20025
- return declared;
20026
- } catch {
20027
- }
20028
- }
20029
- return "";
20030
- }
20031
20049
  #scrollY() {
20032
20050
  if (this.#scrollSource === window) {
20033
20051
  return window.scrollY ?? window.pageYOffset ?? 0;
@@ -20161,7 +20179,7 @@ var ScrollspyController = class extends Controller {
20161
20179
  * only rewrites attributes, so Stimulus fires no target callback: without
20162
20180
  * this, a link re-pointed from `#intro` to `#faq` would keep the controller
20163
20181
  * observing `#intro` for the rest of the page's life. The filter is exactly
20164
- * {@link ANCHOR_ATTRIBUTES}; guarding on `MutationObserver` keeps the
20182
+ * `href` and `data-href`; guarding on `MutationObserver` keeps the
20165
20183
  * controller usable where the API is absent, matching `IntersectionWatcher`'s
20166
20184
  * own support guard.
20167
20185
  */
@@ -21211,7 +21229,7 @@ var SmartStickyHeaderController = class extends Controller {
21211
21229
  #onFocusin = () => this.#apply(false);
21212
21230
  /** Validates `containerSelector` once so connect never parses a selector that throws. */
21213
21231
  containerSelectorValueChanged() {
21214
- this.#containerSelector = this.#validSelector(this.containerSelectorValue);
21232
+ this.#containerSelector = validSelector(this.element, this.containerSelectorValue, "");
21215
21233
  }
21216
21234
  /** Re-decides when application code (or a Turbo morph) changes `offset` at runtime. */
21217
21235
  offsetValueChanged() {
@@ -21241,17 +21259,6 @@ var SmartStickyHeaderController = class extends Controller {
21241
21259
  }
21242
21260
  return window;
21243
21261
  }
21244
- /** Returns `declared` when it parses as a selector, and `""` when it does not. */
21245
- #validSelector(declared) {
21246
- if (declared.length > 0) {
21247
- try {
21248
- this.element.matches(declared);
21249
- return declared;
21250
- } catch {
21251
- }
21252
- }
21253
- return "";
21254
- }
21255
21262
  get #scrollY() {
21256
21263
  const scroller = this.#scrollerEl;
21257
21264
  return scroller === window ? window.scrollY : scroller.scrollTop;
@@ -23199,16 +23206,7 @@ var ThemeController = class extends Controller {
23199
23206
  }
23200
23207
  /** Validates the `target` declaration once, so the render path never parses. */
23201
23208
  targetValueChanged() {
23202
- const selector = this.targetValue;
23203
- if (selector.length > 0) {
23204
- try {
23205
- this.element.matches(selector);
23206
- this.#targetSelector = selector;
23207
- return;
23208
- } catch {
23209
- }
23210
- }
23211
- this.#targetSelector = DEFAULT_TARGET;
23209
+ this.#targetSelector = validSelector(this.element, this.targetValue, DEFAULT_TARGET);
23212
23210
  }
23213
23211
  /** Re-derives the single Tab stop and ARIA for an option set that changed. */
23214
23212
  optionTargetConnected() {
@@ -23221,9 +23219,8 @@ var ThemeController = class extends Controller {
23221
23219
  /**
23222
23220
  * Selects the mode the activated option declares.
23223
23221
  *
23224
- * Read through {@link ThemeController.#optionMode}, the same lane that decides
23225
- * which option is checked, so the two can never disagree about what an option
23226
- * declares.
23222
+ * Read through the same lane that decides which option is checked, so the two
23223
+ * can never disagree about what an option declares.
23227
23224
  */
23228
23225
  set(event) {
23229
23226
  const option = event.currentTarget;
@@ -24280,9 +24277,8 @@ var ToggleGroupController = class extends Controller {
24280
24277
  }
24281
24278
  /** Finds this group's item containing an event target, excluding nested groups. */
24282
24279
  #itemForEventTarget(target) {
24283
- if (!this.#ownsEventTarget(target)) return void 0;
24284
- const node = target;
24285
- return this.itemTargets.find((item) => item === node || item.contains(node));
24280
+ if (!this.#ownsEventTarget(target)) return null;
24281
+ return ownerOf(this.itemTargets, target);
24286
24282
  }
24287
24283
  /** Whether the closest Toggle Group scope around a target is this instance. */
24288
24284
  #ownsEventTarget(target) {
@@ -24540,9 +24536,7 @@ var ToolbarController = class extends Controller {
24540
24536
  }
24541
24537
  /** Index in `controlTargets` of the control owning `target` (it or a descendant). */
24542
24538
  #indexOf(target) {
24543
- const node = target;
24544
- if (!node) return -1;
24545
- return this.controlTargets.findIndex((control) => control === node || control.contains(node));
24539
+ return ownerIndex(this.controlTargets, target);
24546
24540
  }
24547
24541
  /** Controls eligible for the roving tab stop (excludes native disabled / hidden). */
24548
24542
  get #navigableControls() {
@@ -5,6 +5,6 @@ module Stimeo
5
5
  # Kept in lockstep with package.json (stable versions share the same
6
6
  # notation; a prerelease maps npm `-beta.N` ⇔ gem `.pre.beta.N` because
7
7
  # RubyGems forbids dashes). Bump both together.
8
- VERSION = "0.13.0"
8
+ VERSION = "0.14.0"
9
9
  end
10
10
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stimeo-ui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.0
4
+ version: 0.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stimeo Labs
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-13 00:00:00.000000000 Z
11
+ date: 2026-09-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties