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,44 +1,82 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/idle_controller.ts
3
+ var ListenerSet = class {
4
+ #abort = new AbortController();
5
+ add(target, type, handler, options) {
6
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
7
+ }
8
+ dispose() {
9
+ this.#abort.abort();
10
+ this.#abort = new AbortController();
11
+ }
12
+ };
13
+
14
+ var MAX_TIMER_DELAY_MS = 2147483647;
15
+
16
+ var NUMBER_BOUNDS = {
17
+ nonNegative: { finite: true, min: 0 },
18
+ positiveTimer: { finite: true, exclusiveMin: 0, max: MAX_TIMER_DELAY_MS }
19
+ };
20
+ function matchesNumberBounds(value, bounds) {
21
+ if (!Number.isFinite(value)) {
22
+ const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
23
+ if (direction === null) return false;
24
+ if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
25
+ }
26
+ if (bounds.min !== void 0 && value < bounds.min) return false;
27
+ if (bounds.max !== void 0 && value > bounds.max) return false;
28
+ if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
29
+ if (bounds.integer && !Number.isInteger(value)) return false;
30
+ if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
31
+ return true;
32
+ }
33
+
34
+ function readNumber(raw, fallback, bounds) {
35
+ return matchesNumberBounds(raw, bounds) ? raw : fallback;
36
+ }
37
+
38
+ var NumberValueReader = class {
39
+ #lastRejected = /* @__PURE__ */ new Map();
40
+ read(owner, name, raw, fallback, bounds) {
41
+ const resolved = readNumber(raw, fallback, bounds);
42
+ if (matchesNumberBounds(raw, bounds)) {
43
+ this.#lastRejected.delete(name);
44
+ return resolved;
45
+ }
46
+ const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
47
+ const literal = owner.element.getAttribute(attribute);
48
+ if (literal === null) {
49
+ this.#lastRejected.delete(name);
50
+ return resolved;
51
+ }
52
+ if (this.#lastRejected.get(name) !== literal) {
53
+ this.#lastRejected.set(name, literal);
54
+ console.warn(
55
+ `Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
56
+ );
57
+ }
58
+ return resolved;
59
+ }
60
+ };
4
61
 
5
- // src/utils/safe_timeout.ts
6
62
  var TimerRegistry = class {
7
- /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
8
63
  ids = /* @__PURE__ */ new Set();
9
- /**
10
- * Cancels a single tracked timer.
11
- *
12
- * No-ops if the id is unknown (already cleared, fired, or never owned by this
13
- * registry), so callers can clear defensively without guarding.
14
- */
15
64
  clear(id) {
16
65
  if (this.ids.delete(id)) {
17
66
  this.cancel(id);
18
67
  }
19
68
  }
20
- /**
21
- * Cancels every tracked timer. Call this from a controller's `disconnect()`
22
- * to guarantee no timer outlives the element.
23
- */
24
69
  clearAll() {
25
70
  for (const id of this.ids) {
26
71
  this.cancel(id);
27
72
  }
28
73
  this.ids.clear();
29
74
  }
30
- /** Number of timers currently tracked (pending). */
31
75
  get size() {
32
76
  return this.ids.size;
33
77
  }
34
78
  };
35
79
  var SafeTimeout = class extends TimerRegistry {
36
- /**
37
- * Schedules `callback` after `delay` ms and returns the timer id.
38
- *
39
- * The id is removed from the registry automatically when the timeout fires,
40
- * so {@link TimerRegistry.size | size} reflects only still-pending timers.
41
- */
42
80
  set(callback, delay) {
43
81
  const id = this.schedule(() => {
44
82
  this.ids.delete(id);
@@ -55,7 +93,161 @@ var SafeTimeout = class extends TimerRegistry {
55
93
  }
56
94
  };
57
95
 
58
- // src/utils/string_list.ts
96
+ function sharedRegistry(key, create) {
97
+ const symbol = Symbol.for(key);
98
+ const scope = globalThis;
99
+ const existing = scope[symbol];
100
+ if (existing !== void 0) return existing;
101
+ const registry = create();
102
+ Object.defineProperty(globalThis, symbol, { value: registry });
103
+ return registry;
104
+ }
105
+
106
+ var StateRegions = class _StateRegions {
107
+ static #registry = sharedRegistry(
108
+ "stimeo-ui.state-regions.registry.v1",
109
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Set() })
110
+ );
111
+ #whenTrue;
112
+ #whenFalse;
113
+ #record;
114
+ constructor(options, owner) {
115
+ this.#whenTrue = options.whenTrue;
116
+ this.#whenFalse = options.whenFalse ?? null;
117
+ this.#record = `data-${owner}-hidden-region`;
118
+ _StateRegions.#registry.names.add(this.#record);
119
+ }
120
+ reflect(host, isTrue) {
121
+ const shown = this.#inside(host, this.#whenTrue());
122
+ if (!this.#whenFalse) {
123
+ for (const region of shown) this.#write(region, !isTrue);
124
+ return;
125
+ }
126
+ const hiddenSide = this.#inside(host, this.#whenFalse());
127
+ if (shown.length === 0 || hiddenSide.length === 0) {
128
+ for (const region of shown) this.#give(region);
129
+ for (const region of hiddenSide) this.#give(region);
130
+ return;
131
+ }
132
+ for (const region of shown) this.#write(region, !isTrue);
133
+ for (const region of hiddenSide) this.#write(region, isTrue);
134
+ }
135
+ release(host) {
136
+ for (const region of [host, ...host.querySelectorAll("*")]) {
137
+ if (!(region instanceof HTMLElement)) continue;
138
+ const stack = this.#held(region);
139
+ const hold = stack && this.#take(stack);
140
+ if (!stack || !hold) continue;
141
+ const owns = region.hidden === hold.written && !this.#covered(region, stack, hold);
142
+ const index = stack.indexOf(hold);
143
+ stack.splice(index, 1);
144
+ const above = stack[index];
145
+ if (above) above.linked &&= hold.linked;
146
+ if (owns) {
147
+ const below = hold.linked ? stack[index - 1] : void 0;
148
+ if (below) {
149
+ if (region.hidden !== below.written) region.hidden = below.written;
150
+ } else if (region.getAttribute("hidden") !== hold.authored) {
151
+ if (hold.authored === null) region.removeAttribute("hidden");
152
+ else region.setAttribute("hidden", hold.authored);
153
+ }
154
+ }
155
+ this.#mark(region, stack);
156
+ }
157
+ }
158
+ #write(region, hidden) {
159
+ const stack = this.#stack(region);
160
+ const hold = this.#take(stack) ?? this.#acquire(region, stack);
161
+ const shows = !this.#covered(region, stack, hold);
162
+ hold.written = hidden;
163
+ if (shows && region.hidden !== hidden) region.hidden = hidden;
164
+ this.#mark(region, stack);
165
+ }
166
+ #give(region) {
167
+ const stack = this.#held(region);
168
+ if (stack && this.#take(stack)?.written) this.#write(region, false);
169
+ }
170
+ #held(region) {
171
+ return _StateRegions.#registry.stacks.has(region) || region.hasAttribute(this.#record) ? this.#stack(region) : void 0;
172
+ }
173
+ #stack(region) {
174
+ const stacks = _StateRegions.#registry.stacks;
175
+ const stack = stacks.get(region) ?? this.#seed(region);
176
+ stacks.set(region, stack);
177
+ return stack;
178
+ }
179
+ #seed(region) {
180
+ const found = [];
181
+ for (const record of _StateRegions.#registry.names) {
182
+ const read = this.#recorded(region, record);
183
+ if (read) found.push({ ...read, record });
184
+ }
185
+ const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
186
+ return found.sort((one, other) => depth(one) - depth(other)).map(({ record, authored, written, linked }, index) => ({
187
+ instance: null,
188
+ record,
189
+ authored,
190
+ written,
191
+ linked: linked && index > 0
192
+ }));
193
+ }
194
+ #take(stack) {
195
+ const hold = stack.find((held) => held.instance === this) ?? stack.find((held) => held.instance === null && held.record === this.#record);
196
+ if (hold) hold.instance = this;
197
+ return hold;
198
+ }
199
+ #covered(region, stack, hold) {
200
+ return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === region.hidden);
201
+ }
202
+ #acquire(region, stack) {
203
+ const top = stack.at(-1);
204
+ const over = top && region.hidden === top.written ? top : void 0;
205
+ const hold = {
206
+ instance: this,
207
+ record: this.#record,
208
+ authored: over ? over.authored : region.getAttribute("hidden"),
209
+ written: false,
210
+ linked: over !== void 0
211
+ };
212
+ stack.push(hold);
213
+ return hold;
214
+ }
215
+ #mark(region, stack) {
216
+ const records = /* @__PURE__ */ new Map([[this.#record, null]]);
217
+ stack.forEach((hold, index) => {
218
+ const names = stack.slice(0, index).map((under) => under.record);
219
+ const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
220
+ const marker = hold.linked || beneath.length === 0 ? [] : [false];
221
+ records.set(
222
+ hold.record,
223
+ region.getAttribute("hidden") === hold.authored && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.authored, hold.written, ...marker, ...beneath])
224
+ );
225
+ });
226
+ for (const [name, recorded] of records) {
227
+ if (region.getAttribute(name) === recorded) continue;
228
+ if (recorded === null) region.removeAttribute(name);
229
+ else region.setAttribute(name, recorded);
230
+ }
231
+ }
232
+ #recorded(region, name) {
233
+ try {
234
+ const parsed = JSON.parse(region.getAttribute(name) ?? "null");
235
+ if (!Array.isArray(parsed)) return null;
236
+ const [authored, written, ...rest] = parsed;
237
+ if (authored !== null && typeof authored !== "string") return null;
238
+ const linked = rest[0] !== false;
239
+ const beneath = linked ? rest : rest.slice(1);
240
+ if (!beneath.every((under) => typeof under === "string")) return null;
241
+ return typeof written === "boolean" ? { authored, written, linked, beneath } : null;
242
+ } catch {
243
+ return null;
244
+ }
245
+ }
246
+ #inside(host, regions) {
247
+ return regions.filter((region) => host.contains(region));
248
+ }
249
+ };
250
+
59
251
  function parseStringList(raw, fallback = []) {
60
252
  const text = raw.trim();
61
253
  if (text.length === 0) return [...fallback];
@@ -69,7 +261,6 @@ function parseStringList(raw, fallback = []) {
69
261
  return parsed.filter((entry) => typeof entry === "string");
70
262
  }
71
263
 
72
- // src/controllers/idle_controller.ts
73
264
  var DEFAULT_ACTIVITY_EVENTS = [
74
265
  "mousemove",
75
266
  "mousedown",
@@ -78,33 +269,38 @@ var DEFAULT_ACTIVITY_EVENTS = [
78
269
  "touchstart",
79
270
  "scroll"
80
271
  ];
81
- var IdleController = class extends Controller {
272
+ var IdleController = class _IdleController extends Controller {
273
+ #numbers = new NumberValueReader();
82
274
  static values = {
83
275
  timeout: { type: Number, default: 9e5 },
84
276
  promptBefore: { type: Number, default: 0 },
85
- // A JSON list read through `parseStringList` rather than Stimulus's `Array`
86
- // type: that reader throws out of the value observer before any callback
87
- // runs, so one malformed attribute would stop the detector connecting.
88
277
  events: { type: String, default: "" }
89
278
  };
279
+ static valueConstraints = {
280
+ timeout: NUMBER_BOUNDS.positiveTimer,
281
+ promptBefore: NUMBER_BOUNDS.nonNegative
282
+ };
283
+ static targets = ["prompt", "idle"];
90
284
  static events = ["prompt", "idle", "active"];
285
+ #promptRegions = new StateRegions(
286
+ { whenTrue: () => this.promptTargets },
287
+ this.identifier
288
+ );
289
+ #idleRegions = new StateRegions({ whenTrue: () => this.idleTargets }, this.identifier);
91
290
  #timeouts = new SafeTimeout();
92
291
  #idle = false;
93
292
  #prompted = false;
94
- /** Timestamp of the last activity; the timers self-reschedule against it. */
95
293
  #lastActivity = 0;
96
- /**
97
- * Activity types actually registered on `document`, so `disconnect()` unbinds the
98
- * same set even when `events` changed while connected (a Turbo morph can rewrite
99
- * the Value in place, and the removal must match the registration, not the Value).
100
- */
294
+ #connected = false;
295
+ #activityListeners = new ListenerSet();
296
+ #visibilityListeners = new ListenerSet();
101
297
  #boundEvents = [];
102
298
  #onActivity = () => {
103
299
  this.#lastActivity = Date.now();
104
300
  if (this.#idle || this.#prompted) {
105
301
  this.#idle = false;
106
302
  this.#prompted = false;
107
- this.element.removeAttribute("data-idle");
303
+ this.#reflect();
108
304
  this.dispatch("active", { detail: {} });
109
305
  this.#arm();
110
306
  }
@@ -113,62 +309,117 @@ var IdleController = class extends Controller {
113
309
  if (document.visibilityState === "visible") this.#onActivity();
114
310
  };
115
311
  connect() {
312
+ this.#connected = true;
116
313
  this.#idle = false;
117
314
  this.#prompted = false;
118
- this.element.removeAttribute("data-idle");
119
- this.#boundEvents = parseStringList(this.eventsValue, DEFAULT_ACTIVITY_EVENTS);
120
- for (const type of this.#boundEvents) {
121
- document.addEventListener(type, this.#onActivity, { passive: true, capture: true });
122
- }
123
- document.addEventListener("visibilitychange", this.#onVisibility);
315
+ this.#reflect();
316
+ this.#bindActivity(parseStringList(this.eventsValue, DEFAULT_ACTIVITY_EVENTS));
317
+ this.#visibilityListeners.add(document, "visibilitychange", this.#onVisibility);
124
318
  this.#arm();
125
319
  }
126
320
  disconnect() {
127
- for (const type of this.#boundEvents) {
128
- document.removeEventListener(type, this.#onActivity, { capture: true });
129
- }
130
- this.#boundEvents = [];
131
- document.removeEventListener("visibilitychange", this.#onVisibility);
321
+ this.#connected = false;
322
+ this.#activityListeners.dispose();
323
+ this.#visibilityListeners.dispose();
132
324
  this.#timeouts.clearAll();
133
325
  }
134
- /** Schedules the prompt and idle checks from the current activity baseline. */
326
+ eventsValueChanged() {
327
+ if (!this.#connected) return;
328
+ const types = parseStringList(this.eventsValue, DEFAULT_ACTIVITY_EVENTS);
329
+ if (types.length === this.#boundEvents.length && types.every((type, index) => type === this.#boundEvents[index]))
330
+ return;
331
+ this.#bindActivity(types);
332
+ }
333
+ timeoutValueChanged() {
334
+ if (this.#connected) this.#schedule();
335
+ }
336
+ promptBeforeValueChanged() {
337
+ if (this.#connected) this.#schedule();
338
+ }
339
+ #bindActivity(types) {
340
+ this.#activityListeners.dispose();
341
+ for (const type of types) {
342
+ this.#activityListeners.add(document, type, this.#onActivity, {
343
+ passive: true,
344
+ capture: true
345
+ });
346
+ }
347
+ this.#boundEvents = types;
348
+ }
135
349
  #arm() {
136
- this.#timeouts.clearAll();
137
350
  this.#lastActivity = Date.now();
138
- const { promptBeforeValue: prompt, timeoutValue: timeout } = this;
139
- if (prompt > 0 && prompt < timeout) {
140
- this.#timeouts.set(() => this.#checkPrompt(), timeout - prompt);
141
- }
142
- this.#timeouts.set(() => this.#checkIdle(), timeout);
351
+ this.#schedule();
352
+ }
353
+ #schedule() {
354
+ this.#timeouts.clearAll();
355
+ if (this.#idle) return;
356
+ const elapsed = Date.now() - this.#lastActivity;
357
+ const timeout = this.#safeTimeout;
358
+ const prompt = this.#safePromptBefore;
359
+ this.#timeouts.set(() => this.#checkIdle(), this.#delayFor(timeout - elapsed));
360
+ if (this.#prompted || prompt <= 0 || prompt >= timeout) return;
361
+ const left = Math.min(prompt, timeout - elapsed);
362
+ this.#timeouts.set(() => this.#checkPrompt(left), this.#delayFor(timeout - prompt - elapsed));
363
+ }
364
+ #delayFor(remaining) {
365
+ return Math.min(Math.max(remaining, 0), MAX_TIMER_DELAY_MS);
143
366
  }
144
- /**
145
- * Idle-timer callback: go idle only if there has genuinely been no activity for
146
- * `timeout`; otherwise reschedule for the remaining time. This lets activity events
147
- * stay O(1) (a timestamp write) while the deadline still tracks the last activity.
148
- */
149
367
  #checkIdle() {
150
- const remaining = this.timeoutValue - (Date.now() - this.#lastActivity);
368
+ const remaining = this.#safeTimeout - (Date.now() - this.#lastActivity);
151
369
  if (remaining > 0) {
152
- this.#timeouts.set(() => this.#checkIdle(), remaining);
370
+ this.#timeouts.set(() => this.#checkIdle(), Math.min(remaining, MAX_TIMER_DELAY_MS));
153
371
  return;
154
372
  }
155
373
  this.#idle = true;
156
374
  this.#prompted = false;
157
- this.element.setAttribute("data-idle", "true");
375
+ this.#reflect();
158
376
  this.dispatch("idle", { detail: {} });
159
377
  }
160
- /** Prompt-timer callback: warn at `promptBefore` before the idle deadline. */
161
- #checkPrompt() {
162
- const remaining = this.timeoutValue - this.promptBeforeValue - (Date.now() - this.#lastActivity);
378
+ #checkPrompt(left = this.#safePromptBefore) {
379
+ if (this.#idle) return;
380
+ const remaining = this.#safeTimeout - this.#safePromptBefore - (Date.now() - this.#lastActivity);
163
381
  if (remaining > 0) {
164
- this.#timeouts.set(() => this.#checkPrompt(), remaining);
382
+ this.#timeouts.set(() => this.#checkPrompt(), Math.min(remaining, MAX_TIMER_DELAY_MS));
165
383
  return;
166
384
  }
167
385
  this.#prompted = true;
168
- this.dispatch("prompt", { detail: { remaining: this.promptBeforeValue } });
386
+ this.#reflect();
387
+ this.dispatch("prompt", { detail: { remaining: left } });
388
+ }
389
+ promptTargetConnected() {
390
+ this.#promptRegions.reflect(this.element, this.#prompted);
391
+ }
392
+ idleTargetConnected() {
393
+ this.#idleRegions.reflect(this.element, this.#idle);
394
+ }
395
+ #reflect() {
396
+ const { element } = this;
397
+ if (this.#prompted) element.setAttribute("data-prompt", "true");
398
+ else element.removeAttribute("data-prompt");
399
+ if (this.#idle) element.setAttribute("data-idle", "true");
400
+ else element.removeAttribute("data-idle");
401
+ this.#promptRegions.reflect(element, this.#prompted);
402
+ this.#idleRegions.reflect(element, this.#idle);
403
+ }
404
+ get #safeTimeout() {
405
+ return this.#numbers.read(
406
+ this,
407
+ "timeout",
408
+ this.timeoutValue,
409
+ _IdleController.values.timeout.default,
410
+ _IdleController.valueConstraints.timeout
411
+ );
412
+ }
413
+ get #safePromptBefore() {
414
+ return this.#numbers.read(
415
+ this,
416
+ "promptBefore",
417
+ this.promptBeforeValue,
418
+ _IdleController.values.promptBefore.default,
419
+ _IdleController.valueConstraints.promptBefore
420
+ );
169
421
  }
170
422
  };
171
423
 
172
424
  export { IdleController };
173
- //# sourceMappingURL=idle_controller.js.map
174
425
  //# sourceMappingURL=idle_controller.js.map