stimeo-ui 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/README.md +8 -1
  4. data/dist/cable/index.js +363 -436
  5. data/dist/controllers/accordion_controller.js +166 -55
  6. data/dist/controllers/alert_dialog_controller.js +862 -221
  7. data/dist/controllers/announcer_controller.js +135 -198
  8. data/dist/controllers/aspect_ratio_controller.js +0 -10
  9. data/dist/controllers/auto_submit_controller.js +271 -127
  10. data/dist/controllers/avatar_controller.js +171 -105
  11. data/dist/controllers/breadcrumb_controller.js +0 -130
  12. data/dist/controllers/bulk_select_controller.js +262 -52
  13. data/dist/controllers/calendar_controller.js +175 -209
  14. data/dist/controllers/carousel_controller.js +461 -399
  15. data/dist/controllers/character_counter_controller.js +246 -156
  16. data/dist/controllers/checkbox_controller.js +32 -74
  17. data/dist/controllers/clipboard_controller.js +168 -147
  18. data/dist/controllers/collapsible_controller.js +301 -120
  19. data/dist/controllers/color_picker_controller.js +149 -139
  20. data/dist/controllers/combobox_controller.js +360 -133
  21. data/dist/controllers/command_palette_controller.js +970 -358
  22. data/dist/controllers/conditional_fields_controller.js +166 -124
  23. data/dist/controllers/confirm_controller.js +939 -248
  24. data/dist/controllers/context_menu_controller.js +256 -128
  25. data/dist/controllers/count_up_controller.js +73 -21
  26. data/dist/controllers/countdown_controller.js +183 -148
  27. data/dist/controllers/currency_input_controller.js +134 -158
  28. data/dist/controllers/data_grid_controller.js +114 -186
  29. data/dist/controllers/date_range_picker_controller.js +288 -206
  30. data/dist/controllers/dialog_controller.js +858 -223
  31. data/dist/controllers/direct_upload_controller.js +73 -153
  32. data/dist/controllers/dirty_form_controller.js +14 -48
  33. data/dist/controllers/dismissible_controller.js +175 -21
  34. data/dist/controllers/drawer_controller.js +933 -341
  35. data/dist/controllers/dropdown_controller.js +312 -82
  36. data/dist/controllers/editable_controller.js +158 -68
  37. data/dist/controllers/empty_state_controller.js +67 -53
  38. data/dist/controllers/file_dropzone_controller.js +311 -207
  39. data/dist/controllers/filter_controller.js +67 -39
  40. data/dist/controllers/flash_controller.js +172 -314
  41. data/dist/controllers/focus_controller.js +654 -240
  42. data/dist/controllers/form_field_controller.js +147 -132
  43. data/dist/controllers/form_validation_controller.js +13 -86
  44. data/dist/controllers/frame_loading_controller.js +264 -213
  45. data/dist/controllers/highlight_controller.js +107 -75
  46. data/dist/controllers/hover_card_controller.js +309 -118
  47. data/dist/controllers/idle_controller.js +267 -101
  48. data/dist/controllers/input_mask_controller.js +71 -72
  49. data/dist/controllers/intersection_controller.js +147 -106
  50. data/dist/controllers/lazy_frame_controller.js +9 -65
  51. data/dist/controllers/listbox_controller.js +306 -204
  52. data/dist/controllers/local_time_controller.js +54 -64
  53. data/dist/controllers/masonry_controller.js +70 -93
  54. data/dist/controllers/menu_controller.js +214 -156
  55. data/dist/controllers/menubar_controller.js +170 -329
  56. data/dist/controllers/meter_controller.js +151 -67
  57. data/dist/controllers/multi_select_controller.js +593 -358
  58. data/dist/controllers/navigation_menu_controller.js +124 -232
  59. data/dist/controllers/nested_form_controller.js +352 -182
  60. data/dist/controllers/network_status_controller.js +242 -54
  61. data/dist/controllers/number_input_controller.js +320 -263
  62. data/dist/controllers/optimistic_controller.js +16 -76
  63. data/dist/controllers/otp_controller.js +198 -193
  64. data/dist/controllers/overflow_indicator_controller.js +116 -113
  65. data/dist/controllers/overflow_menu_controller.js +343 -283
  66. data/dist/controllers/pagination_controller.js +295 -119
  67. data/dist/controllers/password_reveal_controller.js +389 -121
  68. data/dist/controllers/password_strength_controller.js +99 -153
  69. data/dist/controllers/persist_controller.js +237 -103
  70. data/dist/controllers/pointer_drag_controller.js +185 -165
  71. data/dist/controllers/popover_controller.js +236 -97
  72. data/dist/controllers/portal_controller.js +209 -76
  73. data/dist/controllers/preview_guard_controller.js +232 -116
  74. data/dist/controllers/progress_controller.js +125 -56
  75. data/dist/controllers/radio_group_controller.js +117 -137
  76. data/dist/controllers/range_slider_controller.js +192 -105
  77. data/dist/controllers/rating_controller.js +418 -207
  78. data/dist/controllers/read_more_controller.js +134 -63
  79. data/dist/controllers/reading_progress_controller.js +147 -135
  80. data/dist/controllers/relative_time_controller.js +145 -110
  81. data/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +97 -38
  82. data/dist/controllers/resizable_controller.js +255 -139
  83. data/dist/controllers/roving_controller.js +39 -84
  84. data/dist/controllers/scroll_area_controller.js +428 -225
  85. data/dist/controllers/scroll_restore_controller.js +0 -53
  86. data/dist/controllers/scroll_visibility_controller.js +395 -145
  87. data/dist/controllers/scrollspy_controller.js +181 -242
  88. data/dist/controllers/separator_controller.js +267 -140
  89. data/dist/controllers/sidebar_controller.js +1023 -386
  90. data/dist/controllers/skeleton_controller.js +248 -97
  91. data/dist/controllers/slider_controller.js +159 -79
  92. data/dist/controllers/smart_sticky_header_controller.js +162 -63
  93. data/dist/controllers/sortable_controller.js +0 -112
  94. data/dist/controllers/spinner_controller.js +277 -169
  95. data/dist/controllers/step_indicator_controller.js +99 -46
  96. data/dist/controllers/stepper_controller.js +114 -73
  97. data/dist/controllers/stick_to_bottom_controller.js +262 -138
  98. data/dist/controllers/sticky_observer_controller.js +214 -49
  99. data/dist/controllers/submit_once_controller.js +294 -205
  100. data/dist/controllers/switch_controller.js +33 -60
  101. data/dist/controllers/tabs_controller.js +35 -29
  102. data/dist/controllers/tags_input_controller.js +136 -145
  103. data/dist/controllers/textarea_autosize_controller.js +71 -58
  104. data/dist/controllers/theme_controller.js +130 -112
  105. data/dist/controllers/time_picker_controller.js +146 -80
  106. data/dist/controllers/toast_controller.js +186 -297
  107. data/dist/controllers/toggle_group_controller.js +95 -136
  108. data/dist/controllers/toolbar_controller.js +68 -98
  109. data/dist/controllers/tooltip_controller.js +158 -124
  110. data/dist/controllers/transition_controller.js +123 -119
  111. data/dist/controllers/tree_view_controller.js +150 -345
  112. data/dist/index.js +6370 -9346
  113. data/dist/positioning/index.js +148 -47
  114. data/lib/stimeo/ui/version.rb +1 -1
  115. metadata +3 -3
@@ -1,28 +1,177 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/persist_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
+ function leasedAuthorValue(element, attribute) {
30
+ const suffix = `-${attribute}-lease`;
31
+ for (const name of element.getAttributeNames()) {
32
+ if (!name.startsWith("data-") || !name.endsWith(suffix)) continue;
33
+ const record = readLeaseRecord(element, name);
34
+ if (record) return record.original;
35
+ }
36
+ return void 0;
37
+ }
38
+ (class _AttributeLease {
39
+ static #registry = sharedRegistry(
40
+ "stimeo-ui.attribute-lease.registry.v1",
41
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
42
+ );
43
+ #attribute;
44
+ #record;
45
+ #base;
46
+ #held = /* @__PURE__ */ new Set();
47
+ constructor(attribute, owner, options = {}) {
48
+ this.#attribute = attribute;
49
+ this.#record = `data-${owner}-${attribute}-lease`;
50
+ this.#base = options.base === true;
51
+ const names = _AttributeLease.#registry.names;
52
+ names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
53
+ }
54
+ write(element, value) {
55
+ const stack = this.#stack(element);
56
+ const hold = this.#take(stack) ?? this.#acquire(element, stack);
57
+ this.#held.add(element);
58
+ const shows = !this.#covered(element, stack, hold);
59
+ hold.written = value;
60
+ this.#mark(element, stack);
61
+ if (shows) this.#reflect(element, value);
62
+ }
63
+ return(element) {
64
+ const held = this.#held.delete(element);
65
+ const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
66
+ const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
67
+ const hold = this.#take(stack);
68
+ if (!hold) return false;
69
+ const carries = element.getAttribute(this.#attribute) === hold.written;
70
+ const owns = carries && !this.#covered(element, stack, hold);
71
+ const index = stack.indexOf(hold);
72
+ stack.splice(index, 1);
73
+ const above = stack[index];
74
+ if (above) above.linked &&= hold.linked;
75
+ this.#mark(element, stack);
76
+ if (!owns) return true;
77
+ const below = hold.linked ? stack[index - 1] : void 0;
78
+ this.#reflect(element, below ? below.written : hold.original);
79
+ return true;
80
+ }
81
+ returnAll() {
82
+ for (const element of Array.from(this.#held)) this.return(element);
83
+ }
84
+ #stack(element) {
85
+ const stacks = _AttributeLease.#registry.stacks;
86
+ const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
87
+ stacks.set(element, byAttribute);
88
+ const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
89
+ byAttribute.set(this.#attribute, stack);
90
+ return stack;
91
+ }
92
+ #seed(element) {
93
+ const found = [];
94
+ for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
95
+ const read = readLeaseRecord(element, record);
96
+ if (read) found.push({ ...read, record });
97
+ }
98
+ const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
99
+ return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
100
+ lease: null,
101
+ record,
102
+ written,
103
+ original,
104
+ linked: linked && index > 0
105
+ }));
106
+ }
107
+ #take(stack) {
108
+ const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
109
+ if (hold) hold.lease = this;
110
+ return hold;
111
+ }
112
+ #acquire(element, stack) {
113
+ const current = element.getAttribute(this.#attribute);
114
+ const top = stack.at(-1);
115
+ const bottom = stack[0];
116
+ const over = top?.written === current ? top : void 0;
117
+ const from = this.#base ? bottom : over;
118
+ const hold = {
119
+ lease: this,
120
+ record: this.#record,
121
+ written: current,
122
+ original: from ? from.original : current,
123
+ linked: !this.#base && over !== void 0
124
+ };
125
+ if (this.#base && bottom) {
126
+ bottom.linked = true;
127
+ stack.unshift(hold);
128
+ } else stack.push(hold);
129
+ return hold;
130
+ }
131
+ #covered(element, stack, hold) {
132
+ const current = element.getAttribute(this.#attribute);
133
+ return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
134
+ }
135
+ #mark(element, stack) {
136
+ const records = /* @__PURE__ */ new Map([[this.#record, null]]);
137
+ stack.forEach((hold, index) => {
138
+ const names = stack.slice(0, index).map((under) => under.record);
139
+ const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
140
+ const marker = hold.linked || beneath.length === 0 ? [] : [false];
141
+ records.set(
142
+ hold.record,
143
+ hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
144
+ );
145
+ });
146
+ for (const [name, recorded] of records) {
147
+ if (element.getAttribute(name) === recorded) continue;
148
+ if (recorded === null) element.removeAttribute(name);
149
+ else element.setAttribute(name, recorded);
150
+ }
151
+ }
152
+ #reflect(element, value) {
153
+ if (element.getAttribute(this.#attribute) === value) return;
154
+ if (value === null) element.removeAttribute(this.#attribute);
155
+ else element.setAttribute(this.#attribute, value);
156
+ }
157
+ });
4
158
 
5
- // src/utils/microtask_coalescer.ts
6
159
  var MicrotaskCoalescer = class {
7
160
  #run;
8
161
  #queued = false;
9
162
  #active = false;
10
163
  #generation = 0;
11
- /** @param run - the single reconciliation pass, invoked at most once per batch. */
12
164
  constructor(run) {
13
165
  this.#run = run;
14
166
  }
15
- /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
16
167
  activate() {
17
168
  this.#active = true;
18
169
  }
19
- /** Closes the window and drops any pending pass; call from `disconnect()`. */
20
170
  cancel() {
21
171
  this.#active = false;
22
172
  this.#queued = false;
23
173
  this.#generation += 1;
24
174
  }
25
- /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
26
175
  schedule() {
27
176
  if (!this.#active || this.#queued) return;
28
177
  this.#queued = true;
@@ -35,7 +184,52 @@ var MicrotaskCoalescer = class {
35
184
  }
36
185
  };
37
186
 
38
- // src/utils/safe_storage.ts
187
+ var MAX_TIMER_DELAY_MS = 2147483647;
188
+
189
+ var NUMBER_BOUNDS = {
190
+ timer: { finite: true, min: 0, max: MAX_TIMER_DELAY_MS }};
191
+ function matchesNumberBounds(value, bounds) {
192
+ if (!Number.isFinite(value)) {
193
+ const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
194
+ if (direction === null) return false;
195
+ if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
196
+ }
197
+ if (bounds.min !== void 0 && value < bounds.min) return false;
198
+ if (bounds.max !== void 0 && value > bounds.max) return false;
199
+ if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
200
+ if (bounds.integer && !Number.isInteger(value)) return false;
201
+ if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
202
+ return true;
203
+ }
204
+
205
+ function readNumber(raw, fallback, bounds) {
206
+ return matchesNumberBounds(raw, bounds) ? raw : fallback;
207
+ }
208
+
209
+ var NumberValueReader = class {
210
+ #lastRejected = /* @__PURE__ */ new Map();
211
+ read(owner, name, raw, fallback, bounds) {
212
+ const resolved = readNumber(raw, fallback, bounds);
213
+ if (matchesNumberBounds(raw, bounds)) {
214
+ this.#lastRejected.delete(name);
215
+ return resolved;
216
+ }
217
+ const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
218
+ const literal = owner.element.getAttribute(attribute);
219
+ if (literal === null) {
220
+ this.#lastRejected.delete(name);
221
+ return resolved;
222
+ }
223
+ if (this.#lastRejected.get(name) !== literal) {
224
+ this.#lastRejected.set(name, literal);
225
+ console.warn(
226
+ `Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
227
+ );
228
+ }
229
+ return resolved;
230
+ }
231
+ };
232
+
39
233
  function readLocalStorage(key) {
40
234
  try {
41
235
  return { ok: true, value: window.localStorage.getItem(key) };
@@ -60,43 +254,24 @@ function removeLocalStorage(key) {
60
254
  }
61
255
  }
62
256
 
63
- // src/utils/safe_timeout.ts
64
257
  var TimerRegistry = class {
65
- /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
66
258
  ids = /* @__PURE__ */ new Set();
67
- /**
68
- * Cancels a single tracked timer.
69
- *
70
- * No-ops if the id is unknown (already cleared, fired, or never owned by this
71
- * registry), so callers can clear defensively without guarding.
72
- */
73
259
  clear(id) {
74
260
  if (this.ids.delete(id)) {
75
261
  this.cancel(id);
76
262
  }
77
263
  }
78
- /**
79
- * Cancels every tracked timer. Call this from a controller's `disconnect()`
80
- * to guarantee no timer outlives the element.
81
- */
82
264
  clearAll() {
83
265
  for (const id of this.ids) {
84
266
  this.cancel(id);
85
267
  }
86
268
  this.ids.clear();
87
269
  }
88
- /** Number of timers currently tracked (pending). */
89
270
  get size() {
90
271
  return this.ids.size;
91
272
  }
92
273
  };
93
274
  var SafeTimeout = class extends TimerRegistry {
94
- /**
95
- * Schedules `callback` after `delay` ms and returns the timer id.
96
- *
97
- * The id is removed from the registry automatically when the timeout fires,
98
- * so {@link TimerRegistry.size | size} reflects only still-pending timers.
99
- */
100
275
  set(callback, delay) {
101
276
  const id = this.schedule(() => {
102
277
  this.ids.delete(id);
@@ -113,7 +288,6 @@ var SafeTimeout = class extends TimerRegistry {
113
288
  }
114
289
  };
115
290
 
116
- // src/utils/string_list.ts
117
291
  function parseStringList(raw, fallback = []) {
118
292
  const text = raw.trim();
119
293
  if (text.length === 0) return [...fallback];
@@ -127,19 +301,16 @@ function parseStringList(raw, fallback = []) {
127
301
  return parsed.filter((entry) => typeof entry === "string");
128
302
  }
129
303
 
130
- // src/utils/transient_hooks.ts
131
304
  var TransientHooks = class {
132
305
  #attributes;
133
306
  constructor(options) {
134
307
  this.#attributes = options.attributes;
135
308
  }
136
- /** Drops the declared hooks from `element`; what it carries besides stays. */
137
309
  reset(element) {
138
310
  for (const attribute of this.#attributes) element.removeAttribute(attribute);
139
311
  }
140
312
  };
141
313
 
142
- // src/controllers/persist_controller.ts
143
314
  var TRANSIENT = new TransientHooks({ attributes: ["data-persist-restored"] });
144
315
  var NON_VALUE_TYPES = /* @__PURE__ */ new Set(["file", "submit", "reset", "button", "image"]);
145
316
  var SENSITIVE_TYPES = /* @__PURE__ */ new Set(["password"]);
@@ -171,15 +342,18 @@ var parsePayload = (raw) => {
171
342
  return { version: 1, fields };
172
343
  };
173
344
  var isPersistField = (node) => node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement;
174
- var PersistController = class extends Controller {
345
+ var PersistController = class _PersistController extends Controller {
346
+ #numbers = new NumberValueReader();
175
347
  static targets = ["field"];
176
348
  static values = {
177
349
  key: { type: String, default: "" },
178
350
  debounce: { type: Number, default: DEFAULT_DEBOUNCE },
179
- // A malformed JSON list must fall back without aborting Stimulus connection.
180
351
  exclude: { type: String, default: "" },
181
352
  clearOn: { type: String, default: "" }
182
353
  };
354
+ static valueConstraints = {
355
+ debounce: NUMBER_BOUNDS.timer
356
+ };
183
357
  static actions = ["clear"];
184
358
  static events = ["restore", "save", "clear", "error"];
185
359
  #timeouts = new SafeTimeout();
@@ -208,14 +382,13 @@ var PersistController = class extends Controller {
208
382
  this.#connected = true;
209
383
  this.#logicalKey = this.#resolveLogicalKey();
210
384
  this.#excluded = parseStringList(this.excludeValue, DEFAULT_EXCLUDE);
211
- this.#knownFields = /* @__PURE__ */ new WeakSet();
212
- this.#forcedRestore.clear();
213
385
  this.element.addEventListener("input", this.#onInput);
214
386
  this.element.addEventListener("change", this.#onInput);
215
387
  this.#syncClearOn();
216
388
  this.#restoreDynamic.activate();
217
389
  this.#observer.observe(this.element, {
218
390
  attributes: true,
391
+ attributeOldValue: true,
219
392
  attributeFilter: [
220
393
  "checked",
221
394
  "data-controller",
@@ -240,12 +413,9 @@ var PersistController = class extends Controller {
240
413
  this.#unbindClearOn();
241
414
  this.#observer.disconnect();
242
415
  this.#restoreDynamic.cancel();
243
- this.#forcedRestore.clear();
244
- this.#timeouts.clearAll();
245
416
  TRANSIENT.reset(this.element);
246
417
  this.#payload = null;
247
418
  }
248
- /** Drops the active draft after storage confirms the removal. */
249
419
  clear() {
250
420
  const logicalKey = this.#logicalKey ?? this.#resolveLogicalKey();
251
421
  if (logicalKey === null) return;
@@ -259,19 +429,12 @@ var PersistController = class extends Controller {
259
429
  this.element.removeAttribute("data-persist-restored");
260
430
  this.dispatch("clear", { detail: { key: logicalKey } });
261
431
  }
262
- /** Rebinds the external clear event when its Value changes. */
263
432
  clearOnValueChanged() {
264
433
  if (this.#connected) this.#syncClearOn();
265
434
  }
266
- /** Switches storage namespaces without writing old edits under the new key. */
267
435
  keyValueChanged() {
268
436
  if (this.#connected) this.#switchLogicalKey();
269
437
  }
270
- /** Reschedules a pending write against the current debounce delay. */
271
- debounceValueChanged() {
272
- if (this.#connected && this.#pendingSave !== null) this.#scheduleSave();
273
- }
274
- /** Restores every field the new exclusion list makes eligible. */
275
438
  excludeValueChanged() {
276
439
  const previous = this.#excluded;
277
440
  this.#excluded = parseStringList(this.excludeValue, DEFAULT_EXCLUDE);
@@ -283,7 +446,6 @@ var PersistController = class extends Controller {
283
446
  }
284
447
  this.#restoreDynamic.schedule();
285
448
  }
286
- /** Schedules a debounced save. */
287
449
  #scheduleSave() {
288
450
  const logicalKey = this.#logicalKey;
289
451
  if (logicalKey === null) return;
@@ -294,21 +456,19 @@ var PersistController = class extends Controller {
294
456
  }, this.#debounceDelay);
295
457
  this.#pendingSave = { id, logicalKey };
296
458
  }
297
- /** Cancels the currently pending save, if any. */
298
459
  #cancelPendingSave() {
299
460
  const pending = this.#pendingSave;
300
461
  if (pending === null) return;
301
462
  this.#timeouts.clear(pending.id);
302
463
  this.#pendingSave = null;
303
464
  }
304
- /** Flushes a pending edit through the same success/error event path as a timer. */
305
465
  #flushPendingSave() {
306
466
  const pending = this.#pendingSave;
307
467
  if (pending === null) return;
308
468
  this.#cancelPendingSave();
309
469
  this.#save(pending.logicalKey);
470
+ this.#cancelPendingSave();
310
471
  }
311
- /** Writes the active payload and emits `save` only after storage succeeds. */
312
472
  #save(logicalKey) {
313
473
  const payload = this.#serialize();
314
474
  const result = writeLocalStorage(this.#storageKey(logicalKey), JSON.stringify(payload));
@@ -320,7 +480,6 @@ var PersistController = class extends Controller {
320
480
  this.dispatch("save", { detail: { key: logicalKey } });
321
481
  return true;
322
482
  }
323
- /** Serializes every persistable field into a versioned, typed payload. */
324
483
  #serialize() {
325
484
  const fields = [];
326
485
  for (const { field, key } of this.#fieldEntries()) {
@@ -339,17 +498,13 @@ var PersistController = class extends Controller {
339
498
  }
340
499
  return { version: 1, fields };
341
500
  }
342
- /** Reads, validates, and restores the active namespace. */
343
501
  #loadActiveDraft() {
344
502
  TRANSIENT.reset(this.element);
345
503
  this.#payload = null;
346
504
  this.#knownFields = /* @__PURE__ */ new WeakSet();
347
505
  this.#forcedRestore.clear();
348
506
  const logicalKey = this.#logicalKey;
349
- if (logicalKey === null) {
350
- this.#markCurrentFieldsKnown();
351
- return;
352
- }
507
+ if (logicalKey === null) return;
353
508
  const result = readLocalStorage(this.#storageKey(logicalKey));
354
509
  if (!result.ok) {
355
510
  this.#dispatchError(logicalKey, "read", "unavailable");
@@ -371,7 +526,6 @@ var PersistController = class extends Controller {
371
526
  this.#payload = payload;
372
527
  this.#restoreEntries(null);
373
528
  }
374
- /** Restores fields inserted or made eligible after the initial connection. */
375
529
  #restoreDynamicFields() {
376
530
  const selected = new Set(this.#forcedRestore);
377
531
  this.#forcedRestore.clear();
@@ -380,7 +534,6 @@ var PersistController = class extends Controller {
380
534
  }
381
535
  this.#restoreEntries(selected);
382
536
  }
383
- /** Applies the active payload to all fields, or only the supplied candidates. */
384
537
  #restoreEntries(selected) {
385
538
  const entries = this.#fieldEntries();
386
539
  for (const { field } of entries) this.#knownFields.add(field);
@@ -413,7 +566,6 @@ var PersistController = class extends Controller {
413
566
  this.dispatch("restore", { detail: { key: this.#logicalKey } });
414
567
  }
415
568
  }
416
- /** Applies a type-compatible stored value without coercion. */
417
569
  #applyValue(field, value) {
418
570
  if (field instanceof HTMLInputElement && field.type === "checkbox") {
419
571
  if (typeof value !== "boolean") return false;
@@ -439,16 +591,11 @@ var PersistController = class extends Controller {
439
591
  field.value = value;
440
592
  return true;
441
593
  }
442
- /** Every control this instance owns, before the exclusion list narrows them. */
443
594
  #candidateFields() {
444
595
  return this.hasFieldTarget ? this.fieldTargets.filter(isPersistField) : Array.from(this.element.querySelectorAll("input, textarea, select")).filter(
445
596
  (field) => this.#ownsField(field)
446
597
  );
447
598
  }
448
- /**
449
- * Pairs persistable fields with DOM-order occurrence keys. Every HTML radio
450
- * group (same name and form owner) occupies one shared occurrence slot.
451
- */
452
599
  #fieldEntries() {
453
600
  const entries = [];
454
601
  const eligible = [];
@@ -480,26 +627,22 @@ var PersistController = class extends Controller {
480
627
  }
481
628
  return entries;
482
629
  }
483
- /** Whether a field carries a restorable, non-excluded value. */
484
630
  #persistable(field, excluded) {
485
631
  if (NON_VALUE_TYPES.has(field.type) || SENSITIVE_TYPES.has(field.type)) return false;
632
+ if (SENSITIVE_TYPES.has(leasedAuthorValue(field, "type") ?? "")) return false;
486
633
  if (excluded.includes(field.type)) return false;
487
634
  if (field.name.length > 0 && excluded.includes(field.name)) return false;
488
635
  return true;
489
636
  }
490
- /** A stable storage sub-key for a field (its name, else id). */
491
637
  #keyOf(field) {
492
638
  return field.name || field.id || null;
493
639
  }
494
- /** Whether this instance, rather than a nested Persist host, owns a field. */
495
640
  #ownsField(field) {
496
641
  return field.closest(`[data-controller~="${this.identifier}"]`) === this.element;
497
642
  }
498
- /** Marks all currently eligible fields without applying a payload. */
499
643
  #markCurrentFieldsKnown() {
500
644
  for (const { field } of this.#fieldEntries()) this.#knownFields.add(field);
501
645
  }
502
- /** Switches namespace after flushing the old namespace's pending edit. */
503
646
  #switchLogicalKey() {
504
647
  const next = this.#resolveLogicalKey();
505
648
  if (next === this.#logicalKey) return;
@@ -507,7 +650,6 @@ var PersistController = class extends Controller {
507
650
  this.#logicalKey = next;
508
651
  this.#loadActiveDraft();
509
652
  }
510
- /** Synchronizes the exact event name owned by the `clearOn` Value. */
511
653
  #syncClearOn() {
512
654
  const next = this.#validEventName(this.clearOnValue);
513
655
  if (next === this.#registeredClearOn) return;
@@ -516,52 +658,47 @@ var PersistController = class extends Controller {
516
658
  this.element.addEventListener(next, this.#onClearEvent);
517
659
  this.#registeredClearOn = next;
518
660
  }
519
- /** Removes the event listener using the name that was actually registered. */
520
661
  #unbindClearOn() {
521
662
  if (this.#registeredClearOn === null) return;
522
663
  this.element.removeEventListener(this.#registeredClearOn, this.#onClearEvent);
523
664
  this.#registeredClearOn = null;
524
665
  }
525
- /** Returns a non-whitespace event type, or null when the hook is disabled. */
526
666
  #validEventName(value) {
527
667
  const name = value.trim();
528
668
  return name.length > 0 && !/\s/.test(name) ? name : null;
529
669
  }
530
- /**
531
- * Collects dynamic controls and select-option changes into one restore pass.
532
- *
533
- * @stimeoRuntimeOnly `key` decides whether an id change moves the storage namespace; it decides
534
- * nothing shown.
535
- */
536
670
  #onMutations(records) {
537
671
  let rootIdChanged = false;
672
+ const ownershipBefore = /* @__PURE__ */ new Map();
538
673
  for (const record of records) {
539
674
  if (record.type === "attributes") {
540
675
  if (record.target === this.element && record.attributeName === "id") {
541
676
  rootIdChanged = true;
542
677
  }
543
- this.#collectAttributeCandidate(record.target, record.attributeName);
678
+ if (record.attributeName === OWNERSHIP_ATTRIBUTE) {
679
+ const element = record.target;
680
+ if (!ownershipBefore.has(element)) ownershipBefore.set(element, record.oldValue);
681
+ continue;
682
+ }
683
+ this.#collectControlCandidate(record.target);
544
684
  continue;
545
685
  }
546
686
  for (const node of record.addedNodes) this.#collectRestoreCandidate(node);
547
687
  }
688
+ for (const [element, before] of ownershipBefore) {
689
+ this.#collectOwnershipCandidate(element, before);
690
+ }
548
691
  if (rootIdChanged && this.keyValue.length === 0) this.#switchLogicalKey();
549
692
  this.#restoreDynamic.schedule();
550
693
  }
551
- /**
552
- * Adds the controls one mutated attribute can affect. Only an ownership change on a
553
- * descendant moves fields between Persist hosts, so that is the single case worth a
554
- * subtree sweep; every other observed attribute describes one control, and sweeping
555
- * from its container would re-apply the stored draft over edits still being debounced.
556
- */
557
- #collectAttributeCandidate(target, attributeName) {
558
- if (attributeName === OWNERSHIP_ATTRIBUTE && target !== this.element) {
559
- this.#collectRestoreCandidate(target);
560
- return;
561
- }
562
- this.#collectControlCandidate(target);
694
+ #collectOwnershipCandidate(element, before) {
695
+ const after = element.getAttribute(OWNERSHIP_ATTRIBUTE);
696
+ if (this.#namesThisIdentifier(before) === this.#namesThisIdentifier(after)) return;
697
+ this.#collectRestoreCandidate(element);
698
+ }
699
+ #namesThisIdentifier(tokens) {
700
+ return (tokens ?? "").split(/\s+/).includes(this.identifier);
563
701
  }
564
- /** Adds a native control, or the select that owns a mutated option. */
565
702
  #collectControlCandidate(node) {
566
703
  if (isPersistField(node)) {
567
704
  this.#forcedRestore.add(node);
@@ -572,7 +709,6 @@ var PersistController = class extends Controller {
572
709
  if (select) this.#forcedRestore.add(select);
573
710
  }
574
711
  }
575
- /** Adds every control inside an inserted or newly owned subtree. */
576
712
  #collectRestoreCandidate(node) {
577
713
  this.#collectControlCandidate(node);
578
714
  if (!(node instanceof Element)) return;
@@ -580,31 +716,29 @@ var PersistController = class extends Controller {
580
716
  this.#forcedRestore.add(field);
581
717
  }
582
718
  }
583
- /**
584
- * Resolves the logical key from the Value, falling back to the host id.
585
- *
586
- * @stimeoRuntimeOnly `key` names the storage namespace; the restored fields come from what is
587
- * stored there.
588
- */
589
719
  #resolveLogicalKey() {
590
720
  const key = this.keyValue || this.element.id;
591
721
  return key.length > 0 ? key : null;
592
722
  }
593
- /** Namespaces one logical key within localStorage. */
594
723
  #storageKey(logicalKey) {
595
724
  return `${STORAGE_PREFIX}${logicalKey}`;
596
725
  }
597
- /** Normalizes an invalid `debounce` Value to the `debounce` default. */
598
726
  get #debounceDelay() {
599
- const value = this.debounceValue;
600
- return Number.isFinite(value) && value >= 0 ? value : DEFAULT_DEBOUNCE;
727
+ return this.#safeDebounce;
601
728
  }
602
- /** Dispatches one observable storage failure without exposing browser errors. */
603
729
  #dispatchError(key, operation, reason) {
604
730
  this.dispatch("error", { detail: { key, operation, reason } });
605
731
  }
732
+ get #safeDebounce() {
733
+ return this.#numbers.read(
734
+ this,
735
+ "debounce",
736
+ this.debounceValue,
737
+ _PersistController.values.debounce.default,
738
+ _PersistController.valueConstraints.debounce
739
+ );
740
+ }
606
741
  };
607
742
 
608
743
  export { PersistController };
609
- //# sourceMappingURL=persist_controller.js.map
610
744
  //# sourceMappingURL=persist_controller.js.map