stimeo-ui 0.3.0 → 0.5.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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +124 -0
  3. data/dist/controllers/alert_dialog_controller.js +32 -5
  4. data/dist/controllers/announcer_controller.js +255 -20
  5. data/dist/controllers/aspect_ratio_controller.js +1 -1
  6. data/dist/controllers/breadcrumb_controller.js +5 -1
  7. data/dist/controllers/carousel_controller.js +5 -1
  8. data/dist/controllers/clipboard_controller.js +8 -3
  9. data/dist/controllers/collapsible_controller.js +4 -1
  10. data/dist/controllers/color_picker_controller.js +52 -2
  11. data/dist/controllers/command_palette_controller.js +32 -5
  12. data/dist/controllers/confirm_controller.js +32 -5
  13. data/dist/controllers/context_menu_controller.js +2 -2
  14. data/dist/controllers/countdown_controller.js +117 -13
  15. data/dist/controllers/date_range_picker_controller.js +55 -1
  16. data/dist/controllers/dialog_controller.js +32 -5
  17. data/dist/controllers/direct_upload_controller.js +3 -3
  18. data/dist/controllers/drawer_controller.js +32 -5
  19. data/dist/controllers/empty_state_controller.js +128 -23
  20. data/dist/controllers/flash_controller.js +161 -21
  21. data/dist/controllers/focus_controller.js +32 -5
  22. data/dist/controllers/form_validation_controller.js +8 -2
  23. data/dist/controllers/frame_loading_controller.js +261 -27
  24. data/dist/controllers/highlight_controller.js +38 -1
  25. data/dist/controllers/idle_controller.js +13 -2
  26. data/dist/controllers/local_time_controller.js +102 -6
  27. data/dist/controllers/masonry_controller.js +1 -1
  28. data/dist/controllers/meter_controller.js +147 -26
  29. data/dist/controllers/network_status_controller.js +29 -11
  30. data/dist/controllers/number_input_controller.js +191 -24
  31. data/dist/controllers/overflow_menu_controller.js +34 -7
  32. data/dist/controllers/pagination_controller.js +5 -1
  33. data/dist/controllers/password_strength_controller.js +1 -1
  34. data/dist/controllers/pointer_drag_controller.js +10 -0
  35. data/dist/controllers/portal_controller.js +10 -0
  36. data/dist/controllers/progress_controller.js +123 -12
  37. data/dist/controllers/range_slider_controller.js +449 -93
  38. data/dist/controllers/rating_controller.js +55 -0
  39. data/dist/controllers/relative_time_controller.js +135 -12
  40. data/dist/controllers/scroll_area_controller.js +1 -1
  41. data/dist/controllers/separator_controller.js +13 -17
  42. data/dist/controllers/sidebar_controller.js +37 -8
  43. data/dist/controllers/skeleton_controller.js +143 -22
  44. data/dist/controllers/slider_controller.js +342 -50
  45. data/dist/controllers/spinner_controller.js +244 -28
  46. data/dist/controllers/step_indicator_controller.js +85 -6
  47. data/dist/controllers/stepper_controller.js +2 -0
  48. data/dist/controllers/stick_to_bottom_controller.js +60 -10
  49. data/dist/controllers/switch_controller.js +162 -18
  50. data/dist/controllers/textarea_autosize_controller.js +1 -1
  51. data/dist/controllers/time_picker_controller.js +6 -3
  52. data/dist/controllers/tree_view_controller.js +19 -1
  53. data/dist/index.js +2278 -596
  54. data/lib/stimeo/ui/version.rb +1 -1
  55. metadata +2 -2
@@ -2,6 +2,35 @@ import { Controller } from '@hotwired/stimulus';
2
2
 
3
3
  // src/controllers/overflow_menu_controller.ts
4
4
 
5
+ // src/utils/before_cache_reset.ts
6
+ var BeforeCacheReset = class _BeforeCacheReset {
7
+ /** Every subscribed instance, iterated by the one shared document listener. */
8
+ static #subscribers = /* @__PURE__ */ new Set();
9
+ /** The shared listener; installed while at least one instance is subscribed. */
10
+ static #onBeforeCache = () => {
11
+ for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
12
+ };
13
+ #rewind;
14
+ /** @param rewind - the pass that returns this controller's state to its initial form. */
15
+ constructor(rewind) {
16
+ this.#rewind = rewind;
17
+ }
18
+ /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
19
+ activate() {
20
+ const first = _BeforeCacheReset.#subscribers.size === 0;
21
+ _BeforeCacheReset.#subscribers.add(this);
22
+ if (first) {
23
+ document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
24
+ }
25
+ }
26
+ /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
27
+ deactivate() {
28
+ _BeforeCacheReset.#subscribers.delete(this);
29
+ if (_BeforeCacheReset.#subscribers.size > 0) return;
30
+ document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
31
+ }
32
+ };
33
+
5
34
  // src/utils/focus_candidate.ts
6
35
  function inheritsFieldsetDisabled(control) {
7
36
  let fieldset = control.closest("fieldset[disabled]");
@@ -161,7 +190,7 @@ var INDEX = "data-stimeo--overflow-menu-index";
161
190
  var BOUNDARY = "data-stimeo--overflow-menu-boundary";
162
191
  var SAVED_ROLE = "data-stimeo--overflow-menu-role";
163
192
  var SAVED_TABINDEX = "data-stimeo--overflow-menu-tabindex";
164
- var SAVED_MENU_TARGET = "data-stimeo--overflow-menu-menu-target";
193
+ var SAVED_MENU_TARGET = "data-stimeo--overflow-menu-saved-menu";
165
194
  var MENU_TARGET = "data-stimeo--menu-target";
166
195
  var BOOKKEEPING = [BANKED, INDEX, SAVED_ROLE, SAVED_TABINDEX, SAVED_MENU_TARGET];
167
196
  var OverflowMenuController = class extends Controller {
@@ -183,17 +212,15 @@ var OverflowMenuController = class extends Controller {
183
212
  #lastHidden = null;
184
213
  /** The `tabindex` this instance lends the root for the focus fallback. */
185
214
  #tabindex = new TabindexLoan();
186
- /** Hands Turbo a pristine snapshot: the cache is written after this event. */
187
- #onBeforeCache = () => {
188
- this.#restoreAll();
189
- };
215
+ /** Hands Turbo a pristine snapshot of the bar, with every item back in place. */
216
+ #beforeCache = new BeforeCacheReset(() => this.#restoreAll());
190
217
  connect() {
191
218
  if (!this.hasItemsTarget || !this.hasMoreTarget) return;
192
219
  const trigger = this.#trigger();
193
220
  if (trigger !== null && this.#isBareTrigger(trigger)) {
194
221
  trigger.textContent = this.moreLabelValue;
195
222
  }
196
- document.addEventListener("turbo:before-cache", this.#onBeforeCache);
223
+ this.#beforeCache.activate();
197
224
  this.#layout.observe(this.element);
198
225
  this.#layout.observeViewport();
199
226
  this.update();
@@ -201,7 +228,7 @@ var OverflowMenuController = class extends Controller {
201
228
  disconnect() {
202
229
  this.#layout.disconnect();
203
230
  this.#timers.clearAll();
204
- document.removeEventListener("turbo:before-cache", this.#onBeforeCache);
231
+ this.#beforeCache.deactivate();
205
232
  this.#restoreAll();
206
233
  this.#lastHidden = null;
207
234
  }
@@ -132,7 +132,11 @@ var PaginationController = class _PaginationController extends Controller {
132
132
  if (!Object.is(page, this.pageValue)) this.pageValue = page;
133
133
  this.#render();
134
134
  }
135
- /** Syncs `aria-current` on the page buttons and the prev/next `disabled` state. */
135
+ /**
136
+ * Syncs `aria-current` on the page buttons and the prev/next `disabled` state.
137
+ *
138
+ * @stimeoRenderRoot
139
+ */
136
140
  #render() {
137
141
  const page = this.#page;
138
142
  for (const button of this.pageTargets) {
@@ -126,7 +126,7 @@ var PasswordStrengthController = class _PasswordStrengthController extends Contr
126
126
  this.#toggle("data-strength", band, band.length > 0);
127
127
  this.#toggle("data-below-min", "true", score > 0 && score < this.minScoreValue);
128
128
  const ratio = max > 0 ? score / max : 0;
129
- this.element.style.setProperty("--stimeo-password-strength", String(ratio));
129
+ this.element.style.setProperty("--stimeo--password-strength", String(ratio));
130
130
  }
131
131
  #writeLabel(label) {
132
132
  if (this.hasLabelTarget) this.labelTarget.textContent = label;
@@ -12,6 +12,16 @@ function isReservedArrowChord(event, allow = []) {
12
12
  var DetachGate = class _DetachGate {
13
13
  /** Set while a probe is queued, waiting for a reconnect to cancel it. */
14
14
  #pending = false;
15
+ /**
16
+ * True while a probe is queued — the last disconnect was ambiguous and no
17
+ * reconnect has cancelled it yet. Read it from `connect()` to tell the
18
+ * reconnect half of an in-page move from a first connect: a controller whose
19
+ * initialisation restarts a measurement (a min-duration floor, an elapsed
20
+ * counter) must skip it for the move, where nothing actually restarted.
21
+ */
22
+ get pending() {
23
+ return this.#pending;
24
+ }
15
25
  /**
16
26
  * True when the disconnect is definitely a real detach — the element left
17
27
  * the document, or `data-controller` no longer lists the identifier. False
@@ -6,6 +6,16 @@ import { Controller } from '@hotwired/stimulus';
6
6
  var DetachGate = class _DetachGate {
7
7
  /** Set while a probe is queued, waiting for a reconnect to cancel it. */
8
8
  #pending = false;
9
+ /**
10
+ * True while a probe is queued — the last disconnect was ambiguous and no
11
+ * reconnect has cancelled it yet. Read it from `connect()` to tell the
12
+ * reconnect half of an in-page move from a first connect: a controller whose
13
+ * initialisation restarts a measurement (a min-duration floor, an elapsed
14
+ * counter) must skip it for the move, where nothing actually restarted.
15
+ */
16
+ get pending() {
17
+ return this.#pending;
18
+ }
9
19
  /**
10
20
  * True when the disconnect is definitely a real detach — the element left
11
21
  * the document, or `data-controller` no longer lists the identifier. False
@@ -2,6 +2,23 @@ import { Controller } from '@hotwired/stimulus';
2
2
 
3
3
  // src/controllers/progress_controller.ts
4
4
 
5
+ // src/utils/announce.ts
6
+ function announce(message, options = {}) {
7
+ const text = message.trim();
8
+ if (text.length === 0) return;
9
+ window.dispatchEvent(
10
+ new CustomEvent("stimeo--announcer:announce", {
11
+ detail: { message: text, assertive: options.assertive === true }
12
+ })
13
+ );
14
+ }
15
+ function fillTemplate(template, values) {
16
+ return template.replace(/\{([a-zA-Z][a-zA-Z0-9]*)\}/g, (match, name) => {
17
+ const replacement = values[name];
18
+ return replacement === void 0 ? match : String(replacement);
19
+ });
20
+ }
21
+
5
22
  // src/utils/coerce.ts
6
23
  function toFiniteNumber(raw) {
7
24
  if (raw === null || raw === void 0 || raw === "") return null;
@@ -9,7 +26,57 @@ function toFiniteNumber(raw) {
9
26
  return Number.isFinite(value) ? value : null;
10
27
  }
11
28
 
29
+ // src/utils/microtask_coalescer.ts
30
+ var MicrotaskCoalescer = class {
31
+ #run;
32
+ #queued = false;
33
+ #active = false;
34
+ #generation = 0;
35
+ /** @param run - the single reconciliation pass, invoked at most once per batch. */
36
+ constructor(run) {
37
+ this.#run = run;
38
+ }
39
+ /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
40
+ activate() {
41
+ this.#active = true;
42
+ }
43
+ /** Closes the window and drops any pending pass; call from `disconnect()`. */
44
+ cancel() {
45
+ this.#active = false;
46
+ this.#queued = false;
47
+ this.#generation += 1;
48
+ }
49
+ /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
50
+ schedule() {
51
+ if (!this.#active || this.#queued) return;
52
+ this.#queued = true;
53
+ const generation = this.#generation;
54
+ queueMicrotask(() => {
55
+ if (generation !== this.#generation || !this.#queued || !this.#active) return;
56
+ this.#queued = false;
57
+ this.#run();
58
+ });
59
+ }
60
+ };
61
+
62
+ // src/utils/range.ts
63
+ function rangeFraction(value, min, max) {
64
+ const span = max - min;
65
+ if (!(span > 0)) return 0;
66
+ const clamped = Math.min(max, Math.max(min, value));
67
+ let fraction;
68
+ if (Number.isFinite(span)) {
69
+ fraction = (clamped - min) / span;
70
+ } else {
71
+ const scale = Math.max(Math.abs(min), Math.abs(max));
72
+ fraction = (clamped / scale - min / scale) / (max / scale - min / scale);
73
+ }
74
+ if (!Number.isFinite(fraction)) return 0;
75
+ return Math.min(1, Math.max(0, fraction));
76
+ }
77
+
12
78
  // src/controllers/progress_controller.ts
79
+ var OWNED_VALUE_TEXT = "data-stimeo--progress-owns-valuetext";
13
80
  var ProgressController = class extends Controller {
14
81
  static targets = ["bar"];
15
82
  static values = {
@@ -17,13 +84,27 @@ var ProgressController = class extends Controller {
17
84
  min: { type: Number, default: 0 },
18
85
  max: { type: Number, default: 100 },
19
86
  indeterminate: { type: Boolean, default: false },
20
- valueText: { type: String, default: "" }
87
+ valueText: { type: String, default: "" },
88
+ announceText: { type: String, default: "" }
21
89
  };
22
90
  static actions = ["setValue"];
23
91
  static events = ["change", "complete"];
92
+ /**
93
+ * Collapses a morph that swaps several render inputs at once into one repaint.
94
+ * A single update usually rewrites the whole set, and each Value would otherwise
95
+ * repaint on its own.
96
+ */
97
+ #repaint = new MicrotaskCoalescer(() => {
98
+ this.#render();
99
+ });
24
100
  connect() {
101
+ this.#repaint.activate();
25
102
  this.#render();
26
103
  }
104
+ /** Closes the window in which a queued repaint may still run. */
105
+ disconnect() {
106
+ this.#repaint.cancel();
107
+ }
27
108
  /**
28
109
  * Updates the progress value from an action param (`amount`) or a
29
110
  * `detail.value` CustomEvent, normalizes it into range, syncs ARIA, and
@@ -39,11 +120,30 @@ var ProgressController = class extends Controller {
39
120
  this.dispatch("change", { detail: { value, ratio: this.#ratio } });
40
121
  if (value >= this.maxValue) {
41
122
  this.dispatch("complete", { detail: { value } });
123
+ announce(
124
+ fillTemplate(this.announceTextValue, { value, percent: Math.round(this.#ratio * 100) })
125
+ );
42
126
  }
43
127
  }
44
- /** Re-render when the indeterminate flag is toggled via its data attribute. */
128
+ /** Repaints when application code (or a Turbo morph) changes `indeterminate` at runtime. */
45
129
  indeterminateValueChanged() {
46
- this.#render();
130
+ this.#repaint.schedule();
131
+ }
132
+ /** Repaints when application code (or a Turbo morph) changes `value` at runtime. */
133
+ valueValueChanged() {
134
+ this.#repaint.schedule();
135
+ }
136
+ /** Repaints when application code (or a Turbo morph) changes `min` at runtime. */
137
+ minValueChanged() {
138
+ this.#repaint.schedule();
139
+ }
140
+ /** Repaints when application code (or a Turbo morph) changes `max` at runtime. */
141
+ maxValueChanged() {
142
+ this.#repaint.schedule();
143
+ }
144
+ /** Repaints when application code (or a Turbo morph) changes `valueText` at runtime. */
145
+ valueTextValueChanged() {
146
+ this.#repaint.schedule();
47
147
  }
48
148
  /** Clamps `raw` into the configured `[min, max]` range. */
49
149
  #clamp(raw) {
@@ -51,40 +151,51 @@ var ProgressController = class extends Controller {
51
151
  }
52
152
  /** Current fraction of the range in `[0, 1]`; `0` when the range is empty. */
53
153
  get #ratio() {
54
- const span = this.maxValue - this.minValue;
55
- if (span <= 0) return 0;
56
- return (this.#clamp(this.valueValue) - this.minValue) / span;
154
+ return rangeFraction(this.valueValue, this.minValue, this.maxValue);
57
155
  }
58
- /** Reflects value/range/indeterminate onto ARIA, `data-state`, and the ratio. */
156
+ /**
157
+ * Reflects value/range/indeterminate onto ARIA, `data-state`, and the ratio.
158
+ *
159
+ * @stimeoRenderRoot
160
+ */
59
161
  #render() {
60
162
  this.element.setAttribute("aria-valuemin", String(this.minValue));
61
163
  this.element.setAttribute("aria-valuemax", String(this.maxValue));
62
164
  if (this.indeterminateValue) {
63
165
  this.element.removeAttribute("aria-valuenow");
64
- this.element.removeAttribute("aria-valuetext");
65
- this.element.style.removeProperty("--stimeo-progress-ratio");
166
+ this.#clearOwnValueText();
167
+ this.element.style.removeProperty("--stimeo--progress-ratio");
66
168
  this.element.setAttribute("data-state", "indeterminate");
67
169
  return;
68
170
  }
69
171
  const value = this.#clamp(this.valueValue);
70
172
  this.element.setAttribute("aria-valuenow", String(value));
71
- this.element.style.setProperty("--stimeo-progress-ratio", String(this.#ratio));
173
+ this.element.style.setProperty("--stimeo--progress-ratio", String(this.#ratio));
72
174
  this.element.setAttribute("data-state", "determinate");
73
175
  this.#applyValueText(value);
74
176
  }
75
177
  /**
76
178
  * Sets `aria-valuetext` from the consumer-provided template, substituting
77
179
  * `{value}` and `{percent}`. Left to the consumer so the human-readable text
78
- * stays i18n-neutral in the library; cleared when no template is given.
180
+ * stays i18n-neutral in the library. With no template the attribute belongs to
181
+ * the consumer, so only a text this controller wrote is taken back
182
+ * ({@link OWNED_VALUE_TEXT}).
79
183
  */
80
184
  #applyValueText(value) {
81
185
  if (this.valueTextValue.length === 0) {
82
- this.element.removeAttribute("aria-valuetext");
186
+ this.#clearOwnValueText();
83
187
  return;
84
188
  }
85
189
  const percent = Math.round(this.#ratio * 100);
86
190
  const text = this.valueTextValue.replaceAll("{value}", String(value)).replaceAll("{percent}", String(percent));
87
191
  this.element.setAttribute("aria-valuetext", text);
192
+ this.element.setAttribute(OWNED_VALUE_TEXT, "");
193
+ }
194
+ /** Removes `aria-valuetext` only when this controller is the one that wrote it. */
195
+ #clearOwnValueText() {
196
+ if (!this.element.hasAttribute(OWNED_VALUE_TEXT)) return;
197
+ this.element.removeAttribute("aria-valuetext");
198
+ this.element.removeAttribute(OWNED_VALUE_TEXT);
88
199
  }
89
200
  };
90
201