stimeo-ui 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +184 -0
- package/dist/controllers/announcer_controller.js.map +1 -1
- package/dist/controllers/aspect_ratio_controller.d.ts +3 -5
- package/dist/controllers/aspect_ratio_controller.js +19 -11
- package/dist/controllers/aspect_ratio_controller.js.map +1 -1
- package/dist/controllers/avatar_controller.d.ts +36 -15
- package/dist/controllers/avatar_controller.js +195 -40
- package/dist/controllers/avatar_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.js +5 -1
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +12 -3
- package/dist/controllers/carousel_controller.js +90 -10
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/checkbox_controller.d.ts +31 -5
- package/dist/controllers/checkbox_controller.js +136 -25
- package/dist/controllers/checkbox_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.js +8 -3
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.d.ts +1 -1
- package/dist/controllers/collapsible_controller.js +4 -1
- package/dist/controllers/collapsible_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +9 -3
- package/dist/controllers/color_picker_controller.js +41 -11
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +1 -1
- package/dist/controllers/context_menu_controller.js +2 -2
- package/dist/controllers/context_menu_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.d.ts +5 -3
- package/dist/controllers/countdown_controller.js +5 -1
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +23 -4
- package/dist/controllers/date_range_picker_controller.js +162 -31
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.d.ts +1 -1
- package/dist/controllers/direct_upload_controller.js +3 -3
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/empty_state_controller.d.ts +27 -7
- package/dist/controllers/empty_state_controller.js +107 -16
- package/dist/controllers/empty_state_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.d.ts +7 -1
- package/dist/controllers/file_dropzone_controller.js +26 -3
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/flash_controller.d.ts +25 -5
- package/dist/controllers/flash_controller.js +161 -21
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +2 -3
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.js +8 -2
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.d.ts +19 -2
- package/dist/controllers/frame_loading_controller.js +94 -22
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.d.ts +8 -4
- package/dist/controllers/highlight_controller.js +38 -1
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/hover_card_controller.d.ts +2 -2
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/idle_controller.d.ts +5 -4
- package/dist/controllers/idle_controller.js +39 -6
- package/dist/controllers/idle_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.js +2 -0
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +1 -1
- package/dist/controllers/masonry_controller.js +1 -1
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.js +5 -3
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/meter_controller.js +3 -1
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +46 -12
- package/dist/controllers/multi_select_controller.js +460 -151
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/network_status_controller.d.ts +15 -13
- package/dist/controllers/network_status_controller.js +1 -3
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +29 -3
- package/dist/controllers/number_input_controller.js +455 -64
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.js +5 -1
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.js +38 -1
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_strength_controller.d.ts +4 -4
- package/dist/controllers/password_strength_controller.js +21 -3
- package/dist/controllers/password_strength_controller.js.map +1 -1
- package/dist/controllers/persist_controller.d.ts +4 -4
- package/dist/controllers/persist_controller.js +24 -5
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.js +10 -0
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +2 -2
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +1 -2
- package/dist/controllers/portal_controller.js +10 -0
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/progress_controller.d.ts +1 -1
- package/dist/controllers/progress_controller.js +7 -3
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.d.ts +41 -15
- package/dist/controllers/radio_group_controller.js +540 -56
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.d.ts +23 -0
- package/dist/controllers/range_slider_controller.js +385 -94
- package/dist/controllers/range_slider_controller.js.map +1 -1
- package/dist/controllers/rating_controller.d.ts +36 -31
- package/dist/controllers/rating_controller.js +274 -89
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.js +2 -0
- package/dist/controllers/relative_time_controller.js.map +1 -1
- package/dist/controllers/resizable_controller.js +33 -0
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/roving_controller.d.ts +4 -0
- package/dist/controllers/roving_controller.js +60 -5
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.d.ts +7 -1
- package/dist/controllers/scroll_area_controller.js +155 -23
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.js +33 -0
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/separator_controller.js +13 -17
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.d.ts +2 -2
- package/dist/controllers/skeleton_controller.js +71 -3
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +17 -1
- package/dist/controllers/slider_controller.js +325 -48
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.d.ts +15 -10
- package/dist/controllers/spinner_controller.js +18 -3
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.d.ts +1 -1
- package/dist/controllers/step_indicator_controller.js +3 -1
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stepper_controller.js +2 -0
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/switch_controller.d.ts +12 -8
- package/dist/controllers/switch_controller.js +162 -18
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +41 -11
- package/dist/controllers/tags_input_controller.js +356 -120
- package/dist/controllers/tags_input_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.js +1 -1
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.d.ts +42 -10
- package/dist/controllers/time_picker_controller.js +296 -104
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.d.ts +39 -13
- package/dist/controllers/toggle_group_controller.js +378 -55
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.js +5 -3
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +2 -2
- package/dist/controllers/tooltip_controller.js.map +1 -1
- package/dist/controllers/transition_controller.d.ts +2 -3
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +1 -2
- package/dist/controllers/tree_view_controller.js +24 -4
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.js +3811 -1048
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +92 -4
- package/dist/inspector/cli.js +174 -6
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +234 -8
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +24 -24
- package/dist/inspector/manifest.json +573 -59
- package/dist/positioning/index.d.ts +2 -2
- package/dist/positioning/index.js.map +1 -1
- package/package.json +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/announce.ts","../../src/utils/min_duration_floor.ts","../../src/utils/safe_timeout.ts","../../src/controllers/skeleton_controller.ts"],"names":[],"mappings":"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","file":"skeleton_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","import type { SafeTimeout } from \"./safe_timeout\";\n\n/**\n * Holds a transient state visible for a minimum time before letting it end.\n *\n * A loading indicator that appears and disappears within a few frames reads as a\n * flicker rather than as feedback, so several controllers keep one up for a floor\n * of `minDuration` milliseconds measured from the moment it became visible. The\n * shape is always the same: note the moment, and when the end signal arrives\n * either run it now, because the floor has already passed, or hold it on a timer\n * for whatever is left.\n *\n * The floor **borrows** a {@link SafeTimeout} instead of owning one, so a\n * controller that also schedules other work (a show delay) keeps every timer in a\n * single registry and one `clearAll()` in its teardown still drops them all.\n * Cancelling after such a bulk clear is safe: the registry no-ops on an id it no\n * longer holds.\n *\n * Scope is the floor itself. **When an end signal is allowed to arrive at all\n * stays with the controller**, because the answers differ — one keeps the first\n * queued finish and ignores repeats, another lets the newest replace it — and\n * flattening that into a shared default would silently change what the consumers\n * already promise.\n *\n * @example\n * ```ts\n * readonly #timers = new SafeTimeout();\n * readonly #floor = new MinDurationFloor(this.#timers);\n *\n * #show() { this.#floor.begin(); … }\n * stop() { this.#floor.schedule(this.minDurationValue, () => this.#hide()); }\n * ```\n */\nexport class MinDurationFloor {\n readonly #timers: SafeTimeout;\n\n /** Pending finish timer id, or `null` when nothing is held back. */\n #timerId: number | null = null;\n\n /** Epoch ms the floor is measured from. */\n #since = 0;\n\n /** @param timers - the controller's registry; the floor schedules into it. */\n constructor(timers: SafeTimeout) {\n this.#timers = timers;\n }\n\n /** Starts the floor: call when the state being held becomes visible. */\n begin(): void {\n this.#since = Date.now();\n }\n\n /** True while a finish is held back waiting for the floor to elapse. */\n get pending(): boolean {\n return this.#timerId !== null;\n }\n\n /**\n * Runs `finish` once the floor has elapsed, immediately when it already has.\n *\n * A held-back finish is **replaced**, never stacked: only the most recently\n * queued id is cancellable, so a second timer would outlive every cancel and\n * end a state that has since restarted. Controllers that want the first signal\n * to win guard on {@link pending} before calling.\n */\n schedule(minDuration: number, finish: () => void): void {\n this.cancel();\n const remaining = minDuration - (Date.now() - this.#since);\n if (remaining > 0) {\n this.#timerId = this.#timers.set(() => {\n this.#timerId = null;\n finish();\n }, remaining);\n } else {\n finish();\n }\n }\n\n /** Drops a held-back finish. Safe when none is queued, or after a bulk clear. */\n cancel(): void {\n if (this.#timerId !== null) {\n this.#timers.clear(this.#timerId);\n this.#timerId = null;\n }\n }\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { announce, fillTemplate } from \"../utils/announce\";\nimport { MinDurationFloor } from \"../utils/min_duration_floor\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/**\n * Headless skeleton/placeholder manager. No dedicated APG pattern; it follows\n * the `aria-busy` + show/hide practice.\n *\n * Markup contract (identifier: `stimeo--skeleton`):\n * <div data-controller=\"stimeo--skeleton\" aria-busy=\"true\"\n * data-stimeo--skeleton-min-duration-value=\"300\"\n * data-action=\"content:ready->stimeo--skeleton#ready\">\n * <div aria-hidden=\"true\" data-stimeo--skeleton-target=\"placeholder\"></div>\n * <div hidden data-stimeo--skeleton-target=\"content\"></div>\n * </div>\n *\n * Starts in the loading state (placeholder shown, real content hidden, region\n * `aria-busy=\"true\"`); `ready` swaps to the content and clears busy. The\n * placeholder is `aria-hidden` (decorative) so assistive tech never reads the\n * skeleton. `minDuration` keeps the placeholder up long enough to avoid a flash\n * when content arrives almost immediately.\n *\n * @remarks\n * Behavior only — skeleton shapes/animation are the consumer's. The\n * min-duration wait is held by {@link MinDurationFloor} on a {@link SafeTimeout}\n * torn down on `disconnect()` (Turbo navigation included).\n */\nexport class SkeletonController extends Controller<HTMLElement> {\n static override targets = [\"placeholder\", \"content\"];\n static override values = {\n announceReadyText: { type: String, default: \"\" },\n minDuration: { type: Number, default: 0 },\n };\n static actions = [\"ready\", \"reset\"] as const;\n static events = [\"ready\"] as const;\n\n declare readonly placeholderTarget: HTMLElement;\n declare readonly contentTarget: HTMLElement;\n declare readonly hasPlaceholderTarget: boolean;\n declare readonly hasContentTarget: boolean;\n\n declare minDurationValue: number;\n declare announceReadyTextValue: string;\n\n readonly #timers = new SafeTimeout();\n readonly #floor = new MinDurationFloor(this.#timers);\n\n override connect(): void {\n if (this.#state !== \"ready\") {\n this.#enterLoading();\n }\n }\n\n override disconnect(): void {\n this.#timers.clearAll();\n this.#floor.cancel();\n }\n\n /** Swaps to the real content. Honors `minDuration` to prevent a flash. */\n ready(): void {\n // The first signal wins: a repeat while the floor still holds the reveal back\n // must not restart the wait, or a stream of ready events keeps postponing it.\n if (this.#state === \"ready\" || this.#floor.pending) return;\n this.#floor.schedule(this.minDurationValue, () => this.#reveal());\n }\n\n /** Returns to the loading state (e.g. a Turbo Stream re-fetch). */\n reset(): void {\n this.#floor.cancel();\n this.#enterLoading();\n }\n\n /** Shows the placeholder, hides content, and marks the region busy. */\n #enterLoading(): void {\n this.#floor.begin();\n if (this.hasPlaceholderTarget) this.placeholderTarget.hidden = false;\n if (this.hasContentTarget) this.contentTarget.hidden = true;\n this.element.setAttribute(\"aria-busy\", \"true\");\n this.element.setAttribute(\"data-state\", \"loading\");\n }\n\n /** Hides the placeholder, shows content, and clears the busy state. */\n #reveal(): void {\n if (this.hasPlaceholderTarget) this.placeholderTarget.hidden = true;\n if (this.hasContentTarget) this.contentTarget.hidden = false;\n this.element.setAttribute(\"aria-busy\", \"false\");\n this.element.setAttribute(\"data-state\", \"ready\");\n this.dispatch(\"ready\", { detail: {} });\n // loading → ready is the transition; the skeleton itself carries no words.\n announce(fillTemplate(this.announceReadyTextValue, {}));\n }\n\n /** Current lifecycle phase as reflected on `data-state`. */\n get #state(): string {\n return this.element.getAttribute(\"data-state\") ?? \"loading\";\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/announce.ts","../../src/utils/detach_gate.ts","../../src/utils/min_duration_floor.ts","../../src/utils/safe_timeout.ts","../../src/controllers/skeleton_controller.ts"],"names":[],"mappings":"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","file":"skeleton_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 * Shared \"in-page move vs real detach\" discriminator for `disconnect()`.\n *\n * A mid-session `disconnect()` whose element is still in the document is\n * ambiguous. It can be:\n *\n * - an **in-page move** — a consumer (sortable, a teleport) re-inserted the\n * element; Stimulus reconnects the SAME instance in the same mutation batch\n * and the controller's runtime state must survive;\n * - a **detach that keeps the element** — `data-controller` no longer lists the\n * identifier (a Turbo 8 morph), or the element moved outside the observed\n * root (a scoped `Application.start(root)`, a shadow root); no reconnect will\n * ever come and the controller must tear down or it leaks its sessions,\n * document listeners, and moved nodes.\n *\n * Neither available signal decides this alone. A synchronous `data-controller`\n * token check catches the morph case but misses an observed-root exit (the\n * token stays, the reconnect never comes). Deferring teardown one microtask and\n * letting `connect()` cancel it catches the root exit, but leans on Stimulus\n * reconnecting moved elements within the same mutation batch — internal\n * behavior, not a public contract. {@link DetachGate} composes both: the token\n * check is the synchronous fast path (no probe, no reliance on batching), the\n * microtask probe is the fallback for the genuinely ambiguous remainder.\n *\n * Policy stays with the consumer (the `SafeTimeout` / `FocusTrap` lineage —\n * this util owns the discrimination only):\n *\n * - **Probe** ({@link disconnected}) when the teardown must eventually happen —\n * live drag sessions, document listeners, teleported *content*. Call\n * {@link cancel} from `connect()` (a reconnect means in-page move) and from\n * the head of any direct teardown path (see below).\n * - **Keep** (bare {@link DetachGate.isDetached}) when undoing the ambiguous\n * case would fight the controller's own effect: a controller that moves its\n * OWN element (portal's no-`content` form) exits a scoped observed root as\n * its normal job — a probe-driven teardown would restore the element into\n * the root, reconnect, re-teleport, and disconnect again, forever. Such\n * controllers keep their state on an ambiguous disconnect and tear down only\n * on {@link DetachGate.isDetached}.\n *\n * Event convention for probe consumers: teardown into a dead (detached) tree is\n * silent — consumers restore from their own `connect()`; a detach that keeps the\n * tree alive must end an in-flight session with a `cancel` event, or composing\n * consumers strand their session bookkeeping.\n */\n\n/** The slice of a Stimulus controller the gate inspects. */\nexport interface DetachGateHost {\n readonly element: Element;\n readonly identifier: string;\n}\n\n/**\n * Per-controller-instance gate deciding whether a `disconnect()` is a real\n * detach. Hold one per controller (`#gate = new DetachGate()`) — Stimulus\n * reuses the instance across reconnects, so the pending probe state carries\n * over exactly as the protocol needs.\n *\n * @example\n * ```ts\n * #gate = new DetachGate();\n *\n * connect(): void {\n * this.#gate.cancel(); // a reconnect: the element moved in-page\n * // …\n * }\n *\n * disconnect(): void {\n * this.#gate.disconnected(this, () => this.#teardown());\n * }\n *\n * #teardown(): void {\n * this.#gate.cancel(); // disarm a still-queued probe (double-run guard)\n * // …\n * }\n * ```\n */\nexport class DetachGate {\n /** Set while a probe is queued, waiting for a reconnect to cancel it. */\n #pending = false;\n\n /**\n * True while a probe is queued — the last disconnect was ambiguous and no\n * reconnect has cancelled it yet. Read it from `connect()` to tell the\n * reconnect half of an in-page move from a first connect: a controller whose\n * initialisation restarts a measurement (a min-duration floor, an elapsed\n * counter) must skip it for the move, where nothing actually restarted.\n */\n get pending(): boolean {\n return this.#pending;\n }\n\n /**\n * True when the disconnect is definitely a real detach — the element left\n * the document, or `data-controller` no longer lists the identifier. False\n * means ambiguous (in-page move or observed-root exit), NOT \"alive\".\n */\n static isDetached(host: DetachGateHost): boolean {\n if (!host.element.isConnected) return true;\n const tokens = (host.element.getAttribute(\"data-controller\") ?? \"\").split(/\\s+/);\n return !tokens.includes(host.identifier);\n }\n\n /**\n * Call from `disconnect()`: runs `teardown` synchronously on a definite\n * detach (fast path), otherwise defers it one microtask — a reconnect\n * ({@link cancel} from `connect()`) keeps the state, no reconnect runs it.\n * One microtask is the whole probe window: Stimulus reconnects a moved\n * element within the same mutation batch, before the checkpoint drains.\n */\n disconnected(host: DetachGateHost, teardown: () => void): void {\n if (DetachGate.isDetached(host)) {\n // Also disarms a probe a previous ambiguous disconnect left queued\n // (defer, element removed, disconnect again) — exactly one teardown.\n this.#pending = false;\n teardown();\n return;\n }\n this.#pending = true;\n queueMicrotask(() => {\n if (!this.#pending) return;\n this.#pending = false;\n teardown();\n });\n }\n\n /**\n * Disarms a pending probe. Call from `connect()` (the reconnect that proves\n * an in-page move) and from the head of any teardown path not routed through\n * {@link disconnected} (disabled-toggle, Escape), so an orphaned probe can\n * never run the teardown a second time.\n */\n cancel(): void {\n this.#pending = false;\n }\n}\n","import type { SafeTimeout } from \"./safe_timeout\";\n\n/**\n * Holds a transient state visible for a minimum time before letting it end.\n *\n * A loading indicator that appears and disappears within a few frames reads as a\n * flicker rather than as feedback, so several controllers keep one up for a floor\n * of `minDuration` milliseconds measured from the moment it became visible. The\n * shape is always the same: note the moment, and when the end signal arrives\n * either run it now, because the floor has already passed, or hold it on a timer\n * for whatever is left.\n *\n * The floor **borrows** a {@link SafeTimeout} instead of owning one, so a\n * controller that also schedules other work (a show delay) keeps every timer in a\n * single registry and one `clearAll()` in its teardown still drops them all.\n * Cancelling after such a bulk clear is safe: the registry no-ops on an id it no\n * longer holds.\n *\n * Scope is the floor itself. **When an end signal is allowed to arrive at all\n * stays with the controller**, because the answers differ — one keeps the first\n * queued finish and ignores repeats, another lets the newest replace it — and\n * flattening that into a shared default would silently change what the consumers\n * already promise.\n *\n * @example\n * ```ts\n * readonly #timers = new SafeTimeout();\n * readonly #floor = new MinDurationFloor(this.#timers);\n *\n * #show() { this.#floor.begin(); … }\n * stop() { this.#floor.schedule(this.minDurationValue, () => this.#hide()); }\n * ```\n */\nexport class MinDurationFloor {\n readonly #timers: SafeTimeout;\n\n /** Pending finish timer id, or `null` when nothing is held back. */\n #timerId: number | null = null;\n\n /** Epoch ms the floor is measured from. */\n #since = 0;\n\n /** @param timers - the controller's registry; the floor schedules into it. */\n constructor(timers: SafeTimeout) {\n this.#timers = timers;\n }\n\n /** Starts the floor: call when the state being held becomes visible. */\n begin(): void {\n this.#since = Date.now();\n }\n\n /** True while a finish is held back waiting for the floor to elapse. */\n get pending(): boolean {\n return this.#timerId !== null;\n }\n\n /**\n * Runs `finish` once the floor has elapsed, immediately when it already has.\n *\n * A held-back finish is **replaced**, never stacked: only the most recently\n * queued id is cancellable, so a second timer would outlive every cancel and\n * end a state that has since restarted. Controllers that want the first signal\n * to win guard on {@link pending} before calling.\n */\n schedule(minDuration: number, finish: () => void): void {\n this.cancel();\n const remaining = minDuration - (Date.now() - this.#since);\n if (remaining > 0) {\n this.#timerId = this.#timers.set(() => {\n this.#timerId = null;\n finish();\n }, remaining);\n } else {\n finish();\n }\n }\n\n /** Drops a held-back finish. Safe when none is queued, or after a bulk clear. */\n cancel(): void {\n if (this.#timerId !== null) {\n this.#timers.clear(this.#timerId);\n this.#timerId = null;\n }\n }\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { announce, fillTemplate } from \"../utils/announce\";\nimport { DetachGate } from \"../utils/detach_gate\";\nimport { MinDurationFloor } from \"../utils/min_duration_floor\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/**\n * Headless skeleton/placeholder manager. No dedicated APG pattern; it follows\n * the `aria-busy` + show/hide practice.\n *\n * Markup contract (identifier: `stimeo--skeleton`):\n * <div data-controller=\"stimeo--skeleton\" aria-busy=\"true\"\n * data-stimeo--skeleton-min-duration-value=\"300\"\n * data-action=\"content:ready->stimeo--skeleton#ready\">\n * <div aria-hidden=\"true\" data-stimeo--skeleton-target=\"placeholder\"></div>\n * <div hidden data-stimeo--skeleton-target=\"content\"></div>\n * </div>\n *\n * Starts in the loading state (placeholder shown, real content hidden, region\n * `aria-busy=\"true\"`); `ready` swaps to the content and clears busy. The\n * placeholder is `aria-hidden` (decorative) so assistive tech never reads the\n * skeleton. `minDuration` keeps the placeholder up long enough to avoid a flash\n * when content arrives almost immediately.\n *\n * @remarks\n * Behavior only — skeleton shapes/animation are the consumer's. The\n * min-duration wait is held by {@link MinDurationFloor} on a {@link SafeTimeout},\n * kept across an in-page move and dropped on a real detach via {@link DetachGate}.\n */\nexport class SkeletonController extends Controller<HTMLElement> {\n static override targets = [\"placeholder\", \"content\"];\n static override values = {\n announceReadyText: { type: String, default: \"\" },\n minDuration: { type: Number, default: 0 },\n };\n static actions = [\"ready\", \"reset\"] as const;\n static events = [\"ready\"] as const;\n\n declare readonly placeholderTarget: HTMLElement;\n declare readonly contentTarget: HTMLElement;\n declare readonly hasPlaceholderTarget: boolean;\n declare readonly hasContentTarget: boolean;\n\n declare minDurationValue: number;\n declare announceReadyTextValue: string;\n\n readonly #timers = new SafeTimeout();\n readonly #floor = new MinDurationFloor(this.#timers);\n readonly #gate = new DetachGate();\n\n override connect(): void {\n // A probe still queued means this is the reconnect half of an in-page move.\n // The placeholder never left the screen, so the loading hooks already hold\n // the values `#enterLoading()` would write and the floor is still measuring\n // from the moment the skeleton actually appeared — restarting it here would\n // hold the reveal back for longer than `minDuration` asks.\n const moved = this.#gate.pending;\n this.#gate.cancel();\n if (!moved && this.#state !== \"ready\") {\n this.#enterLoading();\n }\n }\n\n override disconnect(): void {\n this.#gate.disconnected(this, () => this.#teardown());\n }\n\n /** Swaps to the real content. Honors `minDuration` to prevent a flash. */\n ready(): void {\n // The first signal wins: a repeat while the floor still holds the reveal back\n // must not restart the wait, or a stream of ready events keeps postponing it.\n if (this.#state === \"ready\" || this.#floor.pending) return;\n this.#floor.schedule(this.minDurationValue, () => this.#reveal());\n }\n\n /** Returns to the loading state (e.g. a Turbo Stream re-fetch). */\n reset(): void {\n this.#floor.cancel();\n this.#enterLoading();\n }\n\n /** Shows the placeholder, hides content, and marks the region busy. */\n #enterLoading(): void {\n this.#floor.begin();\n if (this.hasPlaceholderTarget) this.placeholderTarget.hidden = false;\n if (this.hasContentTarget) this.contentTarget.hidden = true;\n this.element.setAttribute(\"aria-busy\", \"true\");\n this.element.setAttribute(\"data-state\", \"loading\");\n }\n\n /** Hides the placeholder, shows content, and clears the busy state. */\n #reveal(): void {\n if (this.hasPlaceholderTarget) this.placeholderTarget.hidden = true;\n if (this.hasContentTarget) this.contentTarget.hidden = false;\n this.element.setAttribute(\"aria-busy\", \"false\");\n this.element.setAttribute(\"data-state\", \"ready\");\n this.dispatch(\"ready\", { detail: {} });\n // loading → ready is the transition; the skeleton itself carries no words.\n announce(fillTemplate(this.announceReadyTextValue, {}));\n }\n\n /**\n * Drops the held reveal on a real detach. The markup keeps whatever it last\n * held: an element on its way out of the document has no reader left, and one\n * whose `data-controller` dropped the identifier no longer resolves its own\n * targets, so the rollback could only ever be partial.\n */\n #teardown(): void {\n this.#gate.cancel();\n this.#timers.clearAll();\n this.#floor.cancel();\n }\n\n /** Current lifecycle phase as reflected on `data-state`. */\n get #state(): string {\n return this.element.getAttribute(\"data-state\") ?? \"loading\";\n }\n}\n"]}
|
|
@@ -11,7 +11,7 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
11
11
|
* data-stimeo--slider-value-value="40">
|
|
12
12
|
* <div data-stimeo--slider-target="track"
|
|
13
13
|
* data-action="pointerdown->stimeo--slider#onPointerDown">
|
|
14
|
-
* <div data-stimeo--slider-target="thumb" role="slider" tabindex="0"
|
|
14
|
+
* <div data-stimeo--slider-target="thumb" role="slider" tabindex="0" aria-label="Volume"
|
|
15
15
|
* aria-valuemin="0" aria-valuemax="100" aria-valuenow="40"
|
|
16
16
|
* data-action="keydown->stimeo--slider#onKeydown"></div>
|
|
17
17
|
* </div>
|
|
@@ -31,6 +31,8 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
31
31
|
* - `ArrowRight`/`ArrowUp` increase and `ArrowLeft`/`ArrowDown` decrease by one
|
|
32
32
|
* step; `Home`/`End` jump to the min/max; `PageUp`/`PageDown` move by ten steps.
|
|
33
33
|
* - Pointer press/drag on the track sets the value from the pointer position.
|
|
34
|
+
* - `step` must be finite and positive; invalid runtime input falls back to `1`.
|
|
35
|
+
* Finite endpoints remain allowed even when they do not align to the step grid.
|
|
34
36
|
*
|
|
35
37
|
* The fraction is a value ratio, not a position, so only the consumer knows
|
|
36
38
|
* whether their track mirrors under RTL. Set `logicalTrack` to declare that it
|
|
@@ -77,6 +79,20 @@ declare class SliderController extends Controller<HTMLElement> {
|
|
|
77
79
|
connect(): void;
|
|
78
80
|
/** Cancels any active pointer drag so document listeners never leak. */
|
|
79
81
|
disconnect(): void;
|
|
82
|
+
/** Silently repaints a minimum changed by application code or a Turbo morph. */
|
|
83
|
+
minValueChanged(): void;
|
|
84
|
+
/** Silently repaints a maximum changed by application code or a Turbo morph. */
|
|
85
|
+
maxValueChanged(): void;
|
|
86
|
+
/** Silently repaints a step changed by application code or a Turbo morph. */
|
|
87
|
+
stepValueChanged(): void;
|
|
88
|
+
/** Silently repaints a value changed by application code or a Turbo morph. */
|
|
89
|
+
valueValueChanged(): void;
|
|
90
|
+
/** Hydrates a thumb inserted or replaced at runtime with the current ARIA state. */
|
|
91
|
+
thumbTargetConnected(thumb: HTMLElement): void;
|
|
92
|
+
/** Drops the stale focus owner while allowing a live track gesture to continue. */
|
|
93
|
+
thumbTargetDisconnected(thumb: HTMLElement): void;
|
|
94
|
+
/** Ends a gesture whose geometry target disappeared or ceased being a target. */
|
|
95
|
+
trackTargetDisconnected(track: HTMLElement): void;
|
|
80
96
|
/** Handles keyboard stepping per the APG slider model. */
|
|
81
97
|
onKeydown(event: KeyboardEvent): void;
|
|
82
98
|
/** Begins a pointer drag: sets the value and tracks subsequent movement. */
|
|
@@ -18,6 +18,92 @@ function isReservedArrowChord(event, allow = []) {
|
|
|
18
18
|
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// src/utils/microtask_coalescer.ts
|
|
22
|
+
var MicrotaskCoalescer = class {
|
|
23
|
+
#run;
|
|
24
|
+
#queued = false;
|
|
25
|
+
#active = false;
|
|
26
|
+
#generation = 0;
|
|
27
|
+
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
28
|
+
constructor(run) {
|
|
29
|
+
this.#run = run;
|
|
30
|
+
}
|
|
31
|
+
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
32
|
+
activate() {
|
|
33
|
+
this.#active = true;
|
|
34
|
+
}
|
|
35
|
+
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
36
|
+
cancel() {
|
|
37
|
+
this.#active = false;
|
|
38
|
+
this.#queued = false;
|
|
39
|
+
this.#generation += 1;
|
|
40
|
+
}
|
|
41
|
+
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
42
|
+
schedule() {
|
|
43
|
+
if (!this.#active || this.#queued) return;
|
|
44
|
+
this.#queued = true;
|
|
45
|
+
const generation = this.#generation;
|
|
46
|
+
queueMicrotask(() => {
|
|
47
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
48
|
+
this.#queued = false;
|
|
49
|
+
this.#run();
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// src/utils/owned_pointer_session.ts
|
|
55
|
+
var OwnedPointerSession = class {
|
|
56
|
+
pointerId;
|
|
57
|
+
#owner;
|
|
58
|
+
#handlers;
|
|
59
|
+
#abort = new AbortController();
|
|
60
|
+
#active = true;
|
|
61
|
+
constructor(start, owner, handlers) {
|
|
62
|
+
this.pointerId = start.pointerId;
|
|
63
|
+
this.#owner = owner;
|
|
64
|
+
this.#handlers = handlers;
|
|
65
|
+
const { signal } = this.#abort;
|
|
66
|
+
owner.ownerDocument.addEventListener("pointermove", this.#onMove, { signal });
|
|
67
|
+
owner.ownerDocument.addEventListener("pointerup", this.#onEndEvent, { signal });
|
|
68
|
+
owner.ownerDocument.addEventListener("pointercancel", this.#onEndEvent, { signal });
|
|
69
|
+
owner.addEventListener("lostpointercapture", this.#onLostCapture, { signal });
|
|
70
|
+
try {
|
|
71
|
+
owner.setPointerCapture?.(this.pointerId);
|
|
72
|
+
} catch {
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** Whether this session still owns its pointer and listeners. */
|
|
76
|
+
get active() {
|
|
77
|
+
return this.#active;
|
|
78
|
+
}
|
|
79
|
+
/** Whether `event` belongs to the initiating pointer of the live session. */
|
|
80
|
+
owns(event) {
|
|
81
|
+
return this.#active && event.pointerId === this.pointerId;
|
|
82
|
+
}
|
|
83
|
+
/** Releases capture/listeners and invokes the end callback exactly once. */
|
|
84
|
+
end() {
|
|
85
|
+
if (!this.#active) return;
|
|
86
|
+
this.#active = false;
|
|
87
|
+
this.#abort.abort();
|
|
88
|
+
try {
|
|
89
|
+
this.#owner.releasePointerCapture?.(this.pointerId);
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
this.#handlers.end?.();
|
|
93
|
+
}
|
|
94
|
+
#onMove = (event) => {
|
|
95
|
+
if (this.owns(event)) this.#handlers.move(event);
|
|
96
|
+
};
|
|
97
|
+
#onEndEvent = (event) => {
|
|
98
|
+
if (this.owns(event)) this.end();
|
|
99
|
+
};
|
|
100
|
+
#onLostCapture = (event) => {
|
|
101
|
+
const pointerId = event.pointerId;
|
|
102
|
+
if (typeof pointerId === "number" && pointerId !== this.pointerId) return;
|
|
103
|
+
this.end();
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
|
|
21
107
|
// src/utils/range.ts
|
|
22
108
|
function rangeFraction(value, min, max) {
|
|
23
109
|
const span = max - min;
|
|
@@ -34,6 +120,112 @@ function rangeFraction(value, min, max) {
|
|
|
34
120
|
return Math.min(1, Math.max(0, fraction));
|
|
35
121
|
}
|
|
36
122
|
|
|
123
|
+
// src/utils/stepped_value.ts
|
|
124
|
+
function effectiveStep(step) {
|
|
125
|
+
return Number.isFinite(step) && step > 0 ? step : 1;
|
|
126
|
+
}
|
|
127
|
+
function snapSteppedValue(raw, range) {
|
|
128
|
+
if (!(range.min <= range.max)) return finiteFallback(range.min, range.max);
|
|
129
|
+
const input = Number.isNaN(raw) ? finiteFallback(range.min, range.max) : raw;
|
|
130
|
+
const clamped = Math.min(range.max, Math.max(range.min, input));
|
|
131
|
+
const step = effectiveStep(range.step);
|
|
132
|
+
const base = stepBase(range);
|
|
133
|
+
const candidates = [];
|
|
134
|
+
if (Number.isFinite(range.min)) candidates.push(range.min);
|
|
135
|
+
if (Number.isFinite(range.max)) candidates.push(range.max);
|
|
136
|
+
const gridPosition = (clamped - base) / step;
|
|
137
|
+
if (Number.isFinite(gridPosition)) {
|
|
138
|
+
addGridCandidate(candidates, Math.floor(gridPosition), range, base, step);
|
|
139
|
+
addGridCandidate(candidates, Math.ceil(gridPosition), range, base, step);
|
|
140
|
+
}
|
|
141
|
+
if (candidates.length === 0) return clamped;
|
|
142
|
+
let nearest = candidates[0];
|
|
143
|
+
let nearestDistance = Math.abs(clamped - nearest);
|
|
144
|
+
for (const candidate of candidates.slice(1)) {
|
|
145
|
+
const distance = Math.abs(clamped - candidate);
|
|
146
|
+
if (distance < nearestDistance || nearlyEqual(distance, nearestDistance) && candidate > nearest) {
|
|
147
|
+
nearest = candidate;
|
|
148
|
+
nearestDistance = distance;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return nearest;
|
|
152
|
+
}
|
|
153
|
+
function stepSteppedValue(current, count, range) {
|
|
154
|
+
const value = snapSteppedValue(current, range);
|
|
155
|
+
const distance = Math.abs(Math.trunc(count));
|
|
156
|
+
if (!Number.isFinite(distance) || distance === 0) return value;
|
|
157
|
+
const direction = Math.sign(count);
|
|
158
|
+
const adjacent = adjacentSteppedValue(value, direction, range);
|
|
159
|
+
const raw = adjacent + direction * (distance - 1) * effectiveStep(range.step);
|
|
160
|
+
return snapSteppedValue(raw, range);
|
|
161
|
+
}
|
|
162
|
+
function adjacentSteppedValue(current, direction, range) {
|
|
163
|
+
const step = effectiveStep(range.step);
|
|
164
|
+
const base = stepBase(range);
|
|
165
|
+
const candidates = [];
|
|
166
|
+
if (direction > 0) {
|
|
167
|
+
const position2 = (current - base) / step;
|
|
168
|
+
if (Number.isFinite(position2)) {
|
|
169
|
+
let gridIndex = Math.floor(position2) + 1;
|
|
170
|
+
let candidate = cleanGridValue(base + gridIndex * step, base, step);
|
|
171
|
+
if (candidate < current || nearlyEqual(candidate, current)) {
|
|
172
|
+
gridIndex += 1;
|
|
173
|
+
candidate = cleanGridValue(base + gridIndex * step, base, step);
|
|
174
|
+
}
|
|
175
|
+
if (candidate > current && !nearlyEqual(candidate, current) && within(candidate, range.min, range.max)) {
|
|
176
|
+
candidates.push(clamp(candidate, range));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return clamp(Math.min(...candidates), range);
|
|
180
|
+
}
|
|
181
|
+
const position = (current - base) / step;
|
|
182
|
+
if (Number.isFinite(position)) {
|
|
183
|
+
let gridIndex = Math.ceil(position) - 1;
|
|
184
|
+
let candidate = cleanGridValue(base + gridIndex * step, base, step);
|
|
185
|
+
if (candidate > current || nearlyEqual(candidate, current)) {
|
|
186
|
+
gridIndex -= 1;
|
|
187
|
+
candidate = cleanGridValue(base + gridIndex * step, base, step);
|
|
188
|
+
}
|
|
189
|
+
if (candidate < current && !nearlyEqual(candidate, current) && within(candidate, range.min, range.max)) {
|
|
190
|
+
candidates.push(clamp(candidate, range));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return clamp(Math.max(...candidates), range);
|
|
194
|
+
}
|
|
195
|
+
function addGridCandidate(candidates, index, range, base, step) {
|
|
196
|
+
const candidate = cleanGridValue(base + index * step, base, step);
|
|
197
|
+
if (within(candidate, range.min, range.max)) candidates.push(clamp(candidate, range));
|
|
198
|
+
}
|
|
199
|
+
function stepBase(range) {
|
|
200
|
+
if (range.base !== void 0 && Number.isFinite(range.base)) return range.base;
|
|
201
|
+
return Number.isFinite(range.min) ? range.min : 0;
|
|
202
|
+
}
|
|
203
|
+
function cleanGridValue(value, base, step) {
|
|
204
|
+
const precision = Math.max(decimalPlaces(base), decimalPlaces(step));
|
|
205
|
+
return precision <= 100 ? Number(value.toFixed(precision)) : value;
|
|
206
|
+
}
|
|
207
|
+
function decimalPlaces(value) {
|
|
208
|
+
const [coefficient = "", exponentText] = Math.abs(value).toString().toLowerCase().split("e");
|
|
209
|
+
const fractionLength = coefficient.split(".")[1]?.length ?? 0;
|
|
210
|
+
const exponent = exponentText === void 0 ? 0 : Number(exponentText);
|
|
211
|
+
return Math.max(0, fractionLength - exponent);
|
|
212
|
+
}
|
|
213
|
+
function within(value, min, max) {
|
|
214
|
+
return (value > min || nearlyEqual(value, min)) && (value < max || nearlyEqual(value, max));
|
|
215
|
+
}
|
|
216
|
+
function clamp(value, range) {
|
|
217
|
+
return Math.min(range.max, Math.max(range.min, value));
|
|
218
|
+
}
|
|
219
|
+
function nearlyEqual(left, right) {
|
|
220
|
+
const scale = Math.max(Math.abs(left), Math.abs(right));
|
|
221
|
+
return Number.isFinite(left) && Number.isFinite(right) && Math.abs(left - right) <= Number.EPSILON * scale;
|
|
222
|
+
}
|
|
223
|
+
function finiteFallback(min, max) {
|
|
224
|
+
if (Number.isFinite(min)) return min;
|
|
225
|
+
if (Number.isFinite(max)) return max;
|
|
226
|
+
return 0;
|
|
227
|
+
}
|
|
228
|
+
|
|
37
229
|
// src/controllers/slider_controller.ts
|
|
38
230
|
var FRACTION_PROPERTY = "--stimeo--slider-fraction";
|
|
39
231
|
var SliderController = class extends Controller {
|
|
@@ -47,40 +239,79 @@ var SliderController = class extends Controller {
|
|
|
47
239
|
};
|
|
48
240
|
static actions = ["onKeydown", "onPointerDown"];
|
|
49
241
|
static events = ["change"];
|
|
50
|
-
/**
|
|
51
|
-
#
|
|
242
|
+
/** One initiating pointer owns each live drag and its stable target snapshot. */
|
|
243
|
+
#drag = null;
|
|
244
|
+
/** Collapses a morph that swaps several render Values into one silent repaint. */
|
|
245
|
+
#repaint = new MicrotaskCoalescer(() => this.#render());
|
|
52
246
|
/** Whether the consumer declared a mirroring track and the direction mirrors it. */
|
|
53
247
|
get #mirrored() {
|
|
54
248
|
return this.logicalTrackValue && isRtl(this.element);
|
|
55
249
|
}
|
|
56
250
|
/** Clamps the initial value and renders the starting position. */
|
|
57
251
|
connect() {
|
|
58
|
-
this.#
|
|
252
|
+
this.#repaint.activate();
|
|
253
|
+
this.#commit(this.valueValue, false);
|
|
59
254
|
}
|
|
60
255
|
/** Cancels any active pointer drag so document listeners never leak. */
|
|
61
256
|
disconnect() {
|
|
62
|
-
this.#
|
|
63
|
-
this.#
|
|
257
|
+
this.#repaint.cancel();
|
|
258
|
+
this.#endDrag();
|
|
259
|
+
}
|
|
260
|
+
/** Silently repaints a minimum changed by application code or a Turbo morph. */
|
|
261
|
+
minValueChanged() {
|
|
262
|
+
this.#repaint.schedule();
|
|
263
|
+
}
|
|
264
|
+
/** Silently repaints a maximum changed by application code or a Turbo morph. */
|
|
265
|
+
maxValueChanged() {
|
|
266
|
+
this.#repaint.schedule();
|
|
267
|
+
}
|
|
268
|
+
/** Silently repaints a step changed by application code or a Turbo morph. */
|
|
269
|
+
stepValueChanged() {
|
|
270
|
+
this.#repaint.schedule();
|
|
271
|
+
}
|
|
272
|
+
/** Silently repaints a value changed by application code or a Turbo morph. */
|
|
273
|
+
valueValueChanged() {
|
|
274
|
+
this.#repaint.schedule();
|
|
275
|
+
}
|
|
276
|
+
/** Hydrates a thumb inserted or replaced at runtime with the current ARIA state. */
|
|
277
|
+
thumbTargetConnected(thumb) {
|
|
278
|
+
const value = this.#currentValue();
|
|
279
|
+
this.#renderThumb(thumb, value);
|
|
280
|
+
this.#renderFraction(value);
|
|
281
|
+
if (this.#drag && this.#drag.thumb === null) {
|
|
282
|
+
this.#drag.thumb = thumb;
|
|
283
|
+
thumb.focus();
|
|
284
|
+
}
|
|
285
|
+
this.#repaint.schedule();
|
|
286
|
+
}
|
|
287
|
+
/** Drops the stale focus owner while allowing a live track gesture to continue. */
|
|
288
|
+
thumbTargetDisconnected(thumb) {
|
|
289
|
+
if (this.#drag?.thumb === thumb) this.#drag.thumb = null;
|
|
290
|
+
}
|
|
291
|
+
/** Ends a gesture whose geometry target disappeared or ceased being a target. */
|
|
292
|
+
trackTargetDisconnected(track) {
|
|
293
|
+
if (this.#drag?.track === track) this.#endDrag();
|
|
64
294
|
}
|
|
65
295
|
/** Handles keyboard stepping per the APG slider model. */
|
|
66
296
|
onKeydown(event) {
|
|
67
297
|
if (isReservedArrowChord(event)) return;
|
|
68
|
-
const big = this.stepValue * 10;
|
|
69
298
|
let next = null;
|
|
299
|
+
const current = this.#currentValue();
|
|
300
|
+
const range = this.#steppedRange;
|
|
70
301
|
switch (this.#mirrored ? logicalArrowKey(event.key, this.element) : event.key) {
|
|
71
302
|
case "ArrowRight":
|
|
72
303
|
case "ArrowUp":
|
|
73
|
-
next =
|
|
304
|
+
next = stepSteppedValue(current, 1, range);
|
|
74
305
|
break;
|
|
75
306
|
case "ArrowLeft":
|
|
76
307
|
case "ArrowDown":
|
|
77
|
-
next =
|
|
308
|
+
next = stepSteppedValue(current, -1, range);
|
|
78
309
|
break;
|
|
79
310
|
case "PageUp":
|
|
80
|
-
next =
|
|
311
|
+
next = stepSteppedValue(current, 10, range);
|
|
81
312
|
break;
|
|
82
313
|
case "PageDown":
|
|
83
|
-
next =
|
|
314
|
+
next = stepSteppedValue(current, -10, range);
|
|
84
315
|
break;
|
|
85
316
|
case "Home":
|
|
86
317
|
next = this.minValue;
|
|
@@ -92,56 +323,102 @@ var SliderController = class extends Controller {
|
|
|
92
323
|
return;
|
|
93
324
|
}
|
|
94
325
|
event.preventDefault();
|
|
95
|
-
this.#
|
|
326
|
+
this.#commit(next, true);
|
|
96
327
|
}
|
|
97
328
|
/** Begins a pointer drag: sets the value and tracks subsequent movement. */
|
|
98
329
|
onPointerDown(event) {
|
|
99
|
-
if (!this.hasTrackTarget) return;
|
|
100
|
-
|
|
330
|
+
if (event.button !== 0 || this.#drag || !this.hasTrackTarget || !this.hasThumbTarget) return;
|
|
331
|
+
const track = this.trackTarget;
|
|
332
|
+
const thumb = this.thumbTarget;
|
|
101
333
|
const mirrored = this.#mirrored;
|
|
102
|
-
this.#
|
|
103
|
-
if (
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
334
|
+
const value = this.#valueFromClientX(event.clientX, mirrored, track);
|
|
335
|
+
if (value === null) return;
|
|
336
|
+
event.preventDefault();
|
|
337
|
+
this.#commit(value, true);
|
|
338
|
+
thumb.focus();
|
|
339
|
+
const drag = { pointer: null, track, thumb };
|
|
340
|
+
drag.pointer = new OwnedPointerSession(event, track, {
|
|
341
|
+
move: (move) => {
|
|
342
|
+
if (!track.isConnected) {
|
|
343
|
+
this.#endDrag();
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const moved = this.#valueFromClientX(move.clientX, mirrored, track);
|
|
347
|
+
if (moved !== null) this.#commit(moved, true);
|
|
348
|
+
},
|
|
349
|
+
end: () => {
|
|
350
|
+
if (this.#drag === drag) this.#drag = null;
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
this.#drag = drag;
|
|
354
|
+
}
|
|
355
|
+
/** Maps a pointer X coordinate to a raw value using a stable track snapshot. */
|
|
356
|
+
#valueFromClientX(clientX, mirrored, track) {
|
|
357
|
+
const rect = track.getBoundingClientRect();
|
|
358
|
+
if (rect.width === 0) return null;
|
|
120
359
|
const offset = (clientX - rect.left) / rect.width;
|
|
121
360
|
const fraction = mirrored ? 1 - offset : offset;
|
|
122
|
-
this
|
|
361
|
+
return this.minValue + fraction * (this.maxValue - this.minValue);
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Stores a normalized value and renders synchronously for responsive input.
|
|
365
|
+
* Morph callbacks use {@link #render} instead, so they never write Values back
|
|
366
|
+
* or dispatch a user-facing change event.
|
|
367
|
+
*/
|
|
368
|
+
#commit(raw, notify) {
|
|
369
|
+
const previous = this.#currentValue();
|
|
370
|
+
const value = snapSteppedValue(raw, this.#steppedRange);
|
|
371
|
+
if (!Object.is(this.valueValue, value)) this.valueValue = value;
|
|
372
|
+
this.#renderValue(value);
|
|
373
|
+
if (notify && value !== previous) this.dispatch("change", { detail: { value } });
|
|
123
374
|
}
|
|
124
375
|
/**
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
* change — symmetric with `range-slider` — unless `silent` (the initial
|
|
129
|
-
* connect render, which is not a user edit).
|
|
376
|
+
* Reflects the normalized current Value without mutating or dispatching it.
|
|
377
|
+
*
|
|
378
|
+
* @stimeoRenderRoot
|
|
130
379
|
*/
|
|
131
|
-
#
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
this.valueValue = value;
|
|
380
|
+
#render() {
|
|
381
|
+
this.#renderValue(this.#currentValue());
|
|
382
|
+
}
|
|
383
|
+
/** Reflects one normalized value on the current target and CSS output. */
|
|
384
|
+
#renderValue(value) {
|
|
137
385
|
if (this.hasThumbTarget) {
|
|
138
|
-
this.thumbTarget
|
|
139
|
-
this.thumbTarget.setAttribute("aria-valuemax", String(this.maxValue));
|
|
140
|
-
this.thumbTarget.setAttribute("aria-valuenow", String(value));
|
|
386
|
+
this.#renderThumb(this.thumbTarget, value);
|
|
141
387
|
}
|
|
388
|
+
this.#renderFraction(value);
|
|
389
|
+
}
|
|
390
|
+
/** Writes only ARIA values that differ, including on a replacement target. */
|
|
391
|
+
#renderThumb(thumb, value) {
|
|
392
|
+
const attributes = {
|
|
393
|
+
"aria-valuemin": String(this.minValue),
|
|
394
|
+
"aria-valuemax": String(this.maxValue),
|
|
395
|
+
"aria-valuenow": String(value)
|
|
396
|
+
};
|
|
397
|
+
for (const [name, next] of Object.entries(attributes)) {
|
|
398
|
+
if (thumb.getAttribute(name) !== next) thumb.setAttribute(name, next);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
/** Writes the behavior-only positioning hook only when its value changed. */
|
|
402
|
+
#renderFraction(value) {
|
|
142
403
|
const fraction = rangeFraction(value, this.minValue, this.maxValue);
|
|
143
|
-
|
|
144
|
-
if (
|
|
404
|
+
const next = String(fraction);
|
|
405
|
+
if (this.element.style.getPropertyValue(FRACTION_PROPERTY) !== next) {
|
|
406
|
+
this.element.style.setProperty(FRACTION_PROPERTY, next);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
/** Current normalized value derived from the live declarative inputs. */
|
|
410
|
+
#currentValue() {
|
|
411
|
+
return snapSteppedValue(this.valueValue, this.#steppedRange);
|
|
412
|
+
}
|
|
413
|
+
/** Shared range configuration; finite endpoints remain allowed off the grid. */
|
|
414
|
+
get #steppedRange() {
|
|
415
|
+
return { min: this.minValue, max: this.maxValue, step: this.stepValue };
|
|
416
|
+
}
|
|
417
|
+
/** Ends the current pointer session without dispatching another change. */
|
|
418
|
+
#endDrag() {
|
|
419
|
+
const drag = this.#drag;
|
|
420
|
+
this.#drag = null;
|
|
421
|
+
drag?.pointer?.end();
|
|
145
422
|
}
|
|
146
423
|
};
|
|
147
424
|
|