stimeo-ui 0.6.0 → 0.8.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 (212) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/dist/controllers/alert_dialog_controller.d.ts +2 -0
  3. package/dist/controllers/alert_dialog_controller.js +75 -4
  4. package/dist/controllers/alert_dialog_controller.js.map +1 -1
  5. package/dist/controllers/auto_submit_controller.d.ts +16 -3
  6. package/dist/controllers/auto_submit_controller.js +94 -16
  7. package/dist/controllers/auto_submit_controller.js.map +1 -1
  8. package/dist/controllers/avatar_controller.js +47 -5
  9. package/dist/controllers/avatar_controller.js.map +1 -1
  10. package/dist/controllers/breadcrumb_controller.d.ts +2 -0
  11. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  12. package/dist/controllers/bulk_select_controller.d.ts +2 -0
  13. package/dist/controllers/bulk_select_controller.js.map +1 -1
  14. package/dist/controllers/calendar_controller.d.ts +2 -0
  15. package/dist/controllers/calendar_controller.js.map +1 -1
  16. package/dist/controllers/carousel_controller.d.ts +81 -47
  17. package/dist/controllers/carousel_controller.js +451 -100
  18. package/dist/controllers/carousel_controller.js.map +1 -1
  19. package/dist/controllers/character_counter_controller.d.ts +52 -20
  20. package/dist/controllers/character_counter_controller.js +338 -63
  21. package/dist/controllers/character_counter_controller.js.map +1 -1
  22. package/dist/controllers/checkbox_controller.d.ts +3 -0
  23. package/dist/controllers/checkbox_controller.js.map +1 -1
  24. package/dist/controllers/color_picker_controller.d.ts +3 -0
  25. package/dist/controllers/color_picker_controller.js.map +1 -1
  26. package/dist/controllers/combobox_controller.d.ts +2 -0
  27. package/dist/controllers/combobox_controller.js.map +1 -1
  28. package/dist/controllers/command_palette_controller.d.ts +2 -0
  29. package/dist/controllers/command_palette_controller.js +75 -4
  30. package/dist/controllers/command_palette_controller.js.map +1 -1
  31. package/dist/controllers/conditional_fields_controller.d.ts +42 -14
  32. package/dist/controllers/conditional_fields_controller.js +345 -51
  33. package/dist/controllers/conditional_fields_controller.js.map +1 -1
  34. package/dist/controllers/confirm_controller.d.ts +2 -0
  35. package/dist/controllers/confirm_controller.js +75 -4
  36. package/dist/controllers/confirm_controller.js.map +1 -1
  37. package/dist/controllers/count_up_controller.d.ts +2 -0
  38. package/dist/controllers/count_up_controller.js.map +1 -1
  39. package/dist/controllers/countdown_controller.d.ts +4 -0
  40. package/dist/controllers/countdown_controller.js.map +1 -1
  41. package/dist/controllers/currency_input_controller.d.ts +40 -4
  42. package/dist/controllers/currency_input_controller.js +305 -74
  43. package/dist/controllers/currency_input_controller.js.map +1 -1
  44. package/dist/controllers/data_grid_controller.d.ts +3 -0
  45. package/dist/controllers/data_grid_controller.js.map +1 -1
  46. package/dist/controllers/dialog_controller.js +75 -4
  47. package/dist/controllers/dialog_controller.js.map +1 -1
  48. package/dist/controllers/direct_upload_controller.d.ts +59 -25
  49. package/dist/controllers/direct_upload_controller.js +212 -46
  50. package/dist/controllers/direct_upload_controller.js.map +1 -1
  51. package/dist/controllers/dirty_form_controller.d.ts +14 -6
  52. package/dist/controllers/dirty_form_controller.js +192 -29
  53. package/dist/controllers/dirty_form_controller.js.map +1 -1
  54. package/dist/controllers/dismissible_controller.d.ts +2 -0
  55. package/dist/controllers/dismissible_controller.js +83 -18
  56. package/dist/controllers/dismissible_controller.js.map +1 -1
  57. package/dist/controllers/drawer_controller.js +75 -4
  58. package/dist/controllers/drawer_controller.js.map +1 -1
  59. package/dist/controllers/empty_state_controller.d.ts +2 -0
  60. package/dist/controllers/empty_state_controller.js.map +1 -1
  61. package/dist/controllers/file_dropzone_controller.d.ts +123 -27
  62. package/dist/controllers/file_dropzone_controller.js +386 -63
  63. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  64. package/dist/controllers/filter_controller.d.ts +2 -0
  65. package/dist/controllers/filter_controller.js.map +1 -1
  66. package/dist/controllers/flash_controller.d.ts +7 -1
  67. package/dist/controllers/flash_controller.js +3 -1
  68. package/dist/controllers/flash_controller.js.map +1 -1
  69. package/dist/controllers/focus_controller.d.ts +2 -0
  70. package/dist/controllers/focus_controller.js +75 -4
  71. package/dist/controllers/focus_controller.js.map +1 -1
  72. package/dist/controllers/form_field_controller.d.ts +50 -10
  73. package/dist/controllers/form_field_controller.js +280 -62
  74. package/dist/controllers/form_field_controller.js.map +1 -1
  75. package/dist/controllers/form_validation_controller.d.ts +10 -8
  76. package/dist/controllers/form_validation_controller.js +208 -83
  77. package/dist/controllers/form_validation_controller.js.map +1 -1
  78. package/dist/controllers/frame_loading_controller.d.ts +5 -1
  79. package/dist/controllers/frame_loading_controller.js +2 -1
  80. package/dist/controllers/frame_loading_controller.js.map +1 -1
  81. package/dist/controllers/highlight_controller.d.ts +2 -0
  82. package/dist/controllers/highlight_controller.js.map +1 -1
  83. package/dist/controllers/idle_controller.d.ts +2 -0
  84. package/dist/controllers/idle_controller.js.map +1 -1
  85. package/dist/controllers/input_mask_controller.d.ts +43 -15
  86. package/dist/controllers/input_mask_controller.js +251 -76
  87. package/dist/controllers/input_mask_controller.js.map +1 -1
  88. package/dist/controllers/lazy_frame_controller.d.ts +2 -0
  89. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  90. package/dist/controllers/listbox_controller.d.ts +2 -0
  91. package/dist/controllers/listbox_controller.js.map +1 -1
  92. package/dist/controllers/local_time_controller.d.ts +2 -0
  93. package/dist/controllers/local_time_controller.js.map +1 -1
  94. package/dist/controllers/masonry_controller.d.ts +2 -0
  95. package/dist/controllers/masonry_controller.js.map +1 -1
  96. package/dist/controllers/meter_controller.d.ts +2 -0
  97. package/dist/controllers/meter_controller.js.map +1 -1
  98. package/dist/controllers/multi_select_controller.d.ts +2 -0
  99. package/dist/controllers/multi_select_controller.js.map +1 -1
  100. package/dist/controllers/nested_form_controller.d.ts +52 -13
  101. package/dist/controllers/nested_form_controller.js +450 -42
  102. package/dist/controllers/nested_form_controller.js.map +1 -1
  103. package/dist/controllers/network_status_controller.d.ts +2 -0
  104. package/dist/controllers/network_status_controller.js.map +1 -1
  105. package/dist/controllers/number_input_controller.d.ts +2 -0
  106. package/dist/controllers/number_input_controller.js +47 -5
  107. package/dist/controllers/number_input_controller.js.map +1 -1
  108. package/dist/controllers/otp_controller.d.ts +62 -23
  109. package/dist/controllers/otp_controller.js +472 -114
  110. package/dist/controllers/otp_controller.js.map +1 -1
  111. package/dist/controllers/overflow_indicator_controller.d.ts +2 -0
  112. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  113. package/dist/controllers/overflow_menu_controller.d.ts +2 -0
  114. package/dist/controllers/overflow_menu_controller.js +2 -1
  115. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  116. package/dist/controllers/pagination_controller.d.ts +2 -0
  117. package/dist/controllers/pagination_controller.js +2 -1
  118. package/dist/controllers/pagination_controller.js.map +1 -1
  119. package/dist/controllers/password_reveal_controller.d.ts +2 -0
  120. package/dist/controllers/password_reveal_controller.js.map +1 -1
  121. package/dist/controllers/password_strength_controller.d.ts +2 -0
  122. package/dist/controllers/password_strength_controller.js.map +1 -1
  123. package/dist/controllers/persist_controller.d.ts +30 -16
  124. package/dist/controllers/persist_controller.js +432 -122
  125. package/dist/controllers/persist_controller.js.map +1 -1
  126. package/dist/controllers/pointer_drag_controller.d.ts +3 -0
  127. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  128. package/dist/controllers/popover_controller.js +77 -4
  129. package/dist/controllers/popover_controller.js.map +1 -1
  130. package/dist/controllers/portal_controller.d.ts +2 -0
  131. package/dist/controllers/portal_controller.js.map +1 -1
  132. package/dist/controllers/preview_guard_controller.d.ts +2 -0
  133. package/dist/controllers/preview_guard_controller.js.map +1 -1
  134. package/dist/controllers/progress_controller.d.ts +3 -0
  135. package/dist/controllers/progress_controller.js.map +1 -1
  136. package/dist/controllers/radio_group_controller.d.ts +3 -0
  137. package/dist/controllers/radio_group_controller.js.map +1 -1
  138. package/dist/controllers/rating_controller.d.ts +2 -0
  139. package/dist/controllers/rating_controller.js +9 -5
  140. package/dist/controllers/rating_controller.js.map +1 -1
  141. package/dist/controllers/reading_progress_controller.d.ts +2 -0
  142. package/dist/controllers/reading_progress_controller.js.map +1 -1
  143. package/dist/controllers/resizable_controller.d.ts +2 -0
  144. package/dist/controllers/resizable_controller.js.map +1 -1
  145. package/dist/controllers/roving_controller.d.ts +2 -0
  146. package/dist/controllers/roving_controller.js.map +1 -1
  147. package/dist/controllers/scroll_area_controller.d.ts +23 -15
  148. package/dist/controllers/scroll_area_controller.js +462 -162
  149. package/dist/controllers/scroll_area_controller.js.map +1 -1
  150. package/dist/controllers/scroll_visibility_controller.d.ts +2 -0
  151. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  152. package/dist/controllers/scrollspy_controller.d.ts +2 -0
  153. package/dist/controllers/scrollspy_controller.js.map +1 -1
  154. package/dist/controllers/separator_controller.d.ts +54 -11
  155. package/dist/controllers/separator_controller.js +354 -38
  156. package/dist/controllers/separator_controller.js.map +1 -1
  157. package/dist/controllers/sidebar_controller.js +83 -10
  158. package/dist/controllers/sidebar_controller.js.map +1 -1
  159. package/dist/controllers/skeleton_controller.d.ts +2 -0
  160. package/dist/controllers/skeleton_controller.js.map +1 -1
  161. package/dist/controllers/slider_controller.d.ts +2 -0
  162. package/dist/controllers/slider_controller.js.map +1 -1
  163. package/dist/controllers/smart_sticky_header_controller.d.ts +2 -0
  164. package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
  165. package/dist/controllers/spinner_controller.d.ts +4 -1
  166. package/dist/controllers/spinner_controller.js +7 -5
  167. package/dist/controllers/spinner_controller.js.map +1 -1
  168. package/dist/controllers/step_indicator_controller.d.ts +2 -0
  169. package/dist/controllers/step_indicator_controller.js.map +1 -1
  170. package/dist/controllers/stepper_controller.d.ts +2 -0
  171. package/dist/controllers/stepper_controller.js.map +1 -1
  172. package/dist/controllers/stick_to_bottom_controller.d.ts +2 -0
  173. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  174. package/dist/controllers/sticky_observer_controller.d.ts +2 -0
  175. package/dist/controllers/sticky_observer_controller.js.map +1 -1
  176. package/dist/controllers/submit_once_controller.d.ts +98 -39
  177. package/dist/controllers/submit_once_controller.js +402 -122
  178. package/dist/controllers/submit_once_controller.js.map +1 -1
  179. package/dist/controllers/switch_controller.d.ts +2 -0
  180. package/dist/controllers/switch_controller.js.map +1 -1
  181. package/dist/controllers/tags_input_controller.d.ts +2 -0
  182. package/dist/controllers/tags_input_controller.js.map +1 -1
  183. package/dist/controllers/textarea_autosize_controller.d.ts +31 -9
  184. package/dist/controllers/textarea_autosize_controller.js +131 -3
  185. package/dist/controllers/textarea_autosize_controller.js.map +1 -1
  186. package/dist/controllers/theme_controller.d.ts +2 -0
  187. package/dist/controllers/theme_controller.js +8 -6
  188. package/dist/controllers/theme_controller.js.map +1 -1
  189. package/dist/controllers/time_picker_controller.d.ts +2 -0
  190. package/dist/controllers/time_picker_controller.js.map +1 -1
  191. package/dist/controllers/toast_controller.d.ts +4 -0
  192. package/dist/controllers/toast_controller.js.map +1 -1
  193. package/dist/controllers/toggle_group_controller.d.ts +2 -0
  194. package/dist/controllers/toggle_group_controller.js.map +1 -1
  195. package/dist/controllers/toolbar_controller.js.map +1 -1
  196. package/dist/controllers/transition_controller.d.ts +2 -0
  197. package/dist/controllers/transition_controller.js.map +1 -1
  198. package/dist/controllers/tree_view_controller.d.ts +2 -0
  199. package/dist/controllers/tree_view_controller.js +2 -1
  200. package/dist/controllers/tree_view_controller.js.map +1 -1
  201. package/dist/index.js +4293 -1447
  202. package/dist/index.js.map +1 -1
  203. package/dist/inspector/cli.d.ts +115 -4
  204. package/dist/inspector/cli.js +195 -15
  205. package/dist/inspector/cli.js.map +1 -1
  206. package/dist/inspector/cli_bin.js +257 -47
  207. package/dist/inspector/cli_bin.js.map +1 -1
  208. package/dist/inspector/examples.json +11 -11
  209. package/dist/inspector/manifest.json +615 -66
  210. package/dist/positioning/index.d.ts +2 -0
  211. package/dist/positioning/index.js.map +1 -1
  212. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/safe_timeout.ts","../../src/controllers/password_reveal_controller.ts"],"names":[],"mappings":"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","file":"password_reveal_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 password show/hide (unmask) toggle behavior.\n *\n * Markup contract (identifier: `stimeo--password-reveal`):\n * <div data-controller=\"stimeo--password-reveal\">\n * <input type=\"password\" aria-label=\"Password\"\n * data-stimeo--password-reveal-target=\"input\">\n * <button type=\"button\" aria-pressed=\"false\" aria-label=\"Show password\"\n * data-stimeo--password-reveal-target=\"toggle\"\n * data-action=\"stimeo--password-reveal#toggle\"></button>\n * </div>\n *\n * No dedicated APG pattern; this follows the toggle **Button** practice. The\n * accessible name stays state-independent (\"Show password\") while the pressed\n * state is conveyed by `aria-pressed`.\n *\n * @remarks\n * Behavior only — icon rendering is the consumer's, keyed off `aria-pressed` /\n * `data-state` (`hidden` / `visible`). Flipping `input.type` can drop focus and\n * the caret, so when (and only when) the input was the focused element its focus\n * and selection are restored afterward; when the toggle button holds focus\n * (keyboard use) focus is left on the button. An optional `autoHide` re-masks\n * after a delay, and that timer is torn down on disconnect (Turbo).\n */\nexport class PasswordRevealController extends Controller<HTMLElement> {\n static override targets = [\"input\", \"toggle\"];\n static override values = {\n autoHide: { type: Number, default: 0 },\n };\n static actions = [\"toggle\"] as const;\n static events = [\"toggle\"] as const;\n\n declare readonly inputTarget: HTMLInputElement;\n declare readonly toggleTarget: HTMLElement;\n declare readonly hasInputTarget: boolean;\n declare readonly hasToggleTarget: boolean;\n\n declare autoHideValue: number;\n\n /** Auto re-mask timer; torn down on disconnect. */\n #timers = new SafeTimeout();\n\n override connect(): void {\n this.#reflect(this.#isVisible);\n }\n\n override disconnect(): void {\n this.#timers.clearAll();\n }\n\n /** Toggles the input between masked and revealed. Bound via `data-action`. */\n toggle(): void {\n this.#setVisible(!this.#isVisible);\n }\n\n /** Whether the input is currently revealed (`type=\"text\"`). */\n get #isVisible(): boolean {\n return this.hasInputTarget && this.inputTarget.type === \"text\";\n }\n\n /** Switches the masked/revealed state, preserving focus and caret. */\n #setVisible(visible: boolean): void {\n if (!this.hasInputTarget) return;\n const input = this.inputTarget;\n\n // Only the input's *own* focus is restored across the type change; if the\n // toggle button (keyboard) holds focus, it is left untouched.\n const restoreInputFocus = document.activeElement === input;\n const selectionStart = input.selectionStart;\n const selectionEnd = input.selectionEnd;\n\n input.type = visible ? \"text\" : \"password\";\n\n if (restoreInputFocus) {\n input.focus();\n // `selectionStart` / `selectionEnd` are `number | null` (null for input\n // types that don't expose a selection); only restore when both are present.\n if (selectionStart !== null && selectionEnd !== null) {\n try {\n input.setSelectionRange(selectionStart, selectionEnd);\n } catch {\n // Some input types reject selection access; focus alone is enough.\n }\n }\n }\n\n this.#reflect(visible);\n this.dispatch(\"toggle\", { detail: { visible } });\n\n this.#timers.clearAll();\n if (visible && this.autoHideValue > 0) {\n this.#timers.set(() => this.#setVisible(false), this.autoHideValue);\n }\n }\n\n /** Reflects the visible state onto `aria-pressed` and `data-state`. */\n #reflect(visible: boolean): void {\n if (this.hasToggleTarget) {\n this.toggleTarget.setAttribute(\"aria-pressed\", visible ? \"true\" : \"false\");\n }\n this.element.setAttribute(\"data-state\", visible ? \"visible\" : \"hidden\");\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/safe_timeout.ts","../../src/controllers/password_reveal_controller.ts"],"names":[],"mappings":"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","file":"password_reveal_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 password show/hide (unmask) toggle behavior.\n *\n * Markup contract (identifier: `stimeo--password-reveal`):\n * <div data-controller=\"stimeo--password-reveal\">\n * <input type=\"password\" aria-label=\"Password\"\n * data-stimeo--password-reveal-target=\"input\">\n * <button type=\"button\" aria-pressed=\"false\" aria-label=\"Show password\"\n * data-stimeo--password-reveal-target=\"toggle\"\n * data-action=\"stimeo--password-reveal#toggle\"></button>\n * </div>\n *\n * No dedicated APG pattern; this follows the toggle **Button** practice. The\n * accessible name stays state-independent (\"Show password\") while the pressed\n * state is conveyed by `aria-pressed`.\n *\n * `toggle` dispatches `{ visible: boolean }`.\n *\n * @remarks\n * Behavior only — icon rendering is the consumer's, keyed off `aria-pressed` /\n * `data-state` (`hidden` / `visible`). Flipping `input.type` can drop focus and\n * the caret, so when (and only when) the input was the focused element its focus\n * and selection are restored afterward; when the toggle button holds focus\n * (keyboard use) focus is left on the button. An optional `autoHide` re-masks\n * after a delay, and that timer is torn down on disconnect (Turbo).\n */\nexport class PasswordRevealController extends Controller<HTMLElement> {\n static override targets = [\"input\", \"toggle\"];\n static override values = {\n autoHide: { type: Number, default: 0 },\n };\n static actions = [\"toggle\"] as const;\n static events = [\"toggle\"] as const;\n\n declare readonly inputTarget: HTMLInputElement;\n declare readonly toggleTarget: HTMLElement;\n declare readonly hasInputTarget: boolean;\n declare readonly hasToggleTarget: boolean;\n\n declare autoHideValue: number;\n\n /** Auto re-mask timer; torn down on disconnect. */\n #timers = new SafeTimeout();\n\n override connect(): void {\n this.#reflect(this.#isVisible);\n }\n\n override disconnect(): void {\n this.#timers.clearAll();\n }\n\n /** Toggles the input between masked and revealed. Bound via `data-action`. */\n toggle(): void {\n this.#setVisible(!this.#isVisible);\n }\n\n /** Whether the input is currently revealed (`type=\"text\"`). */\n get #isVisible(): boolean {\n return this.hasInputTarget && this.inputTarget.type === \"text\";\n }\n\n /** Switches the masked/revealed state, preserving focus and caret. */\n #setVisible(visible: boolean): void {\n if (!this.hasInputTarget) return;\n const input = this.inputTarget;\n\n // Only the input's *own* focus is restored across the type change; if the\n // toggle button (keyboard) holds focus, it is left untouched.\n const restoreInputFocus = document.activeElement === input;\n const selectionStart = input.selectionStart;\n const selectionEnd = input.selectionEnd;\n\n input.type = visible ? \"text\" : \"password\";\n\n if (restoreInputFocus) {\n input.focus();\n // `selectionStart` / `selectionEnd` are `number | null` (null for input\n // types that don't expose a selection); only restore when both are present.\n if (selectionStart !== null && selectionEnd !== null) {\n try {\n input.setSelectionRange(selectionStart, selectionEnd);\n } catch {\n // Some input types reject selection access; focus alone is enough.\n }\n }\n }\n\n this.#reflect(visible);\n this.dispatch(\"toggle\", { detail: { visible } });\n\n this.#timers.clearAll();\n if (visible && this.autoHideValue > 0) {\n this.#timers.set(() => this.#setVisible(false), this.autoHideValue);\n }\n }\n\n /** Reflects the visible state onto `aria-pressed` and `data-state`. */\n #reflect(visible: boolean): void {\n if (this.hasToggleTarget) {\n this.toggleTarget.setAttribute(\"aria-pressed\", visible ? \"true\" : \"false\");\n }\n this.element.setAttribute(\"data-state\", visible ? \"visible\" : \"hidden\");\n }\n}\n"]}
@@ -20,6 +20,8 @@ import { Controller } from '@hotwired/stimulus';
20
20
  * `--stimeo--password-strength`, and (when `minScore` is set) `data-below-min`,
21
21
  * and writes the level label into the label target.
22
22
  *
23
+ * `change` dispatches `{ score, level, max, meetsMin }`.
24
+ *
23
25
  * @remarks
24
26
  * Behavior only — the meter/bar look is the consumer's, keyed off the data hooks.
25
27
  * `data-strength` is one of the fixed {@link STRENGTH_BANDS} (not the localizable
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/safe_timeout.ts","../../src/utils/string_list.ts","../../src/controllers/password_strength_controller.ts"],"names":[],"mappings":"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","file":"password_strength_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","/**\n * JSON string-list parsing for the controllers that take a list as a Value.\n *\n * Stimulus offers an `Array` Value type, but its reader runs `JSON.parse` inside\n * the value observer — **before** the `<name>ValueChanged` callback — and\n * rethrows on malformed text. The throw propagates out of the observer that runs\n * during connection, so a single unparseable attribute stops the controller from\n * connecting at all: no lifecycle callback runs and the element is left inert\n * rather than degraded. A consumer cannot guard against it either, because the\n * callback that would hold the `try` never runs.\n *\n * Declaring the attribute as a `String` Value and parsing it here keeps a\n * malformed declaration local to the value it declares. The attribute text is\n * identical either way (`'[\"a\", \"b\"]'`), so markup does not change with the\n * declaration.\n */\n\n/**\n * Parses `raw` as a JSON array of strings.\n *\n * Returns `fallback` when the text is absent, unparseable, or not an array, so a\n * malformed declaration behaves like an omitted one. A parsed array keeps only\n * its string entries: every caller indexes or compares strings, and a stray\n * number would otherwise sit in the list without ever matching.\n *\n * An explicit `\"[]\"` is honoured as an empty list rather than falling back — it\n * is the only way to declare \"none\" against a non-empty default.\n *\n * @param raw - the attribute text, e.g. `'[\"password\"]'`.\n * @param fallback - the list to use when `raw` declares nothing usable.\n */\nexport function parseStringList(raw: string, fallback: readonly string[] = []): string[] {\n // The common case is an undeclared attribute, and `JSON.parse(\"\")` answers it\n // by throwing. Reaching the same fallback without building an exception keeps\n // the callers that ask per element off that path.\n const text = raw.trim();\n if (text.length === 0) return [...fallback];\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n return [...fallback];\n }\n\n if (!Array.isArray(parsed)) return [...fallback];\n return parsed.filter((entry): entry is string => typeof entry === \"string\");\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\nimport { parseStringList } from \"../utils/string_list\";\n\n/** Character classes that contribute to password variety (one point each beyond the first). */\nconst CLASS_PATTERNS: readonly RegExp[] = [/[a-z]/, /[A-Z]/, /[0-9]/, /[^A-Za-z0-9]/];\n\n/** Strength labels used unless the consumer declares its own scale. */\nconst DEFAULT_LEVELS = [\"weak\", \"fair\", \"good\", \"strong\"];\n\n/** Length milestones that each add a strength point. */\nconst LENGTH_MILESTONES: readonly number[] = [8, 12, 16];\n\n/** Upper bound of the raw heuristic points, used to bucket into the level scale. */\nconst MAX_POINTS = LENGTH_MILESTONES.length + (CLASS_PATTERNS.length - 1);\n\n/**\n * Fixed, locale-independent styling bands `data-strength` is drawn from. Kept\n * separate from the (localizable) `levels` labels so `data-strength` stays a\n * stable CSS hook (e.g. `[data-strength=\"weak\"]`) even when `levels` is\n * translated. Ascending: weakest → strongest.\n */\nconst STRENGTH_BANDS = [\"weak\", \"fair\", \"good\", \"strong\"] as const;\n\n/**\n * Headless password-strength behavior: scores the field with a lightweight\n * zero-dependency heuristic and drives a meter plus an `aria-live` label. No\n * dedicated APG pattern; the meter display follows {@link MeterController}.\n *\n * Markup contract (identifier: `stimeo--password-strength`):\n * <div data-controller=\"stimeo--password-strength\">\n * <input type=\"password\" data-stimeo--password-strength-target=\"input\"\n * data-action=\"input->stimeo--password-strength#evaluate\" aria-describedby=\"pw\">\n * <div data-stimeo--password-strength-target=\"meter\" role=\"meter\"\n * aria-valuemin=\"0\" aria-valuemax=\"4\"></div>\n * <span id=\"pw\" data-stimeo--password-strength-target=\"label\" aria-live=\"polite\"></span>\n * </div>\n *\n * On each input the controller scores the password (length milestones + character\n * variety, capped for trivial repetition), syncs the meter's `aria-valuenow`,\n * reflects a stable band on `data-strength`, the `0–1` fill on\n * `--stimeo--password-strength`, and (when `minScore` is set) `data-below-min`,\n * and writes the level label into the label target.\n *\n * @remarks\n * Behavior only — the meter/bar look is the consumer's, keyed off the data hooks.\n * `data-strength` is one of the fixed {@link STRENGTH_BANDS} (not the localizable\n * `levels` text), so consumers can style by it regardless of locale; the visible\n * label receives the matching `levels` entry. Non-text state (meter ARIA,\n * `data-strength`/`data-below-min`, the custom property, and the `change` event)\n * updates **immediately** on every keystroke so styling and consumers stay\n * responsive, while the label — in an `aria-live=\"polite\"` region — is written on\n * a short debounce so a screen reader is not flooded mid-typing. The score is a\n * pure function of the input value (no module-scope state), so `connect()`\n * re-evaluates idempotently after a Turbo cache restore; the debounce timer is\n * owned by {@link SafeTimeout} and torn down on `disconnect()` (Turbo included).\n * The estimator is intentionally not a dictionary/zxcvbn-grade one (kept\n * zero-dep); swap a stronger one in on the consumer side if needed.\n */\nexport class PasswordStrengthController extends Controller<HTMLElement> {\n static override targets = [\"input\", \"meter\", \"label\"];\n static override values = {\n minScore: { type: Number, default: 0 },\n // A JSON list read through `parseStringList` rather than Stimulus's `Array`\n // type: that reader throws out of the value observer before any callback\n // runs, so one malformed attribute would stop the meter connecting.\n levels: { type: String, default: \"\" },\n };\n static actions = [\"evaluate\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly inputTarget: HTMLInputElement;\n declare readonly meterTarget: HTMLElement;\n declare readonly labelTarget: HTMLElement;\n declare readonly hasInputTarget: boolean;\n declare readonly hasMeterTarget: boolean;\n declare readonly hasLabelTarget: boolean;\n\n declare minScoreValue: number;\n declare levelsValue: string;\n\n /** Delay (ms) before the polite live-region label is written, to throttle SR flooding. */\n static readonly #announceDelay = 200;\n\n readonly #timers = new SafeTimeout();\n #announceId: number | null = null;\n\n override connect(): void {\n // Reflect the current value synchronously (no announce): an autofilled or\n // cache-restored field shows the right strength without queuing an SR message.\n this.#update({ announce: false });\n }\n\n override disconnect(): void {\n this.#timers.clearAll();\n this.#announceId = null;\n }\n\n /** Re-evaluates strength from the input. Bound via `data-action` (`input`). */\n evaluate(): void {\n this.#update();\n }\n\n /**\n * Recomputes the strength. The meter ARIA, data hooks, the custom property and\n * the `change` event apply immediately; the live-region label text is debounced\n * unless `announce` is `false` (the initial render).\n */\n #update(options: { announce?: boolean } = {}): void {\n const password = this.hasInputTarget ? this.inputTarget.value : \"\";\n const labels = parseStringList(this.levelsValue, DEFAULT_LEVELS);\n const max = labels.length;\n const score = this.#score(password, max);\n const label = score > 0 ? (labels[score - 1] ?? \"\") : \"\";\n\n this.#reflectMeter(score, max);\n this.#reflectRoot(score, max);\n\n if (options.announce === false) {\n // Initial render (connect / cache-restore): reflect without a change event\n // or a queued screen-reader announcement.\n this.#writeLabel(label);\n return;\n }\n\n this.dispatch(\"change\", {\n detail: { score, level: label, max, meetsMin: score > 0 && score >= this.minScoreValue },\n });\n\n if (this.#announceId !== null) this.#timers.clear(this.#announceId);\n this.#announceId = this.#timers.set(() => {\n this.#writeLabel(label);\n this.#announceId = null;\n }, PasswordStrengthController.#announceDelay);\n }\n\n /** Syncs the meter target's ARIA value attributes (`0..levels.length`). */\n #reflectMeter(score: number, max: number): void {\n if (!this.hasMeterTarget) return;\n this.meterTarget.setAttribute(\"aria-valuemin\", \"0\");\n this.meterTarget.setAttribute(\"aria-valuemax\", String(max));\n this.meterTarget.setAttribute(\"aria-valuenow\", String(score));\n }\n\n /**\n * Reflects the level onto the root: the stable `data-strength` band (absent when\n * empty), the `data-below-min` hook when the score is under `minScore`, and the\n * `0–1` fill the consumer's CSS turns into the bar width.\n */\n #reflectRoot(score: number, max: number): void {\n const band = this.#band(score, max);\n this.#toggle(\"data-strength\", band, band.length > 0);\n // Empty/pristine input (`score === 0`) is never \"below min\": that would let CSS\n // flag an untouched field as failing. Mirror the `change` event's `meetsMin`\n // (`score > 0 && …`) so the hook only marks a *non-empty* password under the\n // threshold. `minScore` defaults to 0, leaving the hook inert until set positive.\n this.#toggle(\"data-below-min\", \"true\", score > 0 && score < this.minScoreValue);\n const ratio = max > 0 ? score / max : 0;\n this.element.style.setProperty(\"--stimeo--password-strength\", String(ratio));\n }\n\n #writeLabel(label: string): void {\n if (this.hasLabelTarget) this.labelTarget.textContent = label;\n }\n\n /**\n * Locale-independent styling band (one of {@link STRENGTH_BANDS}) for `score`\n * out of `max`. Empty input → `\"\"`. Quantizes the `score/max` ratio into the\n * four fixed bands, so a non-default level count still maps onto a stable hook.\n */\n #band(score: number, max: number): string {\n if (score <= 0 || max <= 0) return \"\";\n const index = Math.ceil((score / max) * STRENGTH_BANDS.length) - 1;\n return STRENGTH_BANDS[Math.min(STRENGTH_BANDS.length - 1, Math.max(0, index))] ?? \"\";\n }\n\n /** Sets `name` to `value` when `on`, else removes it (value/presence data hook). */\n #toggle(name: string, value: string, on: boolean): void {\n if (on) {\n this.element.setAttribute(name, value);\n } else {\n this.element.removeAttribute(name);\n }\n }\n\n /**\n * Lightweight zero-dependency strength heuristic returning an integer in\n * `[0, max]` (`max` = number of levels). Empty input is `0` (no level); any\n * non-empty password is at least `1`. Points accrue from length milestones and\n * character-class variety, then bucket into the level scale. A tiny alphabet\n * (≤ 2 distinct characters, e.g. \"aaaa\") is capped as the weakest, so length\n * alone cannot mask trivial repetition.\n */\n #score(password: string, max: number): number {\n if (password.length === 0 || max === 0) return 0;\n\n let points = 0;\n for (const milestone of LENGTH_MILESTONES) {\n if (password.length >= milestone) points += 1;\n }\n // The first present character class is free; each additional one adds a point.\n points += CLASS_PATTERNS.filter((re) => re.test(password)).length - 1;\n\n if (new Set(password).size <= 2) points = 0; // trivial repetition → weakest\n\n const bucketed = Math.round((points / MAX_POINTS) * max);\n return Math.min(max, Math.max(1, bucketed));\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/safe_timeout.ts","../../src/utils/string_list.ts","../../src/controllers/password_strength_controller.ts"],"names":[],"mappings":"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","file":"password_strength_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","/**\n * JSON string-list parsing for the controllers that take a list as a Value.\n *\n * Stimulus offers an `Array` Value type, but its reader runs `JSON.parse` inside\n * the value observer — **before** the `<name>ValueChanged` callback — and\n * rethrows on malformed text. The throw propagates out of the observer that runs\n * during connection, so a single unparseable attribute stops the controller from\n * connecting at all: no lifecycle callback runs and the element is left inert\n * rather than degraded. A consumer cannot guard against it either, because the\n * callback that would hold the `try` never runs.\n *\n * Declaring the attribute as a `String` Value and parsing it here keeps a\n * malformed declaration local to the value it declares. The attribute text is\n * identical either way (`'[\"a\", \"b\"]'`), so markup does not change with the\n * declaration.\n */\n\n/**\n * Parses `raw` as a JSON array of strings.\n *\n * Returns `fallback` when the text is absent, unparseable, or not an array, so a\n * malformed declaration behaves like an omitted one. A parsed array keeps only\n * its string entries: every caller indexes or compares strings, and a stray\n * number would otherwise sit in the list without ever matching.\n *\n * An explicit `\"[]\"` is honoured as an empty list rather than falling back — it\n * is the only way to declare \"none\" against a non-empty default.\n *\n * @param raw - the attribute text, e.g. `'[\"password\"]'`.\n * @param fallback - the list to use when `raw` declares nothing usable.\n */\nexport function parseStringList(raw: string, fallback: readonly string[] = []): string[] {\n // The common case is an undeclared attribute, and `JSON.parse(\"\")` answers it\n // by throwing. Reaching the same fallback without building an exception keeps\n // the callers that ask per element off that path.\n const text = raw.trim();\n if (text.length === 0) return [...fallback];\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n return [...fallback];\n }\n\n if (!Array.isArray(parsed)) return [...fallback];\n return parsed.filter((entry): entry is string => typeof entry === \"string\");\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\nimport { parseStringList } from \"../utils/string_list\";\n\n/** Character classes that contribute to password variety (one point each beyond the first). */\nconst CLASS_PATTERNS: readonly RegExp[] = [/[a-z]/, /[A-Z]/, /[0-9]/, /[^A-Za-z0-9]/];\n\n/** Strength labels used unless the consumer declares its own scale. */\nconst DEFAULT_LEVELS = [\"weak\", \"fair\", \"good\", \"strong\"];\n\n/** Length milestones that each add a strength point. */\nconst LENGTH_MILESTONES: readonly number[] = [8, 12, 16];\n\n/** Upper bound of the raw heuristic points, used to bucket into the level scale. */\nconst MAX_POINTS = LENGTH_MILESTONES.length + (CLASS_PATTERNS.length - 1);\n\n/**\n * Fixed, locale-independent styling bands `data-strength` is drawn from. Kept\n * separate from the (localizable) `levels` labels so `data-strength` stays a\n * stable CSS hook (e.g. `[data-strength=\"weak\"]`) even when `levels` is\n * translated. Ascending: weakest → strongest.\n */\nconst STRENGTH_BANDS = [\"weak\", \"fair\", \"good\", \"strong\"] as const;\n\n/**\n * Headless password-strength behavior: scores the field with a lightweight\n * zero-dependency heuristic and drives a meter plus an `aria-live` label. No\n * dedicated APG pattern; the meter display follows {@link MeterController}.\n *\n * Markup contract (identifier: `stimeo--password-strength`):\n * <div data-controller=\"stimeo--password-strength\">\n * <input type=\"password\" data-stimeo--password-strength-target=\"input\"\n * data-action=\"input->stimeo--password-strength#evaluate\" aria-describedby=\"pw\">\n * <div data-stimeo--password-strength-target=\"meter\" role=\"meter\"\n * aria-valuemin=\"0\" aria-valuemax=\"4\"></div>\n * <span id=\"pw\" data-stimeo--password-strength-target=\"label\" aria-live=\"polite\"></span>\n * </div>\n *\n * On each input the controller scores the password (length milestones + character\n * variety, capped for trivial repetition), syncs the meter's `aria-valuenow`,\n * reflects a stable band on `data-strength`, the `0–1` fill on\n * `--stimeo--password-strength`, and (when `minScore` is set) `data-below-min`,\n * and writes the level label into the label target.\n *\n * `change` dispatches `{ score, level, max, meetsMin }`.\n *\n * @remarks\n * Behavior only — the meter/bar look is the consumer's, keyed off the data hooks.\n * `data-strength` is one of the fixed {@link STRENGTH_BANDS} (not the localizable\n * `levels` text), so consumers can style by it regardless of locale; the visible\n * label receives the matching `levels` entry. Non-text state (meter ARIA,\n * `data-strength`/`data-below-min`, the custom property, and the `change` event)\n * updates **immediately** on every keystroke so styling and consumers stay\n * responsive, while the label — in an `aria-live=\"polite\"` region — is written on\n * a short debounce so a screen reader is not flooded mid-typing. The score is a\n * pure function of the input value (no module-scope state), so `connect()`\n * re-evaluates idempotently after a Turbo cache restore; the debounce timer is\n * owned by {@link SafeTimeout} and torn down on `disconnect()` (Turbo included).\n * The estimator is intentionally not a dictionary/zxcvbn-grade one (kept\n * zero-dep); swap a stronger one in on the consumer side if needed.\n */\nexport class PasswordStrengthController extends Controller<HTMLElement> {\n static override targets = [\"input\", \"meter\", \"label\"];\n static override values = {\n minScore: { type: Number, default: 0 },\n // A JSON list read through `parseStringList` rather than Stimulus's `Array`\n // type: that reader throws out of the value observer before any callback\n // runs, so one malformed attribute would stop the meter connecting.\n levels: { type: String, default: \"\" },\n };\n static actions = [\"evaluate\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly inputTarget: HTMLInputElement;\n declare readonly meterTarget: HTMLElement;\n declare readonly labelTarget: HTMLElement;\n declare readonly hasInputTarget: boolean;\n declare readonly hasMeterTarget: boolean;\n declare readonly hasLabelTarget: boolean;\n\n declare minScoreValue: number;\n declare levelsValue: string;\n\n /** Delay (ms) before the polite live-region label is written, to throttle SR flooding. */\n static readonly #announceDelay = 200;\n\n readonly #timers = new SafeTimeout();\n #announceId: number | null = null;\n\n override connect(): void {\n // Reflect the current value synchronously (no announce): an autofilled or\n // cache-restored field shows the right strength without queuing an SR message.\n this.#update({ announce: false });\n }\n\n override disconnect(): void {\n this.#timers.clearAll();\n this.#announceId = null;\n }\n\n /** Re-evaluates strength from the input. Bound via `data-action` (`input`). */\n evaluate(): void {\n this.#update();\n }\n\n /**\n * Recomputes the strength. The meter ARIA, data hooks, the custom property and\n * the `change` event apply immediately; the live-region label text is debounced\n * unless `announce` is `false` (the initial render).\n */\n #update(options: { announce?: boolean } = {}): void {\n const password = this.hasInputTarget ? this.inputTarget.value : \"\";\n const labels = parseStringList(this.levelsValue, DEFAULT_LEVELS);\n const max = labels.length;\n const score = this.#score(password, max);\n const label = score > 0 ? (labels[score - 1] ?? \"\") : \"\";\n\n this.#reflectMeter(score, max);\n this.#reflectRoot(score, max);\n\n if (options.announce === false) {\n // Initial render (connect / cache-restore): reflect without a change event\n // or a queued screen-reader announcement.\n this.#writeLabel(label);\n return;\n }\n\n this.dispatch(\"change\", {\n detail: { score, level: label, max, meetsMin: score > 0 && score >= this.minScoreValue },\n });\n\n if (this.#announceId !== null) this.#timers.clear(this.#announceId);\n this.#announceId = this.#timers.set(() => {\n this.#writeLabel(label);\n this.#announceId = null;\n }, PasswordStrengthController.#announceDelay);\n }\n\n /** Syncs the meter target's ARIA value attributes (`0..levels.length`). */\n #reflectMeter(score: number, max: number): void {\n if (!this.hasMeterTarget) return;\n this.meterTarget.setAttribute(\"aria-valuemin\", \"0\");\n this.meterTarget.setAttribute(\"aria-valuemax\", String(max));\n this.meterTarget.setAttribute(\"aria-valuenow\", String(score));\n }\n\n /**\n * Reflects the level onto the root: the stable `data-strength` band (absent when\n * empty), the `data-below-min` hook when the score is under `minScore`, and the\n * `0–1` fill the consumer's CSS turns into the bar width.\n */\n #reflectRoot(score: number, max: number): void {\n const band = this.#band(score, max);\n this.#toggle(\"data-strength\", band, band.length > 0);\n // Empty/pristine input (`score === 0`) is never \"below min\": that would let CSS\n // flag an untouched field as failing. Mirror the `change` event's `meetsMin`\n // (`score > 0 && …`) so the hook only marks a *non-empty* password under the\n // threshold. `minScore` defaults to 0, leaving the hook inert until set positive.\n this.#toggle(\"data-below-min\", \"true\", score > 0 && score < this.minScoreValue);\n const ratio = max > 0 ? score / max : 0;\n this.element.style.setProperty(\"--stimeo--password-strength\", String(ratio));\n }\n\n #writeLabel(label: string): void {\n if (this.hasLabelTarget) this.labelTarget.textContent = label;\n }\n\n /**\n * Locale-independent styling band (one of {@link STRENGTH_BANDS}) for `score`\n * out of `max`. Empty input → `\"\"`. Quantizes the `score/max` ratio into the\n * four fixed bands, so a non-default level count still maps onto a stable hook.\n */\n #band(score: number, max: number): string {\n if (score <= 0 || max <= 0) return \"\";\n const index = Math.ceil((score / max) * STRENGTH_BANDS.length) - 1;\n return STRENGTH_BANDS[Math.min(STRENGTH_BANDS.length - 1, Math.max(0, index))] ?? \"\";\n }\n\n /** Sets `name` to `value` when `on`, else removes it (value/presence data hook). */\n #toggle(name: string, value: string, on: boolean): void {\n if (on) {\n this.element.setAttribute(name, value);\n } else {\n this.element.removeAttribute(name);\n }\n }\n\n /**\n * Lightweight zero-dependency strength heuristic returning an integer in\n * `[0, max]` (`max` = number of levels). Empty input is `0` (no level); any\n * non-empty password is at least `1`. Points accrue from length milestones and\n * character-class variety, then bucket into the level scale. A tiny alphabet\n * (≤ 2 distinct characters, e.g. \"aaaa\") is capped as the weakest, so length\n * alone cannot mask trivial repetition.\n */\n #score(password: string, max: number): number {\n if (password.length === 0 || max === 0) return 0;\n\n let points = 0;\n for (const milestone of LENGTH_MILESTONES) {\n if (password.length >= milestone) points += 1;\n }\n // The first present character class is free; each additional one adds a point.\n points += CLASS_PATTERNS.filter((re) => re.test(password)).length - 1;\n\n if (new Set(password).size <= 2) points = 0; // trivial repetition → weakest\n\n const bucketed = Math.round((points / MAX_POINTS) * max);\n return Math.min(max, Math.max(1, bucketed));\n }\n}\n"]}
@@ -1,11 +1,8 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- /** Field controls this controller can persist. */
4
- type PersistField = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
5
3
  /**
6
- * Headless draft-autosave behavior: persists a form's field values to
7
- * `localStorage` and restores them across Turbo navigations and reloads (no APG
8
- * pattern; a state-holding utility).
4
+ * Headless draft-autosave behavior: persists form controls to `localStorage`
5
+ * and restores them across Turbo navigation and reloads.
9
6
  *
10
7
  * Markup contract (identifier: `stimeo--persist`):
11
8
  * <form data-controller="stimeo--persist"
@@ -14,16 +11,25 @@ type PersistField = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
14
11
  * <textarea name="message"></textarea>
15
12
  * </form>
16
13
  *
17
- * On connect it restores any saved values under `key` (falling back to the
18
- * element's `id`), then debounce-saves on every input/change. Password fields (and
19
- * anything in `exclude`) are never written. A `clear()` action — or the `clearOn`
20
- * event (e.g. `submit`) — drops the draft. Restoring never moves focus.
14
+ * The optional `field` targets restrict persistence to explicitly targeted
15
+ * controls. The public `clear` action removes the active draft. `key`,
16
+ * `debounce`, `exclude`, and `clearOn` Values can change while connected;
17
+ * `clearOn` is an event name whose occurrence invokes `clear`.
18
+ *
19
+ * `restore`, `save`, and `clear` dispatch `{ key: string }`. `error` dispatches
20
+ * `{ key: string, operation: "read" | "write" | "remove", reason:
21
+ * "unavailable" | "invalid-payload" }`. Restore writes are intentionally
22
+ * silent native-control mutations; consumers synchronize from the bubbling
23
+ * `restore` event without treating a restored draft as user input.
21
24
  *
22
25
  * @remarks
23
- * Behavior only. State lives entirely in `localStorage` (no module-scope state), so
24
- * instances never interfere. The input listener and debounce timer are removed on
25
- * `disconnect()` (Turbo navigation included), where a pending save is flushed first
26
- * so an in-flight edit is not lost.
26
+ * Behavior only — restoration never moves focus. Passwords are never stored;
27
+ * the default exclusion also omits Rails request metadata while preserving
28
+ * application-owned hidden fields. Distinct logical keys isolate instances;
29
+ * instances using the same key intentionally share one last-writer-wins draft.
30
+ * Every listener, timer, observer, and queued reconciliation is released on
31
+ * `disconnect()`, after any pending edit is flushed through the normal save
32
+ * path. A reconnect removes stale restored state before reading storage.
27
33
  */
28
34
  declare class PersistController extends Controller<HTMLElement> {
29
35
  #private;
@@ -47,8 +53,8 @@ declare class PersistController extends Controller<HTMLElement> {
47
53
  };
48
54
  };
49
55
  static actions: readonly ["clear"];
50
- static events: readonly ["restore", "save", "clear"];
51
- readonly fieldTargets: PersistField[];
56
+ static events: readonly ["restore", "save", "clear", "error"];
57
+ readonly fieldTargets: Element[];
52
58
  readonly hasFieldTarget: boolean;
53
59
  keyValue: string;
54
60
  debounceValue: number;
@@ -56,8 +62,16 @@ declare class PersistController extends Controller<HTMLElement> {
56
62
  clearOnValue: string;
57
63
  connect(): void;
58
64
  disconnect(): void;
59
- /** Drops the saved draft and clears the restored marker. */
65
+ /** Drops the active draft after storage confirms the removal. */
60
66
  clear(): void;
67
+ /** Rebinds the external clear event when its Value changes. */
68
+ clearOnValueChanged(): void;
69
+ /** Switches storage namespaces without writing old edits under the new key. */
70
+ keyValueChanged(): void;
71
+ /** Reschedules a pending write against the current debounce delay. */
72
+ debounceValueChanged(): void;
73
+ /** Restores every field the new exclusion list makes eligible. */
74
+ excludeValueChanged(): void;
61
75
  }
62
76
 
63
77
  export { PersistController };