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
@@ -85,68 +85,67 @@ function monthLabelFormatter(locale) {
85
85
  return intlFormatter(Intl.DateTimeFormat, locale, MONTH_LABEL_OPTIONS, "en");
86
86
  }
87
87
 
88
+ // src/utils/field_mirror.ts
89
+ function writeField(field, value) {
90
+ if (field.value === value) return false;
91
+ field.value = value;
92
+ return true;
93
+ }
94
+ function commitField(target) {
95
+ target.dispatchEvent(new Event("change", { bubbles: true }));
96
+ }
97
+
88
98
  // src/utils/locale.ts
89
99
  function resolveLocale(element, declared = "") {
90
100
  return declared || element.closest("[lang]")?.getAttribute("lang") || void 0;
91
101
  }
92
102
 
93
- // src/utils/safe_timeout.ts
94
- var TimerRegistry = class {
95
- /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
96
- ids = /* @__PURE__ */ new Set();
97
- /**
98
- * Cancels a single tracked timer.
99
- *
100
- * No-ops if the id is unknown (already cleared, fired, or never owned by this
101
- * registry), so callers can clear defensively without guarding.
102
- */
103
- clear(id) {
104
- if (this.ids.delete(id)) {
105
- this.cancel(id);
106
- }
107
- }
108
- /**
109
- * Cancels every tracked timer. Call this from a controller's `disconnect()`
110
- * to guarantee no timer outlives the element.
111
- */
112
- clearAll() {
113
- for (const id of this.ids) {
114
- this.cancel(id);
115
- }
116
- this.ids.clear();
103
+ // src/utils/microtask_coalescer.ts
104
+ var MicrotaskCoalescer = class {
105
+ #run;
106
+ #queued = false;
107
+ #active = false;
108
+ #generation = 0;
109
+ /** @param run - the single reconciliation pass, invoked at most once per batch. */
110
+ constructor(run) {
111
+ this.#run = run;
117
112
  }
118
- /** Number of timers currently tracked (pending). */
119
- get size() {
120
- return this.ids.size;
113
+ /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
114
+ activate() {
115
+ this.#active = true;
121
116
  }
122
- };
123
- var SafeTimeout = class extends TimerRegistry {
124
- /**
125
- * Schedules `callback` after `delay` ms and returns the timer id.
126
- *
127
- * The id is removed from the registry automatically when the timeout fires,
128
- * so {@link TimerRegistry.size | size} reflects only still-pending timers.
129
- */
130
- set(callback, delay) {
131
- const id = this.schedule(() => {
132
- this.ids.delete(id);
133
- callback();
134
- }, delay);
135
- this.ids.add(id);
136
- return id;
137
- }
138
- schedule(callback, delay) {
139
- return window.setTimeout(callback, delay);
117
+ /** Closes the window and drops any pending pass; call from `disconnect()`. */
118
+ cancel() {
119
+ this.#active = false;
120
+ this.#queued = false;
121
+ this.#generation += 1;
140
122
  }
141
- cancel(id) {
142
- window.clearTimeout(id);
123
+ /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
124
+ schedule() {
125
+ if (!this.#active || this.#queued) return;
126
+ this.#queued = true;
127
+ const generation = this.#generation;
128
+ queueMicrotask(() => {
129
+ if (generation !== this.#generation || !this.#queued || !this.#active) return;
130
+ this.#queued = false;
131
+ this.#run();
132
+ });
143
133
  }
144
134
  };
145
135
 
136
+ // src/utils/target_selector.ts
137
+ function targetSelector(identifier, name) {
138
+ return `[data-${identifier}-target~="${name}"]`;
139
+ }
140
+
146
141
  // src/controllers/calendar_controller.ts
147
- var OWNED_DISABLED = "data-stimeo--calendar-owns-disabled";
142
+ var OWNED_DISABLED = "owns-disabled";
148
143
  var CalendarController = class extends Controller {
149
- static targets = ["label", "grid", "day"];
144
+ /** The marker above, in the namespace this controller is registered under. */
145
+ get #ownedDisabled() {
146
+ return `data-${this.identifier}-${OWNED_DISABLED}`;
147
+ }
148
+ static targets = ["label", "grid", "day", "field", "monthField"];
150
149
  static values = {
151
150
  month: { type: String, default: "" },
152
151
  selected: { type: String, default: "" },
@@ -157,68 +156,144 @@ var CalendarController = class extends Controller {
157
156
  locale: { type: String, default: "" }
158
157
  };
159
158
  static actions = ["next", "onKeydown", "prev", "selectByClick"];
160
- static events = ["monthchange", "select"];
159
+ static events = ["monthchange", "reconcile", "select"];
161
160
  /** The date currently receiving focus in the grid (local time). */
162
161
  focusedDate = /* @__PURE__ */ new Date();
163
162
  /**
164
- * Deferred focus moves scheduled after an asynchronous month transition.
165
- * Tracked so {@link disconnect} can cancel any pending move and a detached
166
- * controller never steals focus after the element leaves the DOM (Turbo).
163
+ * Whether `connect()` has run and `disconnect()` has not since. The paints
164
+ * before and during `connect()` describe the grid, so they report nothing.
165
+ */
166
+ #connected = false;
167
+ /**
168
+ * The selection the last paint published — a `YYYY-MM-DD` day, or `""` — which
169
+ * the next move of the selection is measured from. A pick records its own day
170
+ * here before painting it.
171
+ */
172
+ #published = "";
173
+ /**
174
+ * How many times a pick or a change the page made has moved the selection, so
175
+ * a report still pending for a selection a listener has since replaced can tell.
176
+ */
177
+ #selectionMoves = 0;
178
+ /**
179
+ * The month the last paint showed (`YYYY-MM`), or `""` before the first paint.
180
+ * Only a connected grid reports a paint that moves it: settling on the initial
181
+ * month — before `connect()` or in it, from the attribute or derived here — is
182
+ * the grid describing itself, not a navigation, so it must not reach a
183
+ * listener that refetches inventory or pushes history.
184
+ *
185
+ * While `month` is empty this is also the month the next paint shows, so the
186
+ * grid stays where it is; the first `connect()` of this instance sets it to the
187
+ * month the grid opens on.
188
+ */
189
+ #paintedMonth = "";
190
+ /**
191
+ * Whether this instance has connected before. Only its first connection picks
192
+ * the month a grid with no `month` opens on. A later one keeps the month on
193
+ * screen, and so do the paints Stimulus runs before it.
194
+ */
195
+ #opened = false;
196
+ /**
197
+ * Whether the paint about to run was asked for by this grid's own controls.
198
+ * The `month` and `selected` Values are shared with the page — application
199
+ * code and a Turbo morph write them too — so the field mirrors take their
200
+ * "did the user commit this" answer from the route, not from the Value.
201
+ */
202
+ #movedByUser = false;
203
+ /**
204
+ * Repaints the month on screen after `min`, `max` or `weekStart` changed.
205
+ *
206
+ * A morph that swaps several of them repaints once, and the delivery Stimulus
207
+ * makes before `connect()` is refused, because the grid is painted on
208
+ * connect. None of these Values decides the month on screen; `min` and `max`
209
+ * decide whether the requested day is published.
210
+ */
211
+ #repaint = new MicrotaskCoalescer(() => {
212
+ this.#repaintKeepingFocus();
213
+ });
214
+ /**
215
+ * The cell DOM focus was on, its day and the month on screen, recorded by the
216
+ * first repaint of a batch for the pass that settles focus after it.
167
217
  */
168
- #focusTimer = new SafeTimeout();
218
+ #focusOrigin = null;
169
219
  /**
170
- * The painted month the last `monthchange` reported, or `null` before the first
171
- * paint. Settling on the initial month — whether it comes from the attribute or
172
- * is derived here — is the grid describing itself, not a navigation, so it must
173
- * not reach a listener that refetches inventory or pushes history.
220
+ * Settles DOM focus once, after the last repaint of a batch. The callbacks of
221
+ * one mutation repaint one by one and each reads every Value as it stands, so
222
+ * a `selected` or `locale` callback can paint a month before that month's own
223
+ * callback has moved the tab stop into it. Outside a connection no pass is
224
+ * scheduled, so a grid that is not connected moves no focus.
174
225
  */
175
- #announcedMonth = null;
226
+ #settleFocus = new MicrotaskCoalescer(() => {
227
+ this.#restoreFocus();
228
+ });
176
229
  connect() {
177
- if (!this.monthValue) {
178
- const today = /* @__PURE__ */ new Date();
179
- const monthStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}`;
180
- this.monthValue = monthStr;
181
- return;
230
+ this.#repaint.activate();
231
+ this.#settleFocus.activate();
232
+ this.#focusOrigin = null;
233
+ if (!this.monthValue && !this.#opened) {
234
+ this.#paintedMonth = toISOMonthString(parseISODateString(this.#selection) ?? /* @__PURE__ */ new Date());
182
235
  }
236
+ this.#opened = true;
183
237
  this.#initializeFocusedDate();
184
- this.render();
238
+ this.#render();
239
+ this.#connected = true;
185
240
  }
186
- /** Cancels any pending deferred focus so it never fires on a detached element. */
241
+ /**
242
+ * Drops a pending repaint and a pending focus move, and stops reporting, so
243
+ * nothing reaches for a grid that is not connected.
244
+ */
187
245
  disconnect() {
188
- this.#focusTimer.clearAll();
246
+ this.#connected = false;
247
+ this.#repaint.cancel();
248
+ this.#settleFocus.cancel();
189
249
  }
190
250
  /**
191
- * Stimulus lifecycle callback triggered automatically when the monthValue changes.
192
- * Forces a re-render of the date grid and updates labels.
251
+ * Paints the month `month` names, whether application code, a morph or the
252
+ * grid's own navigation wrote it; focus in the grid follows to the tab stop.
193
253
  */
194
254
  monthValueChanged() {
195
255
  if (!this.monthValue) return;
196
256
  this.#syncFocusedDateWithMonth();
197
- this.render();
257
+ this.#repaintKeepingFocus();
198
258
  }
199
259
  /**
200
260
  * Repaints the label when application code changes `locale` at runtime.
201
261
  *
202
262
  * Stimulus runs this for the initial Value too, before `connect()`. The paint
203
263
  * it triggers is idempotent — the month comes from `month` (or, while that is
204
- * still empty, from `focusedDate`), and `monthchange` only reports a move away
205
- * from a month already announced, so the first paint never reports one.
264
+ * still empty, from `focusedDate`) — and a grid that has not connected
265
+ * reports nothing, so that paint never reports a month.
206
266
  */
207
267
  localeValueChanged() {
208
- this.render();
268
+ this.#repaintKeepingFocus();
209
269
  }
210
270
  /**
211
- * Stimulus lifecycle callback triggered automatically when the selectedValue changes.
212
- * Re-renders grid cells to update `aria-selected` indicators.
271
+ * Repaints `aria-selected` and the field when the requested day changes, and
272
+ * moves the tab stop to the day the grid now publishes. A request `min` /
273
+ * `max` withhold is not published, so the tab stop stays where it was.
213
274
  */
214
275
  selectedValueChanged() {
215
- if (this.selectedValue) {
216
- const selected = parseISODateString(this.selectedValue);
217
- if (selected) {
218
- this.focusedDate = selected;
219
- }
220
- }
221
- this.render();
276
+ const selection = parseISODateString(this.#selection);
277
+ if (selection) this.focusedDate = selection;
278
+ this.#repaintKeepingFocus();
279
+ }
280
+ /** Repaints when `min` changes after connect, so the disabled days and the selection follow. */
281
+ minValueChanged() {
282
+ this.#repaint.schedule();
283
+ }
284
+ /** Repaints when `max` changes after connect, so the disabled days and the selection follow. */
285
+ maxValueChanged() {
286
+ this.#repaint.schedule();
287
+ }
288
+ /**
289
+ * Lays the grid out again when `weekStart` changes after connect, so every
290
+ * date moves to the column of its weekday.
291
+ *
292
+ * Only the day cells are painted: column headers are author markup, so the
293
+ * author updates them together with the Value.
294
+ */
295
+ weekStartValueChanged() {
296
+ this.#repaint.schedule();
222
297
  }
223
298
  /** Navigates to the previous month. */
224
299
  prev(event) {
@@ -233,7 +308,7 @@ var CalendarController = class extends Controller {
233
308
  /** Handles day selection when a gridcell is clicked. */
234
309
  selectByClick(event) {
235
310
  const dayElement = event.target?.closest(
236
- "[data-stimeo--calendar-target='day']"
311
+ targetSelector(this.identifier, "day")
237
312
  );
238
313
  if (!dayElement) return;
239
314
  this.selectDayElement(dayElement);
@@ -243,7 +318,7 @@ var CalendarController = class extends Controller {
243
318
  if (event.defaultPrevented) return;
244
319
  if (isReservedArrowChord(event)) return;
245
320
  const dayElement = event.target?.closest(
246
- "[data-stimeo--calendar-target='day']"
321
+ targetSelector(this.identifier, "day")
247
322
  );
248
323
  if (!dayElement) return;
249
324
  const date = parseISODateString(dayElement.getAttribute("data-date") ?? "");
@@ -308,8 +383,8 @@ var CalendarController = class extends Controller {
308
383
  }
309
384
  }
310
385
  /**
311
- * First day of the month the grid paints: the `month` Value when it parses,
312
- * otherwise the focused date's month.
386
+ * First day of the month the grid paints: the `month` Value when it parses —
387
+ * while it is empty, the month on screen — otherwise the focused date's month.
313
388
  *
314
389
  * A malformed `month` falls back instead of stopping the paint: every cell
315
390
  * keeps its `aria-selected` and the grid keeps its tab stop, so a Value typo
@@ -318,12 +393,22 @@ var CalendarController = class extends Controller {
318
393
  * the paint and the `monthchange` report cannot name different months.
319
394
  */
320
395
  #paintedMonthStart() {
321
- const monthInfo = parseISOMonthString(this.monthValue);
396
+ const monthInfo = parseISOMonthString(this.#monthRequest);
322
397
  return monthInfo ? new Date(monthInfo.year, monthInfo.month - 1, 1) : new Date(this.focusedDate.getFullYear(), this.focusedDate.getMonth(), 1);
323
398
  }
324
399
  /**
325
- * Renders the grid days and updates the month/year label, then reports the
326
- * painted month once it differs from the one already announced.
400
+ * The month the grid is asked to paint: `month`, or while that is empty the
401
+ * month on screen, which `connect()` opens on the month of the published
402
+ * selection, else on the current one.
403
+ */
404
+ get #monthRequest() {
405
+ return this.monthValue || this.#paintedMonth;
406
+ }
407
+ /**
408
+ * Renders the grid days and updates the month/year label, mirrors both
409
+ * fields, then reports each field the user's move moved and — once connected
410
+ * — a painted month that differs from the last one painted, and a published
411
+ * selection that a change the page made moved.
327
412
  *
328
413
  * The report belongs to the paint, not to the `month` Value: the Value is only
329
414
  * one of the things that decide the month on screen. While a malformed `month`
@@ -331,24 +416,36 @@ var CalendarController = class extends Controller {
331
416
  * move the painted month too, and both repaint from here. Reading the report
332
417
  * off the paint keeps the event naming the month the grid shows, in the
333
418
  * `YYYY-MM` the detail contract promises, whichever route repainted.
419
+ *
420
+ * @stimeoRenderRoot
334
421
  */
335
- render() {
422
+ #render() {
336
423
  const monthStart = this.#paintedMonthStart();
337
424
  const year = monthStart.getFullYear();
338
425
  const month = monthStart.getMonth() + 1;
426
+ const selection = this.#selection;
339
427
  if (this.hasLabelTarget) {
340
428
  const locale = resolveLocale(this.element, this.localeValue);
341
429
  this.labelTarget.textContent = monthLabelFormatter(locale).format(monthStart);
342
430
  }
343
431
  const days = this.#calculateGridDays(year, month);
344
432
  const dayElements = this.dayTargets;
433
+ const consumerDisabled = /* @__PURE__ */ new Map();
434
+ for (const el of dayElements) {
435
+ const shown = el.getAttribute("data-date");
436
+ const value = el.getAttribute("aria-disabled");
437
+ if (!shown || value === null || el.hasAttribute(this.#ownedDisabled)) continue;
438
+ consumerDisabled.set(shown, value);
439
+ }
345
440
  for (const [index, date] of days.entries()) {
346
441
  const el = dayElements[index];
347
442
  if (!el) continue;
348
443
  const dateStr = toISODateString(date);
349
444
  if (el.getAttribute("data-date") !== dateStr) {
350
445
  el.removeAttribute("aria-disabled");
351
- el.removeAttribute(OWNED_DISABLED);
446
+ el.removeAttribute(this.#ownedDisabled);
447
+ const kept = consumerDisabled.get(dateStr);
448
+ if (kept !== void 0) el.setAttribute("aria-disabled", kept);
352
449
  }
353
450
  el.setAttribute("data-date", dateStr);
354
451
  el.textContent = String(date.getDate());
@@ -356,17 +453,16 @@ var CalendarController = class extends Controller {
356
453
  el.setAttribute("data-outside", String(isOutside));
357
454
  const todayStr = toISODateString(/* @__PURE__ */ new Date());
358
455
  el.setAttribute("data-today", String(dateStr === todayStr));
359
- const isSelected = dateStr === this.selectedValue;
360
- el.setAttribute("aria-selected", String(isSelected));
456
+ el.setAttribute("aria-selected", String(dateStr === selection));
361
457
  const isFocused = toISODateString(this.focusedDate) === dateStr;
362
458
  el.setAttribute("tabindex", isFocused ? "0" : "-1");
363
459
  const isDisabled = this.#isDateOutOfBounds(dateStr);
364
460
  if (isDisabled) {
365
- if (!el.hasAttribute("aria-disabled")) el.setAttribute(OWNED_DISABLED, "");
461
+ if (!el.hasAttribute("aria-disabled")) el.setAttribute(this.#ownedDisabled, "");
366
462
  el.setAttribute("aria-disabled", "true");
367
- } else if (el.hasAttribute(OWNED_DISABLED)) {
463
+ } else if (el.hasAttribute(this.#ownedDisabled)) {
368
464
  el.removeAttribute("aria-disabled");
369
- el.removeAttribute(OWNED_DISABLED);
465
+ el.removeAttribute(this.#ownedDisabled);
370
466
  }
371
467
  }
372
468
  if (!dayElements.some((el) => el.getAttribute("tabindex") === "0")) {
@@ -374,50 +470,151 @@ var CalendarController = class extends Controller {
374
470
  fallback?.setAttribute("tabindex", "0");
375
471
  }
376
472
  const painted = toISOMonthString(monthStart);
377
- const previous = this.#announcedMonth;
378
- this.#announcedMonth = painted;
379
- if (previous !== null && previous !== painted) {
473
+ const previous = this.#paintedMonth;
474
+ this.#paintedMonth = painted;
475
+ const reconciled = this.#settleSelection(selection);
476
+ const move = this.#selectionMoves;
477
+ for (const [field, value] of this.#mirrorFields(painted, selection, reconciled)) {
478
+ if (field.value === value) commitField(field);
479
+ }
480
+ if (this.#connected && previous !== painted && this.#paintedMonth === painted) {
380
481
  this.dispatch("monthchange", { detail: { month: painted } });
381
482
  }
483
+ if (reconciled && move === this.#selectionMoves) {
484
+ this.dispatch("reconcile", { detail: { date: selection } });
485
+ }
486
+ }
487
+ /**
488
+ * Records the selection a paint publishes and says whether it moved from the
489
+ * one published before, which a connected grid reports as `reconcile`. A pick
490
+ * records its own day before its paint, so that paint finds no move, and
491
+ * nothing counts before the grid connects. A move counts as a move of the
492
+ * selection.
493
+ */
494
+ #settleSelection(selection) {
495
+ const published = this.#published;
496
+ this.#published = selection;
497
+ if (!this.#connected || selection === published) return false;
498
+ this.#selectionMoves += 1;
499
+ return true;
500
+ }
501
+ /**
502
+ * Repaints the grid for a Value callback or a pick. The first repaint of a
503
+ * batch records where DOM focus stood on a day, for the settle pass; focus
504
+ * outside the grid is left alone.
505
+ */
506
+ #repaintKeepingFocus() {
507
+ if (!this.#focusOrigin) {
508
+ const cell = this.dayTargets.find((day) => day === document.activeElement);
509
+ if (cell) {
510
+ this.#focusOrigin = {
511
+ cell,
512
+ date: cell.getAttribute("data-date"),
513
+ month: this.#paintedMonth
514
+ };
515
+ this.#settleFocus.schedule();
516
+ }
517
+ }
518
+ this.#render();
519
+ }
520
+ /**
521
+ * Puts DOM focus back on a day it can stay on once a batch of repaints has
522
+ * landed.
523
+ *
524
+ * While the painted month stays, a bound only toggles `aria-disabled` and a
525
+ * disabled day stays focusable, so nothing is called (`focus()` would scroll
526
+ * the day into view); `weekStart` moves every date to another cell, so focus
527
+ * follows its date, or goes to the tab stop once the date has left the grid.
528
+ * When the painted month moves, every cell shows another day, and focus goes
529
+ * to the tab stop of the new month. Focus a listener of the paint's events
530
+ * moved off the recorded cell stays where it went.
531
+ */
532
+ #restoreFocus() {
533
+ const origin = this.#focusOrigin;
534
+ this.#focusOrigin = null;
535
+ if (!origin || document.activeElement !== origin.cell) return;
536
+ const moved = this.#paintedMonth !== origin.month;
537
+ if (!moved && origin.cell.getAttribute("data-date") === origin.date) return;
538
+ const destination = (moved ? void 0 : this.dayTargets.find((cell) => cell.getAttribute("data-date") === origin.date)) ?? this.dayTargets.find((cell) => cell.getAttribute("tabindex") === "0");
539
+ destination?.focus();
540
+ }
541
+ /** Fills a selected-day field inserted or replaced at runtime with the published selection. */
542
+ fieldTargetConnected(field) {
543
+ writeField(field, this.#selection);
544
+ }
545
+ /** Fills a painted-month field inserted or replaced at runtime. */
546
+ monthFieldTargetConnected(field) {
547
+ writeField(field, toISOMonthString(this.#paintedMonthStart()));
548
+ }
549
+ /**
550
+ * Mirrors the painted month and the published selection into their optional
551
+ * fields.
552
+ *
553
+ * @param reconciled - Whether a change the page made moved the selection in
554
+ * this paint, which `reconcile` reports rather than the field.
555
+ * @returns Each field this grid's own controls moved, with the value written
556
+ * to it, for the paint to report.
557
+ */
558
+ #mirrorFields(painted, selection, reconciled) {
559
+ const byUser = this.#movedByUser;
560
+ this.#movedByUser = false;
561
+ const moved = [];
562
+ if (this.hasFieldTarget && writeField(this.fieldTarget, selection) && !reconciled) {
563
+ moved.push([this.fieldTarget, selection]);
564
+ }
565
+ if (this.hasMonthFieldTarget && writeField(this.monthFieldTarget, painted)) {
566
+ moved.push([this.monthFieldTarget, painted]);
567
+ }
568
+ return byUser ? moved : [];
382
569
  }
383
570
  selectDayElement(dayElement) {
384
571
  if (dayElement.getAttribute("aria-disabled") === "true") return;
385
572
  const dateStr = dayElement.getAttribute("data-date");
386
- if (!dateStr) return;
573
+ if (!dateStr || this.#isDateOutOfBounds(dateStr)) return;
387
574
  this.selectedValue = dateStr;
388
575
  const selected = parseISODateString(dateStr);
389
576
  if (selected) this.focusedDate = selected;
390
- this.render();
577
+ this.#movedByUser = true;
578
+ this.#selectionMoves += 1;
579
+ const move = this.#selectionMoves;
580
+ this.#published = dateStr;
581
+ this.#repaintKeepingFocus();
582
+ if (move !== this.#selectionMoves) return;
391
583
  this.dispatch("select", { detail: { date: dateStr } });
392
584
  }
393
585
  #focusAndNavigateToDate(date) {
394
586
  this.focusedDate = date;
395
587
  const targetMonthStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
396
- const isMonthTransition = targetMonthStr !== this.monthValue;
397
- if (isMonthTransition) {
588
+ this.#movedByUser = true;
589
+ if (targetMonthStr !== this.#monthRequest) {
398
590
  this.monthValue = targetMonthStr;
399
- } else {
400
- this.render();
401
- }
402
- const focusTarget = () => {
403
- const dateStr = toISODateString(date);
404
- const targetEl = this.dayTargets.find((el) => el.getAttribute("data-date") === dateStr);
405
- targetEl?.focus();
406
- };
407
- if (isMonthTransition) {
408
- this.#focusTimer.set(focusTarget, 0);
409
- } else {
410
- focusTarget();
591
+ return;
411
592
  }
593
+ this.#render();
594
+ const dateStr = toISODateString(date);
595
+ this.dayTargets.find((el) => el.getAttribute("data-date") === dateStr)?.focus();
412
596
  }
413
597
  #isDateOutOfBounds(dateStr) {
414
598
  if (this.minValue && dateStr < this.minValue) return true;
415
599
  if (this.maxValue && dateStr > this.maxValue) return true;
416
600
  return false;
417
601
  }
602
+ /** The day `selected` requests: the Value when it names a real day, else `""`. */
603
+ get #requestedDay() {
604
+ return parseISODateString(this.selectedValue) ? this.selectedValue : "";
605
+ }
606
+ /** The selection published for `requested`: the day while `min` / `max` allow it, else `""`. */
607
+ #publishable(requested) {
608
+ return requested && !this.#isDateOutOfBounds(requested) ? requested : "";
609
+ }
610
+ /** The selection the grid publishes now. */
611
+ get #selection() {
612
+ return this.#publishable(this.#requestedDay);
613
+ }
418
614
  #shiftMonth(delta) {
419
- const monthInfo = parseISOMonthString(this.monthValue);
615
+ const monthInfo = parseISOMonthString(this.#monthRequest);
420
616
  if (!monthInfo) return;
617
+ this.#movedByUser = true;
421
618
  const nextMonthDate = new Date(monthInfo.year, monthInfo.month - 1 + delta, 1);
422
619
  this.monthValue = `${nextMonthDate.getFullYear()}-${String(nextMonthDate.getMonth() + 1).padStart(2, "0")}`;
423
620
  }
@@ -456,15 +653,18 @@ var CalendarController = class extends Controller {
456
653
  target.setDate(start.getDate() + 6);
457
654
  return target;
458
655
  }
656
+ /**
657
+ * Opens the tab stop on the published selection, else on today when the shown
658
+ * month holds it, else on the 1st of the shown month. A selection the grid
659
+ * does not show falls back to the 1st in the paint.
660
+ */
459
661
  #initializeFocusedDate() {
460
- if (this.selectedValue) {
461
- const selected = parseISODateString(this.selectedValue);
462
- if (selected) {
463
- this.focusedDate = selected;
464
- return;
465
- }
662
+ const selection = parseISODateString(this.#selection);
663
+ if (selection) {
664
+ this.focusedDate = selection;
665
+ return;
466
666
  }
467
- const monthInfo = parseISOMonthString(this.monthValue);
667
+ const monthInfo = parseISOMonthString(this.#monthRequest);
468
668
  if (monthInfo) {
469
669
  const today = /* @__PURE__ */ new Date();
470
670
  if (today.getFullYear() === monthInfo.year && today.getMonth() === monthInfo.month - 1) {