stimeo-ui 0.14.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 (107) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +212 -0
  3. data/README.md +120 -0
  4. data/dist/cable/index.js +123 -29
  5. data/dist/controllers/accordion_controller.js +98 -9
  6. data/dist/controllers/announcer_controller.js +96 -62
  7. data/dist/controllers/auto_submit_controller.js +83 -7
  8. data/dist/controllers/avatar_controller.js +1 -1
  9. data/dist/controllers/breadcrumb_controller.js +38 -11
  10. data/dist/controllers/bulk_select_controller.js +7 -6
  11. data/dist/controllers/calendar_controller.js +340 -123
  12. data/dist/controllers/carousel_controller.js +263 -38
  13. data/dist/controllers/character_counter_controller.js +40 -2
  14. data/dist/controllers/checkbox_controller.js +81 -12
  15. data/dist/controllers/clipboard_controller.js +63 -5
  16. data/dist/controllers/collapsible_controller.js +99 -14
  17. data/dist/controllers/color_picker_controller.js +80 -34
  18. data/dist/controllers/combobox_controller.js +106 -16
  19. data/dist/controllers/command_palette_controller.js +35 -3
  20. data/dist/controllers/conditional_fields_controller.js +85 -17
  21. data/dist/controllers/confirm_controller.js +3 -0
  22. data/dist/controllers/context_menu_controller.js +32 -12
  23. data/dist/controllers/countdown_controller.js +129 -26
  24. data/dist/controllers/currency_input_controller.js +221 -67
  25. data/dist/controllers/data_grid_controller.js +195 -29
  26. data/dist/controllers/date_range_picker_controller.js +151 -30
  27. data/dist/controllers/dialog_controller.js +35 -8
  28. data/dist/controllers/direct_upload_controller.js +22 -4
  29. data/dist/controllers/dirty_form_controller.js +46 -13
  30. data/dist/controllers/dismissible_controller.js +1 -0
  31. data/dist/controllers/drawer_controller.js +54 -19
  32. data/dist/controllers/dropdown_controller.js +36 -9
  33. data/dist/controllers/editable_controller.js +34 -0
  34. data/dist/controllers/file_dropzone_controller.js +144 -51
  35. data/dist/controllers/filter_controller.js +20 -6
  36. data/dist/controllers/flash_controller.js +432 -71
  37. data/dist/controllers/focus_controller.js +1 -0
  38. data/dist/controllers/form_field_controller.js +7 -5
  39. data/dist/controllers/form_validation_controller.js +19 -13
  40. data/dist/controllers/frame_loading_controller.js +45 -8
  41. data/dist/controllers/highlight_controller.js +82 -25
  42. data/dist/controllers/hover_card_controller.js +40 -14
  43. data/dist/controllers/idle_controller.js +90 -5
  44. data/dist/controllers/input_mask_controller.js +65 -9
  45. data/dist/controllers/intersection_controller.js +3 -0
  46. data/dist/controllers/lazy_frame_controller.js +11 -2
  47. data/dist/controllers/listbox_controller.js +203 -45
  48. data/dist/controllers/local_time_controller.js +10 -5
  49. data/dist/controllers/masonry_controller.js +31 -15
  50. data/dist/controllers/menu_controller.js +45 -16
  51. data/dist/controllers/menubar_controller.js +58 -24
  52. data/dist/controllers/meter_controller.js +9 -5
  53. data/dist/controllers/multi_select_controller.js +278 -104
  54. data/dist/controllers/navigation_menu_controller.js +48 -15
  55. data/dist/controllers/nested_form_controller.js +37 -8
  56. data/dist/controllers/network_status_controller.js +9 -1
  57. data/dist/controllers/number_input_controller.js +124 -21
  58. data/dist/controllers/optimistic_controller.js +42 -5
  59. data/dist/controllers/otp_controller.js +198 -55
  60. data/dist/controllers/overflow_indicator_controller.js +115 -21
  61. data/dist/controllers/overflow_menu_controller.js +141 -44
  62. data/dist/controllers/pagination_controller.js +74 -28
  63. data/dist/controllers/password_reveal_controller.js +59 -2
  64. data/dist/controllers/persist_controller.js +30 -8
  65. data/dist/controllers/pointer_drag_controller.js +131 -52
  66. data/dist/controllers/popover_controller.js +45 -11
  67. data/dist/controllers/portal_controller.js +6 -2
  68. data/dist/controllers/preview_guard_controller.js +16 -1
  69. data/dist/controllers/progress_controller.js +8 -4
  70. data/dist/controllers/radio_group_controller.js +42 -17
  71. data/dist/controllers/range_slider_controller.js +88 -42
  72. data/dist/controllers/rating_controller.js +39 -15
  73. data/dist/controllers/read_more_controller.js +100 -7
  74. data/dist/controllers/reading_progress_controller.js +65 -19
  75. data/dist/controllers/relative_time_controller.js +10 -5
  76. data/dist/controllers/resizable_controller.js +82 -22
  77. data/dist/controllers/scroll_area_controller.js +75 -27
  78. data/dist/controllers/scroll_restore_controller.js +37 -16
  79. data/dist/controllers/scroll_visibility_controller.js +49 -30
  80. data/dist/controllers/scrollspy_controller.js +71 -26
  81. data/dist/controllers/separator_controller.js +66 -37
  82. data/dist/controllers/sidebar_controller.js +77 -18
  83. data/dist/controllers/skeleton_controller.js +6 -1
  84. data/dist/controllers/slider_controller.js +82 -47
  85. data/dist/controllers/smart_sticky_header_controller.js +60 -26
  86. data/dist/controllers/sortable_controller.js +17 -2
  87. data/dist/controllers/spinner_controller.js +10 -2
  88. data/dist/controllers/step_indicator_controller.js +18 -17
  89. data/dist/controllers/stepper_controller.js +101 -19
  90. data/dist/controllers/stick_to_bottom_controller.js +104 -8
  91. data/dist/controllers/submit_once_controller.js +45 -9
  92. data/dist/controllers/switch_controller.js +101 -10
  93. data/dist/controllers/tabs_controller.js +21 -2
  94. data/dist/controllers/tags_input_controller.js +209 -59
  95. data/dist/controllers/textarea_autosize_controller.js +29 -3
  96. data/dist/controllers/theme_controller.js +64 -14
  97. data/dist/controllers/time_picker_controller.js +23 -8
  98. data/dist/controllers/toast_controller.js +451 -105
  99. data/dist/controllers/toggle_group_controller.js +159 -23
  100. data/dist/controllers/toolbar_controller.js +32 -0
  101. data/dist/controllers/tooltip_controller.js +39 -13
  102. data/dist/controllers/transition_controller.js +4 -0
  103. data/dist/controllers/tree_view_controller.js +169 -16
  104. data/dist/index.js +5002 -1911
  105. data/dist/positioning/index.js +2 -0
  106. data/lib/stimeo/ui/version.rb +1 -1
  107. metadata +2 -2
@@ -99,7 +99,12 @@ var SafeInterval = class extends TimerRegistry {
99
99
 
100
100
  // src/controllers/countdown_controller.ts
101
101
  var SECOND_MS = 1e3;
102
+ var OWNED_STATUS = "owns-status";
102
103
  var CountdownController = class extends Controller {
104
+ /** The status marker, in the namespace this controller is registered under. */
105
+ get #ownedStatus() {
106
+ return `data-${this.identifier}-${OWNED_STATUS}`;
107
+ }
103
108
  static targets = ["days", "hours", "minutes", "seconds", "status"];
104
109
  static values = {
105
110
  deadline: { type: String, default: "" },
@@ -115,6 +120,12 @@ var CountdownController = class extends Controller {
115
120
  #intervalId = null;
116
121
  /** Collapses a morph that swaps several render inputs at once into one re-derive. */
117
122
  #resync = new MicrotaskCoalescer(() => this.#resyncToValues());
123
+ /**
124
+ * Whether `connect()` has run for this connection. Stimulus delivers Value
125
+ * callbacks ahead of it, while the run state is still the markup's own and not
126
+ * yet settled; `connect()` brings the status up to date once it is.
127
+ */
128
+ #connected = false;
118
129
  /** Epoch-ms anchor: the deadline (down) or the count-up origin (up). */
119
130
  #reference = 0;
120
131
  /** Amount (ms) captured at pause, so resume can restore the same display. */
@@ -127,28 +138,25 @@ var CountdownController = class extends Controller {
127
138
  */
128
139
  #renderedAmount = 0;
129
140
  connect() {
141
+ this.#connected = true;
130
142
  this.#resync.activate();
131
143
  this.#initReference();
132
144
  const amount = this.#currentAmount();
133
145
  this.#render(amount);
134
146
  this.#pausedAmount = this.#renderedAmount;
135
147
  const authored = this.element.getAttribute("data-state");
148
+ const settled = authored === "complete" && this.#isSettled(amount);
136
149
  if (authored === null) {
137
- if (this.autostartValue && this.#isValidDeadline) {
138
- this.start();
139
- return;
140
- }
150
+ if (this.autostartValue && this.#isValidDeadline) this.start();
151
+ else this.element.setAttribute("data-state", "paused");
152
+ } else if (!settled) {
141
153
  this.element.setAttribute("data-state", "paused");
142
- return;
154
+ if (authored === "running") this.start();
143
155
  }
144
- if (authored === "complete" && this.#isDown && amount <= 0) {
145
- return;
146
- }
147
- const wasRunning = authored === "running";
148
- this.element.setAttribute("data-state", "paused");
149
- if (wasRunning) this.start();
156
+ this.#syncStatus();
150
157
  }
151
158
  disconnect() {
159
+ this.#connected = false;
152
160
  this.#resync.cancel();
153
161
  this.#intervals.clearAll();
154
162
  this.#intervalId = null;
@@ -161,6 +169,31 @@ var CountdownController = class extends Controller {
161
169
  directionValueChanged() {
162
170
  this.#resync.schedule();
163
171
  }
172
+ /**
173
+ * Follows a completion label swapped in place by a morph.
174
+ *
175
+ * Render only: `complete` is neither dispatched nor announced again. The status
176
+ * changes only after the countdown has completed, and only while it still shows
177
+ * the text this controller wrote there and nothing else, so text or an element
178
+ * the consumer put in the slot stays. A change delivered ahead of `connect()` is
179
+ * left to `connect()`.
180
+ */
181
+ completeLabelValueChanged() {
182
+ if (this.#connected) this.#syncStatus();
183
+ }
184
+ /**
185
+ * Keeps a status that still shows this controller's text, and nothing else, on
186
+ * what the run state calls for: the label in force while complete; otherwise the
187
+ * text is taken back, marker and all. A status holding anything else is the
188
+ * consumer's and is left alone, marker included.
189
+ *
190
+ * @stimeoRenderRoot
191
+ */
192
+ #syncStatus() {
193
+ if (!this.hasStatusTarget || !this.#ownsStatus(this.statusTarget)) return;
194
+ if (this.#state === "complete") this.#writeStatus(this.statusTarget, this.completeLabelValue);
195
+ else this.#releaseStatus(this.statusTarget);
196
+ }
164
197
  /**
165
198
  * Points the anchor at the current `deadline` / `direction` and repaints.
166
199
  *
@@ -168,13 +201,27 @@ var CountdownController = class extends Controller {
168
201
  * paused timer run or replay a milestone. A running one needs no restart either —
169
202
  * every tick reads the anchor, so moving it is enough. While paused the stored
170
203
  * amount follows the new reading, or resume would continue from the old deadline.
204
+ * A completion the new reading no longer settles (a deadline moved forward, or a
205
+ * count turned `up`) is handed back paused, as `connect()` hands back a restored
206
+ * one, so `resume()` counts again; the completion text this controller wrote goes
207
+ * with it, from a status slot that still shows it and nothing else.
208
+ *
209
+ * @stimeoRenderRoot
171
210
  */
172
211
  #resyncToValues() {
173
212
  this.#initReference();
174
- this.#render(this.#currentAmount());
213
+ const amount = this.#currentAmount();
214
+ this.#render(amount);
215
+ if (this.#state === "complete" && !this.#isSettled(amount)) {
216
+ this.element.setAttribute("data-state", "paused");
217
+ this.#syncStatus();
218
+ }
175
219
  if (this.#state !== "running") this.#pausedAmount = this.#renderedAmount;
176
220
  }
177
- /** Starts (or restarts after pause) ticking toward the deadline. */
221
+ /**
222
+ * Starts (or restarts after pause) ticking toward the deadline. Counting on from a
223
+ * completion takes this controller's completion text back out of the status.
224
+ */
178
225
  start() {
179
226
  if (this.#state === "running" || !this.#isValidDeadline) return;
180
227
  if (this.#isDown && this.#currentAmount() <= 0) {
@@ -212,10 +259,8 @@ var CountdownController = class extends Controller {
212
259
  this.#initReference();
213
260
  const amount = this.#currentAmount();
214
261
  this.#render(amount);
215
- if (this.hasStatusTarget && this.statusTarget.textContent === this.completeLabelValue) {
216
- this.statusTarget.textContent = "";
217
- }
218
262
  this.element.setAttribute("data-state", "paused");
263
+ this.#syncStatus();
219
264
  if (wasRunning && this.#isValidDeadline) {
220
265
  this.#pausedAmount = 0;
221
266
  this.start();
@@ -223,9 +268,19 @@ var CountdownController = class extends Controller {
223
268
  this.#pausedAmount = this.#renderedAmount;
224
269
  }
225
270
  }
226
- /** Schedules the repeating tick and marks the timer running. */
271
+ /**
272
+ * Schedules the repeating tick and marks the timer running. This is the one way
273
+ * into `running`, and a running timer keeps none of this controller's completion
274
+ * text in the status, so the status is settled here as on every other way out of
275
+ * `complete`: `start()` leaves a completion directly when `direction` flips to
276
+ * `up` and `start()` runs in the same task, ahead of the direction callback.
277
+ *
278
+ * @stimeoRuntimeOnly `interval` is the period of the one timer this call arms; the running state
279
+ * it writes does not depend on a Value.
280
+ */
227
281
  #runInterval() {
228
282
  this.element.setAttribute("data-state", "running");
283
+ this.#syncStatus();
229
284
  this.#intervalId = this.#intervals.set(() => this.#tick(), this.intervalValue);
230
285
  }
231
286
  /** Cancels the repeating tick, if any. */
@@ -246,21 +301,73 @@ var CountdownController = class extends Controller {
246
301
  this.#complete();
247
302
  }
248
303
  }
249
- /** Stops at zero, marks completion, announces it, and emits `complete`. */
304
+ /**
305
+ * Stops at zero, marks completion, writes the completion label, emits `complete`,
306
+ * and announces it.
307
+ *
308
+ * @stimeoRenderRoot
309
+ */
250
310
  #complete() {
251
311
  this.#teardownInterval();
252
312
  this.#render(0);
253
313
  this.element.setAttribute("data-state", "complete");
254
- if (this.hasStatusTarget && this.completeLabelValue.length > 0) {
255
- this.statusTarget.textContent = this.completeLabelValue;
256
- }
314
+ this.#claimStatus();
257
315
  this.dispatch("complete", { detail: {} });
316
+ this.#announceCompletion();
317
+ }
318
+ /**
319
+ * Writes the label in force into the status slot as this controller's text. An
320
+ * empty label puts no text there: it claims a slot that is already empty — no
321
+ * text and no element — empties one that is still this controller's, and leaves
322
+ * any other slot, the consumer's, alone.
323
+ */
324
+ #claimStatus() {
325
+ if (!this.hasStatusTarget) return;
326
+ const status = this.statusTarget;
327
+ const label = this.completeLabelValue;
328
+ if (label === "" && !this.#isEmpty(status) && !this.#ownsStatus(status)) return;
329
+ this.#writeStatus(status, label);
330
+ }
331
+ /**
332
+ * Whether the status slot shows the text this controller wrote there and nothing
333
+ * else: the text matches the marker, and the slot holds no element. Writing the
334
+ * slot's text replaces every child node, so an element the consumer puts in the
335
+ * slot makes it theirs even while the text still matches. An empty marker
336
+ * matches the slot whenever it is empty.
337
+ */
338
+ #ownsStatus(status) {
339
+ return status.getAttribute(this.#ownedStatus) === status.textContent && status.firstElementChild === null;
340
+ }
341
+ /** Whether the status slot holds nothing at all: no text and no element. */
342
+ #isEmpty(status) {
343
+ return status.textContent === "" && status.firstElementChild === null;
344
+ }
345
+ /** Puts `text` into the status slot and records it as this controller's. */
346
+ #writeStatus(status, text) {
347
+ if (status.textContent !== text) status.textContent = text;
348
+ status.setAttribute(this.#ownedStatus, text);
349
+ }
350
+ /** Takes this controller's text out of the status slot, marker and all. */
351
+ #releaseStatus(status) {
352
+ status.textContent = "";
353
+ status.removeAttribute(this.#ownedStatus);
354
+ }
355
+ /**
356
+ * Reads the completion out through the shared announcer.
357
+ *
358
+ * @stimeoRuntimeOnly `announceText` words the one announcement a completion makes.
359
+ */
360
+ #announceCompletion() {
258
361
  announce(fillTemplate(this.announceTextValue, {}));
259
362
  }
260
363
  /** Sets the time anchor from the `deadline` value. */
261
364
  #initReference() {
262
365
  this.#reference = Date.parse(this.deadlineValue);
263
366
  }
367
+ /** Whether a reading of `amount` leaves nothing to count: only a countdown at zero. */
368
+ #isSettled(amount) {
369
+ return this.#isDown && amount <= 0;
370
+ }
264
371
  /** Remaining (down) or elapsed (up) ms, never negative. */
265
372
  #currentAmount() {
266
373
  if (!this.#isValidDeadline) return 0;
@@ -268,11 +375,7 @@ var CountdownController = class extends Controller {
268
375
  const raw = this.#isDown ? this.#reference - now : now - this.#reference;
269
376
  return Math.max(0, raw);
270
377
  }
271
- /**
272
- * Writes the amount into the day/hour/minute/second slots.
273
- *
274
- * @stimeoRenderRoot
275
- */
378
+ /** Writes the amount into the day/hour/minute/second slots. */
276
379
  #render(amount) {
277
380
  const totalSeconds = Math.floor(amount / SECOND_MS);
278
381
  this.#renderedAmount = totalSeconds * SECOND_MS;