stimeo-ui 0.1.0.pre.alpha.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 (85) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +72 -0
  4. data/dist/controllers/accordion_controller.js +76 -0
  5. data/dist/controllers/announcer_controller.js +184 -0
  6. data/dist/controllers/aspect_ratio_controller.js +36 -0
  7. data/dist/controllers/auto_submit_controller.js +147 -0
  8. data/dist/controllers/avatar_controller.js +66 -0
  9. data/dist/controllers/breadcrumb_controller.js +123 -0
  10. data/dist/controllers/bulk_select_controller.js +104 -0
  11. data/dist/controllers/calendar_controller.js +394 -0
  12. data/dist/controllers/character_counter_controller.js +179 -0
  13. data/dist/controllers/checkbox_controller.js +73 -0
  14. data/dist/controllers/combobox_controller.js +186 -0
  15. data/dist/controllers/command_palette_controller.js +381 -0
  16. data/dist/controllers/conditional_fields_controller.js +112 -0
  17. data/dist/controllers/confirm_controller.js +276 -0
  18. data/dist/controllers/context_menu_controller.js +112 -0
  19. data/dist/controllers/countdown_controller.js +202 -0
  20. data/dist/controllers/dialog_controller.js +207 -0
  21. data/dist/controllers/direct_upload_controller.js +212 -0
  22. data/dist/controllers/dirty_form_controller.js +128 -0
  23. data/dist/controllers/dropdown_controller.js +66 -0
  24. data/dist/controllers/empty_state_controller.js +67 -0
  25. data/dist/controllers/flash_controller.js +221 -0
  26. data/dist/controllers/focus_controller.js +216 -0
  27. data/dist/controllers/form_field_controller.js +154 -0
  28. data/dist/controllers/form_validation_controller.js +202 -0
  29. data/dist/controllers/frame_loading_controller.js +177 -0
  30. data/dist/controllers/highlight_controller.js +107 -0
  31. data/dist/controllers/hover_card_controller.js +165 -0
  32. data/dist/controllers/idle_controller.js +141 -0
  33. data/dist/controllers/input_mask_controller.js +166 -0
  34. data/dist/controllers/lazy_frame_controller.js +68 -0
  35. data/dist/controllers/listbox_controller.js +256 -0
  36. data/dist/controllers/local_time_controller.js +81 -0
  37. data/dist/controllers/menu_controller.js +134 -0
  38. data/dist/controllers/meter_controller.js +96 -0
  39. data/dist/controllers/nested_form_controller.js +131 -0
  40. data/dist/controllers/network_status_controller.js +126 -0
  41. data/dist/controllers/number_input_controller.js +306 -0
  42. data/dist/controllers/otp_controller.js +201 -0
  43. data/dist/controllers/overflow_indicator_controller.js +169 -0
  44. data/dist/controllers/overflow_menu_controller.js +274 -0
  45. data/dist/controllers/pagination_controller.js +89 -0
  46. data/dist/controllers/password_strength_controller.js +175 -0
  47. data/dist/controllers/persist_controller.js +259 -0
  48. data/dist/controllers/popover_controller.js +94 -0
  49. data/dist/controllers/portal_controller.js +63 -0
  50. data/dist/controllers/preview_guard_controller.js +69 -0
  51. data/dist/controllers/progress_controller.js +93 -0
  52. data/dist/controllers/radio_group_controller.js +128 -0
  53. data/dist/controllers/rating_controller.js +179 -0
  54. data/dist/controllers/relative_time_controller.js +129 -0
  55. data/dist/controllers/reset_before_cache_controller.js +62 -0
  56. data/dist/controllers/resizable_controller.js +163 -0
  57. data/dist/controllers/roving_controller.js +116 -0
  58. data/dist/controllers/scroll_area_controller.js +183 -0
  59. data/dist/controllers/scroll_visibility_controller.js +103 -0
  60. data/dist/controllers/scrollspy_controller.js +171 -0
  61. data/dist/controllers/skeleton_controller.js +125 -0
  62. data/dist/controllers/slider_controller.js +109 -0
  63. data/dist/controllers/spinner_controller.js +164 -0
  64. data/dist/controllers/step_indicator_controller.js +55 -0
  65. data/dist/controllers/stepper_controller.js +78 -0
  66. data/dist/controllers/stick_to_bottom_controller.js +100 -0
  67. data/dist/controllers/sticky_observer_controller.js +53 -0
  68. data/dist/controllers/submit_once_controller.js +206 -0
  69. data/dist/controllers/switch_controller.js +50 -0
  70. data/dist/controllers/tabs_controller.js +63 -0
  71. data/dist/controllers/textarea_autosize_controller.js +72 -0
  72. data/dist/controllers/theme_controller.js +154 -0
  73. data/dist/controllers/toast_controller.js +310 -0
  74. data/dist/controllers/toggle_group_controller.js +130 -0
  75. data/dist/controllers/toolbar_controller.js +113 -0
  76. data/dist/controllers/tooltip_controller.js +165 -0
  77. data/dist/controllers/transition_controller.js +203 -0
  78. data/dist/index.js +12241 -0
  79. data/dist/positioning/index.js +145 -0
  80. data/lib/generators/stimeo/install/install_generator.rb +114 -0
  81. data/lib/generators/stimeo/install/templates/stimeo.js +12 -0
  82. data/lib/stimeo/ui/version.rb +10 -0
  83. data/lib/stimeo/ui.rb +19 -0
  84. data/lib/stimeo-ui.rb +4 -0
  85. metadata +152 -0
@@ -0,0 +1,179 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ // src/controllers/character_counter_controller.ts
4
+
5
+ // src/utils/safe_timeout.ts
6
+ var TimerRegistry = class {
7
+ /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
8
+ ids = /* @__PURE__ */ new Set();
9
+ /**
10
+ * Cancels a single tracked timer.
11
+ *
12
+ * No-ops if the id is unknown (already cleared, fired, or never owned by this
13
+ * registry), so callers can clear defensively without guarding.
14
+ */
15
+ clear(id) {
16
+ if (this.ids.delete(id)) {
17
+ this.cancel(id);
18
+ }
19
+ }
20
+ /**
21
+ * Cancels every tracked timer. Call this from a controller's `disconnect()`
22
+ * to guarantee no timer outlives the element.
23
+ */
24
+ clearAll() {
25
+ for (const id of this.ids) {
26
+ this.cancel(id);
27
+ }
28
+ this.ids.clear();
29
+ }
30
+ /** Number of timers currently tracked (pending). */
31
+ get size() {
32
+ return this.ids.size;
33
+ }
34
+ };
35
+ var SafeTimeout = class extends TimerRegistry {
36
+ /**
37
+ * Schedules `callback` after `delay` ms and returns the timer id.
38
+ *
39
+ * The id is removed from the registry automatically when the timeout fires,
40
+ * so {@link TimerRegistry.size | size} reflects only still-pending timers.
41
+ */
42
+ set(callback, delay) {
43
+ const id = this.schedule(() => {
44
+ this.ids.delete(id);
45
+ callback();
46
+ }, delay);
47
+ this.ids.add(id);
48
+ return id;
49
+ }
50
+ schedule(callback, delay) {
51
+ return window.setTimeout(callback, delay);
52
+ }
53
+ cancel(id) {
54
+ window.clearTimeout(id);
55
+ }
56
+ };
57
+
58
+ // src/controllers/character_counter_controller.ts
59
+ var ORIGINAL_INVALID = "data-character-counter-original-invalid";
60
+ var CharacterCounterController = class _CharacterCounterController extends Controller {
61
+ static targets = ["input", "output"];
62
+ static values = {
63
+ max: { type: Number, default: 0 },
64
+ warnAt: { type: Number, default: 0 },
65
+ mode: { type: String, default: "remaining" }
66
+ };
67
+ static events = ["change"];
68
+ /** Delay (ms) before the live-region count is written, to throttle SR flooding. */
69
+ static #announceDelay = 200;
70
+ #timeouts = new SafeTimeout();
71
+ #announceId = null;
72
+ /** True while an IME composition is active; intermediate input is skipped. */
73
+ #composing = false;
74
+ #onInput = (event) => {
75
+ if (this.#composing || event.isComposing) return;
76
+ this.#update();
77
+ };
78
+ #onCompositionStart = () => {
79
+ this.#composing = true;
80
+ };
81
+ #onCompositionEnd = () => {
82
+ this.#composing = false;
83
+ this.#update();
84
+ };
85
+ connect() {
86
+ const field = this.#field;
87
+ if (!field) return;
88
+ field.addEventListener("input", this.#onInput);
89
+ field.addEventListener("compositionstart", this.#onCompositionStart);
90
+ field.addEventListener("compositionend", this.#onCompositionEnd);
91
+ this.#update({ announce: false });
92
+ }
93
+ disconnect() {
94
+ const field = this.#field;
95
+ field?.removeEventListener("input", this.#onInput);
96
+ field?.removeEventListener("compositionstart", this.#onCompositionStart);
97
+ field?.removeEventListener("compositionend", this.#onCompositionEnd);
98
+ this.#timeouts.clearAll();
99
+ this.#announceId = null;
100
+ this.#composing = false;
101
+ }
102
+ /**
103
+ * Recomputes length-derived state. Non-text state (data hooks, `aria-invalid`)
104
+ * and the `change` event apply immediately; the live-region count text is
105
+ * debounced unless `announce` is `false` (initial render).
106
+ */
107
+ #update(options = {}) {
108
+ const field = this.#field;
109
+ if (!field) return;
110
+ const length = field.value.length;
111
+ const hasLimit = this.maxValue > 0;
112
+ const remaining = hasLimit ? this.maxValue - length : null;
113
+ const over = hasLimit && length > this.maxValue;
114
+ const near = hasLimit && this.warnAtValue > 0 && !over && remaining !== null && remaining <= this.warnAtValue;
115
+ this.#toggle("data-over-limit", over);
116
+ this.#toggle("data-near-limit", near);
117
+ if (hasLimit && over) {
118
+ if (!field.hasAttribute(ORIGINAL_INVALID)) {
119
+ field.setAttribute(ORIGINAL_INVALID, field.getAttribute("aria-invalid") ?? "");
120
+ }
121
+ field.setAttribute("aria-invalid", "true");
122
+ } else if (field.hasAttribute(ORIGINAL_INVALID)) {
123
+ const original = field.getAttribute(ORIGINAL_INVALID);
124
+ if (original) {
125
+ field.setAttribute("aria-invalid", original);
126
+ } else {
127
+ field.removeAttribute("aria-invalid");
128
+ }
129
+ field.removeAttribute(ORIGINAL_INVALID);
130
+ }
131
+ const text = this.#format(length, remaining);
132
+ if (options.announce === false) {
133
+ this.#writeOutput(text);
134
+ return;
135
+ }
136
+ this.dispatch("change", { detail: { length, remaining, over } });
137
+ if (this.#announceId !== null) this.#timeouts.clear(this.#announceId);
138
+ this.#announceId = this.#timeouts.set(() => {
139
+ this.#writeOutput(text);
140
+ this.#announceId = null;
141
+ }, _CharacterCounterController.#announceDelay);
142
+ }
143
+ /** Builds the count text for the active `mode`. */
144
+ #format(length, remaining) {
145
+ if (remaining === null) return String(length);
146
+ switch (this.modeValue) {
147
+ case "used":
148
+ return String(length);
149
+ case "both":
150
+ return `${length}/${this.maxValue}`;
151
+ default:
152
+ return String(remaining);
153
+ }
154
+ }
155
+ #writeOutput(text) {
156
+ if (this.hasOutputTarget) this.outputTarget.textContent = text;
157
+ }
158
+ /** Sets a presence-style boolean data hook (attribute present when `on`). */
159
+ #toggle(name, on) {
160
+ if (on) {
161
+ this.element.setAttribute(name, "true");
162
+ } else {
163
+ this.element.removeAttribute(name);
164
+ }
165
+ }
166
+ /** The watched field: the `input` target, or the element itself when it is one. */
167
+ get #field() {
168
+ if (this.hasInputTarget) return this.inputTarget;
169
+ const el = this.element;
170
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
171
+ return el;
172
+ }
173
+ return null;
174
+ }
175
+ };
176
+
177
+ export { CharacterCounterController };
178
+ //# sourceMappingURL=character_counter_controller.js.map
179
+ //# sourceMappingURL=character_counter_controller.js.map
@@ -0,0 +1,73 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ // src/controllers/checkbox_controller.ts
4
+ var CheckboxController = class extends Controller {
5
+ static targets = ["parent", "child"];
6
+ static actions = ["onChildChange", "onParentChange"];
7
+ static events = ["change"];
8
+ /** Reflects the initial aggregate (e.g. from server-rendered child states). */
9
+ connect() {
10
+ if (this.childTargets.length > 0) {
11
+ this.#syncFromChildren(false);
12
+ } else {
13
+ this.element.setAttribute("data-state", this.#aggregate());
14
+ }
15
+ }
16
+ /** Cascades the parent's state to every child. Bound via `data-action` (change). */
17
+ onParentChange() {
18
+ if (!this.hasParentTarget) return;
19
+ const checked = this.parentTarget.checked;
20
+ for (const child of this.childTargets) {
21
+ child.checked = checked;
22
+ }
23
+ this.parentTarget.indeterminate = false;
24
+ const state = checked ? "all" : "none";
25
+ this.element.setAttribute("data-state", state);
26
+ this.dispatch("change", { detail: { checked, indeterminate: false, state } });
27
+ }
28
+ /** Recomputes the parent from its children. Bound via `data-action` (change). */
29
+ onChildChange() {
30
+ this.#syncFromChildren(true);
31
+ }
32
+ /**
33
+ * Derives the parent's `checked`/`indeterminate` and the root `data-state` from
34
+ * the children, optionally dispatching `change`.
35
+ */
36
+ #syncFromChildren(dispatch) {
37
+ const state = this.#aggregate();
38
+ if (this.hasParentTarget) {
39
+ this.parentTarget.checked = state === "all";
40
+ this.parentTarget.indeterminate = state === "partial";
41
+ }
42
+ this.element.setAttribute("data-state", state);
43
+ if (dispatch) {
44
+ this.dispatch("change", {
45
+ detail: {
46
+ checked: this.hasParentTarget ? this.parentTarget.checked : state === "all",
47
+ indeterminate: this.hasParentTarget ? this.parentTarget.indeterminate : false,
48
+ state
49
+ }
50
+ });
51
+ }
52
+ }
53
+ /**
54
+ * Computes the aggregate state. With children it counts them; with none it
55
+ * reads the parent so a lone tri-state checkbox still reports a state without
56
+ * its externally-set `indeterminate` being clobbered.
57
+ */
58
+ #aggregate() {
59
+ const children = this.childTargets;
60
+ if (children.length === 0) {
61
+ if (this.hasParentTarget && this.parentTarget.indeterminate) return "partial";
62
+ return this.hasParentTarget && this.parentTarget.checked ? "all" : "none";
63
+ }
64
+ const checked = children.filter((child) => child.checked).length;
65
+ if (checked === 0) return "none";
66
+ if (checked === children.length) return "all";
67
+ return "partial";
68
+ }
69
+ };
70
+
71
+ export { CheckboxController };
72
+ //# sourceMappingURL=checkbox_controller.js.map
73
+ //# sourceMappingURL=checkbox_controller.js.map
@@ -0,0 +1,186 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ // src/controllers/combobox_controller.ts
4
+ var ComboboxController = class extends Controller {
5
+ static targets = ["input", "list", "option"];
6
+ static actions = ["close", "filter", "onKeydown", "open", "selectByClick"];
7
+ static events = ["selected"];
8
+ /** Index into the *visible* options of the active option, or -1 if none. */
9
+ #activeIndex = -1;
10
+ /**
11
+ * Suppresses {@link open} for the duration of the programmatic re-focus in
12
+ * `#select`, so committing a value (which returns focus to the input)
13
+ * does not immediately re-open the listbox via a `focus`-bound action.
14
+ */
15
+ #suppressOpen = false;
16
+ /** Starts closed with no active option and registers the outside-click listener. */
17
+ connect() {
18
+ this.close();
19
+ document.addEventListener("click", this.#onOutsideClick);
20
+ }
21
+ /** Removes the document-level listener registered in {@link connect}. */
22
+ disconnect() {
23
+ document.removeEventListener("click", this.#onOutsideClick);
24
+ }
25
+ /** Filters options by the current input value and opens the listbox. */
26
+ filter() {
27
+ this.open();
28
+ }
29
+ /**
30
+ * Opens the listbox, re-filtering the options against the current input value
31
+ * so the visible options and empty state always match what is typed (e.g.
32
+ * re-opening with a stale non-matching value still surfaces the empty state).
33
+ */
34
+ open() {
35
+ if (!this.hasListTarget || this.#suppressOpen) return;
36
+ this.#applyFilter();
37
+ this.listTarget.hidden = false;
38
+ this.inputTarget.setAttribute("aria-expanded", "true");
39
+ this.#setActive(-1);
40
+ this.#reflectEmptyState();
41
+ }
42
+ /**
43
+ * Hides options that don't match the current input value (case-insensitive
44
+ * substring). An empty query shows every option. Does not change open state.
45
+ */
46
+ #applyFilter() {
47
+ const query = this.inputTarget.value.trim().toLowerCase();
48
+ for (const option of this.optionTargets) {
49
+ const text = (option.textContent ?? "").trim().toLowerCase();
50
+ option.hidden = query.length > 0 && !text.includes(query);
51
+ }
52
+ }
53
+ /** Closes the listbox, clears the active option, and updates ARIA state. */
54
+ close() {
55
+ if (!this.hasListTarget) return;
56
+ this.listTarget.hidden = true;
57
+ this.inputTarget.setAttribute("aria-expanded", "false");
58
+ this.#setActive(-1);
59
+ this.element.removeAttribute("data-stimeo--combobox-empty");
60
+ }
61
+ /** Routes keyboard interaction per the APG combobox model. */
62
+ onKeydown(event) {
63
+ switch (event.key) {
64
+ case "ArrowDown": {
65
+ event.preventDefault();
66
+ if (this.#isClosed) this.open();
67
+ const visible = this.#visibleOptions();
68
+ if (visible.length > 0) {
69
+ const next = this.#activeIndex === -1 ? 0 : (this.#activeIndex + 1) % visible.length;
70
+ this.#setActive(next);
71
+ }
72
+ break;
73
+ }
74
+ case "ArrowUp": {
75
+ event.preventDefault();
76
+ if (this.#isClosed) this.open();
77
+ const visible = this.#visibleOptions();
78
+ if (visible.length > 0) {
79
+ const next = this.#activeIndex === -1 ? visible.length - 1 : (this.#activeIndex - 1 + visible.length) % visible.length;
80
+ this.#setActive(next);
81
+ }
82
+ break;
83
+ }
84
+ case "Home":
85
+ if (!this.#isClosed && this.#visibleOptions().length > 0) {
86
+ event.preventDefault();
87
+ this.#setActive(0);
88
+ }
89
+ break;
90
+ case "End": {
91
+ const visible = this.#visibleOptions();
92
+ if (!this.#isClosed && visible.length > 0) {
93
+ event.preventDefault();
94
+ this.#setActive(visible.length - 1);
95
+ }
96
+ break;
97
+ }
98
+ case "Enter": {
99
+ const visible = this.#visibleOptions();
100
+ const active = this.#activeIndex === -1 ? void 0 : visible[this.#activeIndex];
101
+ if (active) {
102
+ event.preventDefault();
103
+ this.#select(active);
104
+ }
105
+ break;
106
+ }
107
+ case "Escape":
108
+ event.preventDefault();
109
+ this.close();
110
+ break;
111
+ case "Tab":
112
+ this.close();
113
+ break;
114
+ }
115
+ }
116
+ /**
117
+ * Closes the listbox when a click lands outside the combobox. Mirrors the menu
118
+ * button's outside-click behavior; clicks on an option are inside the element,
119
+ * so `#select` (not this handler) closes the popup after committing.
120
+ */
121
+ #onOutsideClick = (event) => {
122
+ if (!this.#isClosed && !this.element.contains(event.target)) this.close();
123
+ };
124
+ /** Selects the clicked option. Bound via `data-action` (click). */
125
+ selectByClick(event) {
126
+ const option = event.currentTarget.closest('[role="option"]');
127
+ if (option) this.#select(option);
128
+ }
129
+ /** Commits an option: fills the input, closes the listbox, notifies listeners. */
130
+ #select(option) {
131
+ const value = option.dataset.value ?? (option.textContent ?? "").trim();
132
+ const changed = this.inputTarget.value !== value;
133
+ this.inputTarget.value = value;
134
+ this.close();
135
+ this.#suppressOpen = true;
136
+ this.inputTarget.focus();
137
+ this.#suppressOpen = false;
138
+ if (changed) {
139
+ this.inputTarget.dispatchEvent(new Event("change", { bubbles: true }));
140
+ }
141
+ this.dispatch("selected", { detail: { value } });
142
+ }
143
+ /**
144
+ * Reflects whether the open listbox currently has zero matching options by
145
+ * toggling `data-stimeo--combobox-empty` on the root element. Behavior only:
146
+ * consumers decide how to present the empty state (hide the list, show a
147
+ * "no results" node, etc.) via CSS keyed off this attribute.
148
+ */
149
+ #reflectEmptyState() {
150
+ const empty = !this.#isClosed && this.#visibleOptions().length === 0;
151
+ if (empty) {
152
+ this.element.setAttribute("data-stimeo--combobox-empty", "");
153
+ } else {
154
+ this.element.removeAttribute("data-stimeo--combobox-empty");
155
+ }
156
+ }
157
+ /**
158
+ * Marks the visible option at `index` active via `aria-selected` and the
159
+ * input's `aria-activedescendant`. Pass `-1` to clear the active option.
160
+ */
161
+ #setActive(index) {
162
+ this.#activeIndex = index;
163
+ const visible = this.#visibleOptions();
164
+ const active = index === -1 ? null : visible[index];
165
+ for (const option of this.optionTargets) {
166
+ option.setAttribute("aria-selected", option === active ? "true" : "false");
167
+ }
168
+ if (active?.id) {
169
+ this.inputTarget.setAttribute("aria-activedescendant", active.id);
170
+ } else {
171
+ this.inputTarget.removeAttribute("aria-activedescendant");
172
+ }
173
+ }
174
+ /** The options currently shown (not filtered out). */
175
+ #visibleOptions() {
176
+ return this.optionTargets.filter((option) => !option.hidden);
177
+ }
178
+ /** Whether the listbox is currently hidden. */
179
+ get #isClosed() {
180
+ return !this.hasListTarget || this.listTarget.hidden !== false;
181
+ }
182
+ };
183
+
184
+ export { ComboboxController };
185
+ //# sourceMappingURL=combobox_controller.js.map
186
+ //# sourceMappingURL=combobox_controller.js.map