stimeo-ui 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/README.md +8 -1
  4. data/dist/cable/index.js +363 -436
  5. data/dist/controllers/accordion_controller.js +166 -55
  6. data/dist/controllers/alert_dialog_controller.js +862 -221
  7. data/dist/controllers/announcer_controller.js +135 -198
  8. data/dist/controllers/aspect_ratio_controller.js +0 -10
  9. data/dist/controllers/auto_submit_controller.js +271 -127
  10. data/dist/controllers/avatar_controller.js +171 -105
  11. data/dist/controllers/breadcrumb_controller.js +0 -130
  12. data/dist/controllers/bulk_select_controller.js +262 -52
  13. data/dist/controllers/calendar_controller.js +175 -209
  14. data/dist/controllers/carousel_controller.js +461 -399
  15. data/dist/controllers/character_counter_controller.js +246 -156
  16. data/dist/controllers/checkbox_controller.js +32 -74
  17. data/dist/controllers/clipboard_controller.js +168 -147
  18. data/dist/controllers/collapsible_controller.js +301 -120
  19. data/dist/controllers/color_picker_controller.js +149 -139
  20. data/dist/controllers/combobox_controller.js +360 -133
  21. data/dist/controllers/command_palette_controller.js +970 -358
  22. data/dist/controllers/conditional_fields_controller.js +166 -124
  23. data/dist/controllers/confirm_controller.js +939 -248
  24. data/dist/controllers/context_menu_controller.js +256 -128
  25. data/dist/controllers/count_up_controller.js +73 -21
  26. data/dist/controllers/countdown_controller.js +183 -148
  27. data/dist/controllers/currency_input_controller.js +134 -158
  28. data/dist/controllers/data_grid_controller.js +114 -186
  29. data/dist/controllers/date_range_picker_controller.js +288 -206
  30. data/dist/controllers/dialog_controller.js +858 -223
  31. data/dist/controllers/direct_upload_controller.js +73 -153
  32. data/dist/controllers/dirty_form_controller.js +14 -48
  33. data/dist/controllers/dismissible_controller.js +175 -21
  34. data/dist/controllers/drawer_controller.js +933 -341
  35. data/dist/controllers/dropdown_controller.js +312 -82
  36. data/dist/controllers/editable_controller.js +158 -68
  37. data/dist/controllers/empty_state_controller.js +67 -53
  38. data/dist/controllers/file_dropzone_controller.js +311 -207
  39. data/dist/controllers/filter_controller.js +67 -39
  40. data/dist/controllers/flash_controller.js +172 -314
  41. data/dist/controllers/focus_controller.js +654 -240
  42. data/dist/controllers/form_field_controller.js +147 -132
  43. data/dist/controllers/form_validation_controller.js +13 -86
  44. data/dist/controllers/frame_loading_controller.js +264 -213
  45. data/dist/controllers/highlight_controller.js +107 -75
  46. data/dist/controllers/hover_card_controller.js +309 -118
  47. data/dist/controllers/idle_controller.js +267 -101
  48. data/dist/controllers/input_mask_controller.js +71 -72
  49. data/dist/controllers/intersection_controller.js +147 -106
  50. data/dist/controllers/lazy_frame_controller.js +9 -65
  51. data/dist/controllers/listbox_controller.js +306 -204
  52. data/dist/controllers/local_time_controller.js +54 -64
  53. data/dist/controllers/masonry_controller.js +70 -93
  54. data/dist/controllers/menu_controller.js +214 -156
  55. data/dist/controllers/menubar_controller.js +170 -329
  56. data/dist/controllers/meter_controller.js +151 -67
  57. data/dist/controllers/multi_select_controller.js +593 -358
  58. data/dist/controllers/navigation_menu_controller.js +124 -232
  59. data/dist/controllers/nested_form_controller.js +352 -182
  60. data/dist/controllers/network_status_controller.js +242 -54
  61. data/dist/controllers/number_input_controller.js +320 -263
  62. data/dist/controllers/optimistic_controller.js +16 -76
  63. data/dist/controllers/otp_controller.js +198 -193
  64. data/dist/controllers/overflow_indicator_controller.js +116 -113
  65. data/dist/controllers/overflow_menu_controller.js +343 -283
  66. data/dist/controllers/pagination_controller.js +295 -119
  67. data/dist/controllers/password_reveal_controller.js +389 -121
  68. data/dist/controllers/password_strength_controller.js +99 -153
  69. data/dist/controllers/persist_controller.js +237 -103
  70. data/dist/controllers/pointer_drag_controller.js +185 -165
  71. data/dist/controllers/popover_controller.js +236 -97
  72. data/dist/controllers/portal_controller.js +209 -76
  73. data/dist/controllers/preview_guard_controller.js +232 -116
  74. data/dist/controllers/progress_controller.js +125 -56
  75. data/dist/controllers/radio_group_controller.js +117 -137
  76. data/dist/controllers/range_slider_controller.js +192 -105
  77. data/dist/controllers/rating_controller.js +418 -207
  78. data/dist/controllers/read_more_controller.js +134 -63
  79. data/dist/controllers/reading_progress_controller.js +147 -135
  80. data/dist/controllers/relative_time_controller.js +145 -110
  81. data/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +97 -38
  82. data/dist/controllers/resizable_controller.js +255 -139
  83. data/dist/controllers/roving_controller.js +39 -84
  84. data/dist/controllers/scroll_area_controller.js +428 -225
  85. data/dist/controllers/scroll_restore_controller.js +0 -53
  86. data/dist/controllers/scroll_visibility_controller.js +395 -145
  87. data/dist/controllers/scrollspy_controller.js +181 -242
  88. data/dist/controllers/separator_controller.js +267 -140
  89. data/dist/controllers/sidebar_controller.js +1023 -386
  90. data/dist/controllers/skeleton_controller.js +248 -97
  91. data/dist/controllers/slider_controller.js +159 -79
  92. data/dist/controllers/smart_sticky_header_controller.js +162 -63
  93. data/dist/controllers/sortable_controller.js +0 -112
  94. data/dist/controllers/spinner_controller.js +277 -169
  95. data/dist/controllers/step_indicator_controller.js +99 -46
  96. data/dist/controllers/stepper_controller.js +114 -73
  97. data/dist/controllers/stick_to_bottom_controller.js +262 -138
  98. data/dist/controllers/sticky_observer_controller.js +214 -49
  99. data/dist/controllers/submit_once_controller.js +294 -205
  100. data/dist/controllers/switch_controller.js +33 -60
  101. data/dist/controllers/tabs_controller.js +35 -29
  102. data/dist/controllers/tags_input_controller.js +136 -145
  103. data/dist/controllers/textarea_autosize_controller.js +71 -58
  104. data/dist/controllers/theme_controller.js +130 -112
  105. data/dist/controllers/time_picker_controller.js +146 -80
  106. data/dist/controllers/toast_controller.js +186 -297
  107. data/dist/controllers/toggle_group_controller.js +95 -136
  108. data/dist/controllers/toolbar_controller.js +68 -98
  109. data/dist/controllers/tooltip_controller.js +158 -124
  110. data/dist/controllers/transition_controller.js +123 -119
  111. data/dist/controllers/tree_view_controller.js +150 -345
  112. data/dist/index.js +6370 -9346
  113. data/dist/positioning/index.js +148 -47
  114. data/lib/stimeo/ui/version.rb +1 -1
  115. metadata +3 -3
@@ -1,20 +1,158 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/hover_card_controller.ts
3
+ function sharedRegistry(key, create) {
4
+ const symbol = Symbol.for(key);
5
+ const scope = globalThis;
6
+ const existing = scope[symbol];
7
+ if (existing !== void 0) return existing;
8
+ const registry = create();
9
+ Object.defineProperty(globalThis, symbol, { value: registry });
10
+ return registry;
11
+ }
12
+
13
+ function isAttributeValue(value) {
14
+ return value === null || typeof value === "string";
15
+ }
16
+ function readLeaseRecord(element, record) {
17
+ try {
18
+ const parsed = JSON.parse(element.getAttribute(record) ?? "null");
19
+ if (!Array.isArray(parsed)) return void 0;
20
+ const [original, written, ...rest] = parsed;
21
+ const linked = rest[0] !== false;
22
+ const beneath = linked ? rest : rest.slice(1);
23
+ if (!beneath.every((under) => typeof under === "string")) return void 0;
24
+ return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
25
+ } catch {
26
+ return void 0;
27
+ }
28
+ }
29
+ var AttributeLease = class _AttributeLease {
30
+ static #registry = sharedRegistry(
31
+ "stimeo-ui.attribute-lease.registry.v1",
32
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
33
+ );
34
+ #attribute;
35
+ #record;
36
+ #base;
37
+ #held = /* @__PURE__ */ new Set();
38
+ constructor(attribute, owner, options = {}) {
39
+ this.#attribute = attribute;
40
+ this.#record = `data-${owner}-${attribute}-lease`;
41
+ this.#base = options.base === true;
42
+ const names = _AttributeLease.#registry.names;
43
+ names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
44
+ }
45
+ write(element, value) {
46
+ const stack = this.#stack(element);
47
+ const hold = this.#take(stack) ?? this.#acquire(element, stack);
48
+ this.#held.add(element);
49
+ const shows = !this.#covered(element, stack, hold);
50
+ hold.written = value;
51
+ this.#mark(element, stack);
52
+ if (shows) this.#reflect(element, value);
53
+ }
54
+ return(element) {
55
+ const held = this.#held.delete(element);
56
+ const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
57
+ const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
58
+ const hold = this.#take(stack);
59
+ if (!hold) return false;
60
+ const carries = element.getAttribute(this.#attribute) === hold.written;
61
+ const owns = carries && !this.#covered(element, stack, hold);
62
+ const index = stack.indexOf(hold);
63
+ stack.splice(index, 1);
64
+ const above = stack[index];
65
+ if (above) above.linked &&= hold.linked;
66
+ this.#mark(element, stack);
67
+ if (!owns) return true;
68
+ const below = hold.linked ? stack[index - 1] : void 0;
69
+ this.#reflect(element, below ? below.written : hold.original);
70
+ return true;
71
+ }
72
+ returnAll() {
73
+ for (const element of Array.from(this.#held)) this.return(element);
74
+ }
75
+ #stack(element) {
76
+ const stacks = _AttributeLease.#registry.stacks;
77
+ const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
78
+ stacks.set(element, byAttribute);
79
+ const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
80
+ byAttribute.set(this.#attribute, stack);
81
+ return stack;
82
+ }
83
+ #seed(element) {
84
+ const found = [];
85
+ for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
86
+ const read = readLeaseRecord(element, record);
87
+ if (read) found.push({ ...read, record });
88
+ }
89
+ const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
90
+ return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
91
+ lease: null,
92
+ record,
93
+ written,
94
+ original,
95
+ linked: linked && index > 0
96
+ }));
97
+ }
98
+ #take(stack) {
99
+ const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
100
+ if (hold) hold.lease = this;
101
+ return hold;
102
+ }
103
+ #acquire(element, stack) {
104
+ const current = element.getAttribute(this.#attribute);
105
+ const top = stack.at(-1);
106
+ const bottom = stack[0];
107
+ const over = top?.written === current ? top : void 0;
108
+ const from = this.#base ? bottom : over;
109
+ const hold = {
110
+ lease: this,
111
+ record: this.#record,
112
+ written: current,
113
+ original: from ? from.original : current,
114
+ linked: !this.#base && over !== void 0
115
+ };
116
+ if (this.#base && bottom) {
117
+ bottom.linked = true;
118
+ stack.unshift(hold);
119
+ } else stack.push(hold);
120
+ return hold;
121
+ }
122
+ #covered(element, stack, hold) {
123
+ const current = element.getAttribute(this.#attribute);
124
+ return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
125
+ }
126
+ #mark(element, stack) {
127
+ const records = /* @__PURE__ */ new Map([[this.#record, null]]);
128
+ stack.forEach((hold, index) => {
129
+ const names = stack.slice(0, index).map((under) => under.record);
130
+ const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
131
+ const marker = hold.linked || beneath.length === 0 ? [] : [false];
132
+ records.set(
133
+ hold.record,
134
+ hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
135
+ );
136
+ });
137
+ for (const [name, recorded] of records) {
138
+ if (element.getAttribute(name) === recorded) continue;
139
+ if (recorded === null) element.removeAttribute(name);
140
+ else element.setAttribute(name, recorded);
141
+ }
142
+ }
143
+ #reflect(element, value) {
144
+ if (element.getAttribute(this.#attribute) === value) return;
145
+ if (value === null) element.removeAttribute(this.#attribute);
146
+ else element.setAttribute(this.#attribute, value);
147
+ }
148
+ };
4
149
 
5
- // src/utils/escape_layer.ts
6
150
  var EscapeLayer = class _EscapeLayer {
7
- static #registries = /* @__PURE__ */ new WeakMap();
8
- #ownerDocument = null;
9
- /** Dismissal callback while active; `null` when inactive. */
10
- #onDismiss = null;
11
- /** Live predicate deciding whether the layer claims a press; `null` = always. */
12
- #claims = null;
13
- /**
14
- * Activates this layer at the top of its document's Escape stack, installing
15
- * the document's shared resolver listener if this is its first layer.
16
- * Re-activating an already-active layer moves it to the top.
17
- */
151
+ static #registries = sharedRegistry(
152
+ "stimeo-ui.escape-layer.registry.v1",
153
+ () => /* @__PURE__ */ new WeakMap()
154
+ );
155
+ #active = null;
18
156
  activate(ownerDocument = document, options) {
19
157
  this.deactivate();
20
158
  let registry = _EscapeLayer.#registries.get(ownerDocument);
@@ -23,44 +161,31 @@ var EscapeLayer = class _EscapeLayer {
23
161
  _EscapeLayer.#registries.set(ownerDocument, registry);
24
162
  ownerDocument.addEventListener("keydown", registry.onKeydown);
25
163
  }
26
- registry.stack.push(this);
27
- this.#ownerDocument = ownerDocument;
28
- this.#onDismiss = options.onDismiss;
29
- this.#claims = options.claims ?? null;
30
- }
31
- /**
32
- * Removes this layer from its document's Escape stack, uninstalling the
33
- * shared listener when the stack empties. Safe to call when inactive.
34
- */
164
+ const entry = { onDismiss: options.onDismiss, claims: options.claims ?? null };
165
+ registry.stack.push(entry);
166
+ this.#active = { document: ownerDocument, entry };
167
+ }
35
168
  deactivate() {
36
- const ownerDocument = this.#ownerDocument;
37
- if (!ownerDocument) return;
38
- const registry = _EscapeLayer.#registries.get(ownerDocument);
169
+ const active = this.#active;
170
+ if (!active) return;
171
+ const registry = _EscapeLayer.#registries.get(active.document);
39
172
  if (registry) {
40
- const index = registry.stack.lastIndexOf(this);
173
+ const index = registry.stack.lastIndexOf(active.entry);
41
174
  if (index >= 0) registry.stack.splice(index, 1);
42
175
  if (registry.stack.length === 0) {
43
- ownerDocument.removeEventListener("keydown", registry.onKeydown);
44
- _EscapeLayer.#registries.delete(ownerDocument);
176
+ active.document.removeEventListener("keydown", registry.onKeydown);
177
+ _EscapeLayer.#registries.delete(active.document);
45
178
  }
46
179
  }
47
- this.#ownerDocument = null;
48
- this.#onDismiss = null;
49
- this.#claims = null;
50
- }
51
- /**
52
- * Whether this active layer would own a press right now: it is the topmost
53
- * layer whose {@link EscapeLayerOptions.claims} passes. Exposed for tests
54
- * and diagnostics — production dismissal goes through the shared listener.
55
- */
180
+ this.#active = null;
181
+ }
56
182
  get ownsEscape() {
57
- const ownerDocument = this.#ownerDocument;
58
- if (!ownerDocument) return false;
59
- const registry = _EscapeLayer.#registries.get(ownerDocument);
183
+ const active = this.#active;
184
+ if (!active) return false;
185
+ const registry = _EscapeLayer.#registries.get(active.document);
60
186
  if (!registry) return false;
61
- return _EscapeLayer.#resolveOwner(registry.stack) === this;
187
+ return _EscapeLayer.#resolveOwner(registry.stack) === active.entry;
62
188
  }
63
- /** Builds a document's registry with its shared resolver listener. */
64
189
  static #createRegistry() {
65
190
  const registry = {
66
191
  stack: [],
@@ -69,60 +194,86 @@ var EscapeLayer = class _EscapeLayer {
69
194
  const owner = _EscapeLayer.#resolveOwner(registry.stack);
70
195
  if (!owner) return;
71
196
  event.preventDefault();
72
- owner.#onDismiss?.();
197
+ owner.onDismiss();
73
198
  }
74
199
  };
75
200
  return registry;
76
201
  }
77
- /** The topmost stack layer whose claims predicate passes, or `null`. */
78
202
  static #resolveOwner(stack) {
79
203
  for (let index = stack.length - 1; index >= 0; index--) {
80
204
  const layer = stack[index];
81
205
  if (!layer) continue;
82
- if (layer.#claims && !layer.#claims()) continue;
206
+ if (layer.claims && !layer.claims()) continue;
83
207
  return layer;
84
208
  }
85
209
  return null;
86
210
  }
87
211
  };
88
212
 
89
- // src/utils/safe_timeout.ts
213
+ var MAX_TIMER_DELAY_MS = 2147483647;
214
+
215
+ var NUMBER_BOUNDS = {
216
+ timer: { finite: true, min: 0, max: MAX_TIMER_DELAY_MS }};
217
+ function matchesNumberBounds(value, bounds) {
218
+ if (!Number.isFinite(value)) {
219
+ const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
220
+ if (direction === null) return false;
221
+ if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
222
+ }
223
+ if (bounds.min !== void 0 && value < bounds.min) return false;
224
+ if (bounds.max !== void 0 && value > bounds.max) return false;
225
+ if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
226
+ if (bounds.integer && !Number.isInteger(value)) return false;
227
+ if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
228
+ return true;
229
+ }
230
+
231
+ function readNumber(raw, fallback, bounds) {
232
+ return matchesNumberBounds(raw, bounds) ? raw : fallback;
233
+ }
234
+
235
+ var NumberValueReader = class {
236
+ #lastRejected = /* @__PURE__ */ new Map();
237
+ read(owner, name, raw, fallback, bounds) {
238
+ const resolved = readNumber(raw, fallback, bounds);
239
+ if (matchesNumberBounds(raw, bounds)) {
240
+ this.#lastRejected.delete(name);
241
+ return resolved;
242
+ }
243
+ const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
244
+ const literal = owner.element.getAttribute(attribute);
245
+ if (literal === null) {
246
+ this.#lastRejected.delete(name);
247
+ return resolved;
248
+ }
249
+ if (this.#lastRejected.get(name) !== literal) {
250
+ this.#lastRejected.set(name, literal);
251
+ console.warn(
252
+ `Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
253
+ );
254
+ }
255
+ return resolved;
256
+ }
257
+ };
258
+
90
259
  var TimerRegistry = class {
91
- /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
92
260
  ids = /* @__PURE__ */ new Set();
93
- /**
94
- * Cancels a single tracked timer.
95
- *
96
- * No-ops if the id is unknown (already cleared, fired, or never owned by this
97
- * registry), so callers can clear defensively without guarding.
98
- */
99
261
  clear(id) {
100
262
  if (this.ids.delete(id)) {
101
263
  this.cancel(id);
102
264
  }
103
265
  }
104
- /**
105
- * Cancels every tracked timer. Call this from a controller's `disconnect()`
106
- * to guarantee no timer outlives the element.
107
- */
108
266
  clearAll() {
109
267
  for (const id of this.ids) {
110
268
  this.cancel(id);
111
269
  }
112
270
  this.ids.clear();
113
271
  }
114
- /** Number of timers currently tracked (pending). */
115
272
  get size() {
116
273
  return this.ids.size;
117
274
  }
118
275
  };
119
276
  var SafeTimeout = class extends TimerRegistry {
120
- /**
121
- * Schedules `callback` after `delay` ms and returns the timer id.
122
- *
123
- * The id is removed from the registry automatically when the timeout fires,
124
- * so {@link TimerRegistry.size | size} reflects only still-pending timers.
125
- */
126
277
  set(callback, delay) {
127
278
  const id = this.schedule(() => {
128
279
  this.ids.delete(id);
@@ -139,7 +290,6 @@ var SafeTimeout = class extends TimerRegistry {
139
290
  }
140
291
  };
141
292
 
142
- // src/utils/scroll_dismiss.ts
143
293
  function observeScrollDismiss(element, onScroll) {
144
294
  const targets = [...scrollParents(element), window];
145
295
  const handler = () => onScroll();
@@ -164,7 +314,6 @@ function isScrollable(element) {
164
314
  return /auto|scroll|overlay/.test(style.overflow + style.overflowX + style.overflowY);
165
315
  }
166
316
 
167
- // src/utils/state_reason.ts
168
317
  var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
169
318
  var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
170
319
  function stateReasonFor(event) {
@@ -174,129 +323,171 @@ function stateReasonFor(event) {
174
323
  return "user";
175
324
  }
176
325
 
177
- // src/controllers/hover_card_controller.ts
178
- var HoverCardController = class extends Controller {
326
+ var HoverCardController = class _HoverCardController extends Controller {
327
+ #numbers = new NumberValueReader();
179
328
  static targets = ["trigger", "card"];
180
329
  static values = {
181
330
  openDelay: { type: Number, default: 300 },
182
331
  closeDelay: { type: Number, default: 200 },
183
332
  closeOnScroll: { type: Boolean, default: false }
184
333
  };
334
+ static valueConstraints = {
335
+ openDelay: NUMBER_BOUNDS.timer,
336
+ closeDelay: NUMBER_BOUNDS.timer
337
+ };
185
338
  static actions = ["close", "open"];
186
339
  static events = ["close", "open"];
187
- /** Pending open/close timers, with their IDs reset on every lifecycle boundary. */
188
340
  #timers = new SafeTimeout();
189
- /** Escape-stack membership while open; the shared resolver dismisses via it. */
190
341
  #escapeLayer = new EscapeLayer();
342
+ #expanded = new AttributeLease("aria-expanded", this.identifier);
191
343
  #pendingOpen = null;
192
344
  #pendingClose = null;
193
- /** Cleanup for the dismiss-on-scroll listeners while open, or `null`. */
194
345
  #stopScrollDismiss = null;
195
- /** Whether state moves are reported: set once `connect()` settled the baseline. */
346
+ #shownCard = null;
347
+ #connected = false;
196
348
  #reporting = false;
197
- /** Starts closed and discards any stale pending state from a prior connection. */
198
349
  connect() {
199
- this.#cancelOpen();
200
- this.#cancelClose();
350
+ this.#cancelTimers();
201
351
  this.#conceal("api");
352
+ this.#connected = true;
202
353
  this.#reporting = true;
203
354
  }
204
- /** Clears timers, the Escape-stack membership, and scroll listeners so nothing outlives the element. */
205
355
  disconnect() {
356
+ this.#connected = false;
206
357
  this.#reporting = false;
207
- this.#cancelOpen();
208
- this.#cancelClose();
209
- this.#escapeLayer.deactivate();
210
- this.#stopScrollDismiss?.();
211
- this.#stopScrollDismiss = null;
358
+ this.#cancelTimers();
359
+ this.#release();
360
+ }
361
+ closeOnScrollValueChanged() {
362
+ if (this.#shownCard) this.#syncScrollDismiss();
363
+ }
364
+ cardTargetDisconnected(card) {
365
+ if (card !== this.#shownCard) return;
366
+ if (this.cardTargets.includes(card)) return;
367
+ this.#cancelTimers();
368
+ this.#release();
369
+ card.hidden = true;
370
+ card.setAttribute("data-state", "closed");
371
+ this.#reflectClosed();
372
+ }
373
+ triggerTargetConnected() {
374
+ if (this.#connected) this.#reflectTrigger();
375
+ }
376
+ triggerTargetDisconnected(trigger) {
377
+ if (!this.triggerTargets.includes(trigger)) this.#expanded.return(trigger);
378
+ if (this.#connected) this.#reflectTrigger();
212
379
  }
213
- /** Opens the card, after `openDelay` ms (or immediately at 0). Cancels a pending close. */
214
380
  open(event) {
215
381
  const reason = stateReasonFor(event);
216
382
  this.#cancelClose();
217
383
  if (this.#isOpen || this.#pendingOpen !== null) return;
218
- if (this.openDelayValue <= 0) {
384
+ if (this.#safeOpenDelay <= 0) {
219
385
  this.#reveal(reason);
220
386
  return;
221
387
  }
222
388
  this.#pendingOpen = this.#timers.set(() => {
223
389
  this.#pendingOpen = null;
224
390
  this.#reveal(reason);
225
- }, this.openDelayValue);
226
- }
227
- /**
228
- * Schedules the card to close after `closeDelay`. Cancels a pending open. The
229
- * delayed callback re-checks whether focus has landed inside the controller
230
- * (e.g. a link in the card) and, if so, aborts the close — covering keyboard
231
- * traversal that the pointer-only hoverable bridge cannot.
232
- */
391
+ }, this.#safeOpenDelay);
392
+ }
233
393
  close(event) {
234
394
  const reason = stateReasonFor(event);
235
395
  this.#cancelOpen();
236
- if (!this.#isOpen || this.#pendingClose !== null) return;
396
+ if (!this.#closable || this.#pendingClose !== null) return;
237
397
  this.#pendingClose = this.#timers.set(() => {
238
398
  this.#pendingClose = null;
239
399
  if (this.element.contains(document.activeElement)) return;
240
400
  this.#conceal(reason);
241
- }, this.closeDelayValue);
242
- }
243
- /**
244
- * Reveals the card, reflects state, reports a move, and joins the Escape stack.
245
- *
246
- * @stimeoRuntimeOnly `closeOnScroll` decides whether this reveal wires the scroll dismissal; what
247
- * is shown does not depend on it.
248
- */
401
+ }, this.#safeCloseDelay);
402
+ }
249
403
  #reveal(reason) {
250
404
  if (!this.hasCardTarget) return;
405
+ const card = this.cardTarget;
251
406
  const was = this.#isOpen;
252
- this.cardTarget.hidden = false;
253
- this.cardTarget.setAttribute("data-state", "open");
254
- if (this.hasTriggerTarget) this.triggerTarget.setAttribute("aria-expanded", "true");
407
+ card.hidden = false;
408
+ card.setAttribute("data-state", "open");
409
+ if (this.hasTriggerTarget) this.#expanded.write(this.triggerTarget, "true");
410
+ this.#shownCard = card;
255
411
  if (!was && this.#reporting) this.dispatch("open", { detail: { reason }, cancelable: false });
412
+ if (this.#shownCard !== card) return;
256
413
  this.#escapeLayer.activate(document, { onDismiss: () => this.#dismiss("escape") });
257
- if (this.closeOnScrollValue && !this.#stopScrollDismiss) {
258
- this.#stopScrollDismiss = observeScrollDismiss(this.element, () => this.#dismiss("scroll"));
259
- }
414
+ this.#syncScrollDismiss();
260
415
  }
261
- /** Hides the card, reflects state, reports a move, and leaves the Escape stack. */
262
416
  #conceal(reason) {
263
417
  const was = this.#isOpen;
264
- this.#escapeLayer.deactivate();
265
- this.#stopScrollDismiss?.();
266
- this.#stopScrollDismiss = null;
418
+ this.#release();
419
+ this.#reflectClosed();
420
+ if (was && this.#reporting) this.dispatch("close", { detail: { reason }, cancelable: false });
421
+ }
422
+ #reflectClosed() {
267
423
  if (this.hasCardTarget) {
268
424
  this.cardTarget.hidden = true;
269
425
  this.cardTarget.setAttribute("data-state", "closed");
270
426
  }
271
- if (this.hasTriggerTarget) this.triggerTarget.setAttribute("aria-expanded", "false");
272
- if (was && this.#reporting) this.dispatch("close", { detail: { reason }, cancelable: false });
427
+ if (this.hasTriggerTarget) this.#expanded.write(this.triggerTarget, "false");
428
+ }
429
+ #reflectTrigger() {
430
+ if (this.hasTriggerTarget) this.#expanded.write(this.triggerTarget, String(this.#isOpen));
431
+ }
432
+ #release() {
433
+ this.#shownCard = null;
434
+ this.#escapeLayer.deactivate();
435
+ this.#stopScrollDismiss?.();
436
+ this.#stopScrollDismiss = null;
437
+ }
438
+ #syncScrollDismiss() {
439
+ if (this.closeOnScrollValue) {
440
+ this.#stopScrollDismiss ??= observeScrollDismiss(this.element, () => this.#dismiss("scroll"));
441
+ return;
442
+ }
443
+ this.#stopScrollDismiss?.();
444
+ this.#stopScrollDismiss = null;
273
445
  }
274
- /** Cancels pending timers and conceals immediately (shared Escape path). */
275
446
  #dismiss(reason) {
447
+ this.#cancelTimers();
448
+ this.#conceal(reason);
449
+ }
450
+ #cancelTimers() {
276
451
  this.#cancelOpen();
277
452
  this.#cancelClose();
278
- this.#conceal(reason);
279
453
  }
280
- /** Cancels any pending open timer. */
281
454
  #cancelOpen() {
282
455
  if (this.#pendingOpen !== null) {
283
456
  this.#timers.clear(this.#pendingOpen);
284
457
  this.#pendingOpen = null;
285
458
  }
286
459
  }
287
- /** Cancels any pending close timer. */
288
460
  #cancelClose() {
289
461
  if (this.#pendingClose !== null) {
290
462
  this.#timers.clear(this.#pendingClose);
291
463
  this.#pendingClose = null;
292
464
  }
293
465
  }
294
- /** Whether the card is currently visible. */
295
466
  get #isOpen() {
296
467
  return this.hasCardTarget && !this.cardTarget.hidden;
297
468
  }
469
+ get #closable() {
470
+ return this.#isOpen || this.#shownCard !== null;
471
+ }
472
+ get #safeOpenDelay() {
473
+ return this.#numbers.read(
474
+ this,
475
+ "openDelay",
476
+ this.openDelayValue,
477
+ _HoverCardController.values.openDelay.default,
478
+ _HoverCardController.valueConstraints.openDelay
479
+ );
480
+ }
481
+ get #safeCloseDelay() {
482
+ return this.#numbers.read(
483
+ this,
484
+ "closeDelay",
485
+ this.closeDelayValue,
486
+ _HoverCardController.values.closeDelay.default,
487
+ _HoverCardController.valueConstraints.closeDelay
488
+ );
489
+ }
298
490
  };
299
491
 
300
492
  export { HoverCardController };
301
- //# sourceMappingURL=hover_card_controller.js.map
302
493
  //# sourceMappingURL=hover_card_controller.js.map