The app HAS a platform layer -- AnchoredPopover (22 importers, with
arrows/typeahead/Escape/anchor-flip), QueryState (21), Modal (17),
PageShell, DataTable, Tabs, Tooltip and ~26 more -- but it sat flat among
~160 feature components with nothing marking it as one. Several of those
files describe themselves as 'the one place' or 'canonical' and were
still bypassed by hand-rolled markup, because a layer nobody can see is a
layer nobody uses; three separate inventory passes of this codebase
misread it the same way.
Moves the 33 domain-free modules into components/ui/, rewrites the
imports in 89 files, and makes the boundary a lint rule rather than a
convention: inside ui/ a one-level-up import (a feature component) and
any lib/api slice are errors. The rule was verified to fire on both and
to leave generic lib/ helpers alone. README.md states which side of the
line a component belongs on and why.
No behaviour change: 3x tsc, eslint, knip, prettier and 687 vitest tests
all green; no backend file touched.
The ~124 px-frozen text-[Npx] labels didn't track the Settings font-scale slider
while their rem-sized neighbours did. Add text-2xs (0.6875rem) / text-3xs
(0.625rem) font-size-ONLY tokens (no paired line-height -> matches the arbitrary
they replace, zero vertical-rhythm shift) and codemod every text-[Npx] to a rem
class (rare 8/9/12/13px -> rem arbitraries).
Also: --hdr-h 68px -> 4.25rem so header content clears the rem-sized pills at
scale 1.3 instead of sliding under; NavSidebar/SidePanel slot widths + their
icon/badge px sizes go rem so the rails keep pace with their (now-scaling)
labels; a w-24/h-[54px] thumb -> h-13.5 to preserve 16:9 at scale. Media
thumbnails, page-width caps and fixed container widths deliberately stay px.
Verified via getComputedStyle at scale 1.06/1.3: text-2xs 11.66->14.3px, header
clearance 72->88px, nav slot 246->302px — all scale proportionally.
- #1: remove the 9 dead `shadow-*` from glass surfaces instead of letting them override the tuned glass
box-shadow. The @layer components move made a JSX shadow-* win AND atomically drop the glass inset
top-edge highlight; deleting the (always-dead) utilities keeps the intended glass depth incl. the
inset sheen. Verified in-browser: a .glass box-shadow again includes its inset segment.
- #3: drop the per-element focus-visible outlines from the rail chrome buttons and SavedViews action
buttons — the global button:focus-visible fallback already draws the identical 2px accent ring, so
they were redundant. Verified: the pin button rings from the global rule alone.
- #5: unify the focus-within outline offset (offset-2) on the SearchBar and ShareDialog pills.
- #2 (double outline on the pill when an inner button is focused): NOT fixed — the clean fix
(has-[input:focus] so only the input rings the pill) does not generate under Tailwind v4 (measured:
no rule emitted). Kept the working focus-within, which is a11y-correct (every element still shows a
focus indicator); the overlapping ring is an accepted cosmetic edge-case.
- #4 (weak ring on solid-fill selected items): deferred to R10 with the selected-paint tokenisation.
Gate green (tsc/eslint/prettier/knip/vitest 79).
Root-fixes the opacity-modifier seed bug behind R10 S0: the theme colors were
bare `var(--x)` hex with no <alpha-value>, so `bg-accent/40`, `ring-accent/40`,
`border-border/60`, `text-muted/70`, … (~99 sites across 45 files) emitted no
CSS rule at all — latent no-ops. v4 implements the opacity modifier via
`color-mix`, so the modifier now works with the existing hex vars, no per-scheme
token duplication needed.
- `npx @tailwindcss/upgrade`: `@tailwind` directives → `@import "tailwindcss"`,
JS config → CSS `@theme` (colors, --font-sans, the z-index tokens preserved),
classname renames (rounded/shadow/blur scale, outline-none→outline-hidden,
ring→ring-2) across 56 files; PostCSS plugin → @tailwindcss/postcss,
autoprefixer removed (v4 prefixes for you).
- `@theme` → `@theme inline` (load-bearing): the color vars are redefined at
runtime under html[data-scheme]/[data-theme]; inline resolves them at the use
site so per-scheme opacity tints track the active scheme (the shadcn v4 pattern).
- Revert one codemod false-positive: it rewrote the English word "rounded" →
"rounded-sm" inside a releaseNotes.ts prose sentence.
Gate green: tsc(app+node) / eslint 0-err / prettier / vitest 79 / knip.
Verified in-browser: bg-accent/15 now computes a scheme-tracking color at 0.15
alpha across all 6 schemes × 2 themes; the hand-rolled glass border/shadow/
radius/backdrop-filter render unchanged under v4.
Enable noUncheckedIndexedAccess and fix all 98 call sites it surfaced across 18
files. Every fix is behaviour-preserving — the flag flagged reads TypeScript
couldn't prove in-bounds, all of which were already guarded by a length/index check,
a findIndex result, or a non-empty invariant:
- read-once so a ternary's narrowing sticks (Feed overrides, linkify mention);
- non-null assertion AFTER an existing guard (PlayerModal/VideoEditor queue+cut-list
indexing, modules step, useUndoable stacks) with a comment stating the invariant;
- nullish fallback where undefined is a real possibility (GlassTuner slider ?? def,
ConfigPanel active group ?? [], descriptionLinks id ?? null);
- optional chaining where the entry genuinely can be absent (PlexBrowse IO entry);
- a non-empty tuple type for the RELATIVE_UNITS constant (encodes "always has [0]").
The single highest-leverage fix: PlayerModal's `active` became `queue?.[index] ??
video` (always Video), clearing 34 of the 45 errors in that file at once. Verified:
tsc/eslint/prettier clean, 56 vitest + 34 pytest + 17/17 e2e green (the e2e suite
exercises the feed/player/channel components touched here).
Prettier 3 (pinned), printWidth 100, double quotes — chosen to match the existing
hand-formatting and minimise reflow. This commit is ONLY the mechanical format pass
(`prettier --write`) plus the config/ignore/scripts, isolated so it never pollutes a
feature diff. Verified behaviour-neutral: typecheck, eslint (0 errors), and vitest
all green before and after. See .git-blame-ignore-revs — `git blame` skips this sha.
navigator.clipboard.writeText is intermittent on iOS Safari (bug 9: the
download share-link copy 'often does neither'). Add lib/clipboard.ts
copyText(): a gesture-safe execCommand path FIRST (before any await can
spend the transient user activation), async Clipboard API as fallback.
Migrate all five copy call sites to it.
Lift the four editable Settings prefs (theme/perf/hints/notifications), their
live-apply effects, the debounced auto-save + baseline echo-guard, and the
server-adopt-on-login into PrefsProvider, exposing the PrefsController via
usePrefs(). SettingsPanel and GlassTuner read it from the hook instead of props;
App's big adopt effect keeps only the shell prefs (panel layouts, rail collapse,
language). App.tsx 655->489 lines. E2E 17/17 incl. settings auto-save. (Platform S6b.3)
Add components/Overlay.tsx (portals to <body>) and route the in-tree full-screen/
floating holdouts through it — ChatDock, OnboardingWizard, GlassTuner — so they
escape any ancestor stacking context (mask/transform/backdrop-filter) instead of
relying on a bare high z-index. Toaster stays in-tree (it's content-column-scoped
by design, outside the masked scroller); Welcome is a page, not an overlay.
Graduate the floating glass tuner from a localhost-only dev tool to a
user-facing opt-in: it's now gated on a per-user `theme.showTuner` pref (off by
default) with a "Show the glass tuner" switch in Settings → Appearance, riding
the existing theme draft-save. Softened the dev-only framing. Behaviour and look
are unchanged; the full preset system is a separate epic.
The tuner writes inline vars (global), so its sliders always showed the dark/
backdrop-on baseline even when viewing light. Add a 'Sync to current theme'
button that clears the inline overrides, reads the live theme-driven values into
the sliders, and headers the Copy-CSS export for the active scope (:root for
dark, html[data-theme="light"] for light) — so light-theme glass can be dialled
in and exported straight into the right block.
Code review (public-repo rule): the GlassTuner + index.css/App.tsx comments named
the prod hostname and dev-process jargon (UAT/HMR/dev ports/epic), which ship in
the public bundle. Genericised the comments — no behaviour change.
Replaces the dev-only backdrop-fade knob with a real control: when Background
image is on (dark theme), a 0-100% 'Image fade' slider appears beneath it and
drives --bg-fade live (higher = fainter). Persists to DB + localStorage via the
existing theme prefs path (ThemePrefs.bgFade, applyTheme sets the CSS var).
Removed --bg-fade from the GlassTuner so the two don't fight over the var.
Land the UAT-approved backdrop direction as a real feature (replacing the dev
thumbnail prototype):
- 6 generated abstract SVG backdrops in public/backdrops/, one tuned to each
accent scheme (dark, mixed-colour, few light areas). Non-figurative + generated
= repo-safe, tiny, accent-matched.
- New data-backdrop="on" state (dark theme + the setting + not perf) paints the
per-scheme image on <body> (app root now transparent so it shows app-wide and
the glass refracts it) AND switches the glass to its translucent tier (the
user's tuned values: surface 50 / card 60 / scrim 30 / blur 10 / edge 25).
Off / light / perf -> flat colour + solid glass (the readable fallback).
- Settings > Appearance 'Background image' toggle (bgImage pref, dark-only),
trilingual. Flat is the fallback + opt-out.
- Remove the FeedBackdrop prototype + its tuner controls; tuner baseline resynced
to the glass-over-image tier, add a Backdrop-fade slider.
GOTCHA fixed: the dark-mode ambient 'background' shorthand reset background-size
to auto (image rendered un-covered); out-specified it with [data-theme="dark"].
Idea from UAT-2: give the feeds the same 'content behind glass' lift the Plex
detail page gets. New FeedBackdrop paints a faint, slowly-rotating feed thumbnail
as a fixed backdrop on <main> (the proven art-backdrop path — a fixed z:-1 layer
is occluded by the root's opaque bg-bg). Blur = tiny-canvas downscale + cover
upscale (no CSS filter; ytimg is CORS-readable so it uses a data-URL, else raw).
Flat --bg stays as the 'perf mode' look. Behind the GlassTuner toggle + a
Backdrop-fade slider (--feedbg-fade) so it's judged/tuned in UAT before anything
is committed for real.
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).
Foundation for the app-wide glass-consistency epic:
- index.css: every look-driving value (blur/saturate/dark-brightness/panel-card-
menu opacity/border/inset/edge/scrim/ambient + mosaic) is now a CSS custom
property defaulting to the historical hard-coded value — one-place tunable, no
visual change until a value is bumped.
- GlassTuner.tsx: dev-only (localhost-gated) always-on corner panel that live-
drives those vars, with treatment presets (Adaptive/Gradient+/Edge+scrim),
per-scheme palette editor, muted thumbnail-mosaic toggle, localStorage persist,
and Copy-CSS export. Deleted once the tuned finals are baked in.
- MosaicBackdrop.tsx: opt-in muted (blur+darken+desaturate) mosaic of on-screen
thumbnails so dark glass has real content to refract.