ember-primitives 0.60.1 → 0.61.1

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.
@@ -0,0 +1,631 @@
1
+ const GROUP_SELECTOR = '.ember-primitives__resizable';
2
+ const PANEL_CLASS = 'ember-primitives__resizable__panel';
3
+ const HANDLE_CLASS = 'ember-primitives__resizable__handle';
4
+ const MEMBER_SELECTOR = `.${PANEL_CLASS}, .${HANDLE_CLASS}`;
5
+ const DEFAULT_MIN = 0;
6
+ const DEFAULT_MAX = 100;
7
+
8
+ /**
9
+ * How far (in %) one keyboard arrow press moves a handle.
10
+ * Holding Shift moves in coarser increments.
11
+ */
12
+ const KEYBOARD_STEP = 1;
13
+ const KEYBOARD_STEP_COARSE = 10;
14
+ function clamp(value, min, max) {
15
+ return Math.min(Math.max(value, min), max);
16
+ }
17
+ function numberAttribute(element, name) {
18
+ const raw = element.getAttribute(name);
19
+ if (raw === null) return undefined;
20
+ const value = parseFloat(raw);
21
+ return Number.isFinite(value) ? value : undefined;
22
+ }
23
+
24
+ /**
25
+ * Panels declare their constraints in the DOM (via data attributes),
26
+ * so the group can discover everything it needs with queries --
27
+ * no registration required.
28
+ */
29
+ function minSizeOf(panel) {
30
+ return numberAttribute(panel, 'data-min-size') ?? DEFAULT_MIN;
31
+ }
32
+ function maxSizeOf(panel) {
33
+ return numberAttribute(panel, 'data-max-size') ?? DEFAULT_MAX;
34
+ }
35
+ function requestedSizeOf(panel) {
36
+ return numberAttribute(panel, 'data-size');
37
+ }
38
+ function isCollapsible(panel) {
39
+ return panel.hasAttribute('data-collapsible');
40
+ }
41
+ function sameMembers(a, b) {
42
+ return a.length === b.length && a.every((element, index) => element === b[index]);
43
+ }
44
+ function precedes(a, b) {
45
+ return Boolean(b.compareDocumentPosition(a) & Node.DOCUMENT_POSITION_PRECEDING);
46
+ }
47
+
48
+ /**
49
+ * The `data-collapsed` attribute is the source of truth for collapse
50
+ * state (it is also the styling hook consumers use).
51
+ */
52
+ function isCollapsed(panel) {
53
+ return isCollapsible(panel) && panel.hasAttribute('data-collapsed');
54
+ }
55
+ function setCollapsed(panel, collapsed) {
56
+ if (collapsed === panel.hasAttribute('data-collapsed')) return;
57
+ if (collapsed) {
58
+ panel.setAttribute('data-collapsed', '');
59
+ } else {
60
+ panel.removeAttribute('data-collapsed');
61
+ }
62
+ }
63
+
64
+ /**
65
+ * The panels immediately before and after the given handle element,
66
+ * in document order.
67
+ */
68
+ function neighborsOf(handleElement, panels) {
69
+ let prev = null;
70
+ let next = null;
71
+ for (const panel of panels) {
72
+ const position = handleElement.compareDocumentPosition(panel);
73
+ if (position & Node.DOCUMENT_POSITION_PRECEDING) {
74
+ prev = panel;
75
+ } else if (position & Node.DOCUMENT_POSITION_FOLLOWING) {
76
+ next = panel;
77
+ break;
78
+ }
79
+ }
80
+ return [prev, next];
81
+ }
82
+
83
+ /**
84
+ * Sizes are floats that get re-derived (measurement, normalization),
85
+ * so "unchanged" means within a small tolerance.
86
+ */
87
+ function isSameSize(existing, size) {
88
+ return existing !== undefined && Math.abs(existing - size) < 0.0001;
89
+ }
90
+
91
+ /**
92
+ * Pixel measurements round to device pixels, so re-measuring a layout
93
+ * yields values that differ from the stored ones by sub-pixel noise.
94
+ * Only differences beyond this (in %) count as real drift worth
95
+ * adopting -- re-encoding identical pixels as slightly different
96
+ * percentages would dirty every panel for no visual change.
97
+ */
98
+ const MEASUREMENT_TOLERANCE = 0.25;
99
+
100
+ /**
101
+ * Skips the write when the attribute already has the desired value,
102
+ * so unchanged elements are left untouched.
103
+ */
104
+ function setAttribute(element, name, value) {
105
+ if (element.getAttribute(name) !== value) {
106
+ element.setAttribute(name, value);
107
+ }
108
+ }
109
+ class GroupState {
110
+ element = null;
111
+ #options;
112
+ #drag = null;
113
+
114
+ /**
115
+ * Current size (%) per panel element.
116
+ * The DOM is the source of truth for membership; this only remembers
117
+ * sizes across relayouts.
118
+ */
119
+ #sizes = new Map();
120
+
121
+ /**
122
+ * The size (%) a collapsible panel had before it was collapsed,
123
+ * so that expanding restores it.
124
+ */
125
+ #previousSizes = new WeakMap();
126
+
127
+ /**
128
+ * Membership as of the last layout, so mutation batches that don't
129
+ * change membership (e.g. content changes inside a panel, or churn
130
+ * within a nested group) can be ignored.
131
+ */
132
+ #knownPanels = [];
133
+ #knownHandles = [];
134
+
135
+ /**
136
+ * The percentage that a flex-grow of 1 represents in this group.
137
+ *
138
+ * Panels without an inline style render at the CSS default
139
+ * (`flex: 1 1 0px`), so an all-equal group needs no styles at all --
140
+ * mounting one writes nothing. The unit is fixed the first time a
141
+ * panel actually diverges, and from then on grow values are written
142
+ * relative to it, so panels whose share doesn't change keep their
143
+ * (possibly absent) style untouched.
144
+ */
145
+ #unit = null;
146
+ constructor(options) {
147
+ this.#options = options;
148
+ }
149
+ get orientation() {
150
+ return this.#options.orientation() ?? 'horizontal';
151
+ }
152
+ get #isHorizontal() {
153
+ return this.orientation === 'horizontal';
154
+ }
155
+
156
+ /**
157
+ * This group's panels, in document order.
158
+ * Panels of nested groups belong to their own group, not this one.
159
+ */
160
+ get panels() {
161
+ return this.#members().panels;
162
+ }
163
+ get handles() {
164
+ return this.#members().handles;
165
+ }
166
+ #members() {
167
+ const panels = [];
168
+ const handles = [];
169
+ const element = this.element;
170
+ if (!element) return {
171
+ panels,
172
+ handles
173
+ };
174
+ for (const member of element.querySelectorAll(MEMBER_SELECTOR)) {
175
+ if (member.closest(GROUP_SELECTOR) !== element) continue;
176
+ (member.classList.contains(PANEL_CLASS) ? panels : handles).push(member);
177
+ }
178
+ return {
179
+ panels,
180
+ handles
181
+ };
182
+ }
183
+ get sizes() {
184
+ return this.panels.map(panel => this.#sizes.get(panel) ?? 0);
185
+ }
186
+
187
+ /**
188
+ * One MutationObserver for every group on the page. Each record is
189
+ * routed to the group that owns it (the nearest group ancestor), so
190
+ * churn inside a nested group never even pings its ancestors.
191
+ */
192
+ static #observed = new Map();
193
+ static #observer = null;
194
+ static #observerOptions = {
195
+ childList: true,
196
+ subtree: true,
197
+ attributes: true,
198
+ attributeFilter: ['data-orientation'],
199
+ // to distinguish real changes from same-value writes (see below)
200
+ attributeOldValue: true
201
+ };
202
+ static #handleMutations(mutations) {
203
+ /**
204
+ * true = the group's own data-orientation changed,
205
+ * false = something in its subtree changed (membership check needed)
206
+ */
207
+ const affected = new Map();
208
+ for (const mutation of mutations) {
209
+ const target = mutation.target;
210
+ if (!(target instanceof Element)) continue;
211
+ if (mutation.type === 'attributes') {
212
+ const group = GroupState.#observed.get(target);
213
+
214
+ /**
215
+ * setAttribute queues a record even when the value is
216
+ * unchanged (renderers do rewrite attributes with the same
217
+ * value); only actual changes warrant a relayout.
218
+ */
219
+ const changed = mutation.attributeName && mutation.oldValue !== target.getAttribute(mutation.attributeName);
220
+ if (group && changed) affected.set(group, true);
221
+ continue;
222
+ }
223
+ const groupElement = target.closest(GROUP_SELECTOR);
224
+ const group = groupElement && GroupState.#observed.get(groupElement);
225
+ if (group && !affected.has(group)) affected.set(group, false);
226
+ }
227
+ for (const [group, orientationChanged] of affected) {
228
+ const members = group.#members();
229
+ if (orientationChanged || group.#membershipChanged(members)) group.#layout(members);
230
+ }
231
+ }
232
+ static #observe(element, group) {
233
+ GroupState.#observed.set(element, group);
234
+ GroupState.#observer ??= new MutationObserver(mutations => GroupState.#handleMutations(mutations));
235
+ GroupState.#observer.observe(element, GroupState.#observerOptions);
236
+ }
237
+ static #unobserve(element) {
238
+ GroupState.#observed.delete(element);
239
+ const observer = GroupState.#observer;
240
+ if (!observer) return;
241
+
242
+ /**
243
+ * MutationObserver has no per-target unobserve; disconnect and
244
+ * re-observe the remaining groups (rare -- teardown only).
245
+ */
246
+ observer.disconnect();
247
+ if (GroupState.#observed.size === 0) {
248
+ GroupState.#observer = null;
249
+ return;
250
+ }
251
+ for (const remaining of GroupState.#observed.keys()) {
252
+ observer.observe(remaining, GroupState.#observerOptions);
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Called (via modifier) when the group element is inserted.
258
+ * Watches for panels being added/removed (and the orientation
259
+ * changing) and performs the initial layout.
260
+ */
261
+ attach = element => {
262
+ this.element = element;
263
+ GroupState.#observe(element, this);
264
+ this.#layout();
265
+ return () => {
266
+ GroupState.#unobserve(element);
267
+ this.element = null;
268
+ };
269
+ };
270
+ #membershipChanged(members) {
271
+ return !sameMembers(members.panels, this.#knownPanels) || !sameMembers(members.handles, this.#knownHandles);
272
+ }
273
+
274
+ /**
275
+ * Panels that already have a size (or request one via `data-size`)
276
+ * keep it; new panels take a share of the remaining space; everything
277
+ * is normalized to 100.
278
+ */
279
+ #layout(members = this.#members()) {
280
+ const {
281
+ panels,
282
+ handles
283
+ } = members;
284
+ if (this.#drag) this.#drag.members = members;
285
+ this.#knownPanels = panels;
286
+ this.#knownHandles = handles;
287
+ if (panels.length === 0) return;
288
+ let changed = false;
289
+
290
+ // forget sizes of panels that left the DOM
291
+ const current = new Set(panels);
292
+ for (const known of this.#sizes.keys()) {
293
+ if (!current.has(known)) {
294
+ this.#sizes.delete(known);
295
+ changed = true;
296
+ }
297
+ }
298
+
299
+ // scratch space for the math below; #sizes itself is only
300
+ // touched at the end, and only where values actually changed
301
+ const computed = new Map();
302
+ const unspecified = [];
303
+ let specifiedTotal = 0;
304
+ for (const panel of panels) {
305
+ const preferred = this.#sizes.get(panel) ?? requestedSizeOf(panel);
306
+ if (preferred === undefined) {
307
+ unspecified.push(panel);
308
+ continue;
309
+ }
310
+ const size = isCollapsed(panel) ? preferred : clamp(preferred, minSizeOf(panel), maxSizeOf(panel));
311
+ computed.set(panel, size);
312
+ specifiedTotal += size;
313
+ }
314
+ if (unspecified.length > 0) {
315
+ /**
316
+ * Panels without a size share the remaining space.
317
+ * When there is none left (e.g. a panel was added to an
318
+ * already-full group), each takes an equal 1/n share and the
319
+ * existing panels scale down to make room -- like a new window
320
+ * opening in a tiling window manager.
321
+ */
322
+ const remaining = 100 - specifiedTotal;
323
+ let share = remaining / unspecified.length;
324
+ if (remaining < 1) {
325
+ share = 100 / panels.length;
326
+ if (specifiedTotal > 0) {
327
+ const scale = Math.max(100 - share * unspecified.length, 0) / specifiedTotal;
328
+ for (const [panel, size] of computed) {
329
+ computed.set(panel, size * scale);
330
+ }
331
+ }
332
+ }
333
+ for (const panel of unspecified) {
334
+ computed.set(panel, clamp(share, minSizeOf(panel), maxSizeOf(panel)));
335
+ }
336
+ }
337
+
338
+ // normalize to 100
339
+ let total = 0;
340
+ for (const size of computed.values()) total += size;
341
+ if (total > 0 && Math.abs(total - 100) > 0.01) {
342
+ for (const [panel, size] of computed) {
343
+ computed.set(panel, size / total * 100);
344
+ }
345
+ }
346
+
347
+ /**
348
+ * Commit minimally: keep the #sizes map, update only entries whose
349
+ * value actually changed (with a small tolerance, so float dust
350
+ * from re-normalizing doesn't count as a change).
351
+ */
352
+ const changedPanels = [];
353
+ for (const [panel, size] of computed) {
354
+ if (!isSameSize(this.#sizes.get(panel), size)) {
355
+ this.#sizes.set(panel, size);
356
+ changedPanels.push(panel);
357
+ changed = true;
358
+ }
359
+ }
360
+ this.#apply(changedPanels, members);
361
+ if (changed) this.#notify(panels);
362
+ }
363
+
364
+ /**
365
+ * Writes the layout back to the DOM: flex sizing for exactly the
366
+ * candidate panels whose rendered share would actually change, plus
367
+ * the handles' ARIA attributes (which are guarded per-attribute).
368
+ * (`data-collapsed` is managed at the explicit collapse/expand
369
+ * points, not derived from sizes -- rendered pixel sizes include
370
+ * borders/padding, so a collapsed panel rarely measures exactly 0.)
371
+ */
372
+ #apply(candidates, members) {
373
+ /**
374
+ * With no unit fixed yet, nothing has ever been written, so every
375
+ * panel renders at the CSS default -- an equal 1/n share.
376
+ */
377
+ const unit = this.#unit ?? 100 / members.panels.length;
378
+ for (const panel of candidates) {
379
+ const size = this.#sizes.get(panel);
380
+ if (size === undefined) continue;
381
+ const inlineGrow = panel.style.flexGrow;
382
+ const impliedPercent = (inlineGrow === '' ? 1 : parseFloat(inlineGrow)) * unit;
383
+ if (isSameSize(impliedPercent, size)) continue;
384
+ this.#unit ??= unit;
385
+ panel.style.flex = `${size / unit} 1 0px`;
386
+ }
387
+ this.#syncHandles(members);
388
+ }
389
+
390
+ /**
391
+ * Per the window-splitter pattern, each handle describes the panel
392
+ * immediately before it. (A splitter between two side-by-side panes
393
+ * is oriented *vertically*, and vice-versa.)
394
+ */
395
+ #syncHandles(members) {
396
+ const {
397
+ panels,
398
+ handles
399
+ } = members;
400
+ const ariaOrientation = this.#isHorizontal ? 'vertical' : 'horizontal';
401
+ let index = 0;
402
+ for (const handle of handles) {
403
+ let following = panels[index];
404
+ while (following && precedes(following, handle)) {
405
+ index++;
406
+ following = panels[index];
407
+ }
408
+ const prev = panels[index - 1];
409
+ setAttribute(handle, 'aria-orientation', ariaOrientation);
410
+ if (!prev) continue;
411
+
412
+ // Panels render their own (component-owned, incrementing) id
413
+ if (prev.id) setAttribute(handle, 'aria-controls', prev.id);
414
+ setAttribute(handle, 'aria-valuemin', `${minSizeOf(prev)}`);
415
+ setAttribute(handle, 'aria-valuemax', `${maxSizeOf(prev)}`);
416
+ setAttribute(handle, 'aria-valuenow', `${Math.round(this.#sizes.get(prev) ?? 0)}`);
417
+ }
418
+ }
419
+ #notify(panels) {
420
+ this.#options.onLayoutChange()?.(panels.map(panel => this.#sizes.get(panel) ?? 0));
421
+ }
422
+
423
+ /**
424
+ * Re-derive percentage sizes from actual rendered pixels.
425
+ * Corrects any drift (e.g. from CSS min-sizes) before an interaction.
426
+ */
427
+ #syncSizesFromDOM(panels, measured) {
428
+ // collapsed panels are 0 even though their borders/padding measure larger
429
+ const px = panels.map((panel, index) => isCollapsed(panel) ? 0 : measured?.[index] ?? this.#pixelSizeOf(panel));
430
+ const total = px.reduce((sum, value) => sum + value, 0);
431
+ if (total <= 0) return;
432
+ panels.forEach((panel, index) => {
433
+ const size = (px[index] ?? 0) / total * 100;
434
+ const existing = this.#sizes.get(panel);
435
+ if (existing === undefined || Math.abs(existing - size) > MEASUREMENT_TOLERANCE) {
436
+ this.#sizes.set(panel, size);
437
+ }
438
+ });
439
+ }
440
+ #pixelSizeOf(panel) {
441
+ const box = panel.getBoundingClientRect();
442
+ return this.#isHorizontal ? box.width : box.height;
443
+ }
444
+
445
+ /**
446
+ * Moves the boundary between the two panels by `requestedDelta` (%),
447
+ * respecting both panels' min/max constraints.
448
+ */
449
+ #applyDelta(prev, next, basePrevSize, baseNextSize, requestedDelta, members) {
450
+ const total = basePrevSize + baseNextSize;
451
+ const prevMin = minSizeOf(prev);
452
+ const prevMax = maxSizeOf(prev);
453
+ const prevCollapsible = isCollapsible(prev);
454
+ let target = basePrevSize + requestedDelta;
455
+ if (prevCollapsible && target < prevMin) {
456
+ /**
457
+ * Collapsible panels snap: below half the minimum they close
458
+ * entirely; between half and the minimum they hold at the minimum.
459
+ * (This also keeps a collapsed panel closed when its handle is
460
+ * dragged further in the closing direction.)
461
+ */
462
+ target = target < prevMin / 2 ? 0 : prevMin;
463
+ } else {
464
+ target = clamp(target, prevMin, prevMax);
465
+ }
466
+
467
+ /**
468
+ * The neighbor absorbs whatever the target panel gives or takes,
469
+ * so its constraints bound the target too.
470
+ */
471
+ const nextMin = total - maxSizeOf(next);
472
+ const nextMax = total - minSizeOf(next);
473
+ if (nextMin > nextMax) return;
474
+ target = clamp(target, nextMin, nextMax);
475
+
476
+ // both panels' constraints cannot be satisfied at once
477
+ if (!prevCollapsible && (target < prevMin || target > prevMax)) return;
478
+
479
+ /**
480
+ * Nothing to do when the clamped result matches the current sizes
481
+ * (e.g. every pointermove past a min/max limit).
482
+ */
483
+ if (isSameSize(this.#sizes.get(prev), target) && isSameSize(this.#sizes.get(next), total - target)) {
484
+ return;
485
+ }
486
+ if (prevCollapsible) {
487
+ if (target === 0 && !isCollapsed(prev)) {
488
+ this.#previousSizes.set(prev, basePrevSize);
489
+ }
490
+ setCollapsed(prev, target === 0);
491
+ }
492
+ this.#sizes.set(prev, target);
493
+ this.#sizes.set(next, total - target);
494
+ this.#apply([prev, next], members);
495
+ this.#notify(members.panels);
496
+ }
497
+ startDrag(handleElement, event) {
498
+ if (event.button !== 0) return;
499
+ if (this.#drag) return;
500
+ const members = this.#members();
501
+ const [prev, next] = neighborsOf(handleElement, members.panels);
502
+ if (!prev || !next) return;
503
+ const measured = members.panels.map(panel => this.#pixelSizeOf(panel));
504
+ this.#syncSizesFromDOM(members.panels, measured);
505
+ const move = moveEvent => this.#dragMove(moveEvent);
506
+ const end = endEvent => this.#endDrag(handleElement, endEvent);
507
+ this.#drag = {
508
+ prev,
509
+ next,
510
+ startPrevSize: this.#sizes.get(prev) ?? 0,
511
+ startNextSize: this.#sizes.get(next) ?? 0,
512
+ startCoordinate: this.#isHorizontal ? event.clientX : event.clientY,
513
+ totalPx: measured.reduce((sum, value) => sum + value, 0),
514
+ members,
515
+ move,
516
+ end
517
+ };
518
+ try {
519
+ /**
520
+ * Retargets all subsequent pointer events to the handle,
521
+ * even when the pointer is over an iframe.
522
+ */
523
+ handleElement.setPointerCapture(event.pointerId);
524
+ } catch {
525
+ // synthetic events (tests) may not have an active pointer
526
+ }
527
+ handleElement.addEventListener('pointermove', move);
528
+ handleElement.addEventListener('pointerup', end);
529
+ handleElement.addEventListener('pointercancel', end);
530
+ handleElement.setAttribute('data-resizing', '');
531
+ document.body.style.cursor = this.#isHorizontal ? 'col-resize' : 'row-resize';
532
+ document.body.style.userSelect = 'none';
533
+ }
534
+ #dragMove(event) {
535
+ const drag = this.#drag;
536
+ if (!drag) return;
537
+ if (drag.totalPx <= 0) return;
538
+ const coordinate = this.#isHorizontal ? event.clientX : event.clientY;
539
+ const deltaPercent = (coordinate - drag.startCoordinate) / drag.totalPx * 100;
540
+ this.#applyDelta(drag.prev, drag.next, drag.startPrevSize, drag.startNextSize, deltaPercent, drag.members);
541
+ }
542
+ #endDrag(handleElement, event) {
543
+ const drag = this.#drag;
544
+ if (!drag) return;
545
+ this.#drag = null;
546
+ try {
547
+ handleElement.releasePointerCapture(event.pointerId);
548
+ } catch {
549
+ // may not have been captured (tests)
550
+ }
551
+ handleElement.removeEventListener('pointermove', drag.move);
552
+ handleElement.removeEventListener('pointerup', drag.end);
553
+ handleElement.removeEventListener('pointercancel', drag.end);
554
+ handleElement.removeAttribute('data-resizing');
555
+ document.body.style.cursor = '';
556
+ document.body.style.userSelect = '';
557
+ }
558
+
559
+ /**
560
+ * Keyboard support for the WAI-ARIA window-splitter pattern.
561
+ */
562
+ handleKeyDown(handleElement, event) {
563
+ const members = this.#members();
564
+ const [prev, next] = neighborsOf(handleElement, members.panels);
565
+ if (!prev || !next) return;
566
+ if (event.key === 'Enter') {
567
+ this.#toggleCollapse(prev, next, members);
568
+ return;
569
+ }
570
+ const step = event.shiftKey ? KEYBOARD_STEP_COARSE : KEYBOARD_STEP;
571
+ const isHorizontal = this.#isHorizontal;
572
+ let toDelta = null;
573
+ switch (event.key) {
574
+ case 'ArrowLeft':
575
+ if (isHorizontal) toDelta = () => -step;
576
+ break;
577
+ case 'ArrowRight':
578
+ if (isHorizontal) toDelta = () => step;
579
+ break;
580
+ case 'ArrowUp':
581
+ if (!isHorizontal) toDelta = () => -step;
582
+ break;
583
+ case 'ArrowDown':
584
+ if (!isHorizontal) toDelta = () => step;
585
+ break;
586
+ case 'Home':
587
+ toDelta = prevSize => minSizeOf(prev) - prevSize;
588
+ break;
589
+ case 'End':
590
+ toDelta = prevSize => maxSizeOf(prev) - prevSize;
591
+ break;
592
+ }
593
+ if (!toDelta) return;
594
+ event.preventDefault();
595
+ this.#syncSizesFromDOM(members.panels);
596
+ const prevSize = this.#sizes.get(prev) ?? 0;
597
+ this.#applyDelta(prev, next, prevSize, this.#sizes.get(next) ?? 0, toDelta(prevSize), members);
598
+ }
599
+
600
+ /**
601
+ * Collapses (or restores) the panel before the handle,
602
+ * giving the space to (or taking it from) the panel after it.
603
+ *
604
+ * Only does anything when the preceding panel is `@collapsible`.
605
+ */
606
+ #toggleCollapse(prev, next, members) {
607
+ if (!isCollapsible(prev)) return;
608
+ this.#syncSizesFromDOM(members.panels);
609
+ const prevSize = this.#sizes.get(prev) ?? 0;
610
+ const nextSize = this.#sizes.get(next) ?? 0;
611
+ if (isCollapsed(prev)) {
612
+ const preferred = this.#previousSizes.get(prev) ?? requestedSizeOf(prev) ?? Math.max(minSizeOf(prev), 10);
613
+ const available = nextSize - minSizeOf(next);
614
+ const restored = Math.min(preferred, available);
615
+ if (restored <= 0) return;
616
+ setCollapsed(prev, false);
617
+ this.#sizes.set(prev, restored);
618
+ this.#sizes.set(next, nextSize - restored);
619
+ } else {
620
+ this.#previousSizes.set(prev, prevSize);
621
+ setCollapsed(prev, true);
622
+ this.#sizes.set(prev, 0);
623
+ this.#sizes.set(next, nextSize + prevSize);
624
+ }
625
+ this.#apply([prev, next], members);
626
+ this.#notify(members.panels);
627
+ }
628
+ }
629
+
630
+ export { GroupState };
631
+ //# sourceMappingURL=state.js.map