stimeo-ui 0.1.0-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 (238) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +72 -0
  3. package/dist/controllers/accordion_controller.d.ts +59 -0
  4. package/dist/controllers/accordion_controller.js +76 -0
  5. package/dist/controllers/accordion_controller.js.map +1 -0
  6. package/dist/controllers/announcer_controller.d.ts +78 -0
  7. package/dist/controllers/announcer_controller.js +184 -0
  8. package/dist/controllers/announcer_controller.js.map +1 -0
  9. package/dist/controllers/aspect_ratio_controller.d.ts +39 -0
  10. package/dist/controllers/aspect_ratio_controller.js +36 -0
  11. package/dist/controllers/aspect_ratio_controller.js.map +1 -0
  12. package/dist/controllers/auto_submit_controller.d.ts +74 -0
  13. package/dist/controllers/auto_submit_controller.js +147 -0
  14. package/dist/controllers/auto_submit_controller.js.map +1 -0
  15. package/dist/controllers/avatar_controller.d.ts +50 -0
  16. package/dist/controllers/avatar_controller.js +66 -0
  17. package/dist/controllers/avatar_controller.js.map +1 -0
  18. package/dist/controllers/breadcrumb_controller.d.ts +60 -0
  19. package/dist/controllers/breadcrumb_controller.js +123 -0
  20. package/dist/controllers/breadcrumb_controller.js.map +1 -0
  21. package/dist/controllers/bulk_select_controller.d.ts +67 -0
  22. package/dist/controllers/bulk_select_controller.js +104 -0
  23. package/dist/controllers/bulk_select_controller.js.map +1 -0
  24. package/dist/controllers/calendar_controller.d.ts +100 -0
  25. package/dist/controllers/calendar_controller.js +394 -0
  26. package/dist/controllers/calendar_controller.js.map +1 -0
  27. package/dist/controllers/character_counter_controller.d.ts +63 -0
  28. package/dist/controllers/character_counter_controller.js +179 -0
  29. package/dist/controllers/character_counter_controller.js.map +1 -0
  30. package/dist/controllers/checkbox_controller.d.ts +52 -0
  31. package/dist/controllers/checkbox_controller.js +73 -0
  32. package/dist/controllers/checkbox_controller.js.map +1 -0
  33. package/dist/controllers/combobox_controller.d.ts +76 -0
  34. package/dist/controllers/combobox_controller.js +186 -0
  35. package/dist/controllers/combobox_controller.js.map +1 -0
  36. package/dist/controllers/command_palette_controller.d.ts +103 -0
  37. package/dist/controllers/command_palette_controller.js +381 -0
  38. package/dist/controllers/command_palette_controller.js.map +1 -0
  39. package/dist/controllers/conditional_fields_controller.d.ts +56 -0
  40. package/dist/controllers/conditional_fields_controller.js +112 -0
  41. package/dist/controllers/conditional_fields_controller.js.map +1 -0
  42. package/dist/controllers/confirm_controller.d.ts +83 -0
  43. package/dist/controllers/confirm_controller.js +276 -0
  44. package/dist/controllers/confirm_controller.js.map +1 -0
  45. package/dist/controllers/context_menu_controller.d.ts +81 -0
  46. package/dist/controllers/context_menu_controller.js +112 -0
  47. package/dist/controllers/context_menu_controller.js.map +1 -0
  48. package/dist/controllers/countdown_controller.d.ts +93 -0
  49. package/dist/controllers/countdown_controller.js +202 -0
  50. package/dist/controllers/countdown_controller.js.map +1 -0
  51. package/dist/controllers/dialog_controller.d.ts +57 -0
  52. package/dist/controllers/dialog_controller.js +207 -0
  53. package/dist/controllers/dialog_controller.js.map +1 -0
  54. package/dist/controllers/direct_upload_controller.d.ts +77 -0
  55. package/dist/controllers/direct_upload_controller.js +212 -0
  56. package/dist/controllers/direct_upload_controller.js.map +1 -0
  57. package/dist/controllers/dirty_form_controller.d.ts +52 -0
  58. package/dist/controllers/dirty_form_controller.js +128 -0
  59. package/dist/controllers/dirty_form_controller.js.map +1 -0
  60. package/dist/controllers/dropdown_controller.d.ts +49 -0
  61. package/dist/controllers/dropdown_controller.js +66 -0
  62. package/dist/controllers/dropdown_controller.js.map +1 -0
  63. package/dist/controllers/empty_state_controller.d.ts +60 -0
  64. package/dist/controllers/empty_state_controller.js +67 -0
  65. package/dist/controllers/empty_state_controller.js.map +1 -0
  66. package/dist/controllers/flash_controller.d.ts +63 -0
  67. package/dist/controllers/flash_controller.js +221 -0
  68. package/dist/controllers/flash_controller.js.map +1 -0
  69. package/dist/controllers/focus_controller.d.ts +69 -0
  70. package/dist/controllers/focus_controller.js +216 -0
  71. package/dist/controllers/focus_controller.js.map +1 -0
  72. package/dist/controllers/form_field_controller.d.ts +79 -0
  73. package/dist/controllers/form_field_controller.js +154 -0
  74. package/dist/controllers/form_field_controller.js.map +1 -0
  75. package/dist/controllers/form_validation_controller.d.ts +114 -0
  76. package/dist/controllers/form_validation_controller.js +202 -0
  77. package/dist/controllers/form_validation_controller.js.map +1 -0
  78. package/dist/controllers/frame_loading_controller.d.ts +58 -0
  79. package/dist/controllers/frame_loading_controller.js +177 -0
  80. package/dist/controllers/frame_loading_controller.js.map +1 -0
  81. package/dist/controllers/highlight_controller.d.ts +50 -0
  82. package/dist/controllers/highlight_controller.js +107 -0
  83. package/dist/controllers/highlight_controller.js.map +1 -0
  84. package/dist/controllers/hover_card_controller.d.ts +79 -0
  85. package/dist/controllers/hover_card_controller.js +165 -0
  86. package/dist/controllers/hover_card_controller.js.map +1 -0
  87. package/dist/controllers/idle_controller.d.ts +51 -0
  88. package/dist/controllers/idle_controller.js +141 -0
  89. package/dist/controllers/idle_controller.js.map +1 -0
  90. package/dist/controllers/input_mask_controller.d.ts +74 -0
  91. package/dist/controllers/input_mask_controller.js +166 -0
  92. package/dist/controllers/input_mask_controller.js.map +1 -0
  93. package/dist/controllers/lazy_frame_controller.d.ts +53 -0
  94. package/dist/controllers/lazy_frame_controller.js +68 -0
  95. package/dist/controllers/lazy_frame_controller.js.map +1 -0
  96. package/dist/controllers/listbox_controller.d.ts +88 -0
  97. package/dist/controllers/listbox_controller.js +256 -0
  98. package/dist/controllers/listbox_controller.js.map +1 -0
  99. package/dist/controllers/local_time_controller.d.ts +60 -0
  100. package/dist/controllers/local_time_controller.js +81 -0
  101. package/dist/controllers/local_time_controller.js.map +1 -0
  102. package/dist/controllers/menu_controller.d.ts +81 -0
  103. package/dist/controllers/menu_controller.js +134 -0
  104. package/dist/controllers/menu_controller.js.map +1 -0
  105. package/dist/controllers/meter_controller.d.ts +93 -0
  106. package/dist/controllers/meter_controller.js +96 -0
  107. package/dist/controllers/meter_controller.js.map +1 -0
  108. package/dist/controllers/nested_form_controller.d.ts +85 -0
  109. package/dist/controllers/nested_form_controller.js +131 -0
  110. package/dist/controllers/nested_form_controller.js.map +1 -0
  111. package/dist/controllers/network_status_controller.d.ts +49 -0
  112. package/dist/controllers/network_status_controller.js +126 -0
  113. package/dist/controllers/network_status_controller.js.map +1 -0
  114. package/dist/controllers/number_input_controller.d.ts +93 -0
  115. package/dist/controllers/number_input_controller.js +306 -0
  116. package/dist/controllers/number_input_controller.js.map +1 -0
  117. package/dist/controllers/otp_controller.d.ts +68 -0
  118. package/dist/controllers/otp_controller.js +201 -0
  119. package/dist/controllers/otp_controller.js.map +1 -0
  120. package/dist/controllers/overflow_indicator_controller.d.ts +62 -0
  121. package/dist/controllers/overflow_indicator_controller.js +169 -0
  122. package/dist/controllers/overflow_indicator_controller.js.map +1 -0
  123. package/dist/controllers/overflow_menu_controller.d.ts +64 -0
  124. package/dist/controllers/overflow_menu_controller.js +274 -0
  125. package/dist/controllers/overflow_menu_controller.js.map +1 -0
  126. package/dist/controllers/pagination_controller.d.ts +69 -0
  127. package/dist/controllers/pagination_controller.js +89 -0
  128. package/dist/controllers/pagination_controller.js.map +1 -0
  129. package/dist/controllers/password_strength_controller.d.ts +67 -0
  130. package/dist/controllers/password_strength_controller.js +175 -0
  131. package/dist/controllers/password_strength_controller.js.map +1 -0
  132. package/dist/controllers/persist_controller.d.ts +63 -0
  133. package/dist/controllers/persist_controller.js +259 -0
  134. package/dist/controllers/persist_controller.js.map +1 -0
  135. package/dist/controllers/popover_controller.d.ts +63 -0
  136. package/dist/controllers/popover_controller.js +94 -0
  137. package/dist/controllers/popover_controller.js.map +1 -0
  138. package/dist/controllers/portal_controller.d.ts +58 -0
  139. package/dist/controllers/portal_controller.js +63 -0
  140. package/dist/controllers/portal_controller.js.map +1 -0
  141. package/dist/controllers/preview_guard_controller.d.ts +47 -0
  142. package/dist/controllers/preview_guard_controller.js +69 -0
  143. package/dist/controllers/preview_guard_controller.js.map +1 -0
  144. package/dist/controllers/progress_controller.d.ts +85 -0
  145. package/dist/controllers/progress_controller.js +93 -0
  146. package/dist/controllers/progress_controller.js.map +1 -0
  147. package/dist/controllers/radio_group_controller.d.ts +49 -0
  148. package/dist/controllers/radio_group_controller.js +128 -0
  149. package/dist/controllers/radio_group_controller.js.map +1 -0
  150. package/dist/controllers/rating_controller.d.ts +82 -0
  151. package/dist/controllers/rating_controller.js +179 -0
  152. package/dist/controllers/rating_controller.js.map +1 -0
  153. package/dist/controllers/relative_time_controller.d.ts +48 -0
  154. package/dist/controllers/relative_time_controller.js +129 -0
  155. package/dist/controllers/relative_time_controller.js.map +1 -0
  156. package/dist/controllers/reset_before_cache_controller.d.ts +54 -0
  157. package/dist/controllers/reset_before_cache_controller.js +62 -0
  158. package/dist/controllers/reset_before_cache_controller.js.map +1 -0
  159. package/dist/controllers/resizable_controller.d.ts +81 -0
  160. package/dist/controllers/resizable_controller.js +163 -0
  161. package/dist/controllers/resizable_controller.js.map +1 -0
  162. package/dist/controllers/roving_controller.d.ts +63 -0
  163. package/dist/controllers/roving_controller.js +116 -0
  164. package/dist/controllers/roving_controller.js.map +1 -0
  165. package/dist/controllers/scroll_area_controller.d.ts +45 -0
  166. package/dist/controllers/scroll_area_controller.js +183 -0
  167. package/dist/controllers/scroll_area_controller.js.map +1 -0
  168. package/dist/controllers/scroll_visibility_controller.d.ts +72 -0
  169. package/dist/controllers/scroll_visibility_controller.js +103 -0
  170. package/dist/controllers/scroll_visibility_controller.js.map +1 -0
  171. package/dist/controllers/scrollspy_controller.d.ts +65 -0
  172. package/dist/controllers/scrollspy_controller.js +171 -0
  173. package/dist/controllers/scrollspy_controller.js.map +1 -0
  174. package/dist/controllers/skeleton_controller.d.ts +50 -0
  175. package/dist/controllers/skeleton_controller.js +125 -0
  176. package/dist/controllers/skeleton_controller.js.map +1 -0
  177. package/dist/controllers/slider_controller.d.ts +76 -0
  178. package/dist/controllers/slider_controller.js +109 -0
  179. package/dist/controllers/slider_controller.js.map +1 -0
  180. package/dist/controllers/spinner_controller.d.ts +62 -0
  181. package/dist/controllers/spinner_controller.js +164 -0
  182. package/dist/controllers/spinner_controller.js.map +1 -0
  183. package/dist/controllers/step_indicator_controller.d.ts +55 -0
  184. package/dist/controllers/step_indicator_controller.js +55 -0
  185. package/dist/controllers/step_indicator_controller.js.map +1 -0
  186. package/dist/controllers/stepper_controller.d.ts +66 -0
  187. package/dist/controllers/stepper_controller.js +78 -0
  188. package/dist/controllers/stepper_controller.js.map +1 -0
  189. package/dist/controllers/stick_to_bottom_controller.d.ts +61 -0
  190. package/dist/controllers/stick_to_bottom_controller.js +100 -0
  191. package/dist/controllers/stick_to_bottom_controller.js.map +1 -0
  192. package/dist/controllers/sticky_observer_controller.d.ts +54 -0
  193. package/dist/controllers/sticky_observer_controller.js +53 -0
  194. package/dist/controllers/sticky_observer_controller.js.map +1 -0
  195. package/dist/controllers/submit_once_controller.d.ts +66 -0
  196. package/dist/controllers/submit_once_controller.js +206 -0
  197. package/dist/controllers/submit_once_controller.js.map +1 -0
  198. package/dist/controllers/switch_controller.d.ts +43 -0
  199. package/dist/controllers/switch_controller.js +50 -0
  200. package/dist/controllers/switch_controller.js.map +1 -0
  201. package/dist/controllers/tabs_controller.d.ts +48 -0
  202. package/dist/controllers/tabs_controller.js +63 -0
  203. package/dist/controllers/tabs_controller.js.map +1 -0
  204. package/dist/controllers/textarea_autosize_controller.d.ts +47 -0
  205. package/dist/controllers/textarea_autosize_controller.js +72 -0
  206. package/dist/controllers/textarea_autosize_controller.js.map +1 -0
  207. package/dist/controllers/theme_controller.d.ts +70 -0
  208. package/dist/controllers/theme_controller.js +154 -0
  209. package/dist/controllers/theme_controller.js.map +1 -0
  210. package/dist/controllers/toast_controller.d.ts +123 -0
  211. package/dist/controllers/toast_controller.js +310 -0
  212. package/dist/controllers/toast_controller.js.map +1 -0
  213. package/dist/controllers/toggle_group_controller.d.ts +56 -0
  214. package/dist/controllers/toggle_group_controller.js +130 -0
  215. package/dist/controllers/toggle_group_controller.js.map +1 -0
  216. package/dist/controllers/toolbar_controller.d.ts +59 -0
  217. package/dist/controllers/toolbar_controller.js +113 -0
  218. package/dist/controllers/toolbar_controller.js.map +1 -0
  219. package/dist/controllers/tooltip_controller.d.ts +74 -0
  220. package/dist/controllers/tooltip_controller.js +165 -0
  221. package/dist/controllers/tooltip_controller.js.map +1 -0
  222. package/dist/controllers/transition_controller.d.ts +86 -0
  223. package/dist/controllers/transition_controller.js +203 -0
  224. package/dist/controllers/transition_controller.js.map +1 -0
  225. package/dist/index.d.ts +1931 -0
  226. package/dist/index.js +12241 -0
  227. package/dist/index.js.map +1 -0
  228. package/dist/inspector/cli.d.ts +149 -0
  229. package/dist/inspector/cli.js +663 -0
  230. package/dist/inspector/cli.js.map +1 -0
  231. package/dist/inspector/cli_bin.d.ts +2 -0
  232. package/dist/inspector/cli_bin.js +665 -0
  233. package/dist/inspector/cli_bin.js.map +1 -0
  234. package/dist/inspector/manifest.json +2235 -0
  235. package/dist/positioning/index.d.ts +226 -0
  236. package/dist/positioning/index.js +145 -0
  237. package/dist/positioning/index.js.map +1 -0
  238. package/package.json +93 -0
@@ -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 @@
1
+ {"version":3,"sources":["../../src/controllers/popover_controller.ts"],"names":[],"mappings":"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","file":"popover_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\n/**\n * Headless, accessible **non-modal popover** behavior.\n *\n * Markup contract (identifier: `stimeo--popover`):\n * <div data-controller=\"stimeo--popover\">\n * <button data-stimeo--popover-target=\"trigger\"\n * aria-haspopup=\"dialog\" aria-expanded=\"false\" aria-controls=\"pop\"\n * data-action=\"click->stimeo--popover#toggle\">Edit profile</button>\n * <div id=\"pop\" data-stimeo--popover-target=\"panel\"\n * role=\"dialog\" aria-label=\"Edit profile\" hidden>…</div>\n * </div>\n *\n * Implements the WAI-ARIA APG **Dialog** pattern run *non-modally* (no\n * `aria-modal`, no focus trap, no `inert`/scroll lock). The background stays\n * fully interactive; this is the modeless counterpart to `stimeo--dialog`. For\n * a roving `role=\"menu\"` of commands use `stimeo--menu`; for decorative-only\n * text use `stimeo--tooltip`.\n *\n * @remarks\n * Behavior only — static placement is the consumer's CSS, and dynamic\n * edge-collision avoidance is delegated to the opt-in `stimeo-ui/positioning`\n * module (this controller never imports it, preserving the zero-dep core). State\n * is exposed via the trigger's `aria-expanded` and the panel's `hidden`.\n *\n * Behavior provided:\n * - Click the trigger to toggle (`aria-expanded` + `hidden` reflect state).\n * - On open, focus moves to the first focusable element inside the panel (or the\n * panel itself if it has none).\n * - `Escape` closes and restores focus to the trigger.\n * - An outside click (anywhere off the controller element) closes and restores\n * focus to the trigger.\n * - Because it is modeless, focus is *not* trapped: when `Tab` moves focus out of\n * the panel it closes (detected via `focusout`) without yanking focus back, so\n * the natural tab destination is preserved.\n */\nexport class PopoverController extends Controller<HTMLElement> {\n static override targets = [\"trigger\", \"panel\"];\n static actions = [\"close\", \"open\", \"toggle\"] as const;\n\n declare readonly triggerTarget: HTMLButtonElement;\n declare readonly panelTarget: HTMLElement;\n declare readonly hasTriggerTarget: boolean;\n declare readonly hasPanelTarget: boolean;\n\n /** Selector for natively focusable elements used to find the first one. */\n static readonly #FOCUSABLE =\n 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])';\n\n /** Starts closed and registers the document-level dismissal listeners. */\n override connect(): void {\n this.close();\n document.addEventListener(\"click\", this.#onOutsideClick);\n document.addEventListener(\"keydown\", this.#onKeydown);\n }\n\n /**\n * Removes the document-level listeners registered in {@link connect}, plus the\n * panel's `focusout` listener if the popover is torn down while open (e.g. a\n * Turbo navigation). `removeEventListener` is a no-op when it was never added,\n * so this is safe in the closed state too — no listener outlives the element.\n */\n override disconnect(): void {\n document.removeEventListener(\"click\", this.#onOutsideClick);\n document.removeEventListener(\"keydown\", this.#onKeydown);\n if (this.hasPanelTarget) this.panelTarget.removeEventListener(\"focusout\", this.#onFocusOut);\n }\n\n /** Toggles the popover. Bound via `data-action` (click on the trigger). */\n toggle(): void {\n if (this.#isOpen) {\n this.close();\n } else {\n this.open();\n }\n }\n\n /** Opens the panel, reflects state, and moves focus inside it. */\n open(): void {\n if (!this.hasPanelTarget || this.#isOpen) return;\n this.panelTarget.hidden = false;\n if (this.hasTriggerTarget) this.triggerTarget.setAttribute(\"aria-expanded\", \"true\");\n // focusout fires as focus settles; register only once the panel is open so an\n // initial close()/connect() does not immediately re-close it.\n this.panelTarget.addEventListener(\"focusout\", this.#onFocusOut);\n this.#focusFirst();\n }\n\n /** Closes the panel and reflects the collapsed state. Bound via `data-action`. */\n close(): void {\n if (!this.hasPanelTarget) return;\n this.panelTarget.removeEventListener(\"focusout\", this.#onFocusOut);\n this.panelTarget.hidden = true;\n if (this.hasTriggerTarget) this.triggerTarget.setAttribute(\"aria-expanded\", \"false\");\n }\n\n /** Moves focus to the first focusable element in the panel, or the panel itself. */\n #focusFirst(): void {\n const first = this.panelTarget.querySelector<HTMLElement>(PopoverController.#FOCUSABLE);\n if (first) {\n first.focus();\n return;\n }\n if (!this.panelTarget.hasAttribute(\"tabindex\")) this.panelTarget.tabIndex = -1;\n this.panelTarget.focus();\n }\n\n /** Closes and restores focus to the trigger (shared by Escape / outside click). */\n #closeAndRestore(): void {\n this.close();\n if (this.hasTriggerTarget) this.triggerTarget.focus();\n }\n\n /** Closes (restoring focus) when a click lands outside the controller element. */\n readonly #onOutsideClick = (event: MouseEvent): void => {\n if (this.#isOpen && !this.element.contains(event.target as Node)) this.#closeAndRestore();\n };\n\n /** Closes (restoring focus) on `Escape` while open. */\n readonly #onKeydown = (event: KeyboardEvent): void => {\n if (event.key === \"Escape\" && this.#isOpen) {\n event.preventDefault();\n this.#closeAndRestore();\n }\n };\n\n /**\n * Closes when focus leaves the panel for an element outside the controller\n * (e.g. `Tab` past the last field). Focus is *not* restored — the natural\n * destination is kept, which is the modeless contract. Moves within the\n * controller (panel → trigger) keep it open.\n */\n readonly #onFocusOut = (event: FocusEvent): void => {\n const next = event.relatedTarget as Node | null;\n if (next && this.element.contains(next)) return;\n this.close();\n };\n\n /** Whether the panel is currently visible. */\n get #isOpen(): boolean {\n return this.hasPanelTarget && !this.panelTarget.hidden;\n }\n}\n"]}
@@ -0,0 +1,58 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ /**
4
+ * Headless **portal / teleport**: moves an element to another place in the DOM (e.g.
5
+ * directly under `body`) on connect and cleans up on disconnect — the shared substrate
6
+ * for overlays that must escape an ancestor's `overflow: hidden`, `transform`, or
7
+ * stacking context (no APG pattern; a DOM utility). Counterpart to Radix `Portal` /
8
+ * Alpine `x-teleport`.
9
+ *
10
+ * Markup contract (identifier: `stimeo--portal`):
11
+ * <div data-controller="stimeo--portal" data-stimeo--portal-to-value="body">
12
+ * <div data-stimeo--portal-target="content">Teleported content</div>
13
+ * </div>
14
+ *
15
+ * Moves `content` (or `this.element` when no `content` target) into the first element
16
+ * matching `to` (default `body`), `append`ed or `prepend`ed per `position`. A comment
17
+ * placeholder records the original spot so `disconnect()` can return the node there
18
+ * (when `restore`) — or remove it — leaving no orphan behind. The moved node carries
19
+ * `data-portaled`.
20
+ *
21
+ * @remarks
22
+ * Behavior only — no positioning (pair with `stimeo-ui/positioning`) and no focus
23
+ * trapping (pair with a Focus Scope / the overlay). Moving a Stimulus element within
24
+ * the same document does not re-fire connect/disconnect, so the move is safe. For Turbo
25
+ * compatibility prefer the `content`-target form: the controller then stays on the
26
+ * in-place source, so its `disconnect()` fires when the original container is replaced
27
+ * and the teleported node is restored/removed rather than orphaned under `body`. The
28
+ * move is idempotent (guarded by the placeholder) and reversed on `disconnect()` (Turbo
29
+ * navigation included).
30
+ */
31
+ declare class PortalController extends Controller<HTMLElement> {
32
+ #private;
33
+ static targets: string[];
34
+ static values: {
35
+ to: {
36
+ type: StringConstructor;
37
+ default: string;
38
+ };
39
+ position: {
40
+ type: StringConstructor;
41
+ default: string;
42
+ };
43
+ restore: {
44
+ type: BooleanConstructor;
45
+ default: boolean;
46
+ };
47
+ };
48
+ static events: readonly ["mount", "unmount"];
49
+ readonly contentTarget: HTMLElement;
50
+ readonly hasContentTarget: boolean;
51
+ toValue: string;
52
+ positionValue: string;
53
+ restoreValue: boolean;
54
+ connect(): void;
55
+ disconnect(): void;
56
+ }
57
+
58
+ export { PortalController };
@@ -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 @@
1
+ {"version":3,"sources":["../../src/controllers/portal_controller.ts"],"names":[],"mappings":"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","file":"portal_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\n/**\n * Teleport bookkeeping keyed by the controller element (stable across the\n * connect/disconnect churn some DOM runtimes emit when an observed element is moved).\n * Holds the moved node and its placeholder so any controller instance for that element\n * can finish the teardown.\n */\nconst portalState = new WeakMap<Element, { node: HTMLElement; placeholder: Comment }>();\n\n/**\n * Headless **portal / teleport**: moves an element to another place in the DOM (e.g.\n * directly under `body`) on connect and cleans up on disconnect — the shared substrate\n * for overlays that must escape an ancestor's `overflow: hidden`, `transform`, or\n * stacking context (no APG pattern; a DOM utility). Counterpart to Radix `Portal` /\n * Alpine `x-teleport`.\n *\n * Markup contract (identifier: `stimeo--portal`):\n * <div data-controller=\"stimeo--portal\" data-stimeo--portal-to-value=\"body\">\n * <div data-stimeo--portal-target=\"content\">Teleported content</div>\n * </div>\n *\n * Moves `content` (or `this.element` when no `content` target) into the first element\n * matching `to` (default `body`), `append`ed or `prepend`ed per `position`. A comment\n * placeholder records the original spot so `disconnect()` can return the node there\n * (when `restore`) — or remove it — leaving no orphan behind. The moved node carries\n * `data-portaled`.\n *\n * @remarks\n * Behavior only — no positioning (pair with `stimeo-ui/positioning`) and no focus\n * trapping (pair with a Focus Scope / the overlay). Moving a Stimulus element within\n * the same document does not re-fire connect/disconnect, so the move is safe. For Turbo\n * compatibility prefer the `content`-target form: the controller then stays on the\n * in-place source, so its `disconnect()` fires when the original container is replaced\n * and the teleported node is restored/removed rather than orphaned under `body`. The\n * move is idempotent (guarded by the placeholder) and reversed on `disconnect()` (Turbo\n * navigation included).\n */\nexport class PortalController extends Controller<HTMLElement> {\n static override targets = [\"content\"];\n static override values = {\n to: { type: String, default: \"body\" },\n position: { type: String, default: \"append\" },\n restore: { type: Boolean, default: true },\n };\n static events = [\"mount\", \"unmount\"] as const;\n\n declare readonly contentTarget: HTMLElement;\n declare readonly hasContentTarget: boolean;\n\n declare toValue: string;\n declare positionValue: string;\n declare restoreValue: boolean;\n\n override connect(): void {\n if (portalState.has(this.element)) return; // already portaled here (idempotent)\n const node = this.hasContentTarget ? this.contentTarget : this.element;\n const destination = this.#destination();\n if (!destination || destination === node || node.contains(destination)) return;\n\n const placeholder = document.createComment(\"stimeo--portal\");\n node.parentNode?.insertBefore(placeholder, node);\n portalState.set(this.element, { node, placeholder });\n\n if (this.positionValue === \"prepend\") {\n destination.prepend(node);\n } else {\n destination.appendChild(node);\n }\n node.setAttribute(\"data-portaled\", \"true\");\n this.dispatch(\"mount\", { detail: { target: destination } });\n }\n\n override disconnect(): void {\n // Moving the controller element (the no-`content` form) makes some runtimes re-emit\n // disconnect/connect. That spurious churn is recognisable: the element is still in the\n // document AND still carries this identifier (Stimulus will reconnect immediately), so\n // ignore it. A genuine teardown — the element left the DOM, or `data-controller` no\n // longer lists us (e.g. a Turbo 8 morph) — must still restore/remove the teleport.\n if (this.element.isConnected && this.#stillControlled()) return;\n const state = portalState.get(this.element);\n if (!state) return;\n portalState.delete(this.element);\n const { node, placeholder } = state;\n\n node.removeAttribute(\"data-portaled\");\n if (this.restoreValue && placeholder.parentNode) {\n placeholder.parentNode.insertBefore(node, placeholder);\n } else {\n node.remove();\n }\n placeholder.remove();\n this.dispatch(\"unmount\", { detail: {} });\n }\n\n /** True while `data-controller` still lists this identifier (spurious-churn signal). */\n #stillControlled(): boolean {\n const tokens = (this.element.getAttribute(\"data-controller\") ?? \"\").split(/\\s+/);\n return tokens.includes(this.identifier);\n }\n\n /** Resolves the destination for `to`, tolerating an invalid selector. */\n #destination(): Element | null {\n const selector = this.toValue.trim();\n if (!selector) return null;\n try {\n return document.querySelector(selector);\n } catch {\n return null;\n }\n }\n}\n"]}
@@ -0,0 +1,47 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ /**
4
+ * Headless **preview guard** (Hotwire-specific): hides or placeholders a volatile element
5
+ * — a balance, a notification count, a live timestamp — *only while Turbo is showing a
6
+ * preview* (`html[data-turbo-preview]`), so a stale cached snapshot does not briefly flash
7
+ * the old value on a back/restore visit. No APG pattern; it keeps displayed information
8
+ * accurate and never moves focus.
9
+ *
10
+ * Markup contract (identifier: `stimeo--preview-guard`):
11
+ * <span data-controller="stimeo--preview-guard"
12
+ * data-stimeo--preview-guard-mode-value="placeholder"
13
+ * data-stimeo--preview-guard-placeholder-value="—">¥123,456</span>
14
+ *
15
+ * Watches `<html>` for the `data-turbo-preview` attribute with a `MutationObserver`.
16
+ * While it is present the element is guarded: `mode="hide"` makes it `visibility: hidden`
17
+ * (its box is kept, so nothing shifts), `mode="placeholder"` swaps its text for
18
+ * `placeholder`. The element carries `data-preview-hidden` and emits `hide`; when the
19
+ * preview clears it is restored and `show` fires.
20
+ *
21
+ * @remarks
22
+ * Behavior only — restoring the *fresh* value is the normal render's job, not this
23
+ * controller's (it just un-hides what was there). State is derived from the DOM (no
24
+ * module-scope state) and the original value is saved on hide and put back on show, so a
25
+ * guard that connects mid-preview hides immediately and a `disconnect()` (Turbo navigation
26
+ * included) restores the element and severs the observer. Focus is never moved.
27
+ */
28
+ declare class PreviewGuardController extends Controller<HTMLElement> {
29
+ #private;
30
+ static values: {
31
+ placeholder: {
32
+ type: StringConstructor;
33
+ default: string;
34
+ };
35
+ mode: {
36
+ type: StringConstructor;
37
+ default: string;
38
+ };
39
+ };
40
+ static events: readonly ["hide", "show"];
41
+ placeholderValue: string;
42
+ modeValue: string;
43
+ connect(): void;
44
+ disconnect(): void;
45
+ }
46
+
47
+ export { PreviewGuardController };
@@ -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 @@
1
+ {"version":3,"sources":["../../src/controllers/preview_guard_controller.ts"],"names":[],"mappings":"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","file":"preview_guard_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\n/**\n * Headless **preview guard** (Hotwire-specific): hides or placeholders a volatile element\n * — a balance, a notification count, a live timestamp — *only while Turbo is showing a\n * preview* (`html[data-turbo-preview]`), so a stale cached snapshot does not briefly flash\n * the old value on a back/restore visit. No APG pattern; it keeps displayed information\n * accurate and never moves focus.\n *\n * Markup contract (identifier: `stimeo--preview-guard`):\n * <span data-controller=\"stimeo--preview-guard\"\n * data-stimeo--preview-guard-mode-value=\"placeholder\"\n * data-stimeo--preview-guard-placeholder-value=\"—\">¥123,456</span>\n *\n * Watches `<html>` for the `data-turbo-preview` attribute with a `MutationObserver`.\n * While it is present the element is guarded: `mode=\"hide\"` makes it `visibility: hidden`\n * (its box is kept, so nothing shifts), `mode=\"placeholder\"` swaps its text for\n * `placeholder`. The element carries `data-preview-hidden` and emits `hide`; when the\n * preview clears it is restored and `show` fires.\n *\n * @remarks\n * Behavior only — restoring the *fresh* value is the normal render's job, not this\n * controller's (it just un-hides what was there). State is derived from the DOM (no\n * module-scope state) and the original value is saved on hide and put back on show, so a\n * guard that connects mid-preview hides immediately and a `disconnect()` (Turbo navigation\n * included) restores the element and severs the observer. Focus is never moved.\n */\nexport class PreviewGuardController extends Controller<HTMLElement> {\n static override values = {\n placeholder: { type: String, default: \"\" },\n mode: { type: String, default: \"hide\" },\n };\n static events = [\"hide\", \"show\"] as const;\n\n declare placeholderValue: string;\n declare modeValue: string;\n\n #observer: MutationObserver | null = null;\n #hidden = false;\n /** Saved inline visibility (hide mode), restored on show. */\n #savedVisibility = \"\";\n /** Saved text (placeholder mode); non-null marks that text — not visibility — was swapped. */\n #savedText: string | null = null;\n\n override connect(): void {\n if (typeof MutationObserver !== \"undefined\") {\n this.#observer = new MutationObserver(() => this.#sync());\n this.#observer.observe(document.documentElement, {\n attributes: true,\n attributeFilter: [\"data-turbo-preview\"],\n });\n }\n // Sync once in case we connect while a preview is already on screen.\n this.#sync();\n }\n\n override disconnect(): void {\n this.#observer?.disconnect();\n this.#observer = null;\n this.#restore(); // never leave the element guarded after teardown\n }\n\n /** Reflects the current `data-turbo-preview` state onto the element. */\n #sync(): void {\n const previewing = document.documentElement.hasAttribute(\"data-turbo-preview\");\n if (previewing && !this.#hidden) this.#hide();\n else if (!previewing && this.#hidden) this.#show();\n }\n\n #hide(): void {\n this.#hidden = true;\n if (this.modeValue === \"placeholder\") {\n this.#savedText = this.element.textContent;\n this.element.textContent = this.placeholderValue;\n } else {\n this.#savedVisibility = this.element.style.visibility;\n this.element.style.visibility = \"hidden\";\n }\n this.element.setAttribute(\"data-preview-hidden\", \"true\");\n this.dispatch(\"hide\", { detail: {} });\n }\n\n #show(): void {\n this.#restore();\n this.dispatch(\"show\", { detail: {} });\n }\n\n /** Reverts the guard. Safe to call when not hidden (no-op) — used by show and teardown. */\n #restore(): void {\n if (!this.#hidden) return;\n this.#hidden = false;\n if (this.#savedText !== null) {\n this.element.textContent = this.#savedText;\n this.#savedText = null;\n } else {\n this.element.style.visibility = this.#savedVisibility;\n }\n this.element.removeAttribute(\"data-preview-hidden\");\n }\n}\n"]}
@@ -0,0 +1,85 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ /**
4
+ * Event shape `setValue` accepts: an action param `amount` or a `detail.value`.
5
+ * Both are typed `number | string` because, while Stimulus coerces numeric action
6
+ * params to numbers, a `progress:set` CustomEvent (or a non-numeric-looking param)
7
+ * may carry a string; {@link ProgressController.setValue} normalizes either form.
8
+ */
9
+ type SetValueEvent = Event & {
10
+ params?: {
11
+ amount?: number | string;
12
+ };
13
+ detail?: {
14
+ value?: number | string;
15
+ };
16
+ };
17
+ /**
18
+ * Headless progress-bar behavior backed by the WAI-ARIA `progressbar` role.
19
+ *
20
+ * Markup contract (identifier: `stimeo--progress`):
21
+ * <div data-controller="stimeo--progress" role="progressbar"
22
+ * aria-label="Upload" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"
23
+ * data-stimeo--progress-value-value="0"
24
+ * data-action="progress:set->stimeo--progress#setValue">
25
+ * <div data-stimeo--progress-target="bar"></div>
26
+ * </div>
27
+ *
28
+ * This is the *progress over time* role (a task advancing toward completion),
29
+ * deliberately distinct from {@link MeterController}'s point-in-time `meter`. The
30
+ * controller owns value normalization and ARIA value-attribute synchronization;
31
+ * the bar's width/animation is the consumer's, driven off the
32
+ * `--stimeo-progress-ratio` (0–1) custom property and `data-state`.
33
+ *
34
+ * @remarks
35
+ * Behavior only — no styling is emitted. An indeterminate bar drops
36
+ * `aria-valuenow` (per ARIA) so assistive tech announces "busy" rather than a
37
+ * stale value; the consumer animates the indeterminate look off
38
+ * `data-state="indeterminate"`.
39
+ */
40
+ declare class ProgressController extends Controller<HTMLElement> {
41
+ #private;
42
+ static targets: string[];
43
+ static values: {
44
+ value: {
45
+ type: NumberConstructor;
46
+ default: number;
47
+ };
48
+ min: {
49
+ type: NumberConstructor;
50
+ default: number;
51
+ };
52
+ max: {
53
+ type: NumberConstructor;
54
+ default: number;
55
+ };
56
+ indeterminate: {
57
+ type: BooleanConstructor;
58
+ default: boolean;
59
+ };
60
+ valueText: {
61
+ type: StringConstructor;
62
+ default: string;
63
+ };
64
+ };
65
+ static actions: readonly ["setValue"];
66
+ static events: readonly ["change", "complete"];
67
+ readonly barTarget: HTMLElement;
68
+ readonly hasBarTarget: boolean;
69
+ valueValue: number;
70
+ minValue: number;
71
+ maxValue: number;
72
+ indeterminateValue: boolean;
73
+ valueTextValue: string;
74
+ connect(): void;
75
+ /**
76
+ * Updates the progress value from an action param (`amount`) or a
77
+ * `detail.value` CustomEvent, normalizes it into range, syncs ARIA, and
78
+ * dispatches `change` (always) plus `complete` when `max` is reached.
79
+ */
80
+ setValue(event: SetValueEvent): void;
81
+ /** Re-render when the indeterminate flag is toggled via its data attribute. */
82
+ indeterminateValueChanged(): void;
83
+ }
84
+
85
+ export { ProgressController };
@@ -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
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utils/coerce.ts","../../src/controllers/progress_controller.ts"],"names":[],"mappings":"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","file":"progress_controller.js","sourcesContent":["/**\n * Numeric coercion shared by the value-bearing status controllers (progress,\n * meter).\n *\n * Stimulus already coerces numeric action params to numbers, but a value can also\n * arrive as a string — via a `*:set` CustomEvent `detail`, or an action param\n * whose attribute does not look numeric. Centralizing the parse keeps `setValue`\n * tolerant of either form while rejecting anything that is not a finite number.\n */\n\n/**\n * Coerces `raw` to a finite number, or returns `null` when it is absent, empty,\n * or not parseable. Empty strings are treated as \"no value\" rather than `0`, so a\n * stray blank param cannot silently reset the value.\n */\nexport function toFiniteNumber(raw: number | string | null | undefined): number | null {\n if (raw === null || raw === undefined || raw === \"\") return null;\n const value = typeof raw === \"number\" ? raw : Number(raw);\n return Number.isFinite(value) ? value : null;\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { toFiniteNumber } from \"../utils/coerce\";\n\n/**\n * Event shape `setValue` accepts: an action param `amount` or a `detail.value`.\n * Both are typed `number | string` because, while Stimulus coerces numeric action\n * params to numbers, a `progress:set` CustomEvent (or a non-numeric-looking param)\n * may carry a string; {@link ProgressController.setValue} normalizes either form.\n */\ntype SetValueEvent = Event & {\n params?: { amount?: number | string };\n detail?: { value?: number | string };\n};\n\n/**\n * Headless progress-bar behavior backed by the WAI-ARIA `progressbar` role.\n *\n * Markup contract (identifier: `stimeo--progress`):\n * <div data-controller=\"stimeo--progress\" role=\"progressbar\"\n * aria-label=\"Upload\" aria-valuemin=\"0\" aria-valuemax=\"100\" aria-valuenow=\"0\"\n * data-stimeo--progress-value-value=\"0\"\n * data-action=\"progress:set->stimeo--progress#setValue\">\n * <div data-stimeo--progress-target=\"bar\"></div>\n * </div>\n *\n * This is the *progress over time* role (a task advancing toward completion),\n * deliberately distinct from {@link MeterController}'s point-in-time `meter`. The\n * controller owns value normalization and ARIA value-attribute synchronization;\n * the bar's width/animation is the consumer's, driven off the\n * `--stimeo-progress-ratio` (0–1) custom property and `data-state`.\n *\n * @remarks\n * Behavior only — no styling is emitted. An indeterminate bar drops\n * `aria-valuenow` (per ARIA) so assistive tech announces \"busy\" rather than a\n * stale value; the consumer animates the indeterminate look off\n * `data-state=\"indeterminate\"`.\n */\nexport class ProgressController extends Controller<HTMLElement> {\n static override targets = [\"bar\"];\n static override values = {\n value: { type: Number, default: 0 },\n min: { type: Number, default: 0 },\n max: { type: Number, default: 100 },\n indeterminate: { type: Boolean, default: false },\n valueText: { type: String, default: \"\" },\n };\n static actions = [\"setValue\"] as const;\n static events = [\"change\", \"complete\"] as const;\n\n declare readonly barTarget: HTMLElement;\n declare readonly hasBarTarget: boolean;\n\n declare valueValue: number;\n declare minValue: number;\n declare maxValue: number;\n declare indeterminateValue: boolean;\n declare valueTextValue: string;\n\n override connect(): void {\n this.#render();\n }\n\n /**\n * Updates the progress value from an action param (`amount`) or a\n * `detail.value` CustomEvent, normalizes it into range, syncs ARIA, and\n * dispatches `change` (always) plus `complete` when `max` is reached.\n */\n setValue(event: SetValueEvent): void {\n const next = toFiniteNumber(event.params?.amount ?? event.detail?.value);\n if (next === null) return;\n const value = this.#clamp(next);\n this.valueValue = value;\n this.indeterminateValue = false;\n this.#render();\n this.dispatch(\"change\", { detail: { value, ratio: this.#ratio } });\n if (value >= this.maxValue) {\n this.dispatch(\"complete\", { detail: { value } });\n }\n }\n\n /** Re-render when the indeterminate flag is toggled via its data attribute. */\n indeterminateValueChanged(): void {\n // Stimulus fires this once during initialization (before `connect`), where\n // touching the DOM is still safe; guard nothing.\n this.#render();\n }\n\n /** Clamps `raw` into the configured `[min, max]` range. */\n #clamp(raw: number): number {\n return Math.min(this.maxValue, Math.max(this.minValue, raw));\n }\n\n /** Current fraction of the range in `[0, 1]`; `0` when the range is empty. */\n get #ratio(): number {\n const span = this.maxValue - this.minValue;\n if (span <= 0) return 0;\n // Clamp first so an out-of-range value (e.g. an initial value past `max`)\n // can never push the ratio — and thus the bar width / percent — outside 0–1.\n return (this.#clamp(this.valueValue) - this.minValue) / span;\n }\n\n /** Reflects value/range/indeterminate onto ARIA, `data-state`, and the ratio. */\n #render(): void {\n this.element.setAttribute(\"aria-valuemin\", String(this.minValue));\n this.element.setAttribute(\"aria-valuemax\", String(this.maxValue));\n\n if (this.indeterminateValue) {\n this.element.removeAttribute(\"aria-valuenow\");\n this.element.removeAttribute(\"aria-valuetext\");\n this.element.style.removeProperty(\"--stimeo-progress-ratio\");\n this.element.setAttribute(\"data-state\", \"indeterminate\");\n return;\n }\n\n const value = this.#clamp(this.valueValue);\n this.element.setAttribute(\"aria-valuenow\", String(value));\n this.element.style.setProperty(\"--stimeo-progress-ratio\", String(this.#ratio));\n this.element.setAttribute(\"data-state\", \"determinate\");\n this.#applyValueText(value);\n }\n\n /**\n * Sets `aria-valuetext` from the consumer-provided template, substituting\n * `{value}` and `{percent}`. Left to the consumer so the human-readable text\n * stays i18n-neutral in the library; cleared when no template is given.\n */\n #applyValueText(value: number): void {\n if (this.valueTextValue.length === 0) {\n this.element.removeAttribute(\"aria-valuetext\");\n return;\n }\n const percent = Math.round(this.#ratio * 100);\n const text = this.valueTextValue\n .replaceAll(\"{value}\", String(value))\n .replaceAll(\"{percent}\", String(percent));\n this.element.setAttribute(\"aria-valuetext\", text);\n }\n}\n"]}
@@ -0,0 +1,49 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ /**
4
+ * Headless, accessible radio-group behavior for **custom** (non-native) radios.
5
+ *
6
+ * Markup contract (identifier: `stimeo--radio-group`):
7
+ * <div data-controller="stimeo--radio-group" role="radiogroup" aria-label="Plan">
8
+ * <div role="radio" aria-checked="true" tabindex="0" data-value="basic"
9
+ * data-stimeo--radio-group-target="radio"
10
+ * data-action="click->stimeo--radio-group#select
11
+ * keydown->stimeo--radio-group#onKeydown">Basic</div>
12
+ * <!-- more radios; exactly one tabindex=0 -->
13
+ * <input type="hidden" data-stimeo--radio-group-target="field" />
14
+ * </div>
15
+ *
16
+ * Implements the WAI-ARIA APG **Radio Group** pattern. Use this only for custom
17
+ * radios (e.g. button-styled cards); native `<input type="radio">` already does
18
+ * this and should be preferred when its look suffices.
19
+ *
20
+ * @remarks
21
+ * Behavior only — selection is exposed through `aria-checked`, the single Tab
22
+ * stop through roving `tabindex` ({@link RovingTabindex}); the consumer styles
23
+ * off `[role="radio"][aria-checked="true"]`. Per APG, **selection follows
24
+ * focus**: arrow keys move focus and select in one step.
25
+ *
26
+ * Behavior provided:
27
+ * - Click selects a radio.
28
+ * - `ArrowDown`/`ArrowRight` select the next, `ArrowUp`/`ArrowLeft` the previous
29
+ * (wrapping); `Home`/`End` the first/last; `Space` selects the focused radio.
30
+ * - The selected radio's `data-value` is mirrored to the optional hidden `field`,
31
+ * and `stimeo--radio-group:change` is dispatched on every change.
32
+ */
33
+ declare class RadioGroupController extends Controller<HTMLElement> {
34
+ #private;
35
+ static targets: string[];
36
+ static actions: readonly ["onKeydown", "select"];
37
+ static events: readonly ["change"];
38
+ readonly radioTargets: HTMLElement[];
39
+ readonly fieldTarget: HTMLInputElement;
40
+ readonly hasFieldTarget: boolean;
41
+ /** Establishes the roving entry point and reflects the initial selection. */
42
+ connect(): void;
43
+ /** Selects the clicked radio. Bound via `data-action` (click). */
44
+ select(event: Event): void;
45
+ /** Arrow/Home/End/Space navigation with selection-follows-focus. */
46
+ onKeydown(event: KeyboardEvent): void;
47
+ }
48
+
49
+ export { RadioGroupController };