stimeo-ui 0.2.0 → 0.3.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 (227) hide show
  1. package/CHANGELOG.md +163 -0
  2. package/dist/cable/index.d.ts +16 -16
  3. package/dist/cable/index.js.map +1 -1
  4. package/dist/controllers/accordion_controller.d.ts +3 -2
  5. package/dist/controllers/accordion_controller.js +10 -0
  6. package/dist/controllers/accordion_controller.js.map +1 -1
  7. package/dist/controllers/alert_dialog_controller.d.ts +64 -0
  8. package/dist/controllers/alert_dialog_controller.js +318 -0
  9. package/dist/controllers/alert_dialog_controller.js.map +1 -0
  10. package/dist/controllers/breadcrumb_controller.d.ts +97 -10
  11. package/dist/controllers/breadcrumb_controller.js +225 -13
  12. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  13. package/dist/controllers/calendar_controller.d.ts +11 -1
  14. package/dist/controllers/calendar_controller.js +89 -22
  15. package/dist/controllers/calendar_controller.js.map +1 -1
  16. package/dist/controllers/carousel_controller.d.ts +115 -0
  17. package/dist/controllers/carousel_controller.js +313 -0
  18. package/dist/controllers/carousel_controller.js.map +1 -0
  19. package/dist/controllers/clipboard_controller.d.ts +72 -0
  20. package/dist/controllers/clipboard_controller.js +144 -0
  21. package/dist/controllers/clipboard_controller.js.map +1 -0
  22. package/dist/controllers/collapsible_controller.d.ts +70 -0
  23. package/dist/controllers/collapsible_controller.js +327 -0
  24. package/dist/controllers/collapsible_controller.js.map +1 -0
  25. package/dist/controllers/color_picker_controller.d.ts +87 -0
  26. package/dist/controllers/color_picker_controller.js +252 -0
  27. package/dist/controllers/color_picker_controller.js.map +1 -0
  28. package/dist/controllers/combobox_controller.d.ts +14 -1
  29. package/dist/controllers/combobox_controller.js +162 -23
  30. package/dist/controllers/combobox_controller.js.map +1 -1
  31. package/dist/controllers/command_palette_controller.d.ts +2 -0
  32. package/dist/controllers/command_palette_controller.js +194 -17
  33. package/dist/controllers/command_palette_controller.js.map +1 -1
  34. package/dist/controllers/confirm_controller.js.map +1 -1
  35. package/dist/controllers/context_menu_controller.d.ts +7 -6
  36. package/dist/controllers/context_menu_controller.js +32 -10
  37. package/dist/controllers/context_menu_controller.js.map +1 -1
  38. package/dist/controllers/count_up_controller.js +8 -1
  39. package/dist/controllers/count_up_controller.js.map +1 -1
  40. package/dist/controllers/currency_input_controller.d.ts +72 -0
  41. package/dist/controllers/currency_input_controller.js +147 -0
  42. package/dist/controllers/currency_input_controller.js.map +1 -0
  43. package/dist/controllers/data_grid_controller.d.ts +89 -0
  44. package/dist/controllers/data_grid_controller.js +246 -0
  45. package/dist/controllers/data_grid_controller.js.map +1 -0
  46. package/dist/controllers/date_range_picker_controller.d.ts +88 -0
  47. package/dist/controllers/date_range_picker_controller.js +441 -0
  48. package/dist/controllers/date_range_picker_controller.js.map +1 -0
  49. package/dist/controllers/dialog_controller.js.map +1 -1
  50. package/dist/controllers/dismissible_controller.d.ts +57 -0
  51. package/dist/controllers/dismissible_controller.js +117 -0
  52. package/dist/controllers/dismissible_controller.js.map +1 -0
  53. package/dist/controllers/drawer_controller.d.ts +96 -0
  54. package/dist/controllers/drawer_controller.js +630 -0
  55. package/dist/controllers/drawer_controller.js.map +1 -0
  56. package/dist/controllers/dropdown_controller.d.ts +1 -2
  57. package/dist/controllers/dropdown_controller.js.map +1 -1
  58. package/dist/controllers/editable_controller.d.ts +73 -0
  59. package/dist/controllers/editable_controller.js +169 -0
  60. package/dist/controllers/editable_controller.js.map +1 -0
  61. package/dist/controllers/file_dropzone_controller.d.ts +92 -0
  62. package/dist/controllers/file_dropzone_controller.js +165 -0
  63. package/dist/controllers/file_dropzone_controller.js.map +1 -0
  64. package/dist/controllers/filter_controller.d.ts +60 -0
  65. package/dist/controllers/filter_controller.js +86 -0
  66. package/dist/controllers/filter_controller.js.map +1 -0
  67. package/dist/controllers/flash_controller.js +36 -5
  68. package/dist/controllers/flash_controller.js.map +1 -1
  69. package/dist/controllers/focus_controller.d.ts +1 -1
  70. package/dist/controllers/focus_controller.js.map +1 -1
  71. package/dist/controllers/form_field_controller.js.map +1 -1
  72. package/dist/controllers/form_validation_controller.js +1 -1
  73. package/dist/controllers/form_validation_controller.js.map +1 -1
  74. package/dist/controllers/highlight_controller.js +6 -4
  75. package/dist/controllers/highlight_controller.js.map +1 -1
  76. package/dist/controllers/hover_card_controller.d.ts +1 -1
  77. package/dist/controllers/hover_card_controller.js.map +1 -1
  78. package/dist/controllers/intersection_controller.js +67 -19
  79. package/dist/controllers/intersection_controller.js.map +1 -1
  80. package/dist/controllers/lazy_frame_controller.js +54 -11
  81. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  82. package/dist/controllers/listbox_controller.d.ts +9 -2
  83. package/dist/controllers/listbox_controller.js +257 -53
  84. package/dist/controllers/listbox_controller.js.map +1 -1
  85. package/dist/controllers/local_time_controller.js +2 -2
  86. package/dist/controllers/local_time_controller.js.map +1 -1
  87. package/dist/controllers/masonry_controller.d.ts +54 -0
  88. package/dist/controllers/masonry_controller.js +142 -0
  89. package/dist/controllers/masonry_controller.js.map +1 -0
  90. package/dist/controllers/menu_controller.d.ts +29 -11
  91. package/dist/controllers/menu_controller.js +104 -17
  92. package/dist/controllers/menu_controller.js.map +1 -1
  93. package/dist/controllers/menubar_controller.d.ts +142 -0
  94. package/dist/controllers/menubar_controller.js +785 -0
  95. package/dist/controllers/menubar_controller.js.map +1 -0
  96. package/dist/controllers/meter_controller.js.map +1 -1
  97. package/dist/controllers/multi_select_controller.d.ts +141 -0
  98. package/dist/controllers/multi_select_controller.js +755 -0
  99. package/dist/controllers/multi_select_controller.js.map +1 -0
  100. package/dist/controllers/navigation_menu_controller.d.ts +146 -0
  101. package/dist/controllers/navigation_menu_controller.js +511 -0
  102. package/dist/controllers/navigation_menu_controller.js.map +1 -0
  103. package/dist/controllers/nested_form_controller.d.ts +4 -5
  104. package/dist/controllers/nested_form_controller.js.map +1 -1
  105. package/dist/controllers/number_input_controller.d.ts +1 -1
  106. package/dist/controllers/number_input_controller.js +7 -0
  107. package/dist/controllers/number_input_controller.js.map +1 -1
  108. package/dist/controllers/otp_controller.js +18 -1
  109. package/dist/controllers/otp_controller.js.map +1 -1
  110. package/dist/controllers/overflow_indicator_controller.d.ts +15 -6
  111. package/dist/controllers/overflow_indicator_controller.js +246 -27
  112. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  113. package/dist/controllers/overflow_menu_controller.d.ts +78 -19
  114. package/dist/controllers/overflow_menu_controller.js +381 -57
  115. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  116. package/dist/controllers/pagination_controller.d.ts +36 -5
  117. package/dist/controllers/pagination_controller.js +163 -32
  118. package/dist/controllers/pagination_controller.js.map +1 -1
  119. package/dist/controllers/password_reveal_controller.d.ts +49 -0
  120. package/dist/controllers/password_reveal_controller.js +117 -0
  121. package/dist/controllers/password_reveal_controller.js.map +1 -0
  122. package/dist/controllers/persist_controller.js +6 -6
  123. package/dist/controllers/persist_controller.js.map +1 -1
  124. package/dist/controllers/pointer_drag_controller.d.ts +2 -3
  125. package/dist/controllers/pointer_drag_controller.js +9 -1
  126. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  127. package/dist/controllers/popover_controller.d.ts +1 -2
  128. package/dist/controllers/popover_controller.js +2 -2
  129. package/dist/controllers/popover_controller.js.map +1 -1
  130. package/dist/controllers/portal_controller.d.ts +4 -4
  131. package/dist/controllers/portal_controller.js.map +1 -1
  132. package/dist/controllers/progress_controller.js.map +1 -1
  133. package/dist/controllers/radio_group_controller.js +22 -3
  134. package/dist/controllers/radio_group_controller.js.map +1 -1
  135. package/dist/controllers/range_slider_controller.d.ts +97 -0
  136. package/dist/controllers/range_slider_controller.js +192 -0
  137. package/dist/controllers/range_slider_controller.js.map +1 -0
  138. package/dist/controllers/rating_controller.js +16 -2
  139. package/dist/controllers/rating_controller.js.map +1 -1
  140. package/dist/controllers/read_more_controller.d.ts +52 -0
  141. package/dist/controllers/read_more_controller.js +238 -0
  142. package/dist/controllers/read_more_controller.js.map +1 -0
  143. package/dist/controllers/reading_progress_controller.js.map +1 -1
  144. package/dist/controllers/resizable_controller.d.ts +3 -1
  145. package/dist/controllers/resizable_controller.js +65 -1
  146. package/dist/controllers/resizable_controller.js.map +1 -1
  147. package/dist/controllers/roving_controller.js +17 -2
  148. package/dist/controllers/roving_controller.js.map +1 -1
  149. package/dist/controllers/scroll_area_controller.js +101 -14
  150. package/dist/controllers/scroll_area_controller.js.map +1 -1
  151. package/dist/controllers/scroll_restore_controller.d.ts +54 -0
  152. package/dist/controllers/scroll_restore_controller.js +93 -0
  153. package/dist/controllers/scroll_restore_controller.js.map +1 -0
  154. package/dist/controllers/scroll_visibility_controller.d.ts +6 -3
  155. package/dist/controllers/scroll_visibility_controller.js +40 -6
  156. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  157. package/dist/controllers/scrollspy_controller.d.ts +64 -15
  158. package/dist/controllers/scrollspy_controller.js +369 -74
  159. package/dist/controllers/scrollspy_controller.js.map +1 -1
  160. package/dist/controllers/separator_controller.d.ts +69 -0
  161. package/dist/controllers/separator_controller.js +96 -0
  162. package/dist/controllers/separator_controller.js.map +1 -0
  163. package/dist/controllers/sidebar_controller.d.ts +107 -0
  164. package/dist/controllers/sidebar_controller.js +761 -0
  165. package/dist/controllers/sidebar_controller.js.map +1 -0
  166. package/dist/controllers/skeleton_controller.js +1 -1
  167. package/dist/controllers/skeleton_controller.js.map +1 -1
  168. package/dist/controllers/slider_controller.d.ts +11 -1
  169. package/dist/controllers/slider_controller.js +32 -6
  170. package/dist/controllers/slider_controller.js.map +1 -1
  171. package/dist/controllers/sortable_controller.d.ts +3 -1
  172. package/dist/controllers/sortable_controller.js +34 -3
  173. package/dist/controllers/sortable_controller.js.map +1 -1
  174. package/dist/controllers/spinner_controller.js +1 -1
  175. package/dist/controllers/spinner_controller.js.map +1 -1
  176. package/dist/controllers/stepper_controller.d.ts +4 -0
  177. package/dist/controllers/stepper_controller.js +28 -12
  178. package/dist/controllers/stepper_controller.js.map +1 -1
  179. package/dist/controllers/stick_to_bottom_controller.d.ts +5 -6
  180. package/dist/controllers/stick_to_bottom_controller.js +9 -4
  181. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  182. package/dist/controllers/sticky_observer_controller.d.ts +17 -4
  183. package/dist/controllers/sticky_observer_controller.js +109 -20
  184. package/dist/controllers/sticky_observer_controller.js.map +1 -1
  185. package/dist/controllers/submit_once_controller.d.ts +2 -2
  186. package/dist/controllers/submit_once_controller.js.map +1 -1
  187. package/dist/controllers/switch_controller.js +1 -0
  188. package/dist/controllers/switch_controller.js.map +1 -1
  189. package/dist/controllers/tabs_controller.d.ts +13 -4
  190. package/dist/controllers/tabs_controller.js +26 -3
  191. package/dist/controllers/tabs_controller.js.map +1 -1
  192. package/dist/controllers/tags_input_controller.d.ts +91 -0
  193. package/dist/controllers/tags_input_controller.js +295 -0
  194. package/dist/controllers/tags_input_controller.js.map +1 -0
  195. package/dist/controllers/theme_controller.d.ts +1 -1
  196. package/dist/controllers/theme_controller.js +42 -13
  197. package/dist/controllers/theme_controller.js.map +1 -1
  198. package/dist/controllers/time_picker_controller.d.ts +67 -0
  199. package/dist/controllers/time_picker_controller.js +231 -0
  200. package/dist/controllers/time_picker_controller.js.map +1 -0
  201. package/dist/controllers/toast_controller.d.ts +5 -14
  202. package/dist/controllers/toast_controller.js +40 -14
  203. package/dist/controllers/toast_controller.js.map +1 -1
  204. package/dist/controllers/toggle_group_controller.js +23 -2
  205. package/dist/controllers/toggle_group_controller.js.map +1 -1
  206. package/dist/controllers/toolbar_controller.d.ts +53 -13
  207. package/dist/controllers/toolbar_controller.js +230 -31
  208. package/dist/controllers/toolbar_controller.js.map +1 -1
  209. package/dist/controllers/tooltip_controller.d.ts +1 -1
  210. package/dist/controllers/tooltip_controller.js.map +1 -1
  211. package/dist/controllers/transition_controller.d.ts +14 -8
  212. package/dist/controllers/transition_controller.js +153 -38
  213. package/dist/controllers/transition_controller.js.map +1 -1
  214. package/dist/controllers/tree_view_controller.d.ts +135 -0
  215. package/dist/controllers/tree_view_controller.js +691 -0
  216. package/dist/controllers/tree_view_controller.js.map +1 -0
  217. package/dist/index.d.ts +27 -1818
  218. package/dist/index.js +4256 -915
  219. package/dist/index.js.map +1 -1
  220. package/dist/inspector/cli.d.ts +375 -39
  221. package/dist/inspector/cli.js +228 -14
  222. package/dist/inspector/cli.js.map +1 -1
  223. package/dist/inspector/cli_bin.js +419 -48
  224. package/dist/inspector/cli_bin.js.map +1 -1
  225. package/dist/inspector/examples.json +26 -26
  226. package/dist/inspector/manifest.json +1054 -127
  227. package/package.json +5 -6
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/controllers/sortable_controller.ts"],"names":[],"mappings":"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","file":"sortable_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\n/** A reorder in flight: one item picked up by pointer or keyboard. */\ninterface SortSession {\n item: HTMLElement;\n /** Index at pickup, for the `reorder` detail and the cancel restore. */\n from: number;\n /** Last primary-axis cumulative delta consumed by keyboard stepping. */\n lastPrimary: number;\n}\n\n/**\n * Headless, accessible **sortable** (single-list reorder) — the a11y-first\n * drag-and-drop that is the flagship consumer of the\n * `stimeo--pointer-drag` primitive. The composition is markup-level and each\n * layer keeps its job: `pointer-drag` (on every item) emits the normalized drag\n * signal with its built-in keyboard alternative, `roving` (on the list) keeps\n * the handles a single Tab stop, and this controller interprets the signal —\n * live-reordering the DOM, announcing each step through a `status` live region,\n * and reporting the final `reorder`. No dedicated APG pattern exists for\n * drag-and-drop; the keyboard model (grab → arrows → drop / Escape) comes from\n * `pointer-drag` and the announcements make it non-visually trackable\n * (WCAG 2.1.1 / 2.5.7 / 4.1.3). Core (zero dependencies).\n *\n * Markup contract (identifier: `stimeo--sortable`):\n * <div data-controller=\"stimeo--sortable\">\n * <ul data-stimeo--sortable-target=\"list\" data-controller=\"stimeo--roving\"\n * data-stimeo--roving-orientation-value=\"vertical\">\n * <li data-stimeo--sortable-target=\"item\" data-stimeo--sortable-name=\"Card A\"\n * data-controller=\"stimeo--pointer-drag\"\n * data-stimeo--pointer-drag-axis-value=\"y\">\n * <span>Card A</span>\n * <button type=\"button\" aria-label=\"Reorder Card A\"\n * data-stimeo--pointer-drag-target=\"handle\"\n * data-stimeo--roving-target=\"item\">⠿</button>\n * </li>\n * …\n * </ul>\n * <span role=\"status\" data-stimeo--sortable-target=\"status\"></span>\n * </div>\n *\n * The composed values must follow the sort axis: roving's `orientation`\n * (which defaults to `horizontal`, so a vertical list has to author\n * `vertical`) and pointer-drag's `axis`. `stimeo check` enforces both\n * alignments as composition rules (`composition-mismatch`).\n *\n * Pointer flow: dragging an item live-moves it whenever the pointer crosses a\n * sibling's midpoint (per `orientation`). Keyboard flow: each synthetic\n * `pointer-drag` move steps the item one position. Dropping dispatches\n * `reorder` (`{ item, from, to }`, zero-based) when the position changed;\n * Escape / `pointercancel` restores the pickup position. Every step is mirrored\n * into the `status` live region (localizable via `data-grabbed` / `data-moved`\n * / `data-dropped` / `data-canceled` templates with `%{name}` / `%{position}` /\n * `%{total}` placeholders; terse English is the fallback).\n *\n * @remarks\n * Behavior only — the ghost/placeholder/drop-hint visuals are the consumer's\n * CSS, keyed off `pointer-drag`'s `data-dragging`/`data-grabbed` on the item\n * and this controller's `data-sortable-dragging` on the root. Persistence is\n * the consumer's `reorder` listener (post the new position; a Turbo Stream\n * broadcast makes the board collaborative). The DOM order is the single source\n * of truth: items are read live on every event, so rows appended by Turbo need\n * no rewiring, and a Turbo cache restore reconnects idempotently (`connect()`\n * clears the transient root hook; a drag cannot survive a navigation). Only the\n * event listeners are held, and they are removed on `disconnect()`. Multi-list\n * / kanban, nested DnD, auto-scroll at the edges, and virtualized lists are\n * deliberately out of this single-list scope.\n */\nexport class SortableController extends Controller<HTMLElement> {\n static override targets = [\"list\", \"item\", \"status\"];\n static override values = {\n orientation: { type: String, default: \"vertical\" },\n };\n static events = [\"reorder\"] as const;\n\n declare readonly hasListTarget: boolean;\n declare readonly listTarget: HTMLElement;\n declare readonly itemTargets: HTMLElement[];\n declare readonly hasStatusTarget: boolean;\n declare readonly statusTarget: HTMLElement;\n declare orientationValue: string;\n\n #session: SortSession | null = null;\n\n override connect(): void {\n // A drag cannot survive a navigation: drop the hook a Turbo cache snapshot\n // may have preserved mid-drag (idempotent reconnect).\n this.element.removeAttribute(\"data-sortable-dragging\");\n this.element.addEventListener(\"stimeo--pointer-drag:start\", this.#onDragStart);\n this.element.addEventListener(\"stimeo--pointer-drag:move\", this.#onDragMove);\n this.element.addEventListener(\"stimeo--pointer-drag:end\", this.#onDragEnd);\n this.element.addEventListener(\"stimeo--pointer-drag:cancel\", this.#onDragCancel);\n }\n\n override disconnect(): void {\n this.element.removeEventListener(\"stimeo--pointer-drag:start\", this.#onDragStart);\n this.element.removeEventListener(\"stimeo--pointer-drag:move\", this.#onDragMove);\n this.element.removeEventListener(\"stimeo--pointer-drag:end\", this.#onDragEnd);\n this.element.removeEventListener(\"stimeo--pointer-drag:cancel\", this.#onDragCancel);\n this.#session = null;\n this.element.removeAttribute(\"data-sortable-dragging\");\n }\n\n /** Picks the item up: remembers its origin and announces the grab. */\n readonly #onDragStart = (event: Event): void => {\n // One reorder at a time: a start from another item (each item has its own\n // pointer-drag instance) must not clobber the live session — its from\n // index and cancel restore would be lost.\n if (this.#session) return;\n const item = this.#itemFor(event.target);\n if (!item) return;\n this.#session = { item, from: this.#items().indexOf(item), lastPrimary: 0 };\n this.element.setAttribute(\"data-sortable-dragging\", \"true\");\n this.#announce(\"grabbed\", item);\n };\n\n readonly #onDragMove = (event: Event): void => {\n const session = this.#session;\n const detail = (event as CustomEvent<Record<string, number | string>>).detail;\n if (!session || this.#itemFor(event.target) !== session.item) return;\n\n if (detail.pointerType === \"keyboard\") {\n this.#stepFromKeyboard(session, detail);\n } else {\n this.#followPointer(session, detail);\n }\n };\n\n /** Drops the item: announces, then reports `reorder` if the position changed. */\n readonly #onDragEnd = (event: Event): void => {\n const session = this.#session;\n if (!session || this.#itemFor(event.target) !== session.item) return;\n this.#session = null;\n this.element.removeAttribute(\"data-sortable-dragging\");\n this.#announce(\"dropped\", session.item);\n const to = this.#items().indexOf(session.item);\n if (to !== session.from) {\n this.dispatch(\"reorder\", { detail: { item: session.item, from: session.from, to } });\n }\n };\n\n /** Restores the pickup position (Escape / OS `pointercancel`). */\n readonly #onDragCancel = (event: Event): void => {\n const session = this.#session;\n if (!session || this.#itemFor(event.target) !== session.item) return;\n this.#session = null;\n this.element.removeAttribute(\"data-sortable-dragging\");\n this.#moveTo(session.item, session.from);\n this.#announce(\"canceled\", session.item);\n };\n\n /**\n * Keyboard stepping: `pointer-drag` reports *cumulative* synthetic deltas, so\n * the difference from the last consumed value is one arrow press — its sign is\n * the direction. Cross-axis arrows never change the primary delta (no move).\n */\n #stepFromKeyboard(session: SortSession, detail: Record<string, number | string>): void {\n const primary = Number(this.#isVertical ? detail.dy : detail.dx) || 0;\n const delta = primary - session.lastPrimary;\n session.lastPrimary = primary;\n if (delta === 0) return;\n\n const items = this.#items();\n const index = items.indexOf(session.item);\n const next = Math.max(0, Math.min(index + (delta > 0 ? 1 : -1), items.length - 1));\n if (next === index) return;\n this.#moveTo(session.item, next);\n this.#announce(\"moved\", session.item);\n }\n\n /**\n * Pointer following: the item moves to the slot whose siblings' midpoints the\n * pointer has passed (per `orientation`). Skipped when the list has no layout\n * geometry (every rect is zero — nothing meaningful to compare against).\n */\n #followPointer(session: SortSession, detail: Record<string, number | string>): void {\n const pointer = Number(this.#isVertical ? detail.y : detail.x) || 0;\n const others = this.#items().filter((item) => item !== session.item);\n if (others.length === 0) return;\n\n let laidOut = false;\n let target = 0;\n for (const other of others) {\n const rect = other.getBoundingClientRect();\n if (rect.width > 0 || rect.height > 0) laidOut = true;\n const midpoint = this.#isVertical ? rect.top + rect.height / 2 : rect.left + rect.width / 2;\n if (pointer > midpoint) target += 1;\n }\n if (!laidOut) return;\n\n // The item's index among all items equals the count of others before it,\n // so it doubles as the current insertion slot in others-space.\n const current = this.#items().indexOf(session.item);\n if (target !== current) {\n this.#moveTo(session.item, target);\n this.#announce(\"moved\", session.item);\n }\n }\n\n /** Reinserts `item` so it lands at `index` among the list's items. */\n #moveTo(item: HTMLElement, index: number): void {\n const others = this.#items().filter((candidate) => candidate !== item);\n const clamped = Math.max(0, Math.min(index, others.length));\n const reference = others[clamped] ?? null;\n // Real browsers drop focus when the focused node is re-inserted (the move\n // is a remove+insert), which would strand the keyboard grab after one\n // arrow press — restore it so focus rides the moved item.\n const active = document.activeElement;\n const hadFocus = active instanceof HTMLElement && item.contains(active);\n this.#list.insertBefore(item, reference);\n if (hadFocus) active.focus();\n }\n\n /**\n * Mirrors a step into the `status` live region. Copy is localizable through\n * `data-grabbed` / `data-moved` / `data-dropped` / `data-canceled` templates on\n * the status element (`%{name}` / `%{position}` / `%{total}` placeholders);\n * terse English is the fallback (the library's shared status-channel design).\n */\n #announce(key: \"grabbed\" | \"moved\" | \"dropped\" | \"canceled\", item: HTMLElement): void {\n if (!this.hasStatusTarget) return;\n const position = String(this.#items().indexOf(item) + 1);\n const total = String(this.#items().length);\n const name = this.#nameOf(item);\n const fallback = {\n grabbed: `Grabbed ${name}, position ${position} of ${total}`,\n moved: `${name}, position ${position} of ${total}`,\n dropped: `Dropped ${name} at position ${position} of ${total}`,\n canceled: `Reorder canceled, ${name} returned to position ${position} of ${total}`,\n } as const;\n // Single-pass function replacement keeps the substitution literal-safe:\n // `$`-sequences in the (author/content-derived) name never expand, and a\n // name that happens to contain a placeholder token is not re-substituted.\n const values: Record<string, string> = { name, position, total };\n const template = this.statusTarget.dataset[key];\n this.statusTarget.textContent = template\n ? template.replace(/%\\{(name|position|total)\\}/g, (match, token: string) => {\n return values[token] ?? match;\n })\n : fallback[key];\n }\n\n /** The announced item name: the authored override, else its collapsed text. */\n #nameOf(item: HTMLElement): string {\n const authored = item.getAttribute(\"data-stimeo--sortable-name\");\n if (authored) return authored;\n return (item.textContent ?? \"\").replace(/\\s+/g, \" \").trim();\n }\n\n /** Resolves the sortable item owning a bubbled `pointer-drag` event. */\n #itemFor(target: EventTarget | null): HTMLElement | null {\n const node = target as Node | null;\n if (!node) return null;\n return this.#items().find((item) => item === node || item.contains(node)) ?? null;\n }\n\n /** The items in live DOM order (targets re-query the DOM on every access). */\n #items(): HTMLElement[] {\n return this.itemTargets;\n }\n\n /** The reorder container: the `list` target when present, else the element. */\n get #list(): HTMLElement {\n return this.hasListTarget ? this.listTarget : this.element;\n }\n\n get #isVertical(): boolean {\n return this.orientationValue !== \"horizontal\";\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/logical_scroll.ts","../../src/controllers/sortable_controller.ts"],"names":[],"mappings":"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","file":"sortable_controller.js","sourcesContent":["/** Normalized scroll position and maximum distance on one logical axis. */\nexport interface LogicalScrollMetrics {\n position: number;\n max: number;\n}\n\n/**\n * Whether horizontal scrolling on `element` follows right-to-left inline flow.\n *\n * Resolved from the **computed** `direction`, so the authoring contract is the\n * usual `dir=\"rtl\"` (or a stylesheet) on the element or any ancestor.\n *\n * Scope: horizontal writing modes. A vertical writing mode (`writing-mode:\n * vertical-rl`) also inverts the horizontal axis, which this check does not\n * model — vertical writing modes are out of scope for the scroll utilities\n * (their consumers describe axes as horizontal/vertical, not inline/block).\n */\nexport function isRtl(element: Element): boolean {\n return window.getComputedStyle(element).direction === \"rtl\";\n}\n\n/**\n * Returns scroll distance from the logical start edge.\n *\n * CSSOM View exposes standards-mode RTL horizontal offsets as `0` at the inline\n * start (right) and increasingly negative values toward the inline end (left).\n * The normalized position is always clamped to `[0, max]`, which also absorbs\n * Safari's elastic overscroll values.\n */\nexport function logicalScrollMetrics(\n element: HTMLElement,\n horizontal: boolean,\n): LogicalScrollMetrics {\n const max = Math.max(\n 0,\n horizontal\n ? element.scrollWidth - element.clientWidth\n : element.scrollHeight - element.clientHeight,\n );\n const raw = horizontal ? element.scrollLeft : element.scrollTop;\n const position = horizontal && isRtl(element) ? -raw : raw;\n return { position: Math.min(max, Math.max(0, position)), max };\n}\n\n/**\n * Converts a logical start/end delta to the physical value accepted by\n * `Element.scrollBy`.\n */\nexport function physicalScrollDelta(\n element: HTMLElement,\n horizontal: boolean,\n logicalDelta: number,\n): number {\n return horizontal && isRtl(element) ? -logicalDelta : logicalDelta;\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { isRtl } from \"../utils/logical_scroll\";\n\n/** A reorder in flight: one item picked up by pointer or keyboard. */\ninterface SortSession {\n item: HTMLElement;\n /** Index at pickup, for the `reorder` detail and the cancel restore. */\n from: number;\n /** Last primary-axis cumulative delta consumed by keyboard stepping. */\n lastPrimary: number;\n}\n\n/**\n * Headless, accessible **sortable** (single-list reorder) — the a11y-first\n * drag-and-drop that is the flagship consumer of the\n * `stimeo--pointer-drag` primitive. The composition is markup-level and each\n * layer keeps its job: `pointer-drag` (on every item) emits the normalized drag\n * signal with its built-in keyboard alternative, `roving` (on the list) keeps\n * the handles a single Tab stop, and this controller interprets the signal —\n * live-reordering the DOM, announcing each step through a `status` live region,\n * and reporting the final `reorder`. No dedicated APG pattern exists for\n * drag-and-drop; the keyboard model (grab → arrows → drop / Escape) comes from\n * `pointer-drag` and the announcements make it non-visually trackable\n * (WCAG 2.1.1 / 2.5.7 / 4.1.3). Core (zero dependencies).\n *\n * Markup contract (identifier: `stimeo--sortable`):\n * <div data-controller=\"stimeo--sortable\">\n * <ul data-stimeo--sortable-target=\"list\" data-controller=\"stimeo--roving\"\n * data-stimeo--roving-orientation-value=\"vertical\">\n * <li data-stimeo--sortable-target=\"item\" data-stimeo--sortable-name=\"Card A\"\n * data-controller=\"stimeo--pointer-drag\"\n * data-stimeo--pointer-drag-axis-value=\"y\">\n * <span>Card A</span>\n * <button type=\"button\" aria-label=\"Reorder Card A\"\n * data-stimeo--pointer-drag-target=\"handle\"\n * data-stimeo--roving-target=\"item\">⠿</button>\n * </li>\n * …\n * </ul>\n * <span role=\"status\" data-stimeo--sortable-target=\"status\"></span>\n * </div>\n *\n * The composed values must follow the sort axis: roving's `orientation`\n * (which defaults to `horizontal`, so a vertical list has to author\n * `vertical`) and pointer-drag's `axis`. `stimeo check` enforces both\n * alignments as composition rules (`composition-mismatch`).\n *\n * Pointer flow: dragging an item live-moves it whenever the pointer crosses a\n * sibling's midpoint (per `orientation`). Keyboard flow: each synthetic\n * `pointer-drag` move steps the item one position. A horizontal list under\n * `dir=\"rtl\"` reverses both, since DOM order then runs right-to-left while\n * `pointer-drag` reports physical coordinates. Dropping dispatches\n * `reorder` (`{ item, from, to }`, zero-based) when the position changed;\n * Escape / `pointercancel` restores the pickup position. Every step is mirrored\n * into the `status` live region (localizable via `data-grabbed` / `data-moved`\n * / `data-dropped` / `data-canceled` templates with `%{name}` / `%{position}` /\n * `%{total}` placeholders; terse English is the fallback).\n *\n * @remarks\n * Behavior only — the ghost/placeholder/drop-hint visuals are the consumer's\n * CSS, keyed off `pointer-drag`'s `data-dragging`/`data-grabbed` on the item\n * and this controller's `data-sortable-dragging` on the root. Persistence is\n * the consumer's `reorder` listener (post the new position; a Turbo Stream\n * broadcast makes the board collaborative). The DOM order is the single source\n * of truth: items are read live on every event, so rows appended by Turbo need\n * no rewiring, and a Turbo cache restore reconnects idempotently (`connect()`\n * clears the transient root hook; a drag cannot survive a navigation). Only the\n * event listeners are held, and they are removed on `disconnect()`. Multi-list\n * / kanban, nested DnD, auto-scroll at the edges, and virtualized lists are\n * deliberately out of this single-list scope.\n */\nexport class SortableController extends Controller<HTMLElement> {\n static override targets = [\"list\", \"item\", \"status\"];\n static override values = {\n orientation: { type: String, default: \"vertical\" },\n };\n static events = [\"reorder\"] as const;\n\n declare readonly hasListTarget: boolean;\n declare readonly listTarget: HTMLElement;\n declare readonly itemTargets: HTMLElement[];\n declare readonly hasStatusTarget: boolean;\n declare readonly statusTarget: HTMLElement;\n declare orientationValue: string;\n\n #session: SortSession | null = null;\n\n override connect(): void {\n // A drag cannot survive a navigation: drop the hook a Turbo cache snapshot\n // may have preserved mid-drag (idempotent reconnect).\n this.element.removeAttribute(\"data-sortable-dragging\");\n this.element.addEventListener(\"stimeo--pointer-drag:start\", this.#onDragStart);\n this.element.addEventListener(\"stimeo--pointer-drag:move\", this.#onDragMove);\n this.element.addEventListener(\"stimeo--pointer-drag:end\", this.#onDragEnd);\n this.element.addEventListener(\"stimeo--pointer-drag:cancel\", this.#onDragCancel);\n }\n\n override disconnect(): void {\n this.element.removeEventListener(\"stimeo--pointer-drag:start\", this.#onDragStart);\n this.element.removeEventListener(\"stimeo--pointer-drag:move\", this.#onDragMove);\n this.element.removeEventListener(\"stimeo--pointer-drag:end\", this.#onDragEnd);\n this.element.removeEventListener(\"stimeo--pointer-drag:cancel\", this.#onDragCancel);\n this.#session = null;\n this.element.removeAttribute(\"data-sortable-dragging\");\n }\n\n /** Picks the item up: remembers its origin and announces the grab. */\n readonly #onDragStart = (event: Event): void => {\n // One reorder at a time: a start from another item (each item has its own\n // pointer-drag instance) must not clobber the live session — its from\n // index and cancel restore would be lost.\n if (this.#session) return;\n const item = this.#itemFor(event.target);\n if (!item) return;\n this.#session = { item, from: this.#items().indexOf(item), lastPrimary: 0 };\n this.element.setAttribute(\"data-sortable-dragging\", \"true\");\n this.#announce(\"grabbed\", item);\n };\n\n readonly #onDragMove = (event: Event): void => {\n const session = this.#session;\n const detail = (event as CustomEvent<Record<string, number | string>>).detail;\n if (!session || this.#itemFor(event.target) !== session.item) return;\n\n if (detail.pointerType === \"keyboard\") {\n this.#stepFromKeyboard(session, detail);\n } else {\n this.#followPointer(session, detail);\n }\n };\n\n /** Drops the item: announces, then reports `reorder` if the position changed. */\n readonly #onDragEnd = (event: Event): void => {\n const session = this.#session;\n if (!session || this.#itemFor(event.target) !== session.item) return;\n this.#session = null;\n this.element.removeAttribute(\"data-sortable-dragging\");\n this.#announce(\"dropped\", session.item);\n const to = this.#items().indexOf(session.item);\n if (to !== session.from) {\n this.dispatch(\"reorder\", { detail: { item: session.item, from: session.from, to } });\n }\n };\n\n /** Restores the pickup position (Escape / OS `pointercancel`). */\n readonly #onDragCancel = (event: Event): void => {\n const session = this.#session;\n if (!session || this.#itemFor(event.target) !== session.item) return;\n this.#session = null;\n this.element.removeAttribute(\"data-sortable-dragging\");\n this.#moveTo(session.item, session.from);\n this.#announce(\"canceled\", session.item);\n };\n\n /**\n * Keyboard stepping: `pointer-drag` reports *cumulative* synthetic deltas, so\n * the difference from the last consumed value is one arrow press — its sign is\n * the direction. Cross-axis arrows never change the primary delta (no move).\n *\n * The sign is physical (`ArrowRight` is always `+dx`), so a right-to-left row\n * has to invert it: there, moving the item rightward means moving it *earlier*\n * in the DOM. Skipping this would also split the two halves of one keypress —\n * `roving` already moves focus logically, so the same arrow would send the\n * focus and the grabbed item opposite ways.\n */\n #stepFromKeyboard(session: SortSession, detail: Record<string, number | string>): void {\n const primary = Number(this.#isVertical ? detail.dy : detail.dx) || 0;\n const delta = primary - session.lastPrimary;\n session.lastPrimary = primary;\n if (delta === 0) return;\n\n const items = this.#items();\n const index = items.indexOf(session.item);\n const step = (delta > 0 ? 1 : -1) * (this.#isReversed ? -1 : 1);\n const next = Math.max(0, Math.min(index + step, items.length - 1));\n if (next === index) return;\n this.#moveTo(session.item, next);\n this.#announce(\"moved\", session.item);\n }\n\n /**\n * Pointer following: the item moves to the slot whose siblings' midpoints the\n * pointer has passed (per `orientation`). Skipped when the list has no layout\n * geometry (every rect is zero — nothing meaningful to compare against).\n */\n #followPointer(session: SortSession, detail: Record<string, number | string>): void {\n const pointer = Number(this.#isVertical ? detail.y : detail.x) || 0;\n const others = this.#items().filter((item) => item !== session.item);\n if (others.length === 0) return;\n\n let laidOut = false;\n let target = 0;\n // `target` counts the siblings that precede the pointer *in DOM order*, and\n // the two agree only while DOM order runs the same way as the coordinate. In\n // a right-to-left row it runs the other way, so the comparison flips;\n // leaving it physical would mix two orderings into one number and drop the\n // item at a slot the pointer never crossed. Resolved once: it cannot change\n // mid-loop, and reading it per sibling would re-run `getComputedStyle` on\n // every pointermove.\n const reversed = this.#isReversed;\n for (const other of others) {\n const rect = other.getBoundingClientRect();\n if (rect.width > 0 || rect.height > 0) laidOut = true;\n const midpoint = this.#isVertical ? rect.top + rect.height / 2 : rect.left + rect.width / 2;\n const precedes = reversed ? pointer < midpoint : pointer > midpoint;\n if (precedes) target += 1;\n }\n if (!laidOut) return;\n\n // The item's index among all items equals the count of others before it,\n // so it doubles as the current insertion slot in others-space.\n const current = this.#items().indexOf(session.item);\n if (target !== current) {\n this.#moveTo(session.item, target);\n this.#announce(\"moved\", session.item);\n }\n }\n\n /** Reinserts `item` so it lands at `index` among the list's items. */\n #moveTo(item: HTMLElement, index: number): void {\n const others = this.#items().filter((candidate) => candidate !== item);\n const clamped = Math.max(0, Math.min(index, others.length));\n const reference = others[clamped] ?? null;\n // Real browsers drop focus when the focused node is re-inserted (the move\n // is a remove+insert), which would strand the keyboard grab after one\n // arrow press — restore it so focus rides the moved item.\n const active = document.activeElement;\n const hadFocus = active instanceof HTMLElement && item.contains(active);\n this.#list.insertBefore(item, reference);\n if (hadFocus) active.focus();\n }\n\n /**\n * Mirrors a step into the `status` live region. Copy is localizable through\n * `data-grabbed` / `data-moved` / `data-dropped` / `data-canceled` templates on\n * the status element (`%{name}` / `%{position}` / `%{total}` placeholders);\n * terse English is the fallback.\n */\n #announce(key: \"grabbed\" | \"moved\" | \"dropped\" | \"canceled\", item: HTMLElement): void {\n if (!this.hasStatusTarget) return;\n const position = String(this.#items().indexOf(item) + 1);\n const total = String(this.#items().length);\n const name = this.#nameOf(item);\n const fallback = {\n grabbed: `Grabbed ${name}, position ${position} of ${total}`,\n moved: `${name}, position ${position} of ${total}`,\n dropped: `Dropped ${name} at position ${position} of ${total}`,\n canceled: `Reorder canceled, ${name} returned to position ${position} of ${total}`,\n } as const;\n // Single-pass function replacement keeps the substitution literal-safe:\n // `$`-sequences in the (author/content-derived) name never expand, and a\n // name that happens to contain a placeholder token is not re-substituted.\n const values: Record<string, string> = { name, position, total };\n const template = this.statusTarget.dataset[key];\n this.statusTarget.textContent = template\n ? template.replace(/%\\{(name|position|total)\\}/g, (match, token: string) => {\n return values[token] ?? match;\n })\n : fallback[key];\n }\n\n /** The announced item name: the authored override, else its collapsed text. */\n #nameOf(item: HTMLElement): string {\n const authored = item.getAttribute(\"data-stimeo--sortable-name\");\n if (authored) return authored;\n return (item.textContent ?? \"\").replace(/\\s+/g, \" \").trim();\n }\n\n /** Resolves the sortable item owning a bubbled `pointer-drag` event. */\n #itemFor(target: EventTarget | null): HTMLElement | null {\n const node = target as Node | null;\n if (!node) return null;\n return this.#items().find((item) => item === node || item.contains(node)) ?? null;\n }\n\n /** The items in live DOM order (targets re-query the DOM on every access). */\n #items(): HTMLElement[] {\n return this.itemTargets;\n }\n\n /** The reorder container: the `list` target when present, else the element. */\n get #list(): HTMLElement {\n return this.hasListTarget ? this.listTarget : this.element;\n }\n\n get #isVertical(): boolean {\n return this.orientationValue !== \"horizontal\";\n }\n\n /**\n * Whether DOM order runs opposite to the primary coordinate — true only for a\n * horizontal row under `dir=\"rtl\"`, where the first item sits at the largest\n * `x`. Both drag paths reach this controller in physical terms (`pointer-drag`\n * documents its deltas as physical and hands RTL to its consumer, which is\n * this controller), so both have to be mapped back onto DOM order here.\n *\n * Read from the list, the element that lays the items out — never from the\n * dragged item, which may carry its own `dir`. The list inherits the computed\n * direction, so authoring `dir` on the root works too. A vertical list is\n * unaffected: writing direction does not mirror the block axis.\n */\n get #isReversed(): boolean {\n return !this.#isVertical && isRtl(this.#list);\n }\n}\n"]}
@@ -69,7 +69,7 @@ var SpinnerController = class extends Controller {
69
69
  #delayTimerId = null;
70
70
  /** Pending min-duration hide timer id, or `null` when none is scheduled. */
71
71
  #hideTimerId = null;
72
- /** Epoch ms when the spinner became visible, used to enforce `minDuration`. */
72
+ /** Epoch ms when the spinner became visible; `minDuration` is measured from it. */
73
73
  #shownAt = 0;
74
74
  connect() {
75
75
  if (!this.element.hasAttribute("data-state")) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/safe_timeout.ts","../../src/controllers/spinner_controller.ts"],"names":[],"mappings":"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","file":"spinner_controller.js","sourcesContent":["/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/**\n * Headless loading-indicator behavior built on the live-region + `aria-busy`\n * practice (no dedicated APG pattern).\n *\n * Markup contract (identifier: `stimeo--spinner`):\n * <div data-controller=\"stimeo--spinner\"\n * data-stimeo--spinner-delay-value=\"150\"\n * data-stimeo--spinner-min-duration-value=\"500\"\n * data-action=\"loading:start->stimeo--spinner#start\n * loading:stop->stimeo--spinner#stop\">\n * <div role=\"status\" aria-live=\"polite\" hidden\n * data-stimeo--spinner-target=\"indicator\">\n * <span data-stimeo--spinner-target=\"message\">Loading…</span>\n * </div>\n * <div aria-busy=\"false\" data-stimeo--spinner-target=\"region\"></div>\n * </div>\n *\n * The indicator is a `role=\"status\"` live region carrying *text* (never an icon\n * alone) so screen readers announce loading; the controlled `region` mirrors the\n * busy state via `aria-busy`. Two timers tame flicker: `delay` suppresses the\n * spinner for fast operations, and `minDuration` keeps it visible long enough to\n * be perceived once shown.\n *\n * @remarks\n * Behavior only — the visual spinner is the consumer's, alongside the text and\n * `aria-hidden=\"true\"`. Both timers are owned by {@link SafeTimeout} and torn\n * down on `disconnect()` (Turbo navigation included).\n */\nexport class SpinnerController extends Controller<HTMLElement> {\n static override targets = [\"indicator\", \"region\", \"message\"];\n static override values = {\n delay: { type: Number, default: 0 },\n minDuration: { type: Number, default: 0 },\n };\n static actions = [\"start\", \"stop\"] as const;\n static events = [\"hide\", \"show\"] as const;\n\n declare readonly indicatorTarget: HTMLElement;\n declare readonly regionTarget: HTMLElement;\n declare readonly messageTarget: HTMLElement;\n declare readonly hasIndicatorTarget: boolean;\n declare readonly hasRegionTarget: boolean;\n declare readonly hasMessageTarget: boolean;\n\n declare delayValue: number;\n declare minDurationValue: number;\n\n readonly #timers = new SafeTimeout();\n\n /** Pending show-delay timer id, or `null` when no start is awaiting its delay. */\n #delayTimerId: number | null = null;\n /** Pending min-duration hide timer id, or `null` when none is scheduled. */\n #hideTimerId: number | null = null;\n /** Epoch ms when the spinner became visible, used to enforce `minDuration`. */\n #shownAt = 0;\n\n override connect(): void {\n if (!this.element.hasAttribute(\"data-state\")) {\n this.element.setAttribute(\"data-state\", \"idle\");\n }\n }\n\n override disconnect(): void {\n this.#timers.clearAll();\n this.#delayTimerId = null;\n this.#hideTimerId = null;\n }\n\n /** Begins loading. Honors `delay` before the spinner actually appears. */\n start(): void {\n if (this.#state === \"loading\") {\n // Already shown (possibly waiting out `minDuration` after a stop): a quick\n // stop→start within that window must keep the spinner visible. Restore the\n // busy state and cancel the pending hide instead of returning a no-op, which\n // would let the stale hide fire and flicker the spinner away mid-load.\n this.#setBusy(true);\n this.#cancelHide();\n return;\n }\n if (this.#state !== \"idle\") return;\n this.#setBusy(true);\n // A pending hide from a previous cycle is now stale.\n this.#cancelHide();\n if (this.delayValue > 0) {\n this.element.setAttribute(\"data-state\", \"pending\");\n this.#delayTimerId = this.#timers.set(() => {\n this.#delayTimerId = null;\n this.#show();\n }, this.delayValue);\n } else {\n this.#show();\n }\n }\n\n /** Ends loading. Honors `minDuration` so a shown spinner does not flicker. */\n stop(): void {\n const state = this.#state;\n if (state === \"pending\") {\n // The delay never elapsed — the spinner never appeared, so just cancel.\n this.#cancelDelay();\n this.#setBusy(false);\n this.element.setAttribute(\"data-state\", \"idle\");\n return;\n }\n if (state !== \"loading\") return;\n\n this.#setBusy(false);\n const remaining = this.minDurationValue - (Date.now() - this.#shownAt);\n if (remaining > 0) {\n this.#hideTimerId = this.#timers.set(() => {\n this.#hideTimerId = null;\n this.#hide();\n }, remaining);\n } else {\n this.#hide();\n }\n }\n\n /** Reveals the indicator, marks the moment shown, and announces via the live region. */\n #show(): void {\n this.#shownAt = Date.now();\n if (this.hasIndicatorTarget) this.indicatorTarget.hidden = false;\n this.element.setAttribute(\"data-state\", \"loading\");\n this.dispatch(\"show\", { detail: {} });\n }\n\n /** Hides the indicator and returns to the idle state. */\n #hide(): void {\n if (this.hasIndicatorTarget) this.indicatorTarget.hidden = true;\n this.element.setAttribute(\"data-state\", \"idle\");\n this.dispatch(\"hide\", { detail: {} });\n }\n\n /** Reflects busy state onto the controlled region (if present). */\n #setBusy(busy: boolean): void {\n if (this.hasRegionTarget) {\n this.regionTarget.setAttribute(\"aria-busy\", String(busy));\n }\n }\n\n #cancelDelay(): void {\n if (this.#delayTimerId !== null) {\n this.#timers.clear(this.#delayTimerId);\n this.#delayTimerId = null;\n }\n }\n\n #cancelHide(): void {\n if (this.#hideTimerId !== null) {\n this.#timers.clear(this.#hideTimerId);\n this.#hideTimerId = null;\n }\n }\n\n /** Current lifecycle phase as reflected on `data-state`. */\n get #state(): string {\n return this.element.getAttribute(\"data-state\") ?? \"idle\";\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/safe_timeout.ts","../../src/controllers/spinner_controller.ts"],"names":[],"mappings":"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","file":"spinner_controller.js","sourcesContent":["/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/**\n * Headless loading-indicator behavior built on the live-region + `aria-busy`\n * practice (no dedicated APG pattern).\n *\n * Markup contract (identifier: `stimeo--spinner`):\n * <div data-controller=\"stimeo--spinner\"\n * data-stimeo--spinner-delay-value=\"150\"\n * data-stimeo--spinner-min-duration-value=\"500\"\n * data-action=\"loading:start->stimeo--spinner#start\n * loading:stop->stimeo--spinner#stop\">\n * <div role=\"status\" aria-live=\"polite\" hidden\n * data-stimeo--spinner-target=\"indicator\">\n * <span data-stimeo--spinner-target=\"message\">Loading…</span>\n * </div>\n * <div aria-busy=\"false\" data-stimeo--spinner-target=\"region\"></div>\n * </div>\n *\n * The indicator is a `role=\"status\"` live region carrying *text* (never an icon\n * alone) so screen readers announce loading; the controlled `region` mirrors the\n * busy state via `aria-busy`. Two timers tame flicker: `delay` suppresses the\n * spinner for fast operations, and `minDuration` keeps it visible long enough to\n * be perceived once shown.\n *\n * @remarks\n * Behavior only — the visual spinner is the consumer's, alongside the text and\n * `aria-hidden=\"true\"`. Both timers are owned by {@link SafeTimeout} and torn\n * down on `disconnect()` (Turbo navigation included).\n */\nexport class SpinnerController extends Controller<HTMLElement> {\n static override targets = [\"indicator\", \"region\", \"message\"];\n static override values = {\n delay: { type: Number, default: 0 },\n minDuration: { type: Number, default: 0 },\n };\n static actions = [\"start\", \"stop\"] as const;\n static events = [\"hide\", \"show\"] as const;\n\n declare readonly indicatorTarget: HTMLElement;\n declare readonly regionTarget: HTMLElement;\n declare readonly messageTarget: HTMLElement;\n declare readonly hasIndicatorTarget: boolean;\n declare readonly hasRegionTarget: boolean;\n declare readonly hasMessageTarget: boolean;\n\n declare delayValue: number;\n declare minDurationValue: number;\n\n readonly #timers = new SafeTimeout();\n\n /** Pending show-delay timer id, or `null` when no start is awaiting its delay. */\n #delayTimerId: number | null = null;\n /** Pending min-duration hide timer id, or `null` when none is scheduled. */\n #hideTimerId: number | null = null;\n /** Epoch ms when the spinner became visible; `minDuration` is measured from it. */\n #shownAt = 0;\n\n override connect(): void {\n if (!this.element.hasAttribute(\"data-state\")) {\n this.element.setAttribute(\"data-state\", \"idle\");\n }\n }\n\n override disconnect(): void {\n this.#timers.clearAll();\n this.#delayTimerId = null;\n this.#hideTimerId = null;\n }\n\n /** Begins loading. Honors `delay` before the spinner actually appears. */\n start(): void {\n if (this.#state === \"loading\") {\n // Already shown (possibly waiting out `minDuration` after a stop): a quick\n // stop→start within that window must keep the spinner visible. Restore the\n // busy state and cancel the pending hide instead of returning a no-op, which\n // would let the stale hide fire and flicker the spinner away mid-load.\n this.#setBusy(true);\n this.#cancelHide();\n return;\n }\n if (this.#state !== \"idle\") return;\n this.#setBusy(true);\n // A pending hide from a previous cycle is now stale.\n this.#cancelHide();\n if (this.delayValue > 0) {\n this.element.setAttribute(\"data-state\", \"pending\");\n this.#delayTimerId = this.#timers.set(() => {\n this.#delayTimerId = null;\n this.#show();\n }, this.delayValue);\n } else {\n this.#show();\n }\n }\n\n /** Ends loading. Honors `minDuration` so a shown spinner does not flicker. */\n stop(): void {\n const state = this.#state;\n if (state === \"pending\") {\n // The delay never elapsed — the spinner never appeared, so just cancel.\n this.#cancelDelay();\n this.#setBusy(false);\n this.element.setAttribute(\"data-state\", \"idle\");\n return;\n }\n if (state !== \"loading\") return;\n\n this.#setBusy(false);\n const remaining = this.minDurationValue - (Date.now() - this.#shownAt);\n if (remaining > 0) {\n this.#hideTimerId = this.#timers.set(() => {\n this.#hideTimerId = null;\n this.#hide();\n }, remaining);\n } else {\n this.#hide();\n }\n }\n\n /** Reveals the indicator, marks the moment shown, and announces via the live region. */\n #show(): void {\n this.#shownAt = Date.now();\n if (this.hasIndicatorTarget) this.indicatorTarget.hidden = false;\n this.element.setAttribute(\"data-state\", \"loading\");\n this.dispatch(\"show\", { detail: {} });\n }\n\n /** Hides the indicator and returns to the idle state. */\n #hide(): void {\n if (this.hasIndicatorTarget) this.indicatorTarget.hidden = true;\n this.element.setAttribute(\"data-state\", \"idle\");\n this.dispatch(\"hide\", { detail: {} });\n }\n\n /** Reflects busy state onto the controlled region (if present). */\n #setBusy(busy: boolean): void {\n if (this.hasRegionTarget) {\n this.regionTarget.setAttribute(\"aria-busy\", String(busy));\n }\n }\n\n #cancelDelay(): void {\n if (this.#delayTimerId !== null) {\n this.#timers.clear(this.#delayTimerId);\n this.#delayTimerId = null;\n }\n }\n\n #cancelHide(): void {\n if (this.#hideTimerId !== null) {\n this.#timers.clear(this.#hideTimerId);\n this.#hideTimerId = null;\n }\n }\n\n /** Current lifecycle phase as reflected on `data-state`. */\n get #state(): string {\n return this.element.getAttribute(\"data-state\") ?? \"idle\";\n }\n}\n"]}
@@ -28,6 +28,7 @@ import { Controller } from '@hotwired/stimulus';
28
28
  * - `goto` jumps to the step in its `index` action param.
29
29
  * - With `linear=true`, `goto` may not skip more than one step ahead of the
30
30
  * current one (moving backward is always allowed).
31
+ * - Runtime changes to the `index` Value re-derive every step's state.
31
32
  * - Each move re-derives `data-state`/`aria-current` and dispatches
32
33
  * `stimeo--stepper:change`.
33
34
  */
@@ -51,6 +52,9 @@ declare class StepperController extends Controller<HTMLElement> {
51
52
  linearValue: boolean;
52
53
  /** Normalizes an out-of-range initial `index` and renders the initial state. */
53
54
  connect(): void;
55
+ disconnect(): void;
56
+ /** Re-renders when Turbo Morph or application code changes `index` at runtime. */
57
+ indexValueChanged(): void;
54
58
  /** Advances to the next step (ignored at the last step). */
55
59
  next(): void;
56
60
  /** Returns to the previous step (ignored at the first step). */
@@ -9,23 +9,32 @@ var StepperController = class extends Controller {
9
9
  };
10
10
  static actions = ["goto", "next", "prev"];
11
11
  static events = ["change"];
12
+ #isConnected = false;
12
13
  /** Normalizes an out-of-range initial `index` and renders the initial state. */
13
14
  connect() {
14
- this.indexValue = this.#clampIndex(this.indexValue);
15
- this.#render();
15
+ this.#isConnected = true;
16
+ this.#normalizeAndRender();
17
+ }
18
+ disconnect() {
19
+ this.#isConnected = false;
20
+ }
21
+ /** Re-renders when Turbo Morph or application code changes `index` at runtime. */
22
+ indexValueChanged() {
23
+ if (!this.#isConnected) return;
24
+ this.#normalizeAndRender();
16
25
  }
17
26
  /** Advances to the next step (ignored at the last step). */
18
27
  next() {
19
- this.#moveTo(this.indexValue + 1);
28
+ this.#moveTo(this.#clampIndex(this.indexValue) + 1);
20
29
  }
21
30
  /** Returns to the previous step (ignored at the first step). */
22
31
  prev() {
23
- this.#moveTo(this.indexValue - 1);
32
+ this.#moveTo(this.#clampIndex(this.indexValue) - 1);
24
33
  }
25
34
  /** Jumps to the step carried in the action's `index` param. */
26
35
  goto(event) {
27
36
  const target = Number(event.params.index);
28
- if (!Number.isFinite(target)) return;
37
+ if (!Number.isFinite(target) || !Number.isInteger(target)) return;
29
38
  this.#moveTo(target);
30
39
  }
31
40
  /**
@@ -35,16 +44,24 @@ var StepperController = class extends Controller {
35
44
  */
36
45
  #moveTo(target) {
37
46
  const total = this.stepTargets.length;
47
+ if (!Number.isFinite(target) || !Number.isInteger(target)) return;
38
48
  if (target < 0 || target >= total) return;
39
- if (target === this.indexValue) return;
40
- if (this.linearValue && target > this.indexValue + 1) return;
41
- const previous = this.indexValue;
49
+ const current = this.#clampIndex(this.indexValue);
50
+ if (target === current) return;
51
+ if (this.linearValue && target > current + 1) return;
52
+ const previous = current;
42
53
  this.indexValue = target;
43
- this.#render();
54
+ this.#render(target);
44
55
  this.dispatch("change", {
45
56
  detail: { index: target, previous, step: this.stepTargets[target] }
46
57
  });
47
58
  }
59
+ /** Normalizes the public Value and reflects it without dispatching an action event. */
60
+ #normalizeAndRender() {
61
+ const normalized = this.#clampIndex(this.indexValue);
62
+ if (!Object.is(normalized, this.indexValue)) this.indexValue = normalized;
63
+ this.#render(normalized);
64
+ }
48
65
  /**
49
66
  * Derives each step's `data-state` and the current button's `aria-current`.
50
67
  *
@@ -52,8 +69,7 @@ var StepperController = class extends Controller {
52
69
  * contract assumes one operable button per step. If a step needs multiple
53
70
  * buttons, mark the navigational one first (or this would target the wrong one).
54
71
  */
55
- #render() {
56
- const current = this.indexValue;
72
+ #render(current) {
57
73
  this.stepTargets.forEach((step, index) => {
58
74
  step.dataset.state = index < current ? "complete" : index === current ? "current" : "upcoming";
59
75
  const button = step.querySelector("button");
@@ -68,7 +84,7 @@ var StepperController = class extends Controller {
68
84
  /** Constrains an index to `[0, total-1]` (or `0` when there are no steps). */
69
85
  #clampIndex(index) {
70
86
  const last = this.stepTargets.length - 1;
71
- if (last < 0) return 0;
87
+ if (last < 0 || !Number.isFinite(index)) return 0;
72
88
  return Math.min(last, Math.max(0, Math.trunc(index)));
73
89
  }
74
90
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/controllers/stepper_controller.ts"],"names":[],"mappings":"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","file":"stepper_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\n/**\n * Headless, accessible stepper / wizard navigation behavior.\n *\n * Markup contract (identifier: `stimeo--stepper`):\n * <ol data-controller=\"stimeo--stepper\" data-stimeo--stepper-index-value=\"0\">\n * <li data-stimeo--stepper-target=\"step\">\n * <button aria-current=\"step\" data-stimeo--stepper-index-param=\"0\"\n * data-action=\"click->stimeo--stepper#goto\">Account</button>\n * </li>\n * <!-- more steps -->\n * </ol>\n *\n * There is no dedicated APG widget; the current step is expressed with\n * `aria-current=\"step\"` on the operable `<button>`. Each step `<li>` also gets a\n * `data-state` (`complete`/`current`/`upcoming`) derived from the current index.\n * For a read-only progress display use\n * {@link StepIndicatorController | Step Indicator}; for panel switching use Tabs.\n *\n * @remarks\n * Behavior only. The controller never traps or restores focus — each step button\n * is in the natural Tab order. `data-state` is purely derived from `index`\n * (completion is not persisted).\n *\n * Behavior provided:\n * - `next`/`prev` move one step, ignoring moves past either end.\n * - `goto` jumps to the step in its `index` action param.\n * - With `linear=true`, `goto` may not skip more than one step ahead of the\n * current one (moving backward is always allowed).\n * - Each move re-derives `data-state`/`aria-current` and dispatches\n * `stimeo--stepper:change`.\n */\nexport class StepperController extends Controller<HTMLElement> {\n static override targets = [\"step\"];\n static override values = {\n index: { type: Number, default: 0 },\n linear: { type: Boolean, default: false },\n };\n static actions = [\"goto\", \"next\", \"prev\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly stepTargets: HTMLElement[];\n declare indexValue: number;\n declare linearValue: boolean;\n\n /** Normalizes an out-of-range initial `index` and renders the initial state. */\n override connect(): void {\n this.indexValue = this.#clampIndex(this.indexValue);\n this.#render();\n }\n\n /** Advances to the next step (ignored at the last step). */\n next(): void {\n this.#moveTo(this.indexValue + 1);\n }\n\n /** Returns to the previous step (ignored at the first step). */\n prev(): void {\n this.#moveTo(this.indexValue - 1);\n }\n\n /** Jumps to the step carried in the action's `index` param. */\n goto(event: { params: { index?: number } }): void {\n const target = Number(event.params.index);\n if (!Number.isFinite(target)) return;\n this.#moveTo(target);\n }\n\n /**\n * Moves the current step to `target` when allowed: in range, not a no-op, and\n * — under `linear` — not skipping more than one step ahead. Re-renders and\n * dispatches `change`.\n */\n #moveTo(target: number): void {\n const total = this.stepTargets.length;\n if (target < 0 || target >= total) return;\n if (target === this.indexValue) return;\n if (this.linearValue && target > this.indexValue + 1) return;\n\n const previous = this.indexValue;\n this.indexValue = target;\n this.#render();\n this.dispatch(\"change\", {\n detail: { index: target, previous, step: this.stepTargets[target] },\n });\n }\n\n /**\n * Derives each step's `data-state` and the current button's `aria-current`.\n *\n * `aria-current=\"step\"` is placed on the step's **first** `<button>`; the markup\n * contract assumes one operable button per step. If a step needs multiple\n * buttons, mark the navigational one first (or this would target the wrong one).\n */\n #render(): void {\n const current = this.indexValue;\n this.stepTargets.forEach((step, index) => {\n step.dataset.state =\n index < current ? \"complete\" : index === current ? \"current\" : \"upcoming\";\n const button = step.querySelector<HTMLElement>(\"button\");\n if (!button) return;\n if (index === current) {\n button.setAttribute(\"aria-current\", \"step\");\n } else {\n button.removeAttribute(\"aria-current\");\n }\n });\n }\n\n /** Constrains an index to `[0, total-1]` (or `0` when there are no steps). */\n #clampIndex(index: number): number {\n const last = this.stepTargets.length - 1;\n if (last < 0) return 0;\n return Math.min(last, Math.max(0, Math.trunc(index)));\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/controllers/stepper_controller.ts"],"names":[],"mappings":"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","file":"stepper_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\n/**\n * Headless, accessible stepper / wizard navigation behavior.\n *\n * Markup contract (identifier: `stimeo--stepper`):\n * <ol data-controller=\"stimeo--stepper\" data-stimeo--stepper-index-value=\"0\">\n * <li data-stimeo--stepper-target=\"step\">\n * <button aria-current=\"step\" data-stimeo--stepper-index-param=\"0\"\n * data-action=\"click->stimeo--stepper#goto\">Account</button>\n * </li>\n * <!-- more steps -->\n * </ol>\n *\n * There is no dedicated APG widget; the current step is expressed with\n * `aria-current=\"step\"` on the operable `<button>`. Each step `<li>` also gets a\n * `data-state` (`complete`/`current`/`upcoming`) derived from the current index.\n * For a read-only progress display use\n * {@link StepIndicatorController | Step Indicator}; for panel switching use Tabs.\n *\n * @remarks\n * Behavior only. The controller never traps or restores focus — each step button\n * is in the natural Tab order. `data-state` is purely derived from `index`\n * (completion is not persisted).\n *\n * Behavior provided:\n * - `next`/`prev` move one step, ignoring moves past either end.\n * - `goto` jumps to the step in its `index` action param.\n * - With `linear=true`, `goto` may not skip more than one step ahead of the\n * current one (moving backward is always allowed).\n * - Runtime changes to the `index` Value re-derive every step's state.\n * - Each move re-derives `data-state`/`aria-current` and dispatches\n * `stimeo--stepper:change`.\n */\nexport class StepperController extends Controller<HTMLElement> {\n static override targets = [\"step\"];\n static override values = {\n index: { type: Number, default: 0 },\n linear: { type: Boolean, default: false },\n };\n static actions = [\"goto\", \"next\", \"prev\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly stepTargets: HTMLElement[];\n declare indexValue: number;\n declare linearValue: boolean;\n\n #isConnected = false;\n\n /** Normalizes an out-of-range initial `index` and renders the initial state. */\n override connect(): void {\n this.#isConnected = true;\n this.#normalizeAndRender();\n }\n\n override disconnect(): void {\n this.#isConnected = false;\n }\n\n /** Re-renders when Turbo Morph or application code changes `index` at runtime. */\n indexValueChanged(): void {\n if (!this.#isConnected) return;\n this.#normalizeAndRender();\n }\n\n /** Advances to the next step (ignored at the last step). */\n next(): void {\n this.#moveTo(this.#clampIndex(this.indexValue) + 1);\n }\n\n /** Returns to the previous step (ignored at the first step). */\n prev(): void {\n this.#moveTo(this.#clampIndex(this.indexValue) - 1);\n }\n\n /** Jumps to the step carried in the action's `index` param. */\n goto(event: { params: { index?: number } }): void {\n const target = Number(event.params.index);\n if (!Number.isFinite(target) || !Number.isInteger(target)) return;\n this.#moveTo(target);\n }\n\n /**\n * Moves the current step to `target` when allowed: in range, not a no-op, and\n * — under `linear` — not skipping more than one step ahead. Re-renders and\n * dispatches `change`.\n */\n #moveTo(target: number): void {\n const total = this.stepTargets.length;\n if (!Number.isFinite(target) || !Number.isInteger(target)) return;\n if (target < 0 || target >= total) return;\n const current = this.#clampIndex(this.indexValue);\n if (target === current) return;\n if (this.linearValue && target > current + 1) return;\n\n const previous = current;\n this.indexValue = target;\n this.#render(target);\n this.dispatch(\"change\", {\n detail: { index: target, previous, step: this.stepTargets[target] },\n });\n }\n\n /** Normalizes the public Value and reflects it without dispatching an action event. */\n #normalizeAndRender(): void {\n const normalized = this.#clampIndex(this.indexValue);\n if (!Object.is(normalized, this.indexValue)) this.indexValue = normalized;\n this.#render(normalized);\n }\n\n /**\n * Derives each step's `data-state` and the current button's `aria-current`.\n *\n * `aria-current=\"step\"` is placed on the step's **first** `<button>`; the markup\n * contract assumes one operable button per step. If a step needs multiple\n * buttons, mark the navigational one first (or this would target the wrong one).\n */\n #render(current: number): void {\n this.stepTargets.forEach((step, index) => {\n step.dataset.state =\n index < current ? \"complete\" : index === current ? \"current\" : \"upcoming\";\n const button = step.querySelector<HTMLElement>(\"button\");\n if (!button) return;\n if (index === current) {\n button.setAttribute(\"aria-current\", \"step\");\n } else {\n button.removeAttribute(\"aria-current\");\n }\n });\n }\n\n /** Constrains an index to `[0, total-1]` (or `0` when there are no steps). */\n #clampIndex(index: number): number {\n const last = this.stepTargets.length - 1;\n if (last < 0 || !Number.isFinite(index)) return 0;\n return Math.min(last, Math.max(0, Math.trunc(index)));\n }\n}\n"]}
@@ -26,12 +26,11 @@ import { Controller } from '@hotwired/stimulus';
26
26
  * `scrollToBottom` action after such inserts.
27
27
  *
28
28
  * Behavior only — it does not add content (Turbo Stream / the consumer does) and is the
29
- * minimal follow primitive, not a full chat UI (no virtualization / message input). It is
30
- * the lightweight member of the scroll family. State is derived from the scroll position
31
- * each pass (no module-scope state), so `connect()` re-syncs after a Turbo Stream insert;
32
- * `behavior` falls back to `auto` under reduced motion; auto-scroll never moves focus; the
33
- * observer and the passive scroll listener are released on `disconnect()` (Turbo
34
- * navigation included).
29
+ * minimal follow primitive, not a full chat UI (no virtualization / message input). State
30
+ * is derived from the scroll position each pass (no module-scope state), so `connect()`
31
+ * re-syncs after a Turbo Stream insert; reduced motion forces an instant jump
32
+ * independently of consumer CSS; auto-scroll never moves focus; the observer and the
33
+ * passive scroll listener are released on `disconnect()` (Turbo navigation included).
35
34
  */
36
35
  declare class StickToBottomController extends Controller<HTMLElement> {
37
36
  #private;
@@ -1,5 +1,12 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
+ // src/controllers/stick_to_bottom_controller.ts
4
+
5
+ // src/utils/reduced_motion.ts
6
+ function prefersReducedMotion() {
7
+ return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
8
+ }
9
+
3
10
  // src/controllers/stick_to_bottom_controller.ts
4
11
  var countElements = (nodes) => {
5
12
  let n = 0;
@@ -86,13 +93,11 @@ var StickToBottomController = class extends Controller {
86
93
  #watched() {
87
94
  return this.hasContentTarget ? this.contentTarget : this.element;
88
95
  }
96
+ /** Forces reduced-motion jumps while preserving the configured normal behavior. */
89
97
  #behavior() {
90
- if (this.#prefersReducedMotion()) return "auto";
98
+ if (prefersReducedMotion()) return "instant";
91
99
  return this.behaviorValue === "smooth" ? "smooth" : "auto";
92
100
  }
93
- #prefersReducedMotion() {
94
- return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
95
- }
96
101
  };
97
102
 
98
103
  export { StickToBottomController };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/controllers/stick_to_bottom_controller.ts"],"names":[],"mappings":"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","file":"stick_to_bottom_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\n/** Counts the element nodes in an added-node list (text nodes are ignored). */\nconst countElements = (nodes: NodeList): number => {\n let n = 0;\n for (const node of nodes) if (node.nodeType === Node.ELEMENT_NODE) n += 1;\n return n;\n};\n\n/**\n * Headless **stick-to-bottom**: auto-follows new content to the bottom of a scroll\n * container (a chat log, a live console) — but only while the user is already near the\n * bottom; if they have scrolled up to read, it holds position and flags that new content\n * arrived instead (no APG pattern; honors WCAG 2.3.3 via `prefers-reduced-motion` and\n * never steals focus).\n *\n * Markup contract (identifier: `stimeo--stick-to-bottom`):\n * <div data-controller=\"stimeo--stick-to-bottom\"\n * data-stimeo--stick-to-bottom-threshold-value=\"80\" style=\"overflow:auto\">\n * <ul data-stimeo--stick-to-bottom-target=\"content\"><!-- Turbo Stream appends --></ul>\n * </div>\n *\n * The container is \"pinned\" while its distance from the bottom is within `threshold`. A\n * `MutationObserver` on `content` (or the element) reacts to appended children: while\n * pinned it scrolls to the bottom; while unpinned it sets `data-has-new` and emits `new`.\n * Scrolling recomputes pinned and reflects `data-pinned`, emitting `pin` on change; the\n * `scrollToBottom` action jumps back down (a \"new messages\" button).\n *\n * @remarks\n * The `MutationObserver` watches `childList` only (not `subtree`), so it follows\n * direct appends to `content` (or the element). Appends made deeper inside a nested\n * wrapper are not detected — keep messages as direct children, or call the public\n * `scrollToBottom` action after such inserts.\n *\n * Behavior only — it does not add content (Turbo Stream / the consumer does) and is the\n * minimal follow primitive, not a full chat UI (no virtualization / message input). It is\n * the lightweight member of the scroll family. State is derived from the scroll position\n * each pass (no module-scope state), so `connect()` re-syncs after a Turbo Stream insert;\n * `behavior` falls back to `auto` under reduced motion; auto-scroll never moves focus; the\n * observer and the passive scroll listener are released on `disconnect()` (Turbo\n * navigation included).\n */\nexport class StickToBottomController extends Controller<HTMLElement> {\n static override targets = [\"content\"];\n static override values = {\n threshold: { type: Number, default: 80 },\n behavior: { type: String, default: \"auto\" },\n };\n static actions = [\"scrollToBottom\"] as const;\n static events = [\"pin\", \"new\"] as const;\n\n declare readonly contentTarget: HTMLElement;\n declare readonly hasContentTarget: boolean;\n\n declare thresholdValue: number;\n declare behaviorValue: string;\n\n #observer: MutationObserver | null = null;\n #pinned = false;\n\n readonly #onScroll = (): void => this.#updatePinned();\n\n override connect(): void {\n this.#pinned = this.#isPinned();\n // Re-sync the hooks from the current geometry — a Turbo cache restore may bring back a\n // stale data-pinned / data-has-new that no longer matches the scroll position.\n this.#reflectPinned();\n\n this.element.addEventListener(\"scroll\", this.#onScroll, { passive: true });\n if (typeof MutationObserver !== \"undefined\") {\n this.#observer = new MutationObserver((mutations) => this.#onMutations(mutations));\n this.#observer.observe(this.#watched(), { childList: true });\n }\n }\n\n override disconnect(): void {\n this.element.removeEventListener(\"scroll\", this.#onScroll);\n this.#observer?.disconnect();\n this.#observer = null;\n }\n\n /** Jumps to the bottom and re-pins (wired to a \"new messages\" button). */\n scrollToBottom(): void {\n this.#scrollToBottom();\n this.element.removeAttribute(\"data-has-new\");\n if (!this.#pinned) {\n this.#pinned = true;\n this.element.setAttribute(\"data-pinned\", \"true\");\n this.dispatch(\"pin\", { detail: { pinned: true } });\n }\n }\n\n /** Follows appended children while pinned; otherwise flags new content. */\n #onMutations(mutations: MutationRecord[]): void {\n let added = 0;\n for (const mutation of mutations) added += countElements(mutation.addedNodes);\n if (added === 0) return;\n\n if (this.#pinned) {\n this.#scrollToBottom();\n } else {\n this.element.setAttribute(\"data-has-new\", \"true\");\n this.dispatch(\"new\", { detail: { count: added } });\n }\n }\n\n /** Recomputes pinned from the scroll position and reflects it on a transition. */\n #updatePinned(): void {\n const pinned = this.#isPinned();\n if (pinned === this.#pinned) return;\n this.#pinned = pinned;\n this.#reflectPinned();\n this.dispatch(\"pin\", { detail: { pinned } });\n }\n\n /** Mirrors the current `#pinned` onto the state hooks (clearing has-new once pinned). */\n #reflectPinned(): void {\n if (this.#pinned) {\n this.element.setAttribute(\"data-pinned\", \"true\");\n this.element.removeAttribute(\"data-has-new\"); // caught up with the bottom\n } else {\n this.element.removeAttribute(\"data-pinned\");\n }\n }\n\n #isPinned(): boolean {\n const el = this.element;\n return el.scrollHeight - el.clientHeight - el.scrollTop <= this.thresholdValue;\n }\n\n #scrollToBottom(): void {\n const top = this.element.scrollHeight;\n if (typeof this.element.scrollTo === \"function\") {\n this.element.scrollTo({ top, behavior: this.#behavior() });\n } else {\n this.element.scrollTop = top;\n }\n }\n\n /** The append-watched element: the `content` target, or the container itself. */\n #watched(): HTMLElement {\n return this.hasContentTarget ? this.contentTarget : this.element;\n }\n\n #behavior(): ScrollBehavior {\n if (this.#prefersReducedMotion()) return \"auto\";\n return this.behaviorValue === \"smooth\" ? \"smooth\" : \"auto\";\n }\n\n #prefersReducedMotion(): boolean {\n return (\n typeof window.matchMedia === \"function\" &&\n window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n );\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/reduced_motion.ts","../../src/controllers/stick_to_bottom_controller.ts"],"names":[],"mappings":"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","file":"stick_to_bottom_controller.js","sourcesContent":["/**\n * Shared `prefers-reduced-motion` lookup for the motion-aware controllers\n * (count-up, highlight, overflow-indicator, scroll-visibility, stick-to-bottom,\n * transition).\n *\n * This one-liner keeps the media query string and the environment guard\n * single-sourced across them. The preference is intentionally re-read on every\n * call — the controllers check it at each animation/scroll start (WCAG 2.2\n * **2.3.3**), so flipping the OS setting takes effect immediately without any\n * listener or cache bookkeeping here.\n */\n\n/**\n * Whether the user currently requests reduced motion.\n *\n * @returns `true` when `(prefers-reduced-motion: reduce)` matches; `false`\n * otherwise, including environments without `window.matchMedia` (treated as\n * \"no preference\").\n */\nexport function prefersReducedMotion(): boolean {\n return (\n typeof window.matchMedia === \"function\" &&\n window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n );\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { prefersReducedMotion } from \"../utils/reduced_motion\";\n\n/** Counts the element nodes in an added-node list (text nodes are ignored). */\nconst countElements = (nodes: NodeList): number => {\n let n = 0;\n for (const node of nodes) if (node.nodeType === Node.ELEMENT_NODE) n += 1;\n return n;\n};\n\n/**\n * Headless **stick-to-bottom**: auto-follows new content to the bottom of a scroll\n * container (a chat log, a live console) — but only while the user is already near the\n * bottom; if they have scrolled up to read, it holds position and flags that new content\n * arrived instead (no APG pattern; honors WCAG 2.3.3 via `prefers-reduced-motion` and\n * never steals focus).\n *\n * Markup contract (identifier: `stimeo--stick-to-bottom`):\n * <div data-controller=\"stimeo--stick-to-bottom\"\n * data-stimeo--stick-to-bottom-threshold-value=\"80\" style=\"overflow:auto\">\n * <ul data-stimeo--stick-to-bottom-target=\"content\"><!-- Turbo Stream appends --></ul>\n * </div>\n *\n * The container is \"pinned\" while its distance from the bottom is within `threshold`. A\n * `MutationObserver` on `content` (or the element) reacts to appended children: while\n * pinned it scrolls to the bottom; while unpinned it sets `data-has-new` and emits `new`.\n * Scrolling recomputes pinned and reflects `data-pinned`, emitting `pin` on change; the\n * `scrollToBottom` action jumps back down (a \"new messages\" button).\n *\n * @remarks\n * The `MutationObserver` watches `childList` only (not `subtree`), so it follows\n * direct appends to `content` (or the element). Appends made deeper inside a nested\n * wrapper are not detected — keep messages as direct children, or call the public\n * `scrollToBottom` action after such inserts.\n *\n * Behavior only — it does not add content (Turbo Stream / the consumer does) and is the\n * minimal follow primitive, not a full chat UI (no virtualization / message input). State\n * is derived from the scroll position each pass (no module-scope state), so `connect()`\n * re-syncs after a Turbo Stream insert; reduced motion forces an instant jump\n * independently of consumer CSS; auto-scroll never moves focus; the observer and the\n * passive scroll listener are released on `disconnect()` (Turbo navigation included).\n */\nexport class StickToBottomController extends Controller<HTMLElement> {\n static override targets = [\"content\"];\n static override values = {\n threshold: { type: Number, default: 80 },\n behavior: { type: String, default: \"auto\" },\n };\n static actions = [\"scrollToBottom\"] as const;\n static events = [\"pin\", \"new\"] as const;\n\n declare readonly contentTarget: HTMLElement;\n declare readonly hasContentTarget: boolean;\n\n declare thresholdValue: number;\n declare behaviorValue: string;\n\n #observer: MutationObserver | null = null;\n #pinned = false;\n\n readonly #onScroll = (): void => this.#updatePinned();\n\n override connect(): void {\n this.#pinned = this.#isPinned();\n // Re-sync the hooks from the current geometry — a Turbo cache restore may bring back a\n // stale data-pinned / data-has-new that disagrees with the current scroll position.\n this.#reflectPinned();\n\n this.element.addEventListener(\"scroll\", this.#onScroll, { passive: true });\n if (typeof MutationObserver !== \"undefined\") {\n this.#observer = new MutationObserver((mutations) => this.#onMutations(mutations));\n this.#observer.observe(this.#watched(), { childList: true });\n }\n }\n\n override disconnect(): void {\n this.element.removeEventListener(\"scroll\", this.#onScroll);\n this.#observer?.disconnect();\n this.#observer = null;\n }\n\n /** Jumps to the bottom and re-pins (wired to a \"new messages\" button). */\n scrollToBottom(): void {\n this.#scrollToBottom();\n this.element.removeAttribute(\"data-has-new\");\n if (!this.#pinned) {\n this.#pinned = true;\n this.element.setAttribute(\"data-pinned\", \"true\");\n this.dispatch(\"pin\", { detail: { pinned: true } });\n }\n }\n\n /** Follows appended children while pinned; otherwise flags new content. */\n #onMutations(mutations: MutationRecord[]): void {\n let added = 0;\n for (const mutation of mutations) added += countElements(mutation.addedNodes);\n if (added === 0) return;\n\n if (this.#pinned) {\n this.#scrollToBottom();\n } else {\n this.element.setAttribute(\"data-has-new\", \"true\");\n this.dispatch(\"new\", { detail: { count: added } });\n }\n }\n\n /** Recomputes pinned from the scroll position and reflects it on a transition. */\n #updatePinned(): void {\n const pinned = this.#isPinned();\n if (pinned === this.#pinned) return;\n this.#pinned = pinned;\n this.#reflectPinned();\n this.dispatch(\"pin\", { detail: { pinned } });\n }\n\n /** Mirrors the current `#pinned` onto the state hooks (clearing has-new once pinned). */\n #reflectPinned(): void {\n if (this.#pinned) {\n this.element.setAttribute(\"data-pinned\", \"true\");\n this.element.removeAttribute(\"data-has-new\"); // caught up with the bottom\n } else {\n this.element.removeAttribute(\"data-pinned\");\n }\n }\n\n #isPinned(): boolean {\n const el = this.element;\n return el.scrollHeight - el.clientHeight - el.scrollTop <= this.thresholdValue;\n }\n\n #scrollToBottom(): void {\n const top = this.element.scrollHeight;\n if (typeof this.element.scrollTo === \"function\") {\n this.element.scrollTo({ top, behavior: this.#behavior() });\n } else {\n this.element.scrollTop = top;\n }\n }\n\n /** The append-watched element: the `content` target, or the container itself. */\n #watched(): HTMLElement {\n return this.hasContentTarget ? this.contentTarget : this.element;\n }\n\n /** Forces reduced-motion jumps while preserving the configured normal behavior. */\n #behavior(): ScrollBehavior {\n if (prefersReducedMotion()) return \"instant\";\n return this.behaviorValue === \"smooth\" ? \"smooth\" : \"auto\";\n }\n}\n"]}
@@ -18,14 +18,17 @@ import { Controller } from '@hotwired/stimulus';
18
18
  *
19
19
  * When the sentinel scrolls out past the top of the viewport (or `rootSelector`
20
20
  * container), the sticky element is considered stuck and `data-stuck="true"` is
21
- * set; otherwise `false`. The `change` event fires on each transition.
21
+ * set; otherwise `false`. The observer's initial snapshot dispatches `change`
22
+ * once with the current state, including after a Turbo reconnect; subsequent
23
+ * notifications dispatch only when that state changes.
22
24
  *
23
25
  * @remarks
24
26
  * Behavior only — `position: sticky`, shadows, and shrink effects are the
25
27
  * consumer's CSS (`[data-stuck="true"] { … }`). `data-stuck` is a visual hook
26
- * only: it carries no ARIA role/state. `offset` feeds a negative top `rootMargin`
27
- * and must match the sticky element's CSS `top`. The observer is disconnected on
28
- * `disconnect()` (Turbo navigation included).
28
+ * only: it carries no ARIA role/state. `offset` is negated numerically for the
29
+ * top `rootMargin` (so negative offsets remain valid) and must match the sticky
30
+ * element's CSS `top`. The observer follows dynamic sentinel targets and value
31
+ * changes, and is disconnected on `disconnect()` (Turbo navigation included).
29
32
  */
30
33
  declare class StickyObserverController extends Controller<HTMLElement> {
31
34
  #private;
@@ -49,6 +52,16 @@ declare class StickyObserverController extends Controller<HTMLElement> {
49
52
  offsetValue: number;
50
53
  connect(): void;
51
54
  disconnect(): void;
55
+ /** Starts observation when a sentinel is inserted after connection. */
56
+ sentinelTargetConnected(): void;
57
+ /** Stops or transfers observation when the current sentinel is removed. */
58
+ sentinelTargetDisconnected(): void;
59
+ /** Reflects the last snapshot onto an element inserted after that snapshot. */
60
+ elementTargetConnected(element: HTMLElement): void;
61
+ /** Rebuilds the observer when Turbo morphs the configured root. */
62
+ rootSelectorValueChanged(): void;
63
+ /** Rebuilds the observer when Turbo morphs the configured top offset. */
64
+ offsetValueChanged(): void;
52
65
  }
53
66
 
54
67
  export { StickyObserverController };