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,8 +1,5 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/skeleton_controller.ts
4
-
5
- // src/utils/announce.ts
6
3
  function announce(message, options = {}) {
7
4
  const text = message.trim();
8
5
  if (text.length === 0) return;
@@ -19,88 +16,203 @@ function fillTemplate(template, values) {
19
16
  });
20
17
  }
21
18
 
22
- // src/utils/detach_gate.ts
19
+ function sharedRegistry(key, create) {
20
+ const symbol = Symbol.for(key);
21
+ const scope = globalThis;
22
+ const existing = scope[symbol];
23
+ if (existing !== void 0) return existing;
24
+ const registry = create();
25
+ Object.defineProperty(globalThis, symbol, { value: registry });
26
+ return registry;
27
+ }
28
+
29
+ function isAttributeValue(value) {
30
+ return value === null || typeof value === "string";
31
+ }
32
+ function readLeaseRecord(element, record) {
33
+ try {
34
+ const parsed = JSON.parse(element.getAttribute(record) ?? "null");
35
+ if (!Array.isArray(parsed)) return void 0;
36
+ const [original, written, ...rest] = parsed;
37
+ const linked = rest[0] !== false;
38
+ const beneath = linked ? rest : rest.slice(1);
39
+ if (!beneath.every((under) => typeof under === "string")) return void 0;
40
+ return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
41
+ } catch {
42
+ return void 0;
43
+ }
44
+ }
45
+ var AttributeLease = class _AttributeLease {
46
+ static #registry = sharedRegistry(
47
+ "stimeo-ui.attribute-lease.registry.v1",
48
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
49
+ );
50
+ #attribute;
51
+ #record;
52
+ #base;
53
+ #held = /* @__PURE__ */ new Set();
54
+ constructor(attribute, owner, options = {}) {
55
+ this.#attribute = attribute;
56
+ this.#record = `data-${owner}-${attribute}-lease`;
57
+ this.#base = options.base === true;
58
+ const names = _AttributeLease.#registry.names;
59
+ names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
60
+ }
61
+ write(element, value) {
62
+ const stack = this.#stack(element);
63
+ const hold = this.#take(stack) ?? this.#acquire(element, stack);
64
+ this.#held.add(element);
65
+ const shows = !this.#covered(element, stack, hold);
66
+ hold.written = value;
67
+ this.#mark(element, stack);
68
+ if (shows) this.#reflect(element, value);
69
+ }
70
+ return(element) {
71
+ const held = this.#held.delete(element);
72
+ const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
73
+ const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
74
+ const hold = this.#take(stack);
75
+ if (!hold) return false;
76
+ const carries = element.getAttribute(this.#attribute) === hold.written;
77
+ const owns = carries && !this.#covered(element, stack, hold);
78
+ const index = stack.indexOf(hold);
79
+ stack.splice(index, 1);
80
+ const above = stack[index];
81
+ if (above) above.linked &&= hold.linked;
82
+ this.#mark(element, stack);
83
+ if (!owns) return true;
84
+ const below = hold.linked ? stack[index - 1] : void 0;
85
+ this.#reflect(element, below ? below.written : hold.original);
86
+ return true;
87
+ }
88
+ returnAll() {
89
+ for (const element of Array.from(this.#held)) this.return(element);
90
+ }
91
+ #stack(element) {
92
+ const stacks = _AttributeLease.#registry.stacks;
93
+ const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
94
+ stacks.set(element, byAttribute);
95
+ const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
96
+ byAttribute.set(this.#attribute, stack);
97
+ return stack;
98
+ }
99
+ #seed(element) {
100
+ const found = [];
101
+ for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
102
+ const read = readLeaseRecord(element, record);
103
+ if (read) found.push({ ...read, record });
104
+ }
105
+ const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
106
+ return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
107
+ lease: null,
108
+ record,
109
+ written,
110
+ original,
111
+ linked: linked && index > 0
112
+ }));
113
+ }
114
+ #take(stack) {
115
+ const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
116
+ if (hold) hold.lease = this;
117
+ return hold;
118
+ }
119
+ #acquire(element, stack) {
120
+ const current = element.getAttribute(this.#attribute);
121
+ const top = stack.at(-1);
122
+ const bottom = stack[0];
123
+ const over = top?.written === current ? top : void 0;
124
+ const from = this.#base ? bottom : over;
125
+ const hold = {
126
+ lease: this,
127
+ record: this.#record,
128
+ written: current,
129
+ original: from ? from.original : current,
130
+ linked: !this.#base && over !== void 0
131
+ };
132
+ if (this.#base && bottom) {
133
+ bottom.linked = true;
134
+ stack.unshift(hold);
135
+ } else stack.push(hold);
136
+ return hold;
137
+ }
138
+ #covered(element, stack, hold) {
139
+ const current = element.getAttribute(this.#attribute);
140
+ return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
141
+ }
142
+ #mark(element, stack) {
143
+ const records = /* @__PURE__ */ new Map([[this.#record, null]]);
144
+ stack.forEach((hold, index) => {
145
+ const names = stack.slice(0, index).map((under) => under.record);
146
+ const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
147
+ const marker = hold.linked || beneath.length === 0 ? [] : [false];
148
+ records.set(
149
+ hold.record,
150
+ hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
151
+ );
152
+ });
153
+ for (const [name, recorded] of records) {
154
+ if (element.getAttribute(name) === recorded) continue;
155
+ if (recorded === null) element.removeAttribute(name);
156
+ else element.setAttribute(name, recorded);
157
+ }
158
+ }
159
+ #reflect(element, value) {
160
+ if (element.getAttribute(this.#attribute) === value) return;
161
+ if (value === null) element.removeAttribute(this.#attribute);
162
+ else element.setAttribute(this.#attribute, value);
163
+ }
164
+ };
165
+
23
166
  var DetachGate = class _DetachGate {
24
- /** Set while a probe is queued, waiting for a reconnect to cancel it. */
25
167
  #pending = false;
26
- /**
27
- * True while a probe is queued — the last disconnect was ambiguous and no
28
- * reconnect has cancelled it yet. Read it from `connect()` to tell the
29
- * reconnect half of an in-page move from a first connect: a controller whose
30
- * initialisation restarts a measurement (a min-duration floor, an elapsed
31
- * counter) must skip it for the move, where nothing actually restarted.
32
- */
33
168
  get pending() {
34
169
  return this.#pending;
35
170
  }
36
- /**
37
- * True when the disconnect is definitely a real detach — the element left
38
- * the document, or `data-controller` no longer lists the identifier. False
39
- * means ambiguous (in-page move or observed-root exit), NOT "alive".
40
- */
41
171
  static isDetached(host) {
42
- if (!host.element.isConnected) return true;
43
- const tokens = (host.element.getAttribute("data-controller") ?? "").split(/\s+/);
44
- return !tokens.includes(host.identifier);
45
- }
46
- /**
47
- * Call from `disconnect()`: runs `teardown` synchronously on a definite
48
- * detach (fast path), otherwise defers it one microtask — a reconnect
49
- * ({@link cancel} from `connect()`) keeps the state, no reconnect runs it.
50
- * One microtask is the whole probe window: Stimulus reconnects a moved
51
- * element within the same mutation batch, before the checkpoint drains.
52
- */
172
+ return !host.element.isConnected || !_DetachGate.#listed(host);
173
+ }
53
174
  disconnected(host, teardown) {
54
- if (_DetachGate.isDetached(host)) {
175
+ const carried = _DetachGate.#carried(host);
176
+ if (!carried && _DetachGate.isDetached(host)) {
55
177
  this.#pending = false;
56
178
  teardown();
57
179
  return;
58
180
  }
59
181
  this.#pending = true;
60
- queueMicrotask(() => {
182
+ const probe = () => {
61
183
  if (!this.#pending) return;
62
184
  this.#pending = false;
63
185
  teardown();
64
- });
186
+ };
187
+ if (carried) setTimeout(probe, 0);
188
+ else queueMicrotask(probe);
189
+ }
190
+ static #carried(host) {
191
+ const element = host.element;
192
+ return !element.isConnected && element.closest("[data-turbo-permanent]") !== null && _DetachGate.#listed(host);
193
+ }
194
+ static #listed(host) {
195
+ const tokens = (host.element.getAttribute("data-controller") ?? "").split(/\s+/);
196
+ return tokens.includes(host.identifier);
65
197
  }
66
- /**
67
- * Disarms a pending probe. Call from `connect()` (the reconnect that proves
68
- * an in-page move) and from the head of any teardown path not routed through
69
- * {@link disconnected} (disabled-toggle, Escape), so an orphaned probe can
70
- * never run the teardown a second time.
71
- */
72
198
  cancel() {
73
199
  this.#pending = false;
74
200
  }
75
201
  };
76
202
 
77
- // src/utils/min_duration_floor.ts
78
203
  var MinDurationFloor = class {
79
204
  #timers;
80
- /** Pending finish timer id, or `null` when nothing is held back. */
81
205
  #timerId = null;
82
- /** Epoch ms the floor is measured from. */
83
206
  #since = 0;
84
- /** @param timers - the controller's registry; the floor schedules into it. */
85
207
  constructor(timers) {
86
208
  this.#timers = timers;
87
209
  }
88
- /** Starts the floor: call when the state being held becomes visible. */
89
210
  begin() {
90
211
  this.#since = Date.now();
91
212
  }
92
- /** True while a finish is held back waiting for the floor to elapse. */
93
213
  get pending() {
94
214
  return this.#timerId !== null;
95
215
  }
96
- /**
97
- * Runs `finish` once the floor has elapsed, immediately when it already has.
98
- *
99
- * A held-back finish is **replaced**, never stacked: only the most recently
100
- * queued id is cancellable, so a second timer would outlive every cancel and
101
- * end a state that has since restarted. Controllers that want the first signal
102
- * to win guard on {@link pending} before calling.
103
- */
104
216
  schedule(minDuration, finish) {
105
217
  this.cancel();
106
218
  const remaining = minDuration - (Date.now() - this.#since);
@@ -113,7 +225,6 @@ var MinDurationFloor = class {
113
225
  finish();
114
226
  }
115
227
  }
116
- /** Drops a held-back finish. Safe when none is queued, or after a bulk clear. */
117
228
  cancel() {
118
229
  if (this.#timerId !== null) {
119
230
  this.#timers.clear(this.#timerId);
@@ -122,43 +233,70 @@ var MinDurationFloor = class {
122
233
  }
123
234
  };
124
235
 
125
- // src/utils/safe_timeout.ts
236
+ var MAX_TIMER_DELAY_MS = 2147483647;
237
+
238
+ var NUMBER_BOUNDS = {
239
+ timer: { finite: true, min: 0, max: MAX_TIMER_DELAY_MS }};
240
+ function matchesNumberBounds(value, bounds) {
241
+ if (!Number.isFinite(value)) {
242
+ const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
243
+ if (direction === null) return false;
244
+ if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
245
+ }
246
+ if (bounds.min !== void 0 && value < bounds.min) return false;
247
+ if (bounds.max !== void 0 && value > bounds.max) return false;
248
+ if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
249
+ if (bounds.integer && !Number.isInteger(value)) return false;
250
+ if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
251
+ return true;
252
+ }
253
+
254
+ function readNumber(raw, fallback, bounds) {
255
+ return matchesNumberBounds(raw, bounds) ? raw : fallback;
256
+ }
257
+
258
+ var NumberValueReader = class {
259
+ #lastRejected = /* @__PURE__ */ new Map();
260
+ read(owner, name, raw, fallback, bounds) {
261
+ const resolved = readNumber(raw, fallback, bounds);
262
+ if (matchesNumberBounds(raw, bounds)) {
263
+ this.#lastRejected.delete(name);
264
+ return resolved;
265
+ }
266
+ const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
267
+ const literal = owner.element.getAttribute(attribute);
268
+ if (literal === null) {
269
+ this.#lastRejected.delete(name);
270
+ return resolved;
271
+ }
272
+ if (this.#lastRejected.get(name) !== literal) {
273
+ this.#lastRejected.set(name, literal);
274
+ console.warn(
275
+ `Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
276
+ );
277
+ }
278
+ return resolved;
279
+ }
280
+ };
281
+
126
282
  var TimerRegistry = class {
127
- /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
128
283
  ids = /* @__PURE__ */ new Set();
129
- /**
130
- * Cancels a single tracked timer.
131
- *
132
- * No-ops if the id is unknown (already cleared, fired, or never owned by this
133
- * registry), so callers can clear defensively without guarding.
134
- */
135
284
  clear(id) {
136
285
  if (this.ids.delete(id)) {
137
286
  this.cancel(id);
138
287
  }
139
288
  }
140
- /**
141
- * Cancels every tracked timer. Call this from a controller's `disconnect()`
142
- * to guarantee no timer outlives the element.
143
- */
144
289
  clearAll() {
145
290
  for (const id of this.ids) {
146
291
  this.cancel(id);
147
292
  }
148
293
  this.ids.clear();
149
294
  }
150
- /** Number of timers currently tracked (pending). */
151
295
  get size() {
152
296
  return this.ids.size;
153
297
  }
154
298
  };
155
299
  var SafeTimeout = class extends TimerRegistry {
156
- /**
157
- * Schedules `callback` after `delay` ms and returns the timer id.
158
- *
159
- * The id is removed from the registry automatically when the timeout fires,
160
- * so {@link TimerRegistry.size | size} reflects only still-pending timers.
161
- */
162
300
  set(callback, delay) {
163
301
  const id = this.schedule(() => {
164
302
  this.ids.delete(id);
@@ -175,19 +313,25 @@ var SafeTimeout = class extends TimerRegistry {
175
313
  }
176
314
  };
177
315
 
178
- // src/controllers/skeleton_controller.ts
179
- var SkeletonController = class extends Controller {
316
+ var SkeletonController = class _SkeletonController extends Controller {
317
+ #numbers = new NumberValueReader();
180
318
  static targets = ["placeholder", "content"];
181
319
  static values = {
182
320
  announceReadyText: { type: String, default: "" },
183
321
  minDuration: { type: Number, default: 0 }
184
322
  };
323
+ static valueConstraints = {
324
+ minDuration: NUMBER_BOUNDS.timer
325
+ };
185
326
  static actions = ["ready", "reset"];
186
327
  static events = ["ready"];
187
328
  #timers = new SafeTimeout();
188
329
  #floor = new MinDurationFloor(this.#timers);
189
330
  #gate = new DetachGate();
331
+ #hidden = new AttributeLease("hidden", this.identifier);
332
+ #connected = false;
190
333
  connect() {
334
+ this.#connected = true;
191
335
  const moved = this.#gate.pending;
192
336
  this.#gate.cancel();
193
337
  if (!moved && this.#state !== "ready") {
@@ -195,52 +339,64 @@ var SkeletonController = class extends Controller {
195
339
  }
196
340
  }
197
341
  disconnect() {
342
+ this.#connected = false;
198
343
  this.#gate.disconnected(this, () => this.#teardown());
199
344
  }
200
- /** Swaps to the real content. Honors `minDuration` to prevent a flash. */
345
+ placeholderTargetConnected() {
346
+ if (this.#connected) this.#reflect(this.#state === "ready");
347
+ }
348
+ placeholderTargetDisconnected(placeholder) {
349
+ if (!this.placeholderTargets.includes(placeholder)) this.#hidden.return(placeholder);
350
+ if (this.#connected) this.#reflect(this.#state === "ready");
351
+ }
352
+ contentTargetConnected() {
353
+ if (this.#connected) this.#reflect(this.#state === "ready");
354
+ }
355
+ contentTargetDisconnected(content) {
356
+ if (!this.contentTargets.includes(content)) this.#hidden.return(content);
357
+ if (this.#connected) this.#reflect(this.#state === "ready");
358
+ }
201
359
  ready() {
202
360
  if (this.#state === "ready" || this.#floor.pending) return;
203
- this.#floor.schedule(this.minDurationValue, () => this.#reveal());
361
+ this.#floor.schedule(this.#safeMinDuration, () => this.#reveal());
204
362
  }
205
- /** Returns to the loading state (e.g. a Turbo Stream re-fetch). */
206
363
  reset() {
207
364
  this.#floor.cancel();
208
365
  this.#enterLoading();
209
366
  }
210
- /** Shows the placeholder, hides content, and marks the region busy. */
211
367
  #enterLoading() {
212
368
  this.#floor.begin();
213
- if (this.hasPlaceholderTarget) this.placeholderTarget.hidden = false;
214
- if (this.hasContentTarget) this.contentTarget.hidden = true;
369
+ this.#reflect(false);
215
370
  this.element.setAttribute("aria-busy", "true");
216
371
  this.element.setAttribute("data-state", "loading");
217
372
  }
218
- /** Hides the placeholder, shows content, and clears the busy state. */
219
373
  #reveal() {
220
- if (this.hasPlaceholderTarget) this.placeholderTarget.hidden = true;
221
- if (this.hasContentTarget) this.contentTarget.hidden = false;
374
+ this.#reflect(true);
222
375
  this.element.setAttribute("aria-busy", "false");
223
376
  this.element.setAttribute("data-state", "ready");
224
377
  this.dispatch("ready", { detail: {} });
225
378
  announce(fillTemplate(this.announceReadyTextValue, {}));
226
379
  }
227
- /**
228
- * Drops the held reveal on a real detach. The markup keeps whatever it last
229
- * held: an element on its way out of the document has no reader left, and one
230
- * whose `data-controller` dropped the identifier no longer resolves its own
231
- * targets, so the rollback could only ever be partial.
232
- */
380
+ #reflect(ready) {
381
+ if (this.hasPlaceholderTarget) this.#hidden.write(this.placeholderTarget, ready ? "" : null);
382
+ if (this.hasContentTarget) this.#hidden.write(this.contentTarget, ready ? null : "");
383
+ }
233
384
  #teardown() {
234
- this.#gate.cancel();
235
- this.#timers.clearAll();
236
385
  this.#floor.cancel();
237
386
  }
238
- /** Current lifecycle phase as reflected on `data-state`. */
239
387
  get #state() {
240
388
  return this.element.getAttribute("data-state") ?? "loading";
241
389
  }
390
+ get #safeMinDuration() {
391
+ return this.#numbers.read(
392
+ this,
393
+ "minDuration",
394
+ this.minDurationValue,
395
+ _SkeletonController.values.minDuration.default,
396
+ _SkeletonController.valueConstraints.minDuration
397
+ );
398
+ }
242
399
  };
243
400
 
244
401
  export { SkeletonController };
245
- //# sourceMappingURL=skeleton_controller.js.map
246
402
  //# sourceMappingURL=skeleton_controller.js.map