From d794524adfde7f2fb00759f474975d73ac542054 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Sun, 9 Aug 2026 01:03:53 +0200 Subject: [PATCH] =?UTF-8?q?feat(theme):=20R10=20S1=20=E2=80=94=20semantic?= =?UTF-8?q?=20status=20colors=20(danger/warn/success)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add theme-scoped --danger/--warn/--success (+ -fg) tokens wired into the v4 @theme inline, replacing ~110 raw red/amber/emerald/green palette refs across 34 files. Fixes light-theme contrast failures (fixed Tailwind shades fell to ~1.7-3:1 on white). Status colors are scheme-independent — recognised by hue, shifting only by theme to hold contrast (bright on dark bg, darkened on light). Solid fills use the paired -fg text; @source inline guarantees the full -fg set exists. IMDb rating gold kept as a deliberate decorative non-status exception. --- frontend/src/components/AdminUsers.tsx | 14 ++++---- frontend/src/components/AuditLog.tsx | 2 +- frontend/src/components/Banner.tsx | 6 ++-- frontend/src/components/ChannelPage.tsx | 2 +- frontend/src/components/Channels.tsx | 2 +- frontend/src/components/ConfigPanel.tsx | 4 +-- frontend/src/components/ConfirmProvider.tsx | 2 +- frontend/src/components/DownloadCenter.tsx | 12 +++---- frontend/src/components/KeyGate.tsx | 8 ++--- frontend/src/components/OnboardingWizard.tsx | 4 +-- frontend/src/components/PlayerModal.tsx | 2 +- frontend/src/components/Playlists.tsx | 4 +-- frontend/src/components/PlaylistsRail.tsx | 2 +- frontend/src/components/PlexBrowse.tsx | 15 ++++---- .../src/components/PlexCollectionEditor.tsx | 2 +- frontend/src/components/PlexInfo.tsx | 1 + frontend/src/components/PlexPlayer.tsx | 4 +-- frontend/src/components/PlexPlaylistView.tsx | 10 +++--- frontend/src/components/ProfileEditor.tsx | 2 +- frontend/src/components/QueryState.tsx | 2 +- frontend/src/components/SavedViewsWidget.tsx | 2 +- frontend/src/components/Scheduler.tsx | 18 +++++----- frontend/src/components/SettingsPanel.tsx | 8 ++--- frontend/src/components/SetupWizard.tsx | 6 ++-- frontend/src/components/ShareDialog.tsx | 6 ++-- frontend/src/components/SyncStatus.tsx | 2 +- frontend/src/components/TagManager.tsx | 2 +- frontend/src/components/Toaster.tsx | 8 ++--- frontend/src/components/VideoCard.tsx | 4 +-- frontend/src/components/VideoEditor.tsx | 2 +- frontend/src/components/WatchPage.tsx | 2 +- frontend/src/components/Welcome.tsx | 6 ++-- frontend/src/components/playlistColumns.tsx | 2 +- frontend/src/components/ui/DraftSaveBar.tsx | 4 +-- frontend/src/index.css | 36 +++++++++++++++++++ 35 files changed, 123 insertions(+), 85 deletions(-) diff --git a/frontend/src/components/AdminUsers.tsx b/frontend/src/components/AdminUsers.tsx index 35df421c..d96df65c 100644 --- a/frontend/src/components/AdminUsers.tsx +++ b/frontend/src/components/AdminUsers.tsx @@ -58,7 +58,7 @@ function Badge({ tone === "accent" ? "border-accent/40 text-accent" : tone === "warning" - ? "border-amber-500/40 text-amber-500" + ? "border-warn/40 text-warn" : "border-border text-muted"; return ( @@ -230,8 +230,8 @@ function UsersRoles({ me }: { me: Me }) { } className={`shrink-0 p-1.5 rounded-lg border disabled:opacity-40 transition ${ u.is_suspended - ? "border-amber-500/40 text-amber-500 hover:bg-amber-500/10" - : "border-border text-muted hover:text-amber-500" + ? "border-warn/40 text-warn hover:bg-warn/10" + : "border-border text-muted hover:text-warn" }`} > {u.is_suspended ? ( @@ -248,7 +248,7 @@ function UsersRoles({ me }: { me: Me }) { onClick={() => onDelete(u)} disabled={u.is_demo || self || pendingIds.has(u.id)} aria-label={t("users.delete.action")} - className="shrink-0 p-1.5 rounded-lg border border-border text-muted hover:text-red-400 disabled:opacity-40 transition" + className="shrink-0 p-1.5 rounded-lg border border-border text-muted hover:text-danger disabled:opacity-40 transition" > @@ -368,7 +368,7 @@ function AdminInvites() { {decided.map((i) => (
{i.email} - + {i.status}
@@ -445,7 +445,7 @@ function AdminDemo() { diff --git a/frontend/src/components/PlaylistsRail.tsx b/frontend/src/components/PlaylistsRail.tsx index ef119df0..b0405e14 100644 --- a/frontend/src/components/PlaylistsRail.tsx +++ b/frontend/src/components/PlaylistsRail.tsx @@ -216,7 +216,7 @@ export default function PlaylistsRail({ diff --git a/frontend/src/components/PlexBrowse.tsx b/frontend/src/components/PlexBrowse.tsx index 5ae2b44f..67d7925b 100644 --- a/frontend/src/components/PlexBrowse.tsx +++ b/frontend/src/components/PlexBrowse.tsx @@ -431,9 +431,9 @@ export default function PlexBrowse() { } const PLAYABLE_TINT: Record = { - direct: "bg-emerald-500", - remux: "bg-amber-500", - transcode: "bg-orange-600", + direct: "bg-success", + remux: "bg-warn", + transcode: "bg-warn", }; function PlexPosterCard({ @@ -503,7 +503,7 @@ function PlexPosterCard({ className="absolute top-1.5 right-1.5 p-1 rounded-full bg-black/60 opacity-0 group-hover:opacity-100 hover:bg-black/80 transition" > )} @@ -733,7 +733,7 @@ function SeasonCard({ } className="absolute top-1.5 right-1.5 p-1 rounded-full bg-black/60 opacity-0 group-hover:opacity-100 hover:bg-black/80 transition" > - + )} {/* Add whole season to a playlist, on hover. */} @@ -920,7 +920,7 @@ function EpisodeCard({ className="absolute top-1.5 right-1.5 p-1 rounded-full bg-black/60 opacity-0 group-hover:opacity-100 hover:bg-black/80 transition" > )} @@ -930,7 +930,7 @@ function EpisodeCard({ onToggleWatched ? "group-hover:opacity-0" : "" }`} > - +
)} {pct > 0 && ( @@ -1099,6 +1099,7 @@ function PlexShowView({ } className={`inline-flex items-center gap-1 ${onFilter ? "cursor-pointer hover:text-accent" : "cursor-default"}`} > + {/* decorative rating gold — intentionally NOT a status token */} · {show.imdb_rating} diff --git a/frontend/src/components/PlexCollectionEditor.tsx b/frontend/src/components/PlexCollectionEditor.tsx index 89e1abd3..48c58080 100644 --- a/frontend/src/components/PlexCollectionEditor.tsx +++ b/frontend/src/components/PlexCollectionEditor.tsx @@ -193,7 +193,7 @@ export default function PlexCollectionEditor({ diff --git a/frontend/src/components/PlexInfo.tsx b/frontend/src/components/PlexInfo.tsx index 436eb799..3817fb67 100644 --- a/frontend/src/components/PlexInfo.tsx +++ b/frontend/src/components/PlexInfo.tsx @@ -188,6 +188,7 @@ export default function PlexInfo({ )} {detail.imdb_rating != null && ( + // decorative rating gold — intentionally NOT a status token @@ -323,7 +323,7 @@ function ShowGroup({ block, ctx }: { block: Extract; ct onClick={() => ctx.onRemove(s.items.map((i) => i.id))} disabled={ctx.busy} title={t("plex.playlist.removeSeason")} - className="ml-1 rounded-sm p-0.5 text-muted hover:text-red-400 disabled:opacity-30" + className="ml-1 rounded-sm p-0.5 text-muted hover:text-danger disabled:opacity-30" > @@ -572,7 +572,7 @@ export default function PlexPlaylistView({ diff --git a/frontend/src/components/ProfileEditor.tsx b/frontend/src/components/ProfileEditor.tsx index 88399970..685436eb 100644 --- a/frontend/src/components/ProfileEditor.tsx +++ b/frontend/src/components/ProfileEditor.tsx @@ -136,7 +136,7 @@ export default function ProfileEditor({ onClose }: { onClose: () => void }) { diff --git a/frontend/src/components/QueryState.tsx b/frontend/src/components/QueryState.tsx index 374413e1..fa397957 100644 --- a/frontend/src/components/QueryState.tsx +++ b/frontend/src/components/QueryState.tsx @@ -44,7 +44,7 @@ export function StateMessage({ aria-live={tone === "error" ? "assertive" : "polite"} > - {tone === "error" && } + {tone === "error" && } {children} {onRetry && ( diff --git a/frontend/src/components/SavedViewsWidget.tsx b/frontend/src/components/SavedViewsWidget.tsx index ec86b165..537bbb97 100644 --- a/frontend/src/components/SavedViewsWidget.tsx +++ b/frontend/src/components/SavedViewsWidget.tsx @@ -331,7 +331,7 @@ function ViewRow({ diff --git a/frontend/src/components/Scheduler.tsx b/frontend/src/components/Scheduler.tsx index cc9b387c..74acbecf 100644 --- a/frontend/src/components/Scheduler.tsx +++ b/frontend/src/components/Scheduler.tsx @@ -46,9 +46,9 @@ function fmtCountdown(s: number): string { const DOT_CLASS: Record = { running: "bg-accent animate-pulse", - ok: "bg-emerald-500", - error: "bg-red-500", - skipped: "bg-amber-500", + ok: "bg-success", + error: "bg-danger", + skipped: "bg-warn", idle: "bg-border", }; @@ -178,7 +178,7 @@ function JobRow({ @@ -234,7 +234,7 @@ function LinkRow({ link, onChanged }: { link: ShareLink; onChanged: () => void } savePass.mutate(""); }} disabled={savePass.isPending} - className="px-2 py-1 rounded-md text-xs text-muted hover:text-red-400 transition" + className="px-2 py-1 rounded-md text-xs text-muted hover:text-danger transition" > {t("downloads.share.removePassword")} diff --git a/frontend/src/components/SyncStatus.tsx b/frontend/src/components/SyncStatus.tsx index 5e5095d5..c0f8cff1 100644 --- a/frontend/src/components/SyncStatus.tsx +++ b/frontend/src/components/SyncStatus.tsx @@ -83,7 +83,7 @@ export default function SyncStatus({ ) : ( // A small check anchors the "all synced" state so it doesn't read as orphaned text. - + {t("header.sync.allSynced")} diff --git a/frontend/src/components/TagManager.tsx b/frontend/src/components/TagManager.tsx index fbcafbf2..7ecb279f 100644 --- a/frontend/src/components/TagManager.tsx +++ b/frontend/src/components/TagManager.tsx @@ -68,7 +68,7 @@ function TagRow({ onClick={onDelete} title={t("tagManager.delete")} aria-label={t("tagManager.delete")} - className="shrink-0 p-1.5 rounded-md text-muted hover:text-red-400 transition" + className="shrink-0 p-1.5 rounded-md text-muted hover:text-danger transition" > diff --git a/frontend/src/components/Toaster.tsx b/frontend/src/components/Toaster.tsx index b6bfc075..d87af574 100644 --- a/frontend/src/components/Toaster.tsx +++ b/frontend/src/components/Toaster.tsx @@ -5,10 +5,10 @@ import { dismiss, getActiveToasts, subscribe, type NotifLevel } from "../lib/not export const LEVEL_STYLE: Record = { info: { icon: Info, color: "text-accent", bar: "bg-accent" }, - success: { icon: CheckCircle2, color: "text-emerald-400", bar: "bg-emerald-400" }, - warning: { icon: AlertTriangle, color: "text-amber-400", bar: "bg-amber-400" }, - error: { icon: AlertCircle, color: "text-red-400", bar: "bg-red-400" }, - fatal: { icon: XCircle, color: "text-red-500", bar: "bg-red-500" }, + success: { icon: CheckCircle2, color: "text-success", bar: "bg-success" }, + warning: { icon: AlertTriangle, color: "text-warn", bar: "bg-warn" }, + error: { icon: AlertCircle, color: "text-danger", bar: "bg-danger" }, + fatal: { icon: XCircle, color: "text-danger", bar: "bg-danger" }, }; export default function Toaster() { diff --git a/frontend/src/components/VideoCard.tsx b/frontend/src/components/VideoCard.tsx index 2b4c7f3d..cf42caaf 100644 --- a/frontend/src/components/VideoCard.tsx +++ b/frontend/src/components/VideoCard.tsx @@ -156,7 +156,7 @@ function RowMetaCells({ video }: { video: Video }) { // "live" dot and a "stream" dot would be the same dot. const status = video.live_status === "live" - ? { Icon: Radio, cls: "text-red-500", label: t("card.live") } + ? { Icon: Radio, cls: "text-danger", label: t("card.live") } : video.live_status === "upcoming" ? { Icon: Clock, cls: "", label: t("card.upcoming") } : video.live_status === "was_live" @@ -250,7 +250,7 @@ function Thumb({ ) : video.live_status === "live" || video.live_status === "upcoming" ? ( {t(`card.${video.live_status}`)} diff --git a/frontend/src/components/VideoEditor.tsx b/frontend/src/components/VideoEditor.tsx index d2a0ed57..52312de2 100644 --- a/frontend/src/components/VideoEditor.tsx +++ b/frontend/src/components/VideoEditor.tsx @@ -460,7 +460,7 @@ export default function VideoEditor({ job, onClose }: { job: DownloadJob; onClos e.stopPropagation(); deleteBoundary(bi); }} - className="absolute top-0 -right-2 p-0.5 rounded-full bg-black/70 text-muted hover:text-red-400" + className="absolute top-0 -right-2 p-0.5 rounded-full bg-black/70 text-muted hover:text-danger" title={t("editor.removeCut")} > diff --git a/frontend/src/components/WatchPage.tsx b/frontend/src/components/WatchPage.tsx index 48e6596b..13e51f58 100644 --- a/frontend/src/components/WatchPage.tsx +++ b/frontend/src/components/WatchPage.tsx @@ -192,7 +192,7 @@ export default function WatchPage() { placeholder={T.passwordPlaceholder} className="w-full rounded-lg bg-surface border border-border px-3 py-2 text-sm outline-hidden focus:ring-2 focus:ring-accent/40" /> - {error &&
{error}
} + {error &&
{error}
}