stimeo-ui 0.4.0 → 0.6.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 +184 -0
- package/dist/controllers/announcer_controller.js.map +1 -1
- package/dist/controllers/aspect_ratio_controller.d.ts +3 -5
- package/dist/controllers/aspect_ratio_controller.js +19 -11
- package/dist/controllers/aspect_ratio_controller.js.map +1 -1
- package/dist/controllers/avatar_controller.d.ts +36 -15
- package/dist/controllers/avatar_controller.js +195 -40
- package/dist/controllers/avatar_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.js +5 -1
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +12 -3
- package/dist/controllers/carousel_controller.js +90 -10
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/checkbox_controller.d.ts +31 -5
- package/dist/controllers/checkbox_controller.js +136 -25
- package/dist/controllers/checkbox_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.js +8 -3
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.d.ts +1 -1
- package/dist/controllers/collapsible_controller.js +4 -1
- package/dist/controllers/collapsible_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +9 -3
- package/dist/controllers/color_picker_controller.js +41 -11
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +1 -1
- package/dist/controllers/context_menu_controller.js +2 -2
- package/dist/controllers/context_menu_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.d.ts +5 -3
- package/dist/controllers/countdown_controller.js +5 -1
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +23 -4
- package/dist/controllers/date_range_picker_controller.js +162 -31
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.d.ts +1 -1
- package/dist/controllers/direct_upload_controller.js +3 -3
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/empty_state_controller.d.ts +27 -7
- package/dist/controllers/empty_state_controller.js +107 -16
- package/dist/controllers/empty_state_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.d.ts +7 -1
- package/dist/controllers/file_dropzone_controller.js +26 -3
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/flash_controller.d.ts +25 -5
- package/dist/controllers/flash_controller.js +161 -21
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +2 -3
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.js +8 -2
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.d.ts +19 -2
- package/dist/controllers/frame_loading_controller.js +94 -22
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.d.ts +8 -4
- package/dist/controllers/highlight_controller.js +38 -1
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/hover_card_controller.d.ts +2 -2
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/idle_controller.d.ts +5 -4
- package/dist/controllers/idle_controller.js +39 -6
- package/dist/controllers/idle_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.js +2 -0
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +1 -1
- package/dist/controllers/masonry_controller.js +1 -1
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.js +5 -3
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/meter_controller.js +3 -1
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +46 -12
- package/dist/controllers/multi_select_controller.js +460 -151
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/network_status_controller.d.ts +15 -13
- package/dist/controllers/network_status_controller.js +1 -3
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +29 -3
- package/dist/controllers/number_input_controller.js +455 -64
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.js +5 -1
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.js +38 -1
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_strength_controller.d.ts +4 -4
- package/dist/controllers/password_strength_controller.js +21 -3
- package/dist/controllers/password_strength_controller.js.map +1 -1
- package/dist/controllers/persist_controller.d.ts +4 -4
- package/dist/controllers/persist_controller.js +24 -5
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.js +10 -0
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +2 -2
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +1 -2
- package/dist/controllers/portal_controller.js +10 -0
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/progress_controller.d.ts +1 -1
- package/dist/controllers/progress_controller.js +7 -3
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.d.ts +41 -15
- package/dist/controllers/radio_group_controller.js +540 -56
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.d.ts +23 -0
- package/dist/controllers/range_slider_controller.js +385 -94
- package/dist/controllers/range_slider_controller.js.map +1 -1
- package/dist/controllers/rating_controller.d.ts +36 -31
- package/dist/controllers/rating_controller.js +274 -89
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.js +2 -0
- package/dist/controllers/relative_time_controller.js.map +1 -1
- package/dist/controllers/resizable_controller.js +33 -0
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/roving_controller.d.ts +4 -0
- package/dist/controllers/roving_controller.js +60 -5
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.d.ts +7 -1
- package/dist/controllers/scroll_area_controller.js +155 -23
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.js +33 -0
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/separator_controller.js +13 -17
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.d.ts +2 -2
- package/dist/controllers/skeleton_controller.js +71 -3
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +17 -1
- package/dist/controllers/slider_controller.js +325 -48
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.d.ts +15 -10
- package/dist/controllers/spinner_controller.js +18 -3
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.d.ts +1 -1
- package/dist/controllers/step_indicator_controller.js +3 -1
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stepper_controller.js +2 -0
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/switch_controller.d.ts +12 -8
- package/dist/controllers/switch_controller.js +162 -18
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +41 -11
- package/dist/controllers/tags_input_controller.js +356 -120
- package/dist/controllers/tags_input_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.js +1 -1
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.d.ts +42 -10
- package/dist/controllers/time_picker_controller.js +296 -104
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.d.ts +39 -13
- package/dist/controllers/toggle_group_controller.js +378 -55
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.js +5 -3
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +2 -2
- package/dist/controllers/tooltip_controller.js.map +1 -1
- package/dist/controllers/transition_controller.d.ts +2 -3
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +1 -2
- package/dist/controllers/tree_view_controller.js +24 -4
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.js +3811 -1048
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +92 -4
- package/dist/inspector/cli.js +174 -6
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +234 -8
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +24 -24
- package/dist/inspector/manifest.json +573 -59
- package/dist/positioning/index.d.ts +2 -2
- package/dist/positioning/index.js.map +1 -1
- package/package.json +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/arrow_step.ts","../../src/utils/escape_layer.ts","../../src/utils/safe_timeout.ts","../../src/controllers/context_menu_controller.ts"],"names":[],"mappings":";;;;;AA+FO,SAAS,oBAAA,CACd,KAAA,EACA,KAAA,GAAkC,EAAC,EAC1B;AACT,EAAA,IAAI,CAAC,KAAA,CAAM,GAAA,CAAI,UAAA,CAAW,OAAO,GAAG,OAAO,KAAA;AAC3C,EAAA,OACG,KAAA,CAAM,MAAA,IAAU,CAAC,KAAA,CAAM,QAAA,CAAS,KAAK,CAAA,IACrC,KAAA,CAAM,OAAA,IAAW,CAAC,KAAA,CAAM,QAAA,CAAS,MAAM,KACvC,KAAA,CAAM,OAAA,IAAW,CAAC,KAAA,CAAM,QAAA,CAAS,MAAM,CAAA,IACvC,KAAA,CAAM,QAAA,IAAY,CAAC,KAAA,CAAM,QAAA,CAAS,OAAO,CAAA;AAE9C;;;AC1CO,SAAS,uBAAuB,OAAA,EAAiC;AACtE,EAAA,OAAO,MAAM;AACX,IAAA,MAAM,MAAA,GAAS,QAAQ,aAAA,CAAc,aAAA;AACrC,IAAA,OAAO,MAAA,KAAW,QAAQ,MAAA,KAAW,OAAA,CAAQ,cAAc,IAAA,IAAQ,OAAA,CAAQ,SAAS,MAAM,CAAA;AAAA,EAC5F,CAAA;AACF;AAEO,IAAM,WAAA,GAAN,MAAM,YAAA,CAAY;AAAA,EACvB,OAAgB,WAAA,mBAAc,IAAI,OAAA,EAAuC;AAAA,EAEzE,cAAA,GAAkC,IAAA;AAAA;AAAA,EAElC,UAAA,GAAkC,IAAA;AAAA;AAAA,EAElC,OAAA,GAAkC,IAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOlC,QAAA,CAAS,aAAA,GAA0B,QAAA,EAAU,OAAA,EAAmC;AAC9E,IAAA,IAAA,CAAK,UAAA,EAAW;AAChB,IAAA,IAAI,QAAA,GAAW,YAAA,CAAY,WAAA,CAAY,GAAA,CAAI,aAAa,CAAA;AACxD,IAAA,IAAI,CAAC,QAAA,EAAU;AACb,MAAA,QAAA,GAAW,aAAY,eAAA,EAAgB;AACvC,MAAA,YAAA,CAAY,WAAA,CAAY,GAAA,CAAI,aAAA,EAAe,QAAQ,CAAA;AACnD,MAAA,aAAA,CAAc,gBAAA,CAAiB,SAAA,EAAW,QAAA,CAAS,SAAS,CAAA;AAAA,IAC9D;AACA,IAAA,QAAA,CAAS,KAAA,CAAM,KAAK,IAAI,CAAA;AACxB,IAAA,IAAA,CAAK,cAAA,GAAiB,aAAA;AACtB,IAAA,IAAA,CAAK,aAAa,OAAA,CAAQ,SAAA;AAC1B,IAAA,IAAA,CAAK,OAAA,GAAU,QAAQ,MAAA,IAAU,IAAA;AAAA,EACnC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,UAAA,GAAmB;AACjB,IAAA,MAAM,gBAAgB,IAAA,CAAK,cAAA;AAC3B,IAAA,IAAI,CAAC,aAAA,EAAe;AAEpB,IAAA,MAAM,QAAA,GAAW,YAAA,CAAY,WAAA,CAAY,GAAA,CAAI,aAAa,CAAA;AAC1D,IAAA,IAAI,QAAA,EAAU;AACZ,MAAA,MAAM,KAAA,GAAQ,QAAA,CAAS,KAAA,CAAM,WAAA,CAAY,IAAI,CAAA;AAC7C,MAAA,IAAI,SAAS,CAAA,EAAG,QAAA,CAAS,KAAA,CAAM,MAAA,CAAO,OAAO,CAAC,CAAA;AAC9C,MAAA,IAAI,QAAA,CAAS,KAAA,CAAM,MAAA,KAAW,CAAA,EAAG;AAC/B,QAAA,aAAA,CAAc,mBAAA,CAAoB,SAAA,EAAW,QAAA,CAAS,SAAS,CAAA;AAC/D,QAAA,YAAA,CAAY,WAAA,CAAY,OAAO,aAAa,CAAA;AAAA,MAC9C;AAAA,IACF;AACA,IAAA,IAAA,CAAK,cAAA,GAAiB,IAAA;AACtB,IAAA,IAAA,CAAK,UAAA,GAAa,IAAA;AAClB,IAAA,IAAA,CAAK,OAAA,GAAU,IAAA;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,IAAI,UAAA,GAAsB;AACxB,IAAA,MAAM,gBAAgB,IAAA,CAAK,cAAA;AAC3B,IAAA,IAAI,CAAC,eAAe,OAAO,KAAA;AAC3B,IAAA,MAAM,QAAA,GAAW,YAAA,CAAY,WAAA,CAAY,GAAA,CAAI,aAAa,CAAA;AAC1D,IAAA,IAAI,CAAC,UAAU,OAAO,KAAA;AACtB,IAAA,OAAO,YAAA,CAAY,aAAA,CAAc,QAAA,CAAS,KAAK,CAAA,KAAM,IAAA;AAAA,EACvD;AAAA;AAAA,EAGA,OAAO,eAAA,GAAuC;AAC5C,IAAA,MAAM,QAAA,GAAgC;AAAA,MACpC,OAAO,EAAC;AAAA,MACR,SAAA,EAAW,CAAC,KAAA,KAA+B;AACzC,QAAA,IAAI,MAAM,GAAA,KAAQ,QAAA,IAAY,KAAA,CAAM,gBAAA,IAAoB,MAAM,WAAA,EAAa;AAC3E,QAAA,MAAM,KAAA,GAAQ,YAAA,CAAY,aAAA,CAAc,QAAA,CAAS,KAAK,CAAA;AACtD,QAAA,IAAI,CAAC,KAAA,EAAO;AACZ,QAAA,KAAA,CAAM,cAAA,EAAe;AACrB,QAAA,KAAA,CAAM,UAAA,IAAa;AAAA,MACrB;AAAA,KACF;AACA,IAAA,OAAO,QAAA;AAAA,EACT;AAAA;AAAA,EAGA,OAAO,cAAc,KAAA,EAA0C;AAC7D,IAAA,KAAA,IAAS,QAAQ,KAAA,CAAM,MAAA,GAAS,CAAA,EAAG,KAAA,IAAS,GAAG,KAAA,EAAA,EAAS;AACtD,MAAA,MAAM,KAAA,GAAQ,MAAM,KAAK,CAAA;AACzB,MAAA,IAAI,CAAC,KAAA,EAAO;AACZ,MAAA,IAAI,KAAA,CAAM,OAAA,IAAW,CAAC,KAAA,CAAM,SAAQ,EAAG;AACvC,MAAA,OAAO,KAAA;AAAA,IACT;AACA,IAAA,OAAO,IAAA;AAAA,EACT;AACF,CAAA;;;ACvIA,IAAe,gBAAf,MAA6B;AAAA;AAAA,EAER,GAAA,uBAAU,GAAA,EAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAczC,MAAM,EAAA,EAAkB;AACtB,IAAA,IAAI,IAAA,CAAK,GAAA,CAAI,MAAA,CAAO,EAAE,CAAA,EAAG;AACvB,MAAA,IAAA,CAAK,OAAO,EAAE,CAAA;AAAA,IAChB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,QAAA,GAAiB;AACf,IAAA,KAAA,MAAW,EAAA,IAAM,KAAK,GAAA,EAAK;AACzB,MAAA,IAAA,CAAK,OAAO,EAAE,CAAA;AAAA,IAChB;AACA,IAAA,IAAA,CAAK,IAAI,KAAA,EAAM;AAAA,EACjB;AAAA;AAAA,EAGA,IAAI,IAAA,GAAe;AACjB,IAAA,OAAO,KAAK,GAAA,CAAI,IAAA;AAAA,EAClB;AACF,CAAA;AAmBO,IAAM,WAAA,GAAN,cAA0B,aAAA,CAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAO7C,GAAA,CAAI,UAAsB,KAAA,EAAuB;AAC/C,IAAA,MAAM,EAAA,GAAK,IAAA,CAAK,QAAA,CAAS,MAAM;AAC7B,MAAA,IAAA,CAAK,GAAA,CAAI,OAAO,EAAE,CAAA;AAClB,MAAA,QAAA,EAAS;AAAA,IACX,GAAG,KAAK,CAAA;AACR,IAAA,IAAA,CAAK,GAAA,CAAI,IAAI,EAAE,CAAA;AACf,IAAA,OAAO,EAAA;AAAA,EACT;AAAA,EAEU,QAAA,CAAS,UAAsB,KAAA,EAAuB;AAC9D,IAAA,OAAO,MAAA,CAAO,UAAA,CAAW,QAAA,EAAU,KAAK,CAAA;AAAA,EAC1C;AAAA,EAEU,OAAO,EAAA,EAAkB;AACjC,IAAA,MAAA,CAAO,aAAa,EAAE,CAAA;AAAA,EACxB;AACF,CAAA;;;ACxCO,IAAM,qBAAA,GAAN,cAAoC,UAAA,CAAwB;AAAA,EACjE,OAAgB,OAAA,GAAU,CAAC,QAAA,EAAU,QAAQ,MAAM,CAAA;AAAA,EACnD,OAAO,OAAA,GAAU,CAAC,UAAA,EAAY,eAAA,EAAiB,mBAAmB,MAAM,CAAA;AAAA,EAQ/D,OAAA,GAAU,IAAI,WAAA,EAAY;AAAA;AAAA,EAG1B,YAAA,GAAe,IAAI,WAAA,EAAY;AAAA;AAAA,EAG/B,OAAA,GAAgB;AACvB,IAAA,IAAA,CAAK,UAAA,EAAW;AAChB,IAAA,IAAA,CAAK,OAAA,CAAQ,gBAAA,CAAiB,OAAA,EAAS,IAAA,CAAK,qBAAqB,IAAI,CAAA;AACrE,IAAA,QAAA,CAAS,gBAAA,CAAiB,OAAA,EAAS,IAAA,CAAK,iBAAA,EAAmB,IAAI,CAAA;AAC/D,IAAA,QAAA,CAAS,gBAAA,CAAiB,aAAA,EAAe,IAAA,CAAK,iBAAA,EAAmB,IAAI,CAAA;AAAA,EACvE;AAAA;AAAA,EAGS,UAAA,GAAmB;AAC1B,IAAA,IAAA,CAAK,QAAQ,QAAA,EAAS;AACtB,IAAA,IAAA,CAAK,aAAa,UAAA,EAAW;AAC7B,IAAA,IAAA,CAAK,OAAA,CAAQ,mBAAA,CAAoB,OAAA,EAAS,IAAA,CAAK,qBAAqB,IAAI,CAAA;AACxE,IAAA,QAAA,CAAS,mBAAA,CAAoB,OAAA,EAAS,IAAA,CAAK,iBAAA,EAAmB,IAAI,CAAA;AAClE,IAAA,QAAA,CAAS,mBAAA,CAAoB,aAAA,EAAe,IAAA,CAAK,iBAAA,EAAmB,IAAI,CAAA;AAAA,EAC1E;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,KAAK,KAAA,EAAyB;AAC5B,IAAA,KAAA,CAAM,cAAA,EAAe;AACrB,IAAA,IAAA,CAAK,OAAA,CAAQ,KAAA,CAAM,OAAA,EAAS,KAAA,CAAM,OAAO,CAAA;AAAA,EAC3C;AAAA;AAAA,EAGA,gBAAgB,KAAA,EAA4B;AAG1C,IAAA,IAAI,MAAM,gBAAA,EAAkB;AAC5B,IAAA,MAAM,eAAe,KAAA,CAAM,GAAA,KAAQ,iBAAkB,KAAA,CAAM,GAAA,KAAQ,SAAS,KAAA,CAAM,QAAA;AAClF,IAAA,IAAI,CAAC,YAAA,EAAc;AACnB,IAAA,KAAA,CAAM,cAAA,EAAe;AACrB,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,eAAA,GACd,IAAA,CAAK,aAAa,qBAAA,EAAsB,GACxC,EAAE,IAAA,EAAM,GAAG,GAAA,EAAK,CAAA,EAAG,KAAA,EAAO,CAAA,EAAG,QAAQ,CAAA,EAAE;AAC3C,IAAA,IAAA,CAAK,OAAA,CAAQ,IAAA,CAAK,IAAA,GAAO,IAAA,CAAK,KAAA,GAAQ,GAAG,IAAA,CAAK,GAAA,GAAM,IAAA,CAAK,MAAA,GAAS,CAAC,CAAA;AAAA,EACrE;AAAA;AAAA,EAGA,cAAc,KAAA,EAA4B;AAGxC,IAAA,IAAI,MAAM,gBAAA,EAAkB;AAC5B,IAAA,IAAI,oBAAA,CAAqB,KAAK,CAAA,EAAG;AACjC,IAAA,MAAM,QAAQ,IAAA,CAAK,eAAA;AACnB,IAAA,MAAM,YAAA,GAAe,KAAA,CAAM,OAAA,CAAQ,KAAA,CAAM,aAAkC,CAAA;AAE3E,IAAA,QAAQ,MAAM,GAAA;AAAK,MACjB,KAAK,WAAA;AACH,QAAA,KAAA,CAAM,cAAA,EAAe;AACrB,QAAA,IAAI,KAAA,CAAM,SAAS,CAAA,EAAG;AACpB,UAAA,MAAM,YAAY,YAAA,GAAe,CAAA,GAAI,CAAA,GAAA,CAAK,YAAA,GAAe,KAAK,KAAA,CAAM,MAAA;AACpE,UAAA,KAAA,CAAM,SAAS,GAAG,KAAA,EAAM;AAAA,QAC1B;AACA,QAAA;AAAA,MACF,KAAK,SAAA;AACH,QAAA,KAAA,CAAM,cAAA,EAAe;AACrB,QAAA,IAAI,KAAA,CAAM,SAAS,CAAA,EAAG;AACpB,UAAA,MAAM,gBAAgB,YAAA,GAAe,CAAA,GAAI,KAAA,CAAM,MAAA,GAAS,IAAI,YAAA,GAAe,CAAA;AAC3E,UAAA,KAAA,CAAA,CAAO,gBAAgB,KAAA,CAAM,MAAA,IAAU,KAAA,CAAM,MAAM,GAAG,KAAA,EAAM;AAAA,QAC9D;AACA,QAAA;AAAA,MACF,KAAK,MAAA;AACH,QAAA,KAAA,CAAM,cAAA,EAAe;AACrB,QAAA,KAAA,CAAM,CAAC,GAAG,KAAA,EAAM;AAChB,QAAA;AAAA,MACF,KAAK,KAAA;AACH,QAAA,KAAA,CAAM,cAAA,EAAe;AACrB,QAAA,KAAA,CAAM,KAAA,CAAM,MAAA,GAAS,CAAC,CAAA,EAAG,KAAA,EAAM;AAC/B,QAAA;AAAA,MACF,KAAK,KAAA;AAGH,QAAA,IAAA,CAAK,QAAQ,QAAA,EAAS;AACtB,QAAA,IAAA,CAAK,QAAQ,GAAA,CAAI,MAAM,IAAA,CAAK,UAAA,IAAc,CAAC,CAAA;AAC3C,QAAA;AAEA;AACJ,EACF;AAAA;AAAA,EAGA,QAAA,GAAiB;AACf,IAAA,IAAA,CAAK,gBAAA,EAAiB;AAAA,EACxB;AAAA;AAAA,EAGA,OAAA,CAAQ,GAAW,CAAA,EAAiB;AAClC,IAAA,IAAI,CAAC,KAAK,aAAA,EAAe;AACzB,IAAA,IAAA,CAAK,QAAQ,QAAA,EAAS;AACtB,IAAA,IAAA,CAAK,YAAA,CAAa,SAAS,QAAA,EAAU;AAAA,MACnC,SAAA,EAAW,MAAM,IAAA,CAAK,gBAAA,EAAiB;AAAA,MACvC,MAAA,EAAQ,sBAAA,CAAuB,IAAA,CAAK,OAAO;AAAA,KAC5C,CAAA;AACD,IAAA,IAAA,CAAK,WAAW,KAAA,CAAM,WAAA,CAAY,yBAAA,EAA2B,CAAA,EAAG,CAAC,CAAA,EAAA,CAAI,CAAA;AACrE,IAAA,IAAA,CAAK,WAAW,KAAA,CAAM,WAAA,CAAY,yBAAA,EAA2B,CAAA,EAAG,CAAC,CAAA,EAAA,CAAI,CAAA;AACrE,IAAA,IAAA,CAAK,WAAW,MAAA,GAAS,KAAA;AACzB,IAAA,IAAI,KAAK,eAAA,EAAiB,IAAA,CAAK,YAAA,CAAa,YAAA,CAAa,cAAc,MAAM,CAAA;AAC7E,IAAA,IAAA,CAAK,eAAA,CAAgB,CAAC,CAAA,EAAG,KAAA,EAAM;AAAA,EACjC;AAAA;AAAA,EAGA,UAAA,GAAmB;AACjB,IAAA,IAAA,CAAK,QAAQ,QAAA,EAAS;AACtB,IAAA,IAAA,CAAK,aAAa,UAAA,EAAW;AAC7B,IAAA,IAAI,CAAC,KAAK,aAAA,EAAe;AACzB,IAAA,IAAA,CAAK,WAAW,MAAA,GAAS,IAAA;AACzB,IAAA,IAAI,KAAK,eAAA,EAAiB,IAAA,CAAK,YAAA,CAAa,YAAA,CAAa,cAAc,QAAQ,CAAA;AAAA,EACjF;AAAA;AAAA,EAGA,gBAAA,GAAyB;AACvB,IAAA,IAAA,CAAK,UAAA,EAAW;AAChB,IAAA,IAAI,IAAA,CAAK,eAAA,EAAiB,IAAA,CAAK,YAAA,CAAa,KAAA,EAAM;AAAA,EACpD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASS,iBAAA,GAAoB,CAAC,KAAA,KAA4B;AACxD,IAAA,IAAI,IAAA,CAAK,OAAA,IAAW,CAAC,IAAA,CAAK,OAAA,CAAQ,SAAS,KAAA,CAAM,MAAc,CAAA,EAAG,IAAA,CAAK,UAAA,EAAW;AAAA,EACpF,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMS,mBAAA,GAAsB,CAAC,KAAA,KAA4B;AAC1D,IAAA,MAAM,SAAS,KAAA,CAAM,MAAA;AACrB,IAAA,IAAI,EAAE,kBAAkB,IAAA,CAAA,EAAO;AAC/B,IAAA,MAAM,QAAA,GAAW,KAAK,WAAA,CAAY,IAAA;AAAA,MAChC,CAAC,SAAS,IAAA,CAAK,YAAA,CAAa,eAAe,CAAA,KAAM,MAAA,IAAU,IAAA,CAAK,QAAA,CAAS,MAAM;AAAA,KACjF;AACA,IAAA,IAAI,CAAC,QAAA,EAAU;AACf,IAAA,KAAA,CAAM,cAAA,EAAe;AACrB,IAAA,KAAA,CAAM,wBAAA,EAAyB;AAAA,EACjC,CAAA;AAAA;AAAA,EAGA,IAAI,eAAA,GAAuC;AACzC,IAAA,OAAO,IAAA,CAAK,YAAY,MAAA,CAAO,CAAC,SAAS,IAAA,CAAK,YAAA,CAAa,IAAI,CAAC,CAAA;AAAA,EAClE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeA,aAAa,IAAA,EAAkC;AAC7C,IAAA,IAAI,IAAA,CAAK,YAAA,CAAa,QAAQ,CAAA,EAAG,OAAO,KAAA;AACxC,IAAA,OAAO,CAAC,IAAA,CAAK,QAAA;AAAA,EACf;AAAA;AAAA,EAGA,IAAI,OAAA,GAAmB;AACrB,IAAA,OAAO,IAAA,CAAK,aAAA,IAAiB,CAAC,IAAA,CAAK,UAAA,CAAW,MAAA;AAAA,EAChD;AACF","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 * 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 * 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 { isReservedArrowChord } from \"../utils/arrow_step\";\nimport { claimsWhileFocusWithin, EscapeLayer } from \"../utils/escape_layer\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\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 only differences from\n * `stimeo--menu` are the *trigger* (a `contextmenu` event or `Shift+F10` /\n * `ContextMenu` key, not a button click) and that the menu is shown at the\n * pointer coordinate.\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 {@link 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 *\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\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 /** Starts closed and registers delegated activation and outside-pointer listeners. */\n override connect(): void {\n this.#closeMenu();\n this.element.addEventListener(\"click\", this.#onItemClickCapture, true);\n document.addEventListener(\"click\", this.#onOutsidePointer, true);\n document.addEventListener(\"contextmenu\", this.#onOutsidePointer, true);\n }\n\n /** Releases the listeners, stack membership, and pending Tab-close task. */\n override disconnect(): void {\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);\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);\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(), 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();\n }\n\n /** Opens the menu at viewport coordinates `(x, y)` and focuses the first item. */\n #openAt(x: number, y: number): void {\n if (!this.hasMenuTarget) return;\n this.#timers.clearAll();\n this.#escapeLayer.activate(document, {\n onDismiss: () => this.#closeAndRestore(),\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 this.#navigableItems[0]?.focus();\n }\n\n /** Hides the menu and reflects the collapsed state on the region. */\n #closeMenu(): void {\n this.#timers.clearAll();\n this.#escapeLayer.deactivate();\n if (!this.hasMenuTarget) return;\n this.menuTarget.hidden = true;\n if (this.hasRegionTarget) this.regionTarget.setAttribute(\"data-state\", \"closed\");\n }\n\n /** Closes the menu and returns focus to the region (Escape / activation). */\n #closeAndRestore(): void {\n this.#closeMenu();\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();\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/arrow_step.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":["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 * 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 * 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 { isReservedArrowChord } from \"../utils/arrow_step\";\nimport { claimsWhileFocusWithin, EscapeLayer } from \"../utils/escape_layer\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\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 only differences from\n * `stimeo--menu` are the *trigger* (a `contextmenu` event or `Shift+F10` /\n * `ContextMenu` key, not a button click) and that the menu is shown at the\n * pointer coordinate.\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 {@link 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 *\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\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 /** Starts closed and registers delegated activation and outside-pointer listeners. */\n override connect(): void {\n this.#closeMenu();\n this.element.addEventListener(\"click\", this.#onItemClickCapture, true);\n document.addEventListener(\"click\", this.#onOutsidePointer, true);\n document.addEventListener(\"contextmenu\", this.#onOutsidePointer, true);\n }\n\n /** Releases the listeners, stack membership, and pending Tab-close task. */\n override disconnect(): void {\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);\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);\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(), 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();\n }\n\n /** Opens the menu at viewport coordinates `(x, y)` and focuses the first item. */\n #openAt(x: number, y: number): void {\n if (!this.hasMenuTarget) return;\n this.#timers.clearAll();\n this.#escapeLayer.activate(document, {\n onDismiss: () => this.#closeAndRestore(),\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 this.#navigableItems[0]?.focus();\n }\n\n /** Hides the menu and reflects the collapsed state on the region. */\n #closeMenu(): void {\n this.#timers.clearAll();\n this.#escapeLayer.deactivate();\n if (!this.hasMenuTarget) return;\n this.menuTarget.hidden = true;\n if (this.hasRegionTarget) this.regionTarget.setAttribute(\"data-state\", \"closed\");\n }\n\n /** Closes the menu and returns focus to the region (Escape / activation). */\n #closeAndRestore(): void {\n this.#closeMenu();\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();\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"]}
|
|
@@ -18,9 +18,11 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
18
18
|
* Computes the time remaining to `deadline` (or elapsed since it, in
|
|
19
19
|
* `direction="up"`), formats it into the day/hour/minute/second slots, and ticks
|
|
20
20
|
* on `interval`. `aria-live="off"` is recommended so the timer is not announced
|
|
21
|
-
* every second; only the milestone (completion) is surfaced —
|
|
22
|
-
* `status`
|
|
23
|
-
*
|
|
21
|
+
* every second; only the milestone (completion) is surfaced — read out through
|
|
22
|
+
* `announceText`, shown in the optional `status` slot when a `completeLabel` is
|
|
23
|
+
* provided, and handed to the consumer as the `complete` event. Keep the `status`
|
|
24
|
+
* slot free of live-region semantics: with `announceText` set it would say the
|
|
25
|
+
* same thing twice.
|
|
24
26
|
*
|
|
25
27
|
* @remarks
|
|
26
28
|
* Behavior only — slot text is updated, not styled. Pause/resume shifts an
|
|
@@ -268,7 +268,11 @@ var CountdownController = class extends Controller {
|
|
|
268
268
|
const raw = this.#isDown ? this.#reference - now : now - this.#reference;
|
|
269
269
|
return Math.max(0, raw);
|
|
270
270
|
}
|
|
271
|
-
/**
|
|
271
|
+
/**
|
|
272
|
+
* Writes the amount into the day/hour/minute/second slots.
|
|
273
|
+
*
|
|
274
|
+
* @stimeoRenderRoot
|
|
275
|
+
*/
|
|
272
276
|
#render(amount) {
|
|
273
277
|
const totalSeconds = Math.floor(amount / SECOND_MS);
|
|
274
278
|
this.#renderedAmount = totalSeconds * SECOND_MS;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/announce.ts","../../src/utils/microtask_coalescer.ts","../../src/utils/safe_timeout.ts","../../src/controllers/countdown_controller.ts"],"names":[],"mappings":";;;;;AAoBO,SAAS,QAAA,CAAS,OAAA,EAAiB,OAAA,GAAmC,EAAC,EAAS;AACrF,EAAA,MAAM,IAAA,GAAO,QAAQ,IAAA,EAAK;AAC1B,EAAA,IAAI,IAAA,CAAK,WAAW,CAAA,EAAG;AACvB,EAAA,MAAA,CAAO,aAAA;AAAA,IACL,IAAI,YAAY,4BAAA,EAA8B;AAAA,MAC5C,QAAQ,EAAE,OAAA,EAAS,MAAM,SAAA,EAAW,OAAA,CAAQ,cAAc,IAAA;AAAK,KAChE;AAAA,GACH;AACF;AAUO,SAAS,YAAA,CAAa,UAAkB,MAAA,EAAiD;AAC9F,EAAA,OAAO,QAAA,CAAS,OAAA,CAAQ,6BAAA,EAA+B,CAAC,OAAO,IAAA,KAAiB;AAC9E,IAAA,MAAM,WAAA,GAAc,OAAO,IAAI,CAAA;AAC/B,IAAA,OAAO,WAAA,KAAgB,MAAA,GAAY,KAAA,GAAQ,MAAA,CAAO,WAAW,CAAA;AAAA,EAC/D,CAAC,CAAA;AACH;;;ACQO,IAAM,qBAAN,MAAyB;AAAA,EACrB,IAAA;AAAA,EACT,OAAA,GAAU,KAAA;AAAA,EACV,OAAA,GAAU,KAAA;AAAA,EACV,WAAA,GAAc,CAAA;AAAA;AAAA,EAGd,YAAY,GAAA,EAAiB;AAC3B,IAAA,IAAA,CAAK,IAAA,GAAO,GAAA;AAAA,EACd;AAAA;AAAA,EAGA,QAAA,GAAiB;AACf,IAAA,IAAA,CAAK,OAAA,GAAU,IAAA;AAAA,EACjB;AAAA;AAAA,EAGA,MAAA,GAAe;AACb,IAAA,IAAA,CAAK,OAAA,GAAU,KAAA;AACf,IAAA,IAAA,CAAK,OAAA,GAAU,KAAA;AACf,IAAA,IAAA,CAAK,WAAA,IAAe,CAAA;AAAA,EACtB;AAAA;AAAA,EAGA,QAAA,GAAiB;AACf,IAAA,IAAI,CAAC,IAAA,CAAK,OAAA,IAAW,IAAA,CAAK,OAAA,EAAS;AACnC,IAAA,IAAA,CAAK,OAAA,GAAU,IAAA;AACf,IAAA,MAAM,aAAa,IAAA,CAAK,WAAA;AACxB,IAAA,cAAA,CAAe,MAAM;AAEnB,MAAA,IAAI,UAAA,KAAe,KAAK,WAAA,IAAe,CAAC,KAAK,OAAA,IAAW,CAAC,KAAK,OAAA,EAAS;AACvE,MAAA,IAAA,CAAK,OAAA,GAAU,KAAA;AACf,MAAA,IAAA,CAAK,IAAA,EAAK;AAAA,IACZ,CAAC,CAAA;AAAA,EACH;AACF,CAAA;;;AC9DA,IAAe,gBAAf,MAA6B;AAAA;AAAA,EAER,GAAA,uBAAU,GAAA,EAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAczC,MAAM,EAAA,EAAkB;AACtB,IAAA,IAAI,IAAA,CAAK,GAAA,CAAI,MAAA,CAAO,EAAE,CAAA,EAAG;AACvB,MAAA,IAAA,CAAK,OAAO,EAAE,CAAA;AAAA,IAChB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,QAAA,GAAiB;AACf,IAAA,KAAA,MAAW,EAAA,IAAM,KAAK,GAAA,EAAK;AACzB,MAAA,IAAA,CAAK,OAAO,EAAE,CAAA;AAAA,IAChB;AACA,IAAA,IAAA,CAAK,IAAI,KAAA,EAAM;AAAA,EACjB;AAAA;AAAA,EAGA,IAAI,IAAA,GAAe;AACjB,IAAA,OAAO,KAAK,GAAA,CAAI,IAAA;AAAA,EAClB;AACF,CAAA;AA8DO,IAAM,YAAA,GAAN,cAA2B,aAAA,CAAc;AAAA;AAAA,EAE9C,GAAA,CAAI,UAAsB,KAAA,EAAuB;AAC/C,IAAA,MAAM,EAAA,GAAK,IAAA,CAAK,QAAA,CAAS,QAAA,EAAU,KAAK,CAAA;AACxC,IAAA,IAAA,CAAK,GAAA,CAAI,IAAI,EAAE,CAAA;AACf,IAAA,OAAO,EAAA;AAAA,EACT;AAAA,EAEU,QAAA,CAAS,UAAsB,KAAA,EAAuB;AAC9D,IAAA,OAAO,MAAA,CAAO,WAAA,CAAY,QAAA,EAAU,KAAK,CAAA;AAAA,EAC3C;AAAA,EAEU,OAAO,EAAA,EAAkB;AACjC,IAAA,MAAA,CAAO,cAAc,EAAE,CAAA;AAAA,EACzB;AACF,CAAA;;;ACpIA,IAAM,SAAA,GAAY,GAAA;AAmCX,IAAM,mBAAA,GAAN,cAAkC,UAAA,CAAwB;AAAA,EAC/D,OAAgB,OAAA,GAAU,CAAC,QAAQ,OAAA,EAAS,SAAA,EAAW,WAAW,QAAQ,CAAA;AAAA,EAC1E,OAAgB,MAAA,GAAS;AAAA,IACvB,QAAA,EAAU,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,EAAA,EAAG;AAAA,IACtC,QAAA,EAAU,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,GAAA,EAAK;AAAA,IACxC,SAAA,EAAW,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,MAAA,EAAO;AAAA,IAC3C,SAAA,EAAW,EAAE,IAAA,EAAM,OAAA,EAAS,SAAS,IAAA,EAAK;AAAA,IAC1C,aAAA,EAAe,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,EAAA,EAAG;AAAA,IAC3C,YAAA,EAAc,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,EAAA;AAAG,GAC5C;AAAA,EACA,OAAO,OAAA,GAAU,CAAC,OAAA,EAAS,OAAA,EAAS,UAAU,OAAO,CAAA;AAAA,EACrD,OAAO,MAAA,GAAS,CAAC,UAAA,EAAY,MAAM,CAAA;AAAA,EAoB1B,UAAA,GAAa,IAAI,YAAA,EAAa;AAAA,EACvC,WAAA,GAA6B,IAAA;AAAA;AAAA,EAEpB,UAAU,IAAI,kBAAA,CAAmB,MAAM,IAAA,CAAK,iBAAiB,CAAA;AAAA;AAAA,EAGtE,UAAA,GAAa,CAAA;AAAA;AAAA,EAEb,aAAA,GAAgB,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOhB,eAAA,GAAkB,CAAA;AAAA,EAET,OAAA,GAAgB;AACvB,IAAA,IAAA,CAAK,QAAQ,QAAA,EAAS;AACtB,IAAA,IAAA,CAAK,cAAA,EAAe;AACpB,IAAA,MAAM,MAAA,GAAS,KAAK,cAAA,EAAe;AACnC,IAAA,IAAA,CAAK,QAAQ,MAAM,CAAA;AAGnB,IAAA,IAAA,CAAK,gBAAgB,IAAA,CAAK,eAAA;AAE1B,IAAA,MAAM,QAAA,GAAW,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,YAAY,CAAA;AACvD,IAAA,IAAI,aAAa,IAAA,EAAM;AAGrB,MAAA,IAAI,IAAA,CAAK,cAAA,IAAkB,IAAA,CAAK,gBAAA,EAAkB;AAChD,QAAA,IAAA,CAAK,KAAA,EAAM;AACX,QAAA;AAAA,MACF;AACA,MAAA,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,YAAA,EAAc,QAAQ,CAAA;AAChD,MAAA;AAAA,IACF;AACA,IAAA,IAAI,QAAA,KAAa,UAAA,IAAc,IAAA,CAAK,OAAA,IAAW,UAAU,CAAA,EAAG;AAI1D,MAAA;AAAA,IACF;AAIA,IAAA,MAAM,aAAa,QAAA,KAAa,SAAA;AAChC,IAAA,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,YAAA,EAAc,QAAQ,CAAA;AAChD,IAAA,IAAI,UAAA,OAAiB,KAAA,EAAM;AAAA,EAC7B;AAAA,EAES,UAAA,GAAmB;AAC1B,IAAA,IAAA,CAAK,QAAQ,MAAA,EAAO;AACpB,IAAA,IAAA,CAAK,WAAW,QAAA,EAAS;AACzB,IAAA,IAAA,CAAK,WAAA,GAAc,IAAA;AAAA,EACrB;AAAA;AAAA,EAGA,oBAAA,GAA6B;AAC3B,IAAA,IAAA,CAAK,QAAQ,QAAA,EAAS;AAAA,EACxB;AAAA;AAAA,EAGA,qBAAA,GAA8B;AAC5B,IAAA,IAAA,CAAK,QAAQ,QAAA,EAAS;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,eAAA,GAAwB;AACtB,IAAA,IAAA,CAAK,cAAA,EAAe;AACpB,IAAA,IAAA,CAAK,OAAA,CAAQ,IAAA,CAAK,cAAA,EAAgB,CAAA;AAClC,IAAA,IAAI,IAAA,CAAK,MAAA,KAAW,SAAA,EAAW,IAAA,CAAK,gBAAgB,IAAA,CAAK,eAAA;AAAA,EAC3D;AAAA;AAAA,EAGA,KAAA,GAAc;AACZ,IAAA,IAAI,IAAA,CAAK,MAAA,KAAW,SAAA,IAAa,CAAC,KAAK,gBAAA,EAAkB;AACzD,IAAA,IAAI,IAAA,CAAK,OAAA,IAAW,IAAA,CAAK,cAAA,MAAoB,CAAA,EAAG;AAI9C,MAAA,IAAI,IAAA,CAAK,MAAA,KAAW,UAAA,EAAY,IAAA,CAAK,SAAA,EAAU;AAC/C,MAAA;AAAA,IACF;AACA,IAAA,IAAA,CAAK,YAAA,EAAa;AAAA,EACpB;AAAA;AAAA,EAGA,KAAA,GAAc;AACZ,IAAA,IAAI,IAAA,CAAK,WAAW,SAAA,EAAW;AAC/B,IAAA,IAAA,CAAK,gBAAgB,IAAA,CAAK,eAAA;AAC1B,IAAA,IAAA,CAAK,iBAAA,EAAkB;AACvB,IAAA,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,YAAA,EAAc,QAAQ,CAAA;AAAA,EAClD;AAAA;AAAA,EAGA,MAAA,GAAe;AACb,IAAA,IAAI,IAAA,CAAK,MAAA,KAAW,QAAA,IAAY,CAAC,KAAK,gBAAA,EAAkB;AACxD,IAAA,MAAM,GAAA,GAAM,KAAK,GAAA,EAAI;AACrB,IAAA,IAAA,CAAK,aAAa,IAAA,CAAK,OAAA,GAAU,MAAM,IAAA,CAAK,aAAA,GAAgB,MAAM,IAAA,CAAK,aAAA;AACvE,IAAA,IAAA,CAAK,KAAA,EAAM;AAAA,EACb;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,KAAA,GAAc;AACZ,IAAA,MAAM,UAAA,GAAa,KAAK,MAAA,KAAW,SAAA;AACnC,IAAA,IAAA,CAAK,iBAAA,EAAkB;AACvB,IAAA,IAAA,CAAK,cAAA,EAAe;AACpB,IAAA,MAAM,MAAA,GAAS,KAAK,cAAA,EAAe;AACnC,IAAA,IAAA,CAAK,QAAQ,MAAM,CAAA;AAMnB,IAAA,IAAI,KAAK,eAAA,IAAmB,IAAA,CAAK,YAAA,CAAa,WAAA,KAAgB,KAAK,kBAAA,EAAoB;AACrF,MAAA,IAAA,CAAK,aAAa,WAAA,GAAc,EAAA;AAAA,IAClC;AAGA,IAAA,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,YAAA,EAAc,QAAQ,CAAA;AAChD,IAAA,IAAI,UAAA,IAAc,KAAK,gBAAA,EAAkB;AAEvC,MAAA,IAAA,CAAK,aAAA,GAAgB,CAAA;AACrB,MAAA,IAAA,CAAK,KAAA,EAAM;AAAA,IACb,CAAA,MAAO;AAIL,MAAA,IAAA,CAAK,gBAAgB,IAAA,CAAK,eAAA;AAAA,IAC5B;AAAA,EACF;AAAA;AAAA,EAGA,YAAA,GAAqB;AACnB,IAAA,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,YAAA,EAAc,SAAS,CAAA;AACjD,IAAA,IAAA,CAAK,WAAA,GAAc,KAAK,UAAA,CAAW,GAAA,CAAI,MAAM,IAAA,CAAK,KAAA,EAAM,EAAG,IAAA,CAAK,aAAa,CAAA;AAAA,EAC/E;AAAA;AAAA,EAGA,iBAAA,GAA0B;AACxB,IAAA,IAAI,IAAA,CAAK,gBAAgB,IAAA,EAAM;AAC7B,MAAA,IAAA,CAAK,UAAA,CAAW,KAAA,CAAM,IAAA,CAAK,WAAW,CAAA;AACtC,MAAA,IAAA,CAAK,WAAA,GAAc,IAAA;AAAA,IACrB;AAAA,EACF;AAAA;AAAA,EAGA,KAAA,GAAc;AACZ,IAAA,MAAM,MAAA,GAAS,KAAK,cAAA,EAAe;AACnC,IAAA,IAAA,CAAK,QAAQ,MAAM,CAAA;AAGnB,IAAA,IAAA,CAAK,SAAS,MAAA,EAAQ;AAAA,MACpB,MAAA,EAAQ,EAAE,SAAA,EAAW,MAAA,EAAQ,WAAW,IAAA,CAAK,OAAA,GAAU,SAAS,IAAA;AAAK,KACtE,CAAA;AACD,IAAA,IAAI,KAAK,OAAA,IAAW,IAAA,CAAK,aAAa,IAAA,CAAK,GAAA,MAAS,CAAA,EAAG;AACrD,MAAA,IAAA,CAAK,SAAA,EAAU;AAAA,IACjB;AAAA,EACF;AAAA;AAAA,EAGA,SAAA,GAAkB;AAChB,IAAA,IAAA,CAAK,iBAAA,EAAkB;AACvB,IAAA,IAAA,CAAK,QAAQ,CAAC,CAAA;AACd,IAAA,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,YAAA,EAAc,UAAU,CAAA;AAClD,IAAA,IAAI,IAAA,CAAK,eAAA,IAAmB,IAAA,CAAK,kBAAA,CAAmB,SAAS,CAAA,EAAG;AAC9D,MAAA,IAAA,CAAK,YAAA,CAAa,cAAc,IAAA,CAAK,kBAAA;AAAA,IACvC;AACA,IAAA,IAAA,CAAK,SAAS,UAAA,EAAY,EAAE,MAAA,EAAQ,IAAI,CAAA;AAGxC,IAAA,QAAA,CAAS,YAAA,CAAa,IAAA,CAAK,iBAAA,EAAmB,EAAE,CAAC,CAAA;AAAA,EACnD;AAAA;AAAA,EAGA,cAAA,GAAuB;AACrB,IAAA,IAAA,CAAK,UAAA,GAAa,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,aAAa,CAAA;AAAA,EACjD;AAAA;AAAA,EAGA,cAAA,GAAyB;AACvB,IAAA,IAAI,CAAC,IAAA,CAAK,gBAAA,EAAkB,OAAO,CAAA;AACnC,IAAA,MAAM,GAAA,GAAM,KAAK,GAAA,EAAI;AACrB,IAAA,MAAM,MAAM,IAAA,CAAK,OAAA,GAAU,KAAK,UAAA,GAAa,GAAA,GAAM,MAAM,IAAA,CAAK,UAAA;AAC9D,IAAA,OAAO,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,GAAG,CAAA;AAAA,EACxB;AAAA;AAAA,EAGA,QAAQ,MAAA,EAAsB;AAC5B,IAAA,MAAM,YAAA,GAAe,IAAA,CAAK,KAAA,CAAM,MAAA,GAAS,SAAS,CAAA;AAClD,IAAA,IAAA,CAAK,kBAAkB,YAAA,GAAe,SAAA;AACtC,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,YAAA,GAAe,KAAK,CAAA;AAC5C,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAO,YAAA,GAAe,QAAS,IAAI,CAAA;AACtD,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,KAAA,CAAO,YAAA,GAAe,OAAQ,EAAE,CAAA;AACrD,IAAA,MAAM,UAAU,YAAA,GAAe,EAAA;AAE/B,IAAA,IAAI,KAAK,aAAA,EAAe,IAAA,CAAK,UAAA,CAAW,WAAA,GAAc,OAAO,IAAI,CAAA;AACjE,IAAA,IAAI,KAAK,cAAA,EAAgB,IAAA,CAAK,YAAY,WAAA,GAAc,IAAA,CAAK,KAAK,KAAK,CAAA;AACvE,IAAA,IAAI,KAAK,gBAAA,EAAkB,IAAA,CAAK,cAAc,WAAA,GAAc,IAAA,CAAK,KAAK,OAAO,CAAA;AAC7E,IAAA,IAAI,KAAK,gBAAA,EAAkB,IAAA,CAAK,cAAc,WAAA,GAAc,IAAA,CAAK,KAAK,OAAO,CAAA;AAAA,EAC/E;AAAA;AAAA,EAGA,KAAK,IAAA,EAAsB;AACzB,IAAA,OAAO,MAAA,CAAO,IAAI,CAAA,CAAE,QAAA,CAAS,GAAG,GAAG,CAAA;AAAA,EACrC;AAAA,EAEA,IAAI,OAAA,GAAmB;AACrB,IAAA,OAAO,KAAK,cAAA,KAAmB,IAAA;AAAA,EACjC;AAAA,EAEA,IAAI,gBAAA,GAA4B;AAC9B,IAAA,OAAO,CAAC,MAAA,CAAO,KAAA,CAAM,IAAA,CAAK,UAAU,CAAA;AAAA,EACtC;AAAA;AAAA,EAGA,IAAI,MAAA,GAAiB;AACnB,IAAA,OAAO,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,YAAY,CAAA,IAAK,QAAA;AAAA,EACpD;AACF","file":"countdown_controller.js","sourcesContent":["/**\n * Sends one message to the page's shared `stimeo--announcer`.\n *\n * A component that has to reach assistive tech does not carry a live region of its\n * own: a region only announces what changes *after* assistive tech already knows\n * about it, which a region that appears (or is un-hidden) with its message cannot\n * satisfy. The one region that can is the announcer sitting in the page from the\n * start, so state changes are handed to it as an event and it does the reading.\n *\n * The event goes to `window` because the announcer is usually a sibling high in the\n * document rather than an ancestor of the component dispatching it.\n *\n * Wording comes from the consumer — the library ships no English strings — so an\n * empty message is silently dropped and nothing is announced.\n *\n * @example\n * ```ts\n * announce(this.announceTextValue, { assertive: false });\n * ```\n */\nexport function announce(message: string, options: { assertive?: boolean } = {}): void {\n const text = message.trim();\n if (text.length === 0) return;\n window.dispatchEvent(\n new CustomEvent(\"stimeo--announcer:announce\", {\n detail: { message: text, assertive: options.assertive === true },\n }),\n );\n}\n\n/**\n * Fills `{name}` placeholders in an announcement template from `values`.\n *\n * The same substitution the value-text templates use, so a consumer writes\n * `\"{percent}% complete\"` in one attribute and gets the same rules everywhere. A\n * placeholder with no matching entry is left as authored rather than blanked, which\n * keeps a typo visible instead of silently swallowing the word.\n */\nexport function fillTemplate(template: string, values: Record<string, string | number>): string {\n return template.replace(/\\{([a-zA-Z][a-zA-Z0-9]*)\\}/g, (match, name: string) => {\n const replacement = values[name];\n return replacement === undefined ? match : String(replacement);\n });\n}\n","/**\n * Collapses many target callbacks from one DOM mutation into a single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element, so\n * replacing a list of N options delivers N callbacks — but the useful unit of\n * work is \"reconcile against the DOM that resulted\", once, after the batch has\n * settled. Every controller that owns a reconcilable target set needs the same\n * shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers the *initial* target callbacks\n * ahead of `connect()`. Reconciling there would compute a fallback against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\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 * 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 { announce, fillTemplate } from \"../utils/announce\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\nimport { SafeInterval } from \"../utils/safe_timeout\";\n\n/** Display granularity: the slots render whole seconds, so amounts snap to these. */\nconst SECOND_MS = 1000;\n\n/**\n * Headless countdown/timer behavior built on the live-region `role=\"timer\"`\n * practice (no dedicated APG pattern).\n *\n * Markup contract (identifier: `stimeo--countdown`):\n * <div data-controller=\"stimeo--countdown\" role=\"timer\" aria-live=\"off\"\n * data-stimeo--countdown-deadline-value=\"2026-12-31T23:59:59+09:00\"\n * data-action=\"countdown:pause->stimeo--countdown#pause\n * countdown:resume->stimeo--countdown#resume\">\n * <span data-stimeo--countdown-target=\"days\">0</span>\n * <span data-stimeo--countdown-target=\"hours\">00</span>\n * <span data-stimeo--countdown-target=\"minutes\">00</span>\n * <span data-stimeo--countdown-target=\"seconds\">00</span>\n * </div>\n *\n * Computes the time remaining to `deadline` (or elapsed since it, in\n * `direction=\"up\"`), formats it into the day/hour/minute/second slots, and ticks\n * on `interval`. `aria-live=\"off\"` is recommended so the timer is not announced\n * every second; only the milestone (completion) is surfaced — via the optional\n * `status` live region when a `completeLabel` is provided, or the `complete`\n * event for the consumer to handle.\n *\n * @remarks\n * Behavior only — slot text is updated, not styled. Pause/resume shifts an\n * internal time anchor so the displayed amount is preserved across a pause, down\n * to the second actually on screen: resuming continues from that reading rather\n * than from the fraction behind it, so the first tick after a resume steps by one\n * unit like every other. The run state lives in `data-state` and the markup is its\n * source of truth — `autostart` only decides the state of a timer whose markup does\n * not carry one. The interval is owned by {@link SafeInterval} and torn down on\n * `disconnect()` (Turbo navigation included); nothing else needs carrying across,\n * because every reading is re-derived from `deadline` and the wall clock.\n */\nexport class CountdownController extends Controller<HTMLElement> {\n static override targets = [\"days\", \"hours\", \"minutes\", \"seconds\", \"status\"];\n static override values = {\n deadline: { type: String, default: \"\" },\n interval: { type: Number, default: 1000 },\n direction: { type: String, default: \"down\" },\n autostart: { type: Boolean, default: true },\n completeLabel: { type: String, default: \"\" },\n announceText: { type: String, default: \"\" },\n };\n static actions = [\"pause\", \"reset\", \"resume\", \"start\"] as const;\n static events = [\"complete\", \"tick\"] as const;\n\n declare readonly daysTarget: HTMLElement;\n declare readonly hoursTarget: HTMLElement;\n declare readonly minutesTarget: HTMLElement;\n declare readonly secondsTarget: HTMLElement;\n declare readonly statusTarget: HTMLElement;\n declare readonly hasDaysTarget: boolean;\n declare readonly hasHoursTarget: boolean;\n declare readonly hasMinutesTarget: boolean;\n declare readonly hasSecondsTarget: boolean;\n declare readonly hasStatusTarget: boolean;\n\n declare deadlineValue: string;\n declare intervalValue: number;\n declare directionValue: string;\n declare autostartValue: boolean;\n declare completeLabelValue: string;\n declare announceTextValue: string;\n\n readonly #intervals = new SafeInterval();\n #intervalId: number | null = null;\n /** Collapses a morph that swaps several render inputs at once into one re-derive. */\n readonly #resync = new MicrotaskCoalescer(() => this.#resyncToValues());\n\n /** Epoch-ms anchor: the deadline (down) or the count-up origin (up). */\n #reference = 0;\n /** Amount (ms) captured at pause, so resume can restore the same display. */\n #pausedAmount = 0;\n /**\n * The amount the slots are currently showing, floored to the second they render.\n * It lags {@link currentAmount} by up to one tick, and it — not the live reading —\n * is what a pause has to preserve: storing the fraction behind the display instead\n * makes the first tick after a resume step by two units.\n */\n #renderedAmount = 0;\n\n override connect(): void {\n this.#resync.activate();\n this.#initReference();\n const amount = this.#currentAmount();\n this.#render(amount);\n // Remember what is on screen so resume() continues from it instead of snapping\n // the anchor to now (which would settle a countdown at once).\n this.#pausedAmount = this.#renderedAmount;\n\n const authored = this.element.getAttribute(\"data-state\");\n if (authored === null) {\n // Nothing to honor: this is the declarative first render, which is the only\n // thing `autostart` governs.\n if (this.autostartValue && this.#isValidDeadline) {\n this.start();\n return;\n }\n this.element.setAttribute(\"data-state\", \"paused\");\n return;\n }\n if (authored === \"complete\" && this.#isDown && amount <= 0) {\n // Still settled, so the milestone stands. Dropping it to the resting state\n // would let a later resume() cross zero a second time and announce the same\n // completion again; only a deadline moved forward re-arms it.\n return;\n }\n // A pause the user made survives the round trip, but nothing carries a live timer\n // across it — a restored \"running\" has to be re-armed here or it renders once and\n // freezes, because start() is a no-op while the attribute still says \"running\".\n const wasRunning = authored === \"running\";\n this.element.setAttribute(\"data-state\", \"paused\");\n if (wasRunning) this.start();\n }\n\n override disconnect(): void {\n this.#resync.cancel();\n this.#intervals.clearAll();\n this.#intervalId = null;\n }\n\n /** Re-derives the display when a morph swaps the deadline in place. */\n deadlineValueChanged(): void {\n this.#resync.schedule();\n }\n\n /** Re-derives the display when a morph flips the counting direction in place. */\n directionValueChanged(): void {\n this.#resync.schedule();\n }\n\n /**\n * Points the anchor at the current `deadline` / `direction` and repaints.\n *\n * Render only: it starts no interval and emits no event, so a morph cannot make a\n * paused timer run or replay a milestone. A running one needs no restart either —\n * every tick reads the anchor, so moving it is enough. While paused the stored\n * amount follows the new reading, or resume would continue from the old deadline.\n */\n #resyncToValues(): void {\n this.#initReference();\n this.#render(this.#currentAmount());\n if (this.#state !== \"running\") this.#pausedAmount = this.#renderedAmount;\n }\n\n /** Starts (or restarts after pause) ticking toward the deadline. */\n start(): void {\n if (this.#state === \"running\" || !this.#isValidDeadline) return;\n if (this.#isDown && this.#currentAmount() <= 0) {\n // Already settled — a restored snapshot keeps data-state=\"complete\". The\n // milestone was announced in the visit that reached zero, so completing again\n // here would re-emit `complete` on every reconnect.\n if (this.#state !== \"complete\") this.#complete();\n return;\n }\n this.#runInterval();\n }\n\n /** Pauses ticking, preserving the currently displayed amount. */\n pause(): void {\n if (this.#state !== \"running\") return;\n this.#pausedAmount = this.#renderedAmount;\n this.#teardownInterval();\n this.element.setAttribute(\"data-state\", \"paused\");\n }\n\n /** Resumes from a pause, continuing from the preserved amount. */\n resume(): void {\n if (this.#state !== \"paused\" || !this.#isValidDeadline) return;\n const now = Date.now();\n this.#reference = this.#isDown ? now + this.#pausedAmount : now - this.#pausedAmount;\n this.start();\n }\n\n /**\n * Re-syncs to the deadline and clears any pause offset, **preserving the current\n * run state**: a running timer keeps counting down from the reset amount, while a\n * paused (or completed) one resets the displayed amount but stays paused until the\n * user resumes — it never silently restarts. The run state is read from the DOM,\n * not re-derived from the declarative `autostart` Value (which governs only markup\n * that states no run state at all); re-deriving it would override a user's pause —\n * the DOM, not a re-run of declarative config, is the source of truth.\n */\n reset(): void {\n const wasRunning = this.#state === \"running\";\n this.#teardownInterval();\n this.#initReference();\n const amount = this.#currentAmount();\n this.#render(amount);\n // A prior complete() may have written a completion message into the status live\n // region; clear it so a reset timer does not keep showing (and announcing to a\n // screen reader) the stale \"finished\" text. Only that message is ours to take\n // back — `complete()` declines to write when `completeLabel` is empty, so\n // anything else in there belongs to the consumer.\n if (this.hasStatusTarget && this.statusTarget.textContent === this.completeLabelValue) {\n this.statusTarget.textContent = \"\";\n }\n // teardownInterval() leaves data-state untouched; drop any lingering \"running\" to a\n // resting \"paused\" so start() (a no-op while \"running\") can re-arm when we resume.\n this.element.setAttribute(\"data-state\", \"paused\");\n if (wasRunning && this.#isValidDeadline) {\n // Was counting down: re-arm and keep running from the reset amount.\n this.#pausedAmount = 0;\n this.start();\n } else {\n // Was paused/complete: hold at the reset amount and wait for the user to resume.\n // Remember what is on screen so resume() restores it instead of snapping the\n // anchor to 0.\n this.#pausedAmount = this.#renderedAmount;\n }\n }\n\n /** Schedules the repeating tick and marks the timer running. */\n #runInterval(): void {\n this.element.setAttribute(\"data-state\", \"running\");\n this.#intervalId = this.#intervals.set(() => this.#tick(), this.intervalValue);\n }\n\n /** Cancels the repeating tick, if any. */\n #teardownInterval(): void {\n if (this.#intervalId !== null) {\n this.#intervals.clear(this.#intervalId);\n this.#intervalId = null;\n }\n }\n\n /** Recomputes, renders, emits `tick`, and completes when a countdown hits 0. */\n #tick(): void {\n const amount = this.#currentAmount();\n this.#render(amount);\n // `remaining` is the displayed amount (time left when counting down, elapsed\n // when counting up); `direction` lets consumers disambiguate the two.\n this.dispatch(\"tick\", {\n detail: { remaining: amount, direction: this.#isDown ? \"down\" : \"up\" },\n });\n if (this.#isDown && this.#reference - Date.now() <= 0) {\n this.#complete();\n }\n }\n\n /** Stops at zero, marks completion, announces it, and emits `complete`. */\n #complete(): void {\n this.#teardownInterval();\n this.#render(0);\n this.element.setAttribute(\"data-state\", \"complete\");\n if (this.hasStatusTarget && this.completeLabelValue.length > 0) {\n this.statusTarget.textContent = this.completeLabelValue;\n }\n this.dispatch(\"complete\", { detail: {} });\n // Reaching zero is the transition; the ticking numbers in between are state\n // the consumer can see, and reading each one would be unusable.\n announce(fillTemplate(this.announceTextValue, {}));\n }\n\n /** Sets the time anchor from the `deadline` value. */\n #initReference(): void {\n this.#reference = Date.parse(this.deadlineValue);\n }\n\n /** Remaining (down) or elapsed (up) ms, never negative. */\n #currentAmount(): number {\n if (!this.#isValidDeadline) return 0;\n const now = Date.now();\n const raw = this.#isDown ? this.#reference - now : now - this.#reference;\n return Math.max(0, raw);\n }\n\n /** Writes the amount into the day/hour/minute/second slots. */\n #render(amount: number): void {\n const totalSeconds = Math.floor(amount / SECOND_MS);\n this.#renderedAmount = totalSeconds * SECOND_MS;\n const days = Math.floor(totalSeconds / 86400);\n const hours = Math.floor((totalSeconds % 86400) / 3600);\n const minutes = Math.floor((totalSeconds % 3600) / 60);\n const seconds = totalSeconds % 60;\n\n if (this.hasDaysTarget) this.daysTarget.textContent = String(days);\n if (this.hasHoursTarget) this.hoursTarget.textContent = this.#pad(hours);\n if (this.hasMinutesTarget) this.minutesTarget.textContent = this.#pad(minutes);\n if (this.hasSecondsTarget) this.secondsTarget.textContent = this.#pad(seconds);\n }\n\n /** Zero-pads a unit to two digits. */\n #pad(unit: number): string {\n return String(unit).padStart(2, \"0\");\n }\n\n get #isDown(): boolean {\n return this.directionValue !== \"up\";\n }\n\n get #isValidDeadline(): boolean {\n return !Number.isNaN(this.#reference);\n }\n\n /** Current lifecycle phase as reflected on `data-state`. */\n get #state(): string {\n return this.element.getAttribute(\"data-state\") ?? \"paused\";\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/announce.ts","../../src/utils/microtask_coalescer.ts","../../src/utils/safe_timeout.ts","../../src/controllers/countdown_controller.ts"],"names":[],"mappings":"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","file":"countdown_controller.js","sourcesContent":["/**\n * Sends one message to the page's shared `stimeo--announcer`.\n *\n * A component that has to reach assistive tech does not carry a live region of its\n * own: a region only announces what changes *after* assistive tech already knows\n * about it, which a region that appears (or is un-hidden) with its message cannot\n * satisfy. The one region that can is the announcer sitting in the page from the\n * start, so state changes are handed to it as an event and it does the reading.\n *\n * The event goes to `window` because the announcer is usually a sibling high in the\n * document rather than an ancestor of the component dispatching it.\n *\n * Wording comes from the consumer — the library ships no English strings — so an\n * empty message is silently dropped and nothing is announced.\n *\n * @example\n * ```ts\n * announce(this.announceTextValue, { assertive: false });\n * ```\n */\nexport function announce(message: string, options: { assertive?: boolean } = {}): void {\n const text = message.trim();\n if (text.length === 0) return;\n window.dispatchEvent(\n new CustomEvent(\"stimeo--announcer:announce\", {\n detail: { message: text, assertive: options.assertive === true },\n }),\n );\n}\n\n/**\n * Fills `{name}` placeholders in an announcement template from `values`.\n *\n * The same substitution the value-text templates use, so a consumer writes\n * `\"{percent}% complete\"` in one attribute and gets the same rules everywhere. A\n * placeholder with no matching entry is left as authored rather than blanked, which\n * keeps a typo visible instead of silently swallowing the word.\n */\nexport function fillTemplate(template: string, values: Record<string, string | number>): string {\n return template.replace(/\\{([a-zA-Z][a-zA-Z0-9]*)\\}/g, (match, name: string) => {\n const replacement = values[name];\n return replacement === undefined ? match : String(replacement);\n });\n}\n","/**\n * Collapses many Stimulus lifecycle callbacks from one DOM mutation into a\n * single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element and\n * `<name>ValueChanged` once per changed attribute. Replacing a list of N options\n * or morphing several render Values therefore delivers N callbacks — but the\n * useful unit of work is \"reconcile against the resulting declarative input\",\n * once, after the batch has settled. Every controller with reconcilable targets\n * or render Values needs the same shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers initial target and Value callbacks\n * ahead of `connect()`. Reconciling there would compute output against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\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 * 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 { announce, fillTemplate } from \"../utils/announce\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\nimport { SafeInterval } from \"../utils/safe_timeout\";\n\n/** Display granularity: the slots render whole seconds, so amounts snap to these. */\nconst SECOND_MS = 1000;\n\n/**\n * Headless countdown/timer behavior built on the live-region `role=\"timer\"`\n * practice (no dedicated APG pattern).\n *\n * Markup contract (identifier: `stimeo--countdown`):\n * <div data-controller=\"stimeo--countdown\" role=\"timer\" aria-live=\"off\"\n * data-stimeo--countdown-deadline-value=\"2026-12-31T23:59:59+09:00\"\n * data-action=\"countdown:pause->stimeo--countdown#pause\n * countdown:resume->stimeo--countdown#resume\">\n * <span data-stimeo--countdown-target=\"days\">0</span>\n * <span data-stimeo--countdown-target=\"hours\">00</span>\n * <span data-stimeo--countdown-target=\"minutes\">00</span>\n * <span data-stimeo--countdown-target=\"seconds\">00</span>\n * </div>\n *\n * Computes the time remaining to `deadline` (or elapsed since it, in\n * `direction=\"up\"`), formats it into the day/hour/minute/second slots, and ticks\n * on `interval`. `aria-live=\"off\"` is recommended so the timer is not announced\n * every second; only the milestone (completion) is surfaced — read out through\n * `announceText`, shown in the optional `status` slot when a `completeLabel` is\n * provided, and handed to the consumer as the `complete` event. Keep the `status`\n * slot free of live-region semantics: with `announceText` set it would say the\n * same thing twice.\n *\n * @remarks\n * Behavior only — slot text is updated, not styled. Pause/resume shifts an\n * internal time anchor so the displayed amount is preserved across a pause, down\n * to the second actually on screen: resuming continues from that reading rather\n * than from the fraction behind it, so the first tick after a resume steps by one\n * unit like every other. The run state lives in `data-state` and the markup is its\n * source of truth — `autostart` only decides the state of a timer whose markup does\n * not carry one. The interval is owned by {@link SafeInterval} and torn down on\n * `disconnect()` (Turbo navigation included); nothing else needs carrying across,\n * because every reading is re-derived from `deadline` and the wall clock.\n */\nexport class CountdownController extends Controller<HTMLElement> {\n static override targets = [\"days\", \"hours\", \"minutes\", \"seconds\", \"status\"];\n static override values = {\n deadline: { type: String, default: \"\" },\n interval: { type: Number, default: 1000 },\n direction: { type: String, default: \"down\" },\n autostart: { type: Boolean, default: true },\n completeLabel: { type: String, default: \"\" },\n announceText: { type: String, default: \"\" },\n };\n static actions = [\"pause\", \"reset\", \"resume\", \"start\"] as const;\n static events = [\"complete\", \"tick\"] as const;\n\n declare readonly daysTarget: HTMLElement;\n declare readonly hoursTarget: HTMLElement;\n declare readonly minutesTarget: HTMLElement;\n declare readonly secondsTarget: HTMLElement;\n declare readonly statusTarget: HTMLElement;\n declare readonly hasDaysTarget: boolean;\n declare readonly hasHoursTarget: boolean;\n declare readonly hasMinutesTarget: boolean;\n declare readonly hasSecondsTarget: boolean;\n declare readonly hasStatusTarget: boolean;\n\n declare deadlineValue: string;\n declare intervalValue: number;\n declare directionValue: string;\n declare autostartValue: boolean;\n declare completeLabelValue: string;\n declare announceTextValue: string;\n\n readonly #intervals = new SafeInterval();\n #intervalId: number | null = null;\n /** Collapses a morph that swaps several render inputs at once into one re-derive. */\n readonly #resync = new MicrotaskCoalescer(() => this.#resyncToValues());\n\n /** Epoch-ms anchor: the deadline (down) or the count-up origin (up). */\n #reference = 0;\n /** Amount (ms) captured at pause, so resume can restore the same display. */\n #pausedAmount = 0;\n /**\n * The amount the slots are currently showing, floored to the second they render.\n * It lags {@link currentAmount} by up to one tick, and it — not the live reading —\n * is what a pause has to preserve: storing the fraction behind the display instead\n * makes the first tick after a resume step by two units.\n */\n #renderedAmount = 0;\n\n override connect(): void {\n this.#resync.activate();\n this.#initReference();\n const amount = this.#currentAmount();\n this.#render(amount);\n // Remember what is on screen so resume() continues from it instead of snapping\n // the anchor to now (which would settle a countdown at once).\n this.#pausedAmount = this.#renderedAmount;\n\n const authored = this.element.getAttribute(\"data-state\");\n if (authored === null) {\n // Nothing to honor: this is the declarative first render, which is the only\n // thing `autostart` governs.\n if (this.autostartValue && this.#isValidDeadline) {\n this.start();\n return;\n }\n this.element.setAttribute(\"data-state\", \"paused\");\n return;\n }\n if (authored === \"complete\" && this.#isDown && amount <= 0) {\n // Still settled, so the milestone stands. Dropping it to the resting state\n // would let a later resume() cross zero a second time and announce the same\n // completion again; only a deadline moved forward re-arms it.\n return;\n }\n // A pause the user made survives the round trip, but nothing carries a live timer\n // across it — a restored \"running\" has to be re-armed here or it renders once and\n // freezes, because start() is a no-op while the attribute still says \"running\".\n const wasRunning = authored === \"running\";\n this.element.setAttribute(\"data-state\", \"paused\");\n if (wasRunning) this.start();\n }\n\n override disconnect(): void {\n this.#resync.cancel();\n this.#intervals.clearAll();\n this.#intervalId = null;\n }\n\n /** Re-derives the display when a morph swaps the deadline in place. */\n deadlineValueChanged(): void {\n this.#resync.schedule();\n }\n\n /** Re-derives the display when a morph flips the counting direction in place. */\n directionValueChanged(): void {\n this.#resync.schedule();\n }\n\n /**\n * Points the anchor at the current `deadline` / `direction` and repaints.\n *\n * Render only: it starts no interval and emits no event, so a morph cannot make a\n * paused timer run or replay a milestone. A running one needs no restart either —\n * every tick reads the anchor, so moving it is enough. While paused the stored\n * amount follows the new reading, or resume would continue from the old deadline.\n */\n #resyncToValues(): void {\n this.#initReference();\n this.#render(this.#currentAmount());\n if (this.#state !== \"running\") this.#pausedAmount = this.#renderedAmount;\n }\n\n /** Starts (or restarts after pause) ticking toward the deadline. */\n start(): void {\n if (this.#state === \"running\" || !this.#isValidDeadline) return;\n if (this.#isDown && this.#currentAmount() <= 0) {\n // Already settled — a restored snapshot keeps data-state=\"complete\". The\n // milestone was announced in the visit that reached zero, so completing again\n // here would re-emit `complete` on every reconnect.\n if (this.#state !== \"complete\") this.#complete();\n return;\n }\n this.#runInterval();\n }\n\n /** Pauses ticking, preserving the currently displayed amount. */\n pause(): void {\n if (this.#state !== \"running\") return;\n this.#pausedAmount = this.#renderedAmount;\n this.#teardownInterval();\n this.element.setAttribute(\"data-state\", \"paused\");\n }\n\n /** Resumes from a pause, continuing from the preserved amount. */\n resume(): void {\n if (this.#state !== \"paused\" || !this.#isValidDeadline) return;\n const now = Date.now();\n this.#reference = this.#isDown ? now + this.#pausedAmount : now - this.#pausedAmount;\n this.start();\n }\n\n /**\n * Re-syncs to the deadline and clears any pause offset, **preserving the current\n * run state**: a running timer keeps counting down from the reset amount, while a\n * paused (or completed) one resets the displayed amount but stays paused until the\n * user resumes — it never silently restarts. The run state is read from the DOM,\n * not re-derived from the declarative `autostart` Value (which governs only markup\n * that states no run state at all); re-deriving it would override a user's pause —\n * the DOM, not a re-run of declarative config, is the source of truth.\n */\n reset(): void {\n const wasRunning = this.#state === \"running\";\n this.#teardownInterval();\n this.#initReference();\n const amount = this.#currentAmount();\n this.#render(amount);\n // A prior complete() may have written a completion message into the status\n // slot; clear it so a reset timer does not keep showing the stale \"finished\"\n // text. Only that message is ours to take back — `complete()` declines to write\n // when `completeLabel` is empty, so anything else in there belongs to the\n // consumer.\n if (this.hasStatusTarget && this.statusTarget.textContent === this.completeLabelValue) {\n this.statusTarget.textContent = \"\";\n }\n // teardownInterval() leaves data-state untouched; drop any lingering \"running\" to a\n // resting \"paused\" so start() (a no-op while \"running\") can re-arm when we resume.\n this.element.setAttribute(\"data-state\", \"paused\");\n if (wasRunning && this.#isValidDeadline) {\n // Was counting down: re-arm and keep running from the reset amount.\n this.#pausedAmount = 0;\n this.start();\n } else {\n // Was paused/complete: hold at the reset amount and wait for the user to resume.\n // Remember what is on screen so resume() restores it instead of snapping the\n // anchor to 0.\n this.#pausedAmount = this.#renderedAmount;\n }\n }\n\n /** Schedules the repeating tick and marks the timer running. */\n #runInterval(): void {\n this.element.setAttribute(\"data-state\", \"running\");\n this.#intervalId = this.#intervals.set(() => this.#tick(), this.intervalValue);\n }\n\n /** Cancels the repeating tick, if any. */\n #teardownInterval(): void {\n if (this.#intervalId !== null) {\n this.#intervals.clear(this.#intervalId);\n this.#intervalId = null;\n }\n }\n\n /** Recomputes, renders, emits `tick`, and completes when a countdown hits 0. */\n #tick(): void {\n const amount = this.#currentAmount();\n this.#render(amount);\n // `remaining` is the displayed amount (time left when counting down, elapsed\n // when counting up); `direction` lets consumers disambiguate the two.\n this.dispatch(\"tick\", {\n detail: { remaining: amount, direction: this.#isDown ? \"down\" : \"up\" },\n });\n if (this.#isDown && this.#reference - Date.now() <= 0) {\n this.#complete();\n }\n }\n\n /** Stops at zero, marks completion, announces it, and emits `complete`. */\n #complete(): void {\n this.#teardownInterval();\n this.#render(0);\n this.element.setAttribute(\"data-state\", \"complete\");\n if (this.hasStatusTarget && this.completeLabelValue.length > 0) {\n this.statusTarget.textContent = this.completeLabelValue;\n }\n this.dispatch(\"complete\", { detail: {} });\n // Reaching zero is the transition; the ticking numbers in between are state\n // the consumer can see, and reading each one would be unusable.\n announce(fillTemplate(this.announceTextValue, {}));\n }\n\n /** Sets the time anchor from the `deadline` value. */\n #initReference(): void {\n this.#reference = Date.parse(this.deadlineValue);\n }\n\n /** Remaining (down) or elapsed (up) ms, never negative. */\n #currentAmount(): number {\n if (!this.#isValidDeadline) return 0;\n const now = Date.now();\n const raw = this.#isDown ? this.#reference - now : now - this.#reference;\n return Math.max(0, raw);\n }\n\n /**\n * Writes the amount into the day/hour/minute/second slots.\n *\n * @stimeoRenderRoot\n */\n #render(amount: number): void {\n const totalSeconds = Math.floor(amount / SECOND_MS);\n this.#renderedAmount = totalSeconds * SECOND_MS;\n const days = Math.floor(totalSeconds / 86400);\n const hours = Math.floor((totalSeconds % 86400) / 3600);\n const minutes = Math.floor((totalSeconds % 3600) / 60);\n const seconds = totalSeconds % 60;\n\n if (this.hasDaysTarget) this.daysTarget.textContent = String(days);\n if (this.hasHoursTarget) this.hoursTarget.textContent = this.#pad(hours);\n if (this.hasMinutesTarget) this.minutesTarget.textContent = this.#pad(minutes);\n if (this.hasSecondsTarget) this.secondsTarget.textContent = this.#pad(seconds);\n }\n\n /** Zero-pads a unit to two digits. */\n #pad(unit: number): string {\n return String(unit).padStart(2, \"0\");\n }\n\n get #isDown(): boolean {\n return this.directionValue !== \"up\";\n }\n\n get #isValidDeadline(): boolean {\n return !Number.isNaN(this.#reference);\n }\n\n /** Current lifecycle phase as reflected on `data-state`. */\n get #state(): string {\n return this.element.getAttribute(\"data-state\") ?? \"paused\";\n }\n}\n"]}
|
|
@@ -8,7 +8,8 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
8
8
|
* Markup contract (identifier: `stimeo--date-range-picker`):
|
|
9
9
|
* <div data-controller="stimeo--date-range-picker"
|
|
10
10
|
* data-stimeo--date-range-picker-min-value="2026-01-01"
|
|
11
|
-
* data-stimeo--date-range-picker-max-value="2026-12-31"
|
|
11
|
+
* data-stimeo--date-range-picker-max-value="2026-12-31"
|
|
12
|
+
* data-stimeo--date-range-picker-disabled-dates-value='["2026-06-15"]'>
|
|
12
13
|
* <button data-action="stimeo--date-range-picker#prev">Prev</button>
|
|
13
14
|
* <span data-stimeo--date-range-picker-target="monthLabel" aria-live="polite"></span>
|
|
14
15
|
* <button data-action="stimeo--date-range-picker#next">Next</button>
|
|
@@ -36,8 +37,18 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
36
37
|
*
|
|
37
38
|
* Selection model: the first click/Enter sets a *pending* start and enters
|
|
38
39
|
* "selecting" mode (preview follows the pointer/focus); the second confirms the
|
|
39
|
-
* end (auto-swapped if earlier than the start) and dispatches `change
|
|
40
|
-
* abandons an in-progress selection,
|
|
40
|
+
* end (auto-swapped if earlier than the start) and dispatches `change` with
|
|
41
|
+
* `{ start, end }` ISO dates. Escape abandons an in-progress selection,
|
|
42
|
+
* restoring the last confirmed range.
|
|
43
|
+
*
|
|
44
|
+
* Availability is declarative on both axes: `min` / `max` bound the selectable
|
|
45
|
+
* interval and `disabledDates` excludes individual dates inside it. Both are
|
|
46
|
+
* render inputs, so a cell's `aria-disabled` is derived during the paint that
|
|
47
|
+
* needs it rather than written back afterwards — selection, preview, and the
|
|
48
|
+
* grid paint all read one predicate and cannot disagree. A painted-month
|
|
49
|
+
* navigation dispatches `monthchange` with `{ month }` as a notification (for
|
|
50
|
+
* fetching a month's availability, say); the fetched result comes back in
|
|
51
|
+
* through `disabledDates`, which repaints on its own.
|
|
41
52
|
*/
|
|
42
53
|
declare class DateRangePickerController extends Controller<HTMLElement> {
|
|
43
54
|
#private;
|
|
@@ -51,9 +62,13 @@ declare class DateRangePickerController extends Controller<HTMLElement> {
|
|
|
51
62
|
type: StringConstructor;
|
|
52
63
|
default: string;
|
|
53
64
|
};
|
|
65
|
+
disabledDates: {
|
|
66
|
+
type: StringConstructor;
|
|
67
|
+
default: string;
|
|
68
|
+
};
|
|
54
69
|
};
|
|
55
70
|
static actions: readonly ["applyPreset", "next", "onKeydown", "prev", "previewTo", "selectDate"];
|
|
56
|
-
static events: readonly ["change"];
|
|
71
|
+
static events: readonly ["change", "monthchange"];
|
|
57
72
|
readonly gridTarget: HTMLElement;
|
|
58
73
|
readonly hasGridTarget: boolean;
|
|
59
74
|
readonly monthLabelTarget: HTMLElement;
|
|
@@ -67,6 +82,8 @@ declare class DateRangePickerController extends Controller<HTMLElement> {
|
|
|
67
82
|
readonly hasEndFieldTarget: boolean;
|
|
68
83
|
minValue: string;
|
|
69
84
|
maxValue: string;
|
|
85
|
+
disabledDatesValue: string;
|
|
86
|
+
/** Seeds and normalizes the range from optional hidden fields, then paints the grid. */
|
|
70
87
|
connect(): void;
|
|
71
88
|
/** Cancels any pending deferred focus so it never fires on a detached element. */
|
|
72
89
|
disconnect(): void;
|
|
@@ -74,6 +91,8 @@ declare class DateRangePickerController extends Controller<HTMLElement> {
|
|
|
74
91
|
minValueChanged(): void;
|
|
75
92
|
/** Repaints when application code (or a Turbo morph) changes `max` at runtime. */
|
|
76
93
|
maxValueChanged(): void;
|
|
94
|
+
/** Re-indexes the unavailable dates and repaints when the declared set changes. */
|
|
95
|
+
disabledDatesValueChanged(): void;
|
|
77
96
|
/** Navigates to the previous month. */
|
|
78
97
|
prev(event?: Event): void;
|
|
79
98
|
/** Navigates to the next month. */
|