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,226 @@
1
+ import { Placement } from '@floating-ui/dom';
2
+ export { Placement } from '@floating-ui/dom';
3
+ import { Controller, Application } from '@hotwired/stimulus';
4
+
5
+ /**
6
+ * Headless **anchored positioning**: keeps a `floating` element placed against an
7
+ * `anchor`, flipping/shifting it away from viewport edges as the page scrolls or
8
+ * resizes. It is the declarative surface of the opt-in {@link attachPositioning}
9
+ * engine (`@floating-ui/dom`-based) — Radix's anchored popper / floating-ui's
10
+ * `autoUpdate`, exposed as a controller. No dedicated APG pattern; it is the
11
+ * placement primitive the popup patterns (Tooltip / Menu / Popover …) build on.
12
+ *
13
+ * Markup contract (identifier: `stimeo--anchored`):
14
+ * <div data-controller="stimeo--anchored"
15
+ * data-stimeo--anchored-placement-value="bottom-start"
16
+ * data-stimeo--anchored-offset-value="8">
17
+ * <button data-stimeo--anchored-target="anchor">Open</button>
18
+ * <div data-stimeo--anchored-target="floating" role="…">…</div>
19
+ * </div>
20
+ *
21
+ * `active` drives tracking (start/stop) and fires on connect, mirroring Focus
22
+ * Scope's `trapValueChanged`; set it `false` while the floating element is hidden
23
+ * so no measurement runs. The other Values map to {@link PositioningOptions} and
24
+ * re-apply live while tracking. Only `position`/`left`/`top` inline styles are
25
+ * written — never decoration — and the resolved (post-flip) side is mirrored onto
26
+ * `data-anchored-placement` on the floating element for CSS hooks (e.g. an arrow).
27
+ *
28
+ * @remarks
29
+ * Behavior only. It does **not** open/close, manage focus, or render an overlay
30
+ * (pair with Dialog / Popover and {@link "../controllers/focus_controller"}), and
31
+ * it does **not** move DOM (pair with Portal). It lives in the opt-in
32
+ * `stimeo-ui/positioning` subpath so the core `import "stimeo-ui"` stays
33
+ * zero-dependency; only consumers who register it pull in `@floating-ui/dom`. The
34
+ * `autoUpdate` cleanup is released on `disconnect()` (Turbo navigation included)
35
+ * so no observer outlives the element, and `#sync` reconciles to a single live
36
+ * observer (keyed on the applied options) so reconnects never stack observers.
37
+ */
38
+ declare class AnchoredController extends Controller<HTMLElement> {
39
+ #private;
40
+ static targets: string[];
41
+ static values: {
42
+ placement: {
43
+ type: StringConstructor;
44
+ default: string;
45
+ };
46
+ offset: {
47
+ type: NumberConstructor;
48
+ default: number;
49
+ };
50
+ flip: {
51
+ type: BooleanConstructor;
52
+ default: boolean;
53
+ };
54
+ shift: {
55
+ type: BooleanConstructor;
56
+ default: boolean;
57
+ };
58
+ padding: {
59
+ type: NumberConstructor;
60
+ default: number;
61
+ };
62
+ strategy: {
63
+ type: StringConstructor;
64
+ default: string;
65
+ };
66
+ active: {
67
+ type: BooleanConstructor;
68
+ default: boolean;
69
+ };
70
+ };
71
+ static events: readonly ["position"];
72
+ readonly anchorTarget: HTMLElement;
73
+ readonly floatingTarget: HTMLElement;
74
+ readonly hasAnchorTarget: boolean;
75
+ readonly hasFloatingTarget: boolean;
76
+ placementValue: string;
77
+ offsetValue: number;
78
+ flipValue: boolean;
79
+ shiftValue: boolean;
80
+ paddingValue: number;
81
+ strategyValue: string;
82
+ activeValue: boolean;
83
+ connect(): void;
84
+ disconnect(): void;
85
+ activeValueChanged(): void;
86
+ placementValueChanged(): void;
87
+ offsetValueChanged(): void;
88
+ flipValueChanged(): void;
89
+ shiftValueChanged(): void;
90
+ paddingValueChanged(): void;
91
+ strategyValueChanged(): void;
92
+ }
93
+
94
+ /**
95
+ * Opt-in shared positioning helper for Stimeo's floating components
96
+ * (popover / tooltip / hover-card / context-menu).
97
+ *
98
+ * **Why this is a separate entry point.** The core library is zero-runtime-dep:
99
+ * `import "stimeo-ui"` pulls in nothing but `@hotwired/stimulus`. Dynamic
100
+ * placement — measuring the viewport / scroll parents to flip and shift a
101
+ * floating element away from screen edges — genuinely needs a small, trustworthy
102
+ * dependency (`@floating-ui/dom`). To keep that cost *opt-in*, this lives at
103
+ * `stimeo-ui/positioning` and is loaded only when a consumer explicitly imports
104
+ * it. The controllers themselves never import this module, so the core install
105
+ * stays dependency-free.
106
+ *
107
+ * **What it does and does not own.** This helper writes **coordinates only** —
108
+ * `position`, `left`, `top` inline
109
+ * styles on the floating element. It never emits color, border, shadow, size, or
110
+ * any other decoration: the consumer's CSS still owns the entire look. Static
111
+ * placement (a fixed `top`/`left` in CSS) needs no JS at all; reach for this only
112
+ * when you want edge-collision avoidance.
113
+ */
114
+ /** Options accepted by {@link position} and {@link attachPositioning}. */
115
+ interface PositioningOptions {
116
+ /**
117
+ * Preferred side of the anchor to place the floating element on. Mirrors
118
+ * floating-ui's `Placement` (e.g. `"bottom"`, `"top-start"`). Default `"bottom"`.
119
+ */
120
+ placement?: Placement;
121
+ /** Gap in pixels between the anchor and the floating element. Default `0`. */
122
+ offset?: number;
123
+ /**
124
+ * Flip to the opposite side when the preferred side would overflow the
125
+ * viewport. Default `true`.
126
+ */
127
+ flip?: boolean;
128
+ /**
129
+ * Shift the floating element along its axis to keep it in view. Default `true`.
130
+ */
131
+ shift?: boolean;
132
+ /**
133
+ * Padding (px) kept between the floating element and the viewport edge when
134
+ * flipping/shifting. Default `0`.
135
+ */
136
+ padding?: number;
137
+ /**
138
+ * CSS positioning strategy written to the floating element. `"absolute"`
139
+ * (default) positions against the nearest positioned ancestor; `"fixed"`
140
+ * positions against the viewport (useful inside `overflow` containers).
141
+ */
142
+ strategy?: "absolute" | "fixed";
143
+ }
144
+ /**
145
+ * The resolved outcome of one positioning pass: the coordinates written to the
146
+ * floating element and the **final** placement after flip/shift. Returned by
147
+ * {@link position} and surfaced per update through {@link attachPositioning}'s
148
+ * `onComputed` callback so callers can react to the resolved side (e.g. flip an
149
+ * arrow, mirror the placement onto a `data-*` hook) without re-measuring.
150
+ */
151
+ interface PositionResult {
152
+ /** Final placement after flip/shift resolved it (e.g. `"top-start"`). */
153
+ placement: Placement;
154
+ /** X coordinate written as the floating element's inline `left`. */
155
+ x: number;
156
+ /** Y coordinate written as the floating element's inline `top`. */
157
+ y: number;
158
+ }
159
+ /**
160
+ * Computes a single placement for `floating` relative to `anchor` and writes the
161
+ * resulting coordinates as inline styles on `floating`.
162
+ *
163
+ * This is the one-shot form: it positions once and returns. For a floating
164
+ * element that must track scrolling/resizing while open, use
165
+ * {@link attachPositioning} instead.
166
+ *
167
+ * Only `position`, `left`, and `top` are written — never any decoration. The
168
+ * resolved {@link PositionResult} (final placement + coordinates) is returned so
169
+ * callers can react to the side flip/shift chose.
170
+ */
171
+ declare function position(anchor: Element, floating: HTMLElement, options?: PositioningOptions): Promise<PositionResult>;
172
+ /**
173
+ * Positions `floating` against `anchor` and keeps it positioned across scroll,
174
+ * resize, and layout changes via floating-ui's `autoUpdate`.
175
+ *
176
+ * Returns a cleanup function that stops tracking; call it when the floating
177
+ * element closes or the controller disconnects (Turbo navigation included) so no
178
+ * observer outlives the element.
179
+ *
180
+ * Pass `onComputed` to receive the resolved {@link PositionResult} on every
181
+ * update (initial placement and each scroll/resize re-computation) — used to
182
+ * mirror the final placement onto a hook or emit an event without re-measuring.
183
+ *
184
+ * @example
185
+ * ```ts
186
+ * import { attachPositioning } from "stimeo-ui/positioning";
187
+ *
188
+ * // when a popover opens:
189
+ * const stop = attachPositioning(trigger, panel, { placement: "bottom-start", offset: 8 });
190
+ * // when it closes:
191
+ * stop();
192
+ * ```
193
+ */
194
+ declare function attachPositioning(anchor: Element, floating: HTMLElement, options?: PositioningOptions, onComputed?: (result: PositionResult) => void): () => void;
195
+ /**
196
+ * Maps the opt-in positioning controller identifiers to their classes. Kept
197
+ * separate from the core `stimeoControllers` (`src/index.ts`) so the core
198
+ * install never imports `@floating-ui/dom`; the Inspector manifest reflects both
199
+ * core and positioning controllers so `stimeo check` recognizes them.
200
+ */
201
+ declare const positioningControllers: {
202
+ readonly "stimeo--anchored": typeof AnchoredController;
203
+ };
204
+ /**
205
+ * Registers the opt-in positioning controllers (e.g. `stimeo--anchored`) on a
206
+ * Stimulus application. Call this **in addition to** `registerStimeo` only when
207
+ * you want the declarative positioning primitives — importing this module is
208
+ * what pulls in `@floating-ui/dom`, so the core stays zero-dependency for
209
+ * consumers who never call it.
210
+ *
211
+ * @param application - The Stimulus application to register controllers on.
212
+ *
213
+ * @example
214
+ * ```ts
215
+ * import { Application } from "@hotwired/stimulus";
216
+ * import { registerStimeo } from "stimeo-ui";
217
+ * import { registerPositioning } from "stimeo-ui/positioning";
218
+ *
219
+ * const application = Application.start();
220
+ * registerStimeo(application);
221
+ * registerPositioning(application); // opt-in: adds stimeo--anchored
222
+ * ```
223
+ */
224
+ declare function registerPositioning(application: Application): void;
225
+
226
+ export { AnchoredController, type PositionResult, type PositioningOptions, attachPositioning, position, positioningControllers, registerPositioning };
@@ -0,0 +1,145 @@
1
+ import { computePosition, autoUpdate, offset, flip, shift } from '@floating-ui/dom';
2
+ import { Controller } from '@hotwired/stimulus';
3
+
4
+ // src/positioning/index.ts
5
+ var AnchoredController = class extends Controller {
6
+ static targets = ["anchor", "floating"];
7
+ static values = {
8
+ placement: { type: String, default: "bottom" },
9
+ offset: { type: Number, default: 0 },
10
+ flip: { type: Boolean, default: true },
11
+ shift: { type: Boolean, default: true },
12
+ padding: { type: Number, default: 0 },
13
+ strategy: { type: String, default: "absolute" },
14
+ active: { type: Boolean, default: true }
15
+ };
16
+ static events = ["position"];
17
+ /** `autoUpdate` cleanup while tracking; `null` when detached. */
18
+ #stop = null;
19
+ /** True between connect and disconnect (Stimulus may fire value callbacks before connect). */
20
+ #connected = false;
21
+ /** Serialized options of the live observer, or `null` when detached — see {@link #sync}. */
22
+ #appliedKey = null;
23
+ connect() {
24
+ this.#connected = true;
25
+ this.#sync();
26
+ }
27
+ disconnect() {
28
+ this.#connected = false;
29
+ this.#sync();
30
+ }
31
+ // Every value change (active or an option) re-syncs. `#sync` is idempotent and
32
+ // order-independent, so no Value has to be declared in a particular position.
33
+ activeValueChanged() {
34
+ this.#sync();
35
+ }
36
+ placementValueChanged() {
37
+ this.#sync();
38
+ }
39
+ offsetValueChanged() {
40
+ this.#sync();
41
+ }
42
+ flipValueChanged() {
43
+ this.#sync();
44
+ }
45
+ shiftValueChanged() {
46
+ this.#sync();
47
+ }
48
+ paddingValueChanged() {
49
+ this.#sync();
50
+ }
51
+ strategyValueChanged() {
52
+ this.#sync();
53
+ }
54
+ /** Current Values mapped to the positioning engine's options. */
55
+ get #options() {
56
+ return {
57
+ placement: this.placementValue,
58
+ offset: this.offsetValue,
59
+ flip: this.flipValue,
60
+ shift: this.shiftValue,
61
+ padding: this.paddingValue,
62
+ // Narrow the free-form Value to the engine's union; anything but "fixed"
63
+ // falls back to the default "absolute".
64
+ strategy: this.strategyValue === "fixed" ? "fixed" : "absolute"
65
+ };
66
+ }
67
+ /**
68
+ * Reconciles the live observer with the desired state — track iff connected,
69
+ * `active`, and both targets exist — re-attaching only when that state or the
70
+ * options actually changed. Stimulus fires the value-changed callbacks on
71
+ * connect in declaration order and may run them before or after `connect()`;
72
+ * keying on the applied options collapses that whole burst (in any order) to a
73
+ * single attach, while an option change at runtime re-attaches exactly once. So
74
+ * correctness never depends on `active` being declared last.
75
+ */
76
+ #sync() {
77
+ const shouldTrack = this.#connected && this.activeValue && this.hasAnchorTarget && this.hasFloatingTarget;
78
+ const key = shouldTrack ? JSON.stringify(this.#options) : null;
79
+ if (key === this.#appliedKey) return;
80
+ this.#detach();
81
+ this.#appliedKey = key;
82
+ if (shouldTrack) this.#attach();
83
+ }
84
+ #attach() {
85
+ this.#stop = attachPositioning(
86
+ this.anchorTarget,
87
+ this.floatingTarget,
88
+ this.#options,
89
+ (result) => this.#onComputed(result)
90
+ );
91
+ }
92
+ #detach() {
93
+ this.#stop?.();
94
+ this.#stop = null;
95
+ }
96
+ /** Reflects the resolved side onto the CSS hook and announces the placement. */
97
+ #onComputed(result) {
98
+ this.floatingTarget.setAttribute("data-anchored-placement", result.placement);
99
+ this.dispatch("position", {
100
+ detail: { placement: result.placement, x: result.x, y: result.y }
101
+ });
102
+ }
103
+ };
104
+
105
+ // src/positioning/index.ts
106
+ function buildMiddleware(options) {
107
+ const padding = options.padding ?? 0;
108
+ const middleware = [];
109
+ if (options.offset) middleware.push(offset(options.offset));
110
+ if (options.flip !== false) middleware.push(flip({ padding }));
111
+ if (options.shift !== false) middleware.push(shift({ padding }));
112
+ return middleware;
113
+ }
114
+ async function position(anchor, floating, options = {}) {
115
+ const strategy = options.strategy ?? "absolute";
116
+ const config = {
117
+ placement: options.placement ?? "bottom",
118
+ middleware: buildMiddleware(options),
119
+ strategy
120
+ };
121
+ const { x, y, placement } = await computePosition(anchor, floating, config);
122
+ Object.assign(floating.style, {
123
+ position: strategy,
124
+ left: `${x}px`,
125
+ top: `${y}px`
126
+ });
127
+ return { x, y, placement };
128
+ }
129
+ function attachPositioning(anchor, floating, options = {}, onComputed) {
130
+ return autoUpdate(anchor, floating, () => {
131
+ void position(anchor, floating, options).then((result) => onComputed?.(result));
132
+ });
133
+ }
134
+ var positioningControllers = {
135
+ "stimeo--anchored": AnchoredController
136
+ };
137
+ function registerPositioning(application) {
138
+ for (const [identifier, controller] of Object.entries(positioningControllers)) {
139
+ application.register(identifier, controller);
140
+ }
141
+ }
142
+
143
+ export { AnchoredController, attachPositioning, position, positioningControllers, registerPositioning };
144
+ //# sourceMappingURL=index.js.map
145
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/positioning/anchored_controller.ts","../../src/positioning/index.ts"],"names":[],"mappings":";;;;AAoCO,IAAM,kBAAA,GAAN,cAAiC,UAAA,CAAwB;AAAA,EAC9D,OAAgB,OAAA,GAAU,CAAC,QAAA,EAAU,UAAU,CAAA;AAAA,EAC/C,OAAgB,MAAA,GAAS;AAAA,IACvB,SAAA,EAAW,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,QAAA,EAAS;AAAA,IAC7C,MAAA,EAAQ,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,CAAA,EAAE;AAAA,IACnC,IAAA,EAAM,EAAE,IAAA,EAAM,OAAA,EAAS,SAAS,IAAA,EAAK;AAAA,IACrC,KAAA,EAAO,EAAE,IAAA,EAAM,OAAA,EAAS,SAAS,IAAA,EAAK;AAAA,IACtC,OAAA,EAAS,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,CAAA,EAAE;AAAA,IACpC,QAAA,EAAU,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,UAAA,EAAW;AAAA,IAC9C,MAAA,EAAQ,EAAE,IAAA,EAAM,OAAA,EAAS,SAAS,IAAA;AAAK,GACzC;AAAA,EACA,OAAO,MAAA,GAAS,CAAC,UAAU,CAAA;AAAA;AAAA,EAgB3B,KAAA,GAA6B,IAAA;AAAA;AAAA,EAE7B,UAAA,GAAa,KAAA;AAAA;AAAA,EAEb,WAAA,GAA6B,IAAA;AAAA,EAEpB,OAAA,GAAgB;AACvB,IAAA,IAAA,CAAK,UAAA,GAAa,IAAA;AAClB,IAAA,IAAA,CAAK,KAAA,EAAM;AAAA,EACb;AAAA,EAES,UAAA,GAAmB;AAC1B,IAAA,IAAA,CAAK,UAAA,GAAa,KAAA;AAClB,IAAA,IAAA,CAAK,KAAA,EAAM;AAAA,EACb;AAAA;AAAA;AAAA,EAIA,kBAAA,GAA2B;AACzB,IAAA,IAAA,CAAK,KAAA,EAAM;AAAA,EACb;AAAA,EACA,qBAAA,GAA8B;AAC5B,IAAA,IAAA,CAAK,KAAA,EAAM;AAAA,EACb;AAAA,EACA,kBAAA,GAA2B;AACzB,IAAA,IAAA,CAAK,KAAA,EAAM;AAAA,EACb;AAAA,EACA,gBAAA,GAAyB;AACvB,IAAA,IAAA,CAAK,KAAA,EAAM;AAAA,EACb;AAAA,EACA,iBAAA,GAA0B;AACxB,IAAA,IAAA,CAAK,KAAA,EAAM;AAAA,EACb;AAAA,EACA,mBAAA,GAA4B;AAC1B,IAAA,IAAA,CAAK,KAAA,EAAM;AAAA,EACb;AAAA,EACA,oBAAA,GAA6B;AAC3B,IAAA,IAAA,CAAK,KAAA,EAAM;AAAA,EACb;AAAA;AAAA,EAGA,IAAI,QAAA,GAA+B;AACjC,IAAA,OAAO;AAAA,MACL,WAAW,IAAA,CAAK,cAAA;AAAA,MAChB,QAAQ,IAAA,CAAK,WAAA;AAAA,MACb,MAAM,IAAA,CAAK,SAAA;AAAA,MACX,OAAO,IAAA,CAAK,UAAA;AAAA,MACZ,SAAS,IAAA,CAAK,YAAA;AAAA;AAAA;AAAA,MAGd,QAAA,EAAU,IAAA,CAAK,aAAA,KAAkB,OAAA,GAAU,OAAA,GAAU;AAAA,KACvD;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,KAAA,GAAc;AACZ,IAAA,MAAM,cACJ,IAAA,CAAK,UAAA,IAAc,KAAK,WAAA,IAAe,IAAA,CAAK,mBAAmB,IAAA,CAAK,iBAAA;AACtE,IAAA,MAAM,MAAM,WAAA,GAAc,IAAA,CAAK,SAAA,CAAU,IAAA,CAAK,QAAQ,CAAA,GAAI,IAAA;AAC1D,IAAA,IAAI,GAAA,KAAQ,KAAK,WAAA,EAAa;AAC9B,IAAA,IAAA,CAAK,OAAA,EAAQ;AACb,IAAA,IAAA,CAAK,WAAA,GAAc,GAAA;AACnB,IAAA,IAAI,WAAA,OAAkB,OAAA,EAAQ;AAAA,EAChC;AAAA,EAEA,OAAA,GAAgB;AACd,IAAA,IAAA,CAAK,KAAA,GAAQ,iBAAA;AAAA,MACX,IAAA,CAAK,YAAA;AAAA,MACL,IAAA,CAAK,cAAA;AAAA,MACL,IAAA,CAAK,QAAA;AAAA,MACL,CAAC,MAAA,KAAW,IAAA,CAAK,WAAA,CAAY,MAAM;AAAA,KACrC;AAAA,EACF;AAAA,EAEA,OAAA,GAAgB;AACd,IAAA,IAAA,CAAK,KAAA,IAAQ;AACb,IAAA,IAAA,CAAK,KAAA,GAAQ,IAAA;AAAA,EACf;AAAA;AAAA,EAGA,YAAY,MAAA,EAA8B;AACxC,IAAA,IAAA,CAAK,cAAA,CAAe,YAAA,CAAa,yBAAA,EAA2B,MAAA,CAAO,SAAS,CAAA;AAC5E,IAAA,IAAA,CAAK,SAAS,UAAA,EAAY;AAAA,MACxB,MAAA,EAAQ,EAAE,SAAA,EAAW,MAAA,CAAO,SAAA,EAAW,GAAG,MAAA,CAAO,CAAA,EAAG,CAAA,EAAG,MAAA,CAAO,CAAA;AAAE,KACjE,CAAA;AAAA,EACH;AACF;;;ACxEA,SAAS,gBAAgB,OAAA,EAA2C;AAClE,EAAA,MAAM,OAAA,GAAU,QAAQ,OAAA,IAAW,CAAA;AACnC,EAAA,MAAM,aAA2B,EAAC;AAClC,EAAA,IAAI,QAAQ,MAAA,EAAQ,UAAA,CAAW,KAAK,MAAA,CAAO,OAAA,CAAQ,MAAM,CAAC,CAAA;AAE1D,EAAA,IAAI,OAAA,CAAQ,SAAS,KAAA,EAAO,UAAA,CAAW,KAAK,IAAA,CAAK,EAAE,OAAA,EAAS,CAAC,CAAA;AAC7D,EAAA,IAAI,OAAA,CAAQ,UAAU,KAAA,EAAO,UAAA,CAAW,KAAK,KAAA,CAAM,EAAE,OAAA,EAAS,CAAC,CAAA;AAC/D,EAAA,OAAO,UAAA;AACT;AAcA,eAAsB,QAAA,CACpB,MAAA,EACA,QAAA,EACA,OAAA,GAA8B,EAAC,EACN;AACzB,EAAA,MAAM,QAAA,GAAW,QAAQ,QAAA,IAAY,UAAA;AACrC,EAAA,MAAM,MAAA,GAAyC;AAAA,IAC7C,SAAA,EAAW,QAAQ,SAAA,IAAa,QAAA;AAAA,IAChC,UAAA,EAAY,gBAAgB,OAAO,CAAA;AAAA,IACnC;AAAA,GACF;AACA,EAAA,MAAM,EAAE,GAAG,CAAA,EAAG,SAAA,KAAc,MAAM,eAAA,CAAgB,MAAA,EAAQ,QAAA,EAAU,MAAM,CAAA;AAC1E,EAAA,MAAA,CAAO,MAAA,CAAO,SAAS,KAAA,EAAO;AAAA,IAC5B,QAAA,EAAU,QAAA;AAAA,IACV,IAAA,EAAM,GAAG,CAAC,CAAA,EAAA,CAAA;AAAA,IACV,GAAA,EAAK,GAAG,CAAC,CAAA,EAAA;AAAA,GACV,CAAA;AACD,EAAA,OAAO,EAAE,CAAA,EAAG,CAAA,EAAG,SAAA,EAAU;AAC3B;AAwBO,SAAS,kBACd,MAAA,EACA,QAAA,EACA,OAAA,GAA8B,IAC9B,UAAA,EACY;AACZ,EAAA,OAAO,UAAA,CAAW,MAAA,EAAQ,QAAA,EAAU,MAAM;AACxC,IAAA,KAAK,QAAA,CAAS,MAAA,EAAQ,QAAA,EAAU,OAAO,CAAA,CAAE,KAAK,CAAC,MAAA,KAAW,UAAA,GAAa,MAAM,CAAC,CAAA;AAAA,EAChF,CAAC,CAAA;AACH;AAQO,IAAM,sBAAA,GAAyB;AAAA,EACpC,kBAAA,EAAoB;AACtB;AAsBO,SAAS,oBAAoB,WAAA,EAAgC;AAClE,EAAA,KAAA,MAAW,CAAC,UAAA,EAAY,UAAU,KAAK,MAAA,CAAO,OAAA,CAAQ,sBAAsB,CAAA,EAAG;AAC7E,IAAA,WAAA,CAAY,QAAA,CAAS,YAAY,UAAU,CAAA;AAAA,EAC7C;AACF","file":"index.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\nimport { attachPositioning, type Placement, type PositioningOptions, type PositionResult } from \".\";\n\n/**\n * Headless **anchored positioning**: keeps a `floating` element placed against an\n * `anchor`, flipping/shifting it away from viewport edges as the page scrolls or\n * resizes. It is the declarative surface of the opt-in {@link attachPositioning}\n * engine (`@floating-ui/dom`-based) — Radix's anchored popper / floating-ui's\n * `autoUpdate`, exposed as a controller. No dedicated APG pattern; it is the\n * placement primitive the popup patterns (Tooltip / Menu / Popover …) build on.\n *\n * Markup contract (identifier: `stimeo--anchored`):\n * <div data-controller=\"stimeo--anchored\"\n * data-stimeo--anchored-placement-value=\"bottom-start\"\n * data-stimeo--anchored-offset-value=\"8\">\n * <button data-stimeo--anchored-target=\"anchor\">Open</button>\n * <div data-stimeo--anchored-target=\"floating\" role=\"…\">…</div>\n * </div>\n *\n * `active` drives tracking (start/stop) and fires on connect, mirroring Focus\n * Scope's `trapValueChanged`; set it `false` while the floating element is hidden\n * so no measurement runs. The other Values map to {@link PositioningOptions} and\n * re-apply live while tracking. Only `position`/`left`/`top` inline styles are\n * written — never decoration — and the resolved (post-flip) side is mirrored onto\n * `data-anchored-placement` on the floating element for CSS hooks (e.g. an arrow).\n *\n * @remarks\n * Behavior only. It does **not** open/close, manage focus, or render an overlay\n * (pair with Dialog / Popover and {@link \"../controllers/focus_controller\"}), and\n * it does **not** move DOM (pair with Portal). It lives in the opt-in\n * `stimeo-ui/positioning` subpath so the core `import \"stimeo-ui\"` stays\n * zero-dependency; only consumers who register it pull in `@floating-ui/dom`. The\n * `autoUpdate` cleanup is released on `disconnect()` (Turbo navigation included)\n * so no observer outlives the element, and `#sync` reconciles to a single live\n * observer (keyed on the applied options) so reconnects never stack observers.\n */\nexport class AnchoredController extends Controller<HTMLElement> {\n static override targets = [\"anchor\", \"floating\"];\n static override values = {\n placement: { type: String, default: \"bottom\" },\n offset: { type: Number, default: 0 },\n flip: { type: Boolean, default: true },\n shift: { type: Boolean, default: true },\n padding: { type: Number, default: 0 },\n strategy: { type: String, default: \"absolute\" },\n active: { type: Boolean, default: true },\n };\n static events = [\"position\"] as const;\n\n declare readonly anchorTarget: HTMLElement;\n declare readonly floatingTarget: HTMLElement;\n declare readonly hasAnchorTarget: boolean;\n declare readonly hasFloatingTarget: boolean;\n\n declare placementValue: string;\n declare offsetValue: number;\n declare flipValue: boolean;\n declare shiftValue: boolean;\n declare paddingValue: number;\n declare strategyValue: string;\n declare activeValue: boolean;\n\n /** `autoUpdate` cleanup while tracking; `null` when detached. */\n #stop: (() => void) | null = null;\n /** True between connect and disconnect (Stimulus may fire value callbacks before connect). */\n #connected = false;\n /** Serialized options of the live observer, or `null` when detached — see {@link #sync}. */\n #appliedKey: string | null = null;\n\n override connect(): void {\n this.#connected = true;\n this.#sync();\n }\n\n override disconnect(): void {\n this.#connected = false;\n this.#sync();\n }\n\n // Every value change (active or an option) re-syncs. `#sync` is idempotent and\n // order-independent, so no Value has to be declared in a particular position.\n activeValueChanged(): void {\n this.#sync();\n }\n placementValueChanged(): void {\n this.#sync();\n }\n offsetValueChanged(): void {\n this.#sync();\n }\n flipValueChanged(): void {\n this.#sync();\n }\n shiftValueChanged(): void {\n this.#sync();\n }\n paddingValueChanged(): void {\n this.#sync();\n }\n strategyValueChanged(): void {\n this.#sync();\n }\n\n /** Current Values mapped to the positioning engine's options. */\n get #options(): PositioningOptions {\n return {\n placement: this.placementValue as Placement,\n offset: this.offsetValue,\n flip: this.flipValue,\n shift: this.shiftValue,\n padding: this.paddingValue,\n // Narrow the free-form Value to the engine's union; anything but \"fixed\"\n // falls back to the default \"absolute\".\n strategy: this.strategyValue === \"fixed\" ? \"fixed\" : \"absolute\",\n };\n }\n\n /**\n * Reconciles the live observer with the desired state — track iff connected,\n * `active`, and both targets exist — re-attaching only when that state or the\n * options actually changed. Stimulus fires the value-changed callbacks on\n * connect in declaration order and may run them before or after `connect()`;\n * keying on the applied options collapses that whole burst (in any order) to a\n * single attach, while an option change at runtime re-attaches exactly once. So\n * correctness never depends on `active` being declared last.\n */\n #sync(): void {\n const shouldTrack =\n this.#connected && this.activeValue && this.hasAnchorTarget && this.hasFloatingTarget;\n const key = shouldTrack ? JSON.stringify(this.#options) : null;\n if (key === this.#appliedKey) return;\n this.#detach();\n this.#appliedKey = key;\n if (shouldTrack) this.#attach();\n }\n\n #attach(): void {\n this.#stop = attachPositioning(\n this.anchorTarget,\n this.floatingTarget,\n this.#options,\n (result) => this.#onComputed(result),\n );\n }\n\n #detach(): void {\n this.#stop?.();\n this.#stop = null;\n }\n\n /** Reflects the resolved side onto the CSS hook and announces the placement. */\n #onComputed(result: PositionResult): void {\n this.floatingTarget.setAttribute(\"data-anchored-placement\", result.placement);\n this.dispatch(\"position\", {\n detail: { placement: result.placement, x: result.x, y: result.y },\n });\n }\n}\n","import {\n autoUpdate,\n type ComputePositionConfig,\n computePosition,\n flip,\n type Middleware,\n offset,\n type Placement,\n shift,\n} from \"@floating-ui/dom\";\nimport type { Application } from \"@hotwired/stimulus\";\nimport { AnchoredController } from \"./anchored_controller\";\n\nexport type { Placement };\nexport { AnchoredController };\n\n/**\n * Opt-in shared positioning helper for Stimeo's floating components\n * (popover / tooltip / hover-card / context-menu).\n *\n * **Why this is a separate entry point.** The core library is zero-runtime-dep:\n * `import \"stimeo-ui\"` pulls in nothing but `@hotwired/stimulus`. Dynamic\n * placement — measuring the viewport / scroll parents to flip and shift a\n * floating element away from screen edges — genuinely needs a small, trustworthy\n * dependency (`@floating-ui/dom`). To keep that cost *opt-in*, this lives at\n * `stimeo-ui/positioning` and is loaded only when a consumer explicitly imports\n * it. The controllers themselves never import this module, so the core install\n * stays dependency-free.\n *\n * **What it does and does not own.** This helper writes **coordinates only** —\n * `position`, `left`, `top` inline\n * styles on the floating element. It never emits color, border, shadow, size, or\n * any other decoration: the consumer's CSS still owns the entire look. Static\n * placement (a fixed `top`/`left` in CSS) needs no JS at all; reach for this only\n * when you want edge-collision avoidance.\n */\n\n/** Options accepted by {@link position} and {@link attachPositioning}. */\nexport interface PositioningOptions {\n /**\n * Preferred side of the anchor to place the floating element on. Mirrors\n * floating-ui's `Placement` (e.g. `\"bottom\"`, `\"top-start\"`). Default `\"bottom\"`.\n */\n placement?: Placement;\n /** Gap in pixels between the anchor and the floating element. Default `0`. */\n offset?: number;\n /**\n * Flip to the opposite side when the preferred side would overflow the\n * viewport. Default `true`.\n */\n flip?: boolean;\n /**\n * Shift the floating element along its axis to keep it in view. Default `true`.\n */\n shift?: boolean;\n /**\n * Padding (px) kept between the floating element and the viewport edge when\n * flipping/shifting. Default `0`.\n */\n padding?: number;\n /**\n * CSS positioning strategy written to the floating element. `\"absolute\"`\n * (default) positions against the nearest positioned ancestor; `\"fixed\"`\n * positions against the viewport (useful inside `overflow` containers).\n */\n strategy?: \"absolute\" | \"fixed\";\n}\n\n/**\n * The resolved outcome of one positioning pass: the coordinates written to the\n * floating element and the **final** placement after flip/shift. Returned by\n * {@link position} and surfaced per update through {@link attachPositioning}'s\n * `onComputed` callback so callers can react to the resolved side (e.g. flip an\n * arrow, mirror the placement onto a `data-*` hook) without re-measuring.\n */\nexport interface PositionResult {\n /** Final placement after flip/shift resolved it (e.g. `\"top-start\"`). */\n placement: Placement;\n /** X coordinate written as the floating element's inline `left`. */\n x: number;\n /** Y coordinate written as the floating element's inline `top`. */\n y: number;\n}\n\n/** Builds the floating-ui middleware stack from {@link PositioningOptions}. */\nfunction buildMiddleware(options: PositioningOptions): Middleware[] {\n const padding = options.padding ?? 0;\n const middleware: Middleware[] = [];\n if (options.offset) middleware.push(offset(options.offset));\n // flip before shift so a side change is considered before nudging along-axis.\n if (options.flip !== false) middleware.push(flip({ padding }));\n if (options.shift !== false) middleware.push(shift({ padding }));\n return middleware;\n}\n\n/**\n * Computes a single placement for `floating` relative to `anchor` and writes the\n * resulting coordinates as inline styles on `floating`.\n *\n * This is the one-shot form: it positions once and returns. For a floating\n * element that must track scrolling/resizing while open, use\n * {@link attachPositioning} instead.\n *\n * Only `position`, `left`, and `top` are written — never any decoration. The\n * resolved {@link PositionResult} (final placement + coordinates) is returned so\n * callers can react to the side flip/shift chose.\n */\nexport async function position(\n anchor: Element,\n floating: HTMLElement,\n options: PositioningOptions = {},\n): Promise<PositionResult> {\n const strategy = options.strategy ?? \"absolute\";\n const config: Partial<ComputePositionConfig> = {\n placement: options.placement ?? \"bottom\",\n middleware: buildMiddleware(options),\n strategy,\n };\n const { x, y, placement } = await computePosition(anchor, floating, config);\n Object.assign(floating.style, {\n position: strategy,\n left: `${x}px`,\n top: `${y}px`,\n });\n return { x, y, placement };\n}\n\n/**\n * Positions `floating` against `anchor` and keeps it positioned across scroll,\n * resize, and layout changes via floating-ui's `autoUpdate`.\n *\n * Returns a cleanup function that stops tracking; call it when the floating\n * element closes or the controller disconnects (Turbo navigation included) so no\n * observer outlives the element.\n *\n * Pass `onComputed` to receive the resolved {@link PositionResult} on every\n * update (initial placement and each scroll/resize re-computation) — used to\n * mirror the final placement onto a hook or emit an event without re-measuring.\n *\n * @example\n * ```ts\n * import { attachPositioning } from \"stimeo-ui/positioning\";\n *\n * // when a popover opens:\n * const stop = attachPositioning(trigger, panel, { placement: \"bottom-start\", offset: 8 });\n * // when it closes:\n * stop();\n * ```\n */\nexport function attachPositioning(\n anchor: Element,\n floating: HTMLElement,\n options: PositioningOptions = {},\n onComputed?: (result: PositionResult) => void,\n): () => void {\n return autoUpdate(anchor, floating, () => {\n void position(anchor, floating, options).then((result) => onComputed?.(result));\n });\n}\n\n/**\n * Maps the opt-in positioning controller identifiers to their classes. Kept\n * separate from the core `stimeoControllers` (`src/index.ts`) so the core\n * install never imports `@floating-ui/dom`; the Inspector manifest reflects both\n * core and positioning controllers so `stimeo check` recognizes them.\n */\nexport const positioningControllers = {\n \"stimeo--anchored\": AnchoredController,\n} as const;\n\n/**\n * Registers the opt-in positioning controllers (e.g. `stimeo--anchored`) on a\n * Stimulus application. Call this **in addition to** `registerStimeo` only when\n * you want the declarative positioning primitives — importing this module is\n * what pulls in `@floating-ui/dom`, so the core stays zero-dependency for\n * consumers who never call it.\n *\n * @param application - The Stimulus application to register controllers on.\n *\n * @example\n * ```ts\n * import { Application } from \"@hotwired/stimulus\";\n * import { registerStimeo } from \"stimeo-ui\";\n * import { registerPositioning } from \"stimeo-ui/positioning\";\n *\n * const application = Application.start();\n * registerStimeo(application);\n * registerPositioning(application); // opt-in: adds stimeo--anchored\n * ```\n */\nexport function registerPositioning(application: Application): void {\n for (const [identifier, controller] of Object.entries(positioningControllers)) {\n application.register(identifier, controller);\n }\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,93 @@
1
+ {
2
+ "name": "stimeo-ui",
3
+ "version": "0.1.0-alpha.1",
4
+ "description": "Headless Stimulus UI framework for Ruby on Rails — behavior-only, accessible components driven by HTML attributes.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Stimeo Labs",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/taiyaky/stimeo-ui.git"
11
+ },
12
+ "homepage": "https://github.com/taiyaky/stimeo-ui#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/taiyaky/stimeo-ui/issues"
15
+ },
16
+ "keywords": [
17
+ "stimulus",
18
+ "rails",
19
+ "hotwire",
20
+ "turbo",
21
+ "headless",
22
+ "ui",
23
+ "components",
24
+ "accessibility",
25
+ "wai-aria",
26
+ "a11y"
27
+ ],
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "sideEffects": false,
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "exports": {
36
+ ".": {
37
+ "types": "./dist/index.d.ts",
38
+ "import": "./dist/index.js"
39
+ },
40
+ "./controllers/*": {
41
+ "types": "./dist/controllers/*.d.ts",
42
+ "import": "./dist/controllers/*.js"
43
+ },
44
+ "./positioning": {
45
+ "types": "./dist/positioning/index.d.ts",
46
+ "import": "./dist/positioning/index.js"
47
+ }
48
+ },
49
+ "main": "./dist/index.js",
50
+ "module": "./dist/index.js",
51
+ "types": "./dist/index.d.ts",
52
+ "bin": {
53
+ "stimeo": "./dist/inspector/cli_bin.js"
54
+ },
55
+ "scripts": {
56
+ "build": "tsup && bun scripts/postbuild.ts",
57
+ "dev": "tsup --watch --onSuccess \"bun scripts/postbuild.ts\"",
58
+ "test": "vitest run",
59
+ "test:watch": "vitest",
60
+ "test:coverage": "vitest run --coverage",
61
+ "typecheck": "tsc --noEmit",
62
+ "lint": "biome check .",
63
+ "lint:fix": "biome check --write .",
64
+ "format": "biome format --write ."
65
+ },
66
+ "peerDependencies": {
67
+ "@floating-ui/dom": ">=1.6.0",
68
+ "@hotwired/stimulus": ">=3.2.0"
69
+ },
70
+ "peerDependenciesMeta": {
71
+ "@floating-ui/dom": {
72
+ "optional": true
73
+ }
74
+ },
75
+ "devDependencies": {
76
+ "@biomejs/biome": "2.4.16",
77
+ "@floating-ui/dom": "1.7.6",
78
+ "@guidepup/virtual-screen-reader": "0.32.1",
79
+ "@hotwired/stimulus": "3.2.2",
80
+ "@hotwired/turbo": "8.0.23",
81
+ "@types/node": "22.19.19",
82
+ "@vitest/coverage-istanbul": "4.1.8",
83
+ "axe-core": "4.12.0",
84
+ "happy-dom": "20.9.0",
85
+ "tsup": "8.5.1",
86
+ "typescript": "6.0.3",
87
+ "vitest": "4.1.8",
88
+ "vitest-axe": "0.1.0"
89
+ },
90
+ "overrides": {
91
+ "rollup": "4.61.1"
92
+ }
93
+ }