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:
@@ -116,7 +116,7 @@ type Ctx = {
|
||||
function Poster({ src, className }: { src: string; className: string }) {
|
||||
return (
|
||||
<div
|
||||
className={`relative shrink-0 overflow-hidden rounded border border-border bg-surface ${className}`}
|
||||
className={`relative shrink-0 overflow-hidden rounded-sm border border-border bg-surface ${className}`}
|
||||
>
|
||||
<img src={src} alt="" loading="lazy" className="h-full w-full object-cover" />
|
||||
</div>
|
||||
@@ -161,11 +161,11 @@ function EpisodeRow({ ep, ctx, dense }: { ep: PlexCard; ctx: Ctx; dense: boolean
|
||||
className="group flex min-w-0 flex-1 items-center gap-2.5 text-left"
|
||||
>
|
||||
<div
|
||||
className={`relative shrink-0 overflow-hidden rounded border border-border bg-surface ${dense ? "aspect-video w-14" : "aspect-video w-20"}`}
|
||||
className={`relative shrink-0 overflow-hidden rounded-sm border border-border bg-surface ${dense ? "aspect-video w-14" : "aspect-video w-20"}`}
|
||||
>
|
||||
<img src={ep.thumb} alt="" loading="lazy" className="h-full w-full object-cover" />
|
||||
{ep.status === "watched" && (
|
||||
<span className="absolute right-0.5 top-0.5 rounded bg-black/70 px-1 text-[9px] text-white">
|
||||
<span className="absolute right-0.5 top-0.5 rounded-sm bg-black/70 px-1 text-[9px] text-white">
|
||||
✓
|
||||
</span>
|
||||
)}
|
||||
@@ -179,7 +179,7 @@ function EpisodeRow({ ep, ctx, dense }: { ep: PlexCard; ctx: Ctx; dense: boolean
|
||||
<button
|
||||
onClick={() => ctx.onRemove([ep.id])}
|
||||
disabled={ctx.busy}
|
||||
className="shrink-0 rounded p-1 text-muted hover:text-red-400 disabled:opacity-30"
|
||||
className="shrink-0 rounded-sm p-1 text-muted hover:text-red-400 disabled:opacity-30"
|
||||
aria-label="remove"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
@@ -210,7 +210,7 @@ function MovieRow({ item, ctx }: { item: PlexCard; ctx: Ctx }) {
|
||||
onClick={() => ctx.onPlay(item.id)}
|
||||
className="group flex min-w-0 flex-1 items-center gap-3 text-left"
|
||||
>
|
||||
<Poster src={item.thumb} className={dense ? "aspect-[2/3] w-10" : "aspect-[2/3] w-14"} />
|
||||
<Poster src={item.thumb} className={dense ? "aspect-2/3 w-10" : "aspect-2/3 w-14"} />
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-sm font-medium">{item.title}</div>
|
||||
<div className="text-xs text-muted">{item.year}</div>
|
||||
@@ -222,7 +222,7 @@ function MovieRow({ item, ctx }: { item: PlexCard; ctx: Ctx }) {
|
||||
<button
|
||||
onClick={() => ctx.onRemove([item.id])}
|
||||
disabled={ctx.busy}
|
||||
className="shrink-0 rounded p-1 text-muted hover:text-red-400 disabled:opacity-30"
|
||||
className="shrink-0 rounded-sm p-1 text-muted hover:text-red-400 disabled:opacity-30"
|
||||
aria-label="remove"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
@@ -256,7 +256,7 @@ function ShowGroup({ block, ctx }: { block: Extract<Block, { kind: "show" }>; ct
|
||||
<DragHandle {...attributes} {...listeners} />
|
||||
<button
|
||||
onClick={() => ctx.toggleShow(block.showKey)}
|
||||
className="grid h-6 w-6 shrink-0 place-items-center rounded text-muted hover:text-fg"
|
||||
className="grid h-6 w-6 shrink-0 place-items-center rounded-sm text-muted hover:text-fg"
|
||||
aria-expanded={open}
|
||||
>
|
||||
{open ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
|
||||
@@ -269,7 +269,7 @@ function ShowGroup({ block, ctx }: { block: Extract<Block, { kind: "show" }>; ct
|
||||
src={ep.thumb}
|
||||
alt=""
|
||||
loading="lazy"
|
||||
className="aspect-[2/3] w-8 rounded border-2 border-surface object-cover"
|
||||
className="aspect-2/3 w-8 rounded-sm border-2 border-surface object-cover"
|
||||
style={{ marginLeft: i ? -14 : 0 }}
|
||||
/>
|
||||
))}
|
||||
@@ -285,7 +285,7 @@ function ShowGroup({ block, ctx }: { block: Extract<Block, { kind: "show" }>; ct
|
||||
onClick={() => ctx.onRemove(block.items.map((i) => i.id))}
|
||||
disabled={ctx.busy}
|
||||
title={t("plex.playlist.removeShow")}
|
||||
className="shrink-0 rounded p-1 text-muted hover:text-red-400 disabled:opacity-30"
|
||||
className="shrink-0 rounded-sm p-1 text-muted hover:text-red-400 disabled:opacity-30"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
@@ -306,7 +306,7 @@ function ShowGroup({ block, ctx }: { block: Extract<Block, { kind: "show" }>; ct
|
||||
{dense && (
|
||||
<button
|
||||
onClick={() => ctx.toggleSeason(sKey)}
|
||||
className="grid h-5 w-5 shrink-0 place-items-center rounded text-muted hover:text-fg"
|
||||
className="grid h-5 w-5 shrink-0 place-items-center rounded-sm text-muted hover:text-fg"
|
||||
aria-expanded={sOpen}
|
||||
>
|
||||
{sOpen ? (
|
||||
@@ -323,7 +323,7 @@ function ShowGroup({ block, ctx }: { block: Extract<Block, { kind: "show" }>; ct
|
||||
onClick={() => ctx.onRemove(s.items.map((i) => i.id))}
|
||||
disabled={ctx.busy}
|
||||
title={t("plex.playlist.removeSeason")}
|
||||
className="ml-1 rounded p-0.5 text-muted hover:text-red-400 disabled:opacity-30"
|
||||
className="ml-1 rounded-sm p-0.5 text-muted hover:text-red-400 disabled:opacity-30"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
@@ -532,7 +532,7 @@ export default function PlexPlaylistView({
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && rename()}
|
||||
onBlur={rename}
|
||||
className="glass-card min-w-0 flex-1 rounded-lg px-3 py-1.5 text-lg font-bold outline-none focus:border-accent"
|
||||
className="glass-card min-w-0 flex-1 rounded-lg px-3 py-1.5 text-lg font-bold outline-hidden focus:border-accent"
|
||||
/>
|
||||
) : (
|
||||
<h1 className="min-w-0 flex-1 truncate text-2xl font-bold">{q.data?.title ?? " "}</h1>
|
||||
|
||||
Reference in New Issue
Block a user