stimeo-ui 0.9.0 → 0.10.0
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.
- package/CHANGELOG.md +74 -0
- package/dist/controllers/accordion_controller.js.map +1 -1
- package/dist/controllers/alert_dialog_controller.js +17 -7
- package/dist/controllers/alert_dialog_controller.js.map +1 -1
- package/dist/controllers/announcer_controller.js +1 -1
- package/dist/controllers/announcer_controller.js.map +1 -1
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/avatar_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/bulk_select_controller.js +2 -2
- package/dist/controllers/bulk_select_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/character_counter_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.d.ts +1 -1
- package/dist/controllers/collapsible_controller.js +1 -1
- package/dist/controllers/collapsible_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +1 -1
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.js +15 -5
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/conditional_fields_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.js +15 -5
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +5 -4
- package/dist/controllers/context_menu_controller.js.map +1 -1
- package/dist/controllers/count_up_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.js +1 -1
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/data_grid_controller.js +1 -1
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.js +15 -5
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.d.ts +2 -2
- package/dist/controllers/direct_upload_controller.js +1 -5
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/drawer_controller.d.ts +2 -2
- package/dist/controllers/drawer_controller.js +17 -7
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +5 -0
- package/dist/controllers/focus_controller.js +45 -7
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_field_controller.js +1 -1
- package/dist/controllers/form_field_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/input_mask_controller.js.map +1 -1
- package/dist/controllers/intersection_controller.d.ts +4 -3
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.d.ts +2 -2
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +3 -1
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/menu_controller.d.ts +1 -1
- package/dist/controllers/menu_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.js +2 -2
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/meter_controller.d.ts +1 -1
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.js +1 -1
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/navigation_menu_controller.js.map +1 -1
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.js +2 -2
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.d.ts +1 -1
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +27 -2
- package/dist/controllers/password_reveal_controller.js +102 -3
- package/dist/controllers/password_reveal_controller.js.map +1 -1
- package/dist/controllers/password_strength_controller.d.ts +71 -17
- package/dist/controllers/password_strength_controller.js +287 -42
- package/dist/controllers/password_strength_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.d.ts +3 -2
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +1 -1
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.js.map +1 -1
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/read_more_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.js.map +1 -1
- package/dist/controllers/reset_before_cache_controller.d.ts +1 -2
- package/dist/controllers/reset_before_cache_controller.js.map +1 -1
- package/dist/controllers/resizable_controller.d.ts +1 -1
- package/dist/controllers/resizable_controller.js +1 -1
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/roving_controller.d.ts +2 -1
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_restore_controller.d.ts +33 -11
- package/dist/controllers/scroll_restore_controller.js +127 -27
- package/dist/controllers/scroll_restore_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.d.ts +42 -10
- package/dist/controllers/scroll_visibility_controller.js +167 -14
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.js +15 -5
- package/dist/controllers/sidebar_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.js +0 -5
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +3 -2
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/theme_controller.d.ts +32 -6
- package/dist/controllers/theme_controller.js +151 -40
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toast_controller.d.ts +2 -2
- package/dist/controllers/toast_controller.js +2 -2
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/transition_controller.d.ts +11 -7
- package/dist/controllers/transition_controller.js +79 -15
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +740 -178
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +3 -4
- package/dist/inspector/cli.js +1 -0
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +6 -5
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +3 -3
- package/dist/inspector/manifest.json +7 -4
- package/dist/positioning/index.d.ts +18 -4
- package/dist/positioning/index.js +84 -25
- package/dist/positioning/index.js.map +1 -1
- package/package.json +5 -5
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
"stimeo--anchored": {
|
|
13
13
|
"file": "examples/anchored/_demo.html.erb",
|
|
14
|
-
"source": "<%# Anchored positioning demo: the panel is positioned against the anchor button by the\n opt-in stimeo--anchored controller (it writes only position/left/top). Choose a side\n with the buttons — demo.js sets the placement value — and the panel mirrors the\n resolved side via data-anchored-placement (shown through demo.css). The controller is\n registered from the opt-in stimeo-ui/positioning subpath in demo.js; demo.css owns the\n look. %>\n<div class=\"anchored-demo\">\n <p class=\"anchored-demo__hint\"><%= t(\"components.anchored.demo.hint\") %></p>\n <div class=\"anchored-demo__controls\" role=\"group\"\n aria-label=\"<%= t('components.anchored.demo.placement_label') %>\">\n <button type=\"button\" class=\"demo-trigger\" data-placement=\"top\" aria-pressed=\"false\">\n <%= t(\"components.anchored.demo.placements.top\") %>\n </button>\n <button type=\"button\" class=\"demo-trigger\" data-placement=\"right\" aria-pressed=\"false\">\n <%= t(\"components.anchored.demo.placements.right\") %>\n </button>\n <button type=\"button\" class=\"demo-trigger\" data-placement=\"bottom\" aria-pressed=\"true\">\n <%= t(\"components.anchored.demo.placements.bottom\") %>\n </button>\n <button type=\"button\" class=\"demo-trigger\" data-placement=\"left\" aria-pressed=\"false\">\n <%= t(\"components.anchored.demo.placements.left\") %>\n </button>\n </div>\n <div class=\"anchored-demo__viewport\">\n <div class=\"anchored-demo__scope\" data-controller=\"stimeo--anchored\"\n data-stimeo--anchored-placement-value=\"bottom\"\n data-stimeo--anchored-offset-value=\"8\"\n data-stimeo--anchored-padding-value=\"8\">\n <button type=\"button\" class=\"demo-trigger anchored-demo__anchor\"\n data-stimeo--anchored-target=\"anchor\">\n <%= t(\"components.anchored.demo.anchor\") %>\n </button>\n <div class=\"anchored-demo__floating\" data-stimeo--anchored-target=\"floating\"></div>\n </div>\n </div>\n</div>\n"
|
|
14
|
+
"source": "<%# Anchored positioning demo: the panel is positioned against the anchor button by the\n opt-in stimeo--anchored controller (it writes only position/left/top). Choose a side\n with the buttons — demo.js sets the placement value — and the panel mirrors the\n resolved side via data-anchored-placement (shown through demo.css). The controller is\n registered from the opt-in stimeo-ui/positioning subpath in demo.js; demo.css owns the\n look. %>\n<div class=\"anchored-demo\">\n <p class=\"anchored-demo__hint\"><%= t(\"components.anchored.demo.hint\") %></p>\n <div class=\"anchored-demo__controls\" role=\"group\"\n aria-label=\"<%= t('components.anchored.demo.placement_label') %>\">\n <button type=\"button\" class=\"demo-trigger\" data-placement=\"top\" aria-pressed=\"false\">\n <%= t(\"components.anchored.demo.placements.top\") %>\n </button>\n <button type=\"button\" class=\"demo-trigger\" data-placement=\"right\" aria-pressed=\"false\">\n <%= t(\"components.anchored.demo.placements.right\") %>\n </button>\n <button type=\"button\" class=\"demo-trigger\" data-placement=\"bottom\" aria-pressed=\"true\">\n <%= t(\"components.anchored.demo.placements.bottom\") %>\n </button>\n <button type=\"button\" class=\"demo-trigger\" data-placement=\"left\" aria-pressed=\"false\">\n <%= t(\"components.anchored.demo.placements.left\") %>\n </button>\n </div>\n <div class=\"anchored-demo__viewport\">\n <div class=\"anchored-demo__scope\" data-controller=\"stimeo--anchored\"\n data-stimeo--anchored-placement-value=\"bottom\"\n data-stimeo--anchored-offset-value=\"8\"\n data-stimeo--anchored-padding-value=\"8\">\n <button type=\"button\" class=\"demo-trigger anchored-demo__anchor\"\n data-stimeo--anchored-target=\"anchor\">\n <%= t(\"components.anchored.demo.anchor\") %>\n </button>\n <div class=\"anchored-demo__floating\" data-stimeo--anchored-target=\"floating\"\n data-placement-label=\"<%= t('components.anchored.demo.placement_prefix') %>\"></div>\n </div>\n </div>\n</div>\n"
|
|
15
15
|
},
|
|
16
16
|
"stimeo--announcer": {
|
|
17
17
|
"file": "examples/announcer/_demo.html.erb",
|
|
@@ -263,7 +263,7 @@
|
|
|
263
263
|
},
|
|
264
264
|
"stimeo--password-strength": {
|
|
265
265
|
"file": "examples/password_strength/_demo.html.erb",
|
|
266
|
-
"source": "<%# Markup for the password-strength demo.\n The controller scores the field with a lightweight zero-dependency heuristic and\n drives the meter (aria-valuenow), the data-strength token,
|
|
266
|
+
"source": "<%# Markup for the password-strength demo.\n The controller scores the field with a lightweight zero-dependency heuristic and\n drives the meter (aria-valuenow), the data-strength token, the 0–1 fill on the\n --stimeo--password-strength custom property, and the visible readout — all on the\n keystroke that caused them. The input sits inside the controller so its data-action\n input->...#evaluate binds directly. Level labels are localized via the levels value;\n demo.css colors the bar by the locale-independent data-strength band, so the look\n never depends on the (translated) label text.\n\n The readout is plain visible output, not a live region: the settled level is\n debounced into the shared stimeo--announcer your app seats once, in its layout.\n Announcing is opt-in through announce-text-value and only fires when the level\n actually changes. The library writes no CSS. %>\n<% levels = t(\"components.password_strength.demo.levels\").to_json %>\n<% announcement = t(\"components.password_strength.demo.announcement\") %>\n<div\n class=\"password-strength\"\n data-controller=\"stimeo--password-strength\"\n data-stimeo--password-strength-levels-value=\"<%= levels %>\"\n data-stimeo--password-strength-announce-text-value=\"<%= announcement %>\">\n <label class=\"password-strength__label\" for=\"password-strength-demo-input\">\n <%= t(\"components.password_strength.demo.label\") %>\n </label>\n <input\n class=\"password-strength__input\"\n id=\"password-strength-demo-input\"\n type=\"password\"\n autocomplete=\"new-password\"\n aria-describedby=\"password-strength-demo-readout\"\n data-stimeo--password-strength-target=\"input\"\n data-action=\"input->stimeo--password-strength#evaluate\">\n <div\n class=\"password-strength__meter\"\n role=\"meter\"\n aria-valuemin=\"0\"\n aria-valuemax=\"4\"\n aria-valuenow=\"0\"\n aria-label=\"<%= t('components.password_strength.demo.meter_label') %>\"\n data-stimeo--password-strength-target=\"meter\">\n <span class=\"password-strength__bar\"></span>\n </div>\n <span\n class=\"password-strength__readout\"\n id=\"password-strength-demo-readout\"\n data-stimeo--password-strength-target=\"label\"></span>\n</div>\n<p class=\"password-strength__hint\"><%= t(\"components.password_strength.demo.hint\") %></p>\n"
|
|
267
267
|
},
|
|
268
268
|
"stimeo--persist": {
|
|
269
269
|
"file": "examples/persist/_demo.html.erb",
|
|
@@ -411,7 +411,7 @@
|
|
|
411
411
|
},
|
|
412
412
|
"stimeo--theme": {
|
|
413
413
|
"file": "examples/theme/_demo.html.erb",
|
|
414
|
-
"source": "<%# Markup for the theme / color-scheme toggle demo.\n The controller persists the light/dark/system choice, follows the OS while in\n system, and writes data-theme + color-scheme onto the target. To avoid theming the\n whole Playground, this demo targets a local preview element instead of <html>, so\n the preview box below reacts to the resolved theme while the page is unaffected. %>\n<div class=\"theme-demo\">\n <div class=\"theme-demo__switcher\" data-controller=\"stimeo--theme\"\n data-stimeo--theme-target-value=\"#theme-demo-preview\"\n data-stimeo--theme-storage-key-value=\"stimeo-theme-demo\"\n data-stimeo--theme-mode-value=\"system\"\n role=\"radiogroup\" aria-label=\"<%= t(\"components.theme.demo.aria_label\") %>\">\n <button type=\"button\" class=\"theme-demo__option\"\n data-stimeo--theme-target=\"option\" role=\"radio\"\n data-action=\"click->stimeo--theme#set\" data-
|
|
414
|
+
"source": "<%# Markup for the theme / color-scheme toggle demo.\n The controller persists the light/dark/system choice, follows the OS while in\n system, and writes data-theme + color-scheme onto the target. To avoid theming the\n whole Playground, this demo targets a local preview element instead of <html>, so\n the preview box below reacts to the resolved theme while the page is unaffected. %>\n<div class=\"theme-demo\">\n <div class=\"theme-demo__switcher\" data-controller=\"stimeo--theme\"\n data-stimeo--theme-target-value=\"#theme-demo-preview\"\n data-stimeo--theme-storage-key-value=\"stimeo-theme-demo\"\n data-stimeo--theme-mode-value=\"system\"\n role=\"radiogroup\" aria-label=\"<%= t(\"components.theme.demo.aria_label\") %>\">\n <button type=\"button\" class=\"theme-demo__option\"\n data-stimeo--theme-target=\"option\" role=\"radio\"\n data-action=\"click->stimeo--theme#set\" data-value=\"light\">\n <%= t(\"components.theme.demo.light\") %>\n </button>\n <button type=\"button\" class=\"theme-demo__option\"\n data-stimeo--theme-target=\"option\" role=\"radio\"\n data-action=\"click->stimeo--theme#set\" data-value=\"dark\">\n <%= t(\"components.theme.demo.dark\") %>\n </button>\n <button type=\"button\" class=\"theme-demo__option\"\n data-stimeo--theme-target=\"option\" role=\"radio\"\n data-action=\"click->stimeo--theme#set\" data-value=\"system\">\n <%= t(\"components.theme.demo.system\") %>\n </button>\n </div>\n\n <div id=\"theme-demo-preview\" class=\"theme-demo__preview\">\n <h3 class=\"theme-demo__preview-title\"><%= t(\"components.theme.demo.preview_title\") %></h3>\n <p><%= t(\"components.theme.demo.preview_text\") %></p>\n </div>\n</div>\n"
|
|
415
415
|
},
|
|
416
416
|
"stimeo--time-picker": {
|
|
417
417
|
"file": "examples/time_picker/_demo.html.erb",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 12,
|
|
3
|
-
"packageVersion": "0.
|
|
3
|
+
"packageVersion": "0.10.0",
|
|
4
4
|
"controllers": {
|
|
5
5
|
"stimeo--accordion": {
|
|
6
6
|
"targets": [
|
|
@@ -3272,15 +3272,18 @@
|
|
|
3272
3272
|
],
|
|
3273
3273
|
"values": [
|
|
3274
3274
|
"minScore",
|
|
3275
|
-
"levels"
|
|
3275
|
+
"levels",
|
|
3276
|
+
"announceText"
|
|
3276
3277
|
],
|
|
3277
3278
|
"valueConstraints": [],
|
|
3278
3279
|
"valueRelations": [],
|
|
3279
3280
|
"actions": [
|
|
3280
|
-
"evaluate"
|
|
3281
|
+
"evaluate",
|
|
3282
|
+
"setScore"
|
|
3281
3283
|
],
|
|
3282
3284
|
"events": [
|
|
3283
|
-
"change"
|
|
3285
|
+
"change",
|
|
3286
|
+
"reconcile"
|
|
3284
3287
|
],
|
|
3285
3288
|
"requiredTargets": [
|
|
3286
3289
|
"input",
|
|
@@ -21,9 +21,16 @@ import { Controller, Application } from '@hotwired/stimulus';
|
|
|
21
21
|
* `active` drives tracking (start/stop) and fires on connect, mirroring Focus
|
|
22
22
|
* Scope's `trapValueChanged`; set it `false` while the floating element is hidden
|
|
23
23
|
* so no measurement runs. The other Values map to {@link PositioningOptions} and
|
|
24
|
-
* re-apply live while tracking.
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* re-apply live while tracking. A declaration the engine cannot use — a
|
|
25
|
+
* `placement` outside its set, a non-finite `offset` or `padding` — falls back to
|
|
26
|
+
* that Value's default, so a typo never reaches the coordinates or the hook. Only
|
|
27
|
+
* `position`/`left`/`top` inline styles are written — never decoration — and the
|
|
28
|
+
* resolved (post-flip) side is mirrored onto `data-anchored-placement` on the
|
|
29
|
+
* floating element for CSS hooks (e.g. an arrow).
|
|
30
|
+
*
|
|
31
|
+
* Tracking follows the targets themselves, not just the Values: the engine holds
|
|
32
|
+
* the two elements it was handed, so a target added, removed or swapped at
|
|
33
|
+
* runtime re-attaches against whatever is there now.
|
|
27
34
|
*
|
|
28
35
|
* `position` dispatches `{ placement, x, y }`.
|
|
29
36
|
*
|
|
@@ -35,7 +42,10 @@ import { Controller, Application } from '@hotwired/stimulus';
|
|
|
35
42
|
* zero-dependency; only consumers who register it pull in `@floating-ui/dom`. The
|
|
36
43
|
* `autoUpdate` cleanup is released on `disconnect()` (Turbo navigation included)
|
|
37
44
|
* so no observer outlives the element, and `#sync` reconciles to a single live
|
|
38
|
-
* observer (keyed on the applied options) so reconnects never stack
|
|
45
|
+
* observer (keyed on the applied options and elements) so reconnects never stack
|
|
46
|
+
* observers. That cleanup stops future updates but cannot cancel one already
|
|
47
|
+
* computing, so a pass that lands after its own attach was replaced stands down
|
|
48
|
+
* instead of writing.
|
|
39
49
|
*/
|
|
40
50
|
declare class AnchoredController extends Controller<HTMLElement> {
|
|
41
51
|
#private;
|
|
@@ -84,6 +94,10 @@ declare class AnchoredController extends Controller<HTMLElement> {
|
|
|
84
94
|
activeValue: boolean;
|
|
85
95
|
connect(): void;
|
|
86
96
|
disconnect(): void;
|
|
97
|
+
anchorTargetConnected(): void;
|
|
98
|
+
anchorTargetDisconnected(): void;
|
|
99
|
+
floatingTargetConnected(): void;
|
|
100
|
+
floatingTargetDisconnected(): void;
|
|
87
101
|
activeValueChanged(): void;
|
|
88
102
|
placementValueChanged(): void;
|
|
89
103
|
offsetValueChanged(): void;
|
|
@@ -2,6 +2,20 @@ import { computePosition, autoUpdate, offset, flip, shift } from '@floating-ui/d
|
|
|
2
2
|
import { Controller } from '@hotwired/stimulus';
|
|
3
3
|
|
|
4
4
|
// src/positioning/index.ts
|
|
5
|
+
var PLACEMENTS = /* @__PURE__ */ new Set([
|
|
6
|
+
"top",
|
|
7
|
+
"top-start",
|
|
8
|
+
"top-end",
|
|
9
|
+
"right",
|
|
10
|
+
"right-start",
|
|
11
|
+
"right-end",
|
|
12
|
+
"bottom",
|
|
13
|
+
"bottom-start",
|
|
14
|
+
"bottom-end",
|
|
15
|
+
"left",
|
|
16
|
+
"left-start",
|
|
17
|
+
"left-end"
|
|
18
|
+
]);
|
|
5
19
|
var AnchoredController = class extends Controller {
|
|
6
20
|
static targets = ["anchor", "floating"];
|
|
7
21
|
static values = {
|
|
@@ -16,10 +30,18 @@ var AnchoredController = class extends Controller {
|
|
|
16
30
|
static events = ["position"];
|
|
17
31
|
/** `autoUpdate` cleanup while tracking; `null` when detached. */
|
|
18
32
|
#stop = null;
|
|
33
|
+
/** Identity of the live attach; `null` when detached — see {@link #onComputed}. */
|
|
34
|
+
#attachId = null;
|
|
19
35
|
/** True between connect and disconnect (Stimulus may fire value callbacks before connect). */
|
|
20
36
|
#connected = false;
|
|
21
|
-
/**
|
|
22
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Serialized options as of the last reconcile, empty only before the first one —
|
|
39
|
+
* a value no serialization produces, so it can never match a real key.
|
|
40
|
+
*/
|
|
41
|
+
#appliedKey = "";
|
|
42
|
+
/** Elements the live observer holds, or `null` when detached — see {@link #sync}. */
|
|
43
|
+
#appliedAnchor = null;
|
|
44
|
+
#appliedFloating = null;
|
|
23
45
|
connect() {
|
|
24
46
|
this.#connected = true;
|
|
25
47
|
this.#sync();
|
|
@@ -28,8 +50,21 @@ var AnchoredController = class extends Controller {
|
|
|
28
50
|
this.#connected = false;
|
|
29
51
|
this.#sync();
|
|
30
52
|
}
|
|
31
|
-
// Every value change (active or an option)
|
|
32
|
-
// order-independent, so no Value
|
|
53
|
+
// Every value change (active or an option) and every target arrival or
|
|
54
|
+
// departure re-syncs. `#sync` is idempotent and order-independent, so no Value
|
|
55
|
+
// has to be declared in a particular position.
|
|
56
|
+
anchorTargetConnected() {
|
|
57
|
+
this.#sync();
|
|
58
|
+
}
|
|
59
|
+
anchorTargetDisconnected() {
|
|
60
|
+
this.#sync();
|
|
61
|
+
}
|
|
62
|
+
floatingTargetConnected() {
|
|
63
|
+
this.#sync();
|
|
64
|
+
}
|
|
65
|
+
floatingTargetDisconnected() {
|
|
66
|
+
this.#sync();
|
|
67
|
+
}
|
|
33
68
|
activeValueChanged() {
|
|
34
69
|
this.#sync();
|
|
35
70
|
}
|
|
@@ -54,48 +89,72 @@ var AnchoredController = class extends Controller {
|
|
|
54
89
|
/** Current Values mapped to the positioning engine's options. */
|
|
55
90
|
get #options() {
|
|
56
91
|
return {
|
|
57
|
-
|
|
58
|
-
|
|
92
|
+
// Narrow every free-form Value to what the engine can use. A placement
|
|
93
|
+
// outside the set would be published on the hook as a resolved side it is
|
|
94
|
+
// not, and a non-finite distance poisons the coordinate it feeds until the
|
|
95
|
+
// browser drops the whole declaration and leaves that axis unplaced.
|
|
96
|
+
placement: PLACEMENTS.has(this.placementValue) ? this.placementValue : "bottom",
|
|
97
|
+
offset: Number.isFinite(this.offsetValue) ? this.offsetValue : 0,
|
|
59
98
|
flip: this.flipValue,
|
|
60
99
|
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".
|
|
100
|
+
padding: Number.isFinite(this.paddingValue) ? this.paddingValue : 0,
|
|
64
101
|
strategy: this.strategyValue === "fixed" ? "fixed" : "absolute"
|
|
65
102
|
};
|
|
66
103
|
}
|
|
67
104
|
/**
|
|
68
105
|
* Reconciles the live observer with the desired state — track iff connected,
|
|
69
|
-
* `active`, and both targets exist — re-attaching only when that state
|
|
70
|
-
* options actually changed. Stimulus fires the value-changed
|
|
71
|
-
* connect in declaration order and may run them before or after
|
|
72
|
-
* keying on
|
|
73
|
-
* single attach, while an option change at runtime
|
|
74
|
-
* correctness never depends on `active` being
|
|
106
|
+
* `active`, and both targets exist — re-attaching only when that state, the
|
|
107
|
+
* options, or the elements actually changed. Stimulus fires the value-changed
|
|
108
|
+
* callbacks on connect in declaration order and may run them before or after
|
|
109
|
+
* `connect()`; keying on what is applied collapses that whole burst (in any
|
|
110
|
+
* order) to a single attach, while an option or target change at runtime
|
|
111
|
+
* re-attaches exactly once. So correctness never depends on `active` being
|
|
112
|
+
* declared last. The elements are part of the key because the engine holds the
|
|
113
|
+
* pair it was handed: a swap leaves the same options behind and would otherwise
|
|
114
|
+
* keep measuring the node that just left the document.
|
|
75
115
|
*/
|
|
76
116
|
#sync() {
|
|
77
117
|
const shouldTrack = this.#connected && this.activeValue && this.hasAnchorTarget && this.hasFloatingTarget;
|
|
78
|
-
const
|
|
79
|
-
|
|
118
|
+
const anchor = shouldTrack ? this.anchorTarget : null;
|
|
119
|
+
const floating = shouldTrack ? this.floatingTarget : null;
|
|
120
|
+
const key = JSON.stringify(this.#options);
|
|
121
|
+
if (key === this.#appliedKey && anchor === this.#appliedAnchor && floating === this.#appliedFloating) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
80
124
|
this.#detach();
|
|
81
125
|
this.#appliedKey = key;
|
|
82
|
-
|
|
126
|
+
this.#appliedAnchor = anchor;
|
|
127
|
+
this.#appliedFloating = floating;
|
|
128
|
+
if (anchor && floating) this.#attach(anchor, floating);
|
|
83
129
|
}
|
|
84
|
-
#attach() {
|
|
130
|
+
#attach(anchor, floating) {
|
|
131
|
+
const id = /* @__PURE__ */ Symbol("anchored-attach");
|
|
132
|
+
this.#attachId = id;
|
|
85
133
|
this.#stop = attachPositioning(
|
|
86
|
-
|
|
87
|
-
|
|
134
|
+
anchor,
|
|
135
|
+
floating,
|
|
88
136
|
this.#options,
|
|
89
|
-
(result) => this.#onComputed(result)
|
|
137
|
+
(result) => this.#onComputed(id, floating, result)
|
|
90
138
|
);
|
|
91
139
|
}
|
|
92
140
|
#detach() {
|
|
93
141
|
this.#stop?.();
|
|
94
142
|
this.#stop = null;
|
|
143
|
+
this.#attachId = null;
|
|
95
144
|
}
|
|
96
|
-
/**
|
|
97
|
-
|
|
98
|
-
|
|
145
|
+
/**
|
|
146
|
+
* Reflects the resolved side onto the CSS hook and announces the placement.
|
|
147
|
+
*
|
|
148
|
+
* The pass carries the attach that started it and the element it positioned,
|
|
149
|
+
* and lands only while that attach is still the live one: `autoUpdate`'s
|
|
150
|
+
* cleanup stops further updates but cannot cancel one already computing, so a
|
|
151
|
+
* pass superseded mid-flight would otherwise write and dispatch afterwards.
|
|
152
|
+
* The element cannot stand in for that identity — an option change re-attaches
|
|
153
|
+
* against the very same pair.
|
|
154
|
+
*/
|
|
155
|
+
#onComputed(id, floating, result) {
|
|
156
|
+
if (id !== this.#attachId) return;
|
|
157
|
+
floating.setAttribute("data-anchored-placement", result.placement);
|
|
99
158
|
this.dispatch("position", {
|
|
100
159
|
detail: { placement: result.placement, x: result.x, y: result.y }
|
|
101
160
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/positioning/anchored_controller.ts","../../src/positioning/index.ts"],"names":[],"mappings":";;;;AAsCO,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;;;AC1EA,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), exposing its `autoUpdate` behavior as a\n * 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 * `position` dispatches `{ placement, x, y }`.\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"]}
|
|
1
|
+
{"version":3,"sources":["../../src/positioning/anchored_controller.ts","../../src/positioning/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\nimport { attachPositioning, type Placement, type PositioningOptions, type PositionResult } from \".\";\n\n/**\n * Every placement the engine accepts. A declaration outside the set has no\n * resolved side to publish, so it falls back to the Value's default rather than\n * reaching the state hook and the event detail as-is.\n */\nconst PLACEMENTS: ReadonlySet<string> = new Set([\n \"top\",\n \"top-start\",\n \"top-end\",\n \"right\",\n \"right-start\",\n \"right-end\",\n \"bottom\",\n \"bottom-start\",\n \"bottom-end\",\n \"left\",\n \"left-start\",\n \"left-end\",\n]);\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), exposing its `autoUpdate` behavior as a\n * 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. A declaration the engine cannot use — a\n * `placement` outside its set, a non-finite `offset` or `padding` — falls back to\n * that Value's default, so a typo never reaches the coordinates or the hook. Only\n * `position`/`left`/`top` inline styles are written — never decoration — and the\n * resolved (post-flip) side is mirrored onto `data-anchored-placement` on the\n * floating element for CSS hooks (e.g. an arrow).\n *\n * Tracking follows the targets themselves, not just the Values: the engine holds\n * the two elements it was handed, so a target added, removed or swapped at\n * runtime re-attaches against whatever is there now.\n *\n * `position` dispatches `{ placement, x, y }`.\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 and elements) so reconnects never stack\n * observers. That cleanup stops future updates but cannot cancel one already\n * computing, so a pass that lands after its own attach was replaced stands down\n * instead of writing.\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 /** Identity of the live attach; `null` when detached — see {@link #onComputed}. */\n #attachId: symbol | null = null;\n /** True between connect and disconnect (Stimulus may fire value callbacks before connect). */\n #connected = false;\n /**\n * Serialized options as of the last reconcile, empty only before the first one —\n * a value no serialization produces, so it can never match a real key.\n */\n #appliedKey = \"\";\n /** Elements the live observer holds, or `null` when detached — see {@link #sync}. */\n #appliedAnchor: Element | null = null;\n #appliedFloating: HTMLElement | 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) and every target arrival or\n // departure re-syncs. `#sync` is idempotent and order-independent, so no Value\n // has to be declared in a particular position.\n anchorTargetConnected(): void {\n this.#sync();\n }\n anchorTargetDisconnected(): void {\n this.#sync();\n }\n floatingTargetConnected(): void {\n this.#sync();\n }\n floatingTargetDisconnected(): void {\n this.#sync();\n }\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 // Narrow every free-form Value to what the engine can use. A placement\n // outside the set would be published on the hook as a resolved side it is\n // not, and a non-finite distance poisons the coordinate it feeds until the\n // browser drops the whole declaration and leaves that axis unplaced.\n placement: PLACEMENTS.has(this.placementValue)\n ? (this.placementValue as Placement)\n : \"bottom\",\n offset: Number.isFinite(this.offsetValue) ? this.offsetValue : 0,\n flip: this.flipValue,\n shift: this.shiftValue,\n padding: Number.isFinite(this.paddingValue) ? this.paddingValue : 0,\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, the\n * options, or the elements actually changed. Stimulus fires the value-changed\n * callbacks on connect in declaration order and may run them before or after\n * `connect()`; keying on what is applied collapses that whole burst (in any\n * order) to a single attach, while an option or target change at runtime\n * re-attaches exactly once. So correctness never depends on `active` being\n * declared last. The elements are part of the key because the engine holds the\n * pair it was handed: a swap leaves the same options behind and would otherwise\n * keep measuring the node that just left the document.\n */\n #sync(): void {\n const shouldTrack =\n this.#connected && this.activeValue && this.hasAnchorTarget && this.hasFloatingTarget;\n const anchor = shouldTrack ? this.anchorTarget : null;\n const floating = shouldTrack ? this.floatingTarget : null;\n // The elements already say whether anything is tracked, so the key only has\n // to say which options are on it.\n const key = JSON.stringify(this.#options);\n if (\n key === this.#appliedKey &&\n anchor === this.#appliedAnchor &&\n floating === this.#appliedFloating\n ) {\n return;\n }\n this.#detach();\n this.#appliedKey = key;\n this.#appliedAnchor = anchor;\n this.#appliedFloating = floating;\n if (anchor && floating) this.#attach(anchor, floating);\n }\n\n #attach(anchor: Element, floating: HTMLElement): void {\n const id = Symbol(\"anchored-attach\");\n this.#attachId = id;\n this.#stop = attachPositioning(anchor, floating, this.#options, (result) =>\n this.#onComputed(id, floating, result),\n );\n }\n\n #detach(): void {\n this.#stop?.();\n this.#stop = null;\n this.#attachId = null;\n }\n\n /**\n * Reflects the resolved side onto the CSS hook and announces the placement.\n *\n * The pass carries the attach that started it and the element it positioned,\n * and lands only while that attach is still the live one: `autoUpdate`'s\n * cleanup stops further updates but cannot cancel one already computing, so a\n * pass superseded mid-flight would otherwise write and dispatch afterwards.\n * The element cannot stand in for that identity — an option change re-attaches\n * against the very same pair.\n */\n #onComputed(id: symbol, floating: HTMLElement, result: PositionResult): void {\n if (id !== this.#attachId) return;\n floating.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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stimeo-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Headless Stimulus UI framework for Ruby on Rails — behavior-only, accessible components driven by HTML attributes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -92,15 +92,15 @@
|
|
|
92
92
|
"@hotwired/stimulus": "3.2.2",
|
|
93
93
|
"@hotwired/turbo": "8.0.23",
|
|
94
94
|
"@types/node": "24.13.3",
|
|
95
|
-
"@vitest/coverage-istanbul": "4.1.
|
|
95
|
+
"@vitest/coverage-istanbul": "4.1.11",
|
|
96
96
|
"axe-core": "4.13.0",
|
|
97
|
-
"happy-dom": "20.11.
|
|
97
|
+
"happy-dom": "20.11.15",
|
|
98
98
|
"tsup": "8.5.1",
|
|
99
99
|
"typescript": "6.0.3",
|
|
100
|
-
"vitest": "4.1.
|
|
100
|
+
"vitest": "4.1.11",
|
|
101
101
|
"vitest-axe": "0.1.0"
|
|
102
102
|
},
|
|
103
103
|
"overrides": {
|
|
104
|
-
"rollup": "4.
|
|
104
|
+
"rollup": "4.63.1"
|
|
105
105
|
}
|
|
106
106
|
}
|