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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user