stimeo-ui 0.9.0 → 0.11.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 (162) hide show
  1. package/CHANGELOG.md +149 -0
  2. package/dist/cable/index.d.ts +79 -14
  3. package/dist/cable/index.js +226 -43
  4. package/dist/cable/index.js.map +1 -1
  5. package/dist/controllers/accordion_controller.js.map +1 -1
  6. package/dist/controllers/alert_dialog_controller.js +17 -7
  7. package/dist/controllers/alert_dialog_controller.js.map +1 -1
  8. package/dist/controllers/announcer_controller.js +1 -1
  9. package/dist/controllers/announcer_controller.js.map +1 -1
  10. package/dist/controllers/auto_submit_controller.js.map +1 -1
  11. package/dist/controllers/avatar_controller.js.map +1 -1
  12. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  13. package/dist/controllers/bulk_select_controller.js +2 -2
  14. package/dist/controllers/bulk_select_controller.js.map +1 -1
  15. package/dist/controllers/calendar_controller.js.map +1 -1
  16. package/dist/controllers/carousel_controller.js.map +1 -1
  17. package/dist/controllers/character_counter_controller.js.map +1 -1
  18. package/dist/controllers/clipboard_controller.js.map +1 -1
  19. package/dist/controllers/collapsible_controller.d.ts +1 -1
  20. package/dist/controllers/collapsible_controller.js +1 -1
  21. package/dist/controllers/collapsible_controller.js.map +1 -1
  22. package/dist/controllers/color_picker_controller.d.ts +1 -1
  23. package/dist/controllers/color_picker_controller.js.map +1 -1
  24. package/dist/controllers/combobox_controller.js.map +1 -1
  25. package/dist/controllers/command_palette_controller.js +15 -5
  26. package/dist/controllers/command_palette_controller.js.map +1 -1
  27. package/dist/controllers/conditional_fields_controller.js.map +1 -1
  28. package/dist/controllers/confirm_controller.js +15 -5
  29. package/dist/controllers/confirm_controller.js.map +1 -1
  30. package/dist/controllers/context_menu_controller.d.ts +5 -4
  31. package/dist/controllers/context_menu_controller.js.map +1 -1
  32. package/dist/controllers/count_up_controller.js.map +1 -1
  33. package/dist/controllers/countdown_controller.js +1 -1
  34. package/dist/controllers/countdown_controller.js.map +1 -1
  35. package/dist/controllers/data_grid_controller.js +1 -1
  36. package/dist/controllers/data_grid_controller.js.map +1 -1
  37. package/dist/controllers/date_range_picker_controller.js.map +1 -1
  38. package/dist/controllers/dialog_controller.js +15 -5
  39. package/dist/controllers/dialog_controller.js.map +1 -1
  40. package/dist/controllers/direct_upload_controller.d.ts +2 -2
  41. package/dist/controllers/direct_upload_controller.js +1 -5
  42. package/dist/controllers/direct_upload_controller.js.map +1 -1
  43. package/dist/controllers/dismissible_controller.js.map +1 -1
  44. package/dist/controllers/drawer_controller.d.ts +2 -2
  45. package/dist/controllers/drawer_controller.js +17 -7
  46. package/dist/controllers/drawer_controller.js.map +1 -1
  47. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  48. package/dist/controllers/flash_controller.js.map +1 -1
  49. package/dist/controllers/focus_controller.d.ts +5 -0
  50. package/dist/controllers/focus_controller.js +45 -7
  51. package/dist/controllers/focus_controller.js.map +1 -1
  52. package/dist/controllers/form_field_controller.js +1 -1
  53. package/dist/controllers/form_field_controller.js.map +1 -1
  54. package/dist/controllers/form_validation_controller.js.map +1 -1
  55. package/dist/controllers/frame_loading_controller.js.map +1 -1
  56. package/dist/controllers/highlight_controller.js.map +1 -1
  57. package/dist/controllers/input_mask_controller.js.map +1 -1
  58. package/dist/controllers/intersection_controller.d.ts +4 -3
  59. package/dist/controllers/intersection_controller.js.map +1 -1
  60. package/dist/controllers/lazy_frame_controller.d.ts +2 -2
  61. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  62. package/dist/controllers/listbox_controller.js.map +1 -1
  63. package/dist/controllers/masonry_controller.d.ts +3 -1
  64. package/dist/controllers/masonry_controller.js.map +1 -1
  65. package/dist/controllers/menu_controller.d.ts +1 -1
  66. package/dist/controllers/menu_controller.js.map +1 -1
  67. package/dist/controllers/menubar_controller.js +2 -2
  68. package/dist/controllers/menubar_controller.js.map +1 -1
  69. package/dist/controllers/meter_controller.d.ts +1 -1
  70. package/dist/controllers/meter_controller.js.map +1 -1
  71. package/dist/controllers/multi_select_controller.js +1 -1
  72. package/dist/controllers/multi_select_controller.js.map +1 -1
  73. package/dist/controllers/navigation_menu_controller.js.map +1 -1
  74. package/dist/controllers/nested_form_controller.js.map +1 -1
  75. package/dist/controllers/number_input_controller.js.map +1 -1
  76. package/dist/controllers/otp_controller.js.map +1 -1
  77. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  78. package/dist/controllers/overflow_menu_controller.js +2 -2
  79. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  80. package/dist/controllers/pagination_controller.d.ts +1 -1
  81. package/dist/controllers/pagination_controller.js.map +1 -1
  82. package/dist/controllers/password_reveal_controller.d.ts +27 -2
  83. package/dist/controllers/password_reveal_controller.js +102 -3
  84. package/dist/controllers/password_reveal_controller.js.map +1 -1
  85. package/dist/controllers/password_strength_controller.d.ts +71 -17
  86. package/dist/controllers/password_strength_controller.js +287 -42
  87. package/dist/controllers/password_strength_controller.js.map +1 -1
  88. package/dist/controllers/pointer_drag_controller.d.ts +23 -5
  89. package/dist/controllers/pointer_drag_controller.js +64 -18
  90. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  91. package/dist/controllers/popover_controller.js.map +1 -1
  92. package/dist/controllers/portal_controller.d.ts +17 -4
  93. package/dist/controllers/portal_controller.js +32 -11
  94. package/dist/controllers/portal_controller.js.map +1 -1
  95. package/dist/controllers/preview_guard_controller.d.ts +22 -14
  96. package/dist/controllers/preview_guard_controller.js +180 -20
  97. package/dist/controllers/preview_guard_controller.js.map +1 -1
  98. package/dist/controllers/progress_controller.js.map +1 -1
  99. package/dist/controllers/radio_group_controller.js.map +1 -1
  100. package/dist/controllers/range_slider_controller.js.map +1 -1
  101. package/dist/controllers/rating_controller.js.map +1 -1
  102. package/dist/controllers/read_more_controller.js.map +1 -1
  103. package/dist/controllers/relative_time_controller.js.map +1 -1
  104. package/dist/controllers/reset_before_cache_controller.d.ts +1 -2
  105. package/dist/controllers/reset_before_cache_controller.js.map +1 -1
  106. package/dist/controllers/resizable_controller.d.ts +1 -1
  107. package/dist/controllers/resizable_controller.js +1 -1
  108. package/dist/controllers/resizable_controller.js.map +1 -1
  109. package/dist/controllers/roving_controller.d.ts +17 -4
  110. package/dist/controllers/roving_controller.js +127 -14
  111. package/dist/controllers/roving_controller.js.map +1 -1
  112. package/dist/controllers/scroll_area_controller.js.map +1 -1
  113. package/dist/controllers/scroll_restore_controller.d.ts +33 -11
  114. package/dist/controllers/scroll_restore_controller.js +127 -27
  115. package/dist/controllers/scroll_restore_controller.js.map +1 -1
  116. package/dist/controllers/scroll_visibility_controller.d.ts +42 -10
  117. package/dist/controllers/scroll_visibility_controller.js +167 -14
  118. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  119. package/dist/controllers/scrollspy_controller.js.map +1 -1
  120. package/dist/controllers/separator_controller.js.map +1 -1
  121. package/dist/controllers/sidebar_controller.js +15 -5
  122. package/dist/controllers/sidebar_controller.js.map +1 -1
  123. package/dist/controllers/skeleton_controller.js.map +1 -1
  124. package/dist/controllers/slider_controller.js.map +1 -1
  125. package/dist/controllers/spinner_controller.js.map +1 -1
  126. package/dist/controllers/step_indicator_controller.js +0 -5
  127. package/dist/controllers/step_indicator_controller.js.map +1 -1
  128. package/dist/controllers/stick_to_bottom_controller.d.ts +26 -9
  129. package/dist/controllers/stick_to_bottom_controller.js +149 -28
  130. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  131. package/dist/controllers/submit_once_controller.js.map +1 -1
  132. package/dist/controllers/switch_controller.js.map +1 -1
  133. package/dist/controllers/tabs_controller.js.map +1 -1
  134. package/dist/controllers/tags_input_controller.js.map +1 -1
  135. package/dist/controllers/textarea_autosize_controller.js.map +1 -1
  136. package/dist/controllers/theme_controller.d.ts +32 -6
  137. package/dist/controllers/theme_controller.js +151 -40
  138. package/dist/controllers/theme_controller.js.map +1 -1
  139. package/dist/controllers/time_picker_controller.js.map +1 -1
  140. package/dist/controllers/toast_controller.d.ts +2 -2
  141. package/dist/controllers/toast_controller.js +2 -2
  142. package/dist/controllers/toast_controller.js.map +1 -1
  143. package/dist/controllers/toggle_group_controller.js.map +1 -1
  144. package/dist/controllers/toolbar_controller.js.map +1 -1
  145. package/dist/controllers/transition_controller.d.ts +11 -7
  146. package/dist/controllers/transition_controller.js +79 -15
  147. package/dist/controllers/transition_controller.js.map +1 -1
  148. package/dist/controllers/tree_view_controller.js.map +1 -1
  149. package/dist/index.d.ts +6 -3
  150. package/dist/index.js +1188 -318
  151. package/dist/index.js.map +1 -1
  152. package/dist/inspector/cli.d.ts +3 -4
  153. package/dist/inspector/cli.js +1 -0
  154. package/dist/inspector/cli.js.map +1 -1
  155. package/dist/inspector/cli_bin.js +6 -5
  156. package/dist/inspector/cli_bin.js.map +1 -1
  157. package/dist/inspector/examples.json +6 -6
  158. package/dist/inspector/manifest.json +15 -32
  159. package/dist/positioning/index.d.ts +18 -4
  160. package/dist/positioning/index.js +84 -25
  161. package/dist/positioning/index.js.map +1 -1
  162. package/package.json +6 -6
@@ -2,6 +2,92 @@ import { Controller } from '@hotwired/stimulus';
2
2
 
3
3
  // src/controllers/password_strength_controller.ts
4
4
 
5
+ // src/utils/announce.ts
6
+ function announce(message, options = {}) {
7
+ const text = message.trim();
8
+ if (text.length === 0) return;
9
+ window.dispatchEvent(
10
+ new CustomEvent("stimeo--announcer:announce", {
11
+ detail: { message: text, assertive: options.assertive === true }
12
+ })
13
+ );
14
+ }
15
+ function fillTemplate(template, values) {
16
+ return template.replace(/\{([a-zA-Z][a-zA-Z0-9]*)\}/g, (match, name) => {
17
+ const replacement = values[name];
18
+ return replacement === void 0 ? match : String(replacement);
19
+ });
20
+ }
21
+
22
+ // src/utils/before_cache_reset.ts
23
+ var BeforeCacheReset = class _BeforeCacheReset {
24
+ /** Every subscribed instance, iterated by the one shared document listener. */
25
+ static #subscribers = /* @__PURE__ */ new Set();
26
+ /** The shared listener; installed while at least one instance is subscribed. */
27
+ static #onBeforeCache = () => {
28
+ for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
29
+ };
30
+ #rewind;
31
+ /** @param rewind - the pass that returns this controller's state to its initial form. */
32
+ constructor(rewind) {
33
+ this.#rewind = rewind;
34
+ }
35
+ /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
36
+ activate() {
37
+ const first = _BeforeCacheReset.#subscribers.size === 0;
38
+ _BeforeCacheReset.#subscribers.add(this);
39
+ if (first) {
40
+ document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
41
+ }
42
+ }
43
+ /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
44
+ deactivate() {
45
+ _BeforeCacheReset.#subscribers.delete(this);
46
+ if (_BeforeCacheReset.#subscribers.size > 0) return;
47
+ document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
48
+ }
49
+ };
50
+
51
+ // src/utils/coerce.ts
52
+ function toFiniteNumber(raw) {
53
+ if (raw === null || raw === void 0 || raw === "") return null;
54
+ const value = typeof raw === "number" ? raw : Number(raw);
55
+ return Number.isFinite(value) ? value : null;
56
+ }
57
+
58
+ // src/utils/microtask_coalescer.ts
59
+ var MicrotaskCoalescer = class {
60
+ #run;
61
+ #queued = false;
62
+ #active = false;
63
+ #generation = 0;
64
+ /** @param run - the single reconciliation pass, invoked at most once per batch. */
65
+ constructor(run) {
66
+ this.#run = run;
67
+ }
68
+ /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
69
+ activate() {
70
+ this.#active = true;
71
+ }
72
+ /** Closes the window and drops any pending pass; call from `disconnect()`. */
73
+ cancel() {
74
+ this.#active = false;
75
+ this.#queued = false;
76
+ this.#generation += 1;
77
+ }
78
+ /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
79
+ schedule() {
80
+ if (!this.#active || this.#queued) return;
81
+ this.#queued = true;
82
+ const generation = this.#generation;
83
+ queueMicrotask(() => {
84
+ if (generation !== this.#generation || !this.#queued || !this.#active) return;
85
+ this.#queued = false;
86
+ this.#run();
87
+ });
88
+ }
89
+ };
90
+
5
91
  // src/utils/safe_timeout.ts
6
92
  var TimerRegistry = class {
7
93
  /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
@@ -75,6 +161,7 @@ var DEFAULT_LEVELS = ["weak", "fair", "good", "strong"];
75
161
  var LENGTH_MILESTONES = [8, 12, 16];
76
162
  var MAX_POINTS = LENGTH_MILESTONES.length + (CLASS_PATTERNS.length - 1);
77
163
  var STRENGTH_BANDS = ["weak", "fair", "good", "strong"];
164
+ var MIN_LEVELS = 2;
78
165
  var PasswordStrengthController = class _PasswordStrengthController extends Controller {
79
166
  static targets = ["input", "meter", "label"];
80
167
  static values = {
@@ -82,50 +169,143 @@ var PasswordStrengthController = class _PasswordStrengthController extends Contr
82
169
  // A JSON list read through `parseStringList` rather than Stimulus's `Array`
83
170
  // type: that reader throws out of the value observer before any callback
84
171
  // runs, so one malformed attribute would stop the meter connecting.
85
- levels: { type: String, default: "" }
172
+ levels: { type: String, default: "" },
173
+ announceText: { type: String, default: "" }
86
174
  };
87
- static actions = ["evaluate"];
88
- static events = ["change"];
89
- /** Delay (ms) before the polite live-region label is written, to throttle SR flooding. */
175
+ static actions = ["evaluate", "setScore"];
176
+ static events = ["change", "reconcile"];
177
+ /** Delay (ms) before one settled level is sent to the shared announcer. */
90
178
  static #announceDelay = 200;
91
179
  #timers = new SafeTimeout();
180
+ #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
181
+ #repaint = new MicrotaskCoalescer(() => this.#reconcile());
182
+ #levels = [...DEFAULT_LEVELS];
92
183
  #announceId = null;
184
+ #announcedLevel = null;
185
+ #externalScore = null;
186
+ #lastDetail = null;
187
+ /** Reflects the current DOM state and opens the reconciliation window. */
93
188
  connect() {
94
- this.#update({ announce: false });
189
+ this.#repaint.activate();
190
+ this.#beforeCache.activate();
191
+ this.#lastDetail = this.#detail(this.#render());
95
192
  }
193
+ /** Releases the reconciliation window, the cache subscription and the debounce. */
96
194
  disconnect() {
97
- this.#timers.clearAll();
98
- this.#announceId = null;
195
+ this.#repaint.cancel();
196
+ this.#beforeCache.deactivate();
197
+ this.#cancelAnnouncement();
198
+ this.#announcedLevel = null;
199
+ this.#externalScore = null;
200
+ this.#lastDetail = null;
99
201
  }
100
202
  /** Re-evaluates strength from the input. Bound via `data-action` (`input`). */
101
203
  evaluate() {
102
- this.#update();
204
+ this.#externalScore = null;
205
+ this.#commit();
103
206
  }
104
207
  /**
105
- * Recomputes the strength. The meter ARIA, data hooks, the custom property and
106
- * the `change` event apply immediately; the live-region label text is debounced
107
- * unless `announce` is `false` (the initial render).
208
+ * Adopts a score computed outside the built-in heuristic, clamped into the
209
+ * declared scale. An unreadable value leaves the current score standing, and
210
+ * the next `evaluate` hands scoring back to the heuristic.
108
211
  */
109
- #update(options = {}) {
110
- const password = this.hasInputTarget ? this.inputTarget.value : "";
111
- const labels = parseStringList(this.levelsValue, DEFAULT_LEVELS);
112
- const max = labels.length;
113
- const score = this.#score(password, max);
114
- const label = score > 0 ? labels[score - 1] ?? "" : "";
212
+ setScore(event) {
213
+ const next = toFiniteNumber(event.params?.score ?? event.detail?.score);
214
+ if (next === null) return;
215
+ this.#externalScore = next;
216
+ this.#commit();
217
+ }
218
+ /** Re-reads the scale when application code or a Turbo morph changes `levels`. */
219
+ levelsValueChanged() {
220
+ this.#levels = this.#readLevels();
221
+ this.#repaint.schedule();
222
+ }
223
+ /** Repaints when application code or a Turbo morph changes `minScore`. */
224
+ minScoreValueChanged() {
225
+ this.#repaint.schedule();
226
+ }
227
+ /** Repaints when application code or a Turbo morph changes `announceText`. */
228
+ announceTextValueChanged() {
229
+ this.#repaint.schedule();
230
+ }
231
+ /** Scores the field added or replaced at runtime. */
232
+ inputTargetConnected() {
233
+ this.#repaint.schedule();
234
+ }
235
+ /** Falls back to the pristine state after the scored field is removed. */
236
+ inputTargetDisconnected() {
237
+ this.#repaint.schedule();
238
+ }
239
+ /** Syncs a meter added or replaced at runtime, which carries no value yet. */
240
+ meterTargetConnected() {
241
+ this.#repaint.schedule();
242
+ }
243
+ /** Repaints the remaining output after a meter is removed. */
244
+ meterTargetDisconnected() {
245
+ this.#repaint.schedule();
246
+ }
247
+ /** Fills a readout added or replaced at runtime. */
248
+ labelTargetConnected() {
249
+ this.#repaint.schedule();
250
+ }
251
+ /** Repaints the remaining output after a readout is removed. */
252
+ labelTargetDisconnected() {
253
+ this.#repaint.schedule();
254
+ }
255
+ /** Reflects one confirmed scoring pass and offers its level to the reader. */
256
+ #commit() {
257
+ const reading = this.#render();
258
+ this.#lastDetail = this.#detail(reading);
259
+ this.dispatch("change", { detail: this.#lastDetail });
260
+ this.#scheduleAnnouncement(reading);
261
+ }
262
+ /** Repaints one mutation batch and reports a changed controller-derived state. */
263
+ #reconcile() {
264
+ const previous = this.#lastDetail;
265
+ const owed = this.#announceId !== null;
266
+ this.#cancelAnnouncement();
267
+ const reading = this.#render();
268
+ const detail = this.#detail(reading);
269
+ this.#lastDetail = detail;
270
+ if (reading.score === 0) this.#announcedLevel = null;
271
+ if (owed) this.#scheduleAnnouncement(reading);
272
+ if (previous && this.#detailsDiffer(previous, detail)) {
273
+ this.dispatch("reconcile", { detail });
274
+ }
275
+ }
276
+ /**
277
+ * Synchronizes the meter ARIA, the root state hooks, the fill custom property
278
+ * and the visible level readout.
279
+ *
280
+ * @stimeoRenderRoot
281
+ */
282
+ #render() {
283
+ const max = this.#levels.length;
284
+ const score = this.#currentScore(max);
285
+ const level = this.#levels[score - 1] ?? "";
286
+ const meetsMin = score > 0 && score >= this.#minScore;
287
+ const band = this.#band(score, max);
115
288
  this.#reflectMeter(score, max);
116
- this.#reflectRoot(score, max);
117
- if (options.announce === false) {
118
- this.#writeLabel(label);
119
- return;
289
+ this.#reflectRoot(score, max, band, meetsMin);
290
+ this.#writeLabel(level);
291
+ return { score, level, max, meetsMin, band };
292
+ }
293
+ /**
294
+ * The declared minimum, or the Value's default (`0`, an inert gate) when the
295
+ * declaration cannot be read as a number. Every comparison against an unread
296
+ * number answers false, which would fail even the strongest password instead
297
+ * of leaving the gate off, so the unreadable declaration is confined to its
298
+ * own Value the way an unreadable scale is.
299
+ */
300
+ get #minScore() {
301
+ return Number.isFinite(this.minScoreValue) ? this.minScoreValue : 0;
302
+ }
303
+ /** The externally supplied score when one stands, else the heuristic's. */
304
+ #currentScore(max) {
305
+ if (this.#externalScore !== null) {
306
+ return Math.min(max, Math.max(0, Math.round(this.#externalScore)));
120
307
  }
121
- this.dispatch("change", {
122
- detail: { score, level: label, max, meetsMin: score > 0 && score >= this.minScoreValue }
123
- });
124
- if (this.#announceId !== null) this.#timers.clear(this.#announceId);
125
- this.#announceId = this.#timers.set(() => {
126
- this.#writeLabel(label);
127
- this.#announceId = null;
128
- }, _PasswordStrengthController.#announceDelay);
308
+ return this.#score(this.hasInputTarget ? this.inputTarget.value : "", max);
129
309
  }
130
310
  /** Syncs the meter target's ARIA value attributes (`0..levels.length`). */
131
311
  #reflectMeter(score, max) {
@@ -139,25 +319,27 @@ var PasswordStrengthController = class _PasswordStrengthController extends Contr
139
319
  * empty), the `data-below-min` hook when the score is under `minScore`, and the
140
320
  * `0–1` fill the consumer's CSS turns into the bar width.
141
321
  */
142
- #reflectRoot(score, max) {
143
- const band = this.#band(score, max);
322
+ #reflectRoot(score, max, band, meetsMin) {
144
323
  this.#toggle("data-strength", band, band.length > 0);
145
- this.#toggle("data-below-min", "true", score > 0 && score < this.minScoreValue);
146
- const ratio = max > 0 ? score / max : 0;
147
- this.element.style.setProperty("--stimeo--password-strength", String(ratio));
324
+ this.#toggle("data-below-min", "true", score > 0 && !meetsMin);
325
+ this.element.style.setProperty("--stimeo--password-strength", String(score / max));
148
326
  }
149
- #writeLabel(label) {
150
- if (this.hasLabelTarget) this.labelTarget.textContent = label;
327
+ /** Writes the visible readout only when its text actually changed. */
328
+ #writeLabel(text) {
329
+ if (!this.hasLabelTarget || this.labelTarget.textContent === text) return;
330
+ this.labelTarget.textContent = text;
151
331
  }
152
332
  /**
153
333
  * Locale-independent styling band (one of {@link STRENGTH_BANDS}) for `score`
154
- * out of `max`. Empty input → `""`. Quantizes the `score/max` ratio into the
155
- * four fixed bands, so a non-default level count still maps onto a stable hook.
334
+ * out of `max`. Empty input → `""`. Maps the position within the declared scale
335
+ * onto the position within the fixed bands, so both ends stay anchored on any
336
+ * level count: the weakest score is always `weak` and the strongest `strong`,
337
+ * and only the middle collapses when the scales differ in size.
156
338
  */
157
339
  #band(score, max) {
158
- if (score <= 0 || max <= 0) return "";
159
- const index = Math.ceil(score / max * STRENGTH_BANDS.length) - 1;
160
- return STRENGTH_BANDS[Math.min(STRENGTH_BANDS.length - 1, Math.max(0, index))] ?? "";
340
+ if (score <= 0) return "";
341
+ const index = Math.round((score - 1) / (max - 1) * (STRENGTH_BANDS.length - 1));
342
+ return STRENGTH_BANDS[index] ?? STRENGTH_BANDS[0];
161
343
  }
162
344
  /** Sets `name` to `value` when `on`, else removes it (value/presence data hook). */
163
345
  #toggle(name, value, on) {
@@ -167,6 +349,11 @@ var PasswordStrengthController = class _PasswordStrengthController extends Contr
167
349
  this.element.removeAttribute(name);
168
350
  }
169
351
  }
352
+ /** The declared level labels, or the defaults when the scale cannot order. */
353
+ #readLevels() {
354
+ const declared = parseStringList(this.levelsValue, DEFAULT_LEVELS);
355
+ return declared.length >= MIN_LEVELS ? declared : [...DEFAULT_LEVELS];
356
+ }
170
357
  /**
171
358
  * Lightweight zero-dependency strength heuristic returning an integer in
172
359
  * `[0, max]` (`max` = number of levels). Empty input is `0` (no level); any
@@ -176,7 +363,7 @@ var PasswordStrengthController = class _PasswordStrengthController extends Contr
176
363
  * alone cannot mask trivial repetition.
177
364
  */
178
365
  #score(password, max) {
179
- if (password.length === 0 || max === 0) return 0;
366
+ if (password.length === 0) return 0;
180
367
  let points = 0;
181
368
  for (const milestone of LENGTH_MILESTONES) {
182
369
  if (password.length >= milestone) points += 1;
@@ -186,6 +373,64 @@ var PasswordStrengthController = class _PasswordStrengthController extends Contr
186
373
  const bucketed = Math.round(points / MAX_POINTS * max);
187
374
  return Math.min(max, Math.max(1, bucketed));
188
375
  }
376
+ /** Debounces one i18n-neutral message for a level transition into the announcer. */
377
+ #scheduleAnnouncement(reading) {
378
+ this.#cancelAnnouncement();
379
+ if (reading.score === 0) {
380
+ this.#announcedLevel = null;
381
+ return;
382
+ }
383
+ if (reading.level === this.#announcedLevel) return;
384
+ const message = fillTemplate(this.announceTextValue, {
385
+ level: reading.level,
386
+ score: reading.score,
387
+ max: reading.max,
388
+ band: reading.band
389
+ });
390
+ if (message.trim().length === 0) return;
391
+ this.#announceId = this.#timers.set(() => {
392
+ this.#announcedLevel = reading.level;
393
+ announce(message);
394
+ this.#announceId = null;
395
+ }, _PasswordStrengthController.#announceDelay);
396
+ }
397
+ /** Cancels the one outstanding message without touching visible output. */
398
+ #cancelAnnouncement() {
399
+ if (this.#announceId !== null) this.#timers.clear(this.#announceId);
400
+ this.#announceId = null;
401
+ }
402
+ /**
403
+ * Returns every output derived from the field value to its pristine form.
404
+ *
405
+ * The value itself is not carried in a Turbo snapshot, so a band, a fill or a
406
+ * readout left behind would describe a password the restored page no longer
407
+ * holds. The pass is silent: `connect()` derives the state again from whatever
408
+ * the restored field contains.
409
+ */
410
+ #rewindForCache() {
411
+ this.#cancelAnnouncement();
412
+ this.#announcedLevel = null;
413
+ this.#externalScore = null;
414
+ this.#lastDetail = null;
415
+ this.#reflectMeter(0, this.#levels.length);
416
+ this.#toggle("data-strength", "", false);
417
+ this.#toggle("data-below-min", "true", false);
418
+ this.element.style.removeProperty("--stimeo--password-strength");
419
+ this.#writeLabel("");
420
+ }
421
+ /** Selects the public event state from the richer internal reading. */
422
+ #detail(reading) {
423
+ return {
424
+ score: reading.score,
425
+ level: reading.level,
426
+ max: reading.max,
427
+ meetsMin: reading.meetsMin
428
+ };
429
+ }
430
+ /** Compares exactly the state carried by `change` and `reconcile`. */
431
+ #detailsDiffer(left, right) {
432
+ return left.score !== right.score || left.level !== right.level || left.max !== right.max || left.meetsMin !== right.meetsMin;
433
+ }
189
434
  };
190
435
 
191
436
  export { PasswordStrengthController };
@@ -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 * `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
+ {"version":3,"sources":["../../src/utils/announce.ts","../../src/utils/before_cache_reset.ts","../../src/utils/coerce.ts","../../src/utils/microtask_coalescer.ts","../../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 * Sends one message to the page's shared `stimeo--announcer`.\n *\n * A component that has to reach assistive tech does not carry a live region of its\n * own: a region only announces what changes *after* assistive tech already knows\n * about it, which a region that appears (or is un-hidden) with its message cannot\n * satisfy. The one region that can is the announcer sitting in the page from the\n * start, so state changes are handed to it as an event and it does the reading.\n *\n * The event goes to `window` because the announcer is usually a sibling high in the\n * document rather than an ancestor of the component dispatching it.\n *\n * Wording comes from the consumer — the library ships no English strings — so an\n * empty message is silently dropped and nothing is announced.\n *\n * @example\n * ```ts\n * announce(this.announceTextValue, { assertive: false });\n * ```\n */\nexport function announce(message: string, options: { assertive?: boolean } = {}): void {\n const text = message.trim();\n if (text.length === 0) return;\n window.dispatchEvent(\n new CustomEvent(\"stimeo--announcer:announce\", {\n detail: { message: text, assertive: options.assertive === true },\n }),\n );\n}\n\n/**\n * Fills `{name}` placeholders in an announcement template from `values`.\n *\n * The same substitution the value-text templates use, so a consumer writes\n * `\"{percent}% complete\"` in one attribute and gets the same rules everywhere. A\n * placeholder with no matching entry is left as authored rather than blanked, which\n * keeps a typo visible instead of silently swallowing the word.\n */\nexport function fillTemplate(template: string, values: Record<string, string | number>): string {\n return template.replace(/\\{([a-zA-Z][a-zA-Z0-9]*)\\}/g, (match, name: string) => {\n const replacement = values[name];\n return replacement === undefined ? match : String(replacement);\n });\n}\n","/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way.\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","/**\n * Numeric coercion for controllers that accept a number through a Value, an action\n * param, or an event detail.\n *\n * Stimulus already coerces numeric action params to numbers, but a value can also\n * arrive as a string — via a `*:set` CustomEvent `detail`, or an action param\n * whose attribute does not look numeric. Centralizing the parse keeps `setValue`\n * tolerant of either form while rejecting anything that is not a finite number.\n */\n\n/**\n * Coerces `raw` to a finite number, or returns `null` when it is absent, empty,\n * or not parseable. Empty strings are treated as \"no value\" rather than `0`, so a\n * stray blank param cannot silently reset the value.\n */\nexport function toFiniteNumber(raw: number | string | null | undefined): number | null {\n if (raw === null || raw === undefined || raw === \"\") return null;\n const value = typeof raw === \"number\" ? raw : Number(raw);\n return Number.isFinite(value) ? value : null;\n}\n","/**\n * Collapses many Stimulus lifecycle callbacks from one DOM mutation into a\n * single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element and\n * `<name>ValueChanged` once per changed attribute. Replacing a list of N options\n * or morphing several render Values therefore delivers N callbacks — but the\n * useful unit of work is \"reconcile against the resulting declarative input\",\n * once, after the batch has settled. Every controller with reconcilable targets\n * or render Values needs the same shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers initial target and Value callbacks\n * ahead of `connect()`. Reconciling there would compute output against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\n }\n}\n","/**\n * 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 { announce, fillTemplate } from \"../utils/announce\";\nimport { BeforeCacheReset } from \"../utils/before_cache_reset\";\nimport { toFiniteNumber } from \"../utils/coerce\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\nimport { parseStringList } from \"../utils/string_list\";\n\n/**\n * Event shape {@link PasswordStrengthController.setScore} accepts: an action param\n * `score` or a `detail.score`. Both are typed `number | string` because, while\n * Stimulus coerces numeric action params to numbers, a `CustomEvent` (or a\n * non-numeric-looking param) may carry a string.\n */\ntype SetScoreEvent = Event & {\n params?: { score?: number | string };\n detail?: { score?: number | string };\n};\n\n/** One scoring pass: the level it lands on plus everything drawn from it. */\ninterface StrengthReading {\n readonly score: number;\n readonly level: string;\n readonly max: number;\n readonly meetsMin: boolean;\n readonly band: string;\n}\n\n/** The public derived state `change` and `reconcile` both carry. */\ntype PasswordStrengthDetail = Pick<StrengthReading, \"score\" | \"level\" | \"max\" | \"meetsMin\">;\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 * Fewest labels an ordered scale can carry. One label orders nothing and none\n * leaves the meter a zero-width range, so a shorter declaration resolves to\n * {@link DEFAULT_LEVELS} — which also makes `max >= 2` an invariant every\n * consumer of the scale can rely on.\n */\nconst MIN_LEVELS = 2;\n\n/**\n * Headless password-strength behavior: scores the field with a lightweight\n * zero-dependency heuristic and drives a meter plus a visible level readout. 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 * data-stimeo--password-strength-announce-text-value=\"Password strength: {level}\">\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-label=\"Password strength\" aria-valuemin=\"0\" aria-valuemax=\"4\"></div>\n * <span id=\"pw\" data-stimeo--password-strength-target=\"label\"></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` and `reconcile` dispatch `{ 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. Every visible and non-text output\n * updates on the keystroke that caused it, so the readout never trails the meter.\n *\n * Assistive notification is opt-in and i18n-neutral: `announceText` accepts\n * `{level}`, `{score}`, `{max}` and `{band}`, and one settled message is handed to\n * the page's shared `stimeo--announcer` after a short debounce. Only a level the\n * reader has not already heard is sent, so typing inside one level stays quiet.\n * The label target is plain visible output and is not itself a live region.\n *\n * The score is a pure function of the field value (no module-scope state), so\n * `connect()` re-evaluates idempotently; the initial reflection never dispatches\n * or announces. Runtime Value and target changes repaint on a microtask and\n * dispatch `reconcile` only when the public derived state actually moves. Because\n * the field value is not part of a Turbo snapshot, everything derived from it is\n * rewound on `turbo:before-cache` — silently, since `connect()` derives it again\n * after a restore. The estimator is intentionally not a dictionary/zxcvbn-grade\n * one (kept zero-dep); a consumer that needs a stronger one computes the score\n * itself and hands it over through {@link setScore}.\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 announceText: { type: String, default: \"\" },\n };\n static actions = [\"evaluate\", \"setScore\"] as const;\n static events = [\"change\", \"reconcile\"] 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 declare announceTextValue: string;\n\n /** Delay (ms) before one settled level is sent to the shared announcer. */\n static readonly #announceDelay = 200;\n\n readonly #timers = new SafeTimeout();\n readonly #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());\n readonly #repaint = new MicrotaskCoalescer(() => this.#reconcile());\n #levels: string[] = [...DEFAULT_LEVELS];\n #announceId: number | null = null;\n #announcedLevel: string | null = null;\n #externalScore: number | null = null;\n #lastDetail: PasswordStrengthDetail | null = null;\n\n /** Reflects the current DOM state and opens the reconciliation window. */\n override connect(): void {\n this.#repaint.activate();\n this.#beforeCache.activate();\n // Reflect the current value synchronously (no event, no announcement): an\n // autofilled or cache-restored field shows the right strength without\n // queuing a screen-reader message.\n this.#lastDetail = this.#detail(this.#render());\n }\n\n /** Releases the reconciliation window, the cache subscription and the debounce. */\n override disconnect(): void {\n this.#repaint.cancel();\n this.#beforeCache.deactivate();\n this.#cancelAnnouncement();\n this.#announcedLevel = null;\n this.#externalScore = null;\n this.#lastDetail = null;\n }\n\n /** Re-evaluates strength from the input. Bound via `data-action` (`input`). */\n evaluate(): void {\n this.#externalScore = null;\n this.#commit();\n }\n\n /**\n * Adopts a score computed outside the built-in heuristic, clamped into the\n * declared scale. An unreadable value leaves the current score standing, and\n * the next `evaluate` hands scoring back to the heuristic.\n */\n setScore(event: SetScoreEvent): void {\n const next = toFiniteNumber(event.params?.score ?? event.detail?.score);\n if (next === null) return;\n this.#externalScore = next;\n this.#commit();\n }\n\n /** Re-reads the scale when application code or a Turbo morph changes `levels`. */\n levelsValueChanged(): void {\n this.#levels = this.#readLevels();\n this.#repaint.schedule();\n }\n\n /** Repaints when application code or a Turbo morph changes `minScore`. */\n minScoreValueChanged(): void {\n this.#repaint.schedule();\n }\n\n /** Repaints when application code or a Turbo morph changes `announceText`. */\n announceTextValueChanged(): void {\n this.#repaint.schedule();\n }\n\n /** Scores the field added or replaced at runtime. */\n inputTargetConnected(): void {\n this.#repaint.schedule();\n }\n\n /** Falls back to the pristine state after the scored field is removed. */\n inputTargetDisconnected(): void {\n this.#repaint.schedule();\n }\n\n /** Syncs a meter added or replaced at runtime, which carries no value yet. */\n meterTargetConnected(): void {\n this.#repaint.schedule();\n }\n\n /** Repaints the remaining output after a meter is removed. */\n meterTargetDisconnected(): void {\n this.#repaint.schedule();\n }\n\n /** Fills a readout added or replaced at runtime. */\n labelTargetConnected(): void {\n this.#repaint.schedule();\n }\n\n /** Repaints the remaining output after a readout is removed. */\n labelTargetDisconnected(): void {\n this.#repaint.schedule();\n }\n\n /** Reflects one confirmed scoring pass and offers its level to the reader. */\n #commit(): void {\n const reading = this.#render();\n this.#lastDetail = this.#detail(reading);\n this.dispatch(\"change\", { detail: this.#lastDetail });\n this.#scheduleAnnouncement(reading);\n }\n\n /** Repaints one mutation batch and reports a changed controller-derived state. */\n #reconcile(): void {\n const previous = this.#lastDetail;\n // An edit inside the debounce window has already earned an announcement.\n // Reconciliation never originates one, but it must not carry a stale one\n // through either: the pending message is built from the level and the\n // template that held when it was queued, and both can move here. It is\n // retargeted at the settled reading so the reader hears what ends up shown.\n const owed = this.#announceId !== null;\n this.#cancelAnnouncement();\n const reading = this.#render();\n const detail = this.#detail(reading);\n this.#lastDetail = detail;\n // A blank readout has no level left to have been heard, here as much as on\n // the keystroke that empties the field, so the next level typed is news.\n if (reading.score === 0) this.#announcedLevel = null;\n if (owed) this.#scheduleAnnouncement(reading);\n if (previous && this.#detailsDiffer(previous, detail)) {\n this.dispatch(\"reconcile\", { detail });\n }\n }\n\n /**\n * Synchronizes the meter ARIA, the root state hooks, the fill custom property\n * and the visible level readout.\n *\n * @stimeoRenderRoot\n */\n #render(): StrengthReading {\n const max = this.#levels.length;\n const score = this.#currentScore(max);\n const level = this.#levels[score - 1] ?? \"\";\n const meetsMin = score > 0 && score >= this.#minScore;\n const band = this.#band(score, max);\n\n this.#reflectMeter(score, max);\n this.#reflectRoot(score, max, band, meetsMin);\n this.#writeLabel(level);\n return { score, level, max, meetsMin, band };\n }\n\n /**\n * The declared minimum, or the Value's default (`0`, an inert gate) when the\n * declaration cannot be read as a number. Every comparison against an unread\n * number answers false, which would fail even the strongest password instead\n * of leaving the gate off, so the unreadable declaration is confined to its\n * own Value the way an unreadable scale is.\n */\n get #minScore(): number {\n return Number.isFinite(this.minScoreValue) ? this.minScoreValue : 0;\n }\n\n /** The externally supplied score when one stands, else the heuristic's. */\n #currentScore(max: number): number {\n if (this.#externalScore !== null) {\n return Math.min(max, Math.max(0, Math.round(this.#externalScore)));\n }\n return this.#score(this.hasInputTarget ? this.inputTarget.value : \"\", max);\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, band: string, meetsMin: boolean): void {\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. The hook mirrors the event's `meetsMin`,\n // so it only marks a *non-empty* password under the threshold. `minScore`\n // defaults to 0, leaving the hook inert until set positive.\n this.#toggle(\"data-below-min\", \"true\", score > 0 && !meetsMin);\n this.element.style.setProperty(\"--stimeo--password-strength\", String(score / max));\n }\n\n /** Writes the visible readout only when its text actually changed. */\n #writeLabel(text: string): void {\n if (!this.hasLabelTarget || this.labelTarget.textContent === text) return;\n this.labelTarget.textContent = text;\n }\n\n /**\n * Locale-independent styling band (one of {@link STRENGTH_BANDS}) for `score`\n * out of `max`. Empty input → `\"\"`. Maps the position within the declared scale\n * onto the position within the fixed bands, so both ends stay anchored on any\n * level count: the weakest score is always `weak` and the strongest `strong`,\n * and only the middle collapses when the scales differ in size.\n */\n #band(score: number, max: number): string {\n if (score <= 0) return \"\";\n const index = Math.round(((score - 1) / (max - 1)) * (STRENGTH_BANDS.length - 1));\n // A scored password always lands inside the band list; an unplaceable one\n // takes the weakest rather than claiming strength it was not measured to have.\n return STRENGTH_BANDS[index] ?? STRENGTH_BANDS[0];\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 /** The declared level labels, or the defaults when the scale cannot order. */\n #readLevels(): string[] {\n const declared = parseStringList(this.levelsValue, DEFAULT_LEVELS);\n return declared.length >= MIN_LEVELS ? declared : [...DEFAULT_LEVELS];\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) 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 /** Debounces one i18n-neutral message for a level transition into the announcer. */\n #scheduleAnnouncement(reading: StrengthReading): void {\n this.#cancelAnnouncement();\n if (reading.score === 0) {\n // An emptied field has no level to read, and the next one typed is news.\n this.#announcedLevel = null;\n return;\n }\n if (reading.level === this.#announcedLevel) return;\n\n const message = fillTemplate(this.announceTextValue, {\n level: reading.level,\n score: reading.score,\n max: reading.max,\n band: reading.band,\n });\n if (message.trim().length === 0) return;\n\n this.#announceId = this.#timers.set(() => {\n this.#announcedLevel = reading.level;\n announce(message);\n this.#announceId = null;\n }, PasswordStrengthController.#announceDelay);\n }\n\n /** Cancels the one outstanding message without touching visible output. */\n #cancelAnnouncement(): void {\n if (this.#announceId !== null) this.#timers.clear(this.#announceId);\n this.#announceId = null;\n }\n\n /**\n * Returns every output derived from the field value to its pristine form.\n *\n * The value itself is not carried in a Turbo snapshot, so a band, a fill or a\n * readout left behind would describe a password the restored page no longer\n * holds. The pass is silent: `connect()` derives the state again from whatever\n * the restored field contains.\n */\n #rewindForCache(): void {\n this.#cancelAnnouncement();\n this.#announcedLevel = null;\n this.#externalScore = null;\n this.#lastDetail = null;\n this.#reflectMeter(0, this.#levels.length);\n this.#toggle(\"data-strength\", \"\", false);\n this.#toggle(\"data-below-min\", \"true\", false);\n this.element.style.removeProperty(\"--stimeo--password-strength\");\n this.#writeLabel(\"\");\n }\n\n /** Selects the public event state from the richer internal reading. */\n #detail(reading: StrengthReading): PasswordStrengthDetail {\n return {\n score: reading.score,\n level: reading.level,\n max: reading.max,\n meetsMin: reading.meetsMin,\n };\n }\n\n /** Compares exactly the state carried by `change` and `reconcile`. */\n #detailsDiffer(left: PasswordStrengthDetail, right: PasswordStrengthDetail): boolean {\n return (\n left.score !== right.score ||\n left.level !== right.level ||\n left.max !== right.max ||\n left.meetsMin !== right.meetsMin\n );\n }\n}\n"]}
@@ -4,8 +4,9 @@ import { Controller } from '@hotwired/stimulus';
4
4
  * Headless **pointer-drag primitive**: a normalized pointer/touch/mouse drag
5
5
  * lifecycle with a built-in keyboard alternative. It deliberately does NOT decide
6
6
  * what a drag *means* (reorder, dismiss, resize…) — it only emits a clean,
7
- * accessibility-friendly drag signal that higher-level behaviors (sortable,
8
- * swipe-actions, bottom-sheet, split-pane…) compose from. No dedicated APG
7
+ * accessibility-friendly drag signal that higher-level behaviors compose from —
8
+ * `stimeo--sortable` is one, and reordering, dismissal, and resizing are the usual
9
+ * meanings a consumer layers on. No dedicated APG
9
10
  * pattern exists for drag-and-drop; the keyboard model below is the accepted
10
11
  * alternative-input technique (WCAG 2.1.1 / 2.5.7 Dragging Movements). Core
11
12
  * (zero dependencies).
@@ -74,6 +75,7 @@ declare class PointerDragController extends Controller<HTMLElement> {
74
75
  default: boolean;
75
76
  };
76
77
  };
78
+ static actions: readonly ["reset"];
77
79
  static events: readonly ["start", "move", "end", "cancel"];
78
80
  readonly handleTargets: HTMLElement[];
79
81
  readonly hasHandleTarget: boolean;
@@ -84,11 +86,27 @@ declare class PointerDragController extends Controller<HTMLElement> {
84
86
  followValue: boolean;
85
87
  connect(): void;
86
88
  disconnect(): void;
87
- /** Prepares handles added at runtime (touch-action + focusability). */
89
+ /**
90
+ * Returns the element to its origin: drops the committed follow offset and the
91
+ * inline `translate` that carries it.
92
+ *
93
+ * In follow mode the inline `translate` belongs to this controller, and the
94
+ * committed offset lives in a field the DOM cannot reach — so a consumer that
95
+ * wants the element back at the start, or that has written a position of its
96
+ * own, needs this to make the two agree again. An in-flight drag is cancelled
97
+ * first, or its deltas would land on top of the offset just cleared.
98
+ */
99
+ reset(): void;
88
100
  handleTargetConnected(handle: HTMLElement): void;
89
101
  handleTargetDisconnected(handle: HTMLElement): void;
90
- /** Re-derives the handles' touch-action when the axis changes. */
91
- axisValueChanged(): void;
102
+ /**
103
+ * Re-derives the handles' touch-action when the axis changes.
104
+ *
105
+ * Two layers, like the `tabindex` loan: the marker says the value was once
106
+ * ours, and the current inline value says it still is. A consumer that wrote
107
+ * its own `touch-action` after connect keeps it.
108
+ */
109
+ axisValueChanged(_value: string, previousValue: string | undefined): void;
92
110
  /** Cancels any in-flight session when the controller is disabled mid-drag. */
93
111
  disabledValueChanged(): void;
94
112
  }