stimeo-ui 0.16.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 +31 -0
  3. data/README.md +8 -1
  4. data/dist/cable/index.js +363 -436
  5. data/dist/controllers/accordion_controller.js +166 -55
  6. data/dist/controllers/alert_dialog_controller.js +862 -221
  7. data/dist/controllers/announcer_controller.js +135 -198
  8. data/dist/controllers/aspect_ratio_controller.js +0 -10
  9. data/dist/controllers/auto_submit_controller.js +271 -127
  10. data/dist/controllers/avatar_controller.js +171 -105
  11. data/dist/controllers/breadcrumb_controller.js +0 -130
  12. data/dist/controllers/bulk_select_controller.js +262 -52
  13. data/dist/controllers/calendar_controller.js +175 -209
  14. data/dist/controllers/carousel_controller.js +461 -399
  15. data/dist/controllers/character_counter_controller.js +246 -156
  16. data/dist/controllers/checkbox_controller.js +32 -74
  17. data/dist/controllers/clipboard_controller.js +168 -147
  18. data/dist/controllers/collapsible_controller.js +301 -120
  19. data/dist/controllers/color_picker_controller.js +149 -139
  20. data/dist/controllers/combobox_controller.js +360 -133
  21. data/dist/controllers/command_palette_controller.js +970 -358
  22. data/dist/controllers/conditional_fields_controller.js +166 -124
  23. data/dist/controllers/confirm_controller.js +939 -248
  24. data/dist/controllers/context_menu_controller.js +256 -128
  25. data/dist/controllers/count_up_controller.js +73 -21
  26. data/dist/controllers/countdown_controller.js +183 -148
  27. data/dist/controllers/currency_input_controller.js +134 -158
  28. data/dist/controllers/data_grid_controller.js +114 -186
  29. data/dist/controllers/date_range_picker_controller.js +288 -206
  30. data/dist/controllers/dialog_controller.js +858 -223
  31. data/dist/controllers/direct_upload_controller.js +73 -153
  32. data/dist/controllers/dirty_form_controller.js +14 -48
  33. data/dist/controllers/dismissible_controller.js +175 -21
  34. data/dist/controllers/drawer_controller.js +933 -341
  35. data/dist/controllers/dropdown_controller.js +312 -82
  36. data/dist/controllers/editable_controller.js +158 -68
  37. data/dist/controllers/empty_state_controller.js +67 -53
  38. data/dist/controllers/file_dropzone_controller.js +311 -207
  39. data/dist/controllers/filter_controller.js +67 -39
  40. data/dist/controllers/flash_controller.js +172 -314
  41. data/dist/controllers/focus_controller.js +654 -240
  42. data/dist/controllers/form_field_controller.js +147 -132
  43. data/dist/controllers/form_validation_controller.js +13 -86
  44. data/dist/controllers/frame_loading_controller.js +264 -213
  45. data/dist/controllers/highlight_controller.js +107 -75
  46. data/dist/controllers/hover_card_controller.js +309 -118
  47. data/dist/controllers/idle_controller.js +267 -101
  48. data/dist/controllers/input_mask_controller.js +71 -72
  49. data/dist/controllers/intersection_controller.js +147 -106
  50. data/dist/controllers/lazy_frame_controller.js +9 -65
  51. data/dist/controllers/listbox_controller.js +306 -204
  52. data/dist/controllers/local_time_controller.js +54 -64
  53. data/dist/controllers/masonry_controller.js +70 -93
  54. data/dist/controllers/menu_controller.js +214 -156
  55. data/dist/controllers/menubar_controller.js +170 -329
  56. data/dist/controllers/meter_controller.js +151 -67
  57. data/dist/controllers/multi_select_controller.js +593 -358
  58. data/dist/controllers/navigation_menu_controller.js +124 -232
  59. data/dist/controllers/nested_form_controller.js +352 -182
  60. data/dist/controllers/network_status_controller.js +242 -54
  61. data/dist/controllers/number_input_controller.js +320 -263
  62. data/dist/controllers/optimistic_controller.js +16 -76
  63. data/dist/controllers/otp_controller.js +198 -193
  64. data/dist/controllers/overflow_indicator_controller.js +116 -113
  65. data/dist/controllers/overflow_menu_controller.js +343 -283
  66. data/dist/controllers/pagination_controller.js +295 -119
  67. data/dist/controllers/password_reveal_controller.js +389 -121
  68. data/dist/controllers/password_strength_controller.js +99 -153
  69. data/dist/controllers/persist_controller.js +237 -103
  70. data/dist/controllers/pointer_drag_controller.js +185 -165
  71. data/dist/controllers/popover_controller.js +236 -97
  72. data/dist/controllers/portal_controller.js +209 -76
  73. data/dist/controllers/preview_guard_controller.js +232 -116
  74. data/dist/controllers/progress_controller.js +125 -56
  75. data/dist/controllers/radio_group_controller.js +117 -137
  76. data/dist/controllers/range_slider_controller.js +192 -105
  77. data/dist/controllers/rating_controller.js +418 -207
  78. data/dist/controllers/read_more_controller.js +134 -63
  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 +255 -139
  83. data/dist/controllers/roving_controller.js +39 -84
  84. data/dist/controllers/scroll_area_controller.js +428 -225
  85. data/dist/controllers/scroll_restore_controller.js +0 -53
  86. data/dist/controllers/scroll_visibility_controller.js +395 -145
  87. data/dist/controllers/scrollspy_controller.js +181 -242
  88. data/dist/controllers/separator_controller.js +267 -140
  89. data/dist/controllers/sidebar_controller.js +1023 -386
  90. data/dist/controllers/skeleton_controller.js +248 -97
  91. data/dist/controllers/slider_controller.js +159 -79
  92. data/dist/controllers/smart_sticky_header_controller.js +162 -63
  93. data/dist/controllers/sortable_controller.js +0 -112
  94. data/dist/controllers/spinner_controller.js +277 -169
  95. data/dist/controllers/step_indicator_controller.js +99 -46
  96. data/dist/controllers/stepper_controller.js +114 -73
  97. data/dist/controllers/stick_to_bottom_controller.js +262 -138
  98. data/dist/controllers/sticky_observer_controller.js +214 -49
  99. data/dist/controllers/submit_once_controller.js +294 -205
  100. data/dist/controllers/switch_controller.js +33 -60
  101. data/dist/controllers/tabs_controller.js +35 -29
  102. data/dist/controllers/tags_input_controller.js +136 -145
  103. data/dist/controllers/textarea_autosize_controller.js +71 -58
  104. data/dist/controllers/theme_controller.js +130 -112
  105. data/dist/controllers/time_picker_controller.js +146 -80
  106. data/dist/controllers/toast_controller.js +186 -297
  107. data/dist/controllers/toggle_group_controller.js +95 -136
  108. data/dist/controllers/toolbar_controller.js +68 -98
  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 +150 -345
  112. data/dist/index.js +6370 -9346
  113. data/dist/positioning/index.js +148 -47
  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,31 +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
185
  #relayout() {
220
186
  const items = this.itemTargets;
221
187
  const columns = this.#columnCount();
@@ -251,20 +217,31 @@ var MasonryController = class extends Controller {
251
217
  this.dispatch("layout", { detail: { columns } });
252
218
  }
253
219
  }
254
- /**
255
- * Derives how many columns fit: `floor((width + gap) / (minColumnWidth + gap))`,
256
- * never fewer than one. When the width is unmeasurable (detached, or a layout
257
- * engine that reports `0`), it falls back to a single column so every item still
258
- * gets a valid `data-column`.
259
- */
260
220
  #columnCount() {
261
221
  const width = this.element.getBoundingClientRect().width;
262
222
  const denominator = this.#minColumnWidth + this.#gap;
263
223
  if (width <= 0 || denominator <= 0) return 1;
264
224
  return Math.max(1, Math.floor((width + this.#gap) / denominator));
265
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
+ }
266
244
  };
267
245
 
268
246
  export { MasonryController };
269
- //# sourceMappingURL=masonry_controller.js.map
270
247
  //# sourceMappingURL=masonry_controller.js.map