stimeo-ui 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +136 -0
  3. data/README.md +27 -11
  4. data/dist/cable/index.js +384 -449
  5. data/dist/controllers/accordion_controller.js +174 -55
  6. data/dist/controllers/alert_dialog_controller.js +903 -235
  7. data/dist/controllers/announcer_controller.js +238 -205
  8. data/dist/controllers/aspect_ratio_controller.js +0 -10
  9. data/dist/controllers/auto_submit_controller.js +414 -145
  10. data/dist/controllers/avatar_controller.js +185 -107
  11. data/dist/controllers/breadcrumb_controller.js +0 -130
  12. data/dist/controllers/bulk_select_controller.js +282 -58
  13. data/dist/controllers/calendar_controller.js +228 -260
  14. data/dist/controllers/carousel_controller.js +536 -436
  15. data/dist/controllers/character_counter_controller.js +493 -207
  16. data/dist/controllers/checkbox_controller.js +38 -74
  17. data/dist/controllers/clipboard_controller.js +261 -149
  18. data/dist/controllers/collapsible_controller.js +304 -120
  19. data/dist/controllers/color_picker_controller.js +178 -146
  20. data/dist/controllers/combobox_controller.js +522 -156
  21. data/dist/controllers/command_palette_controller.js +1156 -395
  22. data/dist/controllers/conditional_fields_controller.js +300 -171
  23. data/dist/controllers/confirm_controller.js +980 -262
  24. data/dist/controllers/context_menu_controller.js +346 -153
  25. data/dist/controllers/count_up_controller.js +73 -21
  26. data/dist/controllers/countdown_controller.js +281 -158
  27. data/dist/controllers/currency_input_controller.js +575 -225
  28. data/dist/controllers/data_grid_controller.js +248 -214
  29. data/dist/controllers/date_range_picker_controller.js +338 -235
  30. data/dist/controllers/dialog_controller.js +899 -237
  31. data/dist/controllers/direct_upload_controller.js +97 -167
  32. data/dist/controllers/dirty_form_controller.js +72 -69
  33. data/dist/controllers/dismissible_controller.js +196 -24
  34. data/dist/controllers/drawer_controller.js +974 -355
  35. data/dist/controllers/dropdown_controller.js +339 -96
  36. data/dist/controllers/editable_controller.js +315 -88
  37. data/dist/controllers/empty_state_controller.js +79 -58
  38. data/dist/controllers/file_dropzone_controller.js +322 -211
  39. data/dist/controllers/filter_controller.js +77 -42
  40. data/dist/controllers/flash_controller.js +238 -347
  41. data/dist/controllers/focus_controller.js +679 -240
  42. data/dist/controllers/form_field_controller.js +156 -132
  43. data/dist/controllers/form_validation_controller.js +31 -97
  44. data/dist/controllers/frame_loading_controller.js +275 -217
  45. data/dist/controllers/highlight_controller.js +107 -75
  46. data/dist/controllers/hover_card_controller.js +327 -130
  47. data/dist/controllers/idle_controller.js +271 -101
  48. data/dist/controllers/input_mask_controller.js +483 -97
  49. data/dist/controllers/intersection_controller.js +150 -106
  50. data/dist/controllers/lazy_frame_controller.js +24 -68
  51. data/dist/controllers/listbox_controller.js +523 -240
  52. data/dist/controllers/local_time_controller.js +102 -77
  53. data/dist/controllers/masonry_controller.js +80 -96
  54. data/dist/controllers/menu_controller.js +302 -188
  55. data/dist/controllers/menubar_controller.js +232 -345
  56. data/dist/controllers/meter_controller.js +417 -87
  57. data/dist/controllers/multi_select_controller.js +785 -393
  58. data/dist/controllers/navigation_menu_controller.js +190 -250
  59. data/dist/controllers/nested_form_controller.js +396 -192
  60. data/dist/controllers/network_status_controller.js +268 -62
  61. data/dist/controllers/number_input_controller.js +610 -303
  62. data/dist/controllers/optimistic_controller.js +45 -87
  63. data/dist/controllers/otp_controller.js +658 -255
  64. data/dist/controllers/overflow_indicator_controller.js +122 -113
  65. data/dist/controllers/overflow_menu_controller.js +470 -295
  66. data/dist/controllers/pagination_controller.js +332 -123
  67. data/dist/controllers/password_reveal_controller.js +392 -121
  68. data/dist/controllers/password_strength_controller.js +111 -158
  69. data/dist/controllers/persist_controller.js +165 -111
  70. data/dist/controllers/pointer_drag_controller.js +434 -188
  71. data/dist/controllers/popover_controller.js +269 -113
  72. data/dist/controllers/portal_controller.js +215 -76
  73. data/dist/controllers/preview_guard_controller.js +243 -116
  74. data/dist/controllers/progress_controller.js +391 -78
  75. data/dist/controllers/radio_group_controller.js +278 -211
  76. data/dist/controllers/range_slider_controller.js +220 -113
  77. data/dist/controllers/rating_controller.js +458 -211
  78. data/dist/controllers/read_more_controller.js +138 -63
  79. data/dist/controllers/reading_progress_controller.js +166 -137
  80. data/dist/controllers/relative_time_controller.js +211 -112
  81. data/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +103 -38
  82. data/dist/controllers/resizable_controller.js +299 -150
  83. data/dist/controllers/roving_controller.js +49 -88
  84. data/dist/controllers/scroll_area_controller.js +490 -246
  85. data/dist/controllers/scroll_restore_controller.js +14 -55
  86. data/dist/controllers/scroll_visibility_controller.js +432 -149
  87. data/dist/controllers/scrollspy_controller.js +193 -248
  88. data/dist/controllers/separator_controller.js +285 -142
  89. data/dist/controllers/sidebar_controller.js +1069 -404
  90. data/dist/controllers/skeleton_controller.js +259 -101
  91. data/dist/controllers/slider_controller.js +187 -86
  92. data/dist/controllers/smart_sticky_header_controller.js +173 -66
  93. data/dist/controllers/sortable_controller.js +26 -125
  94. data/dist/controllers/spinner_controller.js +288 -173
  95. data/dist/controllers/step_indicator_controller.js +105 -46
  96. data/dist/controllers/stepper_controller.js +120 -73
  97. data/dist/controllers/stick_to_bottom_controller.js +275 -140
  98. data/dist/controllers/sticky_observer_controller.js +220 -49
  99. data/dist/controllers/submit_once_controller.js +376 -227
  100. data/dist/controllers/switch_controller.js +201 -84
  101. data/dist/controllers/tabs_controller.js +41 -30
  102. data/dist/controllers/tags_input_controller.js +293 -173
  103. data/dist/controllers/textarea_autosize_controller.js +108 -74
  104. data/dist/controllers/theme_controller.js +140 -116
  105. data/dist/controllers/time_picker_controller.js +331 -127
  106. data/dist/controllers/toast_controller.js +244 -324
  107. data/dist/controllers/toggle_group_controller.js +252 -191
  108. data/dist/controllers/toolbar_controller.js +219 -120
  109. data/dist/controllers/tooltip_controller.js +158 -124
  110. data/dist/controllers/transition_controller.js +123 -119
  111. data/dist/controllers/tree_view_controller.js +283 -369
  112. data/dist/index.js +8573 -10410
  113. data/dist/positioning/index.js +154 -47
  114. data/lib/generators/stimeo/install/install_generator.rb +7 -2
  115. data/lib/generators/stimeo/install/templates/stimeo.js +19 -6
  116. data/lib/stimeo/ui/version.rb +1 -1
  117. metadata +3 -3
@@ -1,40 +1,48 @@
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
  }
7
+ function hasModifierChord(event) {
8
+ return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
9
+ }
10
10
 
11
- // src/utils/event_owner.ts
12
11
  function ownerIndex(candidates, node) {
13
12
  if (!(node instanceof Node)) return -1;
14
13
  return candidates.findIndex((candidate) => candidate.contains(node));
15
14
  }
16
15
 
17
- // src/utils/microtask_coalescer.ts
16
+ var ListenerSet = class {
17
+ #abort = new AbortController();
18
+ add(target, type, handler, options) {
19
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
20
+ }
21
+ dispose() {
22
+ this.#abort.abort();
23
+ this.#abort = new AbortController();
24
+ }
25
+ };
26
+
18
27
  var MicrotaskCoalescer = class {
19
28
  #run;
20
29
  #queued = false;
21
30
  #active = false;
22
31
  #generation = 0;
23
- /** @param run - the single reconciliation pass, invoked at most once per batch. */
24
32
  constructor(run) {
25
33
  this.#run = run;
26
34
  }
27
- /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
28
35
  activate() {
29
36
  this.#active = true;
30
37
  }
31
- /** Closes the window and drops any pending pass; call from `disconnect()`. */
32
38
  cancel() {
33
39
  this.#active = false;
34
40
  this.#queued = false;
35
41
  this.#generation += 1;
36
42
  }
37
- /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
43
+ get pending() {
44
+ return this.#queued;
45
+ }
38
46
  schedule() {
39
47
  if (!this.#active || this.#queued) return;
40
48
  this.#queued = true;
@@ -47,85 +55,227 @@ var MicrotaskCoalescer = class {
47
55
  }
48
56
  };
49
57
 
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;
58
+ var MorphRenderWatcher = class {
59
+ #listeners = new ListenerSet();
60
+ #pass;
61
+ #element = null;
62
+ #onMorph = () => this.#pass.schedule();
63
+ constructor(run) {
64
+ this.#pass = new MicrotaskCoalescer(run);
62
65
  }
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
- }
66
+ observe(element) {
67
+ if (this.#element === element) return;
68
+ this.disconnect();
69
+ this.#element = element;
70
+ this.#pass.activate();
71
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
72
+ }
73
+ schedule() {
74
+ this.#pass.schedule();
75
+ }
76
+ get pending() {
77
+ return this.#pass.pending;
78
+ }
79
+ disconnect() {
80
+ this.#listeners.dispose();
81
+ this.#pass.cancel();
82
+ this.#element = null;
70
83
  }
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);
84
+ };
85
+
86
+ var NUMBER_BOUNDS = {
87
+ finite: { finite: true },
88
+ positive: { finite: true, exclusiveMin: 0 }};
89
+ function matchesNumberBounds(value, bounds) {
90
+ if (!Number.isFinite(value)) {
91
+ const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
92
+ if (direction === null) return false;
93
+ if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
94
+ }
95
+ if (bounds.min !== void 0 && value < bounds.min) return false;
96
+ if (bounds.max !== void 0 && value > bounds.max) return false;
97
+ if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
98
+ if (bounds.integer && !Number.isInteger(value)) return false;
99
+ if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
100
+ return true;
101
+ }
102
+
103
+ function readNumber(raw, fallback, bounds) {
104
+ return matchesNumberBounds(raw, bounds) ? raw : fallback;
105
+ }
106
+
107
+ var NumberValueReader = class {
108
+ #lastRejected = /* @__PURE__ */ new Map();
109
+ read(owner, name, raw, fallback, bounds) {
110
+ const resolved = readNumber(raw, fallback, bounds);
111
+ if (matchesNumberBounds(raw, bounds)) {
112
+ this.#lastRejected.delete(name);
113
+ return resolved;
114
+ }
115
+ const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
116
+ const literal = owner.element.getAttribute(attribute);
117
+ if (literal === null) {
118
+ this.#lastRejected.delete(name);
119
+ return resolved;
120
+ }
121
+ if (this.#lastRejected.get(name) !== literal) {
122
+ this.#lastRejected.set(name, literal);
123
+ console.warn(
124
+ `Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
125
+ );
126
+ }
127
+ return resolved;
76
128
  }
77
129
  };
78
130
 
79
- // src/utils/tabindex_loan.ts
80
- var TabindexLoan = class {
131
+ function setAttributeIfChanged(element, name, value, beforeWrite) {
132
+ if (element.getAttribute(name) === value) return false;
133
+ if (value === null) element.removeAttribute(name);
134
+ else element.setAttribute(name, value);
135
+ return true;
136
+ }
137
+ function deepActiveElement(ownerDocument) {
138
+ let active = ownerDocument.activeElement;
139
+ while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
140
+ return active;
141
+ }
142
+
143
+ function queryOpenDescendants(root, selector) {
144
+ const matches = [];
145
+ const roots = [root];
146
+ for (const scope of roots) {
147
+ matches.push(...scope.querySelectorAll(selector));
148
+ if (scope.nodeType === Node.ELEMENT_NODE) {
149
+ const shadow = scope.shadowRoot;
150
+ if (shadow) roots.push(shadow);
151
+ }
152
+ for (const element of scope.querySelectorAll("*")) {
153
+ if (element.shadowRoot) roots.push(element.shadowRoot);
154
+ }
155
+ }
156
+ return matches;
157
+ }
158
+
159
+ function sharedRegistry(key, create) {
160
+ const symbol = Symbol.for(key);
161
+ const scope = globalThis;
162
+ const existing = scope[symbol];
163
+ if (existing !== void 0) return existing;
164
+ const registry = create();
165
+ Object.defineProperty(globalThis, symbol, { value: registry });
166
+ return registry;
167
+ }
168
+
169
+ function holdsFocus(element) {
170
+ return element.matches(":focus") || deepActiveElement(element.ownerDocument) === element;
171
+ }
172
+ var TabindexLoan = class _TabindexLoan {
173
+ static #registry = sharedRegistry(
174
+ "stimeo-ui.tabindex-loan.registry.v1",
175
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Set() })
176
+ );
81
177
  #value;
178
+ #record;
179
+ #preserveFocus;
82
180
  #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") {
181
+ constructor(value, owner, options = {}) {
91
182
  this.#value = value;
183
+ this.#record = `data-${owner}-tabindex-loan`;
184
+ this.#preserveFocus = options.preserveFocus === true;
185
+ _TabindexLoan.#registry.names.add(this.#record);
92
186
  }
93
- /** Lends `element` the value; no-ops when it already carries a `tabindex`. */
94
- lend(element) {
95
- if (element.hasAttribute("tabindex")) return;
96
- element.setAttribute("tabindex", this.#value);
187
+ lend(element, options = {}) {
188
+ const stack = this.#stack(element);
189
+ const current = element.getAttribute("tabindex");
190
+ const held = stack.find((hold) => hold.loan === this) ?? stack.find(
191
+ (hold) => hold.loan === null && hold.record === this.#record && hold.value === this.#value
192
+ );
193
+ const linked = stack.at(-1)?.value === current;
194
+ if (!held && options.onlyIfUnfocusable && (current !== null || element.tabIndex >= 0)) return;
195
+ if (!held && current !== null && !linked) return;
97
196
  this.#lent.add(element);
98
- this.#beforeCache.activate();
197
+ if (held) held.loan = this;
198
+ else stack.push({ loan: this, record: this.#record, value: this.#value, linked });
199
+ this.#mark(element, stack);
200
+ if (current === null || !held && current !== this.#value) {
201
+ element.setAttribute("tabindex", this.#value);
202
+ }
203
+ }
204
+ return(element) {
205
+ const stack = this.#stack(element);
206
+ const hold = stack.find((held) => held.loan === this) ?? stack.find(
207
+ (held) => held.loan === null && held.record === this.#record && held.value === this.#value
208
+ );
209
+ if (!hold) return false;
210
+ if (!this.#release(element, stack, hold, this.#preserveFocus)) {
211
+ hold.loan = this;
212
+ this.#lent.add(element);
213
+ return false;
214
+ }
215
+ this.#lent.delete(element);
216
+ return true;
99
217
  }
100
- /** Takes back every loan whose value is still the one that was lent. */
101
218
  returnAll() {
102
- for (const element of this.#lent) {
103
- if (element.getAttribute("tabindex") === this.#value) element.removeAttribute("tabindex");
219
+ for (const element of Array.from(this.#lent)) this.return(element);
220
+ }
221
+ reclaim(element) {
222
+ if (element.getAttribute(this.#record) !== this.#value) return;
223
+ const stack = this.#stack(element);
224
+ const hold = stack.find((held) => held.loan === null && held.record === this.#record);
225
+ if (hold && !holdsFocus(element)) this.#release(element, stack, hold);
226
+ }
227
+ reclaimWithin(root) {
228
+ for (const element of queryOpenDescendants(root, `[${this.#record}]`)) {
229
+ this.reclaim(element);
104
230
  }
105
- this.#lent.clear();
106
- this.#beforeCache.deactivate();
107
231
  }
108
- };
109
-
110
- // src/utils/transient_hooks.ts
111
- var TransientHooks = class {
112
- #attributes;
113
- constructor(options) {
114
- this.#attributes = options.attributes;
232
+ #stack(element) {
233
+ const stacks = _TabindexLoan.#registry.stacks;
234
+ const stack = stacks.get(element) ?? this.#seed(element);
235
+ stacks.set(element, stack);
236
+ return stack;
115
237
  }
116
- /** Drops the declared hooks from `element`; what it carries besides stays. */
117
- reset(element) {
118
- for (const attribute of this.#attributes) element.removeAttribute(attribute);
238
+ #seed(element) {
239
+ const current = element.getAttribute("tabindex");
240
+ const holds = [];
241
+ for (const record of _TabindexLoan.#registry.names) {
242
+ const value = element.getAttribute(record);
243
+ if (value !== null) holds.push({ loan: null, record, value, linked: true });
244
+ }
245
+ holds.sort((one, other) => Number(one.value === current) - Number(other.value === current));
246
+ for (const [index, hold] of holds.entries()) hold.linked = index > 0;
247
+ return holds;
248
+ }
249
+ #release(element, stack, hold, preserveFocus = false) {
250
+ const current = element.getAttribute("tabindex");
251
+ const index = stack.indexOf(hold);
252
+ const owns = current === hold.value && !stack.slice(index + 1).some((above2) => above2.value === current);
253
+ if (owns && preserveFocus && holdsFocus(element)) return false;
254
+ stack.splice(index, 1);
255
+ const above = stack[index];
256
+ if (above) above.linked &&= hold.linked;
257
+ this.#mark(element, stack);
258
+ if (!owns) return true;
259
+ const below = hold.linked ? stack[index - 1] : void 0;
260
+ if (!below) element.removeAttribute("tabindex");
261
+ else if (below.value !== current) element.setAttribute("tabindex", below.value);
262
+ return true;
263
+ }
264
+ #mark(element, stack) {
265
+ const records = /* @__PURE__ */ new Map([[this.#record, null]]);
266
+ for (const hold of stack) records.set(hold.record, hold.value);
267
+ for (const [name, recorded] of records) {
268
+ setAttributeIfChanged(element, name, recorded);
269
+ }
119
270
  }
120
271
  };
121
272
 
122
- // src/controllers/resizable_controller.ts
123
- var TRANSIENT = new TransientHooks({ attributes: ["data-dragging"] });
124
273
  var DEFAULT_MIN = 0;
125
274
  var DEFAULT_MAX = 100;
126
275
  var DEFAULT_VALUE = 50;
127
276
  var DEFAULT_STEP = 1;
128
- var ResizableController = class extends Controller {
277
+ var ResizableController = class _ResizableController extends Controller {
278
+ #numbers = new NumberValueReader();
129
279
  static targets = ["primary", "secondary", "separator"];
130
280
  static values = {
131
281
  min: { type: Number, default: DEFAULT_MIN },
@@ -133,55 +283,39 @@ var ResizableController = class extends Controller {
133
283
  step: { type: Number, default: DEFAULT_STEP },
134
284
  value: { type: Number, default: DEFAULT_VALUE }
135
285
  };
286
+ static valueConstraints = {
287
+ min: NUMBER_BOUNDS.finite,
288
+ max: NUMBER_BOUNDS.finite,
289
+ step: NUMBER_BOUNDS.positive,
290
+ value: NUMBER_BOUNDS.finite
291
+ };
136
292
  static actions = ["onKeydown", "onPointerDown", "toggle"];
137
293
  static events = ["change", "reconcile"];
138
- /** Where the divider sat before the current collapse; `null` when unknown. */
139
294
  #valueBeforeCollapse = null;
140
- /**
141
- * The position last painted: moved by each move the user makes — every step of
142
- * a drag included — and by each repaint that finds a page-driven move.
143
- */
144
295
  #painted = DEFAULT_VALUE;
145
- /**
146
- * The position last confirmed: the one connecting published, then moved by
147
- * each `change` and `reconcile`. A drag moves the painted position step by
148
- * step and this one only when it ends.
149
- */
150
296
  #confirmed = DEFAULT_VALUE;
151
- /** Aborts in-progress pointer-drag listeners when the drag ends or on teardown. */
152
- #dragAbort = null;
153
- /** `tabindex` lent to a pane so `F6` can put focus on it; panes carry none. */
154
- #paneTabindex = new TabindexLoan();
155
- /** Releases the pane cycle listener bound in {@link connect}. */
156
- #cycleAbort = null;
157
- /** Folds the range and position inputs of one morph batch into a single paint. */
158
- #repaint = new MicrotaskCoalescer(() => this.#render());
297
+ #drag = null;
298
+ #paneTabindex = new TabindexLoan("-1", this.identifier);
299
+ #listeners = new ListenerSet();
300
+ #repaint = new MorphRenderWatcher(() => this.#render());
159
301
  connect() {
160
- TRANSIENT.reset(this.element);
161
- this.#repaint.activate();
302
+ this.#paneTabindex.reclaimWithin(this.element);
303
+ this.#repaint.observe(this.element);
162
304
  this.#confirmed = this.#position;
163
305
  this.#render();
164
- this.#cycleAbort = new AbortController();
165
- this.element.addEventListener("keydown", this.#onCycleKeydown, {
166
- signal: this.#cycleAbort.signal
167
- });
306
+ this.#listeners.add(this.element, "keydown", this.#onCycleKeydown);
168
307
  }
169
- /** Cancels any active pointer drag so listeners never leak past disconnect. */
170
308
  disconnect() {
171
- this.#dragAbort?.abort();
172
- this.#dragAbort = null;
173
- this.#cycleAbort?.abort();
174
- this.#cycleAbort = null;
175
- this.#repaint.cancel();
309
+ this.#endDrag();
310
+ this.#listeners.dispose();
311
+ this.#repaint.disconnect();
176
312
  this.#paneTabindex.returnAll();
177
- this.element.removeAttribute("data-dragging");
178
313
  }
179
- /** Moves focus to the next pane on `F6`, wrapping; entering at the first one. */
180
314
  #onCycleKeydown = (event) => {
181
315
  if (event.key !== "F6") return;
182
316
  if (event.isComposing) return;
183
317
  if (event.defaultPrevented) return;
184
- if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;
318
+ if (hasModifierChord(event)) return;
185
319
  const panes = [];
186
320
  if (this.hasPrimaryTarget) panes.push(this.primaryTarget);
187
321
  if (this.hasSecondaryTarget) panes.push(this.secondaryTarget);
@@ -192,40 +326,42 @@ var ResizableController = class extends Controller {
192
326
  this.#paneTabindex.lend(next);
193
327
  next.focus();
194
328
  };
195
- /** Re-renders when application code (or a Turbo morph) changes `value` at runtime. */
196
329
  valueValueChanged() {
197
330
  this.#repaint.schedule();
198
331
  }
199
- /** Re-renders when application code (or a Turbo morph) changes `min` at runtime. */
200
332
  minValueChanged() {
201
333
  this.#repaint.schedule();
202
334
  }
203
- /** Re-renders when application code (or a Turbo morph) changes `max` at runtime. */
204
335
  maxValueChanged() {
205
336
  this.#repaint.schedule();
206
337
  }
207
- /** Re-publishes range and position onto a separator swapped in after connect. */
208
338
  separatorTargetConnected() {
209
339
  this.#repaint.schedule();
210
340
  }
211
- /** Starts active pointer drag tracking and locks capture. */
341
+ separatorTargetDisconnected(separator) {
342
+ if (this.#drag?.separator === separator && !this.separatorTargets.includes(separator)) {
343
+ this.#endDrag();
344
+ }
345
+ this.#repaint.schedule();
346
+ }
212
347
  onPointerDown(event) {
213
348
  if (!this.hasSeparatorTarget || event.button !== 0) return;
214
349
  event.preventDefault();
215
- const separator = this.separatorTarget;
350
+ const separators = this.separatorTargets;
351
+ const separator = separators[ownerIndex(separators, event.target)] ?? this.separatorTarget;
216
352
  separator.setPointerCapture(event.pointerId);
217
353
  separator.focus();
218
354
  this.element.setAttribute("data-dragging", "true");
219
- this.#dragAbort?.abort();
355
+ this.#drag?.abort.abort();
220
356
  const abort = new AbortController();
221
- this.#dragAbort = abort;
357
+ this.#drag = { separator, pointerId: event.pointerId, abort };
222
358
  separator.addEventListener("pointermove", this.#onPointerMove, { signal: abort.signal });
223
359
  separator.addEventListener("pointerup", this.#onPointerUp, { signal: abort.signal });
224
360
  separator.addEventListener("pointercancel", this.#onPointerUp, { signal: abort.signal });
225
361
  }
226
- /** Keydown adjustments for ArrowUp/Down/Left/Right and Home/End. */
227
362
  onKeydown(event) {
228
363
  if (isReservedArrowChord(event)) return;
364
+ if ((event.key === "Home" || event.key === "End") && hasModifierChord(event)) return;
229
365
  if (!this.hasSeparatorTarget) return;
230
366
  const { min, max } = this.#range;
231
367
  const isVertical = this.#isVertical;
@@ -279,7 +415,6 @@ var ResizableController = class extends Controller {
279
415
  this.#commit(nextValue);
280
416
  }
281
417
  }
282
- /** Collapses the primary pane to its minimum, or returns it to the last position. */
283
418
  toggle() {
284
419
  const { min, max } = this.#range;
285
420
  if (this.#position > min) {
@@ -298,26 +433,25 @@ var ResizableController = class extends Controller {
298
433
  const fraction = Number.isFinite(raw) ? Math.max(0, Math.min(raw, 1)) : 0;
299
434
  this.#move(Math.round(fraction * 100));
300
435
  };
301
- #onPointerUp = (event) => {
302
- this.element.removeAttribute("data-dragging");
303
- this.#dragAbort?.abort();
304
- this.#dragAbort = null;
305
- if (this.hasSeparatorTarget) {
306
- this.separatorTarget.releasePointerCapture(event.pointerId);
307
- }
436
+ #onPointerUp = () => {
437
+ this.#endDrag();
308
438
  this.#confirm();
309
439
  };
310
- /** Moves to a position the user chose and reports it if it moved. */
440
+ #endDrag() {
441
+ this.element.removeAttribute("data-dragging");
442
+ const drag = this.#drag;
443
+ this.#drag = null;
444
+ if (!drag) return;
445
+ drag.abort.abort();
446
+ try {
447
+ drag.separator.releasePointerCapture?.(drag.pointerId);
448
+ } catch {
449
+ }
450
+ }
311
451
  #commit(raw) {
312
452
  this.#move(raw);
313
453
  this.#confirm();
314
454
  }
315
- /**
316
- * Stores the clamped position the user moved to and paints it. The painted
317
- * position moves with it before anything is reported, so the repaint the Value
318
- * write schedules — and any move a subscriber makes while `change` is
319
- * dispatched — is measured from this position.
320
- */
321
455
  #move(raw) {
322
456
  const { min, max } = this.#range;
323
457
  const position = Math.max(min, Math.min(raw, max));
@@ -325,14 +459,6 @@ var ResizableController = class extends Controller {
325
459
  this.#painted = position;
326
460
  this.#paint();
327
461
  }
328
- /**
329
- * Publishes the position the Values declare without writing it back. A
330
- * position the user did not paint is the page's move, reported once as
331
- * `reconcile` unless it lands on the position last confirmed — a drag the page
332
- * puts back where it started has nothing new to report.
333
- *
334
- * @stimeoRenderRoot
335
- */
336
462
  #render() {
337
463
  this.#paint();
338
464
  const position = this.#position;
@@ -342,11 +468,9 @@ var ResizableController = class extends Controller {
342
468
  this.#confirmed = position;
343
469
  this.dispatch("reconcile", { detail: this.#detail(position) });
344
470
  }
345
- /**
346
- * Publishes the position: the fraction consumer CSS multiplies a pane by, and
347
- * the range assistive tech reads off the separator.
348
- */
349
471
  #paint() {
472
+ if (this.#drag) this.element.setAttribute("data-dragging", "true");
473
+ else this.element.removeAttribute("data-dragging");
350
474
  const { min, max } = this.#range;
351
475
  const position = this.#position;
352
476
  this.element.style.setProperty("--stimeo--resizable-fraction", String(position / 100));
@@ -356,43 +480,68 @@ var ResizableController = class extends Controller {
356
480
  this.separatorTarget.setAttribute("aria-valuemax", String(max));
357
481
  }
358
482
  }
359
- /**
360
- * Reports the published position as `change` when the user's operation moved
361
- * it from the position last confirmed. A key at an edge, a press released
362
- * without moving and a drag released where it started report nothing.
363
- */
364
483
  #confirm() {
365
484
  const position = this.#position;
366
485
  if (position === this.#confirmed) return;
367
486
  this.#confirmed = position;
368
487
  this.dispatch("change", { detail: this.#detail(position) });
369
488
  }
370
- /** The detail both events carry for `position`. */
371
489
  #detail(position) {
372
490
  return { value: position, fraction: position / 100 };
373
491
  }
374
- /** The declared range after validation; an unreadable bound uses its default. */
375
492
  get #range() {
376
- const min = Number.isFinite(this.minValue) ? this.minValue : DEFAULT_MIN;
377
- const max = Number.isFinite(this.maxValue) ? this.maxValue : DEFAULT_MAX;
493
+ const min = this.#safeMin;
494
+ const max = this.#safeMax;
378
495
  return { min, max: Math.max(min, max) };
379
496
  }
380
- /** The declared position, clamped into the validated range. */
381
497
  get #position() {
382
498
  const { min, max } = this.#range;
383
- const value = Number.isFinite(this.valueValue) ? this.valueValue : DEFAULT_VALUE;
499
+ const value = this.#safeValue;
384
500
  return Math.max(min, Math.min(value, max));
385
501
  }
386
- /** The keyboard increment; a non-positive or unreadable one uses the default. */
387
502
  get #step() {
388
- return Number.isFinite(this.stepValue) && this.stepValue > 0 ? this.stepValue : DEFAULT_STEP;
503
+ return this.#safeStep;
389
504
  }
390
- /** Whether the divider runs vertically; a separator that does not say is horizontal. */
391
505
  get #isVertical() {
392
506
  return this.hasSeparatorTarget && this.separatorTarget.getAttribute("aria-orientation") === "vertical";
393
507
  }
508
+ get #safeMin() {
509
+ return this.#numbers.read(
510
+ this,
511
+ "min",
512
+ this.minValue,
513
+ _ResizableController.values.min.default,
514
+ _ResizableController.valueConstraints.min
515
+ );
516
+ }
517
+ get #safeMax() {
518
+ return this.#numbers.read(
519
+ this,
520
+ "max",
521
+ this.maxValue,
522
+ _ResizableController.values.max.default,
523
+ _ResizableController.valueConstraints.max
524
+ );
525
+ }
526
+ get #safeStep() {
527
+ return this.#numbers.read(
528
+ this,
529
+ "step",
530
+ this.stepValue,
531
+ _ResizableController.values.step.default,
532
+ _ResizableController.valueConstraints.step
533
+ );
534
+ }
535
+ get #safeValue() {
536
+ return this.#numbers.read(
537
+ this,
538
+ "value",
539
+ this.valueValue,
540
+ _ResizableController.values.value.default,
541
+ _ResizableController.valueConstraints.value
542
+ );
543
+ }
394
544
  };
395
545
 
396
546
  export { ResizableController };
397
- //# sourceMappingURL=resizable_controller.js.map
398
547
  //# sourceMappingURL=resizable_controller.js.map