stimeo-ui 0.2.0 → 0.2.1

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 (124) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/dist/controllers/accordion_controller.d.ts +3 -2
  3. package/dist/controllers/accordion_controller.js.map +1 -1
  4. package/dist/controllers/alert_dialog_controller.d.ts +64 -0
  5. package/dist/controllers/alert_dialog_controller.js +318 -0
  6. package/dist/controllers/alert_dialog_controller.js.map +1 -0
  7. package/dist/controllers/carousel_controller.d.ts +100 -0
  8. package/dist/controllers/carousel_controller.js +272 -0
  9. package/dist/controllers/carousel_controller.js.map +1 -0
  10. package/dist/controllers/clipboard_controller.d.ts +72 -0
  11. package/dist/controllers/clipboard_controller.js +144 -0
  12. package/dist/controllers/clipboard_controller.js.map +1 -0
  13. package/dist/controllers/collapsible_controller.d.ts +70 -0
  14. package/dist/controllers/collapsible_controller.js +327 -0
  15. package/dist/controllers/collapsible_controller.js.map +1 -0
  16. package/dist/controllers/color_picker_controller.d.ts +76 -0
  17. package/dist/controllers/color_picker_controller.js +213 -0
  18. package/dist/controllers/color_picker_controller.js.map +1 -0
  19. package/dist/controllers/count_up_controller.js +8 -1
  20. package/dist/controllers/count_up_controller.js.map +1 -1
  21. package/dist/controllers/currency_input_controller.d.ts +72 -0
  22. package/dist/controllers/currency_input_controller.js +147 -0
  23. package/dist/controllers/currency_input_controller.js.map +1 -0
  24. package/dist/controllers/data_grid_controller.d.ts +71 -0
  25. package/dist/controllers/data_grid_controller.js +168 -0
  26. package/dist/controllers/data_grid_controller.js.map +1 -0
  27. package/dist/controllers/date_range_picker_controller.d.ts +87 -0
  28. package/dist/controllers/date_range_picker_controller.js +417 -0
  29. package/dist/controllers/date_range_picker_controller.js.map +1 -0
  30. package/dist/controllers/dismissible_controller.d.ts +57 -0
  31. package/dist/controllers/dismissible_controller.js +117 -0
  32. package/dist/controllers/dismissible_controller.js.map +1 -0
  33. package/dist/controllers/drawer_controller.d.ts +96 -0
  34. package/dist/controllers/drawer_controller.js +630 -0
  35. package/dist/controllers/drawer_controller.js.map +1 -0
  36. package/dist/controllers/editable_controller.d.ts +73 -0
  37. package/dist/controllers/editable_controller.js +168 -0
  38. package/dist/controllers/editable_controller.js.map +1 -0
  39. package/dist/controllers/file_dropzone_controller.d.ts +92 -0
  40. package/dist/controllers/file_dropzone_controller.js +165 -0
  41. package/dist/controllers/file_dropzone_controller.js.map +1 -0
  42. package/dist/controllers/filter_controller.d.ts +60 -0
  43. package/dist/controllers/filter_controller.js +86 -0
  44. package/dist/controllers/filter_controller.js.map +1 -0
  45. package/dist/controllers/flash_controller.js +36 -5
  46. package/dist/controllers/flash_controller.js.map +1 -1
  47. package/dist/controllers/highlight_controller.js +6 -4
  48. package/dist/controllers/highlight_controller.js.map +1 -1
  49. package/dist/controllers/intersection_controller.js +41 -18
  50. package/dist/controllers/intersection_controller.js.map +1 -1
  51. package/dist/controllers/lazy_frame_controller.js +33 -11
  52. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  53. package/dist/controllers/masonry_controller.d.ts +54 -0
  54. package/dist/controllers/masonry_controller.js +142 -0
  55. package/dist/controllers/masonry_controller.js.map +1 -0
  56. package/dist/controllers/menubar_controller.d.ts +62 -0
  57. package/dist/controllers/menubar_controller.js +433 -0
  58. package/dist/controllers/menubar_controller.js.map +1 -0
  59. package/dist/controllers/multi_select_controller.d.ts +115 -0
  60. package/dist/controllers/multi_select_controller.js +472 -0
  61. package/dist/controllers/multi_select_controller.js.map +1 -0
  62. package/dist/controllers/navigation_menu_controller.d.ts +99 -0
  63. package/dist/controllers/navigation_menu_controller.js +384 -0
  64. package/dist/controllers/navigation_menu_controller.js.map +1 -0
  65. package/dist/controllers/overflow_indicator_controller.d.ts +12 -4
  66. package/dist/controllers/overflow_indicator_controller.js +178 -27
  67. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  68. package/dist/controllers/password_reveal_controller.d.ts +49 -0
  69. package/dist/controllers/password_reveal_controller.js +117 -0
  70. package/dist/controllers/password_reveal_controller.js.map +1 -0
  71. package/dist/controllers/range_slider_controller.d.ts +87 -0
  72. package/dist/controllers/range_slider_controller.js +166 -0
  73. package/dist/controllers/range_slider_controller.js.map +1 -0
  74. package/dist/controllers/read_more_controller.d.ts +52 -0
  75. package/dist/controllers/read_more_controller.js +194 -0
  76. package/dist/controllers/read_more_controller.js.map +1 -0
  77. package/dist/controllers/scroll_area_controller.js +15 -2
  78. package/dist/controllers/scroll_area_controller.js.map +1 -1
  79. package/dist/controllers/scroll_restore_controller.d.ts +54 -0
  80. package/dist/controllers/scroll_restore_controller.js +93 -0
  81. package/dist/controllers/scroll_restore_controller.js.map +1 -0
  82. package/dist/controllers/scroll_visibility_controller.js +8 -4
  83. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  84. package/dist/controllers/scrollspy_controller.js +33 -11
  85. package/dist/controllers/scrollspy_controller.js.map +1 -1
  86. package/dist/controllers/separator_controller.d.ts +54 -0
  87. package/dist/controllers/separator_controller.js +87 -0
  88. package/dist/controllers/separator_controller.js.map +1 -0
  89. package/dist/controllers/sidebar_controller.d.ts +107 -0
  90. package/dist/controllers/sidebar_controller.js +761 -0
  91. package/dist/controllers/sidebar_controller.js.map +1 -0
  92. package/dist/controllers/stepper_controller.d.ts +4 -0
  93. package/dist/controllers/stepper_controller.js +28 -12
  94. package/dist/controllers/stepper_controller.js.map +1 -1
  95. package/dist/controllers/stick_to_bottom_controller.js +8 -4
  96. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  97. package/dist/controllers/sticky_observer_controller.d.ts +17 -4
  98. package/dist/controllers/sticky_observer_controller.js +88 -20
  99. package/dist/controllers/sticky_observer_controller.js.map +1 -1
  100. package/dist/controllers/tabs_controller.d.ts +3 -2
  101. package/dist/controllers/tabs_controller.js.map +1 -1
  102. package/dist/controllers/tags_input_controller.d.ts +91 -0
  103. package/dist/controllers/tags_input_controller.js +275 -0
  104. package/dist/controllers/tags_input_controller.js.map +1 -0
  105. package/dist/controllers/theme_controller.js +20 -10
  106. package/dist/controllers/theme_controller.js.map +1 -1
  107. package/dist/controllers/time_picker_controller.d.ts +67 -0
  108. package/dist/controllers/time_picker_controller.js +212 -0
  109. package/dist/controllers/time_picker_controller.js.map +1 -0
  110. package/dist/controllers/toast_controller.d.ts +1 -9
  111. package/dist/controllers/toast_controller.js +36 -9
  112. package/dist/controllers/toast_controller.js.map +1 -1
  113. package/dist/controllers/transition_controller.d.ts +14 -8
  114. package/dist/controllers/transition_controller.js +153 -38
  115. package/dist/controllers/transition_controller.js.map +1 -1
  116. package/dist/controllers/tree_view_controller.d.ts +54 -0
  117. package/dist/controllers/tree_view_controller.js +275 -0
  118. package/dist/controllers/tree_view_controller.js.map +1 -0
  119. package/dist/index.d.ts +27 -1818
  120. package/dist/index.js +811 -295
  121. package/dist/index.js.map +1 -1
  122. package/dist/inspector/examples.json +2 -2
  123. package/dist/inspector/manifest.json +15 -3
  124. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/controllers/theme_controller.ts"],"names":[],"mappings":"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","file":"theme_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\n/** The three selectable modes; `system` follows the OS `prefers-color-scheme`. */\ntype ThemeMode = \"light\" | \"dark\" | \"system\";\n/** The two effective (resolved) themes applied to the root. */\ntype ResolvedTheme = \"light\" | \"dark\";\n\nconst MODES: readonly ThemeMode[] = [\"light\", \"dark\", \"system\"];\nconst isMode = (value: unknown): value is ThemeMode =>\n typeof value === \"string\" && (MODES as readonly string[]).includes(value);\n\n/**\n * Headless **theme / color-scheme toggle** — persists a light/dark/system choice to\n * `localStorage`, follows the OS setting while in `system`, and reflects the\n * effective theme onto the root for the consumer's CSS. Ships no colors; only state\n * hooks.\n *\n * Two markup contracts (identifier: `stimeo--theme`):\n *\n * Canonical 3-value radiogroup (use this when `system` is offered):\n * <div data-controller=\"stimeo--theme\" data-stimeo--theme-mode-value=\"system\"\n * role=\"radiogroup\" aria-label=\"Theme\">\n * <button data-stimeo--theme-target=\"option\" role=\"radio\"\n * data-action=\"click->stimeo--theme#set\"\n * data-stimeo--theme-mode-param=\"light\">Light</button>\n * <button data-stimeo--theme-target=\"option\" role=\"radio\"\n * data-action=\"click->stimeo--theme#set\"\n * data-stimeo--theme-mode-param=\"dark\">Dark</button>\n * <button data-stimeo--theme-target=\"option\" role=\"radio\"\n * data-action=\"click->stimeo--theme#set\"\n * data-stimeo--theme-mode-param=\"system\">System</button>\n * </div>\n *\n * Auxiliary 2-value toggle (light↔dark only — `system` is not representable):\n * <button data-controller=\"stimeo--theme\" data-action=\"click->stimeo--theme#toggle\"\n * aria-pressed=\"false\">Dark mode</button>\n *\n * @remarks\n * Behavior only — the actual palette is the consumer's CSS keyed off `data-theme`\n * on the root. It applies `data-theme` (the *resolved* light/dark) and a matching\n * `color-scheme` to the `target` element (`<html>` by default), keeps the radiogroup\n * `aria-checked` + roving tabindex (APG radio) or the single button's `aria-pressed`\n * in sync, and never moves focus. The `prefers-color-scheme` listener is attached on\n * `connect()` and removed on `disconnect()` (Turbo included). FOUC avoidance for the\n * very first paint is an inline `<head>` snippet (documented), not this controller.\n */\nexport class ThemeController extends Controller<HTMLElement> {\n static override targets = [\"option\"];\n static override values = {\n mode: { type: String, default: \"system\" },\n storageKey: { type: String, default: \"stimeo-theme\" },\n target: { type: String, default: \"html\" },\n };\n static actions = [\"set\", \"toggle\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly optionTargets: HTMLElement[];\n declare readonly hasOptionTarget: boolean;\n\n declare modeValue: string;\n declare storageKeyValue: string;\n declare targetValue: string;\n\n /** The OS dark-mode query, watched so `system` tracks live changes. */\n #media: MediaQueryList | null = null;\n\n /** Re-resolves while in `system` mode when the OS preference flips. */\n readonly #onMediaChange = (): void => {\n if (this.modeValue === \"system\") {\n this.#applyTheme();\n // Re-sync controls too: a 2-value toggle's `aria-pressed` reflects the\n // *resolved* theme, so it must follow the OS flip — not only `data-theme`.\n this.#syncControls();\n this.#dispatchChange();\n }\n };\n\n /** Arrow/Home/End navigation for the radiogroup (APG radio pattern). */\n readonly #onKeydown = (event: KeyboardEvent): void => {\n // Resolved once per keydown: every `optionTargets` access re-queries the scope.\n const options = this.optionTargets;\n if (options.length === 0) return;\n const target = event.target as HTMLElement | null;\n const current = options.indexOf(target as HTMLElement);\n if (current === -1) return;\n\n const last = options.length - 1;\n let next = current;\n switch (event.key) {\n case \"ArrowDown\":\n case \"ArrowRight\":\n next = current === last ? 0 : current + 1;\n break;\n case \"ArrowUp\":\n case \"ArrowLeft\":\n next = current === 0 ? last : current - 1;\n break;\n case \"Home\":\n next = 0;\n break;\n case \"End\":\n next = last;\n break;\n default:\n return;\n }\n event.preventDefault();\n const option = options[next];\n if (!option) return;\n option.focus();\n this.#setMode(this.#optionMode(option));\n };\n\n override connect(): void {\n const stored = this.#readStored();\n if (stored) this.modeValue = stored;\n\n this.#media = window.matchMedia?.(\"(prefers-color-scheme: dark)\") ?? null;\n this.#media?.addEventListener(\"change\", this.#onMediaChange);\n if (this.hasOptionTarget) this.element.addEventListener(\"keydown\", this.#onKeydown);\n\n this.#applyTheme();\n this.#syncControls();\n }\n\n override disconnect(): void {\n this.#media?.removeEventListener(\"change\", this.#onMediaChange);\n this.element.removeEventListener(\"keydown\", this.#onKeydown);\n }\n\n /** Selects an explicit mode from the `mode` action param (radiogroup option). */\n set(event: Event): void {\n const mode = (event as { params?: Record<string, unknown> }).params?.mode;\n if (isMode(mode)) this.#setMode(mode);\n }\n\n /** Toggles light↔dark for the 2-value single-button contract. */\n toggle(): void {\n this.#setMode(this.#resolved() === \"dark\" ? \"light\" : \"dark\");\n }\n\n /** Central mode change: persist, apply to the root, resync controls, announce. */\n #setMode(mode: ThemeMode): void {\n this.modeValue = mode;\n this.#writeStored(mode);\n this.#applyTheme();\n this.#syncControls();\n this.#dispatchChange();\n }\n\n /** Writes `data-theme` + `color-scheme` (the resolved theme) onto the target. */\n #applyTheme(): void {\n const root = this.#targetElement();\n if (!root) return;\n const resolved = this.#resolved();\n root.setAttribute(\"data-theme\", resolved);\n root.style.setProperty(\"color-scheme\", resolved);\n }\n\n /** Keeps the radiogroup (aria-checked + roving tabindex) or toggle (aria-pressed) in sync. */\n #syncControls(): void {\n const options = this.optionTargets;\n if (options.length > 0) {\n let hasTabbable = false;\n for (const option of options) {\n const selected = this.#optionMode(option) === this.modeValue;\n option.setAttribute(\"aria-checked\", String(selected));\n option.tabIndex = selected ? 0 : -1;\n hasTabbable ||= selected;\n }\n // APG: a radiogroup with no selection keeps its first radio tabbable.\n const first = options[0];\n if (!hasTabbable && first) first.tabIndex = 0;\n return;\n }\n this.element.setAttribute(\"aria-pressed\", String(this.#resolved() === \"dark\"));\n }\n\n /** Emits `change` with the selected mode and the resolved theme. */\n #dispatchChange(): void {\n this.dispatch(\"change\", { detail: { mode: this.modeValue, resolved: this.#resolved() } });\n }\n\n /** The effective theme: the OS preference when `system`, else the mode itself. */\n #resolved(): ResolvedTheme {\n if (this.modeValue === \"dark\") return \"dark\";\n if (this.modeValue === \"light\") return \"light\";\n return this.#media?.matches ? \"dark\" : \"light\";\n }\n\n /** Reads an option's mode from its action param attribute. */\n #optionMode(option: HTMLElement): ThemeMode {\n const mode = option.getAttribute(\"data-stimeo--theme-mode-param\");\n return isMode(mode) ? mode : \"system\";\n }\n\n /** Resolves the state-hook target (`<html>` by default). */\n #targetElement(): HTMLElement | null {\n if (this.targetValue === \"html\" || this.targetValue === \":root\")\n return document.documentElement;\n return document.querySelector<HTMLElement>(this.targetValue);\n }\n\n /** Reads a persisted, validated mode from `localStorage` (null when absent/blocked). */\n #readStored(): ThemeMode | null {\n try {\n const value = window.localStorage.getItem(this.storageKeyValue);\n return isMode(value) ? value : null;\n } catch {\n return null;\n }\n }\n\n /** Persists the mode, swallowing storage errors (private mode / quota). */\n #writeStored(mode: ThemeMode): void {\n try {\n window.localStorage.setItem(this.storageKeyValue, mode);\n } catch {\n /* storage unavailable — the in-DOM state hooks still apply this session */\n }\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/safe_storage.ts","../../src/controllers/theme_controller.ts"],"names":[],"mappings":"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","file":"theme_controller.js","sourcesContent":["/**\n * Guarded `localStorage` access shared by the preference-persisting controllers\n * (sidebar, theme).\n *\n * `window.localStorage` can throw on access or read/write — storage disabled by\n * browser policy, sandboxed frames, private modes, or quota exhaustion. These\n * helpers centralize the try/catch so callers treat persistence as best-effort:\n * a failed read behaves like an unset key and a failed write is dropped,\n * leaving the in-DOM state as this session's source of truth.\n *\n * @remarks\n * Only the guarded access lives here — interpreting the stored string\n * (validation, defaults, key derivation) stays in each controller. The\n * `stimeo--persist` controller keeps its own storage layer on purpose: storage\n * *is* its domain (engine selection, per-field bookkeeping, TTL semantics).\n */\n\n/**\n * Reads `key` from `localStorage`.\n *\n * @param key - The storage key to read.\n * @returns The stored string, or `null` when the key is unset or storage is\n * unavailable (the read threw).\n */\nexport function readLocalStorage(key: string): string | null {\n try {\n return window.localStorage.getItem(key);\n } catch {\n return null;\n }\n}\n\n/**\n * Writes `value` under `key` in `localStorage`, ignoring failures (private\n * mode / quota / storage disabled) — persistence is best-effort by contract.\n *\n * @param key - The storage key to write.\n * @param value - The string to store.\n */\nexport function writeLocalStorage(key: string, value: string): void {\n try {\n window.localStorage.setItem(key, value);\n } catch {\n // Best-effort: the in-DOM state still applies for this session.\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { readLocalStorage, writeLocalStorage } from \"../utils/safe_storage\";\n\n/** The three selectable modes; `system` follows the OS `prefers-color-scheme`. */\ntype ThemeMode = \"light\" | \"dark\" | \"system\";\n/** The two effective (resolved) themes applied to the root. */\ntype ResolvedTheme = \"light\" | \"dark\";\n\nconst MODES: readonly ThemeMode[] = [\"light\", \"dark\", \"system\"];\nconst isMode = (value: unknown): value is ThemeMode =>\n typeof value === \"string\" && (MODES as readonly string[]).includes(value);\n\n/**\n * Headless **theme / color-scheme toggle** — persists a light/dark/system choice to\n * `localStorage`, follows the OS setting while in `system`, and reflects the\n * effective theme onto the root for the consumer's CSS. Ships no colors; only state\n * hooks.\n *\n * Two markup contracts (identifier: `stimeo--theme`):\n *\n * Canonical 3-value radiogroup (use this when `system` is offered):\n * <div data-controller=\"stimeo--theme\" data-stimeo--theme-mode-value=\"system\"\n * role=\"radiogroup\" aria-label=\"Theme\">\n * <button data-stimeo--theme-target=\"option\" role=\"radio\"\n * data-action=\"click->stimeo--theme#set\"\n * data-stimeo--theme-mode-param=\"light\">Light</button>\n * <button data-stimeo--theme-target=\"option\" role=\"radio\"\n * data-action=\"click->stimeo--theme#set\"\n * data-stimeo--theme-mode-param=\"dark\">Dark</button>\n * <button data-stimeo--theme-target=\"option\" role=\"radio\"\n * data-action=\"click->stimeo--theme#set\"\n * data-stimeo--theme-mode-param=\"system\">System</button>\n * </div>\n *\n * Auxiliary 2-value toggle (light↔dark only — `system` is not representable):\n * <button data-controller=\"stimeo--theme\" data-action=\"click->stimeo--theme#toggle\"\n * aria-pressed=\"false\">Dark mode</button>\n *\n * @remarks\n * Behavior only — the actual palette is the consumer's CSS keyed off `data-theme`\n * on the root. It applies `data-theme` (the *resolved* light/dark) and a matching\n * `color-scheme` to the `target` element (`<html>` by default), keeps the radiogroup\n * `aria-checked` + roving tabindex (APG radio) or the single button's `aria-pressed`\n * in sync, and never moves focus. The `prefers-color-scheme` listener is attached on\n * `connect()` and removed on `disconnect()` (Turbo included). FOUC avoidance for the\n * very first paint is an inline `<head>` snippet (documented), not this controller.\n */\nexport class ThemeController extends Controller<HTMLElement> {\n static override targets = [\"option\"];\n static override values = {\n mode: { type: String, default: \"system\" },\n storageKey: { type: String, default: \"stimeo-theme\" },\n target: { type: String, default: \"html\" },\n };\n static actions = [\"set\", \"toggle\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly optionTargets: HTMLElement[];\n declare readonly hasOptionTarget: boolean;\n\n declare modeValue: string;\n declare storageKeyValue: string;\n declare targetValue: string;\n\n /** The OS dark-mode query, watched so `system` tracks live changes. */\n #media: MediaQueryList | null = null;\n\n /** Re-resolves while in `system` mode when the OS preference flips. */\n readonly #onMediaChange = (): void => {\n if (this.modeValue === \"system\") {\n this.#applyTheme();\n // Re-sync controls too: a 2-value toggle's `aria-pressed` reflects the\n // *resolved* theme, so it must follow the OS flip — not only `data-theme`.\n this.#syncControls();\n this.#dispatchChange();\n }\n };\n\n /** Arrow/Home/End navigation for the radiogroup (APG radio pattern). */\n readonly #onKeydown = (event: KeyboardEvent): void => {\n // Resolved once per keydown: every `optionTargets` access re-queries the scope.\n const options = this.optionTargets;\n if (options.length === 0) return;\n const target = event.target as HTMLElement | null;\n const current = options.indexOf(target as HTMLElement);\n if (current === -1) return;\n\n const last = options.length - 1;\n let next = current;\n switch (event.key) {\n case \"ArrowDown\":\n case \"ArrowRight\":\n next = current === last ? 0 : current + 1;\n break;\n case \"ArrowUp\":\n case \"ArrowLeft\":\n next = current === 0 ? last : current - 1;\n break;\n case \"Home\":\n next = 0;\n break;\n case \"End\":\n next = last;\n break;\n default:\n return;\n }\n event.preventDefault();\n const option = options[next];\n if (!option) return;\n option.focus();\n this.#setMode(this.#optionMode(option));\n };\n\n override connect(): void {\n const stored = this.#readStored();\n if (stored) this.modeValue = stored;\n\n this.#media = window.matchMedia?.(\"(prefers-color-scheme: dark)\") ?? null;\n this.#media?.addEventListener(\"change\", this.#onMediaChange);\n if (this.hasOptionTarget) this.element.addEventListener(\"keydown\", this.#onKeydown);\n\n this.#applyTheme();\n this.#syncControls();\n }\n\n override disconnect(): void {\n this.#media?.removeEventListener(\"change\", this.#onMediaChange);\n this.element.removeEventListener(\"keydown\", this.#onKeydown);\n }\n\n /** Selects an explicit mode from the `mode` action param (radiogroup option). */\n set(event: Event): void {\n const mode = (event as { params?: Record<string, unknown> }).params?.mode;\n if (isMode(mode)) this.#setMode(mode);\n }\n\n /** Toggles light↔dark for the 2-value single-button contract. */\n toggle(): void {\n this.#setMode(this.#resolved() === \"dark\" ? \"light\" : \"dark\");\n }\n\n /** Central mode change: persist, apply to the root, resync controls, announce. */\n #setMode(mode: ThemeMode): void {\n this.modeValue = mode;\n this.#writeStored(mode);\n this.#applyTheme();\n this.#syncControls();\n this.#dispatchChange();\n }\n\n /** Writes `data-theme` + `color-scheme` (the resolved theme) onto the target. */\n #applyTheme(): void {\n const root = this.#targetElement();\n if (!root) return;\n const resolved = this.#resolved();\n root.setAttribute(\"data-theme\", resolved);\n root.style.setProperty(\"color-scheme\", resolved);\n }\n\n /** Keeps the radiogroup (aria-checked + roving tabindex) or toggle (aria-pressed) in sync. */\n #syncControls(): void {\n const options = this.optionTargets;\n if (options.length > 0) {\n let hasTabbable = false;\n for (const option of options) {\n const selected = this.#optionMode(option) === this.modeValue;\n option.setAttribute(\"aria-checked\", String(selected));\n option.tabIndex = selected ? 0 : -1;\n hasTabbable ||= selected;\n }\n // APG: a radiogroup with no selection keeps its first radio tabbable.\n const first = options[0];\n if (!hasTabbable && first) first.tabIndex = 0;\n return;\n }\n this.element.setAttribute(\"aria-pressed\", String(this.#resolved() === \"dark\"));\n }\n\n /** Emits `change` with the selected mode and the resolved theme. */\n #dispatchChange(): void {\n this.dispatch(\"change\", { detail: { mode: this.modeValue, resolved: this.#resolved() } });\n }\n\n /** The effective theme: the OS preference when `system`, else the mode itself. */\n #resolved(): ResolvedTheme {\n if (this.modeValue === \"dark\") return \"dark\";\n if (this.modeValue === \"light\") return \"light\";\n return this.#media?.matches ? \"dark\" : \"light\";\n }\n\n /** Reads an option's mode from its action param attribute. */\n #optionMode(option: HTMLElement): ThemeMode {\n const mode = option.getAttribute(\"data-stimeo--theme-mode-param\");\n return isMode(mode) ? mode : \"system\";\n }\n\n /** Resolves the state-hook target (`<html>` by default). */\n #targetElement(): HTMLElement | null {\n if (this.targetValue === \"html\" || this.targetValue === \":root\")\n return document.documentElement;\n return document.querySelector<HTMLElement>(this.targetValue);\n }\n\n /** Reads a persisted, validated mode from `localStorage` (null when absent/blocked). */\n #readStored(): ThemeMode | null {\n const value = readLocalStorage(this.storageKeyValue);\n return isMode(value) ? value : null;\n }\n\n /** Persists the mode, swallowing storage errors (private mode / quota). */\n #writeStored(mode: ThemeMode): void {\n writeLocalStorage(this.storageKeyValue, mode);\n }\n}\n"]}
@@ -0,0 +1,67 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ /**
4
+ * Headless, accessible **time picker** behavior. Each segment (hour, minute,
5
+ * optional second, optional AM/PM) is an APG **Spinbutton**; the controller
6
+ * composes them into an `HH:MM[:SS]` value mirrored to a hidden field.
7
+ *
8
+ * Markup contract (identifier: `stimeo--time-picker`):
9
+ * <div data-controller="stimeo--time-picker"
10
+ * data-stimeo--time-picker-hour-cycle-value="24"
11
+ * role="group" aria-label="Time">
12
+ * <span role="spinbutton" aria-label="Hours" tabindex="0"
13
+ * aria-valuenow="9" aria-valuemin="0" aria-valuemax="23" aria-valuetext="09"
14
+ * data-segment="hour" data-stimeo--time-picker-target="segment"
15
+ * data-action="keydown->stimeo--time-picker#onKeydown">09</span>
16
+ * <span aria-hidden="true">:</span>
17
+ * <span role="spinbutton" aria-label="Minutes" tabindex="0"
18
+ * data-segment="minute" data-stimeo--time-picker-target="segment"
19
+ * data-action="keydown->stimeo--time-picker#onKeydown">30</span>
20
+ * <input type="hidden" data-stimeo--time-picker-target="field" />
21
+ * </div>
22
+ *
23
+ * @remarks
24
+ * Behavior only — no styling, no locale formatting. Every segment is its own Tab
25
+ * stop (multi-tabstop, *not* roving); `ArrowLeft`/`ArrowRight` are an auxiliary
26
+ * move between segments. `ArrowUp`/`ArrowDown` step the focused segment, wrapping
27
+ * (and, when `wrap` is set, carrying minutes→hours and seconds→minutes). Typing
28
+ * digits enters a value directly and advances after two digits. AM/PM is modeled
29
+ * as a `meridiem` spinbutton that toggles; the hidden field is always 24-hour.
30
+ */
31
+ declare class TimePickerController extends Controller<HTMLElement> {
32
+ #private;
33
+ static targets: string[];
34
+ static values: {
35
+ hourCycle: {
36
+ type: NumberConstructor;
37
+ default: number;
38
+ };
39
+ step: {
40
+ type: NumberConstructor;
41
+ default: number;
42
+ };
43
+ seconds: {
44
+ type: BooleanConstructor;
45
+ default: boolean;
46
+ };
47
+ wrap: {
48
+ type: BooleanConstructor;
49
+ default: boolean;
50
+ };
51
+ };
52
+ static actions: readonly ["onKeydown"];
53
+ static events: readonly ["change"];
54
+ readonly segmentTargets: HTMLElement[];
55
+ readonly fieldTarget: HTMLInputElement;
56
+ readonly hasFieldTarget: boolean;
57
+ hourCycleValue: number;
58
+ stepValue: number;
59
+ secondsValue: boolean;
60
+ wrapValue: boolean;
61
+ /** Seeds each segment from its initial `aria-valuenow` and syncs the field. */
62
+ connect(): void;
63
+ /** Handles stepping, inter-segment focus moves, jumps, and direct entry. */
64
+ onKeydown(event: KeyboardEvent): void;
65
+ }
66
+
67
+ export { TimePickerController };
@@ -0,0 +1,212 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ // src/controllers/time_picker_controller.ts
4
+ var AM = 0;
5
+ var PM = 1;
6
+ var TimePickerController = class extends Controller {
7
+ static targets = ["segment", "field"];
8
+ static values = {
9
+ hourCycle: { type: Number, default: 24 },
10
+ step: { type: Number, default: 1 },
11
+ seconds: { type: Boolean, default: false },
12
+ wrap: { type: Boolean, default: true }
13
+ };
14
+ static actions = ["onKeydown"];
15
+ static events = ["change"];
16
+ /** Current numeric value per segment kind (hours are the *displayed* hours). */
17
+ #state = { hour: 0, minute: 0, second: 0, meridiem: AM };
18
+ /** Direct-entry digit buffer and the segment it belongs to. */
19
+ #typeBuffer = "";
20
+ #typeSegment = null;
21
+ /** Last composed field value, to suppress duplicate `change` dispatches. */
22
+ #lastValue = "";
23
+ /** Seeds each segment from its initial `aria-valuenow` and syncs the field. */
24
+ connect() {
25
+ for (const segment of this.segmentTargets) {
26
+ const kind = this.#kindOf(segment);
27
+ if (!kind) continue;
28
+ const now = Number(segment.getAttribute("aria-valuenow"));
29
+ const { min, max } = this.#bounds(kind);
30
+ this.#state[kind] = Number.isFinite(now) ? Math.min(max, Math.max(min, now)) : min;
31
+ }
32
+ for (const segment of this.segmentTargets) this.#renderSegment(segment);
33
+ this.#syncField(false);
34
+ }
35
+ /** Handles stepping, inter-segment focus moves, jumps, and direct entry. */
36
+ onKeydown(event) {
37
+ const segment = event.target?.closest(
38
+ "[data-stimeo--time-picker-target='segment']"
39
+ );
40
+ const kind = segment ? this.#kindOf(segment) : null;
41
+ if (!segment || !kind) return;
42
+ switch (event.key) {
43
+ case "ArrowUp":
44
+ event.preventDefault();
45
+ this.#step(kind, this.#delta(kind));
46
+ break;
47
+ case "ArrowDown":
48
+ event.preventDefault();
49
+ this.#step(kind, -this.#delta(kind));
50
+ break;
51
+ case "ArrowLeft":
52
+ event.preventDefault();
53
+ this.#focusSibling(segment, -1);
54
+ break;
55
+ case "ArrowRight":
56
+ event.preventDefault();
57
+ this.#focusSibling(segment, 1);
58
+ break;
59
+ case "Home":
60
+ event.preventDefault();
61
+ this.#set(kind, this.#bounds(kind).min);
62
+ break;
63
+ case "End":
64
+ event.preventDefault();
65
+ this.#set(kind, this.#bounds(kind).max);
66
+ break;
67
+ default:
68
+ if (/^[0-9]$/.test(event.key)) {
69
+ event.preventDefault();
70
+ this.#typeDigit(segment, kind, event.key);
71
+ }
72
+ return;
73
+ }
74
+ this.#typeBuffer = "";
75
+ this.#typeSegment = null;
76
+ }
77
+ /** The per-step delta: minutes step by `step`, others by 1, meridiem toggles. */
78
+ #delta(kind) {
79
+ return kind === "minute" ? this.stepValue : 1;
80
+ }
81
+ /** Steps a segment, wrapping at its bounds and carrying over when enabled. */
82
+ #step(kind, delta) {
83
+ if (kind === "meridiem") {
84
+ this.#set("meridiem", this.#state.meridiem === AM ? PM : AM);
85
+ return;
86
+ }
87
+ const { min, max } = this.#bounds(kind);
88
+ const span = max - min + 1;
89
+ const raw = this.#state[kind] + delta;
90
+ if (raw > max || raw < min) {
91
+ if (!this.wrapValue) {
92
+ this.#set(kind, Math.min(max, Math.max(min, raw)));
93
+ return;
94
+ }
95
+ const wrapped = ((raw - min) % span + span) % span + min;
96
+ const carry = Math.floor((raw - min) / span);
97
+ this.#set(kind, wrapped);
98
+ this.#carry(kind, carry);
99
+ return;
100
+ }
101
+ this.#set(kind, raw);
102
+ }
103
+ /** Propagates a wrap carry from `kind` into the next larger segment. */
104
+ #carry(kind, amount) {
105
+ if (amount === 0) return;
106
+ if (kind === "second") this.#step("minute", amount);
107
+ else if (kind === "minute") this.#step("hour", amount);
108
+ }
109
+ /**
110
+ * Sets a segment's value (clamped to its `[min, max]` bounds), re-renders it,
111
+ * and resyncs the composed field. Clamping here guards the direct-entry path:
112
+ * typing `0` into a 12-hour hour (min 1) must not commit an out-of-range
113
+ * `aria-valuenow="0"`. The stepping path already passes in-bounds values, so
114
+ * the clamp is a no-op there.
115
+ */
116
+ #set(kind, value) {
117
+ const { min, max } = this.#bounds(kind);
118
+ this.#state[kind] = Math.min(max, Math.max(min, value));
119
+ const segment = this.segmentTargets.find((s) => this.#kindOf(s) === kind);
120
+ if (segment) this.#renderSegment(segment);
121
+ this.#syncField(true);
122
+ }
123
+ /** Accumulates a typed digit, committing and advancing after two digits. */
124
+ #typeDigit(segment, kind, digit) {
125
+ if (kind === "meridiem") return;
126
+ if (this.#typeSegment !== kind) this.#typeBuffer = "";
127
+ this.#typeSegment = kind;
128
+ const { max } = this.#bounds(kind);
129
+ const candidate = Number(`${this.#typeBuffer}${digit}`);
130
+ if (candidate <= max) this.#typeBuffer = `${this.#typeBuffer}${digit}`;
131
+ else this.#typeBuffer = digit;
132
+ this.#set(kind, Number(this.#typeBuffer));
133
+ if (this.#typeBuffer.length >= 2 || Number(this.#typeBuffer) * 10 > max) {
134
+ this.#typeBuffer = "";
135
+ this.#typeSegment = null;
136
+ this.#focusSibling(segment, 1);
137
+ }
138
+ }
139
+ /** Moves focus to the previous/next segment, if one exists. */
140
+ #focusSibling(segment, direction) {
141
+ const index = this.segmentTargets.indexOf(segment);
142
+ const next = this.segmentTargets[index + direction];
143
+ next?.focus();
144
+ }
145
+ /** Reflects a segment's current value onto its ARIA/text representation. */
146
+ #renderSegment(segment) {
147
+ const kind = this.#kindOf(segment);
148
+ if (!kind) return;
149
+ const value = this.#state[kind];
150
+ if (kind === "meridiem") {
151
+ const text2 = value === PM ? "PM" : "AM";
152
+ segment.setAttribute("aria-valuenow", String(value));
153
+ segment.setAttribute("aria-valuetext", text2);
154
+ segment.setAttribute("aria-valuemin", String(AM));
155
+ segment.setAttribute("aria-valuemax", String(PM));
156
+ segment.textContent = text2;
157
+ return;
158
+ }
159
+ const { min, max } = this.#bounds(kind);
160
+ const text = String(value).padStart(2, "0");
161
+ segment.setAttribute("aria-valuenow", String(value));
162
+ segment.setAttribute("aria-valuetext", text);
163
+ segment.setAttribute("aria-valuemin", String(min));
164
+ segment.setAttribute("aria-valuemax", String(max));
165
+ segment.textContent = text;
166
+ }
167
+ /** Composes `HH:MM[:SS]` (24-hour) into the hidden field; dispatches `change`. */
168
+ #syncField(notify) {
169
+ const h24 = this.#hours24();
170
+ const parts = [pad(h24), pad(this.#state.minute)];
171
+ if (this.secondsValue) parts.push(pad(this.#state.second));
172
+ const value = parts.join(":");
173
+ if (this.hasFieldTarget && this.fieldTarget.value !== value) {
174
+ this.fieldTarget.value = value;
175
+ }
176
+ if (notify && value !== this.#lastValue) this.dispatch("change", { detail: { value } });
177
+ this.#lastValue = value;
178
+ }
179
+ /** Converts the displayed hour (+ meridiem in 12-hour mode) to 24-hour. */
180
+ #hours24() {
181
+ if (this.hourCycleValue !== 12) return this.#state.hour;
182
+ const base = this.#state.hour % 12;
183
+ return base + (this.#state.meridiem === PM ? 12 : 0);
184
+ }
185
+ /** The inclusive `[min, max]` bounds for a segment kind. */
186
+ #bounds(kind) {
187
+ switch (kind) {
188
+ case "hour":
189
+ return this.hourCycleValue === 12 ? { min: 1, max: 12 } : { min: 0, max: 23 };
190
+ case "minute":
191
+ case "second":
192
+ return { min: 0, max: 59 };
193
+ case "meridiem":
194
+ return { min: AM, max: PM };
195
+ }
196
+ }
197
+ /** Reads a segment's declared kind, or null when absent/invalid. */
198
+ #kindOf(segment) {
199
+ const kind = segment.getAttribute("data-segment");
200
+ if (kind === "hour" || kind === "minute" || kind === "second" || kind === "meridiem") {
201
+ return kind;
202
+ }
203
+ return null;
204
+ }
205
+ };
206
+ function pad(value) {
207
+ return String(value).padStart(2, "0");
208
+ }
209
+
210
+ export { TimePickerController };
211
+ //# sourceMappingURL=time_picker_controller.js.map
212
+ //# sourceMappingURL=time_picker_controller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/controllers/time_picker_controller.ts"],"names":["text"],"mappings":"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","file":"time_picker_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\n/** A time segment kind, as declared by `data-segment` on each spinbutton. */\ntype SegmentKind = \"hour\" | \"minute\" | \"second\" | \"meridiem\";\n\n/** Meridiem encoding: 0 = AM, 1 = PM. */\nconst AM = 0;\nconst PM = 1;\n\n/**\n * Headless, accessible **time picker** behavior. Each segment (hour, minute,\n * optional second, optional AM/PM) is an APG **Spinbutton**; the controller\n * composes them into an `HH:MM[:SS]` value mirrored to a hidden field.\n *\n * Markup contract (identifier: `stimeo--time-picker`):\n * <div data-controller=\"stimeo--time-picker\"\n * data-stimeo--time-picker-hour-cycle-value=\"24\"\n * role=\"group\" aria-label=\"Time\">\n * <span role=\"spinbutton\" aria-label=\"Hours\" tabindex=\"0\"\n * aria-valuenow=\"9\" aria-valuemin=\"0\" aria-valuemax=\"23\" aria-valuetext=\"09\"\n * data-segment=\"hour\" data-stimeo--time-picker-target=\"segment\"\n * data-action=\"keydown->stimeo--time-picker#onKeydown\">09</span>\n * <span aria-hidden=\"true\">:</span>\n * <span role=\"spinbutton\" aria-label=\"Minutes\" tabindex=\"0\"\n * data-segment=\"minute\" data-stimeo--time-picker-target=\"segment\"\n * data-action=\"keydown->stimeo--time-picker#onKeydown\">30</span>\n * <input type=\"hidden\" data-stimeo--time-picker-target=\"field\" />\n * </div>\n *\n * @remarks\n * Behavior only — no styling, no locale formatting. Every segment is its own Tab\n * stop (multi-tabstop, *not* roving); `ArrowLeft`/`ArrowRight` are an auxiliary\n * move between segments. `ArrowUp`/`ArrowDown` step the focused segment, wrapping\n * (and, when `wrap` is set, carrying minutes→hours and seconds→minutes). Typing\n * digits enters a value directly and advances after two digits. AM/PM is modeled\n * as a `meridiem` spinbutton that toggles; the hidden field is always 24-hour.\n */\nexport class TimePickerController extends Controller<HTMLElement> {\n static override targets = [\"segment\", \"field\"];\n static override values = {\n hourCycle: { type: Number, default: 24 },\n step: { type: Number, default: 1 },\n seconds: { type: Boolean, default: false },\n wrap: { type: Boolean, default: true },\n };\n static actions = [\"onKeydown\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly segmentTargets: HTMLElement[];\n declare readonly fieldTarget: HTMLInputElement;\n declare readonly hasFieldTarget: boolean;\n declare hourCycleValue: number;\n declare stepValue: number;\n declare secondsValue: boolean;\n declare wrapValue: boolean;\n\n /** Current numeric value per segment kind (hours are the *displayed* hours). */\n #state: Record<SegmentKind, number> = { hour: 0, minute: 0, second: 0, meridiem: AM };\n /** Direct-entry digit buffer and the segment it belongs to. */\n #typeBuffer = \"\";\n #typeSegment: SegmentKind | null = null;\n /** Last composed field value, to suppress duplicate `change` dispatches. */\n #lastValue = \"\";\n\n /** Seeds each segment from its initial `aria-valuenow` and syncs the field. */\n override connect(): void {\n for (const segment of this.segmentTargets) {\n const kind = this.#kindOf(segment);\n if (!kind) continue;\n const now = Number(segment.getAttribute(\"aria-valuenow\"));\n const { min, max } = this.#bounds(kind);\n // Clamp the seeded value so malformed markup (out-of-range aria-valuenow)\n // never propagates into the rendered state or the composed field.\n this.#state[kind] = Number.isFinite(now) ? Math.min(max, Math.max(min, now)) : min;\n }\n for (const segment of this.segmentTargets) this.#renderSegment(segment);\n this.#syncField(false);\n }\n\n /** Handles stepping, inter-segment focus moves, jumps, and direct entry. */\n onKeydown(event: KeyboardEvent): void {\n const segment = (event.target as HTMLElement | null)?.closest<HTMLElement>(\n \"[data-stimeo--time-picker-target='segment']\",\n );\n const kind = segment ? this.#kindOf(segment) : null;\n if (!segment || !kind) return;\n\n switch (event.key) {\n case \"ArrowUp\":\n event.preventDefault();\n this.#step(kind, this.#delta(kind));\n break;\n case \"ArrowDown\":\n event.preventDefault();\n this.#step(kind, -this.#delta(kind));\n break;\n case \"ArrowLeft\":\n event.preventDefault();\n this.#focusSibling(segment, -1);\n break;\n case \"ArrowRight\":\n event.preventDefault();\n this.#focusSibling(segment, 1);\n break;\n case \"Home\":\n event.preventDefault();\n this.#set(kind, this.#bounds(kind).min);\n break;\n case \"End\":\n event.preventDefault();\n this.#set(kind, this.#bounds(kind).max);\n break;\n default:\n if (/^[0-9]$/.test(event.key)) {\n event.preventDefault();\n this.#typeDigit(segment, kind, event.key);\n }\n return;\n }\n // Any non-digit action ends the current direct-entry sequence.\n this.#typeBuffer = \"\";\n this.#typeSegment = null;\n }\n\n /** The per-step delta: minutes step by `step`, others by 1, meridiem toggles. */\n #delta(kind: SegmentKind): number {\n return kind === \"minute\" ? this.stepValue : 1;\n }\n\n /** Steps a segment, wrapping at its bounds and carrying over when enabled. */\n #step(kind: SegmentKind, delta: number): void {\n if (kind === \"meridiem\") {\n this.#set(\"meridiem\", this.#state.meridiem === AM ? PM : AM);\n return;\n }\n const { min, max } = this.#bounds(kind);\n const span = max - min + 1;\n const raw = this.#state[kind] + delta;\n\n if (raw > max || raw < min) {\n if (!this.wrapValue) {\n this.#set(kind, Math.min(max, Math.max(min, raw)));\n return;\n }\n // Wrap within the segment, carrying the overflow into the larger unit.\n const wrapped = ((((raw - min) % span) + span) % span) + min;\n const carry = Math.floor((raw - min) / span);\n this.#set(kind, wrapped);\n this.#carry(kind, carry);\n return;\n }\n this.#set(kind, raw);\n }\n\n /** Propagates a wrap carry from `kind` into the next larger segment. */\n #carry(kind: SegmentKind, amount: number): void {\n if (amount === 0) return;\n if (kind === \"second\") this.#step(\"minute\", amount);\n else if (kind === \"minute\") this.#step(\"hour\", amount);\n // Hours wrap on their own (no day rollover); meridiem has no carry.\n }\n\n /**\n * Sets a segment's value (clamped to its `[min, max]` bounds), re-renders it,\n * and resyncs the composed field. Clamping here guards the direct-entry path:\n * typing `0` into a 12-hour hour (min 1) must not commit an out-of-range\n * `aria-valuenow=\"0\"`. The stepping path already passes in-bounds values, so\n * the clamp is a no-op there.\n */\n #set(kind: SegmentKind, value: number): void {\n const { min, max } = this.#bounds(kind);\n this.#state[kind] = Math.min(max, Math.max(min, value));\n const segment = this.segmentTargets.find((s) => this.#kindOf(s) === kind);\n if (segment) this.#renderSegment(segment);\n this.#syncField(true);\n }\n\n /** Accumulates a typed digit, committing and advancing after two digits. */\n #typeDigit(segment: HTMLElement, kind: SegmentKind, digit: string): void {\n if (kind === \"meridiem\") return;\n if (this.#typeSegment !== kind) this.#typeBuffer = \"\";\n this.#typeSegment = kind;\n\n const { max } = this.#bounds(kind);\n const candidate = Number(`${this.#typeBuffer}${digit}`);\n if (candidate <= max) this.#typeBuffer = `${this.#typeBuffer}${digit}`;\n else this.#typeBuffer = digit; // restart from this digit when it overflows\n\n this.#set(kind, Number(this.#typeBuffer));\n\n // Two digits (or a value that can't grow further) completes the segment.\n if (this.#typeBuffer.length >= 2 || Number(this.#typeBuffer) * 10 > max) {\n this.#typeBuffer = \"\";\n this.#typeSegment = null;\n this.#focusSibling(segment, 1);\n }\n }\n\n /** Moves focus to the previous/next segment, if one exists. */\n #focusSibling(segment: HTMLElement, direction: 1 | -1): void {\n const index = this.segmentTargets.indexOf(segment);\n const next = this.segmentTargets[index + direction];\n next?.focus();\n }\n\n /** Reflects a segment's current value onto its ARIA/text representation. */\n #renderSegment(segment: HTMLElement): void {\n const kind = this.#kindOf(segment);\n if (!kind) return;\n const value = this.#state[kind];\n\n if (kind === \"meridiem\") {\n const text = value === PM ? \"PM\" : \"AM\";\n segment.setAttribute(\"aria-valuenow\", String(value));\n segment.setAttribute(\"aria-valuetext\", text);\n segment.setAttribute(\"aria-valuemin\", String(AM));\n segment.setAttribute(\"aria-valuemax\", String(PM));\n segment.textContent = text;\n return;\n }\n\n const { min, max } = this.#bounds(kind);\n const text = String(value).padStart(2, \"0\");\n segment.setAttribute(\"aria-valuenow\", String(value));\n segment.setAttribute(\"aria-valuetext\", text);\n segment.setAttribute(\"aria-valuemin\", String(min));\n segment.setAttribute(\"aria-valuemax\", String(max));\n segment.textContent = text;\n }\n\n /** Composes `HH:MM[:SS]` (24-hour) into the hidden field; dispatches `change`. */\n #syncField(notify: boolean): void {\n const h24 = this.#hours24();\n const parts = [pad(h24), pad(this.#state.minute)];\n if (this.secondsValue) parts.push(pad(this.#state.second));\n const value = parts.join(\":\");\n\n if (this.hasFieldTarget && this.fieldTarget.value !== value) {\n this.fieldTarget.value = value;\n }\n // Dispatch only when the composed value actually changed, so a no-op step\n // (e.g. Home at the minimum) does not emit a redundant event.\n if (notify && value !== this.#lastValue) this.dispatch(\"change\", { detail: { value } });\n this.#lastValue = value;\n }\n\n /** Converts the displayed hour (+ meridiem in 12-hour mode) to 24-hour. */\n #hours24(): number {\n if (this.hourCycleValue !== 12) return this.#state.hour;\n const base = this.#state.hour % 12; // 12 → 0\n return base + (this.#state.meridiem === PM ? 12 : 0);\n }\n\n /** The inclusive `[min, max]` bounds for a segment kind. */\n #bounds(kind: SegmentKind): { min: number; max: number } {\n switch (kind) {\n case \"hour\":\n return this.hourCycleValue === 12 ? { min: 1, max: 12 } : { min: 0, max: 23 };\n case \"minute\":\n case \"second\":\n return { min: 0, max: 59 };\n case \"meridiem\":\n return { min: AM, max: PM };\n }\n }\n\n /** Reads a segment's declared kind, or null when absent/invalid. */\n #kindOf(segment: HTMLElement): SegmentKind | null {\n const kind = segment.getAttribute(\"data-segment\");\n if (kind === \"hour\" || kind === \"minute\" || kind === \"second\" || kind === \"meridiem\") {\n return kind;\n }\n return null;\n }\n}\n\n/** Zero-pads a number to two digits. */\nfunction pad(value: number): string {\n return String(value).padStart(2, \"0\");\n}\n"]}
@@ -109,13 +109,5 @@ declare class ToastController extends Controller<HTMLElement> {
109
109
  */
110
110
  enforceMaxLimit(): void;
111
111
  }
112
- /**
113
- * Converts the first value of a CSS `transition-duration` string to milliseconds.
114
- * Browsers normalize computed `<time>` values to seconds (e.g. `0.2s`), but parse
115
- * `ms` defensively so a consumer's `ms` transition is not delayed 1000x.
116
- *
117
- * Pure (no `this`); exported so it can be unit-tested directly.
118
- */
119
- declare function cssTimeToMs(value: string): number;
120
112
 
121
- export { ToastController, cssTimeToMs };
113
+ export { ToastController };
@@ -55,6 +55,40 @@ var SafeTimeout = class extends TimerRegistry {
55
55
  }
56
56
  };
57
57
 
58
+ // src/utils/transition_completion.ts
59
+ function timeMs(value) {
60
+ const trimmed = value.trim();
61
+ const amount = Number.parseFloat(trimmed);
62
+ if (!Number.isFinite(amount)) return 0;
63
+ if (trimmed.endsWith("ms")) return amount;
64
+ if (trimmed.endsWith("s")) return amount * 1e3;
65
+ return 0;
66
+ }
67
+ function cssList(value) {
68
+ return value.split(",").map((item) => item.trim()).filter(Boolean);
69
+ }
70
+ function transitionTimings(style) {
71
+ const properties = cssList(style.transitionProperty);
72
+ const durations = cssList(style.transitionDuration).map(timeMs);
73
+ const delays = cssList(style.transitionDelay).map(timeMs);
74
+ const effectiveProperties = properties.length > 0 ? properties : Array.from({ length: Math.max(durations.length, delays.length, 1) }, () => "all");
75
+ const effectiveDurations = durations.length > 0 ? durations : [0];
76
+ const effectiveDelays = delays.length > 0 ? delays : [0];
77
+ return effectiveProperties.filter((property) => property !== "none").map((property, index) => ({
78
+ property,
79
+ totalMs: Math.max(
80
+ 0,
81
+ (effectiveDurations[index % effectiveDurations.length] ?? 0) + (effectiveDelays[index % effectiveDelays.length] ?? 0)
82
+ )
83
+ }));
84
+ }
85
+ function maxTotalMs(timings) {
86
+ return timings.reduce((max, { totalMs }) => Math.max(max, totalMs), 0);
87
+ }
88
+ function maxTransitionTotalMs(style) {
89
+ return maxTotalMs(transitionTimings(style));
90
+ }
91
+
58
92
  // src/controllers/toast_controller.ts
59
93
  var DELEGATED_EVENTS = ["click", "focusin", "focusout", "keydown", "mouseover", "mouseout"];
60
94
  var ToastController = class extends Controller {
@@ -304,8 +338,7 @@ var ToastController = class extends Controller {
304
338
  this.listTarget.removeChild(element);
305
339
  this.dispatch("dismiss", { detail: { item: element, reason } });
306
340
  };
307
- const transitions = window.getComputedStyle(element).transitionDuration;
308
- const duration = cssTimeToMs(transitions);
341
+ const duration = maxTransitionTotalMs(window.getComputedStyle(element));
309
342
  if (duration > 0) {
310
343
  this.#timers.set(finalize, duration);
311
344
  } else {
@@ -377,13 +410,7 @@ var ToastController = class extends Controller {
377
410
  this.#rafHandles.delete(element);
378
411
  }
379
412
  };
380
- function cssTimeToMs(value) {
381
- const first = value.split(",")[0]?.trim() ?? "";
382
- const amount = Number.parseFloat(first);
383
- if (Number.isNaN(amount)) return 0;
384
- return first.endsWith("ms") ? amount : amount * 1e3;
385
- }
386
413
 
387
- export { ToastController, cssTimeToMs };
414
+ export { ToastController };
388
415
  //# sourceMappingURL=toast_controller.js.map
389
416
  //# sourceMappingURL=toast_controller.js.map