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.
@@ -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
- "transition: transform var(--s-panel-ms) ease;",
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
- "transition: transform var(--s-panel-ms) ease, visibility 0s;",
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, so the dismissed page isn't reachable while it
102
- // waits for Aberdeen's removal timer; on entry it flips instantly (the `0s`
103
- // above), or the opening page would refuse the focus handed to it mid-slide.
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
- "transition: transform var(--s-panel-ms) ease, visibility var(--s-panel-ms);",
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
- * where it stands, inert, and leaves the DOM on `transitionend`. A fixed timer
535
- * would race the transition and pull the element a frame early, making the
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
- * sideways `left` moves, the narrow-screen nav slide. Published below as the
114
- * `--s-panel-ms` custom property, so CSS and JS can't drift apart.
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 animates its `left` — every mover in a pass shares
142
- // this one duration and ease-out, so panels travelling the same distance
143
- // travel as one — a CREATED panel joins the strip beside the old position
144
- // of the panels beneath it and rides their slide while fading in, and a
145
- // CLOSED one fades out where it stood. Nothing else ever animates.
146
- // A plain `left`, never a transform: a transformed element is
147
- // composited, costing it subpixel text antialiasing. No `width`
148
- // transition either animating one reflows the column every frame. And
149
- // the fade is `linear` while the moves ease out: an eased opacity spends
150
- // its last stretch near zero, reading as a vanish.
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
- "z-index:2 transition: left var(--s-panel-ms) ease-out, opacity var(--s-panel-ms) linear;",
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 where it stood, beneath every live panel
170
- // (declared after `.s-panel-new`, so closing mid-enter drops a panel to
171
- // the bottom), and leaves the DOM when the fade ends (see `playExit`).
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`: it holds through the move out
183
- // (flipping only at its end) and lifts instantly on the move back in
184
- // (the base transition above doesn't list it).
185
- "&.s-panel-hidden, &.s-panel-parked": "visibility:hidden " +
186
- "transition: left var(--s-panel-ms) ease-out, opacity var(--s-panel-ms) linear, visibility var(--s-panel-ms);",
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
- for (const entry of existing.values())
520
- this.beginClose(entry);
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
- * where it stands, inert, and leaves the DOM on `transitionend`. A fixed timer
567
- * would race the transition and pull the element a frame early, making the
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
- const drop = () => {
581
- clearTimeout(timer);
582
- this.timers.delete(timer);
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
- const timer = setTimeout(drop, PAGE_MS + 80);
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. Placed panels get their new positionstheir standing
1271
- // transitions carry them there. Newcomers, transitions still off, get
1272
- // their *start* state instead: their strip position anchored to the OLD
1273
- // position of the nearest placed panel beneath them (`delta`), so the
1274
- // slide in is one motion with the panels making room. With nothing
1275
- // beneath them to come from or nothing moving that start is where
1276
- // they already stand, and they simply fade in.
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({ entry, x });
1308
- const entering = entry.enter && shown;
1309
- el.style.left = `${entering ? x + delta : x}px`;
1310
- if (entering)
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
- el.classList.toggle("s-panel-hidden", i < first);
1324
- el.classList.toggle("s-panel-parked", i > cur);
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
- // start states (and a snap pass's transition-free geometry) to animate from.
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 — newcomers get their transitions and their resting place: the
1334
- // slide starts now, and the fade with it — unless the panel is holding
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 { entry, x } of fresh) {
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.left = `${x}px`;
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 — by timer, a hair
1353
- // past it, since a resize can snap the fade short without ever firing a
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.