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/local_time_controller.ts
4
-
5
- // src/utils/intl_format.ts
6
3
  var formatters = /* @__PURE__ */ new WeakMap();
7
4
  function intlFormatter(factory, locale, options, fallbackLocale) {
8
5
  const cache = cacheFor(factory);
@@ -31,32 +28,37 @@ function construct(factory, locale, options, fallbackLocale) {
31
28
  }
32
29
  }
33
30
 
34
- // src/utils/locale.ts
35
31
  function resolveLocale(element, declared = "") {
36
32
  return declared || element.closest("[lang]")?.getAttribute("lang") || void 0;
37
33
  }
38
34
 
39
- // src/utils/microtask_coalescer.ts
35
+ var ListenerSet = class {
36
+ #abort = new AbortController();
37
+ add(target, type, handler, options) {
38
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
39
+ }
40
+ dispose() {
41
+ this.#abort.abort();
42
+ this.#abort = new AbortController();
43
+ }
44
+ };
45
+
40
46
  var MicrotaskCoalescer = class {
41
47
  #run;
42
48
  #queued = false;
43
49
  #active = false;
44
50
  #generation = 0;
45
- /** @param run - the single reconciliation pass, invoked at most once per batch. */
46
51
  constructor(run) {
47
52
  this.#run = run;
48
53
  }
49
- /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
50
54
  activate() {
51
55
  this.#active = true;
52
56
  }
53
- /** Closes the window and drops any pending pass; call from `disconnect()`. */
54
57
  cancel() {
55
58
  this.#active = false;
56
59
  this.#queued = false;
57
60
  this.#generation += 1;
58
61
  }
59
- /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
60
62
  schedule() {
61
63
  if (!this.#active || this.#queued) return;
62
64
  this.#queued = true;
@@ -69,7 +71,31 @@ var MicrotaskCoalescer = class {
69
71
  }
70
72
  };
71
73
 
72
- // src/controllers/local_time_controller.ts
74
+ var MorphRenderWatcher = class {
75
+ #listeners = new ListenerSet();
76
+ #pass;
77
+ #element = null;
78
+ #onMorph = () => this.#pass.schedule();
79
+ constructor(run) {
80
+ this.#pass = new MicrotaskCoalescer(run);
81
+ }
82
+ observe(element) {
83
+ if (this.#element === element) return;
84
+ this.disconnect();
85
+ this.#element = element;
86
+ this.#pass.activate();
87
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
88
+ }
89
+ schedule() {
90
+ this.#pass.schedule();
91
+ }
92
+ disconnect() {
93
+ this.#listeners.dispose();
94
+ this.#pass.cancel();
95
+ this.#element = null;
96
+ }
97
+ };
98
+
73
99
  var STYLES = /* @__PURE__ */ new Set(["full", "long", "medium", "short"]);
74
100
  function toStyle(value) {
75
101
  return STYLES.has(value) ? value : void 0;
@@ -83,55 +109,48 @@ var LocalTimeController = class extends Controller {
83
109
  titleFormat: { type: String, default: "" }
84
110
  };
85
111
  static events = ["format"];
86
- /** Collapses a morph that swaps several render inputs at once into one repaint. */
87
- #resync = new MicrotaskCoalescer(() => this.#render());
88
- /**
89
- * Watches the one render input that is not a Value. Only `datetime` is filtered
90
- * in, so the text and `title` this controller writes cannot re-enter the pass.
91
- */
112
+ #resync = new MorphRenderWatcher(() => {
113
+ const datetimeChanged = this.#datetimeWatch.takeRecords().length > 0;
114
+ const notify = this.#formatPending || datetimeChanged;
115
+ this.#formatPending = false;
116
+ this.#render(notify);
117
+ });
118
+ #formatPending = false;
92
119
  #datetimeWatch = new MutationObserver(() => {
120
+ this.#formatPending = true;
93
121
  this.#resync.schedule();
94
122
  });
95
123
  connect() {
96
- this.#resync.activate();
124
+ this.#resync.observe(this.element);
97
125
  this.#datetimeWatch.observe(this.element, { attributeFilter: ["datetime"] });
126
+ this.#formatPending = false;
98
127
  this.#render();
99
128
  }
100
129
  disconnect() {
101
- this.#resync.cancel();
130
+ this.#resync.disconnect();
102
131
  this.#datetimeWatch.disconnect();
103
132
  }
104
- /** Repaints when application code (or a Turbo morph) changes `locale` at runtime. */
105
133
  localeValueChanged() {
134
+ this.#formatPending = true;
106
135
  this.#resync.schedule();
107
136
  }
108
- /** Repaints when application code (or a Turbo morph) changes `timeZone` at runtime. */
109
137
  timeZoneValueChanged() {
138
+ this.#formatPending = true;
110
139
  this.#resync.schedule();
111
140
  }
112
- /** Repaints when application code (or a Turbo morph) changes `dateStyle` at runtime. */
113
141
  dateStyleValueChanged() {
142
+ this.#formatPending = true;
114
143
  this.#resync.schedule();
115
144
  }
116
- /** Repaints when application code (or a Turbo morph) changes `timeStyle` at runtime. */
117
145
  timeStyleValueChanged() {
146
+ this.#formatPending = true;
118
147
  this.#resync.schedule();
119
148
  }
120
- /** Repaints when application code (or a Turbo morph) changes `titleFormat` at runtime. */
121
149
  titleFormatValueChanged() {
150
+ this.#formatPending = true;
122
151
  this.#resync.schedule();
123
152
  }
124
- /**
125
- * Formats the instant in `datetime` against the current Values and writes it out.
126
- *
127
- * The `format` event rides with every pass, including a repaint a morph triggers:
128
- * its condition is that formatting was applied, and a repaint applies it with a
129
- * new result. A pass that cannot format writes nothing and emits nothing, so the
130
- * authored absolute text stays as the fallback.
131
- *
132
- * @stimeoRenderRoot
133
- */
134
- #render() {
153
+ #render(notify = true) {
135
154
  const date = this.#parse();
136
155
  if (date === null) return;
137
156
  const formatted = this.#applyFormat(date, this.dateStyleValue, this.timeStyleValue);
@@ -139,31 +158,14 @@ var LocalTimeController = class extends Controller {
139
158
  this.element.textContent = formatted;
140
159
  const title = this.#title(date);
141
160
  if (title !== null) this.element.setAttribute("title", title);
142
- this.dispatch("format", { detail: { formatted } });
161
+ if (notify) this.dispatch("format", { detail: { formatted } });
143
162
  }
144
- /**
145
- * Parses the UTC `datetime` attribute into a {@link Date}, or `null`. Whitespace
146
- * around the attribute value is tolerated.
147
- */
148
163
  #parse() {
149
164
  const raw = this.element.getAttribute("datetime");
150
165
  if (!raw) return null;
151
166
  const ms = Date.parse(this.#asUtc(raw.trim()));
152
167
  return Number.isNaN(ms) ? null : new Date(ms);
153
168
  }
154
- /**
155
- * Reads a timezone-less date-time as UTC — the input contract of this
156
- * controller — since `Date.parse` would otherwise read `"2026-06-08T12:30:00"` in
157
- * the *runtime's* local zone, contradicting "the server emits UTC". Values that
158
- * already carry `Z` or a `±hh:mm` offset (and bare `YYYY-MM-DD` dates, already
159
- * parsed as UTC) are returned unchanged.
160
- *
161
- * HTML accepts a space where ISO 8601 wants `T`, and `Date.parse` of that form is
162
- * left to each engine, so a whole value shaped that way is normalized to the `T`
163
- * separator first. The pattern is anchored: a value trailing anything else — a
164
- * zone word such as `"2026-06-08 12:30:00 UTC"` — is handed to `Date.parse` as
165
- * authored instead of being turned into a string nothing can parse.
166
- */
167
169
  #asUtc(value) {
168
170
  const isoLike = value.replace(
169
171
  /^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)$/,
@@ -173,21 +175,10 @@ var LocalTimeController = class extends Controller {
173
175
  const hasZone = /(Z|[+-]\d{2}:?\d{2})$/.test(isoLike);
174
176
  return hasTime && !hasZone ? `${isoLike}Z` : isoLike;
175
177
  }
176
- /**
177
- * Builds the optional detailed `title`. `titleFormat` is an `Intl` style
178
- * keyword applied to *both* date and time; empty (the default) adds no title.
179
- */
180
178
  #title(date) {
181
179
  if (this.titleFormatValue.length === 0) return null;
182
180
  return this.#applyFormat(date, this.titleFormatValue, this.titleFormatValue);
183
181
  }
184
- /**
185
- * Formats `date` with `Intl.DateTimeFormat`, including each style only when it
186
- * is a valid keyword (so a consumer can show date-only or time-only by clearing
187
- * the other). Returns `null` when neither style is usable or the declaration
188
- * names a locale or time zone `Intl` rejects, so the caller can leave the
189
- * authored text untouched.
190
- */
191
182
  #applyFormat(date, dateStyle, timeStyle) {
192
183
  const options = {
193
184
  dateStyle: toStyle(dateStyle),
@@ -205,5 +196,4 @@ var LocalTimeController = class extends Controller {
205
196
  };
206
197
 
207
198
  export { LocalTimeController };
208
- //# sourceMappingURL=local_time_controller.js.map
209
199
  //# sourceMappingURL=local_time_controller.js.map
@@ -1,19 +1,14 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/masonry_controller.ts
4
-
5
- // src/utils/layout_observer.ts
6
3
  var LayoutObserver = class {
7
4
  #callback;
8
5
  #resizeObserverFactory;
9
6
  #resizeObserver = null;
10
7
  #observingViewport = false;
11
8
  #loadContainer = null;
12
- /** Stable bound handler so add/removeEventListener target the same reference. */
13
9
  #handleViewportResize = () => {
14
10
  this.#callback();
15
11
  };
16
- /** Stable bound handler for the capture-phase `load`; see {@link observeDescendantLoads}. */
17
12
  #handleDescendantLoad = () => {
18
13
  this.#callback();
19
14
  };
@@ -21,11 +16,6 @@ var LayoutObserver = class {
21
16
  this.#callback = callback;
22
17
  this.#resizeObserverFactory = options.resizeObserverFactory ?? (typeof ResizeObserver === "undefined" ? null : (cb) => new ResizeObserver(cb));
23
18
  }
24
- /**
25
- * Starts observing an element's size. Repeated calls observe additional
26
- * elements through the same shared observer. No-ops when no
27
- * `ResizeObserver` implementation is available.
28
- */
29
19
  observe(element) {
30
20
  if (!this.#resizeObserverFactory) return;
31
21
  if (!this.#resizeObserver) {
@@ -35,47 +25,28 @@ var LayoutObserver = class {
35
25
  }
36
26
  this.#resizeObserver.observe(element);
37
27
  }
38
- /** Stops observing a single element while leaving any others in place. */
39
28
  unobserve(element) {
40
29
  this.#resizeObserver?.unobserve(element);
41
30
  }
42
- /** Starts observing viewport resizes. Idempotent: the listener is added once. */
43
31
  observeViewport() {
44
32
  if (this.#observingViewport) return;
45
33
  this.#observingViewport = true;
46
34
  window.addEventListener("resize", this.#handleViewportResize);
47
35
  }
48
- /** Stops observing viewport resizes without affecting element observation. */
49
36
  unobserveViewport() {
50
37
  if (!this.#observingViewport) return;
51
38
  this.#observingViewport = false;
52
39
  window.removeEventListener("resize", this.#handleViewportResize);
53
40
  }
54
- /**
55
- * Starts reporting a `load` from anywhere inside `container` — an image or a
56
- * frame settling changes the box it sits in, and it measures as zero high until
57
- * then. `load` does not bubble, so the subscription is a capture-phase listener
58
- * on the container itself and nothing the caller spells.
59
- *
60
- * **One container at a time.** A further call moves the observation, so a widget
61
- * whose content element is swapped at runtime releases the element it let go by
62
- * naming the new one — there is no second place for the release to drift from.
63
- */
64
41
  observeDescendantLoads(container) {
65
42
  this.unobserveDescendantLoads();
66
43
  this.#loadContainer = container;
67
44
  container.addEventListener("load", this.#handleDescendantLoad, true);
68
45
  }
69
- /** Stops reporting descendant loads without affecting element or viewport observation. */
70
46
  unobserveDescendantLoads() {
71
47
  this.#loadContainer?.removeEventListener("load", this.#handleDescendantLoad, true);
72
48
  this.#loadContainer = null;
73
49
  }
74
- /**
75
- * Releases every observation: disconnects the {@link ResizeObserver} and removes
76
- * the viewport and descendant-load listeners. Safe to call multiple times. Call
77
- * this from a controller's `disconnect()`.
78
- */
79
50
  disconnect() {
80
51
  this.#resizeObserver?.disconnect();
81
52
  this.#resizeObserver = null;
@@ -84,27 +55,22 @@ var LayoutObserver = class {
84
55
  }
85
56
  };
86
57
 
87
- // src/utils/microtask_coalescer.ts
88
58
  var MicrotaskCoalescer = class {
89
59
  #run;
90
60
  #queued = false;
91
61
  #active = false;
92
62
  #generation = 0;
93
- /** @param run - the single reconciliation pass, invoked at most once per batch. */
94
63
  constructor(run) {
95
64
  this.#run = run;
96
65
  }
97
- /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
98
66
  activate() {
99
67
  this.#active = true;
100
68
  }
101
- /** Closes the window and drops any pending pass; call from `disconnect()`. */
102
69
  cancel() {
103
70
  this.#active = false;
104
71
  this.#queued = false;
105
72
  this.#generation += 1;
106
73
  }
107
- /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
108
74
  schedule() {
109
75
  if (!this.#active || this.#queued) return;
110
76
  this.#queued = true;
@@ -117,69 +83,87 @@ var MicrotaskCoalescer = class {
117
83
  }
118
84
  };
119
85
 
120
- // src/controllers/masonry_controller.ts
86
+ var NUMBER_BOUNDS = {
87
+ finite: { finite: true }};
88
+ function matchesNumberBounds(value, bounds) {
89
+ if (!Number.isFinite(value)) {
90
+ const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
91
+ if (direction === null) return false;
92
+ if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
93
+ }
94
+ if (bounds.min !== void 0 && value < bounds.min) return false;
95
+ if (bounds.max !== void 0 && value > bounds.max) return false;
96
+ if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
97
+ if (bounds.integer && !Number.isInteger(value)) return false;
98
+ if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
99
+ return true;
100
+ }
101
+
102
+ function readNumber(raw, fallback, bounds) {
103
+ return matchesNumberBounds(raw, bounds) ? raw : fallback;
104
+ }
105
+
106
+ var NumberValueReader = class {
107
+ #lastRejected = /* @__PURE__ */ new Map();
108
+ read(owner, name, raw, fallback, bounds) {
109
+ const resolved = readNumber(raw, fallback, bounds);
110
+ if (matchesNumberBounds(raw, bounds)) {
111
+ this.#lastRejected.delete(name);
112
+ return resolved;
113
+ }
114
+ const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
115
+ const literal = owner.element.getAttribute(attribute);
116
+ if (literal === null) {
117
+ this.#lastRejected.delete(name);
118
+ return resolved;
119
+ }
120
+ if (this.#lastRejected.get(name) !== literal) {
121
+ this.#lastRejected.set(name, literal);
122
+ console.warn(
123
+ `Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
124
+ );
125
+ }
126
+ return resolved;
127
+ }
128
+ };
129
+
121
130
  var COLUMNS_PROPERTY = "--stimeo--masonry-columns";
122
131
  var DEFAULT_MIN_COLUMN_WIDTH = 240;
123
132
  var DEFAULT_GAP = 16;
124
- function usableNumber(value, fallback) {
125
- return Number.isFinite(value) ? value : fallback;
126
- }
127
- var MasonryController = class extends Controller {
133
+ var MasonryController = class _MasonryController extends Controller {
134
+ #numbers = new NumberValueReader();
128
135
  static targets = ["item"];
129
136
  static values = {
130
137
  minColumnWidth: { type: Number, default: DEFAULT_MIN_COLUMN_WIDTH },
131
138
  gap: { type: Number, default: DEFAULT_GAP }
132
139
  };
140
+ static valueConstraints = {
141
+ minColumnWidth: NUMBER_BOUNDS.finite,
142
+ gap: NUMBER_BOUNDS.finite
143
+ };
133
144
  static events = ["layout"];
134
- /**
135
- * The declared numbers after validation, so the layout path never sees a value
136
- * it cannot compute with. Both are resolved once per declaration change rather
137
- * than on every pass.
138
- */
139
145
  #minColumnWidth = DEFAULT_MIN_COLUMN_WIDTH;
140
146
  #gap = DEFAULT_GAP;
141
- /**
142
- * Collapses every re-layout trigger of one DOM mutation into a single pass, and
143
- * refuses to run before `connect()` or after `disconnect()`.
144
- *
145
- * The triggers arrive in bursts — a resize stream, a morph that syncs several
146
- * attributes, a batch of rows — and each pass measures every item, so folding
147
- * them keeps the work proportional to the batch rather than to the events in it.
148
- */
149
147
  #reconcile = new MicrotaskCoalescer(() => this.#relayout());
150
- /** Items that left the target set and still carry the column hook. */
151
148
  #released = /* @__PURE__ */ new Set();
152
149
  #layout = new LayoutObserver(() => this.#reconcile.schedule());
153
150
  #mutationObserver = null;
154
- /** Last published column count, so `layout` fires only on real changes. */
155
151
  #lastColumns = 0;
156
- /** Resolves the declared column width once, falling back when it is unreadable. */
157
152
  minColumnWidthValueChanged() {
158
- this.#minColumnWidth = usableNumber(this.minColumnWidthValue, DEFAULT_MIN_COLUMN_WIDTH);
153
+ this.#minColumnWidth = this.#safeMinColumnWidth;
159
154
  this.#reconcile.schedule();
160
155
  }
161
- /** Resolves the declared gap once, falling back when it is unreadable. */
162
156
  gapValueChanged() {
163
- this.#gap = usableNumber(this.gapValue, DEFAULT_GAP);
157
+ this.#gap = this.#safeGap;
164
158
  this.#reconcile.schedule();
165
159
  }
166
- /** Packs an element that became an item without moving in the DOM. */
167
160
  itemTargetConnected() {
168
161
  this.#reconcile.schedule();
169
162
  }
170
- /**
171
- * Queues the column hook of an element that stopped being an item for removal.
172
- *
173
- * The removal is queued rather than immediate because teardown reports every
174
- * target as disconnected: doing it here would strip the whole grid just before
175
- * a Turbo snapshot is taken. The coalescer's `cancel` drops the queue
176
- * with the pass, so only a genuine target change reaches it.
177
- */
178
163
  itemTargetDisconnected(item) {
179
164
  this.#released.add(item);
180
165
  this.#reconcile.schedule();
181
166
  }
182
- /** Observes size/content changes and performs the first layout pass. */
183
167
  connect() {
184
168
  this.#layout.observe(this.element);
185
169
  this.#layout.observeViewport();
@@ -191,33 +175,13 @@ var MasonryController = class extends Controller {
191
175
  this.#relayout();
192
176
  this.#reconcile.activate();
193
177
  }
194
- /** Releases every observation so nothing fires after detach. */
195
178
  disconnect() {
196
179
  this.#reconcile.cancel();
197
- this.#released.clear();
198
180
  this.#layout.disconnect();
199
181
  this.#mutationObserver?.disconnect();
200
182
  this.#mutationObserver = null;
201
183
  this.#lastColumns = 0;
202
184
  }
203
- /**
204
- * Recomputes the column count and assigns every item to the shortest column.
205
- * Runs automatically on connect, on resize, on item add/remove, when a declared
206
- * number changes, and when a descendant resource loads (private — there is no
207
- * public action; the observers, the target callbacks and the capture-phase
208
- * `load` listener drive it). Items are walked in DOM order; each lands in the
209
- * column with the least accumulated height, which keeps the packing balanced
210
- * without reordering the DOM.
211
- *
212
- * Every box is measured before anything is written. Interleaving the two would
213
- * make a consumer's `data-column` rule invalidate style once per item, and the
214
- * next measurement then has to settle layout again — once per item instead of
215
- * once per pass. The assignment is independent of the measurement because the
216
- * columns are uniform in width, so the order of the two passes does not change
217
- * the result.
218
- *
219
- * @stimeoRenderRoot
220
- */
221
185
  #relayout() {
222
186
  const items = this.itemTargets;
223
187
  const columns = this.#columnCount();
@@ -253,20 +217,31 @@ var MasonryController = class extends Controller {
253
217
  this.dispatch("layout", { detail: { columns } });
254
218
  }
255
219
  }
256
- /**
257
- * Derives how many columns fit: `floor((width + gap) / (minColumnWidth + gap))`,
258
- * never fewer than one. When the width is unmeasurable (detached, or a layout
259
- * engine that reports `0`), it falls back to a single column so every item still
260
- * gets a valid `data-column`.
261
- */
262
220
  #columnCount() {
263
221
  const width = this.element.getBoundingClientRect().width;
264
222
  const denominator = this.#minColumnWidth + this.#gap;
265
223
  if (width <= 0 || denominator <= 0) return 1;
266
224
  return Math.max(1, Math.floor((width + this.#gap) / denominator));
267
225
  }
226
+ get #safeMinColumnWidth() {
227
+ return this.#numbers.read(
228
+ this,
229
+ "minColumnWidth",
230
+ this.minColumnWidthValue,
231
+ _MasonryController.values.minColumnWidth.default,
232
+ _MasonryController.valueConstraints.minColumnWidth
233
+ );
234
+ }
235
+ get #safeGap() {
236
+ return this.#numbers.read(
237
+ this,
238
+ "gap",
239
+ this.gapValue,
240
+ _MasonryController.values.gap.default,
241
+ _MasonryController.valueConstraints.gap
242
+ );
243
+ }
268
244
  };
269
245
 
270
246
  export { MasonryController };
271
- //# sourceMappingURL=masonry_controller.js.map
272
247
  //# sourceMappingURL=masonry_controller.js.map