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,259 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ // src/controllers/persist_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/persist_controller.ts
59
+ var NON_VALUE_TYPES = /* @__PURE__ */ new Set(["file", "submit", "reset", "button", "image"]);
60
+ var STORAGE_PREFIX = "stimeo--persist:";
61
+ var OCCURRENCE_SEP = "\0";
62
+ var PersistController = class extends Controller {
63
+ static targets = ["field"];
64
+ static values = {
65
+ key: { type: String, default: "" },
66
+ debounce: { type: Number, default: 400 },
67
+ exclude: { type: Array, default: ["password"] },
68
+ clearOn: { type: String, default: "" }
69
+ };
70
+ static actions = ["clear"];
71
+ static events = ["restore", "save", "clear"];
72
+ #timeouts = new SafeTimeout();
73
+ #saveId = null;
74
+ #onInput = () => {
75
+ this.#scheduleSave();
76
+ };
77
+ #onClearEvent = () => {
78
+ this.clear();
79
+ };
80
+ connect() {
81
+ if (this.#storageKey === null) return;
82
+ this.#restore();
83
+ this.element.addEventListener("input", this.#onInput);
84
+ this.element.addEventListener("change", this.#onInput);
85
+ if (this.clearOnValue.length > 0) {
86
+ this.element.addEventListener(this.clearOnValue, this.#onClearEvent);
87
+ }
88
+ }
89
+ disconnect() {
90
+ this.element.removeEventListener("input", this.#onInput);
91
+ this.element.removeEventListener("change", this.#onInput);
92
+ if (this.clearOnValue.length > 0) {
93
+ this.element.removeEventListener(this.clearOnValue, this.#onClearEvent);
94
+ }
95
+ if (this.#saveId !== null) {
96
+ this.#timeouts.clear(this.#saveId);
97
+ this.#saveId = null;
98
+ this.#write();
99
+ }
100
+ this.#timeouts.clearAll();
101
+ }
102
+ /** Drops the saved draft and clears the restored marker. */
103
+ clear() {
104
+ const key = this.#storageKey;
105
+ if (key === null) return;
106
+ if (this.#saveId !== null) {
107
+ this.#timeouts.clear(this.#saveId);
108
+ this.#saveId = null;
109
+ }
110
+ this.#removeItem(key);
111
+ this.element.removeAttribute("data-persist-restored");
112
+ this.dispatch("clear", { detail: { key: this.#logicalKey } });
113
+ }
114
+ /** Schedules a debounced save. */
115
+ #scheduleSave() {
116
+ if (this.#saveId !== null) this.#timeouts.clear(this.#saveId);
117
+ this.#saveId = this.#timeouts.set(() => {
118
+ this.#saveId = null;
119
+ this.#save();
120
+ }, this.debounceValue);
121
+ }
122
+ /** Writes the current values and emits `save`. */
123
+ #save() {
124
+ this.#write();
125
+ this.dispatch("save", { detail: { key: this.#logicalKey } });
126
+ }
127
+ /** Serializes persistable fields and stores them under the storage key. */
128
+ #write() {
129
+ const key = this.#storageKey;
130
+ if (key === null) return;
131
+ const data = {};
132
+ for (const { field, key: fieldKey } of this.#fieldEntries()) {
133
+ if (field instanceof HTMLInputElement && field.type === "checkbox") {
134
+ data[fieldKey] = field.checked;
135
+ } else if (field instanceof HTMLInputElement && field.type === "radio") {
136
+ if (field.checked) data[fieldKey] = field.value;
137
+ } else if (field instanceof HTMLSelectElement && field.multiple) {
138
+ data[fieldKey] = Array.from(field.selectedOptions).map((o) => o.value);
139
+ } else {
140
+ data[fieldKey] = field.value;
141
+ }
142
+ }
143
+ this.#setItem(key, JSON.stringify(data));
144
+ }
145
+ /** Applies any saved values to the fields, without moving focus. */
146
+ #restore() {
147
+ const key = this.#storageKey;
148
+ if (key === null) return;
149
+ const raw = this.#getItem(key);
150
+ if (raw === null) return;
151
+ let data;
152
+ try {
153
+ data = JSON.parse(raw);
154
+ } catch {
155
+ return;
156
+ }
157
+ let restoredAny = false;
158
+ for (const { field, key: fieldKey } of this.#fieldEntries()) {
159
+ if (!Object.hasOwn(data, fieldKey)) continue;
160
+ this.#applyValue(field, data[fieldKey]);
161
+ restoredAny = true;
162
+ }
163
+ if (restoredAny) {
164
+ this.element.setAttribute("data-persist-restored", "true");
165
+ this.dispatch("restore", { detail: { key: this.#logicalKey } });
166
+ }
167
+ }
168
+ /** Sets a single field's value from a stored entry. */
169
+ #applyValue(field, value) {
170
+ if (field instanceof HTMLInputElement && field.type === "checkbox") {
171
+ field.checked = Boolean(value);
172
+ } else if (field instanceof HTMLInputElement && field.type === "radio") {
173
+ field.checked = field.value === value;
174
+ } else if (field instanceof HTMLSelectElement && field.multiple) {
175
+ const selected = new Set(Array.isArray(value) ? value.map(String) : []);
176
+ for (const option of Array.from(field.options)) {
177
+ option.selected = selected.has(option.value);
178
+ }
179
+ } else {
180
+ field.value = String(value);
181
+ }
182
+ }
183
+ /**
184
+ * Persistable fields paired with a stable storage key. Uniquely-named fields key
185
+ * by their name (unchanged, backward-compatible). Repeated same-name fields
186
+ * (e.g. a `tags[]` checkbox group or array text inputs) are disambiguated by
187
+ * DOM-order occurrence — the first keeps its plain `name` (backward-
188
+ * compatible), later ones get a NUL-separated index suffix — so each is stored and
189
+ * restored individually instead of the last one clobbering the rest. Radios are
190
+ * the exception: a group intentionally shares one key (one value per name).
191
+ */
192
+ #fieldEntries() {
193
+ const entries = [];
194
+ const occurrence = /* @__PURE__ */ new Map();
195
+ for (const field of this.#fields()) {
196
+ const name = this.#keyOf(field);
197
+ if (name === null) continue;
198
+ if (field instanceof HTMLInputElement && field.type === "radio") {
199
+ entries.push({ field, key: name });
200
+ continue;
201
+ }
202
+ const seen = occurrence.get(name) ?? 0;
203
+ occurrence.set(name, seen + 1);
204
+ entries.push({ field, key: seen === 0 ? name : `${name}${OCCURRENCE_SEP}${seen}` });
205
+ }
206
+ return entries;
207
+ }
208
+ /** The fields to persist: `field` targets, or the element's named controls. */
209
+ #fields() {
210
+ const candidates = this.hasFieldTarget ? this.fieldTargets : Array.from(this.element.querySelectorAll("input, textarea, select"));
211
+ return candidates.filter((field) => this.#persistable(field));
212
+ }
213
+ /** Whether a field carries a restorable, non-excluded value. */
214
+ #persistable(field) {
215
+ if (this.#keyOf(field) === null) return false;
216
+ const type = field instanceof HTMLInputElement ? field.type : "";
217
+ if (NON_VALUE_TYPES.has(type)) return false;
218
+ if (this.excludeValue.includes(type)) return false;
219
+ if (field.name.length > 0 && this.excludeValue.includes(field.name)) return false;
220
+ return true;
221
+ }
222
+ /** A stable storage sub-key for a field (its name, else id). */
223
+ #keyOf(field) {
224
+ return field.name || field.id || null;
225
+ }
226
+ /** The logical key (key Value or element id), or null when neither is set. */
227
+ get #logicalKey() {
228
+ const key = this.keyValue || this.element.id;
229
+ return key.length > 0 ? key : null;
230
+ }
231
+ /** The prefixed localStorage key, or null when persistence is disabled. */
232
+ get #storageKey() {
233
+ const logical = this.#logicalKey;
234
+ return logical === null ? null : `${STORAGE_PREFIX}${logical}`;
235
+ }
236
+ #getItem(key) {
237
+ try {
238
+ return window.localStorage.getItem(key);
239
+ } catch {
240
+ return null;
241
+ }
242
+ }
243
+ #setItem(key, value) {
244
+ try {
245
+ window.localStorage.setItem(key, value);
246
+ } catch {
247
+ }
248
+ }
249
+ #removeItem(key) {
250
+ try {
251
+ window.localStorage.removeItem(key);
252
+ } catch {
253
+ }
254
+ }
255
+ };
256
+
257
+ export { PersistController };
258
+ //# sourceMappingURL=persist_controller.js.map
259
+ //# sourceMappingURL=persist_controller.js.map
@@ -0,0 +1,94 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ // src/controllers/popover_controller.ts
4
+ var PopoverController = class _PopoverController extends Controller {
5
+ static targets = ["trigger", "panel"];
6
+ static actions = ["close", "open", "toggle"];
7
+ /** Selector for natively focusable elements used to find the first one. */
8
+ static #FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
9
+ /** Starts closed and registers the document-level dismissal listeners. */
10
+ connect() {
11
+ this.close();
12
+ document.addEventListener("click", this.#onOutsideClick);
13
+ document.addEventListener("keydown", this.#onKeydown);
14
+ }
15
+ /**
16
+ * Removes the document-level listeners registered in {@link connect}, plus the
17
+ * panel's `focusout` listener if the popover is torn down while open (e.g. a
18
+ * Turbo navigation). `removeEventListener` is a no-op when it was never added,
19
+ * so this is safe in the closed state too — no listener outlives the element.
20
+ */
21
+ disconnect() {
22
+ document.removeEventListener("click", this.#onOutsideClick);
23
+ document.removeEventListener("keydown", this.#onKeydown);
24
+ if (this.hasPanelTarget) this.panelTarget.removeEventListener("focusout", this.#onFocusOut);
25
+ }
26
+ /** Toggles the popover. Bound via `data-action` (click on the trigger). */
27
+ toggle() {
28
+ if (this.#isOpen) {
29
+ this.close();
30
+ } else {
31
+ this.open();
32
+ }
33
+ }
34
+ /** Opens the panel, reflects state, and moves focus inside it. */
35
+ open() {
36
+ if (!this.hasPanelTarget || this.#isOpen) return;
37
+ this.panelTarget.hidden = false;
38
+ if (this.hasTriggerTarget) this.triggerTarget.setAttribute("aria-expanded", "true");
39
+ this.panelTarget.addEventListener("focusout", this.#onFocusOut);
40
+ this.#focusFirst();
41
+ }
42
+ /** Closes the panel and reflects the collapsed state. Bound via `data-action`. */
43
+ close() {
44
+ if (!this.hasPanelTarget) return;
45
+ this.panelTarget.removeEventListener("focusout", this.#onFocusOut);
46
+ this.panelTarget.hidden = true;
47
+ if (this.hasTriggerTarget) this.triggerTarget.setAttribute("aria-expanded", "false");
48
+ }
49
+ /** Moves focus to the first focusable element in the panel, or the panel itself. */
50
+ #focusFirst() {
51
+ const first = this.panelTarget.querySelector(_PopoverController.#FOCUSABLE);
52
+ if (first) {
53
+ first.focus();
54
+ return;
55
+ }
56
+ if (!this.panelTarget.hasAttribute("tabindex")) this.panelTarget.tabIndex = -1;
57
+ this.panelTarget.focus();
58
+ }
59
+ /** Closes and restores focus to the trigger (shared by Escape / outside click). */
60
+ #closeAndRestore() {
61
+ this.close();
62
+ if (this.hasTriggerTarget) this.triggerTarget.focus();
63
+ }
64
+ /** Closes (restoring focus) when a click lands outside the controller element. */
65
+ #onOutsideClick = (event) => {
66
+ if (this.#isOpen && !this.element.contains(event.target)) this.#closeAndRestore();
67
+ };
68
+ /** Closes (restoring focus) on `Escape` while open. */
69
+ #onKeydown = (event) => {
70
+ if (event.key === "Escape" && this.#isOpen) {
71
+ event.preventDefault();
72
+ this.#closeAndRestore();
73
+ }
74
+ };
75
+ /**
76
+ * Closes when focus leaves the panel for an element outside the controller
77
+ * (e.g. `Tab` past the last field). Focus is *not* restored — the natural
78
+ * destination is kept, which is the modeless contract. Moves within the
79
+ * controller (panel → trigger) keep it open.
80
+ */
81
+ #onFocusOut = (event) => {
82
+ const next = event.relatedTarget;
83
+ if (next && this.element.contains(next)) return;
84
+ this.close();
85
+ };
86
+ /** Whether the panel is currently visible. */
87
+ get #isOpen() {
88
+ return this.hasPanelTarget && !this.panelTarget.hidden;
89
+ }
90
+ };
91
+
92
+ export { PopoverController };
93
+ //# sourceMappingURL=popover_controller.js.map
94
+ //# sourceMappingURL=popover_controller.js.map
@@ -0,0 +1,63 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ // src/controllers/portal_controller.ts
4
+ var portalState = /* @__PURE__ */ new WeakMap();
5
+ var PortalController = class extends Controller {
6
+ static targets = ["content"];
7
+ static values = {
8
+ to: { type: String, default: "body" },
9
+ position: { type: String, default: "append" },
10
+ restore: { type: Boolean, default: true }
11
+ };
12
+ static events = ["mount", "unmount"];
13
+ connect() {
14
+ if (portalState.has(this.element)) return;
15
+ const node = this.hasContentTarget ? this.contentTarget : this.element;
16
+ const destination = this.#destination();
17
+ if (!destination || destination === node || node.contains(destination)) return;
18
+ const placeholder = document.createComment("stimeo--portal");
19
+ node.parentNode?.insertBefore(placeholder, node);
20
+ portalState.set(this.element, { node, placeholder });
21
+ if (this.positionValue === "prepend") {
22
+ destination.prepend(node);
23
+ } else {
24
+ destination.appendChild(node);
25
+ }
26
+ node.setAttribute("data-portaled", "true");
27
+ this.dispatch("mount", { detail: { target: destination } });
28
+ }
29
+ disconnect() {
30
+ if (this.element.isConnected && this.#stillControlled()) return;
31
+ const state = portalState.get(this.element);
32
+ if (!state) return;
33
+ portalState.delete(this.element);
34
+ const { node, placeholder } = state;
35
+ node.removeAttribute("data-portaled");
36
+ if (this.restoreValue && placeholder.parentNode) {
37
+ placeholder.parentNode.insertBefore(node, placeholder);
38
+ } else {
39
+ node.remove();
40
+ }
41
+ placeholder.remove();
42
+ this.dispatch("unmount", { detail: {} });
43
+ }
44
+ /** True while `data-controller` still lists this identifier (spurious-churn signal). */
45
+ #stillControlled() {
46
+ const tokens = (this.element.getAttribute("data-controller") ?? "").split(/\s+/);
47
+ return tokens.includes(this.identifier);
48
+ }
49
+ /** Resolves the destination for `to`, tolerating an invalid selector. */
50
+ #destination() {
51
+ const selector = this.toValue.trim();
52
+ if (!selector) return null;
53
+ try {
54
+ return document.querySelector(selector);
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+ };
60
+
61
+ export { PortalController };
62
+ //# sourceMappingURL=portal_controller.js.map
63
+ //# sourceMappingURL=portal_controller.js.map
@@ -0,0 +1,69 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ // src/controllers/preview_guard_controller.ts
4
+ var PreviewGuardController = class extends Controller {
5
+ static values = {
6
+ placeholder: { type: String, default: "" },
7
+ mode: { type: String, default: "hide" }
8
+ };
9
+ static events = ["hide", "show"];
10
+ #observer = null;
11
+ #hidden = false;
12
+ /** Saved inline visibility (hide mode), restored on show. */
13
+ #savedVisibility = "";
14
+ /** Saved text (placeholder mode); non-null marks that text — not visibility — was swapped. */
15
+ #savedText = null;
16
+ connect() {
17
+ if (typeof MutationObserver !== "undefined") {
18
+ this.#observer = new MutationObserver(() => this.#sync());
19
+ this.#observer.observe(document.documentElement, {
20
+ attributes: true,
21
+ attributeFilter: ["data-turbo-preview"]
22
+ });
23
+ }
24
+ this.#sync();
25
+ }
26
+ disconnect() {
27
+ this.#observer?.disconnect();
28
+ this.#observer = null;
29
+ this.#restore();
30
+ }
31
+ /** Reflects the current `data-turbo-preview` state onto the element. */
32
+ #sync() {
33
+ const previewing = document.documentElement.hasAttribute("data-turbo-preview");
34
+ if (previewing && !this.#hidden) this.#hide();
35
+ else if (!previewing && this.#hidden) this.#show();
36
+ }
37
+ #hide() {
38
+ this.#hidden = true;
39
+ if (this.modeValue === "placeholder") {
40
+ this.#savedText = this.element.textContent;
41
+ this.element.textContent = this.placeholderValue;
42
+ } else {
43
+ this.#savedVisibility = this.element.style.visibility;
44
+ this.element.style.visibility = "hidden";
45
+ }
46
+ this.element.setAttribute("data-preview-hidden", "true");
47
+ this.dispatch("hide", { detail: {} });
48
+ }
49
+ #show() {
50
+ this.#restore();
51
+ this.dispatch("show", { detail: {} });
52
+ }
53
+ /** Reverts the guard. Safe to call when not hidden (no-op) — used by show and teardown. */
54
+ #restore() {
55
+ if (!this.#hidden) return;
56
+ this.#hidden = false;
57
+ if (this.#savedText !== null) {
58
+ this.element.textContent = this.#savedText;
59
+ this.#savedText = null;
60
+ } else {
61
+ this.element.style.visibility = this.#savedVisibility;
62
+ }
63
+ this.element.removeAttribute("data-preview-hidden");
64
+ }
65
+ };
66
+
67
+ export { PreviewGuardController };
68
+ //# sourceMappingURL=preview_guard_controller.js.map
69
+ //# sourceMappingURL=preview_guard_controller.js.map
@@ -0,0 +1,93 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ // src/controllers/progress_controller.ts
4
+
5
+ // src/utils/coerce.ts
6
+ function toFiniteNumber(raw) {
7
+ if (raw === null || raw === void 0 || raw === "") return null;
8
+ const value = typeof raw === "number" ? raw : Number(raw);
9
+ return Number.isFinite(value) ? value : null;
10
+ }
11
+
12
+ // src/controllers/progress_controller.ts
13
+ var ProgressController = class extends Controller {
14
+ static targets = ["bar"];
15
+ static values = {
16
+ value: { type: Number, default: 0 },
17
+ min: { type: Number, default: 0 },
18
+ max: { type: Number, default: 100 },
19
+ indeterminate: { type: Boolean, default: false },
20
+ valueText: { type: String, default: "" }
21
+ };
22
+ static actions = ["setValue"];
23
+ static events = ["change", "complete"];
24
+ connect() {
25
+ this.#render();
26
+ }
27
+ /**
28
+ * Updates the progress value from an action param (`amount`) or a
29
+ * `detail.value` CustomEvent, normalizes it into range, syncs ARIA, and
30
+ * dispatches `change` (always) plus `complete` when `max` is reached.
31
+ */
32
+ setValue(event) {
33
+ const next = toFiniteNumber(event.params?.amount ?? event.detail?.value);
34
+ if (next === null) return;
35
+ const value = this.#clamp(next);
36
+ this.valueValue = value;
37
+ this.indeterminateValue = false;
38
+ this.#render();
39
+ this.dispatch("change", { detail: { value, ratio: this.#ratio } });
40
+ if (value >= this.maxValue) {
41
+ this.dispatch("complete", { detail: { value } });
42
+ }
43
+ }
44
+ /** Re-render when the indeterminate flag is toggled via its data attribute. */
45
+ indeterminateValueChanged() {
46
+ this.#render();
47
+ }
48
+ /** Clamps `raw` into the configured `[min, max]` range. */
49
+ #clamp(raw) {
50
+ return Math.min(this.maxValue, Math.max(this.minValue, raw));
51
+ }
52
+ /** Current fraction of the range in `[0, 1]`; `0` when the range is empty. */
53
+ get #ratio() {
54
+ const span = this.maxValue - this.minValue;
55
+ if (span <= 0) return 0;
56
+ return (this.#clamp(this.valueValue) - this.minValue) / span;
57
+ }
58
+ /** Reflects value/range/indeterminate onto ARIA, `data-state`, and the ratio. */
59
+ #render() {
60
+ this.element.setAttribute("aria-valuemin", String(this.minValue));
61
+ this.element.setAttribute("aria-valuemax", String(this.maxValue));
62
+ if (this.indeterminateValue) {
63
+ this.element.removeAttribute("aria-valuenow");
64
+ this.element.removeAttribute("aria-valuetext");
65
+ this.element.style.removeProperty("--stimeo-progress-ratio");
66
+ this.element.setAttribute("data-state", "indeterminate");
67
+ return;
68
+ }
69
+ const value = this.#clamp(this.valueValue);
70
+ this.element.setAttribute("aria-valuenow", String(value));
71
+ this.element.style.setProperty("--stimeo-progress-ratio", String(this.#ratio));
72
+ this.element.setAttribute("data-state", "determinate");
73
+ this.#applyValueText(value);
74
+ }
75
+ /**
76
+ * Sets `aria-valuetext` from the consumer-provided template, substituting
77
+ * `{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.
79
+ */
80
+ #applyValueText(value) {
81
+ if (this.valueTextValue.length === 0) {
82
+ this.element.removeAttribute("aria-valuetext");
83
+ return;
84
+ }
85
+ const percent = Math.round(this.#ratio * 100);
86
+ const text = this.valueTextValue.replaceAll("{value}", String(value)).replaceAll("{percent}", String(percent));
87
+ this.element.setAttribute("aria-valuetext", text);
88
+ }
89
+ };
90
+
91
+ export { ProgressController };
92
+ //# sourceMappingURL=progress_controller.js.map
93
+ //# sourceMappingURL=progress_controller.js.map