staffa 0.18.0 → 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.d.ts +2 -2
- package/dist/components/main.js +9 -7
- package/dist/components/panels.d.ts +31 -11
- package/dist/components/panels.js +244 -126
- package/dist/staffa.esm.js +1 -1
- package/dist/theme.js +14 -5
- package/package.json +3 -3
- package/skill/MainOptions.md +2 -2
- package/skill/Panel.md +6 -5
- package/src/components/main.ts +11 -9
- package/src/components/panels.ts +242 -125
- package/src/theme.ts +14 -5
|
@@ -110,11 +110,12 @@ 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
|
-
/** How long a freshly pushed `loading` panel holds its
|
|
117
|
+
export const PAGE_MS = 250;
|
|
118
|
+
/** How long a freshly pushed `loading` panel holds its fade-in. */
|
|
118
119
|
const LOADING_HOLD_MS = 300;
|
|
119
120
|
/**
|
|
120
121
|
* The bounds of a column: at most 540px — the width columns aim for, the area
|
|
@@ -123,66 +124,77 @@ const LOADING_HOLD_MS = 300;
|
|
|
123
124
|
*/
|
|
124
125
|
const SMALL_MIN_PX = 360;
|
|
125
126
|
export const SMALL_MAX_PX = 540;
|
|
126
|
-
/**
|
|
127
|
-
* Two `z-index` steps per panel: a panel sits on the odd layer for its depth in
|
|
128
|
-
* the stack, a *closing* one on the even layer just below. So a replacement
|
|
129
|
-
* comes in over the panel it replaces, while a closing panel fades out over
|
|
130
|
-
* whatever it was covering.
|
|
131
|
-
*/
|
|
132
|
-
const LAYER_STEP = 2;
|
|
133
127
|
// ─── Module-level styling ────────────────────────────────────────────────────
|
|
134
128
|
A.insertGlobalCss({
|
|
135
129
|
":root": `--s-panel-ms:${PAGE_MS}ms`,
|
|
136
130
|
// The clipping viewport the columns slide through; panels are absolutely
|
|
137
131
|
// positioned inside it, sized and offset from JS (see `layout()`).
|
|
138
|
-
// `isolation` keeps their z-index layers
|
|
139
|
-
//
|
|
140
|
-
//
|
|
132
|
+
// `isolation` keeps their z-index layers below the shell's own chrome. It
|
|
133
|
+
// paints the same PANEL_SHEEN as every panel, so columns and the ground
|
|
134
|
+
// beside them read as one surface.
|
|
141
135
|
// `overflow:clip`, not `hidden`: a hidden box is still a scroll container,
|
|
142
136
|
// and anything that scrolls it (find-in-page, an in-page anchor) shifts every
|
|
143
137
|
// column sideways permanently, with nothing to scroll it back.
|
|
144
138
|
".s-panels": "flex:1 min-width:0 min-height:0 position:relative overflow:clip isolation:isolate " +
|
|
145
139
|
PANEL_SHEEN,
|
|
146
140
|
".s-panel": {
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
141
|
+
// The shell knows three motions, and this vocabulary is all of them:
|
|
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.
|
|
159
|
+
// Layering is fixed per state, not per stack depth: live panels never
|
|
160
|
+
// overlap each other (the strip keeps them adjacent, see `layout()`), so
|
|
161
|
+
// only the fading ones need an order — below, in both directions, per
|
|
162
|
+
// the classes underneath.
|
|
156
163
|
// PANEL_SHEEN gives every panel an opaque ground (panels animate over one
|
|
157
164
|
// another, and two transparent ones mean text sliding over text).
|
|
158
165
|
"&": "position:absolute top:0 bottom:0 left:0 display:flex flex-direction:column " +
|
|
159
166
|
PANEL_SHEEN + " " +
|
|
160
|
-
|
|
167
|
+
`z-index:2 transition: transform ${PAGE_MS}ms ease-out, opacity ${PAGE_MS}ms linear;`,
|
|
161
168
|
// The hairline between two columns, fading at both ends (like the sidebar's
|
|
162
169
|
// `.s-nav-sep`). Columns tile with no gutter — each brings its own `$3` of
|
|
163
170
|
// padding — so this sits exactly on the boundary.
|
|
164
171
|
"&.s-panel-sep::before": "content:'' position:absolute left:0 top:0.6rem bottom:0.6rem width:1px z-index:1 " +
|
|
165
172
|
"background: linear-gradient(to bottom, transparent, $s-faint 18%, $s-faint 82%, transparent);",
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
//
|
|
174
|
-
"&.s-panel-closing": "opacity:0 pointer-events:none",
|
|
175
|
-
// The
|
|
176
|
-
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
173
|
+
// Still owed or playing its entry fade: beneath the settled panels, so
|
|
174
|
+
// whatever slides across its spot passes over it. Dropped once the fade
|
|
175
|
+
// is over (see `releaseEnter`).
|
|
176
|
+
"&.s-panel-new": "z-index:1",
|
|
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`).
|
|
181
|
+
"&.s-panel-closing": "z-index:0 opacity:0 pointer-events:none",
|
|
182
|
+
// The fade-in's start state: a newcomer wears it from creation until its
|
|
183
|
+
// content is ready — usually the very pass that placed it, later for a
|
|
184
|
+
// `loading` panel holding out for data — sliding invisibly meanwhile.
|
|
185
|
+
// Dropping the class is what starts the fade (see `releaseEnter`).
|
|
186
|
+
"&.s-panel-enter": "opacity:0",
|
|
187
|
+
// Off screen but open: the strip simply continues past the viewport's
|
|
188
|
+
// edges, so these rest at their true positions, clipped — being crowded
|
|
189
|
+
// out or revealed is an ordinary move, not a fade. Both keep their DOM —
|
|
190
|
+
// and so their scroll position and half-typed forms — hence
|
|
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",
|
|
186
198
|
},
|
|
187
199
|
// The scroll container, with the column's own padding. Its scrollbar sits
|
|
188
200
|
// flush against the column edge (unlike content mode's inset one), so it meets
|
|
@@ -289,6 +301,8 @@ export class PanelStackController {
|
|
|
289
301
|
lastGeom;
|
|
290
302
|
layoutQueued = false;
|
|
291
303
|
timers = new Set();
|
|
304
|
+
/** Elements playing their exit fade, each riding its anchor's motion (see `layout`). */
|
|
305
|
+
exiting = new Set();
|
|
292
306
|
/** The arrangement the navigation in flight is heading for; see {@link intended}. */
|
|
293
307
|
intent = null;
|
|
294
308
|
/** The navigation the router hasn't settled yet, if any. */
|
|
@@ -497,12 +511,6 @@ export class PanelStackController {
|
|
|
497
511
|
const pinned = route.current.state.pinned;
|
|
498
512
|
const seedPins = new Set(Array.isArray(pinned) ? pinned.map(String) : []);
|
|
499
513
|
const existing = new Map(this.$state.live.map((entry) => [entry.path, entry]));
|
|
500
|
-
// A replacement — something enters while something closes — just fades:
|
|
501
|
-
// the new panel in on top, the old ones out in place beneath it (see the
|
|
502
|
-
// `.s-panel-drift` comment). A pure push or a pure close drifts sideways.
|
|
503
|
-
const entering = nav !== "load" && nav !== "back";
|
|
504
|
-
const drift = !(entering && target.stack.some((p) => !existing.has(p)) &&
|
|
505
|
-
this.$state.live.some((e) => !target.stack.includes(e.path)));
|
|
506
514
|
const next = [];
|
|
507
515
|
for (const path of target.stack) {
|
|
508
516
|
const kept = existing.get(path);
|
|
@@ -513,17 +521,23 @@ export class PanelStackController {
|
|
|
513
521
|
continue;
|
|
514
522
|
}
|
|
515
523
|
const entry = this.createEntry(path, next.length <= target.focus, seedPins.has(path));
|
|
516
|
-
// An initial load just appears
|
|
517
|
-
//
|
|
518
|
-
|
|
524
|
+
// An initial load just appears; any later navigation animates its new
|
|
525
|
+
// panels in — beneath what's already there, so a back that re-creates
|
|
526
|
+
// a panel plays out under the one it takes away.
|
|
527
|
+
if (nav !== "load")
|
|
519
528
|
entry.enter = true;
|
|
520
|
-
entry.drift = drift;
|
|
521
|
-
}
|
|
522
529
|
next.push(entry);
|
|
523
530
|
this.$open[path] = entry;
|
|
524
531
|
}
|
|
525
|
-
|
|
526
|
-
|
|
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
|
+
}
|
|
527
541
|
this.$state.live = next;
|
|
528
542
|
this.$state.focus = Math.min(target.focus, next.length - 1);
|
|
529
543
|
this.scheduleLayout();
|
|
@@ -562,23 +576,16 @@ export class PanelStackController {
|
|
|
562
576
|
* at the end of the animation. Only the element lingers to play that animation,
|
|
563
577
|
* which is what `drawPanel`'s `destroy=` hook hands to {@link playExit}.
|
|
564
578
|
*/
|
|
565
|
-
beginClose(entry,
|
|
579
|
+
beginClose(entry, anchor) {
|
|
566
580
|
entry.closing = true;
|
|
567
|
-
entry.
|
|
581
|
+
entry.anchor = anchor;
|
|
568
582
|
entry.$panel.visible = false;
|
|
569
|
-
// Frozen one layer below where it was, so it fades out over the panel it
|
|
570
|
-
// uncovers and under the one replacing it (see LAYER_STEP). Set here, while
|
|
571
|
-
// the element is still ours — a moment later `entry.el` is gone.
|
|
572
|
-
if (entry.el)
|
|
573
|
-
entry.el.style.zIndex = String(LAYER_STEP * this.$state.live.indexOf(entry));
|
|
574
583
|
delete this.$open[entry.path];
|
|
575
584
|
}
|
|
576
585
|
/**
|
|
577
|
-
* A closed panel's send-off, run by Aberdeen once its scope is gone: it fades
|
|
578
|
-
*
|
|
579
|
-
*
|
|
580
|
-
* panel appear to fade half-way and vanish; the timeout is only a fallback for
|
|
581
|
-
* 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}).
|
|
582
589
|
*/
|
|
583
590
|
playExit(entry, el) {
|
|
584
591
|
// A panel being *redrawn* replaces its element through here too; that one
|
|
@@ -588,20 +595,42 @@ export class PanelStackController {
|
|
|
588
595
|
return;
|
|
589
596
|
}
|
|
590
597
|
el.classList.add("s-panel-closing");
|
|
591
|
-
if (entry.drift)
|
|
592
|
-
el.classList.add("s-panel-drift");
|
|
593
598
|
el.setAttribute("inert", "");
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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);
|
|
597
606
|
el.remove();
|
|
598
|
-
};
|
|
599
|
-
el.addEventListener("transitionend", (e) => {
|
|
600
|
-
if (e.target === el && e.propertyName === "opacity")
|
|
601
|
-
drop();
|
|
602
607
|
});
|
|
603
|
-
|
|
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);
|
|
604
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));
|
|
605
634
|
}
|
|
606
635
|
// ── Navigation ─────────────────────────────────────────────────────────
|
|
607
636
|
/**
|
|
@@ -1265,9 +1294,11 @@ export class PanelStackController {
|
|
|
1265
1294
|
first = i;
|
|
1266
1295
|
}
|
|
1267
1296
|
}
|
|
1268
|
-
//
|
|
1269
|
-
//
|
|
1270
|
-
|
|
1297
|
+
// A run that doesn't fill the fixed-width area centres in it — unless
|
|
1298
|
+
// panels sit crowded out on its left: then it hugs the left edge instead,
|
|
1299
|
+
// so the strip crosses that edge with no gap and a reveal is a plain
|
|
1300
|
+
// slide back in.
|
|
1301
|
+
const left = first > 0 ? 0 : (geom.area - runSum) / 2;
|
|
1271
1302
|
for (let i = first; i <= cur; i++)
|
|
1272
1303
|
live[i].width = width(live[i]);
|
|
1273
1304
|
// Never-visible panels get their would-be width, so a reveal doesn't start
|
|
@@ -1276,68 +1307,156 @@ export class PanelStackController {
|
|
|
1276
1307
|
if (!entry.width)
|
|
1277
1308
|
entry.width = width(entry);
|
|
1278
1309
|
}
|
|
1279
|
-
// Phase 1 —
|
|
1280
|
-
//
|
|
1281
|
-
//
|
|
1310
|
+
// Phase 1 — lay the strip out for this frame: each panel flush against
|
|
1311
|
+
// its neighbours, the visible run [first..cur] in the viewport, earlier
|
|
1312
|
+
// panels continuing off its left edge and parked ones held past its
|
|
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.
|
|
1282
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
|
+
}
|
|
1283
1334
|
let x = left;
|
|
1335
|
+
let delta = 0;
|
|
1336
|
+
for (let i = 0; i < first; i++)
|
|
1337
|
+
x -= live[i].width;
|
|
1284
1338
|
for (let i = 0; i < n; i++) {
|
|
1285
1339
|
const entry = live[i];
|
|
1286
1340
|
const el = entry.el;
|
|
1287
1341
|
const shown = i >= first && i <= cur;
|
|
1288
|
-
//
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1342
|
+
// Parked panels never dip into the viewport, however short the run.
|
|
1343
|
+
if (i === cur + 1)
|
|
1344
|
+
x = Math.max(x, geom.area);
|
|
1345
|
+
// A panel that mounts while still fetching holds its fade for a
|
|
1346
|
+
// moment, so it can appear with real content instead of empty.
|
|
1347
|
+
if (entry.enter || !entry.placed) {
|
|
1348
|
+
if (!entry.$panel.loading || entry.holdDone)
|
|
1349
|
+
entry.$ui.holding = false;
|
|
1350
|
+
else if (!entry.$ui.holding) {
|
|
1351
|
+
entry.$ui.holding = true;
|
|
1352
|
+
this.holdEnter(entry);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
if (entry.placed) {
|
|
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
|
+
}
|
|
1367
|
+
el.style.left = `${x}px`;
|
|
1368
|
+
// A fade held back for content starts the moment its hold lifts.
|
|
1369
|
+
if (entry.enter && !entry.$ui.holding)
|
|
1370
|
+
this.releaseEnter(entry);
|
|
1371
|
+
}
|
|
1372
|
+
else {
|
|
1373
|
+
fresh.push(entry);
|
|
1374
|
+
el.style.left = `${x}px`;
|
|
1375
|
+
if (entry.enter && shown) {
|
|
1376
|
+
el.style.transform = `translateX(${delta}px)`;
|
|
1377
|
+
el.classList.add("s-panel-enter", "s-panel-new");
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
el.style.width = `${entry.width}px`;
|
|
1381
|
+
x += entry.width;
|
|
1292
1382
|
// Written only on a change, so per-panel UI hanging off `visible` or
|
|
1293
1383
|
// `width` isn't rebuilt by every pass.
|
|
1294
1384
|
if (entry.$panel.visible !== shown)
|
|
1295
1385
|
entry.$panel.visible = shown;
|
|
1296
1386
|
if (entry.$panel.width !== entry.width)
|
|
1297
1387
|
entry.$panel.width = entry.width;
|
|
1298
|
-
if (shown)
|
|
1299
|
-
x += entry.width;
|
|
1300
1388
|
el.classList.toggle("s-panel-sep", shown && i > first);
|
|
1301
|
-
// Off-screen panels
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
if (!entry.$panel.loading || entry.holdDone)
|
|
1311
|
-
entry.$ui.holding = false;
|
|
1312
|
-
else if (!entry.$ui.holding) {
|
|
1313
|
-
entry.$ui.holding = true;
|
|
1314
|
-
this.holdEnter(entry);
|
|
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");
|
|
1315
1398
|
}
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
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;
|
|
1412
|
+
el.toggleAttribute("inert", !shown);
|
|
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)`;
|
|
1322
1425
|
}
|
|
1323
1426
|
}
|
|
1324
1427
|
// Phase 2 — reading a layout property forces the browser to adopt those
|
|
1325
|
-
//
|
|
1326
|
-
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)
|
|
1327
1430
|
void container.offsetWidth;
|
|
1328
1431
|
if (snap)
|
|
1329
1432
|
shell.classList.remove("s-shell-snap");
|
|
1330
|
-
// Phase 3 —
|
|
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
|
|
1435
|
+
// for its content, which keeps the fade's start state on until then.
|
|
1436
|
+
for (const el of movers) {
|
|
1437
|
+
el.style.transition = "";
|
|
1438
|
+
el.style.transform = "";
|
|
1439
|
+
}
|
|
1331
1440
|
for (const entry of fresh) {
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
entry.el.classList.remove("s-panel-enter", "s-panel-drift");
|
|
1336
|
-
entry.enter = false;
|
|
1441
|
+
const el = entry.el;
|
|
1442
|
+
el.style.transition = "";
|
|
1443
|
+
el.style.transform = "";
|
|
1337
1444
|
entry.placed = true;
|
|
1445
|
+
if (!entry.$ui.holding)
|
|
1446
|
+
this.releaseEnter(entry);
|
|
1338
1447
|
}
|
|
1339
1448
|
}
|
|
1340
|
-
/**
|
|
1449
|
+
/** Start (or skip) a newcomer's fade-in; any slide is already underway. */
|
|
1450
|
+
releaseEnter(entry) {
|
|
1451
|
+
entry.enter = false;
|
|
1452
|
+
const el = entry.el;
|
|
1453
|
+
if (!el || !el.classList.contains("s-panel-enter"))
|
|
1454
|
+
return;
|
|
1455
|
+
el.classList.remove("s-panel-enter");
|
|
1456
|
+
// Keep it beneath its elders until the fade is over.
|
|
1457
|
+
this.afterTransition(el, "opacity", () => el.classList.remove("s-panel-new"));
|
|
1458
|
+
}
|
|
1459
|
+
/** Let a `loading` panel's fade-in wait — but not indefinitely. */
|
|
1341
1460
|
holdEnter(entry) {
|
|
1342
1461
|
const timer = setTimeout(() => {
|
|
1343
1462
|
this.timers.delete(timer);
|
|
@@ -1350,16 +1469,15 @@ export class PanelStackController {
|
|
|
1350
1469
|
this.timers.add(timer);
|
|
1351
1470
|
}
|
|
1352
1471
|
}
|
|
1353
|
-
/** Put a panel at rest: `x` from the region's left edge, `width` pixels wide, on layer `z`. */
|
|
1354
|
-
function place(el, x, width, z) {
|
|
1355
|
-
el.style.left = `${x}px`;
|
|
1356
|
-
el.style.width = `${width}px`;
|
|
1357
|
-
el.style.zIndex = String(z);
|
|
1358
|
-
}
|
|
1359
1472
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
1360
1473
|
function sameStack(a, b) {
|
|
1361
1474
|
return a.length === b.length && a.every((v, i) => v === b[i]);
|
|
1362
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
|
+
}
|
|
1363
1481
|
/**
|
|
1364
1482
|
* The first non-empty text inside `el`, trimmed and capped at a name-like
|
|
1365
1483
|
* length — the stand-in title for a panel that never set one.
|