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,40 +1,42 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/resizable_controller.ts
4
-
5
- // src/utils/arrow_step.ts
6
3
  function isReservedArrowChord(event, allow = []) {
7
4
  if (!event.key.startsWith("Arrow")) return false;
8
5
  return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
9
6
  }
10
7
 
11
- // src/utils/event_owner.ts
12
8
  function ownerIndex(candidates, node) {
13
9
  if (!(node instanceof Node)) return -1;
14
10
  return candidates.findIndex((candidate) => candidate.contains(node));
15
11
  }
16
12
 
17
- // src/utils/microtask_coalescer.ts
13
+ var ListenerSet = class {
14
+ #abort = new AbortController();
15
+ add(target, type, handler, options) {
16
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
17
+ }
18
+ dispose() {
19
+ this.#abort.abort();
20
+ this.#abort = new AbortController();
21
+ }
22
+ };
23
+
18
24
  var MicrotaskCoalescer = class {
19
25
  #run;
20
26
  #queued = false;
21
27
  #active = false;
22
28
  #generation = 0;
23
- /** @param run - the single reconciliation pass, invoked at most once per batch. */
24
29
  constructor(run) {
25
30
  this.#run = run;
26
31
  }
27
- /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
28
32
  activate() {
29
33
  this.#active = true;
30
34
  }
31
- /** Closes the window and drops any pending pass; call from `disconnect()`. */
32
35
  cancel() {
33
36
  this.#active = false;
34
37
  this.#queued = false;
35
38
  this.#generation += 1;
36
39
  }
37
- /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
38
40
  schedule() {
39
41
  if (!this.#active || this.#queued) return;
40
42
  this.#queued = true;
@@ -47,72 +49,197 @@ var MicrotaskCoalescer = class {
47
49
  }
48
50
  };
49
51
 
50
- // src/utils/before_cache_reset.ts
51
- var BeforeCacheReset = class _BeforeCacheReset {
52
- /** Every subscribed instance, iterated by the one shared document listener. */
53
- static #subscribers = /* @__PURE__ */ new Set();
54
- /** The shared listener; installed while at least one instance is subscribed. */
55
- static #onBeforeCache = () => {
56
- for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
57
- };
58
- #rewind;
59
- /** @param rewind - the pass that returns this controller's state to its initial form. */
60
- constructor(rewind) {
61
- this.#rewind = rewind;
52
+ var MorphRenderWatcher = class {
53
+ #listeners = new ListenerSet();
54
+ #pass;
55
+ #element = null;
56
+ #onMorph = () => this.#pass.schedule();
57
+ constructor(run) {
58
+ this.#pass = new MicrotaskCoalescer(run);
62
59
  }
63
- /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
64
- activate() {
65
- const first = _BeforeCacheReset.#subscribers.size === 0;
66
- _BeforeCacheReset.#subscribers.add(this);
67
- if (first) {
68
- document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
69
- }
60
+ observe(element) {
61
+ if (this.#element === element) return;
62
+ this.disconnect();
63
+ this.#element = element;
64
+ this.#pass.activate();
65
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
70
66
  }
71
- /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
72
- deactivate() {
73
- _BeforeCacheReset.#subscribers.delete(this);
74
- if (_BeforeCacheReset.#subscribers.size > 0) return;
75
- document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
67
+ schedule() {
68
+ this.#pass.schedule();
69
+ }
70
+ disconnect() {
71
+ this.#listeners.dispose();
72
+ this.#pass.cancel();
73
+ this.#element = null;
76
74
  }
77
75
  };
78
76
 
79
- // src/utils/tabindex_loan.ts
80
- var TabindexLoan = class {
77
+ var NUMBER_BOUNDS = {
78
+ finite: { finite: true },
79
+ positive: { finite: true, exclusiveMin: 0 }};
80
+ function matchesNumberBounds(value, bounds) {
81
+ if (!Number.isFinite(value)) {
82
+ const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
83
+ if (direction === null) return false;
84
+ if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
85
+ }
86
+ if (bounds.min !== void 0 && value < bounds.min) return false;
87
+ if (bounds.max !== void 0 && value > bounds.max) return false;
88
+ if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
89
+ if (bounds.integer && !Number.isInteger(value)) return false;
90
+ if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
91
+ return true;
92
+ }
93
+
94
+ function readNumber(raw, fallback, bounds) {
95
+ return matchesNumberBounds(raw, bounds) ? raw : fallback;
96
+ }
97
+
98
+ var NumberValueReader = class {
99
+ #lastRejected = /* @__PURE__ */ new Map();
100
+ read(owner, name, raw, fallback, bounds) {
101
+ const resolved = readNumber(raw, fallback, bounds);
102
+ if (matchesNumberBounds(raw, bounds)) {
103
+ this.#lastRejected.delete(name);
104
+ return resolved;
105
+ }
106
+ const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
107
+ const literal = owner.element.getAttribute(attribute);
108
+ if (literal === null) {
109
+ this.#lastRejected.delete(name);
110
+ return resolved;
111
+ }
112
+ if (this.#lastRejected.get(name) !== literal) {
113
+ this.#lastRejected.set(name, literal);
114
+ console.warn(
115
+ `Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
116
+ );
117
+ }
118
+ return resolved;
119
+ }
120
+ };
121
+
122
+ function queryOpenDescendants(root, selector) {
123
+ const matches = [];
124
+ const roots = [root];
125
+ for (const scope of roots) {
126
+ matches.push(...scope.querySelectorAll(selector));
127
+ if (scope.nodeType === Node.ELEMENT_NODE) {
128
+ const shadow = scope.shadowRoot;
129
+ if (shadow) roots.push(shadow);
130
+ }
131
+ for (const element of scope.querySelectorAll("*")) {
132
+ if (element.shadowRoot) roots.push(element.shadowRoot);
133
+ }
134
+ }
135
+ return matches;
136
+ }
137
+
138
+ function sharedRegistry(key, create) {
139
+ const symbol = Symbol.for(key);
140
+ const scope = globalThis;
141
+ const existing = scope[symbol];
142
+ if (existing !== void 0) return existing;
143
+ const registry = create();
144
+ Object.defineProperty(globalThis, symbol, { value: registry });
145
+ return registry;
146
+ }
147
+
148
+ var TabindexLoan = class _TabindexLoan {
149
+ static #registry = sharedRegistry(
150
+ "stimeo-ui.tabindex-loan.registry.v1",
151
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Set() })
152
+ );
81
153
  #value;
154
+ #record;
82
155
  #lent = /* @__PURE__ */ new Set();
83
- /** Returns live loans before Turbo can copy them into its page snapshot. */
84
- #beforeCache = new BeforeCacheReset(() => this.returnAll());
85
- /**
86
- * @param value - the `tabindex` to lend. `"-1"` (the default) is
87
- * programmatically focusable but not a Tab stop; `"0"` is a real Tab stop,
88
- * which a scroll region with no focusable content of its own needs.
89
- */
90
- constructor(value = "-1") {
156
+ constructor(value, owner) {
91
157
  this.#value = value;
158
+ this.#record = `data-${owner}-tabindex-loan`;
159
+ _TabindexLoan.#registry.names.add(this.#record);
92
160
  }
93
- /** Lends `element` the value; no-ops when it already carries a `tabindex`. */
94
161
  lend(element) {
95
- if (element.hasAttribute("tabindex")) return;
96
- element.setAttribute("tabindex", this.#value);
162
+ const stack = this.#stack(element);
163
+ const current = element.getAttribute("tabindex");
164
+ const held = stack.find((hold) => hold.loan === this) ?? stack.find(
165
+ (hold) => hold.loan === null && hold.record === this.#record && hold.value === this.#value
166
+ );
167
+ const linked = stack.at(-1)?.value === current;
168
+ if (!held && current !== null && !linked) return;
97
169
  this.#lent.add(element);
98
- this.#beforeCache.activate();
170
+ if (held) held.loan = this;
171
+ else stack.push({ loan: this, record: this.#record, value: this.#value, linked });
172
+ this.#mark(element, stack);
173
+ if (current === null || !held && current !== this.#value) {
174
+ element.setAttribute("tabindex", this.#value);
175
+ }
99
176
  }
100
- /** Takes back every loan whose value is still the one that was lent. */
101
177
  returnAll() {
102
178
  for (const element of this.#lent) {
103
- if (element.getAttribute("tabindex") === this.#value) element.removeAttribute("tabindex");
179
+ const stack = this.#stack(element);
180
+ const hold = stack.find((held) => held.loan === this);
181
+ if (hold) this.#release(element, stack, hold);
104
182
  }
105
183
  this.#lent.clear();
106
- this.#beforeCache.deactivate();
184
+ }
185
+ reclaim(element) {
186
+ if (element.getAttribute(this.#record) !== this.#value) return;
187
+ const stack = this.#stack(element);
188
+ const hold = stack.find((held) => held.loan === null && held.record === this.#record);
189
+ if (hold && !element.matches(":focus")) this.#release(element, stack, hold);
190
+ }
191
+ reclaimWithin(root) {
192
+ for (const element of queryOpenDescendants(root, `[${this.#record}]`)) {
193
+ this.reclaim(element);
194
+ }
195
+ }
196
+ #stack(element) {
197
+ const stacks = _TabindexLoan.#registry.stacks;
198
+ const stack = stacks.get(element) ?? this.#seed(element);
199
+ stacks.set(element, stack);
200
+ return stack;
201
+ }
202
+ #seed(element) {
203
+ const current = element.getAttribute("tabindex");
204
+ const holds = [];
205
+ for (const record of _TabindexLoan.#registry.names) {
206
+ const value = element.getAttribute(record);
207
+ if (value !== null) holds.push({ loan: null, record, value, linked: true });
208
+ }
209
+ holds.sort((one, other) => Number(one.value === current) - Number(other.value === current));
210
+ for (const [index, hold] of holds.entries()) hold.linked = index > 0;
211
+ return holds;
212
+ }
213
+ #release(element, stack, hold) {
214
+ const current = element.getAttribute("tabindex");
215
+ const index = stack.indexOf(hold);
216
+ const owns = current === hold.value && !stack.slice(index + 1).some((above2) => above2.value === current);
217
+ stack.splice(index, 1);
218
+ const above = stack[index];
219
+ if (above) above.linked &&= hold.linked;
220
+ this.#mark(element, stack);
221
+ if (!owns) return;
222
+ const below = hold.linked ? stack[index - 1] : void 0;
223
+ if (!below) element.removeAttribute("tabindex");
224
+ else if (below.value !== current) element.setAttribute("tabindex", below.value);
225
+ }
226
+ #mark(element, stack) {
227
+ const records = /* @__PURE__ */ new Map([[this.#record, null]]);
228
+ for (const hold of stack) records.set(hold.record, hold.value);
229
+ for (const [name, recorded] of records) {
230
+ if (element.getAttribute(name) === recorded) continue;
231
+ if (recorded === null) element.removeAttribute(name);
232
+ else element.setAttribute(name, recorded);
233
+ }
107
234
  }
108
235
  };
109
236
 
110
- // src/controllers/resizable_controller.ts
111
237
  var DEFAULT_MIN = 0;
112
238
  var DEFAULT_MAX = 100;
113
239
  var DEFAULT_VALUE = 50;
114
240
  var DEFAULT_STEP = 1;
115
- var ResizableController = class extends Controller {
241
+ var ResizableController = class _ResizableController extends Controller {
242
+ #numbers = new NumberValueReader();
116
243
  static targets = ["primary", "secondary", "separator"];
117
244
  static values = {
118
245
  min: { type: Number, default: DEFAULT_MIN },
@@ -120,38 +247,38 @@ var ResizableController = class extends Controller {
120
247
  step: { type: Number, default: DEFAULT_STEP },
121
248
  value: { type: Number, default: DEFAULT_VALUE }
122
249
  };
250
+ static valueConstraints = {
251
+ min: NUMBER_BOUNDS.finite,
252
+ max: NUMBER_BOUNDS.finite,
253
+ step: NUMBER_BOUNDS.positive,
254
+ value: NUMBER_BOUNDS.finite
255
+ };
123
256
  static actions = ["onKeydown", "onPointerDown", "toggle"];
124
- static events = ["change"];
125
- /** Where the divider sat before the current collapse; `null` when unknown. */
257
+ static events = ["change", "reconcile"];
126
258
  #valueBeforeCollapse = null;
127
- /** Aborts in-progress pointer-drag listeners when the drag ends or on teardown. */
128
- #dragAbort = null;
129
- /** `tabindex` lent to a pane so `F6` can put focus on it; panes carry none. */
130
- #paneTabindex = new TabindexLoan();
131
- /** Releases the pane cycle listener bound in {@link connect}. */
259
+ #painted = DEFAULT_VALUE;
260
+ #confirmed = DEFAULT_VALUE;
261
+ #drag = null;
262
+ #paneTabindex = new TabindexLoan("-1", this.identifier);
132
263
  #cycleAbort = null;
133
- /** Folds the range and position inputs of one morph batch into a single paint. */
134
- #repaint = new MicrotaskCoalescer(() => this.#render());
264
+ #repaint = new MorphRenderWatcher(() => this.#render());
135
265
  connect() {
136
- this.element.removeAttribute("data-dragging");
137
- this.#repaint.activate();
266
+ this.#paneTabindex.reclaimWithin(this.element);
267
+ this.#repaint.observe(this.element);
268
+ this.#confirmed = this.#position;
138
269
  this.#render();
139
270
  this.#cycleAbort = new AbortController();
140
271
  this.element.addEventListener("keydown", this.#onCycleKeydown, {
141
272
  signal: this.#cycleAbort.signal
142
273
  });
143
274
  }
144
- /** Cancels any active pointer drag so listeners never leak past disconnect. */
145
275
  disconnect() {
146
- this.#dragAbort?.abort();
147
- this.#dragAbort = null;
276
+ this.#endDrag();
148
277
  this.#cycleAbort?.abort();
149
278
  this.#cycleAbort = null;
150
- this.#repaint.cancel();
279
+ this.#repaint.disconnect();
151
280
  this.#paneTabindex.returnAll();
152
- this.element.removeAttribute("data-dragging");
153
281
  }
154
- /** Moves focus to the next pane on `F6`, wrapping; entering at the first one. */
155
282
  #onCycleKeydown = (event) => {
156
283
  if (event.key !== "F6") return;
157
284
  if (event.isComposing) return;
@@ -167,38 +294,39 @@ var ResizableController = class extends Controller {
167
294
  this.#paneTabindex.lend(next);
168
295
  next.focus();
169
296
  };
170
- /** Re-renders when application code (or a Turbo morph) changes `value` at runtime. */
171
297
  valueValueChanged() {
172
298
  this.#repaint.schedule();
173
299
  }
174
- /** Re-renders when application code (or a Turbo morph) changes `min` at runtime. */
175
300
  minValueChanged() {
176
301
  this.#repaint.schedule();
177
302
  }
178
- /** Re-renders when application code (or a Turbo morph) changes `max` at runtime. */
179
303
  maxValueChanged() {
180
304
  this.#repaint.schedule();
181
305
  }
182
- /** Re-publishes range and position onto a separator swapped in after connect. */
183
306
  separatorTargetConnected() {
184
307
  this.#repaint.schedule();
185
308
  }
186
- /** Starts active pointer drag tracking and locks capture. */
309
+ separatorTargetDisconnected(separator) {
310
+ if (this.#drag?.separator === separator && !this.separatorTargets.includes(separator)) {
311
+ this.#endDrag();
312
+ }
313
+ this.#repaint.schedule();
314
+ }
187
315
  onPointerDown(event) {
188
316
  if (!this.hasSeparatorTarget || event.button !== 0) return;
189
317
  event.preventDefault();
190
- const separator = this.separatorTarget;
318
+ const separators = this.separatorTargets;
319
+ const separator = separators[ownerIndex(separators, event.target)] ?? this.separatorTarget;
191
320
  separator.setPointerCapture(event.pointerId);
192
321
  separator.focus();
193
322
  this.element.setAttribute("data-dragging", "true");
194
- this.#dragAbort?.abort();
323
+ this.#drag?.abort.abort();
195
324
  const abort = new AbortController();
196
- this.#dragAbort = abort;
325
+ this.#drag = { separator, pointerId: event.pointerId, abort };
197
326
  separator.addEventListener("pointermove", this.#onPointerMove, { signal: abort.signal });
198
327
  separator.addEventListener("pointerup", this.#onPointerUp, { signal: abort.signal });
199
328
  separator.addEventListener("pointercancel", this.#onPointerUp, { signal: abort.signal });
200
329
  }
201
- /** Keydown adjustments for ArrowUp/Down/Left/Right and Home/End. */
202
330
  onKeydown(event) {
203
331
  if (isReservedArrowChord(event)) return;
204
332
  if (!this.hasSeparatorTarget) return;
@@ -251,55 +379,67 @@ var ResizableController = class extends Controller {
251
379
  }
252
380
  if (handled) {
253
381
  event.preventDefault();
254
- this.valueValue = Math.max(min, Math.min(nextValue, max));
255
- this.#render();
256
- this.#dispatchChange();
382
+ this.#commit(nextValue);
257
383
  }
258
384
  }
259
- /** Collapses the primary pane to its minimum, or returns it to the last position. */
260
385
  toggle() {
261
386
  const { min, max } = this.#range;
262
387
  if (this.#position > min) {
263
388
  this.#valueBeforeCollapse = this.#position;
264
- this.valueValue = min;
389
+ this.#commit(min);
265
390
  } else {
266
- this.valueValue = this.#valueBeforeCollapse ?? max;
391
+ const restored = this.#valueBeforeCollapse ?? max;
267
392
  this.#valueBeforeCollapse = null;
393
+ this.#commit(restored);
268
394
  }
269
- this.#render();
270
- this.#dispatchChange();
271
395
  }
272
396
  #onPointerMove = (event) => {
273
397
  if (!this.hasSeparatorTarget) return;
274
398
  const rect = this.element.getBoundingClientRect();
275
399
  const raw = this.#isVertical ? (event.clientX - rect.left) / rect.width : (event.clientY - rect.top) / rect.height;
276
400
  const fraction = Number.isFinite(raw) ? Math.max(0, Math.min(raw, 1)) : 0;
277
- const { min, max } = this.#range;
278
- this.valueValue = Math.max(min, Math.min(Math.round(fraction * 100), max));
279
- this.#render();
401
+ this.#move(Math.round(fraction * 100));
280
402
  };
281
- #onPointerUp = (event) => {
403
+ #onPointerUp = () => {
404
+ this.#endDrag();
405
+ this.#confirm();
406
+ };
407
+ #endDrag() {
282
408
  this.element.removeAttribute("data-dragging");
283
- this.#dragAbort?.abort();
284
- this.#dragAbort = null;
285
- if (this.hasSeparatorTarget) {
286
- this.separatorTarget.releasePointerCapture(event.pointerId);
409
+ const drag = this.#drag;
410
+ this.#drag = null;
411
+ if (!drag) return;
412
+ drag.abort.abort();
413
+ try {
414
+ drag.separator.releasePointerCapture?.(drag.pointerId);
415
+ } catch {
287
416
  }
288
- this.#dispatchChange();
289
- };
290
- /**
291
- * Publishes the position: the fraction consumer CSS multiplies a pane by, and
292
- * the range assistive tech reads off the separator. The clamped position is
293
- * written back to the Value so it, ARIA, CSS, and dispatched events agree.
294
- *
295
- * @stimeoRenderRoot
296
- */
417
+ }
418
+ #commit(raw) {
419
+ this.#move(raw);
420
+ this.#confirm();
421
+ }
422
+ #move(raw) {
423
+ const { min, max } = this.#range;
424
+ const position = Math.max(min, Math.min(raw, max));
425
+ this.valueValue = position;
426
+ this.#painted = position;
427
+ this.#paint();
428
+ }
297
429
  #render() {
430
+ this.#paint();
431
+ const position = this.#position;
432
+ if (position === this.#painted) return;
433
+ this.#painted = position;
434
+ if (position === this.#confirmed) return;
435
+ this.#confirmed = position;
436
+ this.dispatch("reconcile", { detail: this.#detail(position) });
437
+ }
438
+ #paint() {
439
+ if (this.#drag) this.element.setAttribute("data-dragging", "true");
440
+ else this.element.removeAttribute("data-dragging");
298
441
  const { min, max } = this.#range;
299
442
  const position = this.#position;
300
- if (this.valueValue !== position) {
301
- this.valueValue = position;
302
- }
303
443
  this.element.style.setProperty("--stimeo--resizable-fraction", String(position / 100));
304
444
  if (this.hasSeparatorTarget) {
305
445
  this.separatorTarget.setAttribute("aria-valuenow", String(position));
@@ -307,32 +447,68 @@ var ResizableController = class extends Controller {
307
447
  this.separatorTarget.setAttribute("aria-valuemax", String(max));
308
448
  }
309
449
  }
310
- #dispatchChange() {
311
- const fraction = this.valueValue / 100;
312
- this.dispatch("change", { detail: { value: this.valueValue, fraction } });
450
+ #confirm() {
451
+ const position = this.#position;
452
+ if (position === this.#confirmed) return;
453
+ this.#confirmed = position;
454
+ this.dispatch("change", { detail: this.#detail(position) });
455
+ }
456
+ #detail(position) {
457
+ return { value: position, fraction: position / 100 };
313
458
  }
314
- /** The declared range after validation; an unreadable bound uses its default. */
315
459
  get #range() {
316
- const min = Number.isFinite(this.minValue) ? this.minValue : DEFAULT_MIN;
317
- const max = Number.isFinite(this.maxValue) ? this.maxValue : DEFAULT_MAX;
460
+ const min = this.#safeMin;
461
+ const max = this.#safeMax;
318
462
  return { min, max: Math.max(min, max) };
319
463
  }
320
- /** The declared position, clamped into the validated range. */
321
464
  get #position() {
322
465
  const { min, max } = this.#range;
323
- const value = Number.isFinite(this.valueValue) ? this.valueValue : DEFAULT_VALUE;
466
+ const value = this.#safeValue;
324
467
  return Math.max(min, Math.min(value, max));
325
468
  }
326
- /** The keyboard increment; a non-positive or unreadable one uses the default. */
327
469
  get #step() {
328
- return Number.isFinite(this.stepValue) && this.stepValue > 0 ? this.stepValue : DEFAULT_STEP;
470
+ return this.#safeStep;
329
471
  }
330
- /** Whether the divider runs vertically; a separator that does not say is horizontal. */
331
472
  get #isVertical() {
332
473
  return this.hasSeparatorTarget && this.separatorTarget.getAttribute("aria-orientation") === "vertical";
333
474
  }
475
+ get #safeMin() {
476
+ return this.#numbers.read(
477
+ this,
478
+ "min",
479
+ this.minValue,
480
+ _ResizableController.values.min.default,
481
+ _ResizableController.valueConstraints.min
482
+ );
483
+ }
484
+ get #safeMax() {
485
+ return this.#numbers.read(
486
+ this,
487
+ "max",
488
+ this.maxValue,
489
+ _ResizableController.values.max.default,
490
+ _ResizableController.valueConstraints.max
491
+ );
492
+ }
493
+ get #safeStep() {
494
+ return this.#numbers.read(
495
+ this,
496
+ "step",
497
+ this.stepValue,
498
+ _ResizableController.values.step.default,
499
+ _ResizableController.valueConstraints.step
500
+ );
501
+ }
502
+ get #safeValue() {
503
+ return this.#numbers.read(
504
+ this,
505
+ "value",
506
+ this.valueValue,
507
+ _ResizableController.values.value.default,
508
+ _ResizableController.valueConstraints.value
509
+ );
510
+ }
334
511
  };
335
512
 
336
513
  export { ResizableController };
337
- //# sourceMappingURL=resizable_controller.js.map
338
514
  //# sourceMappingURL=resizable_controller.js.map