stimeo-ui 0.5.0 → 0.7.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 (230) hide show
  1. package/CHANGELOG.md +178 -0
  2. package/dist/controllers/alert_dialog_controller.d.ts +2 -0
  3. package/dist/controllers/alert_dialog_controller.js +75 -4
  4. package/dist/controllers/alert_dialog_controller.js.map +1 -1
  5. package/dist/controllers/aspect_ratio_controller.d.ts +1 -3
  6. package/dist/controllers/aspect_ratio_controller.js +19 -11
  7. package/dist/controllers/aspect_ratio_controller.js.map +1 -1
  8. package/dist/controllers/auto_submit_controller.d.ts +2 -0
  9. package/dist/controllers/auto_submit_controller.js.map +1 -1
  10. package/dist/controllers/avatar_controller.d.ts +36 -15
  11. package/dist/controllers/avatar_controller.js +237 -40
  12. package/dist/controllers/avatar_controller.js.map +1 -1
  13. package/dist/controllers/breadcrumb_controller.d.ts +2 -0
  14. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  15. package/dist/controllers/bulk_select_controller.d.ts +2 -0
  16. package/dist/controllers/bulk_select_controller.js.map +1 -1
  17. package/dist/controllers/calendar_controller.d.ts +2 -0
  18. package/dist/controllers/calendar_controller.js.map +1 -1
  19. package/dist/controllers/carousel_controller.d.ts +12 -3
  20. package/dist/controllers/carousel_controller.js +85 -9
  21. package/dist/controllers/carousel_controller.js.map +1 -1
  22. package/dist/controllers/character_counter_controller.d.ts +52 -20
  23. package/dist/controllers/character_counter_controller.js +338 -63
  24. package/dist/controllers/character_counter_controller.js.map +1 -1
  25. package/dist/controllers/checkbox_controller.d.ts +34 -5
  26. package/dist/controllers/checkbox_controller.js +136 -25
  27. package/dist/controllers/checkbox_controller.js.map +1 -1
  28. package/dist/controllers/color_picker_controller.d.ts +10 -1
  29. package/dist/controllers/color_picker_controller.js +35 -9
  30. package/dist/controllers/color_picker_controller.js.map +1 -1
  31. package/dist/controllers/combobox_controller.d.ts +2 -0
  32. package/dist/controllers/combobox_controller.js.map +1 -1
  33. package/dist/controllers/command_palette_controller.d.ts +2 -0
  34. package/dist/controllers/command_palette_controller.js +75 -4
  35. package/dist/controllers/command_palette_controller.js.map +1 -1
  36. package/dist/controllers/conditional_fields_controller.d.ts +42 -14
  37. package/dist/controllers/conditional_fields_controller.js +345 -51
  38. package/dist/controllers/conditional_fields_controller.js.map +1 -1
  39. package/dist/controllers/confirm_controller.d.ts +2 -0
  40. package/dist/controllers/confirm_controller.js +75 -4
  41. package/dist/controllers/confirm_controller.js.map +1 -1
  42. package/dist/controllers/count_up_controller.d.ts +2 -0
  43. package/dist/controllers/count_up_controller.js.map +1 -1
  44. package/dist/controllers/countdown_controller.d.ts +4 -0
  45. package/dist/controllers/countdown_controller.js.map +1 -1
  46. package/dist/controllers/currency_input_controller.d.ts +2 -0
  47. package/dist/controllers/currency_input_controller.js.map +1 -1
  48. package/dist/controllers/data_grid_controller.d.ts +3 -0
  49. package/dist/controllers/data_grid_controller.js.map +1 -1
  50. package/dist/controllers/date_range_picker_controller.d.ts +23 -4
  51. package/dist/controllers/date_range_picker_controller.js +157 -30
  52. package/dist/controllers/date_range_picker_controller.js.map +1 -1
  53. package/dist/controllers/dialog_controller.js +75 -4
  54. package/dist/controllers/dialog_controller.js.map +1 -1
  55. package/dist/controllers/direct_upload_controller.d.ts +56 -24
  56. package/dist/controllers/direct_upload_controller.js +201 -45
  57. package/dist/controllers/direct_upload_controller.js.map +1 -1
  58. package/dist/controllers/dirty_form_controller.d.ts +14 -6
  59. package/dist/controllers/dirty_form_controller.js +192 -29
  60. package/dist/controllers/dirty_form_controller.js.map +1 -1
  61. package/dist/controllers/dismissible_controller.d.ts +2 -0
  62. package/dist/controllers/dismissible_controller.js +83 -18
  63. package/dist/controllers/dismissible_controller.js.map +1 -1
  64. package/dist/controllers/drawer_controller.js +75 -4
  65. package/dist/controllers/drawer_controller.js.map +1 -1
  66. package/dist/controllers/empty_state_controller.d.ts +2 -0
  67. package/dist/controllers/empty_state_controller.js.map +1 -1
  68. package/dist/controllers/file_dropzone_controller.d.ts +9 -1
  69. package/dist/controllers/file_dropzone_controller.js +26 -3
  70. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  71. package/dist/controllers/filter_controller.d.ts +2 -0
  72. package/dist/controllers/filter_controller.js.map +1 -1
  73. package/dist/controllers/flash_controller.d.ts +4 -0
  74. package/dist/controllers/flash_controller.js.map +1 -1
  75. package/dist/controllers/focus_controller.d.ts +4 -3
  76. package/dist/controllers/focus_controller.js +75 -4
  77. package/dist/controllers/focus_controller.js.map +1 -1
  78. package/dist/controllers/form_field_controller.d.ts +50 -10
  79. package/dist/controllers/form_field_controller.js +280 -62
  80. package/dist/controllers/form_field_controller.js.map +1 -1
  81. package/dist/controllers/form_validation_controller.d.ts +10 -8
  82. package/dist/controllers/form_validation_controller.js +208 -83
  83. package/dist/controllers/form_validation_controller.js.map +1 -1
  84. package/dist/controllers/frame_loading_controller.d.ts +2 -0
  85. package/dist/controllers/frame_loading_controller.js.map +1 -1
  86. package/dist/controllers/highlight_controller.d.ts +2 -0
  87. package/dist/controllers/highlight_controller.js.map +1 -1
  88. package/dist/controllers/hover_card_controller.d.ts +2 -2
  89. package/dist/controllers/hover_card_controller.js.map +1 -1
  90. package/dist/controllers/idle_controller.d.ts +5 -3
  91. package/dist/controllers/idle_controller.js +27 -5
  92. package/dist/controllers/idle_controller.js.map +1 -1
  93. package/dist/controllers/input_mask_controller.d.ts +2 -0
  94. package/dist/controllers/input_mask_controller.js.map +1 -1
  95. package/dist/controllers/lazy_frame_controller.d.ts +2 -0
  96. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  97. package/dist/controllers/listbox_controller.d.ts +2 -0
  98. package/dist/controllers/listbox_controller.js.map +1 -1
  99. package/dist/controllers/local_time_controller.d.ts +2 -0
  100. package/dist/controllers/local_time_controller.js.map +1 -1
  101. package/dist/controllers/masonry_controller.d.ts +2 -0
  102. package/dist/controllers/masonry_controller.js.map +1 -1
  103. package/dist/controllers/menubar_controller.js +5 -3
  104. package/dist/controllers/menubar_controller.js.map +1 -1
  105. package/dist/controllers/meter_controller.d.ts +2 -0
  106. package/dist/controllers/meter_controller.js.map +1 -1
  107. package/dist/controllers/multi_select_controller.d.ts +48 -12
  108. package/dist/controllers/multi_select_controller.js +460 -151
  109. package/dist/controllers/multi_select_controller.js.map +1 -1
  110. package/dist/controllers/nested_form_controller.d.ts +2 -0
  111. package/dist/controllers/nested_form_controller.js.map +1 -1
  112. package/dist/controllers/network_status_controller.d.ts +2 -0
  113. package/dist/controllers/network_status_controller.js.map +1 -1
  114. package/dist/controllers/number_input_controller.d.ts +26 -6
  115. package/dist/controllers/number_input_controller.js +317 -51
  116. package/dist/controllers/number_input_controller.js.map +1 -1
  117. package/dist/controllers/otp_controller.d.ts +3 -0
  118. package/dist/controllers/otp_controller.js.map +1 -1
  119. package/dist/controllers/overflow_indicator_controller.d.ts +2 -0
  120. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  121. package/dist/controllers/overflow_menu_controller.d.ts +2 -0
  122. package/dist/controllers/overflow_menu_controller.js +6 -1
  123. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  124. package/dist/controllers/pagination_controller.d.ts +2 -0
  125. package/dist/controllers/pagination_controller.js +35 -1
  126. package/dist/controllers/pagination_controller.js.map +1 -1
  127. package/dist/controllers/password_reveal_controller.d.ts +2 -0
  128. package/dist/controllers/password_reveal_controller.js.map +1 -1
  129. package/dist/controllers/password_strength_controller.d.ts +5 -3
  130. package/dist/controllers/password_strength_controller.js +20 -2
  131. package/dist/controllers/password_strength_controller.js.map +1 -1
  132. package/dist/controllers/persist_controller.d.ts +33 -19
  133. package/dist/controllers/persist_controller.js +449 -120
  134. package/dist/controllers/persist_controller.js.map +1 -1
  135. package/dist/controllers/pointer_drag_controller.d.ts +3 -0
  136. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  137. package/dist/controllers/popover_controller.d.ts +2 -2
  138. package/dist/controllers/popover_controller.js +77 -4
  139. package/dist/controllers/popover_controller.js.map +1 -1
  140. package/dist/controllers/portal_controller.d.ts +3 -2
  141. package/dist/controllers/portal_controller.js.map +1 -1
  142. package/dist/controllers/preview_guard_controller.d.ts +2 -0
  143. package/dist/controllers/preview_guard_controller.js.map +1 -1
  144. package/dist/controllers/progress_controller.d.ts +3 -0
  145. package/dist/controllers/progress_controller.js.map +1 -1
  146. package/dist/controllers/radio_group_controller.d.ts +44 -15
  147. package/dist/controllers/radio_group_controller.js +540 -56
  148. package/dist/controllers/radio_group_controller.js.map +1 -1
  149. package/dist/controllers/rating_controller.d.ts +38 -31
  150. package/dist/controllers/rating_controller.js +276 -89
  151. package/dist/controllers/rating_controller.js.map +1 -1
  152. package/dist/controllers/reading_progress_controller.d.ts +2 -0
  153. package/dist/controllers/reading_progress_controller.js.map +1 -1
  154. package/dist/controllers/resizable_controller.d.ts +2 -0
  155. package/dist/controllers/resizable_controller.js +33 -0
  156. package/dist/controllers/resizable_controller.js.map +1 -1
  157. package/dist/controllers/roving_controller.d.ts +6 -0
  158. package/dist/controllers/roving_controller.js +60 -5
  159. package/dist/controllers/roving_controller.js.map +1 -1
  160. package/dist/controllers/scroll_area_controller.d.ts +25 -11
  161. package/dist/controllers/scroll_area_controller.js +557 -125
  162. package/dist/controllers/scroll_area_controller.js.map +1 -1
  163. package/dist/controllers/scroll_visibility_controller.d.ts +2 -0
  164. package/dist/controllers/scroll_visibility_controller.js +33 -0
  165. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  166. package/dist/controllers/scrollspy_controller.d.ts +2 -0
  167. package/dist/controllers/scrollspy_controller.js.map +1 -1
  168. package/dist/controllers/separator_controller.d.ts +54 -11
  169. package/dist/controllers/separator_controller.js +354 -38
  170. package/dist/controllers/separator_controller.js.map +1 -1
  171. package/dist/controllers/sidebar_controller.js +83 -10
  172. package/dist/controllers/sidebar_controller.js.map +1 -1
  173. package/dist/controllers/skeleton_controller.d.ts +2 -0
  174. package/dist/controllers/skeleton_controller.js.map +1 -1
  175. package/dist/controllers/slider_controller.d.ts +2 -0
  176. package/dist/controllers/slider_controller.js.map +1 -1
  177. package/dist/controllers/smart_sticky_header_controller.d.ts +2 -0
  178. package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
  179. package/dist/controllers/spinner_controller.d.ts +2 -0
  180. package/dist/controllers/spinner_controller.js.map +1 -1
  181. package/dist/controllers/step_indicator_controller.d.ts +2 -0
  182. package/dist/controllers/step_indicator_controller.js.map +1 -1
  183. package/dist/controllers/stepper_controller.d.ts +2 -0
  184. package/dist/controllers/stepper_controller.js.map +1 -1
  185. package/dist/controllers/stick_to_bottom_controller.d.ts +2 -0
  186. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  187. package/dist/controllers/sticky_observer_controller.d.ts +2 -0
  188. package/dist/controllers/sticky_observer_controller.js.map +1 -1
  189. package/dist/controllers/submit_once_controller.d.ts +94 -38
  190. package/dist/controllers/submit_once_controller.js +399 -121
  191. package/dist/controllers/submit_once_controller.js.map +1 -1
  192. package/dist/controllers/switch_controller.d.ts +2 -0
  193. package/dist/controllers/switch_controller.js.map +1 -1
  194. package/dist/controllers/tags_input_controller.d.ts +43 -11
  195. package/dist/controllers/tags_input_controller.js +356 -120
  196. package/dist/controllers/tags_input_controller.js.map +1 -1
  197. package/dist/controllers/textarea_autosize_controller.d.ts +2 -0
  198. package/dist/controllers/textarea_autosize_controller.js.map +1 -1
  199. package/dist/controllers/theme_controller.d.ts +2 -0
  200. package/dist/controllers/theme_controller.js +8 -6
  201. package/dist/controllers/theme_controller.js.map +1 -1
  202. package/dist/controllers/time_picker_controller.d.ts +44 -13
  203. package/dist/controllers/time_picker_controller.js +296 -107
  204. package/dist/controllers/time_picker_controller.js.map +1 -1
  205. package/dist/controllers/toast_controller.d.ts +4 -0
  206. package/dist/controllers/toast_controller.js.map +1 -1
  207. package/dist/controllers/toggle_group_controller.d.ts +41 -13
  208. package/dist/controllers/toggle_group_controller.js +378 -55
  209. package/dist/controllers/toggle_group_controller.js.map +1 -1
  210. package/dist/controllers/toolbar_controller.js +5 -3
  211. package/dist/controllers/toolbar_controller.js.map +1 -1
  212. package/dist/controllers/tooltip_controller.d.ts +2 -2
  213. package/dist/controllers/tooltip_controller.js.map +1 -1
  214. package/dist/controllers/transition_controller.d.ts +4 -3
  215. package/dist/controllers/transition_controller.js.map +1 -1
  216. package/dist/controllers/tree_view_controller.d.ts +2 -0
  217. package/dist/controllers/tree_view_controller.js +7 -4
  218. package/dist/controllers/tree_view_controller.js.map +1 -1
  219. package/dist/index.js +4963 -1581
  220. package/dist/index.js.map +1 -1
  221. package/dist/inspector/cli.d.ts +123 -6
  222. package/dist/inspector/cli.js +212 -18
  223. package/dist/inspector/cli.js.map +1 -1
  224. package/dist/inspector/cli_bin.js +274 -50
  225. package/dist/inspector/cli_bin.js.map +1 -1
  226. package/dist/inspector/examples.json +15 -15
  227. package/dist/inspector/manifest.json +602 -80
  228. package/dist/positioning/index.d.ts +4 -2
  229. package/dist/positioning/index.js.map +1 -1
  230. package/package.json +1 -1
@@ -18,9 +18,46 @@ function isReservedArrowChord(event, allow = []) {
18
18
  return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
19
19
  }
20
20
 
21
+ // src/utils/microtask_coalescer.ts
22
+ var MicrotaskCoalescer = class {
23
+ #run;
24
+ #queued = false;
25
+ #active = false;
26
+ #generation = 0;
27
+ /** @param run - the single reconciliation pass, invoked at most once per batch. */
28
+ constructor(run) {
29
+ this.#run = run;
30
+ }
31
+ /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
32
+ activate() {
33
+ this.#active = true;
34
+ }
35
+ /** Closes the window and drops any pending pass; call from `disconnect()`. */
36
+ cancel() {
37
+ this.#active = false;
38
+ this.#queued = false;
39
+ this.#generation += 1;
40
+ }
41
+ /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
42
+ schedule() {
43
+ if (!this.#active || this.#queued) return;
44
+ this.#queued = true;
45
+ const generation = this.#generation;
46
+ queueMicrotask(() => {
47
+ if (generation !== this.#generation || !this.#queued || !this.#active) return;
48
+ this.#queued = false;
49
+ this.#run();
50
+ });
51
+ }
52
+ };
53
+
21
54
  // src/controllers/time_picker_controller.ts
22
55
  var AM = 0;
23
56
  var PM = 1;
57
+ var SECONDS_PER_MINUTE = 60;
58
+ var SECONDS_PER_HOUR = 60 * SECONDS_PER_MINUTE;
59
+ var SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR;
60
+ var hasModifier = (event) => event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
24
61
  var TimePickerController = class extends Controller {
25
62
  static targets = ["segment", "field"];
26
63
  static values = {
@@ -30,35 +67,80 @@ var TimePickerController = class extends Controller {
30
67
  wrap: { type: Boolean, default: true }
31
68
  };
32
69
  static actions = ["onKeydown"];
33
- static events = ["change"];
34
- /** Current numeric value per segment kind (hours are the *displayed* hours). */
35
- #state = { hour: 0, minute: 0, second: 0, meridiem: AM };
70
+ static events = ["change", "reconcile"];
71
+ /** Collapses target, Value, and retained-attribute morphs into one silent render. */
72
+ #reconcile = new MicrotaskCoalescer(() => this.#reconcileDom());
73
+ /** Canonical state; the displayed hour and meridiem are derived from this value. */
74
+ #state = { hour: 0, minute: 0, second: 0 };
36
75
  /** Direct-entry digit buffer and the segment it belongs to. */
37
76
  #typeBuffer = "";
38
77
  #typeSegment = null;
39
- /** Last composed field value, to suppress duplicate `change` dispatches. */
78
+ /** Last composed value, used to suppress duplicate user notifications. */
40
79
  #lastValue = "";
41
- /** Seeds each segment from its initial `aria-valuenow` and syncs the field. */
80
+ #observer = null;
81
+ #connected = false;
82
+ #domDirty = false;
83
+ /** Seeds state from the DOM, renders canonical ARIA, and starts morph observation. */
42
84
  connect() {
43
- for (const segment of this.segmentTargets) {
44
- const kind = this.#kindOf(segment);
45
- if (!kind) continue;
46
- const now = Number(segment.getAttribute("aria-valuenow"));
47
- const { min, max } = this.#bounds(kind);
48
- this.#state[kind] = Number.isFinite(now) ? Math.min(max, Math.max(min, now)) : min;
49
- }
50
- for (const segment of this.segmentTargets) this.#renderSegment(segment);
51
- this.#syncField(false);
85
+ this.#connected = true;
86
+ this.#reconcile.activate();
87
+ this.element.addEventListener("focusout", this.#onFocusOut);
88
+ this.#domDirty = false;
89
+ this.#adoptDomState();
90
+ this.#render();
91
+ this.#observeMutations();
92
+ }
93
+ /** Releases the observer, listener, queued reconciliation, and transient input state. */
94
+ disconnect() {
95
+ this.#connected = false;
96
+ this.#reconcile.cancel();
97
+ this.element.removeEventListener("focusout", this.#onFocusOut);
98
+ this.#observer?.disconnect();
99
+ this.#observer = null;
100
+ this.#domDirty = false;
101
+ this.#clearTypeBuffer();
102
+ this.#lastValue = "";
103
+ }
104
+ /** Re-renders the canonical instant when 12/24-hour presentation changes at runtime. */
105
+ hourCycleValueChanged() {
106
+ this.#reconcile.schedule();
107
+ }
108
+ /** Recomposes the optional seconds portion after a runtime Value change. */
109
+ secondsValueChanged() {
110
+ this.#reconcile.schedule();
111
+ }
112
+ /** Adopts a segment inserted or replaced by a Turbo morph. */
113
+ segmentTargetConnected() {
114
+ this.#domDirty = true;
115
+ this.#clearTypeBuffer();
116
+ this.#reconcile.schedule();
117
+ }
118
+ /** Rebuilds state after a segment leaves the retained controller element. */
119
+ segmentTargetDisconnected() {
120
+ this.#domDirty = true;
121
+ this.#clearTypeBuffer();
122
+ this.#reconcile.schedule();
123
+ }
124
+ /** Reflects the current composed value into a field added or replaced at runtime. */
125
+ fieldTargetConnected() {
126
+ this.#reconcile.schedule();
127
+ }
128
+ /** Reconciles silently when the optional field leaves at runtime. */
129
+ fieldTargetDisconnected() {
130
+ this.#reconcile.schedule();
52
131
  }
53
- /** Handles stepping, inter-segment focus moves, jumps, and direct entry. */
132
+ /** Handles APG stepping, inter-segment focus moves, jumps, and direct entry. */
54
133
  onKeydown(event) {
55
134
  if (isReservedArrowChord(event)) return;
56
135
  const segment = event.target?.closest(
57
- "[data-stimeo--time-picker-target='segment']"
136
+ "[data-stimeo--time-picker-target~='segment']"
58
137
  );
59
- const kind = segment ? this.#kindOf(segment) : null;
60
- if (!segment || !kind) return;
61
- switch (logicalArrowKey(event.key, this.element)) {
138
+ if (!segment || !this.segmentTargets.includes(segment)) return;
139
+ const kind = this.#kindOf(segment);
140
+ if (!kind) return;
141
+ const key = logicalArrowKey(event.key, this.element);
142
+ if ((key === "Home" || key === "End") && hasModifier(event)) return;
143
+ switch (key) {
62
144
  case "ArrowUp":
63
145
  event.preventDefault();
64
146
  this.#step(kind, this.#delta(kind));
@@ -77,134 +159,202 @@ var TimePickerController = class extends Controller {
77
159
  break;
78
160
  case "Home":
79
161
  event.preventDefault();
80
- this.#set(kind, this.#bounds(kind).min);
162
+ this.#setDisplayed(kind, this.#bounds(kind).min);
163
+ this.#commitRender();
81
164
  break;
82
165
  case "End":
83
166
  event.preventDefault();
84
- this.#set(kind, this.#bounds(kind).max);
167
+ this.#setDisplayed(kind, this.#bounds(kind).max);
168
+ this.#commitRender();
85
169
  break;
86
170
  default:
87
- if (/^[0-9]$/.test(event.key)) {
171
+ if (kind !== "meridiem" && /^[0-9]$/.test(event.key) && !hasModifier(event)) {
88
172
  event.preventDefault();
89
173
  this.#typeDigit(segment, kind, event.key);
174
+ return;
90
175
  }
91
176
  return;
92
177
  }
93
- this.#typeBuffer = "";
94
- this.#typeSegment = null;
178
+ this.#clearTypeBuffer();
95
179
  }
96
- /** The per-step delta: minutes step by `step`, others by 1, meridiem toggles. */
180
+ /** Clears direct-entry state when Tab, Shift+Tab, or pointer focus leaves a segment. */
181
+ #onFocusOut = (event) => {
182
+ const segment = event.target?.closest(
183
+ "[data-stimeo--time-picker-target~='segment']"
184
+ );
185
+ if (!segment || !this.segmentTargets.includes(segment)) return;
186
+ this.#clearTypeBuffer();
187
+ };
188
+ /** Uses the normalized positive-integer minute step; other segments step by one. */
97
189
  #delta(kind) {
98
- return kind === "minute" ? this.stepValue : 1;
190
+ if (kind !== "minute") return 1;
191
+ const step = Math.trunc(this.stepValue);
192
+ return Number.isFinite(step) && step > 0 ? step : 1;
99
193
  }
100
- /** Steps a segment, wrapping at its bounds and carrying over when enabled. */
194
+ /** Mutates one complete action and then renders/notifies its final value once. */
101
195
  #step(kind, delta) {
102
- if (kind === "meridiem") {
103
- this.#set("meridiem", this.#state.meridiem === AM ? PM : AM);
104
- return;
105
- }
106
- const { min, max } = this.#bounds(kind);
107
- const span = max - min + 1;
108
- const raw = this.#state[kind] + delta;
109
- if (raw > max || raw < min) {
110
- if (!this.wrapValue) {
111
- this.#set(kind, Math.min(max, Math.max(min, raw)));
112
- return;
113
- }
114
- const wrapped = ((raw - min) % span + span) % span + min;
115
- const carry = Math.floor((raw - min) / span);
116
- this.#set(kind, wrapped);
117
- this.#carry(kind, carry);
118
- return;
196
+ if (kind === "meridiem" && this.wrapValue) {
197
+ this.#state.hour = modulo(this.#state.hour + 12, 24);
198
+ } else if (this.wrapValue) {
199
+ const scale = kind === "hour" ? SECONDS_PER_HOUR : kind === "minute" ? SECONDS_PER_MINUTE : 1;
200
+ this.#setFromSeconds(this.#secondsOfDay + delta * scale);
201
+ } else if (kind === "hour") {
202
+ this.#state.hour = Math.min(23, Math.max(0, this.#state.hour + delta));
203
+ } else {
204
+ const { min, max } = this.#bounds(kind);
205
+ const displayed = this.#displayValue(kind);
206
+ this.#setDisplayed(kind, Math.min(max, Math.max(min, displayed + delta)));
119
207
  }
120
- this.#set(kind, raw);
121
- }
122
- /** Propagates a wrap carry from `kind` into the next larger segment. */
123
- #carry(kind, amount) {
124
- if (kind === "second") this.#step("minute", amount);
125
- else if (kind === "minute") this.#step("hour", amount);
126
- }
127
- /**
128
- * Sets a segment's value (clamped to its `[min, max]` bounds), re-renders it,
129
- * and resyncs the composed field. Clamping here guards the direct-entry path:
130
- * typing `0` into a 12-hour hour (min 1) must not commit an out-of-range
131
- * `aria-valuenow="0"`. The stepping path already passes in-bounds values, so
132
- * the clamp is a no-op there.
133
- */
134
- #set(kind, value) {
135
- const { min, max } = this.#bounds(kind);
136
- this.#state[kind] = Math.min(max, Math.max(min, value));
137
- const segment = this.segmentTargets.find((s) => this.#kindOf(s) === kind);
138
- if (segment) this.#renderSegment(segment);
139
- this.#syncField(true);
208
+ this.#commitRender();
140
209
  }
141
- /** Accumulates a typed digit, committing and advancing after two digits. */
210
+ /** Accumulates a typed digit, rendering once and advancing after completion. */
142
211
  #typeDigit(segment, kind, digit) {
143
- if (kind === "meridiem") return;
144
212
  if (this.#typeSegment !== kind) this.#typeBuffer = "";
145
213
  this.#typeSegment = kind;
146
214
  const { max } = this.#bounds(kind);
147
215
  const candidate = Number(`${this.#typeBuffer}${digit}`);
148
- if (candidate <= max) this.#typeBuffer = `${this.#typeBuffer}${digit}`;
149
- else this.#typeBuffer = digit;
150
- this.#set(kind, Number(this.#typeBuffer));
216
+ this.#typeBuffer = candidate <= max ? `${this.#typeBuffer}${digit}` : digit;
217
+ this.#setDisplayed(kind, Number(this.#typeBuffer));
218
+ this.#commitRender();
151
219
  if (this.#typeBuffer.length >= 2 || Number(this.#typeBuffer) * 10 > max) {
152
- this.#typeBuffer = "";
153
- this.#typeSegment = null;
220
+ this.#clearTypeBuffer();
154
221
  this.#focusSibling(segment, 1);
155
222
  }
156
223
  }
157
- /** Moves focus to the previous/next segment, if one exists. */
224
+ /** Moves focus to the previous/next valid segment, if one exists. */
158
225
  #focusSibling(segment, direction) {
159
- const index = this.segmentTargets.indexOf(segment);
160
- const next = this.segmentTargets[index + direction];
161
- next?.focus();
226
+ const segments = this.segmentTargets.filter((candidate) => this.#kindOf(candidate) !== null);
227
+ const index = segments.indexOf(segment);
228
+ segments[index + direction]?.focus();
229
+ }
230
+ /** Clears the direct-entry buffer and its owning segment together. */
231
+ #clearTypeBuffer() {
232
+ this.#typeBuffer = "";
233
+ this.#typeSegment = null;
234
+ }
235
+ /** Reconciles one coalesced target, Value, or retained-attribute mutation batch. */
236
+ #reconcileDom() {
237
+ if (this.#domDirty) {
238
+ this.#domDirty = false;
239
+ this.#clearTypeBuffer();
240
+ this.#adoptDomState();
241
+ }
242
+ this.#render();
243
+ }
244
+ /** Rebuilds canonical state from the targets; an absent hour represents midnight. */
245
+ #adoptDomState() {
246
+ let displayedHour = 0;
247
+ let meridiem = AM;
248
+ let minute = 0;
249
+ let second = 0;
250
+ for (const segment of this.segmentTargets) {
251
+ const kind = this.#kindOf(segment);
252
+ if (!kind) continue;
253
+ const value = this.#authoredValue(segment, kind);
254
+ if (kind === "hour") displayedHour = value;
255
+ else if (kind === "minute") minute = value;
256
+ else if (kind === "second") second = value;
257
+ else meridiem = value;
258
+ }
259
+ this.#state = {
260
+ hour: this.hourCycleValue === 12 ? displayedHour % 12 + (meridiem === PM ? 12 : 0) : displayedHour,
261
+ minute,
262
+ second
263
+ };
162
264
  }
163
- /** Reflects a segment's current value onto its ARIA/text representation. */
265
+ /** Reads and integer-clamps one authored `aria-valuenow`. */
266
+ #authoredValue(segment, kind) {
267
+ const raw = Number(segment.getAttribute("aria-valuenow"));
268
+ const { min, max } = this.#bounds(kind);
269
+ if (!Number.isFinite(raw)) return min;
270
+ return Math.min(max, Math.max(min, Math.trunc(raw)));
271
+ }
272
+ /** Writes a displayed segment value back into canonical state without rendering. */
273
+ #setDisplayed(kind, raw) {
274
+ const { min, max } = this.#bounds(kind);
275
+ const value = Math.min(max, Math.max(min, Math.trunc(raw)));
276
+ if (kind === "hour") {
277
+ this.#state.hour = this.hourCycleValue === 12 ? value % 12 + (this.#state.hour >= 12 ? 12 : 0) : value;
278
+ } else if (kind === "minute" || kind === "second") {
279
+ this.#state[kind] = value;
280
+ } else {
281
+ this.#state.hour = this.#state.hour % 12 + (value === PM ? 12 : 0);
282
+ }
283
+ }
284
+ /** Converts seconds with day wrapping into the canonical three-unit state. */
285
+ #setFromSeconds(raw) {
286
+ const seconds = modulo(raw, SECONDS_PER_DAY);
287
+ this.#state.hour = Math.floor(seconds / SECONDS_PER_HOUR);
288
+ this.#state.minute = Math.floor(seconds % SECONDS_PER_HOUR / SECONDS_PER_MINUTE);
289
+ this.#state.second = seconds % SECONDS_PER_MINUTE;
290
+ }
291
+ /** Reflects canonical state to every segment without dispatching user events. */
292
+ #renderSegments() {
293
+ this.#withoutObservation(() => {
294
+ for (const segment of this.segmentTargets) this.#renderSegment(segment);
295
+ });
296
+ }
297
+ /**
298
+ * Reflects canonical state during connection or DOM reconciliation.
299
+ *
300
+ * No user edit reaches here, so `change` never fires from this path. A pass
301
+ * that moves the committed value reports `reconcile` instead.
302
+ *
303
+ * @stimeoRenderRoot
304
+ */
305
+ #render() {
306
+ this.#renderSegments();
307
+ const value = this.#composedValue;
308
+ const previous = this.#lastValue;
309
+ this.#writeField(value);
310
+ this.#lastValue = value;
311
+ if (previous !== "" && value !== previous) {
312
+ this.dispatch("reconcile", { detail: { value } });
313
+ }
314
+ }
315
+ /** Reflects one completed user action and dispatches its final change exactly once. */
316
+ #commitRender() {
317
+ this.#renderSegments();
318
+ const value = this.#composedValue;
319
+ const fieldChanged = this.#writeField(value);
320
+ if (fieldChanged) this.fieldTarget.dispatchEvent(new Event("change", { bubbles: true }));
321
+ if (value !== this.#lastValue) this.dispatch("change", { detail: { value } });
322
+ this.#lastValue = value;
323
+ }
324
+ /** Reflects one segment's value and controller-owned ARIA bounds/text. */
164
325
  #renderSegment(segment) {
165
326
  const kind = this.#kindOf(segment);
166
327
  if (!kind) return;
167
- const value = this.#state[kind];
168
- if (kind === "meridiem") {
169
- const text2 = value === PM ? "PM" : "AM";
170
- segment.setAttribute("aria-valuenow", String(value));
171
- segment.setAttribute("aria-valuetext", text2);
172
- segment.setAttribute("aria-valuemin", String(AM));
173
- segment.setAttribute("aria-valuemax", String(PM));
174
- segment.textContent = text2;
175
- return;
176
- }
328
+ const value = this.#displayValue(kind);
329
+ const text = kind === "meridiem" ? value === PM ? "PM" : "AM" : pad(value);
177
330
  const { min, max } = this.#bounds(kind);
178
- const text = String(value).padStart(2, "0");
179
331
  segment.setAttribute("aria-valuenow", String(value));
180
332
  segment.setAttribute("aria-valuetext", text);
181
333
  segment.setAttribute("aria-valuemin", String(min));
182
334
  segment.setAttribute("aria-valuemax", String(max));
183
335
  segment.textContent = text;
184
336
  }
185
- /** Composes `HH:MM[:SS]` and notifies both native-form and widget consumers. */
186
- #syncField(notify) {
187
- const h24 = this.#hours24();
188
- const parts = [pad(h24), pad(this.#state.minute)];
337
+ /** Writes a composed value to the optional form field and reports whether it changed. */
338
+ #writeField(value) {
339
+ if (!this.hasFieldTarget || this.fieldTarget.value === value) return false;
340
+ this.fieldTarget.value = value;
341
+ return true;
342
+ }
343
+ /** The canonical form value composed as `HH:MM[:SS]`. */
344
+ get #composedValue() {
345
+ const parts = [pad(this.#state.hour), pad(this.#state.minute)];
189
346
  if (this.secondsValue) parts.push(pad(this.#state.second));
190
- const value = parts.join(":");
191
- const fieldChanged = this.hasFieldTarget && this.fieldTarget.value !== value;
192
- if (fieldChanged) {
193
- this.fieldTarget.value = value;
194
- }
195
- if (notify && fieldChanged) {
196
- this.fieldTarget.dispatchEvent(new Event("change", { bubbles: true }));
197
- }
198
- if (notify && value !== this.#lastValue) this.dispatch("change", { detail: { value } });
199
- this.#lastValue = value;
347
+ return parts.join(":");
200
348
  }
201
- /** Converts the displayed hour (+ meridiem in 12-hour mode) to 24-hour. */
202
- #hours24() {
203
- if (this.hourCycleValue !== 12) return this.#state.hour;
204
- const base = this.#state.hour % 12;
205
- return base + (this.#state.meridiem === PM ? 12 : 0);
349
+ /** Returns a canonical unit in the presentation form exposed by its segment. */
350
+ #displayValue(kind) {
351
+ if (kind === "hour") {
352
+ return this.hourCycleValue === 12 ? this.#state.hour % 12 || 12 : this.#state.hour;
353
+ }
354
+ if (kind === "meridiem") return this.#state.hour >= 12 ? PM : AM;
355
+ return this.#state[kind];
206
356
  }
207
- /** The inclusive `[min, max]` bounds for a segment kind. */
357
+ /** The inclusive controller-owned bounds for a segment kind. */
208
358
  #bounds(kind) {
209
359
  switch (kind) {
210
360
  case "hour":
@@ -216,6 +366,38 @@ var TimePickerController = class extends Controller {
216
366
  return { min: AM, max: PM };
217
367
  }
218
368
  }
369
+ /** Watches retained segment state while excluding this controller's own reflections. */
370
+ #observeMutations() {
371
+ const observer = new MutationObserver((records) => {
372
+ const changed = records.some(
373
+ ({ target }) => target instanceof HTMLElement && this.segmentTargets.includes(target)
374
+ );
375
+ if (!changed) return;
376
+ this.#domDirty = true;
377
+ this.#clearTypeBuffer();
378
+ this.#reconcile.schedule();
379
+ });
380
+ this.#observer = observer;
381
+ this.#observeWith(observer);
382
+ }
383
+ /** Registers retained attribute observation on the controller subtree. */
384
+ #observeWith(observer) {
385
+ observer.observe(this.element, {
386
+ subtree: true,
387
+ attributes: true,
388
+ attributeFilter: ["aria-valuenow", "data-segment"]
389
+ });
390
+ }
391
+ /** Temporarily pauses observation so derived ARIA writes cannot become DOM input. */
392
+ #withoutObservation(run) {
393
+ const observer = this.#observer;
394
+ observer?.disconnect();
395
+ try {
396
+ run();
397
+ } finally {
398
+ if (observer && this.#connected) this.#observeWith(observer);
399
+ }
400
+ }
219
401
  /** Reads a segment's declared kind, or null when absent/invalid. */
220
402
  #kindOf(segment) {
221
403
  const kind = segment.getAttribute("data-segment");
@@ -224,7 +406,14 @@ var TimePickerController = class extends Controller {
224
406
  }
225
407
  return null;
226
408
  }
409
+ /** Canonical state as seconds since the start of its nominal day. */
410
+ get #secondsOfDay() {
411
+ return this.#state.hour * SECONDS_PER_HOUR + this.#state.minute * SECONDS_PER_MINUTE + this.#state.second;
412
+ }
227
413
  };
414
+ function modulo(value, modulus) {
415
+ return (value % modulus + modulus) % modulus;
416
+ }
228
417
  function pad(value) {
229
418
  return String(value).padStart(2, "0");
230
419
  }