stimeo-ui 0.2.0 → 0.2.1

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +59 -0
  3. data/dist/controllers/alert_dialog_controller.js +318 -0
  4. data/dist/controllers/carousel_controller.js +272 -0
  5. data/dist/controllers/clipboard_controller.js +144 -0
  6. data/dist/controllers/collapsible_controller.js +327 -0
  7. data/dist/controllers/color_picker_controller.js +213 -0
  8. data/dist/controllers/count_up_controller.js +8 -1
  9. data/dist/controllers/currency_input_controller.js +147 -0
  10. data/dist/controllers/data_grid_controller.js +168 -0
  11. data/dist/controllers/date_range_picker_controller.js +417 -0
  12. data/dist/controllers/dismissible_controller.js +117 -0
  13. data/dist/controllers/drawer_controller.js +630 -0
  14. data/dist/controllers/editable_controller.js +168 -0
  15. data/dist/controllers/file_dropzone_controller.js +165 -0
  16. data/dist/controllers/filter_controller.js +86 -0
  17. data/dist/controllers/flash_controller.js +36 -5
  18. data/dist/controllers/highlight_controller.js +6 -4
  19. data/dist/controllers/intersection_controller.js +41 -18
  20. data/dist/controllers/lazy_frame_controller.js +33 -11
  21. data/dist/controllers/masonry_controller.js +142 -0
  22. data/dist/controllers/menubar_controller.js +433 -0
  23. data/dist/controllers/multi_select_controller.js +472 -0
  24. data/dist/controllers/navigation_menu_controller.js +384 -0
  25. data/dist/controllers/overflow_indicator_controller.js +178 -27
  26. data/dist/controllers/password_reveal_controller.js +117 -0
  27. data/dist/controllers/range_slider_controller.js +166 -0
  28. data/dist/controllers/read_more_controller.js +194 -0
  29. data/dist/controllers/scroll_area_controller.js +15 -2
  30. data/dist/controllers/scroll_restore_controller.js +93 -0
  31. data/dist/controllers/scroll_visibility_controller.js +8 -4
  32. data/dist/controllers/scrollspy_controller.js +33 -11
  33. data/dist/controllers/separator_controller.js +87 -0
  34. data/dist/controllers/sidebar_controller.js +761 -0
  35. data/dist/controllers/stepper_controller.js +28 -12
  36. data/dist/controllers/stick_to_bottom_controller.js +8 -4
  37. data/dist/controllers/sticky_observer_controller.js +88 -20
  38. data/dist/controllers/tags_input_controller.js +275 -0
  39. data/dist/controllers/theme_controller.js +20 -10
  40. data/dist/controllers/time_picker_controller.js +212 -0
  41. data/dist/controllers/toast_controller.js +36 -9
  42. data/dist/controllers/transition_controller.js +153 -38
  43. data/dist/controllers/tree_view_controller.js +275 -0
  44. data/dist/index.js +811 -295
  45. data/lib/stimeo/ui/version.rb +1 -1
  46. metadata +28 -2
@@ -0,0 +1,275 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ // src/controllers/tree_view_controller.ts
4
+
5
+ // src/utils/roving_tabindex.ts
6
+ var RovingTabindex = class {
7
+ /** Returns the current ordered item elements; called on every operation. */
8
+ #getItems;
9
+ /**
10
+ * @param getItems - Returns the current ordered item elements. Called on every
11
+ * operation so the live target list is always used.
12
+ */
13
+ constructor(getItems) {
14
+ this.#getItems = getItems;
15
+ }
16
+ /** Index of the currently tabbable item (`tabindex="0"`), or `-1` if none. */
17
+ get activeIndex() {
18
+ return this.#getItems().findIndex((item) => item.tabIndex === 0);
19
+ }
20
+ /**
21
+ * Makes exactly the item at `index` tabbable (`tabindex="0"`) and removes every
22
+ * other item from the Tab sequence (`tabindex="-1"`). An out-of-range `index`
23
+ * (e.g. `-1`) leaves all items at `-1`, which a controller can use to express
24
+ * "nothing is currently tabbable".
25
+ *
26
+ * @param index - Position of the item to make tabbable.
27
+ * @param options - Pass `{ focus: true }` to also move DOM focus to that item.
28
+ */
29
+ setActive(index, { focus = false } = {}) {
30
+ const items = this.#getItems();
31
+ items.forEach((item, i) => {
32
+ item.tabIndex = i === index ? 0 : -1;
33
+ });
34
+ if (focus) items[index]?.focus();
35
+ }
36
+ };
37
+
38
+ // src/utils/safe_timeout.ts
39
+ var TimerRegistry = class {
40
+ /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
41
+ ids = /* @__PURE__ */ new Set();
42
+ /**
43
+ * Cancels a single tracked timer.
44
+ *
45
+ * No-ops if the id is unknown (already cleared, fired, or never owned by this
46
+ * registry), so callers can clear defensively without guarding.
47
+ */
48
+ clear(id) {
49
+ if (this.ids.delete(id)) {
50
+ this.cancel(id);
51
+ }
52
+ }
53
+ /**
54
+ * Cancels every tracked timer. Call this from a controller's `disconnect()`
55
+ * to guarantee no timer outlives the element.
56
+ */
57
+ clearAll() {
58
+ for (const id of this.ids) {
59
+ this.cancel(id);
60
+ }
61
+ this.ids.clear();
62
+ }
63
+ /** Number of timers currently tracked (pending). */
64
+ get size() {
65
+ return this.ids.size;
66
+ }
67
+ };
68
+ var SafeTimeout = class extends TimerRegistry {
69
+ /**
70
+ * Schedules `callback` after `delay` ms and returns the timer id.
71
+ *
72
+ * The id is removed from the registry automatically when the timeout fires,
73
+ * so {@link TimerRegistry.size | size} reflects only still-pending timers.
74
+ */
75
+ set(callback, delay) {
76
+ const id = this.schedule(() => {
77
+ this.ids.delete(id);
78
+ callback();
79
+ }, delay);
80
+ this.ids.add(id);
81
+ return id;
82
+ }
83
+ schedule(callback, delay) {
84
+ return window.setTimeout(callback, delay);
85
+ }
86
+ cancel(id) {
87
+ window.clearTimeout(id);
88
+ }
89
+ };
90
+
91
+ // src/controllers/tree_view_controller.ts
92
+ var TYPEAHEAD_TIMEOUT = 500;
93
+ var TreeViewController = class extends Controller {
94
+ static targets = ["item", "group"];
95
+ static actions = ["onClick", "onKeydown"];
96
+ static events = ["select", "toggle"];
97
+ #roving = new RovingTabindex(() => this.itemTargets);
98
+ #typeahead = "";
99
+ #typeaheadTimer = 0;
100
+ #timers = new SafeTimeout();
101
+ /** Establishes the single tab stop (keeps an existing one, else the first). */
102
+ connect() {
103
+ const active = this.#roving.activeIndex;
104
+ this.#roving.setActive(active === -1 ? 0 : active);
105
+ }
106
+ /** Clears the pending typeahead-reset timer. */
107
+ disconnect() {
108
+ this.#timers.clearAll();
109
+ }
110
+ /**
111
+ * Routes tree keyboard interaction. Because `treeitem`s nest, only the handler
112
+ * on the nearest item to the event target acts; the same keydown bubbling to an
113
+ * ancestor item's handler is ignored to avoid double moves / selections.
114
+ */
115
+ onKeydown(event) {
116
+ const item = event.currentTarget;
117
+ if (event.target.closest('[role="treeitem"]') !== item) return;
118
+ switch (event.key) {
119
+ case "ArrowDown":
120
+ event.preventDefault();
121
+ this.#moveBy(item, 1);
122
+ break;
123
+ case "ArrowUp":
124
+ event.preventDefault();
125
+ this.#moveBy(item, -1);
126
+ break;
127
+ case "ArrowRight":
128
+ event.preventDefault();
129
+ this.#expandOrEnter(item);
130
+ break;
131
+ case "ArrowLeft":
132
+ event.preventDefault();
133
+ this.#collapseOrLeave(item);
134
+ break;
135
+ case "Home":
136
+ event.preventDefault();
137
+ this.#focusEdge(0);
138
+ break;
139
+ case "End":
140
+ event.preventDefault();
141
+ this.#focusEdge(-1);
142
+ break;
143
+ case "Enter":
144
+ case " ":
145
+ event.preventDefault();
146
+ this.#select(item);
147
+ break;
148
+ default:
149
+ if (this.#isPrintable(event)) {
150
+ event.preventDefault();
151
+ this.#typeaheadTo(item, event.key);
152
+ }
153
+ break;
154
+ }
155
+ }
156
+ /** Selects the clicked item (nearest to the target only). */
157
+ onClick(event) {
158
+ const item = event.currentTarget;
159
+ if (event.target.closest('[role="treeitem"]') !== item) return;
160
+ this.#focusItem(item);
161
+ this.#select(item);
162
+ }
163
+ /** Moves focus to the next (`delta=1`) or previous visible item, if any. */
164
+ #moveBy(item, delta) {
165
+ const visible = this.#visibleItems;
166
+ const current = visible.indexOf(item);
167
+ const next = visible[current + delta];
168
+ if (next) this.#focusItem(next);
169
+ }
170
+ /** `ArrowRight`: expand a collapsed parent, else step into the first child. */
171
+ #expandOrEnter(item) {
172
+ const group = this.#childGroup(item);
173
+ if (!group) return;
174
+ if (this.#isExpanded(item)) {
175
+ const firstChild = group.querySelector(':scope > [role="treeitem"]');
176
+ if (firstChild) this.#focusItem(firstChild);
177
+ } else {
178
+ this.#setExpanded(item, true);
179
+ }
180
+ }
181
+ /** `ArrowLeft`: collapse an expanded parent, else step out to the parent item. */
182
+ #collapseOrLeave(item) {
183
+ if (this.#childGroup(item) && this.#isExpanded(item)) {
184
+ this.#setExpanded(item, false);
185
+ return;
186
+ }
187
+ const parent = this.#parentItem(item);
188
+ if (parent) this.#focusItem(parent);
189
+ }
190
+ /** Focuses the first (`0`) or last (`-1`) visible item. */
191
+ #focusEdge(index) {
192
+ const visible = this.#visibleItems;
193
+ const target = index < 0 ? visible[visible.length - 1] : visible[index];
194
+ if (target) this.#focusItem(target);
195
+ }
196
+ /** Applies single selection and dispatches `select`. */
197
+ #select(item) {
198
+ for (const candidate of this.itemTargets) {
199
+ candidate.setAttribute("aria-selected", candidate === item ? "true" : "false");
200
+ }
201
+ this.dispatch("select", { detail: { item } });
202
+ }
203
+ /** Toggles `aria-expanded` + the child group's `hidden`, dispatching `toggle`. */
204
+ #setExpanded(item, expanded) {
205
+ const group = this.#childGroup(item);
206
+ if (!group) return;
207
+ item.setAttribute("aria-expanded", String(expanded));
208
+ group.hidden = !expanded;
209
+ this.dispatch("toggle", { detail: { item, expanded } });
210
+ }
211
+ /** Makes `item` the single tab stop and moves DOM focus to it. */
212
+ #focusItem(item) {
213
+ const index = this.itemTargets.indexOf(item);
214
+ if (index !== -1) this.#roving.setActive(index, { focus: true });
215
+ }
216
+ /** Advances the typeahead buffer and focuses the next matching visible item. */
217
+ #typeaheadTo(item, char) {
218
+ this.#timers.clear(this.#typeaheadTimer);
219
+ this.#typeahead += char.toLowerCase();
220
+ this.#typeaheadTimer = this.#timers.set(() => {
221
+ this.#typeahead = "";
222
+ }, TYPEAHEAD_TIMEOUT);
223
+ const visible = this.#visibleItems;
224
+ const start = visible.indexOf(item);
225
+ for (let offset = 1; offset <= visible.length; offset += 1) {
226
+ const candidate = visible[(start + offset) % visible.length];
227
+ if (candidate && this.#label(candidate).startsWith(this.#typeahead)) {
228
+ this.#focusItem(candidate);
229
+ return;
230
+ }
231
+ }
232
+ }
233
+ /** The visible items: those with no collapsed (`hidden`) ancestor group. */
234
+ get #visibleItems() {
235
+ return this.itemTargets.filter((item) => {
236
+ let node = item.parentElement;
237
+ while (node && node !== this.element) {
238
+ if (node.matches('[role="group"]') && node.hidden) return false;
239
+ node = node.parentElement;
240
+ }
241
+ return true;
242
+ });
243
+ }
244
+ /** The child `group` owned directly by `item`, or `null` for a leaf. */
245
+ #childGroup(item) {
246
+ return item.querySelector(':scope > [role="group"]');
247
+ }
248
+ /** The nearest ancestor `treeitem`, or `null` at the root level. */
249
+ #parentItem(item) {
250
+ return item.parentElement?.closest('[role="treeitem"]') ?? null;
251
+ }
252
+ /** Whether a parent item is currently expanded. */
253
+ #isExpanded(item) {
254
+ return item.getAttribute("aria-expanded") === "true";
255
+ }
256
+ /** `item`'s own label text (excluding any nested child group), lowercased. */
257
+ #label(item) {
258
+ let text = "";
259
+ for (const node of Array.from(item.childNodes)) {
260
+ if (node.nodeType === Node.ELEMENT_NODE && node.matches('[role="group"]')) {
261
+ continue;
262
+ }
263
+ text += node.textContent ?? "";
264
+ }
265
+ return text.trim().toLowerCase();
266
+ }
267
+ /** Whether `event.key` is a single printable character (no modifier chord). */
268
+ #isPrintable(event) {
269
+ return event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey;
270
+ }
271
+ };
272
+
273
+ export { TreeViewController };
274
+ //# sourceMappingURL=tree_view_controller.js.map
275
+ //# sourceMappingURL=tree_view_controller.js.map