stimeo-ui 0.16.0 → 0.18.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 (117) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +136 -0
  3. data/README.md +27 -11
  4. data/dist/cable/index.js +384 -449
  5. data/dist/controllers/accordion_controller.js +174 -55
  6. data/dist/controllers/alert_dialog_controller.js +903 -235
  7. data/dist/controllers/announcer_controller.js +238 -205
  8. data/dist/controllers/aspect_ratio_controller.js +0 -10
  9. data/dist/controllers/auto_submit_controller.js +414 -145
  10. data/dist/controllers/avatar_controller.js +185 -107
  11. data/dist/controllers/breadcrumb_controller.js +0 -130
  12. data/dist/controllers/bulk_select_controller.js +282 -58
  13. data/dist/controllers/calendar_controller.js +228 -260
  14. data/dist/controllers/carousel_controller.js +536 -436
  15. data/dist/controllers/character_counter_controller.js +493 -207
  16. data/dist/controllers/checkbox_controller.js +38 -74
  17. data/dist/controllers/clipboard_controller.js +261 -149
  18. data/dist/controllers/collapsible_controller.js +304 -120
  19. data/dist/controllers/color_picker_controller.js +178 -146
  20. data/dist/controllers/combobox_controller.js +522 -156
  21. data/dist/controllers/command_palette_controller.js +1156 -395
  22. data/dist/controllers/conditional_fields_controller.js +300 -171
  23. data/dist/controllers/confirm_controller.js +980 -262
  24. data/dist/controllers/context_menu_controller.js +346 -153
  25. data/dist/controllers/count_up_controller.js +73 -21
  26. data/dist/controllers/countdown_controller.js +281 -158
  27. data/dist/controllers/currency_input_controller.js +575 -225
  28. data/dist/controllers/data_grid_controller.js +248 -214
  29. data/dist/controllers/date_range_picker_controller.js +338 -235
  30. data/dist/controllers/dialog_controller.js +899 -237
  31. data/dist/controllers/direct_upload_controller.js +97 -167
  32. data/dist/controllers/dirty_form_controller.js +72 -69
  33. data/dist/controllers/dismissible_controller.js +196 -24
  34. data/dist/controllers/drawer_controller.js +974 -355
  35. data/dist/controllers/dropdown_controller.js +339 -96
  36. data/dist/controllers/editable_controller.js +315 -88
  37. data/dist/controllers/empty_state_controller.js +79 -58
  38. data/dist/controllers/file_dropzone_controller.js +322 -211
  39. data/dist/controllers/filter_controller.js +77 -42
  40. data/dist/controllers/flash_controller.js +238 -347
  41. data/dist/controllers/focus_controller.js +679 -240
  42. data/dist/controllers/form_field_controller.js +156 -132
  43. data/dist/controllers/form_validation_controller.js +31 -97
  44. data/dist/controllers/frame_loading_controller.js +275 -217
  45. data/dist/controllers/highlight_controller.js +107 -75
  46. data/dist/controllers/hover_card_controller.js +327 -130
  47. data/dist/controllers/idle_controller.js +271 -101
  48. data/dist/controllers/input_mask_controller.js +483 -97
  49. data/dist/controllers/intersection_controller.js +150 -106
  50. data/dist/controllers/lazy_frame_controller.js +24 -68
  51. data/dist/controllers/listbox_controller.js +523 -240
  52. data/dist/controllers/local_time_controller.js +102 -77
  53. data/dist/controllers/masonry_controller.js +80 -96
  54. data/dist/controllers/menu_controller.js +302 -188
  55. data/dist/controllers/menubar_controller.js +232 -345
  56. data/dist/controllers/meter_controller.js +417 -87
  57. data/dist/controllers/multi_select_controller.js +785 -393
  58. data/dist/controllers/navigation_menu_controller.js +190 -250
  59. data/dist/controllers/nested_form_controller.js +396 -192
  60. data/dist/controllers/network_status_controller.js +268 -62
  61. data/dist/controllers/number_input_controller.js +610 -303
  62. data/dist/controllers/optimistic_controller.js +45 -87
  63. data/dist/controllers/otp_controller.js +658 -255
  64. data/dist/controllers/overflow_indicator_controller.js +122 -113
  65. data/dist/controllers/overflow_menu_controller.js +470 -295
  66. data/dist/controllers/pagination_controller.js +332 -123
  67. data/dist/controllers/password_reveal_controller.js +392 -121
  68. data/dist/controllers/password_strength_controller.js +111 -158
  69. data/dist/controllers/persist_controller.js +165 -111
  70. data/dist/controllers/pointer_drag_controller.js +434 -188
  71. data/dist/controllers/popover_controller.js +269 -113
  72. data/dist/controllers/portal_controller.js +215 -76
  73. data/dist/controllers/preview_guard_controller.js +243 -116
  74. data/dist/controllers/progress_controller.js +391 -78
  75. data/dist/controllers/radio_group_controller.js +278 -211
  76. data/dist/controllers/range_slider_controller.js +220 -113
  77. data/dist/controllers/rating_controller.js +458 -211
  78. data/dist/controllers/read_more_controller.js +138 -63
  79. data/dist/controllers/reading_progress_controller.js +166 -137
  80. data/dist/controllers/relative_time_controller.js +211 -112
  81. data/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +103 -38
  82. data/dist/controllers/resizable_controller.js +299 -150
  83. data/dist/controllers/roving_controller.js +49 -88
  84. data/dist/controllers/scroll_area_controller.js +490 -246
  85. data/dist/controllers/scroll_restore_controller.js +14 -55
  86. data/dist/controllers/scroll_visibility_controller.js +432 -149
  87. data/dist/controllers/scrollspy_controller.js +193 -248
  88. data/dist/controllers/separator_controller.js +285 -142
  89. data/dist/controllers/sidebar_controller.js +1069 -404
  90. data/dist/controllers/skeleton_controller.js +259 -101
  91. data/dist/controllers/slider_controller.js +187 -86
  92. data/dist/controllers/smart_sticky_header_controller.js +173 -66
  93. data/dist/controllers/sortable_controller.js +26 -125
  94. data/dist/controllers/spinner_controller.js +288 -173
  95. data/dist/controllers/step_indicator_controller.js +105 -46
  96. data/dist/controllers/stepper_controller.js +120 -73
  97. data/dist/controllers/stick_to_bottom_controller.js +275 -140
  98. data/dist/controllers/sticky_observer_controller.js +220 -49
  99. data/dist/controllers/submit_once_controller.js +376 -227
  100. data/dist/controllers/switch_controller.js +201 -84
  101. data/dist/controllers/tabs_controller.js +41 -30
  102. data/dist/controllers/tags_input_controller.js +293 -173
  103. data/dist/controllers/textarea_autosize_controller.js +108 -74
  104. data/dist/controllers/theme_controller.js +140 -116
  105. data/dist/controllers/time_picker_controller.js +331 -127
  106. data/dist/controllers/toast_controller.js +244 -324
  107. data/dist/controllers/toggle_group_controller.js +252 -191
  108. data/dist/controllers/toolbar_controller.js +219 -120
  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 +283 -369
  112. data/dist/index.js +8573 -10410
  113. data/dist/positioning/index.js +154 -47
  114. data/lib/generators/stimeo/install/install_generator.rb +7 -2
  115. data/lib/generators/stimeo/install/templates/stimeo.js +19 -6
  116. data/lib/stimeo/ui/version.rb +1 -1
  117. metadata +3 -3
@@ -1,68 +1,64 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/dirty_form_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
+ var PREFIX = "__stimeo_raw_v1_";
14
+ sharedRegistry(
15
+ "stimeo-ui.form-data-projection.registry.v1",
16
+ () => /* @__PURE__ */ new WeakMap()
17
+ );
18
+ function decodedName(key) {
19
+ if (!key.startsWith(PREFIX)) return null;
20
+ const separator = PREFIX.length + 32;
21
+ if (key[separator] !== ":") return null;
22
+ try {
23
+ const name = JSON.parse(key.slice(separator + 1));
24
+ return typeof name === "string" ? name : null;
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+ function projectedFieldName(name) {
30
+ return decodedName(name) ?? name;
31
+ }
4
32
 
5
- // src/utils/listener_set.ts
6
33
  var ListenerSet = class {
7
- /** The generation every `add` joins until the next `dispose()`. */
8
34
  #abort = new AbortController();
9
- /**
10
- * Attaches `handler` to the open generation, exactly as the caller spelled it.
11
- *
12
- * The set supplies the signal, so `options` carries everything else the DOM
13
- * accepts — `capture` included, which has to match at release time and no
14
- * longer has a second place to drift from.
15
- */
16
35
  add(target, type, handler, options) {
17
36
  target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
18
37
  }
19
- /**
20
- * Releases every listener of the open generation, synchronously, and opens the
21
- * next one. Idempotent, and safe before anything has been added.
22
- */
23
38
  dispose() {
24
39
  this.#abort.abort();
25
40
  this.#abort = new AbortController();
26
41
  }
27
42
  };
28
43
 
29
- // src/utils/safe_timeout.ts
30
44
  var TimerRegistry = class {
31
- /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
32
45
  ids = /* @__PURE__ */ new Set();
33
- /**
34
- * Cancels a single tracked timer.
35
- *
36
- * No-ops if the id is unknown (already cleared, fired, or never owned by this
37
- * registry), so callers can clear defensively without guarding.
38
- */
39
46
  clear(id) {
40
47
  if (this.ids.delete(id)) {
41
48
  this.cancel(id);
42
49
  }
43
50
  }
44
- /**
45
- * Cancels every tracked timer. Call this from a controller's `disconnect()`
46
- * to guarantee no timer outlives the element.
47
- */
48
51
  clearAll() {
49
52
  for (const id of this.ids) {
50
53
  this.cancel(id);
51
54
  }
52
55
  this.ids.clear();
53
56
  }
54
- /** Number of timers currently tracked (pending). */
55
57
  get size() {
56
58
  return this.ids.size;
57
59
  }
58
60
  };
59
61
  var SafeTimeout = class extends TimerRegistry {
60
- /**
61
- * Schedules `callback` after `delay` ms and returns the timer id.
62
- *
63
- * The id is removed from the registry automatically when the timeout fires,
64
- * so {@link TimerRegistry.size | size} reflects only still-pending timers.
65
- */
66
62
  set(callback, delay) {
67
63
  const id = this.schedule(() => {
68
64
  this.ids.delete(id);
@@ -79,21 +75,45 @@ var SafeTimeout = class extends TimerRegistry {
79
75
  }
80
76
  };
81
77
 
82
- // src/utils/transient_hooks.ts
78
+ var SubmissionOwner = class {
79
+ #synthetic = /* @__PURE__ */ Symbol("synthetic submission");
80
+ #active = null;
81
+ begin(event) {
82
+ this.#active = { source: event.target, token: this.#tokenOf(event) };
83
+ }
84
+ resolve(event) {
85
+ if (this.#active === null || this.#active.source !== event.target || this.#active.token !== this.#tokenOf(event)) {
86
+ return null;
87
+ }
88
+ this.clear();
89
+ return this.#detailOf(event)?.success === true;
90
+ }
91
+ clear() {
92
+ this.#active = null;
93
+ }
94
+ #tokenOf(event) {
95
+ return this.#detailOf(event)?.formSubmission ?? this.#synthetic;
96
+ }
97
+ #detailOf(event) {
98
+ return event.detail;
99
+ }
100
+ };
101
+
83
102
  var TransientHooks = class {
84
103
  #attributes;
85
104
  constructor(options) {
86
105
  this.#attributes = options.attributes;
87
106
  }
88
- /** Drops the declared hooks from `element`; what it carries besides stays. */
89
107
  reset(element) {
90
108
  for (const attribute of this.#attributes) element.removeAttribute(attribute);
91
109
  }
92
110
  };
93
111
 
94
- // src/controllers/dirty_form_controller.ts
95
112
  var TRANSIENT = new TransientHooks({ attributes: ["data-dirty"] });
96
- var visitCoordinators = /* @__PURE__ */ new WeakMap();
113
+ var visitCoordinators = sharedRegistry(
114
+ "stimeo-ui.dirty-form.registry.v1",
115
+ () => /* @__PURE__ */ new WeakMap()
116
+ );
97
117
  var TurboVisitCoordinator = class {
98
118
  #document;
99
119
  #participants = /* @__PURE__ */ new Set();
@@ -127,7 +147,6 @@ var TurboVisitCoordinator = class {
127
147
  }
128
148
  this.#participants.add(participant);
129
149
  }
130
- /** Removes a participant and reports whether the coordinator became empty. */
131
150
  remove(participant) {
132
151
  this.#participants.delete(participant);
133
152
  if (this.#participants.size > 0) return false;
@@ -160,14 +179,13 @@ var DirtyFormController = class extends Controller {
160
179
  static events = ["dirty", "guard"];
161
180
  #baseline = "";
162
181
  #dirty = false;
163
- #beforeunloadBound = false;
164
182
  #timeouts = new SafeTimeout();
165
183
  #listeners = new ListenerSet();
166
- /** Whether the active submission still represents the form's current values. */
184
+ #unloadListeners = new ListenerSet();
185
+ #submission = new SubmissionOwner();
167
186
  #submitting = false;
168
187
  #submitAttempt = null;
169
188
  #submittedBaseline = null;
170
- #activeSubmission = null;
171
189
  #unregisterVisit = null;
172
190
  #onFieldChange = () => {
173
191
  this.#submitting = false;
@@ -187,26 +205,19 @@ var DirtyFormController = class extends Controller {
187
205
  this.#submitAttempt.snapshot = this.#serialize();
188
206
  };
189
207
  #onSubmitStart = (event) => {
190
- const detail = event.detail;
191
- this.#activeSubmission = detail?.formSubmission ?? event;
208
+ if (!this.#ownsSource(event.target)) return;
209
+ this.#submission.begin(event);
192
210
  this.#submittedBaseline = this.#submitAttempt?.snapshot ?? this.#serialize();
193
211
  this.#submitAttempt = null;
194
212
  this.#submitting = this.#serialize() === this.#submittedBaseline;
195
213
  };
196
214
  #onSubmitEnd = (event) => {
197
- const detail = event.detail;
198
- if (detail?.formSubmission !== void 0 && (this.#activeSubmission === null || detail.formSubmission !== this.#activeSubmission)) {
199
- return;
200
- }
215
+ const success = this.#submission.resolve(event);
216
+ if (success === null) return;
201
217
  const submittedBaseline = this.#submittedBaseline;
202
218
  this.#submitting = false;
203
219
  this.#submittedBaseline = null;
204
- this.#activeSubmission = null;
205
- if (detail?.success === false) return;
206
- if (submittedBaseline === null) {
207
- this.markClean();
208
- return;
209
- }
220
+ if (!success || submittedBaseline === null) return;
210
221
  this.#baseline = submittedBaseline;
211
222
  this.#evaluate();
212
223
  };
@@ -216,7 +227,6 @@ var DirtyFormController = class extends Controller {
216
227
  event.returnValue = this.messageValue;
217
228
  };
218
229
  connect() {
219
- this.#resetSubmission();
220
230
  this.#baseline = this.#serialize();
221
231
  this.#setDirty(false);
222
232
  TRANSIENT.reset(this.element);
@@ -226,6 +236,7 @@ var DirtyFormController = class extends Controller {
226
236
  this.#listeners.add(this.element, "formdata", this.#onFormData);
227
237
  this.#listeners.add(this.element, "turbo:submit-start", this.#onSubmitStart);
228
238
  this.#listeners.add(this.element, "turbo:submit-end", this.#onSubmitEnd);
239
+ this.#unregisterVisit?.();
229
240
  this.#unregisterVisit = registerVisitParticipant(this.element.ownerDocument, {
230
241
  form: this.element,
231
242
  evaluate: (event) => this.#evaluateVisit(event)
@@ -238,17 +249,14 @@ var DirtyFormController = class extends Controller {
238
249
  this.#unbindBeforeUnload();
239
250
  this.#resetSubmission();
240
251
  }
241
- /** Re-baselines to the current values and clears the dirty state (e.g. after a save). */
242
252
  markClean() {
243
253
  this.#baseline = this.#serialize();
244
254
  this.#setDirty(false);
245
255
  }
246
- /** Adopts restored values without erasing an already-dirty user revision. */
247
256
  acceptRestore() {
248
257
  if (this.#dirty) return;
249
258
  this.markClean();
250
259
  }
251
- /** Recomputes dirty against the baseline and flips state when it changes. */
252
260
  #evaluate() {
253
261
  this.#setDirty(this.#serialize() !== this.#baseline);
254
262
  }
@@ -264,7 +272,6 @@ var DirtyFormController = class extends Controller {
264
272
  }
265
273
  this.dispatch("dirty", { detail: { dirty } });
266
274
  }
267
- /** Evaluates this form for the document-level Turbo visit coordinator. */
268
275
  #evaluateVisit(event) {
269
276
  if (!this.#dirty || this.#guardSuppressed()) return null;
270
277
  const guard = this.dispatch("guard", { detail: { event }, cancelable: true });
@@ -274,14 +281,10 @@ var DirtyFormController = class extends Controller {
274
281
  return { kind: "confirm", message: this.messageValue };
275
282
  }
276
283
  #bindBeforeUnload() {
277
- if (this.#beforeunloadBound) return;
278
- window.addEventListener("beforeunload", this.#onBeforeUnload);
279
- this.#beforeunloadBound = true;
284
+ this.#unloadListeners.add(window, "beforeunload", this.#onBeforeUnload);
280
285
  }
281
286
  #unbindBeforeUnload() {
282
- if (!this.#beforeunloadBound) return;
283
- window.removeEventListener("beforeunload", this.#onBeforeUnload);
284
- this.#beforeunloadBound = false;
287
+ this.#unloadListeners.dispose();
285
288
  }
286
289
  #guardSuppressed() {
287
290
  return this.#submitting && (this.#submitAttempt === null || !this.#submitAttempt.event.defaultPrevented);
@@ -291,9 +294,11 @@ var DirtyFormController = class extends Controller {
291
294
  this.#submitting = false;
292
295
  this.#submitAttempt = null;
293
296
  this.#submittedBaseline = null;
294
- this.#activeSubmission = null;
297
+ this.#submission.clear();
298
+ }
299
+ #ownsSource(target) {
300
+ return target === this.element || target instanceof Element && target.closest("form") === this.element;
295
301
  }
296
- /** Stable serialization of the form's controls for change detection. */
297
302
  #serialize() {
298
303
  const parts = [];
299
304
  for (const el of Array.from(this.element.elements)) {
@@ -310,15 +315,13 @@ var DirtyFormController = class extends Controller {
310
315
  }
311
316
  return JSON.stringify(parts);
312
317
  }
313
- /** A stable key for a control, or null for elements without value semantics. */
314
318
  #nameOf(el) {
315
319
  if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
316
- return el.name || el.id || "";
320
+ return projectedFieldName(el.name) || el.id || "";
317
321
  }
318
322
  return null;
319
323
  }
320
324
  };
321
325
 
322
326
  export { DirtyFormController };
323
- //# sourceMappingURL=dirty_form_controller.js.map
324
327
  //# sourceMappingURL=dirty_form_controller.js.map
@@ -1,8 +1,158 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/dismissible_controller.ts
3
+ function setAttributeIfChanged(element, name, value, beforeWrite) {
4
+ if (element.getAttribute(name) === value) return false;
5
+ if (value === null) element.removeAttribute(name);
6
+ else element.setAttribute(name, value);
7
+ return true;
8
+ }
9
+
10
+ function sharedRegistry(key, create) {
11
+ const symbol = Symbol.for(key);
12
+ const scope = globalThis;
13
+ const existing = scope[symbol];
14
+ if (existing !== void 0) return existing;
15
+ const registry2 = create();
16
+ Object.defineProperty(globalThis, symbol, { value: registry2 });
17
+ return registry2;
18
+ }
19
+
20
+ var registry = sharedRegistry(
21
+ "stimeo-ui.attribute-lease.registry.v1",
22
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
23
+ );
24
+ function isAttributeValue(value) {
25
+ return value === null || typeof value === "string";
26
+ }
27
+ function readLeaseRecord(element, record) {
28
+ try {
29
+ const parsed = JSON.parse(element.getAttribute(record) ?? "null");
30
+ if (!Array.isArray(parsed)) return void 0;
31
+ const [original, written, ...rest] = parsed;
32
+ const linked = rest[0] !== false;
33
+ const beneath = linked ? rest : rest.slice(1);
34
+ if (!beneath.every((under) => typeof under === "string")) return void 0;
35
+ return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
36
+ } catch {
37
+ return void 0;
38
+ }
39
+ }
40
+ function attributeStack(element, attribute) {
41
+ const byAttribute = registry.stacks.get(element) ?? /* @__PURE__ */ new Map();
42
+ registry.stacks.set(element, byAttribute);
43
+ const known = byAttribute.get(attribute);
44
+ if (known) return known;
45
+ const found = [];
46
+ for (const record of registry.names.get(attribute) ?? []) {
47
+ const read = readLeaseRecord(element, record);
48
+ if (read) found.push({ ...read, record });
49
+ }
50
+ const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
51
+ const stack = found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
52
+ lease: null,
53
+ record,
54
+ written,
55
+ original,
56
+ linked: linked && index > 0
57
+ }));
58
+ byAttribute.set(attribute, stack);
59
+ return stack;
60
+ }
61
+ function findHold(stack, lease, record) {
62
+ return stack.find((held) => held.lease === lease) ?? stack.find((held) => held.lease === null && held.record === record);
63
+ }
64
+ var AttributeLease = class {
65
+ #attribute;
66
+ #record;
67
+ #base;
68
+ #preserveAuthor;
69
+ #held = /* @__PURE__ */ new Set();
70
+ constructor(attribute, owner, options = {}) {
71
+ this.#attribute = attribute;
72
+ this.#record = `data-${owner}-${attribute}-lease`;
73
+ this.#base = options.base === true;
74
+ this.#preserveAuthor = options.preserveAuthor === true;
75
+ const names = registry.names;
76
+ names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
77
+ }
78
+ write(element, value) {
79
+ const stack = attributeStack(element, this.#attribute);
80
+ const hold = this.#take(stack) ?? this.#acquire(element, stack);
81
+ this.#held.add(element);
82
+ const shows = !this.#covered(element, stack, hold);
83
+ hold.written = value;
84
+ this.#mark(element, stack);
85
+ if (shows) this.#reflect(element, value);
86
+ }
87
+ return(element) {
88
+ const held = this.#held.delete(element);
89
+ const known = registry.stacks.get(element)?.has(this.#attribute);
90
+ const stack = held || known || element.hasAttribute(this.#record) ? attributeStack(element, this.#attribute) : [];
91
+ const hold = this.#take(stack);
92
+ if (!hold) return false;
93
+ const carries = element.getAttribute(this.#attribute) === hold.written;
94
+ const owns = carries && !this.#covered(element, stack, hold);
95
+ const index = stack.indexOf(hold);
96
+ stack.splice(index, 1);
97
+ const above = stack[index];
98
+ if (above) above.linked &&= hold.linked;
99
+ this.#mark(element, stack);
100
+ if (!owns) return true;
101
+ const below = hold.linked ? stack[index - 1] : void 0;
102
+ this.#reflect(element, below ? below.written : hold.original);
103
+ return true;
104
+ }
105
+ returnAll() {
106
+ for (const element of Array.from(this.#held)) this.return(element);
107
+ }
108
+ #take(stack) {
109
+ const hold = findHold(stack, this, this.#record);
110
+ if (hold) hold.lease = this;
111
+ return hold;
112
+ }
113
+ #acquire(element, stack) {
114
+ const current = element.getAttribute(this.#attribute);
115
+ const top = stack.at(-1);
116
+ const bottom = stack[0];
117
+ const over = top?.written === current ? top : void 0;
118
+ const from = this.#base ? this.#preserveAuthor && !stack.some((held) => held.written === current) ? void 0 : bottom : over;
119
+ const hold = {
120
+ lease: this,
121
+ record: this.#record,
122
+ written: current,
123
+ original: from ? from.original : current,
124
+ linked: !this.#base && over !== void 0
125
+ };
126
+ if (this.#base && bottom) {
127
+ bottom.linked = true;
128
+ stack.unshift(hold);
129
+ } else stack.push(hold);
130
+ return hold;
131
+ }
132
+ #covered(element, stack, hold) {
133
+ const current = element.getAttribute(this.#attribute);
134
+ return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
135
+ }
136
+ #mark(element, stack) {
137
+ const records = /* @__PURE__ */ new Map([[this.#record, null]]);
138
+ stack.forEach((hold, index) => {
139
+ const names = stack.slice(0, index).map((under) => under.record);
140
+ const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
141
+ const marker = hold.linked || beneath.length === 0 ? [] : [false];
142
+ records.set(
143
+ hold.record,
144
+ hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
145
+ );
146
+ });
147
+ for (const [name, recorded] of records) {
148
+ setAttributeIfChanged(element, name, recorded);
149
+ }
150
+ }
151
+ #reflect(element, value) {
152
+ setAttributeIfChanged(element, this.#attribute, value);
153
+ }
154
+ };
4
155
 
5
- // src/utils/focus_candidate.ts
6
156
  function inheritsFieldsetDisabled(control) {
7
157
  let fieldset = control.closest("fieldset[disabled]");
8
158
  while (fieldset) {
@@ -12,8 +162,19 @@ function inheritsFieldsetDisabled(control) {
12
162
  }
13
163
  return false;
14
164
  }
165
+ function unslotted(element) {
166
+ return element.assignedSlot === null && element.parentElement?.shadowRoot != null;
167
+ }
168
+ function flatTreeParent(element) {
169
+ if (unslotted(element)) return null;
170
+ const parent = element.assignedSlot ?? element.parentNode;
171
+ if (parent instanceof ShadowRoot) return parent.host;
172
+ return parent instanceof Element ? parent : null;
173
+ }
15
174
  function canTakeFocus(element) {
16
- if (element.closest("[hidden], [inert]")) return false;
175
+ for (let node = element; node; node = flatTreeParent(node)) {
176
+ if (unslotted(node) || node.matches("[hidden], [inert]")) return false;
177
+ }
17
178
  if (element instanceof HTMLInputElement && element.type === "hidden") return false;
18
179
  if (!("disabled" in element)) return true;
19
180
  if (element.disabled) return false;
@@ -76,7 +237,17 @@ function tabStopsWithin(root) {
76
237
  );
77
238
  }
78
239
 
79
- // src/controllers/dismissible_controller.ts
240
+ var ListenerSet = class {
241
+ #abort = new AbortController();
242
+ add(target, type, handler, options) {
243
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
244
+ }
245
+ dispose() {
246
+ this.#abort.abort();
247
+ this.#abort = new AbortController();
248
+ }
249
+ };
250
+
80
251
  var DismissibleController = class extends Controller {
81
252
  static targets = ["root", "fallback"];
82
253
  static values = {
@@ -85,25 +256,33 @@ var DismissibleController = class extends Controller {
85
256
  };
86
257
  static actions = ["dismiss"];
87
258
  static events = ["dismiss"];
259
+ #openDefault = new AttributeLease("data-state", this.identifier);
260
+ #connected = false;
261
+ #escapeListeners = new ListenerSet();
88
262
  connect() {
89
- const root = this.#root;
90
- if (!root.hasAttribute("data-state")) {
91
- root.setAttribute("data-state", "open");
92
- }
263
+ this.#establishOpen(this.#root);
93
264
  this.#syncEscapeListener();
265
+ this.#connected = true;
94
266
  }
95
267
  disconnect() {
96
- this.element.removeEventListener("keydown", this.#onKeydown);
268
+ this.#connected = false;
269
+ this.#escapeListeners.dispose();
270
+ }
271
+ rootTargetConnected(root) {
272
+ if (!this.#connected) return;
273
+ if (!this.rootTargets.includes(this.element)) this.#openDefault.return(this.element);
274
+ this.#establishOpen(root);
275
+ }
276
+ rootTargetDisconnected(root) {
277
+ if (!this.rootTargets.includes(root)) this.#openDefault.return(root);
278
+ if (this.#connected && this.hasRootTarget) this.#establishOpen(this.rootTarget);
97
279
  }
98
- /** Keeps the Escape listener aligned with a live `closeOnEscape` Value. */
99
280
  closeOnEscapeValueChanged() {
100
281
  this.#syncEscapeListener();
101
282
  }
102
- /** Dismisses the element. Bound via `data-action` (click on the close button). */
103
283
  dismiss() {
104
284
  this.#performDismiss();
105
285
  }
106
- /** Dismisses on Escape when `closeOnEscape` is set and focus is inside. */
107
286
  #onKeydown = (event) => {
108
287
  if (event.key !== "Escape" || event.defaultPrevented || event.isComposing) return;
109
288
  if (!this.closeOnEscapeValue) return;
@@ -112,18 +291,18 @@ var DismissibleController = class extends Controller {
112
291
  event.preventDefault();
113
292
  this.#performDismiss();
114
293
  };
115
- /** Adds or removes the one stable listener reference without duplicating it. */
116
294
  #syncEscapeListener() {
117
- this.element.removeEventListener("keydown", this.#onKeydown);
295
+ this.#escapeListeners.dispose();
118
296
  if (this.closeOnEscapeValue) {
119
- this.element.addEventListener("keydown", this.#onKeydown);
297
+ this.#escapeListeners.add(this.element, "keydown", this.#onKeydown);
120
298
  }
121
299
  }
122
- /** The element to dismiss: the explicit `root` target, or the host element. */
300
+ #establishOpen(root) {
301
+ if (!root.hasAttribute("data-state")) this.#openDefault.write(root, "open");
302
+ }
123
303
  get #root() {
124
304
  return this.hasRootTarget ? this.rootTarget : this.element;
125
305
  }
126
- /** @stimeoRuntimeOnly `mode` decides how this one dismissal takes the element away. */
127
306
  #performDismiss() {
128
307
  const root = this.#root;
129
308
  const mode = this.modeValue === "hide" ? "hide" : "remove";
@@ -136,10 +315,6 @@ var DismissibleController = class extends Controller {
136
315
  root.remove();
137
316
  }
138
317
  }
139
- /**
140
- * Moves focus out of `root` *before* it is removed, but only when focus is
141
- * actually inside it — otherwise the user's place elsewhere is left undisturbed.
142
- */
143
318
  #retreatFocus(root) {
144
319
  const active = document.activeElement;
145
320
  if (!(active instanceof HTMLElement) || !root.contains(active)) return;
@@ -149,7 +324,6 @@ var DismissibleController = class extends Controller {
149
324
  }
150
325
  document.body.focus();
151
326
  }
152
- /** Yields available focus fallbacks in precedence order, stopping after focus succeeds. */
153
327
  *#focusFallbacks(root) {
154
328
  const explicitFallback = this.hasFallbackTarget ? this.fallbackTarget : null;
155
329
  if (explicitFallback && !root.contains(explicitFallback) && this.#isExplicitFocusAvailable(explicitFallback)) {
@@ -171,7 +345,6 @@ var DismissibleController = class extends Controller {
171
345
  yield element;
172
346
  }
173
347
  }
174
- /** Whether an explicit fallback can receive programmatic focus right now. */
175
348
  #isExplicitFocusAvailable(element) {
176
349
  const programmaticallyFocusable = element.hasAttribute("tabindex") || isTabStop(element);
177
350
  return programmaticallyFocusable && canTakeFocus(element) && isRenderedForFocus(element);
@@ -179,5 +352,4 @@ var DismissibleController = class extends Controller {
179
352
  };
180
353
 
181
354
  export { DismissibleController };
182
- //# sourceMappingURL=dismissible_controller.js.map
183
355
  //# sourceMappingURL=dismissible_controller.js.map