stimeo-ui 0.15.0 → 0.16.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 (267) hide show
  1. package/CHANGELOG.md +178 -0
  2. package/README.md +120 -0
  3. package/dist/cable/index.js +63 -15
  4. package/dist/cable/index.js.map +1 -1
  5. package/dist/controllers/accordion_controller.d.ts +30 -3
  6. package/dist/controllers/accordion_controller.js +98 -9
  7. package/dist/controllers/accordion_controller.js.map +1 -1
  8. package/dist/controllers/alert_dialog_controller.d.ts +8 -7
  9. package/dist/controllers/alert_dialog_controller.js.map +1 -1
  10. package/dist/controllers/announcer_controller.d.ts +3 -2
  11. package/dist/controllers/announcer_controller.js +8 -1
  12. package/dist/controllers/announcer_controller.js.map +1 -1
  13. package/dist/controllers/auto_submit_controller.js +43 -2
  14. package/dist/controllers/auto_submit_controller.js.map +1 -1
  15. package/dist/controllers/avatar_controller.js +1 -1
  16. package/dist/controllers/avatar_controller.js.map +1 -1
  17. package/dist/controllers/breadcrumb_controller.d.ts +10 -7
  18. package/dist/controllers/breadcrumb_controller.js +9 -8
  19. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  20. package/dist/controllers/bulk_select_controller.d.ts +4 -4
  21. package/dist/controllers/bulk_select_controller.js +7 -6
  22. package/dist/controllers/bulk_select_controller.js.map +1 -1
  23. package/dist/controllers/calendar_controller.d.ts +85 -23
  24. package/dist/controllers/calendar_controller.js +323 -123
  25. package/dist/controllers/calendar_controller.js.map +1 -1
  26. package/dist/controllers/carousel_controller.d.ts +67 -22
  27. package/dist/controllers/carousel_controller.js +263 -38
  28. package/dist/controllers/carousel_controller.js.map +1 -1
  29. package/dist/controllers/character_counter_controller.d.ts +1 -1
  30. package/dist/controllers/character_counter_controller.js +40 -2
  31. package/dist/controllers/character_counter_controller.js.map +1 -1
  32. package/dist/controllers/checkbox_controller.js +54 -10
  33. package/dist/controllers/checkbox_controller.js.map +1 -1
  34. package/dist/controllers/clipboard_controller.d.ts +28 -2
  35. package/dist/controllers/clipboard_controller.js +63 -5
  36. package/dist/controllers/clipboard_controller.js.map +1 -1
  37. package/dist/controllers/collapsible_controller.d.ts +25 -1
  38. package/dist/controllers/collapsible_controller.js +99 -14
  39. package/dist/controllers/collapsible_controller.js.map +1 -1
  40. package/dist/controllers/color_picker_controller.d.ts +29 -8
  41. package/dist/controllers/color_picker_controller.js +80 -34
  42. package/dist/controllers/color_picker_controller.js.map +1 -1
  43. package/dist/controllers/combobox_controller.d.ts +10 -1
  44. package/dist/controllers/combobox_controller.js +106 -16
  45. package/dist/controllers/combobox_controller.js.map +1 -1
  46. package/dist/controllers/command_palette_controller.d.ts +3 -3
  47. package/dist/controllers/command_palette_controller.js +35 -3
  48. package/dist/controllers/command_palette_controller.js.map +1 -1
  49. package/dist/controllers/conditional_fields_controller.js +56 -11
  50. package/dist/controllers/conditional_fields_controller.js.map +1 -1
  51. package/dist/controllers/confirm_controller.js +3 -0
  52. package/dist/controllers/confirm_controller.js.map +1 -1
  53. package/dist/controllers/context_menu_controller.d.ts +6 -0
  54. package/dist/controllers/context_menu_controller.js +32 -12
  55. package/dist/controllers/context_menu_controller.js.map +1 -1
  56. package/dist/controllers/countdown_controller.d.ts +30 -1
  57. package/dist/controllers/countdown_controller.js +129 -26
  58. package/dist/controllers/countdown_controller.js.map +1 -1
  59. package/dist/controllers/currency_input_controller.d.ts +74 -15
  60. package/dist/controllers/currency_input_controller.js +205 -58
  61. package/dist/controllers/currency_input_controller.js.map +1 -1
  62. package/dist/controllers/data_grid_controller.d.ts +63 -18
  63. package/dist/controllers/data_grid_controller.js +195 -29
  64. package/dist/controllers/data_grid_controller.js.map +1 -1
  65. package/dist/controllers/date_range_picker_controller.d.ts +34 -6
  66. package/dist/controllers/date_range_picker_controller.js +139 -27
  67. package/dist/controllers/date_range_picker_controller.js.map +1 -1
  68. package/dist/controllers/dialog_controller.d.ts +10 -3
  69. package/dist/controllers/dialog_controller.js +35 -8
  70. package/dist/controllers/dialog_controller.js.map +1 -1
  71. package/dist/controllers/direct_upload_controller.js +22 -4
  72. package/dist/controllers/direct_upload_controller.js.map +1 -1
  73. package/dist/controllers/dirty_form_controller.d.ts +2 -2
  74. package/dist/controllers/dirty_form_controller.js +14 -1
  75. package/dist/controllers/dirty_form_controller.js.map +1 -1
  76. package/dist/controllers/dismissible_controller.js +1 -0
  77. package/dist/controllers/dismissible_controller.js.map +1 -1
  78. package/dist/controllers/drawer_controller.d.ts +18 -10
  79. package/dist/controllers/drawer_controller.js +54 -19
  80. package/dist/controllers/drawer_controller.js.map +1 -1
  81. package/dist/controllers/dropdown_controller.d.ts +9 -3
  82. package/dist/controllers/dropdown_controller.js +36 -9
  83. package/dist/controllers/dropdown_controller.js.map +1 -1
  84. package/dist/controllers/editable_controller.js +34 -0
  85. package/dist/controllers/editable_controller.js.map +1 -1
  86. package/dist/controllers/file_dropzone_controller.js +144 -51
  87. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  88. package/dist/controllers/filter_controller.d.ts +10 -4
  89. package/dist/controllers/filter_controller.js +20 -6
  90. package/dist/controllers/filter_controller.js.map +1 -1
  91. package/dist/controllers/flash_controller.d.ts +26 -6
  92. package/dist/controllers/flash_controller.js +305 -46
  93. package/dist/controllers/flash_controller.js.map +1 -1
  94. package/dist/controllers/focus_controller.js +1 -0
  95. package/dist/controllers/focus_controller.js.map +1 -1
  96. package/dist/controllers/form_field_controller.js +7 -5
  97. package/dist/controllers/form_field_controller.js.map +1 -1
  98. package/dist/controllers/form_validation_controller.js +19 -13
  99. package/dist/controllers/form_validation_controller.js.map +1 -1
  100. package/dist/controllers/frame_loading_controller.js +16 -2
  101. package/dist/controllers/frame_loading_controller.js.map +1 -1
  102. package/dist/controllers/highlight_controller.js +19 -2
  103. package/dist/controllers/highlight_controller.js.map +1 -1
  104. package/dist/controllers/hover_card_controller.d.ts +10 -2
  105. package/dist/controllers/hover_card_controller.js +40 -14
  106. package/dist/controllers/hover_card_controller.js.map +1 -1
  107. package/dist/controllers/idle_controller.d.ts +16 -3
  108. package/dist/controllers/idle_controller.js +90 -5
  109. package/dist/controllers/idle_controller.js.map +1 -1
  110. package/dist/controllers/input_mask_controller.d.ts +5 -2
  111. package/dist/controllers/input_mask_controller.js +65 -9
  112. package/dist/controllers/input_mask_controller.js.map +1 -1
  113. package/dist/controllers/intersection_controller.js +3 -0
  114. package/dist/controllers/intersection_controller.js.map +1 -1
  115. package/dist/controllers/lazy_frame_controller.js +11 -2
  116. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  117. package/dist/controllers/listbox_controller.d.ts +50 -8
  118. package/dist/controllers/listbox_controller.js +203 -45
  119. package/dist/controllers/listbox_controller.js.map +1 -1
  120. package/dist/controllers/masonry_controller.js +0 -2
  121. package/dist/controllers/masonry_controller.js.map +1 -1
  122. package/dist/controllers/menu_controller.d.ts +9 -3
  123. package/dist/controllers/menu_controller.js +45 -16
  124. package/dist/controllers/menu_controller.js.map +1 -1
  125. package/dist/controllers/menubar_controller.d.ts +11 -0
  126. package/dist/controllers/menubar_controller.js +58 -24
  127. package/dist/controllers/menubar_controller.js.map +1 -1
  128. package/dist/controllers/meter_controller.js +9 -5
  129. package/dist/controllers/meter_controller.js.map +1 -1
  130. package/dist/controllers/multi_select_controller.d.ts +18 -3
  131. package/dist/controllers/multi_select_controller.js +278 -104
  132. package/dist/controllers/multi_select_controller.js.map +1 -1
  133. package/dist/controllers/navigation_menu_controller.d.ts +11 -0
  134. package/dist/controllers/navigation_menu_controller.js +48 -15
  135. package/dist/controllers/navigation_menu_controller.js.map +1 -1
  136. package/dist/controllers/nested_form_controller.js +37 -8
  137. package/dist/controllers/nested_form_controller.js.map +1 -1
  138. package/dist/controllers/network_status_controller.js +9 -1
  139. package/dist/controllers/network_status_controller.js.map +1 -1
  140. package/dist/controllers/number_input_controller.d.ts +36 -8
  141. package/dist/controllers/number_input_controller.js +124 -21
  142. package/dist/controllers/number_input_controller.js.map +1 -1
  143. package/dist/controllers/optimistic_controller.js +14 -1
  144. package/dist/controllers/optimistic_controller.js.map +1 -1
  145. package/dist/controllers/otp_controller.d.ts +22 -7
  146. package/dist/controllers/otp_controller.js +198 -55
  147. package/dist/controllers/otp_controller.js.map +1 -1
  148. package/dist/controllers/overflow_indicator_controller.d.ts +8 -12
  149. package/dist/controllers/overflow_indicator_controller.js +84 -13
  150. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  151. package/dist/controllers/overflow_menu_controller.d.ts +26 -6
  152. package/dist/controllers/overflow_menu_controller.js +112 -41
  153. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  154. package/dist/controllers/pagination_controller.d.ts +19 -11
  155. package/dist/controllers/pagination_controller.js +74 -28
  156. package/dist/controllers/pagination_controller.js.map +1 -1
  157. package/dist/controllers/password_reveal_controller.d.ts +15 -1
  158. package/dist/controllers/password_reveal_controller.js +59 -2
  159. package/dist/controllers/password_reveal_controller.js.map +1 -1
  160. package/dist/controllers/persist_controller.js +30 -8
  161. package/dist/controllers/persist_controller.js.map +1 -1
  162. package/dist/controllers/pointer_drag_controller.js +131 -52
  163. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  164. package/dist/controllers/popover_controller.d.ts +9 -3
  165. package/dist/controllers/popover_controller.js +45 -11
  166. package/dist/controllers/popover_controller.js.map +1 -1
  167. package/dist/controllers/portal_controller.d.ts +1 -1
  168. package/dist/controllers/portal_controller.js +6 -2
  169. package/dist/controllers/portal_controller.js.map +1 -1
  170. package/dist/controllers/preview_guard_controller.js +16 -1
  171. package/dist/controllers/preview_guard_controller.js.map +1 -1
  172. package/dist/controllers/progress_controller.js +8 -4
  173. package/dist/controllers/progress_controller.js.map +1 -1
  174. package/dist/controllers/radio_group_controller.d.ts +6 -4
  175. package/dist/controllers/radio_group_controller.js +42 -17
  176. package/dist/controllers/radio_group_controller.js.map +1 -1
  177. package/dist/controllers/range_slider_controller.d.ts +49 -1
  178. package/dist/controllers/range_slider_controller.js +88 -42
  179. package/dist/controllers/range_slider_controller.js.map +1 -1
  180. package/dist/controllers/rating_controller.d.ts +14 -3
  181. package/dist/controllers/rating_controller.js +39 -15
  182. package/dist/controllers/rating_controller.js.map +1 -1
  183. package/dist/controllers/read_more_controller.d.ts +24 -2
  184. package/dist/controllers/read_more_controller.js +69 -2
  185. package/dist/controllers/read_more_controller.js.map +1 -1
  186. package/dist/controllers/resizable_controller.d.ts +16 -2
  187. package/dist/controllers/resizable_controller.js +82 -22
  188. package/dist/controllers/resizable_controller.js.map +1 -1
  189. package/dist/controllers/scroll_area_controller.js +5 -1
  190. package/dist/controllers/scroll_area_controller.js.map +1 -1
  191. package/dist/controllers/scroll_visibility_controller.js +0 -1
  192. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  193. package/dist/controllers/scrollspy_controller.d.ts +3 -2
  194. package/dist/controllers/scrollspy_controller.js +6 -0
  195. package/dist/controllers/scrollspy_controller.js.map +1 -1
  196. package/dist/controllers/separator_controller.d.ts +41 -14
  197. package/dist/controllers/separator_controller.js +66 -37
  198. package/dist/controllers/separator_controller.js.map +1 -1
  199. package/dist/controllers/sidebar_controller.d.ts +20 -3
  200. package/dist/controllers/sidebar_controller.js +77 -18
  201. package/dist/controllers/sidebar_controller.js.map +1 -1
  202. package/dist/controllers/skeleton_controller.js +6 -1
  203. package/dist/controllers/skeleton_controller.js.map +1 -1
  204. package/dist/controllers/slider_controller.d.ts +45 -7
  205. package/dist/controllers/slider_controller.js +82 -47
  206. package/dist/controllers/slider_controller.js.map +1 -1
  207. package/dist/controllers/smart_sticky_header_controller.js +11 -1
  208. package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
  209. package/dist/controllers/sortable_controller.js +17 -2
  210. package/dist/controllers/sortable_controller.js.map +1 -1
  211. package/dist/controllers/spinner_controller.js +10 -2
  212. package/dist/controllers/spinner_controller.js.map +1 -1
  213. package/dist/controllers/step_indicator_controller.d.ts +19 -17
  214. package/dist/controllers/step_indicator_controller.js +18 -17
  215. package/dist/controllers/step_indicator_controller.js.map +1 -1
  216. package/dist/controllers/stepper_controller.d.ts +34 -9
  217. package/dist/controllers/stepper_controller.js +101 -19
  218. package/dist/controllers/stepper_controller.js.map +1 -1
  219. package/dist/controllers/stick_to_bottom_controller.d.ts +19 -0
  220. package/dist/controllers/stick_to_bottom_controller.js +75 -5
  221. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  222. package/dist/controllers/submit_once_controller.d.ts +3 -2
  223. package/dist/controllers/submit_once_controller.js +16 -3
  224. package/dist/controllers/submit_once_controller.js.map +1 -1
  225. package/dist/controllers/switch_controller.d.ts +29 -7
  226. package/dist/controllers/switch_controller.js +101 -10
  227. package/dist/controllers/switch_controller.js.map +1 -1
  228. package/dist/controllers/tabs_controller.d.ts +12 -0
  229. package/dist/controllers/tabs_controller.js +21 -2
  230. package/dist/controllers/tabs_controller.js.map +1 -1
  231. package/dist/controllers/tags_input_controller.d.ts +15 -3
  232. package/dist/controllers/tags_input_controller.js +209 -59
  233. package/dist/controllers/tags_input_controller.js.map +1 -1
  234. package/dist/controllers/theme_controller.d.ts +20 -3
  235. package/dist/controllers/theme_controller.js +64 -14
  236. package/dist/controllers/theme_controller.js.map +1 -1
  237. package/dist/controllers/time_picker_controller.js +23 -8
  238. package/dist/controllers/time_picker_controller.js.map +1 -1
  239. package/dist/controllers/toast_controller.d.ts +52 -15
  240. package/dist/controllers/toast_controller.js +337 -54
  241. package/dist/controllers/toast_controller.js.map +1 -1
  242. package/dist/controllers/toggle_group_controller.d.ts +49 -7
  243. package/dist/controllers/toggle_group_controller.js +159 -23
  244. package/dist/controllers/toggle_group_controller.js.map +1 -1
  245. package/dist/controllers/toolbar_controller.js +32 -0
  246. package/dist/controllers/toolbar_controller.js.map +1 -1
  247. package/dist/controllers/tooltip_controller.d.ts +8 -0
  248. package/dist/controllers/tooltip_controller.js +39 -13
  249. package/dist/controllers/tooltip_controller.js.map +1 -1
  250. package/dist/controllers/transition_controller.js +4 -0
  251. package/dist/controllers/transition_controller.js.map +1 -1
  252. package/dist/controllers/tree_view_controller.d.ts +39 -8
  253. package/dist/controllers/tree_view_controller.js +169 -16
  254. package/dist/controllers/tree_view_controller.js.map +1 -1
  255. package/dist/index.d.ts +28 -1
  256. package/dist/index.js +4588 -1535
  257. package/dist/index.js.map +1 -1
  258. package/dist/inspector/cli.d.ts +72 -6
  259. package/dist/inspector/cli.js +262 -51
  260. package/dist/inspector/cli.js.map +1 -1
  261. package/dist/inspector/cli_bin.js +309 -51
  262. package/dist/inspector/cli_bin.js.map +1 -1
  263. package/dist/inspector/examples.json +40 -40
  264. package/dist/inspector/manifest.json +525 -59
  265. package/dist/positioning/index.js +2 -0
  266. package/dist/positioning/index.js.map +1 -1
  267. package/package.json +2 -2
@@ -52,6 +52,33 @@ function isReservedArrowChord(event, allow = []) {
52
52
  return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
53
53
  }
54
54
 
55
+ // src/utils/element_part.ts
56
+ function matchingPart(root, selector) {
57
+ return root.matches(selector) ? root : root.querySelector(selector);
58
+ }
59
+ function writeLabel(slot, text) {
60
+ if (slot.childElementCount === 0) {
61
+ slot.textContent = text;
62
+ return;
63
+ }
64
+ let first = null;
65
+ for (const node of Array.from(slot.childNodes)) {
66
+ if (node.nodeType !== Node.TEXT_NODE) continue;
67
+ if (first) node.remove();
68
+ else first = node;
69
+ }
70
+ if (first) first.nodeValue = text;
71
+ else slot.prepend(text);
72
+ }
73
+ function readLabel(slot) {
74
+ if (!slot) return "";
75
+ let text = "";
76
+ for (const node of Array.from(slot.childNodes)) {
77
+ if (node.nodeType === Node.TEXT_NODE) text += node.nodeValue ?? "";
78
+ }
79
+ return text.trim();
80
+ }
81
+
55
82
  // src/utils/roving_tabindex.ts
56
83
  var RovingTabindex = class {
57
84
  /** Returns the current ordered item elements; called on every operation. */
@@ -99,7 +126,7 @@ var ChipRow = class {
99
126
  constructor(options) {
100
127
  this.#directionElement = options.directionElement;
101
128
  this.#getItems = options.getItems;
102
- this.#getButton = options.getButton ?? ((item) => item.querySelector("button"));
129
+ this.#getButton = options.getButton ?? ((item) => matchingPart(item, "button"));
103
130
  this.#onRemove = options.onRemove;
104
131
  this.#focusAfterEnd = options.focusAfterEnd;
105
132
  }
@@ -224,11 +251,19 @@ var ChipRow = class {
224
251
  };
225
252
 
226
253
  // src/utils/composition_tracker.ts
254
+ var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
255
+ "insertCompositionText",
256
+ "insertFromComposition",
257
+ "deleteCompositionText",
258
+ "deleteByComposition"
259
+ ]);
227
260
  var CompositionTracker = class {
228
261
  #observedTargets = /* @__PURE__ */ new Set();
229
262
  #activeTargets = /* @__PURE__ */ new Set();
230
263
  #onStart;
231
264
  #onEnd;
265
+ /** The field whose confirming `input` is still owed, while the window is open. */
266
+ #confirmedTarget = null;
232
267
  constructor(options = {}) {
233
268
  this.#onStart = options.onStart;
234
269
  this.#onEnd = options.onEnd;
@@ -238,6 +273,7 @@ var CompositionTracker = class {
238
273
  if (this.#observedTargets.has(target)) return;
239
274
  target.addEventListener("compositionstart", this.#handleStart);
240
275
  target.addEventListener("compositionend", this.#handleEnd);
276
+ target.addEventListener("keydown", this.#handleKeydown);
241
277
  this.#observedTargets.add(target);
242
278
  }
243
279
  /** Stops tracking one target and clears any active composition it owned. */
@@ -245,31 +281,77 @@ var CompositionTracker = class {
245
281
  if (!this.#observedTargets.delete(target)) return;
246
282
  target.removeEventListener("compositionstart", this.#handleStart);
247
283
  target.removeEventListener("compositionend", this.#handleEnd);
284
+ target.removeEventListener("keydown", this.#handleKeydown);
248
285
  this.#activeTargets.delete(target);
286
+ if (this.#confirmedTarget === target) this.#confirmedTarget = null;
249
287
  }
250
288
  /** Releases every listener and clears state so reconnect starts cleanly. */
251
289
  disconnect() {
252
290
  for (const target of this.#observedTargets) {
253
291
  target.removeEventListener("compositionstart", this.#handleStart);
254
292
  target.removeEventListener("compositionend", this.#handleEnd);
293
+ target.removeEventListener("keydown", this.#handleKeydown);
255
294
  }
256
295
  this.#observedTargets.clear();
257
296
  this.#activeTargets.clear();
297
+ this.#confirmedTarget = null;
258
298
  }
259
299
  /** True when lifecycle tracking or the current event reports composition. */
260
300
  isComposing(event) {
261
301
  return this.#activeTargets.size > 0 || event?.isComposing === true;
262
302
  }
303
+ /**
304
+ * Whether `event` is the `input` echoing the composition just confirmed.
305
+ *
306
+ * Asking closes the window either way, so one confirmation is folded at most
307
+ * once and a consumer asks once per `input`.
308
+ */
309
+ consumesConfirmedInput(event) {
310
+ const confirmed = this.#confirmedTarget;
311
+ this.#confirmedTarget = null;
312
+ if (confirmed === null || confirmed !== event.target) return false;
313
+ const inputType = event.inputType;
314
+ return !inputType || COMPOSITION_INPUT_TYPES.has(inputType);
315
+ }
263
316
  #handleStart = (event) => {
317
+ this.#confirmedTarget = null;
264
318
  if (event.currentTarget) this.#activeTargets.add(event.currentTarget);
265
319
  this.#onStart?.(event);
266
320
  };
267
321
  #handleEnd = (event) => {
268
322
  if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);
323
+ this.#confirmedTarget = event.target;
269
324
  this.#onEnd?.(event);
270
325
  };
326
+ /** A key on an observed field opens an edit of its own, so no echo is owed. */
327
+ #handleKeydown = () => {
328
+ this.#confirmedTarget = null;
329
+ };
271
330
  };
272
331
 
332
+ // src/utils/field_mirror.ts
333
+ function writeFields(container, values, { name, form = "" }) {
334
+ const current = [...container.children];
335
+ const submits = current.length === values.length && current.every(
336
+ (child, index) => child instanceof HTMLInputElement && child.type === "hidden" && child.value === values[index] && child.name === name && (child.getAttribute("form") ?? "") === form
337
+ );
338
+ if (submits) return false;
339
+ container.replaceChildren(
340
+ ...values.map((value) => {
341
+ const input = document.createElement("input");
342
+ input.type = "hidden";
343
+ input.name = name;
344
+ input.value = value;
345
+ if (form !== "") input.setAttribute("form", form);
346
+ return input;
347
+ })
348
+ );
349
+ return true;
350
+ }
351
+ function commitField(target) {
352
+ target.dispatchEvent(new Event("change", { bubbles: true }));
353
+ }
354
+
273
355
  // src/utils/microtask_coalescer.ts
274
356
  var MicrotaskCoalescer = class {
275
357
  #run;
@@ -315,6 +397,47 @@ function scrollOptionIntoView(list, option) {
315
397
  }
316
398
  }
317
399
 
400
+ // src/utils/state_regions.ts
401
+ var StateRegions = class {
402
+ #whenTrue;
403
+ #whenFalse;
404
+ #taken = /* @__PURE__ */ new WeakSet();
405
+ constructor(options) {
406
+ this.#whenTrue = options.whenTrue;
407
+ this.#whenFalse = options.whenFalse ?? null;
408
+ }
409
+ /** Shows the regions inside `host` that belong to `isTrue` and hides the others. */
410
+ reflect(host, isTrue) {
411
+ const shown = this.#inside(host, this.#whenTrue());
412
+ if (!this.#whenFalse) {
413
+ for (const region of shown) this.#write(region, !isTrue);
414
+ return;
415
+ }
416
+ const hiddenSide = this.#inside(host, this.#whenFalse());
417
+ if (shown.length === 0 || hiddenSide.length === 0) {
418
+ for (const region of shown) this.#give(region);
419
+ for (const region of hiddenSide) this.#give(region);
420
+ return;
421
+ }
422
+ for (const region of shown) this.#write(region, !isTrue);
423
+ for (const region of hiddenSide) this.#write(region, isTrue);
424
+ }
425
+ /** Writes `hidden` where it moves, noting which regions it takes out of view. */
426
+ #write(region, hidden) {
427
+ if (hidden) this.#taken.add(region);
428
+ else this.#taken.delete(region);
429
+ if (region.hidden !== hidden) region.hidden = hidden;
430
+ }
431
+ /** Returns a region this instance hid; one it never hid keeps what it carries. */
432
+ #give(region) {
433
+ if (this.#taken.delete(region)) region.hidden = false;
434
+ }
435
+ /** The declared regions that sit within `host`, which may be the host itself. */
436
+ #inside(host, regions) {
437
+ return regions.filter((region) => host.contains(region));
438
+ }
439
+ };
440
+
318
441
  // src/utils/before_cache_reset.ts
319
442
  var BeforeCacheReset = class _BeforeCacheReset {
320
443
  /** Every subscribed instance, iterated by the one shared document listener. */
@@ -375,6 +498,85 @@ var TabindexLoan = class {
375
498
  }
376
499
  };
377
500
 
501
+ // src/utils/target_selector.ts
502
+ function targetSelector(identifier, name) {
503
+ return `[data-${identifier}-target~="${name}"]`;
504
+ }
505
+
506
+ // src/utils/template_row.ts
507
+ function cloneTemplateRoot(template) {
508
+ const root = template.content.firstElementChild;
509
+ return root instanceof HTMLElement ? root.cloneNode(true) : null;
510
+ }
511
+ var TemplateRow = class {
512
+ #options;
513
+ #warned = false;
514
+ constructor(options) {
515
+ this.#options = options;
516
+ }
517
+ /** The attribute selector for one declared part, in this controller's namespace. */
518
+ selector(name) {
519
+ return targetSelector(this.#options.identifier, name);
520
+ }
521
+ /** Re-arms the once-per-connection diagnostic. */
522
+ connect() {
523
+ this.#warned = false;
524
+ }
525
+ /**
526
+ * Names one missing part on the console, at most once per connection, and
527
+ * returns `null` so a caller can hand it straight back.
528
+ */
529
+ report(missing) {
530
+ return this.#say(`lacks ${missing}`);
531
+ }
532
+ /** Writes one diagnostic per connection, saying what the template got wrong. */
533
+ #say(problem) {
534
+ if (this.#warned) return null;
535
+ this.#warned = true;
536
+ const { identifier, outcome, noun } = this.#options;
537
+ console.warn(`Stimeo UI: "${identifier}" ${outcome} because its ${noun} ${problem}.`);
538
+ return null;
539
+ }
540
+ /**
541
+ * Clones the row and resolves its parts, or names what the template got wrong
542
+ * and returns `null`. `values` fills the button's authored `aria-label`.
543
+ */
544
+ instantiate(template, values) {
545
+ const { root: rootName, required, optional, button: buttonName } = this.#options;
546
+ const root = cloneTemplateRoot(template);
547
+ if (!root?.matches(this.selector(rootName))) {
548
+ return this.report(`${this.#article(rootName)} "${rootName}" root`);
549
+ }
550
+ const count = template.content.children.length;
551
+ if (count !== 1) {
552
+ return this.#say(`holds ${count} elements, and the row has to be its only one`);
553
+ }
554
+ const slots = {};
555
+ for (const name2 of required) {
556
+ const part = this.#resolve(root, name2);
557
+ if (!part) return this.report(`${this.#article(name2)} "${name2}" target`);
558
+ slots[name2] = part;
559
+ }
560
+ for (const name2 of optional ?? []) slots[name2] = this.#resolve(root, name2);
561
+ const named = this.#resolve(root, buttonName);
562
+ if (!(named instanceof HTMLButtonElement)) {
563
+ return this.report(`${this.#article(buttonName)} "${buttonName}" target <button>`);
564
+ }
565
+ const button = named;
566
+ const name = button.getAttribute("aria-label")?.trim() ?? "";
567
+ if (name === "") return this.report(`a non-empty aria-label on its "${buttonName}" target`);
568
+ button.setAttribute("aria-label", fillTemplate(name, values));
569
+ return { root, slots, button };
570
+ }
571
+ /** The row may be its own part; otherwise the first descendant carrying the target. */
572
+ #resolve(root, name) {
573
+ return matchingPart(root, this.selector(name));
574
+ }
575
+ #article(name) {
576
+ return /^[aeiou]/i.test(name) ? "an" : "a";
577
+ }
578
+ };
579
+
378
580
  // src/controllers/multi_select_controller.ts
379
581
  var MultiSelectController = class extends Controller {
380
582
  static targets = [
@@ -386,7 +588,8 @@ var MultiSelectController = class extends Controller {
386
588
  "tagTemplate",
387
589
  "label",
388
590
  "remove",
389
- "fields"
591
+ "fields",
592
+ "empty"
390
593
  ];
391
594
  static values = {
392
595
  max: { type: Number, default: 0 },
@@ -405,36 +608,37 @@ var MultiSelectController = class extends Controller {
405
608
  #selectionValues = [];
406
609
  /** Whether initial normalization finished, so a fields callback cannot mirror stale state. */
407
610
  #connected = false;
611
+ /** Owns `hidden` on the regions declared for the empty-result state. */
612
+ #emptyRegion = new StateRegions({ whenTrue: () => this.emptyTargets });
408
613
  /** Collapses one batch of target callbacks into a single final-DOM reconciliation. */
409
614
  #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());
410
- /** Absorbs the browser's redundant final input after compositionend. */
411
- #ignorePostCompositionInput = false;
412
615
  /** Owns IME lifecycle state; confirmed text emits one filter result. */
413
616
  #composition = new CompositionTracker({
414
- onStart: () => {
415
- this.#ignorePostCompositionInput = false;
416
- },
417
- onEnd: () => {
418
- this.#ignorePostCompositionInput = true;
419
- queueMicrotask(() => {
420
- this.#ignorePostCompositionInput = false;
421
- });
422
- this.filter();
423
- }
617
+ // Apply the confirmed query even in browsers that omit a final input event;
618
+ // where one follows, the tracker folds it so consumers receive one filter
619
+ // event for the confirmed text, not two.
620
+ onEnd: () => this.filter()
621
+ });
622
+ /** Builds one chip from the authored template and owns its diagnostic. */
623
+ #rows = new TemplateRow({
624
+ identifier: this.identifier,
625
+ root: "tag",
626
+ required: ["label"],
627
+ button: "remove",
628
+ outcome: "changed no selection",
629
+ noun: "chip template"
424
630
  });
425
- /** Whether this connection already reported its unusable chip template. */
426
- #warnedTemplate = false;
427
631
  /** Shared delegated interaction for the replaceable row of removable chips. */
428
632
  #chipRow = new ChipRow({
429
633
  directionElement: this.element,
430
634
  getItems: () => this.tagTargets,
431
- getButton: (tag) => tag.querySelector('button[data-stimeo--multi-select-target~="remove"]'),
635
+ getButton: (tag) => matchingPart(tag, `button${this.#rows.selector("remove")}`),
432
636
  onRemove: (index) => this.#removeTagAt(index),
433
637
  focusAfterEnd: () => this.#focusInput()
434
638
  });
435
639
  /** Starts closed, syncs chips for any pre-selected options, and listens out. */
436
640
  connect() {
437
- this.#warnedTemplate = false;
641
+ this.#rows.connect();
438
642
  this.#normalizeSelection([]);
439
643
  this.close();
440
644
  if (this.hasTagsTarget) {
@@ -454,14 +658,14 @@ var MultiSelectController = class extends Controller {
454
658
  */
455
659
  #rebuildTags() {
456
660
  if (!this.hasTagsTarget || !this.hasTagTemplateTarget) return;
457
- const fragments = [];
661
+ const chips = [];
458
662
  for (const option of this.#selectedOptions) {
459
- const fragment = this.#buildTag(option);
460
- if (!fragment) return;
461
- fragments.push(fragment);
663
+ const chip = this.#buildTag(option);
664
+ if (!chip) return;
665
+ chips.push(chip);
462
666
  }
463
667
  for (const tag of this.tagTargets) tag.remove();
464
- this.tagsTarget.append(...fragments);
668
+ this.tagsTarget.append(...chips);
465
669
  this.#chipRow.ensureTabStop();
466
670
  }
467
671
  /** Tears down document and chip listeners on disconnect (Turbo included). */
@@ -469,7 +673,6 @@ var MultiSelectController = class extends Controller {
469
673
  this.#connected = false;
470
674
  this.#reconcile.cancel();
471
675
  this.#composition.disconnect();
472
- this.#ignorePostCompositionInput = false;
473
676
  this.#chipRow.disconnect();
474
677
  document.removeEventListener("click", this.#onOutsideClick, true);
475
678
  this.#releaseTabindex();
@@ -483,6 +686,10 @@ var MultiSelectController = class extends Controller {
483
686
  option.removeAttribute("data-active");
484
687
  this.#scheduleOptionReconcile();
485
688
  }
689
+ /** Settles a region inserted after connect on the side of the current state. */
690
+ emptyTargetConnected() {
691
+ if (this.#connected) this.#reflectEmpty();
692
+ }
486
693
  /** Rebinds delegated chip interaction when Turbo replaces the tags container. */
487
694
  tagsTargetConnected(tags) {
488
695
  this.#chipRow.connect(tags);
@@ -524,6 +731,8 @@ var MultiSelectController = class extends Controller {
524
731
  * for an unrelated option would drop focus from a chip's remove button to
525
732
  * `<body>`, losing the keyboard user's place for something that did not concern
526
733
  * them.
734
+ *
735
+ * @stimeoRenderRoot
527
736
  */
528
737
  #reconcileOptions() {
529
738
  const visible = this.#visibleOptions;
@@ -535,7 +744,7 @@ var MultiSelectController = class extends Controller {
535
744
  const previousLabels = new Map(
536
745
  this.tagTargets.map((tag) => [
537
746
  tag.dataset.value ?? "",
538
- (tag.querySelector('[data-stimeo--multi-select-target~="label"]')?.textContent ?? "").trim()
747
+ readLabel(matchingPart(tag, this.#rows.selector("label")))
539
748
  ])
540
749
  );
541
750
  const priority = [
@@ -613,15 +822,11 @@ var MultiSelectController = class extends Controller {
613
822
  /** Removes composition listeners when the active input is replaced or removed. */
614
823
  inputTargetDisconnected(input) {
615
824
  this.#composition.unobserve(input);
616
- this.#ignorePostCompositionInput = false;
617
825
  }
618
826
  /** Filters confirmed input text, opens, and re-seeds the active option. */
619
827
  filter(event) {
620
828
  if (!this.hasInputTarget) return;
621
- if (event && this.#ignorePostCompositionInput) {
622
- this.#ignorePostCompositionInput = false;
623
- return;
624
- }
829
+ if (event && this.#composition.consumesConfirmedInput(event)) return;
625
830
  if (this.#composition.isComposing(event)) return;
626
831
  const query = this.inputTarget.value.trim().toLowerCase();
627
832
  for (const option of this.optionTargets) {
@@ -639,7 +844,8 @@ var MultiSelectController = class extends Controller {
639
844
  *
640
845
  * Needs the input, which owns `aria-expanded` and `aria-activedescendant`: a
641
846
  * list shown without one is a popup no assistive technology is told about. So
642
- * opening is skipped entirely, where {@link close} still closes.
847
+ * opening is skipped entirely, where {@link MultiSelectController.close | close}
848
+ * still closes.
643
849
  */
644
850
  open() {
645
851
  if (!this.hasListTarget || !this.hasInputTarget) {
@@ -811,7 +1017,7 @@ var MultiSelectController = class extends Controller {
811
1017
  option.setAttribute("aria-selected", "true");
812
1018
  }
813
1019
  this.#refreshRoving();
814
- this.#syncFields();
1020
+ this.#syncFields(true);
815
1021
  const values = this.#values;
816
1022
  this.#selectionValues = values;
817
1023
  this.#announceTransition(
@@ -836,79 +1042,47 @@ var MultiSelectController = class extends Controller {
836
1042
  for (const tag of this.tagTargets) {
837
1043
  const option = options.get(tag.dataset.value ?? "");
838
1044
  const text = this.#optionLabel(option);
839
- const label = tag.querySelector('[data-stimeo--multi-select-target~="label"]');
840
- const button = tag.querySelector(
841
- 'button[data-stimeo--multi-select-target~="remove"]'
842
- );
1045
+ const label = matchingPart(tag, this.#rows.selector("label"));
1046
+ const button = matchingPart(tag, `button${this.#rows.selector("remove")}`);
843
1047
  const name = this.#removeName(text, this.#optionValue(option));
844
1048
  if (!label || !button || !name) continue;
845
- if (label.textContent !== text) label.textContent = text;
1049
+ if (readLabel(label) !== text) writeLabel(label, text);
846
1050
  if (button.getAttribute("aria-label") !== name) button.setAttribute("aria-label", name);
847
1051
  }
848
1052
  }
849
1053
  /** Builds one chip from the template for `option`. */
850
1054
  #appendTag(option) {
851
1055
  if (!this.hasTagsTarget) {
852
- this.#warnTemplate('a "tags" target to append the chip to');
1056
+ this.#rows.report('a "tags" target to append the chip to');
853
1057
  return false;
854
1058
  }
855
- const fragment = this.#buildTag(option);
856
- if (!fragment) return false;
857
- this.tagsTarget.appendChild(fragment);
1059
+ const chip = this.#buildTag(option);
1060
+ if (!chip) return false;
1061
+ this.tagsTarget.appendChild(chip);
858
1062
  return true;
859
1063
  }
860
1064
  /**
861
1065
  * Builds one fully named chip without mutating the live tag row, or `null`
862
- * when the authored template cannot produce one. Both callers establish the
863
- * template first: a field authored without `tagTemplate` renders no chips at
864
- * all — a supported configuration — and never reaches here.
1066
+ * when the authored template cannot produce one — in which case the selection
1067
+ * stays untouched and the row names the missing part once per connection.
1068
+ * Both callers establish the template first: a field authored without
1069
+ * `tagTemplate` renders no chips at all — a supported configuration — and
1070
+ * never reaches here.
865
1071
  */
866
1072
  #buildTag(option) {
867
- const fragment = this.tagTemplateTarget.content.cloneNode(true);
868
- const tag = fragment.querySelector('[data-stimeo--multi-select-target~="tag"]');
869
- const label = fragment.querySelector(
870
- '[data-stimeo--multi-select-target~="label"]'
871
- );
872
- const button = fragment.querySelector(
873
- 'button[data-stimeo--multi-select-target~="remove"]'
874
- );
875
1073
  const text = this.#optionLabel(option);
876
1074
  const value = this.#optionValue(option);
877
- const removeName = button?.getAttribute("aria-label")?.trim() ?? "";
878
- if (!tag) return this.#warnTemplate('a "tag" target');
879
- if (!label) return this.#warnTemplate('a "label" target');
880
- if (!button) return this.#warnTemplate('a "remove" target <button>');
881
- if (removeName === "") {
882
- return this.#warnTemplate('a non-empty aria-label on its "remove" target');
883
- }
884
- tag.dataset.value = value;
885
- label.textContent = text;
886
- button.setAttribute("aria-label", fillTemplate(removeName, { label: text, value }));
887
- button.tabIndex = -1;
888
- return fragment;
889
- }
890
- /**
891
- * Reports an unusable chip template to the author, once per connection.
892
- *
893
- * The selection itself stays untouched — no `aria-selected`, chip, hidden
894
- * field, announcement, or event moves. Without this line the only symptom is
895
- * a listbox whose options refuse to select, and the two causes the Inspector
896
- * cannot see statically (a name that renders empty from a missing
897
- * translation, a server-rendered template) would have no diagnostic anywhere.
898
- */
899
- #warnTemplate(missing) {
900
- if (!this.#warnedTemplate) {
901
- this.#warnedTemplate = true;
902
- console.warn(
903
- `Stimeo UI: "${this.identifier}" changed no selection because its chip template lacks ${missing}.`
904
- );
905
- }
906
- return null;
1075
+ const row = this.#rows.instantiate(this.tagTemplateTarget, { label: text, value });
1076
+ if (!row) return null;
1077
+ row.root.dataset.value = value;
1078
+ writeLabel(row.slots.label, text);
1079
+ row.button.tabIndex = -1;
1080
+ return row.root;
907
1081
  }
908
1082
  /** Expands the current template's localized remove-button name. */
909
1083
  #removeName(label, value) {
910
1084
  if (!this.hasTagTemplateTarget) return null;
911
- const template = this.tagTemplateTarget.content.querySelector('button[data-stimeo--multi-select-target~="remove"]')?.getAttribute("aria-label")?.trim();
1085
+ const template = this.tagTemplateTarget.content.querySelector(`button${this.#rows.selector("remove")}`)?.getAttribute("aria-label")?.trim();
912
1086
  return template ? fillTemplate(template, { label, value }) : null;
913
1087
  }
914
1088
  /** Removes the chip mirroring `option`, if present. */
@@ -926,7 +1100,7 @@ var MultiSelectController = class extends Controller {
926
1100
  if (option) option.setAttribute("aria-selected", "false");
927
1101
  tag.remove();
928
1102
  this.#refreshRoving();
929
- this.#syncFields();
1103
+ this.#syncFields(true);
930
1104
  const values = this.#values;
931
1105
  this.#selectionValues = values;
932
1106
  this.#announceTransition(
@@ -947,9 +1121,9 @@ var MultiSelectController = class extends Controller {
947
1121
  * `aria-activedescendant` (the attribute is removed, not emptied, when null).
948
1122
  *
949
1123
  * The state half runs even with no input, so a `close()` that cannot touch ARIA
950
- * still clears it: {@link open} seeds an active option only when there is none,
951
- * so a stale one makes the next open skip the seeding and a replacement input
952
- * gets no `aria-activedescendant` at all.
1124
+ * still clears it: {@link MultiSelectController.open | open} seeds an active
1125
+ * option only when there is none, so a stale one makes the next open skip the
1126
+ * seeding and a replacement input gets no `aria-activedescendant` at all.
953
1127
  */
954
1128
  #setActive(option) {
955
1129
  const activeId = option ? ensureId(option, "stimeo-ms-opt") : null;
@@ -977,12 +1151,14 @@ var MultiSelectController = class extends Controller {
977
1151
  this.#setActive(active);
978
1152
  }
979
1153
  }
980
- /** Reflects whether the open list currently has no visible option targets. */
1154
+ /**
1155
+ * Reflects whether the open list currently has no visible option targets, on
1156
+ * the root's state attribute and on the regions declared for that state.
1157
+ */
981
1158
  #reflectEmpty() {
982
- this.element.toggleAttribute(
983
- "data-stimeo--multi-select-empty",
984
- !this.#isClosed && this.#visibleOptions.length === 0
985
- );
1159
+ const empty = !this.#isClosed && this.#visibleOptions.length === 0;
1160
+ this.element.toggleAttribute(`data-${this.identifier}-empty`, empty);
1161
+ this.#emptyRegion.reflect(this.element, empty);
986
1162
  }
987
1163
  /**
988
1164
  * Mirrors the selected values into named hidden inputs under the `fields`
@@ -991,24 +1167,22 @@ var MultiSelectController = class extends Controller {
991
1167
  * `form` value is set, each input gets a matching `form` attribute so the picker
992
1168
  * can submit with a `<form>` it lives outside of.
993
1169
  */
994
- #syncFields() {
1170
+ #syncFields(notify = false) {
995
1171
  if (!this.hasFieldsTarget) return;
996
- this.fieldsTarget.replaceChildren(
997
- ...this.#values.map((value) => {
998
- const input = document.createElement("input");
999
- input.type = "hidden";
1000
- input.name = this.nameValue;
1001
- input.value = value;
1002
- if (this.hasFormValue && this.formValue !== "") input.setAttribute("form", this.formValue);
1003
- return input;
1004
- })
1005
- );
1172
+ const options = { name: this.nameValue, form: this.formValue };
1173
+ if (writeFields(this.fieldsTarget, this.#values, options) && notify) {
1174
+ commitField(this.fieldsTarget);
1175
+ }
1006
1176
  }
1007
1177
  /** Keeps exactly one chip remove button tabbable after the set changes. */
1008
1178
  #refreshRoving() {
1009
1179
  this.#chipRow.ensureTabStop();
1010
1180
  }
1011
- /** Sends one localized selection transition through the page's shared announcer. */
1181
+ /**
1182
+ * Sends one localized selection transition through the page's shared announcer.
1183
+ *
1184
+ * @stimeoRuntimeOnly The texts word the one announcement of this change.
1185
+ */
1012
1186
  #announceTransition(selected, label, value, count) {
1013
1187
  const template = selected ? this.announceTextValue : this.announceRemovedTextValue;
1014
1188
  announce(fillTemplate(template, { label, value, count }));