stimeo-ui 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +8 -1
- package/dist/cable/index.d.ts +79 -23
- package/dist/cable/index.js +363 -436
- package/dist/cable/index.js.map +1 -1
- package/dist/controllers/accordion_controller.d.ts +4 -2
- package/dist/controllers/accordion_controller.js +166 -55
- package/dist/controllers/accordion_controller.js.map +1 -1
- package/dist/controllers/alert_dialog_controller.d.ts +12 -2
- package/dist/controllers/alert_dialog_controller.js +862 -221
- package/dist/controllers/alert_dialog_controller.js.map +1 -1
- package/dist/controllers/announcer_controller.d.ts +35 -5
- package/dist/controllers/announcer_controller.js +135 -198
- package/dist/controllers/announcer_controller.js.map +1 -1
- package/dist/controllers/aspect_ratio_controller.js +0 -10
- package/dist/controllers/aspect_ratio_controller.js.map +1 -1
- package/dist/controllers/auto_submit_controller.d.ts +30 -6
- package/dist/controllers/auto_submit_controller.js +271 -127
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/avatar_controller.js +171 -105
- package/dist/controllers/avatar_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.js +0 -130
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/bulk_select_controller.d.ts +18 -0
- package/dist/controllers/bulk_select_controller.js +262 -52
- package/dist/controllers/bulk_select_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.d.ts +24 -5
- package/dist/controllers/calendar_controller.js +175 -209
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +34 -7
- package/dist/controllers/carousel_controller.js +461 -399
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/character_counter_controller.d.ts +17 -1
- package/dist/controllers/character_counter_controller.js +246 -156
- package/dist/controllers/character_counter_controller.js.map +1 -1
- package/dist/controllers/checkbox_controller.d.ts +7 -2
- package/dist/controllers/checkbox_controller.js +32 -74
- package/dist/controllers/checkbox_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.d.ts +16 -5
- package/dist/controllers/clipboard_controller.js +168 -147
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.d.ts +16 -4
- package/dist/controllers/collapsible_controller.js +301 -120
- package/dist/controllers/collapsible_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +18 -6
- package/dist/controllers/color_picker_controller.js +149 -139
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_controller.d.ts +23 -5
- package/dist/controllers/combobox_controller.js +360 -133
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.d.ts +45 -14
- package/dist/controllers/command_palette_controller.js +970 -358
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/conditional_fields_controller.d.ts +15 -2
- package/dist/controllers/conditional_fields_controller.js +166 -124
- package/dist/controllers/conditional_fields_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.d.ts +49 -12
- package/dist/controllers/confirm_controller.js +939 -248
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +31 -4
- package/dist/controllers/context_menu_controller.js +256 -128
- package/dist/controllers/context_menu_controller.js.map +1 -1
- package/dist/controllers/count_up_controller.d.ts +13 -1
- package/dist/controllers/count_up_controller.js +73 -21
- package/dist/controllers/count_up_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.d.ts +45 -1
- package/dist/controllers/countdown_controller.js +183 -148
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +21 -2
- package/dist/controllers/currency_input_controller.js +134 -158
- package/dist/controllers/currency_input_controller.js.map +1 -1
- package/dist/controllers/data_grid_controller.d.ts +15 -4
- package/dist/controllers/data_grid_controller.js +114 -186
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +30 -5
- package/dist/controllers/date_range_picker_controller.js +288 -206
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.d.ts +13 -1
- package/dist/controllers/dialog_controller.js +858 -223
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.d.ts +24 -7
- package/dist/controllers/direct_upload_controller.js +73 -153
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/dirty_form_controller.js +14 -48
- package/dist/controllers/dirty_form_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.d.ts +19 -0
- package/dist/controllers/dismissible_controller.js +175 -21
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/drawer_controller.d.ts +39 -11
- package/dist/controllers/drawer_controller.js +933 -341
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/dropdown_controller.d.ts +31 -1
- package/dist/controllers/dropdown_controller.js +312 -82
- package/dist/controllers/dropdown_controller.js.map +1 -1
- package/dist/controllers/editable_controller.d.ts +12 -1
- package/dist/controllers/editable_controller.js +158 -68
- package/dist/controllers/editable_controller.js.map +1 -1
- package/dist/controllers/empty_state_controller.js +67 -53
- package/dist/controllers/empty_state_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.d.ts +60 -18
- package/dist/controllers/file_dropzone_controller.js +311 -207
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/filter_controller.js +67 -39
- package/dist/controllers/filter_controller.js.map +1 -1
- package/dist/controllers/flash_controller.d.ts +31 -8
- package/dist/controllers/flash_controller.js +172 -314
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +12 -3
- package/dist/controllers/focus_controller.js +654 -240
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_field_controller.js +147 -132
- package/dist/controllers/form_field_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.js +13 -86
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.d.ts +41 -9
- package/dist/controllers/frame_loading_controller.js +264 -213
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.d.ts +22 -2
- package/dist/controllers/highlight_controller.js +107 -75
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/hover_card_controller.d.ts +56 -3
- package/dist/controllers/hover_card_controller.js +309 -118
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/idle_controller.d.ts +42 -1
- package/dist/controllers/idle_controller.js +267 -101
- package/dist/controllers/idle_controller.js.map +1 -1
- package/dist/controllers/input_mask_controller.js +71 -72
- package/dist/controllers/input_mask_controller.js.map +1 -1
- package/dist/controllers/intersection_controller.d.ts +32 -13
- package/dist/controllers/intersection_controller.js +147 -106
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.d.ts +16 -0
- package/dist/controllers/lazy_frame_controller.js +9 -65
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.d.ts +34 -2
- package/dist/controllers/listbox_controller.js +306 -204
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.d.ts +1 -1
- package/dist/controllers/local_time_controller.js +54 -64
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +11 -3
- package/dist/controllers/masonry_controller.js +70 -93
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/menu_controller.d.ts +32 -7
- package/dist/controllers/menu_controller.js +214 -156
- package/dist/controllers/menu_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.d.ts +4 -4
- package/dist/controllers/menubar_controller.js +170 -329
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/meter_controller.d.ts +20 -0
- package/dist/controllers/meter_controller.js +151 -67
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +32 -4
- package/dist/controllers/multi_select_controller.js +593 -358
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/navigation_menu_controller.d.ts +8 -1
- package/dist/controllers/navigation_menu_controller.js +124 -232
- package/dist/controllers/navigation_menu_controller.js.map +1 -1
- package/dist/controllers/nested_form_controller.d.ts +10 -0
- package/dist/controllers/nested_form_controller.js +352 -182
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/network_status_controller.d.ts +23 -0
- package/dist/controllers/network_status_controller.js +242 -54
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +26 -2
- package/dist/controllers/number_input_controller.js +320 -263
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/optimistic_controller.js +16 -76
- package/dist/controllers/optimistic_controller.js.map +1 -1
- package/dist/controllers/otp_controller.d.ts +20 -4
- package/dist/controllers/otp_controller.js +198 -193
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/overflow_indicator_controller.d.ts +5 -0
- package/dist/controllers/overflow_indicator_controller.js +116 -113
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.d.ts +48 -11
- package/dist/controllers/overflow_menu_controller.js +343 -283
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.d.ts +35 -6
- package/dist/controllers/pagination_controller.js +295 -119
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +45 -8
- package/dist/controllers/password_reveal_controller.js +389 -121
- package/dist/controllers/password_reveal_controller.js.map +1 -1
- package/dist/controllers/password_strength_controller.d.ts +14 -5
- package/dist/controllers/password_strength_controller.js +99 -153
- package/dist/controllers/password_strength_controller.js.map +1 -1
- package/dist/controllers/persist_controller.d.ts +11 -4
- package/dist/controllers/persist_controller.js +237 -103
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.d.ts +19 -7
- package/dist/controllers/pointer_drag_controller.js +185 -165
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +38 -2
- package/dist/controllers/popover_controller.js +236 -97
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +44 -4
- package/dist/controllers/portal_controller.js +209 -76
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/preview_guard_controller.d.ts +10 -4
- package/dist/controllers/preview_guard_controller.js +232 -116
- package/dist/controllers/preview_guard_controller.js.map +1 -1
- package/dist/controllers/progress_controller.d.ts +11 -0
- package/dist/controllers/progress_controller.js +125 -56
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.d.ts +14 -5
- package/dist/controllers/radio_group_controller.js +117 -137
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.d.ts +43 -3
- package/dist/controllers/range_slider_controller.js +192 -105
- package/dist/controllers/range_slider_controller.js.map +1 -1
- package/dist/controllers/rating_controller.d.ts +20 -4
- package/dist/controllers/rating_controller.js +418 -207
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/read_more_controller.d.ts +2 -0
- package/dist/controllers/read_more_controller.js +134 -63
- package/dist/controllers/read_more_controller.js.map +1 -1
- package/dist/controllers/reading_progress_controller.d.ts +1 -1
- package/dist/controllers/reading_progress_controller.js +147 -135
- package/dist/controllers/reading_progress_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.d.ts +18 -4
- package/dist/controllers/relative_time_controller.js +145 -110
- package/dist/controllers/relative_time_controller.js.map +1 -1
- package/dist/controllers/reset_on_restore_controller.d.ts +85 -0
- package/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +97 -38
- package/dist/controllers/reset_on_restore_controller.js.map +1 -0
- package/dist/controllers/resizable_controller.d.ts +28 -2
- package/dist/controllers/resizable_controller.js +255 -139
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/roving_controller.js +39 -84
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.d.ts +7 -3
- package/dist/controllers/scroll_area_controller.js +428 -225
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_restore_controller.js +0 -53
- package/dist/controllers/scroll_restore_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.d.ts +28 -12
- package/dist/controllers/scroll_visibility_controller.js +395 -145
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.d.ts +6 -1
- package/dist/controllers/scrollspy_controller.js +181 -242
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.d.ts +19 -4
- package/dist/controllers/separator_controller.js +267 -140
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.d.ts +64 -7
- package/dist/controllers/sidebar_controller.js +1023 -386
- package/dist/controllers/sidebar_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.d.ts +23 -0
- package/dist/controllers/skeleton_controller.js +248 -97
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +27 -1
- package/dist/controllers/slider_controller.js +159 -79
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/smart_sticky_header_controller.d.ts +21 -3
- package/dist/controllers/smart_sticky_header_controller.js +162 -63
- package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
- package/dist/controllers/sortable_controller.js +0 -112
- package/dist/controllers/sortable_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.d.ts +39 -4
- package/dist/controllers/spinner_controller.js +277 -169
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.d.ts +5 -0
- package/dist/controllers/step_indicator_controller.js +99 -46
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stepper_controller.d.ts +12 -3
- package/dist/controllers/stepper_controller.js +114 -73
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +6 -0
- package/dist/controllers/stick_to_bottom_controller.js +262 -138
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/sticky_observer_controller.d.ts +11 -0
- package/dist/controllers/sticky_observer_controller.js +214 -49
- package/dist/controllers/sticky_observer_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.d.ts +23 -8
- package/dist/controllers/submit_once_controller.js +294 -205
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.d.ts +12 -1
- package/dist/controllers/switch_controller.js +33 -60
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tabs_controller.d.ts +2 -2
- package/dist/controllers/tabs_controller.js +35 -29
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +12 -0
- package/dist/controllers/tags_input_controller.js +136 -145
- package/dist/controllers/tags_input_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.d.ts +10 -0
- package/dist/controllers/textarea_autosize_controller.js +71 -58
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/theme_controller.d.ts +21 -9
- package/dist/controllers/theme_controller.js +130 -112
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.d.ts +18 -1
- package/dist/controllers/time_picker_controller.js +146 -80
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toast_controller.d.ts +36 -11
- package/dist/controllers/toast_controller.js +186 -297
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.d.ts +12 -3
- package/dist/controllers/toggle_group_controller.js +95 -136
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.js +68 -98
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +45 -3
- package/dist/controllers/tooltip_controller.js +158 -124
- package/dist/controllers/tooltip_controller.js.map +1 -1
- package/dist/controllers/transition_controller.d.ts +22 -4
- package/dist/controllers/transition_controller.js +123 -119
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +21 -11
- package/dist/controllers/tree_view_controller.js +150 -345
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.js +6370 -9346
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +23 -3
- package/dist/inspector/cli.js +92 -37
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +92 -56
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +8 -8
- package/dist/inspector/manifest.json +787 -100
- package/dist/positioning/index.d.ts +8 -0
- package/dist/positioning/index.js +148 -47
- package/dist/positioning/index.js.map +1 -1
- package/package.json +6 -6
- package/dist/controllers/reset_before_cache_controller.d.ts +0 -76
- package/dist/controllers/reset_before_cache_controller.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/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 * Largest delay a timer can hold: the platform stores it in a 32-bit signed\n * integer, and anything larger overflows to `1`, so a delay meant to be far in\n * the future fires almost immediately. A declared delay above this bound names\n * no delay at all, and a controller reading one falls back to its default.\n */\nexport const MAX_TIMER_DELAY_MS = 2_147_483_647;\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","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 * Suffix of the marker on the text this controller wrote into the `status` slot;\n * the marker holds that text as its value. The slot is this controller's only while\n * it shows that text and nothing else: the two still agree, and the slot holds no\n * element. While the marker holds text, a slot the consumer rewrites or empties\n * afterwards is theirs; a marker with an empty value owns the empty slot, including\n * one the consumer empties again after writing into it. An element the consumer\n * puts in the slot makes it theirs either way, for as long as it is there. A\n * restored page carries the claim along with the text.\n */\nconst OWNED_STATUS = \"owns-status\";\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 * The slot is shared with the consumer. Completion writes the label in force and\n * marks it with `data-<identifier>-owns-status`, whose value is that text; while\n * the slot still shows that text and nothing else, the controller keeps it on the\n * current `completeLabel` and takes it back once the timer leaves the complete\n * state — `reset`, `start()` counting on from it, or a completion the new reading\n * does not settle (a deadline moved forward or a count turned `up`, in place or\n * before a restore). All but `start()` leave it `paused`, so `resume()` counts\n * again.\n * An empty label puts no text there, so it never displaces what the slot already\n * holds; completing with one claims an empty slot — no text and no element — with\n * an empty marker, which owns the slot whenever it is empty. Text the consumer put\n * there stays theirs while it is there, and so does a slot they put an element in,\n * whatever its text: the label is neither followed into it nor taken back out of\n * it, and its marker stays, until the element is gone. Only a completion with a\n * label writes over either.\n *\n * `complete` dispatches `{}`; `tick` dispatches\n * `{ remaining: number, direction: \"down\" | \"up\" }`, where `remaining` is in\n * milliseconds.\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 `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 /** The status marker, in the namespace this controller is registered under. */\n get #ownedStatus(): string {\n return `data-${this.identifier}-${OWNED_STATUS}`;\n }\n\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 * Whether `connect()` has run for this connection. Stimulus delivers Value\n * callbacks ahead of it, while the run state is still the markup's own and not\n * yet settled; `connect()` brings the status up to date once it is.\n */\n #connected = false;\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.#connected = true;\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 // A completion that is still settled 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 reading that no longer settles it — a deadline\n // moved forward, or a count turned `up` — re-arms it.\n const settled = authored === \"complete\" && this.#isSettled(amount);\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) this.start();\n else this.element.setAttribute(\"data-state\", \"paused\");\n } else if (!settled) {\n // A pause the user made survives the round trip, but nothing carries a live\n // timer across it — a restored \"running\" has to be re-armed here or it renders\n // once and freezes, because start() is a no-op while the attribute still says\n // \"running\".\n this.element.setAttribute(\"data-state\", \"paused\");\n if (authored === \"running\") this.start();\n }\n // Value callbacks ran ahead of this, before the run state above was settled, so a\n // status still holding this controller's text is brought up to date here.\n this.#syncStatus();\n }\n\n override disconnect(): void {\n this.#connected = false;\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 * Follows a completion label swapped in place by a morph.\n *\n * Render only: `complete` is neither dispatched nor announced again. The status\n * changes only after the countdown has completed, and only while it still shows\n * the text this controller wrote there and nothing else, so text or an element\n * the consumer put in the slot stays. A change delivered ahead of `connect()` is\n * left to `connect()`.\n */\n completeLabelValueChanged(): void {\n if (this.#connected) this.#syncStatus();\n }\n\n /**\n * Keeps a status that still shows this controller's text, and nothing else, on\n * what the run state calls for: the label in force while complete; otherwise the\n * text is taken back, marker and all. A status holding anything else is the\n * consumer's and is left alone, marker included.\n *\n * @stimeoRenderRoot\n */\n #syncStatus(): void {\n if (!this.hasStatusTarget || !this.#ownsStatus(this.statusTarget)) return;\n if (this.#state === \"complete\") this.#writeStatus(this.statusTarget, this.completeLabelValue);\n else this.#releaseStatus(this.statusTarget);\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 * A completion the new reading no longer settles (a deadline moved forward, or a\n * count turned `up`) is handed back paused, as `connect()` hands back a restored\n * one, so `resume()` counts again; the completion text this controller wrote goes\n * with it, from a status slot that still shows it and nothing else.\n *\n * @stimeoRenderRoot\n */\n #resyncToValues(): void {\n this.#initReference();\n const amount = this.#currentAmount();\n this.#render(amount);\n if (this.#state === \"complete\" && !this.#isSettled(amount)) {\n this.element.setAttribute(\"data-state\", \"paused\");\n this.#syncStatus();\n }\n if (this.#state !== \"running\") this.#pausedAmount = this.#renderedAmount;\n }\n\n /**\n * Starts (or restarts after pause) ticking toward the deadline. Counting on from a\n * completion takes this controller's completion text back out of the status.\n */\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 // 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 // A reset timer has left the complete state, so the completion text this\n // controller wrote leaves the status slot with it; a slot the consumer rewrote or\n // put an element in stays as it is.\n this.#syncStatus();\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 /**\n * Schedules the repeating tick and marks the timer running. This is the one way\n * into `running`, and a running timer keeps none of this controller's completion\n * text in the status, so the status is settled here as on every other way out of\n * `complete`: `start()` leaves a completion directly when `direction` flips to\n * `up` and `start()` runs in the same task, ahead of the direction callback.\n *\n * @stimeoRuntimeOnly `interval` is the period of the one timer this call arms; the running state\n * it writes does not depend on a Value.\n */\n #runInterval(): void {\n this.element.setAttribute(\"data-state\", \"running\");\n this.#syncStatus();\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 /**\n * Stops at zero, marks completion, writes the completion label, emits `complete`,\n * and announces it.\n *\n * @stimeoRenderRoot\n */\n #complete(): void {\n this.#teardownInterval();\n this.#render(0);\n this.element.setAttribute(\"data-state\", \"complete\");\n this.#claimStatus();\n this.dispatch(\"complete\", { detail: {} });\n this.#announceCompletion();\n }\n\n /**\n * Writes the label in force into the status slot as this controller's text. An\n * empty label puts no text there: it claims a slot that is already empty — no\n * text and no element — empties one that is still this controller's, and leaves\n * any other slot, the consumer's, alone.\n */\n #claimStatus(): void {\n if (!this.hasStatusTarget) return;\n const status = this.statusTarget;\n const label = this.completeLabelValue;\n if (label === \"\" && !this.#isEmpty(status) && !this.#ownsStatus(status)) return;\n this.#writeStatus(status, label);\n }\n\n /**\n * Whether the status slot shows the text this controller wrote there and nothing\n * else: the text matches the marker, and the slot holds no element. Writing the\n * slot's text replaces every child node, so an element the consumer puts in the\n * slot makes it theirs even while the text still matches. An empty marker\n * matches the slot whenever it is empty.\n */\n #ownsStatus(status: HTMLElement): boolean {\n return (\n status.getAttribute(this.#ownedStatus) === status.textContent &&\n status.firstElementChild === null\n );\n }\n\n /** Whether the status slot holds nothing at all: no text and no element. */\n #isEmpty(status: HTMLElement): boolean {\n return status.textContent === \"\" && status.firstElementChild === null;\n }\n\n /** Puts `text` into the status slot and records it as this controller's. */\n #writeStatus(status: HTMLElement, text: string): void {\n if (status.textContent !== text) status.textContent = text;\n status.setAttribute(this.#ownedStatus, text);\n }\n\n /** Takes this controller's text out of the status slot, marker and all. */\n #releaseStatus(status: HTMLElement): void {\n status.textContent = \"\";\n status.removeAttribute(this.#ownedStatus);\n }\n\n /**\n * Reads the completion out through the shared announcer.\n *\n * @stimeoRuntimeOnly `announceText` words the one announcement a completion makes.\n */\n #announceCompletion(): void {\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 /** Whether a reading of `amount` leaves nothing to count: only a countdown at zero. */\n #isSettled(amount: number): boolean {\n return this.#isDown && amount <= 0;\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/listener_set.ts","../../src/utils/microtask_coalescer.ts","../../src/utils/morph_render_watcher.ts","../../src/utils/timer_bounds.ts","../../src/utils/number_bounds.ts","../../src/utils/coerce.ts","../../src/utils/number_value.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 * Owns every listener a controller attaches for one connected lifetime and\n * releases them together, so an event name is spelled once and the release\n * cannot drift from the registration.\n *\n * Detaching by hand repeats three things at the release site — the type, the\n * handler reference and the capture flag — and any one of them drifting leaves\n * a listener alive on an element the controller no longer drives. Aborting the\n * signal a registration was created with releases it whatever it was made of,\n * so the release site names nothing.\n *\n * One set per lifetime, held as a field and never rebuilt. {@link\n * ListenerSet.dispose} is the only thing that closes a generation, and it opens\n * the next one in the same step, so the lifecycle hooks can call `add` and\n * `dispose` in any order any number of times, and no `add` ever meets an\n * already aborted signal — a DOM that registers a listener on such a signal\n * would keep it forever. Rebuilding the set instead would leak:\n * `addEventListener` matches an existing registration on target, type, callback\n * and capture alone, so re-adding the same handler while the first registration\n * is live is discarded, and that live registration keeps the signal it was\n * attached with — a set built by a second `connect()` can never release what it\n * appears to own.\n *\n * Scope is the registration only. Timers, observers, leases and the order they\n * are unwound in stay with the controller, which places `dispose()` in its\n * cleanup sequence. A receiver that changes at runtime (a\n * swapped target) is rebound by calling `dispose()` and then `add()` again from\n * the rebind, not by a second set.\n *\n * The set must own every registration of a tuple it holds: a listener the same\n * target already carries for the same type, callback and capture flag keeps the\n * right to remove it, and this set's `add` for that tuple is discarded. A\n * subscription shared between instances behind a participant count, or one that\n * lives for less than the connection, keeps its own pair of calls.\n *\n * @example\n * ```ts\n * readonly #listeners = new ListenerSet();\n *\n * connect(): void {\n * this.#listeners.add(this.element, \"submit\", this.#onNativeSubmit, { capture: true });\n * this.#listeners.add(this.element, \"turbo:submit-start\", this.#onSubmitStart);\n * this.#listeners.add(this.element, \"turbo:submit-end\", this.#onSubmitEnd);\n * }\n *\n * disconnect(): void {\n * this.#listeners.dispose();\n * }\n * ```\n */\nexport class ListenerSet {\n /** The generation every `add` joins until the next `dispose()`. */\n #abort = new AbortController();\n\n /**\n * Attaches `handler` to the open generation, exactly as the caller spelled it.\n *\n * The set supplies the signal, so `options` carries everything else the DOM\n * accepts — `capture` included, which has to match at release time and no\n * longer has a second place to drift from.\n */\n add(\n target: EventTarget,\n type: string,\n handler: EventListener,\n options?: Omit<AddEventListenerOptions, \"signal\">,\n ): void {\n target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });\n }\n\n /**\n * Releases every listener of the open generation, synchronously, and opens the\n * next one. Idempotent, and safe before anything has been added.\n */\n dispose(): void {\n this.#abort.abort();\n this.#abort = new AbortController();\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 * @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","import { ListenerSet } from \"./listener_set\";\nimport { MicrotaskCoalescer } from \"./microtask_coalescer\";\n\n/**\n * Coalesces retained-element morphs and explicit lifecycle requests into one pass.\n *\n * Observe the controller's element after its initial state is settled. Morphs on\n * that element and bubbling morphs on its descendants share the same microtask\n * as {@link schedule}. The callback repairs derived output; the controller owns\n * any logical transition reporting. {@link disconnect} releases the listener and\n * cancels pending work, including work from an earlier connected lifetime.\n */\nexport class MorphRenderWatcher {\n readonly #listeners = new ListenerSet();\n readonly #pass: MicrotaskCoalescer;\n #element: Element | null = null;\n readonly #onMorph = (): void => this.#pass.schedule();\n\n /** @param run - the single pass that repairs the settled controller's output. */\n constructor(run: () => void) {\n this.#pass = new MicrotaskCoalescer(run);\n }\n\n /** Opens observation; observing the same element again preserves queued work. */\n observe(element: Element): void {\n if (this.#element === element) return;\n this.disconnect();\n this.#element = element;\n this.#pass.activate();\n this.#listeners.add(element, \"turbo:morph-element\", this.#onMorph);\n }\n\n /** Requests one pass; requests outside an observed lifetime are ignored. */\n schedule(): void {\n this.#pass.schedule();\n }\n\n /** Releases observation and pending work synchronously; safe to repeat. */\n disconnect(): void {\n this.#listeners.dispose();\n this.#pass.cancel();\n this.#element = null;\n }\n}\n","/**\n * Largest delay a timer can hold: the platform stores it in a 32-bit signed\n * integer, and anything larger overflows to `1`, so a delay meant to be far in\n * the future fires almost immediately. A declared delay above this bound names\n * no delay at all, and a controller reading one falls back to its default.\n */\nexport const MAX_TIMER_DELAY_MS = 2_147_483_647;\n","import { MAX_TIMER_DELAY_MS } from \"./timer_bounds\";\n\n/** Semantic constraints on an already decoded number, independent of the DOM. */\nexport interface NumberBounds {\n /** Finite numbers are required, including when this field is omitted. */\n readonly finite?: true;\n /** Inclusive lower endpoint. */\n readonly min?: number;\n /** Inclusive upper endpoint. */\n readonly max?: number;\n /** Exclusive lower endpoint. */\n readonly exclusiveMin?: number;\n /** Require a number without a fractional part. */\n readonly integer?: boolean;\n /** Exact decoded numbers accepted by a discrete contract. */\n readonly allowedValues?: readonly number[];\n /** JSON-safe exceptions to finite-number checking for unbounded endpoints. */\n readonly allowInfinity?: \"negative\" | \"positive\" | \"both\";\n}\n\n/** One constraint for each Number Value, including shorthand declarations. */\nexport type NumberValueConstraints<Values> = {\n readonly [Key in keyof Values as Values[Key] extends\n | NumberConstructor\n | { type: NumberConstructor }\n ? Key\n : never]: NumberBounds;\n};\n\n/** Reusable semantic domains; fractional inputs remain fractional unless excluded. */\nexport const NUMBER_BOUNDS = {\n finite: { finite: true },\n nonNegative: { finite: true, min: 0 },\n positive: { finite: true, exclusiveMin: 0 },\n nonNegativeInteger: { finite: true, min: 0, integer: true },\n positiveInteger: { finite: true, exclusiveMin: 0, integer: true },\n lowerBound: { finite: true, allowInfinity: \"negative\" },\n upperBound: { finite: true, allowInfinity: \"positive\" },\n timer: { finite: true, min: 0, max: MAX_TIMER_DELAY_MS },\n positiveTimer: { finite: true, exclusiveMin: 0, max: MAX_TIMER_DELAY_MS },\n} as const satisfies Record<string, NumberBounds>;\n\n/**\n * Checks all declared bounds without rounding, clamping, or coercing the input.\n * Infinity exceptions relax only finite checking; every other bound still applies.\n */\nexport function matchesNumberBounds(value: number, bounds: NumberBounds): boolean {\n if (!Number.isFinite(value)) {\n const direction = value === Infinity ? \"positive\" : value === -Infinity ? \"negative\" : null;\n if (direction === null) return false;\n if (bounds.allowInfinity !== \"both\" && bounds.allowInfinity !== direction) return false;\n }\n if (bounds.min !== undefined && value < bounds.min) return false;\n if (bounds.max !== undefined && value > bounds.max) return false;\n if (bounds.exclusiveMin !== undefined && value <= bounds.exclusiveMin) return false;\n if (bounds.integer && !Number.isInteger(value)) return false;\n if (bounds.allowedValues !== undefined && !bounds.allowedValues.includes(value)) return false;\n return true;\n}\n\n/** Decodes a Number Value literal; action params use their own JSON decoder. */\nexport function decodeNumberValue(raw: string): number {\n return Number(raw.replace(/_/g, \"\"));\n}\n","/**\n * Numeric coercion for controllers that accept a number through a Value, an action\n * param, or an event detail.\n *\n * Stimulus decodes action params as JSON when possible and leaves other text\n * untouched. CustomEvent detail can also supply a number or numeric string.\n * The reader accepts those two types while rejecting blank and non-finite input.\n */\n\nimport { matchesNumberBounds, type NumberBounds } from \"./number_bounds\";\n\n/**\n * Reads a finite number or nonblank numeric string; other input returns `null`.\n * Blank strings cannot silently reset a value to zero.\n */\nexport function toFiniteNumber(raw: unknown): number | null {\n if (typeof raw !== \"number\" && typeof raw !== \"string\") return null;\n if (typeof raw === \"string\" && raw.trim().length === 0) return null;\n const value = Number(raw);\n return Number.isFinite(value) ? value : null;\n}\n\n/**\n * Returns an accepted decoded Number Value or the caller's fallback unchanged.\n * The caller owns the fallback contract; this reader does not parse or write a Value.\n */\nexport function readNumber(raw: number, fallback: number, bounds: NumberBounds): number {\n return matchesNumberBounds(raw, bounds) ? raw : fallback;\n}\n","import { readNumber } from \"./coerce\";\nimport { matchesNumberBounds, type NumberBounds } from \"./number_bounds\";\n\n/** The instance identity carried by a Number Value read. */\nexport interface NumberValueOwner {\n readonly identifier: string;\n readonly element: Element;\n}\n\n/** Reads current declarations at the point of use without rewriting their attributes. */\nexport class NumberValueReader {\n /** The last rejected literal per Value; no history grows for an unchanged instance. */\n readonly #lastRejected = new Map<string, string>();\n\n /** Resolves the declared number using its own fallback and shared class contract. */\n read(\n owner: NumberValueOwner,\n name: string,\n raw: number,\n fallback: number,\n bounds: NumberBounds,\n ): number {\n const resolved = readNumber(raw, fallback, bounds);\n if (matchesNumberBounds(raw, bounds)) {\n this.#lastRejected.delete(name);\n return resolved;\n }\n const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;\n const literal = owner.element.getAttribute(attribute);\n if (literal === null) {\n this.#lastRejected.delete(name);\n return resolved;\n }\n if (this.#lastRejected.get(name) !== literal) {\n this.#lastRejected.set(name, literal);\n console.warn(\n `Stimeo UI: \"${owner.identifier}\" has an invalid number Value \"${name}\" declaration ${JSON.stringify(literal)}; using ${fallback}.`,\n );\n }\n return resolved;\n }\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\nexport { MAX_TIMER_DELAY_MS } from \"./timer_bounds\";\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { announce, fillTemplate } from \"../utils/announce\";\nimport { MorphRenderWatcher } from \"../utils/morph_render_watcher\";\nimport { NUMBER_BOUNDS, type NumberValueConstraints } from \"../utils/number_bounds\";\nimport { NumberValueReader } from \"../utils/number_value\";\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 * Suffix of the marker on the text this controller wrote into the `status` slot;\n * the marker holds that text as its value. The slot is this controller's only while\n * it shows that text and nothing else: the two still agree, and the slot holds no\n * element. While the marker holds text, a slot the consumer rewrites or empties\n * afterwards is theirs; a marker with an empty value owns the empty slot, including\n * one the consumer empties again after writing into it. An element the consumer\n * puts in the slot makes it theirs either way, for as long as it is there. A\n * restored page carries the claim along with the text.\n */\nconst OWNED_STATUS = \"owns-status\";\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 * The slot is shared with the consumer. Completion writes the label in force and\n * marks it with `data-<identifier>-owns-status`, whose value is that text; while\n * the slot still shows that text and nothing else, the controller keeps it on the\n * current `completeLabel` and takes it back once the timer leaves the complete\n * state — `reset`, `start()` counting on from it, or a completion the new reading\n * does not settle (a deadline moved forward or a count turned `up`, in place or\n * before a restore). All but `start()` leave it `paused`, so `resume()` counts\n * again.\n * An empty label puts no text there, so it never displaces what the slot already\n * holds; completing with one claims an empty slot — no text and no element — with\n * an empty marker, which owns the slot whenever it is empty. Text the consumer put\n * there stays theirs while it is there, and so does a slot they put an element in,\n * whatever its text: the label is neither followed into it nor taken back out of\n * it, and its marker stays, until the element is gone. Only a completion with a\n * label writes over either.\n *\n * `complete` dispatches `{}`; `tick` dispatches\n * `{ remaining: number, direction: \"down\" | \"up\" }`, where `remaining` is in\n * milliseconds.\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 `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. A change to\n * `interval` while the timer runs re-arms the tick at the new period, keeping the run\n * state, the reading and the completion; a paused or completed timer picks it up when\n * it next runs.\n */\nexport class CountdownController extends Controller<HTMLElement> {\n /** Numeric read boundaries share one reader for this controller instance. */\n readonly #numbers = new NumberValueReader();\n\n /** The status marker, in the namespace this controller is registered under. */\n get #ownedStatus(): string {\n return `data-${this.identifier}-${OWNED_STATUS}`;\n }\n\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\n static valueConstraints = {\n interval: NUMBER_BOUNDS.positiveTimer,\n } satisfies NumberValueConstraints<typeof CountdownController.values>;\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 statusTargets: 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 /** The period the running interval was armed with, which a new `interval` is compared with. */\n #armedPeriod = 0;\n /** Collapses a morph that swaps several render inputs at once into one re-derive. */\n readonly #resync = new MorphRenderWatcher(() => this.#resyncToValues());\n /**\n * Whether `connect()` has run for this connection. Stimulus delivers Value\n * callbacks ahead of it, while the run state is still the markup's own and not\n * yet settled; `connect()` brings the status up to date once it is.\n */\n #connected = false;\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 #phase = \"paused\";\n #valuesDirty = false;\n readonly #statusWrites = new WeakMap<HTMLElement, { original: string; text: string }>();\n\n override connect(): void {\n this.#connected = true;\n this.#resync.observe(this.element);\n this.#valuesDirty = false;\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 this.#phase = authored ?? \"paused\";\n // A completion that is still settled 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 reading that no longer settles it — a deadline\n // moved forward, or a count turned `up` — re-arms it.\n const settled = authored === \"complete\" && this.#isSettled(amount);\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) this.start();\n else this.#setPhase(\"paused\");\n } else if (!settled) {\n // A pause the user made survives the round trip, but nothing carries a live\n // timer across it — a restored \"running\" has to be re-armed here or it renders\n // once and freezes, because start() is a no-op while the attribute still says\n // \"running\".\n this.#setPhase(\"paused\");\n if (authored === \"running\") this.start();\n }\n // Value callbacks ran ahead of this, before the run state above was settled, so a\n // status still holding this controller's text is brought up to date here.\n this.#syncStatus();\n }\n\n override disconnect(): void {\n this.#connected = false;\n this.#resync.disconnect();\n this.#intervals.clearAll();\n this.#intervalId = null;\n }\n\n /** Writes the reading on screen into a days slot that arrives at runtime. */\n daysTargetConnected(): void {\n this.#resync.schedule();\n }\n\n /** Writes the reading on screen into the days slot that stays after another leaves. */\n daysTargetDisconnected(): void {\n this.#resync.schedule();\n }\n\n /** Writes the reading on screen into an hours slot that arrives at runtime. */\n hoursTargetConnected(): void {\n this.#resync.schedule();\n }\n\n /** Writes the reading on screen into the hours slot that stays after another leaves. */\n hoursTargetDisconnected(): void {\n this.#resync.schedule();\n }\n\n /** Writes the reading on screen into a minutes slot that arrives at runtime. */\n minutesTargetConnected(): void {\n this.#resync.schedule();\n }\n\n /** Writes the reading on screen into the minutes slot that stays after another leaves. */\n minutesTargetDisconnected(): void {\n this.#resync.schedule();\n }\n\n /** Writes the reading on screen into a seconds slot that arrives at runtime. */\n secondsTargetConnected(): void {\n this.#resync.schedule();\n }\n\n /** Writes the reading on screen into the seconds slot that stays after another leaves. */\n secondsTargetDisconnected(): void {\n this.#resync.schedule();\n }\n\n /** Brings a status that arrives at runtime, when it shows this controller's text, in line. */\n statusTargetConnected(): void {\n this.#resync.schedule();\n }\n\n /**\n * Takes the claim off a status that no longer resolves, even after `disconnect()`, when\n * it still shows this controller's text alone; the text stays. Brings the status that\n * stays in line.\n */\n statusTargetDisconnected(status: HTMLElement): void {\n if (!this.statusTargets.includes(status) && this.#ownsStatus(status)) {\n status.removeAttribute(this.#ownedStatus);\n this.#statusWrites.delete(status);\n }\n this.#resync.schedule();\n }\n\n /** Re-derives the display when a morph swaps the deadline in place. */\n deadlineValueChanged(): void {\n this.#valuesDirty = true;\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.#valuesDirty = true;\n this.#resync.schedule();\n }\n\n /**\n * Follows a completion label swapped in place by a morph.\n *\n * Render only: `complete` is neither dispatched nor announced again. The status\n * changes only after the countdown has completed, and only while it still shows\n * the text this controller wrote there and nothing else, so text or an element\n * the consumer put in the slot stays. A change delivered ahead of `connect()` is\n * left to `connect()`.\n */\n completeLabelValueChanged(): void {\n if (this.#connected) this.#syncStatus();\n }\n\n /**\n * Follows a tick period changed at runtime. A running countdown swaps its one interval\n * for one at the new period, starting from now; its run state, reading, deadline and\n * completion are untouched, and nothing ticks or repaints until the next period ends. A\n * paused or completed countdown starts nothing and uses the new period when it next\n * runs, and a declaration that resolves to the period already running changes nothing.\n * Stimulus also calls this ahead of `connect()`, when no interval runs.\n */\n intervalValueChanged(): void {\n if (this.#intervalId === null || this.#armedPeriod === this.#safeInterval) return;\n this.#teardownInterval();\n this.#armTick();\n }\n\n /**\n * Keeps a status that still shows this controller's text, and nothing else, on\n * what the run state calls for: the label in force while complete; otherwise the\n * text is taken back, marker and all. A status holding anything else is the\n * consumer's and is left alone, marker included.\n *\n * @stimeoRenderRoot\n */\n #syncStatus(): void {\n if (!this.hasStatusTarget || !this.#ownsStatus(this.statusTarget)) return;\n if (this.#state === \"complete\") this.#writeStatus(this.statusTarget, this.completeLabelValue);\n else this.#releaseStatus(this.statusTarget);\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. A running one left without a\n * parseable deadline stops instead and rests paused at zero, since `start()` and\n * `resume()` arm nothing on such an anchor; a parseable deadline declared later\n * repaints it and waits for `resume()`. While paused the stored amount follows the\n * new reading, or resume would continue from the old deadline. A completion the new\n * reading no longer settles (a deadline moved forward, or a count turned `up`) is\n * handed back paused, as `connect()` hands back a restored one, so `resume()` counts\n * again; the completion text this controller wrote goes with it, from a status slot\n * that still shows it and nothing else.\n *\n * @stimeoRenderRoot\n */\n #resyncToValues(): void {\n this.#setPhase(this.#state);\n if (!this.#valuesDirty) {\n this.#render(this.#renderedAmount);\n this.#syncStatus();\n return;\n }\n this.#valuesDirty = false;\n this.#initReference();\n const amount = this.#currentAmount();\n this.#render(amount);\n if (this.#state === \"running\" && !this.#isValidDeadline) {\n this.#teardownInterval();\n this.#setPhase(\"paused\");\n }\n if (this.#state === \"complete\" && !this.#isSettled(amount)) this.#setPhase(\"paused\");\n if (this.#state !== \"running\") this.#pausedAmount = this.#renderedAmount;\n this.#syncStatus();\n }\n\n /**\n * Starts (or restarts after pause) ticking toward the deadline. Counting on from a\n * completion takes this controller's completion text back out of the status.\n */\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.#setPhase(\"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 // 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.#setPhase(\"paused\");\n // A reset timer has left the complete state, so the completion text this\n // controller wrote leaves the status slot with it; a slot the consumer rewrote or\n // put an element in stays as it is.\n this.#syncStatus();\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 /**\n * Schedules the repeating tick and marks the timer running. This is the one way\n * into `running`, and a running timer keeps none of this controller's completion\n * text in the status, so the status is settled here as on every other way out of\n * `complete`: `start()` leaves a completion directly when `direction` flips to\n * `up` and `start()` runs in the same task, ahead of the direction callback.\n */\n #runInterval(): void {\n this.#setPhase(\"running\");\n this.#syncStatus();\n this.#armTick();\n }\n\n /**\n * Arms the repeating tick at the declared period and records that period.\n *\n * @stimeoRuntimeOnly `interval` is the period of the one timer this call arms.\n */\n #armTick(): void {\n this.#armedPeriod = this.#safeInterval;\n this.#intervalId = this.#intervals.set(() => this.#tick(), this.#armedPeriod);\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 /**\n * Stops at zero, marks completion, writes the completion label, emits `complete`,\n * and announces it.\n */\n #complete(): void {\n this.#teardownInterval();\n this.#render(0);\n this.#setPhase(\"complete\");\n this.#claimStatus();\n this.dispatch(\"complete\", { detail: {} });\n this.#announceCompletion();\n }\n\n /**\n * Writes the label in force into the status slot as this controller's text. An\n * empty label puts no text there: it claims a slot that is already empty — no\n * text and no element — empties one that is still this controller's, and leaves\n * any other slot, the consumer's, alone.\n */\n #claimStatus(): void {\n if (!this.hasStatusTarget) return;\n const status = this.statusTarget;\n const label = this.completeLabelValue;\n if (label === \"\" && !this.#isEmpty(status) && !this.#ownsStatus(status)) return;\n this.#writeStatus(status, label);\n }\n\n /**\n * Whether the status slot shows the text this controller wrote there and nothing\n * else: the text matches the marker, and the slot holds no element. Writing the\n * slot's text replaces every child node, so an element the consumer puts in the\n * slot makes it theirs even while the text still matches. An empty marker\n * matches the slot whenever it is empty.\n */\n #ownsStatus(status: HTMLElement): boolean {\n const held = this.#statusWrites.get(status);\n if (\n !status.hasAttribute(this.#ownedStatus) &&\n held &&\n status.firstElementChild === null &&\n (status.textContent === held.text || status.textContent === held.original)\n )\n return true;\n return (\n status.getAttribute(this.#ownedStatus) === status.textContent &&\n status.firstElementChild === null\n );\n }\n\n /** Whether the status slot holds nothing at all: no text and no element. */\n #isEmpty(status: HTMLElement): boolean {\n return status.textContent === \"\" && status.firstElementChild === null;\n }\n\n /** Puts `text` into the status slot and records it as this controller's. */\n #writeStatus(status: HTMLElement, text: string): void {\n const original = this.#statusWrites.get(status)?.original ?? status.textContent ?? \"\";\n this.#statusWrites.set(status, { original, text });\n if (status.textContent !== text) status.textContent = text;\n if (status.getAttribute(this.#ownedStatus) !== text)\n status.setAttribute(this.#ownedStatus, text);\n }\n\n /** Takes this controller's text out of the status slot, marker and all. */\n #releaseStatus(status: HTMLElement): void {\n status.textContent = \"\";\n status.removeAttribute(this.#ownedStatus);\n this.#statusWrites.delete(status);\n }\n\n /**\n * Reads the completion out through the shared announcer.\n *\n * @stimeoRuntimeOnly `announceText` words the one announcement a completion makes.\n */\n #announceCompletion(): void {\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 /** Whether a reading of `amount` leaves nothing to count: only a countdown at zero. */\n #isSettled(amount: number): boolean {\n return this.#isDown && amount <= 0;\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 /** Publishes the phase and retains it while a morph removes its output. */\n #setPhase(phase: string): void {\n this.#phase = phase;\n this.element.setAttribute(\"data-state\", phase);\n }\n\n /** Current lifecycle phase as reflected on `data-state`. */\n get #state(): string {\n return this.element.getAttribute(\"data-state\") ?? this.#phase;\n }\n /** Current `interval` declaration resolved against its numeric contract. */\n get #safeInterval(): number {\n return this.#numbers.read(\n this,\n \"interval\",\n this.intervalValue,\n CountdownController.values.interval.default,\n CountdownController.valueConstraints.interval,\n );\n }\n}\n"]}
|
|
@@ -72,7 +72,8 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
72
72
|
* Value change, when the controller connects, and when a display arrives; the
|
|
73
73
|
* hot path only ever sees validated values through cached `Intl.NumberFormat`
|
|
74
74
|
* instances, never built per keystroke. Late-arriving or swapped `field` /
|
|
75
|
-
* `srValue` / `display` targets are re-synced on connection
|
|
75
|
+
* `srValue` / `display` targets are re-synced on connection; one that arrives
|
|
76
|
+
* behind the target in charge is re-synced when that target leaves.
|
|
76
77
|
*
|
|
77
78
|
* Reconnecting starts from the committed value: a display still showing this
|
|
78
79
|
* controller's own rendering is not read back but re-rendered from that value
|
|
@@ -106,6 +107,14 @@ declare class CurrencyInputController extends Controller<HTMLElement> {
|
|
|
106
107
|
default: number;
|
|
107
108
|
};
|
|
108
109
|
};
|
|
110
|
+
static valueConstraints: {
|
|
111
|
+
precision: {
|
|
112
|
+
finite: true;
|
|
113
|
+
integer: true;
|
|
114
|
+
min: number;
|
|
115
|
+
max: number;
|
|
116
|
+
};
|
|
117
|
+
};
|
|
109
118
|
static actions: readonly ["format", "onInput"];
|
|
110
119
|
static events: readonly ["change", "reconcile"];
|
|
111
120
|
readonly displayTarget: HTMLInputElement;
|
|
@@ -148,18 +157,28 @@ declare class CurrencyInputController extends Controller<HTMLElement> {
|
|
|
148
157
|
* under the declarations the page carries now, reading this controller's own
|
|
149
158
|
* rendering with the separators it was written under; an amount that differs
|
|
150
159
|
* from the committed one is the page's, so it is reported as `reconcile`.
|
|
160
|
+
* A display that arrives behind the one in charge is only tracked: the one
|
|
161
|
+
* ahead may be composing against the declarations in force, and the arrival
|
|
162
|
+
* is read when that one leaves.
|
|
151
163
|
*/
|
|
152
164
|
displayTargetConnected(target: HTMLInputElement): void;
|
|
153
165
|
/**
|
|
154
166
|
* Stops tracking a display that leaves. A composition running on it hears no
|
|
155
167
|
* `compositionend` any more, so it ends here; a change it held waits for the
|
|
156
|
-
* next display to be read.
|
|
168
|
+
* next display to be read. A composition running on another display still hears
|
|
169
|
+
* its own `compositionend`, so its text is left to it. A display that stays is
|
|
170
|
+
* read again as a declaration change reads it, so an amount it holds that
|
|
171
|
+
* differs from the committed one is the page's and is reported as `reconcile`.
|
|
157
172
|
*/
|
|
158
173
|
displayTargetDisconnected(target: HTMLInputElement): void;
|
|
159
174
|
/** Syncs a late-arriving hidden field without touching the display or events. */
|
|
160
175
|
fieldTargetConnected(): void;
|
|
176
|
+
/** Syncs the hidden field that stays when an earlier one leaves, silently. */
|
|
177
|
+
fieldTargetDisconnected(): void;
|
|
161
178
|
/** Syncs a late-arriving screen-reader span the same way. */
|
|
162
179
|
srValueTargetConnected(): void;
|
|
180
|
+
/** Syncs the screen-reader span that stays when an earlier one leaves, silently. */
|
|
181
|
+
srValueTargetDisconnected(): void;
|
|
163
182
|
/** Re-groups digits as the user types, preserving the caret position. */
|
|
164
183
|
onInput(event: Event): void;
|
|
165
184
|
/** Applies the fixed-precision rounding on blur. */
|