stimeo-ui 0.16.0 → 0.18.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 (117) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +136 -0
  3. data/README.md +27 -11
  4. data/dist/cable/index.js +384 -449
  5. data/dist/controllers/accordion_controller.js +174 -55
  6. data/dist/controllers/alert_dialog_controller.js +903 -235
  7. data/dist/controllers/announcer_controller.js +238 -205
  8. data/dist/controllers/aspect_ratio_controller.js +0 -10
  9. data/dist/controllers/auto_submit_controller.js +414 -145
  10. data/dist/controllers/avatar_controller.js +185 -107
  11. data/dist/controllers/breadcrumb_controller.js +0 -130
  12. data/dist/controllers/bulk_select_controller.js +282 -58
  13. data/dist/controllers/calendar_controller.js +228 -260
  14. data/dist/controllers/carousel_controller.js +536 -436
  15. data/dist/controllers/character_counter_controller.js +493 -207
  16. data/dist/controllers/checkbox_controller.js +38 -74
  17. data/dist/controllers/clipboard_controller.js +261 -149
  18. data/dist/controllers/collapsible_controller.js +304 -120
  19. data/dist/controllers/color_picker_controller.js +178 -146
  20. data/dist/controllers/combobox_controller.js +522 -156
  21. data/dist/controllers/command_palette_controller.js +1156 -395
  22. data/dist/controllers/conditional_fields_controller.js +300 -171
  23. data/dist/controllers/confirm_controller.js +980 -262
  24. data/dist/controllers/context_menu_controller.js +346 -153
  25. data/dist/controllers/count_up_controller.js +73 -21
  26. data/dist/controllers/countdown_controller.js +281 -158
  27. data/dist/controllers/currency_input_controller.js +575 -225
  28. data/dist/controllers/data_grid_controller.js +248 -214
  29. data/dist/controllers/date_range_picker_controller.js +338 -235
  30. data/dist/controllers/dialog_controller.js +899 -237
  31. data/dist/controllers/direct_upload_controller.js +97 -167
  32. data/dist/controllers/dirty_form_controller.js +72 -69
  33. data/dist/controllers/dismissible_controller.js +196 -24
  34. data/dist/controllers/drawer_controller.js +974 -355
  35. data/dist/controllers/dropdown_controller.js +339 -96
  36. data/dist/controllers/editable_controller.js +315 -88
  37. data/dist/controllers/empty_state_controller.js +79 -58
  38. data/dist/controllers/file_dropzone_controller.js +322 -211
  39. data/dist/controllers/filter_controller.js +77 -42
  40. data/dist/controllers/flash_controller.js +238 -347
  41. data/dist/controllers/focus_controller.js +679 -240
  42. data/dist/controllers/form_field_controller.js +156 -132
  43. data/dist/controllers/form_validation_controller.js +31 -97
  44. data/dist/controllers/frame_loading_controller.js +275 -217
  45. data/dist/controllers/highlight_controller.js +107 -75
  46. data/dist/controllers/hover_card_controller.js +327 -130
  47. data/dist/controllers/idle_controller.js +271 -101
  48. data/dist/controllers/input_mask_controller.js +483 -97
  49. data/dist/controllers/intersection_controller.js +150 -106
  50. data/dist/controllers/lazy_frame_controller.js +24 -68
  51. data/dist/controllers/listbox_controller.js +523 -240
  52. data/dist/controllers/local_time_controller.js +102 -77
  53. data/dist/controllers/masonry_controller.js +80 -96
  54. data/dist/controllers/menu_controller.js +302 -188
  55. data/dist/controllers/menubar_controller.js +232 -345
  56. data/dist/controllers/meter_controller.js +417 -87
  57. data/dist/controllers/multi_select_controller.js +785 -393
  58. data/dist/controllers/navigation_menu_controller.js +190 -250
  59. data/dist/controllers/nested_form_controller.js +396 -192
  60. data/dist/controllers/network_status_controller.js +268 -62
  61. data/dist/controllers/number_input_controller.js +610 -303
  62. data/dist/controllers/optimistic_controller.js +45 -87
  63. data/dist/controllers/otp_controller.js +658 -255
  64. data/dist/controllers/overflow_indicator_controller.js +122 -113
  65. data/dist/controllers/overflow_menu_controller.js +470 -295
  66. data/dist/controllers/pagination_controller.js +332 -123
  67. data/dist/controllers/password_reveal_controller.js +392 -121
  68. data/dist/controllers/password_strength_controller.js +111 -158
  69. data/dist/controllers/persist_controller.js +165 -111
  70. data/dist/controllers/pointer_drag_controller.js +434 -188
  71. data/dist/controllers/popover_controller.js +269 -113
  72. data/dist/controllers/portal_controller.js +215 -76
  73. data/dist/controllers/preview_guard_controller.js +243 -116
  74. data/dist/controllers/progress_controller.js +391 -78
  75. data/dist/controllers/radio_group_controller.js +278 -211
  76. data/dist/controllers/range_slider_controller.js +220 -113
  77. data/dist/controllers/rating_controller.js +458 -211
  78. data/dist/controllers/read_more_controller.js +138 -63
  79. data/dist/controllers/reading_progress_controller.js +166 -137
  80. data/dist/controllers/relative_time_controller.js +211 -112
  81. data/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +103 -38
  82. data/dist/controllers/resizable_controller.js +299 -150
  83. data/dist/controllers/roving_controller.js +49 -88
  84. data/dist/controllers/scroll_area_controller.js +490 -246
  85. data/dist/controllers/scroll_restore_controller.js +14 -55
  86. data/dist/controllers/scroll_visibility_controller.js +432 -149
  87. data/dist/controllers/scrollspy_controller.js +193 -248
  88. data/dist/controllers/separator_controller.js +285 -142
  89. data/dist/controllers/sidebar_controller.js +1069 -404
  90. data/dist/controllers/skeleton_controller.js +259 -101
  91. data/dist/controllers/slider_controller.js +187 -86
  92. data/dist/controllers/smart_sticky_header_controller.js +173 -66
  93. data/dist/controllers/sortable_controller.js +26 -125
  94. data/dist/controllers/spinner_controller.js +288 -173
  95. data/dist/controllers/step_indicator_controller.js +105 -46
  96. data/dist/controllers/stepper_controller.js +120 -73
  97. data/dist/controllers/stick_to_bottom_controller.js +275 -140
  98. data/dist/controllers/sticky_observer_controller.js +220 -49
  99. data/dist/controllers/submit_once_controller.js +376 -227
  100. data/dist/controllers/switch_controller.js +201 -84
  101. data/dist/controllers/tabs_controller.js +41 -30
  102. data/dist/controllers/tags_input_controller.js +293 -173
  103. data/dist/controllers/textarea_autosize_controller.js +108 -74
  104. data/dist/controllers/theme_controller.js +140 -116
  105. data/dist/controllers/time_picker_controller.js +331 -127
  106. data/dist/controllers/toast_controller.js +244 -324
  107. data/dist/controllers/toggle_group_controller.js +252 -191
  108. data/dist/controllers/toolbar_controller.js +219 -120
  109. data/dist/controllers/tooltip_controller.js +158 -124
  110. data/dist/controllers/transition_controller.js +123 -119
  111. data/dist/controllers/tree_view_controller.js +283 -369
  112. data/dist/index.js +8573 -10410
  113. data/dist/positioning/index.js +154 -47
  114. data/lib/generators/stimeo/install/install_generator.rb +7 -2
  115. data/lib/generators/stimeo/install/templates/stimeo.js +19 -6
  116. data/lib/stimeo/ui/version.rb +1 -1
  117. metadata +3 -3
@@ -1,8 +1,12 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/password_strength_controller.ts
3
+ function fillTemplate(template, values) {
4
+ return template.replace(/\{([a-zA-Z][a-zA-Z0-9]*)\}/g, (match, name) => {
5
+ const replacement = Object.hasOwn(values, name) ? values[name] : void 0;
6
+ return replacement === void 0 ? match : String(replacement);
7
+ });
8
+ }
4
9
 
5
- // src/utils/announce.ts
6
10
  function announce(message, options = {}) {
7
11
  const text = message.trim();
8
12
  if (text.length === 0) return;
@@ -12,70 +16,63 @@ function announce(message, options = {}) {
12
16
  })
13
17
  );
14
18
  }
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
19
 
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
- };
20
+ var NUMBER_BOUNDS = {
21
+ nonNegative: { finite: true, min: 0 }};
22
+ function matchesNumberBounds(value, bounds) {
23
+ if (!Number.isFinite(value)) {
24
+ const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
25
+ if (direction === null) return false;
26
+ if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
27
+ }
28
+ if (bounds.min !== void 0 && value < bounds.min) return false;
29
+ if (bounds.max !== void 0 && value > bounds.max) return false;
30
+ if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
31
+ if (bounds.integer && !Number.isInteger(value)) return false;
32
+ if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
33
+ return true;
34
+ }
50
35
 
51
- // src/utils/coerce.ts
52
36
  function toFiniteNumber(raw) {
53
- if (raw === null || raw === void 0 || raw === "") return null;
54
- const value = typeof raw === "number" ? raw : Number(raw);
37
+ if (typeof raw !== "number" && typeof raw !== "string") return null;
38
+ if (typeof raw === "string" && raw.trim().length === 0) return null;
39
+ const value = Number(raw);
55
40
  return Number.isFinite(value) ? value : null;
56
41
  }
42
+ function readNumber(raw, fallback, bounds) {
43
+ return matchesNumberBounds(raw, bounds) ? raw : fallback;
44
+ }
45
+
46
+ var ListenerSet = class {
47
+ #abort = new AbortController();
48
+ add(target, type, handler, options) {
49
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
50
+ }
51
+ dispose() {
52
+ this.#abort.abort();
53
+ this.#abort = new AbortController();
54
+ }
55
+ };
57
56
 
58
- // src/utils/microtask_coalescer.ts
59
57
  var MicrotaskCoalescer = class {
60
58
  #run;
61
59
  #queued = false;
62
60
  #active = false;
63
61
  #generation = 0;
64
- /** @param run - the single reconciliation pass, invoked at most once per batch. */
65
62
  constructor(run) {
66
63
  this.#run = run;
67
64
  }
68
- /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
69
65
  activate() {
70
66
  this.#active = true;
71
67
  }
72
- /** Closes the window and drops any pending pass; call from `disconnect()`. */
73
68
  cancel() {
74
69
  this.#active = false;
75
70
  this.#queued = false;
76
71
  this.#generation += 1;
77
72
  }
78
- /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
73
+ get pending() {
74
+ return this.#queued;
75
+ }
79
76
  schedule() {
80
77
  if (!this.#active || this.#queued) return;
81
78
  this.#queued = true;
@@ -88,43 +85,76 @@ var MicrotaskCoalescer = class {
88
85
  }
89
86
  };
90
87
 
91
- // src/utils/safe_timeout.ts
88
+ var MorphRenderWatcher = class {
89
+ #listeners = new ListenerSet();
90
+ #pass;
91
+ #element = null;
92
+ #onMorph = () => this.#pass.schedule();
93
+ constructor(run) {
94
+ this.#pass = new MicrotaskCoalescer(run);
95
+ }
96
+ observe(element) {
97
+ if (this.#element === element) return;
98
+ this.disconnect();
99
+ this.#element = element;
100
+ this.#pass.activate();
101
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
102
+ }
103
+ schedule() {
104
+ this.#pass.schedule();
105
+ }
106
+ get pending() {
107
+ return this.#pass.pending;
108
+ }
109
+ disconnect() {
110
+ this.#listeners.dispose();
111
+ this.#pass.cancel();
112
+ this.#element = null;
113
+ }
114
+ };
115
+
116
+ var NumberValueReader = class {
117
+ #lastRejected = /* @__PURE__ */ new Map();
118
+ read(owner, name, raw, fallback, bounds) {
119
+ const resolved = readNumber(raw, fallback, bounds);
120
+ if (matchesNumberBounds(raw, bounds)) {
121
+ this.#lastRejected.delete(name);
122
+ return resolved;
123
+ }
124
+ const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
125
+ const literal = owner.element.getAttribute(attribute);
126
+ if (literal === null) {
127
+ this.#lastRejected.delete(name);
128
+ return resolved;
129
+ }
130
+ if (this.#lastRejected.get(name) !== literal) {
131
+ this.#lastRejected.set(name, literal);
132
+ console.warn(
133
+ `Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
134
+ );
135
+ }
136
+ return resolved;
137
+ }
138
+ };
139
+
92
140
  var TimerRegistry = class {
93
- /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
94
141
  ids = /* @__PURE__ */ new Set();
95
- /**
96
- * Cancels a single tracked timer.
97
- *
98
- * No-ops if the id is unknown (already cleared, fired, or never owned by this
99
- * registry), so callers can clear defensively without guarding.
100
- */
101
142
  clear(id) {
102
143
  if (this.ids.delete(id)) {
103
144
  this.cancel(id);
104
145
  }
105
146
  }
106
- /**
107
- * Cancels every tracked timer. Call this from a controller's `disconnect()`
108
- * to guarantee no timer outlives the element.
109
- */
110
147
  clearAll() {
111
148
  for (const id of this.ids) {
112
149
  this.cancel(id);
113
150
  }
114
151
  this.ids.clear();
115
152
  }
116
- /** Number of timers currently tracked (pending). */
117
153
  get size() {
118
154
  return this.ids.size;
119
155
  }
120
156
  };
121
157
  var SafeTimeout = class extends TimerRegistry {
122
- /**
123
- * Schedules `callback` after `delay` ms and returns the timer id.
124
- *
125
- * The id is removed from the registry automatically when the timeout fires,
126
- * so {@link TimerRegistry.size | size} reflects only still-pending timers.
127
- */
128
158
  set(callback, delay) {
129
159
  const id = this.schedule(() => {
130
160
  this.ids.delete(id);
@@ -141,7 +171,6 @@ var SafeTimeout = class extends TimerRegistry {
141
171
  }
142
172
  };
143
173
 
144
- // src/utils/string_list.ts
145
174
  function parseStringList(raw, fallback = []) {
146
175
  const text = raw.trim();
147
176
  if (text.length === 0) return [...fallback];
@@ -155,7 +184,6 @@ function parseStringList(raw, fallback = []) {
155
184
  return parsed.filter((entry) => typeof entry === "string");
156
185
  }
157
186
 
158
- // src/controllers/password_strength_controller.ts
159
187
  var CLASS_PATTERNS = [/[a-z]/, /[A-Z]/, /[0-9]/, /[^A-Za-z0-9]/];
160
188
  var DEFAULT_LEVELS = ["weak", "fair", "good", "strong"];
161
189
  var LENGTH_MILESTONES = [8, 12, 16];
@@ -163,107 +191,85 @@ var MAX_POINTS = LENGTH_MILESTONES.length + (CLASS_PATTERNS.length - 1);
163
191
  var STRENGTH_BANDS = ["weak", "fair", "good", "strong"];
164
192
  var MIN_LEVELS = 2;
165
193
  var PasswordStrengthController = class _PasswordStrengthController extends Controller {
194
+ #numbers = new NumberValueReader();
166
195
  static targets = ["input", "meter", "label"];
167
196
  static values = {
168
197
  minScore: { type: Number, default: 0 },
169
- // A JSON list read through `parseStringList` rather than Stimulus's `Array`
170
- // type: that reader throws out of the value observer before any callback
171
- // runs, so one malformed attribute would stop the meter connecting.
172
198
  levels: { type: String, default: "" },
173
199
  announceText: { type: String, default: "" }
174
200
  };
201
+ static valueConstraints = {
202
+ minScore: NUMBER_BOUNDS.nonNegative
203
+ };
175
204
  static actions = ["evaluate", "setScore"];
176
205
  static events = ["change", "reconcile"];
177
- /** Delay (ms) before one settled level is sent to the shared announcer. */
178
206
  static #announceDelay = 200;
179
207
  #timers = new SafeTimeout();
180
- #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
181
- #repaint = new MicrotaskCoalescer(() => this.#reconcile());
208
+ #repaint = new MorphRenderWatcher(() => this.#reconcile());
182
209
  #levels = [...DEFAULT_LEVELS];
183
210
  #announceId = null;
184
211
  #announcedLevel = null;
185
212
  #externalScore = null;
186
213
  #lastDetail = null;
187
- /** Reflects the current DOM state and opens the reconciliation window. */
188
214
  connect() {
189
- this.#repaint.activate();
190
- this.#beforeCache.activate();
215
+ this.#repaint.observe(this.element);
191
216
  this.#lastDetail = this.#detail(this.#render());
217
+ this.#repaint.schedule();
192
218
  }
193
- /** Releases the reconciliation window, the cache subscription and the debounce. */
194
219
  disconnect() {
195
- this.#repaint.cancel();
196
- this.#beforeCache.deactivate();
220
+ this.#repaint.disconnect();
197
221
  this.#cancelAnnouncement();
198
222
  this.#announcedLevel = null;
199
223
  this.#externalScore = null;
200
224
  this.#lastDetail = null;
201
225
  }
202
- /** Re-evaluates strength from the input. Bound via `data-action` (`input`). */
203
226
  evaluate() {
204
227
  this.#externalScore = null;
205
228
  this.#commit();
206
229
  }
207
- /**
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.
211
- */
212
230
  setScore(event) {
213
231
  const next = toFiniteNumber(event.params?.score ?? event.detail?.score);
214
232
  if (next === null) return;
215
233
  this.#externalScore = next;
216
234
  this.#commit();
217
235
  }
218
- /** Re-reads the scale when application code or a Turbo morph changes `levels`. */
219
236
  levelsValueChanged() {
220
237
  this.#levels = this.#readLevels();
221
238
  this.#repaint.schedule();
222
239
  }
223
- /** Repaints when application code or a Turbo morph changes `minScore`. */
224
240
  minScoreValueChanged() {
225
241
  this.#repaint.schedule();
226
242
  }
227
- /** Repaints when application code or a Turbo morph changes `announceText`. */
228
243
  announceTextValueChanged() {
229
244
  this.#repaint.schedule();
230
245
  }
231
- /** Scores the field added or replaced at runtime. */
232
246
  inputTargetConnected() {
233
247
  this.#repaint.schedule();
234
248
  }
235
- /** Falls back to the pristine state after the scored field is removed. */
236
249
  inputTargetDisconnected() {
237
250
  this.#repaint.schedule();
238
251
  }
239
- /** Syncs a meter added or replaced at runtime, which carries no value yet. */
240
252
  meterTargetConnected() {
241
253
  this.#repaint.schedule();
242
254
  }
243
- /** Repaints the remaining output after a meter is removed. */
244
255
  meterTargetDisconnected() {
245
256
  this.#repaint.schedule();
246
257
  }
247
- /** Fills a readout added or replaced at runtime. */
248
258
  labelTargetConnected() {
249
259
  this.#repaint.schedule();
250
260
  }
251
- /** Repaints the remaining output after a readout is removed. */
252
261
  labelTargetDisconnected() {
253
262
  this.#repaint.schedule();
254
263
  }
255
- /** Reflects one confirmed scoring pass and offers its level to the reader. */
256
264
  #commit() {
257
265
  const reading = this.#render();
258
266
  this.#lastDetail = this.#detail(reading);
259
267
  this.dispatch("change", { detail: this.#lastDetail });
260
268
  this.#scheduleAnnouncement(reading);
261
269
  }
262
- /** Repaints one mutation batch and reports a changed controller-derived state. */
263
270
  #reconcile() {
264
271
  const previous = this.#lastDetail;
265
272
  const owed = this.#announceId !== null;
266
- this.#cancelAnnouncement();
267
273
  const reading = this.#render();
268
274
  const detail = this.#detail(reading);
269
275
  this.#lastDetail = detail;
@@ -273,12 +279,6 @@ var PasswordStrengthController = class _PasswordStrengthController extends Contr
273
279
  this.dispatch("reconcile", { detail });
274
280
  }
275
281
  }
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
282
  #render() {
283
283
  const max = this.#levels.length;
284
284
  const score = this.#currentScore(max);
@@ -290,58 +290,35 @@ var PasswordStrengthController = class _PasswordStrengthController extends Contr
290
290
  this.#writeLabel(level);
291
291
  return { score, level, max, meetsMin, band };
292
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
293
  get #minScore() {
301
- return Number.isFinite(this.minScoreValue) ? this.minScoreValue : 0;
294
+ return this.#safeMinScore;
302
295
  }
303
- /** The externally supplied score when one stands, else the heuristic's. */
304
296
  #currentScore(max) {
305
297
  if (this.#externalScore !== null) {
306
298
  return Math.min(max, Math.max(0, Math.round(this.#externalScore)));
307
299
  }
308
300
  return this.#score(this.hasInputTarget ? this.inputTarget.value : "", max);
309
301
  }
310
- /** Syncs the meter target's ARIA value attributes (`0..levels.length`). */
311
302
  #reflectMeter(score, max) {
312
303
  if (!this.hasMeterTarget) return;
313
304
  this.meterTarget.setAttribute("aria-valuemin", "0");
314
305
  this.meterTarget.setAttribute("aria-valuemax", String(max));
315
306
  this.meterTarget.setAttribute("aria-valuenow", String(score));
316
307
  }
317
- /**
318
- * Reflects the level onto the root: the stable `data-strength` band (absent when
319
- * empty), the `data-below-min` hook when the score is under `minScore`, and the
320
- * `0–1` fill the consumer's CSS turns into the bar width.
321
- */
322
308
  #reflectRoot(score, max, band, meetsMin) {
323
309
  this.#toggle("data-strength", band, band.length > 0);
324
310
  this.#toggle("data-below-min", "true", score > 0 && !meetsMin);
325
311
  this.element.style.setProperty("--stimeo--password-strength", String(score / max));
326
312
  }
327
- /** Writes the visible readout only when its text actually changed. */
328
313
  #writeLabel(text) {
329
314
  if (!this.hasLabelTarget || this.labelTarget.textContent === text) return;
330
315
  this.labelTarget.textContent = text;
331
316
  }
332
- /**
333
- * Locale-independent styling band (one of {@link STRENGTH_BANDS}) for `score`
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.
338
- */
339
317
  #band(score, max) {
340
318
  if (score <= 0) return "";
341
319
  const index = Math.round((score - 1) / (max - 1) * (STRENGTH_BANDS.length - 1));
342
320
  return STRENGTH_BANDS[index] ?? STRENGTH_BANDS[0];
343
321
  }
344
- /** Sets `name` to `value` when `on`, else removes it (value/presence data hook). */
345
322
  #toggle(name, value, on) {
346
323
  if (on) {
347
324
  this.element.setAttribute(name, value);
@@ -349,19 +326,10 @@ var PasswordStrengthController = class _PasswordStrengthController extends Contr
349
326
  this.element.removeAttribute(name);
350
327
  }
351
328
  }
352
- /** The declared level labels, or the defaults when the scale cannot order. */
353
329
  #readLevels() {
354
330
  const declared = parseStringList(this.levelsValue, DEFAULT_LEVELS);
355
331
  return declared.length >= MIN_LEVELS ? declared : [...DEFAULT_LEVELS];
356
332
  }
357
- /**
358
- * Lightweight zero-dependency strength heuristic returning an integer in
359
- * `[0, max]` (`max` = number of levels). Empty input is `0` (no level); any
360
- * non-empty password is at least `1`. Points accrue from length milestones and
361
- * character-class variety, then bucket into the level scale. A tiny alphabet
362
- * (≤ 2 distinct characters, e.g. "aaaa") is capped as the weakest, so length
363
- * alone cannot mask trivial repetition.
364
- */
365
333
  #score(password, max) {
366
334
  if (password.length === 0) return 0;
367
335
  let points = 0;
@@ -373,7 +341,6 @@ var PasswordStrengthController = class _PasswordStrengthController extends Contr
373
341
  const bucketed = Math.round(points / MAX_POINTS * max);
374
342
  return Math.min(max, Math.max(1, bucketed));
375
343
  }
376
- /** Debounces one i18n-neutral message for a level transition into the announcer. */
377
344
  #scheduleAnnouncement(reading) {
378
345
  this.#cancelAnnouncement();
379
346
  if (reading.score === 0) {
@@ -394,31 +361,10 @@ var PasswordStrengthController = class _PasswordStrengthController extends Contr
394
361
  this.#announceId = null;
395
362
  }, _PasswordStrengthController.#announceDelay);
396
363
  }
397
- /** Cancels the one outstanding message without touching visible output. */
398
364
  #cancelAnnouncement() {
399
365
  if (this.#announceId !== null) this.#timers.clear(this.#announceId);
400
366
  this.#announceId = null;
401
367
  }
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
368
  #detail(reading) {
423
369
  return {
424
370
  score: reading.score,
@@ -427,12 +373,19 @@ var PasswordStrengthController = class _PasswordStrengthController extends Contr
427
373
  meetsMin: reading.meetsMin
428
374
  };
429
375
  }
430
- /** Compares exactly the state carried by `change` and `reconcile`. */
431
376
  #detailsDiffer(left, right) {
432
377
  return left.score !== right.score || left.level !== right.level || left.max !== right.max || left.meetsMin !== right.meetsMin;
433
378
  }
379
+ get #safeMinScore() {
380
+ return this.#numbers.read(
381
+ this,
382
+ "minScore",
383
+ this.minScoreValue,
384
+ _PasswordStrengthController.values.minScore.default,
385
+ _PasswordStrengthController.valueConstraints.minScore
386
+ );
387
+ }
434
388
  };
435
389
 
436
390
  export { PasswordStrengthController };
437
- //# sourceMappingURL=password_strength_controller.js.map
438
391
  //# sourceMappingURL=password_strength_controller.js.map