staffa 0.8.0 → 0.8.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.
- package/README.md +6 -2
- package/dist/components/main.d.ts +5 -0
- package/dist/components/main.js +35 -13
- package/dist/components/panels.d.ts +51 -8
- package/dist/components/panels.js +181 -83
- package/dist/staffa.esm.js +1 -1
- package/package.json +1 -1
- package/skill/MainOptions.md +5 -0
- package/skill/Page.md +13 -2
- package/skill/SKILL.md +6 -2
- package/src/components/main.ts +39 -12
- package/src/components/panels.ts +214 -89
|
@@ -122,21 +122,32 @@ const SHELL_PX = 1280;
|
|
|
122
122
|
const GUTTER_PX = 24;
|
|
123
123
|
/** Don't pair smalls when half the content area would be narrower than this. */
|
|
124
124
|
const PAIR_MIN_PX = 360;
|
|
125
|
+
/**
|
|
126
|
+
* Panels are layered by their depth in the stack, two `z-index` steps per panel:
|
|
127
|
+
* a panel sits on the odd layer for its depth, and a *closing* one drops to the
|
|
128
|
+
* even layer just below, where it is frozen for the length of its fade. So a
|
|
129
|
+
* panel that replaces another comes in over it, while one that closes fades out
|
|
130
|
+
* over whatever it was covering — which is the way round both should read.
|
|
131
|
+
*/
|
|
132
|
+
const LAYER_STEP = 2;
|
|
125
133
|
// ─── Module-level styling ────────────────────────────────────────────────────
|
|
126
134
|
A.insertGlobalCss({
|
|
127
135
|
":root": `--s-panel-ms:${PANEL_MS}ms`,
|
|
128
136
|
// The clipping viewport that the columns slide through. Panels are absolutely
|
|
129
137
|
// positioned inside it, with their width and x offset set from JS (see
|
|
130
|
-
// `layout()`), so they can animate between arrangements.
|
|
131
|
-
|
|
138
|
+
// `layout()`), so they can animate between arrangements. `isolation` keeps the
|
|
139
|
+
// layers they stack themselves in (see LAYER_STEP) to themselves: the region
|
|
140
|
+
// as a whole still sits under the shell's own chrome — the sticky top bar, and
|
|
141
|
+
// the nav page that slides across the body — however deep the stack gets.
|
|
142
|
+
".s-panels": "flex:1 min-width:0 min-height:0 position:relative overflow:hidden isolation:isolate",
|
|
132
143
|
".s-panel": {
|
|
133
144
|
// A panel rests at a plain `left` offset and carries no transform: a
|
|
134
145
|
// transformed element is composited, which costs it subpixel text
|
|
135
146
|
// antialiasing. `transform` is used only to play the enter/exit slides,
|
|
136
|
-
// where the compositing is what makes them cheap. There is deliberately
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
147
|
+
// where the compositing is what makes them cheap. There is deliberately no
|
|
148
|
+
// `width` transition: a width changes only when the window resizes or when
|
|
149
|
+
// the page itself asks for another layout, and animating one would reflow
|
|
150
|
+
// the column's content on every frame of it.
|
|
140
151
|
// Every duration is `--s-panel-ms`, so a column's move, its neighbour's fade
|
|
141
152
|
// and the chrome recentering around them all run as one motion. The drift
|
|
142
153
|
// eases out (it should read as a slow settle) while the fade runs *linear*
|
|
@@ -144,6 +155,10 @@ A.insertGlobalCss({
|
|
|
144
155
|
// zero, which looks like the panel vanishing rather than fading.
|
|
145
156
|
// No `overflow:hidden` here: the scroll container below clips the content
|
|
146
157
|
// itself, and the pair hairline sits in the gutter *outside* the panel.
|
|
158
|
+
// Layering is set from JS (`layout()` and `beginClose`) rather than left to
|
|
159
|
+
// DOM order: a closing panel is no longer part of the reactive list, so
|
|
160
|
+
// where its element sits among the live ones is Aberdeen's business, not a
|
|
161
|
+
// thing to depend on. `LAYER_*` says what the numbers mean.
|
|
147
162
|
"&": "position:absolute top:0 bottom:0 left:0 display:flex flex-direction:column " +
|
|
148
163
|
"visibility:visible transition: left var(--s-panel-ms) ease, transform var(--s-panel-ms) ease-out, opacity var(--s-panel-ms) linear, visibility 0s;",
|
|
149
164
|
// The scroll container. Mirrors content mode's `main > .s-content`: same
|
|
@@ -163,8 +178,8 @@ A.insertGlobalCss({
|
|
|
163
178
|
// dropped, which is what makes the panel settle instead of jumping.
|
|
164
179
|
"&.s-panel-enter": "opacity:0 transition:none transform: translateX(8cqw);",
|
|
165
180
|
// On its way out: fading where it stands, drifting the same short distance,
|
|
166
|
-
// and out of reach while it does. It
|
|
167
|
-
//
|
|
181
|
+
// and out of reach while it does. It leaves the DOM when the fade itself
|
|
182
|
+
// ends (see `playExit`), never part-way through it.
|
|
168
183
|
"&.s-panel-closing": "opacity:0 pointer-events:none transform: translateX(8cqw);",
|
|
169
184
|
// Crowded out from under the visible run. It keeps its DOM (and thus its
|
|
170
185
|
// scroll position and half-typed forms), so `display:none` is out —
|
|
@@ -216,6 +231,8 @@ export class PanelController {
|
|
|
216
231
|
*/
|
|
217
232
|
$state = A.proxy({ paths: [], topId: 0 });
|
|
218
233
|
containerEl;
|
|
234
|
+
/** The shell's measurements, shared by everything drawn since they were taken. */
|
|
235
|
+
geom;
|
|
219
236
|
/** The body width at the last layout; a change means a window resize → snap. */
|
|
220
237
|
lastBodyW = -1;
|
|
221
238
|
layoutQueued = false;
|
|
@@ -346,6 +363,9 @@ export class PanelController {
|
|
|
346
363
|
* rule 5 promises to keep.
|
|
347
364
|
*/
|
|
348
365
|
commit(target, nav) {
|
|
366
|
+
// The panels this commit mounts size themselves as they draw, so make them
|
|
367
|
+
// measure the shell as it is now rather than trusting the last pass's numbers.
|
|
368
|
+
this.geom = undefined;
|
|
349
369
|
const existing = new Map(this.live.map((entry) => [entry.path, entry]));
|
|
350
370
|
const next = [];
|
|
351
371
|
for (const path of target) {
|
|
@@ -398,34 +418,53 @@ export class PanelController {
|
|
|
398
418
|
return entry;
|
|
399
419
|
}
|
|
400
420
|
/**
|
|
401
|
-
*
|
|
402
|
-
*
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
*
|
|
421
|
+
* Take a panel out of the shell. The *scope* goes now: its cleaners run this
|
|
422
|
+
* tick, so whatever the panel registered with `A.clean` — subscriptions,
|
|
423
|
+
* timers, an open portal — is torn down when the panel closes, not when its
|
|
424
|
+
* animation is over. Only the element lingers, to play that animation, which
|
|
425
|
+
* is what the `destroy=` hook in `drawPanel` is for: Aberdeen hands the
|
|
426
|
+
* element to {@link playExit} instead of removing it.
|
|
407
427
|
*/
|
|
408
428
|
beginClose(entry) {
|
|
409
429
|
entry.closing = true;
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
430
|
+
// Frozen one layer below where it was, which is still above everything it
|
|
431
|
+
// was covering: it fades out over the panel it uncovers, and under the one
|
|
432
|
+
// that takes its place (see LAYER_STEP). Set here, while the element is
|
|
433
|
+
// still ours — a moment later the scope, and with it `entry.el`, is gone.
|
|
434
|
+
if (entry.el)
|
|
435
|
+
entry.el.style.zIndex = String(LAYER_STEP * this.live.indexOf(entry));
|
|
436
|
+
this.byId.delete(entry.id);
|
|
437
|
+
delete this.$ids[String(entry.id)];
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* A closed panel's send-off, run by Aberdeen once the panel's scope is gone (so
|
|
441
|
+
* the content it shows is frozen, which is exactly what a departing column
|
|
442
|
+
* should be): it fades where it stands, inert, and leaves the DOM when the fade
|
|
443
|
+
* itself ends. Removing it on a fixed timer instead would race the transition —
|
|
444
|
+
* pull the element a frame early and the panel appears to fade half-way and
|
|
445
|
+
* then vanish. The timeout is just a fallback for when no `transitionend` is
|
|
446
|
+
* coming at all (transitions off, or an element that never got placed).
|
|
447
|
+
*/
|
|
448
|
+
playExit(entry, el) {
|
|
449
|
+
// Only a close is worth animating. A panel being *redrawn* (a reactive
|
|
450
|
+
// dependency in its handler) replaces its element through here too, and that
|
|
451
|
+
// one simply goes, so the new one isn't drawn over a ghost of the old.
|
|
452
|
+
if (!entry.closing) {
|
|
453
|
+
el.remove();
|
|
454
|
+
return;
|
|
424
455
|
}
|
|
425
|
-
|
|
456
|
+
el.classList.add("s-panel-closing");
|
|
457
|
+
el.setAttribute("inert", "");
|
|
458
|
+
const drop = () => {
|
|
459
|
+
clearTimeout(timer);
|
|
426
460
|
this.timers.delete(timer);
|
|
427
|
-
|
|
428
|
-
}
|
|
461
|
+
el.remove();
|
|
462
|
+
};
|
|
463
|
+
el.addEventListener("transitionend", (e) => {
|
|
464
|
+
if (e.target === el && e.propertyName === "opacity")
|
|
465
|
+
drop();
|
|
466
|
+
});
|
|
467
|
+
const timer = setTimeout(drop, PANEL_MS + 80);
|
|
429
468
|
this.timers.add(timer);
|
|
430
469
|
}
|
|
431
470
|
// ── Navigation ─────────────────────────────────────────────────────────
|
|
@@ -576,9 +615,13 @@ export class PanelController {
|
|
|
576
615
|
*/
|
|
577
616
|
drawStack() {
|
|
578
617
|
const container = A("div.s-panels role=main", () => {
|
|
618
|
+
// Published before the first panel draws, rather than from the return
|
|
619
|
+
// value below: a panel sizes itself from the shell's measurements (see
|
|
620
|
+
// `measure`), and the first ones do that while this very call is still
|
|
621
|
+
// running. `A()` without arguments is "the element we're in".
|
|
622
|
+
this.containerEl = A();
|
|
579
623
|
A.onEach(this.$ids, (_order, id) => this.drawPanel(Number(id)), (order, id) => [order, Number(id)]);
|
|
580
624
|
});
|
|
581
|
-
this.containerEl = container;
|
|
582
625
|
if (typeof ResizeObserver !== "undefined") {
|
|
583
626
|
const ro = new ResizeObserver(() => this.layout());
|
|
584
627
|
// The region *and* the body it sits in: the region alone misses a shell
|
|
@@ -597,7 +640,30 @@ export class PanelController {
|
|
|
597
640
|
const entry = this.byId.get(id);
|
|
598
641
|
if (!entry)
|
|
599
642
|
return;
|
|
600
|
-
|
|
643
|
+
let el;
|
|
644
|
+
// How much room the panel wants, resolved *before* its content is drawn: an
|
|
645
|
+
// element that arrives without a width has no box for its content to measure
|
|
646
|
+
// itself against until the next frame's layout pass, which is a frame too
|
|
647
|
+
// late for anything that sizes itself from its container. So the panel is
|
|
648
|
+
// created at the width the window gives its layout — "medium" until the page
|
|
649
|
+
// says otherwise. Reactively, too: a page that changes its mind later (when
|
|
650
|
+
// its data arrives, say) reflows in place rather than being redrawn, and the
|
|
651
|
+
// columns beside it slide over to make room.
|
|
652
|
+
A(() => {
|
|
653
|
+
const asked = entry.$page.layout;
|
|
654
|
+
entry.layout = asked === "small" || asked === "large" ? asked : "medium";
|
|
655
|
+
const width = this.roomFor(entry.layout);
|
|
656
|
+
if (!width)
|
|
657
|
+
return;
|
|
658
|
+
entry.width = width;
|
|
659
|
+
// The first run has no element to put it on yet — it's created with this
|
|
660
|
+
// width, just below. Later runs are the page changing its layout.
|
|
661
|
+
if (!el)
|
|
662
|
+
return;
|
|
663
|
+
el.style.width = `${width}px`;
|
|
664
|
+
this.scheduleLayout();
|
|
665
|
+
});
|
|
666
|
+
el = A(`section.s-panel${entry.width ? ` w:${entry.width}px` : ""}`, "destroy=", (node) => this.playExit(entry, node), () => {
|
|
601
667
|
const contentEl = A("div.s-content", () => {
|
|
602
668
|
entry.draw(entry.$page);
|
|
603
669
|
// After the content, so there is something to scroll when restoring.
|
|
@@ -613,17 +679,12 @@ export class PanelController {
|
|
|
613
679
|
A("div.s-panel-loading aria-hidden=true", () => { A("i"); A("i"); A("i"); });
|
|
614
680
|
});
|
|
615
681
|
});
|
|
616
|
-
// How much room the panel wants, settled right after its handler's
|
|
617
|
-
// synchronous run — deliberately once: a column that changed its mind
|
|
618
|
-
// later would reflow itself and shove its neighbours around.
|
|
619
|
-
const asked = A.peek(entry.$page, "layout");
|
|
620
|
-
entry.layout = asked === "small" || asked === "large" ? asked : "medium";
|
|
621
682
|
entry.el = el;
|
|
622
|
-
//
|
|
623
|
-
// the panel its
|
|
624
|
-
// upcoming frame, before anything is painted. A redraw (a
|
|
625
|
-
// dependency inside the handler) lands here too, with a brand-new
|
|
626
|
-
// that has to be placed again before it may animate.
|
|
683
|
+
// It has its width, but nothing animates from the arbitrary initial spot;
|
|
684
|
+
// `layout()` gives the panel its place in the run (and turns transitions
|
|
685
|
+
// back on) in the upcoming frame, before anything is painted. A redraw (a
|
|
686
|
+
// reactive dependency inside the handler) lands here too, with a brand-new
|
|
687
|
+
// element that has to be placed again before it may animate.
|
|
627
688
|
entry.placed = false;
|
|
628
689
|
el.style.transition = "none";
|
|
629
690
|
A.clean(() => { if (entry.el === el)
|
|
@@ -645,6 +706,65 @@ export class PanelController {
|
|
|
645
706
|
this.layout();
|
|
646
707
|
});
|
|
647
708
|
}
|
|
709
|
+
/**
|
|
710
|
+
* Measure the shell, and with it the width the window gives a panel of each
|
|
711
|
+
* layout. Measured on the *shell*, not on the panel region: the region's width
|
|
712
|
+
* is the layout engine's own output, so reading it back would nail the layout
|
|
713
|
+
* to whatever it happened to be a frame ago. Fractional widths throughout — a
|
|
714
|
+
* rounded column edge would drift a pixel away from the chrome above it.
|
|
715
|
+
*
|
|
716
|
+
* `undefined` while the shell has no width to speak of (it isn't in a document
|
|
717
|
+
* yet, or it's `display:none`); the next pass tries again.
|
|
718
|
+
*/
|
|
719
|
+
measure() {
|
|
720
|
+
const container = this.containerEl;
|
|
721
|
+
const inner = container?.parentElement;
|
|
722
|
+
const body = inner?.parentElement;
|
|
723
|
+
if (!container || !inner || !body)
|
|
724
|
+
return undefined;
|
|
725
|
+
const total = body.getBoundingClientRect().width;
|
|
726
|
+
if (!total)
|
|
727
|
+
return undefined;
|
|
728
|
+
// Everything that sits beside the columns: the sidebar and its hairline,
|
|
729
|
+
// either of which may be display:none on a narrow shell.
|
|
730
|
+
let chrome = 0;
|
|
731
|
+
for (const child of inner.children) {
|
|
732
|
+
if (child !== container)
|
|
733
|
+
chrome += child.getBoundingClientRect().width;
|
|
734
|
+
}
|
|
735
|
+
// The standard page is SHELL_PX wide, capped by the window; what it leaves
|
|
736
|
+
// beside the sidebar is the *standard* content area. Widths are a pure
|
|
737
|
+
// function of the window — never of what else is open — so a panel NEVER
|
|
738
|
+
// resizes because a neighbour came or went; only a window resize (the
|
|
739
|
+
// snap pass in `layout`) changes them:
|
|
740
|
+
// - "medium" fills the standard content area exactly;
|
|
741
|
+
// - "small" is half of it (minus the gutter) whenever that half is still
|
|
742
|
+
// a usable column, and the whole of it on narrower screens;
|
|
743
|
+
// - "large" ignores the standard width and takes everything the window
|
|
744
|
+
// has — which also means nothing ever fits beside it.
|
|
745
|
+
const medium = Math.max(0, Math.min(SHELL_PX, total) - chrome);
|
|
746
|
+
const half = (medium - GUTTER_PX) / 2;
|
|
747
|
+
return {
|
|
748
|
+
total,
|
|
749
|
+
chrome,
|
|
750
|
+
small: half >= PAIR_MIN_PX ? half : medium,
|
|
751
|
+
medium,
|
|
752
|
+
large: Math.max(0, total - chrome),
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* The measurements this pass runs on. Taken once per layout pass and per
|
|
757
|
+
* commit, and shared with the panels drawn in between — they all size
|
|
758
|
+
* themselves against the same shell, and a `getBoundingClientRect()` each
|
|
759
|
+
* would be a forced reflow each, in the middle of building their DOM.
|
|
760
|
+
*/
|
|
761
|
+
geometry() {
|
|
762
|
+
return (this.geom ??= this.measure());
|
|
763
|
+
}
|
|
764
|
+
/** How wide a panel of this layout is, right now; 0 while the shell can't be measured. */
|
|
765
|
+
roomFor(layout) {
|
|
766
|
+
return this.geometry()?.[layout] ?? 0;
|
|
767
|
+
}
|
|
648
768
|
/**
|
|
649
769
|
* Size and position every panel, and publish the width of the whole ensemble
|
|
650
770
|
* (sidebar + separator + columns) for the shell to centre itself on.
|
|
@@ -655,10 +775,8 @@ export class PanelController {
|
|
|
655
775
|
*/
|
|
656
776
|
layout() {
|
|
657
777
|
const container = this.containerEl;
|
|
658
|
-
const inner = container?.parentElement;
|
|
659
|
-
const body = inner?.parentElement;
|
|
660
778
|
const shell = container?.closest(".s-main");
|
|
661
|
-
if (!container || !
|
|
779
|
+
if (!container || !shell)
|
|
662
780
|
return;
|
|
663
781
|
const n = this.live.length;
|
|
664
782
|
// A panel that hasn't drawn yet has no width to contribute, which would make
|
|
@@ -666,45 +784,22 @@ export class PanelController {
|
|
|
666
784
|
// Every mount schedules another pass, so simply wait for it.
|
|
667
785
|
if (!n || this.live.some((entry) => !entry.el))
|
|
668
786
|
return;
|
|
669
|
-
//
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
const total = body.getBoundingClientRect().width;
|
|
674
|
-
if (!total)
|
|
787
|
+
// This pass measures afresh — it is the one thing that runs after a resize.
|
|
788
|
+
this.geom = undefined;
|
|
789
|
+
const geom = this.geometry();
|
|
790
|
+
if (!geom)
|
|
675
791
|
return;
|
|
676
792
|
const stacking = this.opts.stacking !== false;
|
|
677
793
|
// A window resize (or the very first pass) must be adopted instantly —
|
|
678
794
|
// geometry tracking the window through a 450ms transition reads as lag,
|
|
679
795
|
// and a shell animating itself into place on load reads as a glitch.
|
|
680
796
|
// `.s-shell-snap` suppresses every standing transition for this one pass.
|
|
681
|
-
const snap = this.lastBodyW !== total;
|
|
797
|
+
const snap = this.lastBodyW !== geom.total;
|
|
682
798
|
if (snap) {
|
|
683
|
-
this.lastBodyW = total;
|
|
799
|
+
this.lastBodyW = geom.total;
|
|
684
800
|
shell.classList.add("s-shell-snap");
|
|
685
801
|
}
|
|
686
|
-
|
|
687
|
-
// either of which may be display:none on a narrow shell.
|
|
688
|
-
let chrome = 0;
|
|
689
|
-
for (const child of inner.children) {
|
|
690
|
-
if (child !== container)
|
|
691
|
-
chrome += child.getBoundingClientRect().width;
|
|
692
|
-
}
|
|
693
|
-
// The standard page is SHELL_PX wide, capped by the window; what it leaves
|
|
694
|
-
// beside the sidebar is the *standard* content area. Widths are a pure
|
|
695
|
-
// function of the window — never of what else is open — so a panel NEVER
|
|
696
|
-
// resizes because a neighbour came or went; only a window resize (the
|
|
697
|
-
// snap pass above) changes them:
|
|
698
|
-
// - "medium" fills the standard content area exactly;
|
|
699
|
-
// - "small" is half of it (minus the gutter) whenever that half is still
|
|
700
|
-
// a usable column, and the whole of it on narrower screens;
|
|
701
|
-
// - "large" ignores the standard width and takes everything the window
|
|
702
|
-
// has — which also means nothing ever fits beside it.
|
|
703
|
-
const stdRoom = Math.max(0, Math.min(SHELL_PX, total) - chrome);
|
|
704
|
-
const fullRoom = Math.max(0, total - chrome);
|
|
705
|
-
const halfW = (stdRoom - GUTTER_PX) / 2;
|
|
706
|
-
const smallW = halfW >= PAIR_MIN_PX ? halfW : stdRoom;
|
|
707
|
-
const width = (entry) => entry.layout === "small" ? smallW : entry.layout === "large" ? fullRoom : stdRoom;
|
|
802
|
+
const width = (entry) => geom[entry.layout];
|
|
708
803
|
// The visible run: as many top-of-stack panels as the window fits, at the
|
|
709
804
|
// sizes the window gives them. The top panel always shows.
|
|
710
805
|
let first = n - 1;
|
|
@@ -712,7 +807,7 @@ export class PanelController {
|
|
|
712
807
|
if (stacking) {
|
|
713
808
|
for (let i = n - 2; i >= 0; i--) {
|
|
714
809
|
const sum = runSum + GUTTER_PX + width(this.live[i]);
|
|
715
|
-
if (sum >
|
|
810
|
+
if (sum > geom.large)
|
|
716
811
|
break;
|
|
717
812
|
runSum = sum;
|
|
718
813
|
first = i;
|
|
@@ -724,7 +819,7 @@ export class PanelController {
|
|
|
724
819
|
// wider than the window. So the page is the familiar 1280px until extra
|
|
725
820
|
// columns genuinely fit, and stretches — centred — to hold the ones that
|
|
726
821
|
// do; with a "large" up that's the window's edges.
|
|
727
|
-
const area = Math.min(
|
|
822
|
+
const area = Math.min(geom.large, Math.max(geom.medium, runSum));
|
|
728
823
|
for (let i = first; i < n; i++)
|
|
729
824
|
this.live[i].width = width(this.live[i]);
|
|
730
825
|
// Panels that have never been visible get their would-be width too, so a
|
|
@@ -738,7 +833,7 @@ export class PanelController {
|
|
|
738
833
|
// The consumers transition their max-width (see main.ts), so the
|
|
739
834
|
// recentring plays along with the panel that caused it instead of
|
|
740
835
|
// snapping.
|
|
741
|
-
shell.style.setProperty("--s-shell-w", `${chrome + area}px`);
|
|
836
|
+
shell.style.setProperty("--s-shell-w", `${geom.chrome + area}px`);
|
|
742
837
|
// Phase 1 — every panel's *start* state for this frame. Panels already on
|
|
743
838
|
// screen simply move (their standing transition animates it); freshly
|
|
744
839
|
// mounted ones still have transitions switched off, so what we set here is
|
|
@@ -750,8 +845,10 @@ export class PanelController {
|
|
|
750
845
|
const el = entry.el;
|
|
751
846
|
const shown = i >= first;
|
|
752
847
|
// Visible panels are left-aligned in the content area, a gutter apart;
|
|
753
|
-
// hidden ones park at its left edge, keeping their last width.
|
|
754
|
-
|
|
848
|
+
// hidden ones park at its left edge, keeping their last width. Deeper
|
|
849
|
+
// panels layer over shallower ones, each on the odd layer for its depth
|
|
850
|
+
// (see LAYER_STEP).
|
|
851
|
+
place(el, shown ? x : 0, entry.width, LAYER_STEP * i + 1);
|
|
755
852
|
if (shown)
|
|
756
853
|
x += entry.width + GUTTER_PX;
|
|
757
854
|
el.classList.toggle("s-panel-sep", shown && i > first);
|
|
@@ -805,10 +902,11 @@ export class PanelController {
|
|
|
805
902
|
this.timers.add(timer);
|
|
806
903
|
}
|
|
807
904
|
}
|
|
808
|
-
/** Put a panel at rest: `x` from the region's left edge, `width` pixels wide
|
|
809
|
-
function place(el, x, width) {
|
|
905
|
+
/** Put a panel at rest: `x` from the region's left edge, `width` pixels wide, on layer `z`. */
|
|
906
|
+
function place(el, x, width, z) {
|
|
810
907
|
el.style.left = `${x}px`;
|
|
811
908
|
el.style.width = `${width}px`;
|
|
909
|
+
el.style.zIndex = String(z);
|
|
812
910
|
}
|
|
813
911
|
// ─── Guards ──────────────────────────────────────────────────────────────────
|
|
814
912
|
/**
|