stimeo-ui 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (332) hide show
  1. package/CHANGELOG.md +209 -0
  2. package/README.md +128 -1
  3. package/dist/cable/index.d.ts +79 -23
  4. package/dist/cable/index.js +392 -417
  5. package/dist/cable/index.js.map +1 -1
  6. package/dist/controllers/accordion_controller.d.ts +34 -5
  7. package/dist/controllers/accordion_controller.js +228 -28
  8. package/dist/controllers/accordion_controller.js.map +1 -1
  9. package/dist/controllers/alert_dialog_controller.d.ts +20 -9
  10. package/dist/controllers/alert_dialog_controller.js +862 -221
  11. package/dist/controllers/alert_dialog_controller.js.map +1 -1
  12. package/dist/controllers/announcer_controller.d.ts +38 -7
  13. package/dist/controllers/announcer_controller.js +135 -191
  14. package/dist/controllers/announcer_controller.js.map +1 -1
  15. package/dist/controllers/aspect_ratio_controller.js +0 -10
  16. package/dist/controllers/aspect_ratio_controller.js.map +1 -1
  17. package/dist/controllers/auto_submit_controller.d.ts +30 -6
  18. package/dist/controllers/auto_submit_controller.js +295 -110
  19. package/dist/controllers/auto_submit_controller.js.map +1 -1
  20. package/dist/controllers/avatar_controller.js +172 -106
  21. package/dist/controllers/avatar_controller.js.map +1 -1
  22. package/dist/controllers/breadcrumb_controller.d.ts +10 -7
  23. package/dist/controllers/breadcrumb_controller.js +0 -129
  24. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  25. package/dist/controllers/bulk_select_controller.d.ts +22 -4
  26. package/dist/controllers/bulk_select_controller.js +264 -53
  27. package/dist/controllers/bulk_select_controller.js.map +1 -1
  28. package/dist/controllers/calendar_controller.d.ts +109 -28
  29. package/dist/controllers/calendar_controller.js +346 -180
  30. package/dist/controllers/calendar_controller.js.map +1 -1
  31. package/dist/controllers/carousel_controller.d.ts +99 -27
  32. package/dist/controllers/carousel_controller.js +587 -300
  33. package/dist/controllers/carousel_controller.js.map +1 -1
  34. package/dist/controllers/character_counter_controller.d.ts +18 -2
  35. package/dist/controllers/character_counter_controller.js +271 -143
  36. package/dist/controllers/character_counter_controller.js.map +1 -1
  37. package/dist/controllers/checkbox_controller.d.ts +7 -2
  38. package/dist/controllers/checkbox_controller.js +77 -75
  39. package/dist/controllers/checkbox_controller.js.map +1 -1
  40. package/dist/controllers/clipboard_controller.d.ts +44 -7
  41. package/dist/controllers/clipboard_controller.js +187 -108
  42. package/dist/controllers/clipboard_controller.js.map +1 -1
  43. package/dist/controllers/collapsible_controller.d.ts +41 -5
  44. package/dist/controllers/collapsible_controller.js +374 -108
  45. package/dist/controllers/collapsible_controller.js.map +1 -1
  46. package/dist/controllers/color_picker_controller.d.ts +47 -14
  47. package/dist/controllers/color_picker_controller.js +183 -127
  48. package/dist/controllers/color_picker_controller.js.map +1 -1
  49. package/dist/controllers/combobox_controller.d.ts +33 -6
  50. package/dist/controllers/combobox_controller.js +426 -109
  51. package/dist/controllers/combobox_controller.js.map +1 -1
  52. package/dist/controllers/command_palette_controller.d.ts +45 -14
  53. package/dist/controllers/command_palette_controller.js +994 -350
  54. package/dist/controllers/command_palette_controller.js.map +1 -1
  55. package/dist/controllers/conditional_fields_controller.d.ts +15 -2
  56. package/dist/controllers/conditional_fields_controller.js +212 -125
  57. package/dist/controllers/conditional_fields_controller.js.map +1 -1
  58. package/dist/controllers/confirm_controller.d.ts +49 -12
  59. package/dist/controllers/confirm_controller.js +939 -245
  60. package/dist/controllers/confirm_controller.js.map +1 -1
  61. package/dist/controllers/context_menu_controller.d.ts +36 -3
  62. package/dist/controllers/context_menu_controller.js +275 -127
  63. package/dist/controllers/context_menu_controller.js.map +1 -1
  64. package/dist/controllers/count_up_controller.d.ts +13 -1
  65. package/dist/controllers/count_up_controller.js +73 -21
  66. package/dist/controllers/count_up_controller.js.map +1 -1
  67. package/dist/controllers/countdown_controller.d.ts +75 -2
  68. package/dist/controllers/countdown_controller.js +235 -97
  69. package/dist/controllers/countdown_controller.js.map +1 -1
  70. package/dist/controllers/currency_input_controller.d.ts +93 -15
  71. package/dist/controllers/currency_input_controller.js +241 -118
  72. package/dist/controllers/currency_input_controller.js.map +1 -1
  73. package/dist/controllers/data_grid_controller.d.ts +78 -22
  74. package/dist/controllers/data_grid_controller.js +229 -135
  75. package/dist/controllers/data_grid_controller.js.map +1 -1
  76. package/dist/controllers/date_range_picker_controller.d.ts +64 -11
  77. package/dist/controllers/date_range_picker_controller.js +359 -165
  78. package/dist/controllers/date_range_picker_controller.js.map +1 -1
  79. package/dist/controllers/dialog_controller.d.ts +22 -3
  80. package/dist/controllers/dialog_controller.js +887 -225
  81. package/dist/controllers/dialog_controller.js.map +1 -1
  82. package/dist/controllers/direct_upload_controller.d.ts +24 -7
  83. package/dist/controllers/direct_upload_controller.js +80 -142
  84. package/dist/controllers/direct_upload_controller.js.map +1 -1
  85. package/dist/controllers/dirty_form_controller.d.ts +2 -2
  86. package/dist/controllers/dirty_form_controller.js +26 -47
  87. package/dist/controllers/dirty_form_controller.js.map +1 -1
  88. package/dist/controllers/dismissible_controller.d.ts +19 -0
  89. package/dist/controllers/dismissible_controller.js +175 -20
  90. package/dist/controllers/dismissible_controller.js.map +1 -1
  91. package/dist/controllers/drawer_controller.d.ts +52 -16
  92. package/dist/controllers/drawer_controller.js +963 -336
  93. package/dist/controllers/drawer_controller.js.map +1 -1
  94. package/dist/controllers/dropdown_controller.d.ts +39 -3
  95. package/dist/controllers/dropdown_controller.js +342 -85
  96. package/dist/controllers/dropdown_controller.js.map +1 -1
  97. package/dist/controllers/editable_controller.d.ts +12 -1
  98. package/dist/controllers/editable_controller.js +182 -58
  99. package/dist/controllers/editable_controller.js.map +1 -1
  100. package/dist/controllers/empty_state_controller.js +67 -53
  101. package/dist/controllers/empty_state_controller.js.map +1 -1
  102. package/dist/controllers/file_dropzone_controller.d.ts +60 -18
  103. package/dist/controllers/file_dropzone_controller.js +426 -229
  104. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  105. package/dist/controllers/filter_controller.d.ts +10 -4
  106. package/dist/controllers/filter_controller.js +78 -36
  107. package/dist/controllers/filter_controller.js.map +1 -1
  108. package/dist/controllers/flash_controller.d.ts +57 -14
  109. package/dist/controllers/flash_controller.js +328 -211
  110. package/dist/controllers/flash_controller.js.map +1 -1
  111. package/dist/controllers/focus_controller.d.ts +12 -3
  112. package/dist/controllers/focus_controller.js +654 -239
  113. package/dist/controllers/focus_controller.js.map +1 -1
  114. package/dist/controllers/form_field_controller.js +153 -136
  115. package/dist/controllers/form_field_controller.js.map +1 -1
  116. package/dist/controllers/form_validation_controller.js +29 -96
  117. package/dist/controllers/form_validation_controller.js.map +1 -1
  118. package/dist/controllers/frame_loading_controller.d.ts +41 -9
  119. package/dist/controllers/frame_loading_controller.js +263 -198
  120. package/dist/controllers/frame_loading_controller.js.map +1 -1
  121. package/dist/controllers/highlight_controller.d.ts +22 -2
  122. package/dist/controllers/highlight_controller.js +119 -70
  123. package/dist/controllers/highlight_controller.js.map +1 -1
  124. package/dist/controllers/hover_card_controller.d.ts +65 -4
  125. package/dist/controllers/hover_card_controller.js +335 -118
  126. package/dist/controllers/hover_card_controller.js.map +1 -1
  127. package/dist/controllers/idle_controller.d.ts +58 -4
  128. package/dist/controllers/idle_controller.js +316 -65
  129. package/dist/controllers/idle_controller.js.map +1 -1
  130. package/dist/controllers/input_mask_controller.d.ts +5 -2
  131. package/dist/controllers/input_mask_controller.js +116 -61
  132. package/dist/controllers/input_mask_controller.js.map +1 -1
  133. package/dist/controllers/intersection_controller.d.ts +32 -13
  134. package/dist/controllers/intersection_controller.js +147 -103
  135. package/dist/controllers/intersection_controller.js.map +1 -1
  136. package/dist/controllers/lazy_frame_controller.d.ts +16 -0
  137. package/dist/controllers/lazy_frame_controller.js +9 -56
  138. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  139. package/dist/controllers/listbox_controller.d.ts +84 -10
  140. package/dist/controllers/listbox_controller.js +413 -153
  141. package/dist/controllers/listbox_controller.js.map +1 -1
  142. package/dist/controllers/local_time_controller.d.ts +1 -1
  143. package/dist/controllers/local_time_controller.js +54 -64
  144. package/dist/controllers/local_time_controller.js.map +1 -1
  145. package/dist/controllers/masonry_controller.d.ts +11 -3
  146. package/dist/controllers/masonry_controller.js +70 -95
  147. package/dist/controllers/masonry_controller.js.map +1 -1
  148. package/dist/controllers/menu_controller.d.ts +40 -9
  149. package/dist/controllers/menu_controller.js +254 -167
  150. package/dist/controllers/menu_controller.js.map +1 -1
  151. package/dist/controllers/menubar_controller.d.ts +15 -4
  152. package/dist/controllers/menubar_controller.js +206 -331
  153. package/dist/controllers/menubar_controller.js.map +1 -1
  154. package/dist/controllers/meter_controller.d.ts +20 -0
  155. package/dist/controllers/meter_controller.js +159 -71
  156. package/dist/controllers/meter_controller.js.map +1 -1
  157. package/dist/controllers/multi_select_controller.d.ts +50 -7
  158. package/dist/controllers/multi_select_controller.js +797 -388
  159. package/dist/controllers/multi_select_controller.js.map +1 -1
  160. package/dist/controllers/navigation_menu_controller.d.ts +19 -1
  161. package/dist/controllers/navigation_menu_controller.js +156 -231
  162. package/dist/controllers/navigation_menu_controller.js.map +1 -1
  163. package/dist/controllers/nested_form_controller.d.ts +10 -0
  164. package/dist/controllers/nested_form_controller.js +373 -174
  165. package/dist/controllers/nested_form_controller.js.map +1 -1
  166. package/dist/controllers/network_status_controller.d.ts +23 -0
  167. package/dist/controllers/network_status_controller.js +241 -45
  168. package/dist/controllers/network_status_controller.js.map +1 -1
  169. package/dist/controllers/number_input_controller.d.ts +62 -10
  170. package/dist/controllers/number_input_controller.js +387 -227
  171. package/dist/controllers/number_input_controller.js.map +1 -1
  172. package/dist/controllers/optimistic_controller.js +28 -75
  173. package/dist/controllers/optimistic_controller.js.map +1 -1
  174. package/dist/controllers/otp_controller.d.ts +38 -7
  175. package/dist/controllers/otp_controller.js +344 -196
  176. package/dist/controllers/otp_controller.js.map +1 -1
  177. package/dist/controllers/overflow_indicator_controller.d.ts +13 -12
  178. package/dist/controllers/overflow_indicator_controller.js +167 -93
  179. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  180. package/dist/controllers/overflow_menu_controller.d.ts +66 -9
  181. package/dist/controllers/overflow_menu_controller.js +408 -277
  182. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  183. package/dist/controllers/pagination_controller.d.ts +52 -15
  184. package/dist/controllers/pagination_controller.js +342 -120
  185. package/dist/controllers/pagination_controller.js.map +1 -1
  186. package/dist/controllers/password_reveal_controller.d.ts +60 -9
  187. package/dist/controllers/password_reveal_controller.js +424 -99
  188. package/dist/controllers/password_reveal_controller.js.map +1 -1
  189. package/dist/controllers/password_strength_controller.d.ts +14 -5
  190. package/dist/controllers/password_strength_controller.js +99 -153
  191. package/dist/controllers/password_strength_controller.js.map +1 -1
  192. package/dist/controllers/persist_controller.d.ts +11 -4
  193. package/dist/controllers/persist_controller.js +252 -96
  194. package/dist/controllers/persist_controller.js.map +1 -1
  195. package/dist/controllers/pointer_drag_controller.d.ts +19 -7
  196. package/dist/controllers/pointer_drag_controller.js +263 -164
  197. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  198. package/dist/controllers/popover_controller.d.ts +46 -4
  199. package/dist/controllers/popover_controller.js +267 -94
  200. package/dist/controllers/popover_controller.js.map +1 -1
  201. package/dist/controllers/portal_controller.d.ts +44 -4
  202. package/dist/controllers/portal_controller.js +209 -72
  203. package/dist/controllers/portal_controller.js.map +1 -1
  204. package/dist/controllers/preview_guard_controller.d.ts +10 -4
  205. package/dist/controllers/preview_guard_controller.js +243 -112
  206. package/dist/controllers/preview_guard_controller.js.map +1 -1
  207. package/dist/controllers/progress_controller.d.ts +11 -0
  208. package/dist/controllers/progress_controller.js +132 -59
  209. package/dist/controllers/progress_controller.js.map +1 -1
  210. package/dist/controllers/radio_group_controller.d.ts +19 -8
  211. package/dist/controllers/radio_group_controller.js +138 -133
  212. package/dist/controllers/radio_group_controller.js.map +1 -1
  213. package/dist/controllers/range_slider_controller.d.ts +92 -4
  214. package/dist/controllers/range_slider_controller.js +245 -112
  215. package/dist/controllers/range_slider_controller.js.map +1 -1
  216. package/dist/controllers/rating_controller.d.ts +33 -6
  217. package/dist/controllers/rating_controller.js +429 -194
  218. package/dist/controllers/rating_controller.js.map +1 -1
  219. package/dist/controllers/read_more_controller.d.ts +26 -2
  220. package/dist/controllers/read_more_controller.js +185 -47
  221. package/dist/controllers/read_more_controller.js.map +1 -1
  222. package/dist/controllers/reading_progress_controller.d.ts +1 -1
  223. package/dist/controllers/reading_progress_controller.js +147 -135
  224. package/dist/controllers/reading_progress_controller.js.map +1 -1
  225. package/dist/controllers/relative_time_controller.d.ts +18 -4
  226. package/dist/controllers/relative_time_controller.js +145 -110
  227. package/dist/controllers/relative_time_controller.js.map +1 -1
  228. package/dist/controllers/reset_on_restore_controller.d.ts +85 -0
  229. package/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +97 -38
  230. package/dist/controllers/reset_on_restore_controller.js.map +1 -0
  231. package/dist/controllers/resizable_controller.d.ts +44 -4
  232. package/dist/controllers/resizable_controller.js +294 -118
  233. package/dist/controllers/resizable_controller.js.map +1 -1
  234. package/dist/controllers/roving_controller.js +39 -84
  235. package/dist/controllers/roving_controller.js.map +1 -1
  236. package/dist/controllers/scroll_area_controller.d.ts +7 -3
  237. package/dist/controllers/scroll_area_controller.js +428 -221
  238. package/dist/controllers/scroll_area_controller.js.map +1 -1
  239. package/dist/controllers/scroll_restore_controller.js +0 -53
  240. package/dist/controllers/scroll_restore_controller.js.map +1 -1
  241. package/dist/controllers/scroll_visibility_controller.d.ts +28 -12
  242. package/dist/controllers/scroll_visibility_controller.js +395 -146
  243. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  244. package/dist/controllers/scrollspy_controller.d.ts +9 -3
  245. package/dist/controllers/scrollspy_controller.js +181 -236
  246. package/dist/controllers/scrollspy_controller.js.map +1 -1
  247. package/dist/controllers/separator_controller.d.ts +59 -17
  248. package/dist/controllers/separator_controller.js +288 -132
  249. package/dist/controllers/separator_controller.js.map +1 -1
  250. package/dist/controllers/sidebar_controller.d.ts +81 -7
  251. package/dist/controllers/sidebar_controller.js +1071 -375
  252. package/dist/controllers/sidebar_controller.js.map +1 -1
  253. package/dist/controllers/skeleton_controller.d.ts +23 -0
  254. package/dist/controllers/skeleton_controller.js +248 -92
  255. package/dist/controllers/skeleton_controller.js.map +1 -1
  256. package/dist/controllers/slider_controller.d.ts +72 -8
  257. package/dist/controllers/slider_controller.js +211 -96
  258. package/dist/controllers/slider_controller.js.map +1 -1
  259. package/dist/controllers/smart_sticky_header_controller.d.ts +21 -3
  260. package/dist/controllers/smart_sticky_header_controller.js +163 -54
  261. package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
  262. package/dist/controllers/sortable_controller.js +13 -110
  263. package/dist/controllers/sortable_controller.js.map +1 -1
  264. package/dist/controllers/spinner_controller.d.ts +39 -4
  265. package/dist/controllers/spinner_controller.js +277 -161
  266. package/dist/controllers/spinner_controller.js.map +1 -1
  267. package/dist/controllers/step_indicator_controller.d.ts +24 -17
  268. package/dist/controllers/step_indicator_controller.js +107 -53
  269. package/dist/controllers/step_indicator_controller.js.map +1 -1
  270. package/dist/controllers/stepper_controller.d.ts +43 -9
  271. package/dist/controllers/stepper_controller.js +171 -48
  272. package/dist/controllers/stepper_controller.js.map +1 -1
  273. package/dist/controllers/stick_to_bottom_controller.d.ts +25 -0
  274. package/dist/controllers/stick_to_bottom_controller.js +310 -116
  275. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  276. package/dist/controllers/sticky_observer_controller.d.ts +11 -0
  277. package/dist/controllers/sticky_observer_controller.js +214 -49
  278. package/dist/controllers/sticky_observer_controller.js.map +1 -1
  279. package/dist/controllers/submit_once_controller.d.ts +22 -6
  280. package/dist/controllers/submit_once_controller.js +294 -192
  281. package/dist/controllers/submit_once_controller.js.map +1 -1
  282. package/dist/controllers/switch_controller.d.ts +40 -7
  283. package/dist/controllers/switch_controller.js +107 -43
  284. package/dist/controllers/switch_controller.js.map +1 -1
  285. package/dist/controllers/tabs_controller.d.ts +13 -1
  286. package/dist/controllers/tabs_controller.js +50 -25
  287. package/dist/controllers/tabs_controller.js.map +1 -1
  288. package/dist/controllers/tags_input_controller.d.ts +27 -3
  289. package/dist/controllers/tags_input_controller.js +290 -149
  290. package/dist/controllers/tags_input_controller.js.map +1 -1
  291. package/dist/controllers/textarea_autosize_controller.d.ts +10 -0
  292. package/dist/controllers/textarea_autosize_controller.js +71 -58
  293. package/dist/controllers/textarea_autosize_controller.js.map +1 -1
  294. package/dist/controllers/theme_controller.d.ts +37 -8
  295. package/dist/controllers/theme_controller.js +148 -80
  296. package/dist/controllers/theme_controller.js.map +1 -1
  297. package/dist/controllers/time_picker_controller.d.ts +18 -1
  298. package/dist/controllers/time_picker_controller.js +162 -81
  299. package/dist/controllers/time_picker_controller.js.map +1 -1
  300. package/dist/controllers/toast_controller.d.ts +85 -23
  301. package/dist/controllers/toast_controller.js +365 -193
  302. package/dist/controllers/toast_controller.js.map +1 -1
  303. package/dist/controllers/toggle_group_controller.d.ts +60 -9
  304. package/dist/controllers/toggle_group_controller.js +194 -99
  305. package/dist/controllers/toggle_group_controller.js.map +1 -1
  306. package/dist/controllers/toolbar_controller.js +92 -90
  307. package/dist/controllers/toolbar_controller.js.map +1 -1
  308. package/dist/controllers/tooltip_controller.d.ts +52 -2
  309. package/dist/controllers/tooltip_controller.js +185 -125
  310. package/dist/controllers/tooltip_controller.js.map +1 -1
  311. package/dist/controllers/transition_controller.d.ts +22 -4
  312. package/dist/controllers/transition_controller.js +123 -115
  313. package/dist/controllers/transition_controller.js.map +1 -1
  314. package/dist/controllers/tree_view_controller.d.ts +59 -18
  315. package/dist/controllers/tree_view_controller.js +248 -290
  316. package/dist/controllers/tree_view_controller.js.map +1 -1
  317. package/dist/index.d.ts +33 -3
  318. package/dist/index.js +8649 -8572
  319. package/dist/index.js.map +1 -1
  320. package/dist/inspector/cli.d.ts +95 -9
  321. package/dist/inspector/cli.js +343 -77
  322. package/dist/inspector/cli.js.map +1 -1
  323. package/dist/inspector/cli_bin.js +390 -96
  324. package/dist/inspector/cli_bin.js.map +1 -1
  325. package/dist/inspector/examples.json +45 -45
  326. package/dist/inspector/manifest.json +1308 -155
  327. package/dist/positioning/index.d.ts +8 -0
  328. package/dist/positioning/index.js +148 -45
  329. package/dist/positioning/index.js.map +1 -1
  330. package/package.json +7 -7
  331. package/dist/controllers/reset_before_cache_controller.d.ts +0 -76
  332. package/dist/controllers/reset_before_cache_controller.js.map +0 -1
@@ -1,8 +1,5 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/file_dropzone_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,91 +16,216 @@ function fillTemplate(template, values) {
19
16
  });
20
17
  }
21
18
 
22
- // src/utils/before_cache_reset.ts
23
- var BeforeCacheReset = class _BeforeCacheReset {
24
- /** Every subscribed instance, iterated by the one shared document listener. */
25
- static #subscribers = /* @__PURE__ */ new Set();
26
- /** The shared listener; installed while at least one instance is subscribed. */
27
- static #onBeforeCache = () => {
28
- for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
29
- };
30
- #rewind;
31
- /** @param rewind - the pass that returns this controller's state to its initial form. */
32
- constructor(rewind) {
33
- this.#rewind = rewind;
34
- }
35
- /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
36
- activate() {
37
- const first = _BeforeCacheReset.#subscribers.size === 0;
38
- _BeforeCacheReset.#subscribers.add(this);
39
- if (first) {
40
- document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
19
+ function sharedRegistry(key, create) {
20
+ const symbol = Symbol.for(key);
21
+ const scope = globalThis;
22
+ const existing = scope[symbol];
23
+ if (existing !== void 0) return existing;
24
+ const registry = create();
25
+ Object.defineProperty(globalThis, symbol, { value: registry });
26
+ return registry;
27
+ }
28
+
29
+ function isAttributeValue(value) {
30
+ return value === null || typeof value === "string";
31
+ }
32
+ function readLeaseRecord(element, record) {
33
+ try {
34
+ const parsed = JSON.parse(element.getAttribute(record) ?? "null");
35
+ if (!Array.isArray(parsed)) return void 0;
36
+ const [original, written, ...rest] = parsed;
37
+ const linked = rest[0] !== false;
38
+ const beneath = linked ? rest : rest.slice(1);
39
+ if (!beneath.every((under) => typeof under === "string")) return void 0;
40
+ return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
41
+ } catch {
42
+ return void 0;
43
+ }
44
+ }
45
+ var AttributeLease = class _AttributeLease {
46
+ static #registry = sharedRegistry(
47
+ "stimeo-ui.attribute-lease.registry.v1",
48
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
49
+ );
50
+ #attribute;
51
+ #record;
52
+ #base;
53
+ #held = /* @__PURE__ */ new Set();
54
+ constructor(attribute, owner, options = {}) {
55
+ this.#attribute = attribute;
56
+ this.#record = `data-${owner}-${attribute}-lease`;
57
+ this.#base = options.base === true;
58
+ const names = _AttributeLease.#registry.names;
59
+ names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
60
+ }
61
+ write(element, value) {
62
+ const stack = this.#stack(element);
63
+ const hold = this.#take(stack) ?? this.#acquire(element, stack);
64
+ this.#held.add(element);
65
+ const shows = !this.#covered(element, stack, hold);
66
+ hold.written = value;
67
+ this.#mark(element, stack);
68
+ if (shows) this.#reflect(element, value);
69
+ }
70
+ return(element) {
71
+ const held = this.#held.delete(element);
72
+ const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
73
+ const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
74
+ const hold = this.#take(stack);
75
+ if (!hold) return false;
76
+ const carries = element.getAttribute(this.#attribute) === hold.written;
77
+ const owns = carries && !this.#covered(element, stack, hold);
78
+ const index = stack.indexOf(hold);
79
+ stack.splice(index, 1);
80
+ const above = stack[index];
81
+ if (above) above.linked &&= hold.linked;
82
+ this.#mark(element, stack);
83
+ if (!owns) return true;
84
+ const below = hold.linked ? stack[index - 1] : void 0;
85
+ this.#reflect(element, below ? below.written : hold.original);
86
+ return true;
87
+ }
88
+ returnAll() {
89
+ for (const element of Array.from(this.#held)) this.return(element);
90
+ }
91
+ #stack(element) {
92
+ const stacks = _AttributeLease.#registry.stacks;
93
+ const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
94
+ stacks.set(element, byAttribute);
95
+ const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
96
+ byAttribute.set(this.#attribute, stack);
97
+ return stack;
98
+ }
99
+ #seed(element) {
100
+ const found = [];
101
+ for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
102
+ const read = readLeaseRecord(element, record);
103
+ if (read) found.push({ ...read, record });
104
+ }
105
+ const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
106
+ return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
107
+ lease: null,
108
+ record,
109
+ written,
110
+ original,
111
+ linked: linked && index > 0
112
+ }));
113
+ }
114
+ #take(stack) {
115
+ const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
116
+ if (hold) hold.lease = this;
117
+ return hold;
118
+ }
119
+ #acquire(element, stack) {
120
+ const current = element.getAttribute(this.#attribute);
121
+ const top = stack.at(-1);
122
+ const bottom = stack[0];
123
+ const over = top?.written === current ? top : void 0;
124
+ const from = this.#base ? bottom : over;
125
+ const hold = {
126
+ lease: this,
127
+ record: this.#record,
128
+ written: current,
129
+ original: from ? from.original : current,
130
+ linked: !this.#base && over !== void 0
131
+ };
132
+ if (this.#base && bottom) {
133
+ bottom.linked = true;
134
+ stack.unshift(hold);
135
+ } else stack.push(hold);
136
+ return hold;
137
+ }
138
+ #covered(element, stack, hold) {
139
+ const current = element.getAttribute(this.#attribute);
140
+ return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
141
+ }
142
+ #mark(element, stack) {
143
+ const records = /* @__PURE__ */ new Map([[this.#record, null]]);
144
+ stack.forEach((hold, index) => {
145
+ const names = stack.slice(0, index).map((under) => under.record);
146
+ const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
147
+ const marker = hold.linked || beneath.length === 0 ? [] : [false];
148
+ records.set(
149
+ hold.record,
150
+ hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
151
+ );
152
+ });
153
+ for (const [name, recorded] of records) {
154
+ if (element.getAttribute(name) === recorded) continue;
155
+ if (recorded === null) element.removeAttribute(name);
156
+ else element.setAttribute(name, recorded);
41
157
  }
42
158
  }
43
- /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
44
- deactivate() {
45
- _BeforeCacheReset.#subscribers.delete(this);
46
- if (_BeforeCacheReset.#subscribers.size > 0) return;
47
- document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
159
+ #reflect(element, value) {
160
+ if (element.getAttribute(this.#attribute) === value) return;
161
+ if (value === null) element.removeAttribute(this.#attribute);
162
+ else element.setAttribute(this.#attribute, value);
48
163
  }
49
164
  };
50
165
 
51
- // src/utils/detach_gate.ts
52
166
  var DetachGate = class _DetachGate {
53
- /** Set while a probe is queued, waiting for a reconnect to cancel it. */
54
167
  #pending = false;
55
- /**
56
- * True while a probe is queued — the last disconnect was ambiguous and no
57
- * reconnect has cancelled it yet. Read it from `connect()` to tell the
58
- * reconnect half of an in-page move from a first connect: a controller whose
59
- * initialisation restarts a measurement (a min-duration floor, an elapsed
60
- * counter) must skip it for the move, where nothing actually restarted.
61
- */
62
168
  get pending() {
63
169
  return this.#pending;
64
170
  }
65
- /**
66
- * True when the disconnect is definitely a real detach — the element left
67
- * the document, or `data-controller` no longer lists the identifier. False
68
- * means ambiguous (in-page move or observed-root exit), NOT "alive".
69
- */
70
171
  static isDetached(host) {
71
- if (!host.element.isConnected) return true;
72
- const tokens = (host.element.getAttribute("data-controller") ?? "").split(/\s+/);
73
- return !tokens.includes(host.identifier);
74
- }
75
- /**
76
- * Call from `disconnect()`: runs `teardown` synchronously on a definite
77
- * detach (fast path), otherwise defers it one microtask — a reconnect
78
- * ({@link cancel} from `connect()`) keeps the state, no reconnect runs it.
79
- * One microtask is the whole probe window: Stimulus reconnects a moved
80
- * element within the same mutation batch, before the checkpoint drains.
81
- */
172
+ return !host.element.isConnected || !_DetachGate.#listed(host);
173
+ }
82
174
  disconnected(host, teardown) {
83
- if (_DetachGate.isDetached(host)) {
175
+ const carried = _DetachGate.#carried(host);
176
+ if (!carried && _DetachGate.isDetached(host)) {
84
177
  this.#pending = false;
85
178
  teardown();
86
179
  return;
87
180
  }
88
181
  this.#pending = true;
89
- queueMicrotask(() => {
182
+ const probe = () => {
90
183
  if (!this.#pending) return;
91
184
  this.#pending = false;
92
185
  teardown();
93
- });
186
+ };
187
+ if (carried) setTimeout(probe, 0);
188
+ else queueMicrotask(probe);
189
+ }
190
+ static #carried(host) {
191
+ const element = host.element;
192
+ return !element.isConnected && element.closest("[data-turbo-permanent]") !== null && _DetachGate.#listed(host);
193
+ }
194
+ static #listed(host) {
195
+ const tokens = (host.element.getAttribute("data-controller") ?? "").split(/\s+/);
196
+ return tokens.includes(host.identifier);
94
197
  }
95
- /**
96
- * Disarms a pending probe. Call from `connect()` (the reconnect that proves
97
- * an in-page move) and from the head of any teardown path not routed through
98
- * {@link disconnected} (disabled-toggle, Escape), so an orphaned probe can
99
- * never run the teardown a second time.
100
- */
101
198
  cancel() {
102
199
  this.#pending = false;
103
200
  }
104
201
  };
105
202
 
106
- // src/utils/focus_candidate.ts
203
+ function matchingPart(root, selector) {
204
+ return root.matches(selector) ? root : root.querySelector(selector);
205
+ }
206
+ function writeLabel(slot, text) {
207
+ if (slot.childElementCount === 0) {
208
+ slot.textContent = text;
209
+ return;
210
+ }
211
+ let first = null;
212
+ for (const node of Array.from(slot.childNodes)) {
213
+ if (node.nodeType !== Node.TEXT_NODE) continue;
214
+ if (first) node.remove();
215
+ else first = node;
216
+ }
217
+ if (first) first.nodeValue = text;
218
+ else slot.prepend(text);
219
+ }
220
+
221
+ function ownerIndex(candidates, node) {
222
+ if (!(node instanceof Node)) return -1;
223
+ return candidates.findIndex((candidate) => candidate.contains(node));
224
+ }
225
+ function ownerOf(candidates, node) {
226
+ return candidates[ownerIndex(candidates, node)] ?? null;
227
+ }
228
+
107
229
  function inheritsFieldsetDisabled(control) {
108
230
  let fieldset = control.closest("fieldset[disabled]");
109
231
  while (fieldset) {
@@ -114,10 +236,123 @@ function inheritsFieldsetDisabled(control) {
114
236
  return false;
115
237
  }
116
238
 
117
- // src/controllers/file_dropzone_controller.ts
239
+ var NUMBER_BOUNDS = {
240
+ nonNegative: { finite: true, min: 0 }};
241
+ function matchesNumberBounds(value, bounds) {
242
+ if (!Number.isFinite(value)) {
243
+ const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
244
+ if (direction === null) return false;
245
+ if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
246
+ }
247
+ if (bounds.min !== void 0 && value < bounds.min) return false;
248
+ if (bounds.max !== void 0 && value > bounds.max) return false;
249
+ if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
250
+ if (bounds.integer && !Number.isInteger(value)) return false;
251
+ if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
252
+ return true;
253
+ }
254
+
255
+ function readNumber(raw, fallback, bounds) {
256
+ return matchesNumberBounds(raw, bounds) ? raw : fallback;
257
+ }
258
+
259
+ var NumberValueReader = class {
260
+ #lastRejected = /* @__PURE__ */ new Map();
261
+ read(owner, name, raw, fallback, bounds) {
262
+ const resolved = readNumber(raw, fallback, bounds);
263
+ if (matchesNumberBounds(raw, bounds)) {
264
+ this.#lastRejected.delete(name);
265
+ return resolved;
266
+ }
267
+ const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
268
+ const literal = owner.element.getAttribute(attribute);
269
+ if (literal === null) {
270
+ this.#lastRejected.delete(name);
271
+ return resolved;
272
+ }
273
+ if (this.#lastRejected.get(name) !== literal) {
274
+ this.#lastRejected.set(name, literal);
275
+ console.warn(
276
+ `Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
277
+ );
278
+ }
279
+ return resolved;
280
+ }
281
+ };
282
+
283
+ function targetSelector(identifier, name) {
284
+ return `[data-${identifier}-target~="${name}"]`;
285
+ }
286
+
287
+ function cloneTemplateRoot(template) {
288
+ const root = template.content.firstElementChild;
289
+ return root instanceof HTMLElement ? root.cloneNode(true) : null;
290
+ }
291
+ var TemplateRow = class {
292
+ #options;
293
+ #warned = false;
294
+ constructor(options) {
295
+ this.#options = options;
296
+ }
297
+ selector(name) {
298
+ return targetSelector(this.#options.identifier, name);
299
+ }
300
+ connect() {
301
+ this.#warned = false;
302
+ }
303
+ report(missing) {
304
+ return this.#say(`lacks ${missing}`);
305
+ }
306
+ #say(problem) {
307
+ if (this.#warned) return null;
308
+ this.#warned = true;
309
+ const { identifier, outcome, noun } = this.#options;
310
+ console.warn(`Stimeo UI: "${identifier}" ${outcome} because its ${noun} ${problem}.`);
311
+ return null;
312
+ }
313
+ instantiate(template, values) {
314
+ const { root: rootName, required, optional, button: buttonName } = this.#options;
315
+ const root = cloneTemplateRoot(template);
316
+ if (!root?.matches(this.selector(rootName))) {
317
+ return this.report(`${this.#article(rootName)} "${rootName}" root`);
318
+ }
319
+ const count = template.content.children.length;
320
+ if (count !== 1) {
321
+ return this.#say(`holds ${count} elements, and the row has to be its only one`);
322
+ }
323
+ const slots = {};
324
+ for (const name2 of required) {
325
+ const part = this.#resolve(root, name2);
326
+ if (!part) return this.report(`${this.#article(name2)} "${name2}" target`);
327
+ slots[name2] = part;
328
+ }
329
+ for (const name2 of optional ?? []) slots[name2] = this.#resolve(root, name2);
330
+ const named = this.#resolve(root, buttonName);
331
+ if (!(named instanceof HTMLButtonElement)) {
332
+ return this.report(`${this.#article(buttonName)} "${buttonName}" target <button>`);
333
+ }
334
+ const button = named;
335
+ const name = button.getAttribute("aria-label")?.trim() ?? "";
336
+ if (name === "") return this.report(`a non-empty aria-label on its "${buttonName}" target`);
337
+ button.setAttribute("aria-label", fillTemplate(name, values));
338
+ return { root, slots, button };
339
+ }
340
+ #resolve(root, name) {
341
+ return matchingPart(root, this.selector(name));
342
+ }
343
+ #article(name) {
344
+ return /^[aeiou]/i.test(name) ? "an" : "a";
345
+ }
346
+ };
347
+
118
348
  var DRAGOVER_ATTRIBUTE = "data-dragover";
119
- var INVALID_ATTRIBUTE = "data-stimeo--file-dropzone-invalid";
120
- var FileDropzoneController = class extends Controller {
349
+ var INVALID_ATTRIBUTE = "invalid";
350
+ var GENERATED_ATTRIBUTE = "generated";
351
+ var FileDropzoneController = class _FileDropzoneController extends Controller {
352
+ #numbers = new NumberValueReader();
353
+ get #invalidAttribute() {
354
+ return `data-${this.identifier}-${INVALID_ATTRIBUTE}`;
355
+ }
121
356
  static targets = [
122
357
  "zone",
123
358
  "trigger",
@@ -141,93 +376,118 @@ var FileDropzoneController = class extends Controller {
141
376
  announceRejectedDuplicateText: { type: String, default: "" },
142
377
  announceRejectedCountText: { type: String, default: "" }
143
378
  };
379
+ static valueConstraints = {
380
+ maxSize: NUMBER_BOUNDS.nonNegative,
381
+ maxFiles: NUMBER_BOUNDS.nonNegative
382
+ };
144
383
  static actions = ["onChange", "onDragLeave", "onDragOver", "onDrop", "openDialog"];
145
384
  static events = ["change", "reject", "reconcile"];
146
- /** Selected files paired with their rendered item and any preview objectURL. */
147
385
  #entries = [];
148
- /** Whether a drag is currently over the zone; the source for `data-dragover`. */
149
386
  #dragging = false;
150
- /** Whether this connection already reported its unusable item template. */
151
- #warnedTemplate = false;
387
+ #invalid = false;
388
+ #dragoverHook = new AttributeLease(DRAGOVER_ATTRIBUTE, this.identifier);
389
+ #invalidHook = new AttributeLease(this.#invalidAttribute, this.identifier);
390
+ #rows = new TemplateRow({
391
+ identifier: this.identifier,
392
+ root: "item",
393
+ required: ["name"],
394
+ optional: ["thumb"],
395
+ button: "remove",
396
+ outcome: "added no file",
397
+ noun: "item template"
398
+ });
152
399
  #gate = new DetachGate();
153
- #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
154
- /** Subscribes to the Turbo cache rewind and re-arms the template diagnostic. */
400
+ #listWatch = new MutationObserver((records) => {
401
+ const lost = records.some(
402
+ (record) => Array.from(record.removedNodes).some(
403
+ (node) => this.#entries.some((entry) => entry.item === node)
404
+ )
405
+ );
406
+ if (!lost) return;
407
+ this.#rehome();
408
+ this.#paintZone();
409
+ });
410
+ #connected = false;
411
+ get #generatedAttribute() {
412
+ return `data-${this.identifier}-${GENERATED_ATTRIBUTE}`;
413
+ }
155
414
  connect() {
415
+ const moved = this.#gate.pending;
156
416
  this.#gate.cancel();
157
- this.#warnedTemplate = false;
158
- this.#beforeCache.activate();
159
- }
160
- /**
161
- * Releases the delegated listeners. The selection itself survives an in-page
162
- * move and is released only once the gate proves a real detach — revoking a
163
- * preview URL on a move would leave a live item pointing at a dead `blob:`.
164
- */
417
+ this.#connected = true;
418
+ this.#rows.connect();
419
+ if (!moved) this.#discardInherited();
420
+ }
165
421
  disconnect() {
422
+ this.#connected = false;
166
423
  for (const list of this.listTargets) list.removeEventListener("click", this.#onItemClick);
167
- this.#beforeCache.deactivate();
424
+ this.#listWatch.disconnect();
168
425
  this.#gate.disconnected(this, () => this.#teardown());
169
426
  }
170
- /**
171
- * Binds removal and restores client-only previews for every list this controller
172
- * renders into — the one present at connect and any Turbo puts in its place.
173
- * This is the only place the listener is attached, so the pair with
174
- * {@link listTargetDisconnected} keeps it from outliving the element it is on.
175
- */
427
+ #discardInherited() {
428
+ let discarded = false;
429
+ for (const list of this.listTargets) {
430
+ for (const item of Array.from(list.children)) {
431
+ if (!item.hasAttribute(this.#generatedAttribute)) continue;
432
+ item.remove();
433
+ discarded = true;
434
+ }
435
+ }
436
+ for (const zone of this.zoneTargets) {
437
+ this.#dragoverHook.return(zone);
438
+ this.#invalidHook.return(zone);
439
+ }
440
+ if (!discarded) return;
441
+ this.#dragging = false;
442
+ this.#invalid = false;
443
+ this.#syncInput();
444
+ this.dispatch("reconcile", { detail: { files: this.#files } });
445
+ }
176
446
  listTargetConnected(list) {
177
- this.#bindList(list);
447
+ list.addEventListener("click", this.#onItemClick);
448
+ this.#listWatch.observe(list, { childList: true });
449
+ this.#rehome();
178
450
  }
179
- /** Releases only the list target that actually disconnected. */
180
451
  listTargetDisconnected(list) {
181
452
  list.removeEventListener("click", this.#onItemClick);
453
+ if (this.#connected) this.#rehome();
182
454
  }
183
- /** Binds delegated removal once and moves live preview items into the current list. */
184
- #bindList(list) {
185
- list.addEventListener("click", this.#onItemClick);
186
- for (const entry of this.#entries) {
187
- if (!list.contains(entry.item)) list.appendChild(entry.item);
188
- }
455
+ inputTargetConnected() {
456
+ if (this.#connected) this.#syncInput();
457
+ }
458
+ inputTargetDisconnected() {
459
+ if (this.#connected) this.#syncInput();
460
+ }
461
+ zoneTargetConnected() {
462
+ if (this.#connected) this.#paintZone();
463
+ }
464
+ zoneTargetDisconnected(zone) {
465
+ if (this.zoneTargets.includes(zone)) return;
466
+ this.#dragoverHook.return(zone);
467
+ this.#invalidHook.return(zone);
468
+ if (this.#connected) this.#paintZone();
189
469
  }
190
- /** Opens the native file dialog. Bound via `data-action` (trigger click). */
191
470
  openDialog() {
192
471
  if (this.#isDisabled) return;
193
472
  this.inputTarget.click();
194
473
  }
195
- /**
196
- * Adds the files chosen through the native dialog. The input holds only what
197
- * the dialog just returned, so the accepted set is written back over it once
198
- * the batch is validated.
199
- */
200
- onChange() {
201
- const files = this.inputTarget.files;
474
+ onChange(event) {
475
+ const files = (ownerOf(this.inputTargets, event?.target) ?? this.inputTarget).files;
202
476
  if (files) this.#addFiles(files);
203
- else this.#syncInput();
204
- }
205
- /**
206
- * Marks the zone as a drop target and announces the affordance once per drag.
207
- * Bound via `data-action` (dragover). Leaving the default alone is what refuses
208
- * the drop, so a disabled field and a drag an inner dropzone already claimed
209
- * both fall through untouched.
210
- */
477
+ }
211
478
  onDragOver(event) {
212
479
  if (event.defaultPrevented || this.#isDisabled) return;
213
480
  event.preventDefault();
214
481
  if (this.#dragging) return;
215
482
  this.#dragging = true;
216
- if (this.hasZoneTarget) this.zoneTarget.setAttribute(DRAGOVER_ATTRIBUTE, "");
483
+ this.#paintZone();
217
484
  announce(fillTemplate(this.announceDragTextValue, { total: this.#entries.length }));
218
485
  }
219
- /**
220
- * Clears the drag-over flag when the pointer leaves the zone. Bound via
221
- * `data-action` (dragleave). `dragleave` bubbles from every descendant the
222
- * pointer crosses, so the flag only drops when the element being entered is
223
- * outside the zone (or there is none, the pointer having left the window).
224
- */
225
486
  onDragLeave(event) {
226
487
  const next = event.relatedTarget;
227
488
  if (this.hasZoneTarget && next instanceof Node && this.zoneTarget.contains(next)) return;
228
489
  this.#endDrag();
229
490
  }
230
- /** Accepts dropped files, clearing the drag-over state. Bound via `data-action` (drop). */
231
491
  onDrop(event) {
232
492
  if (event.defaultPrevented) return;
233
493
  this.#endDrag();
@@ -235,26 +495,16 @@ var FileDropzoneController = class extends Controller {
235
495
  event.preventDefault();
236
496
  if (event.dataTransfer?.files) this.#addFiles(event.dataTransfer.files);
237
497
  }
238
- /**
239
- * Removes the file whose remove button was clicked. Delegated on the list
240
- * container rather than bound per item via `data-action`, so it works the instant
241
- * an item is appended without waiting on Stimulus to wire a freshly created element.
242
- * Only the item's declared `remove` target counts, so an authored second control
243
- * inside an item does what it says instead of silently discarding the file. The
244
- * button has to belong to a tracked item, which is what makes a list this
245
- * controller no longer renders into inert — its items moved out with it.
246
- */
247
498
  #onItemClick = (event) => {
248
499
  const button = event.target.closest(
249
- 'button[data-stimeo--file-dropzone-target~="remove"]'
500
+ `button${this.#rows.selector("remove")}`
250
501
  );
251
502
  const index = this.#entries.findIndex((entry) => entry.item.contains(button));
252
503
  if (index !== -1) this.#removeAt(index);
253
504
  };
254
- /** Validates each incoming file, renders the accepted ones, and reports the batch. */
255
505
  #addFiles(files) {
256
506
  this.#rehome();
257
- if (this.hasZoneTarget) this.zoneTarget.removeAttribute(INVALID_ATTRIBUTE);
507
+ this.#invalid = false;
258
508
  const rejected = /* @__PURE__ */ new Map();
259
509
  const turnedAway = [];
260
510
  let addedName = "";
@@ -262,7 +512,7 @@ var FileDropzoneController = class extends Controller {
262
512
  for (const file of Array.from(files)) {
263
513
  const reason = this.#validate(file);
264
514
  if (reason !== null) {
265
- if (this.hasZoneTarget) this.zoneTarget.setAttribute(INVALID_ATTRIBUTE, "");
515
+ this.#invalid = true;
266
516
  const batch = rejected.get(reason);
267
517
  if (batch) batch.count += 1;
268
518
  else rejected.set(reason, { name: file.name, count: 1 });
@@ -273,6 +523,7 @@ var FileDropzoneController = class extends Controller {
273
523
  if (added === 0) addedName = file.name;
274
524
  added += 1;
275
525
  }
526
+ this.#paintZone();
276
527
  this.#syncInput();
277
528
  if (added > 0) {
278
529
  this.#announce(this.announceAddedTextValue, addedName, added);
@@ -285,14 +536,9 @@ var FileDropzoneController = class extends Controller {
285
536
  this.#announce(this.#rejectText(reason), batch.name, batch.count);
286
537
  }
287
538
  }
288
- /**
289
- * Returns the rejection reason for `file`, or `null` when it is acceptable.
290
- * The file's own defects are decided first, so a full list still tells the user
291
- * which files it would never have taken.
292
- */
293
539
  #validate(file) {
294
540
  if (!this.#matchesAccept(file)) return "type";
295
- if (this.maxSizeValue > 0 && file.size > this.maxSizeValue) return "size";
541
+ if (this.#safeMaxSize > 0 && file.size > this.#safeMaxSize) return "size";
296
542
  if (!this.allowDuplicatesValue && this.#entries.some((entry) => this.#isSame(entry.file, file))) {
297
543
  return "duplicate";
298
544
  }
@@ -300,36 +546,24 @@ var FileDropzoneController = class extends Controller {
300
546
  if (limit > 0 && this.#entries.length >= limit) return "count";
301
547
  return null;
302
548
  }
303
- /**
304
- * Whether two files are the same selection. `File` objects from separate picks
305
- * are never the same reference, so identity is the triple the platform exposes.
306
- */
307
549
  #isSame(a, b) {
308
550
  return a.name === b.name && a.size === b.size && a.lastModified === b.lastModified;
309
551
  }
310
- /**
311
- * Builds one preview item (name, optional thumbnail, remove button) and reports
312
- * whether it was rendered.
313
- */
314
552
  #appendFile(file) {
315
- if (!this.hasListTarget) return this.#warnTemplate('a "list" target to render into');
316
- if (!this.hasItemTemplateTarget) return this.#warnTemplate('an "itemTemplate" target');
317
- const fragment = this.itemTemplateTarget.content.cloneNode(true);
318
- const item = this.#slot(fragment, "item");
319
- const name = this.#slot(fragment, "name");
320
- const thumb = this.#slot(fragment, "thumb");
321
- const button = fragment.querySelector(
322
- 'button[data-stimeo--file-dropzone-target~="remove"]'
323
- );
324
- const removeName = button?.getAttribute("aria-label")?.trim() ?? "";
325
- if (!item) return this.#warnTemplate('an "item" root');
326
- if (!name) return this.#warnTemplate('a "name" element');
327
- if (!button) return this.#warnTemplate('a "remove" target <button>');
328
- if (removeName === "") {
329
- return this.#warnTemplate('a non-empty aria-label on its "remove" target');
553
+ if (!this.hasListTarget) {
554
+ this.#rows.report('a "list" target to render into');
555
+ return false;
556
+ }
557
+ if (!this.hasItemTemplateTarget) {
558
+ this.#rows.report('an "itemTemplate" target');
559
+ return false;
330
560
  }
331
- name.textContent = file.name;
332
- button.setAttribute("aria-label", fillTemplate(removeName, { name: file.name }));
561
+ const row = this.#rows.instantiate(this.itemTemplateTarget, { name: file.name });
562
+ if (!row) return false;
563
+ const item = row.root;
564
+ item.setAttribute(this.#generatedAttribute, "");
565
+ writeLabel(row.slots.name, file.name);
566
+ const thumb = row.slots.thumb;
333
567
  let url;
334
568
  if (thumb && file.type.startsWith("image/")) {
335
569
  url = URL.createObjectURL(file);
@@ -339,33 +573,10 @@ var FileDropzoneController = class extends Controller {
339
573
  } else if (thumb) {
340
574
  thumb.hidden = true;
341
575
  }
342
- this.listTarget.appendChild(fragment);
576
+ this.listTarget.appendChild(item);
343
577
  this.#entries.push({ file, item, url });
344
578
  return true;
345
579
  }
346
- /** Resolves one declared part inside a cloned item template. */
347
- #slot(fragment, name) {
348
- return fragment.querySelector(`[data-stimeo--file-dropzone-target~="${name}"]`);
349
- }
350
- /**
351
- * Reports an unusable item template to the author, once per connection.
352
- *
353
- * The addition itself stays a no-op — nothing about the selection, the native
354
- * input, the announcements, or the events changes. Without this line the only
355
- * symptom is a picker that accepts no file at all, and the causes the Inspector
356
- * cannot see statically (a server-rendered template, a name that renders empty
357
- * from a missing translation) would have no diagnostic anywhere.
358
- */
359
- #warnTemplate(missing) {
360
- if (!this.#warnedTemplate) {
361
- this.#warnedTemplate = true;
362
- console.warn(
363
- `Stimeo UI: "${this.identifier}" added no file because its item template lacks ${missing}.`
364
- );
365
- }
366
- return false;
367
- }
368
- /** Removes entry `index`, revokes its preview, and re-homes focus. */
369
580
  #removeAt(index) {
370
581
  const entry = this.#entries[index];
371
582
  if (!entry) return;
@@ -382,7 +593,6 @@ var FileDropzoneController = class extends Controller {
382
593
  (buttons[index] ?? buttons[buttons.length - 1])?.focus();
383
594
  }
384
595
  }
385
- /** Whether `file` satisfies the input's `accept` list (empty accepts all). */
386
596
  #matchesAccept(file) {
387
597
  const accept = this.inputTarget.accept.trim();
388
598
  if (accept === "") return true;
@@ -396,15 +606,9 @@ var FileDropzoneController = class extends Controller {
396
606
  return type === token;
397
607
  });
398
608
  }
399
- /**
400
- * Sends one consumer-worded message to the page's shared announcer. `{name}` is
401
- * the file the message is about, `{count}` how many files it covers, and
402
- * `{total}` how many are selected once the batch has settled.
403
- */
404
609
  #announce(template, name, count) {
405
610
  announce(fillTemplate(template, { name, count, total: this.#entries.length }));
406
611
  }
407
- /** The consumer's wording for one rejection reason. */
408
612
  #rejectText(reason) {
409
613
  switch (reason) {
410
614
  case "type":
@@ -417,11 +621,6 @@ var FileDropzoneController = class extends Controller {
417
621
  return this.announceRejectedCountTextValue;
418
622
  }
419
623
  }
420
- /**
421
- * Mirrors the accepted set onto the native input, so a plain form submit carries
422
- * the dropped files. Skipped where `DataTransfer` cannot be constructed: the
423
- * widget keeps working and the consumer still receives every `File` on `change`.
424
- */
425
624
  #syncInput() {
426
625
  if (!this.hasInputTarget) return;
427
626
  const transfer = this.#newTransfer();
@@ -429,7 +628,6 @@ var FileDropzoneController = class extends Controller {
429
628
  for (const entry of this.#entries) transfer.items.add(entry.file);
430
629
  this.inputTarget.files = transfer.files;
431
630
  }
432
- /** A usable empty `DataTransfer`, or `null` where the platform has none. */
433
631
  #newTransfer() {
434
632
  try {
435
633
  return new DataTransfer();
@@ -437,35 +635,23 @@ var FileDropzoneController = class extends Controller {
437
635
  return null;
438
636
  }
439
637
  }
440
- /**
441
- * Moves surviving preview items back under the current list. A morph that
442
- * empties the list in place leaves the selection with no rendering, and the
443
- * files it holds cannot be rebuilt from the DOM, so the items are re-homed
444
- * rather than forgotten.
445
- */
446
638
  #rehome() {
447
639
  if (!this.hasListTarget) return;
448
640
  for (const entry of this.#entries) {
449
641
  if (!this.listTarget.contains(entry.item)) this.listTarget.appendChild(entry.item);
450
642
  }
451
643
  }
452
- /** Drops the drag-over state, whether the drag ended in a drop or left the zone. */
453
644
  #endDrag() {
454
645
  this.#dragging = false;
455
- if (this.hasZoneTarget) this.zoneTarget.removeAttribute(DRAGOVER_ATTRIBUTE);
456
- }
457
- /**
458
- * Discards the selection and every state attribute this controller wrote, so
459
- * neither a cached snapshot nor a stranded subtree keeps items whose files are
460
- * gone. Silent: `change` means a selection the user changed, and the cache
461
- * rewind reports itself as `reconcile` instead.
462
- */
463
- #rewindForCache() {
464
- const had = this.#entries.length > 0;
465
- this.#reset();
466
- if (had) this.dispatch("reconcile", { detail: { files: this.#files } });
646
+ this.#paintZone();
647
+ }
648
+ #paintZone() {
649
+ if (!this.hasZoneTarget) return;
650
+ this.#dragoverHook.write(this.zoneTarget, this.#dragging ? "" : null);
651
+ this.#invalidHook.write(this.zoneTarget, this.#invalid ? "" : null);
467
652
  }
468
653
  #reset() {
654
+ this.#invalid = false;
469
655
  for (const entry of this.#entries) {
470
656
  if (entry.url) URL.revokeObjectURL(entry.url);
471
657
  entry.item.remove();
@@ -473,39 +659,50 @@ var FileDropzoneController = class extends Controller {
473
659
  this.#entries.length = 0;
474
660
  this.#syncInput();
475
661
  this.#endDrag();
476
- if (this.hasZoneTarget) this.zoneTarget.removeAttribute(INVALID_ATTRIBUTE);
477
662
  }
478
- /** Releases the selection once the disconnect is known to be a real detach. */
479
663
  #teardown() {
480
- this.#gate.cancel();
481
664
  this.#reset();
482
665
  }
483
- /** Whether the field refuses input, natively or through an ancestor `fieldset`. */
484
666
  get #isDisabled() {
485
667
  return this.inputTarget.disabled || inheritsFieldsetDisabled(this.inputTarget);
486
668
  }
487
- /** Effective file cap: `maxFiles`, or 1 when the input is single-select. */
488
669
  get #effectiveMaxFiles() {
489
- if (this.maxFilesValue > 0) return this.maxFilesValue;
670
+ if (this.#safeMaxFiles > 0) return this.#safeMaxFiles;
490
671
  return this.inputTarget.multiple ? 0 : 1;
491
672
  }
492
- /** The declared remove button of each rendered item, in selection order. */
493
673
  get #removeButtons() {
494
674
  const buttons = [];
495
675
  for (const entry of this.#entries) {
496
- const button = entry.item.querySelector(
497
- 'button[data-stimeo--file-dropzone-target~="remove"]'
676
+ const button = matchingPart(
677
+ entry.item,
678
+ `button${this.#rows.selector("remove")}`
498
679
  );
499
680
  if (button) buttons.push(button);
500
681
  }
501
682
  return buttons;
502
683
  }
503
- /** The accepted files in selection order. */
504
684
  get #files() {
505
685
  return this.#entries.map((entry) => entry.file);
506
686
  }
687
+ get #safeMaxSize() {
688
+ return this.#numbers.read(
689
+ this,
690
+ "maxSize",
691
+ this.maxSizeValue,
692
+ _FileDropzoneController.values.maxSize.default,
693
+ _FileDropzoneController.valueConstraints.maxSize
694
+ );
695
+ }
696
+ get #safeMaxFiles() {
697
+ return this.#numbers.read(
698
+ this,
699
+ "maxFiles",
700
+ this.maxFilesValue,
701
+ _FileDropzoneController.values.maxFiles.default,
702
+ _FileDropzoneController.valueConstraints.maxFiles
703
+ );
704
+ }
507
705
  };
508
706
 
509
707
  export { FileDropzoneController };
510
- //# sourceMappingURL=file_dropzone_controller.js.map
511
708
  //# sourceMappingURL=file_dropzone_controller.js.map