stimeo-ui 0.15.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 +209 -0
  3. data/README.md +128 -1
  4. data/dist/cable/index.js +392 -417
  5. data/dist/controllers/accordion_controller.js +228 -28
  6. data/dist/controllers/alert_dialog_controller.js +862 -221
  7. data/dist/controllers/announcer_controller.js +135 -191
  8. data/dist/controllers/aspect_ratio_controller.js +0 -10
  9. data/dist/controllers/auto_submit_controller.js +295 -110
  10. data/dist/controllers/avatar_controller.js +172 -106
  11. data/dist/controllers/breadcrumb_controller.js +0 -129
  12. data/dist/controllers/bulk_select_controller.js +264 -53
  13. data/dist/controllers/calendar_controller.js +346 -180
  14. data/dist/controllers/carousel_controller.js +587 -300
  15. data/dist/controllers/character_counter_controller.js +271 -143
  16. data/dist/controllers/checkbox_controller.js +77 -75
  17. data/dist/controllers/clipboard_controller.js +187 -108
  18. data/dist/controllers/collapsible_controller.js +374 -108
  19. data/dist/controllers/color_picker_controller.js +183 -127
  20. data/dist/controllers/combobox_controller.js +426 -109
  21. data/dist/controllers/command_palette_controller.js +994 -350
  22. data/dist/controllers/conditional_fields_controller.js +212 -125
  23. data/dist/controllers/confirm_controller.js +939 -245
  24. data/dist/controllers/context_menu_controller.js +275 -127
  25. data/dist/controllers/count_up_controller.js +73 -21
  26. data/dist/controllers/countdown_controller.js +235 -97
  27. data/dist/controllers/currency_input_controller.js +241 -118
  28. data/dist/controllers/data_grid_controller.js +229 -135
  29. data/dist/controllers/date_range_picker_controller.js +359 -165
  30. data/dist/controllers/dialog_controller.js +887 -225
  31. data/dist/controllers/direct_upload_controller.js +80 -142
  32. data/dist/controllers/dirty_form_controller.js +26 -47
  33. data/dist/controllers/dismissible_controller.js +175 -20
  34. data/dist/controllers/drawer_controller.js +963 -336
  35. data/dist/controllers/dropdown_controller.js +342 -85
  36. data/dist/controllers/editable_controller.js +182 -58
  37. data/dist/controllers/empty_state_controller.js +67 -53
  38. data/dist/controllers/file_dropzone_controller.js +426 -229
  39. data/dist/controllers/filter_controller.js +78 -36
  40. data/dist/controllers/flash_controller.js +328 -211
  41. data/dist/controllers/focus_controller.js +654 -239
  42. data/dist/controllers/form_field_controller.js +153 -136
  43. data/dist/controllers/form_validation_controller.js +29 -96
  44. data/dist/controllers/frame_loading_controller.js +263 -198
  45. data/dist/controllers/highlight_controller.js +119 -70
  46. data/dist/controllers/hover_card_controller.js +335 -118
  47. data/dist/controllers/idle_controller.js +316 -65
  48. data/dist/controllers/input_mask_controller.js +116 -61
  49. data/dist/controllers/intersection_controller.js +147 -103
  50. data/dist/controllers/lazy_frame_controller.js +9 -56
  51. data/dist/controllers/listbox_controller.js +413 -153
  52. data/dist/controllers/local_time_controller.js +54 -64
  53. data/dist/controllers/masonry_controller.js +70 -95
  54. data/dist/controllers/menu_controller.js +254 -167
  55. data/dist/controllers/menubar_controller.js +206 -331
  56. data/dist/controllers/meter_controller.js +159 -71
  57. data/dist/controllers/multi_select_controller.js +797 -388
  58. data/dist/controllers/navigation_menu_controller.js +156 -231
  59. data/dist/controllers/nested_form_controller.js +373 -174
  60. data/dist/controllers/network_status_controller.js +241 -45
  61. data/dist/controllers/number_input_controller.js +387 -227
  62. data/dist/controllers/optimistic_controller.js +28 -75
  63. data/dist/controllers/otp_controller.js +344 -196
  64. data/dist/controllers/overflow_indicator_controller.js +167 -93
  65. data/dist/controllers/overflow_menu_controller.js +408 -277
  66. data/dist/controllers/pagination_controller.js +342 -120
  67. data/dist/controllers/password_reveal_controller.js +424 -99
  68. data/dist/controllers/password_strength_controller.js +99 -153
  69. data/dist/controllers/persist_controller.js +252 -96
  70. data/dist/controllers/pointer_drag_controller.js +263 -164
  71. data/dist/controllers/popover_controller.js +267 -94
  72. data/dist/controllers/portal_controller.js +209 -72
  73. data/dist/controllers/preview_guard_controller.js +243 -112
  74. data/dist/controllers/progress_controller.js +132 -59
  75. data/dist/controllers/radio_group_controller.js +138 -133
  76. data/dist/controllers/range_slider_controller.js +245 -112
  77. data/dist/controllers/rating_controller.js +429 -194
  78. data/dist/controllers/read_more_controller.js +185 -47
  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 +294 -118
  83. data/dist/controllers/roving_controller.js +39 -84
  84. data/dist/controllers/scroll_area_controller.js +428 -221
  85. data/dist/controllers/scroll_restore_controller.js +0 -53
  86. data/dist/controllers/scroll_visibility_controller.js +395 -146
  87. data/dist/controllers/scrollspy_controller.js +181 -236
  88. data/dist/controllers/separator_controller.js +288 -132
  89. data/dist/controllers/sidebar_controller.js +1071 -375
  90. data/dist/controllers/skeleton_controller.js +248 -92
  91. data/dist/controllers/slider_controller.js +211 -96
  92. data/dist/controllers/smart_sticky_header_controller.js +163 -54
  93. data/dist/controllers/sortable_controller.js +13 -110
  94. data/dist/controllers/spinner_controller.js +277 -161
  95. data/dist/controllers/step_indicator_controller.js +107 -53
  96. data/dist/controllers/stepper_controller.js +171 -48
  97. data/dist/controllers/stick_to_bottom_controller.js +310 -116
  98. data/dist/controllers/sticky_observer_controller.js +214 -49
  99. data/dist/controllers/submit_once_controller.js +294 -192
  100. data/dist/controllers/switch_controller.js +107 -43
  101. data/dist/controllers/tabs_controller.js +50 -25
  102. data/dist/controllers/tags_input_controller.js +290 -149
  103. data/dist/controllers/textarea_autosize_controller.js +71 -58
  104. data/dist/controllers/theme_controller.js +148 -80
  105. data/dist/controllers/time_picker_controller.js +162 -81
  106. data/dist/controllers/toast_controller.js +365 -193
  107. data/dist/controllers/toggle_group_controller.js +194 -99
  108. data/dist/controllers/toolbar_controller.js +92 -90
  109. data/dist/controllers/tooltip_controller.js +185 -125
  110. data/dist/controllers/transition_controller.js +123 -115
  111. data/dist/controllers/tree_view_controller.js +248 -290
  112. data/dist/index.js +8649 -8572
  113. data/dist/positioning/index.js +148 -45
  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,113 +314,180 @@ function isScrollable(element) {
164
314
  return /auto|scroll|overlay/.test(style.overflow + style.overflowX + style.overflowY);
165
315
  }
166
316
 
167
- // src/controllers/hover_card_controller.ts
168
- var HoverCardController = class extends Controller {
317
+ var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
318
+ var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
319
+ function stateReasonFor(event) {
320
+ if (!event) return "api";
321
+ if (FOCUS_EVENTS.has(event.type)) return "focus";
322
+ if (POINTER_EVENTS.has(event.type)) return "pointer";
323
+ return "user";
324
+ }
325
+
326
+ var HoverCardController = class _HoverCardController extends Controller {
327
+ #numbers = new NumberValueReader();
169
328
  static targets = ["trigger", "card"];
170
329
  static values = {
171
330
  openDelay: { type: Number, default: 300 },
172
331
  closeDelay: { type: Number, default: 200 },
173
332
  closeOnScroll: { type: Boolean, default: false }
174
333
  };
334
+ static valueConstraints = {
335
+ openDelay: NUMBER_BOUNDS.timer,
336
+ closeDelay: NUMBER_BOUNDS.timer
337
+ };
175
338
  static actions = ["close", "open"];
176
- /** Pending open/close timers, with their IDs reset on every lifecycle boundary. */
339
+ static events = ["close", "open"];
177
340
  #timers = new SafeTimeout();
178
- /** Escape-stack membership while open; the shared resolver dismisses via it. */
179
341
  #escapeLayer = new EscapeLayer();
342
+ #expanded = new AttributeLease("aria-expanded", this.identifier);
180
343
  #pendingOpen = null;
181
344
  #pendingClose = null;
182
- /** Cleanup for the dismiss-on-scroll listeners while open, or `null`. */
183
345
  #stopScrollDismiss = null;
184
- /** Starts closed and discards any stale pending state from a prior connection. */
346
+ #shownCard = null;
347
+ #connected = false;
348
+ #reporting = false;
185
349
  connect() {
186
- this.#cancelOpen();
187
- this.#cancelClose();
188
- this.#conceal();
350
+ this.#cancelTimers();
351
+ this.#conceal("api");
352
+ this.#connected = true;
353
+ this.#reporting = true;
189
354
  }
190
- /** Clears timers, the Escape-stack membership, and scroll listeners so nothing outlives the element. */
191
355
  disconnect() {
192
- this.#cancelOpen();
193
- this.#cancelClose();
194
- this.#escapeLayer.deactivate();
195
- this.#stopScrollDismiss?.();
196
- this.#stopScrollDismiss = null;
356
+ this.#connected = false;
357
+ this.#reporting = false;
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();
197
372
  }
198
- /** Opens the card, after `openDelay` ms (or immediately at 0). Cancels a pending close. */
199
- open() {
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();
379
+ }
380
+ open(event) {
381
+ const reason = stateReasonFor(event);
200
382
  this.#cancelClose();
201
383
  if (this.#isOpen || this.#pendingOpen !== null) return;
202
- if (this.openDelayValue <= 0) {
203
- this.#reveal();
384
+ if (this.#safeOpenDelay <= 0) {
385
+ this.#reveal(reason);
204
386
  return;
205
387
  }
206
388
  this.#pendingOpen = this.#timers.set(() => {
207
389
  this.#pendingOpen = null;
208
- this.#reveal();
209
- }, this.openDelayValue);
210
- }
211
- /**
212
- * Schedules the card to close after `closeDelay`. Cancels a pending open. The
213
- * delayed callback re-checks whether focus has landed inside the controller
214
- * (e.g. a link in the card) and, if so, aborts the close — covering keyboard
215
- * traversal that the pointer-only hoverable bridge cannot.
216
- */
217
- close() {
390
+ this.#reveal(reason);
391
+ }, this.#safeOpenDelay);
392
+ }
393
+ close(event) {
394
+ const reason = stateReasonFor(event);
218
395
  this.#cancelOpen();
219
- if (!this.#isOpen || this.#pendingClose !== null) return;
396
+ if (!this.#closable || this.#pendingClose !== null) return;
220
397
  this.#pendingClose = this.#timers.set(() => {
221
398
  this.#pendingClose = null;
222
399
  if (this.element.contains(document.activeElement)) return;
223
- this.#conceal();
224
- }, this.closeDelayValue);
400
+ this.#conceal(reason);
401
+ }, this.#safeCloseDelay);
225
402
  }
226
- /** Reveals the card, reflects state, and joins the Escape stack / scroll watcher. */
227
- #reveal() {
403
+ #reveal(reason) {
228
404
  if (!this.hasCardTarget) return;
229
- this.cardTarget.hidden = false;
230
- this.cardTarget.setAttribute("data-state", "open");
231
- if (this.hasTriggerTarget) this.triggerTarget.setAttribute("aria-expanded", "true");
232
- this.#escapeLayer.activate(document, { onDismiss: () => this.#dismiss() });
233
- if (this.closeOnScrollValue && !this.#stopScrollDismiss) {
234
- this.#stopScrollDismiss = observeScrollDismiss(this.element, () => this.#dismiss());
405
+ const card = this.cardTarget;
406
+ const was = this.#isOpen;
407
+ card.hidden = false;
408
+ card.setAttribute("data-state", "open");
409
+ if (this.hasTriggerTarget) this.#expanded.write(this.triggerTarget, "true");
410
+ this.#shownCard = card;
411
+ if (!was && this.#reporting) this.dispatch("open", { detail: { reason }, cancelable: false });
412
+ if (this.#shownCard !== card) return;
413
+ this.#escapeLayer.activate(document, { onDismiss: () => this.#dismiss("escape") });
414
+ this.#syncScrollDismiss();
415
+ }
416
+ #conceal(reason) {
417
+ const was = this.#isOpen;
418
+ this.#release();
419
+ this.#reflectClosed();
420
+ if (was && this.#reporting) this.dispatch("close", { detail: { reason }, cancelable: false });
421
+ }
422
+ #reflectClosed() {
423
+ if (this.hasCardTarget) {
424
+ this.cardTarget.hidden = true;
425
+ this.cardTarget.setAttribute("data-state", "closed");
235
426
  }
427
+ if (this.hasTriggerTarget) this.#expanded.write(this.triggerTarget, "false");
236
428
  }
237
- /** Hides the card, reflects state, and leaves the Escape stack / scroll watcher. */
238
- #conceal() {
429
+ #reflectTrigger() {
430
+ if (this.hasTriggerTarget) this.#expanded.write(this.triggerTarget, String(this.#isOpen));
431
+ }
432
+ #release() {
433
+ this.#shownCard = null;
239
434
  this.#escapeLayer.deactivate();
240
435
  this.#stopScrollDismiss?.();
241
436
  this.#stopScrollDismiss = null;
242
- if (this.hasCardTarget) {
243
- this.cardTarget.hidden = true;
244
- this.cardTarget.setAttribute("data-state", "closed");
437
+ }
438
+ #syncScrollDismiss() {
439
+ if (this.closeOnScrollValue) {
440
+ this.#stopScrollDismiss ??= observeScrollDismiss(this.element, () => this.#dismiss("scroll"));
441
+ return;
245
442
  }
246
- if (this.hasTriggerTarget) this.triggerTarget.setAttribute("aria-expanded", "false");
443
+ this.#stopScrollDismiss?.();
444
+ this.#stopScrollDismiss = null;
247
445
  }
248
- /** Cancels pending timers and conceals immediately (shared Escape path). */
249
- #dismiss() {
446
+ #dismiss(reason) {
447
+ this.#cancelTimers();
448
+ this.#conceal(reason);
449
+ }
450
+ #cancelTimers() {
250
451
  this.#cancelOpen();
251
452
  this.#cancelClose();
252
- this.#conceal();
253
453
  }
254
- /** Cancels any pending open timer. */
255
454
  #cancelOpen() {
256
455
  if (this.#pendingOpen !== null) {
257
456
  this.#timers.clear(this.#pendingOpen);
258
457
  this.#pendingOpen = null;
259
458
  }
260
459
  }
261
- /** Cancels any pending close timer. */
262
460
  #cancelClose() {
263
461
  if (this.#pendingClose !== null) {
264
462
  this.#timers.clear(this.#pendingClose);
265
463
  this.#pendingClose = null;
266
464
  }
267
465
  }
268
- /** Whether the card is currently visible. */
269
466
  get #isOpen() {
270
467
  return this.hasCardTarget && !this.cardTarget.hidden;
271
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
+ }
272
490
  };
273
491
 
274
492
  export { HoverCardController };
275
- //# sourceMappingURL=hover_card_controller.js.map
276
493
  //# sourceMappingURL=hover_card_controller.js.map