stimeo-ui 0.8.0 → 0.10.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 (167) hide show
  1. package/CHANGELOG.md +163 -0
  2. package/dist/controllers/accordion_controller.js.map +1 -1
  3. package/dist/controllers/alert_dialog_controller.js +17 -7
  4. package/dist/controllers/alert_dialog_controller.js.map +1 -1
  5. package/dist/controllers/announcer_controller.js +1 -1
  6. package/dist/controllers/announcer_controller.js.map +1 -1
  7. package/dist/controllers/auto_submit_controller.js.map +1 -1
  8. package/dist/controllers/avatar_controller.js.map +1 -1
  9. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  10. package/dist/controllers/bulk_select_controller.d.ts +62 -15
  11. package/dist/controllers/bulk_select_controller.js +139 -28
  12. package/dist/controllers/bulk_select_controller.js.map +1 -1
  13. package/dist/controllers/calendar_controller.js.map +1 -1
  14. package/dist/controllers/carousel_controller.js.map +1 -1
  15. package/dist/controllers/character_counter_controller.js.map +1 -1
  16. package/dist/controllers/clipboard_controller.d.ts +48 -15
  17. package/dist/controllers/clipboard_controller.js +102 -20
  18. package/dist/controllers/clipboard_controller.js.map +1 -1
  19. package/dist/controllers/collapsible_controller.d.ts +1 -1
  20. package/dist/controllers/collapsible_controller.js +1 -1
  21. package/dist/controllers/collapsible_controller.js.map +1 -1
  22. package/dist/controllers/color_picker_controller.d.ts +37 -6
  23. package/dist/controllers/color_picker_controller.js +180 -43
  24. package/dist/controllers/color_picker_controller.js.map +1 -1
  25. package/dist/controllers/combobox_controller.js.map +1 -1
  26. package/dist/controllers/command_palette_controller.js +15 -5
  27. package/dist/controllers/command_palette_controller.js.map +1 -1
  28. package/dist/controllers/conditional_fields_controller.js.map +1 -1
  29. package/dist/controllers/confirm_controller.js +15 -5
  30. package/dist/controllers/confirm_controller.js.map +1 -1
  31. package/dist/controllers/context_menu_controller.d.ts +5 -4
  32. package/dist/controllers/context_menu_controller.js.map +1 -1
  33. package/dist/controllers/count_up_controller.js.map +1 -1
  34. package/dist/controllers/countdown_controller.js +1 -1
  35. package/dist/controllers/countdown_controller.js.map +1 -1
  36. package/dist/controllers/data_grid_controller.d.ts +25 -9
  37. package/dist/controllers/data_grid_controller.js +151 -25
  38. package/dist/controllers/data_grid_controller.js.map +1 -1
  39. package/dist/controllers/date_range_picker_controller.js.map +1 -1
  40. package/dist/controllers/dialog_controller.js +15 -5
  41. package/dist/controllers/dialog_controller.js.map +1 -1
  42. package/dist/controllers/direct_upload_controller.d.ts +2 -2
  43. package/dist/controllers/direct_upload_controller.js +1 -5
  44. package/dist/controllers/direct_upload_controller.js.map +1 -1
  45. package/dist/controllers/dismissible_controller.js.map +1 -1
  46. package/dist/controllers/drawer_controller.d.ts +2 -2
  47. package/dist/controllers/drawer_controller.js +17 -7
  48. package/dist/controllers/drawer_controller.js.map +1 -1
  49. package/dist/controllers/editable_controller.d.ts +34 -9
  50. package/dist/controllers/editable_controller.js +83 -30
  51. package/dist/controllers/editable_controller.js.map +1 -1
  52. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  53. package/dist/controllers/filter_controller.d.ts +15 -3
  54. package/dist/controllers/filter_controller.js +32 -1
  55. package/dist/controllers/filter_controller.js.map +1 -1
  56. package/dist/controllers/flash_controller.js.map +1 -1
  57. package/dist/controllers/focus_controller.d.ts +5 -0
  58. package/dist/controllers/focus_controller.js +45 -7
  59. package/dist/controllers/focus_controller.js.map +1 -1
  60. package/dist/controllers/form_field_controller.js +1 -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.js.map +1 -1
  64. package/dist/controllers/highlight_controller.js.map +1 -1
  65. package/dist/controllers/input_mask_controller.js.map +1 -1
  66. package/dist/controllers/intersection_controller.d.ts +4 -3
  67. package/dist/controllers/intersection_controller.js.map +1 -1
  68. package/dist/controllers/lazy_frame_controller.d.ts +2 -2
  69. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  70. package/dist/controllers/listbox_controller.js.map +1 -1
  71. package/dist/controllers/masonry_controller.d.ts +36 -5
  72. package/dist/controllers/masonry_controller.js +129 -17
  73. package/dist/controllers/masonry_controller.js.map +1 -1
  74. package/dist/controllers/menu_controller.d.ts +1 -1
  75. package/dist/controllers/menu_controller.js.map +1 -1
  76. package/dist/controllers/menubar_controller.js +2 -2
  77. package/dist/controllers/menubar_controller.js.map +1 -1
  78. package/dist/controllers/meter_controller.d.ts +1 -1
  79. package/dist/controllers/meter_controller.js.map +1 -1
  80. package/dist/controllers/multi_select_controller.js +1 -1
  81. package/dist/controllers/multi_select_controller.js.map +1 -1
  82. package/dist/controllers/navigation_menu_controller.js.map +1 -1
  83. package/dist/controllers/nested_form_controller.js.map +1 -1
  84. package/dist/controllers/number_input_controller.js.map +1 -1
  85. package/dist/controllers/otp_controller.d.ts +4 -1
  86. package/dist/controllers/otp_controller.js +29 -16
  87. package/dist/controllers/otp_controller.js.map +1 -1
  88. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  89. package/dist/controllers/overflow_menu_controller.js +2 -2
  90. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  91. package/dist/controllers/pagination_controller.d.ts +1 -1
  92. package/dist/controllers/pagination_controller.js.map +1 -1
  93. package/dist/controllers/password_reveal_controller.d.ts +27 -2
  94. package/dist/controllers/password_reveal_controller.js +102 -3
  95. package/dist/controllers/password_reveal_controller.js.map +1 -1
  96. package/dist/controllers/password_strength_controller.d.ts +71 -17
  97. package/dist/controllers/password_strength_controller.js +287 -42
  98. package/dist/controllers/password_strength_controller.js.map +1 -1
  99. package/dist/controllers/pointer_drag_controller.d.ts +3 -2
  100. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  101. package/dist/controllers/popover_controller.js.map +1 -1
  102. package/dist/controllers/portal_controller.d.ts +1 -1
  103. package/dist/controllers/portal_controller.js.map +1 -1
  104. package/dist/controllers/progress_controller.js.map +1 -1
  105. package/dist/controllers/radio_group_controller.js.map +1 -1
  106. package/dist/controllers/range_slider_controller.js.map +1 -1
  107. package/dist/controllers/rating_controller.js.map +1 -1
  108. package/dist/controllers/read_more_controller.js.map +1 -1
  109. package/dist/controllers/relative_time_controller.js.map +1 -1
  110. package/dist/controllers/reset_before_cache_controller.d.ts +26 -4
  111. package/dist/controllers/reset_before_cache_controller.js +51 -5
  112. package/dist/controllers/reset_before_cache_controller.js.map +1 -1
  113. package/dist/controllers/resizable_controller.d.ts +23 -7
  114. package/dist/controllers/resizable_controller.js +128 -55
  115. package/dist/controllers/resizable_controller.js.map +1 -1
  116. package/dist/controllers/roving_controller.d.ts +2 -1
  117. package/dist/controllers/roving_controller.js.map +1 -1
  118. package/dist/controllers/scroll_area_controller.js.map +1 -1
  119. package/dist/controllers/scroll_restore_controller.d.ts +33 -11
  120. package/dist/controllers/scroll_restore_controller.js +127 -27
  121. package/dist/controllers/scroll_restore_controller.js.map +1 -1
  122. package/dist/controllers/scroll_visibility_controller.d.ts +42 -10
  123. package/dist/controllers/scroll_visibility_controller.js +167 -14
  124. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  125. package/dist/controllers/scrollspy_controller.js.map +1 -1
  126. package/dist/controllers/separator_controller.js.map +1 -1
  127. package/dist/controllers/sidebar_controller.js +15 -5
  128. package/dist/controllers/sidebar_controller.js.map +1 -1
  129. package/dist/controllers/skeleton_controller.js.map +1 -1
  130. package/dist/controllers/slider_controller.js.map +1 -1
  131. package/dist/controllers/spinner_controller.js.map +1 -1
  132. package/dist/controllers/step_indicator_controller.js +0 -5
  133. package/dist/controllers/step_indicator_controller.js.map +1 -1
  134. package/dist/controllers/stick_to_bottom_controller.d.ts +3 -2
  135. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  136. package/dist/controllers/submit_once_controller.js.map +1 -1
  137. package/dist/controllers/switch_controller.js.map +1 -1
  138. package/dist/controllers/tabs_controller.js.map +1 -1
  139. package/dist/controllers/tags_input_controller.js.map +1 -1
  140. package/dist/controllers/textarea_autosize_controller.js.map +1 -1
  141. package/dist/controllers/theme_controller.d.ts +32 -6
  142. package/dist/controllers/theme_controller.js +151 -40
  143. package/dist/controllers/theme_controller.js.map +1 -1
  144. package/dist/controllers/time_picker_controller.js.map +1 -1
  145. package/dist/controllers/toast_controller.d.ts +2 -2
  146. package/dist/controllers/toast_controller.js +2 -2
  147. package/dist/controllers/toast_controller.js.map +1 -1
  148. package/dist/controllers/toggle_group_controller.js.map +1 -1
  149. package/dist/controllers/toolbar_controller.js.map +1 -1
  150. package/dist/controllers/transition_controller.d.ts +11 -7
  151. package/dist/controllers/transition_controller.js +79 -15
  152. package/dist/controllers/transition_controller.js.map +1 -1
  153. package/dist/controllers/tree_view_controller.js.map +1 -1
  154. package/dist/index.d.ts +6 -3
  155. package/dist/index.js +1612 -529
  156. package/dist/index.js.map +1 -1
  157. package/dist/inspector/cli.d.ts +3 -4
  158. package/dist/inspector/cli.js +3 -0
  159. package/dist/inspector/cli.js.map +1 -1
  160. package/dist/inspector/cli_bin.js +8 -5
  161. package/dist/inspector/cli_bin.js.map +1 -1
  162. package/dist/inspector/examples.json +25 -25
  163. package/dist/inspector/manifest.json +18 -11
  164. package/dist/positioning/index.d.ts +18 -4
  165. package/dist/positioning/index.js +84 -25
  166. package/dist/positioning/index.js.map +1 -1
  167. package/package.json +5 -5
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/arrow_step.ts","../../src/utils/microtask_coalescer.ts","../../src/utils/option_scroll.ts","../../src/utils/safe_timeout.ts","../../src/utils/typeahead.ts","../../src/controllers/listbox_controller.ts"],"names":[],"mappings":"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","file":"listbox_controller.js","sourcesContent":["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 * Collapses many Stimulus lifecycle callbacks from one DOM mutation into a\n * single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element and\n * `<name>ValueChanged` once per changed attribute. Replacing a list of N options\n * or morphing several render Values therefore delivers N callbacks — but the\n * useful unit of work is \"reconcile against the resulting declarative input\",\n * once, after the batch has settled. Every controller with reconcilable targets\n * or render Values needs the same shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers initial target and Value callbacks\n * ahead of `connect()`. Reconciling there would compute output against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\n }\n}\n","/**\n * Scroll-follow for virtually-focused listbox options.\n *\n * Listbox-style widgets that track their active option with\n * `aria-activedescendant` keep DOM focus on the input/trigger, so the\n * browser's native \"scroll the focused element into view\" never runs — in a\n * scrollable list (`max-height` + `overflow`) the active option can walk right\n * out of sight on ArrowDown/ArrowUp. This helper keeps it visible.\n *\n * It adjusts the LIST's own `scrollTop` only — deliberately not\n * `scrollIntoView({ block: \"nearest\" })`, which may also scroll ancestor\n * scrolling boxes (the page) when a floating popup pokes past a viewport\n * edge, desyncing anchored/marker-composed placements. (Command-palette uses\n * `scrollIntoView` because its page is scroll-locked behind the modal;\n * page-floating popups must use this helper instead.)\n */\n\n/**\n * Scrolls `option` into view within `list` by minimally adjusting\n * `list.scrollTop`. No-ops when the list does not actually scroll. Never\n * touches any other scrolling box.\n *\n * @param list - The scrollable listbox container.\n * @param option - The (virtually focused) option to keep visible.\n */\nexport function scrollOptionIntoView(list: HTMLElement, option: HTMLElement): void {\n if (list.scrollHeight <= list.clientHeight) return;\n const listRect = list.getBoundingClientRect();\n const optionRect = option.getBoundingClientRect();\n if (optionRect.top < listRect.top) {\n list.scrollTop -= listRect.top - optionRect.top;\n } else if (optionRect.bottom > listRect.bottom) {\n list.scrollTop += optionRect.bottom - listRect.bottom;\n }\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","/**\n * Shared type-ahead (first-letter navigation) primitive for composite widgets.\n *\n * The APG asks list-like widgets to move focus by typed characters: one\n * character jumps to the next item starting with it, and several characters\n * typed in quick succession narrow the match to that prefix. Getting that right\n * needs three pieces of bookkeeping — accumulating the query, dropping it after\n * an idle window, and resolving the label each candidate is matched against —\n * and single-sourcing them here keeps every widget on the same rules instead of\n * letting them drift apart.\n *\n * The helpers here own *only* that mechanical part. They are deliberately\n * **policy-free** in the same sense as `RovingTabindex`: which elements are\n * candidates, where the search starts, and what happens on a match (real DOM\n * focus, a roving tab stop, or a virtual `aria-activedescendant`) differ per APG\n * pattern, so each controller keeps those decisions and calls\n * {@link findTypeaheadMatch} with a candidate list it assembled itself.\n *\n * Three decisions are worth stating up front:\n *\n * - **A repeated character collapses the stored query**, rather than deriving a\n * shorter one at search time. That is what lets a follow-up character resume\n * narrowing: `s`, `s`, `e` searches `\"se\"`, not the dead `\"sse\"` that no label\n * can match. Growing the query instead leaves the widget frozen on exactly\n * that sequence, which is why the choice is made here and not per widget.\n * - **`aria-label` wins over text**, so type-ahead matches what a screen reader\n * announces — but only when it contributes a name. A blank (empty or\n * whitespace-only) one is skipped, because accname skips it too: honoring it\n * would leave an element that AT announces by its text unreachable by that\n * text. This mirrors the accessible-name computation rather than the raw text.\n * - **`Space` and composition input are never type-ahead.** `Space` natively\n * activates a `<button>`-based item, and characters still being composed are\n * not a committed query.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and contracts belong on the members.\n */\n\nimport { SafeTimeout } from \"./safe_timeout\";\n\n/** Idle window after which an accumulated query is dropped, in milliseconds. */\nexport const TYPEAHEAD_RESET_MS = 500;\n\n/** Construction options for {@link Typeahead}. */\nexport interface TypeaheadOptions {\n /** Idle window before the query resets. Defaults to {@link TYPEAHEAD_RESET_MS}. */\n resetMs?: number;\n}\n\n/**\n * Accumulates typed characters into a search query and drops it when idle.\n *\n * @remarks\n * Owns a {@link SafeTimeout} of its own, so a consumer's `#timers.clearAll()` does\n * **not** reach the pending reset — call {@link Typeahead.reset} from `disconnect()`.\n */\nexport class Typeahead {\n /** Timer registry for the pending idle reset; private so `reset()` is the only exit. */\n readonly #timers = new SafeTimeout();\n /** Idle window before the query resets, in milliseconds. */\n readonly #resetMs: number;\n /** The accumulated lowercase query, empty when idle. */\n #query = \"\";\n /** Id of the pending reset timer, `0` when none is scheduled. */\n #timerId = 0;\n\n /** @param options - Overrides for the idle window. */\n constructor({ resetMs = TYPEAHEAD_RESET_MS }: TypeaheadOptions = {}) {\n this.#resetMs = resetMs;\n }\n\n /** The query a search would currently run with; empty while idle. */\n get query(): string {\n return this.#query;\n }\n\n /**\n * Folds `key` into the query, restarts the idle window, and returns the query to\n * search with. A repeated character collapses the query to that one character.\n */\n push(key: string): string {\n const char = key.toLowerCase();\n const repeated = this.#query.length > 0 && [...this.#query].every((c) => c === char);\n this.#query = repeated ? char : this.#query + char;\n this.#timers.clear(this.#timerId);\n this.#timerId = this.#timers.set(() => this.reset(), this.#resetMs);\n return this.#query;\n }\n\n /** Clears the query and cancels the pending idle reset. */\n reset(): void {\n this.#query = \"\";\n this.#timers.clear(this.#timerId);\n this.#timerId = 0;\n }\n}\n\n/** Whether `event` is a bare printable character usable for type-ahead. */\nexport function isTypeaheadKey(event: KeyboardEvent): boolean {\n return (\n event.key.length === 1 &&\n event.key !== \" \" &&\n !event.ctrlKey &&\n !event.metaKey &&\n !event.altKey &&\n !event.isComposing\n );\n}\n\n/**\n * The name `element` is matched under, normalized for comparison.\n *\n * @param fallbackText - Produces the name when `aria-label` contributes none.\n * Defaults to the element's `textContent`; pass a narrower source when part of\n * the subtree is not part of the name (a tree item's nested child group, say).\n * Called lazily, so an `aria-label` costs no subtree walk.\n */\nexport function typeaheadLabel(element: HTMLElement, fallbackText?: () => string): string {\n // Blank means absent, matching accname: a whitespace-only `aria-label` is\n // skipped there too, so honoring it here would make an element that a screen\n // reader announces by its text unreachable by that text.\n const label = element.getAttribute(\"aria-label\")?.trim();\n if (label) return label.toLowerCase();\n const text = fallbackText ? fallbackText() : (element.textContent ?? \"\");\n return text.trim().toLowerCase();\n}\n\n/**\n * Index of the first item after `from` whose label starts with `query`, wrapping\n * and evaluating `from` itself last, or `-1` when nothing matches.\n *\n * @param items - Candidates in navigation order, already filtered by the caller.\n * @param from - Index the search moves on from; `-1` starts at the first item. Any\n * out-of-range value is folded back in, so `indexOf` results pass straight through.\n * @param query - Lowercase query, normally the return of {@link Typeahead.push}.\n * @param label - Resolves a candidate's name. Defaults to {@link typeaheadLabel}.\n */\nexport function findTypeaheadMatch(\n items: readonly HTMLElement[],\n from: number,\n query: string,\n label: (item: HTMLElement) => string = (item) => typeaheadLabel(item),\n): number {\n // An empty query would prefix-match everything; an empty list never enters the\n // loop, so it needs no guard of its own.\n if (query === \"\") return -1;\n const count = items.length;\n for (let step = 1; step <= count; step += 1) {\n // Folded twice so a negative `from` still lands on a real slot: `%` keeps the\n // sign of its left operand, and a negative index would read `undefined`.\n const index = (((from + step) % count) + count) % count;\n const candidate = items[index];\n if (candidate && label(candidate).startsWith(query)) return index;\n }\n return -1;\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { isReservedArrowChord } from \"../utils/arrow_step\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\nimport { scrollOptionIntoView } from \"../utils/option_scroll\";\nimport { findTypeaheadMatch, isTypeaheadKey, Typeahead } from \"../utils/typeahead\";\n\n/**\n * Headless, accessible select-only listbox behavior.\n *\n * Markup contract (identifier: `stimeo--listbox`):\n * <div data-controller=\"stimeo--listbox\">\n * <span id=\"lb-label\">Favorite fruit</span>\n * <button type=\"button\" role=\"combobox\" aria-haspopup=\"listbox\"\n * aria-expanded=\"false\" aria-controls=\"lb-list\"\n * aria-labelledby=\"lb-label lb-value\"\n * data-stimeo--listbox-target=\"trigger\"\n * data-action=\"click->stimeo--listbox#toggle\n * keydown->stimeo--listbox#onTriggerKeydown\">\n * <span id=\"lb-value\" data-stimeo--listbox-target=\"value\">Choose…</span>\n * </button>\n * <ul id=\"lb-list\" role=\"listbox\" aria-label=\"Options\" hidden\n * data-stimeo--listbox-target=\"list\">\n * <li id=\"opt-1\" role=\"option\" aria-selected=\"false\" data-value=\"1\"\n * data-stimeo--listbox-target=\"option\"\n * data-action=\"click->stimeo--listbox#select\">Option 1</li>\n * <!-- more options -->\n * </ul>\n * <input type=\"hidden\" data-stimeo--listbox-target=\"field\" />\n * </div>\n *\n * Implements the WAI-ARIA APG **Listbox** pattern in its collapsed\n * (Select-Only Combobox) form. Focus stays on the trigger; the active option is\n * tracked with `aria-activedescendant` rather than by moving DOM focus. For a\n * text-filtered popup use {@link ComboboxController | Combobox}; for multiple\n * selection use Multi-Select.\n *\n * `change` dispatches `{ value: string, option: HTMLElement }`.\n *\n * @remarks\n * Behavior only. Static placement is the consumer's CSS; dynamic placement is\n * delegated to the opt-in `stimeo-ui/positioning` module. The look is keyed off\n * `aria-selected` / `data-active`. Because `role=\"combobox\"` is not named by its\n * contents, give the trigger an accessible name via `aria-labelledby` (a visible\n * label plus the value span) or `aria-label`.\n *\n * Behavior provided:\n * - Open/close the list, syncing `aria-expanded` and the list's `hidden`.\n * - `ArrowDown`/`ArrowUp` (wrapping), `Home`/`End`, and printable-character\n * typeahead move the active option; opening picks the selected option (else\n * the first).\n * - Single selection syncs `aria-selected`, reflects the label into the trigger\n * value and the field's value, and dispatches `stimeo--listbox:change` plus a\n * native bubbling `change` on the field when its value actually changed.\n * Making the field a validatable mirror (`<input type=\"text\" hidden required>`\n * instead of `type=\"hidden\"`) lets `stimeo--form-validation` enforce native\n * constraints on the committed value with no extra JavaScript.\n * - `Enter`/`Space` select and close; `Escape` and outside click / `Tab` close;\n * closing via select/Escape returns focus to the trigger.\n */\nexport class ListboxController extends Controller<HTMLElement> {\n static override targets = [\"trigger\", \"value\", \"list\", \"option\", \"field\"];\n static actions = [\"close\", \"onTriggerKeydown\", \"open\", \"select\", \"toggle\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly triggerTarget: HTMLElement;\n declare readonly valueTarget: HTMLElement;\n declare readonly listTarget: HTMLElement;\n declare readonly optionTargets: HTMLElement[];\n declare readonly fieldTarget: HTMLInputElement;\n declare readonly hasTriggerTarget: boolean;\n declare readonly hasValueTarget: boolean;\n declare readonly hasListTarget: boolean;\n declare readonly hasFieldTarget: boolean;\n\n /** Stable ID of the active option; DOM targets are resolved afresh before use. */\n #activeId: string | null = null;\n /** Target ID order captured while an option is active, used only for removal fallback. */\n #activeOrder: string[] = [];\n #connected = false;\n /** Collapses one mutation batch of target callbacks into a single pass. */\n readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileActive());\n /** Accumulated typeahead query and its idle-reset timer. */\n readonly #typeahead = new Typeahead();\n\n /** Establishes the ARIA baseline, starts closed, and listens for outside clicks. */\n override connect(): void {\n this.#normalizeSelection();\n this.close();\n document.addEventListener(\"click\", this.#onOutsideClick, true);\n this.#connected = true;\n this.#reconcile.activate();\n }\n\n /**\n * Establishes an inactive baseline for a late option, re-resolves active\n * identity, and re-applies the selection baseline.\n */\n optionTargetConnected(option: HTMLElement): void {\n option.removeAttribute(\"data-active\");\n if (!this.#connected) return;\n this.#normalizeSelection();\n this.#queueOptionReconciliation();\n }\n\n /** Removes controller-owned active state and reconciles the surviving targets. */\n optionTargetDisconnected(option: HTMLElement): void {\n option.removeAttribute(\"data-active\");\n if (this.#connected) this.#queueOptionReconciliation();\n }\n\n /**\n * Brings the authored DOM to the shape the APG requires, and derives the state\n * that follows from the initial selection.\n *\n * Three things happen, and only these three — which option is chosen is the\n * author's, and is never changed:\n *\n * 1. Every option gets an explicit value. An absent `aria-selected` means \"not\n * selectable\" in ARIA, so a forgotten attribute hides a selectable option\n * from assistive technology.\n * 2. At most one stays `true`. The first in DOM order wins, since that is the\n * only deterministic reading of \"which one did the author mean\".\n * 3. The trigger label and the hidden field are derived from that selection.\n * Without this the widget announces a choice it does not submit: the popup\n * says \"Banana\", the trigger still says \"Choose…\", and the form posts \"\".\n *\n * No `change` fires — nothing changed, this is the initial state being told\n * properly. The scan is the `option` target set: a `role=\"option\"` without the\n * target is outside the contract and is neither counted nor written.\n */\n #normalizeSelection(): void {\n const options = this.optionTargets;\n const selected = options.find((option) => option.getAttribute(\"aria-selected\") === \"true\");\n for (const option of options) {\n option.setAttribute(\"aria-selected\", option === selected ? \"true\" : \"false\");\n }\n if (selected) this.#applySelection(selected);\n }\n\n /** Removes the document listener and clears the typeahead timer. */\n override disconnect(): void {\n this.#connected = false;\n this.#reconcile.cancel();\n document.removeEventListener(\"click\", this.#onOutsideClick, true);\n // The typeahead keeps its idle reset in a `SafeTimeout` of its own, so this is\n // the only teardown that reaches it — there is no controller-level registry to\n // fall back on. Every timer this controller can schedule lives in there.\n this.#typeahead.reset();\n }\n\n /**\n * Toggles the list on a real mouse click. Keyboard activation of the\n * `<button>` also fires a click (`detail === 0`); the keydown handler already\n * drives that, so the synthetic click is ignored to avoid double-toggling.\n */\n toggle(event: MouseEvent): void {\n if (event.detail === 0) return;\n if (this.#isClosed) {\n this.open();\n } else {\n this.close();\n }\n }\n\n /** Yields claimed keys; otherwise routes the APG select-only keyboard model. */\n onTriggerKeydown(event: KeyboardEvent): void {\n if (event.defaultPrevented) return;\n if (isReservedArrowChord(event)) return;\n if (!this.#isClosed) this.#reconcileActive();\n const options = this.optionTargets;\n const length = options.length;\n const activeIndex = this.#findActiveIndex(options);\n if (this.#isClosed) {\n switch (event.key) {\n case \"Enter\":\n case \" \":\n case \"ArrowDown\":\n case \"ArrowUp\":\n event.preventDefault();\n this.open();\n break;\n default:\n break;\n }\n return;\n }\n\n // With no options, only Escape/Tab are meaningful; ignore navigation and\n // typeahead so the active index can never become NaN (`% 0`).\n if (length === 0 && event.key !== \"Escape\" && event.key !== \"Tab\") {\n return;\n }\n\n switch (event.key) {\n case \"ArrowDown\":\n event.preventDefault();\n this.#setActive(activeIndex < 0 ? 0 : (activeIndex + 1) % length);\n break;\n case \"ArrowUp\":\n event.preventDefault();\n this.#setActive(activeIndex < 0 ? length - 1 : (activeIndex - 1 + length) % length);\n break;\n case \"Home\":\n event.preventDefault();\n this.#setActive(0);\n break;\n case \"End\":\n event.preventDefault();\n this.#setActive(length - 1);\n break;\n case \"Enter\":\n case \" \":\n event.preventDefault();\n this.#commitActive();\n break;\n case \"Escape\":\n // The entry guard already yields a press another handler owned. A press\n // during IME composition never dismisses, keeping one rule across the\n // widgets (the trigger is a button, so composition does not start here).\n if (event.isComposing) break;\n event.preventDefault();\n this.close();\n this.triggerTarget.focus();\n break;\n case \"Tab\":\n // Let focus leave naturally; just don't keep a stale popup open.\n this.close();\n break;\n default:\n if (isTypeaheadKey(event)) {\n event.preventDefault();\n this.#typeaheadTo(options, activeIndex, event.key);\n }\n break;\n }\n }\n\n /** Selects the clicked option and closes, returning focus to the trigger. */\n select(event: Event): void {\n const option = (event.currentTarget as HTMLElement).closest<HTMLElement>('[role=\"option\"]');\n if (!option || !this.optionTargets.includes(option)) return;\n this.#selectOption(option);\n this.close();\n this.triggerTarget.focus();\n }\n\n /** Opens the list and activates the selected option (else the first). */\n open(): void {\n if (!this.hasListTarget) return;\n this.listTarget.hidden = false;\n this.triggerTarget.setAttribute(\"aria-expanded\", \"true\");\n if (this.optionTargets.length === 0) {\n // An empty listbox has nothing to activate; leave activedescendant cleared.\n this.#setActive(-1);\n return;\n }\n const selected = this.optionTargets.findIndex(\n (option) => option.getAttribute(\"aria-selected\") === \"true\",\n );\n this.#setActive(selected === -1 ? 0 : selected);\n }\n\n /** Closes the list, clears the active option, and resets the typeahead buffer. */\n close(): void {\n if (!this.hasListTarget) return;\n this.listTarget.hidden = true;\n this.triggerTarget.setAttribute(\"aria-expanded\", \"false\");\n this.#setActive(-1);\n this.#typeahead.reset();\n }\n\n /** Commits the active option (keyboard) and closes, returning focus. */\n #commitActive(): void {\n this.#reconcileActive();\n const options = this.optionTargets;\n const activeIndex = this.#findActiveIndex(options);\n const option = activeIndex < 0 ? undefined : options[activeIndex];\n if (option) this.#selectOption(option);\n this.close();\n this.triggerTarget.focus();\n }\n\n /** Applies selection: `aria-selected`, trigger label, hidden field, `change`. */\n #selectOption(option: HTMLElement): void {\n const { value, fieldChanged } = this.#applySelection(option);\n if (fieldChanged) {\n // A native bubbling change (matching <select> semantics: only on an actual\n // value change) so form-level behaviors — validation re-checks, auto-submit\n // — hear the commit without knowing this widget.\n this.fieldTarget.dispatchEvent(new Event(\"change\", { bubbles: true }));\n }\n this.dispatch(\"change\", { detail: { value, option } });\n }\n\n /**\n * Writes `option` into `aria-selected`, the trigger label and the hidden field.\n * Emits nothing — {@link #normalizeSelection} reuses this at connect, where the\n * state is being described rather than changed.\n */\n #applySelection(option: HTMLElement): { value: string; fieldChanged: boolean } {\n for (const candidate of this.optionTargets) {\n candidate.setAttribute(\"aria-selected\", candidate === option ? \"true\" : \"false\");\n }\n const label = (option.textContent ?? \"\").trim();\n const value = option.dataset.value ?? label;\n if (this.hasValueTarget) this.valueTarget.textContent = label;\n const fieldChanged = this.hasFieldTarget && this.fieldTarget.value !== value;\n if (fieldChanged) this.fieldTarget.value = value;\n return { value, fieldChanged };\n }\n\n /**\n * Marks the option at `index` active via `data-active` and the trigger's\n * `aria-activedescendant`. Pass `-1` to clear it (the attribute is removed, not\n * set to empty, per the APG).\n */\n #setActive(index: number): void {\n const options = this.optionTargets;\n const active = index < 0 ? null : (options[index] ?? null);\n // Only the options whose marker actually changes are written, so a held arrow\n // key costs two attribute writes rather than one per option. The whole set is\n // still read: that is what makes a stray marker — one a morph left behind on\n // an element that never re-connected as a target — heal on the next move.\n for (const option of options) {\n const marked = option.hasAttribute(\"data-active\");\n if (option === active) {\n if (!marked) option.setAttribute(\"data-active\", \"\");\n } else if (marked) {\n option.removeAttribute(\"data-active\");\n }\n }\n if (active?.id) {\n this.#activeId = active.id;\n this.triggerTarget.setAttribute(\"aria-activedescendant\", active.id);\n } else {\n this.#activeId = null;\n this.triggerTarget.removeAttribute(\"aria-activedescendant\");\n }\n this.#activeOrder = active ? options.map((option) => option.id).filter(Boolean) : [];\n // Virtual focus never triggers the browser's native focus-scrolling, so a\n // scrollable list must follow the active option itself (list-only scroll).\n if (active && this.hasListTarget) scrollOptionIntoView(this.listTarget, active);\n }\n\n /** Resolves active state against the current target collection. */\n #reconcileActive(): void {\n if (!this.hasTriggerTarget || this.#isClosed) {\n if (this.hasTriggerTarget) this.#setActive(-1);\n return;\n }\n\n const options = this.optionTargets;\n const currentIndex = this.#findActiveIndex(options);\n if (currentIndex >= 0) {\n const active = options[currentIndex] ?? null;\n const marked = options.filter((option) => option.hasAttribute(\"data-active\"));\n const stateMatches =\n this.#activeId === (active?.id || null) &&\n marked.length === 1 &&\n marked[0] === active &&\n this.triggerTarget.getAttribute(\"aria-activedescendant\") === (active?.id || null);\n\n if (stateMatches) {\n // Keep the deletion fallback snapshot current without re-scrolling the\n // already-active option before every keyboard command.\n this.#activeOrder = options.map((option) => option.id).filter(Boolean);\n } else {\n this.#setActive(currentIndex);\n }\n return;\n }\n\n const activeId = this.triggerTarget.getAttribute(\"aria-activedescendant\") ?? this.#activeId;\n this.#setActive(activeId ? this.#findFallbackIndex(options, activeId) : -1);\n }\n\n /** Finds the live target carrying the stable ID, or the active marker for an ID-less option. */\n #findActiveIndex(options: readonly HTMLElement[]): number {\n const activeId =\n (this.hasTriggerTarget ? this.triggerTarget.getAttribute(\"aria-activedescendant\") : null) ??\n this.#activeId;\n if (activeId) return options.findIndex((option) => option.id === activeId);\n return options.findIndex((option) => option.hasAttribute(\"data-active\"));\n }\n\n /** Chooses a surviving former successor, then a former predecessor. */\n #findFallbackIndex(options: readonly HTMLElement[], activeId: string): number {\n const oldIndex = this.#activeOrder.indexOf(activeId);\n if (oldIndex < 0) return -1;\n const indexesById = new Map(options.map((option, index) => [option.id, index]));\n for (let index = oldIndex + 1; index < this.#activeOrder.length; index += 1) {\n const fallback = indexesById.get(this.#activeOrder[index] ?? \"\");\n if (fallback !== undefined) return fallback;\n }\n for (let index = oldIndex - 1; index >= 0; index -= 1) {\n const fallback = indexesById.get(this.#activeOrder[index] ?? \"\");\n if (fallback !== undefined) return fallback;\n }\n return -1;\n }\n\n /** Coalesces all target callbacks from one MutationObserver batch. */\n #queueOptionReconciliation(): void {\n this.#reconcile.schedule();\n }\n\n /**\n * Advances the typeahead query and activates the next matching option.\n *\n * The search resumes just after the active option so repeating a character\n * cycles through the options starting with it, rather than re-activating the\n * same first match on every press.\n */\n #typeaheadTo(options: HTMLElement[], activeIndex: number, char: string): void {\n const index = findTypeaheadMatch(options, activeIndex, this.#typeahead.push(char));\n if (index !== -1) this.#setActive(index);\n }\n\n /** Closes on an outside click before an inside handler can detach its target. */\n readonly #onOutsideClick = (event: MouseEvent): void => {\n if (!this.#isClosed && !this.element.contains(event.target as Node)) this.close();\n };\n\n /** Whether the list is currently hidden. */\n get #isClosed(): boolean {\n return !this.hasListTarget || this.listTarget.hidden !== false;\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/arrow_step.ts","../../src/utils/microtask_coalescer.ts","../../src/utils/option_scroll.ts","../../src/utils/safe_timeout.ts","../../src/utils/typeahead.ts","../../src/controllers/listbox_controller.ts"],"names":[],"mappings":";;;;;AA+FO,SAAS,oBAAA,CACd,KAAA,EACA,KAAA,GAAkC,EAAC,EAC1B;AACT,EAAA,IAAI,CAAC,KAAA,CAAM,GAAA,CAAI,UAAA,CAAW,OAAO,GAAG,OAAO,KAAA;AAC3C,EAAA,OACG,KAAA,CAAM,MAAA,IAAU,CAAC,KAAA,CAAM,QAAA,CAAS,KAAK,CAAA,IACrC,KAAA,CAAM,OAAA,IAAW,CAAC,KAAA,CAAM,QAAA,CAAS,MAAM,KACvC,KAAA,CAAM,OAAA,IAAW,CAAC,KAAA,CAAM,QAAA,CAAS,MAAM,CAAA,IACvC,KAAA,CAAM,QAAA,IAAY,CAAC,KAAA,CAAM,QAAA,CAAS,OAAO,CAAA;AAE9C;;;ACrDO,IAAM,qBAAN,MAAyB;AAAA,EACrB,IAAA;AAAA,EACT,OAAA,GAAU,KAAA;AAAA,EACV,OAAA,GAAU,KAAA;AAAA,EACV,WAAA,GAAc,CAAA;AAAA;AAAA,EAGd,YAAY,GAAA,EAAiB;AAC3B,IAAA,IAAA,CAAK,IAAA,GAAO,GAAA;AAAA,EACd;AAAA;AAAA,EAGA,QAAA,GAAiB;AACf,IAAA,IAAA,CAAK,OAAA,GAAU,IAAA;AAAA,EACjB;AAAA;AAAA,EAGA,MAAA,GAAe;AACb,IAAA,IAAA,CAAK,OAAA,GAAU,KAAA;AACf,IAAA,IAAA,CAAK,OAAA,GAAU,KAAA;AACf,IAAA,IAAA,CAAK,WAAA,IAAe,CAAA;AAAA,EACtB;AAAA;AAAA,EAGA,QAAA,GAAiB;AACf,IAAA,IAAI,CAAC,IAAA,CAAK,OAAA,IAAW,IAAA,CAAK,OAAA,EAAS;AACnC,IAAA,IAAA,CAAK,OAAA,GAAU,IAAA;AACf,IAAA,MAAM,aAAa,IAAA,CAAK,WAAA;AACxB,IAAA,cAAA,CAAe,MAAM;AAEnB,MAAA,IAAI,UAAA,KAAe,KAAK,WAAA,IAAe,CAAC,KAAK,OAAA,IAAW,CAAC,KAAK,OAAA,EAAS;AACvE,MAAA,IAAA,CAAK,OAAA,GAAU,KAAA;AACf,MAAA,IAAA,CAAK,IAAA,EAAK;AAAA,IACZ,CAAC,CAAA;AAAA,EACH;AACF,CAAA;;;AC/DO,SAAS,oBAAA,CAAqB,MAAmB,MAAA,EAA2B;AACjF,EAAA,IAAI,IAAA,CAAK,YAAA,IAAgB,IAAA,CAAK,YAAA,EAAc;AAC5C,EAAA,MAAM,QAAA,GAAW,KAAK,qBAAA,EAAsB;AAC5C,EAAA,MAAM,UAAA,GAAa,OAAO,qBAAA,EAAsB;AAChD,EAAA,IAAI,UAAA,CAAW,GAAA,GAAM,QAAA,CAAS,GAAA,EAAK;AACjC,IAAA,IAAA,CAAK,SAAA,IAAa,QAAA,CAAS,GAAA,GAAM,UAAA,CAAW,GAAA;AAAA,EAC9C,CAAA,MAAA,IAAW,UAAA,CAAW,MAAA,GAAS,QAAA,CAAS,MAAA,EAAQ;AAC9C,IAAA,IAAA,CAAK,SAAA,IAAa,UAAA,CAAW,MAAA,GAAS,QAAA,CAAS,MAAA;AAAA,EACjD;AACF;;;ACVA,IAAe,gBAAf,MAA6B;AAAA;AAAA,EAER,GAAA,uBAAU,GAAA,EAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAczC,MAAM,EAAA,EAAkB;AACtB,IAAA,IAAI,IAAA,CAAK,GAAA,CAAI,MAAA,CAAO,EAAE,CAAA,EAAG;AACvB,MAAA,IAAA,CAAK,OAAO,EAAE,CAAA;AAAA,IAChB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,QAAA,GAAiB;AACf,IAAA,KAAA,MAAW,EAAA,IAAM,KAAK,GAAA,EAAK;AACzB,MAAA,IAAA,CAAK,OAAO,EAAE,CAAA;AAAA,IAChB;AACA,IAAA,IAAA,CAAK,IAAI,KAAA,EAAM;AAAA,EACjB;AAAA;AAAA,EAGA,IAAI,IAAA,GAAe;AACjB,IAAA,OAAO,KAAK,GAAA,CAAI,IAAA;AAAA,EAClB;AACF,CAAA;AAmBO,IAAM,WAAA,GAAN,cAA0B,aAAA,CAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAO7C,GAAA,CAAI,UAAsB,KAAA,EAAuB;AAC/C,IAAA,MAAM,EAAA,GAAK,IAAA,CAAK,QAAA,CAAS,MAAM;AAC7B,MAAA,IAAA,CAAK,GAAA,CAAI,OAAO,EAAE,CAAA;AAClB,MAAA,QAAA,EAAS;AAAA,IACX,GAAG,KAAK,CAAA;AACR,IAAA,IAAA,CAAK,GAAA,CAAI,IAAI,EAAE,CAAA;AACf,IAAA,OAAO,EAAA;AAAA,EACT;AAAA,EAEU,QAAA,CAAS,UAAsB,KAAA,EAAuB;AAC9D,IAAA,OAAO,MAAA,CAAO,UAAA,CAAW,QAAA,EAAU,KAAK,CAAA;AAAA,EAC1C;AAAA,EAEU,OAAO,EAAA,EAAkB;AACjC,IAAA,MAAA,CAAO,aAAa,EAAE,CAAA;AAAA,EACxB;AACF,CAAA;;;AC7DO,IAAM,kBAAA,GAAqB,GAAA;AAe3B,IAAM,YAAN,MAAgB;AAAA;AAAA,EAEZ,OAAA,GAAU,IAAI,WAAA,EAAY;AAAA;AAAA,EAE1B,QAAA;AAAA;AAAA,EAET,MAAA,GAAS,EAAA;AAAA;AAAA,EAET,QAAA,GAAW,CAAA;AAAA;AAAA,EAGX,YAAY,EAAE,OAAA,GAAU,kBAAA,EAAmB,GAAsB,EAAC,EAAG;AACnE,IAAA,IAAA,CAAK,QAAA,GAAW,OAAA;AAAA,EAClB;AAAA;AAAA,EAGA,IAAI,KAAA,GAAgB;AAClB,IAAA,OAAO,IAAA,CAAK,MAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,KAAK,GAAA,EAAqB;AACxB,IAAA,MAAM,IAAA,GAAO,IAAI,WAAA,EAAY;AAC7B,IAAA,MAAM,QAAA,GAAW,IAAA,CAAK,MAAA,CAAO,MAAA,GAAS,KAAK,CAAC,GAAG,IAAA,CAAK,MAAM,CAAA,CAAE,KAAA,CAAM,CAAC,CAAA,KAAM,MAAM,IAAI,CAAA;AACnF,IAAA,IAAA,CAAK,MAAA,GAAS,QAAA,GAAW,IAAA,GAAO,IAAA,CAAK,MAAA,GAAS,IAAA;AAC9C,IAAA,IAAA,CAAK,OAAA,CAAQ,KAAA,CAAM,IAAA,CAAK,QAAQ,CAAA;AAChC,IAAA,IAAA,CAAK,QAAA,GAAW,KAAK,OAAA,CAAQ,GAAA,CAAI,MAAM,IAAA,CAAK,KAAA,EAAM,EAAG,IAAA,CAAK,QAAQ,CAAA;AAClE,IAAA,OAAO,IAAA,CAAK,MAAA;AAAA,EACd;AAAA;AAAA,EAGA,KAAA,GAAc;AACZ,IAAA,IAAA,CAAK,MAAA,GAAS,EAAA;AACd,IAAA,IAAA,CAAK,OAAA,CAAQ,KAAA,CAAM,IAAA,CAAK,QAAQ,CAAA;AAChC,IAAA,IAAA,CAAK,QAAA,GAAW,CAAA;AAAA,EAClB;AACF,CAAA;AAGO,SAAS,eAAe,KAAA,EAA+B;AAC5D,EAAA,OACE,MAAM,GAAA,CAAI,MAAA,KAAW,CAAA,IACrB,KAAA,CAAM,QAAQ,GAAA,IACd,CAAC,KAAA,CAAM,OAAA,IACP,CAAC,KAAA,CAAM,OAAA,IACP,CAAC,KAAA,CAAM,MAAA,IACP,CAAC,KAAA,CAAM,WAAA;AAEX;AAUO,SAAS,cAAA,CAAe,SAAsB,YAAA,EAAqC;AAIxF,EAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,YAAA,CAAa,YAAY,GAAG,IAAA,EAAK;AACvD,EAAA,IAAI,KAAA,EAAO,OAAO,KAAA,CAAM,WAAA,EAAY;AACpC,EAAA,MAAM,IAAA,GAAwC,QAAQ,WAAA,IAAe,EAAA;AACrE,EAAA,OAAO,IAAA,CAAK,IAAA,EAAK,CAAE,WAAA,EAAY;AACjC;AAYO,SAAS,kBAAA,CACd,OACA,IAAA,EACA,KAAA,EACA,QAAuC,CAAC,IAAA,KAAS,cAAA,CAAe,IAAI,CAAA,EAC5D;AAGR,EAAA,IAAI,KAAA,KAAU,IAAI,OAAO,EAAA;AACzB,EAAA,MAAM,QAAQ,KAAA,CAAM,MAAA;AACpB,EAAA,KAAA,IAAS,IAAA,GAAO,CAAA,EAAG,IAAA,IAAQ,KAAA,EAAO,QAAQ,CAAA,EAAG;AAG3C,IAAA,MAAM,KAAA,GAAA,CAAA,CAAW,IAAA,GAAO,IAAA,IAAQ,KAAA,GAAS,KAAA,IAAS,KAAA;AAClD,IAAA,MAAM,SAAA,GAAY,MAAM,KAAK,CAAA;AAC7B,IAAA,IAAI,aAAa,KAAA,CAAM,SAAS,EAAE,UAAA,CAAW,KAAK,GAAG,OAAO,KAAA;AAAA,EAC9D;AACA,EAAA,OAAO,EAAA;AACT;;;ACjGO,IAAM,iBAAA,GAAN,cAAgC,UAAA,CAAwB;AAAA,EAC7D,OAAgB,OAAA,GAAU,CAAC,WAAW,OAAA,EAAS,MAAA,EAAQ,UAAU,OAAO,CAAA;AAAA,EACxE,OAAO,OAAA,GAAU,CAAC,SAAS,kBAAA,EAAoB,MAAA,EAAQ,UAAU,QAAQ,CAAA;AAAA,EACzE,OAAO,MAAA,GAAS,CAAC,QAAQ,CAAA;AAAA;AAAA,EAazB,SAAA,GAA2B,IAAA;AAAA;AAAA,EAE3B,eAAyB,EAAC;AAAA,EAC1B,UAAA,GAAa,KAAA;AAAA;AAAA,EAEJ,aAAa,IAAI,kBAAA,CAAmB,MAAM,IAAA,CAAK,kBAAkB,CAAA;AAAA;AAAA,EAEjE,UAAA,GAAa,IAAI,SAAA,EAAU;AAAA;AAAA,EAG3B,OAAA,GAAgB;AACvB,IAAA,IAAA,CAAK,mBAAA,EAAoB;AACzB,IAAA,IAAA,CAAK,KAAA,EAAM;AACX,IAAA,QAAA,CAAS,gBAAA,CAAiB,OAAA,EAAS,IAAA,CAAK,eAAA,EAAiB,IAAI,CAAA;AAC7D,IAAA,IAAA,CAAK,UAAA,GAAa,IAAA;AAClB,IAAA,IAAA,CAAK,WAAW,QAAA,EAAS;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,sBAAsB,MAAA,EAA2B;AAC/C,IAAA,MAAA,CAAO,gBAAgB,aAAa,CAAA;AACpC,IAAA,IAAI,CAAC,KAAK,UAAA,EAAY;AACtB,IAAA,IAAA,CAAK,mBAAA,EAAoB;AACzB,IAAA,IAAA,CAAK,0BAAA,EAA2B;AAAA,EAClC;AAAA;AAAA,EAGA,yBAAyB,MAAA,EAA2B;AAClD,IAAA,MAAA,CAAO,gBAAgB,aAAa,CAAA;AACpC,IAAA,IAAI,IAAA,CAAK,UAAA,EAAY,IAAA,CAAK,0BAAA,EAA2B;AAAA,EACvD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAsBA,mBAAA,GAA4B;AAC1B,IAAA,MAAM,UAAU,IAAA,CAAK,aAAA;AACrB,IAAA,MAAM,QAAA,GAAW,QAAQ,IAAA,CAAK,CAAC,WAAW,MAAA,CAAO,YAAA,CAAa,eAAe,CAAA,KAAM,MAAM,CAAA;AACzF,IAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC5B,MAAA,MAAA,CAAO,YAAA,CAAa,eAAA,EAAiB,MAAA,KAAW,QAAA,GAAW,SAAS,OAAO,CAAA;AAAA,IAC7E;AACA,IAAA,IAAI,QAAA,EAAU,IAAA,CAAK,eAAA,CAAgB,QAAQ,CAAA;AAAA,EAC7C;AAAA;AAAA,EAGS,UAAA,GAAmB;AAC1B,IAAA,IAAA,CAAK,UAAA,GAAa,KAAA;AAClB,IAAA,IAAA,CAAK,WAAW,MAAA,EAAO;AACvB,IAAA,QAAA,CAAS,mBAAA,CAAoB,OAAA,EAAS,IAAA,CAAK,eAAA,EAAiB,IAAI,CAAA;AAIhE,IAAA,IAAA,CAAK,WAAW,KAAA,EAAM;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,OAAO,KAAA,EAAyB;AAC9B,IAAA,IAAI,KAAA,CAAM,WAAW,CAAA,EAAG;AACxB,IAAA,IAAI,KAAK,SAAA,EAAW;AAClB,MAAA,IAAA,CAAK,IAAA,EAAK;AAAA,IACZ,CAAA,MAAO;AACL,MAAA,IAAA,CAAK,KAAA,EAAM;AAAA,IACb;AAAA,EACF;AAAA;AAAA,EAGA,iBAAiB,KAAA,EAA4B;AAC3C,IAAA,IAAI,MAAM,gBAAA,EAAkB;AAC5B,IAAA,IAAI,oBAAA,CAAqB,KAAK,CAAA,EAAG;AACjC,IAAA,IAAI,CAAC,IAAA,CAAK,SAAA,EAAW,IAAA,CAAK,gBAAA,EAAiB;AAC3C,IAAA,MAAM,UAAU,IAAA,CAAK,aAAA;AACrB,IAAA,MAAM,SAAS,OAAA,CAAQ,MAAA;AACvB,IAAA,MAAM,WAAA,GAAc,IAAA,CAAK,gBAAA,CAAiB,OAAO,CAAA;AACjD,IAAA,IAAI,KAAK,SAAA,EAAW;AAClB,MAAA,QAAQ,MAAM,GAAA;AAAK,QACjB,KAAK,OAAA;AAAA,QACL,KAAK,GAAA;AAAA,QACL,KAAK,WAAA;AAAA,QACL,KAAK,SAAA;AACH,UAAA,KAAA,CAAM,cAAA,EAAe;AACrB,UAAA,IAAA,CAAK,IAAA,EAAK;AACV,UAAA;AAEA;AAEJ,MAAA;AAAA,IACF;AAIA,IAAA,IAAI,WAAW,CAAA,IAAK,KAAA,CAAM,QAAQ,QAAA,IAAY,KAAA,CAAM,QAAQ,KAAA,EAAO;AACjE,MAAA;AAAA,IACF;AAEA,IAAA,QAAQ,MAAM,GAAA;AAAK,MACjB,KAAK,WAAA;AACH,QAAA,KAAA,CAAM,cAAA,EAAe;AACrB,QAAA,IAAA,CAAK,WAAW,WAAA,GAAc,CAAA,GAAI,CAAA,GAAA,CAAK,WAAA,GAAc,KAAK,MAAM,CAAA;AAChE,QAAA;AAAA,MACF,KAAK,SAAA;AACH,QAAA,KAAA,CAAM,cAAA,EAAe;AACrB,QAAA,IAAA,CAAK,UAAA,CAAW,cAAc,CAAA,GAAI,MAAA,GAAS,KAAK,WAAA,GAAc,CAAA,GAAI,UAAU,MAAM,CAAA;AAClF,QAAA;AAAA,MACF,KAAK,MAAA;AACH,QAAA,KAAA,CAAM,cAAA,EAAe;AACrB,QAAA,IAAA,CAAK,WAAW,CAAC,CAAA;AACjB,QAAA;AAAA,MACF,KAAK,KAAA;AACH,QAAA,KAAA,CAAM,cAAA,EAAe;AACrB,QAAA,IAAA,CAAK,UAAA,CAAW,SAAS,CAAC,CAAA;AAC1B,QAAA;AAAA,MACF,KAAK,OAAA;AAAA,MACL,KAAK,GAAA;AACH,QAAA,KAAA,CAAM,cAAA,EAAe;AACrB,QAAA,IAAA,CAAK,aAAA,EAAc;AACnB,QAAA;AAAA,MACF,KAAK,QAAA;AAIH,QAAA,IAAI,MAAM,WAAA,EAAa;AACvB,QAAA,KAAA,CAAM,cAAA,EAAe;AACrB,QAAA,IAAA,CAAK,KAAA,EAAM;AACX,QAAA,IAAA,CAAK,cAAc,KAAA,EAAM;AACzB,QAAA;AAAA,MACF,KAAK,KAAA;AAEH,QAAA,IAAA,CAAK,KAAA,EAAM;AACX,QAAA;AAAA,MACF;AACE,QAAA,IAAI,cAAA,CAAe,KAAK,CAAA,EAAG;AACzB,UAAA,KAAA,CAAM,cAAA,EAAe;AACrB,UAAA,IAAA,CAAK,YAAA,CAAa,OAAA,EAAS,WAAA,EAAa,KAAA,CAAM,GAAG,CAAA;AAAA,QACnD;AACA,QAAA;AAAA;AACJ,EACF;AAAA;AAAA,EAGA,OAAO,KAAA,EAAoB;AACzB,IAAA,MAAM,MAAA,GAAU,KAAA,CAAM,aAAA,CAA8B,OAAA,CAAqB,iBAAiB,CAAA;AAC1F,IAAA,IAAI,CAAC,MAAA,IAAU,CAAC,KAAK,aAAA,CAAc,QAAA,CAAS,MAAM,CAAA,EAAG;AACrD,IAAA,IAAA,CAAK,cAAc,MAAM,CAAA;AACzB,IAAA,IAAA,CAAK,KAAA,EAAM;AACX,IAAA,IAAA,CAAK,cAAc,KAAA,EAAM;AAAA,EAC3B;AAAA;AAAA,EAGA,IAAA,GAAa;AACX,IAAA,IAAI,CAAC,KAAK,aAAA,EAAe;AACzB,IAAA,IAAA,CAAK,WAAW,MAAA,GAAS,KAAA;AACzB,IAAA,IAAA,CAAK,aAAA,CAAc,YAAA,CAAa,eAAA,EAAiB,MAAM,CAAA;AACvD,IAAA,IAAI,IAAA,CAAK,aAAA,CAAc,MAAA,KAAW,CAAA,EAAG;AAEnC,MAAA,IAAA,CAAK,WAAW,EAAE,CAAA;AAClB,MAAA;AAAA,IACF;AACA,IAAA,MAAM,QAAA,GAAW,KAAK,aAAA,CAAc,SAAA;AAAA,MAClC,CAAC,MAAA,KAAW,MAAA,CAAO,YAAA,CAAa,eAAe,CAAA,KAAM;AAAA,KACvD;AACA,IAAA,IAAA,CAAK,UAAA,CAAW,QAAA,KAAa,EAAA,GAAK,CAAA,GAAI,QAAQ,CAAA;AAAA,EAChD;AAAA;AAAA,EAGA,KAAA,GAAc;AACZ,IAAA,IAAI,CAAC,KAAK,aAAA,EAAe;AACzB,IAAA,IAAA,CAAK,WAAW,MAAA,GAAS,IAAA;AACzB,IAAA,IAAA,CAAK,aAAA,CAAc,YAAA,CAAa,eAAA,EAAiB,OAAO,CAAA;AACxD,IAAA,IAAA,CAAK,WAAW,EAAE,CAAA;AAClB,IAAA,IAAA,CAAK,WAAW,KAAA,EAAM;AAAA,EACxB;AAAA;AAAA,EAGA,aAAA,GAAsB;AACpB,IAAA,IAAA,CAAK,gBAAA,EAAiB;AACtB,IAAA,MAAM,UAAU,IAAA,CAAK,aAAA;AACrB,IAAA,MAAM,WAAA,GAAc,IAAA,CAAK,gBAAA,CAAiB,OAAO,CAAA;AACjD,IAAA,MAAM,MAAA,GAAS,WAAA,GAAc,CAAA,GAAI,MAAA,GAAY,QAAQ,WAAW,CAAA;AAChE,IAAA,IAAI,MAAA,EAAQ,IAAA,CAAK,aAAA,CAAc,MAAM,CAAA;AACrC,IAAA,IAAA,CAAK,KAAA,EAAM;AACX,IAAA,IAAA,CAAK,cAAc,KAAA,EAAM;AAAA,EAC3B;AAAA;AAAA,EAGA,cAAc,MAAA,EAA2B;AACvC,IAAA,MAAM,EAAE,KAAA,EAAO,YAAA,EAAa,GAAI,IAAA,CAAK,gBAAgB,MAAM,CAAA;AAC3D,IAAA,IAAI,YAAA,EAAc;AAIhB,MAAA,IAAA,CAAK,WAAA,CAAY,cAAc,IAAI,KAAA,CAAM,UAAU,EAAE,OAAA,EAAS,IAAA,EAAM,CAAC,CAAA;AAAA,IACvE;AACA,IAAA,IAAA,CAAK,QAAA,CAAS,UAAU,EAAE,MAAA,EAAQ,EAAE,KAAA,EAAO,MAAA,IAAU,CAAA;AAAA,EACvD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,gBAAgB,MAAA,EAA+D;AAC7E,IAAA,KAAA,MAAW,SAAA,IAAa,KAAK,aAAA,EAAe;AAC1C,MAAA,SAAA,CAAU,YAAA,CAAa,eAAA,EAAiB,SAAA,KAAc,MAAA,GAAS,SAAS,OAAO,CAAA;AAAA,IACjF;AACA,IAAA,MAAM,KAAA,GAAA,CAAS,MAAA,CAAO,WAAA,IAAe,EAAA,EAAI,IAAA,EAAK;AAC9C,IAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,OAAA,CAAQ,KAAA,IAAS,KAAA;AACtC,IAAA,IAAI,IAAA,CAAK,cAAA,EAAgB,IAAA,CAAK,WAAA,CAAY,WAAA,GAAc,KAAA;AACxD,IAAA,MAAM,YAAA,GAAe,IAAA,CAAK,cAAA,IAAkB,IAAA,CAAK,YAAY,KAAA,KAAU,KAAA;AACvE,IAAA,IAAI,YAAA,EAAc,IAAA,CAAK,WAAA,CAAY,KAAA,GAAQ,KAAA;AAC3C,IAAA,OAAO,EAAE,OAAO,YAAA,EAAa;AAAA,EAC/B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,WAAW,KAAA,EAAqB;AAC9B,IAAA,MAAM,UAAU,IAAA,CAAK,aAAA;AACrB,IAAA,MAAM,SAAS,KAAA,GAAQ,CAAA,GAAI,IAAA,GAAQ,OAAA,CAAQ,KAAK,CAAA,IAAK,IAAA;AAKrD,IAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC5B,MAAA,MAAM,MAAA,GAAS,MAAA,CAAO,YAAA,CAAa,aAAa,CAAA;AAChD,MAAA,IAAI,WAAW,MAAA,EAAQ;AACrB,QAAA,IAAI,CAAC,MAAA,EAAQ,MAAA,CAAO,YAAA,CAAa,eAAe,EAAE,CAAA;AAAA,MACpD,WAAW,MAAA,EAAQ;AACjB,QAAA,MAAA,CAAO,gBAAgB,aAAa,CAAA;AAAA,MACtC;AAAA,IACF;AACA,IAAA,IAAI,QAAQ,EAAA,EAAI;AACd,MAAA,IAAA,CAAK,YAAY,MAAA,CAAO,EAAA;AACxB,MAAA,IAAA,CAAK,aAAA,CAAc,YAAA,CAAa,uBAAA,EAAyB,MAAA,CAAO,EAAE,CAAA;AAAA,IACpE,CAAA,MAAO;AACL,MAAA,IAAA,CAAK,SAAA,GAAY,IAAA;AACjB,MAAA,IAAA,CAAK,aAAA,CAAc,gBAAgB,uBAAuB,CAAA;AAAA,IAC5D;AACA,IAAA,IAAA,CAAK,YAAA,GAAe,MAAA,GAAS,OAAA,CAAQ,GAAA,CAAI,CAAC,MAAA,KAAW,MAAA,CAAO,EAAE,CAAA,CAAE,MAAA,CAAO,OAAO,CAAA,GAAI,EAAC;AAGnF,IAAA,IAAI,UAAU,IAAA,CAAK,aAAA,EAAe,oBAAA,CAAqB,IAAA,CAAK,YAAY,MAAM,CAAA;AAAA,EAChF;AAAA;AAAA,EAGA,gBAAA,GAAyB;AACvB,IAAA,IAAI,CAAC,IAAA,CAAK,gBAAA,IAAoB,IAAA,CAAK,SAAA,EAAW;AAC5C,MAAA,IAAI,IAAA,CAAK,gBAAA,EAAkB,IAAA,CAAK,UAAA,CAAW,EAAE,CAAA;AAC7C,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,UAAU,IAAA,CAAK,aAAA;AACrB,IAAA,MAAM,YAAA,GAAe,IAAA,CAAK,gBAAA,CAAiB,OAAO,CAAA;AAClD,IAAA,IAAI,gBAAgB,CAAA,EAAG;AACrB,MAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,YAAY,CAAA,IAAK,IAAA;AACxC,MAAA,MAAM,MAAA,GAAS,QAAQ,MAAA,CAAO,CAAC,WAAW,MAAA,CAAO,YAAA,CAAa,aAAa,CAAC,CAAA;AAC5E,MAAA,MAAM,eACJ,IAAA,CAAK,SAAA,MAAe,QAAQ,EAAA,IAAM,IAAA,CAAA,IAClC,OAAO,MAAA,KAAW,CAAA,IAClB,OAAO,CAAC,CAAA,KAAM,UACd,IAAA,CAAK,aAAA,CAAc,aAAa,uBAAuB,CAAA,MAAO,QAAQ,EAAA,IAAM,IAAA,CAAA;AAE9E,MAAA,IAAI,YAAA,EAAc;AAGhB,QAAA,IAAA,CAAK,YAAA,GAAe,QAAQ,GAAA,CAAI,CAAC,WAAW,MAAA,CAAO,EAAE,CAAA,CAAE,MAAA,CAAO,OAAO,CAAA;AAAA,MACvE,CAAA,MAAO;AACL,QAAA,IAAA,CAAK,WAAW,YAAY,CAAA;AAAA,MAC9B;AACA,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,WAAW,IAAA,CAAK,aAAA,CAAc,YAAA,CAAa,uBAAuB,KAAK,IAAA,CAAK,SAAA;AAClF,IAAA,IAAA,CAAK,WAAW,QAAA,GAAW,IAAA,CAAK,mBAAmB,OAAA,EAAS,QAAQ,IAAI,EAAE,CAAA;AAAA,EAC5E;AAAA;AAAA,EAGA,iBAAiB,OAAA,EAAyC;AACxD,IAAA,MAAM,QAAA,GAAA,CACH,KAAK,gBAAA,GAAmB,IAAA,CAAK,cAAc,YAAA,CAAa,uBAAuB,CAAA,GAAI,IAAA,KACpF,IAAA,CAAK,SAAA;AACP,IAAA,IAAI,QAAA,SAAiB,OAAA,CAAQ,SAAA,CAAU,CAAC,MAAA,KAAW,MAAA,CAAO,OAAO,QAAQ,CAAA;AACzE,IAAA,OAAO,QAAQ,SAAA,CAAU,CAAC,WAAW,MAAA,CAAO,YAAA,CAAa,aAAa,CAAC,CAAA;AAAA,EACzE;AAAA;AAAA,EAGA,kBAAA,CAAmB,SAAiC,QAAA,EAA0B;AAC5E,IAAA,MAAM,QAAA,GAAW,IAAA,CAAK,YAAA,CAAa,OAAA,CAAQ,QAAQ,CAAA;AACnD,IAAA,IAAI,QAAA,GAAW,GAAG,OAAO,EAAA;AACzB,IAAA,MAAM,WAAA,GAAc,IAAI,GAAA,CAAI,OAAA,CAAQ,GAAA,CAAI,CAAC,MAAA,EAAQ,KAAA,KAAU,CAAC,MAAA,CAAO,EAAA,EAAI,KAAK,CAAC,CAAC,CAAA;AAC9E,IAAA,KAAA,IAAS,KAAA,GAAQ,WAAW,CAAA,EAAG,KAAA,GAAQ,KAAK,YAAA,CAAa,MAAA,EAAQ,SAAS,CAAA,EAAG;AAC3E,MAAA,MAAM,WAAW,WAAA,CAAY,GAAA,CAAI,KAAK,YAAA,CAAa,KAAK,KAAK,EAAE,CAAA;AAC/D,MAAA,IAAI,QAAA,KAAa,QAAW,OAAO,QAAA;AAAA,IACrC;AACA,IAAA,KAAA,IAAS,QAAQ,QAAA,GAAW,CAAA,EAAG,KAAA,IAAS,CAAA,EAAG,SAAS,CAAA,EAAG;AACrD,MAAA,MAAM,WAAW,WAAA,CAAY,GAAA,CAAI,KAAK,YAAA,CAAa,KAAK,KAAK,EAAE,CAAA;AAC/D,MAAA,IAAI,QAAA,KAAa,QAAW,OAAO,QAAA;AAAA,IACrC;AACA,IAAA,OAAO,EAAA;AAAA,EACT;AAAA;AAAA,EAGA,0BAAA,GAAmC;AACjC,IAAA,IAAA,CAAK,WAAW,QAAA,EAAS;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,YAAA,CAAa,OAAA,EAAwB,WAAA,EAAqB,IAAA,EAAoB;AAC5E,IAAA,MAAM,KAAA,GAAQ,mBAAmB,OAAA,EAAS,WAAA,EAAa,KAAK,UAAA,CAAW,IAAA,CAAK,IAAI,CAAC,CAAA;AACjF,IAAA,IAAI,KAAA,KAAU,EAAA,EAAI,IAAA,CAAK,UAAA,CAAW,KAAK,CAAA;AAAA,EACzC;AAAA;AAAA,EAGS,eAAA,GAAkB,CAAC,KAAA,KAA4B;AACtD,IAAA,IAAI,CAAC,IAAA,CAAK,SAAA,IAAa,CAAC,IAAA,CAAK,OAAA,CAAQ,QAAA,CAAS,KAAA,CAAM,MAAc,CAAA,EAAG,IAAA,CAAK,KAAA,EAAM;AAAA,EAClF,CAAA;AAAA;AAAA,EAGA,IAAI,SAAA,GAAqB;AACvB,IAAA,OAAO,CAAC,IAAA,CAAK,aAAA,IAAiB,IAAA,CAAK,WAAW,MAAA,KAAW,KAAA;AAAA,EAC3D;AACF","file":"listbox_controller.js","sourcesContent":["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 * Collapses many Stimulus lifecycle callbacks from one DOM mutation into a\n * single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element and\n * `<name>ValueChanged` once per changed attribute. Replacing a list of N options\n * or morphing several render Values therefore delivers N callbacks — but the\n * useful unit of work is \"reconcile against the resulting declarative input\",\n * once, after the batch has settled. Every controller with reconcilable targets\n * or render Values needs the same shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers initial target and Value callbacks\n * ahead of `connect()`. Reconciling there would compute output against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\n }\n}\n","/**\n * Scroll-follow for virtually-focused listbox options.\n *\n * Listbox-style widgets that track their active option with\n * `aria-activedescendant` keep DOM focus on the input/trigger, so the\n * browser's native \"scroll the focused element into view\" never runs — in a\n * scrollable list (`max-height` + `overflow`) the active option can walk right\n * out of sight on ArrowDown/ArrowUp. This helper keeps it visible.\n *\n * It adjusts the LIST's own `scrollTop` only — deliberately not\n * `scrollIntoView({ block: \"nearest\" })`, which may also scroll ancestor\n * scrolling boxes (the page) when a floating popup pokes past a viewport\n * edge, desyncing anchored/marker-composed placements. (Command-palette uses\n * `scrollIntoView` because its page is scroll-locked behind the modal;\n * page-floating popups must use this helper instead.)\n */\n\n/**\n * Scrolls `option` into view within `list` by minimally adjusting\n * `list.scrollTop`. No-ops when the list does not actually scroll. Never\n * touches any other scrolling box.\n *\n * @param list - The scrollable listbox container.\n * @param option - The (virtually focused) option to keep visible.\n */\nexport function scrollOptionIntoView(list: HTMLElement, option: HTMLElement): void {\n if (list.scrollHeight <= list.clientHeight) return;\n const listRect = list.getBoundingClientRect();\n const optionRect = option.getBoundingClientRect();\n if (optionRect.top < listRect.top) {\n list.scrollTop -= listRect.top - optionRect.top;\n } else if (optionRect.bottom > listRect.bottom) {\n list.scrollTop += optionRect.bottom - listRect.bottom;\n }\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","/**\n * Shared type-ahead (first-letter navigation) primitive for composite widgets.\n *\n * The APG asks list-like widgets to move focus by typed characters: one\n * character jumps to the next item starting with it, and several characters\n * typed in quick succession narrow the match to that prefix. Getting that right\n * needs three pieces of bookkeeping — accumulating the query, dropping it after\n * an idle window, and resolving the label each candidate is matched against —\n * and single-sourcing them here keeps every widget on the same rules instead of\n * letting them drift apart.\n *\n * The helpers here own *only* that mechanical part. They are deliberately\n * **policy-free** in the same sense as `RovingTabindex`: which elements are\n * candidates, where the search starts, and what happens on a match (real DOM\n * focus, a roving tab stop, or a virtual `aria-activedescendant`) differ per APG\n * pattern, so each controller keeps those decisions and calls\n * {@link findTypeaheadMatch} with a candidate list it assembled itself.\n *\n * Three decisions are worth stating up front:\n *\n * - **A repeated character collapses the stored query**, rather than deriving a\n * shorter one at search time. That is what lets a follow-up character resume\n * narrowing: `s`, `s`, `e` searches `\"se\"`, not the dead `\"sse\"` that no label\n * can match. Growing the query instead leaves the widget frozen on exactly\n * that sequence, which is why the choice is made here and not per widget.\n * - **`aria-label` wins over text**, so type-ahead matches what a screen reader\n * announces — but only when it contributes a name. A blank (empty or\n * whitespace-only) one is skipped, because accname skips it too: honoring it\n * would leave an element that AT announces by its text unreachable by that\n * text. This mirrors the accessible-name computation rather than the raw text.\n * - **`Space` and composition input are never type-ahead.** `Space` natively\n * activates a `<button>`-based item, and characters still being composed are\n * not a committed query.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and contracts belong on the members.\n */\n\nimport { SafeTimeout } from \"./safe_timeout\";\n\n/** Idle window after which an accumulated query is dropped, in milliseconds. */\nexport const TYPEAHEAD_RESET_MS = 500;\n\n/** Construction options for {@link Typeahead}. */\nexport interface TypeaheadOptions {\n /** Idle window before the query resets. Defaults to {@link TYPEAHEAD_RESET_MS}. */\n resetMs?: number;\n}\n\n/**\n * Accumulates typed characters into a search query and drops it when idle.\n *\n * @remarks\n * Owns a {@link SafeTimeout} of its own, so a consumer's `#timers.clearAll()` does\n * **not** reach the pending reset — call {@link Typeahead.reset} from `disconnect()`.\n */\nexport class Typeahead {\n /** Timer registry for the pending idle reset; private so `reset()` is the only exit. */\n readonly #timers = new SafeTimeout();\n /** Idle window before the query resets, in milliseconds. */\n readonly #resetMs: number;\n /** The accumulated lowercase query, empty when idle. */\n #query = \"\";\n /** Id of the pending reset timer, `0` when none is scheduled. */\n #timerId = 0;\n\n /** @param options - Overrides for the idle window. */\n constructor({ resetMs = TYPEAHEAD_RESET_MS }: TypeaheadOptions = {}) {\n this.#resetMs = resetMs;\n }\n\n /** The query a search would currently run with; empty while idle. */\n get query(): string {\n return this.#query;\n }\n\n /**\n * Folds `key` into the query, restarts the idle window, and returns the query to\n * search with. A repeated character collapses the query to that one character.\n */\n push(key: string): string {\n const char = key.toLowerCase();\n const repeated = this.#query.length > 0 && [...this.#query].every((c) => c === char);\n this.#query = repeated ? char : this.#query + char;\n this.#timers.clear(this.#timerId);\n this.#timerId = this.#timers.set(() => this.reset(), this.#resetMs);\n return this.#query;\n }\n\n /** Clears the query and cancels the pending idle reset. */\n reset(): void {\n this.#query = \"\";\n this.#timers.clear(this.#timerId);\n this.#timerId = 0;\n }\n}\n\n/** Whether `event` is a bare printable character usable for type-ahead. */\nexport function isTypeaheadKey(event: KeyboardEvent): boolean {\n return (\n event.key.length === 1 &&\n event.key !== \" \" &&\n !event.ctrlKey &&\n !event.metaKey &&\n !event.altKey &&\n !event.isComposing\n );\n}\n\n/**\n * The name `element` is matched under, normalized for comparison.\n *\n * @param fallbackText - Produces the name when `aria-label` contributes none.\n * Defaults to the element's `textContent`; pass a narrower source when part of\n * the subtree is not part of the name (a tree item's nested child group, say).\n * Called lazily, so an `aria-label` costs no subtree walk.\n */\nexport function typeaheadLabel(element: HTMLElement, fallbackText?: () => string): string {\n // Blank means absent, matching accname: a whitespace-only `aria-label` is\n // skipped there too, so honoring it here would make an element that a screen\n // reader announces by its text unreachable by that text.\n const label = element.getAttribute(\"aria-label\")?.trim();\n if (label) return label.toLowerCase();\n const text = fallbackText ? fallbackText() : (element.textContent ?? \"\");\n return text.trim().toLowerCase();\n}\n\n/**\n * Index of the first item after `from` whose label starts with `query`, wrapping\n * and evaluating `from` itself last, or `-1` when nothing matches.\n *\n * @param items - Candidates in navigation order, already filtered by the caller.\n * @param from - Index the search moves on from; `-1` starts at the first item. Any\n * out-of-range value is folded back in, so `indexOf` results pass straight through.\n * @param query - Lowercase query, normally the return of {@link Typeahead.push}.\n * @param label - Resolves a candidate's name. Defaults to {@link typeaheadLabel}.\n */\nexport function findTypeaheadMatch(\n items: readonly HTMLElement[],\n from: number,\n query: string,\n label: (item: HTMLElement) => string = (item) => typeaheadLabel(item),\n): number {\n // An empty query would prefix-match everything; an empty list never enters the\n // loop, so it needs no guard of its own.\n if (query === \"\") return -1;\n const count = items.length;\n for (let step = 1; step <= count; step += 1) {\n // Folded twice so a negative `from` still lands on a real slot: `%` keeps the\n // sign of its left operand, and a negative index would read `undefined`.\n const index = (((from + step) % count) + count) % count;\n const candidate = items[index];\n if (candidate && label(candidate).startsWith(query)) return index;\n }\n return -1;\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { isReservedArrowChord } from \"../utils/arrow_step\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\nimport { scrollOptionIntoView } from \"../utils/option_scroll\";\nimport { findTypeaheadMatch, isTypeaheadKey, Typeahead } from \"../utils/typeahead\";\n\n/**\n * Headless, accessible select-only listbox behavior.\n *\n * Markup contract (identifier: `stimeo--listbox`):\n * <div data-controller=\"stimeo--listbox\">\n * <span id=\"lb-label\">Favorite fruit</span>\n * <button type=\"button\" role=\"combobox\" aria-haspopup=\"listbox\"\n * aria-expanded=\"false\" aria-controls=\"lb-list\"\n * aria-labelledby=\"lb-label lb-value\"\n * data-stimeo--listbox-target=\"trigger\"\n * data-action=\"click->stimeo--listbox#toggle\n * keydown->stimeo--listbox#onTriggerKeydown\">\n * <span id=\"lb-value\" data-stimeo--listbox-target=\"value\">Choose…</span>\n * </button>\n * <ul id=\"lb-list\" role=\"listbox\" aria-label=\"Options\" hidden\n * data-stimeo--listbox-target=\"list\">\n * <li id=\"opt-1\" role=\"option\" aria-selected=\"false\" data-value=\"1\"\n * data-stimeo--listbox-target=\"option\"\n * data-action=\"click->stimeo--listbox#select\">Option 1</li>\n * <!-- more options -->\n * </ul>\n * <input type=\"hidden\" data-stimeo--listbox-target=\"field\" />\n * </div>\n *\n * Implements the WAI-ARIA APG **Listbox** pattern in its collapsed\n * (Select-Only Combobox) form. Focus stays on the trigger; the active option is\n * tracked with `aria-activedescendant` rather than by moving DOM focus. For a\n * text-filtered popup use {@link ComboboxController | Combobox}; for multiple\n * selection use Multi-Select.\n *\n * `change` dispatches `{ value: string, option: HTMLElement }`.\n *\n * @remarks\n * Behavior only. Static placement is the consumer's CSS; dynamic placement is\n * delegated to the opt-in `stimeo-ui/positioning` module. The look is keyed off\n * `aria-selected` / `data-active`. Because `role=\"combobox\"` is not named by its\n * contents, give the trigger an accessible name via `aria-labelledby` (a visible\n * label plus the value span) or `aria-label`.\n *\n * Behavior provided:\n * - Open/close the list, syncing `aria-expanded` and the list's `hidden`.\n * - `ArrowDown`/`ArrowUp` (wrapping), `Home`/`End`, and printable-character\n * typeahead move the active option; opening picks the selected option (else\n * the first).\n * - Single selection syncs `aria-selected`, reflects the label into the trigger\n * value and the field's value, and dispatches `stimeo--listbox:change` plus a\n * native bubbling `change` on the field when its value actually changed.\n * Making the field a validatable mirror (`<input type=\"text\" hidden required>`\n * instead of `type=\"hidden\"`) lets `stimeo--form-validation` enforce native\n * constraints on the committed value with no extra JavaScript.\n * - `Enter`/`Space` select and close; `Escape` and outside click / `Tab` close;\n * closing via select/Escape returns focus to the trigger.\n */\nexport class ListboxController extends Controller<HTMLElement> {\n static override targets = [\"trigger\", \"value\", \"list\", \"option\", \"field\"];\n static actions = [\"close\", \"onTriggerKeydown\", \"open\", \"select\", \"toggle\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly triggerTarget: HTMLElement;\n declare readonly valueTarget: HTMLElement;\n declare readonly listTarget: HTMLElement;\n declare readonly optionTargets: HTMLElement[];\n declare readonly fieldTarget: HTMLInputElement;\n declare readonly hasTriggerTarget: boolean;\n declare readonly hasValueTarget: boolean;\n declare readonly hasListTarget: boolean;\n declare readonly hasFieldTarget: boolean;\n\n /** Stable ID of the active option; DOM targets are resolved afresh before use. */\n #activeId: string | null = null;\n /** Target ID order captured while an option is active, used only for removal fallback. */\n #activeOrder: string[] = [];\n #connected = false;\n /** Collapses one mutation batch of target callbacks into a single pass. */\n readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileActive());\n /** Accumulated typeahead query and its idle-reset timer. */\n readonly #typeahead = new Typeahead();\n\n /** Establishes the ARIA baseline, starts closed, and listens for outside clicks. */\n override connect(): void {\n this.#normalizeSelection();\n this.close();\n document.addEventListener(\"click\", this.#onOutsideClick, true);\n this.#connected = true;\n this.#reconcile.activate();\n }\n\n /**\n * Establishes an inactive baseline for a late option, re-resolves active\n * identity, and re-applies the selection baseline.\n */\n optionTargetConnected(option: HTMLElement): void {\n option.removeAttribute(\"data-active\");\n if (!this.#connected) return;\n this.#normalizeSelection();\n this.#queueOptionReconciliation();\n }\n\n /** Removes controller-owned active state and reconciles the surviving targets. */\n optionTargetDisconnected(option: HTMLElement): void {\n option.removeAttribute(\"data-active\");\n if (this.#connected) this.#queueOptionReconciliation();\n }\n\n /**\n * Brings the authored DOM to the shape the APG requires, and derives the state\n * that follows from the initial selection.\n *\n * Three things happen, and only these three — which option is chosen is the\n * author's, and is never changed:\n *\n * 1. Every option gets an explicit value. An absent `aria-selected` means \"not\n * selectable\" in ARIA, so a forgotten attribute hides a selectable option\n * from assistive technology.\n * 2. At most one stays `true`. The first in DOM order wins, since that is the\n * only deterministic reading of \"which one did the author mean\".\n * 3. The trigger label and the hidden field are derived from that selection.\n * Without this the widget announces a choice it does not submit: the popup\n * says \"Banana\", the trigger still says \"Choose…\", and the form posts \"\".\n *\n * No `change` fires — nothing changed, this is the initial state being told\n * properly. The scan is the `option` target set: a `role=\"option\"` without the\n * target is outside the contract and is neither counted nor written.\n */\n #normalizeSelection(): void {\n const options = this.optionTargets;\n const selected = options.find((option) => option.getAttribute(\"aria-selected\") === \"true\");\n for (const option of options) {\n option.setAttribute(\"aria-selected\", option === selected ? \"true\" : \"false\");\n }\n if (selected) this.#applySelection(selected);\n }\n\n /** Removes the document listener and clears the typeahead timer. */\n override disconnect(): void {\n this.#connected = false;\n this.#reconcile.cancel();\n document.removeEventListener(\"click\", this.#onOutsideClick, true);\n // The typeahead keeps its idle reset in a `SafeTimeout` of its own, so this is\n // the only teardown that reaches it — there is no controller-level registry to\n // fall back on. Every timer this controller can schedule lives in there.\n this.#typeahead.reset();\n }\n\n /**\n * Toggles the list on a real mouse click. Keyboard activation of the\n * `<button>` also fires a click (`detail === 0`); the keydown handler already\n * drives that, so the synthetic click is ignored to avoid double-toggling.\n */\n toggle(event: MouseEvent): void {\n if (event.detail === 0) return;\n if (this.#isClosed) {\n this.open();\n } else {\n this.close();\n }\n }\n\n /** Yields claimed keys; otherwise routes the APG select-only keyboard model. */\n onTriggerKeydown(event: KeyboardEvent): void {\n if (event.defaultPrevented) return;\n if (isReservedArrowChord(event)) return;\n if (!this.#isClosed) this.#reconcileActive();\n const options = this.optionTargets;\n const length = options.length;\n const activeIndex = this.#findActiveIndex(options);\n if (this.#isClosed) {\n switch (event.key) {\n case \"Enter\":\n case \" \":\n case \"ArrowDown\":\n case \"ArrowUp\":\n event.preventDefault();\n this.open();\n break;\n default:\n break;\n }\n return;\n }\n\n // With no options, only Escape/Tab are meaningful; ignore navigation and\n // typeahead so the active index can never become NaN (`% 0`).\n if (length === 0 && event.key !== \"Escape\" && event.key !== \"Tab\") {\n return;\n }\n\n switch (event.key) {\n case \"ArrowDown\":\n event.preventDefault();\n this.#setActive(activeIndex < 0 ? 0 : (activeIndex + 1) % length);\n break;\n case \"ArrowUp\":\n event.preventDefault();\n this.#setActive(activeIndex < 0 ? length - 1 : (activeIndex - 1 + length) % length);\n break;\n case \"Home\":\n event.preventDefault();\n this.#setActive(0);\n break;\n case \"End\":\n event.preventDefault();\n this.#setActive(length - 1);\n break;\n case \"Enter\":\n case \" \":\n event.preventDefault();\n this.#commitActive();\n break;\n case \"Escape\":\n // The entry guard already yields a press another handler owned. A press\n // during IME composition never dismisses, keeping one rule across the\n // widgets (the trigger is a button, so composition does not start here).\n if (event.isComposing) break;\n event.preventDefault();\n this.close();\n this.triggerTarget.focus();\n break;\n case \"Tab\":\n // Let focus leave naturally; just don't keep a stale popup open.\n this.close();\n break;\n default:\n if (isTypeaheadKey(event)) {\n event.preventDefault();\n this.#typeaheadTo(options, activeIndex, event.key);\n }\n break;\n }\n }\n\n /** Selects the clicked option and closes, returning focus to the trigger. */\n select(event: Event): void {\n const option = (event.currentTarget as HTMLElement).closest<HTMLElement>('[role=\"option\"]');\n if (!option || !this.optionTargets.includes(option)) return;\n this.#selectOption(option);\n this.close();\n this.triggerTarget.focus();\n }\n\n /** Opens the list and activates the selected option (else the first). */\n open(): void {\n if (!this.hasListTarget) return;\n this.listTarget.hidden = false;\n this.triggerTarget.setAttribute(\"aria-expanded\", \"true\");\n if (this.optionTargets.length === 0) {\n // An empty listbox has nothing to activate; leave activedescendant cleared.\n this.#setActive(-1);\n return;\n }\n const selected = this.optionTargets.findIndex(\n (option) => option.getAttribute(\"aria-selected\") === \"true\",\n );\n this.#setActive(selected === -1 ? 0 : selected);\n }\n\n /** Closes the list, clears the active option, and resets the typeahead buffer. */\n close(): void {\n if (!this.hasListTarget) return;\n this.listTarget.hidden = true;\n this.triggerTarget.setAttribute(\"aria-expanded\", \"false\");\n this.#setActive(-1);\n this.#typeahead.reset();\n }\n\n /** Commits the active option (keyboard) and closes, returning focus. */\n #commitActive(): void {\n this.#reconcileActive();\n const options = this.optionTargets;\n const activeIndex = this.#findActiveIndex(options);\n const option = activeIndex < 0 ? undefined : options[activeIndex];\n if (option) this.#selectOption(option);\n this.close();\n this.triggerTarget.focus();\n }\n\n /** Applies selection: `aria-selected`, trigger label, hidden field, `change`. */\n #selectOption(option: HTMLElement): void {\n const { value, fieldChanged } = this.#applySelection(option);\n if (fieldChanged) {\n // A native bubbling change (matching <select> semantics: only on an actual\n // value change) so form-level behaviors — validation re-checks, auto-submit\n // — hear the commit without knowing this widget.\n this.fieldTarget.dispatchEvent(new Event(\"change\", { bubbles: true }));\n }\n this.dispatch(\"change\", { detail: { value, option } });\n }\n\n /**\n * Writes `option` into `aria-selected`, the trigger label and the hidden field.\n * Emits nothing — {@link #normalizeSelection} reuses this at connect, where the\n * state is being described rather than changed.\n */\n #applySelection(option: HTMLElement): { value: string; fieldChanged: boolean } {\n for (const candidate of this.optionTargets) {\n candidate.setAttribute(\"aria-selected\", candidate === option ? \"true\" : \"false\");\n }\n const label = (option.textContent ?? \"\").trim();\n const value = option.dataset.value ?? label;\n if (this.hasValueTarget) this.valueTarget.textContent = label;\n const fieldChanged = this.hasFieldTarget && this.fieldTarget.value !== value;\n if (fieldChanged) this.fieldTarget.value = value;\n return { value, fieldChanged };\n }\n\n /**\n * Marks the option at `index` active via `data-active` and the trigger's\n * `aria-activedescendant`. Pass `-1` to clear it (the attribute is removed, not\n * set to empty, per the APG).\n */\n #setActive(index: number): void {\n const options = this.optionTargets;\n const active = index < 0 ? null : (options[index] ?? null);\n // Only the options whose marker actually changes are written, so a held arrow\n // key costs two attribute writes rather than one per option. The whole set is\n // still read: that is what makes a stray marker — one a morph left behind on\n // an element that never re-connected as a target — heal on the next move.\n for (const option of options) {\n const marked = option.hasAttribute(\"data-active\");\n if (option === active) {\n if (!marked) option.setAttribute(\"data-active\", \"\");\n } else if (marked) {\n option.removeAttribute(\"data-active\");\n }\n }\n if (active?.id) {\n this.#activeId = active.id;\n this.triggerTarget.setAttribute(\"aria-activedescendant\", active.id);\n } else {\n this.#activeId = null;\n this.triggerTarget.removeAttribute(\"aria-activedescendant\");\n }\n this.#activeOrder = active ? options.map((option) => option.id).filter(Boolean) : [];\n // Virtual focus never triggers the browser's native focus-scrolling, so a\n // scrollable list must follow the active option itself (list-only scroll).\n if (active && this.hasListTarget) scrollOptionIntoView(this.listTarget, active);\n }\n\n /** Resolves active state against the current target collection. */\n #reconcileActive(): void {\n if (!this.hasTriggerTarget || this.#isClosed) {\n if (this.hasTriggerTarget) this.#setActive(-1);\n return;\n }\n\n const options = this.optionTargets;\n const currentIndex = this.#findActiveIndex(options);\n if (currentIndex >= 0) {\n const active = options[currentIndex] ?? null;\n const marked = options.filter((option) => option.hasAttribute(\"data-active\"));\n const stateMatches =\n this.#activeId === (active?.id || null) &&\n marked.length === 1 &&\n marked[0] === active &&\n this.triggerTarget.getAttribute(\"aria-activedescendant\") === (active?.id || null);\n\n if (stateMatches) {\n // Keep the deletion fallback snapshot current without re-scrolling the\n // already-active option before every keyboard command.\n this.#activeOrder = options.map((option) => option.id).filter(Boolean);\n } else {\n this.#setActive(currentIndex);\n }\n return;\n }\n\n const activeId = this.triggerTarget.getAttribute(\"aria-activedescendant\") ?? this.#activeId;\n this.#setActive(activeId ? this.#findFallbackIndex(options, activeId) : -1);\n }\n\n /** Finds the live target carrying the stable ID, or the active marker for an ID-less option. */\n #findActiveIndex(options: readonly HTMLElement[]): number {\n const activeId =\n (this.hasTriggerTarget ? this.triggerTarget.getAttribute(\"aria-activedescendant\") : null) ??\n this.#activeId;\n if (activeId) return options.findIndex((option) => option.id === activeId);\n return options.findIndex((option) => option.hasAttribute(\"data-active\"));\n }\n\n /** Chooses a surviving former successor, then a former predecessor. */\n #findFallbackIndex(options: readonly HTMLElement[], activeId: string): number {\n const oldIndex = this.#activeOrder.indexOf(activeId);\n if (oldIndex < 0) return -1;\n const indexesById = new Map(options.map((option, index) => [option.id, index]));\n for (let index = oldIndex + 1; index < this.#activeOrder.length; index += 1) {\n const fallback = indexesById.get(this.#activeOrder[index] ?? \"\");\n if (fallback !== undefined) return fallback;\n }\n for (let index = oldIndex - 1; index >= 0; index -= 1) {\n const fallback = indexesById.get(this.#activeOrder[index] ?? \"\");\n if (fallback !== undefined) return fallback;\n }\n return -1;\n }\n\n /** Coalesces all target callbacks from one MutationObserver batch. */\n #queueOptionReconciliation(): void {\n this.#reconcile.schedule();\n }\n\n /**\n * Advances the typeahead query and activates the next matching option.\n *\n * The search resumes just after the active option so repeating a character\n * cycles through the options starting with it, rather than re-activating the\n * same first match on every press.\n */\n #typeaheadTo(options: HTMLElement[], activeIndex: number, char: string): void {\n const index = findTypeaheadMatch(options, activeIndex, this.#typeahead.push(char));\n if (index !== -1) this.#setActive(index);\n }\n\n /** Closes on an outside click before an inside handler can detach its target. */\n readonly #onOutsideClick = (event: MouseEvent): void => {\n if (!this.#isClosed && !this.element.contains(event.target as Node)) this.close();\n };\n\n /** Whether the list is currently hidden. */\n get #isClosed(): boolean {\n return !this.hasListTarget || this.listTarget.hidden !== false;\n }\n}\n"]}
@@ -19,16 +19,32 @@ import { Controller } from '@hotwired/stimulus';
19
19
  * element as the `--stimeo--masonry-columns` custom property and each item gets a
20
20
  * `data-column` index, so the consumer's CSS owns the actual placement.
21
21
  *
22
- * `layout` dispatches `{ columns: number }`.
22
+ * `layout` dispatches `{ columns: number }` whenever the published result moves —
23
+ * the column count changed, or some item landed in a different column. A pass that
24
+ * reproduces the previous result stays silent.
23
25
  *
24
26
  * @remarks
25
27
  * Behavior only. **DOM order is never changed** — reading order and focus order
26
28
  * stay the source markup order (WCAG 1.3.2). The visual packing is purely the
27
29
  * column assignment a consumer reads from `data-column`; this controller writes no
28
- * positioning styles. Re-layout runs on connect, on resize ({@link LayoutObserver}),
29
- * and on item add/remove ({@link MutationObserver}); both observers are released on
30
- * `disconnect()` (Turbo navigation included). Use only for independent cards whose
31
- * visual order carries no meaning.
30
+ * positioning styles. Use only for independent cards whose visual order carries no
31
+ * meaning.
32
+ *
33
+ * Re-layout runs on connect, on resize ({@link LayoutObserver}), on item
34
+ * add/remove ({@link MutationObserver}), on an item joining or leaving the target
35
+ * set, when a declared number changes, and when a descendant resource loads.
36
+ * Everything but the first pass is folded into one microtask, so a burst of
37
+ * triggers costs one pass. The observers, the `load` listener and any pending pass
38
+ * are released on `disconnect()` (Turbo navigation included).
39
+ *
40
+ * Consumer contract:
41
+ * - A declaration that cannot be read as a number (`"240px"`, an infinity) falls
42
+ * back to that Value's default and the grid keeps working; `0` and negatives are
43
+ * readable numbers and are used as declared; the count falls back to one column
44
+ * only when `minColumnWidth + gap` is not positive, or the container has no
45
+ * measurable width.
46
+ * - `data-column` belongs to this controller: it is written on every item it owns
47
+ * and taken back from an element that stops being one.
32
48
  */
33
49
  declare class MasonryController extends Controller<HTMLElement> {
34
50
  #private;
@@ -47,6 +63,21 @@ declare class MasonryController extends Controller<HTMLElement> {
47
63
  readonly itemTargets: HTMLElement[];
48
64
  minColumnWidthValue: number;
49
65
  gapValue: number;
66
+ /** Resolves the declared column width once, falling back when it is unreadable. */
67
+ minColumnWidthValueChanged(): void;
68
+ /** Resolves the declared gap once, falling back when it is unreadable. */
69
+ gapValueChanged(): void;
70
+ /** Packs an element that became an item without moving in the DOM. */
71
+ itemTargetConnected(): void;
72
+ /**
73
+ * Queues the column hook of an element that stopped being an item for removal.
74
+ *
75
+ * The removal is queued rather than immediate because teardown reports every
76
+ * target as disconnected: doing it here would strip the whole grid just before
77
+ * a Turbo snapshot is taken. {@link MicrotaskCoalescer.cancel} drops the queue
78
+ * with the pass, so only a genuine target change reaches it.
79
+ */
80
+ itemTargetDisconnected(item: HTMLElement): void;
50
81
  /** Observes size/content changes and performs the first layout pass. */
51
82
  connect(): void;
52
83
  /** Releases both observers and the load listener so nothing fires after detach. */
@@ -58,16 +58,72 @@ var LayoutObserver = class {
58
58
  }
59
59
  };
60
60
 
61
+ // src/utils/microtask_coalescer.ts
62
+ var MicrotaskCoalescer = class {
63
+ #run;
64
+ #queued = false;
65
+ #active = false;
66
+ #generation = 0;
67
+ /** @param run - the single reconciliation pass, invoked at most once per batch. */
68
+ constructor(run) {
69
+ this.#run = run;
70
+ }
71
+ /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
72
+ activate() {
73
+ this.#active = true;
74
+ }
75
+ /** Closes the window and drops any pending pass; call from `disconnect()`. */
76
+ cancel() {
77
+ this.#active = false;
78
+ this.#queued = false;
79
+ this.#generation += 1;
80
+ }
81
+ /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
82
+ schedule() {
83
+ if (!this.#active || this.#queued) return;
84
+ this.#queued = true;
85
+ const generation = this.#generation;
86
+ queueMicrotask(() => {
87
+ if (generation !== this.#generation || !this.#queued || !this.#active) return;
88
+ this.#queued = false;
89
+ this.#run();
90
+ });
91
+ }
92
+ };
93
+
61
94
  // src/controllers/masonry_controller.ts
62
95
  var COLUMNS_PROPERTY = "--stimeo--masonry-columns";
96
+ var DEFAULT_MIN_COLUMN_WIDTH = 240;
97
+ var DEFAULT_GAP = 16;
98
+ function usableNumber(value, fallback) {
99
+ return Number.isFinite(value) ? value : fallback;
100
+ }
63
101
  var MasonryController = class extends Controller {
64
102
  static targets = ["item"];
65
103
  static values = {
66
- minColumnWidth: { type: Number, default: 240 },
67
- gap: { type: Number, default: 16 }
104
+ minColumnWidth: { type: Number, default: DEFAULT_MIN_COLUMN_WIDTH },
105
+ gap: { type: Number, default: DEFAULT_GAP }
68
106
  };
69
107
  static events = ["layout"];
70
- #layout = new LayoutObserver(() => this.#relayout());
108
+ /**
109
+ * The declared numbers after validation, so the layout path never sees a value
110
+ * it cannot compute with. Both are resolved once per declaration change rather
111
+ * than on every pass.
112
+ */
113
+ #minColumnWidth = DEFAULT_MIN_COLUMN_WIDTH;
114
+ #gap = DEFAULT_GAP;
115
+ /**
116
+ * Collapses every re-layout trigger of one DOM mutation into a single pass, and
117
+ * refuses to run before `connect()` or after `disconnect()`.
118
+ *
119
+ * The triggers arrive in bursts — a resize stream, a morph that syncs several
120
+ * attributes, a batch of rows — and each pass measures every item, so folding
121
+ * them keeps the work proportional to the batch rather than to the events in it.
122
+ */
123
+ #reconcile = new MicrotaskCoalescer(() => this.#relayout());
124
+ /** Items that left the target set and still carry the column hook. */
125
+ #released = /* @__PURE__ */ new Set();
126
+ #layout = new LayoutObserver(() => this.#reconcile.schedule());
71
127
  #mutationObserver = null;
72
128
  /** Last published column count, so `layout` fires only on real changes. */
73
129
  #lastColumns = 0;
@@ -77,20 +133,49 @@ var MasonryController = class extends Controller {
77
133
  * first pass ran before they settled; `load` does not bubble, so this is bound in
78
134
  * the capture phase to catch every descendant.
79
135
  */
80
- #onLoad = () => this.#relayout();
136
+ #onLoad = () => this.#reconcile.schedule();
137
+ /** Resolves the declared column width once, falling back when it is unreadable. */
138
+ minColumnWidthValueChanged() {
139
+ this.#minColumnWidth = usableNumber(this.minColumnWidthValue, DEFAULT_MIN_COLUMN_WIDTH);
140
+ this.#reconcile.schedule();
141
+ }
142
+ /** Resolves the declared gap once, falling back when it is unreadable. */
143
+ gapValueChanged() {
144
+ this.#gap = usableNumber(this.gapValue, DEFAULT_GAP);
145
+ this.#reconcile.schedule();
146
+ }
147
+ /** Packs an element that became an item without moving in the DOM. */
148
+ itemTargetConnected() {
149
+ this.#reconcile.schedule();
150
+ }
151
+ /**
152
+ * Queues the column hook of an element that stopped being an item for removal.
153
+ *
154
+ * The removal is queued rather than immediate because teardown reports every
155
+ * target as disconnected: doing it here would strip the whole grid just before
156
+ * a Turbo snapshot is taken. {@link MicrotaskCoalescer.cancel} drops the queue
157
+ * with the pass, so only a genuine target change reaches it.
158
+ */
159
+ itemTargetDisconnected(item) {
160
+ this.#released.add(item);
161
+ this.#reconcile.schedule();
162
+ }
81
163
  /** Observes size/content changes and performs the first layout pass. */
82
164
  connect() {
83
165
  this.#layout.observe(this.element);
84
166
  this.#layout.observeViewport();
85
167
  if (typeof MutationObserver !== "undefined") {
86
- this.#mutationObserver = new MutationObserver(() => this.#relayout());
168
+ this.#mutationObserver = new MutationObserver(() => this.#reconcile.schedule());
87
169
  this.#mutationObserver.observe(this.element, { childList: true, subtree: true });
88
170
  }
89
171
  this.element.addEventListener("load", this.#onLoad, true);
90
172
  this.#relayout();
173
+ this.#reconcile.activate();
91
174
  }
92
175
  /** Releases both observers and the load listener so nothing fires after detach. */
93
176
  disconnect() {
177
+ this.#reconcile.cancel();
178
+ this.#released.clear();
94
179
  this.#layout.disconnect();
95
180
  this.#mutationObserver?.disconnect();
96
181
  this.#mutationObserver = null;
@@ -99,26 +184,53 @@ var MasonryController = class extends Controller {
99
184
  }
100
185
  /**
101
186
  * Recomputes the column count and assigns every item to the shortest column.
102
- * Runs automatically on connect, on resize, on item add/remove, and when a
103
- * descendant resource loads (private — there is no public action; the observers
104
- * and the capture-phase `load` listener drive it). Items are walked in DOM
105
- * order; each lands in the column with the least accumulated height, which
106
- * keeps the packing balanced without reordering the DOM.
187
+ * Runs automatically on connect, on resize, on item add/remove, when a declared
188
+ * number changes, and when a descendant resource loads (private — there is no
189
+ * public action; the observers, the target callbacks and the capture-phase
190
+ * `load` listener drive it). Items are walked in DOM order; each lands in the
191
+ * column with the least accumulated height, which keeps the packing balanced
192
+ * without reordering the DOM.
193
+ *
194
+ * Every box is measured before anything is written. Interleaving the two would
195
+ * make a consumer's `data-column` rule invalidate style once per item, and the
196
+ * next measurement then has to settle layout again — once per item instead of
197
+ * once per pass. The assignment is independent of the measurement because the
198
+ * columns are uniform in width, so the order of the two passes does not change
199
+ * the result.
200
+ *
201
+ * @stimeoRenderRoot
107
202
  */
108
203
  #relayout() {
109
204
  const items = this.itemTargets;
110
205
  const columns = this.#columnCount();
206
+ const boxes = items.map((item) => item.getBoundingClientRect().height);
207
+ let changed = false;
208
+ if (this.#released.size > 0) {
209
+ const owned = new Set(items);
210
+ for (const released of this.#released) {
211
+ if (owned.has(released)) continue;
212
+ if (released.hasAttribute("data-column")) {
213
+ released.removeAttribute("data-column");
214
+ changed = true;
215
+ }
216
+ }
217
+ this.#released.clear();
218
+ }
111
219
  const heights = new Array(columns).fill(0);
112
- for (const item of items) {
220
+ items.forEach((item, index) => {
113
221
  let shortest = 0;
114
222
  for (let col = 1; col < columns; col++) {
115
223
  if ((heights[col] ?? 0) < (heights[shortest] ?? 0)) shortest = col;
116
224
  }
117
- item.setAttribute("data-column", String(shortest));
118
- heights[shortest] = (heights[shortest] ?? 0) + item.getBoundingClientRect().height + this.gapValue;
119
- }
225
+ const assigned = String(shortest);
226
+ if (item.getAttribute("data-column") !== assigned) {
227
+ item.setAttribute("data-column", assigned);
228
+ changed = true;
229
+ }
230
+ heights[shortest] = (heights[shortest] ?? 0) + (boxes[index] ?? 0) + this.#gap;
231
+ });
120
232
  this.element.style.setProperty(COLUMNS_PROPERTY, String(columns));
121
- if (columns !== this.#lastColumns) {
233
+ if (columns !== this.#lastColumns || changed) {
122
234
  this.#lastColumns = columns;
123
235
  this.dispatch("layout", { detail: { columns } });
124
236
  }
@@ -131,9 +243,9 @@ var MasonryController = class extends Controller {
131
243
  */
132
244
  #columnCount() {
133
245
  const width = this.element.getBoundingClientRect().width;
134
- const denominator = this.minColumnWidthValue + this.gapValue;
246
+ const denominator = this.#minColumnWidth + this.#gap;
135
247
  if (width <= 0 || denominator <= 0) return 1;
136
- return Math.max(1, Math.floor((width + this.gapValue) / denominator));
248
+ return Math.max(1, Math.floor((width + this.#gap) / denominator));
137
249
  }
138
250
  };
139
251