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,8 +1,152 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/editable_controller.ts
3
+ function sharedRegistry(key, create) {
4
+ const symbol = Symbol.for(key);
5
+ const scope = globalThis;
6
+ const existing = scope[symbol];
7
+ if (existing !== void 0) return existing;
8
+ const registry = create();
9
+ Object.defineProperty(globalThis, symbol, { value: registry });
10
+ return registry;
11
+ }
12
+
13
+ function isAttributeValue(value) {
14
+ return value === null || typeof value === "string";
15
+ }
16
+ function readLeaseRecord(element, record) {
17
+ try {
18
+ const parsed = JSON.parse(element.getAttribute(record) ?? "null");
19
+ if (!Array.isArray(parsed)) return void 0;
20
+ const [original, written, ...rest] = parsed;
21
+ const linked = rest[0] !== false;
22
+ const beneath = linked ? rest : rest.slice(1);
23
+ if (!beneath.every((under) => typeof under === "string")) return void 0;
24
+ return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
25
+ } catch {
26
+ return void 0;
27
+ }
28
+ }
29
+ var AttributeLease = class _AttributeLease {
30
+ static #registry = sharedRegistry(
31
+ "stimeo-ui.attribute-lease.registry.v1",
32
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
33
+ );
34
+ #attribute;
35
+ #record;
36
+ #base;
37
+ #held = /* @__PURE__ */ new Set();
38
+ constructor(attribute, owner, options = {}) {
39
+ this.#attribute = attribute;
40
+ this.#record = `data-${owner}-${attribute}-lease`;
41
+ this.#base = options.base === true;
42
+ const names = _AttributeLease.#registry.names;
43
+ names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
44
+ }
45
+ write(element, value) {
46
+ const stack = this.#stack(element);
47
+ const hold = this.#take(stack) ?? this.#acquire(element, stack);
48
+ this.#held.add(element);
49
+ const shows = !this.#covered(element, stack, hold);
50
+ hold.written = value;
51
+ this.#mark(element, stack);
52
+ if (shows) this.#reflect(element, value);
53
+ }
54
+ return(element) {
55
+ const held = this.#held.delete(element);
56
+ const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
57
+ const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
58
+ const hold = this.#take(stack);
59
+ if (!hold) return false;
60
+ const carries = element.getAttribute(this.#attribute) === hold.written;
61
+ const owns = carries && !this.#covered(element, stack, hold);
62
+ const index = stack.indexOf(hold);
63
+ stack.splice(index, 1);
64
+ const above = stack[index];
65
+ if (above) above.linked &&= hold.linked;
66
+ this.#mark(element, stack);
67
+ if (!owns) return true;
68
+ const below = hold.linked ? stack[index - 1] : void 0;
69
+ this.#reflect(element, below ? below.written : hold.original);
70
+ return true;
71
+ }
72
+ returnAll() {
73
+ for (const element of Array.from(this.#held)) this.return(element);
74
+ }
75
+ #stack(element) {
76
+ const stacks = _AttributeLease.#registry.stacks;
77
+ const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
78
+ stacks.set(element, byAttribute);
79
+ const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
80
+ byAttribute.set(this.#attribute, stack);
81
+ return stack;
82
+ }
83
+ #seed(element) {
84
+ const found = [];
85
+ for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
86
+ const read = readLeaseRecord(element, record);
87
+ if (read) found.push({ ...read, record });
88
+ }
89
+ const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
90
+ return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
91
+ lease: null,
92
+ record,
93
+ written,
94
+ original,
95
+ linked: linked && index > 0
96
+ }));
97
+ }
98
+ #take(stack) {
99
+ const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
100
+ if (hold) hold.lease = this;
101
+ return hold;
102
+ }
103
+ #acquire(element, stack) {
104
+ const current = element.getAttribute(this.#attribute);
105
+ const top = stack.at(-1);
106
+ const bottom = stack[0];
107
+ const over = top?.written === current ? top : void 0;
108
+ const from = this.#base ? bottom : over;
109
+ const hold = {
110
+ lease: this,
111
+ record: this.#record,
112
+ written: current,
113
+ original: from ? from.original : current,
114
+ linked: !this.#base && over !== void 0
115
+ };
116
+ if (this.#base && bottom) {
117
+ bottom.linked = true;
118
+ stack.unshift(hold);
119
+ } else stack.push(hold);
120
+ return hold;
121
+ }
122
+ #covered(element, stack, hold) {
123
+ const current = element.getAttribute(this.#attribute);
124
+ return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
125
+ }
126
+ #mark(element, stack) {
127
+ const records = /* @__PURE__ */ new Map([[this.#record, null]]);
128
+ stack.forEach((hold, index) => {
129
+ const names = stack.slice(0, index).map((under) => under.record);
130
+ const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
131
+ const marker = hold.linked || beneath.length === 0 ? [] : [false];
132
+ records.set(
133
+ hold.record,
134
+ hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
135
+ );
136
+ });
137
+ for (const [name, recorded] of records) {
138
+ if (element.getAttribute(name) === recorded) continue;
139
+ if (recorded === null) element.removeAttribute(name);
140
+ else element.setAttribute(name, recorded);
141
+ }
142
+ }
143
+ #reflect(element, value) {
144
+ if (element.getAttribute(this.#attribute) === value) return;
145
+ if (value === null) element.removeAttribute(this.#attribute);
146
+ else element.setAttribute(this.#attribute, value);
147
+ }
148
+ };
4
149
 
5
- // src/utils/composition_tracker.ts
6
150
  var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
7
151
  "insertCompositionText",
8
152
  "insertFromComposition",
@@ -14,13 +158,11 @@ var CompositionTracker = class {
14
158
  #activeTargets = /* @__PURE__ */ new Set();
15
159
  #onStart;
16
160
  #onEnd;
17
- /** The field whose confirming `input` is still owed, while the window is open. */
18
161
  #confirmedTarget = null;
19
162
  constructor(options = {}) {
20
163
  this.#onStart = options.onStart;
21
164
  this.#onEnd = options.onEnd;
22
165
  }
23
- /** Starts lifecycle tracking for `target`; repeated calls are idempotent. */
24
166
  observe(target) {
25
167
  if (this.#observedTargets.has(target)) return;
26
168
  target.addEventListener("compositionstart", this.#handleStart);
@@ -28,7 +170,6 @@ var CompositionTracker = class {
28
170
  target.addEventListener("keydown", this.#handleKeydown);
29
171
  this.#observedTargets.add(target);
30
172
  }
31
- /** Stops tracking one target and clears any active composition it owned. */
32
173
  unobserve(target) {
33
174
  if (!this.#observedTargets.delete(target)) return;
34
175
  target.removeEventListener("compositionstart", this.#handleStart);
@@ -37,7 +178,6 @@ var CompositionTracker = class {
37
178
  this.#activeTargets.delete(target);
38
179
  if (this.#confirmedTarget === target) this.#confirmedTarget = null;
39
180
  }
40
- /** Releases every listener and clears state so reconnect starts cleanly. */
41
181
  disconnect() {
42
182
  for (const target of this.#observedTargets) {
43
183
  target.removeEventListener("compositionstart", this.#handleStart);
@@ -48,16 +188,9 @@ var CompositionTracker = class {
48
188
  this.#activeTargets.clear();
49
189
  this.#confirmedTarget = null;
50
190
  }
51
- /** True when lifecycle tracking or the current event reports composition. */
52
191
  isComposing(event) {
53
192
  return this.#activeTargets.size > 0 || event?.isComposing === true;
54
193
  }
55
- /**
56
- * Whether `event` is the `input` echoing the composition just confirmed.
57
- *
58
- * Asking closes the window either way, so one confirmation is folded at most
59
- * once and a consumer asks once per `input`.
60
- */
61
194
  consumesConfirmedInput(event) {
62
195
  const confirmed = this.#confirmedTarget;
63
196
  this.#confirmedTarget = null;
@@ -75,13 +208,11 @@ var CompositionTracker = class {
75
208
  this.#confirmedTarget = event.target;
76
209
  this.#onEnd?.(event);
77
210
  };
78
- /** A key on an observed field opens an edit of its own, so no echo is owed. */
79
211
  #handleKeydown = () => {
80
212
  this.#confirmedTarget = null;
81
213
  };
82
214
  };
83
215
 
84
- // src/controllers/editable_controller.ts
85
216
  var EditableController = class extends Controller {
86
217
  static targets = ["display", "input"];
87
218
  static values = {
@@ -89,31 +220,11 @@ var EditableController = class extends Controller {
89
220
  };
90
221
  static actions = ["cancel", "edit", "onDisplayKeydown", "onKeydown", "revert", "save"];
91
222
  static events = ["cancel", "change"];
92
- /** The value captured when edit mode began, used to detect real changes. */
93
223
  #previousValue = "";
94
- /**
95
- * The value the last save replaced, or `null` when there is nothing to undo.
96
- * Cleared on connect: a value from before a page restore is unrecoverable, so
97
- * `revert()` must not resurrect one.
98
- */
99
224
  #revertValue = null;
100
- /**
101
- * Owns IME lifecycle state for the edit surface, so a keydown that belongs to
102
- * a composition (cancel or confirm) is never treated as an edit command.
103
- */
104
225
  #composition = new CompositionTracker();
105
- /**
106
- * Watches focus leaving the editor from wherever it currently sits.
107
- *
108
- * `focusout` bubbles where `blur` does not, so one listener on the root sees
109
- * every departure — including one from a Save or Cancel button the consumer
110
- * placed beside the input. Binding the input alone would make the promise
111
- * "saves wherever focus moved" true only for focus that leaves the input
112
- * itself, and tabbing straight past an inner button would strand the editor
113
- * open.
114
- *
115
- * @stimeoRuntimeOnly `submitOnBlur` decides whether this one blur commits the edit.
116
- */
226
+ #hidden = new AttributeLease("hidden", this.identifier);
227
+ #connected = false;
117
228
  #onFocusOut = (event) => {
118
229
  const from = event.target;
119
230
  if (this.hasDisplayTarget && from instanceof Node && this.displayTarget.contains(from)) return;
@@ -121,32 +232,34 @@ var EditableController = class extends Controller {
121
232
  if (next instanceof Node && this.element.contains(next)) return;
122
233
  if (this.submitOnBlurValue) this.#commit(false);
123
234
  };
124
- /** Establishes the initial display mode (display shown, input hidden). */
125
235
  connect() {
236
+ this.#connected = true;
126
237
  if (this.hasInputTarget) this.#composition.observe(this.inputTarget);
127
238
  this.element.addEventListener("focusout", this.#onFocusOut);
128
239
  this.#revertValue = null;
129
240
  this.#setMode("display");
130
241
  }
131
- /** Releases the composition and focus listeners so nothing outlives the element. */
132
242
  disconnect() {
243
+ this.#connected = false;
133
244
  this.#composition.disconnect();
134
245
  this.element.removeEventListener("focusout", this.#onFocusOut);
135
246
  }
136
- /** Tracks an input added initially or after connect (e.g. a Turbo swap). */
137
247
  inputTargetConnected(input) {
138
248
  this.#composition.observe(input);
139
249
  this.#applyMode();
140
250
  }
141
- /** Removes composition listeners when the active input is replaced or removed. */
142
251
  inputTargetDisconnected(input) {
143
252
  this.#composition.unobserve(input);
253
+ if (!this.inputTargets.includes(input)) this.#hidden.return(input);
254
+ if (this.#connected) this.#applyMode();
144
255
  }
145
- /** Re-hides or re-shows a display element that arrived after the mode was set. */
146
256
  displayTargetConnected() {
147
257
  this.#applyMode();
148
258
  }
149
- /** Enters edit mode: seeds the input from the declared value, focuses, selects. */
259
+ displayTargetDisconnected(display) {
260
+ if (!this.displayTargets.includes(display)) this.#hidden.return(display);
261
+ if (this.#connected) this.#applyMode();
262
+ }
150
263
  edit() {
151
264
  if (this.#isEditing || !this.hasInputTarget || !this.hasDisplayTarget) return;
152
265
  this.#previousValue = this.#currentValue;
@@ -155,35 +268,26 @@ var EditableController = class extends Controller {
155
268
  this.inputTarget.focus();
156
269
  this.inputTarget.select();
157
270
  }
158
- /** Commits the edit and returns focus to the display element. */
159
271
  save() {
160
272
  this.#commit(true);
161
273
  }
162
- /** Discards edits, returns to display mode, and dispatches `cancel`. */
163
274
  cancel() {
164
275
  if (!this.#isEditing) return;
165
276
  this.#setMode("display");
166
277
  if (this.hasDisplayTarget) this.displayTarget.focus();
167
278
  this.dispatch("cancel", { detail: {} });
168
279
  }
169
- /**
170
- * Puts back the value the last save replaced — for a consumer whose server
171
- * rejected it. Silent by design: a `change` here would re-enter the same
172
- * handler that asked for the undo. One save, one undo.
173
- */
174
280
  revert() {
175
281
  if (this.#revertValue === null || this.#isEditing) return;
176
282
  this.#writeValue(this.#revertValue);
177
283
  this.#revertValue = null;
178
284
  }
179
- /** Adds `F2` as an editing entry point alongside the button's native activation. */
180
285
  onDisplayKeydown(event) {
181
286
  if (event.key === "F2") {
182
287
  event.preventDefault();
183
288
  this.edit();
184
289
  }
185
290
  }
186
- /** Commits on `Enter` (or `Ctrl+Enter` when multiline) and cancels on `Escape`. */
187
291
  onKeydown(event) {
188
292
  if (this.#composition.isComposing(event)) return;
189
293
  if (event.key === "Escape") {
@@ -199,13 +303,6 @@ var EditableController = class extends Controller {
199
303
  this.#commit(true);
200
304
  }
201
305
  }
202
- /**
203
- * Returns to display mode, storing the input's value and dispatching `change`
204
- * when it differs from where editing began.
205
- *
206
- * @param restoreFocus - Move focus back to the display element (explicit
207
- * commit) rather than honoring the user's new focus target (blur).
208
- */
209
306
  #commit(restoreFocus) {
210
307
  if (!this.#isEditing || !this.hasInputTarget) return;
211
308
  const value = this.inputTarget.value.trim();
@@ -218,39 +315,32 @@ var EditableController = class extends Controller {
218
315
  this.dispatch("change", { detail: { value, previous } });
219
316
  }
220
317
  }
221
- /** Stores `value` wherever the display element declares it. */
222
318
  #writeValue(value) {
223
319
  if (!this.hasDisplayTarget) return;
224
320
  const display = this.displayTarget;
225
321
  if (display.hasAttribute("data-value")) display.dataset.value = value;
226
322
  else display.textContent = value;
227
323
  }
228
- /** Derives both elements' visibility from the mode currently in the DOM. */
229
324
  #applyMode() {
230
325
  const editing = this.#isEditing;
231
- if (this.hasDisplayTarget) this.displayTarget.hidden = editing;
232
- if (this.hasInputTarget) this.inputTarget.hidden = !editing;
326
+ if (this.hasDisplayTarget) this.#hidden.write(this.displayTarget, editing ? "" : null);
327
+ if (this.hasInputTarget) this.#hidden.write(this.inputTarget, editing ? null : "");
233
328
  }
234
- /** Records the mode, then brings both elements in line with it. */
235
329
  #setMode(mode) {
236
330
  this.element.dataset.mode = mode;
237
331
  this.#applyMode();
238
332
  }
239
- /** The value the display element declares, trimmed. */
240
333
  get #currentValue() {
241
334
  const display = this.displayTarget;
242
335
  return (display.dataset.value ?? display.textContent ?? "").trim();
243
336
  }
244
- /** Whether the editing control is a multi-line `<textarea>`. */
245
337
  get #isMultiline() {
246
338
  return this.inputTarget.tagName === "TEXTAREA";
247
339
  }
248
- /** Whether the controller is currently in edit mode. */
249
340
  get #isEditing() {
250
341
  return this.element.dataset.mode === "editing";
251
342
  }
252
343
  };
253
344
 
254
345
  export { EditableController };
255
- //# sourceMappingURL=editable_controller.js.map
256
346
  //# sourceMappingURL=editable_controller.js.map
@@ -1,8 +1,5 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/empty_state_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,7 +16,70 @@ function fillTemplate(template, values) {
19
16
  });
20
17
  }
21
18
 
22
- // src/controllers/empty_state_controller.ts
19
+ var ListenerSet = class {
20
+ #abort = new AbortController();
21
+ add(target, type, handler, options) {
22
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
23
+ }
24
+ dispose() {
25
+ this.#abort.abort();
26
+ this.#abort = new AbortController();
27
+ }
28
+ };
29
+
30
+ var MicrotaskCoalescer = class {
31
+ #run;
32
+ #queued = false;
33
+ #active = false;
34
+ #generation = 0;
35
+ constructor(run) {
36
+ this.#run = run;
37
+ }
38
+ activate() {
39
+ this.#active = true;
40
+ }
41
+ cancel() {
42
+ this.#active = false;
43
+ this.#queued = false;
44
+ this.#generation += 1;
45
+ }
46
+ schedule() {
47
+ if (!this.#active || this.#queued) return;
48
+ this.#queued = true;
49
+ const generation = this.#generation;
50
+ queueMicrotask(() => {
51
+ if (generation !== this.#generation || !this.#queued || !this.#active) return;
52
+ this.#queued = false;
53
+ this.#run();
54
+ });
55
+ }
56
+ };
57
+
58
+ var MorphRenderWatcher = class {
59
+ #listeners = new ListenerSet();
60
+ #pass;
61
+ #element = null;
62
+ #onMorph = () => this.#pass.schedule();
63
+ constructor(run) {
64
+ this.#pass = new MicrotaskCoalescer(run);
65
+ }
66
+ observe(element) {
67
+ if (this.#element === element) return;
68
+ this.disconnect();
69
+ this.#element = element;
70
+ this.#pass.activate();
71
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
72
+ }
73
+ schedule() {
74
+ this.#pass.schedule();
75
+ }
76
+ disconnect() {
77
+ this.#listeners.dispose();
78
+ this.#pass.cancel();
79
+ this.#element = null;
80
+ }
81
+ };
82
+
23
83
  var EmptyStateController = class extends Controller {
24
84
  static targets = ["list", "empty"];
25
85
  static values = {
@@ -29,68 +89,40 @@ var EmptyStateController = class extends Controller {
29
89
  };
30
90
  static events = ["change"];
31
91
  #observer = null;
32
- /** Whether the controller is between `connect()` and `disconnect()`. */
33
92
  #connected = false;
34
- /** Last applied empty state; `null` until the first sync so connect emits nothing. */
35
93
  #empty = null;
94
+ #morphRender = new MorphRenderWatcher(() => this.#resync());
36
95
  connect() {
96
+ this.#morphRender.observe(this.element);
37
97
  this.#connected = true;
38
98
  this.#syncObservation();
39
99
  this.#update();
40
100
  }
41
101
  disconnect() {
102
+ this.#morphRender.disconnect();
42
103
  this.#connected = false;
43
104
  this.#stopObserving();
44
105
  }
45
- /** Follows a `list` element swapped in at runtime (Turbo Stream `replace` / morph). */
46
106
  listTargetConnected() {
47
107
  this.#resync();
48
108
  }
49
- /** Releases the observation when the `list` element leaves the target set. */
50
109
  listTargetDisconnected() {
51
110
  this.#resync();
52
111
  }
53
- /** Syncs an `empty` element that arrives — or is replaced — at runtime. */
54
112
  emptyTargetConnected() {
55
113
  this.#resync();
56
114
  }
57
- /**
58
- * Syncs the `empty` element that remains when one leaves the target set. A
59
- * single-target getter resolves to the first `empty` element in document order,
60
- * so a swap that inserts the replacement *before* removing the original (Turbo
61
- * Stream `after` / `before` / `append` followed by `remove`) leaves the
62
- * replacement untouched until the original goes — this callback is that moment.
63
- */
64
115
  emptyTargetDisconnected() {
65
116
  this.#resync();
66
117
  }
67
- /** Re-renders when application code (or a Turbo morph) changes `itemSelector` at runtime. */
68
118
  itemSelectorValueChanged() {
69
119
  this.#resync();
70
120
  }
71
- /**
72
- * Re-points the observation and re-renders after a target or selector change.
73
- * The `#connected` guard is load-bearing: Stimulus runs value and target
74
- * callbacks for the initial markup *before* `connect()` and runs target
75
- * callbacks during teardown *after* `disconnect()`, and re-observing there
76
- * would outlive the controller.
77
- */
78
121
  #resync() {
79
122
  if (!this.#connected) return;
80
123
  this.#syncObservation();
81
124
  this.#update();
82
125
  }
83
- /**
84
- * Points the mutation observation at the current `list` target — re-resolved on
85
- * every sync rather than captured at connect, so an element swapped in at
86
- * runtime is observed instead of the detached original.
87
- *
88
- * The observation covers exactly what the count predicate reads. With no
89
- * `itemSelector` the count is the child element count, which only `childList`
90
- * can change. With one, the predicate reads the children themselves, so
91
- * attribute and descendant mutations are watched too — and the controller's own
92
- * writes are filtered back out, or toggling `hidden` would re-enter the render.
93
- */
94
126
  #syncObservation() {
95
127
  this.#stopObserving();
96
128
  if (!this.hasListTarget || typeof MutationObserver === "undefined") return;
@@ -108,21 +140,11 @@ var EmptyStateController = class extends Controller {
108
140
  this.#observer?.disconnect();
109
141
  this.#observer = null;
110
142
  }
111
- /**
112
- * Whether a mutation can change the item count. An attribute written on a
113
- * target this controller owns is its own echo — `hidden` on `list` / `empty`,
114
- * and the hooks on the controller element when the list *is* that element.
115
- */
116
143
  #affectsCount(record) {
117
144
  if (record.type !== "attributes") return true;
118
145
  const own = record.target === this.listTarget || this.hasEmptyTarget && record.target === this.emptyTarget;
119
146
  return !own;
120
147
  }
121
- /**
122
- * Renders the count, then reports a crossed boundary. The announcement copy is
123
- * read here rather than while rendering: it is the wording of the report, not
124
- * an input to what is displayed.
125
- */
126
148
  #update() {
127
149
  const count = this.#render();
128
150
  if (count === null) return;
@@ -135,12 +157,6 @@ var EmptyStateController = class extends Controller {
135
157
  fillTemplate(empty ? this.announceTextValue : this.announceFilledTextValue, { count })
136
158
  );
137
159
  }
138
- /**
139
- * Syncs visibility and the state hooks to the current item count, and returns
140
- * it. `null` when there is no `list` target to count.
141
- *
142
- * @stimeoRenderRoot
143
- */
144
160
  #render() {
145
161
  if (!this.hasListTarget) return null;
146
162
  const count = this.#count();
@@ -155,7 +171,6 @@ var EmptyStateController = class extends Controller {
155
171
  if (this.hasEmptyTarget) this.emptyTarget.hidden = !empty;
156
172
  return count;
157
173
  }
158
- /** Item count: element children matching `itemSelector`, or all element children. */
159
174
  #count() {
160
175
  const selector = this.itemSelectorValue;
161
176
  if (selector.length === 0) return this.listTarget.childElementCount;
@@ -168,5 +183,4 @@ var EmptyStateController = class extends Controller {
168
183
  };
169
184
 
170
185
  export { EmptyStateController };
171
- //# sourceMappingURL=empty_state_controller.js.map
172
186
  //# sourceMappingURL=empty_state_controller.js.map