stimeo-ui 0.16.0 → 0.17.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 +31 -0
- package/README.md +8 -1
- package/dist/cable/index.d.ts +79 -23
- package/dist/cable/index.js +363 -436
- package/dist/cable/index.js.map +1 -1
- package/dist/controllers/accordion_controller.d.ts +4 -2
- package/dist/controllers/accordion_controller.js +166 -55
- package/dist/controllers/accordion_controller.js.map +1 -1
- package/dist/controllers/alert_dialog_controller.d.ts +12 -2
- package/dist/controllers/alert_dialog_controller.js +862 -221
- package/dist/controllers/alert_dialog_controller.js.map +1 -1
- package/dist/controllers/announcer_controller.d.ts +35 -5
- package/dist/controllers/announcer_controller.js +135 -198
- package/dist/controllers/announcer_controller.js.map +1 -1
- package/dist/controllers/aspect_ratio_controller.js +0 -10
- package/dist/controllers/aspect_ratio_controller.js.map +1 -1
- package/dist/controllers/auto_submit_controller.d.ts +30 -6
- package/dist/controllers/auto_submit_controller.js +271 -127
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/avatar_controller.js +171 -105
- package/dist/controllers/avatar_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.js +0 -130
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/bulk_select_controller.d.ts +18 -0
- package/dist/controllers/bulk_select_controller.js +262 -52
- package/dist/controllers/bulk_select_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.d.ts +24 -5
- package/dist/controllers/calendar_controller.js +175 -209
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +34 -7
- package/dist/controllers/carousel_controller.js +461 -399
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/character_counter_controller.d.ts +17 -1
- package/dist/controllers/character_counter_controller.js +246 -156
- package/dist/controllers/character_counter_controller.js.map +1 -1
- package/dist/controllers/checkbox_controller.d.ts +7 -2
- package/dist/controllers/checkbox_controller.js +32 -74
- package/dist/controllers/checkbox_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.d.ts +16 -5
- package/dist/controllers/clipboard_controller.js +168 -147
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.d.ts +16 -4
- package/dist/controllers/collapsible_controller.js +301 -120
- package/dist/controllers/collapsible_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +18 -6
- package/dist/controllers/color_picker_controller.js +149 -139
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_controller.d.ts +23 -5
- package/dist/controllers/combobox_controller.js +360 -133
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.d.ts +45 -14
- package/dist/controllers/command_palette_controller.js +970 -358
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/conditional_fields_controller.d.ts +15 -2
- package/dist/controllers/conditional_fields_controller.js +166 -124
- package/dist/controllers/conditional_fields_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.d.ts +49 -12
- package/dist/controllers/confirm_controller.js +939 -248
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +31 -4
- package/dist/controllers/context_menu_controller.js +256 -128
- package/dist/controllers/context_menu_controller.js.map +1 -1
- package/dist/controllers/count_up_controller.d.ts +13 -1
- package/dist/controllers/count_up_controller.js +73 -21
- package/dist/controllers/count_up_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.d.ts +45 -1
- package/dist/controllers/countdown_controller.js +183 -148
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +21 -2
- package/dist/controllers/currency_input_controller.js +134 -158
- package/dist/controllers/currency_input_controller.js.map +1 -1
- package/dist/controllers/data_grid_controller.d.ts +15 -4
- package/dist/controllers/data_grid_controller.js +114 -186
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +30 -5
- package/dist/controllers/date_range_picker_controller.js +288 -206
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.d.ts +13 -1
- package/dist/controllers/dialog_controller.js +858 -223
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.d.ts +24 -7
- package/dist/controllers/direct_upload_controller.js +73 -153
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/dirty_form_controller.js +14 -48
- package/dist/controllers/dirty_form_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.d.ts +19 -0
- package/dist/controllers/dismissible_controller.js +175 -21
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/drawer_controller.d.ts +39 -11
- package/dist/controllers/drawer_controller.js +933 -341
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/dropdown_controller.d.ts +31 -1
- package/dist/controllers/dropdown_controller.js +312 -82
- package/dist/controllers/dropdown_controller.js.map +1 -1
- package/dist/controllers/editable_controller.d.ts +12 -1
- package/dist/controllers/editable_controller.js +158 -68
- package/dist/controllers/editable_controller.js.map +1 -1
- package/dist/controllers/empty_state_controller.js +67 -53
- package/dist/controllers/empty_state_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.d.ts +60 -18
- package/dist/controllers/file_dropzone_controller.js +311 -207
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/filter_controller.js +67 -39
- package/dist/controllers/filter_controller.js.map +1 -1
- package/dist/controllers/flash_controller.d.ts +31 -8
- package/dist/controllers/flash_controller.js +172 -314
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +12 -3
- package/dist/controllers/focus_controller.js +654 -240
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_field_controller.js +147 -132
- package/dist/controllers/form_field_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.js +13 -86
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.d.ts +41 -9
- package/dist/controllers/frame_loading_controller.js +264 -213
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.d.ts +22 -2
- package/dist/controllers/highlight_controller.js +107 -75
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/hover_card_controller.d.ts +56 -3
- package/dist/controllers/hover_card_controller.js +309 -118
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/idle_controller.d.ts +42 -1
- package/dist/controllers/idle_controller.js +267 -101
- package/dist/controllers/idle_controller.js.map +1 -1
- package/dist/controllers/input_mask_controller.js +71 -72
- package/dist/controllers/input_mask_controller.js.map +1 -1
- package/dist/controllers/intersection_controller.d.ts +32 -13
- package/dist/controllers/intersection_controller.js +147 -106
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.d.ts +16 -0
- package/dist/controllers/lazy_frame_controller.js +9 -65
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.d.ts +34 -2
- package/dist/controllers/listbox_controller.js +306 -204
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.d.ts +1 -1
- package/dist/controllers/local_time_controller.js +54 -64
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +11 -3
- package/dist/controllers/masonry_controller.js +70 -93
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/menu_controller.d.ts +32 -7
- package/dist/controllers/menu_controller.js +214 -156
- package/dist/controllers/menu_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.d.ts +4 -4
- package/dist/controllers/menubar_controller.js +170 -329
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/meter_controller.d.ts +20 -0
- package/dist/controllers/meter_controller.js +151 -67
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +32 -4
- package/dist/controllers/multi_select_controller.js +593 -358
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/navigation_menu_controller.d.ts +8 -1
- package/dist/controllers/navigation_menu_controller.js +124 -232
- package/dist/controllers/navigation_menu_controller.js.map +1 -1
- package/dist/controllers/nested_form_controller.d.ts +10 -0
- package/dist/controllers/nested_form_controller.js +352 -182
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/network_status_controller.d.ts +23 -0
- package/dist/controllers/network_status_controller.js +242 -54
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +26 -2
- package/dist/controllers/number_input_controller.js +320 -263
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/optimistic_controller.js +16 -76
- package/dist/controllers/optimistic_controller.js.map +1 -1
- package/dist/controllers/otp_controller.d.ts +20 -4
- package/dist/controllers/otp_controller.js +198 -193
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/overflow_indicator_controller.d.ts +5 -0
- package/dist/controllers/overflow_indicator_controller.js +116 -113
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.d.ts +48 -11
- package/dist/controllers/overflow_menu_controller.js +343 -283
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.d.ts +35 -6
- package/dist/controllers/pagination_controller.js +295 -119
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +45 -8
- package/dist/controllers/password_reveal_controller.js +389 -121
- package/dist/controllers/password_reveal_controller.js.map +1 -1
- package/dist/controllers/password_strength_controller.d.ts +14 -5
- package/dist/controllers/password_strength_controller.js +99 -153
- package/dist/controllers/password_strength_controller.js.map +1 -1
- package/dist/controllers/persist_controller.d.ts +11 -4
- package/dist/controllers/persist_controller.js +237 -103
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.d.ts +19 -7
- package/dist/controllers/pointer_drag_controller.js +185 -165
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +38 -2
- package/dist/controllers/popover_controller.js +236 -97
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +44 -4
- package/dist/controllers/portal_controller.js +209 -76
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/preview_guard_controller.d.ts +10 -4
- package/dist/controllers/preview_guard_controller.js +232 -116
- package/dist/controllers/preview_guard_controller.js.map +1 -1
- package/dist/controllers/progress_controller.d.ts +11 -0
- package/dist/controllers/progress_controller.js +125 -56
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.d.ts +14 -5
- package/dist/controllers/radio_group_controller.js +117 -137
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.d.ts +43 -3
- package/dist/controllers/range_slider_controller.js +192 -105
- package/dist/controllers/range_slider_controller.js.map +1 -1
- package/dist/controllers/rating_controller.d.ts +20 -4
- package/dist/controllers/rating_controller.js +418 -207
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/read_more_controller.d.ts +2 -0
- package/dist/controllers/read_more_controller.js +134 -63
- package/dist/controllers/read_more_controller.js.map +1 -1
- package/dist/controllers/reading_progress_controller.d.ts +1 -1
- package/dist/controllers/reading_progress_controller.js +147 -135
- package/dist/controllers/reading_progress_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.d.ts +18 -4
- package/dist/controllers/relative_time_controller.js +145 -110
- package/dist/controllers/relative_time_controller.js.map +1 -1
- package/dist/controllers/reset_on_restore_controller.d.ts +85 -0
- package/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +97 -38
- package/dist/controllers/reset_on_restore_controller.js.map +1 -0
- package/dist/controllers/resizable_controller.d.ts +28 -2
- package/dist/controllers/resizable_controller.js +255 -139
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/roving_controller.js +39 -84
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.d.ts +7 -3
- package/dist/controllers/scroll_area_controller.js +428 -225
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_restore_controller.js +0 -53
- package/dist/controllers/scroll_restore_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.d.ts +28 -12
- package/dist/controllers/scroll_visibility_controller.js +395 -145
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.d.ts +6 -1
- package/dist/controllers/scrollspy_controller.js +181 -242
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.d.ts +19 -4
- package/dist/controllers/separator_controller.js +267 -140
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.d.ts +64 -7
- package/dist/controllers/sidebar_controller.js +1023 -386
- package/dist/controllers/sidebar_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.d.ts +23 -0
- package/dist/controllers/skeleton_controller.js +248 -97
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +27 -1
- package/dist/controllers/slider_controller.js +159 -79
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/smart_sticky_header_controller.d.ts +21 -3
- package/dist/controllers/smart_sticky_header_controller.js +162 -63
- package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
- package/dist/controllers/sortable_controller.js +0 -112
- package/dist/controllers/sortable_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.d.ts +39 -4
- package/dist/controllers/spinner_controller.js +277 -169
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.d.ts +5 -0
- package/dist/controllers/step_indicator_controller.js +99 -46
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stepper_controller.d.ts +12 -3
- package/dist/controllers/stepper_controller.js +114 -73
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +6 -0
- package/dist/controllers/stick_to_bottom_controller.js +262 -138
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/sticky_observer_controller.d.ts +11 -0
- package/dist/controllers/sticky_observer_controller.js +214 -49
- package/dist/controllers/sticky_observer_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.d.ts +23 -8
- package/dist/controllers/submit_once_controller.js +294 -205
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.d.ts +12 -1
- package/dist/controllers/switch_controller.js +33 -60
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tabs_controller.d.ts +2 -2
- package/dist/controllers/tabs_controller.js +35 -29
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +12 -0
- package/dist/controllers/tags_input_controller.js +136 -145
- package/dist/controllers/tags_input_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.d.ts +10 -0
- package/dist/controllers/textarea_autosize_controller.js +71 -58
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/theme_controller.d.ts +21 -9
- package/dist/controllers/theme_controller.js +130 -112
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.d.ts +18 -1
- package/dist/controllers/time_picker_controller.js +146 -80
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toast_controller.d.ts +36 -11
- package/dist/controllers/toast_controller.js +186 -297
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.d.ts +12 -3
- package/dist/controllers/toggle_group_controller.js +95 -136
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.js +68 -98
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +45 -3
- package/dist/controllers/tooltip_controller.js +158 -124
- package/dist/controllers/tooltip_controller.js.map +1 -1
- package/dist/controllers/transition_controller.d.ts +22 -4
- package/dist/controllers/transition_controller.js +123 -119
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +21 -11
- package/dist/controllers/tree_view_controller.js +150 -345
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.js +6370 -9346
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +23 -3
- package/dist/inspector/cli.js +92 -37
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +92 -56
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +8 -8
- package/dist/inspector/manifest.json +787 -100
- package/dist/positioning/index.d.ts +8 -0
- package/dist/positioning/index.js +148 -47
- package/dist/positioning/index.js.map +1 -1
- package/package.json +6 -6
- package/dist/controllers/reset_before_cache_controller.d.ts +0 -76
- package/dist/controllers/reset_before_cache_controller.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/arrow_step.ts","../../src/utils/escape_layer.ts","../../src/utils/safe_timeout.ts","../../src/utils/state_reason.ts","../../src/controllers/context_menu_controller.ts"],"names":[],"mappings":"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","file":"context_menu_controller.js","sourcesContent":["import { isRtl } from \"./logical_scroll\";\n\n/**\n * Turns an arrow key into a **logical** step: `+1` for \"next\", `-1` for\n * \"previous\", `0` when the key names neither.\n *\n * APG defines the horizontal pair as *next / previous* and says a vertical\n * arrangement swaps in Down/Up for the same meaning — so the pair is one axis's\n * spelling of an order, and the order reverses with the writing direction. Only\n * the horizontal pair reverses. Down/Up name an axis the writing direction does\n * not mirror, and returning them unchanged is the point: many controllers fold\n * both pairs into one branch, where swapping the branches under RTL would flip\n * the vertical axis too — a bug that reads as \"the arrows work\" until someone\n * presses Down.\n *\n * **Direction is read from the element the caller passes, which should be the\n * container that lays the items out** — not the focused child. A child may carry\n * its own `dir` (an LTR input inside an RTL form is ordinary authoring), and\n * probing per handler makes two handlers disagree at the boundary between them.\n *\n * This decides direction only. Whether the axis is even active (an\n * `orientation=\"horizontal\"` widget ignoring Down/Up), how far the step lands,\n * and what wrapping does all stay with the caller.\n *\n * **It encodes the list-order convention: `ArrowDown` is *next*.** Widgets that\n * pair the arrows by *value* instead — `ArrowUp` meaning \"more\", as a rating or a\n * slider does — must not use this, or their vertical axis inverts. Reverse the\n * horizontal pair on its own there.\n *\n * @example\n * ```ts\n * const step = logicalArrowStep(event.key, this.element);\n * if (step === 0) return;\n * this.#roving.setActive(rovingMove(current, length, step, \"wrap\"), { focus: true });\n * ```\n */\nexport function logicalArrowStep(key: string, element: Element): 1 | -1 | 0 {\n if (key === \"ArrowDown\") return 1;\n if (key === \"ArrowUp\") return -1;\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return 0;\n const forward = isRtl(element) ? \"ArrowLeft\" : \"ArrowRight\";\n return key === forward ? 1 : -1;\n}\n\n/**\n * Rewrites `key` so an existing LTR-shaped branch keeps working under RTL:\n * `ArrowRight` and `ArrowLeft` trade places, everything else passes through.\n *\n * The alternative — negating a delta — silently breaks handlers whose two\n * horizontal branches are **not mirror images**. A grid that clamps one edge but\n * not the other, or a segmented field guarding `index > 0` on one side and\n * `index < length - 1` on the other, ends up applying the wrong guard to the\n * wrong direction. Swapping the key leaves each branch, guards and all, exactly\n * where its author put it.\n *\n * Same rule as {@link logicalArrowStep} about which element to read: pass the\n * container that lays the items out, not the focused child.\n *\n * @example\n * ```ts\n * switch (logicalArrowKey(event.key, this.element)) {\n * case \"ArrowLeft\": // \"previous\" — whatever direction that is on screen\n * ```\n */\nexport function logicalArrowKey(key: string, element: Element): string {\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return key;\n if (!isRtl(element)) return key;\n return key === \"ArrowRight\" ? \"ArrowLeft\" : \"ArrowRight\";\n}\n\n/** Modifiers a widget may claim on an arrow key, named for the `allow` list. */\nexport type ArrowModifier = \"alt\" | \"ctrl\" | \"meta\" | \"shift\";\n\n/**\n * True when an arrow key arrived carrying a modifier the widget must leave to\n * the browser: return without calling `preventDefault()` and without moving any\n * state.\n *\n * A bare arrow belongs to the widget; a chorded one usually does not.\n * `Alt`/`Meta` plus a horizontal arrow is history back/forward on every desktop\n * browser, and a widget that swallows it makes the shortcut work or not\n * depending on where focus happens to sit — a coin-flip the user cannot see.\n *\n * `allow` is for the combinations APG assigns to a pattern **and the widget\n * actually implements** — today only Combobox's optional `Alt+Down`/`Alt+Up`.\n * Listing one the widget does not implement defeats the point: the chord then\n * runs the plain-arrow branch, which is exactly what this guard exists to stop.\n * Non-arrow keys return `false`, so chorded letters and\n * `Control+Home`/`Control+End` are untouched.\n *\n * @example\n * ```ts\n * if (isReservedArrowChord(event)) return;\n * ```\n */\nexport function isReservedArrowChord(\n event: KeyboardEvent,\n allow: readonly ArrowModifier[] = [],\n): boolean {\n if (!event.key.startsWith(\"Arrow\")) return false;\n return (\n (event.altKey && !allow.includes(\"alt\")) ||\n (event.ctrlKey && !allow.includes(\"ctrl\")) ||\n (event.metaKey && !allow.includes(\"meta\")) ||\n (event.shiftKey && !allow.includes(\"shift\"))\n );\n}\n\n/**\n * True when a press arrived carrying any modifier, for the keys the browser and\n * the OS own outright: return without calling `preventDefault()` and without\n * moving any state.\n *\n * `Control+Home` and `Control+End` jump the document to its ends, and a widget\n * that swallows them makes the shortcut work or not depending on where focus\n * happens to sit. {@link isReservedArrowChord} answers the same question for the\n * arrows, but returns `false` for every other key so that this one can decide.\n *\n * There is no `allow` list here on purpose. APG assigns no modifier chord to\n * `Home`/`End`, so a widget that wanted one would be claiming a combination the\n * pattern never gave it.\n *\n * @example\n * ```ts\n * case \"Home\":\n * case \"End\":\n * if (hasModifierChord(event)) return;\n * ```\n */\nexport function hasModifierChord(event: KeyboardEvent): boolean {\n return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;\n}\n","/**\n * Single resolver for layered Escape dismissal.\n *\n * Every Escape-dismissable overlay layer (modal focus traps, disclosure\n * overlays like dropdown/popover/menu, and hover-triggered transient layers)\n * registers here while it is open. One document-level listener per document\n * resolves each press to exactly one owner and invokes that layer's\n * {@link EscapeLayerOptions.onDismiss} — controllers never listen for a\n * dismissing Escape themselves.\n *\n * @remarks\n * Each document owns an activation-ordered stack. The owner of a press is the\n * **topmost layer whose {@link EscapeLayerOptions.claims} passes**; a layer\n * that declines is transparent, so a background overlay opened behind a modal\n * never blocks it. Because a layer opened from within another is necessarily\n * activated later, LIFO order is also inner-first for nested layers — no DOM\n * inspection is needed.\n *\n * The shared listener runs in the document bubble phase and honors\n * `event.defaultPrevented`, so an element-level widget handler that consumes\n * Escape first (an editor cancelling its edit, a combobox closing its list)\n * always wins over every registered layer — the deepest handler resolves the\n * press. A keydown that is part of an IME composition (`event.isComposing`)\n * cancels the composition, never a layer, and is ignored here for every layer\n * at once.\n *\n * A WeakMap keeps documents collectible; the listener is installed only while\n * a document's stack is non-empty, and controller lifecycle hooks guarantee\n * that disconnected layers never remain registered.\n */\n\n/** Behavior a layer registers when it activates. */\nexport interface EscapeLayerOptions {\n /**\n * Dismisses the layer. Called by the shared resolver when this layer owns a\n * press; the resolver has already consumed the event (`preventDefault()`),\n * so the callback only needs to close and place focus per the widget's\n * contract.\n */\n onDismiss: () => void;\n /**\n * Whether the layer claims the current press. Evaluated per press, so it can\n * depend on live state (e.g. \"focus is inside me or fell to the body\"). A\n * declining layer is skipped and the next layer down is consulted; omitting\n * it means the layer always claims while active.\n */\n claims?: () => boolean;\n}\n\n/** A document's stack plus the one shared listener bound to it. */\ninterface EscapeLayerRegistry {\n stack: EscapeLayer[];\n onKeydown: (event: KeyboardEvent) => void;\n}\n\n/**\n * Claims predicate shared by the click-opened disclosure overlays (dropdown /\n * popover / navigation-menu / menu / context-menu / menubar): the layer claims\n * a press while focus is inside `element`, or after focus fell to the body —\n * a click on non-focusable overlay content blurs to `<body>`, and Escape must\n * still close the overlay (the \"body-focus rescue\"). A press made after focus\n * moved to another interactive element is declined, so closing never yanks\n * focus away from where the user deliberately went.\n */\nexport function claimsWhileFocusWithin(element: Element): () => boolean {\n return () => {\n const active = element.ownerDocument.activeElement;\n return active === null || active === element.ownerDocument.body || element.contains(active);\n };\n}\n\nexport class EscapeLayer {\n static readonly #registries = new WeakMap<Document, EscapeLayerRegistry>();\n\n #ownerDocument: Document | null = null;\n /** Dismissal callback while active; `null` when inactive. */\n #onDismiss: (() => void) | null = null;\n /** Live predicate deciding whether the layer claims a press; `null` = always. */\n #claims: (() => boolean) | null = null;\n\n /**\n * Activates this layer at the top of its document's Escape stack, installing\n * the document's shared resolver listener if this is its first layer.\n * Re-activating an already-active layer moves it to the top.\n */\n activate(ownerDocument: Document = document, options: EscapeLayerOptions): void {\n this.deactivate();\n let registry = EscapeLayer.#registries.get(ownerDocument);\n if (!registry) {\n registry = EscapeLayer.#createRegistry();\n EscapeLayer.#registries.set(ownerDocument, registry);\n ownerDocument.addEventListener(\"keydown\", registry.onKeydown);\n }\n registry.stack.push(this);\n this.#ownerDocument = ownerDocument;\n this.#onDismiss = options.onDismiss;\n this.#claims = options.claims ?? null;\n }\n\n /**\n * Removes this layer from its document's Escape stack, uninstalling the\n * shared listener when the stack empties. Safe to call when inactive.\n */\n deactivate(): void {\n const ownerDocument = this.#ownerDocument;\n if (!ownerDocument) return;\n\n const registry = EscapeLayer.#registries.get(ownerDocument);\n if (registry) {\n const index = registry.stack.lastIndexOf(this);\n if (index >= 0) registry.stack.splice(index, 1);\n if (registry.stack.length === 0) {\n ownerDocument.removeEventListener(\"keydown\", registry.onKeydown);\n EscapeLayer.#registries.delete(ownerDocument);\n }\n }\n this.#ownerDocument = null;\n this.#onDismiss = null;\n this.#claims = null;\n }\n\n /**\n * Whether this active layer would own a press right now: it is the topmost\n * layer whose {@link EscapeLayerOptions.claims} passes. Exposed for tests\n * and diagnostics — production dismissal goes through the shared listener.\n */\n get ownsEscape(): boolean {\n const ownerDocument = this.#ownerDocument;\n if (!ownerDocument) return false;\n const registry = EscapeLayer.#registries.get(ownerDocument);\n if (!registry) return false;\n return EscapeLayer.#resolveOwner(registry.stack) === this;\n }\n\n /** Builds a document's registry with its shared resolver listener. */\n static #createRegistry(): EscapeLayerRegistry {\n const registry: EscapeLayerRegistry = {\n stack: [],\n onKeydown: (event: KeyboardEvent): void => {\n if (event.key !== \"Escape\" || event.defaultPrevented || event.isComposing) return;\n const owner = EscapeLayer.#resolveOwner(registry.stack);\n if (!owner) return;\n event.preventDefault();\n owner.#onDismiss?.();\n },\n };\n return registry;\n }\n\n /** The topmost stack layer whose claims predicate passes, or `null`. */\n static #resolveOwner(stack: EscapeLayer[]): EscapeLayer | null {\n for (let index = stack.length - 1; index >= 0; index--) {\n const layer = stack[index];\n if (!layer) continue;\n if (layer.#claims && !layer.#claims()) continue;\n return layer;\n }\n return null;\n }\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Largest delay a timer can hold: the platform stores it in a 32-bit signed\n * integer, and anything larger overflows to `1`, so a delay meant to be far in\n * the future fires almost immediately. A declared delay above this bound names\n * no delay at all, and a controller reading one falls back to its default.\n */\nexport const MAX_TIMER_DELAY_MS = 2_147_483_647;\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","/**\n * Why a component's public open/closed state moved, carried as `detail.reason`\n * on the state event that reports the move.\n *\n * The attribute a component publishes (`aria-expanded`, `hidden`, `data-state`)\n * only says *what* the state is now. A subscriber that mirrors the state\n * elsewhere, or reports it, needs *why*: closing on `Escape` and closing because\n * the consumer called the action are the same attribute write and different\n * events to the page around it.\n *\n * `\"api\"` is the one every subscriber has to look at. A close the consumer asked\n * for arrives back at their own listener, so a listener that closes something\n * else on `close` loops unless it ignores its own calls.\n *\n * | Reason | The state moved because |\n * | --- | --- |\n * | `\"user\"` | a control of this component was operated |\n * | `\"select\"` | an item inside it was activated |\n * | `\"escape\"` | `Escape` was pressed and this layer owned it |\n * | `\"outside\"` | a pointer landed outside it, or on its backdrop |\n * | `\"focus\"` | focus entered or left it |\n * | `\"pointer\"` | the pointer entered or left it |\n * | `\"scroll\"` | a tracked scroll container scrolled |\n * | `\"api\"` | a public action was called with no DOM event |\n */\nexport type StateReason =\n | \"user\"\n | \"select\"\n | \"escape\"\n | \"outside\"\n | \"focus\"\n | \"pointer\"\n | \"scroll\"\n | \"api\";\n\n/** Focus modality: the events a focus move delivers to an action. */\nconst FOCUS_EVENTS = new Set([\"blur\", \"focus\", \"focusin\", \"focusout\"]);\n\n/** Pointer modality: the crossing events an action is bound to for hover. */\nconst POINTER_EVENTS = new Set([\"mouseenter\", \"mouseleave\", \"pointerenter\", \"pointerleave\"]);\n\n/**\n * Reads the interaction behind a **public action** from the DOM event it was\n * handed.\n *\n * Stimulus always passes the event to an action it invoked from `data-action`,\n * and a consumer calling the method themselves passes nothing — which is what\n * separates `\"api\"` from the rest.\n *\n * Only an action entry may resolve a reason this way. Inside the component the\n * event type no longer identifies the interaction: one `click` is the trigger\n * being pressed, the page outside being pressed, and an item being activated,\n * and those paths pass their own reason instead.\n *\n * @param event - The event the action received, if any.\n * @returns `\"api\"` with no event, else the modality the event type names, else\n * `\"user\"`.\n *\n * @example\n * ```ts\n * toggle(event?: Event): void {\n * this.#apply(!this.#isOpen, stateReasonFor(event));\n * }\n * ```\n */\nexport function stateReasonFor(event?: Event | null): \"user\" | \"focus\" | \"pointer\" | \"api\" {\n if (!event) return \"api\";\n if (FOCUS_EVENTS.has(event.type)) return \"focus\";\n if (POINTER_EVENTS.has(event.type)) return \"pointer\";\n return \"user\";\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { isReservedArrowChord } from \"../utils/arrow_step\";\nimport { claimsWhileFocusWithin, EscapeLayer } from \"../utils/escape_layer\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\nimport { type StateReason, stateReasonFor } from \"../utils/state_reason\";\n\n/**\n * Headless, accessible **context menu** behavior.\n *\n * Markup contract (identifier: `stimeo--context-menu`):\n * <div data-controller=\"stimeo--context-menu\">\n * <div data-stimeo--context-menu-target=\"region\" tabindex=\"0\"\n * aria-haspopup=\"menu\" aria-controls=\"ctx\"\n * data-action=\"contextmenu->stimeo--context-menu#open\n * keydown->stimeo--context-menu#onRegionKeydown\">…</div>\n * <ul id=\"ctx\" role=\"menu\" data-stimeo--context-menu-target=\"menu\" hidden>\n * <li role=\"none\">\n * <button role=\"menuitem\" tabindex=\"-1\"\n * data-stimeo--context-menu-target=\"item\"\n * data-action=\"click->stimeo--context-menu#activate\n * keydown->stimeo--context-menu#onItemKeydown\">…</button>\n * </li>\n * </ul>\n * </div>\n *\n * Implements the WAI-ARIA APG **Menu** pattern. The trigger is a `contextmenu`\n * event or the `Shift+F10` / `ContextMenu` key rather than a button click, the\n * menu is shown at the pointer coordinate, open state is exposed as `data-state`\n * rather than `aria-expanded`, and each item carries its own `data-action`\n * bindings.\n *\n * @remarks\n * Behavior only — the controller reflects the click coordinate as the CSS custom\n * properties `--stimeo--context-menu-x` / `--stimeo--context-menu-y` on the menu\n * so the consumer's CSS can place it (works standalone, no positioning module\n * required). Viewport-edge flip/shift is delegated to the opt-in\n * `stimeo-ui/positioning` module, which this controller never imports.\n *\n * Open state is exposed on the region as `data-state` (`open`/`closed`) — a CSS\n * hook, not an ARIA one. `aria-expanded` is deliberately *not* set on the region\n * because it is a generic container, not a role that supports that state (doing so\n * is an ARIA violation); the region's static `aria-haspopup=\"menu\"` advertises the\n * popup, and assistive tech perceives the open state when focus moves into the\n * `role=\"menu\"`.\n *\n * Behavior provided:\n * - `contextmenu` on the region suppresses the browser menu and opens this one at\n * the pointer; `Shift+F10` / `ContextMenu` opens it at the region's center.\n * - On open, focus moves to the first item; the region's `data-state` syncs.\n * - Roving focus inside the menu: `ArrowUp`/`ArrowDown` (wrapping), `Home`/`End`.\n * - Activating an enabled item (click / native `Enter`/`Space` on the button)\n * closes the menu and restores focus to the region. `aria-disabled` activation\n * is blocked before consumer click handlers run.\n * - `Escape` closes and restores focus to the region. While open the menu is a\n * layer on the shared `EscapeLayer` stack; it claims a press only while\n * focus is inside the controller or fell to the body, so one keypress closes\n * exactly one layer. `Tab` lets the browser move focus first, then closes on\n * the next task. An outside click or context-menu invocation closes without\n * stealing focus from its destination.\n * - Each move of the open state is reported: `stimeo--context-menu:open` and\n * `stimeo--context-menu:close` dispatch `{ reason: StateReason }`, after the\n * state attributes are written. Both are informational, so neither is\n * cancelable. A call that leaves the state where it already was, the\n * normalization in {@link connect}, and {@link disconnect} are all silent.\n *\n * Roving focus skips `hidden` and natively `disabled` items. An\n * `aria-disabled=\"true\"` item stays reachable by arrow keys — APG marks that\n * attribute precisely for controls that must remain discoverable — while its\n * activation is suppressed, so it announces itself and does nothing.\n */\nexport class ContextMenuController extends Controller<HTMLElement> {\n static override targets = [\"region\", \"menu\", \"item\"];\n static actions = [\"activate\", \"onItemKeydown\", \"onRegionKeydown\", \"open\"] as const;\n static events = [\"close\", \"open\"] as const;\n\n declare readonly regionTarget: HTMLElement;\n declare readonly menuTarget: HTMLElement;\n declare readonly itemTargets: HTMLButtonElement[];\n declare readonly hasRegionTarget: boolean;\n declare readonly hasMenuTarget: boolean;\n\n readonly #timers = new SafeTimeout();\n\n /** Escape-stack membership while open; the shared resolver dismisses via it. */\n readonly #escapeLayer = new EscapeLayer();\n\n /** Whether state moves are reported: set once `connect()` settled the baseline. */\n #reporting = false;\n\n /** Starts closed and registers delegated activation and outside-pointer listeners. */\n override connect(): void {\n this.#closeMenu(\"api\");\n this.element.addEventListener(\"click\", this.#onItemClickCapture, true);\n document.addEventListener(\"click\", this.#onOutsidePointer, true);\n document.addEventListener(\"contextmenu\", this.#onOutsidePointer, true);\n this.#reporting = true;\n }\n\n /** Releases the listeners, stack membership, and pending Tab-close task. */\n override disconnect(): void {\n this.#reporting = false;\n this.#timers.clearAll();\n this.#escapeLayer.deactivate();\n this.element.removeEventListener(\"click\", this.#onItemClickCapture, true);\n document.removeEventListener(\"click\", this.#onOutsidePointer, true);\n document.removeEventListener(\"contextmenu\", this.#onOutsidePointer, true);\n }\n\n /**\n * Opens the menu from a `contextmenu` event: suppresses the native menu and\n * places this one at the pointer coordinate.\n */\n open(event: MouseEvent): void {\n event.preventDefault();\n this.#openAt(event.clientX, event.clientY, stateReasonFor(event));\n }\n\n /** Keyboard entry on the region: `Shift+F10` / `ContextMenu` open at center. */\n onRegionKeydown(event: KeyboardEvent): void {\n // A descendant widget that already claimed the key (a grabbed drag handle, a\n // nested menu) must not ALSO act on it — composition depends on this yield.\n if (event.defaultPrevented) return;\n const isContextKey = event.key === \"ContextMenu\" || (event.key === \"F10\" && event.shiftKey);\n if (!isContextKey) return;\n event.preventDefault();\n const rect = this.hasRegionTarget\n ? this.regionTarget.getBoundingClientRect()\n : { left: 0, top: 0, width: 0, height: 0 };\n this.#openAt(rect.left + rect.width / 2, rect.top + rect.height / 2, stateReasonFor(event));\n }\n\n /** Roving focus and closing keys inside the menu. */\n onItemKeydown(event: KeyboardEvent): void {\n // A descendant widget that already claimed the key (a grabbed drag handle, a\n // nested menu) must not ALSO act on it — composition depends on this yield.\n if (event.defaultPrevented) return;\n if (isReservedArrowChord(event)) return;\n const items = this.#navigableItems;\n const currentIndex = items.indexOf(event.currentTarget as HTMLButtonElement);\n\n switch (event.key) {\n case \"ArrowDown\":\n event.preventDefault();\n if (items.length > 0) {\n const nextIndex = currentIndex < 0 ? 0 : (currentIndex + 1) % items.length;\n items[nextIndex]?.focus();\n }\n break;\n case \"ArrowUp\":\n event.preventDefault();\n if (items.length > 0) {\n const previousIndex = currentIndex < 0 ? items.length - 1 : currentIndex - 1;\n items[(previousIndex + items.length) % items.length]?.focus();\n }\n break;\n case \"Home\":\n event.preventDefault();\n items[0]?.focus();\n break;\n case \"End\":\n event.preventDefault();\n items[items.length - 1]?.focus();\n break;\n case \"Tab\":\n // Closing synchronously removes the focused item before the browser's\n // default Tab action, which can restart traversal at the document head.\n this.#timers.clearAll();\n this.#timers.set(() => this.#closeMenu(\"focus\"), 0);\n break;\n default:\n break;\n }\n }\n\n /** Closes after an item is activated and restores focus to the region. */\n activate(): void {\n this.#closeAndRestore(\"select\");\n }\n\n /** Opens the menu at viewport coordinates `(x, y)` and focuses the first item. */\n #openAt(x: number, y: number, reason: StateReason): void {\n if (!this.hasMenuTarget) return;\n this.#timers.clearAll();\n const was = this.#isOpen;\n this.#escapeLayer.activate(document, {\n onDismiss: () => this.#closeAndRestore(\"escape\"),\n claims: claimsWhileFocusWithin(this.element),\n });\n this.menuTarget.style.setProperty(\"--stimeo--context-menu-x\", `${x}px`);\n this.menuTarget.style.setProperty(\"--stimeo--context-menu-y\", `${y}px`);\n this.menuTarget.hidden = false;\n if (this.hasRegionTarget) this.regionTarget.setAttribute(\"data-state\", \"open\");\n if (!was && this.#reporting) this.dispatch(\"open\", { detail: { reason }, cancelable: false });\n // A subscriber may close it again from the handler above; focusing then puts\n // the caret on an item nobody can see.\n if (!this.#isOpen) return;\n this.#navigableItems[0]?.focus();\n }\n\n /** Hides the menu, reflects the collapsed state on the region, and reports a move. */\n #closeMenu(reason: StateReason): void {\n this.#timers.clearAll();\n this.#escapeLayer.deactivate();\n if (!this.hasMenuTarget) return;\n const was = this.#isOpen;\n this.menuTarget.hidden = true;\n if (this.hasRegionTarget) this.regionTarget.setAttribute(\"data-state\", \"closed\");\n if (was && this.#reporting) this.dispatch(\"close\", { detail: { reason }, cancelable: false });\n }\n\n /** Closes the menu and returns focus to the region (Escape / activation). */\n #closeAndRestore(reason: StateReason): void {\n this.#closeMenu(reason);\n if (this.hasRegionTarget) this.regionTarget.focus();\n }\n\n /**\n * Closes when a click or context-menu invocation lands outside this instance.\n * Both subscriptions observe in the capture phase, so the target is judged\n * against the tree the user actually pressed even when a consumer handler\n * detaches it, and application code that stops bubbling cannot leave the menu\n * open.\n */\n readonly #onOutsidePointer = (event: MouseEvent): void => {\n if (this.#isOpen && !this.element.contains(event.target as Node)) this.#closeMenu(\"outside\");\n };\n\n /**\n * Captures clicks so `aria-disabled` commands cannot reach consumer handlers.\n * Native Enter/Space activation also synthesizes a click and is blocked here.\n */\n readonly #onItemClickCapture = (event: MouseEvent): void => {\n const target = event.target;\n if (!(target instanceof Node)) return;\n const disabled = this.itemTargets.some(\n (item) => item.getAttribute(\"aria-disabled\") === \"true\" && item.contains(target),\n );\n if (!disabled) return;\n event.preventDefault();\n event.stopImmediatePropagation();\n };\n\n /** Menu items eligible for roving focus (excludes hidden / natively disabled). */\n get #navigableItems(): HTMLButtonElement[] {\n return this.itemTargets.filter((item) => this.#isNavigable(item));\n }\n\n /**\n * An item can take roving focus unless it is `hidden` or a natively `disabled`\n * form control. CSS-only visibility is not detectable here and is the\n * consumer's responsibility.\n *\n * **`aria-disabled=\"true\"` stays reachable.** APG separates the two attributes\n * by intent: `disabled` is for controls whose existence can be inferred from a\n * neighbour (a greyed Next next to a Prev), while `aria-disabled` marks a\n * control that must stay *discoverable* — and it names menu items as the\n * example. Skipping it would hide the command's existence from a keyboard user\n * entirely. Activation is suppressed separately, so the item announces itself\n * and does nothing.\n */\n #isNavigable(item: HTMLButtonElement): boolean {\n if (item.hasAttribute(\"hidden\")) return false;\n return !item.disabled;\n }\n\n /** Whether the menu is currently visible. */\n get #isOpen(): boolean {\n return this.hasMenuTarget && !this.menuTarget.hidden;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/state_reason.ts","../../src/utils/action_source.ts","../../src/utils/arrow_step.ts","../../src/utils/shared_registry.ts","../../src/utils/attribute_lease.ts","../../src/utils/escape_layer.ts","../../src/utils/safe_timeout.ts","../../src/controllers/context_menu_controller.ts"],"names":[],"mappings":"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","file":"context_menu_controller.js","sourcesContent":["/**\n * Why a component's public state moved, carried as `detail.reason`\n * on the state event that reports the move.\n *\n * The attribute a component publishes (`aria-expanded`, `hidden`, `data-state`)\n * only says *what* the state is now. A subscriber that mirrors the state\n * elsewhere, or reports it, needs *why*: closing on `Escape` and closing because\n * the consumer called the action are the same attribute write and different\n * events to the page around it.\n *\n * `\"api\"` is the one every subscriber has to look at. A close the consumer asked\n * for arrives back at their own listener, so a listener that closes something\n * else on `close` loops unless it ignores its own calls.\n *\n * | Reason | The state moved because |\n * | --- | --- |\n * | `\"user\"` | a control of this component was operated |\n * | `\"select\"` | an item inside it was activated |\n * | `\"escape\"` | `Escape` was pressed and this layer owned it |\n * | `\"outside\"` | a pointer landed outside it, or on its backdrop |\n * | `\"focus\"` | focus entered or left it |\n * | `\"pointer\"` | the pointer entered or left it |\n * | `\"scroll\"` | a tracked scroll container scrolled |\n * | `\"api\"` | a public action was called with no DOM event |\n * | `\"timeout\"` | an automatic timer advanced or dismissed the state |\n * | `\"limit\"` | a capacity limit dismissed an item |\n * | `\"media\"` | an operating-system media preference changed |\n */\nexport type StateReason =\n | \"user\"\n | \"select\"\n | \"escape\"\n | \"outside\"\n | \"focus\"\n | \"pointer\"\n | \"scroll\"\n | \"api\"\n | \"timeout\"\n | \"limit\"\n | \"media\";\n\n/** Focus modality: the events a focus move delivers to an action. */\nconst FOCUS_EVENTS = new Set([\"blur\", \"focus\", \"focusin\", \"focusout\"]);\n\n/** Pointer modality: the crossing events an action is bound to for hover. */\nconst POINTER_EVENTS = new Set([\"mouseenter\", \"mouseleave\", \"pointerenter\", \"pointerleave\"]);\n\n/**\n * Reads the interaction behind a **public action** from the DOM event it was\n * handed.\n *\n * Stimulus always passes the event to an action it invoked from `data-action`,\n * and a consumer calling the method themselves passes nothing — which is what\n * separates `\"api\"` from the rest.\n *\n * Only an action entry may resolve a reason this way. Inside the component the\n * event type no longer identifies the interaction: one `click` is the trigger\n * being pressed, the page outside being pressed, and an item being activated,\n * and those paths pass their own reason instead.\n *\n * @param event - The event the action received, if any.\n * @returns `\"api\"` with no event, else the modality the event type names, else\n * `\"user\"`.\n *\n * @example\n * ```ts\n * toggle(event?: Event): void {\n * this.#apply(!this.#isOpen, stateReasonFor(event));\n * }\n * ```\n */\nexport function stateReasonFor(event?: Event | null): \"user\" | \"focus\" | \"pointer\" | \"api\" {\n if (!event) return \"api\";\n if (FOCUS_EVENTS.has(event.type)) return \"focus\";\n if (POINTER_EVENTS.has(event.type)) return \"pointer\";\n return \"user\";\n}\n","import { stateReasonFor } from \"./state_reason\";\n\n/** The DOM binding and origin of a public action, or its explicit API element. */\nexport interface ActionSource {\n readonly event: Event | null;\n readonly host: Element | null;\n readonly origin: Element | null;\n readonly reason: ReturnType<typeof stateReasonFor>;\n}\n\n/**\n * Normalizes an action's DOM event or explicit element without choosing an owned target.\n *\n * Event hosts and origins stay separate so consumers can yield to nested controls.\n * An explicit element supplies both, with no DOM event and an `\"api\"` reason.\n * Consumers resolve current targets and enforce their own scope and interaction rules.\n *\n * @param source - The DOM event, explicit element, or absent optional argument.\n * @returns The source elements, original event and public interaction reason.\n */\nexport function actionSource(source?: Event | HTMLElement): ActionSource {\n if (source instanceof HTMLElement) {\n return { event: null, host: source, origin: source, reason: \"api\" };\n }\n const event = source ?? null;\n return {\n event,\n host: event?.currentTarget instanceof Element ? event.currentTarget : null,\n origin: event?.target instanceof Element ? event.target : null,\n reason: stateReasonFor(event),\n };\n}\n","import { isRtl } from \"./logical_scroll\";\n\n/**\n * Turns an arrow key into a **logical** step: `+1` for \"next\", `-1` for\n * \"previous\", `0` when the key names neither.\n *\n * APG defines the horizontal pair as *next / previous* and says a vertical\n * arrangement swaps in Down/Up for the same meaning — so the pair is one axis's\n * spelling of an order, and the order reverses with the writing direction. Only\n * the horizontal pair reverses. Down/Up name an axis the writing direction does\n * not mirror, and returning them unchanged is the point: many controllers fold\n * both pairs into one branch, where swapping the branches under RTL would flip\n * the vertical axis too — a bug that reads as \"the arrows work\" until someone\n * presses Down.\n *\n * **Direction is read from the element the caller passes, which should be the\n * container that lays the items out** — not the focused child. A child may carry\n * its own `dir` (an LTR input inside an RTL form is ordinary authoring), and\n * probing per handler makes two handlers disagree at the boundary between them.\n *\n * This decides direction only. Whether the axis is even active (an\n * `orientation=\"horizontal\"` widget ignoring Down/Up), how far the step lands,\n * and what wrapping does all stay with the caller.\n *\n * **It encodes the list-order convention: `ArrowDown` is *next*.** Widgets that\n * pair the arrows by *value* instead — `ArrowUp` meaning \"more\", as a rating or a\n * slider does — must not use this, or their vertical axis inverts. Reverse the\n * horizontal pair on its own there.\n *\n * @example\n * ```ts\n * const step = logicalArrowStep(event.key, this.element);\n * if (step === 0) return;\n * this.#roving.setActive(rovingMove(current, length, step, \"wrap\"), { focus: true });\n * ```\n */\nexport function logicalArrowStep(key: string, element: Element): 1 | -1 | 0 {\n if (key === \"ArrowDown\") return 1;\n if (key === \"ArrowUp\") return -1;\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return 0;\n const forward = isRtl(element) ? \"ArrowLeft\" : \"ArrowRight\";\n return key === forward ? 1 : -1;\n}\n\n/**\n * Rewrites `key` so an existing LTR-shaped branch keeps working under RTL:\n * `ArrowRight` and `ArrowLeft` trade places, everything else passes through.\n *\n * The alternative — negating a delta — silently breaks handlers whose two\n * horizontal branches are **not mirror images**. A grid that clamps one edge but\n * not the other, or a segmented field guarding `index > 0` on one side and\n * `index < length - 1` on the other, ends up applying the wrong guard to the\n * wrong direction. Swapping the key leaves each branch, guards and all, exactly\n * where its author put it.\n *\n * Same rule as {@link logicalArrowStep} about which element to read: pass the\n * container that lays the items out, not the focused child.\n *\n * @example\n * ```ts\n * switch (logicalArrowKey(event.key, this.element)) {\n * case \"ArrowLeft\": // \"previous\" — whatever direction that is on screen\n * ```\n */\nexport function logicalArrowKey(key: string, element: Element): string {\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return key;\n if (!isRtl(element)) return key;\n return key === \"ArrowRight\" ? \"ArrowLeft\" : \"ArrowRight\";\n}\n\n/** Modifiers a widget may claim on an arrow key, named for the `allow` list. */\nexport type ArrowModifier = \"alt\" | \"ctrl\" | \"meta\" | \"shift\";\n\n/**\n * True when an arrow key arrived carrying a modifier the widget must leave to\n * the browser: return without calling `preventDefault()` and without moving any\n * state.\n *\n * A bare arrow belongs to the widget; a chorded one usually does not.\n * `Alt`/`Meta` plus a horizontal arrow is history back/forward on every desktop\n * browser, and a widget that swallows it makes the shortcut work or not\n * depending on where focus happens to sit — a coin-flip the user cannot see.\n *\n * `allow` is for the combinations APG assigns to a pattern **and the widget\n * actually implements** — today only Combobox's optional `Alt+Down`/`Alt+Up`.\n * Listing one the widget does not implement defeats the point: the chord then\n * runs the plain-arrow branch, which is exactly what this guard exists to stop.\n * Non-arrow keys return `false`, so chorded letters and\n * `Control+Home`/`Control+End` are untouched.\n *\n * @example\n * ```ts\n * if (isReservedArrowChord(event)) return;\n * ```\n */\nexport function isReservedArrowChord(\n event: KeyboardEvent,\n allow: readonly ArrowModifier[] = [],\n): boolean {\n if (!event.key.startsWith(\"Arrow\")) return false;\n return (\n (event.altKey && !allow.includes(\"alt\")) ||\n (event.ctrlKey && !allow.includes(\"ctrl\")) ||\n (event.metaKey && !allow.includes(\"meta\")) ||\n (event.shiftKey && !allow.includes(\"shift\"))\n );\n}\n\n/**\n * True when a press arrived carrying any modifier, for the keys the browser and\n * the OS own outright: return without calling `preventDefault()` and without\n * moving any state.\n *\n * `Control+Home` and `Control+End` jump the document to its ends, and a widget\n * that swallows them makes the shortcut work or not depending on where focus\n * happens to sit. {@link isReservedArrowChord} answers the same question for the\n * arrows, but returns `false` for every other key so that this one can decide.\n *\n * There is no `allow` list here on purpose. APG assigns no modifier chord to\n * `Home`/`End`, so a widget that wanted one would be claiming a combination the\n * pattern never gave it.\n *\n * @example\n * ```ts\n * case \"Home\":\n * case \"End\":\n * if (hasModifierChord(event)) return;\n * ```\n */\nexport function hasModifierChord(event: KeyboardEvent): boolean {\n return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;\n}\n","/**\n * One registry per page for every copy of the library.\n *\n * Each `stimeo-ui/controllers/*` file, and each opt-in entry, is built on its own and carries\n * its own copy of every utility it uses, so a page that imports controllers one by one, or one\n * of them next to the barrel, evaluates a utility several times. A registry that coordinates\n * instances of different controllers — the stacks of leased values, the loans of a\n * `tabindex`, the Escape layers, the focus traps — must not be split by that: a dialog inside\n * a drawer, or a character counter and a form field on one input, coordinate whichever files\n * they came from. So such a registry lives once on `globalThis`, under a `Symbol.for` key the\n * copies agree on, and the first copy to ask for it creates it.\n *\n * The key names the registry and the version of its shape\n * (`stimeo-ui.<utility>.registry.v<n>`). Copies whose registries have the same shape share\n * one; a release that changes the shape changes the version, so a copy of another release\n * keeps a registry of its own instead of reading entries it does not understand — the two\n * then coordinate within each release only. An entry another copy put in a registry is read\n * through its fields alone, never through `#private` members, which belong to the class of\n * the copy that created it.\n */\n\n/**\n * The registry stored under `key`, created by `create` on first use.\n *\n * @param key - `stimeo-ui.<utility>.registry.v<n>`: the version is that of the shape `create`\n * returns.\n * @param create - Builds the empty registry.\n */\nexport function sharedRegistry<T extends object>(key: string, create: () => T): T {\n const symbol = Symbol.for(key);\n const scope = globalThis as unknown as Record<symbol, T | undefined>;\n const existing = scope[symbol];\n if (existing !== undefined) return existing;\n const registry = create();\n Object.defineProperty(globalThis, symbol, { value: registry });\n return registry;\n}\n","import { sharedRegistry } from \"./shared_registry\";\n\n/** What a lease's record says: the author's value, its last write, the leases beneath. */\ninterface AttributeLeaseRecord {\n readonly original: string | null;\n readonly written: string | null;\n /** Whether it hands back what the lease right beneath it wrote last. */\n readonly linked: boolean;\n /** The records of the other owners' leases beneath it, nearest first. */\n readonly beneath: readonly string[];\n}\n\n/** One lease's hold on one element's attribute, in the stack every lease of it shares. */\ninterface AttributeHold {\n /** The lease holding it; `null` for a hold read from a copy's record until its lease takes it. */\n lease: object | null;\n readonly record: string;\n written: string | null;\n /** What it hands back with nothing beneath it: the author's value. */\n readonly original: string | null;\n /** Whether it hands back what the hold beneath it wrote last instead. */\n linked: boolean;\n}\n\n/** The holds on each element by attribute, and every lease's record name by attribute. */\ninterface AttributeLeaseRegistry {\n readonly stacks: WeakMap<Element, Map<string, AttributeHold[]>>;\n readonly names: Map<string, Set<string>>;\n}\n\n/** Whether a parsed record entry is an attribute value: a string, or `null` for absence. */\nfunction isAttributeValue(value: unknown): value is string | null {\n return value === null || typeof value === \"string\";\n}\n\n/**\n * Reads the record a lease left on `element`: the author's value, the value the lease wrote\n * last, whether it hands back the value beneath it (`false` before the names when it does not)\n * and the records beneath it. `undefined` when the record is missing or holds anything else.\n */\nfunction readLeaseRecord(element: Element, record: string): AttributeLeaseRecord | undefined {\n try {\n const parsed: unknown = JSON.parse(element.getAttribute(record) ?? \"null\");\n if (!Array.isArray(parsed)) return undefined;\n const [original, written, ...rest] = parsed as unknown[];\n const linked = rest[0] !== false;\n const beneath = linked ? rest : rest.slice(1);\n if (!beneath.every((under): under is string => typeof under === \"string\")) return undefined;\n return isAttributeValue(original) && isAttributeValue(written)\n ? { original, written, linked, beneath }\n : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * The value `element`'s author wrote for `attribute`, as the record a lease of any owner leaves\n * on the element carries it; `undefined` while no lease leaves one, as when nothing has moved\n * the attribute off the author's value. A field a reveal turned from `password` into `text`\n * still reads `\"password\"` here.\n */\nexport function leasedAuthorValue(element: Element, attribute: string): string | null | undefined {\n const suffix = `-${attribute}-lease`;\n for (const name of element.getAttributeNames()) {\n if (!name.startsWith(\"data-\") || !name.endsWith(suffix)) continue;\n const record = readLeaseRecord(element, name);\n if (record) return record.original;\n }\n return undefined;\n}\n\n/**\n * Temporarily controls one attribute across a changing set of elements.\n *\n * The first write remembers the authored value, including the distinction between\n * an absent attribute and an authored empty string. Returning a lease restores that\n * value only while the attribute still matches the controller's last write. If a\n * consumer changed it in the meantime, the consumer owns the new value and teardown\n * leaves it alone.\n *\n * A `null` write deliberately removes the attribute while retaining the lease. This\n * is useful for derived ARIA whose valid absence is itself controller state, such as\n * an unbounded `aria-valuemin` or a blank spinbutton's `aria-valuenow`.\n *\n * **Every lease of an attribute shares one stack of holds per element**, whatever its owner\n * and whichever file of the package constructed it (the stacks are one per page): the holds\n * sit in the order their leases first wrote the element, each with the value it wrote last.\n * The element's value is the highest hold's that carries it, and the page's when none does.\n * A write reaches the element unless a hold above the writer carries the element's value: the\n * top hold always shows its writes, a hold beneath it shows nothing while a hold above keeps\n * its value on the element, and once the page has put a value there that no hold carries, the\n * next write of any hold replaces it. A return gives a value back only from the hold the\n * element's value is: the value the hold beneath it wrote last, or the author's once nothing\n * is beneath it; any other return only leaves the stack, and the page's value stays. Holds\n * that wrote the same value are told apart by the stack, never by the value: the higher one\n * keeps it. A first write over a top hold whose value the page has replaced takes the\n * element's value as the author's. Two instances of one controller on one element — an\n * element outside their scope — stack the same way.\n *\n * A **base lease** (`{ base: true }`) holds the element's own state rather than a value over\n * it — a form field's ARIA on its control is one — so its first write goes to the bottom of\n * the stack, beneath the leases of other owners already there: a temporary value another\n * owner leased stays shown and returns to the base lease's latest value.\n *\n * **The stack travels with the element.** While a hold shows a value other than the author's,\n * lies on another hold or is covered by one, the element carries its owner's record\n * `data-<owner>-<attribute>-lease`: JSON of the author's value and the value the hold wrote\n * last (each a string, or `null` for an absent attribute), `false` where the hold hands back\n * the author's value rather than the latest value of the hold beneath it, and the records of\n * every hold beneath it, nearest first. Returning the last lease of an owner removes its\n * record. A copy of the element — a page Turbo restores from its cache is one — therefore\n * carries the stack, and the first lease to touch the copy reads it back under the record\n * names of the leases constructed so far, which include every lease that wrote a record the\n * cache holds, and orders the holds by the records each one lists beneath it. Each lease takes\n * the hold its owner's record describes over in place. Complete records for distinct owners\n * preserve their order across a copy. The records describe holds by owner, not by instance:\n * one owner holding the element more than once leaves only its topmost hold's record, which\n * the first of its leases to write or return on the copy takes. Lower holds of that owner and\n * their positions are absent; mutually listed owners cannot express their full live order.\n * A hold that showed the author's value alone carries no record and is taken by its first\n * write on the copy. Missing or unreadable records also leave their holds unknown, so copying\n * the DOM does not promise identical decisions for every live stack. Without a record it can\n * read, a new stack takes the current value as the author's. A Turbo morph removes records\n * absent from the server HTML; existing holds remain in memory until their leases return.\n *\n * The lease has no lifecycle of its own, so it never subscribes to document events,\n * and nothing rewinds a live page before Turbo caches it: `turbo:before-cache` also\n * fires on pages that stay.\n */\nexport class AttributeLease<T extends Element = Element> {\n /** The holds on each element, bottom first, and the record names; one per page. */\n static readonly #registry = sharedRegistry(\n \"stimeo-ui.attribute-lease.registry.v1\",\n (): AttributeLeaseRegistry => ({ stacks: new WeakMap(), names: new Map() }),\n );\n readonly #attribute: string;\n readonly #record: string;\n readonly #base: boolean;\n readonly #held = new Set<T>();\n\n /**\n * @param attribute - The attribute whose temporary values this lease owns.\n * @param owner - Names the record on the element; the controller's identifier.\n * @param options - `base`: written beneath the other owners' leases.\n */\n constructor(attribute: string, owner: string, options: { readonly base?: boolean } = {}) {\n this.#attribute = attribute;\n this.#record = `data-${owner}-${attribute}-lease`;\n this.#base = options.base === true;\n const names = AttributeLease.#registry.names;\n names.set(attribute, (names.get(attribute) ?? new Set<string>()).add(this.#record));\n }\n\n /** Writes or removes the leased attribute while preserving its authored value. */\n write(element: T, value: string | null): void {\n const stack = this.#stack(element);\n const hold = this.#take(stack) ?? this.#acquire(element, stack);\n this.#held.add(element);\n const shows = !this.#covered(element, stack, hold);\n hold.written = value;\n this.#mark(element, stack);\n if (shows) this.#reflect(element, value);\n }\n\n /**\n * Returns one lease, also on a restored copy, to the value the hold beneath it wrote last or\n * the author's, without overwriting a value a consumer wrote since. Says whether there was a\n * lease to return: a hold this lease has, or the hold a copy's record describes while no live\n * lease of the owner has taken it — a copy of a page an earlier connection wrote carries one.\n */\n return(element: T): boolean {\n const held = this.#held.delete(element);\n const known = AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);\n const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];\n const hold = this.#take(stack);\n if (!hold) return false;\n const carries = element.getAttribute(this.#attribute) === hold.written;\n const owns = carries && !this.#covered(element, stack, hold);\n const index = stack.indexOf(hold);\n stack.splice(index, 1);\n const above = stack[index];\n if (above) above.linked &&= hold.linked;\n this.#mark(element, stack);\n if (!owns) return true;\n const below = hold.linked ? stack[index - 1] : undefined;\n this.#reflect(element, below ? below.written : hold.original);\n return true;\n }\n\n /** Returns every lease it holds, with the same ownership check as {@link return}. */\n returnAll(): void {\n for (const element of Array.from(this.#held)) this.return(element);\n }\n\n /** The element's stack, read from its records the first time. */\n #stack(element: T): AttributeHold[] {\n const stacks = AttributeLease.#registry.stacks;\n const byAttribute = stacks.get(element) ?? new Map<string, AttributeHold[]>();\n stacks.set(element, byAttribute);\n const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);\n byAttribute.set(this.#attribute, stack);\n return stack;\n }\n\n /** The holds a copy's records describe, each above every hold its record lists beneath. */\n #seed(element: T): AttributeHold[] {\n const found: (AttributeLeaseRecord & { readonly record: string })[] = [];\n for (const record of AttributeLease.#registry.names.get(this.#attribute) ?? []) {\n const read = readLeaseRecord(element, record);\n if (read) found.push({ ...read, record });\n }\n const depth = ({ beneath }: (typeof found)[number]) =>\n beneath.filter((name) => found.some((other) => other.record === name)).length;\n return found\n .sort((one, other) => depth(one) - depth(other))\n .map(({ record, written, original, linked }, index) => ({\n lease: null,\n record,\n written,\n original,\n linked: linked && index > 0,\n }));\n }\n\n /** Its hold in `stack`, taking over one read from its record. */\n #take(stack: AttributeHold[]): AttributeHold | undefined {\n const hold =\n stack.find((held) => held.lease === this) ??\n stack.find((held) => held.lease === null && held.record === this.#record);\n if (hold) hold.lease = this;\n return hold;\n }\n\n /** A new hold: on top, over the top hold while the element shows it; a base one at the bottom. */\n #acquire(element: T, stack: AttributeHold[]): AttributeHold {\n const current = element.getAttribute(this.#attribute);\n const top = stack.at(-1);\n const bottom = stack[0];\n const over = top?.written === current ? top : undefined;\n const from = this.#base ? bottom : over;\n const hold: AttributeHold = {\n lease: this,\n record: this.#record,\n written: current,\n original: from ? from.original : current,\n linked: !this.#base && over !== undefined,\n };\n if (this.#base && bottom) {\n bottom.linked = true;\n stack.unshift(hold);\n } else stack.push(hold);\n return hold;\n }\n\n /** Whether a hold above `hold` carries the element's value, which is then that hold's. */\n #covered(element: T, stack: readonly AttributeHold[], hold: AttributeHold): boolean {\n const current = element.getAttribute(this.#attribute);\n return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);\n }\n\n /** Leaves each owner's record of its topmost hold, or none where nothing needs one. */\n #mark(element: T, stack: readonly AttributeHold[]): void {\n const records = new Map<string, string | null>([[this.#record, null]]);\n stack.forEach((hold, index) => {\n const names = stack.slice(0, index).map((under) => under.record);\n const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);\n const marker = hold.linked || beneath.length === 0 ? [] : [false];\n records.set(\n hold.record,\n hold.written === hold.original && beneath.length === 0 && index === stack.length - 1\n ? null\n : JSON.stringify([hold.original, hold.written, ...marker, ...beneath]),\n );\n });\n for (const [name, recorded] of records) {\n if (element.getAttribute(name) === recorded) continue;\n if (recorded === null) element.removeAttribute(name);\n else element.setAttribute(name, recorded);\n }\n }\n\n /** Reflects only a real value transition, avoiding self-triggered mutation work. */\n #reflect(element: T, value: string | null): void {\n if (element.getAttribute(this.#attribute) === value) return;\n if (value === null) element.removeAttribute(this.#attribute);\n else element.setAttribute(this.#attribute, value);\n }\n}\n","/**\n * Single resolver for layered Escape dismissal.\n *\n * Every Escape-dismissable overlay layer (modal focus traps, disclosure\n * overlays like dropdown/popover/menu, and hover-triggered transient layers)\n * registers here while it is open. One document-level listener per document\n * resolves each press to exactly one owner and invokes that layer's\n * {@link EscapeLayerOptions.onDismiss} — controllers never listen for a\n * dismissing Escape themselves.\n *\n * @remarks\n * Each document owns an activation-ordered stack. The owner of a press is the\n * **topmost layer whose {@link EscapeLayerOptions.claims} passes**; a layer\n * that declines is transparent, so a background overlay opened behind a modal\n * never blocks it. Because a layer opened from within another is necessarily\n * activated later, LIFO order is also inner-first for nested layers — no DOM\n * inspection is needed.\n *\n * The shared listener runs in the document bubble phase and honors\n * `event.defaultPrevented`, so an element-level widget handler that consumes\n * Escape first (an editor cancelling its edit, a combobox closing its list)\n * always wins over every registered layer — the deepest handler resolves the\n * press. A keydown that is part of an IME composition (`event.isComposing`)\n * cancels the composition, never a layer, and is ignored here for every layer\n * at once.\n *\n * A WeakMap keeps documents collectible; the listener is installed only while\n * a document's stack is non-empty, and controller lifecycle hooks guarantee\n * that disconnected layers never remain registered. The map is one per page\n * (`sharedRegistry`), so the layers of controllers imported from different\n * files share one stack and one listener.\n */\n\nimport { sharedRegistry } from \"./shared_registry\";\n\n/** Behavior a layer registers when it activates. */\nexport interface EscapeLayerOptions {\n /**\n * Dismisses the layer. Called by the shared resolver when this layer owns a\n * press; the resolver has already consumed the event (`preventDefault()`),\n * so the callback only needs to close and place focus per the widget's\n * contract.\n */\n onDismiss: () => void;\n /**\n * Whether the layer claims the current press. Evaluated per press, so it can\n * depend on live state (e.g. \"focus is inside me or fell to the body\"). A\n * declining layer is skipped and the next layer down is consulted; omitting\n * it means the layer always claims while active.\n */\n claims?: () => boolean;\n}\n\n/** An active layer on its document's stack: what the resolver reads, from any copy. */\ninterface LayerEntry {\n readonly onDismiss: () => void;\n readonly claims: (() => boolean) | null;\n}\n\n/** A document's stack plus the one shared listener bound to it. */\ninterface EscapeLayerRegistry {\n readonly stack: LayerEntry[];\n readonly onKeydown: (event: KeyboardEvent) => void;\n}\n\n/**\n * Claims predicate shared by the click-opened disclosure overlays (dropdown /\n * popover / navigation-menu / menu / context-menu / menubar): the layer claims\n * a press while focus is inside `element`, or after focus fell to the body —\n * a click on non-focusable overlay content blurs to `<body>`, and Escape must\n * still close the overlay (the \"body-focus rescue\"). A press made after focus\n * moved to another interactive element is declined, so closing never yanks\n * focus away from where the user deliberately went.\n */\nexport function claimsWhileFocusWithin(element: Element): () => boolean {\n return () => {\n const active = element.ownerDocument.activeElement;\n return active === null || active === element.ownerDocument.body || element.contains(active);\n };\n}\n\nexport class EscapeLayer {\n static readonly #registries = sharedRegistry(\n \"stimeo-ui.escape-layer.registry.v1\",\n () => new WeakMap<Document, EscapeLayerRegistry>(),\n );\n\n /** The document and the entry of this layer while it is active. */\n #active: { readonly document: Document; readonly entry: LayerEntry } | null = null;\n\n /**\n * Activates this layer at the top of its document's Escape stack, installing\n * the document's shared resolver listener if this is its first layer.\n * Re-activating an already-active layer moves it to the top.\n */\n activate(ownerDocument: Document = document, options: EscapeLayerOptions): void {\n this.deactivate();\n let registry = EscapeLayer.#registries.get(ownerDocument);\n if (!registry) {\n registry = EscapeLayer.#createRegistry();\n EscapeLayer.#registries.set(ownerDocument, registry);\n ownerDocument.addEventListener(\"keydown\", registry.onKeydown);\n }\n const entry = { onDismiss: options.onDismiss, claims: options.claims ?? null };\n registry.stack.push(entry);\n this.#active = { document: ownerDocument, entry };\n }\n\n /**\n * Removes this layer from its document's Escape stack, uninstalling the\n * shared listener when the stack empties. Safe to call when inactive.\n */\n deactivate(): void {\n const active = this.#active;\n if (!active) return;\n\n const registry = EscapeLayer.#registries.get(active.document);\n if (registry) {\n const index = registry.stack.lastIndexOf(active.entry);\n if (index >= 0) registry.stack.splice(index, 1);\n if (registry.stack.length === 0) {\n active.document.removeEventListener(\"keydown\", registry.onKeydown);\n EscapeLayer.#registries.delete(active.document);\n }\n }\n this.#active = null;\n }\n\n /**\n * Whether this active layer would own a press right now: it is the topmost\n * layer whose {@link EscapeLayerOptions.claims} passes. Exposed for tests\n * and diagnostics — production dismissal goes through the shared listener.\n */\n get ownsEscape(): boolean {\n const active = this.#active;\n if (!active) return false;\n const registry = EscapeLayer.#registries.get(active.document);\n if (!registry) return false;\n return EscapeLayer.#resolveOwner(registry.stack) === active.entry;\n }\n\n /** Builds a document's registry with its shared resolver listener. */\n static #createRegistry(): EscapeLayerRegistry {\n const registry: EscapeLayerRegistry = {\n stack: [],\n onKeydown: (event: KeyboardEvent): void => {\n if (event.key !== \"Escape\" || event.defaultPrevented || event.isComposing) return;\n const owner = EscapeLayer.#resolveOwner(registry.stack);\n if (!owner) return;\n event.preventDefault();\n owner.onDismiss();\n },\n };\n return registry;\n }\n\n /** The topmost stack layer whose claims predicate passes, or `null`. */\n static #resolveOwner(stack: readonly LayerEntry[]): LayerEntry | null {\n for (let index = stack.length - 1; index >= 0; index--) {\n const layer = stack[index];\n if (!layer) continue;\n if (layer.claims && !layer.claims()) continue;\n return layer;\n }\n return null;\n }\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\nexport { MAX_TIMER_DELAY_MS } from \"./timer_bounds\";\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { actionSource } from \"../utils/action_source\";\nimport { isReservedArrowChord } from \"../utils/arrow_step\";\nimport { AttributeLease } from \"../utils/attribute_lease\";\nimport { claimsWhileFocusWithin, EscapeLayer } from \"../utils/escape_layer\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\nimport { type StateReason, stateReasonFor } from \"../utils/state_reason\";\n\n/**\n * Headless, accessible **context menu** behavior.\n *\n * Markup contract (identifier: `stimeo--context-menu`):\n * <div data-controller=\"stimeo--context-menu\">\n * <div data-stimeo--context-menu-target=\"region\" tabindex=\"0\"\n * aria-haspopup=\"menu\" aria-controls=\"ctx\"\n * data-action=\"contextmenu->stimeo--context-menu#open\n * keydown->stimeo--context-menu#onRegionKeydown\">…</div>\n * <ul id=\"ctx\" role=\"menu\" data-stimeo--context-menu-target=\"menu\" hidden>\n * <li role=\"none\">\n * <button role=\"menuitem\" tabindex=\"-1\"\n * data-stimeo--context-menu-target=\"item\"\n * data-action=\"click->stimeo--context-menu#activate\n * keydown->stimeo--context-menu#onItemKeydown\">…</button>\n * </li>\n * </ul>\n * </div>\n *\n * Implements the WAI-ARIA APG **Menu** pattern. The trigger is a `contextmenu`\n * event or the `Shift+F10` / `ContextMenu` key rather than a button click, the\n * menu is shown at the pointer coordinate, open state is exposed as `data-state`\n * rather than `aria-expanded`, and each item carries its own `data-action`\n * bindings.\n *\n * @remarks\n * Behavior only — the controller reflects the click coordinate as the CSS custom\n * properties `--stimeo--context-menu-x` / `--stimeo--context-menu-y` on the menu\n * so the consumer's CSS can place it (works standalone, no positioning module\n * required). Viewport-edge flip/shift is delegated to the opt-in\n * `stimeo-ui/positioning` module, which this controller never imports.\n *\n * Open state is exposed on the region as `data-state` (`open`/`closed`) — a CSS\n * hook, not an ARIA one. `aria-expanded` is deliberately *not* set on the region\n * because it is a generic container, not a role that supports that state (doing so\n * is an ARIA violation); the region's static `aria-haspopup=\"menu\"` advertises the\n * popup, and assistive tech perceives the open state when focus moves into the\n * `role=\"menu\"`.\n *\n * Behavior provided:\n * - `contextmenu` on the region suppresses the browser menu and opens this one at\n * the pointer; `Shift+F10` / `ContextMenu` opens it at the region's center.\n * - On open, focus moves to the first item; the region's `data-state` syncs.\n * - Roving focus inside the menu: `ArrowUp`/`ArrowDown` (wrapping), `Home`/`End`.\n * - Activating an enabled item (click / native `Enter`/`Space` on the button)\n * closes the menu and restores focus to the region. `aria-disabled` activation\n * is blocked before consumer click handlers run.\n * - `Escape` closes and restores focus to the region. While open the menu is a\n * layer on the shared `EscapeLayer` stack; it claims a press only while\n * focus is inside the controller or fell to the body, so one keypress closes\n * exactly one layer. `Tab` lets the browser move focus first, then closes on\n * the next task. An outside click or context-menu invocation closes without\n * stealing focus from its destination.\n * - A region or a menu that takes over — in one task, or after an earlier one\n * leaves in a later task — carries the open state, and the menu that takes over\n * an open one is placed where it was opened; focus stays where the swap left it.\n * With no menu left the region reads closed and the menu leaves the Escape stack.\n * One that stops resolving as the target gets back the `data-state` or `hidden` it\n * carried before this controller wrote on it.\n * - Each move of the open state is reported: `stimeo--context-menu:open` and\n * `stimeo--context-menu:close` dispatch `{ reason: StateReason }`, after the\n * state attributes are written. Both are informational, so neither is\n * cancelable. A call that leaves the state where it already was, the\n * normalization in {@link connect}, a region or a menu that takes over, and\n * {@link disconnect} are all silent.\n *\n * Roving focus skips `hidden` and natively `disabled` items. An\n * `aria-disabled=\"true\"` item stays reachable by arrow keys — APG marks that\n * attribute precisely for controls that must remain discoverable — while its\n * activation is suppressed, so it announces itself and does nothing.\n */\nexport class ContextMenuController extends Controller<HTMLElement> {\n /** The custom properties that carry the coordinate the menu was opened at, x then y. */\n static readonly #COORDINATE = [\"--stimeo--context-menu-x\", \"--stimeo--context-menu-y\"] as const;\n\n static override targets = [\"region\", \"menu\", \"item\"];\n static actions = [\"activate\", \"onItemKeydown\", \"onRegionKeydown\", \"open\"] as const;\n static events = [\"close\", \"open\"] as const;\n\n declare readonly regionTarget: HTMLElement;\n declare readonly regionTargets: HTMLElement[];\n declare readonly menuTarget: HTMLElement;\n declare readonly menuTargets: HTMLElement[];\n declare readonly itemTargets: HTMLButtonElement[];\n declare readonly hasRegionTarget: boolean;\n declare readonly hasMenuTarget: boolean;\n\n readonly #timers = new SafeTimeout();\n\n /** Escape-stack membership while open; the shared resolver dismisses via it. */\n readonly #escapeLayer = new EscapeLayer();\n /** Borrows `hidden` on the menu, to give back when an element stops being the menu. */\n readonly #menuHidden = new AttributeLease<HTMLElement>(\"hidden\", this.identifier);\n /** Borrows `data-state` on the region, for the same return. */\n readonly #regionState = new AttributeLease<HTMLElement>(\"data-state\", this.identifier);\n /** The menu the open state was last applied to. */\n #menu: HTMLElement | null = null;\n\n /** Whether `connect()` has run for this connection; target callbacks arrive outside it too. */\n #connected = false;\n\n /** Whether state moves are reported: set once `connect()` settled the baseline. */\n #reporting = false;\n\n /** Starts closed and registers delegated activation and outside-pointer listeners. */\n override connect(): void {\n this.#closeMenu(\"api\");\n this.element.addEventListener(\"click\", this.#onItemClickCapture, true);\n document.addEventListener(\"click\", this.#onOutsidePointer, true);\n document.addEventListener(\"contextmenu\", this.#onOutsidePointer, true);\n this.#connected = true;\n this.#reporting = true;\n }\n\n /** Releases the listeners, stack membership, and pending Tab-close task. */\n override disconnect(): void {\n this.#connected = false;\n this.#reporting = false;\n this.#timers.clearAll();\n this.#escapeLayer.deactivate();\n this.element.removeEventListener(\"click\", this.#onItemClickCapture, true);\n document.removeEventListener(\"click\", this.#onOutsidePointer, true);\n document.removeEventListener(\"contextmenu\", this.#onOutsidePointer, true);\n }\n\n /** Brings a region that arrives after connect to the open state. */\n regionTargetConnected(): void {\n if (this.#connected) this.#reflectRegion();\n }\n\n /**\n * Gives a region that no longer resolves as one its own `data-state` back — after\n * `disconnect()` too, since dropping the identifier leaves the element on the page — and\n * brings the region that stays to the open state.\n */\n regionTargetDisconnected(region: HTMLElement): void {\n if (!this.regionTargets.includes(region)) this.#regionState.return(region);\n if (this.#connected) this.#reflectRegion();\n }\n\n /** Applies the open state to a menu that arrives after connect in front of the others. */\n menuTargetConnected(): void {\n this.#adoptMenu();\n }\n\n /**\n * Applies the open state to the menu left, then gives a menu that no longer resolves as the\n * target its own `hidden` back — after `disconnect()` too, since dropping the identifier\n * leaves the element on the page. The open state is read off the departing menu first,\n * while it still carries it.\n */\n menuTargetDisconnected(menu: HTMLElement): void {\n this.#adoptMenu();\n if (!this.menuTargets.includes(menu)) this.#menuHidden.return(menu);\n }\n\n /**\n * Opens at a mouse event's coordinate, an explicit element's center, or the\n * region's center with no argument. Opening focuses the new menu in all paths.\n * An explicit element may be an anchor outside the controller.\n */\n open(input?: Event | HTMLElement): void {\n const source = actionSource(input);\n source.event?.preventDefault();\n if (source.event instanceof MouseEvent) {\n this.#openAt(source.event.clientX, source.event.clientY, source.reason);\n return;\n }\n const anchor = source.host ?? (this.hasRegionTarget ? this.regionTarget : null);\n const rect = anchor?.getBoundingClientRect() ?? { left: 0, top: 0, width: 0, height: 0 };\n this.#openAt(rect.left + rect.width / 2, rect.top + rect.height / 2, source.reason);\n }\n\n /** Keyboard entry on the region: `Shift+F10` / `ContextMenu` open at center. */\n onRegionKeydown(event: KeyboardEvent): void {\n // A descendant widget that already claimed the key (a grabbed drag handle, a\n // nested menu) must not ALSO act on it — composition depends on this yield.\n if (event.defaultPrevented) return;\n const isContextKey = event.key === \"ContextMenu\" || (event.key === \"F10\" && event.shiftKey);\n if (!isContextKey) return;\n event.preventDefault();\n const rect = this.hasRegionTarget\n ? this.regionTarget.getBoundingClientRect()\n : { left: 0, top: 0, width: 0, height: 0 };\n this.#openAt(rect.left + rect.width / 2, rect.top + rect.height / 2, stateReasonFor(event));\n }\n\n /** Roving focus and closing keys inside the menu. */\n onItemKeydown(event: KeyboardEvent): void {\n // A descendant widget that already claimed the key (a grabbed drag handle, a\n // nested menu) must not ALSO act on it — composition depends on this yield.\n if (event.defaultPrevented) return;\n if (isReservedArrowChord(event)) return;\n const items = this.#navigableItems;\n const currentIndex = items.indexOf(event.currentTarget as HTMLButtonElement);\n\n switch (event.key) {\n case \"ArrowDown\":\n event.preventDefault();\n if (items.length > 0) {\n const nextIndex = currentIndex < 0 ? 0 : (currentIndex + 1) % items.length;\n items[nextIndex]?.focus();\n }\n break;\n case \"ArrowUp\":\n event.preventDefault();\n if (items.length > 0) {\n const previousIndex = currentIndex < 0 ? items.length - 1 : currentIndex - 1;\n items[(previousIndex + items.length) % items.length]?.focus();\n }\n break;\n case \"Home\":\n event.preventDefault();\n items[0]?.focus();\n break;\n case \"End\":\n event.preventDefault();\n items[items.length - 1]?.focus();\n break;\n case \"Tab\":\n // Closing synchronously removes the focused item before the browser's\n // default Tab action, which can restart traversal at the document head.\n this.#timers.clearAll();\n this.#timers.set(() => this.#closeMenu(\"focus\"), 0);\n break;\n default:\n break;\n }\n }\n\n /** Closes after an item is activated and restores focus to the region. */\n activate(): void {\n this.#closeAndRestore(\"select\");\n }\n\n /** Opens the menu at viewport coordinates `(x, y)` and focuses the first item. */\n #openAt(x: number, y: number, reason: StateReason): void {\n if (!this.hasMenuTarget) return;\n this.#timers.clearAll();\n const menu = this.menuTarget;\n const was = this.#isOpen;\n this.#escapeLayer.activate(document, {\n onDismiss: () => this.#closeAndRestore(\"escape\"),\n claims: claimsWhileFocusWithin(this.element),\n });\n menu.style.setProperty(ContextMenuController.#COORDINATE[0], `${x}px`);\n menu.style.setProperty(ContextMenuController.#COORDINATE[1], `${y}px`);\n this.#menuHidden.write(menu, null);\n this.#menu = menu;\n if (this.hasRegionTarget) this.#regionState.write(this.regionTarget, \"open\");\n if (!was && this.#reporting) this.dispatch(\"open\", { detail: { reason }, cancelable: false });\n // A subscriber may close it again from the handler above; focusing then puts\n // the caret on an item nobody can see.\n if (!this.#isOpen) return;\n this.#navigableItems[0]?.focus();\n }\n\n /** Hides the menu, reflects the collapsed state on the region, and reports a move. */\n #closeMenu(reason: StateReason): void {\n this.#timers.clearAll();\n this.#escapeLayer.deactivate();\n const menu = this.hasMenuTarget ? this.menuTarget : null;\n this.#menu = menu;\n if (!menu) return;\n const was = this.#isOpen;\n this.#menuHidden.write(menu, \"\");\n if (this.hasRegionTarget) this.#regionState.write(this.regionTarget, \"closed\");\n if (was && this.#reporting) this.dispatch(\"close\", { detail: { reason }, cancelable: false });\n }\n\n /**\n * Moves the open state onto the menu that is now first, when that menu changed: one that\n * takes over an open menu is shown where that one was opened, and one that arrives with none\n * before it arrives closed. With none left, the menu leaves the Escape stack. The region then\n * reads the menu, changed or not. Focus stays where the swap left it, and nothing is\n * dispatched.\n */\n #adoptMenu(): void {\n if (!this.#connected) return;\n const menu = this.hasMenuTarget ? this.menuTarget : null;\n const previous = this.#menu;\n this.#menu = menu;\n if (!menu) {\n this.#escapeLayer.deactivate();\n } else if (menu !== previous) {\n const open = previous !== null && !previous.hidden;\n if (open) {\n for (const property of ContextMenuController.#COORDINATE) {\n menu.style.setProperty(property, previous.style.getPropertyValue(property));\n }\n }\n this.#menuHidden.write(menu, open ? null : \"\");\n }\n this.#reflectRegion();\n }\n\n /** Writes the open state onto the region that is first. */\n #reflectRegion(): void {\n if (!this.hasRegionTarget) return;\n this.#regionState.write(this.regionTarget, this.#isOpen ? \"open\" : \"closed\");\n }\n\n /** Closes the menu and returns focus to the region (Escape / activation). */\n #closeAndRestore(reason: StateReason): void {\n this.#closeMenu(reason);\n if (this.hasRegionTarget) this.regionTarget.focus();\n }\n\n /**\n * Closes when a click or context-menu invocation lands outside this instance.\n * Both subscriptions observe in the capture phase, so the target is judged\n * against the tree the user actually pressed even when a consumer handler\n * detaches it, and application code that stops bubbling cannot leave the menu\n * open.\n */\n readonly #onOutsidePointer = (event: MouseEvent): void => {\n if (this.#isOpen && !this.element.contains(event.target as Node)) this.#closeMenu(\"outside\");\n };\n\n /**\n * Captures clicks so `aria-disabled` commands cannot reach consumer handlers.\n * Native Enter/Space activation also synthesizes a click and is blocked here.\n */\n readonly #onItemClickCapture = (event: MouseEvent): void => {\n const target = event.target;\n if (!(target instanceof Node)) return;\n const disabled = this.itemTargets.some(\n (item) => item.getAttribute(\"aria-disabled\") === \"true\" && item.contains(target),\n );\n if (!disabled) return;\n event.preventDefault();\n event.stopImmediatePropagation();\n };\n\n /** Menu items eligible for roving focus (excludes hidden / natively disabled). */\n get #navigableItems(): HTMLButtonElement[] {\n return this.itemTargets.filter((item) => this.#isNavigable(item));\n }\n\n /**\n * An item can take roving focus unless it is `hidden` or a natively `disabled`\n * form control. CSS-only visibility is not detectable here and is the\n * consumer's responsibility.\n *\n * **`aria-disabled=\"true\"` stays reachable.** APG separates the two attributes\n * by intent: `disabled` is for controls whose existence can be inferred from a\n * neighbour (a greyed Next next to a Prev), while `aria-disabled` marks a\n * control that must stay *discoverable* — and it names menu items as the\n * example. Skipping it would hide the command's existence from a keyboard user\n * entirely. Activation is suppressed separately, so the item announces itself\n * and does nothing.\n */\n #isNavigable(item: HTMLButtonElement): boolean {\n if (item.hasAttribute(\"hidden\")) return false;\n return !item.disabled;\n }\n\n /** Whether the menu is currently visible. */\n get #isOpen(): boolean {\n return this.hasMenuTarget && !this.menuTarget.hidden;\n }\n}\n"]}
|
|
@@ -16,7 +16,9 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
16
16
|
* real value); `start` animates the displayed text from `from` to it over
|
|
17
17
|
* `duration` ms with an ease-out curve, then restores the exact authored text.
|
|
18
18
|
* With `once` (default) later starts are ignored (`data-count-up-done` records
|
|
19
|
-
* a finished run across Turbo cache restores).
|
|
19
|
+
* a finished run across Turbo cache restores). `from` and `duration` are read as
|
|
20
|
+
* a run starts: a running animation keeps the values it began with, the next run
|
|
21
|
+
* reads the new ones, and a change on its own starts nothing.
|
|
20
22
|
*
|
|
21
23
|
* Only the text node holding the number is animated, so sibling markup — a unit
|
|
22
24
|
* in a `<small>`, a label in a `<b>` — is left where the author put it.
|
|
@@ -51,6 +53,15 @@ declare class CountUpController extends Controller<HTMLElement> {
|
|
|
51
53
|
default: boolean;
|
|
52
54
|
};
|
|
53
55
|
};
|
|
56
|
+
static valueConstraints: {
|
|
57
|
+
duration: {
|
|
58
|
+
readonly finite: true;
|
|
59
|
+
readonly exclusiveMin: 0;
|
|
60
|
+
};
|
|
61
|
+
from: {
|
|
62
|
+
readonly finite: true;
|
|
63
|
+
};
|
|
64
|
+
};
|
|
54
65
|
static actions: readonly ["start"];
|
|
55
66
|
static events: readonly ["end"];
|
|
56
67
|
durationValue: number;
|
|
@@ -61,6 +72,7 @@ declare class CountUpController extends Controller<HTMLElement> {
|
|
|
61
72
|
/**
|
|
62
73
|
* Starts the animation (typically from `stimeo--intersection:enter` via
|
|
63
74
|
* `data-action`). No-ops while running, and after a finished run when `once`.
|
|
75
|
+
* The run keeps the `from` and `duration` read here to its last frame.
|
|
64
76
|
*/
|
|
65
77
|
start(): void;
|
|
66
78
|
}
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
// src/controllers/count_up_controller.ts
|
|
4
|
-
|
|
5
|
-
// src/utils/authored_integer.ts
|
|
6
3
|
var TOKEN_BODY = /\d[\d.,'’’ _]*\d|\d/;
|
|
7
4
|
var SIGN_OPENER = /[\s([]/;
|
|
8
5
|
function authoredInteger(text) {
|
|
@@ -24,29 +21,75 @@ function authoredInteger(text) {
|
|
|
24
21
|
return signed ? -magnitude || 0 : magnitude;
|
|
25
22
|
}
|
|
26
23
|
|
|
27
|
-
|
|
24
|
+
var NUMBER_BOUNDS = {
|
|
25
|
+
finite: { finite: true },
|
|
26
|
+
positive: { finite: true, exclusiveMin: 0 }};
|
|
27
|
+
function matchesNumberBounds(value, bounds) {
|
|
28
|
+
if (!Number.isFinite(value)) {
|
|
29
|
+
const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
|
|
30
|
+
if (direction === null) return false;
|
|
31
|
+
if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
|
|
32
|
+
}
|
|
33
|
+
if (bounds.min !== void 0 && value < bounds.min) return false;
|
|
34
|
+
if (bounds.max !== void 0 && value > bounds.max) return false;
|
|
35
|
+
if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
|
|
36
|
+
if (bounds.integer && !Number.isInteger(value)) return false;
|
|
37
|
+
if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readNumber(raw, fallback, bounds) {
|
|
42
|
+
return matchesNumberBounds(raw, bounds) ? raw : fallback;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
var NumberValueReader = class {
|
|
46
|
+
#lastRejected = /* @__PURE__ */ new Map();
|
|
47
|
+
read(owner, name, raw, fallback, bounds) {
|
|
48
|
+
const resolved = readNumber(raw, fallback, bounds);
|
|
49
|
+
if (matchesNumberBounds(raw, bounds)) {
|
|
50
|
+
this.#lastRejected.delete(name);
|
|
51
|
+
return resolved;
|
|
52
|
+
}
|
|
53
|
+
const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
|
|
54
|
+
const literal = owner.element.getAttribute(attribute);
|
|
55
|
+
if (literal === null) {
|
|
56
|
+
this.#lastRejected.delete(name);
|
|
57
|
+
return resolved;
|
|
58
|
+
}
|
|
59
|
+
if (this.#lastRejected.get(name) !== literal) {
|
|
60
|
+
this.#lastRejected.set(name, literal);
|
|
61
|
+
console.warn(
|
|
62
|
+
`Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return resolved;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
28
69
|
function prefersReducedMotion() {
|
|
29
70
|
return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
30
71
|
}
|
|
31
72
|
|
|
32
|
-
// src/controllers/count_up_controller.ts
|
|
33
73
|
var DEFAULT_DURATION = 1200;
|
|
34
|
-
var CountUpController = class extends Controller {
|
|
74
|
+
var CountUpController = class _CountUpController extends Controller {
|
|
75
|
+
#numbers = new NumberValueReader();
|
|
35
76
|
static values = {
|
|
36
77
|
duration: { type: Number, default: DEFAULT_DURATION },
|
|
37
78
|
from: { type: Number, default: 0 },
|
|
38
79
|
once: { type: Boolean, default: true }
|
|
39
80
|
};
|
|
81
|
+
static valueConstraints = {
|
|
82
|
+
duration: NUMBER_BOUNDS.positive,
|
|
83
|
+
from: NUMBER_BOUNDS.finite
|
|
84
|
+
};
|
|
40
85
|
static actions = ["start"];
|
|
41
86
|
static events = ["end"];
|
|
42
87
|
#frame = null;
|
|
43
|
-
/** The animation length, with a declaration outside its domain read as the default. */
|
|
44
88
|
get #duration() {
|
|
45
|
-
return
|
|
89
|
+
return this.#safeDuration;
|
|
46
90
|
}
|
|
47
|
-
/** The starting value, with a declaration that is not a finite number read as zero. */
|
|
48
91
|
get #from() {
|
|
49
|
-
return
|
|
92
|
+
return this.#safeFrom;
|
|
50
93
|
}
|
|
51
94
|
connect() {
|
|
52
95
|
const ticker = this.#ownedTicker();
|
|
@@ -58,10 +101,6 @@ var CountUpController = class extends Controller {
|
|
|
58
101
|
disconnect() {
|
|
59
102
|
if (this.#frame !== null) this.#settle();
|
|
60
103
|
}
|
|
61
|
-
/**
|
|
62
|
-
* Starts the animation (typically from `stimeo--intersection:enter` via
|
|
63
|
-
* `data-action`). No-ops while running, and after a finished run when `once`.
|
|
64
|
-
*/
|
|
65
104
|
start() {
|
|
66
105
|
if (this.#frame !== null) return;
|
|
67
106
|
if (this.onceValue && this.element.hasAttribute("data-count-up-done")) return;
|
|
@@ -76,9 +115,10 @@ var CountUpController = class extends Controller {
|
|
|
76
115
|
}
|
|
77
116
|
const ticker = this.#wrap(node, authored);
|
|
78
117
|
const from = this.#from;
|
|
118
|
+
const duration = this.#duration;
|
|
79
119
|
const started = performance.now();
|
|
80
120
|
const step = (now) => {
|
|
81
|
-
const t = Math.min((now - started) /
|
|
121
|
+
const t = Math.min((now - started) / duration, 1);
|
|
82
122
|
const eased = 1 - (1 - t) ** 3;
|
|
83
123
|
ticker.textContent = String(Math.round(from + (target - from) * eased));
|
|
84
124
|
if (t < 1) {
|
|
@@ -90,7 +130,6 @@ var CountUpController = class extends Controller {
|
|
|
90
130
|
};
|
|
91
131
|
this.#frame = requestAnimationFrame(step);
|
|
92
132
|
}
|
|
93
|
-
/** The first text node that displays a number, or null when the host has none. */
|
|
94
133
|
#numericNode() {
|
|
95
134
|
const walker = document.createTreeWalker(this.element, NodeFilter.SHOW_TEXT);
|
|
96
135
|
let node = walker.nextNode();
|
|
@@ -99,11 +138,9 @@ var CountUpController = class extends Controller {
|
|
|
99
138
|
}
|
|
100
139
|
return node;
|
|
101
140
|
}
|
|
102
|
-
/** The wrapper this controller published, if one outlived its run. */
|
|
103
141
|
#ownedTicker() {
|
|
104
142
|
return this.element.querySelector("[data-count-up-label]");
|
|
105
143
|
}
|
|
106
|
-
/** Publishes the ticking number inside a named wrapper, replacing `node`. */
|
|
107
144
|
#wrap(node, authored) {
|
|
108
145
|
const ticker = document.createElement("span");
|
|
109
146
|
ticker.setAttribute("data-count-up-label", "true");
|
|
@@ -113,11 +150,9 @@ var CountUpController = class extends Controller {
|
|
|
113
150
|
ticker.append(node);
|
|
114
151
|
return ticker;
|
|
115
152
|
}
|
|
116
|
-
/** Puts `text` back where the wrapper stood, leaving the rest of the host alone. */
|
|
117
153
|
#unwrap(ticker, text) {
|
|
118
154
|
ticker.replaceWith(document.createTextNode(text ?? ticker.textContent ?? ""));
|
|
119
155
|
}
|
|
120
|
-
/** Ends the run: cancels the frame and restores the authored presentation. */
|
|
121
156
|
#settle() {
|
|
122
157
|
if (this.#frame !== null) cancelAnimationFrame(this.#frame);
|
|
123
158
|
this.#frame = null;
|
|
@@ -125,8 +160,25 @@ var CountUpController = class extends Controller {
|
|
|
125
160
|
if (ticker !== null) this.#unwrap(ticker, ticker.getAttribute("aria-label"));
|
|
126
161
|
this.element.setAttribute("data-count-up-done", "true");
|
|
127
162
|
}
|
|
163
|
+
get #safeDuration() {
|
|
164
|
+
return this.#numbers.read(
|
|
165
|
+
this,
|
|
166
|
+
"duration",
|
|
167
|
+
this.durationValue,
|
|
168
|
+
_CountUpController.values.duration.default,
|
|
169
|
+
_CountUpController.valueConstraints.duration
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
get #safeFrom() {
|
|
173
|
+
return this.#numbers.read(
|
|
174
|
+
this,
|
|
175
|
+
"from",
|
|
176
|
+
this.fromValue,
|
|
177
|
+
_CountUpController.values.from.default,
|
|
178
|
+
_CountUpController.valueConstraints.from
|
|
179
|
+
);
|
|
180
|
+
}
|
|
128
181
|
};
|
|
129
182
|
|
|
130
183
|
export { CountUpController };
|
|
131
|
-
//# sourceMappingURL=count_up_controller.js.map
|
|
132
184
|
//# sourceMappingURL=count_up_controller.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/authored_integer.ts","../../src/utils/reduced_motion.ts","../../src/controllers/count_up_controller.ts"],"names":[],"mappings":"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","file":"count_up_controller.js","sourcesContent":["/** Digits plus the characters that may separate them inside one number. */\nconst TOKEN_BODY = /\\d[\\d.,'’’ _]*\\d|\\d/;\n/** A `-` opens a token only against one of these on its left. */\nconst SIGN_OPENER = /[\\s([]/;\n\n/**\n * Reads the integer that a human-formatted string displays.\n *\n * Takes the first numeric token, drops group separators, and truncates any\n * fractional part toward zero — so `\"1,200 users\"`, `\"$1,299.99\"` and\n * `\"1.299,99\"` read 1200, 1299 and 1299 without asking the platform which\n * locale is in play. A separator counts as grouping only where the digits that\n * follow it form a run of exactly three and the leading run could head a\n * grouped number; anything else is the decimal point, and the digits after it\n * are the fraction.\n *\n * `-` counts as a sign only where it opens the token — at the start of the\n * string, or after whitespace or an opening bracket — so a hyphen inside a\n * label (`\"Sign-ups: 1,200\"`, `\"Top-10 users\"`) never becomes one.\n *\n * `\"1.200\"` is genuinely ambiguous (1200 grouped, 1.2 with a decimal point) and\n * reads as 1200: display numbers group far more often than they carry a\n * fraction in exactly three digits.\n *\n * Returns `null` when the string holds no digits at all. What that means is the\n * caller's to decide — starting nothing, or falling back to a base value.\n */\nexport function authoredInteger(text: string): number | null {\n const firstDigit = /\\d/.exec(text);\n if (firstDigit === null) return null;\n\n const at = firstDigit.index;\n const signed =\n at > 0 && text[at - 1] === \"-\" && (at === 1 || SIGN_OPENER.test(text[at - 2] as string));\n\n const body = TOKEN_BODY.exec(text.slice(at)) as RegExpExecArray;\n const runs = body[0].split(/\\D+/);\n const separators = body[0].match(/\\D+/g) ?? [];\n\n // The leading run heads a grouped number only when it is short enough to be a\n // group itself and carries no leading zero; \"0.500\" and \"012,3\" are decimals.\n const lead = runs[0] as string;\n const heads = lead.length <= 3 && !lead.startsWith(\"0\");\n let digits = lead;\n for (let i = 1; heads && i < runs.length; i += 1) {\n if (separators[i - 1]?.length !== 1 || runs[i]?.length !== 3) break;\n digits += runs[i];\n }\n\n const magnitude = Number.parseInt(digits, 10);\n return signed ? -magnitude || 0 : magnitude;\n}\n","/**\n * Shared `prefers-reduced-motion` lookup for the motion-aware controllers.\n *\n * This one-liner keeps the media query string and the environment guard\n * single-sourced across them. The preference is intentionally re-read on every\n * call — the controllers check it at each animation/scroll start (WCAG 2.2\n * **2.3.3**), so flipping the OS setting takes effect immediately without any\n * listener or cache bookkeeping here.\n */\n\n/**\n * Whether the user currently requests reduced motion.\n *\n * @returns `true` when `(prefers-reduced-motion: reduce)` matches; `false`\n * otherwise, including environments without `window.matchMedia` (treated as\n * \"no preference\").\n */\nexport function prefersReducedMotion(): boolean {\n return (\n typeof window.matchMedia === \"function\" &&\n window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n );\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { authoredInteger } from \"../utils/authored_integer\";\nimport { prefersReducedMotion } from \"../utils/reduced_motion\";\n\n/** The animation length used when `duration` falls outside its domain. */\nconst DEFAULT_DURATION = 1200;\n\n/**\n * Headless **count-up**: animates a number from `from` up to the value already\n * in the DOM — typically started the moment the element scrolls into view by\n * composing with the `stimeo--intersection` primitive (the classic \"animate\n * when visible\"). Distinct from `stimeo--countdown` (time-based). Core\n * (zero dependencies).\n *\n * Markup contract (identifier: `stimeo--count-up`):\n * <span data-controller=\"stimeo--intersection stimeo--count-up\"\n * data-stimeo--intersection-once-value=\"true\"\n * data-action=\"stimeo--intersection:enter->stimeo--count-up#start\">1200</span>\n *\n * The **final number stays authored in the markup** (SEO / no-JS / SR read the\n * real value); `start` animates the displayed text from `from` to it over\n * `duration` ms with an ease-out curve, then restores the exact authored text.\n * With `once` (default) later starts are ignored (`data-count-up-done` records\n * a finished run across Turbo cache restores).\n *\n * Only the text node holding the number is animated, so sibling markup — a unit\n * in a `<small>`, a label in a `<b>` — is left where the author put it.\n *\n * `end` dispatches `{ value }`.\n *\n * @remarks\n * Behavior only — no formatting is imposed: the authored text is read for the\n * integer it displays and restored verbatim at the end; intermediate frames\n * render plain integers. Accessibility: when the user prefers reduced motion the\n * animation is skipped entirely (the value just stays final — WCAG 2.3.3).\n * During a run the ticking number is wrapped in a `role=\"img\"` element named\n * with the authored text, which is where a name is allowed to live — the host\n * keeps whatever semantics it had, so a `<dd>` stays a definition. That wrapper\n * doubles as the interrupted-run record `connect()` restores from after a Turbo\n * cache snapshot taken mid-animation. The animation frame is canceled on\n * `disconnect()` (Turbo navigation included) and the authored text restored.\n */\nexport class CountUpController extends Controller<HTMLElement> {\n static override values = {\n duration: { type: Number, default: DEFAULT_DURATION },\n from: { type: Number, default: 0 },\n once: { type: Boolean, default: true },\n };\n static actions = [\"start\"] as const;\n static events = [\"end\"] as const;\n\n declare durationValue: number;\n declare fromValue: number;\n declare onceValue: boolean;\n\n #frame: number | null = null;\n\n /** The animation length, with a declaration outside its domain read as the default. */\n get #duration(): number {\n return Number.isFinite(this.durationValue) && this.durationValue > 0\n ? this.durationValue\n : DEFAULT_DURATION;\n }\n\n /** The starting value, with a declaration that is not a finite number read as zero. */\n get #from(): number {\n return Number.isFinite(this.fromValue) ? this.fromValue : 0;\n }\n\n override connect(): void {\n // Turbo snapshots the page BEFORE the body swap, so a cached page can hold\n // a mid-animation frame (disconnect()'s settle runs too late for it). The\n // wrapper this controller owns flags the interrupted run and carries the\n // authored text in the name it published.\n const ticker = this.#ownedTicker();\n if (ticker === null) return;\n const authored = ticker.getAttribute(\"aria-label\");\n this.#unwrap(ticker, authored);\n if (authored !== null) this.element.setAttribute(\"data-count-up-done\", \"true\");\n }\n\n override disconnect(): void {\n // A run cannot survive the element: settle instantly so the cached\n // snapshot holds the real value, never a mid-animation frame.\n if (this.#frame !== null) this.#settle();\n }\n\n /**\n * Starts the animation (typically from `stimeo--intersection:enter` via\n * `data-action`). No-ops while running, and after a finished run when `once`.\n */\n start(): void {\n if (this.#frame !== null) return;\n if (this.onceValue && this.element.hasAttribute(\"data-count-up-done\")) return;\n\n const node = this.#numericNode();\n if (node === null) return;\n const authored = node.data;\n const target = authoredInteger(authored) as number;\n\n // Reduced motion: no ticking, just the final value (WCAG 2.3.3).\n if (prefersReducedMotion()) {\n this.element.setAttribute(\"data-count-up-done\", \"true\");\n this.dispatch(\"end\", { detail: { value: target } });\n return;\n }\n\n // A name may not live on every host — a `<span>` prohibits one, and a role\n // that permits naming would cost a `<dd>` its own semantics — so the ticking\n // number gets a wrapper that permits naming, and the name carries the\n // authored text AT should hear instead of the ticks.\n const ticker = this.#wrap(node, authored);\n const from = this.#from;\n const started = performance.now();\n const step = (now: number): void => {\n const t = Math.min((now - started) / this.#duration, 1);\n const eased = 1 - (1 - t) ** 3; // ease-out cubic\n ticker.textContent = String(Math.round(from + (target - from) * eased));\n if (t < 1) {\n this.#frame = requestAnimationFrame(step);\n } else {\n this.#settle();\n this.dispatch(\"end\", { detail: { value: target } });\n }\n };\n this.#frame = requestAnimationFrame(step);\n }\n\n /** The first text node that displays a number, or null when the host has none. */\n #numericNode(): Text | null {\n const walker = document.createTreeWalker(this.element, NodeFilter.SHOW_TEXT);\n let node = walker.nextNode() as Text | null;\n while (node !== null && authoredInteger(node.data) === null) {\n node = walker.nextNode() as Text | null;\n }\n return node;\n }\n\n /** The wrapper this controller published, if one outlived its run. */\n #ownedTicker(): HTMLElement | null {\n return this.element.querySelector<HTMLElement>(\"[data-count-up-label]\");\n }\n\n /** Publishes the ticking number inside a named wrapper, replacing `node`. */\n #wrap(node: Text, authored: string): HTMLElement {\n const ticker = document.createElement(\"span\");\n ticker.setAttribute(\"data-count-up-label\", \"true\");\n ticker.setAttribute(\"role\", \"img\");\n ticker.setAttribute(\"aria-label\", authored);\n node.replaceWith(ticker);\n ticker.append(node);\n return ticker;\n }\n\n /** Puts `text` back where the wrapper stood, leaving the rest of the host alone. */\n #unwrap(ticker: HTMLElement, text: string | null): void {\n ticker.replaceWith(document.createTextNode(text ?? ticker.textContent ?? \"\"));\n }\n\n /** Ends the run: cancels the frame and restores the authored presentation. */\n #settle(): void {\n if (this.#frame !== null) cancelAnimationFrame(this.#frame);\n this.#frame = null;\n const ticker = this.#ownedTicker();\n if (ticker !== null) this.#unwrap(ticker, ticker.getAttribute(\"aria-label\"));\n this.element.setAttribute(\"data-count-up-done\", \"true\");\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/authored_integer.ts","../../src/utils/number_bounds.ts","../../src/utils/coerce.ts","../../src/utils/number_value.ts","../../src/utils/reduced_motion.ts","../../src/controllers/count_up_controller.ts"],"names":[],"mappings":"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","file":"count_up_controller.js","sourcesContent":["/** Digits plus the characters that may separate them inside one number. */\nconst TOKEN_BODY = /\\d[\\d.,'’’ _]*\\d|\\d/;\n/** A `-` opens a token only against one of these on its left. */\nconst SIGN_OPENER = /[\\s([]/;\n\n/**\n * Reads the integer that a human-formatted string displays.\n *\n * Takes the first numeric token, drops group separators, and truncates any\n * fractional part toward zero — so `\"1,200 users\"`, `\"$1,299.99\"` and\n * `\"1.299,99\"` read 1200, 1299 and 1299 without asking the platform which\n * locale is in play. A separator counts as grouping only where the digits that\n * follow it form a run of exactly three and the leading run could head a\n * grouped number; anything else is the decimal point, and the digits after it\n * are the fraction.\n *\n * `-` counts as a sign only where it opens the token — at the start of the\n * string, or after whitespace or an opening bracket — so a hyphen inside a\n * label (`\"Sign-ups: 1,200\"`, `\"Top-10 users\"`) never becomes one.\n *\n * `\"1.200\"` is genuinely ambiguous (1200 grouped, 1.2 with a decimal point) and\n * reads as 1200: display numbers group far more often than they carry a\n * fraction in exactly three digits.\n *\n * Returns `null` when the string holds no digits at all. What that means is the\n * caller's to decide — starting nothing, or falling back to a base value.\n */\nexport function authoredInteger(text: string): number | null {\n const firstDigit = /\\d/.exec(text);\n if (firstDigit === null) return null;\n\n const at = firstDigit.index;\n const signed =\n at > 0 && text[at - 1] === \"-\" && (at === 1 || SIGN_OPENER.test(text[at - 2] as string));\n\n const body = TOKEN_BODY.exec(text.slice(at)) as RegExpExecArray;\n const runs = body[0].split(/\\D+/);\n const separators = body[0].match(/\\D+/g) ?? [];\n\n // The leading run heads a grouped number only when it is short enough to be a\n // group itself and carries no leading zero; \"0.500\" and \"012,3\" are decimals.\n const lead = runs[0] as string;\n const heads = lead.length <= 3 && !lead.startsWith(\"0\");\n let digits = lead;\n for (let i = 1; heads && i < runs.length; i += 1) {\n if (separators[i - 1]?.length !== 1 || runs[i]?.length !== 3) break;\n digits += runs[i];\n }\n\n const magnitude = Number.parseInt(digits, 10);\n return signed ? -magnitude || 0 : magnitude;\n}\n","import { MAX_TIMER_DELAY_MS } from \"./timer_bounds\";\n\n/** Semantic constraints on an already decoded number, independent of the DOM. */\nexport interface NumberBounds {\n /** Finite numbers are required, including when this field is omitted. */\n readonly finite?: true;\n /** Inclusive lower endpoint. */\n readonly min?: number;\n /** Inclusive upper endpoint. */\n readonly max?: number;\n /** Exclusive lower endpoint. */\n readonly exclusiveMin?: number;\n /** Require a number without a fractional part. */\n readonly integer?: boolean;\n /** Exact decoded numbers accepted by a discrete contract. */\n readonly allowedValues?: readonly number[];\n /** JSON-safe exceptions to finite-number checking for unbounded endpoints. */\n readonly allowInfinity?: \"negative\" | \"positive\" | \"both\";\n}\n\n/** One constraint for each Number Value, including shorthand declarations. */\nexport type NumberValueConstraints<Values> = {\n readonly [Key in keyof Values as Values[Key] extends\n | NumberConstructor\n | { type: NumberConstructor }\n ? Key\n : never]: NumberBounds;\n};\n\n/** Reusable semantic domains; fractional inputs remain fractional unless excluded. */\nexport const NUMBER_BOUNDS = {\n finite: { finite: true },\n nonNegative: { finite: true, min: 0 },\n positive: { finite: true, exclusiveMin: 0 },\n nonNegativeInteger: { finite: true, min: 0, integer: true },\n positiveInteger: { finite: true, exclusiveMin: 0, integer: true },\n lowerBound: { finite: true, allowInfinity: \"negative\" },\n upperBound: { finite: true, allowInfinity: \"positive\" },\n timer: { finite: true, min: 0, max: MAX_TIMER_DELAY_MS },\n positiveTimer: { finite: true, exclusiveMin: 0, max: MAX_TIMER_DELAY_MS },\n} as const satisfies Record<string, NumberBounds>;\n\n/**\n * Checks all declared bounds without rounding, clamping, or coercing the input.\n * Infinity exceptions relax only finite checking; every other bound still applies.\n */\nexport function matchesNumberBounds(value: number, bounds: NumberBounds): boolean {\n if (!Number.isFinite(value)) {\n const direction = value === Infinity ? \"positive\" : value === -Infinity ? \"negative\" : null;\n if (direction === null) return false;\n if (bounds.allowInfinity !== \"both\" && bounds.allowInfinity !== direction) return false;\n }\n if (bounds.min !== undefined && value < bounds.min) return false;\n if (bounds.max !== undefined && value > bounds.max) return false;\n if (bounds.exclusiveMin !== undefined && value <= bounds.exclusiveMin) return false;\n if (bounds.integer && !Number.isInteger(value)) return false;\n if (bounds.allowedValues !== undefined && !bounds.allowedValues.includes(value)) return false;\n return true;\n}\n\n/** Decodes a Number Value literal; action params use their own JSON decoder. */\nexport function decodeNumberValue(raw: string): number {\n return Number(raw.replace(/_/g, \"\"));\n}\n","/**\n * Numeric coercion for controllers that accept a number through a Value, an action\n * param, or an event detail.\n *\n * Stimulus decodes action params as JSON when possible and leaves other text\n * untouched. CustomEvent detail can also supply a number or numeric string.\n * The reader accepts those two types while rejecting blank and non-finite input.\n */\n\nimport { matchesNumberBounds, type NumberBounds } from \"./number_bounds\";\n\n/**\n * Reads a finite number or nonblank numeric string; other input returns `null`.\n * Blank strings cannot silently reset a value to zero.\n */\nexport function toFiniteNumber(raw: unknown): number | null {\n if (typeof raw !== \"number\" && typeof raw !== \"string\") return null;\n if (typeof raw === \"string\" && raw.trim().length === 0) return null;\n const value = Number(raw);\n return Number.isFinite(value) ? value : null;\n}\n\n/**\n * Returns an accepted decoded Number Value or the caller's fallback unchanged.\n * The caller owns the fallback contract; this reader does not parse or write a Value.\n */\nexport function readNumber(raw: number, fallback: number, bounds: NumberBounds): number {\n return matchesNumberBounds(raw, bounds) ? raw : fallback;\n}\n","import { readNumber } from \"./coerce\";\nimport { matchesNumberBounds, type NumberBounds } from \"./number_bounds\";\n\n/** The instance identity carried by a Number Value read. */\nexport interface NumberValueOwner {\n readonly identifier: string;\n readonly element: Element;\n}\n\n/** Reads current declarations at the point of use without rewriting their attributes. */\nexport class NumberValueReader {\n /** The last rejected literal per Value; no history grows for an unchanged instance. */\n readonly #lastRejected = new Map<string, string>();\n\n /** Resolves the declared number using its own fallback and shared class contract. */\n read(\n owner: NumberValueOwner,\n name: string,\n raw: number,\n fallback: number,\n bounds: NumberBounds,\n ): number {\n const resolved = readNumber(raw, fallback, bounds);\n if (matchesNumberBounds(raw, bounds)) {\n this.#lastRejected.delete(name);\n return resolved;\n }\n const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;\n const literal = owner.element.getAttribute(attribute);\n if (literal === null) {\n this.#lastRejected.delete(name);\n return resolved;\n }\n if (this.#lastRejected.get(name) !== literal) {\n this.#lastRejected.set(name, literal);\n console.warn(\n `Stimeo UI: \"${owner.identifier}\" has an invalid number Value \"${name}\" declaration ${JSON.stringify(literal)}; using ${fallback}.`,\n );\n }\n return resolved;\n }\n}\n","/**\n * Shared `prefers-reduced-motion` lookup for the motion-aware controllers.\n *\n * This one-liner keeps the media query string and the environment guard\n * single-sourced across them. The preference is intentionally re-read on every\n * call — the controllers check it at each animation/scroll start (WCAG 2.2\n * **2.3.3**), so flipping the OS setting takes effect immediately without any\n * listener or cache bookkeeping here.\n */\n\n/**\n * Whether the user currently requests reduced motion.\n *\n * @returns `true` when `(prefers-reduced-motion: reduce)` matches; `false`\n * otherwise, including environments without `window.matchMedia` (treated as\n * \"no preference\").\n */\nexport function prefersReducedMotion(): boolean {\n return (\n typeof window.matchMedia === \"function\" &&\n window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n );\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { authoredInteger } from \"../utils/authored_integer\";\nimport { NUMBER_BOUNDS, type NumberValueConstraints } from \"../utils/number_bounds\";\nimport { NumberValueReader } from \"../utils/number_value\";\nimport { prefersReducedMotion } from \"../utils/reduced_motion\";\n\n/** The animation length used when `duration` falls outside its domain. */\nconst DEFAULT_DURATION = 1200;\n\n/**\n * Headless **count-up**: animates a number from `from` up to the value already\n * in the DOM — typically started the moment the element scrolls into view by\n * composing with the `stimeo--intersection` primitive (the classic \"animate\n * when visible\"). Distinct from `stimeo--countdown` (time-based). Core\n * (zero dependencies).\n *\n * Markup contract (identifier: `stimeo--count-up`):\n * <span data-controller=\"stimeo--intersection stimeo--count-up\"\n * data-stimeo--intersection-once-value=\"true\"\n * data-action=\"stimeo--intersection:enter->stimeo--count-up#start\">1200</span>\n *\n * The **final number stays authored in the markup** (SEO / no-JS / SR read the\n * real value); `start` animates the displayed text from `from` to it over\n * `duration` ms with an ease-out curve, then restores the exact authored text.\n * With `once` (default) later starts are ignored (`data-count-up-done` records\n * a finished run across Turbo cache restores). `from` and `duration` are read as\n * a run starts: a running animation keeps the values it began with, the next run\n * reads the new ones, and a change on its own starts nothing.\n *\n * Only the text node holding the number is animated, so sibling markup — a unit\n * in a `<small>`, a label in a `<b>` — is left where the author put it.\n *\n * `end` dispatches `{ value }`.\n *\n * @remarks\n * Behavior only — no formatting is imposed: the authored text is read for the\n * integer it displays and restored verbatim at the end; intermediate frames\n * render plain integers. Accessibility: when the user prefers reduced motion the\n * animation is skipped entirely (the value just stays final — WCAG 2.3.3).\n * During a run the ticking number is wrapped in a `role=\"img\"` element named\n * with the authored text, which is where a name is allowed to live — the host\n * keeps whatever semantics it had, so a `<dd>` stays a definition. That wrapper\n * doubles as the interrupted-run record `connect()` restores from after a Turbo\n * cache snapshot taken mid-animation. The animation frame is canceled on\n * `disconnect()` (Turbo navigation included) and the authored text restored.\n */\nexport class CountUpController extends Controller<HTMLElement> {\n /** Numeric read boundaries share one reader for this controller instance. */\n readonly #numbers = new NumberValueReader();\n\n static override values = {\n duration: { type: Number, default: DEFAULT_DURATION },\n from: { type: Number, default: 0 },\n once: { type: Boolean, default: true },\n };\n\n static valueConstraints = {\n duration: NUMBER_BOUNDS.positive,\n from: NUMBER_BOUNDS.finite,\n } satisfies NumberValueConstraints<typeof CountUpController.values>;\n static actions = [\"start\"] as const;\n static events = [\"end\"] as const;\n\n declare durationValue: number;\n declare fromValue: number;\n declare onceValue: boolean;\n\n #frame: number | null = null;\n\n /** The animation length, with a declaration outside its domain read as the default. */\n get #duration(): number {\n return this.#safeDuration;\n }\n\n /** The starting value, with a declaration that is not a finite number read as zero. */\n get #from(): number {\n return this.#safeFrom;\n }\n\n override connect(): void {\n // Turbo snapshots the page BEFORE the body swap, so a cached page can hold\n // a mid-animation frame (disconnect()'s settle runs too late for it). The\n // wrapper this controller owns flags the interrupted run and carries the\n // authored text in the name it published.\n const ticker = this.#ownedTicker();\n if (ticker === null) return;\n const authored = ticker.getAttribute(\"aria-label\");\n this.#unwrap(ticker, authored);\n if (authored !== null) this.element.setAttribute(\"data-count-up-done\", \"true\");\n }\n\n override disconnect(): void {\n // Cancel the live run and restore the element's authored text.\n if (this.#frame !== null) this.#settle();\n }\n\n /**\n * Starts the animation (typically from `stimeo--intersection:enter` via\n * `data-action`). No-ops while running, and after a finished run when `once`.\n * The run keeps the `from` and `duration` read here to its last frame.\n */\n start(): void {\n if (this.#frame !== null) return;\n if (this.onceValue && this.element.hasAttribute(\"data-count-up-done\")) return;\n\n const node = this.#numericNode();\n if (node === null) return;\n const authored = node.data;\n const target = authoredInteger(authored) as number;\n\n // Reduced motion: no ticking, just the final value (WCAG 2.3.3).\n if (prefersReducedMotion()) {\n this.element.setAttribute(\"data-count-up-done\", \"true\");\n this.dispatch(\"end\", { detail: { value: target } });\n return;\n }\n\n // A name may not live on every host — a `<span>` prohibits one, and a role\n // that permits naming would cost a `<dd>` its own semantics — so the ticking\n // number gets a wrapper that permits naming, and the name carries the\n // authored text AT should hear instead of the ticks.\n const ticker = this.#wrap(node, authored);\n const from = this.#from;\n const duration = this.#duration;\n const started = performance.now();\n const step = (now: number): void => {\n const t = Math.min((now - started) / duration, 1);\n const eased = 1 - (1 - t) ** 3; // ease-out cubic\n ticker.textContent = String(Math.round(from + (target - from) * eased));\n if (t < 1) {\n this.#frame = requestAnimationFrame(step);\n } else {\n this.#settle();\n this.dispatch(\"end\", { detail: { value: target } });\n }\n };\n this.#frame = requestAnimationFrame(step);\n }\n\n /** The first text node that displays a number, or null when the host has none. */\n #numericNode(): Text | null {\n const walker = document.createTreeWalker(this.element, NodeFilter.SHOW_TEXT);\n let node = walker.nextNode() as Text | null;\n while (node !== null && authoredInteger(node.data) === null) {\n node = walker.nextNode() as Text | null;\n }\n return node;\n }\n\n /** The wrapper this controller published, if one outlived its run. */\n #ownedTicker(): HTMLElement | null {\n return this.element.querySelector<HTMLElement>(\"[data-count-up-label]\");\n }\n\n /** Publishes the ticking number inside a named wrapper, replacing `node`. */\n #wrap(node: Text, authored: string): HTMLElement {\n const ticker = document.createElement(\"span\");\n ticker.setAttribute(\"data-count-up-label\", \"true\");\n ticker.setAttribute(\"role\", \"img\");\n ticker.setAttribute(\"aria-label\", authored);\n node.replaceWith(ticker);\n ticker.append(node);\n return ticker;\n }\n\n /** Puts `text` back where the wrapper stood, leaving the rest of the host alone. */\n #unwrap(ticker: HTMLElement, text: string | null): void {\n ticker.replaceWith(document.createTextNode(text ?? ticker.textContent ?? \"\"));\n }\n\n /** Ends the run: cancels the frame and restores the authored presentation. */\n #settle(): void {\n if (this.#frame !== null) cancelAnimationFrame(this.#frame);\n this.#frame = null;\n const ticker = this.#ownedTicker();\n if (ticker !== null) this.#unwrap(ticker, ticker.getAttribute(\"aria-label\"));\n this.element.setAttribute(\"data-count-up-done\", \"true\");\n }\n /** Current `duration` declaration resolved against its numeric contract. */\n get #safeDuration(): number {\n return this.#numbers.read(\n this,\n \"duration\",\n this.durationValue,\n CountUpController.values.duration.default,\n CountUpController.valueConstraints.duration,\n );\n }\n\n /** Current `from` declaration resolved against its numeric contract. */\n get #safeFrom(): number {\n return this.#numbers.read(\n this,\n \"from\",\n this.fromValue,\n CountUpController.values.from.default,\n CountUpController.valueConstraints.from,\n );\n }\n}\n"]}
|