stimeo-ui 0.15.0 → 0.17.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 (115) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +209 -0
  3. data/README.md +128 -1
  4. data/dist/cable/index.js +392 -417
  5. data/dist/controllers/accordion_controller.js +228 -28
  6. data/dist/controllers/alert_dialog_controller.js +862 -221
  7. data/dist/controllers/announcer_controller.js +135 -191
  8. data/dist/controllers/aspect_ratio_controller.js +0 -10
  9. data/dist/controllers/auto_submit_controller.js +295 -110
  10. data/dist/controllers/avatar_controller.js +172 -106
  11. data/dist/controllers/breadcrumb_controller.js +0 -129
  12. data/dist/controllers/bulk_select_controller.js +264 -53
  13. data/dist/controllers/calendar_controller.js +346 -180
  14. data/dist/controllers/carousel_controller.js +587 -300
  15. data/dist/controllers/character_counter_controller.js +271 -143
  16. data/dist/controllers/checkbox_controller.js +77 -75
  17. data/dist/controllers/clipboard_controller.js +187 -108
  18. data/dist/controllers/collapsible_controller.js +374 -108
  19. data/dist/controllers/color_picker_controller.js +183 -127
  20. data/dist/controllers/combobox_controller.js +426 -109
  21. data/dist/controllers/command_palette_controller.js +994 -350
  22. data/dist/controllers/conditional_fields_controller.js +212 -125
  23. data/dist/controllers/confirm_controller.js +939 -245
  24. data/dist/controllers/context_menu_controller.js +275 -127
  25. data/dist/controllers/count_up_controller.js +73 -21
  26. data/dist/controllers/countdown_controller.js +235 -97
  27. data/dist/controllers/currency_input_controller.js +241 -118
  28. data/dist/controllers/data_grid_controller.js +229 -135
  29. data/dist/controllers/date_range_picker_controller.js +359 -165
  30. data/dist/controllers/dialog_controller.js +887 -225
  31. data/dist/controllers/direct_upload_controller.js +80 -142
  32. data/dist/controllers/dirty_form_controller.js +26 -47
  33. data/dist/controllers/dismissible_controller.js +175 -20
  34. data/dist/controllers/drawer_controller.js +963 -336
  35. data/dist/controllers/dropdown_controller.js +342 -85
  36. data/dist/controllers/editable_controller.js +182 -58
  37. data/dist/controllers/empty_state_controller.js +67 -53
  38. data/dist/controllers/file_dropzone_controller.js +426 -229
  39. data/dist/controllers/filter_controller.js +78 -36
  40. data/dist/controllers/flash_controller.js +328 -211
  41. data/dist/controllers/focus_controller.js +654 -239
  42. data/dist/controllers/form_field_controller.js +153 -136
  43. data/dist/controllers/form_validation_controller.js +29 -96
  44. data/dist/controllers/frame_loading_controller.js +263 -198
  45. data/dist/controllers/highlight_controller.js +119 -70
  46. data/dist/controllers/hover_card_controller.js +335 -118
  47. data/dist/controllers/idle_controller.js +316 -65
  48. data/dist/controllers/input_mask_controller.js +116 -61
  49. data/dist/controllers/intersection_controller.js +147 -103
  50. data/dist/controllers/lazy_frame_controller.js +9 -56
  51. data/dist/controllers/listbox_controller.js +413 -153
  52. data/dist/controllers/local_time_controller.js +54 -64
  53. data/dist/controllers/masonry_controller.js +70 -95
  54. data/dist/controllers/menu_controller.js +254 -167
  55. data/dist/controllers/menubar_controller.js +206 -331
  56. data/dist/controllers/meter_controller.js +159 -71
  57. data/dist/controllers/multi_select_controller.js +797 -388
  58. data/dist/controllers/navigation_menu_controller.js +156 -231
  59. data/dist/controllers/nested_form_controller.js +373 -174
  60. data/dist/controllers/network_status_controller.js +241 -45
  61. data/dist/controllers/number_input_controller.js +387 -227
  62. data/dist/controllers/optimistic_controller.js +28 -75
  63. data/dist/controllers/otp_controller.js +344 -196
  64. data/dist/controllers/overflow_indicator_controller.js +167 -93
  65. data/dist/controllers/overflow_menu_controller.js +408 -277
  66. data/dist/controllers/pagination_controller.js +342 -120
  67. data/dist/controllers/password_reveal_controller.js +424 -99
  68. data/dist/controllers/password_strength_controller.js +99 -153
  69. data/dist/controllers/persist_controller.js +252 -96
  70. data/dist/controllers/pointer_drag_controller.js +263 -164
  71. data/dist/controllers/popover_controller.js +267 -94
  72. data/dist/controllers/portal_controller.js +209 -72
  73. data/dist/controllers/preview_guard_controller.js +243 -112
  74. data/dist/controllers/progress_controller.js +132 -59
  75. data/dist/controllers/radio_group_controller.js +138 -133
  76. data/dist/controllers/range_slider_controller.js +245 -112
  77. data/dist/controllers/rating_controller.js +429 -194
  78. data/dist/controllers/read_more_controller.js +185 -47
  79. data/dist/controllers/reading_progress_controller.js +147 -135
  80. data/dist/controllers/relative_time_controller.js +145 -110
  81. data/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +97 -38
  82. data/dist/controllers/resizable_controller.js +294 -118
  83. data/dist/controllers/roving_controller.js +39 -84
  84. data/dist/controllers/scroll_area_controller.js +428 -221
  85. data/dist/controllers/scroll_restore_controller.js +0 -53
  86. data/dist/controllers/scroll_visibility_controller.js +395 -146
  87. data/dist/controllers/scrollspy_controller.js +181 -236
  88. data/dist/controllers/separator_controller.js +288 -132
  89. data/dist/controllers/sidebar_controller.js +1071 -375
  90. data/dist/controllers/skeleton_controller.js +248 -92
  91. data/dist/controllers/slider_controller.js +211 -96
  92. data/dist/controllers/smart_sticky_header_controller.js +163 -54
  93. data/dist/controllers/sortable_controller.js +13 -110
  94. data/dist/controllers/spinner_controller.js +277 -161
  95. data/dist/controllers/step_indicator_controller.js +107 -53
  96. data/dist/controllers/stepper_controller.js +171 -48
  97. data/dist/controllers/stick_to_bottom_controller.js +310 -116
  98. data/dist/controllers/sticky_observer_controller.js +214 -49
  99. data/dist/controllers/submit_once_controller.js +294 -192
  100. data/dist/controllers/switch_controller.js +107 -43
  101. data/dist/controllers/tabs_controller.js +50 -25
  102. data/dist/controllers/tags_input_controller.js +290 -149
  103. data/dist/controllers/textarea_autosize_controller.js +71 -58
  104. data/dist/controllers/theme_controller.js +148 -80
  105. data/dist/controllers/time_picker_controller.js +162 -81
  106. data/dist/controllers/toast_controller.js +365 -193
  107. data/dist/controllers/toggle_group_controller.js +194 -99
  108. data/dist/controllers/toolbar_controller.js +92 -90
  109. data/dist/controllers/tooltip_controller.js +185 -125
  110. data/dist/controllers/transition_controller.js +123 -115
  111. data/dist/controllers/tree_view_controller.js +248 -290
  112. data/dist/index.js +8649 -8572
  113. data/dist/positioning/index.js +148 -45
  114. data/lib/stimeo/ui/version.rb +1 -1
  115. metadata +3 -3
@@ -1,8 +1,5 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/bulk_select_controller.ts
4
-
5
- // src/utils/announce.ts
6
3
  function announce(message, options = {}) {
7
4
  const text = message.trim();
8
5
  if (text.length === 0) return;
@@ -19,27 +16,180 @@ function fillTemplate(template, values) {
19
16
  });
20
17
  }
21
18
 
22
- // src/utils/microtask_coalescer.ts
19
+ function sharedRegistry(key, create) {
20
+ const symbol = Symbol.for(key);
21
+ const scope = globalThis;
22
+ const existing = scope[symbol];
23
+ if (existing !== void 0) return existing;
24
+ const registry = create();
25
+ Object.defineProperty(globalThis, symbol, { value: registry });
26
+ return registry;
27
+ }
28
+
29
+ function isAttributeValue(value) {
30
+ return value === null || typeof value === "string";
31
+ }
32
+ function readLeaseRecord(element, record) {
33
+ try {
34
+ const parsed = JSON.parse(element.getAttribute(record) ?? "null");
35
+ if (!Array.isArray(parsed)) return void 0;
36
+ const [original, written, ...rest] = parsed;
37
+ const linked = rest[0] !== false;
38
+ const beneath = linked ? rest : rest.slice(1);
39
+ if (!beneath.every((under) => typeof under === "string")) return void 0;
40
+ return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
41
+ } catch {
42
+ return void 0;
43
+ }
44
+ }
45
+ var AttributeLease = class _AttributeLease {
46
+ static #registry = sharedRegistry(
47
+ "stimeo-ui.attribute-lease.registry.v1",
48
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
49
+ );
50
+ #attribute;
51
+ #record;
52
+ #base;
53
+ #held = /* @__PURE__ */ new Set();
54
+ constructor(attribute, owner, options = {}) {
55
+ this.#attribute = attribute;
56
+ this.#record = `data-${owner}-${attribute}-lease`;
57
+ this.#base = options.base === true;
58
+ const names = _AttributeLease.#registry.names;
59
+ names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
60
+ }
61
+ write(element, value) {
62
+ const stack = this.#stack(element);
63
+ const hold = this.#take(stack) ?? this.#acquire(element, stack);
64
+ this.#held.add(element);
65
+ const shows = !this.#covered(element, stack, hold);
66
+ hold.written = value;
67
+ this.#mark(element, stack);
68
+ if (shows) this.#reflect(element, value);
69
+ }
70
+ return(element) {
71
+ const held = this.#held.delete(element);
72
+ const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
73
+ const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
74
+ const hold = this.#take(stack);
75
+ if (!hold) return false;
76
+ const carries = element.getAttribute(this.#attribute) === hold.written;
77
+ const owns = carries && !this.#covered(element, stack, hold);
78
+ const index = stack.indexOf(hold);
79
+ stack.splice(index, 1);
80
+ const above = stack[index];
81
+ if (above) above.linked &&= hold.linked;
82
+ this.#mark(element, stack);
83
+ if (!owns) return true;
84
+ const below = hold.linked ? stack[index - 1] : void 0;
85
+ this.#reflect(element, below ? below.written : hold.original);
86
+ return true;
87
+ }
88
+ returnAll() {
89
+ for (const element of Array.from(this.#held)) this.return(element);
90
+ }
91
+ #stack(element) {
92
+ const stacks = _AttributeLease.#registry.stacks;
93
+ const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
94
+ stacks.set(element, byAttribute);
95
+ const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
96
+ byAttribute.set(this.#attribute, stack);
97
+ return stack;
98
+ }
99
+ #seed(element) {
100
+ const found = [];
101
+ for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
102
+ const read = readLeaseRecord(element, record);
103
+ if (read) found.push({ ...read, record });
104
+ }
105
+ const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
106
+ return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
107
+ lease: null,
108
+ record,
109
+ written,
110
+ original,
111
+ linked: linked && index > 0
112
+ }));
113
+ }
114
+ #take(stack) {
115
+ const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
116
+ if (hold) hold.lease = this;
117
+ return hold;
118
+ }
119
+ #acquire(element, stack) {
120
+ const current = element.getAttribute(this.#attribute);
121
+ const top = stack.at(-1);
122
+ const bottom = stack[0];
123
+ const over = top?.written === current ? top : void 0;
124
+ const from = this.#base ? bottom : over;
125
+ const hold = {
126
+ lease: this,
127
+ record: this.#record,
128
+ written: current,
129
+ original: from ? from.original : current,
130
+ linked: !this.#base && over !== void 0
131
+ };
132
+ if (this.#base && bottom) {
133
+ bottom.linked = true;
134
+ stack.unshift(hold);
135
+ } else stack.push(hold);
136
+ return hold;
137
+ }
138
+ #covered(element, stack, hold) {
139
+ const current = element.getAttribute(this.#attribute);
140
+ return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
141
+ }
142
+ #mark(element, stack) {
143
+ const records = /* @__PURE__ */ new Map([[this.#record, null]]);
144
+ stack.forEach((hold, index) => {
145
+ const names = stack.slice(0, index).map((under) => under.record);
146
+ const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
147
+ const marker = hold.linked || beneath.length === 0 ? [] : [false];
148
+ records.set(
149
+ hold.record,
150
+ hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
151
+ );
152
+ });
153
+ for (const [name, recorded] of records) {
154
+ if (element.getAttribute(name) === recorded) continue;
155
+ if (recorded === null) element.removeAttribute(name);
156
+ else element.setAttribute(name, recorded);
157
+ }
158
+ }
159
+ #reflect(element, value) {
160
+ if (element.getAttribute(this.#attribute) === value) return;
161
+ if (value === null) element.removeAttribute(this.#attribute);
162
+ else element.setAttribute(this.#attribute, value);
163
+ }
164
+ };
165
+
166
+ var ListenerSet = class {
167
+ #abort = new AbortController();
168
+ add(target, type, handler, options) {
169
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
170
+ }
171
+ dispose() {
172
+ this.#abort.abort();
173
+ this.#abort = new AbortController();
174
+ }
175
+ };
176
+
23
177
  var MicrotaskCoalescer = class {
24
178
  #run;
25
179
  #queued = false;
26
180
  #active = false;
27
181
  #generation = 0;
28
- /** @param run - the single reconciliation pass, invoked at most once per batch. */
29
182
  constructor(run) {
30
183
  this.#run = run;
31
184
  }
32
- /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
33
185
  activate() {
34
186
  this.#active = true;
35
187
  }
36
- /** Closes the window and drops any pending pass; call from `disconnect()`. */
37
188
  cancel() {
38
189
  this.#active = false;
39
190
  this.#queued = false;
40
191
  this.#generation += 1;
41
192
  }
42
- /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
43
193
  schedule() {
44
194
  if (!this.#active || this.#queued) return;
45
195
  this.#queued = true;
@@ -52,26 +202,92 @@ var MicrotaskCoalescer = class {
52
202
  }
53
203
  };
54
204
 
55
- // src/controllers/bulk_select_controller.ts
56
- var BulkSelectController = class extends Controller {
205
+ var MorphRenderWatcher = class {
206
+ #listeners = new ListenerSet();
207
+ #pass;
208
+ #element = null;
209
+ #onMorph = () => this.#pass.schedule();
210
+ constructor(run) {
211
+ this.#pass = new MicrotaskCoalescer(run);
212
+ }
213
+ observe(element) {
214
+ if (this.#element === element) return;
215
+ this.disconnect();
216
+ this.#element = element;
217
+ this.#pass.activate();
218
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
219
+ }
220
+ schedule() {
221
+ this.#pass.schedule();
222
+ }
223
+ disconnect() {
224
+ this.#listeners.dispose();
225
+ this.#pass.cancel();
226
+ this.#element = null;
227
+ }
228
+ };
229
+
230
+ var NUMBER_BOUNDS = {
231
+ nonNegative: { finite: true, min: 0 }};
232
+ function matchesNumberBounds(value, bounds) {
233
+ if (!Number.isFinite(value)) {
234
+ const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
235
+ if (direction === null) return false;
236
+ if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
237
+ }
238
+ if (bounds.min !== void 0 && value < bounds.min) return false;
239
+ if (bounds.max !== void 0 && value > bounds.max) return false;
240
+ if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
241
+ if (bounds.integer && !Number.isInteger(value)) return false;
242
+ if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
243
+ return true;
244
+ }
245
+
246
+ function readNumber(raw, fallback, bounds) {
247
+ return matchesNumberBounds(raw, bounds) ? raw : fallback;
248
+ }
249
+
250
+ var NumberValueReader = class {
251
+ #lastRejected = /* @__PURE__ */ new Map();
252
+ read(owner, name, raw, fallback, bounds) {
253
+ const resolved = readNumber(raw, fallback, bounds);
254
+ if (matchesNumberBounds(raw, bounds)) {
255
+ this.#lastRejected.delete(name);
256
+ return resolved;
257
+ }
258
+ const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
259
+ const literal = owner.element.getAttribute(attribute);
260
+ if (literal === null) {
261
+ this.#lastRejected.delete(name);
262
+ return resolved;
263
+ }
264
+ if (this.#lastRejected.get(name) !== literal) {
265
+ this.#lastRejected.set(name, literal);
266
+ console.warn(
267
+ `Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
268
+ );
269
+ }
270
+ return resolved;
271
+ }
272
+ };
273
+
274
+ var BulkSelectController = class _BulkSelectController extends Controller {
275
+ #numbers = new NumberValueReader();
57
276
  static targets = ["all", "item", "bar", "count", "selectAllPages"];
58
277
  static values = {
59
278
  totalCount: { type: Number, default: 0 },
60
279
  announceText: { type: String, default: "" }
61
280
  };
281
+ static valueConstraints = {
282
+ totalCount: NUMBER_BOUNDS.nonNegative
283
+ };
62
284
  static actions = ["clear", "selectAllPages"];
63
285
  static events = ["change", "reconcile"];
64
- /** All-pages mode is a transient UI state, mirrored to `data-all-pages` so a
65
- * `connect()` over markup that already carries the attribute rehydrates the
66
- * mode — a morph, a Turbo Stream, a server that renders it back, or a restore
67
- * visit, whose cached snapshot carries the attribute too. */
68
286
  #allPagesMode = false;
69
- /** Last emitted figures, so a recompute reports only on a real change. */
70
287
  #lastCount = -1;
71
288
  #lastAllPages = false;
72
- /** Collapses every signal from one DOM or Value update into one repair. */
73
- #reconcile = new MicrotaskCoalescer(() => this.#reconcileNow());
74
- /** Delegated `change` handler covering the select-all box and every row. */
289
+ #reconcile = new MorphRenderWatcher(() => this.#reconcileNow());
290
+ #barHidden = new AttributeLease("hidden", this.identifier);
75
291
  #onChange = (event) => {
76
292
  const target = event.target;
77
293
  if (!target) return;
@@ -84,43 +300,45 @@ var BulkSelectController = class extends Controller {
84
300
  };
85
301
  connect() {
86
302
  this.#allPagesMode = this.element.dataset.allPages === "true";
87
- this.#reconcile.activate();
303
+ this.#reconcile.observe(this.element);
88
304
  this.element.addEventListener("change", this.#onChange);
89
305
  this.#recompute();
90
306
  }
91
307
  disconnect() {
92
308
  this.element.removeEventListener("change", this.#onChange);
93
- this.#reconcile.cancel();
309
+ this.#reconcile.disconnect();
94
310
  }
95
- /** Repairs the figures for a row that arrived at runtime. */
96
311
  itemTargetConnected() {
97
312
  this.#reconcile.schedule();
98
313
  }
99
- /** Repairs the figures after a row leaves, so a removed selection stops counting. */
100
314
  itemTargetDisconnected() {
101
315
  this.#reconcile.schedule();
102
316
  }
103
- /** Reflects the current selection onto a select-all box added at runtime. */
104
317
  allTargetConnected() {
105
318
  this.#reconcile.schedule();
106
319
  }
107
- /** Repairs the figures after the select-all box leaves. */
108
320
  allTargetDisconnected() {
109
321
  this.#reconcile.schedule();
110
322
  }
111
- /** Repaints the count for a total that changed at runtime, rejecting non-finite ones. */
323
+ barTargetConnected() {
324
+ this.#reconcile.schedule();
325
+ }
326
+ barTargetDisconnected(bar) {
327
+ if (!this.barTargets.includes(bar)) this.#barHidden.return(bar);
328
+ this.#reconcile.schedule();
329
+ }
330
+ countTargetConnected() {
331
+ this.#reconcile.schedule();
332
+ }
333
+ countTargetDisconnected() {
334
+ this.#reconcile.schedule();
335
+ }
112
336
  totalCountValueChanged() {
113
- if (!Number.isFinite(this.totalCountValue)) {
114
- this.totalCountValue = 0;
115
- return;
116
- }
117
337
  this.#reconcile.schedule();
118
338
  }
119
- /** Repaints so wording changed at runtime is used by the next announcement. */
120
339
  announceTextValueChanged() {
121
340
  this.#reconcile.schedule();
122
341
  }
123
- /** Clears every selection (rows + select-all) and exits all-pages mode. */
124
342
  clear() {
125
343
  for (const item of this.itemTargets) item.checked = false;
126
344
  if (this.hasAllTarget) {
@@ -130,23 +348,14 @@ var BulkSelectController = class extends Controller {
130
348
  this.#exitAllPages();
131
349
  this.#reportChange(this.#recompute());
132
350
  }
133
- /**
134
- * Enters "select all across pages" mode: the count shows `totalCount`, and every
135
- * row on this page is checked.
136
- *
137
- * The mode's claim is that the whole set is selected, so leaving a visible row
138
- * unchecked would put the page and the count in open disagreement.
139
- */
140
351
  selectAllPages() {
141
352
  this.#allPagesMode = true;
142
353
  this.#checkEveryRow();
143
354
  this.#reportChange(this.#recompute());
144
355
  }
145
- /** Marks every row on this page selected. */
146
356
  #checkEveryRow() {
147
357
  for (const item of this.itemTargets) item.checked = true;
148
358
  }
149
- /** Mirrors the select-all box to every row, then recomputes. */
150
359
  #applyAll() {
151
360
  if (!this.hasAllTarget) return;
152
361
  const { checked } = this.allTarget;
@@ -157,7 +366,6 @@ var BulkSelectController = class extends Controller {
157
366
  #exitAllPages() {
158
367
  this.#allPagesMode = false;
159
368
  }
160
- /** Repairs the derived state after the page moved rows or a render input. */
161
369
  #reconcileNow() {
162
370
  if (this.#allPagesMode) this.#checkEveryRow();
163
371
  const detail = this.#recompute();
@@ -165,23 +373,18 @@ var BulkSelectController = class extends Controller {
165
373
  this.dispatch("reconcile", { detail });
166
374
  this.#announce(detail);
167
375
  }
168
- /** Reports a selection the user moved. */
169
376
  #reportChange(detail) {
170
377
  if (!detail) return;
171
378
  this.dispatch("change", { detail });
172
379
  this.#announce(detail);
173
380
  }
174
- /** Hands the count to the shared announcer, worded by the consumer. */
175
381
  #announce(detail) {
176
382
  announce(fillTemplate(this.announceTextValue, { count: detail.count }));
177
383
  }
178
- /**
179
- * Recomputes the count, the select-all checked/indeterminate state, and the bar
180
- * visibility from the current DOM. Returns the figures when the emitted count or
181
- * all-pages flag actually moved, and `null` when they did not.
182
- *
183
- * @stimeoRenderRoot
184
- */
384
+ get #totalCount() {
385
+ const declared = this.#safeTotalCount;
386
+ return declared;
387
+ }
185
388
  #recompute() {
186
389
  const items = this.itemTargets;
187
390
  const total = items.length;
@@ -191,13 +394,13 @@ var BulkSelectController = class extends Controller {
191
394
  this.allTarget.checked = total > 0 && checked === total;
192
395
  this.allTarget.indeterminate = checked > 0 && checked < total;
193
396
  }
194
- const count = allPages ? this.totalCountValue : checked;
397
+ const count = allPages ? this.#totalCount : checked;
195
398
  const show = allPages || checked > 0;
196
399
  if (this.hasBarTarget) {
197
400
  if (!show && this.hasAllTarget && this.barTarget.contains(document.activeElement)) {
198
401
  this.allTarget.focus();
199
402
  }
200
- this.barTarget.hidden = !show;
403
+ this.#barHidden.write(this.barTarget, show ? null : "");
201
404
  }
202
405
  if (this.hasCountTarget) this.countTarget.textContent = String(count);
203
406
  this.element.setAttribute("data-selected-count", String(checked));
@@ -208,8 +411,16 @@ var BulkSelectController = class extends Controller {
208
411
  this.#lastAllPages = allPages;
209
412
  return changed ? { count, allPages } : null;
210
413
  }
414
+ get #safeTotalCount() {
415
+ return this.#numbers.read(
416
+ this,
417
+ "totalCount",
418
+ this.totalCountValue,
419
+ _BulkSelectController.values.totalCount.default,
420
+ _BulkSelectController.valueConstraints.totalCount
421
+ );
422
+ }
211
423
  };
212
424
 
213
425
  export { BulkSelectController };
214
- //# sourceMappingURL=bulk_select_controller.js.map
215
426
  //# sourceMappingURL=bulk_select_controller.js.map