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:
parent
22cce807a0
commit
dc51aeeae9
6 changed files with 108 additions and 173 deletions
|
|
@ -1,5 +1,4 @@
|
||||||
import { useEffect, useMemo, useState } from "react";
|
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.
|
* 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: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// def = the BAKED baseline in index.css (:root). "Current" preset + the export diff both key off it.
|
||||||
const SLIDERS: Slider[] = [
|
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-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-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: 78 },
|
{ 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: 84 },
|
{ 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: 92 },
|
{ 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-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-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: "--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 },
|
{ k: "--ambient", label: "Ambient intensity", min: 0, max: 2, step: 0.05, unit: "", def: 1.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 },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const PALETTE: { k: string; label: string }[] = [
|
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.
|
// Each preset resets every slider to its default, then applies these overrides.
|
||||||
const PRESETS: { id: string; name: string; hint: string; over: Record<string, number> }[] = [
|
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",
|
id: "media",
|
||||||
name: "Adaptive (rec.)",
|
name: "Media (glassy)",
|
||||||
hint: "Solid-enough dark glass",
|
hint: "For art-backed pages",
|
||||||
over: {
|
over: {
|
||||||
"--glass-surface-alpha": 94,
|
"--glass-blur": 10,
|
||||||
"--glass-card-alpha": 94,
|
"--glass-saturate": 1.6,
|
||||||
"--glass-menu-alpha": 98,
|
"--glass-dark-bright": 1.2,
|
||||||
"--glass-scrim": 14,
|
"--glass-surface-alpha": 50,
|
||||||
"--glass-edge": 10,
|
"--glass-card-alpha": 60,
|
||||||
"--ambient": 1.1,
|
"--glass-menu-alpha": 66,
|
||||||
"--glass-dark-bright": 1.25,
|
"--glass-border-alpha": 80,
|
||||||
"--glass-blur": 20,
|
"--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",
|
id: "readable",
|
||||||
name: "Edge + scrim",
|
name: "Max readable",
|
||||||
hint: "Max readability",
|
hint: "Solid + strong scrim",
|
||||||
over: { "--glass-edge": 42, "--glass-scrim": 45, "--glass-surface-alpha": 84, "--glass-card-alpha": 90 },
|
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 root = () => document.documentElement;
|
||||||
const readVar = (k: string) => getComputedStyle(root()).getPropertyValue(k).trim();
|
const readVar = (k: string) => getComputedStyle(root()).getPropertyValue(k).trim();
|
||||||
|
|
||||||
type Saved = {
|
type Saved = {
|
||||||
vars?: Record<string, number>;
|
vars?: Record<string, number>;
|
||||||
palette?: Record<string, string>;
|
palette?: Record<string, string>;
|
||||||
mosaic?: boolean;
|
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -110,7 +103,6 @@ export default function GlassTuner() {
|
||||||
return { ...base, ...(initial.vars || {}) };
|
return { ...base, ...(initial.vars || {}) };
|
||||||
});
|
});
|
||||||
const [palette, setPalette] = useState<Record<string, string>>(() => initial.palette || {});
|
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 [open, setOpen] = useState<boolean>(() => initial.open ?? true);
|
||||||
const [tab, setTab] = useState<"glass" | "palette">("glass");
|
const [tab, setTab] = useState<"glass" | "palette">("glass");
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
@ -168,18 +160,12 @@ export default function GlassTuner() {
|
||||||
persist({ vars: next });
|
persist({ vars: next });
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleMosaic(v: boolean) {
|
|
||||||
setMosaic(v);
|
|
||||||
persist({ mosaic: v });
|
|
||||||
}
|
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
[...ALL, ...PALETTE].forEach((s) => root().style.removeProperty(s.k));
|
[...ALL, ...PALETTE].forEach((s) => root().style.removeProperty(s.k));
|
||||||
const base: Record<string, number> = {};
|
const base: Record<string, number> = {};
|
||||||
ALL.forEach((s) => (base[s.k] = s.def));
|
ALL.forEach((s) => (base[s.k] = s.def));
|
||||||
setVars(base);
|
setVars(base);
|
||||||
setPalette({});
|
setPalette({});
|
||||||
toggleMosaic(false);
|
|
||||||
localStorage.removeItem(LS_KEY);
|
localStorage.removeItem(LS_KEY);
|
||||||
const sw: Record<string, string> = {};
|
const sw: Record<string, string> = {};
|
||||||
PALETTE.forEach((p) => {
|
PALETTE.forEach((p) => {
|
||||||
|
|
@ -216,8 +202,6 @@ export default function GlassTuner() {
|
||||||
if (!DEV_TUNER) return null;
|
if (!DEV_TUNER) return null;
|
||||||
|
|
||||||
return (
|
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 && (
|
{!open && (
|
||||||
<button
|
<button
|
||||||
|
|
@ -265,12 +249,6 @@ export default function GlassTuner() {
|
||||||
</div>
|
</div>
|
||||||
</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 */}
|
{/* Tab switch */}
|
||||||
<div className="flex gap-1.5">
|
<div className="flex gap-1.5">
|
||||||
{(["glass", "palette"] as const).map((t) => (
|
{(["glass", "palette"] as const).map((t) => (
|
||||||
|
|
@ -291,13 +269,6 @@ export default function GlassTuner() {
|
||||||
{SLIDERS.map((s) => (
|
{SLIDERS.map((s) => (
|
||||||
<Ctl key={s.k} s={s} value={vars[s.k]} onChange={(v) => setVar(s.k, v)} />
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -343,7 +314,6 @@ export default function GlassTuner() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
import { useEffect, useMemo, useState } from "react";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DEV-ONLY, OPT-IN backdrop: a heavily blurred + darkened + desaturated mosaic of the real
|
|
||||||
* thumbnails currently on screen, so dark "glass" surfaces have actual content to refract.
|
|
||||||
* Deliberately muted (see the --mosaic-* vars in index.css) — a soft texture, never a collage.
|
|
||||||
*
|
|
||||||
* Thumbnails are sampled straight from the rendered DOM (the feed/channel image tags), so it
|
|
||||||
* needs no data wiring and works on whatever page mounts it. Looks richest on the feed; on a
|
|
||||||
* thumbnail-less page it simply falls back to the plain background.
|
|
||||||
*
|
|
||||||
* Toggled by the GlassTuner. Not shipped to prod (the tuner that mounts it is localhost-gated).
|
|
||||||
*/
|
|
||||||
export default function MosaicBackdrop({ cols = 8, rows = 6 }: { cols?: number; rows?: number }) {
|
|
||||||
const [srcs, setSrcs] = useState<string[]>([]);
|
|
||||||
|
|
||||||
// Snapshot the on-screen thumbnails once per mount (toggling the tuner off/on re-samples on a
|
|
||||||
// content-rich page). Looks richest on the feed; elsewhere it just falls back to the plain bg.
|
|
||||||
useEffect(() => {
|
|
||||||
const found = new Set<string>();
|
|
||||||
document.querySelectorAll<HTMLImageElement>("img").forEach((img) => {
|
|
||||||
const s = img.currentSrc || img.src;
|
|
||||||
if (!s) return;
|
|
||||||
// YouTube thumbnails / channel avatars — skip our own tiny UI icons/data URIs.
|
|
||||||
if (/ytimg\.com|ggpht\.com|googleusercontent\.com/.test(s)) found.add(s);
|
|
||||||
});
|
|
||||||
setSrcs([...found]);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
document.documentElement.dataset.mosaic = "1";
|
|
||||||
return () => {
|
|
||||||
delete document.documentElement.dataset.mosaic;
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const tiles = useMemo(() => {
|
|
||||||
const n = cols * rows;
|
|
||||||
if (srcs.length === 0) return [];
|
|
||||||
return Array.from({ length: n }, (_, i) => srcs[i % srcs.length]);
|
|
||||||
}, [srcs, cols, rows]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mosaic-backdrop" aria-hidden="true">
|
|
||||||
<div
|
|
||||||
className="mosaic-tiles"
|
|
||||||
style={{
|
|
||||||
gridTemplateColumns: `repeat(${cols}, 1fr)`,
|
|
||||||
gridTemplateRows: `repeat(${rows}, 1fr)`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{tiles.map((src, i) => (
|
|
||||||
<span key={i} style={{ backgroundImage: `url("${src}")` }} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -905,7 +905,7 @@ function PlexShowView({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 max-w-[1200px] mx-auto">
|
<div className="p-4 max-w-[1200px] mx-auto glass-media">
|
||||||
<BackBtn onBack={onBack} label={t("plex.backToLibrary")} />
|
<BackBtn onBack={onBack} label={t("plex.backToLibrary")} />
|
||||||
|
|
||||||
{q.isLoading || !d || !show ? (
|
{q.isLoading || !d || !show ? (
|
||||||
|
|
@ -1103,7 +1103,7 @@ function PlexSeasonView({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 max-w-[1200px] mx-auto">
|
<div className="p-4 max-w-[1200px] mx-auto glass-media">
|
||||||
<BackBtn onBack={onBack} label={t("plex.series.backToShow")} />
|
<BackBtn onBack={onBack} label={t("plex.series.backToShow")} />
|
||||||
|
|
||||||
{q.isLoading || !d || !se ? (
|
{q.isLoading || !d || !se ? (
|
||||||
|
|
|
||||||
|
|
@ -90,12 +90,11 @@ export default function PlexInfo({
|
||||||
t(`plex.info.stripSource.${src}`, { defaultValue: src.toUpperCase() });
|
t(`plex.info.stripSource.${src}`, { defaultValue: src.toUpperCase() });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={overlay ? "relative w-full max-w-3xl" : "relative"}>
|
<div className={overlay ? "relative w-full max-w-3xl" : "relative glass-media"}>
|
||||||
<div className={overlay ? "p-5" : "flex flex-col gap-4 p-3 sm:p-4"}>
|
<div className={overlay ? "p-5" : "flex flex-col gap-4 p-3 sm:p-4"}>
|
||||||
{/* Hero block: floats as a frosted glass panel over the fixed art backdrop (page variant). */}
|
{/* Hero block: floats as a frosted glass panel over the fixed art backdrop (page variant). */}
|
||||||
<div
|
<div
|
||||||
className={overlay ? "" : "glass rounded-2xl p-4 sm:p-6"}
|
className={overlay ? "" : "glass rounded-2xl p-4 sm:p-6"}
|
||||||
style={overlay ? undefined : { background: "color-mix(in srgb, var(--surface) 55%, transparent)" }}
|
|
||||||
>
|
>
|
||||||
{/* Header: close (overlay) / customize */}
|
{/* Header: close (overlay) / customize */}
|
||||||
<div className="mb-4 flex items-start gap-3">
|
<div className="mb-4 flex items-start gap-3">
|
||||||
|
|
@ -302,7 +301,6 @@ export default function PlexInfo({
|
||||||
{cast.length > 0 && (
|
{cast.length > 0 && (
|
||||||
<div
|
<div
|
||||||
className={overlay ? "mt-5" : "glass-card rounded-2xl p-4 sm:p-5"}
|
className={overlay ? "mt-5" : "glass-card rounded-2xl p-4 sm:p-5"}
|
||||||
style={overlay ? undefined : { background: "color-mix(in srgb, var(--card) 55%, transparent)" }}
|
|
||||||
>
|
>
|
||||||
<h2 className={`mb-2 text-sm font-semibold ${overlay ? "text-white/80" : ""}`}>
|
<h2 className={`mb-2 text-sm font-semibold ${overlay ? "text-white/80" : ""}`}>
|
||||||
{t("plex.info.cast")}
|
{t("plex.info.cast")}
|
||||||
|
|
@ -365,7 +363,6 @@ export default function PlexInfo({
|
||||||
<div
|
<div
|
||||||
key={col.id}
|
key={col.id}
|
||||||
className={overlay ? "mt-6" : "glass-card rounded-2xl p-4 sm:p-5"}
|
className={overlay ? "mt-6" : "glass-card rounded-2xl p-4 sm:p-5"}
|
||||||
style={overlay ? undefined : { background: "color-mix(in srgb, var(--card) 55%, transparent)" }}
|
|
||||||
>
|
>
|
||||||
<div className="mb-2 flex items-center justify-between gap-3">
|
<div className="mb-2 flex items-center justify-between gap-3">
|
||||||
<h2 className={`text-sm font-semibold ${overlay ? "text-white/80" : ""}`}>{col.title}</h2>
|
<h2 className={`text-sm font-semibold ${overlay ? "text-white/80" : ""}`}>{col.title}</h2>
|
||||||
|
|
|
||||||
|
|
@ -6,27 +6,38 @@
|
||||||
--font-scale: 1.06;
|
--font-scale: 1.06;
|
||||||
|
|
||||||
/* ===== Glass tunables — every value that materially drives the look lives here so it can be
|
/* ===== Glass tunables — every value that materially drives the look lives here so it can be
|
||||||
tuned in ONE place (and live-driven by the dev GlassTuner during UAT). Defaults below equal
|
tuned in ONE place (and live-driven by the dev GlassTuner during UAT). ===== */
|
||||||
the historical hard-coded values, so nothing changes until a value is bumped. When the user
|
/* GLOBAL baseline = "Adaptive": solid-enough dark glass that reads well on content-less chrome
|
||||||
signs off on tuned values, bake the finals right here. ===== */
|
(nav/header/dialogs/filters/settings — where there's nothing behind the glass to refract).
|
||||||
--glass-blur: 30px; /* backdrop blur radius (2026 sweet spot is ~8–16px; 30 is heavy) */
|
Baked from UAT 2026-07-12. Rich-backdrop surfaces override these via `.glass-media` below. */
|
||||||
|
--glass-blur: 20px; /* backdrop blur radius */
|
||||||
--glass-saturate: 1.8; /* backdrop saturation */
|
--glass-saturate: 1.8; /* backdrop saturation */
|
||||||
--glass-dark-bright: 1.4; /* extra brightness lift applied to .glass/.glass-menu in dark mode only */
|
--glass-dark-bright: 1.25; /* extra brightness lift applied to .glass/.glass-menu in dark mode only */
|
||||||
--glass-surface-alpha: 78%; /* .glass panel translucency (higher = more solid = "adaptive") */
|
--glass-surface-alpha: 94%; /* .glass panel translucency (higher = more solid = "adaptive") */
|
||||||
--glass-card-alpha: 84%; /* .glass-card translucency */
|
--glass-card-alpha: 94%; /* .glass-card translucency */
|
||||||
--glass-menu-alpha: 92%; /* .glass-menu translucency */
|
--glass-menu-alpha: 98%; /* .glass-menu translucency */
|
||||||
--glass-border-alpha: 78%; /* .glass border opacity */
|
--glass-border-alpha: 78%; /* .glass border opacity */
|
||||||
--glass-inset: 16%; /* top specular highlight strength (#fff N%) */
|
--glass-inset: 16%; /* top specular highlight strength (#fff N%) */
|
||||||
--glass-edge: 0%; /* extra edge-light mixed into the TOP border (#fff N%) — 0 = off */
|
--glass-edge: 10%; /* extra edge-light mixed into the TOP border (#fff N%) — 0 = off */
|
||||||
--glass-scrim: 0%; /* under-content scrim: darkens surfaces toward --bg for text contrast — 0 = off */
|
--glass-scrim: 14%; /* under-content scrim: darkens surfaces toward --bg for text contrast — 0 = off */
|
||||||
--ambient: 1; /* multiplier on the ambient accent backdrop pools */
|
--ambient: 1.1; /* multiplier on the ambient accent backdrop pools */
|
||||||
|
}
|
||||||
|
|
||||||
/* Optional muted thumbnail-mosaic backdrop (off unless <MosaicBackdrop/> mounts). Conservative
|
/* ===== Rich-backdrop scope: surfaces that float over real imagery (Plex art hero/detail, and
|
||||||
defaults: heavy blur + low saturation + darkened so it reads as subtle texture, never a rainbow. */
|
later channel-page banners) want MORE translucency so the picture shows through — the "glass
|
||||||
--mosaic-blur: 64px;
|
over content" look. Put `.glass-media` on such a page root; every .glass* inside inherits these
|
||||||
--mosaic-sat: 0.7;
|
(UAT-tuned 2026-07-12). Global chrome stays solid via the :root defaults above. ===== */
|
||||||
--mosaic-bright: 0.5;
|
.glass-media {
|
||||||
--mosaic-opacity: 0.5;
|
--glass-blur: 10px;
|
||||||
|
--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%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Make native controls (date picker, scrollbars) follow the theme. */
|
/* Make native controls (date picker, scrollbars) follow the theme. */
|
||||||
|
|
@ -133,37 +144,6 @@ html[data-theme="dark"] .toast-card {
|
||||||
border-color: color-mix(in srgb, #fff 50%, transparent);
|
border-color: color-mix(in srgb, #fff 50%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== Optional muted thumbnail-mosaic backdrop (mounted by <MosaicBackdrop/>) =====
|
|
||||||
Gives dark glass real content to refract. Kept conservative: heavy blur + low saturation +
|
|
||||||
darkened, so it reads as a soft texture behind everything, never a colourful collage. When it
|
|
||||||
is present, the ambient accent pools on <body> are dimmed so the two don't fight. */
|
|
||||||
.mosaic-backdrop {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
z-index: -1;
|
|
||||||
overflow: hidden;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
.mosaic-backdrop .mosaic-tiles {
|
|
||||||
position: absolute;
|
|
||||||
inset: -10%;
|
|
||||||
display: grid;
|
|
||||||
gap: 0;
|
|
||||||
filter: blur(var(--mosaic-blur)) saturate(var(--mosaic-sat)) brightness(var(--mosaic-bright));
|
|
||||||
opacity: var(--mosaic-opacity);
|
|
||||||
}
|
|
||||||
.mosaic-backdrop .mosaic-tiles > span {
|
|
||||||
display: block;
|
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
}
|
|
||||||
html[data-mosaic="1"] body {
|
|
||||||
background: var(--bg);
|
|
||||||
}
|
|
||||||
html[data-perf="1"] .mosaic-backdrop {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Performance mode (Settings → Appearance) drops the expensive blur + soft shadows. */
|
/* Performance mode (Settings → Appearance) drops the expensive blur + soft shadows. */
|
||||||
html[data-perf="1"] .glass,
|
html[data-perf="1"] .glass,
|
||||||
html[data-perf="1"] .glass-menu,
|
html[data-perf="1"] .glass-menu,
|
||||||
|
|
@ -316,6 +296,50 @@ html[data-scheme="youtube"][data-theme="light"] {
|
||||||
--accent-fg: #ffffff;
|
--accent-fg: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Starship — deep space blue-black with a bright azure "LCARS/computer" accent */
|
||||||
|
html[data-scheme="starship"][data-theme="dark"] {
|
||||||
|
--bg: #050b16;
|
||||||
|
--surface: #0b1728;
|
||||||
|
--card: #101f36;
|
||||||
|
--border: #1f3557;
|
||||||
|
--fg: #dfeaff;
|
||||||
|
--muted: #8aa6cc;
|
||||||
|
--accent: #38b6ff;
|
||||||
|
--accent-fg: #04101f;
|
||||||
|
}
|
||||||
|
html[data-scheme="starship"][data-theme="light"] {
|
||||||
|
--bg: #eef4fb;
|
||||||
|
--surface: #ffffff;
|
||||||
|
--card: #ffffff;
|
||||||
|
--border: #d3e0f0;
|
||||||
|
--fg: #0b1b30;
|
||||||
|
--muted: #4a5f7d;
|
||||||
|
--accent: #0b6bd6;
|
||||||
|
--accent-fg: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Matrix — near-black with a phosphor-green terminal accent */
|
||||||
|
html[data-scheme="matrix"][data-theme="dark"] {
|
||||||
|
--bg: #030a06;
|
||||||
|
--surface: #08130d;
|
||||||
|
--card: #0b1c12;
|
||||||
|
--border: #163d27;
|
||||||
|
--fg: #d6ffe4;
|
||||||
|
--muted: #6fae86;
|
||||||
|
--accent: #2fe36e;
|
||||||
|
--accent-fg: #02160a;
|
||||||
|
}
|
||||||
|
html[data-scheme="matrix"][data-theme="light"] {
|
||||||
|
--bg: #f0f7f1;
|
||||||
|
--surface: #ffffff;
|
||||||
|
--card: #ffffff;
|
||||||
|
--border: #cfe6d6;
|
||||||
|
--fg: #08130b;
|
||||||
|
--muted: #4c6b56;
|
||||||
|
--accent: #12a150;
|
||||||
|
--accent-fg: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
/* Toast auto-dismiss countdown bar (shrinks left-to-right to zero) */
|
/* Toast auto-dismiss countdown bar (shrinks left-to-right to zero) */
|
||||||
@keyframes toastbar {
|
@keyframes toastbar {
|
||||||
from {
|
from {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
import { LS, readAccountMerged, writeAccount } from "./storage";
|
import { LS, readAccountMerged, writeAccount } from "./storage";
|
||||||
|
|
||||||
type Mode = "dark" | "light";
|
type Mode = "dark" | "light";
|
||||||
export type Scheme = "midnight" | "forest" | "slate" | "youtube";
|
export type Scheme = "midnight" | "forest" | "slate" | "youtube" | "starship" | "matrix";
|
||||||
|
|
||||||
export const SCHEMES: { id: Scheme; name: string; swatch: string }[] = [
|
export const SCHEMES: { id: Scheme; name: string; swatch: string }[] = [
|
||||||
{ id: "midnight", name: "Midnight", swatch: "#6d8cff" },
|
{ id: "midnight", name: "Midnight", swatch: "#6d8cff" },
|
||||||
{ id: "forest", name: "Forest", swatch: "#2dd4bf" },
|
{ id: "forest", name: "Forest", swatch: "#2dd4bf" },
|
||||||
{ id: "slate", name: "Slate", swatch: "#e8913c" },
|
{ id: "slate", name: "Slate", swatch: "#e8913c" },
|
||||||
{ id: "youtube", name: "YouTube", swatch: "#ff3b46" },
|
{ id: "youtube", name: "YouTube", swatch: "#ff3b46" },
|
||||||
|
{ id: "starship", name: "Starship", swatch: "#38b6ff" },
|
||||||
|
{ id: "matrix", name: "Matrix", swatch: "#2fe36e" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export interface ThemePrefs {
|
export interface ThemePrefs {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue