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/src/components/panels.ts
CHANGED
|
@@ -416,10 +416,11 @@ function matchRoute(r: { segs: Seg[] }, segments: string[]): Record<string, any>
|
|
|
416
416
|
|
|
417
417
|
/**
|
|
418
418
|
* The one duration every bit of shell motion shares: the enter/exit fades, the
|
|
419
|
-
*
|
|
420
|
-
*
|
|
419
|
+
* slides, the narrow-screen nav slide. Interpolated into every transition as a
|
|
420
|
+
* literal — the shared constant is what keeps CSS and JS in step — and also
|
|
421
|
+
* published as the `--s-panel-ms` custom property for app CSS.
|
|
421
422
|
*/
|
|
422
|
-
const PAGE_MS = 250;
|
|
423
|
+
export const PAGE_MS = 250;
|
|
423
424
|
/** How long a freshly pushed `loading` panel holds its fade-in. */
|
|
424
425
|
const LOADING_HOLD_MS = 300;
|
|
425
426
|
/**
|
|
@@ -447,16 +448,23 @@ A.insertGlobalCss({
|
|
|
447
448
|
PANEL_SHEEN,
|
|
448
449
|
".s-panel": {
|
|
449
450
|
// The shell knows three motions, and this vocabulary is all of them:
|
|
450
|
-
// a panel that MOVES
|
|
451
|
-
//
|
|
452
|
-
//
|
|
453
|
-
//
|
|
454
|
-
// CLOSED one
|
|
455
|
-
//
|
|
456
|
-
//
|
|
457
|
-
//
|
|
458
|
-
//
|
|
459
|
-
//
|
|
451
|
+
// a panel that MOVES slides — every mover in a pass shares this one
|
|
452
|
+
// duration and ease-out, so panels travelling the same distance travel
|
|
453
|
+
// as one — a CREATED panel joins the strip beside the old position of
|
|
454
|
+
// the panels beneath it and rides their slide while fading in, and a
|
|
455
|
+
// CLOSED one keeps its seat beside the surviving panel beneath it and
|
|
456
|
+
// rides that panel's slide while fading out — its creation played
|
|
457
|
+
// backwards. Nothing else ever animates.
|
|
458
|
+
// A slide is a `transform` playing out to none (see `layout`), never an
|
|
459
|
+
// animated `left`: left is a layout property, so animating it would
|
|
460
|
+
// relayout and repaint every travelling column on every frame, where a
|
|
461
|
+
// transform moves composited layers. `left` always holds the resting
|
|
462
|
+
// position — it is not in the transition list — and the transform
|
|
463
|
+
// exists only while a panel travels, so text gets its subpixel
|
|
464
|
+
// antialiasing back the moment it settles. No `width` transition
|
|
465
|
+
// either — animating one reflows the column every frame. And the fade
|
|
466
|
+
// is `linear` while the moves ease out: an eased opacity spends its
|
|
467
|
+
// last stretch near zero, reading as a vanish.
|
|
460
468
|
// Layering is fixed per state, not per stack depth: live panels never
|
|
461
469
|
// overlap each other (the strip keeps them adjacent, see `layout()`), so
|
|
462
470
|
// only the fading ones need an order — below, in both directions, per
|
|
@@ -466,7 +474,7 @@ A.insertGlobalCss({
|
|
|
466
474
|
"&":
|
|
467
475
|
"position:absolute top:0 bottom:0 left:0 display:flex flex-direction:column " +
|
|
468
476
|
PANEL_SHEEN + " " +
|
|
469
|
-
|
|
477
|
+
`z-index:2 transition: transform ${PAGE_MS}ms ease-out, opacity ${PAGE_MS}ms linear;`,
|
|
470
478
|
// The hairline between two columns, fading at both ends (like the sidebar's
|
|
471
479
|
// `.s-nav-sep`). Columns tile with no gutter — each brings its own `$3` of
|
|
472
480
|
// padding — so this sits exactly on the boundary.
|
|
@@ -477,9 +485,10 @@ A.insertGlobalCss({
|
|
|
477
485
|
// whatever slides across its spot passes over it. Dropped once the fade
|
|
478
486
|
// is over (see `releaseEnter`).
|
|
479
487
|
"&.s-panel-new": "z-index:1",
|
|
480
|
-
// On its way out: it fades
|
|
481
|
-
// (declared after `.s-panel-new`, so closing
|
|
482
|
-
// the bottom), and leaves the DOM when the
|
|
488
|
+
// On its way out: it fades, still riding the strip (see `layout`),
|
|
489
|
+
// beneath every live panel (declared after `.s-panel-new`, so closing
|
|
490
|
+
// mid-enter drops a panel to the bottom), and leaves the DOM when the
|
|
491
|
+
// fade ends (see `playExit`).
|
|
483
492
|
"&.s-panel-closing": "z-index:0 opacity:0 pointer-events:none",
|
|
484
493
|
// The fade-in's start state: a newcomer wears it from creation until its
|
|
485
494
|
// content is ready — usually the very pass that placed it, later for a
|
|
@@ -490,12 +499,13 @@ A.insertGlobalCss({
|
|
|
490
499
|
// edges, so these rest at their true positions, clipped — being crowded
|
|
491
500
|
// out or revealed is an ordinary move, not a fade. Both keep their DOM —
|
|
492
501
|
// and so their scroll position and half-typed forms — hence
|
|
493
|
-
// `visibility`, not `display:none
|
|
494
|
-
//
|
|
495
|
-
//
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
502
|
+
// `visibility`, not `display:none`. The flip is timed by `layout()`,
|
|
503
|
+
// never transitioned: a panel sliding offstage gets the class once its
|
|
504
|
+
// slide ends and drops it the moment it is revealed. `visibility` is
|
|
505
|
+
// not compositable, and Chrome composites an element's transitions as
|
|
506
|
+
// a group, so listing it here would drag the slide itself onto the
|
|
507
|
+
// main thread — relaid out and repainted every frame.
|
|
508
|
+
"&.s-panel-hidden, &.s-panel-parked": "visibility:hidden",
|
|
499
509
|
},
|
|
500
510
|
// The scroll container, with the column's own padding. Its scrollbar sits
|
|
501
511
|
// flush against the column edge (unlike content mode's inset one), so it meets
|
|
@@ -601,12 +611,24 @@ interface PanelEntry {
|
|
|
601
611
|
hash?: string;
|
|
602
612
|
/** Set once the panel is on its way out, playing its exit animation. */
|
|
603
613
|
closing?: boolean;
|
|
614
|
+
/**
|
|
615
|
+
* The nearest surviving panel beneath this one when it closed. The exit
|
|
616
|
+
* rides that panel's motion (see `layout`), the mirror of how a newcomer
|
|
617
|
+
* rides the panels it opens over, so a close plays the open backwards.
|
|
618
|
+
*/
|
|
619
|
+
anchor?: string;
|
|
604
620
|
/** Set while the fade-in is still owed (a `loading` hold can owe it past placement). */
|
|
605
621
|
enter?: boolean;
|
|
606
622
|
/** Whether the panel has been through a full layout pass (and so may animate). */
|
|
607
623
|
placed?: boolean;
|
|
608
624
|
/** Whether its `loading` hold has already expired, so it can't hold again. */
|
|
609
625
|
holdDone?: boolean;
|
|
626
|
+
/**
|
|
627
|
+
* Whether the last layout pass put this panel off screen. The deferred
|
|
628
|
+
* visibility flip (see `layout`) consults it when it fires, so a panel
|
|
629
|
+
* revealed again before its slide out ended is never hidden by mistake.
|
|
630
|
+
*/
|
|
631
|
+
offstage?: boolean;
|
|
610
632
|
/** What the panel asks for, kept in step with its `$panel.maxWidth`. */
|
|
611
633
|
maxWidth: PanelSize;
|
|
612
634
|
/**
|
|
@@ -813,6 +835,8 @@ export class PanelStackController implements PanelStack {
|
|
|
813
835
|
private lastGeom?: Geometry;
|
|
814
836
|
private layoutQueued = false;
|
|
815
837
|
private timers = new Set<ReturnType<typeof setTimeout>>();
|
|
838
|
+
/** Elements playing their exit fade, each riding its anchor's motion (see `layout`). */
|
|
839
|
+
private exiting = new Set<{ el: HTMLElement; anchor?: string; ride: number }>();
|
|
816
840
|
/** The arrangement the navigation in flight is heading for; see {@link intended}. */
|
|
817
841
|
private intent: Arrangement | null = null;
|
|
818
842
|
/** The navigation the router hasn't settled yet, if any. */
|
|
@@ -1049,7 +1073,13 @@ export class PanelStackController implements PanelStack {
|
|
|
1049
1073
|
next.push(entry);
|
|
1050
1074
|
this.$open[path] = entry;
|
|
1051
1075
|
}
|
|
1052
|
-
|
|
1076
|
+
// What remains in `existing` closes, each remembering the nearest
|
|
1077
|
+
// surviving panel beneath it — the anchor its exit fade rides.
|
|
1078
|
+
let anchor: string | undefined;
|
|
1079
|
+
for (const entry of this.$state.live) {
|
|
1080
|
+
if (existing.has(entry.path)) this.beginClose(entry, anchor);
|
|
1081
|
+
else anchor = entry.path;
|
|
1082
|
+
}
|
|
1053
1083
|
this.$state.live = next;
|
|
1054
1084
|
this.$state.focus = Math.min(target.focus, next.length - 1);
|
|
1055
1085
|
this.scheduleLayout();
|
|
@@ -1090,18 +1120,17 @@ export class PanelStackController implements PanelStack {
|
|
|
1090
1120
|
* at the end of the animation. Only the element lingers to play that animation,
|
|
1091
1121
|
* which is what `drawPanel`'s `destroy=` hook hands to {@link playExit}.
|
|
1092
1122
|
*/
|
|
1093
|
-
private beginClose(entry: PanelEntry): void {
|
|
1123
|
+
private beginClose(entry: PanelEntry, anchor?: string): void {
|
|
1094
1124
|
entry.closing = true;
|
|
1125
|
+
entry.anchor = anchor;
|
|
1095
1126
|
entry.$panel.visible = false;
|
|
1096
1127
|
delete this.$open[entry.path];
|
|
1097
1128
|
}
|
|
1098
1129
|
|
|
1099
1130
|
/**
|
|
1100
|
-
* A closed panel's send-off, run by Aberdeen once its scope is gone: it fades
|
|
1101
|
-
*
|
|
1102
|
-
*
|
|
1103
|
-
* panel appear to fade half-way and vanish; the timeout is only a fallback for
|
|
1104
|
-
* when no `transitionend` is coming (transitions off, element never placed).
|
|
1131
|
+
* A closed panel's send-off, run by Aberdeen once its scope is gone: it fades,
|
|
1132
|
+
* inert, riding its anchor's slide (see `layout`), and leaves the DOM once
|
|
1133
|
+
* the fade is over (see {@link afterFade}).
|
|
1105
1134
|
*/
|
|
1106
1135
|
private playExit(entry: PanelEntry, el: HTMLElement): void {
|
|
1107
1136
|
// A panel being *redrawn* replaces its element through here too; that one
|
|
@@ -1109,16 +1138,36 @@ export class PanelStackController implements PanelStack {
|
|
|
1109
1138
|
if (!entry.closing) { el.remove(); return; }
|
|
1110
1139
|
el.classList.add("s-panel-closing");
|
|
1111
1140
|
el.setAttribute("inert", "");
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1141
|
+
// Only a placed panel has a seat on the strip to ride out from.
|
|
1142
|
+
const exit = entry.placed ? { el, anchor: entry.anchor, ride: 0 } : null;
|
|
1143
|
+
if (exit) this.exiting.add(exit);
|
|
1144
|
+
this.afterTransition(el, "opacity", () => {
|
|
1145
|
+
if (exit) this.exiting.delete(exit);
|
|
1115
1146
|
el.remove();
|
|
1116
|
-
};
|
|
1117
|
-
el.addEventListener("transitionend", (e: TransitionEvent) => {
|
|
1118
|
-
if (e.target === el && e.propertyName === "opacity") drop();
|
|
1119
1147
|
});
|
|
1120
|
-
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
* Run `done` once `el`'s transition of `prop` is over. The real signal is
|
|
1152
|
+
* `transitionend` — or `transitioncancel`, for one a resize snaps short —
|
|
1153
|
+
* so the transition's actual length rules, however long: DevTools' slowed
|
|
1154
|
+
* animations stretch it tenfold without touching any timer. The timer only
|
|
1155
|
+
* stands in for a transition that never starts at all (transitions off,
|
|
1156
|
+
* element never placed, nothing to travel), which is why one proving real
|
|
1157
|
+
* (`transitionrun`) disarms it.
|
|
1158
|
+
*/
|
|
1159
|
+
private afterTransition(el: HTMLElement, prop: string, done: () => void): void {
|
|
1160
|
+
let called = false;
|
|
1161
|
+
const timer = setTimeout(() => finish(), PAGE_MS + 80);
|
|
1121
1162
|
this.timers.add(timer);
|
|
1163
|
+
const disarm = () => { clearTimeout(timer); this.timers.delete(timer); };
|
|
1164
|
+
const finish = () => { disarm(); if (!called) { called = true; done(); } };
|
|
1165
|
+
const forProp = (fn: () => void) => (e: TransitionEvent) => {
|
|
1166
|
+
if (e.target === el && e.propertyName === prop) fn();
|
|
1167
|
+
};
|
|
1168
|
+
el.addEventListener("transitionrun", forProp(disarm));
|
|
1169
|
+
el.addEventListener("transitionend", forProp(finish));
|
|
1170
|
+
el.addEventListener("transitioncancel", forProp(finish));
|
|
1122
1171
|
}
|
|
1123
1172
|
|
|
1124
1173
|
// ── Navigation ─────────────────────────────────────────────────────────
|
|
@@ -1821,14 +1870,23 @@ export class PanelStackController implements PanelStack {
|
|
|
1821
1870
|
// Phase 1 — lay the strip out for this frame: each panel flush against
|
|
1822
1871
|
// its neighbours, the visible run [first..cur] in the viewport, earlier
|
|
1823
1872
|
// panels continuing off its left edge and parked ones held past its
|
|
1824
|
-
// right.
|
|
1825
|
-
//
|
|
1826
|
-
//
|
|
1827
|
-
//
|
|
1828
|
-
//
|
|
1829
|
-
//
|
|
1830
|
-
//
|
|
1831
|
-
|
|
1873
|
+
// right. `left` gets each new resting position outright — it never
|
|
1874
|
+
// animates — and the slide to it is a FLIP: a mover is held at its
|
|
1875
|
+
// current visual spot by a transform (its mid-slide offset plus the
|
|
1876
|
+
// distance), a newcomer at its start beside the OLD position of the
|
|
1877
|
+
// nearest placed panel beneath it (`delta`, so the slide in is one
|
|
1878
|
+
// motion with the panels making room; with nothing beneath it to come
|
|
1879
|
+
// from, it simply fades in place). Phase 3 plays every held transform
|
|
1880
|
+
// out to none. A snap pass holds nothing: geometry must land, not
|
|
1881
|
+
// travel.
|
|
1882
|
+
const fresh: PanelEntry[] = [];
|
|
1883
|
+
const movers: HTMLElement[] = [];
|
|
1884
|
+
const deltas = new Map<string, number>();
|
|
1885
|
+
// All reads before all writes: a mover may still be mid-slide, and its
|
|
1886
|
+
// current offset must come out of the computed style before this pass
|
|
1887
|
+
// dirties it (one style recalc, then cached).
|
|
1888
|
+
const txs = new Map<PanelEntry, number>();
|
|
1889
|
+
if (!snap) for (const entry of live) { if (entry.placed) txs.set(entry, transformX(entry.el!)); }
|
|
1832
1890
|
let x = left;
|
|
1833
1891
|
let delta = 0;
|
|
1834
1892
|
for (let i = 0; i < first; i++) x -= live[i].width;
|
|
@@ -1846,14 +1904,26 @@ export class PanelStackController implements PanelStack {
|
|
|
1846
1904
|
}
|
|
1847
1905
|
if (entry.placed) {
|
|
1848
1906
|
delta = parseFloat(el.style.left) - x;
|
|
1907
|
+
deltas.set(entry.path, delta);
|
|
1908
|
+
if (delta && !snap) {
|
|
1909
|
+
// Hold the visual spot while `left` jumps beneath it. The
|
|
1910
|
+
// inline transition keeps the transform still — and only it:
|
|
1911
|
+
// running fades carry on, and one starting this pass (a
|
|
1912
|
+
// release below) still gets its linear curve.
|
|
1913
|
+
el.style.transition = `opacity ${PAGE_MS}ms linear`;
|
|
1914
|
+
el.style.transform = `translateX(${(txs.get(entry) ?? 0) + delta}px)`;
|
|
1915
|
+
movers.push(el);
|
|
1916
|
+
}
|
|
1849
1917
|
el.style.left = `${x}px`;
|
|
1850
1918
|
// A fade held back for content starts the moment its hold lifts.
|
|
1851
1919
|
if (entry.enter && !entry.$ui.holding) this.releaseEnter(entry);
|
|
1852
1920
|
} else {
|
|
1853
|
-
fresh.push(
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1921
|
+
fresh.push(entry);
|
|
1922
|
+
el.style.left = `${x}px`;
|
|
1923
|
+
if (entry.enter && shown) {
|
|
1924
|
+
el.style.transform = `translateX(${delta}px)`;
|
|
1925
|
+
el.classList.add("s-panel-enter", "s-panel-new");
|
|
1926
|
+
}
|
|
1857
1927
|
}
|
|
1858
1928
|
el.style.width = `${entry.width}px`;
|
|
1859
1929
|
x += entry.width;
|
|
@@ -1862,23 +1932,58 @@ export class PanelStackController implements PanelStack {
|
|
|
1862
1932
|
if (entry.$panel.visible !== shown) entry.$panel.visible = shown;
|
|
1863
1933
|
if (entry.$panel.width !== entry.width) entry.$panel.width = entry.width;
|
|
1864
1934
|
el.classList.toggle("s-panel-sep", shown && i > first);
|
|
1865
|
-
// Off-screen panels stop rendering, keeping their DOM.
|
|
1866
|
-
|
|
1867
|
-
|
|
1935
|
+
// Off-screen panels stop rendering, keeping their DOM. A revealed
|
|
1936
|
+
// panel is visible at once; one sliding offstage stays visible for
|
|
1937
|
+
// the whole slide, its visibility class deferred to the slide's
|
|
1938
|
+
// end — but applied outright when it is already invisible, about
|
|
1939
|
+
// to be (a newcomer offstage from birth), or snapping.
|
|
1940
|
+
const hidden = i < first;
|
|
1941
|
+
const wasOff = el.classList.contains("s-panel-hidden") || el.classList.contains("s-panel-parked");
|
|
1942
|
+
if (shown) {
|
|
1943
|
+
el.classList.remove("s-panel-hidden", "s-panel-parked");
|
|
1944
|
+
} else if (wasOff || !entry.placed || snap) {
|
|
1945
|
+
el.classList.toggle("s-panel-hidden", hidden);
|
|
1946
|
+
el.classList.toggle("s-panel-parked", !hidden);
|
|
1947
|
+
} else {
|
|
1948
|
+
this.afterTransition(el, "transform", () => {
|
|
1949
|
+
if (entry.el !== el || !entry.offstage) return;
|
|
1950
|
+
el.classList.toggle("s-panel-hidden", hidden);
|
|
1951
|
+
el.classList.toggle("s-panel-parked", !hidden);
|
|
1952
|
+
});
|
|
1953
|
+
}
|
|
1954
|
+
entry.offstage = !shown;
|
|
1868
1955
|
el.toggleAttribute("inert", !shown);
|
|
1869
1956
|
}
|
|
1870
1957
|
|
|
1958
|
+
// Closing panels keep their seat on the strip while they fade: each
|
|
1959
|
+
// travels exactly as far as its anchor — the surviving panel it stood
|
|
1960
|
+
// on — so a close slides back out the way the open slid in, and one
|
|
1961
|
+
// whose anchor stays put is the plain crossfade it should be. A plain
|
|
1962
|
+
// retarget, no holding: the transition picks the slide up from wherever
|
|
1963
|
+
// the element is now.
|
|
1964
|
+
for (const exit of this.exiting) {
|
|
1965
|
+
const d = exit.anchor == null ? undefined : deltas.get(exit.anchor);
|
|
1966
|
+
if (d) {
|
|
1967
|
+
exit.ride -= d;
|
|
1968
|
+
exit.el.style.transform = `translateX(${exit.ride}px)`;
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1871
1972
|
// Phase 2 — reading a layout property forces the browser to adopt those
|
|
1872
|
-
//
|
|
1873
|
-
if (fresh.length || snap) void container.offsetWidth;
|
|
1973
|
+
// held spots (and a snap pass's transition-free geometry) to slide from.
|
|
1974
|
+
if (fresh.length || movers.length || snap) void container.offsetWidth;
|
|
1874
1975
|
if (snap) shell.classList.remove("s-shell-snap");
|
|
1875
|
-
// Phase 3 —
|
|
1876
|
-
//
|
|
1976
|
+
// Phase 3 — every held transform plays out to none: the slides start
|
|
1977
|
+
// now, and the newcomers' fades with them — unless a panel is holding
|
|
1877
1978
|
// for its content, which keeps the fade's start state on until then.
|
|
1878
|
-
for (const
|
|
1979
|
+
for (const el of movers) {
|
|
1980
|
+
el.style.transition = "";
|
|
1981
|
+
el.style.transform = "";
|
|
1982
|
+
}
|
|
1983
|
+
for (const entry of fresh) {
|
|
1879
1984
|
const el = entry.el!;
|
|
1880
1985
|
el.style.transition = "";
|
|
1881
|
-
el.style.
|
|
1986
|
+
el.style.transform = "";
|
|
1882
1987
|
entry.placed = true;
|
|
1883
1988
|
if (!entry.$ui.holding) this.releaseEnter(entry);
|
|
1884
1989
|
}
|
|
@@ -1890,14 +1995,8 @@ export class PanelStackController implements PanelStack {
|
|
|
1890
1995
|
const el = entry.el;
|
|
1891
1996
|
if (!el || !el.classList.contains("s-panel-enter")) return;
|
|
1892
1997
|
el.classList.remove("s-panel-enter");
|
|
1893
|
-
// Keep it beneath its elders until the fade is over
|
|
1894
|
-
|
|
1895
|
-
// `transitionend`.
|
|
1896
|
-
const timer = setTimeout(() => {
|
|
1897
|
-
this.timers.delete(timer);
|
|
1898
|
-
el.classList.remove("s-panel-new");
|
|
1899
|
-
}, PAGE_MS + 80);
|
|
1900
|
-
this.timers.add(timer);
|
|
1998
|
+
// Keep it beneath its elders until the fade is over.
|
|
1999
|
+
this.afterTransition(el, "opacity", () => el.classList.remove("s-panel-new"));
|
|
1901
2000
|
}
|
|
1902
2001
|
|
|
1903
2002
|
/** Let a `loading` panel's fade-in wait — but not indefinitely. */
|
|
@@ -1920,6 +2019,12 @@ function sameStack(a: string[], b: string[]): boolean {
|
|
|
1920
2019
|
return a.length === b.length && a.every((v, i) => v === b[i]);
|
|
1921
2020
|
}
|
|
1922
2021
|
|
|
2022
|
+
/** The X offset of `el`'s computed transform — where a slide has it right now. */
|
|
2023
|
+
function transformX(el: HTMLElement): number {
|
|
2024
|
+
const t = getComputedStyle(el).transform;
|
|
2025
|
+
return t && t !== "none" ? new DOMMatrixReadOnly(t).m41 : 0;
|
|
2026
|
+
}
|
|
2027
|
+
|
|
1923
2028
|
/**
|
|
1924
2029
|
* The first non-empty text inside `el`, trimmed and capped at a name-like
|
|
1925
2030
|
* length — the stand-in title for a panel that never set one.
|
package/src/theme.ts
CHANGED
|
@@ -32,9 +32,11 @@ import A from "aberdeen";
|
|
|
32
32
|
* declaration, at the given angle. A shared constant rather than a `--s-sheen`
|
|
33
33
|
* custom property: `var()`s inside a custom property resolve where it is
|
|
34
34
|
* *defined*, so every surface would get the page's wash instead of its own.
|
|
35
|
+
* The shorthand names `$s-bg` as well as the gradient: a `background:` that
|
|
36
|
+
* gave only an image would reset the colour under it to transparent.
|
|
35
37
|
*/
|
|
36
38
|
const sheen = (angle: string) =>
|
|
37
|
-
`background: linear-gradient(${angle}, color-mix(in oklab, $s-bg, white 9%), color-mix(in oklab, $s-bg, black 9%));`;
|
|
39
|
+
`background: $s-bg linear-gradient(${angle}, color-mix(in oklab, $s-bg, white 9%), color-mix(in oklab, $s-bg, black 9%));`;
|
|
38
40
|
|
|
39
41
|
/** The surface sheen: the wash every surface (and the page) is painted with. */
|
|
40
42
|
export const SURFACE_SHEEN = sheen("170deg");
|
|
@@ -120,7 +122,14 @@ A.insertGlobalCss({
|
|
|
120
122
|
// A lightweight reset: bare semantic HTML, with less ugly defaults.
|
|
121
123
|
"*, *::before, *::after": "box-sizing:border-box",
|
|
122
124
|
html: "text-size-adjust:100%",
|
|
123
|
-
|
|
125
|
+
// The page wears the same wash as a surface, on a page at least as tall as
|
|
126
|
+
// what that wash is painted on: <body>'s background is propagated to the
|
|
127
|
+
// canvas, which covers the whole viewport, while the gradient is still
|
|
128
|
+
// *sized* from the root box. So a page shorter than the viewport — a bare
|
|
129
|
+
// spinner while an app boots — would tile its wash down the rest of the
|
|
130
|
+
// canvas in bands. `dvh`, not `vh`: a mobile viewport that grows as the URL
|
|
131
|
+
// bar retracts would open a band again.
|
|
132
|
+
body: "m:0 p:$3 min-height:100dvh line-height:1.5 font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; -webkit-font-smoothing:antialiased text:$s-text " + SURFACE_SHEEN,
|
|
124
133
|
// The contextual link colour: `--s-link` on neutral surfaces, the ink on accent ones.
|
|
125
134
|
a: "color: $s-link-fg; text-decoration:underline text-underline-offset:2px; transition: color 0.12s, filter 0.12s;",
|
|
126
135
|
"a:hover": "filter: brightness(1.15)",
|
|
@@ -196,9 +205,9 @@ A.insertGlobalCss({
|
|
|
196
205
|
"--s-muted: color-mix(in oklab, $s-text, $s-bg 42%); " +
|
|
197
206
|
"--s-faint: color-mix(in oklab, $s-text, $s-bg 80%); " +
|
|
198
207
|
"color:$s-text accent-color:$s-accent scrollbar-width:thin scrollbar-color: $s-faint transparent;",
|
|
199
|
-
// Subtle single-colour gradient sheen, painted on every surface (
|
|
200
|
-
|
|
201
|
-
".s-s": "r:$s-radius",
|
|
208
|
+
// Subtle single-colour gradient sheen, painted on every surface (the page
|
|
209
|
+
// gets its own, up in the reset).
|
|
210
|
+
".s-s": SURFACE_SHEEN + " r:$s-radius",
|
|
202
211
|
// A neutral surface owns a hairline border, so a card reads as a card without
|
|
203
212
|
// any component help. `:where()` keeps it zero-specificity, so a bar that wants
|
|
204
213
|
// only a divider overrides it with a single plain rule.
|