feat(glass): bake two-tier defaults (Adaptive chrome + media scope), add Starship/Matrix schemes

From UAT: glass reads strongest where there's content behind it, so split the
look into two tiers instead of one global compromise.
- index.css: global :root defaults are now the 'Adaptive' baseline (solid-enough
  dark glass for content-less chrome). New .glass-media scope class carries the
  UAT-tuned translucent values (blur 10 / surface 50% / card 60% / scrim 30% /
  edge 25%) for surfaces that float over real artwork.
- Apply .glass-media to the art-backed Plex detail views (PlexInfo page variant,
  PlexShowView, PlexSeasonView) and drop PlexInfo's hard-coded inline 55%
  overrides so those panels are scope-driven and consistent (hero no longer
  reads solid while the season strips are translucent).
- Two new colour schemes: Starship (deep-space blue, azure accent) and Matrix
  (near-black, phosphor green), each with dark+light tokens + swatches.
- Remove the thumbnail-mosaic backdrop (did nothing usefully; its home is the
  future 'videos behind glass' phase) — deletes MosaicBackdrop, mosaic vars/CSS,
  and the tuner's mosaic controls. GlassTuner presets resynced to the new
  baseline (Current / Media / Max-readable).
This commit is contained in:
2026-07-12 20:12:25 +02:00
parent 2004ab004e
commit cfd02ba471
6 changed files with 108 additions and 173 deletions
+30 -60
View File
@@ -1,5 +1,4 @@
import { useEffect, useMemo, useState } from "react";
import MosaicBackdrop from "./MosaicBackdrop";
/**
* DEV-ONLY live appearance tuner. Mounted at the App root so it stays reachable on every page.
@@ -25,25 +24,19 @@ type Slider = {
def: number;
};
// def = the BAKED baseline in index.css (:root). "Current" preset + the export diff both key off it.
const SLIDERS: Slider[] = [
{ k: "--glass-blur", label: "Blur radius", min: 0, max: 40, step: 1, unit: "px", def: 30 },
{ k: "--glass-blur", label: "Blur radius", min: 0, max: 40, step: 1, unit: "px", def: 20 },
{ k: "--glass-saturate", label: "Saturation", min: 1, max: 2.5, step: 0.05, unit: "", def: 1.8 },
{ k: "--glass-dark-bright", label: "Dark brightness", min: 1, max: 2, step: 0.05, unit: "", def: 1.4 },
{ k: "--glass-surface-alpha", label: "Panel opacity", min: 40, max: 100, step: 1, unit: "%", def: 78 },
{ k: "--glass-card-alpha", label: "Card opacity", min: 40, max: 100, step: 1, unit: "%", def: 84 },
{ k: "--glass-menu-alpha", label: "Menu opacity", min: 60, max: 100, step: 1, unit: "%", def: 92 },
{ k: "--glass-dark-bright", label: "Dark brightness", min: 1, max: 2, step: 0.05, unit: "", def: 1.25 },
{ k: "--glass-surface-alpha", label: "Panel opacity", min: 40, max: 100, step: 1, unit: "%", def: 94 },
{ k: "--glass-card-alpha", label: "Card opacity", min: 40, max: 100, step: 1, unit: "%", def: 94 },
{ k: "--glass-menu-alpha", label: "Menu opacity", min: 60, max: 100, step: 1, unit: "%", def: 98 },
{ k: "--glass-border-alpha", label: "Border opacity", min: 20, max: 100, step: 1, unit: "%", def: 78 },
{ k: "--glass-edge", label: "Edge-light rim", min: 0, max: 60, step: 1, unit: "%", def: 0 },
{ k: "--glass-edge", label: "Edge-light rim", min: 0, max: 60, step: 1, unit: "%", def: 10 },
{ k: "--glass-inset", label: "Top highlight", min: 0, max: 40, step: 1, unit: "%", def: 16 },
{ k: "--glass-scrim", label: "Under-text scrim", min: 0, max: 85, step: 1, unit: "%", def: 0 },
{ k: "--ambient", label: "Ambient intensity", min: 0, max: 2, step: 0.05, unit: "", def: 1 },
];
const MOSAIC_SLIDERS: Slider[] = [
{ k: "--mosaic-blur", label: "Mosaic blur", min: 20, max: 100, step: 1, unit: "px", def: 64 },
{ k: "--mosaic-sat", label: "Mosaic saturation", min: 0, max: 1.5, step: 0.05, unit: "", def: 0.7 },
{ k: "--mosaic-bright", label: "Mosaic brightness", min: 0.2, max: 1, step: 0.05, unit: "", def: 0.5 },
{ k: "--mosaic-opacity", label: "Mosaic opacity", min: 0, max: 1, step: 0.05, unit: "", def: 0.5 },
{ k: "--glass-scrim", label: "Under-text scrim", min: 0, max: 85, step: 1, unit: "%", def: 14 },
{ k: "--ambient", label: "Ambient intensity", min: 0, max: 2, step: 0.05, unit: "", def: 1.1 },
];
const PALETTE: { k: string; label: string }[] = [
@@ -58,39 +51,39 @@ const PALETTE: { k: string; label: string }[] = [
// Each preset resets every slider to its default, then applies these overrides.
const PRESETS: { id: string; name: string; hint: string; over: Record<string, number> }[] = [
{ id: "current", name: "Current", hint: "The shipped defaults", over: {} },
{ id: "current", name: "Current", hint: "The baked baseline", over: {} },
{
id: "adaptive",
name: "Adaptive (rec.)",
hint: "Solid-enough dark glass",
id: "media",
name: "Media (glassy)",
hint: "For art-backed pages",
over: {
"--glass-surface-alpha": 94,
"--glass-card-alpha": 94,
"--glass-menu-alpha": 98,
"--glass-scrim": 14,
"--glass-edge": 10,
"--ambient": 1.1,
"--glass-dark-bright": 1.25,
"--glass-blur": 20,
"--glass-blur": 10,
"--glass-saturate": 1.6,
"--glass-dark-bright": 1.2,
"--glass-surface-alpha": 50,
"--glass-card-alpha": 60,
"--glass-menu-alpha": 66,
"--glass-border-alpha": 80,
"--glass-edge": 25,
"--glass-inset": 15,
"--glass-scrim": 30,
},
},
{ id: "gradient", name: "Gradient+", hint: "Stronger accent pools", over: { "--ambient": 1.6, "--glass-scrim": 6 } },
{
id: "wcag",
name: "Edge + scrim",
hint: "Max readability",
over: { "--glass-edge": 42, "--glass-scrim": 45, "--glass-surface-alpha": 84, "--glass-card-alpha": 90 },
id: "readable",
name: "Max readable",
hint: "Solid + strong scrim",
over: { "--glass-edge": 42, "--glass-scrim": 45, "--glass-surface-alpha": 96, "--glass-card-alpha": 96 },
},
];
const ALL = [...SLIDERS, ...MOSAIC_SLIDERS];
const ALL = SLIDERS;
const root = () => document.documentElement;
const readVar = (k: string) => getComputedStyle(root()).getPropertyValue(k).trim();
type Saved = {
vars?: Record<string, number>;
palette?: Record<string, string>;
mosaic?: boolean;
open?: boolean;
};
@@ -110,7 +103,6 @@ export default function GlassTuner() {
return { ...base, ...(initial.vars || {}) };
});
const [palette, setPalette] = useState<Record<string, string>>(() => initial.palette || {});
const [mosaic, setMosaic] = useState<boolean>(() => Boolean(initial.mosaic));
const [open, setOpen] = useState<boolean>(() => initial.open ?? true);
const [tab, setTab] = useState<"glass" | "palette">("glass");
const [copied, setCopied] = useState(false);
@@ -168,18 +160,12 @@ export default function GlassTuner() {
persist({ vars: next });
}
function toggleMosaic(v: boolean) {
setMosaic(v);
persist({ mosaic: v });
}
function reset() {
[...ALL, ...PALETTE].forEach((s) => root().style.removeProperty(s.k));
const base: Record<string, number> = {};
ALL.forEach((s) => (base[s.k] = s.def));
setVars(base);
setPalette({});
toggleMosaic(false);
localStorage.removeItem(LS_KEY);
const sw: Record<string, string> = {};
PALETTE.forEach((p) => {
@@ -216,9 +202,7 @@ export default function GlassTuner() {
if (!DEV_TUNER) return null;
return (
<>
{mosaic && <MosaicBackdrop />}
<div className="fixed right-0 top-24 z-[9999] flex items-start" style={{ pointerEvents: "none" }}>
<div className="fixed right-0 top-24 z-[9999] flex items-start" style={{ pointerEvents: "none" }}>
{!open && (
<button
onClick={() => persistOpen(true)}
@@ -265,12 +249,6 @@ export default function GlassTuner() {
</div>
</div>
{/* Mosaic toggle */}
<label className="flex items-center gap-2 text-xs text-fg cursor-pointer">
<input type="checkbox" checked={mosaic} onChange={(e) => toggleMosaic(e.target.checked)} className="accent-accent" />
Thumbnail-mosaic backdrop <span className="text-muted">(muted)</span>
</label>
{/* Tab switch */}
<div className="flex gap-1.5">
{(["glass", "palette"] as const).map((t) => (
@@ -291,13 +269,6 @@ export default function GlassTuner() {
{SLIDERS.map((s) => (
<Ctl key={s.k} s={s} value={vars[s.k]} onChange={(v) => setVar(s.k, v)} />
))}
{mosaic && (
<div className="flex flex-col gap-2.5 pt-1 border-t border-border/50">
{MOSAIC_SLIDERS.map((s) => (
<Ctl key={s.k} s={s} value={vars[s.k]} onChange={(v) => setVar(s.k, v)} />
))}
</div>
)}
</div>
)}
@@ -342,8 +313,7 @@ export default function GlassTuner() {
</div>
</div>
)}
</div>
</>
</div>
);
}