stimeo-ui 0.5.0 → 0.6.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 (31) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +116 -0
  3. data/dist/controllers/aspect_ratio_controller.js +19 -11
  4. data/dist/controllers/avatar_controller.js +195 -40
  5. data/dist/controllers/carousel_controller.js +85 -9
  6. data/dist/controllers/checkbox_controller.js +136 -25
  7. data/dist/controllers/color_picker_controller.js +35 -9
  8. data/dist/controllers/date_range_picker_controller.js +157 -30
  9. data/dist/controllers/file_dropzone_controller.js +26 -3
  10. data/dist/controllers/idle_controller.js +27 -5
  11. data/dist/controllers/menubar_controller.js +5 -3
  12. data/dist/controllers/multi_select_controller.js +460 -151
  13. data/dist/controllers/number_input_controller.js +275 -51
  14. data/dist/controllers/overflow_menu_controller.js +4 -0
  15. data/dist/controllers/pagination_controller.js +33 -0
  16. data/dist/controllers/password_strength_controller.js +20 -2
  17. data/dist/controllers/persist_controller.js +24 -5
  18. data/dist/controllers/radio_group_controller.js +540 -56
  19. data/dist/controllers/rating_controller.js +272 -89
  20. data/dist/controllers/resizable_controller.js +33 -0
  21. data/dist/controllers/roving_controller.js +60 -5
  22. data/dist/controllers/scroll_area_controller.js +154 -22
  23. data/dist/controllers/scroll_visibility_controller.js +33 -0
  24. data/dist/controllers/tags_input_controller.js +356 -120
  25. data/dist/controllers/time_picker_controller.js +296 -107
  26. data/dist/controllers/toggle_group_controller.js +378 -55
  27. data/dist/controllers/toolbar_controller.js +5 -3
  28. data/dist/controllers/tree_view_controller.js +5 -3
  29. data/dist/index.js +2637 -781
  30. data/lib/stimeo/ui/version.rb +1 -1
  31. metadata +2 -2
@@ -2,6 +2,43 @@ import { Controller } from '@hotwired/stimulus';
2
2
 
3
3
  // src/controllers/scroll_area_controller.ts
4
4
 
5
+ // src/utils/attribute_lease.ts
6
+ var AttributeLease = class {
7
+ #attribute;
8
+ #records = /* @__PURE__ */ new Map();
9
+ /** @param attribute - The attribute whose temporary values this lease owns. */
10
+ constructor(attribute) {
11
+ this.#attribute = attribute;
12
+ }
13
+ /** Writes or removes the leased attribute while preserving its authored value. */
14
+ write(element, value) {
15
+ const existing = this.#records.get(element);
16
+ if (existing) {
17
+ existing.written = value;
18
+ } else {
19
+ this.#records.set(element, {
20
+ original: element.getAttribute(this.#attribute),
21
+ written: value
22
+ });
23
+ }
24
+ if (value === null) element.removeAttribute(this.#attribute);
25
+ else element.setAttribute(this.#attribute, value);
26
+ }
27
+ /** Returns one lease without overwriting a value subsequently authored by a consumer. */
28
+ return(element) {
29
+ const record = this.#records.get(element);
30
+ if (!record) return;
31
+ this.#records.delete(element);
32
+ if (element.getAttribute(this.#attribute) !== record.written) return;
33
+ if (record.original === null) element.removeAttribute(this.#attribute);
34
+ else element.setAttribute(this.#attribute, record.original);
35
+ }
36
+ /** Returns every outstanding lease using the same ownership check as {@link return}. */
37
+ returnAll() {
38
+ for (const element of Array.from(this.#records.keys())) this.return(element);
39
+ }
40
+ };
41
+
5
42
  // src/utils/layout_observer.ts
6
43
  var LayoutObserver = class {
7
44
  #callback;
@@ -72,10 +109,74 @@ function logicalScrollMetrics(element, horizontal) {
72
109
  return { position: Math.min(max, Math.max(0, position)), max };
73
110
  }
74
111
 
112
+ // src/utils/microtask_coalescer.ts
113
+ var MicrotaskCoalescer = class {
114
+ #run;
115
+ #queued = false;
116
+ #active = false;
117
+ #generation = 0;
118
+ /** @param run - the single reconciliation pass, invoked at most once per batch. */
119
+ constructor(run) {
120
+ this.#run = run;
121
+ }
122
+ /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
123
+ activate() {
124
+ this.#active = true;
125
+ }
126
+ /** Closes the window and drops any pending pass; call from `disconnect()`. */
127
+ cancel() {
128
+ this.#active = false;
129
+ this.#queued = false;
130
+ this.#generation += 1;
131
+ }
132
+ /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
133
+ schedule() {
134
+ if (!this.#active || this.#queued) return;
135
+ this.#queued = true;
136
+ const generation = this.#generation;
137
+ queueMicrotask(() => {
138
+ if (generation !== this.#generation || !this.#queued || !this.#active) return;
139
+ this.#queued = false;
140
+ this.#run();
141
+ });
142
+ }
143
+ };
144
+
145
+ // src/utils/before_cache_reset.ts
146
+ var BeforeCacheReset = class _BeforeCacheReset {
147
+ /** Every subscribed instance, iterated by the one shared document listener. */
148
+ static #subscribers = /* @__PURE__ */ new Set();
149
+ /** The shared listener; installed while at least one instance is subscribed. */
150
+ static #onBeforeCache = () => {
151
+ for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
152
+ };
153
+ #rewind;
154
+ /** @param rewind - the pass that returns this controller's state to its initial form. */
155
+ constructor(rewind) {
156
+ this.#rewind = rewind;
157
+ }
158
+ /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
159
+ activate() {
160
+ const first = _BeforeCacheReset.#subscribers.size === 0;
161
+ _BeforeCacheReset.#subscribers.add(this);
162
+ if (first) {
163
+ document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
164
+ }
165
+ }
166
+ /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
167
+ deactivate() {
168
+ _BeforeCacheReset.#subscribers.delete(this);
169
+ if (_BeforeCacheReset.#subscribers.size > 0) return;
170
+ document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
171
+ }
172
+ };
173
+
75
174
  // src/utils/tabindex_loan.ts
76
175
  var TabindexLoan = class {
77
176
  #value;
78
177
  #lent = /* @__PURE__ */ new Set();
178
+ /** Returns live loans before Turbo can copy them into its page snapshot. */
179
+ #beforeCache = new BeforeCacheReset(() => this.returnAll());
79
180
  /**
80
181
  * @param value - the `tabindex` to lend. `"-1"` (the default) is
81
182
  * programmatically focusable but not a Tab stop; `"0"` is a real Tab stop,
@@ -89,6 +190,7 @@ var TabindexLoan = class {
89
190
  if (element.hasAttribute("tabindex")) return;
90
191
  element.setAttribute("tabindex", this.#value);
91
192
  this.#lent.add(element);
193
+ this.#beforeCache.activate();
92
194
  }
93
195
  /** Takes back every loan whose value is still the one that was lent. */
94
196
  returnAll() {
@@ -96,6 +198,7 @@ var TabindexLoan = class {
96
198
  if (element.getAttribute("tabindex") === this.#value) element.removeAttribute("tabindex");
97
199
  }
98
200
  this.#lent.clear();
201
+ this.#beforeCache.deactivate();
99
202
  }
100
203
  };
101
204
 
@@ -117,29 +220,56 @@ var ScrollAreaController = class extends Controller {
117
220
  };
118
221
  static events = ["reach"];
119
222
  #layout = new LayoutObserver(() => this.#update());
223
+ /** Current element receiving scroll, resize, mutation, and keyboard-reach behavior. */
224
+ #viewport = null;
225
+ /** Guards target callbacks before connect and after disconnect. */
226
+ #connected = false;
227
+ /** Collapses target replacement callbacks into one final-DOM rebind. */
228
+ #rebind = new MicrotaskCoalescer(() => this.#syncViewport());
120
229
  /** Re-checks the tab stop when the viewport's focusable content comes or goes. */
121
230
  #content = null;
122
231
  /** Last edge reported via `reach`, so the event fires once per arrival. */
123
232
  #lastEdge = null;
124
233
  /** Whether this controller added `tabindex`, so teardown only removes its own. */
125
234
  #tabindex = new TabindexLoan("0");
126
- /** Whether this controller added `role="region"`, for symmetric teardown. */
127
- #addedRole = false;
235
+ /** Temporarily owns a derived `role="region"` without losing an authored replacement. */
236
+ #role = new AttributeLease("role");
128
237
  #onScroll = () => {
129
238
  this.#update();
130
239
  };
131
240
  connect() {
132
- if (!this.hasViewportTarget) return;
133
- this.viewportTarget.addEventListener("scroll", this.#onScroll, { passive: true });
134
- this.#layout.observe(this.viewportTarget);
241
+ this.#connected = true;
242
+ this.#rebind.activate();
135
243
  this.#layout.observeViewport();
244
+ this.#syncViewport();
245
+ }
246
+ /** Schedules a complete observer/listener rebind for a runtime viewport target. */
247
+ viewportTargetConnected() {
248
+ this.#rebind.schedule();
249
+ }
250
+ /** Schedules cleanup or replacement binding after a viewport leaves. */
251
+ viewportTargetDisconnected() {
252
+ this.#rebind.schedule();
253
+ }
254
+ /** Rebinds every viewport-owned resource against the final target in this mutation batch. */
255
+ #syncViewport() {
256
+ if (!this.#connected) return;
257
+ const next = this.hasViewportTarget ? this.viewportTarget : null;
258
+ if (next === this.#viewport) {
259
+ this.#update();
260
+ return;
261
+ }
262
+ if (this.#viewport) this.#unbindViewport(this.#viewport);
263
+ this.#viewport = next;
264
+ if (!next) return;
265
+ next.addEventListener("scroll", this.#onScroll, { passive: true });
266
+ this.#layout.observe(next);
136
267
  if (typeof MutationObserver !== "undefined") {
137
268
  this.#content = new MutationObserver(() => {
138
- if (!this.hasViewportTarget) return;
139
- const vp = this.viewportTarget;
140
- this.#syncKeyboardReach(vp, this.#syncOverflow(vp));
269
+ if (this.#viewport !== next) return;
270
+ this.#syncKeyboardReach(next, this.#syncOverflow(next));
141
271
  });
142
- this.#content.observe(this.viewportTarget, {
272
+ this.#content.observe(next, {
143
273
  subtree: true,
144
274
  childList: true,
145
275
  attributes: true
@@ -148,19 +278,26 @@ var ScrollAreaController = class extends Controller {
148
278
  this.#update();
149
279
  }
150
280
  disconnect() {
151
- if (this.hasViewportTarget) {
152
- this.viewportTarget.removeEventListener("scroll", this.#onScroll);
153
- this.#clearAddedAttributes(this.viewportTarget);
154
- }
281
+ this.#connected = false;
282
+ this.#rebind.cancel();
283
+ if (this.#viewport) this.#unbindViewport(this.#viewport);
155
284
  this.#layout.disconnect();
285
+ this.#lastEdge = null;
286
+ }
287
+ /** Releases every resource and derived attribute owned by one former viewport. */
288
+ #unbindViewport(viewport) {
289
+ viewport.removeEventListener("scroll", this.#onScroll);
290
+ this.#layout.unobserve(viewport);
156
291
  this.#content?.disconnect();
157
292
  this.#content = null;
293
+ this.#clearAddedAttributes(viewport);
294
+ if (this.#viewport === viewport) this.#viewport = null;
158
295
  this.#lastEdge = null;
159
296
  }
160
297
  /** Re-measures overflow and scroll position and reflects the state hooks. */
161
298
  #update() {
162
- if (!this.hasViewportTarget) return;
163
- const vp = this.viewportTarget;
299
+ const vp = this.#viewport;
300
+ if (!vp) return;
164
301
  const overflowing = this.#syncOverflow(vp);
165
302
  this.#syncKeyboardReach(vp, overflowing);
166
303
  const { position, progress } = this.#measurePosition(vp);
@@ -174,7 +311,6 @@ var ScrollAreaController = class extends Controller {
174
311
  this.#lastEdge = null;
175
312
  }
176
313
  }
177
- /** Whether the viewport can scroll on the configured axis. */
178
314
  /**
179
315
  * Measures overflow and reflects the `data-overflow` hook.
180
316
  *
@@ -219,8 +355,7 @@ var ScrollAreaController = class extends Controller {
219
355
  if (wantsTabindex) {
220
356
  this.#tabindex.lend(vp);
221
357
  if (!vp.hasAttribute("role") && this.#hasAccessibleName(vp)) {
222
- vp.setAttribute("role", "region");
223
- this.#addedRole = true;
358
+ this.#role.write(vp, "region");
224
359
  }
225
360
  } else {
226
361
  this.#clearAddedAttributes(vp);
@@ -229,10 +364,7 @@ var ScrollAreaController = class extends Controller {
229
364
  /** Removes (and resets the flags for) only the attributes this controller added. */
230
365
  #clearAddedAttributes(vp) {
231
366
  this.#tabindex.returnAll();
232
- if (this.#addedRole) {
233
- vp.removeAttribute("role");
234
- this.#addedRole = false;
235
- }
367
+ this.#role.return(vp);
236
368
  }
237
369
  /**
238
370
  * Whether the viewport owns something the user can Tab to *right now*.
@@ -7,10 +7,41 @@ function prefersReducedMotion() {
7
7
  return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
8
8
  }
9
9
 
10
+ // src/utils/before_cache_reset.ts
11
+ var BeforeCacheReset = class _BeforeCacheReset {
12
+ /** Every subscribed instance, iterated by the one shared document listener. */
13
+ static #subscribers = /* @__PURE__ */ new Set();
14
+ /** The shared listener; installed while at least one instance is subscribed. */
15
+ static #onBeforeCache = () => {
16
+ for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
17
+ };
18
+ #rewind;
19
+ /** @param rewind - the pass that returns this controller's state to its initial form. */
20
+ constructor(rewind) {
21
+ this.#rewind = rewind;
22
+ }
23
+ /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
24
+ activate() {
25
+ const first = _BeforeCacheReset.#subscribers.size === 0;
26
+ _BeforeCacheReset.#subscribers.add(this);
27
+ if (first) {
28
+ document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
29
+ }
30
+ }
31
+ /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
32
+ deactivate() {
33
+ _BeforeCacheReset.#subscribers.delete(this);
34
+ if (_BeforeCacheReset.#subscribers.size > 0) return;
35
+ document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
36
+ }
37
+ };
38
+
10
39
  // src/utils/tabindex_loan.ts
11
40
  var TabindexLoan = class {
12
41
  #value;
13
42
  #lent = /* @__PURE__ */ new Set();
43
+ /** Returns live loans before Turbo can copy them into its page snapshot. */
44
+ #beforeCache = new BeforeCacheReset(() => this.returnAll());
14
45
  /**
15
46
  * @param value - the `tabindex` to lend. `"-1"` (the default) is
16
47
  * programmatically focusable but not a Tab stop; `"0"` is a real Tab stop,
@@ -24,6 +55,7 @@ var TabindexLoan = class {
24
55
  if (element.hasAttribute("tabindex")) return;
25
56
  element.setAttribute("tabindex", this.#value);
26
57
  this.#lent.add(element);
58
+ this.#beforeCache.activate();
27
59
  }
28
60
  /** Takes back every loan whose value is still the one that was lent. */
29
61
  returnAll() {
@@ -31,6 +63,7 @@ var TabindexLoan = class {
31
63
  if (element.getAttribute("tabindex") === this.#value) element.removeAttribute("tabindex");
32
64
  }
33
65
  this.#lent.clear();
66
+ this.#beforeCache.deactivate();
34
67
  }
35
68
  };
36
69