build(deps): migrate Tailwind CSS v3.4 → v4

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.
This commit is contained in:
2026-08-08 00:50:05 +02:00
parent 1199bb0eaf
commit 70bda98868
60 changed files with 822 additions and 809 deletions
+15 -15
View File
@@ -470,7 +470,7 @@ function PlexPosterCard({
onOpen();
}
}}
className="relative aspect-[2/3] rounded-xl overflow-hidden bg-card border border-border cursor-pointer"
className="relative aspect-2/3 rounded-xl overflow-hidden bg-card border border-border cursor-pointer"
>
<img
src={card.thumb}
@@ -522,18 +522,18 @@ function PlexPosterCard({
)}
{/* Watched badge when not hovering (the toggle replaces it on hover). */}
{card.status === "watched" && (
<span className="pointer-events-none absolute top-1.5 right-1.5 text-[10px] bg-black/70 text-white px-1.5 py-0.5 rounded group-hover:opacity-0">
<span className="pointer-events-none absolute top-1.5 right-1.5 text-[10px] bg-black/70 text-white px-1.5 py-0.5 rounded-sm group-hover:opacity-0">
{t("plex.watched")}
</span>
)}
{card.type === "show" && card.season_count != null && (
<span className="absolute bottom-1.5 left-1.5 text-[10px] bg-black/70 text-white px-1.5 py-0.5 rounded">
<span className="absolute bottom-1.5 left-1.5 text-[10px] bg-black/70 text-white px-1.5 py-0.5 rounded-sm">
{t("plex.seasons", { count: card.season_count })}
</span>
)}
{/* Aggregate in-progress badge for a partially-watched show. */}
{card.type === "show" && card.status === "in_progress" && (
<span className="pointer-events-none absolute top-1.5 right-1.5 text-[10px] bg-black/75 text-accent px-1.5 py-0.5 rounded group-hover:opacity-0">
<span className="pointer-events-none absolute top-1.5 right-1.5 text-[10px] bg-black/75 text-accent px-1.5 py-0.5 rounded-sm group-hover:opacity-0">
{t("plex.inProgress")}
</span>
)}
@@ -545,7 +545,7 @@ function PlexPosterCard({
)}
{/* Type tag — tell a movie card from a show card at a glance (unified feed). */}
<span
className="absolute bottom-1.5 right-1.5 rounded bg-black/65 p-0.5 text-white/90"
className="absolute bottom-1.5 right-1.5 rounded-sm bg-black/65 p-0.5 text-white/90"
title={t(
card.type === "show" ? "plex.filter.scopeOpt.show" : "plex.filter.scopeOpt.movie",
)}
@@ -670,7 +670,7 @@ function ActionBtn({
onClick={onClick}
disabled={disabled}
className={`inline-flex items-center gap-2 rounded-xl px-3 py-2 text-sm font-medium transition disabled:opacity-50 ${
primary ? "bg-accent text-accent-fg shadow-sm hover:opacity-90" : "glass-card glass-hover"
primary ? "bg-accent text-accent-fg shadow-xs hover:opacity-90" : "glass-card glass-hover"
}`}
>
<Icon className="w-4 h-4 shrink-0" />
@@ -707,7 +707,7 @@ function SeasonCard({
onOpen();
}
}}
className="relative aspect-[2/3] rounded-xl overflow-hidden bg-card border border-border cursor-pointer"
className="relative aspect-2/3 rounded-xl overflow-hidden bg-card border border-border cursor-pointer"
>
<img
src={se.thumb}
@@ -750,16 +750,16 @@ function SeasonCard({
</button>
)}
{watched && (
<span className="pointer-events-none absolute top-1.5 right-1.5 text-[10px] bg-black/70 text-white px-1.5 py-0.5 rounded group-hover:opacity-0">
<span className="pointer-events-none absolute top-1.5 right-1.5 text-[10px] bg-black/70 text-white px-1.5 py-0.5 rounded-sm group-hover:opacity-0">
{t("plex.watched")}
</span>
)}
{se.status === "in_progress" && (
<span className="pointer-events-none absolute top-1.5 right-1.5 text-[10px] bg-black/75 text-accent px-1.5 py-0.5 rounded group-hover:opacity-0">
<span className="pointer-events-none absolute top-1.5 right-1.5 text-[10px] bg-black/75 text-accent px-1.5 py-0.5 rounded-sm group-hover:opacity-0">
{t("plex.inProgress")}
</span>
)}
<span className="absolute bottom-1.5 left-1.5 text-[10px] bg-black/70 text-white px-1.5 py-0.5 rounded">
<span className="absolute bottom-1.5 left-1.5 text-[10px] bg-black/70 text-white px-1.5 py-0.5 rounded-sm">
{t("plex.series.episodeCount", { count: se.episode_count })}
</span>
</div>
@@ -846,7 +846,7 @@ function RelatedStrip({ related, onOpen }: { related: PlexCard[]; onOpen: (id: s
onClick={() => onOpen(r.id)}
className="group w-[130px] shrink-0 text-left"
>
<div className="relative aspect-[2/3] rounded-xl overflow-hidden bg-card border border-border">
<div className="relative aspect-2/3 rounded-xl overflow-hidden bg-card border border-border">
<img
src={r.thumb}
alt=""
@@ -854,7 +854,7 @@ function RelatedStrip({ related, onOpen }: { related: PlexCard[]; onOpen: (id: s
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-200"
/>
{r.status === "watched" && (
<span className="absolute top-1.5 right-1.5 text-[10px] bg-black/70 text-white px-1.5 py-0.5 rounded">
<span className="absolute top-1.5 right-1.5 text-[10px] bg-black/70 text-white px-1.5 py-0.5 rounded-sm">
{t("plex.watched")}
</span>
)}
@@ -963,7 +963,7 @@ function EpisodeCard({
onClick={onAdd}
title={t("plex.playlist.addEpisode")}
aria-label={t("plex.playlist.addEpisode")}
className="shrink-0 rounded p-1 text-muted transition hover:text-accent"
className="shrink-0 rounded-sm p-1 text-muted transition hover:text-accent"
>
<ListPlus className="w-4 h-4" />
</button>
@@ -1054,7 +1054,7 @@ function PlexShowView({
<img
src={show.thumb}
alt=""
className="w-full aspect-[2/3] object-cover rounded-xl border border-border"
className="w-full aspect-2/3 object-cover rounded-xl border border-border"
/>
{show.resume && (
<div className="absolute inset-0 rounded-xl bg-black/40 opacity-0 group-hover/poster:opacity-100 transition grid place-items-center">
@@ -1286,7 +1286,7 @@ function PlexSeasonView({
<img
src={se.thumb}
alt=""
className="w-full aspect-[2/3] object-cover rounded-xl border border-border"
className="w-full aspect-2/3 object-cover rounded-xl border border-border"
/>
{se.resume && (
<div className="absolute inset-0 rounded-xl bg-black/40 opacity-0 group-hover/poster:opacity-100 transition grid place-items-center">