staffa 0.18.1 → 0.18.2
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/dist/components/main.js +9 -7
- package/dist/components/panels.d.ts +22 -5
- package/dist/components/panels.js +169 -66
- package/dist/staffa.esm.js +1 -1
- package/dist/theme.js +14 -5
- package/package.json +1 -1
- package/src/components/main.ts +9 -7
- package/src/components/panels.ts +171 -66
- package/src/theme.ts +14 -5
package/dist/components/main.js
CHANGED
|
@@ -7,7 +7,7 @@ import { drawMenu, isFloatingMenuOpen, consumeBranchNav, anyCurrent, registerMen
|
|
|
7
7
|
import { menu as menuIcon, x as closeIcon } from "../icons.js";
|
|
8
8
|
import { iconButton } from "./button.js";
|
|
9
9
|
import { isDialogOpen } from "./dialog.js";
|
|
10
|
-
import { PanelStackController, SMALL_MAX_PX } from "./panels.js";
|
|
10
|
+
import { PAGE_MS, PanelStackController, SMALL_MAX_PX } from "./panels.js";
|
|
11
11
|
/** The default nav column, hairline included — see {@link MainOptions.navWidth}. */
|
|
12
12
|
const NAV_W = 200;
|
|
13
13
|
A.insertGlobalCss({
|
|
@@ -66,7 +66,7 @@ A.insertGlobalCss({
|
|
|
66
66
|
// — past the viewport edge. The transition is dormant except for the
|
|
67
67
|
// incoming half of the nav-panel hand-off (see `slideContentIn`).
|
|
68
68
|
".s-body main": "flex:1 min-width:0 min-height:0 overflow-x:hidden overflow-y:auto display:flex flex-direction:column " +
|
|
69
|
-
|
|
69
|
+
`transition: transform ${PAGE_MS}ms ease;`,
|
|
70
70
|
// A one-shot starting position: parked one screen to the right, with the
|
|
71
71
|
// transition off so it snaps there. Removing the class animates it home.
|
|
72
72
|
".s-body main.s-slide-in": "transform: translateX(100%); transition:none",
|
|
@@ -95,14 +95,16 @@ A.insertGlobalCss({
|
|
|
95
95
|
// Under the sticky header's 10: they never overlap, but the bar should win.
|
|
96
96
|
"position:absolute inset:0 z-index:5 display:flex flex-direction:column " +
|
|
97
97
|
"overflow-y:auto overscroll-behavior:contain border:0 r:0 padding:$2 gap:$1 " +
|
|
98
|
-
|
|
98
|
+
`transition: transform ${PAGE_MS}ms ease, visibility 0s;`,
|
|
99
99
|
// Parked one screen left: what the `create=`/`destroy=` hooks transition out
|
|
100
100
|
// of and back into. On dismissal (this rule's transition) `visibility` flips
|
|
101
|
-
// only at the slide's end
|
|
102
|
-
//
|
|
103
|
-
//
|
|
101
|
+
// only at the slide's end — a delayed zero-length transition, whose constant
|
|
102
|
+
// start value costs nothing per frame — so the dismissed page isn't
|
|
103
|
+
// reachable while it waits for Aberdeen's removal timer; on entry it flips
|
|
104
|
+
// instantly (the `0s` above), or the opening page would refuse the focus
|
|
105
|
+
// handed to it mid-slide.
|
|
104
106
|
"&.s-nav-page-off": "transform:translateX(-100%) pointer-events:none visibility:hidden " +
|
|
105
|
-
|
|
107
|
+
`transition: transform ${PAGE_MS}ms ease, visibility 0s ${PAGE_MS}ms;`,
|
|
106
108
|
// Roomier than the dropdown's: every row here is a thumb target.
|
|
107
109
|
".s-menu-item": "padding: $2 $3; min-height:3rem font-size:1.05em gap:$3",
|
|
108
110
|
},
|
|
@@ -287,6 +287,13 @@ export interface Panel<P = Record<string, string | number | string[]>> {
|
|
|
287
287
|
*/
|
|
288
288
|
open(href: string, how?: "push" | "replace" | "open"): Promise<boolean>;
|
|
289
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* The one duration every bit of shell motion shares: the enter/exit fades, the
|
|
292
|
+
* slides, the narrow-screen nav slide. Interpolated into every transition as a
|
|
293
|
+
* literal — the shared constant is what keeps CSS and JS in step — and also
|
|
294
|
+
* published as the `--s-panel-ms` custom property for app CSS.
|
|
295
|
+
*/
|
|
296
|
+
export declare const PAGE_MS = 250;
|
|
290
297
|
export declare const SMALL_MAX_PX = 540;
|
|
291
298
|
/** Options the stack needs from its shell. */
|
|
292
299
|
export interface PanelStackOptions {
|
|
@@ -453,6 +460,8 @@ export declare class PanelStackController implements PanelStack {
|
|
|
453
460
|
private lastGeom?;
|
|
454
461
|
private layoutQueued;
|
|
455
462
|
private timers;
|
|
463
|
+
/** Elements playing their exit fade, each riding its anchor's motion (see `layout`). */
|
|
464
|
+
private exiting;
|
|
456
465
|
/** The arrangement the navigation in flight is heading for; see {@link intended}. */
|
|
457
466
|
private intent;
|
|
458
467
|
/** The navigation the router hasn't settled yet, if any. */
|
|
@@ -530,13 +539,21 @@ export declare class PanelStackController implements PanelStack {
|
|
|
530
539
|
*/
|
|
531
540
|
private beginClose;
|
|
532
541
|
/**
|
|
533
|
-
* A closed panel's send-off, run by Aberdeen once its scope is gone: it fades
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
* panel appear to fade half-way and vanish; the timeout is only a fallback for
|
|
537
|
-
* when no `transitionend` is coming (transitions off, element never placed).
|
|
542
|
+
* A closed panel's send-off, run by Aberdeen once its scope is gone: it fades,
|
|
543
|
+
* inert, riding its anchor's slide (see `layout`), and leaves the DOM once
|
|
544
|
+
* the fade is over (see {@link afterFade}).
|
|
538
545
|
*/
|
|
539
546
|
private playExit;
|
|
547
|
+
/**
|
|
548
|
+
* Run `done` once `el`'s transition of `prop` is over. The real signal is
|
|
549
|
+
* `transitionend` — or `transitioncancel`, for one a resize snaps short —
|
|
550
|
+
* so the transition's actual length rules, however long: DevTools' slowed
|
|
551
|
+
* animations stretch it tenfold without touching any timer. The timer only
|
|
552
|
+
* stands in for a transition that never starts at all (transitions off,
|
|
553
|
+
* element never placed, nothing to travel), which is why one proving real
|
|
554
|
+
* (`transitionrun`) disarms it.
|
|
555
|
+
*/
|
|
556
|
+
private afterTransition;
|
|
540
557
|
/**
|
|
541
558
|
* The arrangement navigation works from: the one we're on the way to while a
|
|
542
559
|
* change is still settling, the one on screen otherwise. That window is common
|
|
@@ -110,10 +110,11 @@ function matchRoute(r, segments) {
|
|
|
110
110
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
111
111
|
/**
|
|
112
112
|
* The one duration every bit of shell motion shares: the enter/exit fades, the
|
|
113
|
-
*
|
|
114
|
-
*
|
|
113
|
+
* slides, the narrow-screen nav slide. Interpolated into every transition as a
|
|
114
|
+
* literal — the shared constant is what keeps CSS and JS in step — and also
|
|
115
|
+
* published as the `--s-panel-ms` custom property for app CSS.
|
|
115
116
|
*/
|
|
116
|
-
const PAGE_MS = 250;
|
|
117
|
+
export const PAGE_MS = 250;
|
|
117
118
|
/** How long a freshly pushed `loading` panel holds its fade-in. */
|
|
118
119
|
const LOADING_HOLD_MS = 300;
|
|
119
120
|
/**
|
|
@@ -138,16 +139,23 @@ A.insertGlobalCss({
|
|
|
138
139
|
PANEL_SHEEN,
|
|
139
140
|
".s-panel": {
|
|
140
141
|
// The shell knows three motions, and this vocabulary is all of them:
|
|
141
|
-
// a panel that MOVES
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
// CLOSED one
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
142
|
+
// a panel that MOVES slides — every mover in a pass shares this one
|
|
143
|
+
// duration and ease-out, so panels travelling the same distance travel
|
|
144
|
+
// as one — a CREATED panel joins the strip beside the old position of
|
|
145
|
+
// the panels beneath it and rides their slide while fading in, and a
|
|
146
|
+
// CLOSED one keeps its seat beside the surviving panel beneath it and
|
|
147
|
+
// rides that panel's slide while fading out — its creation played
|
|
148
|
+
// backwards. Nothing else ever animates.
|
|
149
|
+
// A slide is a `transform` playing out to none (see `layout`), never an
|
|
150
|
+
// animated `left`: left is a layout property, so animating it would
|
|
151
|
+
// relayout and repaint every travelling column on every frame, where a
|
|
152
|
+
// transform moves composited layers. `left` always holds the resting
|
|
153
|
+
// position — it is not in the transition list — and the transform
|
|
154
|
+
// exists only while a panel travels, so text gets its subpixel
|
|
155
|
+
// antialiasing back the moment it settles. No `width` transition
|
|
156
|
+
// either — animating one reflows the column every frame. And the fade
|
|
157
|
+
// is `linear` while the moves ease out: an eased opacity spends its
|
|
158
|
+
// last stretch near zero, reading as a vanish.
|
|
151
159
|
// Layering is fixed per state, not per stack depth: live panels never
|
|
152
160
|
// overlap each other (the strip keeps them adjacent, see `layout()`), so
|
|
153
161
|
// only the fading ones need an order — below, in both directions, per
|
|
@@ -156,7 +164,7 @@ A.insertGlobalCss({
|
|
|
156
164
|
// another, and two transparent ones mean text sliding over text).
|
|
157
165
|
"&": "position:absolute top:0 bottom:0 left:0 display:flex flex-direction:column " +
|
|
158
166
|
PANEL_SHEEN + " " +
|
|
159
|
-
|
|
167
|
+
`z-index:2 transition: transform ${PAGE_MS}ms ease-out, opacity ${PAGE_MS}ms linear;`,
|
|
160
168
|
// The hairline between two columns, fading at both ends (like the sidebar's
|
|
161
169
|
// `.s-nav-sep`). Columns tile with no gutter — each brings its own `$3` of
|
|
162
170
|
// padding — so this sits exactly on the boundary.
|
|
@@ -166,9 +174,10 @@ A.insertGlobalCss({
|
|
|
166
174
|
// whatever slides across its spot passes over it. Dropped once the fade
|
|
167
175
|
// is over (see `releaseEnter`).
|
|
168
176
|
"&.s-panel-new": "z-index:1",
|
|
169
|
-
// On its way out: it fades
|
|
170
|
-
// (declared after `.s-panel-new`, so closing
|
|
171
|
-
// the bottom), and leaves the DOM when the
|
|
177
|
+
// On its way out: it fades, still riding the strip (see `layout`),
|
|
178
|
+
// beneath every live panel (declared after `.s-panel-new`, so closing
|
|
179
|
+
// mid-enter drops a panel to the bottom), and leaves the DOM when the
|
|
180
|
+
// fade ends (see `playExit`).
|
|
172
181
|
"&.s-panel-closing": "z-index:0 opacity:0 pointer-events:none",
|
|
173
182
|
// The fade-in's start state: a newcomer wears it from creation until its
|
|
174
183
|
// content is ready — usually the very pass that placed it, later for a
|
|
@@ -179,11 +188,13 @@ A.insertGlobalCss({
|
|
|
179
188
|
// edges, so these rest at their true positions, clipped — being crowded
|
|
180
189
|
// out or revealed is an ordinary move, not a fade. Both keep their DOM —
|
|
181
190
|
// and so their scroll position and half-typed forms — hence
|
|
182
|
-
// `visibility`, not `display:none
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
191
|
+
// `visibility`, not `display:none`. The flip is timed by `layout()`,
|
|
192
|
+
// never transitioned: a panel sliding offstage gets the class once its
|
|
193
|
+
// slide ends and drops it the moment it is revealed. `visibility` is
|
|
194
|
+
// not compositable, and Chrome composites an element's transitions as
|
|
195
|
+
// a group, so listing it here would drag the slide itself onto the
|
|
196
|
+
// main thread — relaid out and repainted every frame.
|
|
197
|
+
"&.s-panel-hidden, &.s-panel-parked": "visibility:hidden",
|
|
187
198
|
},
|
|
188
199
|
// The scroll container, with the column's own padding. Its scrollbar sits
|
|
189
200
|
// flush against the column edge (unlike content mode's inset one), so it meets
|
|
@@ -290,6 +301,8 @@ export class PanelStackController {
|
|
|
290
301
|
lastGeom;
|
|
291
302
|
layoutQueued = false;
|
|
292
303
|
timers = new Set();
|
|
304
|
+
/** Elements playing their exit fade, each riding its anchor's motion (see `layout`). */
|
|
305
|
+
exiting = new Set();
|
|
293
306
|
/** The arrangement the navigation in flight is heading for; see {@link intended}. */
|
|
294
307
|
intent = null;
|
|
295
308
|
/** The navigation the router hasn't settled yet, if any. */
|
|
@@ -516,8 +529,15 @@ export class PanelStackController {
|
|
|
516
529
|
next.push(entry);
|
|
517
530
|
this.$open[path] = entry;
|
|
518
531
|
}
|
|
519
|
-
|
|
520
|
-
|
|
532
|
+
// What remains in `existing` closes, each remembering the nearest
|
|
533
|
+
// surviving panel beneath it — the anchor its exit fade rides.
|
|
534
|
+
let anchor;
|
|
535
|
+
for (const entry of this.$state.live) {
|
|
536
|
+
if (existing.has(entry.path))
|
|
537
|
+
this.beginClose(entry, anchor);
|
|
538
|
+
else
|
|
539
|
+
anchor = entry.path;
|
|
540
|
+
}
|
|
521
541
|
this.$state.live = next;
|
|
522
542
|
this.$state.focus = Math.min(target.focus, next.length - 1);
|
|
523
543
|
this.scheduleLayout();
|
|
@@ -556,17 +576,16 @@ export class PanelStackController {
|
|
|
556
576
|
* at the end of the animation. Only the element lingers to play that animation,
|
|
557
577
|
* which is what `drawPanel`'s `destroy=` hook hands to {@link playExit}.
|
|
558
578
|
*/
|
|
559
|
-
beginClose(entry) {
|
|
579
|
+
beginClose(entry, anchor) {
|
|
560
580
|
entry.closing = true;
|
|
581
|
+
entry.anchor = anchor;
|
|
561
582
|
entry.$panel.visible = false;
|
|
562
583
|
delete this.$open[entry.path];
|
|
563
584
|
}
|
|
564
585
|
/**
|
|
565
|
-
* A closed panel's send-off, run by Aberdeen once its scope is gone: it fades
|
|
566
|
-
*
|
|
567
|
-
*
|
|
568
|
-
* panel appear to fade half-way and vanish; the timeout is only a fallback for
|
|
569
|
-
* when no `transitionend` is coming (transitions off, element never placed).
|
|
586
|
+
* A closed panel's send-off, run by Aberdeen once its scope is gone: it fades,
|
|
587
|
+
* inert, riding its anchor's slide (see `layout`), and leaves the DOM once
|
|
588
|
+
* the fade is over (see {@link afterFade}).
|
|
570
589
|
*/
|
|
571
590
|
playExit(entry, el) {
|
|
572
591
|
// A panel being *redrawn* replaces its element through here too; that one
|
|
@@ -577,17 +596,41 @@ export class PanelStackController {
|
|
|
577
596
|
}
|
|
578
597
|
el.classList.add("s-panel-closing");
|
|
579
598
|
el.setAttribute("inert", "");
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
599
|
+
// Only a placed panel has a seat on the strip to ride out from.
|
|
600
|
+
const exit = entry.placed ? { el, anchor: entry.anchor, ride: 0 } : null;
|
|
601
|
+
if (exit)
|
|
602
|
+
this.exiting.add(exit);
|
|
603
|
+
this.afterTransition(el, "opacity", () => {
|
|
604
|
+
if (exit)
|
|
605
|
+
this.exiting.delete(exit);
|
|
583
606
|
el.remove();
|
|
584
|
-
};
|
|
585
|
-
el.addEventListener("transitionend", (e) => {
|
|
586
|
-
if (e.target === el && e.propertyName === "opacity")
|
|
587
|
-
drop();
|
|
588
607
|
});
|
|
589
|
-
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Run `done` once `el`'s transition of `prop` is over. The real signal is
|
|
611
|
+
* `transitionend` — or `transitioncancel`, for one a resize snaps short —
|
|
612
|
+
* so the transition's actual length rules, however long: DevTools' slowed
|
|
613
|
+
* animations stretch it tenfold without touching any timer. The timer only
|
|
614
|
+
* stands in for a transition that never starts at all (transitions off,
|
|
615
|
+
* element never placed, nothing to travel), which is why one proving real
|
|
616
|
+
* (`transitionrun`) disarms it.
|
|
617
|
+
*/
|
|
618
|
+
afterTransition(el, prop, done) {
|
|
619
|
+
let called = false;
|
|
620
|
+
const timer = setTimeout(() => finish(), PAGE_MS + 80);
|
|
590
621
|
this.timers.add(timer);
|
|
622
|
+
const disarm = () => { clearTimeout(timer); this.timers.delete(timer); };
|
|
623
|
+
const finish = () => { disarm(); if (!called) {
|
|
624
|
+
called = true;
|
|
625
|
+
done();
|
|
626
|
+
} };
|
|
627
|
+
const forProp = (fn) => (e) => {
|
|
628
|
+
if (e.target === el && e.propertyName === prop)
|
|
629
|
+
fn();
|
|
630
|
+
};
|
|
631
|
+
el.addEventListener("transitionrun", forProp(disarm));
|
|
632
|
+
el.addEventListener("transitionend", forProp(finish));
|
|
633
|
+
el.addEventListener("transitioncancel", forProp(finish));
|
|
591
634
|
}
|
|
592
635
|
// ── Navigation ─────────────────────────────────────────────────────────
|
|
593
636
|
/**
|
|
@@ -1267,14 +1310,27 @@ export class PanelStackController {
|
|
|
1267
1310
|
// Phase 1 — lay the strip out for this frame: each panel flush against
|
|
1268
1311
|
// its neighbours, the visible run [first..cur] in the viewport, earlier
|
|
1269
1312
|
// panels continuing off its left edge and parked ones held past its
|
|
1270
|
-
// right.
|
|
1271
|
-
//
|
|
1272
|
-
//
|
|
1273
|
-
//
|
|
1274
|
-
//
|
|
1275
|
-
//
|
|
1276
|
-
//
|
|
1313
|
+
// right. `left` gets each new resting position outright — it never
|
|
1314
|
+
// animates — and the slide to it is a FLIP: a mover is held at its
|
|
1315
|
+
// current visual spot by a transform (its mid-slide offset plus the
|
|
1316
|
+
// distance), a newcomer at its start beside the OLD position of the
|
|
1317
|
+
// nearest placed panel beneath it (`delta`, so the slide in is one
|
|
1318
|
+
// motion with the panels making room; with nothing beneath it to come
|
|
1319
|
+
// from, it simply fades in place). Phase 3 plays every held transform
|
|
1320
|
+
// out to none. A snap pass holds nothing: geometry must land, not
|
|
1321
|
+
// travel.
|
|
1277
1322
|
const fresh = [];
|
|
1323
|
+
const movers = [];
|
|
1324
|
+
const deltas = new Map();
|
|
1325
|
+
// All reads before all writes: a mover may still be mid-slide, and its
|
|
1326
|
+
// current offset must come out of the computed style before this pass
|
|
1327
|
+
// dirties it (one style recalc, then cached).
|
|
1328
|
+
const txs = new Map();
|
|
1329
|
+
if (!snap)
|
|
1330
|
+
for (const entry of live) {
|
|
1331
|
+
if (entry.placed)
|
|
1332
|
+
txs.set(entry, transformX(entry.el));
|
|
1333
|
+
}
|
|
1278
1334
|
let x = left;
|
|
1279
1335
|
let delta = 0;
|
|
1280
1336
|
for (let i = 0; i < first; i++)
|
|
@@ -1298,17 +1354,28 @@ export class PanelStackController {
|
|
|
1298
1354
|
}
|
|
1299
1355
|
if (entry.placed) {
|
|
1300
1356
|
delta = parseFloat(el.style.left) - x;
|
|
1357
|
+
deltas.set(entry.path, delta);
|
|
1358
|
+
if (delta && !snap) {
|
|
1359
|
+
// Hold the visual spot while `left` jumps beneath it. The
|
|
1360
|
+
// inline transition keeps the transform still — and only it:
|
|
1361
|
+
// running fades carry on, and one starting this pass (a
|
|
1362
|
+
// release below) still gets its linear curve.
|
|
1363
|
+
el.style.transition = `opacity ${PAGE_MS}ms linear`;
|
|
1364
|
+
el.style.transform = `translateX(${(txs.get(entry) ?? 0) + delta}px)`;
|
|
1365
|
+
movers.push(el);
|
|
1366
|
+
}
|
|
1301
1367
|
el.style.left = `${x}px`;
|
|
1302
1368
|
// A fade held back for content starts the moment its hold lifts.
|
|
1303
1369
|
if (entry.enter && !entry.$ui.holding)
|
|
1304
1370
|
this.releaseEnter(entry);
|
|
1305
1371
|
}
|
|
1306
1372
|
else {
|
|
1307
|
-
fresh.push(
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1373
|
+
fresh.push(entry);
|
|
1374
|
+
el.style.left = `${x}px`;
|
|
1375
|
+
if (entry.enter && shown) {
|
|
1376
|
+
el.style.transform = `translateX(${delta}px)`;
|
|
1311
1377
|
el.classList.add("s-panel-enter", "s-panel-new");
|
|
1378
|
+
}
|
|
1312
1379
|
}
|
|
1313
1380
|
el.style.width = `${entry.width}px`;
|
|
1314
1381
|
x += entry.width;
|
|
@@ -1319,24 +1386,61 @@ export class PanelStackController {
|
|
|
1319
1386
|
if (entry.$panel.width !== entry.width)
|
|
1320
1387
|
entry.$panel.width = entry.width;
|
|
1321
1388
|
el.classList.toggle("s-panel-sep", shown && i > first);
|
|
1322
|
-
// Off-screen panels stop rendering, keeping their DOM.
|
|
1323
|
-
|
|
1324
|
-
|
|
1389
|
+
// Off-screen panels stop rendering, keeping their DOM. A revealed
|
|
1390
|
+
// panel is visible at once; one sliding offstage stays visible for
|
|
1391
|
+
// the whole slide, its visibility class deferred to the slide's
|
|
1392
|
+
// end — but applied outright when it is already invisible, about
|
|
1393
|
+
// to be (a newcomer offstage from birth), or snapping.
|
|
1394
|
+
const hidden = i < first;
|
|
1395
|
+
const wasOff = el.classList.contains("s-panel-hidden") || el.classList.contains("s-panel-parked");
|
|
1396
|
+
if (shown) {
|
|
1397
|
+
el.classList.remove("s-panel-hidden", "s-panel-parked");
|
|
1398
|
+
}
|
|
1399
|
+
else if (wasOff || !entry.placed || snap) {
|
|
1400
|
+
el.classList.toggle("s-panel-hidden", hidden);
|
|
1401
|
+
el.classList.toggle("s-panel-parked", !hidden);
|
|
1402
|
+
}
|
|
1403
|
+
else {
|
|
1404
|
+
this.afterTransition(el, "transform", () => {
|
|
1405
|
+
if (entry.el !== el || !entry.offstage)
|
|
1406
|
+
return;
|
|
1407
|
+
el.classList.toggle("s-panel-hidden", hidden);
|
|
1408
|
+
el.classList.toggle("s-panel-parked", !hidden);
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
entry.offstage = !shown;
|
|
1325
1412
|
el.toggleAttribute("inert", !shown);
|
|
1326
1413
|
}
|
|
1414
|
+
// Closing panels keep their seat on the strip while they fade: each
|
|
1415
|
+
// travels exactly as far as its anchor — the surviving panel it stood
|
|
1416
|
+
// on — so a close slides back out the way the open slid in, and one
|
|
1417
|
+
// whose anchor stays put is the plain crossfade it should be. A plain
|
|
1418
|
+
// retarget, no holding: the transition picks the slide up from wherever
|
|
1419
|
+
// the element is now.
|
|
1420
|
+
for (const exit of this.exiting) {
|
|
1421
|
+
const d = exit.anchor == null ? undefined : deltas.get(exit.anchor);
|
|
1422
|
+
if (d) {
|
|
1423
|
+
exit.ride -= d;
|
|
1424
|
+
exit.el.style.transform = `translateX(${exit.ride}px)`;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1327
1427
|
// Phase 2 — reading a layout property forces the browser to adopt those
|
|
1328
|
-
//
|
|
1329
|
-
if (fresh.length || snap)
|
|
1428
|
+
// held spots (and a snap pass's transition-free geometry) to slide from.
|
|
1429
|
+
if (fresh.length || movers.length || snap)
|
|
1330
1430
|
void container.offsetWidth;
|
|
1331
1431
|
if (snap)
|
|
1332
1432
|
shell.classList.remove("s-shell-snap");
|
|
1333
|
-
// Phase 3 —
|
|
1334
|
-
//
|
|
1433
|
+
// Phase 3 — every held transform plays out to none: the slides start
|
|
1434
|
+
// now, and the newcomers' fades with them — unless a panel is holding
|
|
1335
1435
|
// for its content, which keeps the fade's start state on until then.
|
|
1336
|
-
for (const
|
|
1436
|
+
for (const el of movers) {
|
|
1437
|
+
el.style.transition = "";
|
|
1438
|
+
el.style.transform = "";
|
|
1439
|
+
}
|
|
1440
|
+
for (const entry of fresh) {
|
|
1337
1441
|
const el = entry.el;
|
|
1338
1442
|
el.style.transition = "";
|
|
1339
|
-
el.style.
|
|
1443
|
+
el.style.transform = "";
|
|
1340
1444
|
entry.placed = true;
|
|
1341
1445
|
if (!entry.$ui.holding)
|
|
1342
1446
|
this.releaseEnter(entry);
|
|
@@ -1349,14 +1453,8 @@ export class PanelStackController {
|
|
|
1349
1453
|
if (!el || !el.classList.contains("s-panel-enter"))
|
|
1350
1454
|
return;
|
|
1351
1455
|
el.classList.remove("s-panel-enter");
|
|
1352
|
-
// Keep it beneath its elders until the fade is over
|
|
1353
|
-
|
|
1354
|
-
// `transitionend`.
|
|
1355
|
-
const timer = setTimeout(() => {
|
|
1356
|
-
this.timers.delete(timer);
|
|
1357
|
-
el.classList.remove("s-panel-new");
|
|
1358
|
-
}, PAGE_MS + 80);
|
|
1359
|
-
this.timers.add(timer);
|
|
1456
|
+
// Keep it beneath its elders until the fade is over.
|
|
1457
|
+
this.afterTransition(el, "opacity", () => el.classList.remove("s-panel-new"));
|
|
1360
1458
|
}
|
|
1361
1459
|
/** Let a `loading` panel's fade-in wait — but not indefinitely. */
|
|
1362
1460
|
holdEnter(entry) {
|
|
@@ -1375,6 +1473,11 @@ export class PanelStackController {
|
|
|
1375
1473
|
function sameStack(a, b) {
|
|
1376
1474
|
return a.length === b.length && a.every((v, i) => v === b[i]);
|
|
1377
1475
|
}
|
|
1476
|
+
/** The X offset of `el`'s computed transform — where a slide has it right now. */
|
|
1477
|
+
function transformX(el) {
|
|
1478
|
+
const t = getComputedStyle(el).transform;
|
|
1479
|
+
return t && t !== "none" ? new DOMMatrixReadOnly(t).m41 : 0;
|
|
1480
|
+
}
|
|
1378
1481
|
/**
|
|
1379
1482
|
* The first non-empty text inside `el`, trimmed and capped at a name-like
|
|
1380
1483
|
* length — the stand-in title for a panel that never set one.
|