stimeo-ui 0.7.0 → 0.9.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.
Files changed (77) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/dist/controllers/auto_submit_controller.d.ts +14 -3
  3. package/dist/controllers/auto_submit_controller.js +94 -16
  4. package/dist/controllers/auto_submit_controller.js.map +1 -1
  5. package/dist/controllers/bulk_select_controller.d.ts +62 -15
  6. package/dist/controllers/bulk_select_controller.js +139 -28
  7. package/dist/controllers/bulk_select_controller.js.map +1 -1
  8. package/dist/controllers/carousel_controller.d.ts +81 -47
  9. package/dist/controllers/carousel_controller.js +451 -100
  10. package/dist/controllers/carousel_controller.js.map +1 -1
  11. package/dist/controllers/clipboard_controller.d.ts +48 -15
  12. package/dist/controllers/clipboard_controller.js +102 -20
  13. package/dist/controllers/clipboard_controller.js.map +1 -1
  14. package/dist/controllers/color_picker_controller.d.ts +37 -6
  15. package/dist/controllers/color_picker_controller.js +180 -43
  16. package/dist/controllers/color_picker_controller.js.map +1 -1
  17. package/dist/controllers/currency_input_controller.d.ts +39 -5
  18. package/dist/controllers/currency_input_controller.js +305 -74
  19. package/dist/controllers/currency_input_controller.js.map +1 -1
  20. package/dist/controllers/data_grid_controller.d.ts +25 -9
  21. package/dist/controllers/data_grid_controller.js +150 -24
  22. package/dist/controllers/data_grid_controller.js.map +1 -1
  23. package/dist/controllers/direct_upload_controller.d.ts +3 -1
  24. package/dist/controllers/direct_upload_controller.js +12 -2
  25. package/dist/controllers/direct_upload_controller.js.map +1 -1
  26. package/dist/controllers/editable_controller.d.ts +34 -9
  27. package/dist/controllers/editable_controller.js +83 -30
  28. package/dist/controllers/editable_controller.js.map +1 -1
  29. package/dist/controllers/file_dropzone_controller.d.ts +123 -29
  30. package/dist/controllers/file_dropzone_controller.js +386 -63
  31. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  32. package/dist/controllers/filter_controller.d.ts +15 -3
  33. package/dist/controllers/filter_controller.js +32 -1
  34. package/dist/controllers/filter_controller.js.map +1 -1
  35. package/dist/controllers/flash_controller.d.ts +3 -1
  36. package/dist/controllers/flash_controller.js +3 -1
  37. package/dist/controllers/flash_controller.js.map +1 -1
  38. package/dist/controllers/frame_loading_controller.d.ts +4 -2
  39. package/dist/controllers/frame_loading_controller.js +2 -1
  40. package/dist/controllers/frame_loading_controller.js.map +1 -1
  41. package/dist/controllers/input_mask_controller.d.ts +42 -16
  42. package/dist/controllers/input_mask_controller.js +251 -76
  43. package/dist/controllers/input_mask_controller.js.map +1 -1
  44. package/dist/controllers/masonry_controller.d.ts +34 -5
  45. package/dist/controllers/masonry_controller.js +129 -17
  46. package/dist/controllers/masonry_controller.js.map +1 -1
  47. package/dist/controllers/nested_form_controller.d.ts +51 -14
  48. package/dist/controllers/nested_form_controller.js +450 -42
  49. package/dist/controllers/nested_form_controller.js.map +1 -1
  50. package/dist/controllers/otp_controller.d.ts +64 -25
  51. package/dist/controllers/otp_controller.js +485 -114
  52. package/dist/controllers/otp_controller.js.map +1 -1
  53. package/dist/controllers/reset_before_cache_controller.d.ts +25 -2
  54. package/dist/controllers/reset_before_cache_controller.js +51 -5
  55. package/dist/controllers/reset_before_cache_controller.js.map +1 -1
  56. package/dist/controllers/resizable_controller.d.ts +23 -7
  57. package/dist/controllers/resizable_controller.js +128 -55
  58. package/dist/controllers/resizable_controller.js.map +1 -1
  59. package/dist/controllers/spinner_controller.d.ts +3 -2
  60. package/dist/controllers/spinner_controller.js +7 -5
  61. package/dist/controllers/spinner_controller.js.map +1 -1
  62. package/dist/controllers/submit_once_controller.d.ts +4 -1
  63. package/dist/controllers/submit_once_controller.js +3 -1
  64. package/dist/controllers/submit_once_controller.js.map +1 -1
  65. package/dist/controllers/textarea_autosize_controller.d.ts +30 -10
  66. package/dist/controllers/textarea_autosize_controller.js +131 -3
  67. package/dist/controllers/textarea_autosize_controller.js.map +1 -1
  68. package/dist/index.js +2764 -923
  69. package/dist/index.js.map +1 -1
  70. package/dist/inspector/cli.d.ts +8 -1
  71. package/dist/inspector/cli.js +5 -1
  72. package/dist/inspector/cli.js.map +1 -1
  73. package/dist/inspector/cli_bin.js +5 -1
  74. package/dist/inspector/cli_bin.js.map +1 -1
  75. package/dist/inspector/examples.json +24 -24
  76. package/dist/inspector/manifest.json +193 -30
  77. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/default_attribute.ts","../../src/utils/safe_timeout.ts","../../src/controllers/clipboard_controller.ts"],"names":[],"mappings":"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","file":"clipboard_controller.js","sourcesContent":["/**\n * Adds an attribute default without displacing an authored value.\n *\n * Attribute presence — including an authored empty string — is the ownership\n * boundary. The return value lets a caller remember that it supplied the\n * default when that caller must later restore the authored state.\n *\n * @param element - Element that owns the attribute\n * @param name - Attribute name\n * @param value - Value to write only when the attribute is absent\n * @returns Whether this call added the attribute\n */\nexport function setDefaultAttribute(element: Element, name: string, value: string): boolean {\n if (element.hasAttribute(name)) return false;\n element.setAttribute(name, value);\n return true;\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 { setDefaultAttribute } from \"../utils/default_attribute\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/**\n * Headless copy-to-clipboard behavior with a live-region completion notice.\n *\n * Markup contract (identifier: `stimeo--clipboard`):\n * <div data-controller=\"stimeo--clipboard\"\n * data-stimeo--clipboard-feedback-duration-value=\"2000\">\n * <input type=\"text\" value=\"https://example.com\" readonly\n * data-stimeo--clipboard-target=\"source\">\n * <button type=\"button\" data-stimeo--clipboard-target=\"button\"\n * data-action=\"stimeo--clipboard#copy\">Copy</button>\n * <span role=\"status\" aria-live=\"polite\"\n * data-stimeo--clipboard-target=\"feedback\"></span>\n * </div>\n *\n * No dedicated APG pattern; this follows the Button + live-region practice. The\n * copy uses the standard `navigator.clipboard` API (no extra dependency); when\n * it is unavailable or rejects, the failure is surfaced rather than silently\n * swallowed, and never communicated by icon alone — the `role=\"status\"` region\n * carries text so screen readers announce the outcome.\n *\n * @remarks\n * Behavior only — icon swaps and styling are the consumer's, keyed off\n * `data-state` (`idle` / `copied` / `error`). The completion notice clears\n * itself after `feedbackDuration`; that timer is torn down on disconnect (Turbo)\n * via {@link SafeTimeout}.\n */\nexport class ClipboardController extends Controller<HTMLElement> {\n static override targets = [\"source\", \"button\", \"feedback\"];\n static override values = {\n text: { type: String, default: \"\" },\n feedbackDuration: { type: Number, default: 2000 },\n copiedLabel: { type: String, default: \"Copied\" },\n errorLabel: { type: String, default: \"Copy failed\" },\n };\n static actions = [\"copy\"] as const;\n static events = [\"copy\"] as const;\n\n declare readonly sourceTarget: HTMLElement;\n declare readonly buttonTarget: HTMLElement;\n declare readonly feedbackTarget: HTMLElement;\n declare readonly hasSourceTarget: boolean;\n declare readonly hasButtonTarget: boolean;\n declare readonly hasFeedbackTarget: boolean;\n\n declare textValue: string;\n declare feedbackDurationValue: number;\n declare copiedLabelValue: string;\n declare errorLabelValue: string;\n\n /** Auto-clear timer for the completion notice; torn down on disconnect. */\n #timers = new SafeTimeout();\n\n /**\n * The pending auto-clear timer id, or `null` when none is scheduled. Tracked so\n * a rapid second copy cancels the first window instead of letting a stale timer\n * reset the freshly-shown notice early.\n */\n #resetTimerId: number | null = null;\n\n override connect(): void {\n setDefaultAttribute(this.element, \"data-state\", \"idle\");\n }\n\n override disconnect(): void {\n this.#timers.clearAll();\n }\n\n /**\n * Copies the resolved text and reports the outcome. Bound via `data-action`\n * (click). Always dispatches `stimeo--clipboard:copy` with `{ success, text }`\n * — including on failure — so consumers can react either way.\n */\n async copy(): Promise<void> {\n const text = this.#resolveText();\n let success = false;\n try {\n if (!navigator.clipboard?.writeText) throw new Error(\"Clipboard API unavailable\");\n await navigator.clipboard.writeText(text);\n success = true;\n } catch {\n success = false;\n }\n\n this.#reportResult(success);\n this.dispatch(\"copy\", { detail: { success, text } });\n }\n\n /**\n * The text to copy: the explicit `text` value when set, otherwise the source\n * target's current value (inputs/textareas) or text content.\n */\n #resolveText(): string {\n if (this.textValue.length > 0) return this.textValue;\n if (!this.hasSourceTarget) return \"\";\n const source = this.sourceTarget;\n if (source instanceof HTMLInputElement || source instanceof HTMLTextAreaElement) {\n return source.value;\n }\n return source.textContent ?? \"\";\n }\n\n /** Reflects the result on `data-state`, announces it, and schedules a reset. */\n #reportResult(success: boolean): void {\n this.element.setAttribute(\"data-state\", success ? \"copied\" : \"error\");\n if (this.hasFeedbackTarget) {\n this.feedbackTarget.textContent = success ? this.copiedLabelValue : this.errorLabelValue;\n }\n\n // Cancel any in-flight reset so consecutive copies restart the full window\n // rather than having the earlier timer clear the new notice prematurely.\n if (this.#resetTimerId !== null) {\n this.#timers.clear(this.#resetTimerId);\n this.#resetTimerId = null;\n }\n if (this.feedbackDurationValue > 0) {\n this.#resetTimerId = this.#timers.set(() => {\n this.#resetTimerId = null;\n this.#reset();\n }, this.feedbackDurationValue);\n }\n }\n\n /** Returns to the idle state and clears the completion notice. */\n #reset(): void {\n this.element.setAttribute(\"data-state\", \"idle\");\n if (this.hasFeedbackTarget) {\n this.feedbackTarget.textContent = \"\";\n }\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/announce.ts","../../src/utils/before_cache_reset.ts","../../src/utils/default_attribute.ts","../../src/utils/safe_timeout.ts","../../src/controllers/clipboard_controller.ts"],"names":[],"mappings":"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","file":"clipboard_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 * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way (the `MicrotaskCoalescer` split).\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\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 #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","/**\n * Adds an attribute default without displacing an authored value.\n *\n * Attribute presence — including an authored empty string — is the ownership\n * boundary. The return value lets a caller remember that it supplied the\n * default when that caller must later restore the authored state.\n *\n * @param element - Element that owns the attribute\n * @param name - Attribute name\n * @param value - Value to write only when the attribute is absent\n * @returns Whether this call added the attribute\n */\nexport function setDefaultAttribute(element: Element, name: string, value: string): boolean {\n if (element.hasAttribute(name)) return false;\n element.setAttribute(name, value);\n return true;\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 } from \"../utils/announce\";\nimport { BeforeCacheReset } from \"../utils/before_cache_reset\";\nimport { setDefaultAttribute } from \"../utils/default_attribute\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/**\n * The `data-state` values this controller writes itself. Any other value on the\n * attribute was authored by the consumer and is left alone.\n */\nconst TRANSIENT_STATES = new Set([\"copied\", \"error\"]);\n\n/**\n * Headless copy-to-clipboard behavior with a completion slot and a screen-reader\n * announcement.\n *\n * Markup contract (identifier: `stimeo--clipboard`):\n * <div data-controller=\"stimeo--clipboard\"\n * data-stimeo--clipboard-feedback-duration-value=\"2000\"\n * data-stimeo--clipboard-announce-copied-text-value=\"Copied to clipboard\"\n * data-stimeo--clipboard-announce-error-text-value=\"Copy failed\">\n * <input type=\"text\" value=\"https://example.com\" readonly\n * data-stimeo--clipboard-target=\"source\">\n * <button type=\"button\" data-stimeo--clipboard-target=\"button\"\n * data-action=\"stimeo--clipboard#copy\">Copy</button>\n * <span data-stimeo--clipboard-target=\"feedback\"></span>\n * </div>\n *\n * No dedicated APG pattern; this follows the Button practice plus a status message\n * ({@link https://www.w3.org/WAI/WCAG22/Understanding/status-changes.html | WCAG 2.2 SC 4.1.3}).\n * The copy uses the standard `navigator.clipboard` API (no extra dependency); every\n * failure mode — an API the browser withholds outside a secure context as much as a\n * rejected permission — settles the same way, as `data-state=\"error\"` plus the\n * `copy` event's `success: false`.\n *\n * Targets: `source` (the text to copy, when `text` is not set), `button` (the\n * control that triggers `copy`), `feedback` (the **visible** completion slot).\n *\n * Values: `text` (copy this instead of reading `source`), `feedbackDuration` (ms the\n * completion state is held; `0` or less arms no timer, so it stands until the next\n * copy — a reconnect and the before-cache rewind still clear it), `copiedLabel` /\n * `errorLabel` (what the visible slot shows), `announceCopiedText` /\n * `announceErrorText` (what assistive tech hears; empty announces nothing).\n *\n * @remarks\n * Behavior only — icon swaps and styling are the consumer's, keyed off `data-state`\n * (`idle` / `copied` / `error`).\n *\n * **The `feedback` slot must not carry live-region semantics.** Announcing is the\n * page's shared `stimeo--announcer` job: it is seated before the change it reads,\n * which a slot filled on demand cannot be, and it already collapses a repeat of the\n * same wording so a second copy is still heard. A `role=\"status\"` on the slot as\n * well would say everything twice.\n *\n * `copied` and `error` are transient: the timer that clears them belongs to one\n * connection, so a fresh `connect()` that finds either — a restored snapshot, an\n * in-page move — returns to `idle`, and the state is rewound before Turbo caches\n * the page ({@link BeforeCacheReset}). The rewind is silent: it discards nothing a\n * reconnect does not derive again.\n */\nexport class ClipboardController extends Controller<HTMLElement> {\n static override targets = [\"source\", \"button\", \"feedback\"];\n static override values = {\n text: { type: String, default: \"\" },\n feedbackDuration: { type: Number, default: 2000 },\n copiedLabel: { type: String, default: \"Copied\" },\n errorLabel: { type: String, default: \"Copy failed\" },\n announceCopiedText: { type: String, default: \"\" },\n announceErrorText: { type: String, default: \"\" },\n };\n static actions = [\"copy\"] as const;\n static events = [\"copy\"] as const;\n\n declare readonly sourceTarget: HTMLElement;\n declare readonly buttonTarget: HTMLElement;\n declare readonly feedbackTarget: HTMLElement;\n declare readonly hasSourceTarget: boolean;\n declare readonly hasButtonTarget: boolean;\n declare readonly hasFeedbackTarget: boolean;\n\n declare textValue: string;\n declare feedbackDurationValue: number;\n declare copiedLabelValue: string;\n declare errorLabelValue: string;\n declare announceCopiedTextValue: string;\n declare announceErrorTextValue: string;\n\n /**\n * The pending return to idle — the only timer this controller schedules, so\n * `clearAll()` is exactly \"drop the auto-clear\" and needs no id of its own.\n */\n readonly #timers = new SafeTimeout();\n\n /** Returns the completion state to idle for the snapshot Turbo takes. */\n readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n\n /**\n * Whether this connection is still live. `copy()` suspends on the Clipboard API,\n * and a teardown that lands while it is suspended must win: the continuation\n * would otherwise write to an element nobody owns and arm a timer past the\n * `clearAll()` that was supposed to be the last word.\n */\n #connected = false;\n\n override connect(): void {\n this.#connected = true;\n this.#adopt();\n this.#beforeCache.activate();\n }\n\n override disconnect(): void {\n this.#connected = false;\n this.#beforeCache.deactivate();\n this.#timers.clearAll();\n }\n\n /**\n * Copies the resolved text and reports the outcome. Bound via `data-action`\n * (click). Dispatches `stimeo--clipboard:copy` with `{ success, text }` once per\n * completed attempt — including on failure — so consumers can react either way.\n * An attempt whose connection ended while it was in flight reports nothing.\n */\n async copy(): Promise<void> {\n const text = this.#resolveText();\n let success = false;\n try {\n await navigator.clipboard.writeText(text);\n success = true;\n } catch {\n success = false;\n }\n if (!this.#connected) return;\n\n this.#reportResult(success);\n this.dispatch(\"copy\", { detail: { success, text } });\n }\n\n /**\n * The text to copy: the explicit `text` value when set, otherwise the source\n * target's current value (inputs/textareas) or text content.\n */\n #resolveText(): string {\n if (this.textValue.length > 0) return this.textValue;\n if (!this.hasSourceTarget) return \"\";\n const source = this.sourceTarget;\n if (source instanceof HTMLInputElement || source instanceof HTMLTextAreaElement) {\n return source.value;\n }\n return source.textContent ?? \"\";\n }\n\n /**\n * Reads the current state back from the DOM.\n *\n * A `copied` or `error` found at connect time is this controller's own output\n * from a connection that is gone, and so is the timer that would have cleared it\n * — nothing else would ever return the element to `idle`. Any other authored\n * value belongs to the consumer and only a missing attribute takes the default.\n */\n #adopt(): void {\n if (this.#inTransientState()) {\n this.#reset();\n return;\n }\n setDefaultAttribute(this.element, \"data-state\", \"idle\");\n }\n\n /** Whether `data-state` currently holds one of the values this controller writes. */\n #inTransientState(): boolean {\n const state = this.element.getAttribute(\"data-state\");\n return state !== null && TRANSIENT_STATES.has(state);\n }\n\n /**\n * Returns the completion state to idle for the snapshot Turbo is about to take,\n * so a page reached with the Back button does not report a copy that happened\n * before the navigation. Only a state this controller wrote is rewound — an\n * authored one is the consumer's and has to survive into the snapshot, exactly as\n * `connect()` leaves it alone. State only — no `copy` is dispatched, which would\n * claim a fresh copy ran.\n */\n #rewind(): void {\n if (!this.#inTransientState()) return;\n this.#timers.clearAll();\n this.#reset();\n }\n\n /** Reflects the result, announces it, and schedules the return to idle. */\n #reportResult(success: boolean): void {\n this.element.setAttribute(\"data-state\", success ? \"copied\" : \"error\");\n if (this.hasFeedbackTarget) {\n this.feedbackTarget.textContent = success ? this.copiedLabelValue : this.errorLabelValue;\n }\n announce(success ? this.announceCopiedTextValue : this.announceErrorTextValue);\n\n // Drop any in-flight reset so consecutive copies restart the full window\n // rather than having the earlier timer clear the new result prematurely.\n this.#timers.clearAll();\n if (this.feedbackDurationValue > 0) {\n this.#timers.set(() => this.#reset(), this.feedbackDurationValue);\n }\n }\n\n /** Returns to the idle state and empties the completion slot. */\n #reset(): void {\n this.element.setAttribute(\"data-state\", \"idle\");\n if (this.hasFeedbackTarget) {\n this.feedbackTarget.textContent = \"\";\n }\n }\n}\n"]}
@@ -8,6 +8,7 @@ import { Controller } from '@hotwired/stimulus';
8
8
  * data-stimeo--color-picker-value-value="#3366cc">
9
9
  * <div role="slider" aria-label="Hue" data-channel="hue" tabindex="0"
10
10
  * aria-valuemin="0" aria-valuemax="360" aria-valuenow="210"
11
+ * data-value-text="Hue {value} degrees"
11
12
  * data-stimeo--color-picker-target="slider"
12
13
  * data-action="keydown->stimeo--color-picker#onKeydown
13
14
  * pointerdown->stimeo--color-picker#onPointerDown"></div>
@@ -38,18 +39,36 @@ import { Controller } from '@hotwired/stimulus';
38
39
  * here reads `direction`. A gradient has no logical `to` keyword, so mirroring
39
40
  * one means swapping `to right`/`to left` under a `:dir(rtl)` selector.
40
41
  *
41
- * Pointer-drag listeners on `document` are bound to an
42
- * {@link AbortController} and released on drag end and on `disconnect()` (Turbo
43
- * navigation included). Color is fully reconstructable from the `value` (hex), so
44
- * there is no transient state to restore after a Turbo cache/morph.
42
+ * A channel slider announces its bounds from its own `aria-valuemin`/`aria-valuemax`,
43
+ * falling back per channel when they are absent or blank; the resolved pair is
44
+ * written back, so assistive tech never hears the `slider` role's 0–100 default
45
+ * over a hue that reaches 360. `aria-valuetext` is filled from the slider's
46
+ * `{@link VALUE_TEXT_ATTRIBUTE}` template — `{value}` is the channel value — which
47
+ * keeps the announced wording i18n-neutral; without a template the text is English.
48
+ *
49
+ * A drag belongs to the pointer that started it: only a primary button opens one,
50
+ * and {@link OwnedPointerSession} filters movement and termination by that
51
+ * `pointerId`, so a second finger neither steers nor cuts the gesture. Its
52
+ * listeners are released on drag end, when the slider leaves, and on `disconnect()`
53
+ * (Turbo navigation included).
54
+ *
55
+ * The `value` Value carries the color in both directions: every settled color is
56
+ * written back, so a Turbo cache restore and a form submission both carry the color
57
+ * the user picked. An outside write — application code or a morph — re-seeds the
58
+ * model and reports `reconcile`. The ARIA attributes, `--stimeo--color`, and the
59
+ * mirrored input values are this controller's own output and stay in the DOM as
60
+ * written, which is what makes the restored snapshot show the current color.
45
61
  *
46
62
  * The internal model is integer HSL(A), so a hex → HSL → hex round-trip is not
47
63
  * exactly bijective: a typed hex can normalize to a near (not identical) value
48
64
  * once the HSL sliders are touched. This keeps the model small and zero-dep; use a
49
65
  * dedicated color library on the consumer side if exact hex preservation matters.
50
66
  *
67
+ * While `alpha` is disabled the model stays opaque and an alpha slider authored
68
+ * anyway edits nothing, so the hex and `change`'s `rgba.a` never disagree.
69
+ *
51
70
  * A color the user set through a slider or the hex input is reported as
52
- * `stimeo--color-picker:change`. Toggling `alpha` at runtime can move the
71
+ * `stimeo--color-picker:change`. Changing `alpha` or `value` at runtime can move the
53
72
  * committed color without a user edit, and that arrives as
54
73
  * `stimeo--color-picker:reconcile` with the same detail. Neither fires on connect.
55
74
  */
@@ -86,9 +105,21 @@ declare class ColorPickerController extends Controller<HTMLElement> {
86
105
  disconnect(): void;
87
106
  /** Repaints when application code (or a Turbo morph) changes `alpha` at runtime. */
88
107
  alphaValueChanged(): void;
108
+ /** Adopts a color application code (or a Turbo morph) put in `value` at runtime. */
109
+ valueValueChanged(): void;
110
+ /** Hydrates a channel slider inserted or replaced at runtime. */
111
+ sliderTargetConnected(slider: HTMLElement): void;
112
+ /** Ends a gesture whose geometry target disappeared or ceased being a target. */
113
+ sliderTargetDisconnected(slider: HTMLElement): void;
114
+ /** Fills a hex input inserted or replaced at runtime with the current color. */
115
+ hexTargetConnected(hex: HTMLInputElement): void;
116
+ /** Fills a form field inserted or replaced at runtime with the current color. */
117
+ fieldTargetConnected(field: HTMLInputElement): void;
118
+ /** Publishes the current color on a preview inserted or replaced at runtime. */
119
+ previewTargetConnected(preview: HTMLElement): void;
89
120
  /** Keyboard stepping on the focused channel slider (APG Slider model). */
90
121
  onKeydown(event: KeyboardEvent): void;
91
- /** Begins a pointer drag on a channel slider and tracks movement. */
122
+ /** Begins a primary-button drag on a channel slider, owned by its own pointer. */
92
123
  onPointerDown(event: PointerEvent): void;
93
124
  /** Parses the hex input on confirm and syncs every channel + surface. */
94
125
  onHexInput(): void;
@@ -58,8 +58,62 @@ var MicrotaskCoalescer = class {
58
58
  }
59
59
  };
60
60
 
61
+ // src/utils/owned_pointer_session.ts
62
+ var OwnedPointerSession = class {
63
+ pointerId;
64
+ #owner;
65
+ #handlers;
66
+ #abort = new AbortController();
67
+ #active = true;
68
+ constructor(start, owner, handlers) {
69
+ this.pointerId = start.pointerId;
70
+ this.#owner = owner;
71
+ this.#handlers = handlers;
72
+ const { signal } = this.#abort;
73
+ owner.ownerDocument.addEventListener("pointermove", this.#onMove, { signal });
74
+ owner.ownerDocument.addEventListener("pointerup", this.#onEndEvent, { signal });
75
+ owner.ownerDocument.addEventListener("pointercancel", this.#onEndEvent, { signal });
76
+ owner.addEventListener("lostpointercapture", this.#onLostCapture, { signal });
77
+ try {
78
+ owner.setPointerCapture?.(this.pointerId);
79
+ } catch {
80
+ }
81
+ }
82
+ /** Whether this session still owns its pointer and listeners. */
83
+ get active() {
84
+ return this.#active;
85
+ }
86
+ /** Whether `event` belongs to the initiating pointer of the live session. */
87
+ owns(event) {
88
+ return this.#active && event.pointerId === this.pointerId;
89
+ }
90
+ /** Releases capture/listeners and invokes the end callback exactly once. */
91
+ end() {
92
+ if (!this.#active) return;
93
+ this.#active = false;
94
+ this.#abort.abort();
95
+ try {
96
+ this.#owner.releasePointerCapture?.(this.pointerId);
97
+ } catch {
98
+ }
99
+ this.#handlers.end?.();
100
+ }
101
+ #onMove = (event) => {
102
+ if (this.owns(event)) this.#handlers.move(event);
103
+ };
104
+ #onEndEvent = (event) => {
105
+ if (this.owns(event)) this.end();
106
+ };
107
+ #onLostCapture = (event) => {
108
+ const pointerId = event.pointerId;
109
+ if (typeof pointerId === "number" && pointerId !== this.pointerId) return;
110
+ this.end();
111
+ };
112
+ };
113
+
61
114
  // src/controllers/color_picker_controller.ts
62
115
  var COLOR_PROPERTY = "--stimeo--color";
116
+ var VALUE_TEXT_ATTRIBUTE = "data-value-text";
63
117
  var CHANNEL_RANGE = {
64
118
  hue: [0, 360],
65
119
  saturation: [0, 100],
@@ -79,10 +133,10 @@ var ColorPickerController = class extends Controller {
79
133
  get #mirrored() {
80
134
  return this.logicalTrackValue && isRtl(this.element);
81
135
  }
82
- /** The current color in the editing model. */
136
+ /** The current color in the editing model; its alpha is 100 while `alpha` is off. */
83
137
  #color = { hue: 0, saturation: 0, lightness: 0, alpha: 100 };
84
- /** Aborts in-progress pointer-drag listeners on drag end / teardown. */
85
- #dragAbort = null;
138
+ /** The pointer that owns the live drag, with the slider whose geometry maps it. */
139
+ #drag = null;
86
140
  /** Color the last repaint settled on, so a configuration-driven move is reported once. */
87
141
  #committedHex = null;
88
142
  /**
@@ -93,25 +147,48 @@ var ColorPickerController = class extends Controller {
93
147
  /** Seeds the model from the initial hex value and renders every surface. */
94
148
  connect() {
95
149
  this.#repaint.activate();
96
- const parsed = hexToHsla(this.valueValue);
97
- if (parsed) this.#color = this.alphaValue ? parsed : { ...parsed, alpha: 100 };
150
+ this.#adoptValue();
98
151
  this.#render();
99
152
  }
100
153
  /** Cancels any active pointer drag so document listeners never leak. */
101
154
  disconnect() {
102
155
  this.#repaint.cancel();
103
- this.#dragAbort?.abort();
104
- this.#dragAbort = null;
156
+ this.#endDrag();
105
157
  }
106
158
  /** Repaints when application code (or a Turbo morph) changes `alpha` at runtime. */
107
159
  alphaValueChanged() {
108
160
  this.#repaint.schedule();
109
161
  }
162
+ /** Adopts a color application code (or a Turbo morph) put in `value` at runtime. */
163
+ valueValueChanged() {
164
+ if (this.valueValue === this.#committedHex) return;
165
+ this.#repaint.schedule();
166
+ }
167
+ /** Hydrates a channel slider inserted or replaced at runtime. */
168
+ sliderTargetConnected(slider) {
169
+ this.#renderSlider(slider);
170
+ }
171
+ /** Ends a gesture whose geometry target disappeared or ceased being a target. */
172
+ sliderTargetDisconnected(slider) {
173
+ if (this.#drag?.slider === slider) this.#endDrag();
174
+ }
175
+ /** Fills a hex input inserted or replaced at runtime with the current color. */
176
+ hexTargetConnected(hex) {
177
+ this.#mirrorColor(hex, this.#hexString());
178
+ }
179
+ /** Fills a form field inserted or replaced at runtime with the current color. */
180
+ fieldTargetConnected(field) {
181
+ this.#mirrorColor(field, this.#hexString());
182
+ }
183
+ /** Publishes the current color on a preview inserted or replaced at runtime. */
184
+ previewTargetConnected(preview) {
185
+ this.#publishColor(preview, this.#hexString());
186
+ }
110
187
  /** Keyboard stepping on the focused channel slider (APG Slider model). */
111
188
  onKeydown(event) {
112
189
  if (isReservedArrowChord(event)) return;
113
190
  const slider = event.currentTarget;
114
- const channel = this.#channelOf(slider);
191
+ const channel = this.#editableChannel(slider);
115
192
  if (!channel) return;
116
193
  const [min, max] = this.#rangeOf(slider, channel);
117
194
  const value = this.#color[channel];
@@ -143,34 +220,36 @@ var ColorPickerController = class extends Controller {
143
220
  event.preventDefault();
144
221
  this.#setChannel(channel, next, min, max);
145
222
  }
146
- /** Begins a pointer drag on a channel slider and tracks movement. */
223
+ /** Begins a primary-button drag on a channel slider, owned by its own pointer. */
147
224
  onPointerDown(event) {
225
+ if (event.button !== 0 || this.#drag) return;
148
226
  const slider = event.currentTarget;
149
- const channel = this.#channelOf(slider);
227
+ const channel = this.#editableChannel(slider);
150
228
  if (!channel) return;
151
- event.preventDefault();
152
- slider.focus();
153
229
  const [min, max] = this.#rangeOf(slider, channel);
154
230
  const mirrored = this.#mirrored;
155
231
  const update = (clientX) => {
156
232
  const rect = slider.getBoundingClientRect();
157
- if (rect.width === 0) return;
233
+ if (rect.width === 0) return false;
158
234
  const offset = Math.min(1, Math.max(0, (clientX - rect.left) / rect.width));
159
235
  const fraction = mirrored ? 1 - offset : offset;
160
236
  this.#setChannel(channel, min + fraction * (max - min), min, max);
237
+ return true;
161
238
  };
162
- update(event.clientX);
163
- this.#dragAbort?.abort();
164
- const abort = new AbortController();
165
- this.#dragAbort = abort;
166
- const onMove = (move) => update(move.clientX);
167
- const onUp = () => {
168
- abort.abort();
169
- this.#dragAbort = null;
170
- };
171
- document.addEventListener("pointermove", onMove, { signal: abort.signal });
172
- document.addEventListener("pointerup", onUp, { signal: abort.signal });
173
- document.addEventListener("pointercancel", onUp, { signal: abort.signal });
239
+ if (!update(event.clientX)) return;
240
+ event.preventDefault();
241
+ slider.focus();
242
+ const drag = { pointer: null, slider };
243
+ drag.pointer = new OwnedPointerSession(event, slider, {
244
+ move: (move) => {
245
+ if (slider.isConnected) update(move.clientX);
246
+ else this.#endDrag();
247
+ },
248
+ end: () => {
249
+ if (this.#drag === drag) this.#drag = null;
250
+ }
251
+ });
252
+ this.#drag = drag;
174
253
  }
175
254
  /** Parses the hex input on confirm and syncs every channel + surface. */
176
255
  onHexInput() {
@@ -180,9 +259,22 @@ var ColorPickerController = class extends Controller {
180
259
  this.hexTarget.value = this.#hexString();
181
260
  return;
182
261
  }
183
- this.#color = this.alphaValue ? parsed : { ...parsed, alpha: 100 };
262
+ this.#color = this.#opaqueUnlessEnabled(parsed);
184
263
  this.#commitColor();
185
264
  }
265
+ /** Replaces the model with the color `value` names, leaving an unparsable one alone. */
266
+ #adoptValue() {
267
+ const parsed = hexToHsla(this.valueValue);
268
+ if (parsed) this.#color = this.#opaqueUnlessEnabled(parsed);
269
+ }
270
+ /**
271
+ * The model a parsed color implies: alpha only survives while its channel is
272
+ * enabled, because `hexString()` would otherwise emit `#RRGGBB` while `change`
273
+ * reported `rgba.a < 1`.
274
+ */
275
+ #opaqueUnlessEnabled(parsed) {
276
+ return this.alphaValue ? parsed : { ...parsed, alpha: 100 };
277
+ }
186
278
  /** Clamps and snaps one channel to an integer, then re-renders + emits change. */
187
279
  #setChannel(channel, raw, min, max) {
188
280
  this.#color[channel] = Math.round(Math.min(max, Math.max(min, raw)));
@@ -201,32 +293,57 @@ var ColorPickerController = class extends Controller {
201
293
  }
202
294
  }
203
295
  /**
204
- * Reflects the model onto sliders, the hex input, preview, and form field.
296
+ * Reflects the model onto sliders, the hex input, preview, form field, and the
297
+ * `value` Value it serializes into.
205
298
  *
206
299
  * @stimeoRenderRoot
207
300
  */
208
301
  #render() {
209
- for (const slider of this.sliderTargets) {
210
- const channel = this.#channelOf(slider);
211
- if (!channel) continue;
212
- const value = this.#color[channel];
213
- slider.setAttribute("aria-valuenow", String(value));
214
- slider.setAttribute("aria-valuetext", valueText(channel, value));
215
- }
302
+ for (const slider of this.sliderTargets) this.#renderSlider(slider);
216
303
  const hex = this.#hexString();
217
304
  this.#committedHex = hex;
218
- if (this.hasHexTarget) this.hexTarget.value = hex;
219
- for (const field of this.fieldTargets) field.value = hex;
220
- for (const preview of this.previewTargets) preview.style.setProperty(COLOR_PROPERTY, hex);
221
- this.element.style.setProperty(COLOR_PROPERTY, hex);
305
+ if (this.valueValue !== hex) this.valueValue = hex;
306
+ if (this.hasHexTarget) this.#mirrorColor(this.hexTarget, hex);
307
+ for (const field of this.fieldTargets) this.#mirrorColor(field, hex);
308
+ for (const preview of this.previewTargets) this.#publishColor(preview, hex);
309
+ this.#publishColor(this.element, hex);
310
+ }
311
+ /** Writes one slider's announced range, value, and value text, skipping equal ones. */
312
+ #renderSlider(slider) {
313
+ const channel = this.#channelOf(slider);
314
+ if (!channel) return;
315
+ const [min, max] = this.#rangeOf(slider, channel);
316
+ const value = this.#color[channel];
317
+ const attributes = {
318
+ "aria-valuemin": String(min),
319
+ "aria-valuemax": String(max),
320
+ "aria-valuenow": String(value),
321
+ "aria-valuetext": this.#valueText(slider, channel, value)
322
+ };
323
+ for (const [name, next] of Object.entries(attributes)) {
324
+ if (slider.getAttribute(name) !== next) slider.setAttribute(name, next);
325
+ }
326
+ }
327
+ /** Mirrors the color into an input, leaving an already-equal value untouched. */
328
+ #mirrorColor(input, hex) {
329
+ if (input.value !== hex) input.value = hex;
330
+ }
331
+ /** Publishes the color as the consumer's CSS hook, skipping an equal value. */
332
+ #publishColor(element, hex) {
333
+ if (element.style.getPropertyValue(COLOR_PROPERTY) !== hex) {
334
+ element.style.setProperty(COLOR_PROPERTY, hex);
335
+ }
222
336
  }
223
337
  /**
224
- * Repaints after `alpha` changed at runtime and reports a color this controller
225
- * settled on. Disabling alpha drops it from the model, so the committed color can
226
- * move without a user edit; `change` stays reserved for the picker's own actions.
338
+ * Repaints after a declarative input changed at runtime and reports a color this
339
+ * controller settled on. Disabling alpha drops it from the model and an outside
340
+ * `value` names another color, so the committed color can move without a user
341
+ * edit; `change` stays reserved for the picker's own actions.
227
342
  */
228
343
  #reconcileColor() {
229
344
  const previous = this.#committedHex;
345
+ if (previous !== null && this.valueValue !== previous) this.#adoptValue();
346
+ if (!this.alphaValue) this.#color.alpha = 100;
230
347
  this.#render();
231
348
  if (previous !== null && this.#committedHex !== previous) {
232
349
  this.dispatch("reconcile", { detail: this.#settledDetail() });
@@ -247,7 +364,27 @@ var ColorPickerController = class extends Controller {
247
364
  /** Reads a slider's `data-channel`, if it is a known channel. */
248
365
  #channelOf(slider) {
249
366
  const channel = slider.getAttribute("data-channel");
250
- return channel && channel in CHANNEL_RANGE ? channel : null;
367
+ return channel && Object.hasOwn(CHANNEL_RANGE, channel) ? channel : null;
368
+ }
369
+ /**
370
+ * The channel a slider edits, or null when this picker edits none through it. An
371
+ * alpha slider authored while `alpha` is off edits nothing: moving it would leave
372
+ * the model translucent behind an opaque `#RRGGBB`.
373
+ */
374
+ #editableChannel(slider) {
375
+ const channel = this.#channelOf(slider);
376
+ return channel === "alpha" && !this.alphaValue ? null : channel;
377
+ }
378
+ /** The channel's announced text: the slider's template, or the built-in English. */
379
+ #valueText(slider, channel, value) {
380
+ const template = slider.getAttribute(VALUE_TEXT_ATTRIBUTE);
381
+ return template ? template.replaceAll("{value}", String(value)) : defaultValueText(channel, value);
382
+ }
383
+ /** Ends the live drag so no further movement of that pointer reaches the model. */
384
+ #endDrag() {
385
+ const drag = this.#drag;
386
+ this.#drag = null;
387
+ drag?.pointer?.end();
251
388
  }
252
389
  /**
253
390
  * A slider's `[min, max]` from aria-valuemin/max, falling back per channel.
@@ -263,7 +400,7 @@ var ColorPickerController = class extends Controller {
263
400
  ];
264
401
  }
265
402
  };
266
- function valueText(channel, value) {
403
+ function defaultValueText(channel, value) {
267
404
  const label = channel.charAt(0).toUpperCase() + channel.slice(1);
268
405
  const unit = channel === "hue" ? "degrees" : "percent";
269
406
  return `${label} ${value} ${unit}`;