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,113 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ // src/controllers/toolbar_controller.ts
4
+
5
+ // src/utils/roving_tabindex.ts
6
+ var RovingTabindex = class {
7
+ /** Returns the current ordered item elements; called on every operation. */
8
+ #getItems;
9
+ /**
10
+ * @param getItems - Returns the current ordered item elements. Called on every
11
+ * operation so the live target list is always used.
12
+ */
13
+ constructor(getItems) {
14
+ this.#getItems = getItems;
15
+ }
16
+ /** Index of the currently tabbable item (`tabindex="0"`), or `-1` if none. */
17
+ get activeIndex() {
18
+ return this.#getItems().findIndex((item) => item.tabIndex === 0);
19
+ }
20
+ /**
21
+ * Makes exactly the item at `index` tabbable (`tabindex="0"`) and removes every
22
+ * other item from the Tab sequence (`tabindex="-1"`). An out-of-range `index`
23
+ * (e.g. `-1`) leaves all items at `-1`, which a controller can use to express
24
+ * "nothing is currently tabbable".
25
+ *
26
+ * @param index - Position of the item to make tabbable.
27
+ * @param options - Pass `{ focus: true }` to also move DOM focus to that item.
28
+ */
29
+ setActive(index, { focus = false } = {}) {
30
+ const items = this.#getItems();
31
+ items.forEach((item, i) => {
32
+ item.tabIndex = i === index ? 0 : -1;
33
+ });
34
+ if (focus) items[index]?.focus();
35
+ }
36
+ };
37
+ function rovingMove(current, length, delta, wrap) {
38
+ if (length === 0) return -1;
39
+ const next = current + delta;
40
+ if (wrap === "wrap") return (next + length) % length;
41
+ return Math.min(length - 1, Math.max(0, next));
42
+ }
43
+
44
+ // src/controllers/toolbar_controller.ts
45
+ var ToolbarController = class extends Controller {
46
+ static targets = ["control"];
47
+ static values = {
48
+ orientation: { type: String, default: "horizontal" },
49
+ wrap: { type: Boolean, default: true }
50
+ };
51
+ static actions = ["onKeydown"];
52
+ #roving = new RovingTabindex(() => this.controlTargets);
53
+ /**
54
+ * Establishes the single tab stop: keep an existing one if it is still
55
+ * navigable, else the first navigable control. Disabled / hidden controls are
56
+ * skipped so the toolbar's lone Tab stop never lands on an unfocusable element
57
+ * (which would make the whole group unreachable on Tab re-entry).
58
+ */
59
+ connect() {
60
+ const active = this.#roving.activeIndex;
61
+ const activeEl = active === -1 ? null : this.controlTargets[active];
62
+ if (activeEl && this.#isNavigable(activeEl)) {
63
+ this.#roving.setActive(active);
64
+ return;
65
+ }
66
+ const first = this.#navigableControls[0];
67
+ this.#roving.setActive(first ? this.controlTargets.indexOf(first) : -1);
68
+ }
69
+ /** Arrow/Home/End move focus and the single tab stop. Bound via `data-action`. */
70
+ onKeydown(event) {
71
+ const navigable = this.#navigableControls;
72
+ const current = navigable.indexOf(event.currentTarget);
73
+ if (current === -1) return;
74
+ const length = navigable.length;
75
+ const wrap = this.wrapValue ? "wrap" : "clamp";
76
+ const vertical = this.orientationValue === "vertical";
77
+ const forwardKey = vertical ? "ArrowDown" : "ArrowRight";
78
+ const backwardKey = vertical ? "ArrowUp" : "ArrowLeft";
79
+ let next;
80
+ if (event.key === forwardKey) {
81
+ next = rovingMove(current, length, 1, wrap);
82
+ } else if (event.key === backwardKey) {
83
+ next = rovingMove(current, length, -1, wrap);
84
+ } else if (event.key === "Home") {
85
+ next = 0;
86
+ } else if (event.key === "End") {
87
+ next = length - 1;
88
+ } else {
89
+ return;
90
+ }
91
+ event.preventDefault();
92
+ const target = navigable[next];
93
+ if (target) this.#roving.setActive(this.controlTargets.indexOf(target), { focus: true });
94
+ }
95
+ /** Controls eligible for the roving tab stop (excludes disabled / hidden). */
96
+ get #navigableControls() {
97
+ return this.controlTargets.filter((control) => this.#isNavigable(control));
98
+ }
99
+ /**
100
+ * A control can hold the tab stop unless it is `hidden`, `aria-disabled`, or a
101
+ * natively `disabled` form control. CSS-only visibility cannot be detected
102
+ * headlessly and stays the consumer's responsibility.
103
+ */
104
+ #isNavigable(control) {
105
+ if (control.hasAttribute("hidden")) return false;
106
+ if (control.getAttribute("aria-disabled") === "true") return false;
107
+ return !control.disabled;
108
+ }
109
+ };
110
+
111
+ export { ToolbarController };
112
+ //# sourceMappingURL=toolbar_controller.js.map
113
+ //# sourceMappingURL=toolbar_controller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utils/roving_tabindex.ts","../../src/controllers/toolbar_controller.ts"],"names":[],"mappings":"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","file":"toolbar_controller.js","sourcesContent":["/**\n * Low-level roving-tabindex primitive shared by composite-widget controllers.\n *\n * The APG roving-tabindex pattern keeps a composite widget a single Tab stop:\n * exactly one item is in the Tab sequence (`tabindex=\"0\"`) while the rest are\n * removed from it (`tabindex=\"-1\"`), and the arrow keys move both DOM focus and\n * that single tabbable position together. {@link RovingTabindex} owns *only* that\n * mechanical bookkeeping — \"which one item is tabbable, and move focus there\".\n *\n * It is intentionally **policy-free**. Orientation, wrapping vs. clamping,\n * selection-follows-focus, typeahead, and `Home`/`End` semantics differ per APG\n * pattern (Radio Group, Toolbar, Rating, …); folding them into one helper would\n * flatten those widgets to a lowest common denominator and lose each pattern's\n * correctness. Those decisions therefore stay in each controller, which calls\n * {@link RovingTabindex.setActive} with an index it computed itself (optionally\n * via the pure {@link rovingMove} helper).\n *\n * @remarks\n * Items are read lazily through a getter so a controller can add or remove\n * targets (Stimulus re-scans the DOM) without re-wiring this helper.\n */\nexport class RovingTabindex {\n /** Returns the current ordered item elements; called on every operation. */\n readonly #getItems: () => HTMLElement[];\n\n /**\n * @param getItems - Returns the current ordered item elements. Called on every\n * operation so the live target list is always used.\n */\n constructor(getItems: () => HTMLElement[]) {\n this.#getItems = getItems;\n }\n\n /** Index of the currently tabbable item (`tabindex=\"0\"`), or `-1` if none. */\n get activeIndex(): number {\n return this.#getItems().findIndex((item) => item.tabIndex === 0);\n }\n\n /**\n * Makes exactly the item at `index` tabbable (`tabindex=\"0\"`) and removes every\n * other item from the Tab sequence (`tabindex=\"-1\"`). An out-of-range `index`\n * (e.g. `-1`) leaves all items at `-1`, which a controller can use to express\n * \"nothing is currently tabbable\".\n *\n * @param index - Position of the item to make tabbable.\n * @param options - Pass `{ focus: true }` to also move DOM focus to that item.\n */\n setActive(index: number, { focus = false }: { focus?: boolean } = {}): void {\n const items = this.#getItems();\n items.forEach((item, i) => {\n item.tabIndex = i === index ? 0 : -1;\n });\n if (focus) items[index]?.focus();\n }\n}\n\n/** Edge behavior for {@link rovingMove}: cycle past the ends, or stop at them. */\nexport type RovingWrap = \"wrap\" | \"clamp\";\n\n/**\n * Pure helper that resolves the target index for a one-step directional move.\n *\n * Keyboard/orientation mapping stays in the caller: it decides that a key means\n * `delta` `+1` (next) or `-1` (previous) and whether the widget should `\"wrap\"`\n * (Radio Group, Toolbar) or `\"clamp\"` at the ends.\n *\n * @param current - The index focus is moving from.\n * @param length - Number of items in the set.\n * @param delta - `+1` to move to the next item, `-1` for the previous.\n * @param wrap - `\"wrap\"` cycles around the ends; `\"clamp\"` stops at them.\n * @returns The resolved index, or `-1` when there are no items.\n */\nexport function rovingMove(\n current: number,\n length: number,\n delta: number,\n wrap: RovingWrap,\n): number {\n if (length === 0) return -1;\n const next = current + delta;\n if (wrap === \"wrap\") return (next + length) % length;\n return Math.min(length - 1, Math.max(0, next));\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { RovingTabindex, type RovingWrap, rovingMove } from \"../utils/roving_tabindex\";\n\n/**\n * Headless, accessible toolbar behavior.\n *\n * Markup contract (identifier: `stimeo--toolbar`):\n * <div data-controller=\"stimeo--toolbar\" role=\"toolbar\" aria-label=\"Text formatting\"\n * data-stimeo--toolbar-orientation-value=\"horizontal\">\n * <button type=\"button\" data-stimeo--toolbar-target=\"control\"\n * data-action=\"keydown->stimeo--toolbar#onKeydown\">Bold</button>\n * <!-- more controls; exactly one tabindex=0 -->\n * </div>\n *\n * Implements the WAI-ARIA APG **Toolbar** pattern: the group is a single Tab\n * stop (roving tabindex) and the arrow keys move focus between controls. Each\n * control's own function (press, toggle, open a menu) stays with that element or\n * its own controller — the toolbar only owns navigation.\n *\n * @remarks\n * Behavior only. The roving mechanics are delegated to {@link RovingTabindex};\n * orientation and wrap policy stay here per the APG (they differ per widget).\n *\n * Behavior provided:\n * - Exactly one control is tabbable (`tabindex=\"0\"`); the rest are `-1`.\n * - `ArrowRight`/`ArrowLeft` (horizontal) or `ArrowDown`/`ArrowUp` (vertical)\n * move focus to the next/previous control; `Home`/`End` to the first/last.\n * - With `wrap=true` movement cycles past the ends; with `wrap=false` it stops.\n * - Returning focus from outside lands on the most recently active control,\n * because that is the one left tabbable.\n */\nexport class ToolbarController extends Controller<HTMLElement> {\n static override targets = [\"control\"];\n static override values = {\n orientation: { type: String, default: \"horizontal\" },\n wrap: { type: Boolean, default: true },\n };\n static actions = [\"onKeydown\"] as const;\n\n declare readonly controlTargets: HTMLElement[];\n declare orientationValue: string;\n declare wrapValue: boolean;\n\n readonly #roving = new RovingTabindex(() => this.controlTargets);\n\n /**\n * Establishes the single tab stop: keep an existing one if it is still\n * navigable, else the first navigable control. Disabled / hidden controls are\n * skipped so the toolbar's lone Tab stop never lands on an unfocusable element\n * (which would make the whole group unreachable on Tab re-entry).\n */\n override connect(): void {\n const active = this.#roving.activeIndex;\n const activeEl = active === -1 ? null : this.controlTargets[active];\n if (activeEl && this.#isNavigable(activeEl)) {\n this.#roving.setActive(active);\n return;\n }\n const first = this.#navigableControls[0];\n this.#roving.setActive(first ? this.controlTargets.indexOf(first) : -1);\n }\n\n /** Arrow/Home/End move focus and the single tab stop. Bound via `data-action`. */\n onKeydown(event: KeyboardEvent): void {\n const navigable = this.#navigableControls;\n const current = navigable.indexOf(event.currentTarget as HTMLElement);\n if (current === -1) return;\n\n const length = navigable.length;\n const wrap: RovingWrap = this.wrapValue ? \"wrap\" : \"clamp\";\n const vertical = this.orientationValue === \"vertical\";\n const forwardKey = vertical ? \"ArrowDown\" : \"ArrowRight\";\n const backwardKey = vertical ? \"ArrowUp\" : \"ArrowLeft\";\n\n let next: number;\n if (event.key === forwardKey) {\n next = rovingMove(current, length, 1, wrap);\n } else if (event.key === backwardKey) {\n next = rovingMove(current, length, -1, wrap);\n } else if (event.key === \"Home\") {\n next = 0;\n } else if (event.key === \"End\") {\n next = length - 1;\n } else {\n return;\n }\n\n event.preventDefault();\n const target = navigable[next];\n if (target) this.#roving.setActive(this.controlTargets.indexOf(target), { focus: true });\n }\n\n /** Controls eligible for the roving tab stop (excludes disabled / hidden). */\n get #navigableControls(): HTMLElement[] {\n return this.controlTargets.filter((control) => this.#isNavigable(control));\n }\n\n /**\n * A control can hold the tab stop unless it is `hidden`, `aria-disabled`, or a\n * natively `disabled` form control. CSS-only visibility cannot be detected\n * headlessly and stays the consumer's responsibility.\n */\n #isNavigable(control: HTMLElement): boolean {\n if (control.hasAttribute(\"hidden\")) return false;\n if (control.getAttribute(\"aria-disabled\") === \"true\") return false;\n return !(control as HTMLButtonElement | HTMLInputElement).disabled;\n }\n}\n"]}
@@ -0,0 +1,74 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ /**
4
+ * Headless, accessible **tooltip** behavior.
5
+ *
6
+ * Markup contract (identifier: `stimeo--tooltip`):
7
+ * <span data-controller="stimeo--tooltip">
8
+ * <button data-stimeo--tooltip-target="trigger" aria-describedby="tip"
9
+ * data-action="mouseenter->stimeo--tooltip#show
10
+ * mouseleave->stimeo--tooltip#hide
11
+ * focusin->stimeo--tooltip#show
12
+ * focusout->stimeo--tooltip#hide
13
+ * keydown->stimeo--tooltip#onKeydown">Save</button>
14
+ * <span id="tip" role="tooltip" data-stimeo--tooltip-target="content"
15
+ * data-action="mouseenter->stimeo--tooltip#show
16
+ * mouseleave->stimeo--tooltip#hide" hidden>…</span>
17
+ * </span>
18
+ *
19
+ * Implements the WAI-ARIA APG **Tooltip** pattern and WCAG 2.2 SC 1.4.13
20
+ * (hoverable / dismissible / persistent). The tooltip never receives focus and
21
+ * holds no interactive content — for that use `stimeo--hover-card` or
22
+ * `stimeo--popover`. The `aria-describedby` association is declared in the
23
+ * consumer's markup; this controller only toggles visibility.
24
+ *
25
+ * @remarks
26
+ * Behavior only — placement is the consumer's CSS (static) or the opt-in
27
+ * `stimeo-ui/positioning` module (dynamic); this controller never imports it.
28
+ *
29
+ * Behavior provided:
30
+ * - Show on `mouseenter`/`focusin`, hide on `mouseleave`/`focusout`, each gated by
31
+ * `showDelay`/`hideDelay` to prevent flicker.
32
+ * - **Hoverable bridge**: binding show/hide on the content too means moving the
33
+ * pointer from trigger into the tooltip cancels the pending hide, so it stays up.
34
+ * - **Dismissible**: while shown, `Escape` is watched at the `document` level so it
35
+ * dismisses even when a hover (not focus) triggered it and focus is elsewhere.
36
+ * - Visibility flips `hidden` and `data-state` (`open`/`closed`); the
37
+ * `aria-describedby` reference is always preserved.
38
+ */
39
+ declare class TooltipController extends Controller<HTMLElement> {
40
+ #private;
41
+ static targets: string[];
42
+ static values: {
43
+ showDelay: {
44
+ type: NumberConstructor;
45
+ default: number;
46
+ };
47
+ hideDelay: {
48
+ type: NumberConstructor;
49
+ default: number;
50
+ };
51
+ };
52
+ static actions: readonly ["hide", "onKeydown", "show"];
53
+ readonly contentTarget: HTMLElement;
54
+ readonly hasContentTarget: boolean;
55
+ readonly showDelayValue: number;
56
+ readonly hideDelayValue: number;
57
+ /** Starts hidden. */
58
+ connect(): void;
59
+ /** Clears timers and the document `Escape` listener so nothing outlives the element. */
60
+ disconnect(): void;
61
+ /** Shows the tooltip, after `showDelay` ms (or immediately at 0). Cancels a pending hide. */
62
+ show(): void;
63
+ /** Hides the tooltip, after `hideDelay` ms (or immediately at 0). Cancels a pending show. */
64
+ hide(): void;
65
+ /**
66
+ * Dismisses the tooltip on `Escape` from the trigger. The authoritative
67
+ * dismissal path is the document-level listener (added while shown) so a
68
+ * hover-triggered tooltip is dismissible regardless of focus; this handler
69
+ * keeps the documented `keydown->#onKeydown` binding meaningful too.
70
+ */
71
+ onKeydown(event: KeyboardEvent): void;
72
+ }
73
+
74
+ export { TooltipController };
@@ -0,0 +1,165 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ // src/controllers/tooltip_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/tooltip_controller.ts
59
+ var TooltipController = class extends Controller {
60
+ static targets = ["trigger", "content"];
61
+ static values = {
62
+ showDelay: { type: Number, default: 0 },
63
+ hideDelay: { type: Number, default: 0 }
64
+ };
65
+ static actions = ["hide", "onKeydown", "show"];
66
+ /** Pending show/hide timers, torn down together on disconnect. */
67
+ #timers = new SafeTimeout();
68
+ /** The id of the currently pending show or hide timer, if any. */
69
+ #pendingShow = null;
70
+ #pendingHide = null;
71
+ /** Starts hidden. */
72
+ connect() {
73
+ this.#conceal();
74
+ }
75
+ /** Clears timers and the document `Escape` listener so nothing outlives the element. */
76
+ disconnect() {
77
+ this.#timers.clearAll();
78
+ document.removeEventListener("keydown", this.#onDocumentKeydown);
79
+ }
80
+ /** Shows the tooltip, after `showDelay` ms (or immediately at 0). Cancels a pending hide. */
81
+ show() {
82
+ this.#cancelHide();
83
+ if (this.#isVisible || this.#pendingShow !== null) return;
84
+ if (this.showDelayValue <= 0) {
85
+ this.#reveal();
86
+ return;
87
+ }
88
+ this.#pendingShow = this.#timers.set(() => {
89
+ this.#pendingShow = null;
90
+ this.#reveal();
91
+ }, this.showDelayValue);
92
+ }
93
+ /** Hides the tooltip, after `hideDelay` ms (or immediately at 0). Cancels a pending show. */
94
+ hide() {
95
+ this.#cancelShow();
96
+ if (!this.#isVisible || this.#pendingHide !== null) return;
97
+ if (this.hideDelayValue <= 0) {
98
+ this.#conceal();
99
+ return;
100
+ }
101
+ this.#pendingHide = this.#timers.set(() => {
102
+ this.#pendingHide = null;
103
+ this.#conceal();
104
+ }, this.hideDelayValue);
105
+ }
106
+ /**
107
+ * Dismisses the tooltip on `Escape` from the trigger. The authoritative
108
+ * dismissal path is the document-level listener (added while shown) so a
109
+ * hover-triggered tooltip is dismissible regardless of focus; this handler
110
+ * keeps the documented `keydown->#onKeydown` binding meaningful too.
111
+ */
112
+ onKeydown(event) {
113
+ if (event.key === "Escape" && this.#isVisible) {
114
+ event.preventDefault();
115
+ this.#cancelShow();
116
+ this.#cancelHide();
117
+ this.#conceal();
118
+ }
119
+ }
120
+ /** Reveals the content and starts watching for a dismissing `Escape`. */
121
+ #reveal() {
122
+ if (!this.hasContentTarget) return;
123
+ this.contentTarget.hidden = false;
124
+ this.contentTarget.setAttribute("data-state", "open");
125
+ document.addEventListener("keydown", this.#onDocumentKeydown);
126
+ }
127
+ /** Hides the content and stops watching for `Escape`. */
128
+ #conceal() {
129
+ if (!this.hasContentTarget) return;
130
+ this.contentTarget.hidden = true;
131
+ this.contentTarget.setAttribute("data-state", "closed");
132
+ document.removeEventListener("keydown", this.#onDocumentKeydown);
133
+ }
134
+ /** Document-level `Escape` watcher (active only while shown). */
135
+ #onDocumentKeydown = (event) => {
136
+ if (event.key === "Escape") {
137
+ event.preventDefault();
138
+ this.#cancelShow();
139
+ this.#cancelHide();
140
+ this.#conceal();
141
+ }
142
+ };
143
+ /** Cancels any pending show timer. */
144
+ #cancelShow() {
145
+ if (this.#pendingShow !== null) {
146
+ this.#timers.clear(this.#pendingShow);
147
+ this.#pendingShow = null;
148
+ }
149
+ }
150
+ /** Cancels any pending hide timer. */
151
+ #cancelHide() {
152
+ if (this.#pendingHide !== null) {
153
+ this.#timers.clear(this.#pendingHide);
154
+ this.#pendingHide = null;
155
+ }
156
+ }
157
+ /** Whether the tooltip is currently shown. */
158
+ get #isVisible() {
159
+ return this.hasContentTarget && !this.contentTarget.hidden;
160
+ }
161
+ };
162
+
163
+ export { TooltipController };
164
+ //# sourceMappingURL=tooltip_controller.js.map
165
+ //# sourceMappingURL=tooltip_controller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utils/safe_timeout.ts","../../src/controllers/tooltip_controller.ts"],"names":[],"mappings":"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","file":"tooltip_controller.js","sourcesContent":["/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/**\n * Headless, accessible **tooltip** behavior.\n *\n * Markup contract (identifier: `stimeo--tooltip`):\n * <span data-controller=\"stimeo--tooltip\">\n * <button data-stimeo--tooltip-target=\"trigger\" aria-describedby=\"tip\"\n * data-action=\"mouseenter->stimeo--tooltip#show\n * mouseleave->stimeo--tooltip#hide\n * focusin->stimeo--tooltip#show\n * focusout->stimeo--tooltip#hide\n * keydown->stimeo--tooltip#onKeydown\">Save</button>\n * <span id=\"tip\" role=\"tooltip\" data-stimeo--tooltip-target=\"content\"\n * data-action=\"mouseenter->stimeo--tooltip#show\n * mouseleave->stimeo--tooltip#hide\" hidden>…</span>\n * </span>\n *\n * Implements the WAI-ARIA APG **Tooltip** pattern and WCAG 2.2 SC 1.4.13\n * (hoverable / dismissible / persistent). The tooltip never receives focus and\n * holds no interactive content — for that use `stimeo--hover-card` or\n * `stimeo--popover`. The `aria-describedby` association is declared in the\n * consumer's markup; this controller only toggles visibility.\n *\n * @remarks\n * Behavior only — placement is the consumer's CSS (static) or the opt-in\n * `stimeo-ui/positioning` module (dynamic); this controller never imports it.\n *\n * Behavior provided:\n * - Show on `mouseenter`/`focusin`, hide on `mouseleave`/`focusout`, each gated by\n * `showDelay`/`hideDelay` to prevent flicker.\n * - **Hoverable bridge**: binding show/hide on the content too means moving the\n * pointer from trigger into the tooltip cancels the pending hide, so it stays up.\n * - **Dismissible**: while shown, `Escape` is watched at the `document` level so it\n * dismisses even when a hover (not focus) triggered it and focus is elsewhere.\n * - Visibility flips `hidden` and `data-state` (`open`/`closed`); the\n * `aria-describedby` reference is always preserved.\n */\nexport class TooltipController extends Controller<HTMLElement> {\n static override targets = [\"trigger\", \"content\"];\n static override values = {\n showDelay: { type: Number, default: 0 },\n hideDelay: { type: Number, default: 0 },\n };\n static actions = [\"hide\", \"onKeydown\", \"show\"] as const;\n\n declare readonly contentTarget: HTMLElement;\n declare readonly hasContentTarget: boolean;\n declare readonly showDelayValue: number;\n declare readonly hideDelayValue: number;\n\n /** Pending show/hide timers, torn down together on disconnect. */\n readonly #timers = new SafeTimeout();\n /** The id of the currently pending show or hide timer, if any. */\n #pendingShow: number | null = null;\n #pendingHide: number | null = null;\n\n /** Starts hidden. */\n override connect(): void {\n this.#conceal();\n }\n\n /** Clears timers and the document `Escape` listener so nothing outlives the element. */\n override disconnect(): void {\n this.#timers.clearAll();\n document.removeEventListener(\"keydown\", this.#onDocumentKeydown);\n }\n\n /** Shows the tooltip, after `showDelay` ms (or immediately at 0). Cancels a pending hide. */\n show(): void {\n this.#cancelHide();\n if (this.#isVisible || this.#pendingShow !== null) return;\n if (this.showDelayValue <= 0) {\n this.#reveal();\n return;\n }\n this.#pendingShow = this.#timers.set(() => {\n this.#pendingShow = null;\n this.#reveal();\n }, this.showDelayValue);\n }\n\n /** Hides the tooltip, after `hideDelay` ms (or immediately at 0). Cancels a pending show. */\n hide(): void {\n this.#cancelShow();\n if (!this.#isVisible || this.#pendingHide !== null) return;\n if (this.hideDelayValue <= 0) {\n this.#conceal();\n return;\n }\n this.#pendingHide = this.#timers.set(() => {\n this.#pendingHide = null;\n this.#conceal();\n }, this.hideDelayValue);\n }\n\n /**\n * Dismisses the tooltip on `Escape` from the trigger. The authoritative\n * dismissal path is the document-level listener (added while shown) so a\n * hover-triggered tooltip is dismissible regardless of focus; this handler\n * keeps the documented `keydown->#onKeydown` binding meaningful too.\n */\n onKeydown(event: KeyboardEvent): void {\n if (event.key === \"Escape\" && this.#isVisible) {\n event.preventDefault();\n this.#cancelShow();\n this.#cancelHide();\n this.#conceal();\n }\n }\n\n /** Reveals the content and starts watching for a dismissing `Escape`. */\n #reveal(): void {\n if (!this.hasContentTarget) return;\n this.contentTarget.hidden = false;\n this.contentTarget.setAttribute(\"data-state\", \"open\");\n document.addEventListener(\"keydown\", this.#onDocumentKeydown);\n }\n\n /** Hides the content and stops watching for `Escape`. */\n #conceal(): void {\n if (!this.hasContentTarget) return;\n this.contentTarget.hidden = true;\n this.contentTarget.setAttribute(\"data-state\", \"closed\");\n document.removeEventListener(\"keydown\", this.#onDocumentKeydown);\n }\n\n /** Document-level `Escape` watcher (active only while shown). */\n readonly #onDocumentKeydown = (event: KeyboardEvent): void => {\n if (event.key === \"Escape\") {\n event.preventDefault();\n this.#cancelShow();\n this.#cancelHide();\n this.#conceal();\n }\n };\n\n /** Cancels any pending show timer. */\n #cancelShow(): void {\n if (this.#pendingShow !== null) {\n this.#timers.clear(this.#pendingShow);\n this.#pendingShow = null;\n }\n }\n\n /** Cancels any pending hide timer. */\n #cancelHide(): void {\n if (this.#pendingHide !== null) {\n this.#timers.clear(this.#pendingHide);\n this.#pendingHide = null;\n }\n }\n\n /** Whether the tooltip is currently shown. */\n get #isVisible(): boolean {\n return this.hasContentTarget && !this.contentTarget.hidden;\n }\n}\n"]}
@@ -0,0 +1,86 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ /**
4
+ * Headless **enter/leave transition base**: stages CSS classes for showing and hiding
5
+ * an element (the shared substrate other widgets can lean on instead of hand-rolling
6
+ * it). Counterpart to Headless UI `Transition` / Alpine `x-transition` (no APG pattern;
7
+ * honors WCAG 2.2 **2.3.3** via `prefers-reduced-motion` and keeps the visual state in
8
+ * sync with `hidden`).
9
+ *
10
+ * Markup contract (identifier: `stimeo--transition`):
11
+ * <div data-controller="stimeo--transition"
12
+ * data-stimeo--transition-enter-value="ease-out duration-200"
13
+ * data-stimeo--transition-enter-from-value="opacity-0"
14
+ * data-stimeo--transition-enter-to-value="opacity-100"
15
+ * data-stimeo--transition-leave-value="ease-in duration-150"
16
+ * data-stimeo--transition-leave-from-value="opacity-100"
17
+ * data-stimeo--transition-leave-to-value="opacity-0" hidden>…</div>
18
+ *
19
+ * `enter()` unhides the element, applies `enter` + `enterFrom`, then on the next frame
20
+ * swaps `enterFrom` → `enterTo` so the CSS transition runs, and on `transitionend` (or
21
+ * a safety timeout) settles to `entered`. `leave()` mirrors it and re-applies `hidden`.
22
+ * `toggle()` reverses the current direction. The element carries `data-transition-state`
23
+ * (`entering` / `entered` / `leaving` / `left`) and `entered` / `left` events fire on
24
+ * completion.
25
+ *
26
+ * @remarks
27
+ * Behavior only — the animation itself is the consumer's CSS; this controls *when* the
28
+ * stage classes are applied. Under `prefers-reduced-motion: reduce` it switches
29
+ * instantly (no staging). An interrupting call cancels the in-flight transition and
30
+ * starts the new one. State lives solely in `hidden` / `data-transition-state`, and
31
+ * `connect()` reconciles to a stable state (stripping any half-applied stage classes
32
+ * from a Turbo cache); the `transitionend` listener, rAF, and safety timer are released
33
+ * on `disconnect()` (Turbo navigation included).
34
+ */
35
+ declare class TransitionController extends Controller<HTMLElement> {
36
+ #private;
37
+ static values: {
38
+ enter: {
39
+ type: StringConstructor;
40
+ default: string;
41
+ };
42
+ enterFrom: {
43
+ type: StringConstructor;
44
+ default: string;
45
+ };
46
+ enterTo: {
47
+ type: StringConstructor;
48
+ default: string;
49
+ };
50
+ leave: {
51
+ type: StringConstructor;
52
+ default: string;
53
+ };
54
+ leaveFrom: {
55
+ type: StringConstructor;
56
+ default: string;
57
+ };
58
+ leaveTo: {
59
+ type: StringConstructor;
60
+ default: string;
61
+ };
62
+ timeout: {
63
+ type: NumberConstructor;
64
+ default: number;
65
+ };
66
+ };
67
+ static actions: readonly ["enter", "leave", "toggle"];
68
+ static events: readonly ["entered", "left"];
69
+ enterValue: string;
70
+ enterFromValue: string;
71
+ enterToValue: string;
72
+ leaveValue: string;
73
+ leaveFromValue: string;
74
+ leaveToValue: string;
75
+ timeoutValue: number;
76
+ connect(): void;
77
+ disconnect(): void;
78
+ /** Shows the element with the enter transition. */
79
+ enter(): void;
80
+ /** Hides the element with the leave transition. */
81
+ leave(): void;
82
+ /** Reverses the current direction (enter when hidden/leaving, else leave). */
83
+ toggle(): void;
84
+ }
85
+
86
+ export { TransitionController };