stimeo-ui 0.12.0 → 0.14.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 (183) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/dist/cable/index.d.ts +68 -14
  3. package/dist/cable/index.js +438 -152
  4. package/dist/cable/index.js.map +1 -1
  5. package/dist/controllers/accordion_controller.d.ts +1 -1
  6. package/dist/controllers/accordion_controller.js.map +1 -1
  7. package/dist/controllers/alert_dialog_controller.d.ts +1 -1
  8. package/dist/controllers/alert_dialog_controller.js.map +1 -1
  9. package/dist/controllers/announcer_controller.d.ts +1 -1
  10. package/dist/controllers/announcer_controller.js.map +1 -1
  11. package/dist/controllers/auto_submit_controller.js.map +1 -1
  12. package/dist/controllers/breadcrumb_controller.d.ts +1 -1
  13. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  14. package/dist/controllers/calendar_controller.d.ts +4 -3
  15. package/dist/controllers/calendar_controller.js +34 -2
  16. package/dist/controllers/calendar_controller.js.map +1 -1
  17. package/dist/controllers/carousel_controller.js +9 -7
  18. package/dist/controllers/carousel_controller.js.map +1 -1
  19. package/dist/controllers/character_counter_controller.js.map +1 -1
  20. package/dist/controllers/clipboard_controller.d.ts +2 -2
  21. package/dist/controllers/clipboard_controller.js.map +1 -1
  22. package/dist/controllers/collapsible_controller.d.ts +1 -1
  23. package/dist/controllers/collapsible_controller.js.map +1 -1
  24. package/dist/controllers/color_picker_controller.d.ts +1 -1
  25. package/dist/controllers/color_picker_controller.js.map +1 -1
  26. package/dist/controllers/command_palette_controller.d.ts +3 -3
  27. package/dist/controllers/command_palette_controller.js +2 -2
  28. package/dist/controllers/command_palette_controller.js.map +1 -1
  29. package/dist/controllers/conditional_fields_controller.js +7 -1
  30. package/dist/controllers/conditional_fields_controller.js.map +1 -1
  31. package/dist/controllers/confirm_controller.d.ts +2 -2
  32. package/dist/controllers/confirm_controller.js.map +1 -1
  33. package/dist/controllers/context_menu_controller.d.ts +1 -1
  34. package/dist/controllers/context_menu_controller.js.map +1 -1
  35. package/dist/controllers/countdown_controller.d.ts +1 -1
  36. package/dist/controllers/countdown_controller.js.map +1 -1
  37. package/dist/controllers/currency_input_controller.js +68 -23
  38. package/dist/controllers/currency_input_controller.js.map +1 -1
  39. package/dist/controllers/data_grid_controller.d.ts +1 -1
  40. package/dist/controllers/data_grid_controller.js.map +1 -1
  41. package/dist/controllers/date_range_picker_controller.d.ts +3 -3
  42. package/dist/controllers/date_range_picker_controller.js +34 -10
  43. package/dist/controllers/date_range_picker_controller.js.map +1 -1
  44. package/dist/controllers/dialog_controller.d.ts +3 -3
  45. package/dist/controllers/dialog_controller.js.map +1 -1
  46. package/dist/controllers/direct_upload_controller.js +21 -10
  47. package/dist/controllers/direct_upload_controller.js.map +1 -1
  48. package/dist/controllers/dirty_form_controller.js.map +1 -1
  49. package/dist/controllers/dismissible_controller.d.ts +1 -1
  50. package/dist/controllers/dismissible_controller.js.map +1 -1
  51. package/dist/controllers/drawer_controller.d.ts +2 -2
  52. package/dist/controllers/drawer_controller.js +2 -2
  53. package/dist/controllers/drawer_controller.js.map +1 -1
  54. package/dist/controllers/dropdown_controller.d.ts +2 -2
  55. package/dist/controllers/dropdown_controller.js.map +1 -1
  56. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  57. package/dist/controllers/flash_controller.js.map +1 -1
  58. package/dist/controllers/focus_controller.d.ts +2 -2
  59. package/dist/controllers/focus_controller.js.map +1 -1
  60. package/dist/controllers/form_field_controller.js +7 -1
  61. package/dist/controllers/form_field_controller.js.map +1 -1
  62. package/dist/controllers/form_validation_controller.js.map +1 -1
  63. package/dist/controllers/frame_loading_controller.d.ts +3 -3
  64. package/dist/controllers/frame_loading_controller.js.map +1 -1
  65. package/dist/controllers/highlight_controller.js.map +1 -1
  66. package/dist/controllers/hover_card_controller.d.ts +1 -1
  67. package/dist/controllers/hover_card_controller.js.map +1 -1
  68. package/dist/controllers/idle_controller.d.ts +1 -1
  69. package/dist/controllers/idle_controller.js.map +1 -1
  70. package/dist/controllers/input_mask_controller.d.ts +3 -2
  71. package/dist/controllers/input_mask_controller.js +33 -12
  72. package/dist/controllers/input_mask_controller.js.map +1 -1
  73. package/dist/controllers/intersection_controller.js +10 -5
  74. package/dist/controllers/intersection_controller.js.map +1 -1
  75. package/dist/controllers/lazy_frame_controller.js +10 -5
  76. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  77. package/dist/controllers/listbox_controller.js.map +1 -1
  78. package/dist/controllers/local_time_controller.d.ts +4 -3
  79. package/dist/controllers/local_time_controller.js +34 -7
  80. package/dist/controllers/local_time_controller.js.map +1 -1
  81. package/dist/controllers/masonry_controller.d.ts +2 -2
  82. package/dist/controllers/masonry_controller.js +1 -1
  83. package/dist/controllers/masonry_controller.js.map +1 -1
  84. package/dist/controllers/menu_controller.d.ts +1 -1
  85. package/dist/controllers/menu_controller.js +10 -2
  86. package/dist/controllers/menu_controller.js.map +1 -1
  87. package/dist/controllers/menubar_controller.d.ts +2 -2
  88. package/dist/controllers/menubar_controller.js +10 -2
  89. package/dist/controllers/menubar_controller.js.map +1 -1
  90. package/dist/controllers/navigation_menu_controller.d.ts +1 -1
  91. package/dist/controllers/navigation_menu_controller.js +8 -2
  92. package/dist/controllers/navigation_menu_controller.js.map +1 -1
  93. package/dist/controllers/nested_form_controller.js.map +1 -1
  94. package/dist/controllers/network_status_controller.js.map +1 -1
  95. package/dist/controllers/number_input_controller.js.map +1 -1
  96. package/dist/controllers/optimistic_controller.d.ts +23 -10
  97. package/dist/controllers/optimistic_controller.js +141 -20
  98. package/dist/controllers/optimistic_controller.js.map +1 -1
  99. package/dist/controllers/otp_controller.js +23 -10
  100. package/dist/controllers/otp_controller.js.map +1 -1
  101. package/dist/controllers/overflow_indicator_controller.d.ts +1 -1
  102. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  103. package/dist/controllers/overflow_menu_controller.js +10 -1
  104. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  105. package/dist/controllers/pagination_controller.js.map +1 -1
  106. package/dist/controllers/password_reveal_controller.d.ts +1 -1
  107. package/dist/controllers/password_reveal_controller.js.map +1 -1
  108. package/dist/controllers/password_strength_controller.d.ts +2 -1
  109. package/dist/controllers/password_strength_controller.js.map +1 -1
  110. package/dist/controllers/persist_controller.js.map +1 -1
  111. package/dist/controllers/pointer_drag_controller.js +10 -3
  112. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  113. package/dist/controllers/popover_controller.d.ts +1 -1
  114. package/dist/controllers/popover_controller.js.map +1 -1
  115. package/dist/controllers/portal_controller.d.ts +2 -2
  116. package/dist/controllers/portal_controller.js +21 -10
  117. package/dist/controllers/portal_controller.js.map +1 -1
  118. package/dist/controllers/radio_group_controller.d.ts +1 -1
  119. package/dist/controllers/radio_group_controller.js +11 -3
  120. package/dist/controllers/radio_group_controller.js.map +1 -1
  121. package/dist/controllers/read_more_controller.d.ts +1 -1
  122. package/dist/controllers/read_more_controller.js.map +1 -1
  123. package/dist/controllers/relative_time_controller.d.ts +3 -3
  124. package/dist/controllers/relative_time_controller.js +31 -5
  125. package/dist/controllers/relative_time_controller.js.map +1 -1
  126. package/dist/controllers/reset_before_cache_controller.js +23 -10
  127. package/dist/controllers/reset_before_cache_controller.js.map +1 -1
  128. package/dist/controllers/resizable_controller.js +7 -2
  129. package/dist/controllers/resizable_controller.js.map +1 -1
  130. package/dist/controllers/roving_controller.d.ts +1 -1
  131. package/dist/controllers/roving_controller.js +7 -3
  132. package/dist/controllers/roving_controller.js.map +1 -1
  133. package/dist/controllers/scroll_area_controller.js.map +1 -1
  134. package/dist/controllers/scroll_visibility_controller.d.ts +1 -1
  135. package/dist/controllers/scroll_visibility_controller.js +22 -13
  136. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  137. package/dist/controllers/scrollspy_controller.d.ts +1 -1
  138. package/dist/controllers/scrollspy_controller.js +11 -6
  139. package/dist/controllers/scrollspy_controller.js.map +1 -1
  140. package/dist/controllers/sidebar_controller.d.ts +1 -1
  141. package/dist/controllers/sidebar_controller.js.map +1 -1
  142. package/dist/controllers/skeleton_controller.d.ts +2 -2
  143. package/dist/controllers/skeleton_controller.js.map +1 -1
  144. package/dist/controllers/smart_sticky_header_controller.js +23 -12
  145. package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
  146. package/dist/controllers/spinner_controller.d.ts +3 -3
  147. package/dist/controllers/spinner_controller.js.map +1 -1
  148. package/dist/controllers/sticky_observer_controller.js +10 -5
  149. package/dist/controllers/sticky_observer_controller.js.map +1 -1
  150. package/dist/controllers/submit_once_controller.d.ts +3 -3
  151. package/dist/controllers/submit_once_controller.js.map +1 -1
  152. package/dist/controllers/switch_controller.d.ts +1 -1
  153. package/dist/controllers/switch_controller.js.map +1 -1
  154. package/dist/controllers/tabs_controller.d.ts +1 -1
  155. package/dist/controllers/tabs_controller.js.map +1 -1
  156. package/dist/controllers/textarea_autosize_controller.d.ts +1 -1
  157. package/dist/controllers/textarea_autosize_controller.js.map +1 -1
  158. package/dist/controllers/theme_controller.d.ts +5 -5
  159. package/dist/controllers/theme_controller.js +23 -13
  160. package/dist/controllers/theme_controller.js.map +1 -1
  161. package/dist/controllers/toast_controller.js +1 -1
  162. package/dist/controllers/toast_controller.js.map +1 -1
  163. package/dist/controllers/toggle_group_controller.js +11 -3
  164. package/dist/controllers/toggle_group_controller.js.map +1 -1
  165. package/dist/controllers/toolbar_controller.d.ts +1 -1
  166. package/dist/controllers/toolbar_controller.js +7 -3
  167. package/dist/controllers/toolbar_controller.js.map +1 -1
  168. package/dist/controllers/tooltip_controller.d.ts +1 -1
  169. package/dist/controllers/tooltip_controller.js.map +1 -1
  170. package/dist/controllers/transition_controller.d.ts +1 -1
  171. package/dist/controllers/transition_controller.js.map +1 -1
  172. package/dist/controllers/tree_view_controller.d.ts +4 -3
  173. package/dist/controllers/tree_view_controller.js.map +1 -1
  174. package/dist/index.js +259 -202
  175. package/dist/index.js.map +1 -1
  176. package/dist/inspector/cli.d.ts +39 -2
  177. package/dist/inspector/cli.js +81 -8
  178. package/dist/inspector/cli.js.map +1 -1
  179. package/dist/inspector/cli_bin.js +104 -8
  180. package/dist/inspector/cli_bin.js.map +1 -1
  181. package/dist/inspector/examples.json +5 -5
  182. package/dist/inspector/manifest.json +132 -3
  183. package/package.json +9 -11
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/layout_observer.ts","../../src/controllers/textarea_autosize_controller.ts"],"names":[],"mappings":"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","file":"textarea_autosize_controller.js","sourcesContent":["/**\n * Unified element-size and viewport observation for Stimeo controllers.\n *\n * Widgets whose output is measured — an overflow boundary, a masonry column count,\n * an autosized textarea — need to react both to their *own* box changing — via {@link ResizeObserver} —\n * and to the *viewport* changing — via the `window` `resize` event. Wiring those\n * two sources by hand in every controller risks leaked listeners on\n * `disconnect()`. {@link LayoutObserver} owns both behind one callback and one\n * {@link LayoutObserver.disconnect | disconnect()} that releases everything.\n *\n * Behavior only: the helper reports *that* layout changed; it never reads or\n * writes styles. Consumers decide what to recompute.\n */\n\n/** Invoked whenever an observed element or the viewport changes size. */\nexport type LayoutCallback = () => void;\n\n/** Constructs a {@link ResizeObserver}; injectable so tests stay deterministic. */\nexport type ResizeObserverFactory = (callback: ResizeObserverCallback) => ResizeObserver;\n\n/** Options for {@link LayoutObserver}. */\nexport interface LayoutObserverOptions {\n /**\n * Factory for the {@link ResizeObserver} used by {@link LayoutObserver.observe}.\n * Defaults to the global constructor; override it in tests, or to no-op in\n * environments where `ResizeObserver` is unavailable.\n */\n resizeObserverFactory?: ResizeObserverFactory;\n}\n\n/**\n * Observes element resizes and/or viewport resizes through a single callback,\n * with guaranteed teardown.\n *\n * @example\n * ```ts\n * #layout = new LayoutObserver(() => this.#reposition());\n *\n * connect() {\n * this.#layout.observe(this.panelTarget);\n * this.#layout.observeViewport();\n * }\n *\n * disconnect() {\n * this.#layout.disconnect();\n * }\n * ```\n */\nexport class LayoutObserver {\n readonly #callback: LayoutCallback;\n readonly #resizeObserverFactory: ResizeObserverFactory | null;\n #resizeObserver: ResizeObserver | null = null;\n #observingViewport = false;\n\n /** Stable bound handler so add/removeEventListener target the same reference. */\n readonly #handleViewportResize = (): void => {\n this.#callback();\n };\n\n constructor(callback: LayoutCallback, options: LayoutObserverOptions = {}) {\n this.#callback = callback;\n this.#resizeObserverFactory =\n options.resizeObserverFactory ??\n (typeof ResizeObserver === \"undefined\" ? null : (cb) => new ResizeObserver(cb));\n }\n\n /**\n * Starts observing an element's size. Repeated calls observe additional\n * elements through the same shared observer. No-ops when no\n * `ResizeObserver` implementation is available.\n */\n observe(element: Element): void {\n if (!this.#resizeObserverFactory) return;\n if (!this.#resizeObserver) {\n this.#resizeObserver = this.#resizeObserverFactory(() => {\n this.#callback();\n });\n }\n this.#resizeObserver.observe(element);\n }\n\n /** Stops observing a single element while leaving any others in place. */\n unobserve(element: Element): void {\n this.#resizeObserver?.unobserve(element);\n }\n\n /** Starts observing viewport resizes. Idempotent: the listener is added once. */\n observeViewport(): void {\n if (this.#observingViewport) return;\n this.#observingViewport = true;\n window.addEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /** Stops observing viewport resizes without affecting element observation. */\n unobserveViewport(): void {\n if (!this.#observingViewport) return;\n this.#observingViewport = false;\n window.removeEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /**\n * Releases every observation: disconnects the {@link ResizeObserver} and\n * removes the viewport listener. Safe to call multiple times. Call this from a\n * controller's `disconnect()`.\n */\n disconnect(): void {\n this.#resizeObserver?.disconnect();\n this.#resizeObserver = null;\n this.unobserveViewport();\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { LayoutObserver } from \"../utils/layout_observer\";\n\n/** Parses a CSS pixel length, defaulting to 0 for `auto` / empty / `normal`. */\nfunction px(value: string): number {\n const n = Number.parseFloat(value);\n return Number.isNaN(n) ? 0 : n;\n}\n\n/** The event surface used by `document.fonts` without requiring it in older engines. */\ninterface FontEventSource {\n addEventListener(type: string, listener: EventListener): void;\n removeEventListener(type: string, listener: EventListener): void;\n}\n\n/**\n * Headless autosize behavior for a `<textarea>`: grows the element to fit its\n * content (and clamps to `minRows` / `maxRows`, scrolling past the max). No APG\n * pattern; supports WCAG 1.4.4 by following the text as it grows.\n *\n * Markup contract (identifier: `stimeo--textarea-autosize`):\n * <textarea\n * data-controller=\"stimeo--textarea-autosize\"\n * data-stimeo--textarea-autosize-max-rows-value=\"10\"></textarea>\n *\n * Each re-measure collapses the element (`height:auto`), reads `scrollHeight`,\n * and sets an explicit pixel height clamped between `minRows` and `maxRows` (in\n * line-height units), toggling internal scrolling and the `data-at-max-rows`\n * hook at the cap. Height-only changes preserve focus and caret.\n *\n * Re-measure triggers: `connect()`; `input` and `change` on the element\n * (`change` covers programmatic writers that announce the write with a bubbling\n * event, the convention Stimeo's own field-writing controllers follow); element\n * size reports via the shared {@link LayoutObserver} — width changes\n * (re-wrapping) and the applied inline height being stripped (a Turbo morph\n * syncing attributes); font loading settling on `document.fonts`; runtime\n * `minRows` / `maxRows` changes; and the explicit `resize` action. A silent\n * `.value` assignment produces no DOM signal, so callers follow it with\n * `resize` or dispatch `input` / `change`.\n *\n * `resize` dispatches `{ height, rows }` only when the applied height differs\n * from the height this instance last applied. `connect()` adopts a height left\n * on the element by a previous connection (Turbo restore / morph), so restoring\n * an unchanged element stays silent.\n *\n * @remarks\n * Behavior only — the height is written to the element's own inline style, never\n * a CSS class. State lives on the element (no module-scope state), so `connect()`\n * re-measures after a Turbo navigation / morph. Every subscription (`input` /\n * `change` listeners, the layout observer, the font listeners) is released on\n * `disconnect()` (Turbo navigation included). Measurement is synchronous, so\n * there is no pending animation frame to tear down. The layout callback\n * re-measures only when the element's width changed or the applied inline\n * height vanished, so the controller's own height writes do not re-enter it.\n */\nexport class TextareaAutosizeController extends Controller<HTMLTextAreaElement> {\n static override values = {\n minRows: { type: Number, default: 1 },\n maxRows: { type: Number, default: 0 },\n };\n static actions = [\"resize\"] as const;\n static events = [\"resize\"] as const;\n\n declare minRowsValue: number;\n declare maxRowsValue: number;\n\n #lastHeight = -1;\n #lastWidth = -1;\n #started = false;\n #pending = false;\n #fonts: FontEventSource | null = null;\n\n /**\n * Content triggers arriving while the box is collapsed (`display: none`\n * ancestors) are deferred: `scrollHeight` is 0 there, so measuring would\n * clamp the kept height to the `minRows` floor and dispatch a bogus\n * `resize`. The next report from a rendered box flushes the deferral.\n */\n readonly #remeasure = (): void => {\n if (this.element.clientWidth === 0) {\n this.#pending = true;\n return;\n }\n this.resize();\n };\n\n /**\n * Re-measures when the content width changed (re-wrapping, including a\n * horizontal padding change under `border-box`), the applied inline height\n * was stripped (a Turbo morph syncing attributes from server HTML that has no\n * `style`), or a content trigger was deferred while the box was collapsed.\n * This instance's own height writes match none of these signals, so they\n * cannot re-enter; an authored inline height (e.g. a user dragging the native\n * resize handle) is left alone until the next content trigger. Reports from a\n * still-collapsed box are skipped (nothing is measurable there).\n */\n readonly #layout = new LayoutObserver(() => {\n if (this.element.clientWidth === 0) return;\n const width = this.#contentWidth();\n const stripped = this.element.style.height === \"\";\n if (width === this.#lastWidth && !stripped && !this.#pending) return;\n this.#lastWidth = width;\n this.resize();\n });\n\n override connect(): void {\n // Adopt a height applied by a previous connection (kept by Turbo restore /\n // morph) so re-measuring an unchanged element does not re-dispatch `resize`.\n this.#lastHeight = Math.round(px(this.element.style.height));\n this.#lastWidth = this.#contentWidth();\n this.element.addEventListener(\"input\", this.#remeasure);\n this.element.addEventListener(\"change\", this.#remeasure);\n this.#layout.observe(this.element);\n this.#bindFonts();\n this.#started = true;\n this.resize();\n }\n\n override disconnect(): void {\n this.#started = false;\n this.element.removeEventListener(\"input\", this.#remeasure);\n this.element.removeEventListener(\"change\", this.#remeasure);\n this.#layout.disconnect();\n this.#unbindFonts();\n }\n\n /** Re-clamps when application code or a Turbo morph changes `minRows`. */\n minRowsValueChanged(): void {\n if (this.#started) this.#remeasure();\n }\n\n /** Re-clamps when application code or a Turbo morph changes `maxRows`. */\n maxRowsValueChanged(): void {\n if (this.#started) this.#remeasure();\n }\n\n /** Re-measures the content and applies the clamped height. */\n resize(): void {\n this.#pending = false;\n const el = this.element;\n const style = window.getComputedStyle(el);\n const lineHeight = this.#lineHeight(style);\n const paddingV = px(style.paddingTop) + px(style.paddingBottom);\n const borderV = px(style.borderTopWidth) + px(style.borderBottomWidth);\n const borderBox = style.boxSizing === \"border-box\";\n\n // Collapse first so scrollHeight reflects the content, not the prior height.\n el.style.height = \"auto\";\n const contentHeight = Math.max(0, el.scrollHeight - paddingV);\n const rows = Math.max(1, Math.round(contentHeight / lineHeight));\n\n let targetContent = Math.max(contentHeight, this.minRowsValue * lineHeight);\n let atMax = false;\n if (this.maxRowsValue > 0) {\n const maxContent = this.maxRowsValue * lineHeight;\n if (targetContent > maxContent) {\n targetContent = maxContent;\n atMax = true;\n }\n }\n\n const boxExtra = borderBox ? paddingV + borderV : 0;\n const height = Math.round(targetContent + boxExtra);\n el.style.height = `${height}px`;\n el.style.overflowY = atMax ? \"auto\" : \"hidden\";\n\n if (atMax) {\n el.setAttribute(\"data-at-max-rows\", \"true\");\n } else {\n el.removeAttribute(\"data-at-max-rows\");\n }\n el.style.setProperty(\"--stimeo--textarea-rows\", String(rows));\n\n if (height !== this.#lastHeight) {\n this.#lastHeight = height;\n this.dispatch(\"resize\", { detail: { height, rows } });\n }\n }\n\n /** Content-box width — the wrapping input, unaffected by this instance's height writes. */\n #contentWidth(): number {\n const style = window.getComputedStyle(this.element);\n return Math.max(0, this.element.clientWidth - px(style.paddingLeft) - px(style.paddingRight));\n }\n\n /** Resolved line height, falling back to ~1.2× font-size when `normal`. */\n #lineHeight(style: CSSStyleDeclaration): number {\n const lh = px(style.lineHeight);\n if (lh > 0) return lh;\n const fontSize = px(style.fontSize);\n return fontSize > 0 ? fontSize * 1.2 : 16;\n }\n\n /** Subscribes to font completion when the current engine exposes `document.fonts`. */\n #bindFonts(): void {\n const fonts = (this.element.ownerDocument as Document & { fonts?: FontEventSource }).fonts;\n if (!fonts) return;\n this.#fonts = fonts;\n fonts.addEventListener(\"loadingdone\", this.#remeasure);\n fonts.addEventListener(\"loadingerror\", this.#remeasure);\n }\n\n /** Removes the font completion subscriptions. */\n #unbindFonts(): void {\n this.#fonts?.removeEventListener(\"loadingdone\", this.#remeasure);\n this.#fonts?.removeEventListener(\"loadingerror\", this.#remeasure);\n this.#fonts = null;\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/layout_observer.ts","../../src/controllers/textarea_autosize_controller.ts"],"names":[],"mappings":"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","file":"textarea_autosize_controller.js","sourcesContent":["/**\n * Unified element-size and viewport observation for Stimeo controllers.\n *\n * Widgets whose output is measured — an overflow boundary, a masonry column count,\n * an autosized textarea — need to react both to their *own* box changing — via {@link ResizeObserver} —\n * and to the *viewport* changing — via the `window` `resize` event. Wiring those\n * two sources by hand in every controller risks leaked listeners on\n * `disconnect()`. {@link LayoutObserver} owns both behind one callback and one\n * {@link LayoutObserver.disconnect | disconnect()} that releases everything.\n *\n * Behavior only: the helper reports *that* layout changed; it never reads or\n * writes styles. Consumers decide what to recompute.\n */\n\n/** Invoked whenever an observed element or the viewport changes size. */\nexport type LayoutCallback = () => void;\n\n/** Constructs a {@link ResizeObserver}; injectable so tests stay deterministic. */\nexport type ResizeObserverFactory = (callback: ResizeObserverCallback) => ResizeObserver;\n\n/** Options for {@link LayoutObserver}. */\nexport interface LayoutObserverOptions {\n /**\n * Factory for the {@link ResizeObserver} used by {@link LayoutObserver.observe}.\n * Defaults to the global constructor; override it in tests, or to no-op in\n * environments where `ResizeObserver` is unavailable.\n */\n resizeObserverFactory?: ResizeObserverFactory;\n}\n\n/**\n * Observes element resizes and/or viewport resizes through a single callback,\n * with guaranteed teardown.\n *\n * @example\n * ```ts\n * #layout = new LayoutObserver(() => this.#reposition());\n *\n * connect() {\n * this.#layout.observe(this.panelTarget);\n * this.#layout.observeViewport();\n * }\n *\n * disconnect() {\n * this.#layout.disconnect();\n * }\n * ```\n */\nexport class LayoutObserver {\n readonly #callback: LayoutCallback;\n readonly #resizeObserverFactory: ResizeObserverFactory | null;\n #resizeObserver: ResizeObserver | null = null;\n #observingViewport = false;\n\n /** Stable bound handler so add/removeEventListener target the same reference. */\n readonly #handleViewportResize = (): void => {\n this.#callback();\n };\n\n constructor(callback: LayoutCallback, options: LayoutObserverOptions = {}) {\n this.#callback = callback;\n this.#resizeObserverFactory =\n options.resizeObserverFactory ??\n (typeof ResizeObserver === \"undefined\" ? null : (cb) => new ResizeObserver(cb));\n }\n\n /**\n * Starts observing an element's size. Repeated calls observe additional\n * elements through the same shared observer. No-ops when no\n * `ResizeObserver` implementation is available.\n */\n observe(element: Element): void {\n if (!this.#resizeObserverFactory) return;\n if (!this.#resizeObserver) {\n this.#resizeObserver = this.#resizeObserverFactory(() => {\n this.#callback();\n });\n }\n this.#resizeObserver.observe(element);\n }\n\n /** Stops observing a single element while leaving any others in place. */\n unobserve(element: Element): void {\n this.#resizeObserver?.unobserve(element);\n }\n\n /** Starts observing viewport resizes. Idempotent: the listener is added once. */\n observeViewport(): void {\n if (this.#observingViewport) return;\n this.#observingViewport = true;\n window.addEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /** Stops observing viewport resizes without affecting element observation. */\n unobserveViewport(): void {\n if (!this.#observingViewport) return;\n this.#observingViewport = false;\n window.removeEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /**\n * Releases every observation: disconnects the {@link ResizeObserver} and\n * removes the viewport listener. Safe to call multiple times. Call this from a\n * controller's `disconnect()`.\n */\n disconnect(): void {\n this.#resizeObserver?.disconnect();\n this.#resizeObserver = null;\n this.unobserveViewport();\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { LayoutObserver } from \"../utils/layout_observer\";\n\n/** Parses a CSS pixel length, defaulting to 0 for `auto` / empty / `normal`. */\nfunction px(value: string): number {\n const n = Number.parseFloat(value);\n return Number.isNaN(n) ? 0 : n;\n}\n\n/** The event surface used by `document.fonts` without requiring it in older engines. */\ninterface FontEventSource {\n addEventListener(type: string, listener: EventListener): void;\n removeEventListener(type: string, listener: EventListener): void;\n}\n\n/**\n * Headless autosize behavior for a `<textarea>`: grows the element to fit its\n * content (and clamps to `minRows` / `maxRows`, scrolling past the max). No APG\n * pattern; supports WCAG 1.4.4 by following the text as it grows.\n *\n * Markup contract (identifier: `stimeo--textarea-autosize`):\n * <textarea\n * data-controller=\"stimeo--textarea-autosize\"\n * data-stimeo--textarea-autosize-max-rows-value=\"10\"></textarea>\n *\n * Each re-measure collapses the element (`height:auto`), reads `scrollHeight`,\n * and sets an explicit pixel height clamped between `minRows` and `maxRows` (in\n * line-height units), toggling internal scrolling and the `data-at-max-rows`\n * hook at the cap. Height-only changes preserve focus and caret.\n *\n * Re-measure triggers: `connect()`; `input` and `change` on the element\n * (`change` covers programmatic writers that announce the write with a bubbling\n * event, the convention Stimeo's own field-writing controllers follow); element\n * size reports via the shared `LayoutObserver` — width changes\n * (re-wrapping) and the applied inline height being stripped (a Turbo morph\n * syncing attributes); font loading settling on `document.fonts`; runtime\n * `minRows` / `maxRows` changes; and the explicit `resize` action. A silent\n * `.value` assignment produces no DOM signal, so callers follow it with\n * `resize` or dispatch `input` / `change`.\n *\n * `resize` dispatches `{ height, rows }` only when the applied height differs\n * from the height this instance last applied. `connect()` adopts a height left\n * on the element by a previous connection (Turbo restore / morph), so restoring\n * an unchanged element stays silent.\n *\n * @remarks\n * Behavior only — the height is written to the element's own inline style, never\n * a CSS class. State lives on the element (no module-scope state), so `connect()`\n * re-measures after a Turbo navigation / morph. Every subscription (`input` /\n * `change` listeners, the layout observer, the font listeners) is released on\n * `disconnect()` (Turbo navigation included). Measurement is synchronous, so\n * there is no pending animation frame to tear down. The layout callback\n * re-measures only when the element's width changed or the applied inline\n * height vanished, so the controller's own height writes do not re-enter it.\n */\nexport class TextareaAutosizeController extends Controller<HTMLTextAreaElement> {\n static override values = {\n minRows: { type: Number, default: 1 },\n maxRows: { type: Number, default: 0 },\n };\n static actions = [\"resize\"] as const;\n static events = [\"resize\"] as const;\n\n declare minRowsValue: number;\n declare maxRowsValue: number;\n\n #lastHeight = -1;\n #lastWidth = -1;\n #started = false;\n #pending = false;\n #fonts: FontEventSource | null = null;\n\n /**\n * Content triggers arriving while the box is collapsed (`display: none`\n * ancestors) are deferred: `scrollHeight` is 0 there, so measuring would\n * clamp the kept height to the `minRows` floor and dispatch a bogus\n * `resize`. The next report from a rendered box flushes the deferral.\n */\n readonly #remeasure = (): void => {\n if (this.element.clientWidth === 0) {\n this.#pending = true;\n return;\n }\n this.resize();\n };\n\n /**\n * Re-measures when the content width changed (re-wrapping, including a\n * horizontal padding change under `border-box`), the applied inline height\n * was stripped (a Turbo morph syncing attributes from server HTML that has no\n * `style`), or a content trigger was deferred while the box was collapsed.\n * This instance's own height writes match none of these signals, so they\n * cannot re-enter; an authored inline height (e.g. a user dragging the native\n * resize handle) is left alone until the next content trigger. Reports from a\n * still-collapsed box are skipped (nothing is measurable there).\n */\n readonly #layout = new LayoutObserver(() => {\n if (this.element.clientWidth === 0) return;\n const width = this.#contentWidth();\n const stripped = this.element.style.height === \"\";\n if (width === this.#lastWidth && !stripped && !this.#pending) return;\n this.#lastWidth = width;\n this.resize();\n });\n\n override connect(): void {\n // Adopt a height applied by a previous connection (kept by Turbo restore /\n // morph) so re-measuring an unchanged element does not re-dispatch `resize`.\n this.#lastHeight = Math.round(px(this.element.style.height));\n this.#lastWidth = this.#contentWidth();\n this.element.addEventListener(\"input\", this.#remeasure);\n this.element.addEventListener(\"change\", this.#remeasure);\n this.#layout.observe(this.element);\n this.#bindFonts();\n this.#started = true;\n this.resize();\n }\n\n override disconnect(): void {\n this.#started = false;\n this.element.removeEventListener(\"input\", this.#remeasure);\n this.element.removeEventListener(\"change\", this.#remeasure);\n this.#layout.disconnect();\n this.#unbindFonts();\n }\n\n /** Re-clamps when application code or a Turbo morph changes `minRows`. */\n minRowsValueChanged(): void {\n if (this.#started) this.#remeasure();\n }\n\n /** Re-clamps when application code or a Turbo morph changes `maxRows`. */\n maxRowsValueChanged(): void {\n if (this.#started) this.#remeasure();\n }\n\n /** Re-measures the content and applies the clamped height. */\n resize(): void {\n this.#pending = false;\n const el = this.element;\n const style = window.getComputedStyle(el);\n const lineHeight = this.#lineHeight(style);\n const paddingV = px(style.paddingTop) + px(style.paddingBottom);\n const borderV = px(style.borderTopWidth) + px(style.borderBottomWidth);\n const borderBox = style.boxSizing === \"border-box\";\n\n // Collapse first so scrollHeight reflects the content, not the prior height.\n el.style.height = \"auto\";\n const contentHeight = Math.max(0, el.scrollHeight - paddingV);\n const rows = Math.max(1, Math.round(contentHeight / lineHeight));\n\n let targetContent = Math.max(contentHeight, this.minRowsValue * lineHeight);\n let atMax = false;\n if (this.maxRowsValue > 0) {\n const maxContent = this.maxRowsValue * lineHeight;\n if (targetContent > maxContent) {\n targetContent = maxContent;\n atMax = true;\n }\n }\n\n const boxExtra = borderBox ? paddingV + borderV : 0;\n const height = Math.round(targetContent + boxExtra);\n el.style.height = `${height}px`;\n el.style.overflowY = atMax ? \"auto\" : \"hidden\";\n\n if (atMax) {\n el.setAttribute(\"data-at-max-rows\", \"true\");\n } else {\n el.removeAttribute(\"data-at-max-rows\");\n }\n el.style.setProperty(\"--stimeo--textarea-rows\", String(rows));\n\n if (height !== this.#lastHeight) {\n this.#lastHeight = height;\n this.dispatch(\"resize\", { detail: { height, rows } });\n }\n }\n\n /** Content-box width — the wrapping input, unaffected by this instance's height writes. */\n #contentWidth(): number {\n const style = window.getComputedStyle(this.element);\n return Math.max(0, this.element.clientWidth - px(style.paddingLeft) - px(style.paddingRight));\n }\n\n /** Resolved line height, falling back to ~1.2× font-size when `normal`. */\n #lineHeight(style: CSSStyleDeclaration): number {\n const lh = px(style.lineHeight);\n if (lh > 0) return lh;\n const fontSize = px(style.fontSize);\n return fontSize > 0 ? fontSize * 1.2 : 16;\n }\n\n /** Subscribes to font completion when the current engine exposes `document.fonts`. */\n #bindFonts(): void {\n const fonts = (this.element.ownerDocument as Document & { fonts?: FontEventSource }).fonts;\n if (!fonts) return;\n this.#fonts = fonts;\n fonts.addEventListener(\"loadingdone\", this.#remeasure);\n fonts.addEventListener(\"loadingerror\", this.#remeasure);\n }\n\n /** Removes the font completion subscriptions. */\n #unbindFonts(): void {\n this.#fonts?.removeEventListener(\"loadingdone\", this.#remeasure);\n this.#fonts?.removeEventListener(\"loadingerror\", this.#remeasure);\n this.#fonts = null;\n }\n}\n"]}
@@ -35,7 +35,8 @@ type ThemeMode = "light" | "dark" | "system";
35
35
  * on the root. It applies `data-theme` (the *resolved* light/dark) and a matching
36
36
  * `color-scheme` to the `target` element (`<html>` by default), keeps the radiogroup
37
37
  * `aria-checked` + roving tabindex (APG radio) or the single button's `aria-pressed`
38
- * in sync, and never moves focus. The `prefers-color-scheme` listener is attached on
38
+ * in sync. Arrow/Home/End move focus across the radiogroup as the APG radio pattern
39
+ * requires; applying a theme never moves focus. The `prefers-color-scheme` listener is attached on
39
40
  * `connect()` and removed on `disconnect()` (Turbo included). FOUC avoidance for the
40
41
  * very first paint is an inline `<head>` snippet, not this controller.
41
42
  *
@@ -47,7 +48,7 @@ type ThemeMode = "light" | "dark" | "system";
47
48
  * and none at all when the resolved mode matches no option, where the first one
48
49
  * keeps the Tab stop.
49
50
  *
50
- * The radiogroup stays one Tab stop through {@link RovingTabindex}, re-derived
51
+ * The radiogroup stays one Tab stop through `RovingTabindex`, re-derived
51
52
  * whenever an option enters or leaves, so a set rendered after connect or swapped
52
53
  * by a Turbo morph carries the same single stop as any other.
53
54
  */
@@ -86,9 +87,8 @@ declare class ThemeController extends Controller<HTMLElement> {
86
87
  /**
87
88
  * Selects the mode the activated option declares.
88
89
  *
89
- * Read through {@link ThemeController.#optionMode}, the same lane that decides
90
- * which option is checked, so the two can never disagree about what an option
91
- * declares.
90
+ * Read through the same lane that decides which option is checked, so the two
91
+ * can never disagree about what an option declares.
92
92
  */
93
93
  set(event: Event): void;
94
94
  /** Toggles light↔dark for the 2-value single-button contract. */
@@ -23,6 +23,26 @@ function hasModifierChord(event) {
23
23
  return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
24
24
  }
25
25
 
26
+ // src/utils/declared_value.ts
27
+ function parseDeclared(raw, parse, fallback) {
28
+ try {
29
+ return parse(raw);
30
+ } catch {
31
+ return fallback;
32
+ }
33
+ }
34
+ function validSelector(element, raw, fallback) {
35
+ if (raw.length === 0) return fallback;
36
+ return parseDeclared(
37
+ raw,
38
+ (selector) => {
39
+ element.matches(selector);
40
+ return selector;
41
+ },
42
+ fallback
43
+ );
44
+ }
45
+
26
46
  // src/utils/roving_tabindex.ts
27
47
  var RovingTabindex = class {
28
48
  /** Returns the current ordered item elements; called on every operation. */
@@ -167,16 +187,7 @@ var ThemeController = class extends Controller {
167
187
  }
168
188
  /** Validates the `target` declaration once, so the render path never parses. */
169
189
  targetValueChanged() {
170
- const selector = this.targetValue;
171
- if (selector.length > 0) {
172
- try {
173
- this.element.matches(selector);
174
- this.#targetSelector = selector;
175
- return;
176
- } catch {
177
- }
178
- }
179
- this.#targetSelector = DEFAULT_TARGET;
190
+ this.#targetSelector = validSelector(this.element, this.targetValue, DEFAULT_TARGET);
180
191
  }
181
192
  /** Re-derives the single Tab stop and ARIA for an option set that changed. */
182
193
  optionTargetConnected() {
@@ -189,9 +200,8 @@ var ThemeController = class extends Controller {
189
200
  /**
190
201
  * Selects the mode the activated option declares.
191
202
  *
192
- * Read through {@link ThemeController.#optionMode}, the same lane that decides
193
- * which option is checked, so the two can never disagree about what an option
194
- * declares.
203
+ * Read through the same lane that decides which option is checked, so the two
204
+ * can never disagree about what an option declares.
195
205
  */
196
206
  set(event) {
197
207
  const option = event.currentTarget;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/logical_scroll.ts","../../src/utils/arrow_step.ts","../../src/utils/roving_tabindex.ts","../../src/utils/safe_storage.ts","../../src/controllers/theme_controller.ts"],"names":[],"mappings":"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","file":"theme_controller.js","sourcesContent":["/** Normalized scroll position and maximum distance on one logical axis. */\nexport interface LogicalScrollMetrics {\n position: number;\n max: number;\n}\n\n/**\n * Whether horizontal scrolling on `element` follows right-to-left inline flow.\n *\n * Resolved from the **computed** `direction`, so the authoring contract is the\n * usual `dir=\"rtl\"` (or a stylesheet) on the element or any ancestor.\n *\n * Scope: horizontal writing modes. A vertical writing mode (`writing-mode:\n * vertical-rl`) also inverts the horizontal axis, which this check does not\n * model — vertical writing modes are out of scope for the scroll utilities\n * (their consumers describe axes as horizontal/vertical, not inline/block).\n */\nexport function isRtl(element: Element): boolean {\n return window.getComputedStyle(element).direction === \"rtl\";\n}\n\n/**\n * Returns scroll distance from the logical start edge.\n *\n * CSSOM View exposes standards-mode RTL horizontal offsets as `0` at the inline\n * start (right) and increasingly negative values toward the inline end (left).\n * The normalized position is always clamped to `[0, max]`, which also absorbs\n * Safari's elastic overscroll values.\n */\nexport function logicalScrollMetrics(\n element: HTMLElement,\n horizontal: boolean,\n): LogicalScrollMetrics {\n const max = Math.max(\n 0,\n horizontal\n ? element.scrollWidth - element.clientWidth\n : element.scrollHeight - element.clientHeight,\n );\n const raw = horizontal ? element.scrollLeft : element.scrollTop;\n const position = horizontal && isRtl(element) ? -raw : raw;\n return { position: Math.min(max, Math.max(0, position)), max };\n}\n\n/**\n * Converts a logical start/end delta to the physical value accepted by\n * `Element.scrollBy`.\n */\nexport function physicalScrollDelta(\n element: HTMLElement,\n horizontal: boolean,\n logicalDelta: number,\n): number {\n return horizontal && isRtl(element) ? -logicalDelta : logicalDelta;\n}\n","import { isRtl } from \"./logical_scroll\";\n\n/**\n * Turns an arrow key into a **logical** step: `+1` for \"next\", `-1` for\n * \"previous\", `0` when the key names neither.\n *\n * APG defines the horizontal pair as *next / previous* and says a vertical\n * arrangement swaps in Down/Up for the same meaning — so the pair is one axis's\n * spelling of an order, and the order reverses with the writing direction. Only\n * the horizontal pair reverses. Down/Up name an axis the writing direction does\n * not mirror, and returning them unchanged is the point: many controllers fold\n * both pairs into one branch, where swapping the branches under RTL would flip\n * the vertical axis too — a bug that reads as \"the arrows work\" until someone\n * presses Down.\n *\n * **Direction is read from the element the caller passes, which should be the\n * container that lays the items out** — not the focused child. A child may carry\n * its own `dir` (an LTR input inside an RTL form is ordinary authoring), and\n * probing per handler makes two handlers disagree at the boundary between them.\n *\n * This decides direction only. Whether the axis is even active (an\n * `orientation=\"horizontal\"` widget ignoring Down/Up), how far the step lands,\n * and what wrapping does all stay with the caller.\n *\n * **It encodes the list-order convention: `ArrowDown` is *next*.** Widgets that\n * pair the arrows by *value* instead — `ArrowUp` meaning \"more\", as a rating or a\n * slider does — must not use this, or their vertical axis inverts. Reverse the\n * horizontal pair on its own there.\n *\n * @example\n * ```ts\n * const step = logicalArrowStep(event.key, this.element);\n * if (step === 0) return;\n * this.#roving.setActive(rovingMove(current, length, step, \"wrap\"), { focus: true });\n * ```\n */\nexport function logicalArrowStep(key: string, element: Element): 1 | -1 | 0 {\n if (key === \"ArrowDown\") return 1;\n if (key === \"ArrowUp\") return -1;\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return 0;\n const forward = isRtl(element) ? \"ArrowLeft\" : \"ArrowRight\";\n return key === forward ? 1 : -1;\n}\n\n/**\n * Rewrites `key` so an existing LTR-shaped branch keeps working under RTL:\n * `ArrowRight` and `ArrowLeft` trade places, everything else passes through.\n *\n * The alternative — negating a delta — silently breaks handlers whose two\n * horizontal branches are **not mirror images**. A grid that clamps one edge but\n * not the other, or a segmented field guarding `index > 0` on one side and\n * `index < length - 1` on the other, ends up applying the wrong guard to the\n * wrong direction. Swapping the key leaves each branch, guards and all, exactly\n * where its author put it.\n *\n * Same rule as {@link logicalArrowStep} about which element to read: pass the\n * container that lays the items out, not the focused child.\n *\n * @example\n * ```ts\n * switch (logicalArrowKey(event.key, this.element)) {\n * case \"ArrowLeft\": // \"previous\" — whatever direction that is on screen\n * ```\n */\nexport function logicalArrowKey(key: string, element: Element): string {\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return key;\n if (!isRtl(element)) return key;\n return key === \"ArrowRight\" ? \"ArrowLeft\" : \"ArrowRight\";\n}\n\n/** Modifiers a widget may claim on an arrow key, named for the `allow` list. */\nexport type ArrowModifier = \"alt\" | \"ctrl\" | \"meta\" | \"shift\";\n\n/**\n * True when an arrow key arrived carrying a modifier the widget must leave to\n * the browser: return without calling `preventDefault()` and without moving any\n * state.\n *\n * A bare arrow belongs to the widget; a chorded one usually does not.\n * `Alt`/`Meta` plus a horizontal arrow is history back/forward on every desktop\n * browser, and a widget that swallows it makes the shortcut work or not\n * depending on where focus happens to sit — a coin-flip the user cannot see.\n *\n * `allow` is for the combinations APG assigns to a pattern **and the widget\n * actually implements** — today only Combobox's optional `Alt+Down`/`Alt+Up`.\n * Listing one the widget does not implement defeats the point: the chord then\n * runs the plain-arrow branch, which is exactly what this guard exists to stop.\n * Non-arrow keys return `false`, so chorded letters and\n * `Control+Home`/`Control+End` are untouched.\n *\n * @example\n * ```ts\n * if (isReservedArrowChord(event)) return;\n * ```\n */\nexport function isReservedArrowChord(\n event: KeyboardEvent,\n allow: readonly ArrowModifier[] = [],\n): boolean {\n if (!event.key.startsWith(\"Arrow\")) return false;\n return (\n (event.altKey && !allow.includes(\"alt\")) ||\n (event.ctrlKey && !allow.includes(\"ctrl\")) ||\n (event.metaKey && !allow.includes(\"meta\")) ||\n (event.shiftKey && !allow.includes(\"shift\"))\n );\n}\n\n/**\n * True when a press arrived carrying any modifier, for the keys the browser and\n * the OS own outright: return without calling `preventDefault()` and without\n * moving any state.\n *\n * `Control+Home` and `Control+End` jump the document to its ends, and a widget\n * that swallows them makes the shortcut work or not depending on where focus\n * happens to sit. {@link isReservedArrowChord} answers the same question for the\n * arrows, but returns `false` for every other key so that this one can decide.\n *\n * There is no `allow` list here on purpose. APG assigns no modifier chord to\n * `Home`/`End`, so a widget that wanted one would be claiming a combination the\n * pattern never gave it.\n *\n * @example\n * ```ts\n * case \"Home\":\n * case \"End\":\n * if (hasModifierChord(event)) return;\n * ```\n */\nexport function hasModifierChord(event: KeyboardEvent): boolean {\n return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;\n}\n","/**\n * Low-level roving-tabindex primitive shared by composite-widget controllers.\n *\n * The APG roving-tabindex pattern keeps a composite widget a single Tab stop:\n * exactly one item is in the Tab sequence (`tabindex=\"0\"`) while the rest are\n * removed from it (`tabindex=\"-1\"`), and the arrow keys move both DOM focus and\n * that single tabbable position together. {@link RovingTabindex} owns *only* that\n * mechanical bookkeeping — \"which one item is tabbable, and move focus there\".\n *\n * It is intentionally **policy-free**. Orientation, wrapping vs. clamping,\n * selection-follows-focus, typeahead, and `Home`/`End` semantics differ per APG\n * pattern (Radio Group, Toolbar, Rating, …); folding them into one helper would\n * flatten those widgets to a lowest common denominator and lose each pattern's\n * correctness. Those decisions therefore stay in each controller, which calls\n * {@link RovingTabindex.setActive} with an index it computed itself (optionally\n * via the pure {@link rovingMove} helper).\n *\n * @remarks\n * Items are read lazily through a getter so a controller can add or remove\n * targets (Stimulus re-scans the DOM) without re-wiring this helper. A caller\n * that already captured one event-scoped snapshot may pass it to\n * {@link RovingTabindex.setActive}, avoiding a second live-target scan while\n * preserving one coherent collection for the whole operation.\n */\nexport class RovingTabindex {\n /** Returns the current ordered item elements; called on every operation. */\n readonly #getItems: () => HTMLElement[];\n\n /**\n * @param getItems - Returns the current ordered item elements. Called on every\n * operation so the live target list is always used.\n */\n constructor(getItems: () => HTMLElement[]) {\n this.#getItems = getItems;\n }\n\n /** Index of the currently tabbable item (`tabindex=\"0\"`), or `-1` if none. */\n get activeIndex(): number {\n return this.#getItems().findIndex((item) => item.tabIndex === 0);\n }\n\n /**\n * Makes exactly the item at `index` tabbable (`tabindex=\"0\"`) and removes every\n * other item from the Tab sequence (`tabindex=\"-1\"`). An out-of-range `index`\n * (e.g. `-1`) leaves all items at `-1`, which a controller can use to express\n * \"nothing is currently tabbable\".\n *\n * @param index - Position of the item to make tabbable.\n * @param options - Pass `{ focus: true }` to also move DOM focus to that item,\n * and `items` to reuse an event-scoped collection snapshot.\n */\n setActive(\n index: number,\n options: { focus?: boolean; items?: readonly HTMLElement[] } = {},\n ): void {\n const { focus = false } = options;\n const items = options.items ?? this.#getItems();\n items.forEach((item, i) => {\n item.tabIndex = i === index ? 0 : -1;\n });\n if (focus) items[index]?.focus();\n }\n}\n\n/** Edge behavior for {@link rovingMove}: cycle past the ends, or stop at them. */\nexport type RovingWrap = \"wrap\" | \"clamp\";\n\n/**\n * Pure helper that resolves the target index for a one-step directional move.\n *\n * Keyboard/orientation mapping stays in the caller: it decides that a key means\n * `delta` `+1` (next) or `-1` (previous) and whether the widget should `\"wrap\"`\n * (Radio Group, Toolbar) or `\"clamp\"` at the ends.\n *\n * @param current - The index focus is moving from.\n * @param length - Number of items in the set.\n * @param delta - `+1` to move to the next item, `-1` for the previous.\n * @param wrap - `\"wrap\"` cycles around the ends; `\"clamp\"` stops at them.\n * @returns The resolved index, or `-1` when there are no items.\n */\nexport function rovingMove(\n current: number,\n length: number,\n delta: number,\n wrap: RovingWrap,\n): number {\n if (length === 0) return -1;\n const next = current + delta;\n if (wrap === \"wrap\") return (next + length) % length;\n return Math.min(length - 1, Math.max(0, next));\n}\n","/** A successful guarded storage operation. */\nexport interface StorageSuccess<T> {\n readonly ok: true;\n readonly value: T;\n}\n\n/** A guarded storage operation rejected by the browser. */\nexport interface StorageFailure {\n readonly ok: false;\n readonly error: unknown;\n}\n\n/** Result of accessing browser storage without allowing an exception to escape. */\nexport type StorageResult<T> = StorageSuccess<T> | StorageFailure;\n\n/**\n * Guarded `localStorage` access shared by controllers that persist client state.\n *\n * `window.localStorage` can throw while resolving the storage object or while\n * reading, writing, or removing a key. The result keeps an unavailable store\n * distinct from an available store whose key is unset, so callers can expose an\n * accurate success or failure state without duplicating exception guards.\n */\n\n/**\n * Reads `key` from `localStorage`.\n *\n * @param key - The storage key to read.\n * @returns A successful result containing the stored string or `null`, or a\n * failure result when storage is unavailable.\n */\nexport function readLocalStorage(key: string): StorageResult<string | null> {\n try {\n return { ok: true, value: window.localStorage.getItem(key) };\n } catch (error: unknown) {\n return { ok: false, error };\n }\n}\n\n/**\n * Writes `value` under `key` in `localStorage`.\n *\n * @param key - The storage key to write.\n * @param value - The string to store.\n * @returns Success only after `setItem` completes; otherwise the browser error.\n */\nexport function writeLocalStorage(key: string, value: string): StorageResult<void> {\n try {\n window.localStorage.setItem(key, value);\n return { ok: true, value: undefined };\n } catch (error: unknown) {\n return { ok: false, error };\n }\n}\n\n/**\n * Removes `key` from `localStorage`.\n *\n * @param key - The storage key to remove.\n * @returns Success only after `removeItem` completes; otherwise the browser error.\n */\nexport function removeLocalStorage(key: string): StorageResult<void> {\n try {\n window.localStorage.removeItem(key);\n return { ok: true, value: undefined };\n } catch (error: unknown) {\n return { ok: false, error };\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { hasModifierChord, isReservedArrowChord, logicalArrowStep } from \"../utils/arrow_step\";\nimport { RovingTabindex, rovingMove } from \"../utils/roving_tabindex\";\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/** The mode a declaration falls back to when it cannot be read as one. */\nconst DEFAULT_MODE: ThemeMode = \"system\";\n/** The state-hook target a declaration falls back to when it cannot be parsed. */\nconst DEFAULT_TARGET = \"html\";\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-value=\"light\">Light</button>\n * <button data-stimeo--theme-target=\"option\" role=\"radio\"\n * data-action=\"click->stimeo--theme#set\"\n * data-value=\"dark\">Dark</button>\n * <button data-stimeo--theme-target=\"option\" role=\"radio\"\n * data-action=\"click->stimeo--theme#set\"\n * data-value=\"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 * `change` dispatches `{ mode, resolved }`, and only when one of the two moved.\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, not this controller.\n *\n * Every declaration is validated where it enters, and an unreadable one falls back\n * to that Value's default rather than taking the widget with it: a `mode` outside\n * the three modes reads as `system`, and a `target` that is not a parsable selector\n * reads as `html`. An option whose `data-value` is outside the three can be focused\n * but never becomes the selection, so no more than one option is ever checked —\n * and none at all when the resolved mode matches no option, where the first one\n * keeps the Tab stop.\n *\n * The radiogroup stays one Tab stop through {@link RovingTabindex}, re-derived\n * whenever an option enters or leaves, so a set rendered after connect or swapped\n * by a Turbo morph carries the same single stop as any other.\n */\nexport class ThemeController extends Controller<HTMLElement> {\n static override targets = [\"option\"];\n static override values = {\n mode: { type: String, default: DEFAULT_MODE },\n storageKey: { type: String, default: \"stimeo-theme\" },\n target: { type: String, default: DEFAULT_TARGET },\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 /** Gate for the target callbacks, which Stimulus runs before `connect()`. */\n #connected = false;\n\n /** The `target` declaration after validation; the default when unparsable. */\n #targetSelector = DEFAULT_TARGET;\n\n /** Owns the single Tab stop across the option set (APG radiogroup). */\n readonly #roving = new RovingTabindex(() => this.optionTargets);\n\n /**\n * The pair last reported, so a move can be told from a repeat. Neither side is\n * readable after the fact — assigning the Value updates the mode before any\n * comparison, and the OS query has already flipped by the time it notifies —\n * so what was reported has to be kept rather than recomputed.\n */\n #published: { mode: ThemeMode; resolved: ResolvedTheme } = {\n mode: DEFAULT_MODE,\n resolved: \"light\",\n };\n\n /** Re-resolves while in `system` mode when the OS preference flips. */\n readonly #onMediaChange = (): void => {\n // Only `system` follows the OS; an explicit mode already decided the answer.\n if (this.#mode !== \"system\") return;\n this.#commit();\n };\n\n /** Arrow/Home/End navigation for the radiogroup (APG radio pattern). */\n readonly #onKeydown = (event: KeyboardEvent): void => {\n // A descendant widget that already claimed the key (a grabbed drag handle, a\n // nested menu) must not ALSO act on it — composition depends on this yield.\n if (event.defaultPrevented) return;\n if (isReservedArrowChord(event)) return;\n // Resolved once per keydown: every `optionTargets` access re-queries the scope.\n const options = this.optionTargets;\n const target = event.target as HTMLElement | null;\n const current = options.indexOf(target as HTMLElement);\n if (current === -1) return;\n\n let next = current;\n // Logical, not physical. The helper reverses only the horizontal pair, so\n // folding Down/Up into the same branch stays correct.\n const step = logicalArrowStep(event.key, this.element);\n switch (event.key) {\n case \"ArrowDown\":\n case \"ArrowRight\":\n case \"ArrowUp\":\n case \"ArrowLeft\":\n next = rovingMove(current, options.length, step, \"wrap\");\n break;\n case \"Home\":\n case \"End\":\n // Control+Home jumps the document, not the widget.\n if (hasModifierChord(event)) return;\n next = event.key === \"Home\" ? 0 : options.length - 1;\n break;\n default:\n return;\n }\n event.preventDefault();\n const option = options[next];\n if (!option) return;\n option.focus();\n const mode = this.#optionMode(option);\n // An option outside the three modes can hold focus, but the selection does\n // not follow it there.\n if (mode) this.#setMode(mode);\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 this.element.addEventListener(\"keydown\", this.#onKeydown);\n\n this.#connected = true;\n this.#applyTheme();\n this.#syncControls();\n // Connecting is not a change: seed the baseline instead of announcing one.\n this.#published = this.#current;\n }\n\n override disconnect(): void {\n this.#connected = false;\n this.#media?.removeEventListener(\"change\", this.#onMediaChange);\n this.element.removeEventListener(\"keydown\", this.#onKeydown);\n }\n\n /** Validates the `target` declaration once, so the render path never parses. */\n targetValueChanged(): void {\n const selector = this.targetValue;\n if (selector.length > 0) {\n try {\n this.element.matches(selector);\n this.#targetSelector = selector;\n return;\n } catch {\n // Unparsable selector: fall through to the default below.\n }\n }\n this.#targetSelector = DEFAULT_TARGET;\n }\n\n /** Re-derives the single Tab stop and ARIA for an option set that changed. */\n optionTargetConnected(): void {\n if (this.#connected) this.#syncControls();\n }\n\n /** Re-derives them again when an option leaves, so a Tab stop always remains. */\n optionTargetDisconnected(): void {\n if (this.#connected) this.#syncControls();\n }\n\n /**\n * Selects the mode the activated option declares.\n *\n * Read through {@link ThemeController.#optionMode}, the same lane that decides\n * which option is checked, so the two can never disagree about what an option\n * declares.\n */\n set(event: Event): void {\n const option = event.currentTarget;\n if (!(option instanceof HTMLElement)) return;\n const mode = this.#optionMode(option);\n if (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.#commit();\n }\n\n /**\n * Applies the current mode and reports it, but reports only a real move: the\n * event means \"the selection or the effective theme moved\", so re-choosing the\n * option already chosen is not one.\n */\n #commit(): void {\n this.#applyTheme();\n this.#syncControls();\n const next = this.#current;\n const last = this.#published;\n this.#published = next;\n if (last.mode !== next.mode || last.resolved !== next.resolved) {\n // A copy: the baseline has to survive a listener that writes to what it\n // was handed, or the next unchanged operation reads as a change.\n this.dispatch(\"change\", { detail: { ...next } });\n }\n }\n\n /** The pair the `change` detail carries, read from current state. */\n get #current(): { mode: ThemeMode; resolved: ResolvedTheme } {\n return { mode: this.#mode, resolved: this.#resolved() };\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 const mode = this.#mode;\n let selected = -1;\n options.forEach((option, index) => {\n const isSelected = this.#optionMode(option) === mode;\n option.setAttribute(\"aria-checked\", String(isSelected));\n if (isSelected && selected === -1) selected = index;\n });\n // APG: a radiogroup with no selection keeps its first radio tabbable.\n this.#roving.setActive(selected === -1 ? 0 : selected, { items: options });\n return;\n }\n // `aria-pressed` belongs to the 2-value contract, where the controller sits on\n // the button itself. A radiogroup whose options have not rendered yet is not\n // that, and must not be told it is a toggle.\n if (this.#isToggleButton) {\n this.element.setAttribute(\"aria-pressed\", String(this.#resolved() === \"dark\"));\n }\n }\n\n /** Whether the controller element is the button of the 2-value contract. */\n get #isToggleButton(): boolean {\n return this.element.tagName === \"BUTTON\" || this.element.getAttribute(\"role\") === \"button\";\n }\n\n /** The selected mode after validation; an unreadable declaration is the default. */\n get #mode(): ThemeMode {\n return isMode(this.modeValue) ? this.modeValue : DEFAULT_MODE;\n }\n\n /** The effective theme: the OS preference when `system`, else the mode itself. */\n #resolved(): ResolvedTheme {\n const mode = this.#mode;\n if (mode === \"dark\") return \"dark\";\n if (mode === \"light\") return \"light\";\n return this.#media?.matches ? \"dark\" : \"light\";\n }\n\n /** An option's mode from its `data-value`, or `null` when that is not one of the three. */\n #optionMode(option: HTMLElement): ThemeMode | null {\n const mode = option.getAttribute(\"data-value\");\n return isMode(mode) ? mode : null;\n }\n\n /** Resolves the state-hook target (`<html>` by default). */\n #targetElement(): HTMLElement | null {\n const selector = this.#targetSelector;\n if (selector === DEFAULT_TARGET || selector === \":root\") return document.documentElement;\n return document.querySelector<HTMLElement>(selector);\n }\n\n /** Reads a persisted, validated mode from `localStorage` (null when absent/blocked). */\n #readStored(): ThemeMode | null {\n const result = readLocalStorage(this.storageKeyValue);\n return result.ok && isMode(result.value) ? result.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"]}
1
+ {"version":3,"sources":["../../src/utils/logical_scroll.ts","../../src/utils/arrow_step.ts","../../src/utils/declared_value.ts","../../src/utils/roving_tabindex.ts","../../src/utils/safe_storage.ts","../../src/controllers/theme_controller.ts"],"names":[],"mappings":"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","file":"theme_controller.js","sourcesContent":["/** Normalized scroll position and maximum distance on one logical axis. */\nexport interface LogicalScrollMetrics {\n position: number;\n max: number;\n}\n\n/**\n * Whether horizontal scrolling on `element` follows right-to-left inline flow.\n *\n * Resolved from the **computed** `direction`, so the authoring contract is the\n * usual `dir=\"rtl\"` (or a stylesheet) on the element or any ancestor.\n *\n * Scope: horizontal writing modes. A vertical writing mode (`writing-mode:\n * vertical-rl`) also inverts the horizontal axis, which this check does not\n * model — vertical writing modes are out of scope for the scroll utilities\n * (their consumers describe axes as horizontal/vertical, not inline/block).\n */\nexport function isRtl(element: Element): boolean {\n return window.getComputedStyle(element).direction === \"rtl\";\n}\n\n/**\n * Returns scroll distance from the logical start edge.\n *\n * CSSOM View exposes standards-mode RTL horizontal offsets as `0` at the inline\n * start (right) and increasingly negative values toward the inline end (left).\n * The normalized position is always clamped to `[0, max]`, which also absorbs\n * Safari's elastic overscroll values.\n */\nexport function logicalScrollMetrics(\n element: HTMLElement,\n horizontal: boolean,\n): LogicalScrollMetrics {\n const max = Math.max(\n 0,\n horizontal\n ? element.scrollWidth - element.clientWidth\n : element.scrollHeight - element.clientHeight,\n );\n const raw = horizontal ? element.scrollLeft : element.scrollTop;\n const position = horizontal && isRtl(element) ? -raw : raw;\n return { position: Math.min(max, Math.max(0, position)), max };\n}\n\n/**\n * Converts a logical start/end delta to the physical value accepted by\n * `Element.scrollBy`.\n */\nexport function physicalScrollDelta(\n element: HTMLElement,\n horizontal: boolean,\n logicalDelta: number,\n): number {\n return horizontal && isRtl(element) ? -logicalDelta : logicalDelta;\n}\n","import { isRtl } from \"./logical_scroll\";\n\n/**\n * Turns an arrow key into a **logical** step: `+1` for \"next\", `-1` for\n * \"previous\", `0` when the key names neither.\n *\n * APG defines the horizontal pair as *next / previous* and says a vertical\n * arrangement swaps in Down/Up for the same meaning — so the pair is one axis's\n * spelling of an order, and the order reverses with the writing direction. Only\n * the horizontal pair reverses. Down/Up name an axis the writing direction does\n * not mirror, and returning them unchanged is the point: many controllers fold\n * both pairs into one branch, where swapping the branches under RTL would flip\n * the vertical axis too — a bug that reads as \"the arrows work\" until someone\n * presses Down.\n *\n * **Direction is read from the element the caller passes, which should be the\n * container that lays the items out** — not the focused child. A child may carry\n * its own `dir` (an LTR input inside an RTL form is ordinary authoring), and\n * probing per handler makes two handlers disagree at the boundary between them.\n *\n * This decides direction only. Whether the axis is even active (an\n * `orientation=\"horizontal\"` widget ignoring Down/Up), how far the step lands,\n * and what wrapping does all stay with the caller.\n *\n * **It encodes the list-order convention: `ArrowDown` is *next*.** Widgets that\n * pair the arrows by *value* instead — `ArrowUp` meaning \"more\", as a rating or a\n * slider does — must not use this, or their vertical axis inverts. Reverse the\n * horizontal pair on its own there.\n *\n * @example\n * ```ts\n * const step = logicalArrowStep(event.key, this.element);\n * if (step === 0) return;\n * this.#roving.setActive(rovingMove(current, length, step, \"wrap\"), { focus: true });\n * ```\n */\nexport function logicalArrowStep(key: string, element: Element): 1 | -1 | 0 {\n if (key === \"ArrowDown\") return 1;\n if (key === \"ArrowUp\") return -1;\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return 0;\n const forward = isRtl(element) ? \"ArrowLeft\" : \"ArrowRight\";\n return key === forward ? 1 : -1;\n}\n\n/**\n * Rewrites `key` so an existing LTR-shaped branch keeps working under RTL:\n * `ArrowRight` and `ArrowLeft` trade places, everything else passes through.\n *\n * The alternative — negating a delta — silently breaks handlers whose two\n * horizontal branches are **not mirror images**. A grid that clamps one edge but\n * not the other, or a segmented field guarding `index > 0` on one side and\n * `index < length - 1` on the other, ends up applying the wrong guard to the\n * wrong direction. Swapping the key leaves each branch, guards and all, exactly\n * where its author put it.\n *\n * Same rule as {@link logicalArrowStep} about which element to read: pass the\n * container that lays the items out, not the focused child.\n *\n * @example\n * ```ts\n * switch (logicalArrowKey(event.key, this.element)) {\n * case \"ArrowLeft\": // \"previous\" — whatever direction that is on screen\n * ```\n */\nexport function logicalArrowKey(key: string, element: Element): string {\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return key;\n if (!isRtl(element)) return key;\n return key === \"ArrowRight\" ? \"ArrowLeft\" : \"ArrowRight\";\n}\n\n/** Modifiers a widget may claim on an arrow key, named for the `allow` list. */\nexport type ArrowModifier = \"alt\" | \"ctrl\" | \"meta\" | \"shift\";\n\n/**\n * True when an arrow key arrived carrying a modifier the widget must leave to\n * the browser: return without calling `preventDefault()` and without moving any\n * state.\n *\n * A bare arrow belongs to the widget; a chorded one usually does not.\n * `Alt`/`Meta` plus a horizontal arrow is history back/forward on every desktop\n * browser, and a widget that swallows it makes the shortcut work or not\n * depending on where focus happens to sit — a coin-flip the user cannot see.\n *\n * `allow` is for the combinations APG assigns to a pattern **and the widget\n * actually implements** — today only Combobox's optional `Alt+Down`/`Alt+Up`.\n * Listing one the widget does not implement defeats the point: the chord then\n * runs the plain-arrow branch, which is exactly what this guard exists to stop.\n * Non-arrow keys return `false`, so chorded letters and\n * `Control+Home`/`Control+End` are untouched.\n *\n * @example\n * ```ts\n * if (isReservedArrowChord(event)) return;\n * ```\n */\nexport function isReservedArrowChord(\n event: KeyboardEvent,\n allow: readonly ArrowModifier[] = [],\n): boolean {\n if (!event.key.startsWith(\"Arrow\")) return false;\n return (\n (event.altKey && !allow.includes(\"alt\")) ||\n (event.ctrlKey && !allow.includes(\"ctrl\")) ||\n (event.metaKey && !allow.includes(\"meta\")) ||\n (event.shiftKey && !allow.includes(\"shift\"))\n );\n}\n\n/**\n * True when a press arrived carrying any modifier, for the keys the browser and\n * the OS own outright: return without calling `preventDefault()` and without\n * moving any state.\n *\n * `Control+Home` and `Control+End` jump the document to its ends, and a widget\n * that swallows them makes the shortcut work or not depending on where focus\n * happens to sit. {@link isReservedArrowChord} answers the same question for the\n * arrows, but returns `false` for every other key so that this one can decide.\n *\n * There is no `allow` list here on purpose. APG assigns no modifier chord to\n * `Home`/`End`, so a widget that wanted one would be claiming a combination the\n * pattern never gave it.\n *\n * @example\n * ```ts\n * case \"Home\":\n * case \"End\":\n * if (hasModifierChord(event)) return;\n * ```\n */\nexport function hasModifierChord(event: KeyboardEvent): boolean {\n return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;\n}\n","/**\n * Readers for the String Values whose text can only be checked by handing it to\n * a parser: a CSS selector, a regular-expression source, a JSON object.\n *\n * Stimulus reads each of them as an ordinary string, so the controller connects\n * and the flaw surfaces later, inside the first handler that consumes the value\n * (a `SyntaxError` from `new RegExp`, a `DOMException` from the selector engine),\n * where it takes the whole handler down on every event. Parsing here, once, in\n * the `<name>ValueChanged` callback keeps the failure local to the declaration:\n * an unreadable value falls back to its default, the element stays alive, and\n * the hot path only ever receives a value that parsed.\n *\n * Which default a broken declaration falls back to is the caller's contract, so\n * every reader takes (or returns) the fallback rather than choosing one.\n */\n\n/**\n * The result of `parse(raw)`, or `fallback` when `parse` throws.\n *\n * Any exception counts as \"does not parse\": the parsers these Values feed\n * (`RegExp`, `JSON.parse`, the selector engine) all report a malformed input by\n * throwing, and none of them throws for another reason.\n */\nexport function parseDeclared<T>(raw: string, parse: (raw: string) => T, fallback: T): T {\n try {\n return parse(raw);\n } catch {\n return fallback;\n }\n}\n\n/** What {@link validSelector} needs of the element it probes with. */\nexport interface SelectorProbe {\n matches(selector: string): boolean;\n}\n\n/**\n * `raw` when it is a non-empty selector the DOM accepts, otherwise `fallback`.\n *\n * `element.matches` is the probe: the engine parses the selector and throws for\n * one it cannot read. Whether the selector actually matches `element` plays no\n * part, so a selector aimed at another element still passes.\n *\n * The parameter names the one method the probe uses rather than `Element`, so\n * this module carries no DOM type and the readers below stay importable from\n * the Node-side Inspector, which checks the same declarations statically.\n */\nexport function validSelector(element: SelectorProbe, raw: string, fallback: string): string {\n if (raw.length === 0) return fallback;\n return parseDeclared(\n raw,\n (selector) => {\n element.matches(selector);\n return selector;\n },\n fallback,\n );\n}\n\n/** How `compileRegExp` wraps a source before compiling it. */\nexport type RegExpAnchor = \"none\" | \"exact\";\n\n/**\n * `source` compiled as a `RegExp`, or `null` when it does not compile.\n *\n * `\"exact\"` wraps the source as `^(?:source)$`, so the whole input has to match\n * and an alternation inside the source cannot escape the anchors. The source is\n * compiled on its own first, because an unbalanced source can be made to parse\n * by the wrapper's own parentheses — `0)|(1` becomes `^(?:0)|(1)$` — which would\n * accept a declaration that is not a regular expression and leave half of it\n * outside the anchors. The default that replaces a broken source is the\n * caller's, so `null` is returned rather than a fallback pattern.\n */\nexport function compileRegExp(source: string, anchor: RegExpAnchor = \"none\"): RegExp | null {\n return parseDeclared(\n source,\n (text) => {\n const bare = new RegExp(text);\n return anchor === \"exact\" ? new RegExp(`^(?:${text})$`) : bare;\n },\n null,\n );\n}\n\n/**\n * The JSON object `raw` declares, or `null` when the text does not parse or\n * parses to something other than a plain object (`null`, an array, a scalar).\n * Values are returned as parsed; narrowing them is the caller's contract.\n */\nexport function parseJsonObject(raw: string): Record<string, unknown> | null {\n const parsed = parseDeclared<unknown>(raw, (text) => JSON.parse(text), null);\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) return null;\n return parsed as Record<string, unknown>;\n}\n","/**\n * Low-level roving-tabindex primitive shared by composite-widget controllers.\n *\n * The APG roving-tabindex pattern keeps a composite widget a single Tab stop:\n * exactly one item is in the Tab sequence (`tabindex=\"0\"`) while the rest are\n * removed from it (`tabindex=\"-1\"`), and the arrow keys move both DOM focus and\n * that single tabbable position together. {@link RovingTabindex} owns *only* that\n * mechanical bookkeeping — \"which one item is tabbable, and move focus there\".\n *\n * It is intentionally **policy-free**. Orientation, wrapping vs. clamping,\n * selection-follows-focus, typeahead, and `Home`/`End` semantics differ per APG\n * pattern (Radio Group, Toolbar, Rating, …); folding them into one helper would\n * flatten those widgets to a lowest common denominator and lose each pattern's\n * correctness. Those decisions therefore stay in each controller, which calls\n * {@link RovingTabindex.setActive} with an index it computed itself (optionally\n * via the pure {@link rovingMove} helper).\n *\n * @remarks\n * Items are read lazily through a getter so a controller can add or remove\n * targets (Stimulus re-scans the DOM) without re-wiring this helper. A caller\n * that already captured one event-scoped snapshot may pass it to\n * {@link RovingTabindex.setActive}, avoiding a second live-target scan while\n * preserving one coherent collection for the whole operation.\n */\nexport class RovingTabindex {\n /** Returns the current ordered item elements; called on every operation. */\n readonly #getItems: () => HTMLElement[];\n\n /**\n * @param getItems - Returns the current ordered item elements. Called on every\n * operation so the live target list is always used.\n */\n constructor(getItems: () => HTMLElement[]) {\n this.#getItems = getItems;\n }\n\n /** Index of the currently tabbable item (`tabindex=\"0\"`), or `-1` if none. */\n get activeIndex(): number {\n return this.#getItems().findIndex((item) => item.tabIndex === 0);\n }\n\n /**\n * Makes exactly the item at `index` tabbable (`tabindex=\"0\"`) and removes every\n * other item from the Tab sequence (`tabindex=\"-1\"`). An out-of-range `index`\n * (e.g. `-1`) leaves all items at `-1`, which a controller can use to express\n * \"nothing is currently tabbable\".\n *\n * @param index - Position of the item to make tabbable.\n * @param options - Pass `{ focus: true }` to also move DOM focus to that item,\n * and `items` to reuse an event-scoped collection snapshot.\n */\n setActive(\n index: number,\n options: { focus?: boolean; items?: readonly HTMLElement[] } = {},\n ): void {\n const { focus = false } = options;\n const items = options.items ?? this.#getItems();\n items.forEach((item, i) => {\n item.tabIndex = i === index ? 0 : -1;\n });\n if (focus) items[index]?.focus();\n }\n}\n\n/** Edge behavior for {@link rovingMove}: cycle past the ends, or stop at them. */\nexport type RovingWrap = \"wrap\" | \"clamp\";\n\n/**\n * Pure helper that resolves the target index for a one-step directional move.\n *\n * Keyboard/orientation mapping stays in the caller: it decides that a key means\n * `delta` `+1` (next) or `-1` (previous) and whether the widget should `\"wrap\"`\n * (Radio Group, Toolbar) or `\"clamp\"` at the ends.\n *\n * @param current - The index focus is moving from.\n * @param length - Number of items in the set.\n * @param delta - `+1` to move to the next item, `-1` for the previous.\n * @param wrap - `\"wrap\"` cycles around the ends; `\"clamp\"` stops at them.\n * @returns The resolved index, or `-1` when there are no items.\n */\nexport function rovingMove(\n current: number,\n length: number,\n delta: number,\n wrap: RovingWrap,\n): number {\n if (length === 0) return -1;\n const next = current + delta;\n if (wrap === \"wrap\") return (next + length) % length;\n return Math.min(length - 1, Math.max(0, next));\n}\n","/** A successful guarded storage operation. */\nexport interface StorageSuccess<T> {\n readonly ok: true;\n readonly value: T;\n}\n\n/** A guarded storage operation rejected by the browser. */\nexport interface StorageFailure {\n readonly ok: false;\n readonly error: unknown;\n}\n\n/** Result of accessing browser storage without allowing an exception to escape. */\nexport type StorageResult<T> = StorageSuccess<T> | StorageFailure;\n\n/**\n * Guarded `localStorage` access shared by controllers that persist client state.\n *\n * `window.localStorage` can throw while resolving the storage object or while\n * reading, writing, or removing a key. The result keeps an unavailable store\n * distinct from an available store whose key is unset, so callers can expose an\n * accurate success or failure state without duplicating exception guards.\n */\n\n/**\n * Reads `key` from `localStorage`.\n *\n * @param key - The storage key to read.\n * @returns A successful result containing the stored string or `null`, or a\n * failure result when storage is unavailable.\n */\nexport function readLocalStorage(key: string): StorageResult<string | null> {\n try {\n return { ok: true, value: window.localStorage.getItem(key) };\n } catch (error: unknown) {\n return { ok: false, error };\n }\n}\n\n/**\n * Writes `value` under `key` in `localStorage`.\n *\n * @param key - The storage key to write.\n * @param value - The string to store.\n * @returns Success only after `setItem` completes; otherwise the browser error.\n */\nexport function writeLocalStorage(key: string, value: string): StorageResult<void> {\n try {\n window.localStorage.setItem(key, value);\n return { ok: true, value: undefined };\n } catch (error: unknown) {\n return { ok: false, error };\n }\n}\n\n/**\n * Removes `key` from `localStorage`.\n *\n * @param key - The storage key to remove.\n * @returns Success only after `removeItem` completes; otherwise the browser error.\n */\nexport function removeLocalStorage(key: string): StorageResult<void> {\n try {\n window.localStorage.removeItem(key);\n return { ok: true, value: undefined };\n } catch (error: unknown) {\n return { ok: false, error };\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { hasModifierChord, isReservedArrowChord, logicalArrowStep } from \"../utils/arrow_step\";\nimport { validSelector } from \"../utils/declared_value\";\nimport { RovingTabindex, rovingMove } from \"../utils/roving_tabindex\";\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/** The mode a declaration falls back to when it cannot be read as one. */\nconst DEFAULT_MODE: ThemeMode = \"system\";\n/** The state-hook target a declaration falls back to when it cannot be parsed. */\nconst DEFAULT_TARGET = \"html\";\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-value=\"light\">Light</button>\n * <button data-stimeo--theme-target=\"option\" role=\"radio\"\n * data-action=\"click->stimeo--theme#set\"\n * data-value=\"dark\">Dark</button>\n * <button data-stimeo--theme-target=\"option\" role=\"radio\"\n * data-action=\"click->stimeo--theme#set\"\n * data-value=\"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 * `change` dispatches `{ mode, resolved }`, and only when one of the two moved.\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. Arrow/Home/End move focus across the radiogroup as the APG radio pattern\n * requires; applying a theme 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, not this controller.\n *\n * Every declaration is validated where it enters, and an unreadable one falls back\n * to that Value's default rather than taking the widget with it: a `mode` outside\n * the three modes reads as `system`, and a `target` that is not a parsable selector\n * reads as `html`. An option whose `data-value` is outside the three can be focused\n * but never becomes the selection, so no more than one option is ever checked —\n * and none at all when the resolved mode matches no option, where the first one\n * keeps the Tab stop.\n *\n * The radiogroup stays one Tab stop through `RovingTabindex`, re-derived\n * whenever an option enters or leaves, so a set rendered after connect or swapped\n * by a Turbo morph carries the same single stop as any other.\n */\nexport class ThemeController extends Controller<HTMLElement> {\n static override targets = [\"option\"];\n static override values = {\n mode: { type: String, default: DEFAULT_MODE },\n storageKey: { type: String, default: \"stimeo-theme\" },\n target: { type: String, default: DEFAULT_TARGET },\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 /** Gate for the target callbacks, which Stimulus runs before `connect()`. */\n #connected = false;\n\n /** The `target` declaration after validation; the default when unparsable. */\n #targetSelector = DEFAULT_TARGET;\n\n /** Owns the single Tab stop across the option set (APG radiogroup). */\n readonly #roving = new RovingTabindex(() => this.optionTargets);\n\n /**\n * The pair last reported, so a move can be told from a repeat. Neither side is\n * readable after the fact — assigning the Value updates the mode before any\n * comparison, and the OS query has already flipped by the time it notifies —\n * so what was reported has to be kept rather than recomputed.\n */\n #published: { mode: ThemeMode; resolved: ResolvedTheme } = {\n mode: DEFAULT_MODE,\n resolved: \"light\",\n };\n\n /** Re-resolves while in `system` mode when the OS preference flips. */\n readonly #onMediaChange = (): void => {\n // Only `system` follows the OS; an explicit mode already decided the answer.\n if (this.#mode !== \"system\") return;\n this.#commit();\n };\n\n /** Arrow/Home/End navigation for the radiogroup (APG radio pattern). */\n readonly #onKeydown = (event: KeyboardEvent): void => {\n // A descendant widget that already claimed the key (a grabbed drag handle, a\n // nested menu) must not ALSO act on it — composition depends on this yield.\n if (event.defaultPrevented) return;\n if (isReservedArrowChord(event)) return;\n // Resolved once per keydown: every `optionTargets` access re-queries the scope.\n const options = this.optionTargets;\n const target = event.target as HTMLElement | null;\n const current = options.indexOf(target as HTMLElement);\n if (current === -1) return;\n\n let next = current;\n // Logical, not physical. The helper reverses only the horizontal pair, so\n // folding Down/Up into the same branch stays correct.\n const step = logicalArrowStep(event.key, this.element);\n switch (event.key) {\n case \"ArrowDown\":\n case \"ArrowRight\":\n case \"ArrowUp\":\n case \"ArrowLeft\":\n next = rovingMove(current, options.length, step, \"wrap\");\n break;\n case \"Home\":\n case \"End\":\n // Control+Home jumps the document, not the widget.\n if (hasModifierChord(event)) return;\n next = event.key === \"Home\" ? 0 : options.length - 1;\n break;\n default:\n return;\n }\n event.preventDefault();\n const option = options[next];\n if (!option) return;\n option.focus();\n const mode = this.#optionMode(option);\n // An option outside the three modes can hold focus, but the selection does\n // not follow it there.\n if (mode) this.#setMode(mode);\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 this.element.addEventListener(\"keydown\", this.#onKeydown);\n\n this.#connected = true;\n this.#applyTheme();\n this.#syncControls();\n // Connecting is not a change: seed the baseline instead of announcing one.\n this.#published = this.#current;\n }\n\n override disconnect(): void {\n this.#connected = false;\n this.#media?.removeEventListener(\"change\", this.#onMediaChange);\n this.element.removeEventListener(\"keydown\", this.#onKeydown);\n }\n\n /** Validates the `target` declaration once, so the render path never parses. */\n targetValueChanged(): void {\n this.#targetSelector = validSelector(this.element, this.targetValue, DEFAULT_TARGET);\n }\n\n /** Re-derives the single Tab stop and ARIA for an option set that changed. */\n optionTargetConnected(): void {\n if (this.#connected) this.#syncControls();\n }\n\n /** Re-derives them again when an option leaves, so a Tab stop always remains. */\n optionTargetDisconnected(): void {\n if (this.#connected) this.#syncControls();\n }\n\n /**\n * Selects the mode the activated option declares.\n *\n * Read through the same lane that decides which option is checked, so the two\n * can never disagree about what an option declares.\n */\n set(event: Event): void {\n const option = event.currentTarget;\n if (!(option instanceof HTMLElement)) return;\n const mode = this.#optionMode(option);\n if (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.#commit();\n }\n\n /**\n * Applies the current mode and reports it, but reports only a real move: the\n * event means \"the selection or the effective theme moved\", so re-choosing the\n * option already chosen is not one.\n */\n #commit(): void {\n this.#applyTheme();\n this.#syncControls();\n const next = this.#current;\n const last = this.#published;\n this.#published = next;\n if (last.mode !== next.mode || last.resolved !== next.resolved) {\n // A copy: the baseline has to survive a listener that writes to what it\n // was handed, or the next unchanged operation reads as a change.\n this.dispatch(\"change\", { detail: { ...next } });\n }\n }\n\n /** The pair the `change` detail carries, read from current state. */\n get #current(): { mode: ThemeMode; resolved: ResolvedTheme } {\n return { mode: this.#mode, resolved: this.#resolved() };\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 const mode = this.#mode;\n let selected = -1;\n options.forEach((option, index) => {\n const isSelected = this.#optionMode(option) === mode;\n option.setAttribute(\"aria-checked\", String(isSelected));\n if (isSelected && selected === -1) selected = index;\n });\n // APG: a radiogroup with no selection keeps its first radio tabbable.\n this.#roving.setActive(selected === -1 ? 0 : selected, { items: options });\n return;\n }\n // `aria-pressed` belongs to the 2-value contract, where the controller sits on\n // the button itself. A radiogroup whose options have not rendered yet is not\n // that, and must not be told it is a toggle.\n if (this.#isToggleButton) {\n this.element.setAttribute(\"aria-pressed\", String(this.#resolved() === \"dark\"));\n }\n }\n\n /** Whether the controller element is the button of the 2-value contract. */\n get #isToggleButton(): boolean {\n return this.element.tagName === \"BUTTON\" || this.element.getAttribute(\"role\") === \"button\";\n }\n\n /** The selected mode after validation; an unreadable declaration is the default. */\n get #mode(): ThemeMode {\n return isMode(this.modeValue) ? this.modeValue : DEFAULT_MODE;\n }\n\n /** The effective theme: the OS preference when `system`, else the mode itself. */\n #resolved(): ResolvedTheme {\n const mode = this.#mode;\n if (mode === \"dark\") return \"dark\";\n if (mode === \"light\") return \"light\";\n return this.#media?.matches ? \"dark\" : \"light\";\n }\n\n /** An option's mode from its `data-value`, or `null` when that is not one of the three. */\n #optionMode(option: HTMLElement): ThemeMode | null {\n const mode = option.getAttribute(\"data-value\");\n return isMode(mode) ? mode : null;\n }\n\n /** Resolves the state-hook target (`<html>` by default). */\n #targetElement(): HTMLElement | null {\n const selector = this.#targetSelector;\n if (selector === DEFAULT_TARGET || selector === \":root\") return document.documentElement;\n return document.querySelector<HTMLElement>(selector);\n }\n\n /** Reads a persisted, validated mode from `localStorage` (null when absent/blocked). */\n #readStored(): ThemeMode | null {\n const result = readLocalStorage(this.storageKeyValue);\n return result.ok && isMode(result.value) ? result.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"]}
@@ -101,7 +101,7 @@ var ToastController = class extends Controller {
101
101
  static events = ["dismiss", "show"];
102
102
  /**
103
103
  * Registry for every auto-dismiss and transition-finalize timer the controller
104
- * schedules. {@link SafeTimeout} owns *registration and teardown only*; the
104
+ * schedules. `SafeTimeout` owns *registration and teardown only*; the
105
105
  * pause/resume remaining-time accounting stays in `#activeTimeouts` so the
106
106
  * per-widget WCAG 2.2.1 semantics are not flattened into the helper.
107
107
  */