diff --git a/frontend/src/components/GlassTuner.tsx b/frontend/src/components/GlassTuner.tsx index a51a99e..5b899c5 100644 --- a/frontend/src/components/GlassTuner.tsx +++ b/frontend/src/components/GlassTuner.tsx @@ -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 }[] = [ - { 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; palette?: Record; - mosaic?: boolean; open?: boolean; }; @@ -110,7 +103,6 @@ export default function GlassTuner() { return { ...base, ...(initial.vars || {}) }; }); const [palette, setPalette] = useState>(() => initial.palette || {}); - const [mosaic, setMosaic] = useState(() => Boolean(initial.mosaic)); const [open, setOpen] = useState(() => 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 = {}; ALL.forEach((s) => (base[s.k] = s.def)); setVars(base); setPalette({}); - toggleMosaic(false); localStorage.removeItem(LS_KEY); const sw: Record = {}; PALETTE.forEach((p) => { @@ -216,9 +202,7 @@ export default function GlassTuner() { if (!DEV_TUNER) return null; return ( - <> - {mosaic && } -
+
{!open && (
- {/* Mosaic toggle */} - - {/* Tab switch */}
{(["glass", "palette"] as const).map((t) => ( @@ -291,13 +269,6 @@ export default function GlassTuner() { {SLIDERS.map((s) => ( setVar(s.k, v)} /> ))} - {mosaic && ( -
- {MOSAIC_SLIDERS.map((s) => ( - setVar(s.k, v)} /> - ))} -
- )}
)} @@ -342,8 +313,7 @@ export default function GlassTuner() { )} - - + ); } diff --git a/frontend/src/components/MosaicBackdrop.tsx b/frontend/src/components/MosaicBackdrop.tsx deleted file mode 100644 index 05b8c1e..0000000 --- a/frontend/src/components/MosaicBackdrop.tsx +++ /dev/null @@ -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([]); - - // 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(); - document.querySelectorAll("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 ( - - ); -} diff --git a/frontend/src/components/PlexBrowse.tsx b/frontend/src/components/PlexBrowse.tsx index 4409b5a..3f112a1 100644 --- a/frontend/src/components/PlexBrowse.tsx +++ b/frontend/src/components/PlexBrowse.tsx @@ -905,7 +905,7 @@ function PlexShowView({ } return ( -
+
{q.isLoading || !d || !show ? ( @@ -1103,7 +1103,7 @@ function PlexSeasonView({ } return ( -
+
{q.isLoading || !d || !se ? ( diff --git a/frontend/src/components/PlexInfo.tsx b/frontend/src/components/PlexInfo.tsx index 1c8a28a..f05bcdf 100644 --- a/frontend/src/components/PlexInfo.tsx +++ b/frontend/src/components/PlexInfo.tsx @@ -90,12 +90,11 @@ export default function PlexInfo({ t(`plex.info.stripSource.${src}`, { defaultValue: src.toUpperCase() }); return ( -
+
{/* Hero block: floats as a frosted glass panel over the fixed art backdrop (page variant). */}
{/* Header: close (overlay) / customize */}
@@ -302,7 +301,6 @@ export default function PlexInfo({ {cast.length > 0 && (

{t("plex.info.cast")} @@ -365,7 +363,6 @@ export default function PlexInfo({

{col.title}

diff --git a/frontend/src/index.css b/frontend/src/index.css index e735122..d0618c4 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -6,27 +6,38 @@ --font-scale: 1.06; /* ===== 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 - the historical hard-coded values, so nothing changes until a value is bumped. When the user - signs off on tuned values, bake the finals right here. ===== */ - --glass-blur: 30px; /* backdrop blur radius (2026 sweet spot is ~8–16px; 30 is heavy) */ + tuned in ONE place (and live-driven by the dev GlassTuner during UAT). ===== */ + /* GLOBAL baseline = "Adaptive": solid-enough dark glass that reads well on content-less chrome + (nav/header/dialogs/filters/settings — where there's nothing behind the glass to refract). + 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-dark-bright: 1.4; /* extra brightness lift applied to .glass/.glass-menu in dark mode only */ - --glass-surface-alpha: 78%; /* .glass panel translucency (higher = more solid = "adaptive") */ - --glass-card-alpha: 84%; /* .glass-card translucency */ - --glass-menu-alpha: 92%; /* .glass-menu translucency */ + --glass-dark-bright: 1.25; /* extra brightness lift applied to .glass/.glass-menu in dark mode only */ + --glass-surface-alpha: 94%; /* .glass panel translucency (higher = more solid = "adaptive") */ + --glass-card-alpha: 94%; /* .glass-card translucency */ + --glass-menu-alpha: 98%; /* .glass-menu translucency */ --glass-border-alpha: 78%; /* .glass border opacity */ --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-scrim: 0%; /* under-content scrim: darkens surfaces toward --bg for text contrast — 0 = off */ - --ambient: 1; /* multiplier on the ambient accent backdrop pools */ + --glass-edge: 10%; /* extra edge-light mixed into the TOP border (#fff N%) — 0 = off */ + --glass-scrim: 14%; /* under-content scrim: darkens surfaces toward --bg for text contrast — 0 = off */ + --ambient: 1.1; /* multiplier on the ambient accent backdrop pools */ +} - /* Optional muted thumbnail-mosaic backdrop (off unless mounts). Conservative - defaults: heavy blur + low saturation + darkened so it reads as subtle texture, never a rainbow. */ - --mosaic-blur: 64px; - --mosaic-sat: 0.7; - --mosaic-bright: 0.5; - --mosaic-opacity: 0.5; +/* ===== Rich-backdrop scope: surfaces that float over real imagery (Plex art hero/detail, and + later channel-page banners) want MORE translucency so the picture shows through — the "glass + over content" look. Put `.glass-media` on such a page root; every .glass* inside inherits these + (UAT-tuned 2026-07-12). Global chrome stays solid via the :root defaults above. ===== */ +.glass-media { + --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. */ @@ -133,37 +144,6 @@ html[data-theme="dark"] .toast-card { border-color: color-mix(in srgb, #fff 50%, transparent); } -/* ===== Optional muted thumbnail-mosaic backdrop (mounted by ) ===== - 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 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. */ html[data-perf="1"] .glass, html[data-perf="1"] .glass-menu, @@ -316,6 +296,50 @@ html[data-scheme="youtube"][data-theme="light"] { --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) */ @keyframes toastbar { from { diff --git a/frontend/src/lib/theme.ts b/frontend/src/lib/theme.ts index db8caac..f9aa5ed 100644 --- a/frontend/src/lib/theme.ts +++ b/frontend/src/lib/theme.ts @@ -1,13 +1,15 @@ import { LS, readAccountMerged, writeAccount } from "./storage"; 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 }[] = [ { id: "midnight", name: "Midnight", swatch: "#6d8cff" }, { id: "forest", name: "Forest", swatch: "#2dd4bf" }, { id: "slate", name: "Slate", swatch: "#e8913c" }, { id: "youtube", name: "YouTube", swatch: "#ff3b46" }, + { id: "starship", name: "Starship", swatch: "#38b6ff" }, + { id: "matrix", name: "Matrix", swatch: "#2fe36e" }, ]; export interface ThemePrefs {