commit 9d10cfef2849532bb1e6b0e0687580288221cf38 parent 84540f9f47f0b1fa4d6178dbfcb28806c971d647 Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st> Date: Wed, 1 Jul 2026 01:16:34 -0400 Theme: make the whole family theme-aware + rename Terminal→Selenized + distinct per-family voice The reported bug: editor screens like the Active Jobs cards ignored the theme family. ~100 components (editor/app + shared common/components) hardcoded Tailwind palette colors (bg-white/dark:bg-zinc-900, text-zinc-500, red/amber/green/blue status) that tracked light/dark MODE but not the theme FAMILY, so they stayed zinc/white in Archive/Selenized/Swiss. Token migration - Every such component now uses semantic tokens: neutrals → card/muted/border/ foreground/muted-foreground; status → destructive/warning/success/info (+ -soft fills, /30 borders, -foreground on solid fills); inverted CTAs → primary. - Deliberate exceptions kept: the violet audio-probe "scanning" fills, chart data-series colors (already var(--chart-N), theme-aware), the shadcn modal bg-black scrims, the theater-mode media player (TranscriptModal/PlayerProvider, dark chrome over a black video — tokenizing would invert contrast), and the rose age-restricted badge. - Form-control accent-color tints → accent-brand. Terminal → Selenized - Replaces the ice-cyan "terminal" family with the authentic Solarized successor (Jan Warchoł's selenized): warm-tan light ground (#fbf3db) / deep teal-slate dark ground (#103c48) with the calibrated Selenized accent set. Renamed across themeConfig, tokens.css, the pre-paint ThemeScript + ThemeProvider (a stored "terminal" auto-migrates to "selenized"), and the theme-family e2e spec. Distinct per-family voice (beyond color) - Per-family corner radius + typeface: Swiss = 0px in Archivo (neo-grotesque), Selenized = 0.375rem in JetBrains Mono display + IBM Plex Sans body, Archive/ Base keep soft corners + the Source serif family. Fonts load in common/styles/ fonts.ts; the --font-* reassignments use html[data-theme] specificity to beat next/font's variable class. More things theme-stylable - --surface, --border-strong, --faint, --brand-ink are now defined for EVERY family (base previously lacked them) and exposed via @theme inline as bg-surface / border-border-strong / text-faint / text-brand-ink utilities. Verified: common/editor/export/homepage typecheck clean; editor + homepage production builds compile (new fonts fetch, tokens.css compiles). Changelogs updated (editor + export). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Diffstat:
122 files changed, 1111 insertions(+), 1017 deletions(-)
diff --git a/common/components/Changelog.tsx b/common/components/Changelog.tsx @@ -45,7 +45,7 @@ function H2({ children }: { children?: ReactNode }) { return ( <h2 id={anchor ?? undefined} - className="scroll-mt-20 mt-8 mb-3 text-base font-semibold text-zinc-900 dark:text-zinc-100 first:mt-0 flex items-baseline" + className="scroll-mt-20 mt-8 mb-3 text-base font-semibold text-foreground first:mt-0 flex items-baseline" > <span>{children}</span> {anchor && <CopyLinkButton anchor={anchor} />} @@ -66,58 +66,58 @@ const OVERRIDES = { h3: { props: { className: - "mt-6 mb-2 text-sm font-semibold text-zinc-900 dark:text-zinc-100", + "mt-6 mb-2 text-sm font-semibold text-foreground", }, }, p: { - props: { className: "my-2 text-sm text-zinc-700 dark:text-zinc-300" }, + props: { className: "my-2 text-sm text-muted-foreground" }, }, ul: { props: { className: - "list-disc pl-5 my-2 space-y-1 text-sm text-zinc-700 dark:text-zinc-300 marker:text-zinc-400", + "list-disc pl-5 my-2 space-y-1 text-sm text-muted-foreground marker:text-muted-foreground", }, }, ol: { props: { className: - "list-decimal pl-5 my-2 space-y-1 text-sm text-zinc-700 dark:text-zinc-300 marker:text-zinc-400", + "list-decimal pl-5 my-2 space-y-1 text-sm text-muted-foreground marker:text-muted-foreground", }, }, li: { props: { className: "leading-relaxed" } }, strong: { props: { - className: "font-semibold text-zinc-900 dark:text-zinc-100", + className: "font-semibold text-foreground", }, }, em: { props: { className: "italic" } }, code: { props: { className: - "px-1 py-0.5 rounded bg-zinc-100 dark:bg-zinc-800 font-mono text-[0.85em]", + "px-1 py-0.5 rounded bg-muted font-mono text-[0.85em]", }, }, pre: { props: { className: - "my-3 p-3 rounded-md bg-zinc-100 dark:bg-zinc-900 overflow-x-auto text-xs font-mono", + "my-3 p-3 rounded-md bg-muted overflow-x-auto text-xs font-mono", }, }, a: { props: { className: - "underline decoration-zinc-400 hover:decoration-zinc-700 dark:hover:decoration-zinc-200", + "underline decoration-muted-foreground hover:decoration-foreground", }, }, hr: { props: { - className: "my-6 border-t border-zinc-200 dark:border-zinc-800", + className: "my-6 border-t border-border", }, }, blockquote: { props: { className: - "my-3 pl-3 border-l-2 border-zinc-300 dark:border-zinc-700 text-zinc-600 dark:text-zinc-400", + "my-3 pl-3 border-l-2 border-border text-muted-foreground", }, }, }; diff --git a/common/components/CopyLinkButton.tsx b/common/components/CopyLinkButton.tsx @@ -21,7 +21,7 @@ export function CopyLinkButton({ anchor }: { anchor: string }) { type="button" onClick={onClick} aria-label={`Copy link to ${anchor}`} - className="ml-2 align-middle inline-flex items-center gap-1 text-xs font-normal text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200 transition-colors" + className="ml-2 align-middle inline-flex items-center gap-1 text-xs font-normal text-muted-foreground hover:text-foreground transition-colors" > {copied ? ( <span aria-hidden="true">Copied</span> diff --git a/common/components/QueryGroupView.tsx b/common/components/QueryGroupView.tsx @@ -20,7 +20,7 @@ import { Button } from "./ui/button"; import { Checkbox } from "./ui/checkbox"; const negateCheckboxClass = - "border-destructive/60 data-[state=checked]:bg-destructive data-[state=checked]:border-destructive data-[state=checked]:text-white"; + "border-destructive/60 data-[state=checked]:bg-destructive data-[state=checked]:border-destructive data-[state=checked]:text-destructive-foreground"; type Props = { group: GroupNode; diff --git a/common/components/QueryLeafView.tsx b/common/components/QueryLeafView.tsx @@ -17,7 +17,7 @@ import { Checkbox } from "./ui/checkbox"; import { cn } from "../lib/utils"; const negateCheckboxClass = - "border-destructive/60 data-[state=checked]:bg-destructive data-[state=checked]:border-destructive data-[state=checked]:text-white"; + "border-destructive/60 data-[state=checked]:bg-destructive data-[state=checked]:border-destructive data-[state=checked]:text-destructive-foreground"; type Props = { leaf: LeafNode; @@ -133,7 +133,7 @@ export default function QueryLeafView({ const cachedBadge = state?.cached && !state.active ? ( <span - className="ml-1 text-[10px] uppercase tracking-wide px-1.5 py-0.5 rounded bg-emerald-100 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-300" + className="ml-1 text-[10px] uppercase tracking-wide px-1.5 py-0.5 rounded bg-success-soft text-success" data-cache-hit={leaf.id} title="Served from layer cache" > diff --git a/common/components/ThemeMenu.tsx b/common/components/ThemeMenu.tsx @@ -20,10 +20,10 @@ import { import { cn } from "../lib/utils"; // The theme-family picker: a palette dropdown to choose the family (Base / -// Archive / Terminal / Swiss) and the mode (light / dark / system). Sits beside -// the quick-toggle ThemeToggle. Each family is a pure CSS token swap, so picking -// one only changes `data-theme` on <html> (handled by setTheme). Must render -// inside a <ThemeProvider/>. +// Archive / Selenized / Swiss) and the mode (light / dark / system). Sits beside +// the quick-toggle ThemeToggle. Each family is a CSS token swap (color, plus a +// per-family radius + type voice), so picking one only changes `data-theme` on +// <html> (handled by setTheme). Must render inside a <ThemeProvider/>. export function ThemeMenu({ className }: { className?: string }) { const { theme, mode, setTheme, setMode } = useTheme(); diff --git a/common/components/ThemeProvider.tsx b/common/components/ThemeProvider.tsx @@ -71,8 +71,18 @@ export function ThemeProvider({ // Adopt persisted values into React state (NOT the DOM — the script owns that). useEffect(() => { try { - const t = localStorage.getItem(THEME_KEY) as ThemeFamily | null; + let t = localStorage.getItem(THEME_KEY) as ThemeFamily | "terminal" | null; const m = localStorage.getItem(MODE_KEY) as ThemeMode | null; + // The "terminal" family was renamed to "selenized"; migrate any stored value + // (and rewrite it) so the picker highlights the right entry. + if (t === "terminal") { + t = "selenized"; + try { + localStorage.setItem(THEME_KEY, t); + } catch { + /* ignore */ + } + } if (t) setThemeState(t); if (m) setModeState(m); } catch { diff --git a/common/components/ThemeScript.tsx b/common/components/ThemeScript.tsx @@ -32,6 +32,7 @@ export function ThemeScript({ ")||" + JSON.stringify(defaultTheme) + ";" + + "if(t==='terminal'){t='selenized';}" + // renamed family; migrate old stored value "if(t&&t!=='base'){d.setAttribute('data-theme',t);}else{d.removeAttribute('data-theme');}" + "var m=localStorage.getItem(" + JSON.stringify(MODE_KEY) + diff --git a/common/components/TranscriptSearch.tsx b/common/components/TranscriptSearch.tsx @@ -1388,7 +1388,7 @@ export default function TranscriptSearch() { Reset layers </Button> {(queryDirty || filtersDirty) && ( - <span className="text-xs text-amber-600 dark:text-amber-400"> + <span className="text-xs text-warning"> Press Enter or click Search to apply </span> )} @@ -2064,7 +2064,7 @@ function ResultCard({ onClick={() => openWithMode(group.slug, h)} className={`w-full h-auto justify-start text-left flex gap-3 px-4 py-2 rounded-none font-normal whitespace-normal ${ isActive - ? "bg-amber-100/70 dark:bg-amber-500/10 ring-1 ring-inset ring-amber-400/60" + ? "bg-warning-soft ring-1 ring-inset ring-warning/60" : "" }`} > @@ -2153,7 +2153,7 @@ function ProfilesRow({ role="status" aria-label="profile has unsaved changes" data-testid="profile-dirty-dot" - className="text-base leading-none text-amber-700 dark:text-amber-300" + className="text-base leading-none text-warning" > • </span> @@ -2390,7 +2390,7 @@ function highlight(text: string, q: string, useRegex = false) { parts.push( <mark key={key++} - className="bg-yellow-200 dark:bg-yellow-500/30 text-inherit" + className="bg-warning-soft text-inherit" > {m[0]} </mark>, @@ -2415,7 +2415,7 @@ function highlight(text: string, q: string, useRegex = false) { parts.push( <mark key={idx} - className="bg-yellow-200 dark:bg-yellow-500/30 text-inherit" + className="bg-warning-soft text-inherit" > {text.slice(idx, idx + q.length)} </mark>, diff --git a/common/components/charts/ChartCard.tsx b/common/components/charts/ChartCard.tsx @@ -20,7 +20,7 @@ type Props = { }; const btn = - "rounded-md border border-zinc-200 dark:border-zinc-700 px-2 py-1 text-xs hover:bg-zinc-100 dark:hover:bg-zinc-800 disabled:opacity-50"; + "rounded-md border border-border px-2 py-1 text-xs hover:bg-muted disabled:opacity-50"; export function ChartCard({ config, @@ -74,13 +74,13 @@ export function ChartCard({ return ( <div ref={ref} - className="flex flex-col gap-2 rounded-lg border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 p-3" + className="flex flex-col gap-2 rounded-lg border border-border bg-card p-3" > <div className="flex items-start justify-between gap-2"> <div className="min-w-0"> <h3 className="truncate text-sm font-semibold">{config.title}</h3> {config.search && ( - <p className="text-xs text-zinc-500"> + <p className="text-xs text-muted-foreground"> {loading ? "Searching…" : `${searchState.matchedVideos} videos · ${searchState.totalHits} hits`} @@ -119,7 +119,7 @@ export function ChartCard({ </button> {onRemove && ( <button - className={`${btn} text-red-600 dark:text-red-400`} + className={`${btn} text-destructive`} onClick={onRemove} type="button" > diff --git a/common/components/charts/ChartConfigEditor.tsx b/common/components/charts/ChartConfigEditor.tsx @@ -21,8 +21,8 @@ import { import type { GroupState, LeafState } from "../../lib/searchEval"; import QueryBuilder from "../QueryBuilder"; -const sel = "rounded-md border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm"; -const lbl = "flex flex-col gap-1 text-xs font-medium text-zinc-500"; +const sel = "rounded-md border border-border bg-card px-2 py-1 text-sm"; +const lbl = "flex flex-col gap-1 text-xs font-medium text-muted-foreground"; // The chart editor has no live search evaluation, so the builder renders without // per-leaf/group count badges. @@ -77,7 +77,7 @@ export function ChartConfigEditor({ }; return ( - <div className="flex flex-col gap-4 rounded-lg border border-zinc-300 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-900 p-4"> + <div className="flex flex-col gap-4 rounded-lg border border-border bg-card p-4"> <label className={lbl}> Title <input @@ -132,7 +132,7 @@ export function ChartConfigEditor({ <div className="flex flex-col gap-1"> <span className={lbl}>Data source</span> - <div className="inline-flex w-fit overflow-hidden rounded-md border border-zinc-300 dark:border-zinc-700"> + <div className="inline-flex w-fit overflow-hidden rounded-md border border-border"> {(["metadata", "search"] as const).map((m) => ( <button key={m} @@ -140,8 +140,8 @@ export function ChartConfigEditor({ onClick={() => setMode(m)} className={`px-3 py-1 text-sm capitalize ${ mode === m - ? "bg-blue-600 text-white" - : "bg-white dark:bg-zinc-900 hover:bg-zinc-100 dark:hover:bg-zinc-800" + ? "bg-primary text-primary-foreground" + : "bg-card hover:bg-muted" }`} > {m === "metadata" ? "Metadata" : "Search matches"} @@ -150,7 +150,7 @@ export function ChartConfigEditor({ </div> </div> - <fieldset className="flex flex-wrap items-end gap-3 rounded-md border border-zinc-200 dark:border-zinc-800 p-3"> + <fieldset className="flex flex-wrap items-end gap-3 rounded-md border border-border p-3"> <legend className="px-1 text-xs font-semibold">X axis</legend> <label className={lbl}> Dimension @@ -256,7 +256,7 @@ export function ChartConfigEditor({ </fieldset> {mode === "metadata" && ( - <fieldset className="flex flex-wrap items-end gap-3 rounded-md border border-zinc-200 dark:border-zinc-800 p-3"> + <fieldset className="flex flex-wrap items-end gap-3 rounded-md border border-border p-3"> <legend className="px-1 text-xs font-semibold">Y metric</legend> <label className={lbl}> Aggregate @@ -306,7 +306,7 @@ export function ChartConfigEditor({ )} {mode === "search" && ( - <fieldset className="flex flex-col gap-3 rounded-md border border-zinc-200 dark:border-zinc-800 p-3"> + <fieldset className="flex flex-col gap-3 rounded-md border border-border p-3"> <legend className="px-1 text-xs font-semibold">Search</legend> <label className={`${lbl} self-start`}> Plot @@ -332,7 +332,7 @@ export function ChartConfigEditor({ leafStates={NO_LEAF_STATES} groupStates={NO_GROUP_STATES} /> - <p className="text-xs text-zinc-500"> + <p className="text-xs text-muted-foreground"> Build the query with any number of layers (AND/OR/NOT). Each matched video counts once, or by its hit count. The X axis, grouping, and filters above still apply. @@ -344,14 +344,14 @@ export function ChartConfigEditor({ <button type="button" onClick={save} - className="rounded-md bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700" + className="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90" > Save </button> <button type="button" onClick={onCancel} - className="rounded-md border border-zinc-300 dark:border-zinc-700 px-3 py-1.5 text-sm hover:bg-zinc-100 dark:hover:bg-zinc-800" + className="rounded-md border border-border px-3 py-1.5 text-sm hover:bg-muted" > Cancel </button> diff --git a/common/components/charts/ChartView.tsx b/common/components/charts/ChartView.tsx @@ -68,7 +68,7 @@ export function ChartView({ if (data.categories.length === 0) { return ( - <div className="flex h-[280px] items-center justify-center text-sm text-zinc-500"> + <div className="flex h-[280px] items-center justify-center text-sm text-muted-foreground"> No data for this configuration. </div> ); diff --git a/common/components/charts/ChartsDashboard.tsx b/common/components/charts/ChartsDashboard.tsx @@ -33,9 +33,9 @@ type Props = { }; const primaryBtn = - "rounded-md bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700"; + "rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90"; const ghostBtn = - "rounded-md border border-zinc-300 dark:border-zinc-700 px-3 py-1.5 text-sm hover:bg-zinc-100 dark:hover:bg-zinc-800"; + "rounded-md border border-border px-3 py-1.5 text-sm hover:bg-muted"; const STORAGE_KEY = "charts:dashboard"; @@ -231,10 +231,10 @@ export function ChartsDashboard({ <FilterBar filters={dashboard.filters} channels={channels} onChange={setFilters} /> {showGallery && ( - <div className="flex flex-col gap-4 rounded-lg border border-zinc-200 dark:border-zinc-800 p-3"> + <div className="flex flex-col gap-4 rounded-lg border border-border p-3"> {gallery.map((group) => ( <div key={group.id} className="flex flex-col gap-2"> - <h3 className="text-xs font-semibold uppercase tracking-wide text-zinc-500"> + <h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground"> {group.title} </h3> <div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3"> @@ -243,11 +243,11 @@ export function ChartsDashboard({ key={t.id} type="button" onClick={() => addFromGallery(t)} - className="flex flex-col gap-1 rounded-md border border-zinc-200 dark:border-zinc-700 p-3 text-left hover:bg-zinc-50 dark:hover:bg-zinc-800" + className="flex flex-col gap-1 rounded-md border border-border p-3 text-left hover:bg-muted" > <span className="text-sm font-medium">{t.title}</span> {t.description && ( - <span className="text-xs text-zinc-500">{t.description}</span> + <span className="text-xs text-muted-foreground">{t.description}</span> )} </button> ))} @@ -280,7 +280,7 @@ export function ChartsDashboard({ </div> {dashboard.charts.length === 0 && ( - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> No charts yet — use “Add chart” to start from a template or “New blank” to build one. </p> diff --git a/common/components/charts/FilterBar.tsx b/common/components/charts/FilterBar.tsx @@ -6,8 +6,8 @@ import { ymdToInput, inputToYmd } from "../../lib/ymd"; export type ChannelOption = { slug: string; name: string }; -const field = "rounded-md border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm"; -const label = "text-xs font-medium text-zinc-500"; +const field = "rounded-md border border-border bg-card px-2 py-1 text-sm"; +const label = "text-xs font-medium text-muted-foreground"; export function FilterBar({ filters, @@ -34,7 +34,7 @@ export function FilterBar({ }; return ( - <div className="flex flex-wrap items-end gap-3 rounded-lg border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 p-3"> + <div className="flex flex-wrap items-end gap-3 rounded-lg border border-border bg-card p-3"> <label className="flex flex-col gap-1"> <span className={label}>From</span> <input @@ -66,8 +66,8 @@ export function FilterBar({ onClick={() => togglePlatform(p)} className={`rounded-md border px-2 py-1 text-xs capitalize ${ on - ? "border-blue-500 bg-blue-50 text-blue-700 dark:bg-blue-950 dark:text-blue-200" - : "border-zinc-300 dark:border-zinc-700" + ? "border-info/30 bg-info-soft text-info" + : "border-border" }`} > {p} @@ -103,8 +103,8 @@ export function FilterBar({ onClick={() => toggleChannel(c.slug)} className={`rounded-md border px-2 py-1 text-xs ${ on - ? "border-blue-500 bg-blue-50 text-blue-700 dark:bg-blue-950 dark:text-blue-200" - : "border-zinc-300 dark:border-zinc-700" + ? "border-info/30 bg-info-soft text-info" + : "border-border" }`} > {c.name} diff --git a/common/components/charts/SearchChartPanel.tsx b/common/components/charts/SearchChartPanel.tsx @@ -84,7 +84,7 @@ export function SearchChartPanel({ if (error) { return ( - <p className="text-sm text-red-600 dark:text-red-400"> + <p className="text-sm text-destructive"> Failed to load the stats dataset ({error.message}). It may not have been built yet — run the stats build. </p> @@ -92,7 +92,7 @@ export function SearchChartPanel({ } if (!ready) { return ( - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> Loading stats…{" "} {pageCount > 0 ? `(${loadedPages}/${pageCount} pages)` : ""} </p> @@ -100,9 +100,9 @@ export function SearchChartPanel({ } return ( - <div className="flex flex-col gap-2 rounded-lg border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 p-3"> + <div className="flex flex-col gap-2 rounded-lg border border-border bg-card p-3"> <ChartView data={data} config={config} /> - <p className="text-xs text-zinc-500" data-testid="chart-caption"> + <p className="text-xs text-muted-foreground" data-testid="chart-caption"> {charted} {charted === 1 ? "video" : "videos"} {shape.dataSource === "search" ? ` · ${hits} hits` : ""} {loading ? " · searching…" : ""} diff --git a/common/components/themeConfig.ts b/common/components/themeConfig.ts @@ -9,7 +9,7 @@ export const ACCENT_KEY = "ytdlp-tb:accent"; // Theme FAMILY (palette personality) — orthogonal to light/dark MODE. // "base" is the neutral default (no data-theme attribute). Each family is a pure // CSS token swap (a [data-theme="…"] block in tokens.css); no markup differs. -export type ThemeFamily = "base" | "archive" | "terminal" | "swiss"; +export type ThemeFamily = "base" | "archive" | "selenized" | "swiss"; export type ThemeMode = "light" | "dark" | "system"; @@ -17,7 +17,7 @@ export type ThemeMode = "light" | "dark" | "system"; export const THEME_FAMILIES: { id: ThemeFamily; label: string }[] = [ { id: "base", label: "Base" }, { id: "archive", label: "Archive" }, - { id: "terminal", label: "Terminal" }, + { id: "selenized", label: "Selenized" }, { id: "swiss", label: "Swiss" }, ]; diff --git a/common/components/ui/badge.tsx b/common/components/ui/badge.tsx @@ -13,7 +13,7 @@ const badgeVariants = cva( secondary: "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", destructive: - "bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90", + "bg-destructive text-destructive-foreground focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90", outline: "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground", diff --git a/common/components/ui/button.tsx b/common/components/ui/button.tsx @@ -11,7 +11,7 @@ const buttonVariants = cva( variant: { default: "bg-primary text-primary-foreground hover:bg-primary/90", destructive: - "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40", + "bg-destructive text-destructive-foreground hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40", outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", secondary: diff --git a/common/styles/fonts.ts b/common/styles/fonts.ts @@ -1,20 +1,38 @@ // Shared font source of truth for the whole family. One `next/font/google` call -// site, three canonical CSS-variable tokens consumed by tokens.css's @theme: +// site, exposing CSS-variable tokens consumed by tokens.css's @theme: // --font-display (serif: wordmark, headline numerals) // --font-sans (body / UI) // --font-mono (queries, timestamps, IDs — the "power layer") // // Each app's root layout imports `fontVars` and spreads it onto <html>. This // collapses the previous three divergent setups (export=Geist, editor=system, -// homepage=Source) onto one Adobe Source super-family voice. +// homepage=Source) onto one Adobe Source super-family voice — the DEFAULT/base + +// archive voice. +// +// THEME-FAMILY TYPE VOICES: some families reassign --font-display/sans/mono to a +// different loaded face (see tokens.css, `html[data-theme="…"]` blocks) so a +// theme feels distinct beyond color. Those faces are ALSO loaded here (their +// `.variable` classes must be present on <html> for the @font-face to be active, +// which is why every face is joined into `fontVars`), but only referenced when a +// family opts in: +// • swiss → Archivo (a neo-grotesque; International Typographic Style) +// • selenized → JetBrains Mono display+mono over IBM Plex Sans body (terminal) // // `next/font` is transformed by Next's SWC loader across the whole compiled // module graph, and `transpilePackages: ["yt-dlp-transcript-common"]` puts this // module in that graph for every app — so a shared call site works. If a build -// ever balks, the fallback is to duplicate these three calls per layout using -// the same variable names; the CSS token contract is the real invariant. -import { Source_Serif_4, Source_Sans_3, Source_Code_Pro } from "next/font/google"; +// ever balks, the fallback is to duplicate these calls per layout using the same +// variable names; the CSS token contract is the real invariant. +import { + Source_Serif_4, + Source_Sans_3, + Source_Code_Pro, + Archivo, + JetBrains_Mono, + IBM_Plex_Sans, +} from "next/font/google"; +// --- Base / archive voice (the default super-family) ------------------------ export const fontDisplay = Source_Serif_4({ variable: "--font-display", subsets: ["latin"], @@ -31,5 +49,27 @@ export const fontMono = Source_Code_Pro({ subsets: ["latin"], }); -// Space-joined `.variable` classes for <html className={...}>. -export const fontVars = `${fontDisplay.variable} ${fontSans.variable} ${fontMono.variable}`; +// --- Per-family voices (opted into by tokens.css [data-theme] blocks) -------- +// Swiss: a neo-grotesque for both display and body — tight, objective, Helvetica- +// adjacent. Used for --font-display + --font-sans under [data-theme="swiss"]. +export const fontGrotesk = Archivo({ + variable: "--font-grotesk", + subsets: ["latin"], +}); + +// Selenized: a code face for display + mono (the "terminal" voice). +export const fontCode = JetBrains_Mono({ + variable: "--font-code", + subsets: ["latin"], +}); + +// Selenized: a humanist sans body that pairs with the code face. +export const fontPlex = IBM_Plex_Sans({ + variable: "--font-plex", + subsets: ["latin"], + weight: ["400", "500", "600", "700"], +}); + +// Space-joined `.variable` classes for <html className={...}>. EVERY face must be +// here so its @font-face is registered even if only some families reference it. +export const fontVars = `${fontDisplay.variable} ${fontSans.variable} ${fontMono.variable} ${fontGrotesk.variable} ${fontCode.variable} ${fontPlex.variable}`; diff --git a/common/styles/tokens.css b/common/styles/tokens.css @@ -67,6 +67,15 @@ --color-brand: var(--brand); --color-brand-strong: var(--brand-strong); --color-brand-soft: var(--brand-soft); + --color-brand-ink: var(--brand-ink); + + /* Extra family surfaces/lines (previously only some families defined these and + none were exposed as utilities). Now every family sets them, so components + can use `bg-surface`, `border-border-strong`, `text-faint`, `text-brand-ink` + etc. and stay theme-aware. */ + --color-surface: var(--surface); + --color-border-strong: var(--border-strong); + --color-faint: var(--faint); /* Charts */ --color-chart-1: var(--chart-1); @@ -88,6 +97,26 @@ } /* --------------------------------------------------------------------------- + Per-family NON-COLOR axes — corner radius + type voice. Colour is the main way + families differ; these add a second dimension so Swiss/Selenized don't just + feel like recoloured Archive. Written with `html[data-theme="…"]` (specificity + 0,1,1) so the --font-* reassignments reliably beat next/font's `.variable` + class, which sets --font-display/sans/mono on <html> at specificity 0,1,0. + Base + archive keep the default soft radius and the Source super-family voice. + --------------------------------------------------------------------------- */ +html[data-theme="selenized"] { + --radius: 0.375rem; /* calm, modest rounding — a code editor's corners */ + --font-display: var(--font-code); /* JetBrains Mono wordmark/headings */ + --font-sans: var(--font-plex); /* IBM Plex Sans body */ + --font-mono: var(--font-code); +} +html[data-theme="swiss"] { + --radius: 0px; /* hard 90° corners — International Typographic Style */ + --font-display: var(--font-grotesk); /* Archivo neo-grotesque */ + --font-sans: var(--font-grotesk); +} + +/* --------------------------------------------------------------------------- BASE family (default — no data-theme). Neutral zinc; blue brand accent. Reproduces the prior editor/export look so the kit lands at visual parity. Chart axis/grid/surface/tooltip tokens live here too (they previously existed @@ -114,8 +143,11 @@ --destructive-foreground: #fafafa; --destructive-soft: rgba(220, 38, 38, 0.12); --border: #e4e4e7; + --border-strong: #d4d4d8; --input: #e4e4e7; --ring: #a1a1aa; + --surface: #f7f7f8; + --faint: #a1a1aa; --success: #15803d; --success-foreground: #ffffff; @@ -130,6 +162,7 @@ --brand: #2563eb; --brand-strong: #1d4ed8; --brand-soft: rgba(37, 99, 235, 0.12); + --brand-ink: #ffffff; --chart-1: #2563eb; --chart-2: #16a34a; @@ -161,8 +194,11 @@ --destructive-foreground: #fafafa; --destructive-soft: rgba(248, 113, 113, 0.16); --border: #27272a; + --border-strong: #3f3f46; --input: rgba(255, 255, 255, 0.12); --ring: #71717a; + --surface: #141416; + --faint: #52525b; --success: #4ade80; --success-foreground: #052e16; @@ -177,6 +213,7 @@ --brand: #60a5fa; --brand-strong: #93c5fd; --brand-soft: rgba(96, 165, 250, 0.16); + --brand-ink: #04122e; --chart-1: #60a5fa; --chart-2: #4ade80; @@ -302,113 +339,118 @@ } /* --------------------------------------------------------------------------- - TERMINAL family — an "ice-cyan signal" terminal. Cyan brand on blue-black - (dark) / cool near-white (light). Pure token swap — same markup, recolored. + SELENIZED family — the Solarized successor (Jan Warchoł's "selenized"). The + signature warm-tan ground (#fbf3db) / deep teal-slate ground (#103c48) with a + cyan brand and the calibrated Selenized accent set. A distinct, unmistakable + color identity; the terminal/code voice (JetBrains Mono + IBM Plex Sans) is + applied via the per-family type block near the top of this file. --------------------------------------------------------------------------- */ -[data-theme="terminal"] { - /* Light terminal — cyan ink on a cool near-white. */ - --background: #f2f7f9; - --surface: #e7f0f3; - --foreground: #07242c; - --card: #ffffff; - --card-foreground: #07242c; - --popover: #ffffff; - --popover-foreground: #07242c; - --primary: #0891b2; - --primary-foreground: #ecfeff; - --secondary: #d7eaef; - --secondary-foreground: #07242c; - --muted: #e1eef1; - --muted-foreground: #466a72; - --accent: #d7eaef; - --accent-foreground: #07242c; - --destructive: #be123c; - --destructive-foreground: #ecfeff; - --border: rgba(7, 36, 44, 0.12); - --border-strong: rgba(7, 36, 44, 0.22); - --input: rgba(7, 36, 44, 0.16); - --ring: #0891b2; - --faint: #6b8a91; - --panel: rgba(255, 255, 255, 0.7); - --panel-2: rgba(231, 240, 243, 0.7); - - --success: #047857; - --success-foreground: #ecfeff; - --success-soft: rgba(4, 120, 87, 0.14); - --warning: #b45309; - --warning-foreground: #ecfeff; - --warning-soft: rgba(217, 119, 6, 0.14); - --info: #0369a1; - --info-foreground: #ecfeff; - --info-soft: rgba(2, 132, 199, 0.14); - - --brand: #0891b2; - --brand-strong: #0e7490; - --brand-soft: rgba(8, 145, 178, 0.14); - --brand-ink: #ecfeff; - - --chart-1: #0891b2; - --chart-2: #0d9488; - --chart-3: #0284c7; - --chart-4: #4f46e5; - --chart-5: #059669; - --chart-surface: #ffffff; - --chart-grid: rgba(7, 36, 44, 0.08); - --chart-axis: #5f828a; - --chart-tooltip-bg: #ffffff; +[data-theme="selenized"] { + /* Selenized light — teal-slate ink on the signature warm tan. */ + --background: #fbf3db; + --surface: #ece3cc; + --foreground: #3a4d53; + --card: #fefbf0; + --card-foreground: #3a4d53; + --popover: #fefbf0; + --popover-foreground: #3a4d53; + --primary: #009c8f; + --primary-foreground: #fbf3db; + --secondary: #ece3cc; + --secondary-foreground: #3a4d53; + --muted: #ece3cc; + --muted-foreground: #63757a; + --accent: #d5cdb6; + --accent-foreground: #3a4d53; + --destructive: #d2212d; + --destructive-foreground: #fbf3db; + --destructive-soft: rgba(210, 33, 45, 0.12); + --border: rgba(58, 77, 83, 0.14); + --border-strong: rgba(58, 77, 83, 0.28); + --input: rgba(58, 77, 83, 0.18); + --ring: #009c8f; + --faint: #909995; + --panel: rgba(254, 251, 240, 0.7); + --panel-2: rgba(236, 227, 204, 0.7); + + --success: #489100; + --success-foreground: #fbf3db; + --success-soft: rgba(72, 145, 0, 0.14); + --warning: #ad8900; + --warning-foreground: #fbf3db; + --warning-soft: rgba(173, 137, 0, 0.14); + --info: #0072d4; + --info-foreground: #fbf3db; + --info-soft: rgba(0, 114, 212, 0.12); + + --brand: #009c8f; + --brand-strong: #00887c; + --brand-soft: rgba(0, 156, 143, 0.14); + --brand-ink: #fbf3db; + + --chart-1: #009c8f; + --chart-2: #489100; + --chart-3: #0072d4; + --chart-4: #8762c6; + --chart-5: #c25d1e; + --chart-surface: #fefbf0; + --chart-grid: rgba(58, 77, 83, 0.1); + --chart-axis: #63757a; + --chart-tooltip-bg: #fefbf0; } -[data-theme="terminal"].dark { - /* Dark terminal — phosphor cyan on blue-black. */ - --background: #080f12; - --surface: #0c161b; - --foreground: #cdeef5; - --card: #0c161b; - --card-foreground: #cdeef5; - --popover: #0f1d23; - --popover-foreground: #cdeef5; - --primary: #22d3ee; - --primary-foreground: #04141a; - --secondary: #122b33; - --secondary-foreground: #cdeef5; - --muted: #0f2129; - --muted-foreground: #7fb3c0; - --accent: #102329; - --accent-foreground: #cdeef5; - --destructive: #fb7185; - --destructive-foreground: #04141a; - --border: rgba(140, 220, 235, 0.12); - --border-strong: rgba(140, 220, 235, 0.24); - --input: rgba(140, 220, 235, 0.16); - --ring: #22d3ee; - --faint: #5a8a96; - --panel: rgba(12, 26, 33, 0.6); - --panel-2: rgba(18, 38, 46, 0.5); - - --success: #34d399; - --success-foreground: #04141a; - --success-soft: rgba(52, 211, 153, 0.16); - --warning: #fbbf24; - --warning-foreground: #04141a; - --warning-soft: rgba(251, 191, 36, 0.16); - --info: #38bdf8; - --info-foreground: #04141a; - --info-soft: rgba(56, 189, 248, 0.16); - - --brand: #22d3ee; - --brand-strong: #67e8f9; - --brand-soft: rgba(34, 211, 238, 0.16); - --brand-ink: #04141a; - - --chart-1: #22d3ee; - --chart-2: #2dd4bf; - --chart-3: #38bdf8; - --chart-4: #818cf8; - --chart-5: #34d399; - --chart-surface: #0c161b; - --chart-grid: rgba(140, 220, 235, 0.1); - --chart-axis: #6f9aa6; - --chart-tooltip-bg: #0f1d23; +[data-theme="selenized"].dark { + /* Selenized dark — bright cyan/teal on the deep teal-slate ground. */ + --background: #103c48; + --surface: #0e3640; + --foreground: #adbcbc; + --card: #184956; + --card-foreground: #cad8d9; + --popover: #1c5162; + --popover-foreground: #cad8d9; + --primary: #41c7b9; + --primary-foreground: #103c48; + --secondary: #2d5b69; + --secondary-foreground: #cad8d9; + --muted: #184956; + --muted-foreground: #72898f; + --accent: #2d5b69; + --accent-foreground: #cad8d9; + --destructive: #ff665c; + --destructive-foreground: #103c48; + --destructive-soft: rgba(250, 87, 80, 0.16); + --border: rgba(202, 216, 217, 0.12); + --border-strong: rgba(202, 216, 217, 0.24); + --input: rgba(202, 216, 217, 0.16); + --ring: #41c7b9; + --faint: #5c7278; + --panel: rgba(24, 73, 86, 0.6); + --panel-2: rgba(45, 91, 105, 0.5); + + --success: #84c747; + --success-foreground: #0e2f14; + --success-soft: rgba(132, 199, 71, 0.16); + --warning: #ebc13d; + --warning-foreground: #103c48; + --warning-soft: rgba(235, 193, 61, 0.16); + --info: #58a3ff; + --info-foreground: #041022; + --info-soft: rgba(88, 163, 255, 0.16); + + --brand: #41c7b9; + --brand-strong: #53d6c7; + --brand-soft: rgba(65, 199, 185, 0.16); + --brand-ink: #103c48; + + --chart-1: #41c7b9; + --chart-2: #84c747; + --chart-3: #58a3ff; + --chart-4: #bd96fa; + --chart-5: #fd9456; + --chart-surface: #184956; + --chart-grid: rgba(202, 216, 217, 0.1); + --chart-axis: #72898f; + --chart-tooltip-bg: #1c5162; } /* --------------------------------------------------------------------------- diff --git a/editor/CHANGELOG.md b/editor/CHANGELOG.md @@ -5,7 +5,8 @@ - **Badges and alerts are now colorful and theme-aware in every theme.** The design system only tokenized red (`--destructive`); every green/amber/blue status was hardcoded Tailwind palette that ignored the four theme families. New semantic tokens — **`--success` / `--warning` / `--info`** (each with `-foreground` + a soft fill partner) plus `--destructive-soft` — are defined across all eight family blocks (base/archive/terminal/swiss × light/dark) and registered in `@theme inline`. The shared kit `Badge` gains `success` / `warning` / `info` / `brand` soft-filled variants, and a **new `Alert` component** (`common/components/ui/alert.tsx`) replaces ad-hoc notice divs. High-traffic status UI is migrated onto the tokens: channel stage badges (`StageBadge`), worker state badges + dots (`WorkersView`, `MonitorWidget`), the scheduler status column (`SchedulerView`), and the widget disk strip — so each recolors correctly in Archive, Terminal, and Swiss, light and dark. See `common/styles/tokens.css`, `common/components/ui/{badge,alert}.tsx`. - **Per-video "Exclude from truncated check".** Videos that legitimately have no speech for their back half (e.g. long ambient/music tails) kept getting false-flagged as truncated/incomplete transcripts. A new per-video toggle on the video panel writes an `exclude-truncated-check.json` marker (mirroring the `do-not-clean.json` pattern) that suppresses the flag everywhere it surfaces — the snapshot's `incompleteTranscript` + `shortAudio` buckets (so bulk actions, the `/actionable` lists, and channel row dots all skip it) **and** the per-video panel's "looks truncated" banner, which computes independently of the snapshot. The detection thresholds in `transcriptCoverage.ts` are unchanged (other videos unaffected). See `common/lib/excludeTruncatedCheck{,-server}.ts`, `common/controller/channelSnapshot.ts`, `editor/app/channels/[slug]/videos/[id]/{videoActions.ts,components/VideoPanel.tsx,page.tsx}`, and `editor/app/channels/[slug]/page.tsx`. - **Monitor widget: an optional cleanable-data indicator, and the builder remembers your last config.** The widget builder gains a **Show cleanable indicator** option (URL flag `clean=1`) that adds a strip showing total reclaimable audio (polled from a new `/api/widget/cleanable` route, honoring `excludeFromCleanup`), styled like the disk strip. The builder also now **persists its form to localStorage** (`ytdlp-tb:widget-config`, versioned, SSR-guarded — mirroring `jobsFilterStorage`), so it reopens with your last configuration; the shared/embedded `/widget` URL stays authoritative for what actually renders. See `editor/app/widget/{lib/config.ts,builder/{widgetConfigStorage.ts,components/WidgetBuilder.tsx},components/MonitorWidget.tsx}` and `editor/app/api/widget/cleanable/route.ts`. -- **Two more selectable themes + a theme picker.** A palette menu beside the light/dark toggle (in every app — editor, export, homepage) switches the theme family between **Archive**, **Terminal** (ice-cyan), **Swiss** (red/black/white), and **Base**, each in light/dark/system, persisted client-side. Each family is a pure CSS token swap in `common/styles/tokens.css` (a `[data-theme]` block) — no markup changes — wired through `common/components/{themeConfig.ts,ThemeMenu.tsx}`. +- **The whole editor now follows the selected theme (not just light/dark).** Dozens of editor screens — the Active Jobs cards, channel pipeline/stage views, the jobs table, deploy/scheduler/workers panels, settings/sites forms, the widget builder, and more — hardcoded Tailwind palette colors (`bg-white`/`dark:bg-zinc-900`, `text-zinc-500`, red/amber/green/blue status colors) that tracked light/dark but **ignored the theme family**, so they stayed zinc/white in Archive, Selenized, and Swiss. Every one of these (~100 components across `editor/app` + shared `common/components`) is migrated onto the semantic tokens (`bg-card`, `border-border`, `text-muted-foreground`, `text-foreground`, and `destructive`/`warning`/`success`/`info` for status), so they recolor correctly in every family. The one deliberate exception is the audio-probe "scanning" fill (violet) and chart data-series colors, which are intentional encodings. +- **Two more selectable themes + a theme picker, and the families now feel distinct.** A palette menu beside the light/dark toggle (in every app — editor, export, homepage) switches the theme family between **Archive** (warm reading room), **Selenized** (the Solarized successor — teal-slate/warm-tan), **Swiss** (red/black/white editorial), and **Base** (neutral), each in light/dark/system, persisted client-side. Beyond color, families now also carry their own **corner radius and type voice**: Swiss is hard-cornered (0px) in a neo-grotesque (Archivo), Selenized uses calm rounding with a code voice (JetBrains Mono display + IBM Plex Sans body), Archive/Base keep soft corners and the Source serif family. The token vocabulary also grew — `--surface`, `--border-strong`, `--faint`, `--brand-ink` are now defined for **every** family and exposed as utilities (`bg-surface`, `border-border-strong`, `text-faint`, `text-brand-ink`), so more chrome can be themed. Each family is still a pure CSS token swap in `common/styles/tokens.css` — no markup changes — wired through `common/components/{themeConfig.ts,ThemeMenu.tsx}` and `common/styles/fonts.ts`. (A previously-selected "Terminal" family migrates automatically to "Selenized".) - **Per-site brand accent.** Each site's config (under Sites) gains an optional **Brand accent** field — a hex color that overrides the family brass on that site's public build. Validated on save; blank inherits the family accent. The long-running action logs (download, transcribe, build, deploy, …) are also restyled onto the shared kit (Button + design tokens), keeping their exact accessible labels. See `common/lib/accent.ts`, `common/lib/site.ts`, `common/components/StreamActionLog.tsx`, `editor/app/sites/{components/SiteForm.tsx,actions.ts}`, and `export/app/layout.tsx`. - **Command-first cockpit: a ⌘K command palette that runs actions, plus a live dashboard.** Press **⌘K / Ctrl-K** anywhere to open a fast command palette (rebuilt on cmdk) that jumps to any page **and runs pool/site actions inline** — Sync all channels, Refresh all reports, Detect duplicate shorts, Retry all failed jobs, Drain the queue, Pause/Resume all workers — each firing the server action and reporting the result as a **toast** (Sonner). On a channel page it also offers the channel's status filters. The sidebar and palette now read from one shared nav source (`editor/app/lib/nav.ts`) so they never drift (the old palette listed a stale 5-route subset), and every nav item gains an icon. The **Dashboard** gains a **Live jobs** panel that polls running jobs in real time (reusing the active-jobs monitor), and the whole editor shell is restyled compact onto the shared design tokens/kit with the Source type family. See `editor/app/{layout.tsx,page.tsx,lib/nav.ts,components/{CommandPalette.tsx,ChangelogNavLink.tsx}}` and `common/components/ui/sonner.tsx`. - **New "Family hub URL" setting.** Settings gains an optional hub URL (e.g. `https://archilyzer.com`); every export site renders a link back to it in the header/footer, tying the family of public sites together. Leave it blank for no hub link. Normalized to an absolute http(s) URL on save (`settings.homepageUrl`; see `common/lib/settings.ts`, `editor/app/settings/{components/SettingsForm.tsx,actions.ts}`). Part of the family-wide UI redesign that also restyles the public export sites. diff --git a/editor/app/actionable/components/FixAllIncompleteButton.tsx b/editor/app/actionable/components/FixAllIncompleteButton.tsx @@ -48,7 +48,7 @@ export function FixAllIncompleteButton() { onClick={() => run("redownload")} disabled={running} aria-label="re-download all incomplete transcripts" - className="px-2.5 py-1 rounded-md bg-zinc-900 dark:bg-zinc-100 text-zinc-100 dark:text-zinc-900 text-xs font-medium hover:opacity-90 disabled:opacity-50 whitespace-nowrap" + className="px-2.5 py-1 rounded-md bg-primary text-primary-foreground text-xs font-medium hover:opacity-90 disabled:opacity-50 whitespace-nowrap" > {status.kind === "running" && status.mode === "redownload" ? "Queuing…" @@ -59,7 +59,7 @@ export function FixAllIncompleteButton() { onClick={() => run("clear")} disabled={running} aria-label="clear all incomplete transcripts" - className="px-2.5 py-1 rounded-md border border-zinc-300 dark:border-zinc-700 text-xs font-medium hover:bg-zinc-100 dark:hover:bg-zinc-800 disabled:opacity-50 whitespace-nowrap" + className="px-2.5 py-1 rounded-md border border-border text-xs font-medium hover:bg-muted disabled:opacity-50 whitespace-nowrap" > {status.kind === "running" && status.mode === "clear" ? "Clearing…" @@ -68,7 +68,7 @@ export function FixAllIncompleteButton() { {status.kind === "done" && ( <span aria-label="fix all incomplete transcripts result" - className="text-xs text-zinc-500" + className="text-xs text-muted-foreground" > {status.result.ok ? ( <> @@ -77,7 +77,7 @@ export function FixAllIncompleteButton() { {status.result.channels === 1 ? "" : "s"} ·{" "} <Link href="/jobs" - className="underline hover:text-zinc-900 dark:hover:text-zinc-100" + className="underline hover:text-foreground" > view jobs </Link> @@ -91,7 +91,7 @@ export function FixAllIncompleteButton() { <span role="alert" aria-label="fix all incomplete transcripts error" - className="text-xs text-red-700 dark:text-red-400" + className="text-xs text-destructive" > {status.message} </span> diff --git a/editor/app/actionable/components/InlineActionButton.tsx b/editor/app/actionable/components/InlineActionButton.tsx @@ -149,14 +149,14 @@ export function InlineActionButton({ onClick={handleClick} disabled={running} aria-label={ariaLabel} - className="px-2 py-1 rounded-md bg-zinc-900 dark:bg-zinc-100 text-zinc-100 dark:text-zinc-900 text-xs font-medium hover:opacity-90 disabled:opacity-50 whitespace-nowrap" + className="px-2 py-1 rounded-md bg-primary text-primary-foreground text-xs font-medium hover:opacity-90 disabled:opacity-50 whitespace-nowrap" > {running ? labels.running : labels.idle} </button> {status.kind === "queued" && ( <span aria-label={`${ariaLabel} job`} - className="text-xs text-zinc-500 font-mono" + className="text-xs text-muted-foreground font-mono" > job {status.jobId} </span> @@ -164,7 +164,7 @@ export function InlineActionButton({ {status.kind === "done" && ( <span aria-label={`${ariaLabel} done`} - className="text-xs text-zinc-500" + className="text-xs text-muted-foreground" > {status.message ?? "done"} </span> @@ -173,7 +173,7 @@ export function InlineActionButton({ <span role="alert" aria-label={`${ariaLabel} error`} - className="text-xs text-red-700 dark:text-red-400" + className="text-xs text-destructive" > {status.message} </span> diff --git a/editor/app/actionable/components/RefreshAllReportsButton.tsx b/editor/app/actionable/components/RefreshAllReportsButton.tsx @@ -34,14 +34,14 @@ export function RefreshAllReportsButton() { onClick={handleClick} disabled={running} aria-label="update all reports" - className="px-3 py-2 rounded-md bg-zinc-900 dark:bg-zinc-100 text-zinc-100 dark:text-zinc-900 text-sm font-medium hover:opacity-90 disabled:opacity-50" + className="px-3 py-2 rounded-md bg-primary text-primary-foreground text-sm font-medium hover:opacity-90 disabled:opacity-50" > {running ? "Updating…" : "Update all reports"} </button> {status.kind === "done" && ( <span aria-label="update all reports result" - className="text-xs text-zinc-500" + className="text-xs text-muted-foreground" title={ status.result.skipped.length === 0 ? undefined @@ -54,7 +54,7 @@ export function RefreshAllReportsButton() { {status.result.skipped.length} ·{" "} <Link href="/jobs" - className="underline hover:text-zinc-900 dark:hover:text-zinc-100" + className="underline hover:text-foreground" > view jobs </Link> @@ -64,7 +64,7 @@ export function RefreshAllReportsButton() { <span role="alert" aria-label="update all reports error" - className="text-xs text-red-700 dark:text-red-400" + className="text-xs text-destructive" > {status.message} </span> diff --git a/editor/app/actionable/components/RunDuplicateDetectionButton.tsx b/editor/app/actionable/components/RunDuplicateDetectionButton.tsx @@ -39,7 +39,7 @@ export function RunDuplicateDetectionButton() { value={scope} onChange={(e) => setScope(e.target.value as DuplicateScope)} disabled={running} - className="text-sm rounded-md border border-zinc-300 dark:border-zinc-700 bg-transparent px-2 py-2" + className="text-sm rounded-md border border-border bg-transparent px-2 py-2" > <option value="shorts">Shorts only</option> <option value="all">All durations</option> @@ -49,14 +49,14 @@ export function RunDuplicateDetectionButton() { onClick={handleClick} disabled={running} aria-label="detect duplicate shorts" - className="px-3 py-2 rounded-md bg-zinc-900 dark:bg-zinc-100 text-zinc-100 dark:text-zinc-900 text-sm font-medium hover:opacity-90 disabled:opacity-50" + className="px-3 py-2 rounded-md bg-primary text-primary-foreground text-sm font-medium hover:opacity-90 disabled:opacity-50" > {running ? "Detecting…" : "Detect duplicates"} </button> {status.kind === "done" && status.result.ok && ( <span aria-label="detect duplicate shorts result" - className="text-xs text-zinc-500" + className="text-xs text-muted-foreground" > {status.result.clusters} cluster(s) ·{" "} {status.result.videosInClusters} video(s) @@ -66,7 +66,7 @@ export function RunDuplicateDetectionButton() { <span role="alert" aria-label="detect duplicate shorts error" - className="text-xs text-red-700 dark:text-red-400" + className="text-xs text-destructive" > {status.result.error} </span> @@ -75,7 +75,7 @@ export function RunDuplicateDetectionButton() { <span role="alert" aria-label="detect duplicate shorts error" - className="text-xs text-red-700 dark:text-red-400" + className="text-xs text-destructive" > {status.message} </span> diff --git a/editor/app/actionable/page.tsx b/editor/app/actionable/page.tsx @@ -117,7 +117,7 @@ export default async function ActionablePage() { <Link href={`/channels/${r.channel.slug}?filter=incomplete_transcript`} aria-label={`review incomplete transcripts for ${r.channel.slug}`} - className="inline-flex items-center px-2.5 py-1 rounded-md border border-zinc-300 dark:border-zinc-700 text-xs font-medium hover:bg-zinc-100 dark:hover:bg-zinc-800 whitespace-nowrap" + className="inline-flex items-center px-2.5 py-1 rounded-md border border-border text-xs font-medium hover:bg-muted whitespace-nowrap" > Review </Link> @@ -143,7 +143,7 @@ export default async function ActionablePage() { <Link href={`/channels/${r.channel.slug}?filter=short_audio`} aria-label={`review short-audio downloads for ${r.channel.slug}`} - className="inline-flex items-center px-2.5 py-1 rounded-md border border-zinc-300 dark:border-zinc-700 text-xs font-medium hover:bg-zinc-100 dark:hover:bg-zinc-800 whitespace-nowrap" + className="inline-flex items-center px-2.5 py-1 rounded-md border border-border text-xs font-medium hover:bg-muted whitespace-nowrap" > Review </Link> @@ -223,7 +223,7 @@ export default async function ActionablePage() { {nothingPending ? ( <p aria-label="actionable items empty" - className="text-sm text-zinc-500 border border-dashed border-zinc-300 dark:border-zinc-700 rounded p-4" + className="text-sm text-muted-foreground border border-dashed border-border rounded p-4" > Everything is up to date. </p> @@ -245,7 +245,7 @@ function DuplicatesSection({ report }: { report: DuplicateReport | null }) { <h2 className="text-lg font-semibold">Duplicate shorts</h2> <RunDuplicateDetectionButton /> </div> - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> Cross-platform, cross-channel duplicate detection (metadata pre-cluster → transcript comparison). Flag-only — review and act manually. {report ? ( @@ -268,7 +268,7 @@ function DuplicatesSection({ report }: { report: DuplicateReport | null }) { {clusters.length === 0 ? ( <p aria-label="duplicate-shorts empty" - className="text-sm text-zinc-500 border border-dashed border-zinc-300 dark:border-zinc-700 rounded p-4" + className="text-sm text-muted-foreground border border-dashed border-border rounded p-4" > {report ? "No duplicate clusters found." : "Run detection to scan."} </p> @@ -291,7 +291,7 @@ function DuplicateClusterCard({ cluster }: { cluster: DuplicateCluster }) { return ( <li aria-label={`duplicate cluster ${cluster.clusterId}`} - className="border border-zinc-200 dark:border-zinc-800 rounded-md p-3 flex flex-col gap-2" + className="border border-border rounded-md p-3 flex flex-col gap-2" > <div className="flex items-center gap-2 flex-wrap text-xs"> <Badge>{matchLabel[cluster.matchKind]}</Badge> @@ -301,7 +301,7 @@ function DuplicateClusterCard({ cluster }: { cluster: DuplicateCluster }) { {cluster.contained && <Badge>clip-of-longer</Badge>} {cluster.crossPlatform && <Badge>cross-platform</Badge>} {cluster.crossChannel && <Badge>cross-channel</Badge>} - <span className="text-zinc-500"> + <span className="text-muted-foreground"> {cluster.videoRefs.length} videos · ~{cluster.durationBucket}s </span> </div> @@ -310,11 +310,11 @@ function DuplicateClusterCard({ cluster }: { cluster: DuplicateCluster }) { <li key={ref.slug} className="text-sm flex items-baseline gap-2 flex-wrap"> <Link href={`/channels/${ref.channelSlug}/videos/${ref.id}`} - className="underline hover:text-zinc-900 dark:hover:text-zinc-100" + className="underline hover:text-foreground" > {ref.title || ref.slug} </Link> - <span className="text-xs text-zinc-500 font-mono"> + <span className="text-xs text-muted-foreground font-mono"> {ref.platform} · {ref.channelSlug} · {ref.uploadDate} {ref.hasTranscript ? "" : " · no transcript"} </span> @@ -327,7 +327,7 @@ function DuplicateClusterCard({ cluster }: { cluster: DuplicateCluster }) { function Badge({ children }: { children: React.ReactNode }) { return ( - <span className="inline-flex items-center rounded-full bg-zinc-100 dark:bg-zinc-800 px-2 py-0.5 text-zinc-700 dark:text-zinc-300"> + <span className="inline-flex items-center rounded-full bg-muted px-2 py-0.5 text-muted-foreground"> {children} </span> ); @@ -349,18 +349,18 @@ function Section({ <h2 className="text-lg font-semibold">{config.title}</h2> {config.headerAction} </div> - <p className="text-sm text-zinc-500">{config.description}</p> + <p className="text-sm text-muted-foreground">{config.description}</p> {rows.length === 0 ? ( <p aria-label={`${config.id} empty`} - className="text-sm text-zinc-500 border border-dashed border-zinc-300 dark:border-zinc-700 rounded p-4" + className="text-sm text-muted-foreground border border-dashed border-border rounded p-4" > {config.emptyLabel} </p> ) : ( <div className="overflow-x-auto -mx-4 md:mx-0 md:overflow-visible"> - <table className="text-sm border-y md:border md:border-zinc-200 dark:md:border-zinc-800 border-zinc-200 dark:border-zinc-800 md:rounded-md md:overflow-hidden w-full"> - <thead className="bg-zinc-100 dark:bg-zinc-900"> + <table className="text-sm border-y md:border md:border-border border-border md:rounded-md md:overflow-hidden w-full"> + <thead className="bg-muted"> <tr> <th className="text-left font-medium px-3 py-2">Slug</th> <th className="text-right font-medium px-3 py-2"> @@ -422,19 +422,19 @@ function Row({ return ( <tr aria-label={`${sectionId} row ${channel.slug}`} - className="border-t border-zinc-200 dark:border-zinc-800" + className="border-t border-border" > <td className="px-3 py-2 font-mono"> <Link href={`/channels/${channel.slug}`} - className="underline hover:text-zinc-900 dark:hover:text-zinc-100" + className="underline hover:text-foreground" > {channel.slug} </Link> </td> <td className="px-3 py-2 text-right tabular-nums"> {sectionId === "stale-reports" ? ( - <span className="text-amber-700 dark:text-amber-400"> + <span className="text-warning"> {snapshot ? "stale" : "missing"} </span> ) : ( @@ -442,15 +442,15 @@ function Row({ )} </td> {extraValue !== null && ( - <td className="px-3 py-2 text-right tabular-nums whitespace-nowrap text-zinc-600 dark:text-zinc-400"> + <td className="px-3 py-2 text-right tabular-nums whitespace-nowrap text-muted-foreground"> {extraValue} </td> )} <td className={`px-3 py-2 text-xs whitespace-nowrap ${ isStale - ? "text-amber-700 dark:text-amber-400" - : "text-zinc-500" + ? "text-warning" + : "text-muted-foreground" }`} > {lastReport ? ( @@ -461,7 +461,7 @@ function Row({ "never" )} </td> - <td className="px-3 py-2 text-xs text-zinc-500 whitespace-nowrap"> + <td className="px-3 py-2 text-xs text-muted-foreground whitespace-nowrap"> {lastSync ? new Date(lastSync).toLocaleString() : "never"} </td> <td className="px-3 py-2 text-right"> diff --git a/editor/app/auto-queue/components/AutoQueueView.tsx b/editor/app/auto-queue/components/AutoQueueView.tsx @@ -124,13 +124,13 @@ function KindPanel({ <span className={`text-sm rounded-full px-3 py-1 border ${ running - ? "border-green-300 bg-green-50 text-green-800 dark:border-green-800 dark:bg-green-950 dark:text-green-200" - : "border-zinc-300 bg-zinc-50 text-zinc-600 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-400" + ? "border-success/30 bg-success-soft text-success" + : "border-border bg-card text-muted-foreground" }`} > {running ? "Runner running" : "Runner stopped"} </span> - <span className="text-sm text-zinc-500"> + <span className="text-sm text-muted-foreground"> {status.runner.inFlight.length} in flight · {totalPending} pending </span> <span className="ml-auto flex gap-2"> @@ -139,7 +139,7 @@ function KindPanel({ aria-label={`Start ${title}`} onClick={() => control("start")} disabled={busy || running} - className="px-3 py-1.5 rounded-md bg-zinc-900 dark:bg-zinc-100 text-zinc-100 dark:text-zinc-900 text-sm font-medium hover:opacity-90 disabled:opacity-50" + className="px-3 py-1.5 rounded-md bg-primary text-primary-foreground text-sm font-medium hover:opacity-90 disabled:opacity-50" > Start </button> @@ -149,7 +149,7 @@ function KindPanel({ title="Finish the in-flight items, start no new ones, then stop" onClick={() => control("drain")} disabled={busy || !running} - className="px-3 py-1.5 rounded-md border border-amber-300 dark:border-amber-800 text-sm font-medium text-amber-700 dark:text-amber-300 hover:bg-amber-50 dark:hover:bg-amber-950 disabled:opacity-50" + className="px-3 py-1.5 rounded-md border border-warning/30 text-sm font-medium text-warning hover:bg-warning-soft disabled:opacity-50" > Drain </button> @@ -159,7 +159,7 @@ function KindPanel({ title="Stop now, aborting any in-flight item" onClick={() => control("stop")} disabled={busy || !running} - className="px-3 py-1.5 rounded-md border border-zinc-300 dark:border-zinc-700 text-sm font-medium hover:bg-zinc-100 dark:hover:bg-zinc-800 disabled:opacity-50" + className="px-3 py-1.5 rounded-md border border-border text-sm font-medium hover:bg-muted disabled:opacity-50" > Stop </button> @@ -167,7 +167,7 @@ function KindPanel({ </div> {kind === "transcription" && ( - <p className="text-xs text-zinc-500"> + <p className="text-xs text-muted-foreground"> Auto-transcribe runs at background priority: a manually-triggered transcription preempts it for the next free worker — the in-flight auto transcription finishes (drains), then the manual one runs, then auto @@ -193,13 +193,13 @@ function KindPanel({ <div className="flex flex-col gap-2"> <h3 className="text-sm font-semibold">Pending per rule</h3> {Object.keys(status.pendingByLeaf).length === 0 ? ( - <p className="text-sm text-zinc-500">No rules configured.</p> + <p className="text-sm text-muted-foreground">No rules configured.</p> ) : ( <ul className="flex flex-col gap-1 text-sm"> {Object.entries(status.pendingByLeaf).map(([leafId, count]) => ( <li key={leafId} - className="flex justify-between gap-3 text-zinc-600 dark:text-zinc-300" + className="flex justify-between gap-3 text-muted-foreground" > <span>{leafName(leafId)}</span> <span className="tabular-nums">{count}</span> @@ -212,21 +212,21 @@ function KindPanel({ <div className="flex flex-col gap-2"> <h3 className="text-sm font-semibold">Recent picks</h3> {status.picks.length === 0 ? ( - <p className="text-sm text-zinc-500">Nothing picked yet.</p> + <p className="text-sm text-muted-foreground">Nothing picked yet.</p> ) : ( <ul className="flex flex-col gap-1 text-sm"> {status.picks.slice(0, 15).map((p, i) => ( <li key={`${p.at}-${i}`} - className="flex flex-wrap gap-x-3 text-zinc-600 dark:text-zinc-300" + className="flex flex-wrap gap-x-3 text-muted-foreground" > - <span className="tabular-nums text-zinc-500"> + <span className="tabular-nums text-muted-foreground"> {formatClock(p.at)} </span> <span> {p.channelSlug}/{p.videoId} </span> - <span className="text-zinc-400">{leafName(p.leafId)}</span> + <span className="text-muted-foreground">{leafName(p.leafId)}</span> </li> ))} </ul> @@ -290,11 +290,11 @@ function useNow(): number | null { function CooldownBanner({ cooldowns }: { cooldowns: PlatformCooldownView[] }) { const now = useNow(); return ( - <div className="flex flex-col gap-1 rounded-md border border-amber-300 bg-amber-50 px-3 py-2 text-sm dark:border-amber-800 dark:bg-amber-950"> - <span className="font-medium text-amber-800 dark:text-amber-200"> + <div className="flex flex-col gap-1 rounded-md border border-warning/30 bg-warning-soft px-3 py-2 text-sm"> + <span className="font-medium text-warning"> Rate-limit cooldown — auto-download and manual sync are paused on: </span> - <ul className="flex flex-wrap gap-x-4 gap-y-1 text-amber-700 dark:text-amber-300"> + <ul className="flex flex-wrap gap-x-4 gap-y-1 text-warning"> {cooldowns.map((c) => { const secs = now === null diff --git a/editor/app/auto-queue/components/PolicyTreeEditor.tsx b/editor/app/auto-queue/components/PolicyTreeEditor.tsx @@ -19,9 +19,9 @@ import { saveAutoQueueAction, type SaveResult } from "../actions"; // (writeSettings re-sanitizes it server-side). const inputClass = - "rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm"; + "rounded border border-border bg-card px-2 py-1 text-sm"; const btnClass = - "px-2 py-0.5 rounded border border-zinc-300 dark:border-zinc-700 text-xs hover:bg-zinc-100 dark:hover:bg-zinc-800"; + "px-2 py-0.5 rounded border border-border text-xs hover:bg-muted"; let idSeq = 0; function newId(): string { @@ -148,7 +148,7 @@ export function PolicyTreeEditor({ }; return ( - <div className="flex flex-col gap-3 border border-zinc-200 dark:border-zinc-800 rounded p-3"> + <div className="flex flex-col gap-3 border border-border rounded p-3"> <div className="flex flex-wrap items-center gap-4"> <label className="flex items-center gap-2 text-sm font-medium"> <input @@ -198,17 +198,17 @@ export function PolicyTreeEditor({ type="button" onClick={save} disabled={saving} - className="px-3 py-1.5 rounded-md bg-zinc-900 dark:bg-zinc-100 text-zinc-100 dark:text-zinc-900 text-sm font-medium hover:opacity-90 disabled:opacity-50" + className="px-3 py-1.5 rounded-md bg-primary text-primary-foreground text-sm font-medium hover:opacity-90 disabled:opacity-50" > {saving ? "Saving…" : "Save policy"} </button> {result?.ok === true && ( - <span role="status" className="text-sm text-green-700 dark:text-green-300"> + <span role="status" className="text-sm text-success"> Saved. </span> )} {result?.ok === false && ( - <span role="alert" className="text-sm text-red-700 dark:text-red-300"> + <span role="alert" className="text-sm text-destructive"> {result.error} </span> )} @@ -238,11 +238,11 @@ function NodeEditor(props: EditorProps) { const group = isGroup(node); return ( <div - className="flex flex-col gap-2 rounded border border-zinc-200 dark:border-zinc-800 p-2" + className="flex flex-col gap-2 rounded border border-border p-2" style={{ marginLeft: depth > 0 ? 12 : 0 }} > <div className="flex flex-wrap items-center gap-2"> - <span className="text-xs font-semibold text-zinc-500"> + <span className="text-xs font-semibold text-muted-foreground"> {group ? "Group" : "Rule"} </span> @@ -254,7 +254,7 @@ function NodeEditor(props: EditorProps) { {/* weight, shown when the PARENT is weighted-fair */} {parentMode === "weighted-fair" && ( - <label className="flex items-center gap-1 text-xs text-zinc-500"> + <label className="flex items-center gap-1 text-xs text-muted-foreground"> weight <input type="number" @@ -271,7 +271,7 @@ function NodeEditor(props: EditorProps) { </label> )} - <label className="flex items-center gap-1 text-xs text-zinc-500"> + <label className="flex items-center gap-1 text-xs text-muted-foreground"> max <input type="number" @@ -315,7 +315,7 @@ function NodeEditor(props: EditorProps) { {parentId && ( <button type="button" - className={`${btnClass} text-red-700 dark:text-red-300`} + className={`${btnClass} text-destructive`} onClick={() => props.remove(node.id)} > Remove @@ -340,7 +340,7 @@ function NodeEditor(props: EditorProps) { /> ))} {(node as AutoQueueGroup).children.length === 0 && ( - <p className="text-xs text-zinc-400 pl-2"> + <p className="text-xs text-muted-foreground pl-2"> Empty group — add a rule or nested group below. </p> )} @@ -467,7 +467,7 @@ function LeafControls(props: EditorProps & { leaf: AutoQueueLeaf }) { </select> )} - <label className="flex items-center gap-1 text-xs text-zinc-500"> + <label className="flex items-center gap-1 text-xs text-muted-foreground"> bucket <select value={leaf.match.bucket ?? ""} diff --git a/editor/app/auto-queue/page.tsx b/editor/app/auto-queue/page.tsx @@ -36,7 +36,7 @@ export default async function AutoQueuePage() { Sync schedule </Link> </div> - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> Automatically pick the next transcription across all channels by a priority policy, instead of running one channel batch at a time. Order the rules top-to-bottom for strict priority, or wrap rules in a group set to{" "} diff --git a/editor/app/build/components/ArchiveOptionsControl.tsx b/editor/app/build/components/ArchiveOptionsControl.tsx @@ -15,11 +15,11 @@ type Props = { }; const inputClass = - "font-mono px-2 py-1 rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100"; + "font-mono px-2 py-1 rounded border border-border bg-card text-foreground"; export function ArchiveOptionsControl({ value, onChange, actionLabel }: Props) { return ( - <div className="flex flex-wrap items-center gap-2 text-xs text-zinc-500"> + <div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground"> <label className="flex items-center gap-1"> <span>format</span> <select diff --git a/editor/app/build/components/BuildButtons.tsx b/editor/app/build/components/BuildButtons.tsx @@ -57,7 +57,7 @@ export function BuildButtons({ existingQueues }: Props) { <section className="flex flex-col gap-3"> <div> <h2 className="text-lg font-semibold">Build index</h2> - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> Re-scans <code>transcripts/channels/</code> and rewrites paginated JSON in <code>export/public/</code>. Cheap when nothing changed (mtime short-circuit). @@ -80,10 +80,10 @@ export function BuildButtons({ existingQueues }: Props) { /> </section> - <section className="flex flex-col gap-3 border-t border-zinc-200 dark:border-zinc-800 pt-6"> + <section className="flex flex-col gap-3 border-t border-border pt-6"> <div> <h2 className="text-lg font-semibold">Build stats dataset</h2> - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> Extracts per-video stats (views, likes, comments, duration, categories, …) into <code>export/public/stats/</code> for the viewer charts. Reads engagement metrics from each{" "} @@ -109,10 +109,10 @@ export function BuildButtons({ existingQueues }: Props) { /> </section> - <section className="flex flex-col gap-3 border-t border-zinc-200 dark:border-zinc-800 pt-6"> + <section className="flex flex-col gap-3 border-t border-border pt-6"> <div> <h2 className="text-lg font-semibold">Normalize transcripts</h2> - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> Walks every video and writes{" "} <code>transcript.cues.json</code> next to each raw{" "} <code>.vtt</code>/<code>.json</code>. Idempotent — skips entries @@ -136,10 +136,10 @@ export function BuildButtons({ existingQueues }: Props) { /> </section> - <section className="flex flex-col gap-3 border-t border-zinc-200 dark:border-zinc-800 pt-6"> + <section className="flex flex-col gap-3 border-t border-border pt-6"> <div> <h2 className="text-lg font-semibold">Build transcript archives</h2> - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> Produces one <code><channel>.tar.gz</code> per channel under <code>transcripts/export/archives/</code>, containing each video's compact <code>transcript.cues.json</code> plus a{" "} @@ -171,12 +171,12 @@ export function BuildButtons({ existingQueues }: Props) { /> </section> - <section className="flex flex-col gap-3 border-t border-zinc-200 dark:border-zinc-800 pt-6"> + <section className="flex flex-col gap-3 border-t border-border pt-6"> <div> <h2 className="text-lg font-semibold"> Build combined transcript archive </h2> - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> Produces a single{" "} <code>all-transcripts.tar.gz</code> under{" "} <code>transcripts/export/archives/</code> containing every @@ -210,10 +210,10 @@ export function BuildButtons({ existingQueues }: Props) { /> </section> - <section className="flex flex-col gap-3 border-t border-zinc-200 dark:border-zinc-800 pt-6"> + <section className="flex flex-col gap-3 border-t border-border pt-6"> <div> <h2 className="text-lg font-semibold">Normalize live chat</h2> - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> Walks every video and writes{" "} <code>live_chat.cues.json</code> next to each raw{" "} <code>transcript.live_chat.json</code>. Idempotent — skips @@ -237,10 +237,10 @@ export function BuildButtons({ existingQueues }: Props) { /> </section> - <section className="flex flex-col gap-3 border-t border-zinc-200 dark:border-zinc-800 pt-6"> + <section className="flex flex-col gap-3 border-t border-border pt-6"> <div> <h2 className="text-lg font-semibold">Build live chat archives</h2> - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> Produces one <code><channel>.live_chat.tar.gz</code> per channel under <code>transcripts/export/archives/</code>, containing each video's parsed{" "} @@ -275,12 +275,12 @@ export function BuildButtons({ existingQueues }: Props) { /> </section> - <section className="flex flex-col gap-3 border-t border-zinc-200 dark:border-zinc-800 pt-6"> + <section className="flex flex-col gap-3 border-t border-border pt-6"> <div> <h2 className="text-lg font-semibold"> Build combined live chat archive </h2> - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> Produces a single <code>all-live-chat.tar.gz</code> under{" "} <code>transcripts/export/archives/</code> containing every channel's parsed live chats plus a top-level{" "} diff --git a/editor/app/changelog/page.tsx b/editor/app/changelog/page.tsx @@ -32,10 +32,10 @@ export default function ChangelogPage() { <div className="flex flex-col gap-6 max-w-3xl"> <h1 className="text-2xl font-semibold">Changelog</h1> <Changelog source={source} /> - <section className="flex flex-col gap-3 border-t border-zinc-200 dark:border-zinc-800 pt-6"> + <section className="flex flex-col gap-3 border-t border-border pt-6"> <div> <h2 className="text-lg font-semibold">Cut release</h2> - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> Turns the current <code>## [Unreleased]</code> heading into a dated semver heading. </p> diff --git a/editor/app/channels/[slug]/components/PipelineStageCard.tsx b/editor/app/channels/[slug]/components/PipelineStageCard.tsx @@ -16,23 +16,23 @@ type Props = { }; const TONE_BORDER: Record<StageTone, string> = { - neutral: "border-zinc-200 dark:border-zinc-800", - attention: "border-amber-300 dark:border-amber-800", - danger: "border-red-300 dark:border-red-800", - running: "border-blue-300 dark:border-blue-800", - ok: "border-emerald-300 dark:border-emerald-800", + neutral: "border-border", + attention: "border-warning/30", + danger: "border-destructive/30", + running: "border-info/30", + ok: "border-success/30", }; const TONE_BADGE: Record<StageTone, string> = { neutral: - "bg-zinc-100 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 border-zinc-200 dark:border-zinc-700", + "bg-muted text-muted-foreground border-border", attention: - "bg-amber-50 dark:bg-amber-950 text-amber-800 dark:text-amber-200 border-amber-200 dark:border-amber-800", + "bg-warning-soft text-warning border-warning/30", danger: - "bg-red-50 dark:bg-red-950 text-red-800 dark:text-red-200 border-red-200 dark:border-red-800", + "bg-destructive-soft text-destructive border-destructive/30", running: - "bg-blue-50 dark:bg-blue-950 text-blue-800 dark:text-blue-200 border-blue-200 dark:border-blue-800", - ok: "bg-emerald-50 dark:bg-emerald-950 text-emerald-800 dark:text-emerald-200 border-emerald-200 dark:border-emerald-800", + "bg-info-soft text-info border-info/30", + ok: "bg-success-soft text-success border-success/30", }; export function PipelineStageCard({ @@ -47,21 +47,21 @@ export function PipelineStageCard({ children, }: Props) { const isDanger = id === "danger" || title === "Danger zone"; - const titleColor = isDanger ? "text-red-700 dark:text-red-400" : ""; + const titleColor = isDanger ? "text-destructive" : ""; return ( <details id={`video-stage-${id}`} open={defaultOpen} aria-label={`${title} stage`} - className={`group rounded-md border ${TONE_BORDER[tone]} bg-white dark:bg-zinc-900 scroll-mt-32`} + className={`group rounded-md border ${TONE_BORDER[tone]} bg-card scroll-mt-32`} > <summary - className="cursor-pointer list-none flex items-center justify-between gap-3 px-4 py-3 select-none rounded-md hover:bg-zinc-50 dark:hover:bg-zinc-800/50" + className="cursor-pointer list-none flex items-center justify-between gap-3 px-4 py-3 select-none rounded-md hover:bg-muted" aria-label={`${title} stage summary`} > <div className="flex items-baseline gap-3 min-w-0"> <h2 className={`text-lg font-semibold ${titleColor}`}>{title}</h2> - <span className="text-sm text-zinc-500 truncate">{summary}</span> + <span className="text-sm text-muted-foreground truncate">{summary}</span> </div> <div className="flex items-center gap-2 flex-shrink-0"> {running && ( @@ -91,7 +91,7 @@ export function PipelineStageCard({ <Chevron /> </div> </summary> - <div className="px-4 pb-4 pt-1 flex flex-col gap-4 border-t border-zinc-100 dark:border-zinc-800"> + <div className="px-4 pb-4 pt-1 flex flex-col gap-4 border-t border-border"> {children} </div> </details> @@ -102,7 +102,7 @@ function Chevron() { return ( <svg aria-hidden="true" - className="w-4 h-4 text-zinc-400 transition-transform group-open:rotate-90" + className="w-4 h-4 text-muted-foreground transition-transform group-open:rotate-90" viewBox="0 0 20 20" fill="currentColor" > diff --git a/editor/app/channels/[slug]/components/RefreshSnapshotButton.tsx b/editor/app/channels/[slug]/components/RefreshSnapshotButton.tsx @@ -15,7 +15,7 @@ export function RefreshSnapshotButton({ slug }: { slug: string }) { } disabled={pending} aria-label="refresh channel report" - className="self-start px-3 py-1 rounded-md bg-zinc-900 dark:bg-zinc-100 text-zinc-100 dark:text-zinc-900 text-sm font-medium hover:opacity-90 disabled:opacity-50" + className="self-start px-3 py-1 rounded-md bg-primary text-primary-foreground text-sm font-medium hover:opacity-90 disabled:opacity-50" > {pending ? "Refreshing…" : "Refresh report"} </button> diff --git a/editor/app/channels/[slug]/components/RetryBucketControl.tsx b/editor/app/channels/[slug]/components/RetryBucketControl.tsx @@ -55,13 +55,13 @@ export function RetryBucketControl({ label={`Retry ${actionLabel}`} extraControls={ <> - <label className="flex items-center gap-1 text-xs text-zinc-500"> + <label className="flex items-center gap-1 text-xs text-muted-foreground"> <span>Retry as</span> <select value={handlingOverride} onChange={(e) => setHandlingOverride(e.target.value)} aria-label={`handling override for retry ${actionLabel}`} - className="font-mono px-2 py-1 rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100" + className="font-mono px-2 py-1 rounded border border-border bg-card text-foreground" > <option value="">channel default</option> {HANDLING_VALUES.map((h) => ( @@ -78,7 +78,7 @@ export function RetryBucketControl({ existingQueues={existingQueues} actionLabel={`Retry ${actionLabel}`} /> - <label className="flex items-center gap-1 text-xs text-zinc-500"> + <label className="flex items-center gap-1 text-xs text-muted-foreground"> <input type="checkbox" checked={abortOnError} diff --git a/editor/app/channels/[slug]/components/StageRailItem.tsx b/editor/app/channels/[slug]/components/StageRailItem.tsx @@ -1,27 +1,27 @@ import type { StageStatus, StageTone } from "../lib/stageStatus"; const TONE_ACCENT: Record<StageTone, string> = { - neutral: "text-zinc-700 dark:text-zinc-300", - attention: "text-amber-700 dark:text-amber-300", - danger: "text-red-700 dark:text-red-300", - running: "text-blue-700 dark:text-blue-300", - ok: "text-emerald-700 dark:text-emerald-300", + neutral: "text-muted-foreground", + attention: "text-warning", + danger: "text-destructive", + running: "text-info", + ok: "text-success", }; const TONE_ACTIVE_BG: Record<StageTone, string> = { - neutral: "bg-zinc-100 dark:bg-zinc-800", - attention: "bg-amber-50 dark:bg-amber-950", - danger: "bg-red-50 dark:bg-red-950", - running: "bg-blue-50 dark:bg-blue-950", - ok: "bg-emerald-50 dark:bg-emerald-950", + neutral: "bg-muted", + attention: "bg-warning-soft", + danger: "bg-destructive-soft", + running: "bg-info-soft", + ok: "bg-success-soft", }; const TONE_ACTIVE_BORDER: Record<StageTone, string> = { - neutral: "border-zinc-400 dark:border-zinc-500", - attention: "border-amber-400 dark:border-amber-600", - danger: "border-red-400 dark:border-red-600", - running: "border-blue-400 dark:border-blue-600", - ok: "border-emerald-400 dark:border-emerald-600", + neutral: "border-border", + attention: "border-warning", + danger: "border-destructive", + running: "border-info", + ok: "border-success", }; type Props = { @@ -32,7 +32,7 @@ type Props = { }; export function StageRailItem({ status, active, href, onSelect }: Props) { - const bg = active ? TONE_ACTIVE_BG[status.tone] : "hover:bg-zinc-100 dark:hover:bg-zinc-800/60"; + const bg = active ? TONE_ACTIVE_BG[status.tone] : "hover:bg-muted"; const border = active ? `border-l-2 ${TONE_ACTIVE_BORDER[status.tone]}` : "border-l-2 border-transparent"; @@ -46,7 +46,7 @@ export function StageRailItem({ status, active, href, onSelect }: Props) { {status.running && ( <span aria-hidden="true" - className="inline-block w-2 h-2 rounded-full bg-blue-500 animate-pulse shrink-0" + className="inline-block w-2 h-2 rounded-full bg-info animate-pulse shrink-0" /> )} <span className={`truncate ${active ? "font-semibold" : ""} ${TONE_ACCENT[status.tone]}`}> @@ -56,7 +56,7 @@ export function StageRailItem({ status, active, href, onSelect }: Props) { {status.pending > 0 && ( <span aria-label={`${status.title} pending count`} - className="font-mono text-xs px-1.5 rounded bg-amber-100 dark:bg-amber-900 text-amber-800 dark:text-amber-200" + className="font-mono text-xs px-1.5 rounded bg-warning-soft text-warning" > {status.pending} </span> @@ -64,7 +64,7 @@ export function StageRailItem({ status, active, href, onSelect }: Props) { {status.failed > 0 && ( <span aria-label={`${status.title} failed count`} - className="font-mono text-xs px-1.5 rounded bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200" + className="font-mono text-xs px-1.5 rounded bg-destructive-soft text-destructive" > {status.failed} </span> diff --git a/editor/app/channels/[slug]/components/StagesView.tsx b/editor/app/channels/[slug]/components/StagesView.tsx @@ -11,23 +11,23 @@ import type { StageId, StageStatus, StageTone } from "../lib/stageStatus"; import { StageRailItem } from "./StageRailItem"; const TONE_BORDER: Record<StageTone, string> = { - neutral: "border-zinc-200 dark:border-zinc-800", - attention: "border-amber-300 dark:border-amber-800", - danger: "border-red-300 dark:border-red-800", - running: "border-blue-300 dark:border-blue-800", - ok: "border-emerald-300 dark:border-emerald-800", + neutral: "border-border", + attention: "border-warning/30", + danger: "border-destructive/30", + running: "border-info/30", + ok: "border-success/30", }; const TONE_BADGE: Record<StageTone, string> = { neutral: - "bg-zinc-100 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 border-zinc-200 dark:border-zinc-700", + "bg-muted text-muted-foreground border-border", attention: - "bg-amber-50 dark:bg-amber-950 text-amber-800 dark:text-amber-200 border-amber-200 dark:border-amber-800", + "bg-warning-soft text-warning border-warning/30", danger: - "bg-red-50 dark:bg-red-950 text-red-800 dark:text-red-200 border-red-200 dark:border-red-800", + "bg-destructive-soft text-destructive border-destructive/30", running: - "bg-blue-50 dark:bg-blue-950 text-blue-800 dark:text-blue-200 border-blue-200 dark:border-blue-800", - ok: "bg-emerald-50 dark:bg-emerald-950 text-emerald-800 dark:text-emerald-200 border-emerald-200 dark:border-emerald-800", + "bg-info-soft text-info border-info/30", + ok: "bg-success-soft text-success border-success/30", }; const LG_QUERY = "(min-width: 1024px)"; @@ -136,7 +136,7 @@ export function StagesView({ stages, stageOrder, panels }: Props) { key={id} id={`stage-${id}`} aria-label={`${status.title} stage`} - className={`rounded-md border ${TONE_BORDER[status.tone]} bg-white dark:bg-zinc-900 scroll-mt-44 lg:scroll-mt-28`} + className={`rounded-md border ${TONE_BORDER[status.tone]} bg-card scroll-mt-44 lg:scroll-mt-28`} > <button type="button" @@ -144,15 +144,15 @@ export function StagesView({ stages, stageOrder, panels }: Props) { aria-expanded={isOpen} aria-controls={contentId} aria-label={`${status.title} stage summary`} - className="w-full flex items-center justify-between gap-3 px-4 py-3 text-left rounded-t-md hover:bg-zinc-50 dark:hover:bg-zinc-800/50 lg:hover:bg-transparent lg:dark:hover:bg-transparent lg:cursor-default" + className="w-full flex items-center justify-between gap-3 px-4 py-3 text-left rounded-t-md hover:bg-muted lg:hover:bg-transparent lg:dark:hover:bg-transparent lg:cursor-default" > <div className="flex items-baseline gap-3 min-w-0"> <h2 - className={`text-lg font-semibold ${isDanger ? "text-red-700 dark:text-red-400" : ""}`} + className={`text-lg font-semibold ${isDanger ? "text-destructive" : ""}`} > {status.title} </h2> - <span className="text-sm text-zinc-500 truncate"> + <span className="text-sm text-muted-foreground truncate"> {status.summary} </span> </div> @@ -186,7 +186,7 @@ export function StagesView({ stages, stageOrder, panels }: Props) { </button> <div id={contentId} - className={`px-4 pb-4 pt-1 lg:py-4 flex-col gap-4 border-t border-zinc-100 dark:border-zinc-800 ${isOpen ? "flex" : "hidden"} lg:flex`} + className={`px-4 pb-4 pt-1 lg:py-4 flex-col gap-4 border-t border-border ${isOpen ? "flex" : "hidden"} lg:flex`} > {panels[id]} </div> @@ -202,7 +202,7 @@ function Chevron({ isOpen }: { isOpen: boolean }) { return ( <svg aria-hidden="true" - className={`w-4 h-4 text-zinc-400 transition-transform lg:hidden ${isOpen ? "rotate-90" : ""}`} + className={`w-4 h-4 text-muted-foreground transition-transform lg:hidden ${isOpen ? "rotate-90" : ""}`} viewBox="0 0 20 20" fill="currentColor" > diff --git a/editor/app/channels/[slug]/components/StatusHeader.tsx b/editor/app/channels/[slug]/components/StatusHeader.tsx @@ -35,15 +35,15 @@ export function StatusHeader({ return ( <header aria-label="channel status header" - className="sticky top-0 z-10 -mx-4 md:-mx-6 px-4 md:px-6 py-3 bg-zinc-50/95 dark:bg-zinc-950/95 backdrop-blur border-b border-zinc-200 dark:border-zinc-800" + className="sticky top-0 z-10 -mx-4 md:-mx-6 px-4 md:px-6 py-3 bg-card/95 backdrop-blur border-b border-border" > <div className="flex flex-col gap-3"> <div className="flex items-start justify-between gap-3 flex-wrap"> <div className="flex flex-col gap-1 min-w-0"> - <nav className="flex items-center gap-2 text-sm text-zinc-500"> + <nav className="flex items-center gap-2 text-sm text-muted-foreground"> <Link href="/channels" - className="underline hover:text-zinc-900 dark:hover:text-zinc-100" + className="underline hover:text-foreground" > Channels </Link> @@ -51,7 +51,7 @@ export function StatusHeader({ <span className="font-mono">{slug}</span> </nav> <h1 className="text-2xl font-semibold truncate">{channelName}</h1> - <p className="text-xs text-zinc-500"> + <p className="text-xs text-muted-foreground"> {fresh ? ( "Report generated just now." ) : ( diff --git a/editor/app/channels/[slug]/components/VideoIdList.tsx b/editor/app/channels/[slug]/components/VideoIdList.tsx @@ -19,7 +19,7 @@ export function VideoIdList({ }: Props) { if (ids.length === 0) { return ( - <p className="text-sm text-zinc-500" aria-label={emptyAriaLabel}> + <p className="text-sm text-muted-foreground" aria-label={emptyAriaLabel}> {emptyMessage} </p> ); @@ -27,15 +27,15 @@ export function VideoIdList({ return ( <div aria-label={ariaLabel} - className="rounded border border-zinc-200 dark:border-zinc-800 max-h-72 overflow-auto" + className="rounded border border-border max-h-72 overflow-auto" > - <ul className="divide-y divide-zinc-200 dark:divide-zinc-800"> + <ul className="divide-y divide-border"> {ids.map((id) => ( <li key={id} className="px-3 py-1.5"> <Link href={`/channels/${slug}/videos/${encodeURIComponent(id)}`} aria-label={itemAriaLabel(id)} - className="font-mono text-xs underline hover:text-zinc-900 dark:hover:text-zinc-100" + className="font-mono text-xs underline hover:text-foreground" > {id} </Link> diff --git a/editor/app/channels/[slug]/components/VideoListPane.tsx b/editor/app/channels/[slug]/components/VideoListPane.tsx @@ -386,7 +386,7 @@ export function VideoListPane({ value={query} onChange={(e) => changeQuery(e.target.value)} aria-label="search videos" - className="w-full rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1.5 text-sm" + className="w-full rounded border border-border bg-card px-2 py-1.5 text-sm" /> <div className="flex flex-wrap gap-1.5"> <button @@ -395,8 +395,8 @@ export function VideoListPane({ onClick={clearFilters} className={`text-xs rounded-full border px-2.5 py-1 ${ filters.size === 0 - ? "border-blue-300 dark:border-blue-700 bg-blue-50 dark:bg-blue-950 text-blue-900 dark:text-blue-100" - : "border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 hover:bg-zinc-100 dark:hover:bg-zinc-800" + ? "border-info/30 bg-info-soft text-info" + : "border-border bg-card hover:bg-muted" }`} > All @@ -411,8 +411,8 @@ export function VideoListPane({ onClick={() => toggleFilter(opt.value)} className={`text-xs rounded-full border px-2.5 py-1 ${ active - ? "border-blue-300 dark:border-blue-700 bg-blue-50 dark:bg-blue-950 text-blue-900 dark:text-blue-100" - : "border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 hover:bg-zinc-100 dark:hover:bg-zinc-800" + ? "border-info/30 bg-info-soft text-info" + : "border-border bg-card hover:bg-muted" }`} > {opt.label} @@ -420,7 +420,7 @@ export function VideoListPane({ ); })} </div> - <div className="flex items-center justify-between gap-2 text-xs text-zinc-500"> + <div className="flex items-center justify-between gap-2 text-xs text-muted-foreground"> <span className="flex-shrink-0"> {visibleRows.length} of {rows.length} videos </span> @@ -428,7 +428,7 @@ export function VideoListPane({ <button type="button" onClick={selectFailed} - className="rounded border border-zinc-200 dark:border-zinc-800 px-2 py-0.5 hover:bg-zinc-100 dark:hover:bg-zinc-800" + className="rounded border border-border px-2 py-0.5 hover:bg-muted" > Select failed </button> @@ -436,7 +436,7 @@ export function VideoListPane({ <button type="button" onClick={selectWrongFormat} - className="rounded border border-zinc-200 dark:border-zinc-800 px-2 py-0.5 hover:bg-zinc-100 dark:hover:bg-zinc-800" + className="rounded border border-border px-2 py-0.5 hover:bg-muted" > Select wrong-format </button> @@ -445,7 +445,7 @@ export function VideoListPane({ <button type="button" onClick={selectIncomplete} - className="rounded border border-zinc-200 dark:border-zinc-800 px-2 py-0.5 hover:bg-zinc-100 dark:hover:bg-zinc-800" + className="rounded border border-border px-2 py-0.5 hover:bg-muted" > Select incomplete </button> @@ -454,7 +454,7 @@ export function VideoListPane({ <button type="button" onClick={selectShortAudio} - className="rounded border border-zinc-200 dark:border-zinc-800 px-2 py-0.5 hover:bg-zinc-100 dark:hover:bg-zinc-800" + className="rounded border border-border px-2 py-0.5 hover:bg-muted" > Select short-audio </button> @@ -464,7 +464,7 @@ export function VideoListPane({ <button type="button" onClick={selectFiltered} - className="rounded border border-zinc-200 dark:border-zinc-800 px-2 py-0.5 hover:bg-zinc-100 dark:hover:bg-zinc-800" + className="rounded border border-border px-2 py-0.5 hover:bg-muted" > Select filtered </button> @@ -485,10 +485,10 @@ export function VideoListPane({ <ul aria-label="videos" - className="flex-1 min-h-0 overflow-y-auto flex flex-col divide-y divide-zinc-100 dark:divide-zinc-800 border border-zinc-200 dark:border-zinc-800 rounded" + className="flex-1 min-h-0 overflow-y-auto flex flex-col divide-y divide-border border border-border rounded" > {visibleRows.length === 0 ? ( - <li className="px-3 py-4 text-sm text-zinc-500"> + <li className="px-3 py-4 text-sm text-muted-foreground"> No videos match this filter. </li> ) : ( @@ -501,8 +501,8 @@ export function VideoListPane({ key={row.id} className={`flex items-center gap-2 px-2 py-1.5 text-sm ${ isActive - ? "bg-blue-50 dark:bg-blue-950/40" - : "hover:bg-zinc-50 dark:hover:bg-zinc-800/40" + ? "bg-info-soft" + : "hover:bg-muted" }`} > <input @@ -524,7 +524,7 @@ export function VideoListPane({ {row.running && ( <span aria-label="job running" - className="text-[10px] uppercase tracking-wide rounded-full border border-blue-200 dark:border-blue-800 bg-blue-50 dark:bg-blue-950 text-blue-800 dark:text-blue-200 px-1.5 py-0.5 leading-none" + className="text-[10px] uppercase tracking-wide rounded-full border border-info/30 bg-info-soft text-info px-1.5 py-0.5 leading-none" > running </span> @@ -539,26 +539,26 @@ export function VideoListPane({ {someSelected && ( <div aria-label="bulk action bar" - className="sticky bottom-0 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-md p-2 flex flex-wrap items-center gap-2 text-sm shadow-sm" + className="sticky bottom-0 bg-card border border-border rounded-md p-2 flex flex-wrap items-center gap-2 text-sm shadow-sm" > <span className="font-medium">{selected.size} selected</span> <button type="button" disabled={pending} onClick={() => setSelected(new Set())} - className="px-2 py-1 rounded border border-transparent text-xs text-zinc-500 hover:bg-zinc-100 dark:hover:bg-zinc-800 disabled:opacity-50" + className="px-2 py-1 rounded border border-transparent text-xs text-muted-foreground hover:bg-muted disabled:opacity-50" > Clear </button> <div className="basis-full flex flex-col gap-2"> <div className="flex flex-wrap items-center gap-1.5"> - <label className="flex items-center gap-1.5 text-xs text-zinc-500"> + <label className="flex items-center gap-1.5 text-xs text-muted-foreground"> <span>Action</span> <select value={action} onChange={(e) => setAction(e.target.value as BulkAction)} aria-label="bulk action" - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-1.5 py-1 text-xs text-zinc-900 dark:text-zinc-100" + className="rounded border border-border bg-card px-1.5 py-1 text-xs text-foreground" > {BULK_ACTION_OPTIONS.map((opt) => ( <option key={opt.value} value={opt.value}> @@ -587,7 +587,7 @@ export function VideoListPane({ existingQueues={existingQueues} actionLabel="bulk retry download" /> - <label className="flex items-center gap-1 text-xs text-zinc-500"> + <label className="flex items-center gap-1 text-xs text-muted-foreground"> <input type="checkbox" checked={retryAbortOnError} @@ -609,7 +609,7 @@ export function VideoListPane({ /> )} {action === "delete" && ( - <label className="flex items-center gap-1.5 text-xs text-red-700 dark:text-red-400"> + <label className="flex items-center gap-1.5 text-xs text-destructive"> <span> Type <code className="font-mono">delete</code> to confirm </span> @@ -618,7 +618,7 @@ export function VideoListPane({ value={deleteConfirm} onChange={(e) => setDeleteConfirm(e.target.value)} aria-label="type delete to confirm" - className="w-24 rounded border border-red-300 dark:border-red-800 bg-white dark:bg-zinc-900 px-1.5 py-1 text-xs" + className="w-24 rounded border border-destructive/30 bg-card px-1.5 py-1 text-xs" /> </label> )} @@ -630,13 +630,13 @@ export function VideoListPane({ aria-label="apply bulk action" className={`px-3 py-1 rounded border text-xs disabled:opacity-50 ${ action === "delete" - ? "border-red-300 dark:border-red-800 text-red-700 dark:text-red-300 hover:bg-red-50 dark:hover:bg-red-950" + ? "border-destructive/30 text-destructive hover:bg-destructive-soft" : action === "untranscribable" || action === "remove_audio" || action === "remove_wrong_format" || action === "clear_incomplete" - ? "border-amber-300 dark:border-amber-800 text-amber-700 dark:text-amber-300 hover:bg-amber-50 dark:hover:bg-amber-950" - : "border-zinc-300 dark:border-zinc-700 hover:bg-zinc-100 dark:hover:bg-zinc-800" + ? "border-warning/30 text-warning hover:bg-warning-soft" + : "border-border hover:bg-muted" }`} > Apply @@ -647,7 +647,7 @@ export function VideoListPane({ <p role="status" aria-label="bulk action error" - className="basis-full text-xs text-amber-700 dark:text-amber-300" + className="basis-full text-xs text-warning" > {error} </p> @@ -656,7 +656,7 @@ export function VideoListPane({ <p role="status" aria-label="bulk action result" - className={`basis-full text-xs ${lastResult.ok ? "text-emerald-700 dark:text-emerald-300" : "text-amber-700 dark:text-amber-300"}`} + className={`basis-full text-xs ${lastResult.ok ? "text-success" : "text-warning"}`} > {lastResult.succeeded}/{lastResult.attempted} succeeded {lastResult.failures.length > 0 && @@ -672,25 +672,25 @@ export function VideoListPane({ function StatusGlyphs({ row }: { row: VideoRow }) { const dlColor = row.partial || row.corruptSource || row.corruptFullSource - ? "bg-amber-500" + ? "bg-warning" : row.downloaded - ? "bg-emerald-500" - : "bg-zinc-300 dark:bg-zinc-700"; + ? "bg-success" + : "bg-muted"; const tcColor = row.transcoded === null - ? "bg-transparent border border-zinc-300 dark:border-zinc-700" + ? "bg-transparent border border-border" : row.transcoded - ? "bg-emerald-500" - : "bg-zinc-300 dark:bg-zinc-700"; + ? "bg-success" + : "bg-muted"; const trColor = row.untranscribable - ? "bg-zinc-400 dark:bg-zinc-600" + ? "bg-muted-foreground" : row.incompleteTranscript - ? "bg-amber-500" + ? "bg-warning" : row.transcribed - ? "bg-emerald-500" - : "bg-zinc-300 dark:bg-zinc-700"; + ? "bg-success" + : "bg-muted"; const failureColor = - row.failedTranscription || row.failedTranscoding ? "bg-red-500" : null; + row.failedTranscription || row.failedTranscoding ? "bg-destructive" : null; return ( <span aria-label={`status ${row.status}`} diff --git a/editor/app/channels/[slug]/components/VideoListPaneSection.tsx b/editor/app/channels/[slug]/components/VideoListPaneSection.tsx @@ -84,7 +84,7 @@ export function VideoListPaneSection({ onClick={() => setCollapsed(false)} aria-expanded={false} aria-label="expand video list" - className="hidden lg:flex items-center justify-center w-full h-12 rounded border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 text-zinc-500 hover:text-zinc-900 dark:hover:text-zinc-100 hover:bg-zinc-100 dark:hover:bg-zinc-800" + className="hidden lg:flex items-center justify-center w-full h-12 rounded border border-border bg-card text-muted-foreground hover:text-foreground hover:bg-muted" title="Expand video list" > <span aria-hidden="true" className="text-sm">›</span> @@ -96,7 +96,7 @@ export function VideoListPaneSection({ onClick={() => setCollapsed(true)} aria-expanded={true} aria-label="collapse video list" - className="hidden lg:flex self-end items-center gap-1 text-xs text-zinc-500 hover:text-zinc-900 dark:hover:text-zinc-100 mb-1" + className="hidden lg:flex self-end items-center gap-1 text-xs text-muted-foreground hover:text-foreground mb-1" title="Collapse video list" > <span aria-hidden="true">‹</span> @@ -114,7 +114,7 @@ export function VideoListPaneSection({ onClick={() => setRightCollapsed(false)} aria-expanded={false} aria-label="expand video" - className="hidden lg:flex items-center justify-center w-full h-12 rounded border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 text-zinc-500 hover:text-zinc-900 dark:hover:text-zinc-100 hover:bg-zinc-100 dark:hover:bg-zinc-800" + className="hidden lg:flex items-center justify-center w-full h-12 rounded border border-border bg-card text-muted-foreground hover:text-foreground hover:bg-muted" title="Expand video" > <span aria-hidden="true" className="text-sm">‹</span> @@ -128,7 +128,7 @@ export function VideoListPaneSection({ onClick={() => setRightCollapsed(true)} aria-expanded={true} aria-label="collapse video" - className="hidden lg:flex self-start items-center gap-1 text-xs text-zinc-500 hover:text-zinc-900 dark:hover:text-zinc-100 mb-1" + className="hidden lg:flex self-start items-center gap-1 text-xs text-muted-foreground hover:text-foreground mb-1" title="Collapse video" > <span aria-hidden="true">›</span> diff --git a/editor/app/channels/[slug]/components/stages/CleanupStage.tsx b/editor/app/channels/[slug]/components/stages/CleanupStage.tsx @@ -143,13 +143,13 @@ function WrongFormatAudioSection({ return ( <div aria-label="wrong-format audio section" - className="flex flex-col gap-2 rounded border border-zinc-200 dark:border-zinc-800 p-3" + className="flex flex-col gap-2 rounded border border-border p-3" > <div> <h4 className="text-sm font-semibold"> Dirs with wrong-format audio ({ids.length}) </h4> - <p className="text-xs text-zinc-500"> + <p className="text-xs text-muted-foreground"> Click <em>Refresh report</em> above to recompute. The sweep walks every video dir on disk regardless of this list — the count below reflects the last snapshot only. Dirs marked <em>do not clean</em> are skipped. @@ -165,7 +165,7 @@ function WrongFormatAudioSection({ itemAriaLabel={(id) => `wrong-format audio ${id}`} /> <div className="flex flex-col gap-2"> - <label className="flex items-center gap-2 text-xs text-zinc-600 dark:text-zinc-400"> + <label className="flex items-center gap-2 text-xs text-muted-foreground"> Type <span className="font-mono">remove</span> to confirm @@ -174,7 +174,7 @@ function WrongFormatAudioSection({ value={confirmText} onChange={(e) => setConfirmText(e.target.value)} aria-label="confirm remove wrong-format audio" - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-0.5 font-mono" + className="rounded border border-border bg-card px-2 py-0.5 font-mono" /> </label> <StreamActionLog @@ -218,13 +218,13 @@ function ExtraAudioFormatsSection({ return ( <div aria-label="multiple audio formats section" - className="flex flex-col gap-2 rounded border border-zinc-200 dark:border-zinc-800 p-3" + className="flex flex-col gap-2 rounded border border-border p-3" > <div> <h4 className="text-sm font-semibold"> Dirs with extra audio formats ({ids.length}) </h4> - <p className="text-xs text-zinc-500"> + <p className="text-xs text-muted-foreground"> Click <em>Refresh report</em> above to recompute. The cleanup walks every video dir on disk regardless of this list — the count below reflects the last snapshot only. @@ -240,7 +240,7 @@ function ExtraAudioFormatsSection({ itemAriaLabel={(id) => `multiple audio formats ${id}`} /> <div className="flex flex-col gap-2"> - <label className="flex items-center gap-2 text-xs text-zinc-600 dark:text-zinc-400"> + <label className="flex items-center gap-2 text-xs text-muted-foreground"> Type <span className="font-mono">clean</span> to confirm @@ -249,7 +249,7 @@ function ExtraAudioFormatsSection({ value={confirmText} onChange={(e) => setConfirmText(e.target.value)} aria-label="confirm clean extra audio formats" - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-0.5 font-mono" + className="rounded border border-border bg-card px-2 py-0.5 font-mono" /> </label> <StreamActionLog @@ -297,11 +297,11 @@ function RetentionSection({ return ( <div aria-label="retention and persistence section" - className="flex flex-col gap-3 rounded border border-zinc-200 dark:border-zinc-800 p-3" + className="flex flex-col gap-3 rounded border border-border p-3" > <div> <h3 className="text-base font-semibold">Retention & persistence</h3> - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> The keep-latest window protects the newest videos from cleanup and persists their source containers to the saved-video store. Configure the window size under{" "} @@ -363,7 +363,7 @@ function RetentionSection({ /> )} </div> - <p className="text-xs text-zinc-500"> + <p className="text-xs text-muted-foreground"> <strong>Check kept videos</strong> re-probes the window for deleted-from-source videos and pins them.{" "} <strong>Persist kept now</strong> re-fetches the source container for any @@ -392,7 +392,7 @@ function RetentionSection({ function Stat({ label, value }: { label: string; value: string }) { return ( <div className="flex flex-col"> - <dt className="text-xs uppercase tracking-wide text-zinc-500">{label}</dt> + <dt className="text-xs uppercase tracking-wide text-muted-foreground">{label}</dt> <dd className="font-medium tabular-nums">{value}</dd> </div> ); @@ -400,7 +400,7 @@ function Stat({ label, value }: { label: string; value: string }) { function ReclaimEstimate({ bytes }: { bytes: number }) { return ( - <p className="text-sm text-zinc-600 dark:text-zinc-400"> + <p className="text-sm text-muted-foreground"> Estimated space to reclaim:{" "} <span className="font-medium tabular-nums"> {bytes > 0 ? `~${formatBytes(bytes)}` : "nothing to reclaim"} @@ -413,7 +413,7 @@ function Heading({ title, desc }: { title: string; desc: string }) { return ( <div> <h3 className="text-base font-semibold">{title}</h3> - <p className="text-sm text-zinc-500">{desc}</p> + <p className="text-sm text-muted-foreground">{desc}</p> </div> ); } diff --git a/editor/app/channels/[slug]/components/stages/DiagnosticsStage.tsx b/editor/app/channels/[slug]/components/stages/DiagnosticsStage.tsx @@ -135,7 +135,7 @@ export function DiagnosticsStage({ > <label htmlFor="goto-video-id" - className="text-zinc-600 dark:text-zinc-400" + className="text-muted-foreground" > Video ID </label> @@ -145,11 +145,11 @@ export function DiagnosticsStage({ type="text" placeholder="dQw4w9WgXcQ" required - className="font-mono rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1" + className="font-mono rounded border border-border bg-card px-2 py-1" /> <button type="submit" - className="px-3 py-1 rounded-md bg-zinc-900 dark:bg-zinc-100 text-zinc-100 dark:text-zinc-900 font-medium hover:opacity-90" + className="px-3 py-1 rounded-md bg-primary text-primary-foreground font-medium hover:opacity-90" > Open </button> @@ -172,7 +172,7 @@ export function DiagnosticsStage({ <div className="flex flex-col gap-2" aria-label="channel health"> <div> <h3 className="text-base font-semibold">Channel health</h3> - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> {totals.videos} video dirs · {totals.transcribed} transcribed ·{" "} {totals.downloaded} downloaded {total > 0 ? ` · ${total} abnormal` : ""} @@ -181,7 +181,7 @@ export function DiagnosticsStage({ {populated.length === 0 ? ( <p aria-label="channel health empty" - className="text-sm text-zinc-600 dark:text-zinc-400" + className="text-sm text-muted-foreground" > All clear. </p> @@ -190,13 +190,13 @@ export function DiagnosticsStage({ {populated.map((b) => ( <div key={b.ariaLabel} - className="flex flex-col gap-2 rounded border border-zinc-200 dark:border-zinc-800 p-3" + className="flex flex-col gap-2 rounded border border-border p-3" > <div> <h4 className="text-sm font-semibold"> {b.label} ({b.ids.length}) </h4> - <p className="text-xs text-zinc-500">{b.description}</p> + <p className="text-xs text-muted-foreground">{b.description}</p> </div> <VideoIdList slug={slug} @@ -223,13 +223,13 @@ export function DiagnosticsStage({ {untranscribableIds.length > 0 && ( <div aria-label="marked untranscribable" - className="flex flex-col gap-2 rounded border border-zinc-200 dark:border-zinc-800 p-3" + className="flex flex-col gap-2 rounded border border-border p-3" > <div> <h4 className="text-sm font-semibold"> Marked untranscribable ({untranscribableIds.length}) </h4> - <p className="text-xs text-zinc-500"> + <p className="text-xs text-muted-foreground"> Videos explicitly marked as untranscribable (transcript.json with an empty transcription array). Informational only — no action needed. @@ -270,14 +270,14 @@ function VerifyButton({ slug }: { slug: string }) { type="button" onClick={handleClick} disabled={running} - className="self-start px-3 py-2 rounded-md bg-zinc-900 dark:bg-zinc-100 text-zinc-100 dark:text-zinc-900 text-sm font-medium hover:opacity-90 disabled:opacity-50" + className="self-start px-3 py-2 rounded-md bg-primary text-primary-foreground text-sm font-medium hover:opacity-90 disabled:opacity-50" > {running ? "Verifying…" : "Verify"} </button> {result?.ok === false && ( <div role="alert" - className="rounded border border-red-300 bg-red-50 dark:border-red-800 dark:bg-red-950 px-3 py-2 text-sm text-red-700 dark:text-red-300" + className="rounded border border-destructive/30 bg-destructive-soft px-3 py-2 text-sm text-destructive" > {result.error} </div> @@ -303,12 +303,12 @@ function VerifyResultBucket({ items: string[]; }) { return ( - <div className="rounded border border-zinc-200 dark:border-zinc-800 p-3"> - <div className="text-xs uppercase tracking-wide text-zinc-500"> + <div className="rounded border border-border p-3"> + <div className="text-xs uppercase tracking-wide text-muted-foreground"> {label} ({items.length}) </div> {items.length === 0 ? ( - <p className="text-zinc-500 mt-1">None</p> + <p className="text-muted-foreground mt-1">None</p> ) : ( <ul className="mt-1 font-mono text-xs space-y-0.5"> {items.map((id) => ( @@ -324,7 +324,7 @@ function Heading({ title, desc }: { title: string; desc: string }) { return ( <div> <h3 className="text-base font-semibold">{title}</h3> - <p className="text-sm text-zinc-500">{desc}</p> + <p className="text-sm text-muted-foreground">{desc}</p> </div> ); } @@ -431,12 +431,12 @@ function QuickAvailabilityCheck({ const neverRun = maybeMissing.ids.length === 0 && !maybeMissing.checkedAt; return ( <div - className="mt-2 flex flex-col gap-3 rounded border border-zinc-200 dark:border-zinc-800 p-3" + className="mt-2 flex flex-col gap-3 rounded border border-border p-3" aria-label="quick availability check" > <div> <h4 className="text-sm font-semibold">Quick availability check</h4> - <p className="text-xs text-zinc-500"> + <p className="text-xs text-muted-foreground"> Fetch the channel's current flat playlist (one cheap yt-dlp call, no per-video metadata) and diff it against the videos on disk. Anything missing is flagged below. Note: unlisted videos legitimately drop out @@ -453,7 +453,7 @@ function QuickAvailabilityCheck({ {neverRun ? ( <p aria-label="maybe missing never run" - className="text-sm text-zinc-600 dark:text-zinc-400" + className="text-sm text-muted-foreground" > No quick check run yet. </p> @@ -464,7 +464,7 @@ function QuickAvailabilityCheck({ Maybe-missing ({maybeMissing.ids.length}) </h5> {maybeMissing.checkedAt && ( - <p className="text-xs text-zinc-500"> + <p className="text-xs text-muted-foreground"> Last quick check:{" "} {new Date(maybeMissing.checkedAt).toLocaleString()} </p> @@ -550,7 +550,7 @@ function AvailabilitySummary({ return ( <p aria-label="availability summary empty" - className="text-sm text-zinc-600 dark:text-zinc-400" + className="text-sm text-muted-foreground" > No availability data yet. Run a check above. </p> @@ -568,10 +568,10 @@ function AvailabilitySummary({ className="flex flex-col gap-3" aria-label="availability summary" > - <div className="flex flex-wrap gap-x-3 gap-y-1 text-sm text-zinc-600 dark:text-zinc-400"> + <div className="flex flex-wrap gap-x-3 gap-y-1 text-sm text-muted-foreground"> {counts.map(({ v, n }) => ( <span key={v} aria-label={`availability ${v} count`}> - <span className="font-medium text-zinc-800 dark:text-zinc-200"> + <span className="font-medium text-foreground"> {AVAILABILITY_LABELS[v]} </span>{" "} {n} @@ -579,7 +579,7 @@ function AvailabilitySummary({ ))} {availability.unchecked.length > 0 && ( <span aria-label="availability unchecked count"> - <span className="font-medium text-zinc-800 dark:text-zinc-200"> + <span className="font-medium text-foreground"> Unchecked </span>{" "} {availability.unchecked.length} @@ -591,7 +591,7 @@ function AvailabilitySummary({ {listed.map((v) => ( <div key={v} - className="flex flex-col gap-2 rounded border border-zinc-200 dark:border-zinc-800 p-3" + className="flex flex-col gap-2 rounded border border-border p-3" > <div> <h4 className="text-sm font-semibold"> diff --git a/editor/app/channels/[slug]/components/stages/DownloadStage.tsx b/editor/app/channels/[slug]/components/stages/DownloadStage.tsx @@ -58,7 +58,7 @@ export function DownloadStage({ if (!hasUrl) { return ( - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> Channel has no URL — downloads disabled. </p> ); @@ -83,7 +83,7 @@ export function DownloadStage({ aria-label="abort on error for Download from playlist" /> Abort on error - <span className="text-xs text-zinc-500"> + <span className="text-xs text-muted-foreground"> (stop the run on the first per-video failure) </span> </label> @@ -117,7 +117,7 @@ export function DownloadStage({ onChange={(e) => setIgnoreArchive(e.target.checked)} /> Ignore archive - <span className="text-xs text-zinc-500"> + <span className="text-xs text-muted-foreground"> (re-fetch even if listed in the archive file) </span> </label> @@ -129,7 +129,7 @@ export function DownloadStage({ aria-label="abort on error for Download videos" /> Abort on error - <span className="text-xs text-zinc-500"> + <span className="text-xs text-muted-foreground"> (stop the run on the first per-video failure) </span> </label> @@ -183,7 +183,7 @@ export function DownloadStage({ aria-label="abort on error for Download missing subs" /> Abort on error - <span className="text-xs text-zinc-500"> + <span className="text-xs text-muted-foreground"> (stop the run on the first per-video failure) </span> </label> @@ -263,22 +263,22 @@ function ExcludedFromDownloadCard({ return ( <div aria-label="excluded from download" - className="flex flex-col gap-2 rounded border border-zinc-200 dark:border-zinc-800 p-3" + className="flex flex-col gap-2 rounded border border-border p-3" > <div> <h4 className="text-sm font-semibold"> Excluded from download ({total}) </h4> - <p className="text-xs text-zinc-500"> + <p className="text-xs text-muted-foreground"> Permanently-unavailable videos (members-only, deleted, or private) are skipped by the download flows. To retry one after it becomes public again, re-check its availability in Diagnostics. </p> </div> - <div className="flex flex-wrap gap-x-3 gap-y-1 text-xs text-zinc-600 dark:text-zinc-400"> + <div className="flex flex-wrap gap-x-3 gap-y-1 text-xs text-muted-foreground"> {sections.map((s) => ( <span key={s.key} aria-label={`excluded ${s.ariaPrefix} count`}> - <span className="font-medium text-zinc-800 dark:text-zinc-200"> + <span className="font-medium text-foreground"> {s.label} </span>{" "} {s.ids.length} @@ -289,7 +289,7 @@ function ExcludedFromDownloadCard({ {sections.map((s) => ( <details key={s.key} - className="rounded border border-zinc-200 dark:border-zinc-800 p-2" + className="rounded border border-border p-2" > <summary className="cursor-pointer text-xs font-semibold"> {s.label} ({s.ids.length}) @@ -342,12 +342,12 @@ function NoTranscriptList({ }) { if (ids.length === 0) return null; return ( - <div className="flex flex-col gap-2 rounded border border-zinc-200 dark:border-zinc-800 p-3"> + <div className="flex flex-col gap-2 rounded border border-border p-3"> <div> <h4 className="text-sm font-semibold"> Missing transcript and no download ({ids.length}) </h4> - <p className="text-xs text-zinc-500"> + <p className="text-xs text-muted-foreground"> Video directory exists but has no transcript or audio. </p> </div> @@ -384,12 +384,12 @@ function PartialDownloadsList({ }) { if (ids.length === 0) return null; return ( - <div className="flex flex-col gap-2 rounded border border-zinc-200 dark:border-zinc-800 p-3"> + <div className="flex flex-col gap-2 rounded border border-border p-3"> <div> <h4 className="text-sm font-semibold"> Partial downloads ({ids.length}) </h4> - <p className="text-xs text-zinc-500"> + <p className="text-xs text-muted-foreground"> Video directory has an <code>audio.<ext>.part</code> file but no completed download. Resume picks up via yt-dlp <code>-c</code>; on audio-integrity channels the <code>.part</code> is probed first @@ -420,7 +420,7 @@ function Heading({ title, desc }: { title: string; desc: string }) { return ( <div> <h3 className="text-base font-semibold">{title}</h3> - <p className="text-sm text-zinc-500">{desc}</p> + <p className="text-sm text-muted-foreground">{desc}</p> </div> ); } diff --git a/editor/app/channels/[slug]/components/stages/PlaylistStage.tsx b/editor/app/channels/[slug]/components/stages/PlaylistStage.tsx @@ -30,7 +30,7 @@ export function PlaylistStage({ if (!hasUrl) { return ( - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> Set a <code>url</code> in Configure to enable the pipeline. </p> ); @@ -93,7 +93,7 @@ export function PlaylistStage({ value={importUrl} onChange={(e) => setImportUrl(e.target.value)} placeholder="https://www.youtube.com/watch?v=…" - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm" + className="rounded border border-border bg-card px-2 py-1 text-sm" /> </label> <StreamActionLog @@ -121,7 +121,7 @@ function Heading({ title, desc }: { title: string; desc: string }) { return ( <div> <h3 className="text-base font-semibold">{title}</h3> - <p className="text-sm text-zinc-500">{desc}</p> + <p className="text-sm text-muted-foreground">{desc}</p> </div> ); } diff --git a/editor/app/channels/[slug]/components/stages/TranscodeStage.tsx b/editor/app/channels/[slug]/components/stages/TranscodeStage.tsx @@ -51,7 +51,7 @@ export function TranscodeStage({ setTranscodeFormat(e.target.value as AudioFormat) } aria-label="transcode failures target format" - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm" + className="rounded border border-border bg-card px-2 py-1 text-sm" > {AUDIO_FORMAT_VALUES.map((f) => ( <option key={f} value={f}> @@ -109,12 +109,12 @@ function UntranscodedList({ const [queue, setQueue] = useState(defaultQueueKey); if (ids.length === 0) return null; return ( - <div className="flex flex-col gap-2 rounded border border-zinc-200 dark:border-zinc-800 p-3"> + <div className="flex flex-col gap-2 rounded border border-border p-3"> <div> <h4 className="text-sm font-semibold"> Has audio but missing target format ({ids.length}) </h4> - <p className="text-xs text-zinc-500"> + <p className="text-xs text-muted-foreground"> Audio file present in a different format than the channel's configured audioFormat — re-run the transcoder to produce audio.<format>. Failures land in the failed-transcodings list @@ -166,13 +166,13 @@ function FailedTranscodingsSection({ return ( <div aria-label="failed transcodings section" - className="flex flex-col gap-2 rounded border border-zinc-200 dark:border-zinc-800 p-3" + className="flex flex-col gap-2 rounded border border-border p-3" > <div> <h4 className="text-sm font-semibold"> Failed transcodings ({ids.length}) </h4> - <p className="text-xs text-zinc-500"> + <p className="text-xs text-muted-foreground"> Videos that ffmpeg refused to transcode. Source files are likely corrupt — the button below deletes their video directories on disk and clears this list. @@ -195,11 +195,11 @@ function FailedTranscodingsSection({ runningLabel="Clearing…" label="Clear failed-transcodings list" /> - <p className="text-xs text-zinc-500"> + <p className="text-xs text-muted-foreground"> Clears the list only — video directories stay on disk. Use this when ffmpeg has been fixed or the listing is stale. </p> - <label className="flex items-center gap-2 text-xs text-zinc-600 dark:text-zinc-400"> + <label className="flex items-center gap-2 text-xs text-muted-foreground"> Type <span className="font-mono">remove</span> to confirm @@ -208,7 +208,7 @@ function FailedTranscodingsSection({ value={confirmText} onChange={(e) => setConfirmText(e.target.value)} aria-label="confirm remove failed transcodings" - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-0.5 font-mono" + className="rounded border border-border bg-card px-2 py-0.5 font-mono" /> </label> <StreamActionLog @@ -238,7 +238,7 @@ function Heading({ title, desc }: { title: string; desc: string }) { return ( <div> <h3 className="text-base font-semibold">{title}</h3> - <p className="text-sm text-zinc-500">{desc}</p> + <p className="text-sm text-muted-foreground">{desc}</p> </div> ); } diff --git a/editor/app/channels/[slug]/components/stages/TranscribeStage.tsx b/editor/app/channels/[slug]/components/stages/TranscribeStage.tsx @@ -160,7 +160,7 @@ function TranscribeMissingSection({ missingShard ? String(missingShard.shardIndex + 1) : "", ); const wrapperClass = demoted - ? "flex flex-col gap-2 rounded border border-zinc-200 dark:border-zinc-800 p-3" + ? "flex flex-col gap-2 rounded border border-border p-3" : "flex flex-col gap-2"; return ( <div className={wrapperClass}> @@ -200,7 +200,7 @@ function TranscribeMissingSection({ strict={missingStrictFormat} onStrictChange={setMissingStrictFormat} /> - <label className="flex items-center gap-1 text-xs text-zinc-500"> + <label className="flex items-center gap-1 text-xs text-muted-foreground"> <input type="checkbox" checked={missingReverse} @@ -243,13 +243,13 @@ function FailedTranscriptionsSection({ return ( <div aria-label="failed transcriptions section" - className="flex flex-col gap-2 rounded border border-zinc-200 dark:border-zinc-800 p-3" + className="flex flex-col gap-2 rounded border border-border p-3" > <div> <h4 className="text-sm font-semibold"> Failed transcriptions ({ids.length}) </h4> - <p className="text-xs text-zinc-500"> + <p className="text-xs text-muted-foreground"> Whisper failed on these videos and they're skipped by “Transcribe missing”. Clearing the list lets the next “Transcribe missing” retry them. @@ -265,7 +265,7 @@ function FailedTranscriptionsSection({ /> {ids.length > 0 ? ( <div className="flex flex-col gap-2"> - <label className="flex items-center gap-2 text-xs text-zinc-600 dark:text-zinc-400"> + <label className="flex items-center gap-2 text-xs text-muted-foreground"> Type <span className="font-mono">clear</span> to confirm @@ -274,7 +274,7 @@ function FailedTranscriptionsSection({ value={confirmText} onChange={(e) => setConfirmText(e.target.value)} aria-label="confirm clear failed transcriptions" - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-0.5 font-mono" + className="rounded border border-border bg-card px-2 py-0.5 font-mono" /> </label> <StreamActionLog @@ -317,14 +317,14 @@ function AudioFormatControl({ }) { const strictDisabled = value === "any"; return ( - <span className="flex items-center gap-2 text-xs text-zinc-500"> + <span className="flex items-center gap-2 text-xs text-muted-foreground"> <label className="flex items-center gap-1"> Audio format <select value={value} onChange={(e) => onChange(e.target.value as AudioFormatChoice)} aria-label={`audio format for ${actionLabel}`} - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-1 py-0.5" + className="rounded border border-border bg-card px-1 py-0.5" > <option value="any">any</option> {AUDIO_FORMAT_VALUES.map((f) => ( @@ -370,7 +370,7 @@ function Heading({ > {title} </h3> - <p className={small ? "text-xs text-zinc-500" : "text-sm text-zinc-500"}> + <p className={small ? "text-xs text-muted-foreground" : "text-sm text-muted-foreground"}> {desc} </p> </div> diff --git a/editor/app/channels/[slug]/page.tsx b/editor/app/channels/[slug]/page.tsx @@ -456,7 +456,7 @@ export default async function ChannelDetailPage({ )} {selectedVideoId && selectedTitle && ( <p - className="text-sm text-zinc-500 mt-2 truncate" + className="text-sm text-muted-foreground mt-2 truncate" aria-label="video title" > {selectedTitle} @@ -468,7 +468,7 @@ export default async function ChannelDetailPage({ <details open - className="rounded-md border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900" + className="rounded-md border border-border bg-card" > <summary className="cursor-pointer px-4 py-3 text-sm font-medium select-none"> Channel pipeline & settings @@ -489,7 +489,7 @@ function EmptyVideoPanel({ filtered: number; }) { return ( - <div className="flex items-center justify-center text-sm text-zinc-500 border border-dashed border-zinc-200 dark:border-zinc-800 rounded-md p-12 text-center"> + <div className="flex items-center justify-center text-sm text-muted-foreground border border-dashed border-border rounded-md p-12 text-center"> <div className="flex flex-col gap-1"> <p>Select a video from the list to view actions.</p> <p className="text-xs"> diff --git a/editor/app/channels/[slug]/videos/[id]/components/MediaPlayer.tsx b/editor/app/channels/[slug]/videos/[id]/components/MediaPlayer.tsx @@ -75,7 +75,7 @@ export function MediaPlayer({ src, kind, filename }: Props) { preload="metadata" src={src} aria-label={`media player for ${filename}`} - className="w-full max-w-md max-h-64 bg-black rounded" + className="w-full max-w-md max-h-64 bg-muted rounded" /> )} <StatusLine state={state} filename={filename} /> @@ -93,7 +93,7 @@ function StatusLine({ if (state.phase === "loading") { return ( <p - className="text-xs text-zinc-500" + className="text-xs text-muted-foreground" aria-label={`media status for ${filename}`} > Loading metadata… @@ -105,7 +105,7 @@ function StatusLine({ <p role="alert" aria-label={`media error for ${filename}`} - className="text-xs text-red-600 dark:text-red-400" + className="text-xs text-destructive" > Malformed: {state.message} </p> @@ -113,7 +113,7 @@ function StatusLine({ } return ( <p - className="text-xs text-emerald-700 dark:text-emerald-400" + className="text-xs text-success" aria-label={`media status for ${filename}`} > Decodes OK diff --git a/editor/app/channels/[slug]/videos/[id]/components/TextFilePreview.tsx b/editor/app/channels/[slug]/videos/[id]/components/TextFilePreview.tsx @@ -59,19 +59,19 @@ export function TextFilePreview({ src, filename }: Props) { className="text-sm" aria-label={`preview ${filename}`} > - <summary className="cursor-pointer text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-100"> + <summary className="cursor-pointer text-muted-foreground hover:text-foreground"> Preview contents </summary> <div className="mt-2"> {phase.kind === "loading" && ( - <p role="status" className="text-xs text-zinc-500"> + <p role="status" className="text-xs text-muted-foreground"> Loading… </p> )} {phase.kind === "error" && ( <p role="alert" - className="text-xs text-red-600 dark:text-red-400" + className="text-xs text-destructive" > Failed to load: {phase.message} </p> @@ -79,7 +79,7 @@ export function TextFilePreview({ src, filename }: Props) { {phase.kind === "ready" && ( <pre aria-label={`preview of ${filename}`} - className="text-xs font-mono bg-zinc-100 dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded p-3 h-96 overflow-auto whitespace-pre-wrap" + className="text-xs font-mono bg-muted border border-border rounded p-3 h-96 overflow-auto whitespace-pre-wrap" > {phase.text} </pre> diff --git a/editor/app/channels/[slug]/videos/[id]/components/VideoPanel.tsx b/editor/app/channels/[slug]/videos/[id]/components/VideoPanel.tsx @@ -250,7 +250,7 @@ export function VideoPanel({ <div role="status" aria-label="media archived" - className="rounded border px-3 py-2 text-sm border-sky-300 bg-sky-50 text-sky-900 dark:border-sky-900 dark:bg-sky-950 dark:text-sky-200" + className="rounded border px-3 py-2 text-sm border-info/30 bg-info-soft text-info" > Media archived — cleanup will skip this video. </div> @@ -467,19 +467,19 @@ function VideoNavStrip({ <Link href={prevHref} aria-label="previous video" - className="px-2 py-1 rounded border border-zinc-200 dark:border-zinc-800 hover:bg-zinc-100 dark:hover:bg-zinc-800" + className="px-2 py-1 rounded border border-border hover:bg-muted" > ◀ prev </Link> ) : ( <span aria-hidden="true" - className="px-2 py-1 rounded border border-zinc-100 dark:border-zinc-900 text-zinc-400 dark:text-zinc-600" + className="px-2 py-1 rounded border border-border text-muted-foreground" > ◀ prev </span> )} - <span className="font-mono text-xs text-zinc-500 truncate"> + <span className="font-mono text-xs text-muted-foreground truncate"> {position ? `${position.index + 1} / ${position.total} · ${videoId}` : videoId} @@ -488,14 +488,14 @@ function VideoNavStrip({ <Link href={nextHref} aria-label="next video" - className="px-2 py-1 rounded border border-zinc-200 dark:border-zinc-800 hover:bg-zinc-100 dark:hover:bg-zinc-800" + className="px-2 py-1 rounded border border-border hover:bg-muted" > next ▶ </Link> ) : ( <span aria-hidden="true" - className="px-2 py-1 rounded border border-zinc-100 dark:border-zinc-900 text-zinc-400 dark:text-zinc-600" + className="px-2 py-1 rounded border border-border text-muted-foreground" > next ▶ </span> @@ -519,7 +519,7 @@ function PipelineStatusStrip({ <div role="status" aria-label="pipeline status" - className="flex flex-wrap items-center gap-3 text-xs text-zinc-600 dark:text-zinc-300" + className="flex flex-wrap items-center gap-3 text-xs text-muted-foreground" > <StatusDot on={downloaded} @@ -542,10 +542,10 @@ function StatusDot({ tone?: "danger"; }) { const color = on - ? "bg-emerald-500" + ? "bg-success" : tone === "danger" - ? "bg-red-500" - : "bg-zinc-300 dark:bg-zinc-700"; + ? "bg-destructive" + : "bg-muted"; return ( <span className="inline-flex items-center gap-1.5"> <span @@ -616,14 +616,14 @@ function RedownloadSection({ return ( <div className="flex flex-col gap-3"> - <p className="text-sm text-zinc-500">{desc}</p> + <p className="text-sm text-muted-foreground">{desc}</p> <label className="flex items-center gap-2 text-sm"> Mode <select value={mode} onChange={(e) => setMode(e.target.value as DownloadMode)} aria-label="download mode" - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm" + className="rounded border border-border bg-card px-2 py-1 text-sm" > <option value="pipeline">Designated pipeline</option> <option value="whisper">Audio + Whisper (skip pipeline)</option> @@ -636,7 +636,7 @@ function RedownloadSection({ value={downloadFormat} onChange={(e) => setDownloadFormat(e.target.value)} aria-label="download format" - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm" + className="rounded border border-border bg-card px-2 py-1 text-sm" > <option value="">Inherit (channel / global)</option> {DOWNLOAD_FORMAT_PRESETS.map((p) => ( @@ -683,7 +683,7 @@ function PerFileTranscribeRow({ const [transcribeQueue, setTranscribeQueue] = useState(""); return ( - <div className="flex flex-col gap-2 rounded border border-zinc-200 dark:border-zinc-800 p-3"> + <div className="flex flex-col gap-2 rounded border border-border p-3"> <Heading title={`Transcribe ${file.name}`} desc={ @@ -693,7 +693,7 @@ function PerFileTranscribeRow({ } /> {hasTranscript ? ( - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> Transcribe disabled — transcript already present. </p> ) : ( @@ -744,7 +744,7 @@ function PerFileTranscodeRow({ if (transcodeTargets.length === 0) return null; return ( - <div className="flex flex-col gap-2 rounded border border-zinc-200 dark:border-zinc-800 p-3"> + <div className="flex flex-col gap-2 rounded border border-border p-3"> <Heading title={`Transcode ${file.name}`} desc="Re-encode this audio file with ffmpeg. The source file is kept; the output is named audio.<format>." @@ -755,7 +755,7 @@ function PerFileTranscodeRow({ value={target} onChange={(e) => setTarget(e.target.value as AudioFormat)} aria-label={`transcode target for ${file.name}`} - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm" + className="rounded border border-border bg-card px-2 py-1 text-sm" > {transcodeTargets.map((t) => ( <option key={t} value={t}> @@ -796,7 +796,7 @@ function FilesList({ files: VideoFile[]; }) { if (files.length === 0) { - return <p className="text-sm text-zinc-500">Directory is empty.</p>; + return <p className="text-sm text-muted-foreground">Directory is empty.</p>; } return ( <ul @@ -808,7 +808,7 @@ function FilesList({ return ( <li key={f.name} - className="border border-zinc-200 dark:border-zinc-800 rounded-md p-3 flex flex-col gap-2" + className="border border-border rounded-md p-3 flex flex-col gap-2" > <div className="flex items-baseline justify-between gap-3 flex-wrap"> <span @@ -817,7 +817,7 @@ function FilesList({ > {f.name} </span> - <span className="text-xs text-zinc-500"> + <span className="text-xs text-muted-foreground"> {formatBytes(f.size)} · {new Date(f.mtime).toLocaleString()} </span> </div> @@ -870,7 +870,7 @@ function TranscriptSourceSection({ return ( <div className="flex flex-col gap-3"> - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> Pick which subtitle track is this video's primary transcript. The chosen track is copied to <code>{CANONICAL_VTT}</code> — the canonical name the index and viewer read. Reversible: delete{" "} @@ -879,7 +879,7 @@ function TranscriptSourceSection({ </p> {hasWhisper && ( <p - className="text-sm text-amber-700 dark:text-amber-400" + className="text-sm text-warning" aria-label="whisper precedence note" > A whisper transcript (<code>{WHISPER_FILENAME}</code>) is present and @@ -893,14 +893,14 @@ function TranscriptSourceSection({ <li key={name} aria-label={`transcript track ${name}`} - className="flex items-center justify-between gap-3 rounded border border-zinc-200 dark:border-zinc-800 px-3 py-2" + className="flex items-center justify-between gap-3 rounded border border-border px-3 py-2" > <span className="flex items-center gap-2 font-mono text-sm"> {name} {isPrimary && ( <span aria-label={`primary transcript ${name}`} - className="rounded bg-emerald-100 dark:bg-emerald-950 text-emerald-700 dark:text-emerald-300 px-1.5 py-0.5 text-[10px] font-sans font-medium uppercase tracking-wide" + className="rounded bg-success-soft text-success px-1.5 py-0.5 text-[10px] font-sans font-medium uppercase tracking-wide" > Primary </span> @@ -912,7 +912,7 @@ function TranscriptSourceSection({ disabled={pending} aria-label={`set ${name} as primary transcript`} onClick={() => setPrimary(name)} - className="shrink-0 px-2 py-1 rounded border border-zinc-300 dark:border-zinc-700 text-xs hover:bg-zinc-100 dark:hover:bg-zinc-800 disabled:opacity-50" + className="shrink-0 px-2 py-1 rounded border border-border text-xs hover:bg-muted disabled:opacity-50" > {pending && busyFile === name ? "Setting…" @@ -925,7 +925,7 @@ function TranscriptSourceSection({ </ul> {error && ( <span - className="text-sm text-red-600 dark:text-red-400" + className="text-sm text-destructive" aria-label="set primary transcript error" > {error} @@ -946,7 +946,7 @@ function MarkUntranscribableSection({ const [error, setError] = useState<string | null>(null); return ( <div className="flex flex-col gap-2"> - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> Writes an empty transcript.json so the pipeline stops retrying. Removes this video from the failed list. Reversible — just delete transcript.json. @@ -962,13 +962,13 @@ function MarkUntranscribableSection({ if (!res.ok) setError(res.error); }); }} - className="self-start px-3 py-2 rounded-md bg-zinc-900 dark:bg-zinc-100 text-zinc-100 dark:text-zinc-900 text-sm font-medium hover:opacity-90 disabled:opacity-50" + className="self-start px-3 py-2 rounded-md bg-primary text-primary-foreground text-sm font-medium hover:opacity-90 disabled:opacity-50" > {pending ? "Marking…" : "Mark untranscribable"} </button> {error && ( <span - className="text-sm text-red-600 dark:text-red-400" + className="text-sm text-destructive" aria-label="mark untranscribable error" > {error} @@ -999,7 +999,7 @@ function SourceVideoSection({ if (handling !== "transcribe") { return ( - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> Source-video persistence applies to transcribe-handling channels only. </p> ); @@ -1008,37 +1008,37 @@ function SourceVideoSection({ if (savedVideo) { return ( <div className="flex flex-col gap-3"> - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> The source container for this video is persisted to the saved-video store, so the Clean-audio sweep can reclaim its <code>audio.*</code>{" "} without losing the original. Transcription falls back to it when no audio is on disk. </p> <dl className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-sm"> - <dt className="text-zinc-500">File</dt> + <dt className="text-muted-foreground">File</dt> <dd className="font-mono break-all">{savedVideo.file}</dd> - <dt className="text-zinc-500">Size</dt> + <dt className="text-muted-foreground">Size</dt> <dd className="tabular-nums">{formatBytes(savedVideo.bytes)}</dd> - <dt className="text-zinc-500">Stored</dt> + <dt className="text-muted-foreground">Stored</dt> <dd>{new Date(savedVideo.storedAt).toLocaleString()}</dd> {savedVideo.keepReason && ( <> - <dt className="text-zinc-500">Reason</dt> + <dt className="text-muted-foreground">Reason</dt> <dd>{savedVideo.keepReason}</dd> </> )} {savedVideo.sha256 && ( <> - <dt className="text-zinc-500">sha256</dt> + <dt className="text-muted-foreground">sha256</dt> <dd className="font-mono text-xs break-all"> {savedVideo.sha256} </dd> </> )} - <dt className="text-zinc-500">Location</dt> + <dt className="text-muted-foreground">Location</dt> <dd className="font-mono text-xs break-all">{savedVideo.dir}</dd> </dl> - <p className="text-xs text-zinc-500"> + <p className="text-xs text-muted-foreground"> Unpersist moves the container back into this video's data dir (where it appears under Files and plays inline) and removes the pointer. @@ -1054,13 +1054,13 @@ function SourceVideoSection({ if (!res.ok) setError(res.error); }); }} - className="self-start px-3 py-2 rounded-md border border-zinc-300 dark:border-zinc-700 text-sm font-medium hover:bg-zinc-100 dark:hover:bg-zinc-800 disabled:opacity-50" + className="self-start px-3 py-2 rounded-md border border-border text-sm font-medium hover:bg-muted disabled:opacity-50" > {pending ? "Restoring…" : "Unpersist (move back to data dir)"} </button> {error && ( <span - className="text-sm text-red-600 dark:text-red-400" + className="text-sm text-destructive" aria-label="unpersist error" > {error} @@ -1072,7 +1072,7 @@ function SourceVideoSection({ return ( <div className="flex flex-col gap-3"> - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> Re-fetch this video's full source container and move it into the saved-video store, without disturbing the existing transcript. Use this to archive a video that was downloaded audio-only. (Videos in a @@ -1112,7 +1112,7 @@ function DoNotCleanSection({ const next = !doNotClean; return ( <div className="flex flex-col gap-2"> - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> {doNotClean ? "This video is marked “do not clean”. The transcribed-audio and extra-format cleanup jobs will skip it, so its media is preserved for archiving. Reversible — allow cleanup to remove the marker." : "Mark this video “do not clean” so the cleanup jobs never delete its audio (e.g. a deleted or members-only upload you want to keep). Reversible at any time."} @@ -1132,7 +1132,7 @@ function DoNotCleanSection({ if (!res.ok) setError(res.error); }); }} - className="self-start px-3 py-2 rounded-md bg-zinc-900 dark:bg-zinc-100 text-zinc-100 dark:text-zinc-900 text-sm font-medium hover:opacity-90 disabled:opacity-50" + className="self-start px-3 py-2 rounded-md bg-primary text-primary-foreground text-sm font-medium hover:opacity-90 disabled:opacity-50" > {pending ? "Saving…" @@ -1142,7 +1142,7 @@ function DoNotCleanSection({ </button> {error && ( <span - className="text-sm text-red-600 dark:text-red-400" + className="text-sm text-destructive" aria-label="do not clean error" > {error} @@ -1235,13 +1235,13 @@ function DeleteFileButton({ if (!res.ok) setError(res.error); }); }} - className="self-start px-2 py-1 rounded border border-red-300 dark:border-red-800 text-red-700 dark:text-red-400 text-xs hover:bg-red-50 dark:hover:bg-red-950 disabled:opacity-50" + className="self-start px-2 py-1 rounded border border-destructive/30 text-destructive text-xs hover:bg-destructive-soft disabled:opacity-50" > {pending ? "Deleting…" : "Delete file"} </button> {error && ( <span - className="text-xs text-red-600 dark:text-red-400" + className="text-xs text-destructive" aria-label={`delete error ${filename}`} > {error} @@ -1265,7 +1265,7 @@ function DeleteVideoDirSection({ ); return ( <div className="flex flex-col gap-3"> - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> Removes data/{videoId}/ and all of its contents. Type the video id to confirm. </p> @@ -1279,17 +1279,17 @@ function DeleteVideoDirSection({ required placeholder={videoId} aria-label="confirm video id" - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm font-mono" + className="rounded border border-border bg-card px-2 py-1 text-sm font-mono" /> <button type="submit" - className="px-3 py-1.5 rounded-md bg-red-600 text-white text-sm font-medium hover:bg-red-700" + className="px-3 py-1.5 rounded-md bg-destructive text-destructive-foreground text-sm font-medium hover:bg-destructive/90" > Delete directory </button> {state?.error && ( <p - className="basis-full text-sm text-red-600 dark:text-red-400" + className="basis-full text-sm text-destructive" aria-label="delete directory error" > {state.error} @@ -1309,7 +1309,7 @@ function AvailabilityHistoryList({ return ( <p aria-label="availability history empty" - className="text-sm text-zinc-600 dark:text-zinc-400" + className="text-sm text-muted-foreground" > No availability changes recorded. </p> @@ -1328,13 +1328,13 @@ function AvailabilityHistoryList({ aria-label={`availability history ${e.availability}`} className="flex flex-wrap items-baseline gap-x-2" > - <span className="text-zinc-500 tabular-nums"> + <span className="text-muted-foreground tabular-nums"> {new Date(e.observedAt).toLocaleString()} </span> - <span className="font-medium text-zinc-800 dark:text-zinc-200"> + <span className="font-medium text-foreground"> {e.availability} </span> - <span className="text-xs text-zinc-500">({e.source})</span> + <span className="text-xs text-muted-foreground">({e.source})</span> </li> ))} </ul> @@ -1349,8 +1349,8 @@ function DownloadOutcomeBadge({ if (outcome.status === "ok") return null; const tone = outcome.status === "failed" - ? "border-red-300 bg-red-50 text-red-900 dark:border-red-900 dark:bg-red-950 dark:text-red-200" - : "border-amber-300 bg-amber-50 text-amber-900 dark:border-amber-900 dark:bg-amber-950 dark:text-amber-200"; + ? "border-destructive/30 bg-destructive-soft text-destructive" + : "border-warning/30 bg-warning-soft text-warning"; const label = ((): string => { switch (outcome.status) { case "ok-with-cookies": @@ -1427,7 +1427,7 @@ function IncompleteTranscriptBanner({ <div role="alert" aria-label="incomplete transcript" - className="flex flex-col gap-3 rounded border px-3 py-2 text-sm border-amber-300 bg-amber-50 text-amber-900 dark:border-amber-900 dark:bg-amber-950 dark:text-amber-200" + className="flex flex-col gap-3 rounded border px-3 py-2 text-sm border-warning/30 bg-warning-soft text-warning" > <div className="flex flex-col gap-1"> <span className="font-medium">Transcript looks truncated</span> @@ -1488,7 +1488,7 @@ function ShortAudioBanner({ <div role="alert" aria-label="short audio" - className="flex flex-col gap-3 rounded border px-3 py-2 text-sm border-amber-300 bg-amber-50 text-amber-900 dark:border-amber-900 dark:bg-amber-950 dark:text-amber-200" + className="flex flex-col gap-3 rounded border px-3 py-2 text-sm border-warning/30 bg-warning-soft text-warning" > <div className="flex flex-col gap-1"> <span className="font-medium">Download was truncated at the source</span> @@ -1528,7 +1528,7 @@ function Heading({ title, desc }: { title: string; desc: string }) { return ( <div> <h3 className="text-base font-semibold">{title}</h3> - <p className="text-sm text-zinc-500">{desc}</p> + <p className="text-sm text-muted-foreground">{desc}</p> </div> ); } diff --git a/editor/app/channels/[slug]/videos/[id]/page.tsx b/editor/app/channels/[slug]/videos/[id]/page.tsx @@ -146,17 +146,17 @@ export default async function VideoDetailPage({ return ( <div className="flex flex-col gap-6"> - <div className="flex items-center gap-2 text-sm text-zinc-500"> + <div className="flex items-center gap-2 text-sm text-muted-foreground"> <Link href="/channels" - className="underline hover:text-zinc-900 dark:hover:text-zinc-100" + className="underline hover:text-foreground" > Channels </Link> <span>/</span> <Link href={`/channels/${slug}`} - className="underline hover:text-zinc-900 dark:hover:text-zinc-100 font-mono" + className="underline hover:text-foreground font-mono" > {slug} </Link> @@ -166,13 +166,13 @@ export default async function VideoDetailPage({ <header className="flex flex-col gap-2"> <h1 className="text-2xl font-semibold">{meta.title ?? id}</h1> - <div className="flex flex-wrap gap-3 text-sm text-zinc-500"> + <div className="flex flex-wrap gap-3 text-sm text-muted-foreground"> {meta.webpageUrl && ( <a href={meta.webpageUrl} target="_blank" rel="noreferrer" - className="underline hover:text-zinc-900 dark:hover:text-zinc-100" + className="underline hover:text-foreground" > Open on {new URL(meta.webpageUrl).hostname} </a> diff --git a/editor/app/channels/components/ChannelAvailabilityButton.tsx b/editor/app/channels/components/ChannelAvailabilityButton.tsx @@ -49,7 +49,7 @@ export function ChannelAvailabilityButton({ onClick={handleClick} disabled={running || disabled} aria-label={`check availability ${slug}`} - className="px-2 py-1 rounded-md bg-zinc-200 dark:bg-zinc-800 text-zinc-900 dark:text-zinc-100 text-xs font-medium hover:opacity-90 disabled:opacity-50" + className="px-2 py-1 rounded-md bg-muted text-foreground text-xs font-medium hover:opacity-90 disabled:opacity-50" > {running ? "Checking…" : "Check availability"} </button> @@ -57,7 +57,7 @@ export function ChannelAvailabilityButton({ <span role="alert" aria-label={`check availability ${slug} error`} - className="text-xs text-red-700 dark:text-red-400" + className="text-xs text-destructive" > {status.message} </span> @@ -65,7 +65,7 @@ export function ChannelAvailabilityButton({ {status.kind === "running" && status.jobId && ( <span aria-label={`check availability ${slug} job`} - className="text-xs text-zinc-500 font-mono" + className="text-xs text-muted-foreground font-mono" > job {status.jobId} </span> @@ -73,7 +73,7 @@ export function ChannelAvailabilityButton({ {status.kind === "done" && ( <span aria-label={`check availability ${slug} done`} - className="text-xs text-zinc-500" + className="text-xs text-muted-foreground" > done (job {status.jobId}) </span> diff --git a/editor/app/channels/components/ChannelBuildToggle.tsx b/editor/app/channels/components/ChannelBuildToggle.tsx @@ -27,15 +27,15 @@ export function ChannelBuildToggle({ className={ "inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium border transition-colors " + (excluded - ? "border-zinc-300 dark:border-zinc-700 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-zinc-800" - : "border-emerald-300 dark:border-emerald-800 bg-emerald-50 dark:bg-emerald-950/40 text-emerald-700 dark:text-emerald-300 hover:bg-emerald-100 dark:hover:bg-emerald-900/40") + ? "border-border text-muted-foreground hover:bg-muted" + : "border-success/30 bg-success-soft text-success hover:bg-success/20") } > <span aria-hidden="true" className={ "inline-block h-1.5 w-1.5 rounded-full " + - (excluded ? "bg-zinc-400" : "bg-emerald-500") + (excluded ? "bg-muted-foreground" : "bg-success") } /> {excluded ? "Skipped" : "Included"} diff --git a/editor/app/channels/components/ChannelForm.tsx b/editor/app/channels/components/ChannelForm.tsx @@ -42,7 +42,7 @@ export function ChannelForm({ <input key={name} type="hidden" name={name} defaultValue={value} /> ))} {errorMessage && ( - <div className="rounded border border-red-300 bg-red-50 dark:border-red-800 dark:bg-red-950 px-3 py-2 text-sm text-red-700 dark:text-red-300"> + <div className="rounded border border-destructive/30 bg-destructive-soft px-3 py-2 text-sm text-destructive"> {errorMessage} </div> )} @@ -69,7 +69,7 @@ export function ChannelForm({ placeholder="kebab-case (auto-derived from name if blank)" /> )} - <p className="text-xs text-zinc-500"> + <p className="text-xs text-muted-foreground"> Channel grouping is now configured per site under{" "} <a href="/sites" className="underline"> Sites @@ -111,7 +111,7 @@ export function ChannelForm({ <select name="platform" defaultValue={c?.platform ?? ""} - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm" + className="rounded border border-border bg-card px-2 py-1 text-sm" > <option value="">Auto (detect from URL)</option> <option value="youtube">YouTube</option> @@ -119,7 +119,7 @@ export function ChannelForm({ <option value="odysee">Odysee</option> <option value="twitch">Twitch</option> </select> - <span className="text-xs text-zinc-500"> + <span className="text-xs text-muted-foreground"> Used as the default job queue, so all channels on the same platform run sequentially. </span> @@ -130,12 +130,12 @@ export function ChannelForm({ <label className="flex flex-col gap-1 text-sm"> <span className="font-medium"> Audio format{" "} - <span className="text-zinc-500 font-normal">(transcribe only)</span> + <span className="text-muted-foreground font-normal">(transcribe only)</span> </span> <select name="audioFormat" defaultValue={c?.audioFormat ?? ""} - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm" + className="rounded border border-border bg-card px-2 py-1 text-sm" > <option value="">Default (mp3)</option> <option value="m4a">m4a</option> @@ -148,7 +148,7 @@ export function ChannelForm({ <select name="downloadFormat" defaultValue={c?.downloadFormat ?? ""} - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm" + className="rounded border border-border bg-card px-2 py-1 text-sm" > <option value="">Inherit (global default)</option> {DOWNLOAD_FORMAT_PRESETS.map((p) => ( @@ -157,7 +157,7 @@ export function ChannelForm({ </option> ))} </select> - <span className="text-xs text-zinc-500"> + <span className="text-xs text-muted-foreground"> The yt-dlp <code>-f</code> selector for this channel's downloads. Leave on Inherit to use the global default; choose{" "} <strong>Original</strong> for sources that only serve full-length @@ -174,11 +174,11 @@ export function ChannelForm({ <span className="flex flex-col gap-0.5"> <span className="font-medium"> Keep source video after extraction{" "} - <span className="text-zinc-500 font-normal"> + <span className="text-muted-foreground font-normal"> (transcribe only) </span> </span> - <span className="text-xs text-zinc-500"> + <span className="text-xs text-muted-foreground"> Adds <code>-k</code> to yt-dlp so the original container is kept alongside <code>audio.<format></code>. Useful for debugging corrupt-audio extraction issues. @@ -198,9 +198,9 @@ export function ChannelForm({ max={100000} placeholder="0 (off)" aria-label="keep latest" - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm w-32" + className="rounded border border-border bg-card px-2 py-1 text-sm w-32" /> - <span className="text-xs text-zinc-500"> + <span className="text-xs text-muted-foreground"> Protect the newest N videos from the Clean-audio sweep and persist their source containers to the saved-video store. A kept video later found deleted-from-source is pinned permanently. Blank or 0 disables. @@ -212,13 +212,13 @@ export function ChannelForm({ name="extractionMode" defaultValue={c?.extractionMode ?? ""} aria-label="extraction mode" - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm" + className="rounded border border-border bg-card px-2 py-1 text-sm" > <option value="">Default (yt-dlp extracts)</option> <option value="ytdlp">yt-dlp (-x postprocessor)</option> <option value="app">App (download container, ffmpeg extracts)</option> </select> - <span className="text-xs text-zinc-500"> + <span className="text-xs text-muted-foreground"> Who extracts audio for transcribe-handling downloads. App mode keeps the source container in hand so it can be persisted; the keep-latest rule forces app mode for the videos it persists regardless. @@ -236,19 +236,19 @@ export function ChannelForm({ <label className="flex flex-col gap-1 text-sm"> <span className="font-medium"> Extra yt-dlp args - <span className="text-zinc-500 font-normal"> (one per line)</span> + <span className="text-muted-foreground font-normal"> (one per line)</span> </span> <textarea name="ytdlpExtraArgs" defaultValue={(c?.ytdlpExtraArgs ?? []).join("\n")} rows={4} - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm font-mono" + className="rounded border border-border bg-card px-2 py-1 text-sm font-mono" /> </label> <label className="flex flex-col gap-1 text-sm"> <span className="font-medium"> Sleep between managed downloads - <span className="text-zinc-500 font-normal"> + <span className="text-muted-foreground font-normal"> {" "} (seconds, optional) </span> @@ -263,9 +263,9 @@ export function ChannelForm({ } min={0} max={600} - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm" + className="rounded border border-border bg-card px-2 py-1 text-sm" /> - <span className="text-xs text-zinc-500"> + <span className="text-xs text-muted-foreground"> Pause inserted between per-video yt-dlp invocations for this channel's managed downloads. Leave blank to use the global default from /settings. 0 disables. @@ -275,7 +275,7 @@ export function ChannelForm({ <div className="flex gap-2"> <button type="submit" - className="px-3 py-2 rounded-md bg-zinc-900 dark:bg-zinc-100 text-zinc-100 dark:text-zinc-900 text-sm font-medium hover:opacity-90" + className="px-3 py-2 rounded-md bg-primary text-primary-foreground text-sm font-medium hover:opacity-90" > {submitLabel} </button> @@ -292,7 +292,7 @@ function Section({ children: React.ReactNode; }) { return ( - <fieldset className="flex flex-col gap-3 border border-zinc-200 dark:border-zinc-800 rounded-md px-4 py-3"> + <fieldset className="flex flex-col gap-3 border border-border rounded-md px-4 py-3"> <legend className="px-1 text-sm font-semibold">{title}</legend> {children} </fieldset> @@ -307,12 +307,12 @@ function CollapsibleSection({ children: React.ReactNode; }) { return ( - <details className="group border border-zinc-200 dark:border-zinc-800 rounded-md"> - <summary className="cursor-pointer list-none flex items-center justify-between gap-2 px-4 py-2 text-sm font-semibold select-none rounded-md hover:bg-zinc-50 dark:hover:bg-zinc-800/50"> + <details className="group border border-border rounded-md"> + <summary className="cursor-pointer list-none flex items-center justify-between gap-2 px-4 py-2 text-sm font-semibold select-none rounded-md hover:bg-muted"> <span>{title}</span> <svg aria-hidden="true" - className="w-4 h-4 text-zinc-400 transition-transform group-open:rotate-90" + className="w-4 h-4 text-muted-foreground transition-transform group-open:rotate-90" viewBox="0 0 20 20" fill="currentColor" > @@ -323,7 +323,7 @@ function CollapsibleSection({ /> </svg> </summary> - <div className="px-4 pt-2 pb-4 flex flex-col gap-3 border-t border-zinc-100 dark:border-zinc-800"> + <div className="px-4 pt-2 pb-4 flex flex-col gap-3 border-t border-border"> {children} </div> </details> @@ -339,7 +339,7 @@ function SyncIntervalField({ value }: { value?: number }) { <select name="syncIntervalMinutes" defaultValue={current} - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm" + className="rounded border border-border bg-card px-2 py-1 text-sm" > {SYNC_INTERVAL_PRESETS.map((p) => ( <option key={p.value} value={p.value}> @@ -351,7 +351,7 @@ function SyncIntervalField({ value }: { value?: number }) { <option value={current}>{`Every ${current} minutes`}</option> )} </select> - <span className="text-xs text-zinc-500"> + <span className="text-xs text-muted-foreground"> How often the scheduled sync system auto-syncs this channel, when the global scheduler is enabled in{" "} <a href="/settings" className="underline"> @@ -379,11 +379,11 @@ function AudioCheckFields({ config }: { config?: ChannelConfig }) { <span className="flex flex-col gap-0.5"> <span className="font-medium"> Audio-integrity checking{" "} - <span className="text-zinc-500 font-normal"> + <span className="text-muted-foreground font-normal"> (transcribe only, slow) </span> </span> - <span className="text-xs text-zinc-500"> + <span className="text-xs text-muted-foreground"> For sources that intermittently serve corrupt audio (e.g. Odysee "original" format). Periodically probes the in-progress download via ffmpeg and rolls back to the last known-good snapshot on @@ -404,7 +404,7 @@ function AudioCheckFields({ config }: { config?: ChannelConfig }) { min={AUDIO_CHECK_INTERVAL_MIN_SECONDS} max={AUDIO_CHECK_INTERVAL_MAX_SECONDS} aria-label="audio-check interval seconds" - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm w-32" + className="rounded border border-border bg-card px-2 py-1 text-sm w-32" /> </label> <label className="flex flex-col gap-1 text-sm pl-6"> @@ -419,9 +419,9 @@ function AudioCheckFields({ config }: { config?: ChannelConfig }) { min={AUDIO_CHECK_MAX_ROLLBACKS_MIN} max={AUDIO_CHECK_MAX_ROLLBACKS_MAX} aria-label="audio-check max rollbacks" - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm w-32" + className="rounded border border-border bg-card px-2 py-1 text-sm w-32" /> - <span className="text-xs text-zinc-500"> + <span className="text-xs text-muted-foreground"> After this many rollbacks against the same checkpoint, the download is marked <code>failed-corrupt-source</code>. </span> @@ -438,9 +438,9 @@ function AudioCheckFields({ config }: { config?: ChannelConfig }) { min={AUDIO_CHECK_COPY_TIMEOUT_MIN_SECONDS} max={AUDIO_CHECK_COPY_TIMEOUT_MAX_SECONDS} aria-label="audio-check copy timeout seconds" - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm w-32" + className="rounded border border-border bg-card px-2 py-1 text-sm w-32" /> - <span className="text-xs text-zinc-500"> + <span className="text-xs text-muted-foreground"> Max time SIGSTOP can hold yt-dlp while we snapshot the <code>.part</code>{" "} file. Without reflink (btrfs/xfs), large snapshots may exceed CDN keepalive; the check skips itself rather than holding longer. @@ -456,7 +456,7 @@ function AudioCheckFields({ config }: { config?: ChannelConfig }) { /> <span className="flex flex-col gap-0.5"> <span className="font-medium">Resume during probe (legacy)</span> - <span className="text-xs text-zinc-500"> + <span className="text-xs text-muted-foreground"> Keep downloading while each integrity probe runs. Leave off (default) to pause yt-dlp until the probe finishes — it then never downloads bytes a corrupt verdict would discard and re-fetch, @@ -496,9 +496,9 @@ function Field({ placeholder={placeholder} required={required} readOnly={readOnly} - className={`rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm ${readOnly ? "opacity-60" : ""}`} + className={`rounded border border-border bg-card px-2 py-1 text-sm ${readOnly ? "opacity-60" : ""}`} /> - {hint && <span className="text-xs text-zinc-500">{hint}</span>} + {hint && <span className="text-xs text-muted-foreground">{hint}</span>} </label> ); } diff --git a/editor/app/channels/components/ChannelSyncButton.tsx b/editor/app/channels/components/ChannelSyncButton.tsx @@ -48,7 +48,7 @@ export function ChannelSyncButton({ onClick={handleClick} disabled={running || disabled} aria-label={`sync ${slug}`} - className="px-2 py-1 rounded-md bg-zinc-900 dark:bg-zinc-100 text-zinc-100 dark:text-zinc-900 text-xs font-medium hover:opacity-90 disabled:opacity-50" + className="px-2 py-1 rounded-md bg-primary text-primary-foreground text-xs font-medium hover:opacity-90 disabled:opacity-50" > {running ? "Syncing…" : "Sync"} </button> @@ -56,7 +56,7 @@ export function ChannelSyncButton({ <span role="alert" aria-label={`sync ${slug} error`} - className="text-xs text-red-700 dark:text-red-400" + className="text-xs text-destructive" > {status.message} </span> @@ -64,7 +64,7 @@ export function ChannelSyncButton({ {status.kind === "running" && status.jobId && ( <span aria-label={`sync ${slug} job`} - className="text-xs text-zinc-500 font-mono" + className="text-xs text-muted-foreground font-mono" > job {status.jobId} </span> @@ -72,7 +72,7 @@ export function ChannelSyncButton({ {status.kind === "done" && ( <span aria-label={`sync ${slug} done`} - className="text-xs text-zinc-500" + className="text-xs text-muted-foreground" > done (job {status.jobId}) </span> diff --git a/editor/app/channels/components/ChannelSyncToggle.tsx b/editor/app/channels/components/ChannelSyncToggle.tsx @@ -27,15 +27,15 @@ export function ChannelSyncToggle({ className={ "inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium border transition-colors " + (excluded - ? "border-zinc-300 dark:border-zinc-700 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-zinc-800" - : "border-emerald-300 dark:border-emerald-800 bg-emerald-50 dark:bg-emerald-950/40 text-emerald-700 dark:text-emerald-300 hover:bg-emerald-100 dark:hover:bg-emerald-900/40") + ? "border-border text-muted-foreground hover:bg-muted" + : "border-success/30 bg-success-soft text-success hover:bg-success/20") } > <span aria-hidden="true" className={ "inline-block h-1.5 w-1.5 rounded-full " + - (excluded ? "bg-zinc-400" : "bg-emerald-500") + (excluded ? "bg-muted-foreground" : "bg-success") } /> {excluded ? "Skipped" : "Included"} diff --git a/editor/app/channels/components/ChannelsTable.tsx b/editor/app/channels/components/ChannelsTable.tsx @@ -120,8 +120,8 @@ export function ChannelsTable({ channels }: { channels: ChannelStat[] }) { return ( <div className="overflow-x-auto -mx-4 md:mx-0 md:overflow-visible"> - <table className="text-sm border-y md:border border-zinc-200 dark:border-zinc-800 md:rounded-md md:overflow-hidden w-full"> - <thead className="bg-zinc-100 dark:bg-zinc-900"> + <table className="text-sm border-y md:border border-border md:rounded-md md:overflow-hidden w-full"> + <thead className="bg-muted"> <tr> <SortableTh label="Slug" @@ -191,7 +191,7 @@ export function ChannelsTable({ channels }: { channels: ChannelStat[] }) { <tr key={c.slug} className={ - "border-t border-zinc-200 dark:border-zinc-800 " + + "border-t border-border " + (c.config.excludeFromBuild || c.config.excludeFromSync ? "opacity-60" : "") @@ -200,7 +200,7 @@ export function ChannelsTable({ channels }: { channels: ChannelStat[] }) { <Td className="font-mono"> <Link href={`/channels/${c.slug}`} - className="underline hover:text-zinc-900 dark:hover:text-zinc-100" + className="underline hover:text-foreground" > {c.slug} </Link> @@ -225,7 +225,7 @@ export function ChannelsTable({ channels }: { channels: ChannelStat[] }) { > {c.playlistCount ?? "—"} </Td> - <Td className="text-xs text-zinc-500 whitespace-nowrap"> + <Td className="text-xs text-muted-foreground whitespace-nowrap"> {c.config.lastSyncedAt ? new Date(c.config.lastSyncedAt).toLocaleString() : ""} @@ -294,12 +294,12 @@ function SortableTh({ aria-label={`sort by ${label}`} className={ `inline-flex w-full items-center gap-1 ${justify} ` + - "font-medium hover:text-zinc-900 dark:hover:text-zinc-100 cursor-pointer" + "font-medium hover:text-foreground cursor-pointer" } > <span>{label}</span> {indicator && ( - <span aria-hidden="true" className="text-[10px] text-zinc-500"> + <span aria-hidden="true" className="text-[10px] text-muted-foreground"> {indicator} </span> )} diff --git a/editor/app/channels/components/DeleteChannelForm.tsx b/editor/app/channels/components/DeleteChannelForm.tsx @@ -18,7 +18,7 @@ export function DeleteChannelForm({ slug, action }: Props) { ); return ( <form action={formAction} className="flex flex-col gap-2"> - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> Deleting will remove <code>transcripts/channels/{slug}/</code> and all downloaded videos. Type the slug to confirm. </p> @@ -27,17 +27,17 @@ export function DeleteChannelForm({ slug, action }: Props) { name="confirmSlug" required placeholder={slug} - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm font-mono" + className="rounded border border-border bg-card px-2 py-1 text-sm font-mono" /> <button type="submit" - className="px-3 py-1.5 rounded-md bg-red-600 text-white text-sm font-medium hover:bg-red-700" + className="px-3 py-1.5 rounded-md bg-destructive text-destructive-foreground text-sm font-medium hover:bg-destructive/90" > Delete channel </button> </div> {state?.error && ( - <p className="text-sm text-red-600 dark:text-red-400">{state.error}</p> + <p className="text-sm text-destructive">{state.error}</p> )} </form> ); diff --git a/editor/app/channels/components/SyncAllChannelsButton.tsx b/editor/app/channels/components/SyncAllChannelsButton.tsx @@ -34,14 +34,14 @@ export function SyncAllChannelsButton() { onClick={handleClick} disabled={running} aria-label="sync all channels" - className="px-3 py-2 rounded-md bg-zinc-900 dark:bg-zinc-100 text-zinc-100 dark:text-zinc-900 text-sm font-medium hover:opacity-90 disabled:opacity-50" + className="px-3 py-2 rounded-md bg-primary text-primary-foreground text-sm font-medium hover:opacity-90 disabled:opacity-50" > {running ? "Syncing…" : "Sync all"} </button> {status.kind === "done" && ( <span aria-label="sync all result" - className="text-xs text-zinc-500" + className="text-xs text-muted-foreground" title={ status.result.skipped.length === 0 ? undefined @@ -54,7 +54,7 @@ export function SyncAllChannelsButton() { {status.result.skipped.length} ·{" "} <Link href="/jobs" - className="underline hover:text-zinc-900 dark:hover:text-zinc-100" + className="underline hover:text-foreground" > view jobs </Link> @@ -64,7 +64,7 @@ export function SyncAllChannelsButton() { <span role="alert" aria-label="sync all error" - className="text-xs text-red-700 dark:text-red-400" + className="text-xs text-destructive" > {status.message} </span> diff --git a/editor/app/channels/new/page.tsx b/editor/app/channels/new/page.tsx @@ -8,8 +8,8 @@ export const metadata: Metadata = { title: "New channel — Channels" }; export default function NewChannelPage() { return ( <div className="flex flex-col gap-4"> - <div className="flex items-center gap-2 text-sm text-zinc-500"> - <Link href="/channels" className="underline hover:text-zinc-900 dark:hover:text-zinc-100"> + <div className="flex items-center gap-2 text-sm text-muted-foreground"> + <Link href="/channels" className="underline hover:text-foreground"> Channels </Link> <span>/</span> diff --git a/editor/app/channels/page.tsx b/editor/app/channels/page.tsx @@ -40,14 +40,14 @@ export default async function ChannelsPage({ <SyncAllChannelsButton /> <Link href="/channels/new" - className="px-3 py-2 rounded-md bg-zinc-900 dark:bg-zinc-100 text-zinc-100 dark:text-zinc-900 text-sm font-medium hover:opacity-90" + className="px-3 py-2 rounded-md bg-primary text-primary-foreground text-sm font-medium hover:opacity-90" > New channel </Link> </div> </div> {channels.length === 0 ? ( - <p className="text-sm text-zinc-500 border border-dashed border-zinc-300 dark:border-zinc-700 rounded p-4"> + <p className="text-sm text-muted-foreground border border-dashed border-border rounded p-4"> {active.siteId && !active.isAll ? `No channels assigned to ${active.siteId} yet — add some on the Sites page, or create a channel to add it here.` : "No channels yet."} diff --git a/editor/app/charts/EditorChartsClient.tsx b/editor/app/charts/EditorChartsClient.tsx @@ -31,7 +31,7 @@ function Inner({ if (error) { return ( - <p className="text-sm text-red-600 dark:text-red-400"> + <p className="text-sm text-destructive"> Failed to load the stats dataset ({error.message}). Run{" "} <strong>Build index</strong> then <strong>Build stats dataset</strong>{" "} on the Build page first. @@ -40,7 +40,7 @@ function Inner({ } if (!ready) { return ( - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> Loading stats… {pageCount > 0 ? `(${loadedPages}/${pageCount} pages)` : ""} </p> ); diff --git a/editor/app/charts/page.tsx b/editor/app/charts/page.tsx @@ -22,7 +22,7 @@ export default async function ChartsPage({ return ( <div className="flex flex-col gap-4"> <h1 className="text-2xl font-semibold">Charts</h1> - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> No sites configured. Create one on the{" "} <Link className="underline" href="/sites"> Sites @@ -39,7 +39,7 @@ export default async function ChartsPage({ return ( <div className="flex flex-col gap-4"> <h1 className="text-2xl font-semibold">Charts</h1> - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> Select a specific site from the site selector in the sidebar to edit its charts. </p> @@ -52,7 +52,7 @@ export default async function ChartsPage({ return ( <div className="flex flex-col gap-4"> <h1 className="text-2xl font-semibold">Charts</h1> - <p className="mt-1 text-sm text-zinc-500"> + <p className="mt-1 text-sm text-muted-foreground"> Author the default dashboard for <strong>{siteId}</strong>. Edits save automatically and bake into that site's next export build. Requires the stats dataset — build it on the{" "} diff --git a/editor/app/components/ConcurrencyControl.tsx b/editor/app/components/ConcurrencyControl.tsx @@ -14,7 +14,7 @@ export function ConcurrencyControl({ actionLabel, }: Props) { return ( - <label className="flex items-center gap-1 text-xs text-zinc-500"> + <label className="flex items-center gap-1 text-xs text-muted-foreground"> <span>Parallel</span> <input type="number" @@ -25,7 +25,7 @@ export function ConcurrencyControl({ placeholder={String(defaultLimit)} aria-label={`parallel limit for ${actionLabel}`} onChange={(e) => onChange(e.target.value)} - className="w-16 font-mono px-2 py-1 rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100" + className="w-16 font-mono px-2 py-1 rounded border border-border bg-card text-foreground" /> </label> ); diff --git a/editor/app/components/QueueControl.tsx b/editor/app/components/QueueControl.tsx @@ -43,7 +43,7 @@ export function QueueControl({ setDraft(value); } }} - className="w-44 font-mono px-2 py-1 rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900" + className="w-44 font-mono px-2 py-1 rounded border border-border bg-card" /> <button type="button" @@ -52,7 +52,7 @@ export function QueueControl({ onChange(draft.trim()); setEditing(false); }} - className="px-2 py-1 rounded bg-zinc-900 dark:bg-zinc-100 text-zinc-100 dark:text-zinc-900" + className="px-2 py-1 rounded bg-primary text-primary-foreground" > Set </button> @@ -62,7 +62,7 @@ export function QueueControl({ setEditing(false); setDraft(value); }} - className="text-zinc-500 underline" + className="text-muted-foreground underline" > Cancel </button> @@ -79,7 +79,7 @@ export function QueueControl({ ).sort(); return ( - <label className="flex items-center gap-1 text-xs text-zinc-500"> + <label className="flex items-center gap-1 text-xs text-muted-foreground"> <span className="sr-only">queue for {actionLabel}</span> <select value={value} @@ -93,7 +93,7 @@ export function QueueControl({ } onChange(next); }} - className="font-mono px-2 py-1 rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100" + className="font-mono px-2 py-1 rounded border border-border bg-card text-foreground" > <option value="">(immediate)</option> {named.map((opt) => ( diff --git a/editor/app/components/ShardControl.tsx b/editor/app/components/ShardControl.tsx @@ -89,7 +89,7 @@ export function ShardControl({ <span aria-label={`shard ${op} control`} title={`shard config for ${actionLabel}`} - className="flex flex-wrap items-center gap-2 text-xs text-zinc-500" + className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground" > <span>Shard</span> <input @@ -102,7 +102,7 @@ export function ShardControl({ aria-label={`shard ${op} index`} title="This shard's number (1-based: 1…total)" onChange={(e) => onIndexChange(e.target.value)} - className="w-14 font-mono px-2 py-1 rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100" + className="w-14 font-mono px-2 py-1 rounded border border-border bg-card text-foreground" /> <span>/</span> <input @@ -115,19 +115,19 @@ export function ShardControl({ aria-label={`shard ${op} total`} title="Total number of shards" onChange={(e) => onTotalChange(e.target.value)} - className="w-14 font-mono px-2 py-1 rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100" + className="w-14 font-mono px-2 py-1 rounded border border-border bg-card text-foreground" /> {existing ? ( <span aria-label={`shard ${op} saved`} - className="text-zinc-600 dark:text-zinc-400" + className="text-muted-foreground" > saved: {existing.shardIndex + 1}/{existing.totalShards} ({existing.itemCount} items) </span> ) : ( <span aria-label={`shard ${op} empty`} - className="text-zinc-400" + className="text-muted-foreground" > (no saved shard) </span> @@ -137,7 +137,7 @@ export function ShardControl({ onClick={handleSave} disabled={saving} aria-label={`shard ${op} save`} - className="px-2 py-0.5 rounded border border-zinc-300 dark:border-zinc-700 text-zinc-600 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800 disabled:opacity-40" + className="px-2 py-0.5 rounded border border-border text-muted-foreground hover:bg-muted disabled:opacity-40" > {saving ? "Saving…" : "Save"} </button> @@ -146,7 +146,7 @@ export function ShardControl({ onClick={handleClear} disabled={!existing || clearing} aria-label={`shard ${op} clear`} - className="px-2 py-0.5 rounded border border-zinc-300 dark:border-zinc-700 text-zinc-600 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800 disabled:opacity-40" + className="px-2 py-0.5 rounded border border-border text-muted-foreground hover:bg-muted disabled:opacity-40" > {clearing ? "Clearing…" : "Clear"} </button> @@ -154,7 +154,7 @@ export function ShardControl({ <span role="alert" aria-label={`shard ${op} error`} - className="text-red-700 dark:text-red-400" + className="text-destructive" > {error} </span> diff --git a/editor/app/components/SiteScopeSelect.tsx b/editor/app/components/SiteScopeSelect.tsx @@ -65,7 +65,7 @@ export function SiteScopeSelect({ sites }: { sites: SiteScopeOption[] }) { if (sites.length === 0) { return ( - <div className="px-4 py-2 text-xs text-zinc-500"> + <div className="px-4 py-2 text-xs text-muted-foreground"> No sites — create one under Sites. </div> ); @@ -73,12 +73,12 @@ export function SiteScopeSelect({ sites }: { sites: SiteScopeOption[] }) { return ( <label className="flex flex-col gap-1 px-4 py-2 text-sm"> - <span className="text-xs uppercase tracking-wide text-zinc-500">site</span> + <span className="text-xs uppercase tracking-wide text-muted-foreground">site</span> <select value={resolved.value} onChange={(e) => onChange(e.target.value)} aria-label="Active site" - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm" + className="rounded border border-border bg-card px-2 py-1 text-sm" > {multi && <option value={ALL_SITES}>All sites</option>} {sites.map((s) => ( diff --git a/editor/app/components/SocialLinksField.tsx b/editor/app/components/SocialLinksField.tsx @@ -46,14 +46,14 @@ export function SocialLinksField({ value, onChange, name }: Props) { {value.map((s, idx) => ( <div key={idx} - className="flex flex-col gap-2 border border-zinc-200 dark:border-zinc-800 rounded p-3 bg-zinc-50/50 dark:bg-zinc-900/40" + className="flex flex-col gap-2 border border-border rounded p-3 bg-card" > <div className="flex items-center justify-end gap-1"> <button type="button" onClick={() => move(idx, -1)} disabled={idx === 0} - className="px-2 py-0.5 rounded border border-zinc-300 dark:border-zinc-700 text-xs disabled:opacity-40" + className="px-2 py-0.5 rounded border border-border text-xs disabled:opacity-40" aria-label="Move link up" > ↑ @@ -62,7 +62,7 @@ export function SocialLinksField({ value, onChange, name }: Props) { type="button" onClick={() => move(idx, 1)} disabled={idx === value.length - 1} - className="px-2 py-0.5 rounded border border-zinc-300 dark:border-zinc-700 text-xs disabled:opacity-40" + className="px-2 py-0.5 rounded border border-border text-xs disabled:opacity-40" aria-label="Move link down" > ↓ @@ -70,7 +70,7 @@ export function SocialLinksField({ value, onChange, name }: Props) { <button type="button" onClick={() => onChange(value.filter((_, i) => i !== idx))} - className="px-2 py-0.5 rounded border border-red-300 dark:border-red-800 text-xs text-red-700 dark:text-red-300" + className="px-2 py-0.5 rounded border border-destructive/30 text-xs text-destructive" > Remove </button> @@ -80,21 +80,21 @@ export function SocialLinksField({ value, onChange, name }: Props) { value={s.label} onChange={(e) => update(idx, { label: e.target.value })} placeholder="Label (e.g. GitHub)" - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm" + className="rounded border border-border bg-card px-2 py-1 text-sm" /> <input type="text" value={s.url} onChange={(e) => update(idx, { url: e.target.value })} placeholder="https://…" - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm" + className="rounded border border-border bg-card px-2 py-1 text-sm" /> <textarea value={s.svg} onChange={(e) => update(idx, { svg: e.target.value })} rows={4} placeholder='<svg viewBox="0 0 24 24"><path d="…"/></svg>' - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-xs font-mono" + className="rounded border border-border bg-card px-2 py-1 text-xs font-mono" /> </div> ))} @@ -102,7 +102,7 @@ export function SocialLinksField({ value, onChange, name }: Props) { <button type="button" onClick={() => onChange([...value, { label: "", url: "", svg: "" }])} - className="px-3 py-1.5 rounded border border-zinc-300 dark:border-zinc-700 text-sm hover:bg-zinc-50 dark:hover:bg-zinc-800" + className="px-3 py-1.5 rounded border border-border text-sm hover:bg-muted" > + Add link </button> diff --git a/editor/app/deploy/components/BuildDeployButton.tsx b/editor/app/deploy/components/BuildDeployButton.tsx @@ -24,20 +24,20 @@ export function BuildDeployButton({ let notice: React.ReactNode; if (!siteId) { notice = ( - <p className="text-sm text-amber-600 dark:text-amber-400"> + <p className="text-sm text-warning"> Select a specific site from the sidebar to build & deploy. </p> ); } else if (!cloudflareProject) { notice = ( - <p className="text-sm text-amber-600 dark:text-amber-400"> + <p className="text-sm text-warning"> <strong>{siteTitle ?? siteId}</strong> has no Cloudflare Pages project configured — set one on the site's page to deploy. </p> ); } else { notice = ( - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> Builds then deploys <strong>{siteTitle ?? siteId}</strong> ( <code>{siteId}</code> → <code>{cloudflareProject}</code>). </p> diff --git a/editor/app/deploy/components/BuildExportButton.tsx b/editor/app/deploy/components/BuildExportButton.tsx @@ -28,12 +28,12 @@ export function BuildExportButton({ existingQueues, siteId, siteTitle }: Props) extraControls={ <div className="flex flex-col gap-2"> {siteId ? ( - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> Building <strong>{siteTitle ?? siteId}</strong> ( <code>{siteId}</code>). </p> ) : ( - <p className="text-sm text-amber-600 dark:text-amber-400"> + <p className="text-sm text-warning"> Select a specific site from the sidebar to build. </p> )} diff --git a/editor/app/deploy/components/BuildModeToggle.tsx b/editor/app/deploy/components/BuildModeToggle.tsx @@ -26,11 +26,11 @@ export function BuildModeToggle({ mode: initialMode }: { mode: BuildMode }) { return ( <div className="flex flex-col gap-1.5"> - <span className="text-xs font-medium text-zinc-500">Build mode</span> + <span className="text-xs font-medium text-muted-foreground">Build mode</span> <div role="group" aria-label="Build mode" - className="inline-flex w-fit rounded-md border border-zinc-300 dark:border-zinc-700 overflow-hidden text-sm" + className="inline-flex w-fit rounded-md border border-border overflow-hidden text-sm" > {options.map((opt) => { const active = mode === opt.value; @@ -44,8 +44,8 @@ export function BuildModeToggle({ mode: initialMode }: { mode: BuildMode }) { className={ "px-3 py-1.5 font-medium transition-colors disabled:opacity-60 " + (active - ? "bg-zinc-900 text-zinc-100 dark:bg-zinc-100 dark:text-zinc-900" - : "bg-transparent text-zinc-600 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800") + ? "bg-primary text-primary-foreground" + : "bg-transparent text-muted-foreground hover:bg-muted") } > {opt.label} @@ -54,7 +54,7 @@ export function BuildModeToggle({ mode: initialMode }: { mode: BuildMode }) { })} </div> {mode === "docker" && ( - <span className="text-xs text-amber-600 dark:text-amber-400"> + <span className="text-xs text-warning"> Docker builds are a follow-up — runs the basic build for now. </span> )} diff --git a/editor/app/deploy/components/BuildSitesPanel.tsx b/editor/app/deploy/components/BuildSitesPanel.tsx @@ -59,7 +59,7 @@ export function BuildSitesPanel({ return ( <div className="flex flex-col gap-4"> - <div className="flex flex-col gap-2 rounded-md border border-zinc-200 dark:border-zinc-800 p-3"> + <div className="flex flex-col gap-2 rounded-md border border-border p-3"> <div className="flex flex-wrap gap-x-6 gap-y-2"> {sites.map((s) => ( <label @@ -73,16 +73,16 @@ export function BuildSitesPanel({ aria-label={`Select ${s.siteTitle}`} /> <span className="font-medium">{s.siteTitle}</span> - <code className="text-xs text-zinc-500">{s.siteId}</code> + <code className="text-xs text-muted-foreground">{s.siteId}</code> {!s.cloudflareProject && ( - <span className="text-xs text-amber-600 dark:text-amber-400"> + <span className="text-xs text-warning"> no deploy target </span> )} </label> ))} {sites.length === 0 && ( - <span className="text-sm italic text-zinc-500"> + <span className="text-sm italic text-muted-foreground"> No sites configured. </span> )} @@ -94,11 +94,11 @@ export function BuildSitesPanel({ type="button" onClick={launch} disabled={selected.size === 0} - className="px-3 py-2 rounded-md bg-zinc-900 dark:bg-zinc-100 text-zinc-100 dark:text-zinc-900 text-sm font-medium hover:opacity-90 disabled:opacity-50" + className="px-3 py-2 rounded-md bg-primary text-primary-foreground text-sm font-medium hover:opacity-90 disabled:opacity-50" > {deployAfter ? "Build & deploy selected" : "Build selected"} </button> - <label className="flex items-center gap-2 text-sm text-zinc-600 dark:text-zinc-300"> + <label className="flex items-center gap-2 text-sm text-muted-foreground"> <input type="checkbox" checked={deployAfter} @@ -106,19 +106,19 @@ export function BuildSitesPanel({ /> Deploy after build </label> - <span className="text-xs text-zinc-500"> + <span className="text-xs text-muted-foreground"> {selected.size} selected </span> </div> {missingProject && ( - <p className="text-xs text-amber-600 dark:text-amber-400"> + <p className="text-xs text-warning"> Some selected sites have no Cloudflare Pages project — those will build only (deploy is skipped for them). </p> )} {serial && ( - <p className="text-xs text-zinc-500"> + <p className="text-xs text-muted-foreground"> Basic mode runs these one at a time (the build output tree is shared). Switch to Docker mode for true parallel builds — a follow-up. </p> diff --git a/editor/app/deploy/components/CutReleaseForm.tsx b/editor/app/deploy/components/CutReleaseForm.tsx @@ -26,7 +26,7 @@ export function CutReleaseForm({ return ( <form action={formAction} className="flex flex-col gap-2"> - <div className="text-sm text-zinc-500"> + <div className="text-sm text-muted-foreground"> {lastVersion ? `Last released: ${lastVersion}` : "No releases yet."} @@ -43,17 +43,17 @@ export function CutReleaseForm({ placeholder={suggestedVersion} defaultValue={suggestedVersion} disabled={!hasUnreleased || pending} - className="w-40 px-3 py-2 rounded-md border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 text-sm font-mono" + className="w-40 px-3 py-2 rounded-md border border-border bg-card text-sm font-mono" aria-describedby={`hint-${workspace}`} /> <button type="submit" disabled={!hasUnreleased || pending} - className="px-3 py-2 rounded-md text-sm bg-zinc-900 text-zinc-50 dark:bg-zinc-100 dark:text-zinc-900 hover:opacity-90 disabled:opacity-40 disabled:cursor-not-allowed" + className="px-3 py-2 rounded-md text-sm bg-primary text-primary-foreground hover:opacity-90 disabled:opacity-40 disabled:cursor-not-allowed" > {pending ? "Cutting…" : "Cut release"} </button> - <label className="flex items-center gap-1 text-sm text-zinc-600 dark:text-zinc-300"> + <label className="flex items-center gap-1 text-sm text-muted-foreground"> <input type="checkbox" name="commit" @@ -64,7 +64,7 @@ export function CutReleaseForm({ <span>Commit changelog</span> </label> </div> - <div id={`hint-${workspace}`} className="text-xs text-zinc-500"> + <div id={`hint-${workspace}`} className="text-xs text-muted-foreground"> {hasUnreleased ? "Date stamps to today automatically." : "Nothing pending under [Unreleased] yet."} @@ -72,7 +72,7 @@ export function CutReleaseForm({ {state && !state.ok && ( <div role="alert" - className="text-sm text-rose-700 dark:text-rose-300 bg-rose-50 dark:bg-rose-950/40 border border-rose-200 dark:border-rose-900 rounded-md px-3 py-2" + className="text-sm text-destructive bg-destructive-soft border border-destructive/30 rounded-md px-3 py-2" > {state.error} </div> @@ -80,7 +80,7 @@ export function CutReleaseForm({ {state && state.ok && ( <div role="status" - className="text-sm text-emerald-700 dark:text-emerald-300" + className="text-sm text-success" > Cut release {state.version}. {state.committed ? " Committed." : " (not committed)"} diff --git a/editor/app/deploy/components/DeployButton.tsx b/editor/app/deploy/components/DeployButton.tsx @@ -21,12 +21,12 @@ export function DeployButton({ siteId, siteTitle }: Props) { disabled={!siteId} extraControls={ siteId ? ( - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> Deploying <strong>{siteTitle ?? siteId}</strong> ( <code>{siteId}</code>). </p> ) : ( - <p className="text-sm text-amber-600 dark:text-amber-400"> + <p className="text-sm text-warning"> Select a specific site from the sidebar to deploy. </p> ) diff --git a/editor/app/deploy/components/JobLane.tsx b/editor/app/deploy/components/JobLane.tsx @@ -21,13 +21,13 @@ type Props = { }; const CHIP: Record<LaneStatus, { label: string; cls: string }> = { - starting: { label: "Starting…", cls: "bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300" }, - queued: { label: "Queued", cls: "bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300" }, - running: { label: "Running", cls: "bg-sky-100 text-sky-700 dark:bg-sky-950 dark:text-sky-300" }, - done: { label: "Done", cls: "bg-emerald-100 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-300" }, - failed: { label: "Failed", cls: "bg-red-100 text-red-700 dark:bg-red-950 dark:text-red-300" }, - cancelled: { label: "Cancelled", cls: "bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300" }, - error: { label: "Error", cls: "bg-red-100 text-red-700 dark:bg-red-950 dark:text-red-300" }, + starting: { label: "Starting…", cls: "bg-muted text-muted-foreground" }, + queued: { label: "Queued", cls: "bg-muted text-muted-foreground" }, + running: { label: "Running", cls: "bg-info-soft text-info" }, + done: { label: "Done", cls: "bg-success-soft text-success" }, + failed: { label: "Failed", cls: "bg-destructive-soft text-destructive" }, + cancelled: { label: "Cancelled", cls: "bg-muted text-muted-foreground" }, + error: { label: "Error", cls: "bg-destructive-soft text-destructive" }, }; // A compact, auto-starting lane for one site's build (or build+deploy). Reuses @@ -157,7 +157,7 @@ export function JobLane({ title, subtitle, trigger }: Props) { const live = status === "queued" || status === "running" || status === "starting"; return ( - <div className="rounded-md border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900"> + <div className="rounded-md border border-border bg-card"> <div className="flex items-center gap-3 px-3 py-2"> <span className={`shrink-0 rounded-full px-2 py-0.5 text-xs font-medium ${chip.cls}`} @@ -168,7 +168,7 @@ export function JobLane({ title, subtitle, trigger }: Props) { </span> <div className="min-w-0 flex-1"> <div className="truncate text-sm font-medium">{title}</div> - <div className="truncate text-xs text-zinc-500">{subtitle}</div> + <div className="truncate text-xs text-muted-foreground">{subtitle}</div> </div> {live && jobId && ( <button @@ -176,7 +176,7 @@ export function JobLane({ title, subtitle, trigger }: Props) { onClick={handleCancel} disabled={cancelling} aria-label={`Cancel ${title}`} - className="shrink-0 rounded border border-red-300 dark:border-red-800 px-2 py-1 text-xs font-medium text-red-700 dark:text-red-300 hover:bg-red-50 dark:hover:bg-red-950 disabled:opacity-50" + className="shrink-0 rounded border border-destructive/30 px-2 py-1 text-xs font-medium text-destructive hover:bg-destructive-soft disabled:opacity-50" > {cancelling ? "Cancelling…" : "Cancel"} </button> @@ -185,21 +185,21 @@ export function JobLane({ title, subtitle, trigger }: Props) { {error && ( <div role="alert" - className="mx-3 mb-2 rounded border border-red-300 bg-red-50 dark:border-red-800 dark:bg-red-950 px-2 py-1.5 text-xs text-red-700 dark:text-red-300" + className="mx-3 mb-2 rounded border border-destructive/30 bg-destructive-soft px-2 py-1.5 text-xs text-destructive" > {error} </div> )} {(log || live) && ( <details className="px-3 pb-2"> - <summary className="cursor-pointer text-xs text-zinc-500 select-none"> + <summary className="cursor-pointer text-xs text-muted-foreground select-none"> Log </summary> <pre ref={preRef} role="log" aria-label={`${title} output`} - className="mt-1 h-48 overflow-auto whitespace-pre-wrap rounded border border-zinc-200 dark:border-zinc-800 bg-zinc-100 dark:bg-zinc-950 p-2 text-xs font-mono" + className="mt-1 h-48 overflow-auto whitespace-pre-wrap rounded border border-border bg-muted p-2 text-xs font-mono" > {log || "Waiting for output…"} </pre> diff --git a/editor/app/deploy/page.tsx b/editor/app/deploy/page.tsx @@ -71,7 +71,7 @@ export default async function DeployPage({ <div className="flex flex-col gap-8 max-w-3xl"> <div className="flex flex-col gap-1"> <h1 className="text-2xl font-semibold">Deploy</h1> - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> Cut a release, then build & deploy the active site — or batch several sites at once below. </p> @@ -81,18 +81,18 @@ export default async function DeployPage({ <section className="flex flex-col gap-3"> <div> <h2 className="text-lg font-semibold">Release notes</h2> - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> Bullets under <code>## [Unreleased]</code> in{" "} <code>export/CHANGELOG.md</code> become the next cut release. Cut it before you build & deploy. </p> </div> {hasUnreleased && source ? ( - <div className="rounded-md border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 px-4 py-3"> + <div className="rounded-md border border-border bg-card px-4 py-3"> <Changelog source={source} limit={1} /> </div> ) : ( - <div className="text-sm italic text-zinc-500"> + <div className="text-sm italic text-muted-foreground"> No pending changes yet. </div> )} @@ -105,14 +105,14 @@ export default async function DeployPage({ </section> {/* 2 — Build & deploy (primary path) for the active site. */} - <section className="flex flex-col gap-3 border-t border-zinc-200 dark:border-zinc-800 pt-6"> + <section className="flex flex-col gap-3 border-t border-border pt-6"> <div> <h2 className="text-lg font-semibold">Build & deploy</h2> - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> Builds the static export, then deploys it to Cloudflare Pages in one step. Cancelling before the build finishes skips the deploy. </p> - <p className="text-sm text-zinc-500 mt-1"> + <p className="text-sm text-muted-foreground mt-1"> Last deployed:{" "} {lastDeployedAt ? new Date(lastDeployedAt).toLocaleString() @@ -128,7 +128,7 @@ export default async function DeployPage({ </section> {/* 3 — Individual steps, for when you only want one half. */} - <section className="flex flex-col gap-3 border-t border-zinc-200 dark:border-zinc-800 pt-6"> + <section className="flex flex-col gap-3 border-t border-border pt-6"> <details> <summary className="cursor-pointer text-lg font-semibold select-none"> Individual steps @@ -137,7 +137,7 @@ export default async function DeployPage({ <div className="flex flex-col gap-3"> <div> <h3 className="font-semibold">Build static export</h3> - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> Spawns <code>pnpm run build</code> in <code>export/</code> — index build, then <code>next build</code> to{" "} <code>export/out/</code>. No deploy. @@ -152,7 +152,7 @@ export default async function DeployPage({ <div className="flex flex-col gap-3"> <div> <h3 className="font-semibold">Deploy static export</h3> - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> Publishes the most recently built site at{" "} <code>export/out/</code> to Cloudflare Pages. Build first. </p> @@ -167,10 +167,10 @@ export default async function DeployPage({ </section> {/* 4 — Batch: build (and optionally deploy) several sites at once. */} - <section className="flex flex-col gap-3 border-t border-zinc-200 dark:border-zinc-800 pt-6"> + <section className="flex flex-col gap-3 border-t border-border pt-6"> <div> <h2 className="text-lg font-semibold">Build multiple sites</h2> - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> Pick sites to build (and optionally deploy) together. Each gets its own live log lane. </p> diff --git a/editor/app/homepage/HomepageConfigForm.tsx b/editor/app/homepage/HomepageConfigForm.tsx @@ -5,7 +5,7 @@ import type { HomepageConfig } from "yt-dlp-transcript-common/lib/homepage"; import { saveHomepageConfigAction, type SaveResult } from "./actions"; const input = - "rounded-md border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-3 py-1.5 text-sm"; + "rounded-md border border-border bg-card px-3 py-1.5 text-sm"; const label = "flex flex-col gap-1 text-sm font-medium"; export function HomepageConfigForm({ config }: { config: HomepageConfig }) { @@ -65,17 +65,17 @@ export function HomepageConfigForm({ config }: { config: HomepageConfig }) { <button type="submit" disabled={pending} - className="rounded-md bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50" + className="rounded-md bg-info px-3 py-1.5 text-sm font-medium text-info-foreground hover:bg-info disabled:opacity-50" > {pending ? "Saving…" : "Save config"} </button> {state && !state.ok && ( - <span className="text-sm text-red-600 dark:text-red-400"> + <span className="text-sm text-destructive"> {state.error} </span> )} {state && state.ok && ( - <span className="text-sm text-green-600 dark:text-green-400"> + <span className="text-sm text-success"> Saved. </span> )} diff --git a/editor/app/homepage/page.tsx b/editor/app/homepage/page.tsx @@ -14,7 +14,7 @@ export default function HomepageAdminPage() { <div className="flex flex-col gap-8"> <div> <h1 className="text-2xl font-semibold">Homepage (hub)</h1> - <p className="mt-1 text-sm text-zinc-500"> + <p className="mt-1 text-sm text-muted-foreground"> The Archilyzer hub, built by the <code>homepage</code> package. Its home page is a cross-site stats landing built from the whole channel pool; only its branding is edited here. diff --git a/editor/app/jobs/[id]/components/JobLogTail.tsx b/editor/app/jobs/[id]/components/JobLogTail.tsx @@ -96,11 +96,11 @@ export function JobLogTail({ jobId, initiallyRunning }: Props) { return ( <div className="flex flex-col gap-2"> <div className="flex items-center gap-3 flex-wrap"> - <div className="text-xs text-zinc-500"> + <div className="text-xs text-muted-foreground"> Status: <span aria-label="Job status">{status}</span> </div> {isQueued && queueKey && queuePosition > 0 && ( - <div role="status" className="text-xs text-zinc-500"> + <div role="status" className="text-xs text-muted-foreground"> Queued in <code className="font-mono">{queueKey}</code>, position{" "} {queuePosition} </div> @@ -110,7 +110,7 @@ export function JobLogTail({ jobId, initiallyRunning }: Props) { type="button" onClick={handleCancel} disabled={cancelling} - className="px-3 py-1.5 rounded-md border border-red-300 dark:border-red-800 text-sm font-medium text-red-700 dark:text-red-300 hover:bg-red-50 dark:hover:bg-red-950 disabled:opacity-50" + className="px-3 py-1.5 rounded-md border border-destructive/30 text-sm font-medium text-destructive hover:bg-destructive-soft disabled:opacity-50" > {cancelling ? "Cancelling…" : "Cancel"} </button> @@ -121,7 +121,7 @@ export function JobLogTail({ jobId, initiallyRunning }: Props) { onScroll={handleScroll} role="log" aria-label="Job log" - className="text-xs font-mono bg-zinc-100 dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded p-3 h-96 overflow-auto whitespace-pre-wrap" + className="text-xs font-mono bg-muted border border-border rounded p-3 h-96 overflow-auto whitespace-pre-wrap" > {log || "(empty log)"} </pre> diff --git a/editor/app/jobs/[id]/page.tsx b/editor/app/jobs/[id]/page.tsx @@ -40,10 +40,10 @@ export default async function JobDetailPage({ if (!job) notFound(); return ( <div className="flex flex-col gap-4"> - <div className="flex items-center gap-2 text-sm text-zinc-500"> + <div className="flex items-center gap-2 text-sm text-muted-foreground"> <Link href="/jobs" - className="underline hover:text-zinc-900 dark:hover:text-zinc-100" + className="underline hover:text-foreground" > Jobs </Link> @@ -61,7 +61,7 @@ export default async function JobDetailPage({ Channel:{" "} <Link href={`/channels/${job.channelSlug}`} - className="underline font-mono hover:text-zinc-900 dark:hover:text-zinc-100" + className="underline font-mono hover:text-foreground" > {job.channelSlug} </Link> @@ -70,7 +70,7 @@ export default async function JobDetailPage({ {" / Video: "} <Link href={`/channels/${job.channelSlug}/videos/${encodeURIComponent(job.videoId)}`} - className="underline font-mono hover:text-zinc-900 dark:hover:text-zinc-100" + className="underline font-mono hover:text-foreground" > {job.videoId} </Link> @@ -97,7 +97,7 @@ export default async function JobDetailPage({ value={typeof job.exitCode === "number" ? String(job.exitCode) : "—"} /> </dl> - <p className="text-xs text-zinc-500 font-mono"> + <p className="text-xs text-muted-foreground font-mono"> log: {path.relative(getPaths().monorepoRoot, job.logPath)} </p> <JobLogTail @@ -112,8 +112,8 @@ export default async function JobDetailPage({ function Cell({ label, value }: { label: string; value: string }) { return ( - <div className="rounded border border-zinc-200 dark:border-zinc-800 px-3 py-2"> - <div className="text-xs uppercase tracking-wide text-zinc-500"> + <div className="rounded border border-border px-3 py-2"> + <div className="text-xs uppercase tracking-wide text-muted-foreground"> {label} </div> <div className="mt-1">{value}</div> diff --git a/editor/app/jobs/bookmarks/page.tsx b/editor/app/jobs/bookmarks/page.tsx @@ -14,10 +14,10 @@ export default async function BookmarksPage() { const { bookmarks, missingSlugs } = await loadBookmarksView(); return ( <div className="flex flex-col gap-4"> - <div className="flex items-center gap-2 text-sm text-zinc-500"> + <div className="flex items-center gap-2 text-sm text-muted-foreground"> <Link href="/jobs" - className="underline hover:text-zinc-900 dark:hover:text-zinc-100" + className="underline hover:text-foreground" > Jobs </Link> @@ -26,7 +26,7 @@ export default async function BookmarksPage() { </div> <h1 className="text-2xl font-semibold">Bookmarks</h1> {bookmarks.length === 0 ? ( - <p className="text-sm text-zinc-500 border border-dashed border-zinc-300 dark:border-zinc-700 rounded p-4"> + <p className="text-sm text-muted-foreground border border-dashed border-border rounded p-4"> No bookmarks yet. </p> ) : ( diff --git a/editor/app/jobs/components/ActiveJobsLive.tsx b/editor/app/jobs/components/ActiveJobsLive.tsx @@ -56,7 +56,7 @@ export function ActiveJobsLive({ initial }: { initial: ActiveJobsPayload }) { if (jobs.length === 0) { return ( - <p className="text-sm text-zinc-500 border border-dashed border-zinc-300 dark:border-zinc-700 rounded p-4"> + <p className="text-sm text-muted-foreground border border-dashed border-border rounded p-4"> No active jobs. </p> ); @@ -71,16 +71,16 @@ export function ActiveJobsLive({ initial }: { initial: ActiveJobsPayload }) { <section key={slug} aria-label={`Active jobs for ${displayName}`} - className="flex flex-col gap-3 border border-zinc-200 dark:border-zinc-800 rounded-md p-3 bg-white dark:bg-zinc-900" + className="flex flex-col gap-3 border border-border rounded-md p-3 bg-card" > <div className="flex flex-wrap items-baseline justify-between gap-2"> <Link href={`/channels/${slug}`} - className="text-base font-medium underline hover:text-zinc-900 dark:hover:text-zinc-100" + className="text-base font-medium underline hover:text-foreground" > {displayName} </Link> - <span className="font-mono text-xs text-zinc-500">{slug}</span> + <span className="font-mono text-xs text-muted-foreground">{slug}</span> </div> <RunningJobsList jobs={channelJobs} hideChannelSlug /> </section> @@ -92,7 +92,7 @@ export function ActiveJobsLive({ initial }: { initial: ActiveJobsPayload }) { <section key={kind} aria-label={`System jobs: ${label}`} - className="flex flex-col gap-3 border border-zinc-200 dark:border-zinc-800 rounded-md p-3 bg-white dark:bg-zinc-900" + className="flex flex-col gap-3 border border-border rounded-md p-3 bg-card" > <h2 className="text-base font-medium">{label}</h2> <RunningJobsList jobs={kindJobs} /> diff --git a/editor/app/jobs/components/BookmarkJobButton.tsx b/editor/app/jobs/components/BookmarkJobButton.tsx @@ -41,7 +41,7 @@ export function BookmarkJobButton({ title={error ?? undefined} className={ className ?? - "px-2 py-1 rounded-md bg-zinc-200 dark:bg-zinc-800 text-zinc-900 dark:text-zinc-100 text-xs font-medium hover:opacity-90 disabled:opacity-50" + "px-2 py-1 rounded-md bg-muted text-foreground text-xs font-medium hover:opacity-90 disabled:opacity-50" } > {done ? "Bookmarked ✓" : pending ? "Bookmarking…" : "Bookmark"} diff --git a/editor/app/jobs/components/BookmarkRunButton.tsx b/editor/app/jobs/components/BookmarkRunButton.tsx @@ -69,12 +69,12 @@ export function BookmarkRunButton({ disabled={pending || channelMissing} aria-label={`run bookmark ${bookmark.name}`} title={status?.text ?? detail} - className="max-w-[16rem] truncate px-3 py-1.5 rounded-md bg-zinc-900 dark:bg-zinc-100 text-zinc-100 dark:text-zinc-900 text-xs font-medium hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed" + className="max-w-[16rem] truncate px-3 py-1.5 rounded-md bg-primary text-primary-foreground text-xs font-medium hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed" > {label} </button> {channelMissing ? ( - <span className="text-[11px] text-amber-700 dark:text-amber-300"> + <span className="text-[11px] text-warning"> channel missing </span> ) : status ? ( @@ -82,8 +82,8 @@ export function BookmarkRunButton({ role={status.kind === "error" ? "alert" : "status"} className={ status.kind === "error" - ? "text-[11px] text-red-700 dark:text-red-300" - : "text-[11px] text-zinc-500" + ? "text-[11px] text-destructive" + : "text-[11px] text-muted-foreground" } > {status.text} diff --git a/editor/app/jobs/components/BookmarksList.tsx b/editor/app/jobs/components/BookmarksList.tsx @@ -30,9 +30,9 @@ export function BookmarksList({ return ( <section aria-label="Bookmarked jobs" - className="flex flex-col gap-3 border border-zinc-200 dark:border-zinc-800 rounded-md p-3 bg-zinc-50 dark:bg-zinc-900/40" + className="flex flex-col gap-3 border border-border rounded-md p-3 bg-card" > - <h2 className="text-sm font-medium text-zinc-700 dark:text-zinc-300"> + <h2 className="text-sm font-medium text-muted-foreground"> Bookmarks </h2> <ul className="flex flex-col gap-3"> @@ -98,7 +98,7 @@ function BookmarkRow({ return ( <li aria-label={`bookmark ${bookmark.name}`} - className="flex flex-col gap-2 border-t border-zinc-200 dark:border-zinc-800 pt-3 first:border-t-0 first:pt-0" + className="flex flex-col gap-2 border-t border-border pt-3 first:border-t-0 first:pt-0" > <div className="flex flex-wrap items-center gap-2 text-sm"> {editing ? ( @@ -108,12 +108,12 @@ function BookmarkRow({ value={draftName} onChange={(e) => setDraftName(e.target.value)} aria-label={`new name for ${bookmark.name}`} - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-0.5 text-sm" + className="rounded border border-border bg-card px-2 py-0.5 text-sm" /> <button type="button" onClick={onSaveName} - className="px-2 py-1 rounded-md bg-zinc-900 dark:bg-zinc-100 text-zinc-100 dark:text-zinc-900 text-xs font-medium hover:opacity-90" + className="px-2 py-1 rounded-md bg-primary text-primary-foreground text-xs font-medium hover:opacity-90" > Save </button> @@ -123,7 +123,7 @@ function BookmarkRow({ setEditing(false); setDraftName(bookmark.name); }} - className="px-2 py-1 rounded-md border border-zinc-300 dark:border-zinc-700 text-xs font-medium hover:bg-zinc-100 dark:hover:bg-zinc-800" + className="px-2 py-1 rounded-md border border-border text-xs font-medium hover:bg-muted" > Cancel </button> @@ -138,30 +138,30 @@ function BookmarkRow({ setEditing(true); }} aria-label={`rename bookmark ${bookmark.name}`} - className="px-2 py-0.5 rounded border border-zinc-300 dark:border-zinc-700 text-xs text-zinc-600 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800" + className="px-2 py-0.5 rounded border border-border text-xs text-muted-foreground hover:bg-muted" > Rename </button> </> )} - <span className="text-xs text-zinc-500" title={spec.kind}> + <span className="text-xs text-muted-foreground" title={spec.kind}> {jobKindLabel(spec.kind)} </span> {spec.bucket && ( - <span className="font-mono text-xs text-zinc-500"> + <span className="font-mono text-xs text-muted-foreground"> bucket {spec.bucket} </span> )} <Link href={`/channels/${spec.slug}`} - className="font-mono text-xs underline hover:text-zinc-900 dark:hover:text-zinc-100" + className="font-mono text-xs underline hover:text-foreground" > {spec.slug} </Link> {channelMissing && ( <span aria-label={`channel missing for ${bookmark.name}`} - className="text-xs px-2 py-0.5 rounded bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200" + className="text-xs px-2 py-0.5 rounded bg-warning-soft text-warning" > channel missing </span> @@ -172,7 +172,7 @@ function BookmarkRow({ onClick={() => onMove(-1)} disabled={pending || index === 0} aria-label={`move bookmark ${bookmark.name} up`} - className="px-2 py-0.5 rounded border border-zinc-300 dark:border-zinc-700 text-xs disabled:opacity-40" + className="px-2 py-0.5 rounded border border-border text-xs disabled:opacity-40" > ↑ </button> @@ -181,7 +181,7 @@ function BookmarkRow({ onClick={() => onMove(1)} disabled={pending || index === total - 1} aria-label={`move bookmark ${bookmark.name} down`} - className="px-2 py-0.5 rounded border border-zinc-300 dark:border-zinc-700 text-xs disabled:opacity-40" + className="px-2 py-0.5 rounded border border-border text-xs disabled:opacity-40" > ↓ </button> @@ -192,14 +192,14 @@ function BookmarkRow({ onClick={onDelete} disabled={pending} aria-label={`confirm delete bookmark ${bookmark.name}`} - className="px-2 py-1 rounded-md bg-red-600 text-white text-xs font-medium hover:bg-red-700 disabled:opacity-50" + className="px-2 py-1 rounded-md bg-destructive text-destructive-foreground text-xs font-medium hover:bg-destructive/90 disabled:opacity-50" > {pending ? "Deleting…" : "Confirm"} </button> <button type="button" onClick={() => setConfirmingDelete(false)} - className="px-2 py-1 rounded-md border border-zinc-300 dark:border-zinc-700 text-xs font-medium hover:bg-zinc-100 dark:hover:bg-zinc-800" + className="px-2 py-1 rounded-md border border-border text-xs font-medium hover:bg-muted" > Cancel </button> @@ -209,19 +209,19 @@ function BookmarkRow({ type="button" onClick={() => setConfirmingDelete(true)} aria-label={`delete bookmark ${bookmark.name}`} - className="px-2 py-1 rounded-md border border-red-300 dark:border-red-800 text-xs font-medium text-red-700 dark:text-red-300 hover:bg-red-50 dark:hover:bg-red-950" + className="px-2 py-1 rounded-md border border-destructive/30 text-xs font-medium text-destructive hover:bg-destructive-soft" > Delete </button> )} </div> </div> - <p className="text-xs text-zinc-500" suppressHydrationWarning> + <p className="text-xs text-muted-foreground" suppressHydrationWarning> Created {fmtDate(bookmark.createdAt)} · Last run{" "} {bookmark.lastRunAt ? fmtDate(bookmark.lastRunAt) : "never"} </p> {channelMissing ? ( - <p className="text-xs text-amber-700 dark:text-amber-300"> + <p className="text-xs text-warning"> This channel no longer exists — delete the bookmark or recreate the channel to run it again. </p> diff --git a/editor/app/jobs/components/BookmarksMenu.tsx b/editor/app/jobs/components/BookmarksMenu.tsx @@ -18,15 +18,15 @@ export function BookmarksMenu({ return ( <section aria-label="Bookmarked jobs" - className="flex flex-col gap-3 border border-zinc-200 dark:border-zinc-800 rounded-md p-3 bg-zinc-50 dark:bg-zinc-900/40" + className="flex flex-col gap-3 border border-border rounded-md p-3 bg-card" > <div className="flex items-center justify-between gap-2"> - <h2 className="text-sm font-medium text-zinc-700 dark:text-zinc-300"> + <h2 className="text-sm font-medium text-muted-foreground"> Bookmarks </h2> <Link href="/jobs/bookmarks" - className="text-xs underline text-zinc-500 hover:text-zinc-900 dark:hover:text-zinc-100" + className="text-xs underline text-muted-foreground hover:text-foreground" > Manage </Link> diff --git a/editor/app/jobs/components/CancelJobButton.tsx b/editor/app/jobs/components/CancelJobButton.tsx @@ -24,7 +24,7 @@ export function CancelJobButton({ jobId }: Props) { } }} disabled={busy} - className="px-2 py-1 rounded border border-red-300 dark:border-red-800 text-xs font-medium text-red-700 dark:text-red-300 hover:bg-red-50 dark:hover:bg-red-950 disabled:opacity-50" + className="px-2 py-1 rounded border border-destructive/30 text-xs font-medium text-destructive hover:bg-destructive-soft disabled:opacity-50" > {busy ? "Cancelling…" : "Cancel"} </button> diff --git a/editor/app/jobs/components/ClearArchivedButton.tsx b/editor/app/jobs/components/ClearArchivedButton.tsx @@ -20,7 +20,7 @@ export function ClearArchivedButton() { } }} disabled={busy} - className="px-3 py-2 rounded-md border border-zinc-300 dark:border-zinc-700 text-sm hover:bg-zinc-100 dark:hover:bg-zinc-800 disabled:opacity-50" + className="px-3 py-2 rounded-md border border-border text-sm hover:bg-muted disabled:opacity-50" > {busy ? "Clearing…" : "Clear archived logs"} </button> diff --git a/editor/app/jobs/components/DrainAllButton.tsx b/editor/app/jobs/components/DrainAllButton.tsx @@ -26,7 +26,7 @@ export function DrainAllButton() { } }} disabled={busy} - className="px-3 py-2 rounded-md border border-amber-300 dark:border-amber-800 text-sm font-medium text-amber-700 dark:text-amber-300 hover:bg-amber-50 dark:hover:bg-amber-950 disabled:opacity-50" + className="px-3 py-2 rounded-md border border-warning/30 text-sm font-medium text-warning hover:bg-warning-soft disabled:opacity-50" > {busy ? "Draining…" : "Drain all"} </button> diff --git a/editor/app/jobs/components/DrainJobButton.tsx b/editor/app/jobs/components/DrainJobButton.tsx @@ -14,7 +14,7 @@ export function DrainJobButton({ jobId, draining }: Props) { const router = useRouter(); if (draining) { return ( - <span className="px-2 py-1 rounded border border-amber-300 dark:border-amber-800 text-xs font-medium text-amber-700 dark:text-amber-300"> + <span className="px-2 py-1 rounded border border-warning/30 text-xs font-medium text-warning"> Draining… </span> ); @@ -33,7 +33,7 @@ export function DrainJobButton({ jobId, draining }: Props) { }} disabled={busy} title="Finish the in-flight operations, start no new ones, then release the queue" - className="px-2 py-1 rounded border border-amber-300 dark:border-amber-800 text-xs font-medium text-amber-700 dark:text-amber-300 hover:bg-amber-50 dark:hover:bg-amber-950 disabled:opacity-50" + className="px-2 py-1 rounded border border-warning/30 text-xs font-medium text-warning hover:bg-warning-soft disabled:opacity-50" > {busy ? "Draining…" : "Drain"} </button> diff --git a/editor/app/jobs/components/JobsTable.tsx b/editor/app/jobs/components/JobsTable.tsx @@ -26,17 +26,17 @@ function fmtDuration(ms: number): string { function statusColor(status: string): string { switch (status) { case "queued": - return "bg-zinc-200 text-zinc-700 dark:bg-zinc-700 dark:text-zinc-200"; + return "bg-muted text-muted-foreground"; case "running": - return "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200"; + return "bg-info-soft text-info"; case "done": - return "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"; + return "bg-success-soft text-success"; case "failed": - return "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"; + return "bg-destructive-soft text-destructive"; case "cancelled": - return "bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200"; + return "bg-warning-soft text-warning"; default: - return "bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300"; + return "bg-muted text-muted-foreground"; } } @@ -114,7 +114,7 @@ export function JobsTable({ jobs }: { jobs: JobListEntry[] }) { <div className="flex flex-col gap-2 text-sm"> {kinds.length > 0 && ( <div className="flex flex-wrap items-center gap-2"> - <span className="text-xs uppercase tracking-wide text-zinc-500"> + <span className="text-xs uppercase tracking-wide text-muted-foreground"> kind </span> {kinds.map((kind) => { @@ -126,8 +126,8 @@ export function JobsTable({ jobs }: { jobs: JobListEntry[] }) { onClick={() => toggleKind(kind)} className={`px-2 py-0.5 rounded text-xs border ${ hidden - ? "border-zinc-300 dark:border-zinc-700 text-zinc-400 dark:text-zinc-600 line-through" - : "border-zinc-400 dark:border-zinc-600 bg-zinc-100 dark:bg-zinc-800 text-zinc-800 dark:text-zinc-200" + ? "border-border text-muted-foreground line-through" + : "border-border-strong bg-muted text-foreground" }`} title={kind} > @@ -139,7 +139,7 @@ export function JobsTable({ jobs }: { jobs: JobListEntry[] }) { )} {statuses.length > 0 && ( <div className="flex flex-wrap items-center gap-2"> - <span className="text-xs uppercase tracking-wide text-zinc-500"> + <span className="text-xs uppercase tracking-wide text-muted-foreground"> status </span> {statuses.map((status) => { @@ -151,7 +151,7 @@ export function JobsTable({ jobs }: { jobs: JobListEntry[] }) { onClick={() => toggleStatus(status)} className={`px-2 py-0.5 rounded text-xs uppercase tracking-wide border ${ hidden - ? "border-zinc-300 dark:border-zinc-700 text-zinc-400 dark:text-zinc-600 line-through" + ? "border-border text-muted-foreground line-through" : statusColor(status) }`} > @@ -170,16 +170,16 @@ export function JobsTable({ jobs }: { jobs: JobListEntry[] }) { } placeholder="Search id / channel / video…" aria-label="Search jobs" - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm" + className="rounded border border-border bg-card px-2 py-1 text-sm" /> - <span className="text-xs text-zinc-500"> + <span className="text-xs text-muted-foreground"> Showing {visible.length} of {jobs.length} {hiddenCount > 0 && ` · ${hiddenCount} hidden`} </span> <button type="button" onClick={reset} - className="px-2 py-1 rounded border border-zinc-300 dark:border-zinc-700 text-xs text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800" + className="px-2 py-1 rounded border border-border text-xs text-muted-foreground hover:bg-muted" > Reset filters </button> @@ -187,8 +187,8 @@ export function JobsTable({ jobs }: { jobs: JobListEntry[] }) { </div> <div className="overflow-x-auto -mx-4 md:mx-0 md:overflow-visible"> - <table className="text-sm border-y md:border border-zinc-200 dark:border-zinc-800 md:rounded-md md:overflow-hidden w-full"> - <thead className="bg-zinc-100 dark:bg-zinc-900"> + <table className="text-sm border-y md:border border-border md:rounded-md md:overflow-hidden w-full"> + <thead className="bg-muted"> <tr> <th className="text-left font-medium px-3 py-2">ID</th> <th className="text-left font-medium px-3 py-2">Kind</th> @@ -213,12 +213,12 @@ export function JobsTable({ jobs }: { jobs: JobListEntry[] }) { return ( <tr key={j.id} - className="border-t border-zinc-200 dark:border-zinc-800" + className="border-t border-border" > <td className="px-3 py-2 font-mono text-xs"> <Link href={`/jobs/${j.id}`} - className="underline hover:text-zinc-900 dark:hover:text-zinc-100" + className="underline hover:text-foreground" > {j.id} </Link> @@ -230,7 +230,7 @@ export function JobsTable({ jobs }: { jobs: JobListEntry[] }) { {j.channelSlug ? ( <Link href={`/channels/${j.channelSlug}`} - className="underline hover:text-zinc-900 dark:hover:text-zinc-100" + className="underline hover:text-foreground" > {j.channelSlug} </Link> @@ -242,7 +242,7 @@ export function JobsTable({ jobs }: { jobs: JobListEntry[] }) { {j.channelSlug && j.videoId ? ( <Link href={`/channels/${j.channelSlug}/videos/${encodeURIComponent(j.videoId)}`} - className="underline hover:text-zinc-900 dark:hover:text-zinc-100" + className="underline hover:text-foreground" > {j.videoId} </Link> @@ -265,18 +265,18 @@ export function JobsTable({ jobs }: { jobs: JobListEntry[] }) { </span> </td> <td - className="px-3 py-2 text-xs text-zinc-500 whitespace-nowrap" + className="px-3 py-2 text-xs text-muted-foreground whitespace-nowrap" suppressHydrationWarning > {new Date(startedAt).toLocaleString()} </td> <td - className="px-3 py-2 text-xs text-zinc-500 whitespace-nowrap" + className="px-3 py-2 text-xs text-muted-foreground whitespace-nowrap" suppressHydrationWarning > {fmtDuration(dur)} </td> - <td className="px-3 py-2 text-xs text-zinc-500 text-right whitespace-nowrap"> + <td className="px-3 py-2 text-xs text-muted-foreground text-right whitespace-nowrap"> {j.logSize.toLocaleString()} B </td> <td className="px-3 py-2 text-right"> diff --git a/editor/app/jobs/components/PauseTranscriptionsButton.tsx b/editor/app/jobs/components/PauseTranscriptionsButton.tsx @@ -37,7 +37,7 @@ export function PauseTranscriptionsButton({ type="button" disabled={pending} onClick={toggle} - className="px-3 py-1.5 rounded-md bg-amber-500 text-white text-sm font-medium hover:bg-amber-600 disabled:opacity-50" + className="px-3 py-1.5 rounded-md bg-warning text-warning-foreground text-sm font-medium hover:bg-warning/90 disabled:opacity-50" > Resume Transcriptions </button> @@ -47,7 +47,7 @@ export function PauseTranscriptionsButton({ disabled={pending || disabled} onClick={toggle} title="Stop taking new work; in-flight parakeet jobs finish the current segment, cache it, and resume on the next run" - className="px-3 py-1.5 rounded-md border border-zinc-300 dark:border-zinc-700 text-sm hover:bg-zinc-100 dark:hover:bg-zinc-800 disabled:opacity-50" + className="px-3 py-1.5 rounded-md border border-border text-sm hover:bg-muted disabled:opacity-50" > Pause Transcriptions </button> diff --git a/editor/app/jobs/components/ReorderJobButtons.tsx b/editor/app/jobs/components/ReorderJobButtons.tsx @@ -28,7 +28,7 @@ export function ReorderJobButtons({ jobId, canMoveUp, canMoveDown }: Props) { } }; const btn = - "px-1.5 py-1 rounded border border-zinc-300 dark:border-zinc-700 text-xs font-medium text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800 disabled:opacity-40"; + "px-1.5 py-1 rounded border border-border text-xs font-medium text-muted-foreground hover:bg-muted disabled:opacity-40"; // Only render an action that's actually possible: a first-queued job can't move // up or promote; a last-queued job can't move down. return ( diff --git a/editor/app/jobs/components/RetryAllFailedButton.tsx b/editor/app/jobs/components/RetryAllFailedButton.tsx @@ -22,7 +22,7 @@ export function RetryAllFailedButton() { setBusy(false); } }} - className="px-3 py-1.5 rounded border border-blue-300 dark:border-blue-800 text-sm font-medium text-blue-700 dark:text-blue-300 hover:bg-blue-50 dark:hover:bg-blue-950 disabled:opacity-50" + className="px-3 py-1.5 rounded border border-info/30 text-sm font-medium text-info hover:bg-info-soft disabled:opacity-50" > {busy ? "Retrying…" : "Retry all failed"} </button> diff --git a/editor/app/jobs/components/RetryJobButton.tsx b/editor/app/jobs/components/RetryJobButton.tsx @@ -31,12 +31,12 @@ export function RetryJobButton({ jobId }: Props) { } }} aria-label={`retry job ${jobId}`} - className="px-2 py-1 rounded border border-blue-300 dark:border-blue-800 text-xs font-medium text-blue-700 dark:text-blue-300 hover:bg-blue-50 dark:hover:bg-blue-950 disabled:opacity-50" + className="px-2 py-1 rounded border border-info/30 text-xs font-medium text-info hover:bg-info-soft disabled:opacity-50" > {busy ? "Retrying…" : "Retry"} </button> {note && ( - <span className="text-xs text-zinc-500" title={note}> + <span className="text-xs text-muted-foreground" title={note}> {note} </span> )} diff --git a/editor/app/jobs/components/RunningJobsList.tsx b/editor/app/jobs/components/RunningJobsList.tsx @@ -73,9 +73,9 @@ export function RunningJobsList({ jobs, hideChannelSlug, hideVideoId }: Props) { return ( <section aria-label="Active jobs" - className="flex flex-col gap-2 border border-zinc-200 dark:border-zinc-800 rounded-md p-3 bg-zinc-50 dark:bg-zinc-900/40" + className="flex flex-col gap-2 border border-border rounded-md p-3 bg-card" > - <h2 className="text-sm font-medium text-zinc-700 dark:text-zinc-300"> + <h2 className="text-sm font-medium text-muted-foreground"> Active jobs </h2> <ul className="flex flex-col gap-2"> @@ -104,10 +104,10 @@ function JobRow({ const [showLog, setShowLog] = useState(false); const statusClass = job.status === "running" - ? "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200" - : "bg-zinc-200 text-zinc-700 dark:bg-zinc-700 dark:text-zinc-200"; + ? "bg-info-soft text-info" + : "bg-muted text-muted-foreground"; return ( - <li className="flex flex-col gap-2 border-t border-zinc-200 dark:border-zinc-800 pt-2 first:border-t-0 first:pt-0"> + <li className="flex flex-col gap-2 border-t border-border pt-2 first:border-t-0 first:pt-0"> <div className="flex flex-wrap items-center gap-2 text-sm"> <span className={`text-xs uppercase tracking-wide px-2 py-0.5 rounded ${statusClass}`} @@ -119,7 +119,7 @@ function JobRow({ </span> {job.background && ( <span - className="text-[10px] uppercase tracking-wide px-1.5 py-0.5 rounded bg-amber-100 text-amber-800 dark:bg-amber-900/60 dark:text-amber-200" + className="text-[10px] uppercase tracking-wide px-1.5 py-0.5 rounded bg-warning-soft text-warning" title="Background work — yields to a manually-triggered sync/download on the same platform" > auto @@ -127,14 +127,14 @@ function JobRow({ )} <Link href={`/jobs/${job.id}`} - className="font-mono text-xs underline hover:text-zinc-900 dark:hover:text-zinc-100" + className="font-mono text-xs underline hover:text-foreground" > Job {job.id} </Link> {!hideChannelSlug && job.channelSlug && ( <Link href={`/channels/${job.channelSlug}`} - className="font-mono text-xs underline hover:text-zinc-900 dark:hover:text-zinc-100" + className="font-mono text-xs underline hover:text-foreground" > {job.channelSlug} </Link> @@ -142,13 +142,13 @@ function JobRow({ {!hideVideoId && job.channelSlug && job.videoId && ( <Link href={`/channels/${job.channelSlug}/videos/${encodeURIComponent(job.videoId)}`} - className="font-mono text-xs underline hover:text-zinc-900 dark:hover:text-zinc-100" + className="font-mono text-xs underline hover:text-foreground" > {job.videoId} </Link> )} {job.status === "queued" && ( - <span className="text-xs text-zinc-500"> + <span className="text-xs text-muted-foreground"> queue <code className="font-mono">{job.queueKey || "—"}</code> {job.background && " · waiting behind a manual job"} </span> @@ -171,7 +171,7 @@ function JobRow({ onClick={() => setShowLog((s) => !s)} aria-expanded={showLog} aria-label={`${showLog ? "hide" : "show"} log for job ${job.id}`} - className="px-2 py-1 rounded-md bg-zinc-200 dark:bg-zinc-800 text-zinc-900 dark:text-zinc-100 text-xs font-medium hover:opacity-90" + className="px-2 py-1 rounded-md bg-muted text-foreground text-xs font-medium hover:opacity-90" > {showLog ? "Hide log" : "Show log"} </button> @@ -181,7 +181,7 @@ function JobRow({ {job.tasks && job.tasks.length > 0 && ( <ul aria-label="In-progress operations" - className="flex flex-col gap-1.5 pl-2 border-l-2 border-zinc-200 dark:border-zinc-800" + className="flex flex-col gap-1.5 pl-2 border-l-2 border-border" > {job.tasks.map((t) => ( <li key={t.id}> @@ -241,11 +241,11 @@ function TaskProgressBar({ task }: { task: RunningJobsTask }) { const fillClass = probing ? "bg-violet-400 dark:bg-violet-500" : task.kind === "download" - ? "bg-emerald-300 dark:bg-emerald-700" - : "bg-emerald-500"; + ? "bg-success/60" + : "bg-success"; const pulseClass = probing ? "bg-violet-400 dark:bg-violet-500" - : "bg-amber-400 dark:bg-amber-500"; + : "bg-warning"; const elapsed = now === null ? null @@ -262,16 +262,16 @@ function TaskProgressBar({ task }: { task: RunningJobsTask }) { <div className="flex flex-col gap-0.5"> <div className="flex items-baseline justify-between gap-2 text-xs"> <span className="truncate"> - <span className="text-zinc-500">{verb} </span> + <span className="text-muted-foreground">{verb} </span> <span className="font-mono">{task.label}</span> {task.workerName && ( - <span className="text-zinc-400"> + <span className="text-muted-foreground"> {" "} on <span className="font-mono">{task.workerName}</span> </span> )} </span> - <span className="font-mono text-zinc-500 shrink-0">{meta}</span> + <span className="font-mono text-muted-foreground shrink-0">{meta}</span> </div> <div role="progressbar" @@ -279,7 +279,7 @@ function TaskProgressBar({ task }: { task: RunningJobsTask }) { aria-valuemax={100} aria-valuenow={hasFraction ? pct : undefined} aria-label={`${verb} ${task.label}`} - className="relative h-1.5 w-full overflow-hidden rounded bg-zinc-200 dark:bg-zinc-800" + className="relative h-1.5 w-full overflow-hidden rounded bg-muted" > {hasFraction ? ( <div @@ -318,8 +318,8 @@ function JobProgressBar({ : "estimating…"; const fillClass = progress.metric === "downloads" - ? "bg-emerald-300 dark:bg-emerald-700" - : "bg-emerald-500"; + ? "bg-success/60" + : "bg-success"; return ( <div className="flex flex-col gap-1"> <div @@ -328,14 +328,14 @@ function JobProgressBar({ aria-valuemax={progress.target} aria-valuenow={progress.current} aria-label={label} - className="relative h-2 w-full overflow-hidden rounded bg-zinc-200 dark:bg-zinc-800" + className="relative h-2 w-full overflow-hidden rounded bg-muted" > <div className={`absolute inset-y-0 left-0 ${fillClass}`} style={{ width: `${progress.pct}%` }} /> </div> - <div className="flex items-baseline justify-between gap-2 text-xs text-zinc-500"> + <div className="flex items-baseline justify-between gap-2 text-xs text-muted-foreground"> <span>{label}</span> {etaText ? <span className="shrink-0">{etaText}</span> : null} </div> diff --git a/editor/app/jobs/page.tsx b/editor/app/jobs/page.tsx @@ -30,7 +30,7 @@ export default async function JobsPage() { </div> <BookmarksMenu bookmarks={bookmarks} missingSlugs={missingSlugs} /> {jobs.length === 0 ? ( - <p className="text-sm text-zinc-500 border border-dashed border-zinc-300 dark:border-zinc-700 rounded p-4"> + <p className="text-sm text-muted-foreground border border-dashed border-border rounded p-4"> No jobs have run yet. </p> ) : ( diff --git a/editor/app/page.tsx b/editor/app/page.tsx @@ -113,9 +113,9 @@ export default async function Dashboard({ <Link href="/actionable" aria-label="needs attention" - className="block rounded-md border border-amber-300/70 dark:border-amber-900 bg-amber-50 dark:bg-amber-950/40 px-4 py-3 hover:border-amber-400 dark:hover:border-amber-700 transition-colors" + className="block rounded-md border border-warning/40 bg-warning-soft px-4 py-3 hover:border-warning/60 transition-colors" > - <h2 className="font-mono text-xs uppercase tracking-[0.14em] text-amber-700 dark:text-amber-400 mb-1"> + <h2 className="font-mono text-xs uppercase tracking-[0.14em] text-warning mb-1"> Needs attention </h2> <p className="text-sm"> diff --git a/editor/app/saved-videos/components/SavedVideosControls.tsx b/editor/app/saved-videos/components/SavedVideosControls.tsx @@ -33,14 +33,14 @@ export function SavedVideosControls({ enabled, dest, intervalMinutes }: Props) { <div className="flex flex-col gap-4"> <form action={formAction} - className="flex flex-col gap-3 rounded border border-zinc-200 dark:border-zinc-800 p-4 max-w-xl" + className="flex flex-col gap-3 rounded border border-border p-4 max-w-xl" aria-label="backup config" > <h2 className="text-base font-semibold">Backup configuration</h2> {state?.ok === false && ( <div role="alert" - className="rounded border border-red-300 bg-red-50 dark:border-red-800 dark:bg-red-950 px-3 py-2 text-sm text-red-700 dark:text-red-300" + className="rounded border border-destructive/30 bg-destructive-soft px-3 py-2 text-sm text-destructive" > {state.error} </div> @@ -48,7 +48,7 @@ export function SavedVideosControls({ enabled, dest, intervalMinutes }: Props) { {state?.ok === true && ( <div aria-label="backup config saved" - className="rounded border border-green-300 bg-green-50 dark:border-green-800 dark:bg-green-950 px-3 py-2 text-sm text-green-700 dark:text-green-300" + className="rounded border border-success/30 bg-success-soft px-3 py-2 text-sm text-success" > Saved. </div> @@ -62,9 +62,9 @@ export function SavedVideosControls({ enabled, dest, intervalMinutes }: Props) { onChange={(e) => setDestInput(e.target.value)} placeholder="/mnt/backup/saved-videos" aria-label="backup destination" - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm font-mono" + className="rounded border border-border bg-card px-2 py-1 text-sm font-mono" /> - <span className="text-xs text-zinc-500"> + <span className="text-xs text-muted-foreground"> Local filesystem path (a mounted backup disk) the store is mirrored into. The mirror is additive — it never deletes. Blank disables both scheduled and manual backups. @@ -80,7 +80,7 @@ export function SavedVideosControls({ enabled, dest, intervalMinutes }: Props) { /> <span className="flex flex-col gap-0.5"> <span className="font-medium">Scheduled backup</span> - <span className="text-xs text-zinc-500"> + <span className="text-xs text-muted-foreground"> When on (and a destination is set), the sync scheduler runs the backup on the cadence below. Manual backups work regardless. </span> @@ -94,13 +94,13 @@ export function SavedVideosControls({ enabled, dest, intervalMinutes }: Props) { defaultValue={String(intervalMinutes)} min={1} aria-label="backup interval minutes" - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm w-40" + className="rounded border border-border bg-card px-2 py-1 text-sm w-40" /> </label> <div> <button type="submit" - className="px-3 py-2 rounded-md bg-zinc-900 dark:bg-zinc-100 text-zinc-100 dark:text-zinc-900 text-sm font-medium hover:opacity-90" + className="px-3 py-2 rounded-md bg-primary text-primary-foreground text-sm font-medium hover:opacity-90" > Save backup config </button> @@ -125,7 +125,7 @@ export function SavedVideosControls({ enabled, dest, intervalMinutes }: Props) { /> </div> {!hasDest && ( - <p className="text-xs text-zinc-500"> + <p className="text-xs text-muted-foreground"> Set a backup destination above and save to enable backups. </p> )} diff --git a/editor/app/saved-videos/page.tsx b/editor/app/saved-videos/page.tsx @@ -52,7 +52,7 @@ export default async function SavedVideosPage() { <main className="flex flex-col gap-8 p-6 max-w-4xl"> <header className="flex flex-col gap-1"> <h1 className="text-xl font-semibold">Saved videos</h1> - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> Source video containers persisted by the keep-latest retention rule live in a separate store, leaving the main data volume holding only audio + transcripts. Configure a channel's keep-latest window and @@ -75,18 +75,18 @@ export default async function SavedVideosPage() { <section aria-label="store location" className="flex flex-col gap-1 text-sm"> <div> - <span className="text-zinc-500">Default store dir: </span> + <span className="text-muted-foreground">Default store dir: </span> <span className="font-mono">{paths.savedVideosDir}</span> </div> <div> - <span className="text-zinc-500">Last backup: </span> + <span className="text-muted-foreground">Last backup: </span> {lastBackup ? ( <span>{lastBackup}</span> ) : ( - <span className="text-zinc-500">never</span> + <span className="text-muted-foreground">never</span> )} {backup.enabled && backup.dest && ( - <span className="text-zinc-500"> + <span className="text-muted-foreground"> {" "} · scheduled every {backup.intervalMinutes} min </span> @@ -97,14 +97,14 @@ export default async function SavedVideosPage() { <section aria-label="per-channel saved videos" className="flex flex-col gap-2"> <h2 className="text-base font-semibold">By channel</h2> {channels.length === 0 ? ( - <p className="text-sm text-zinc-600 dark:text-zinc-400"> + <p className="text-sm text-muted-foreground"> No saved videos yet. Set a channel's keep-latest window, then run a sync (or <em>Persist kept now</em> from its Cleanup stage). </p> ) : ( <table className="text-sm border-collapse"> <thead> - <tr className="text-left text-zinc-500"> + <tr className="text-left text-muted-foreground"> <th className="py-1 pr-6 font-medium">Channel</th> <th className="py-1 pr-6 font-medium tabular-nums">Saved</th> <th className="py-1 pr-6 font-medium tabular-nums">Size</th> @@ -115,7 +115,7 @@ export default async function SavedVideosPage() { {channels.map((c) => ( <tr key={c.slug} - className="border-t border-zinc-200 dark:border-zinc-800" + className="border-t border-border" > <td className="py-1 pr-6"> <Link @@ -153,8 +153,8 @@ export default async function SavedVideosPage() { function SummaryCard({ label, value }: { label: string; value: string }) { return ( - <div className="rounded border border-zinc-200 dark:border-zinc-800 p-3"> - <div className="text-xs uppercase tracking-wide text-zinc-500"> + <div className="rounded border border-border p-3"> + <div className="text-xs uppercase tracking-wide text-muted-foreground"> {label} </div> <div className="text-lg font-semibold tabular-nums mt-0.5">{value}</div> diff --git a/editor/app/scheduler/components/ChannelIntervalEditor.tsx b/editor/app/scheduler/components/ChannelIntervalEditor.tsx @@ -12,7 +12,7 @@ import { } from "../intervalPresets"; const inputClass = - "rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm"; + "rounded border border-border bg-card px-2 py-1 text-sm"; // Map a stored per-channel interval to the editor's initial control state. // undefined -> "" (inherit global default) @@ -79,17 +79,17 @@ export function ChannelIntervalEditor({ <button type="submit" disabled={pending} - className="px-2 py-1 rounded-md bg-zinc-900 dark:bg-zinc-100 text-zinc-100 dark:text-zinc-900 text-xs font-medium hover:opacity-90 disabled:opacity-50" + className="px-2 py-1 rounded-md bg-primary text-primary-foreground text-xs font-medium hover:opacity-90 disabled:opacity-50" > {pending ? "Saving…" : "Save"} </button> {state?.ok === true && ( - <span role="status" className="text-xs text-green-700 dark:text-green-300"> + <span role="status" className="text-xs text-success"> Saved </span> )} {state?.ok === false && ( - <span role="alert" className="text-xs text-red-700 dark:text-red-300"> + <span role="alert" className="text-xs text-destructive"> {state.error} </span> )} diff --git a/editor/app/scheduler/components/SchedulerSettingsForm.tsx b/editor/app/scheduler/components/SchedulerSettingsForm.tsx @@ -8,7 +8,7 @@ import { } from "../actions"; const inputClass = - "rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm"; + "rounded border border-border bg-card px-2 py-1 text-sm"; // The headline global controls, editable inline on the scheduler page. Advanced // knobs (concurrency, quiet hours, backoff) stay on /settings to avoid a sprawl; @@ -28,11 +28,11 @@ export function SchedulerSettingsForm({ return ( <form action={formAction} - className="flex flex-col gap-3 border border-zinc-200 dark:border-zinc-800 rounded p-3" + className="flex flex-col gap-3 border border-border rounded p-3" > <div className="flex items-center justify-between gap-3"> <h2 className="text-sm font-semibold">Global controls</h2> - <a href="/settings" className="text-xs underline text-zinc-500"> + <a href="/settings" className="text-xs underline text-muted-foreground"> Advanced (concurrency, quiet hours, backoff) </a> </div> @@ -45,7 +45,7 @@ export function SchedulerSettingsForm({ /> <span className="flex flex-col gap-0.5"> <span className="font-medium">Enable scheduled auto-sync</span> - <span className="text-xs text-zinc-500"> + <span className="text-xs text-muted-foreground"> Master switch. When off, ticks are no-ops and only manual syncs run. </span> </span> @@ -59,7 +59,7 @@ export function SchedulerSettingsForm({ defaultValue={String(scheduler.defaultIntervalMinutes)} className={`${inputClass} w-40`} /> - <span className="text-xs text-zinc-500"> + <span className="text-xs text-muted-foreground"> Cadence for channels left on “Default”. 60 = hourly, 1440 = daily. </span> </label> @@ -71,7 +71,7 @@ export function SchedulerSettingsForm({ defaultValue={String(heartbeatSeconds)} className={`${inputClass} w-40`} /> - <span className="text-xs text-zinc-500"> + <span className="text-xs text-muted-foreground"> 0 = off (use an external cron heartbeat). Clamped to 15–3600s. Turning it on from 0 needs a server restart. </span> @@ -81,17 +81,17 @@ export function SchedulerSettingsForm({ <button type="submit" disabled={pending} - className="px-3 py-1.5 rounded-md bg-zinc-900 dark:bg-zinc-100 text-zinc-100 dark:text-zinc-900 text-sm font-medium hover:opacity-90 disabled:opacity-50" + className="px-3 py-1.5 rounded-md bg-primary text-primary-foreground text-sm font-medium hover:opacity-90 disabled:opacity-50" > {pending ? "Saving…" : "Save controls"} </button> {state?.ok === true && ( - <span role="status" className="text-sm text-green-700 dark:text-green-300"> + <span role="status" className="text-sm text-success"> Saved. </span> )} {state?.ok === false && ( - <span role="alert" className="text-sm text-red-700 dark:text-red-300"> + <span role="alert" className="text-sm text-destructive"> {state.error} </span> )} diff --git a/editor/app/scheduler/components/SchedulerView.tsx b/editor/app/scheduler/components/SchedulerView.tsx @@ -73,13 +73,13 @@ export function SchedulerView({ <span className={`text-sm rounded-full px-3 py-1 border ${ scheduler.enabled - ? "border-green-300 bg-green-50 text-green-800 dark:border-green-800 dark:bg-green-950 dark:text-green-200" - : "border-zinc-300 bg-zinc-50 text-zinc-600 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-400" + ? "border-success/30 bg-success-soft text-success" + : "border-border bg-muted text-muted-foreground" }`} > {scheduler.enabled ? "Scheduler enabled" : "Scheduler disabled"} </span> - <span className="text-sm text-zinc-500"> + <span className="text-sm text-muted-foreground"> {eligible.length} channel{eligible.length === 1 ? "" : "s"} auto-syncing · default {formatInterval(scheduler.defaultIntervalMinutes)} · max{" "} {scheduler.maxConcurrentSyncs} concurrent ·{" "} @@ -91,13 +91,13 @@ export function SchedulerView({ type="button" onClick={runNow} disabled={busy} - className="ml-auto px-3 py-1.5 rounded-md bg-zinc-900 dark:bg-zinc-100 text-zinc-100 dark:text-zinc-900 text-sm font-medium hover:opacity-90 disabled:opacity-50" + className="ml-auto px-3 py-1.5 rounded-md bg-primary text-primary-foreground text-sm font-medium hover:opacity-90 disabled:opacity-50" > {busy ? "Running…" : "Run scheduler now"} </button> </div> {message && ( - <p role="status" className="text-sm text-zinc-600 dark:text-zinc-300"> + <p role="status" className="text-sm text-muted-foreground"> {message} </p> )} @@ -107,9 +107,9 @@ export function SchedulerView({ heartbeatSeconds={heartbeatSeconds} /> - <div className="overflow-x-auto rounded border border-zinc-200 dark:border-zinc-800"> + <div className="overflow-x-auto rounded border border-border"> <table className="w-full text-sm"> - <thead className="bg-zinc-50 dark:bg-zinc-900 text-left text-zinc-500"> + <thead className="bg-card text-left text-muted-foreground"> <tr> <th className="px-3 py-2 font-medium">Channel</th> <th className="px-3 py-2 font-medium">Interval</th> @@ -121,7 +121,7 @@ export function SchedulerView({ <tbody> {channels.length === 0 && ( <tr> - <td colSpan={5} className="px-3 py-4 text-zinc-500"> + <td colSpan={5} className="px-3 py-4 text-muted-foreground"> No channels. </td> </tr> @@ -129,7 +129,7 @@ export function SchedulerView({ {channels.map((c) => ( <tr key={c.slug} - className="border-t border-zinc-100 dark:border-zinc-800" + className="border-t border-border" > <td className="px-3 py-2"> <a href={`/channels/${c.slug}`} className="hover:underline"> @@ -142,10 +142,10 @@ export function SchedulerView({ configured={c.configuredIntervalMinutes} /> </td> - <td className="px-3 py-2 text-zinc-600 dark:text-zinc-300"> + <td className="px-3 py-2 text-muted-foreground"> {c.lastSyncedAt ? formatAgo(c.lastSyncedAt, now) : "never"} </td> - <td className="px-3 py-2 text-zinc-600 dark:text-zinc-300"> + <td className="px-3 py-2 text-muted-foreground"> {nextDueLabel(c, now)} </td> <td className="px-3 py-2">{statusBadge(c)}</td> @@ -158,25 +158,25 @@ export function SchedulerView({ <section className="flex flex-col gap-2"> <h2 className="text-sm font-semibold">Recent ticks</h2> {runs.length === 0 ? ( - <p className="text-sm text-zinc-500">No ticks recorded yet.</p> + <p className="text-sm text-muted-foreground">No ticks recorded yet.</p> ) : ( <ul className="flex flex-col gap-1 text-sm"> {runs.slice(0, 15).map((r, i) => ( <li key={`${r.at}-${i}`} - className="flex flex-wrap gap-x-3 gap-y-0.5 text-zinc-600 dark:text-zinc-300" + className="flex flex-wrap gap-x-3 gap-y-0.5 text-muted-foreground" > - <span className="tabular-nums text-zinc-500"> + <span className="tabular-nums text-muted-foreground"> {formatClock(r.at)} </span> <span> queued {r.queued.length} {r.queued.length > 0 && ( - <span className="text-zinc-400"> ({r.queued.join(", ")})</span> + <span className="text-muted-foreground"> ({r.queued.join(", ")})</span> )} </span> {r.skipped.length > 0 && ( - <span className="text-zinc-400"> + <span className="text-muted-foreground"> skipped {r.skipped.length} </span> )} diff --git a/editor/app/scheduler/page.tsx b/editor/app/scheduler/page.tsx @@ -17,7 +17,7 @@ export default async function SchedulerPage() { Scheduler settings </Link> </div> - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> Per-channel auto-sync cadence, driven by a heartbeat (<code>pnpm sync:tick</code> or the internal timer). Set each channel's interval inline below — pick a preset or type an exact diff --git a/editor/app/settings/components/SettingsForm.tsx b/editor/app/settings/components/SettingsForm.tsx @@ -55,11 +55,11 @@ export function SettingsForm({ initial, apps }: Props) { hint="Bytes per generated transcript page (256 KB – 20 MB). Affects pagination next build." type="number" /> - <fieldset className="flex flex-col gap-3 border border-zinc-200 dark:border-zinc-800 rounded p-3"> + <fieldset className="flex flex-col gap-3 border border-border rounded p-3"> <legend className="px-1 text-sm font-medium"> Transcription workers </legend> - <p className="text-xs text-zinc-500"> + <p className="text-xs text-muted-foreground"> Each worker is one processing slot — one transcription at a time. To run several in parallel on the same engine, add more workers (use{" "} <strong>Copy</strong> to duplicate one). List order is priority (top = @@ -91,7 +91,7 @@ export function SettingsForm({ initial, apps }: Props) { <select name="downloadFormat" defaultValue={initial.downloadFormat} - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm" + className="rounded border border-border bg-card px-2 py-1 text-sm" > {DOWNLOAD_FORMAT_PRESETS.map((p) => ( <option key={p} value={p}> @@ -99,7 +99,7 @@ export function SettingsForm({ initial, apps }: Props) { </option> ))} </select> - <span className="text-xs text-zinc-500"> + <span className="text-xs text-muted-foreground"> The yt-dlp <code>-f</code> selector for managed downloads.{" "} <strong>Auto</strong> picks per-source: Odysee/LBRY downloads its full-length <code>original</code> file (its HLS streams are @@ -133,7 +133,7 @@ export function SettingsForm({ initial, apps }: Props) { <span className="font-medium"> Transcribe immediately after no-subs fallback </span> - <span className="text-xs text-zinc-500"> + <span className="text-xs text-muted-foreground"> When the managed downloader falls back to audio+whisper for a video without captions, run whisper inline instead of leaving the audio for the next “Transcribe missing” pass. @@ -153,7 +153,7 @@ export function SettingsForm({ initial, apps }: Props) { <span className="font-medium"> Skip live and upcoming videos </span> - <span className="text-xs text-zinc-500"> + <span className="text-xs text-muted-foreground"> Before each managed download, a quick metadata-only pass checks whether the video is currently live or scheduled/upcoming and, if so, skips it (finished livestream VODs download normally). The skip @@ -168,7 +168,7 @@ export function SettingsForm({ initial, apps }: Props) { <select name="reportDebouncePreset" defaultValue={initial.reportDebouncePreset} - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm" + className="rounded border border-border bg-card px-2 py-1 text-sm" > <option value="fast">Fast — ~1s after activity, no cap</option> <option value="balanced"> @@ -176,16 +176,16 @@ export function SettingsForm({ initial, apps }: Props) { </option> <option value="lazy">Lazy — ~10s after activity, 60s cap</option> </select> - <span className="text-xs text-zinc-500"> + <span className="text-xs text-muted-foreground"> After any action that changes a channel report (download, transcribe, cleanup, file edits…), the report is regenerated automatically. This controls how long the shared scheduler waits for activity to settle before regenerating, so bursts of actions collapse into one pass. </span> </label> - <fieldset className="flex flex-col gap-3 border border-zinc-200 dark:border-zinc-800 rounded p-3"> + <fieldset className="flex flex-col gap-3 border border-border rounded p-3"> <legend className="px-1 text-sm font-medium">Sync scheduler</legend> - <p className="text-xs text-zinc-500"> + <p className="text-xs text-muted-foreground"> Auto-sync channels on a per-channel cadence (set under each channel's Auto-sync). A heartbeat fires periodically and the server syncs whichever channels are due. Set the internal heartbeat below to run it in-process @@ -201,7 +201,7 @@ export function SettingsForm({ initial, apps }: Props) { /> <span className="flex flex-col gap-1"> <span className="font-medium">Enable scheduled auto-sync</span> - <span className="text-xs text-zinc-500"> + <span className="text-xs text-muted-foreground"> Master switch. When off, ticks are no-ops and only manual syncs run. </span> @@ -250,7 +250,7 @@ export function SettingsForm({ initial, apps }: Props) { type="number" /> </div> - <p className="text-xs text-zinc-500 -mt-1"> + <p className="text-xs text-muted-foreground -mt-1"> Optional local-clock window when auto-sync is suppressed (may wrap past midnight, e.g. 22 → 6). Leave both blank to always allow. </p> @@ -268,14 +268,14 @@ export function SettingsForm({ initial, apps }: Props) { type="number" /> </div> - <p className="text-xs text-zinc-500 -mt-1"> + <p className="text-xs text-muted-foreground -mt-1"> After N consecutive failed syncs a channel waits base·2^(N-1) minutes (capped at max) before retrying. </p> </fieldset> - <fieldset className="flex flex-col gap-3 border border-zinc-200 dark:border-zinc-800 rounded p-3"> + <fieldset className="flex flex-col gap-3 border border-border rounded p-3"> <legend className="px-1 text-sm font-medium">Build pipeline</legend> - <p className="text-xs text-zinc-500"> + <p className="text-xs text-muted-foreground"> How the static export is built. <strong>Basic</strong> runs the build in <code>export/</code> and serializes builds on one queue.{" "} <strong>Docker</strong> will run each site's build in an isolated @@ -292,7 +292,7 @@ export function SettingsForm({ initial, apps }: Props) { <select name="buildMode" defaultValue={initial.buildPipeline.mode} - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm" + className="rounded border border-border bg-card px-2 py-1 text-sm" > <option value="basic">Basic — serial build queue</option> <option value="docker"> @@ -320,9 +320,9 @@ export function SettingsForm({ initial, apps }: Props) { hint="Path (relative to the repo root) used to build the image." /> </fieldset> - <fieldset className="flex flex-col gap-3 border border-zinc-200 dark:border-zinc-800 rounded p-3"> + <fieldset className="flex flex-col gap-3 border border-border rounded p-3"> <legend className="px-1 text-sm font-medium">Social links</legend> - <p className="text-xs text-zinc-500"> + <p className="text-xs text-muted-foreground"> Default social links shown in every site's footer. Each site can override these from its own page (Sites → a site → Social links). </p> @@ -335,14 +335,14 @@ export function SettingsForm({ initial, apps }: Props) { <div className="flex items-center gap-3"> <button type="submit" - className="px-3 py-2 rounded-md bg-zinc-900 dark:bg-zinc-100 text-zinc-100 dark:text-zinc-900 text-sm font-medium hover:opacity-90" + className="px-3 py-2 rounded-md bg-primary text-primary-foreground text-sm font-medium hover:opacity-90" > Save settings </button> {state?.ok === true && ( <span role="status" - className="text-sm text-green-700 dark:text-green-300" + className="text-sm text-success" > Saved. </span> @@ -350,7 +350,7 @@ export function SettingsForm({ initial, apps }: Props) { {state && state.ok === false && ( <span role="alert" - className="text-sm text-red-700 dark:text-red-300" + className="text-sm text-destructive" > {state.error} </span> @@ -383,9 +383,9 @@ function Field({ name={name} defaultValue={defaultValue} required={required} - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm" + className="rounded border border-border bg-card px-2 py-1 text-sm" /> - {hint && <span className="text-xs text-zinc-500">{hint}</span>} + {hint && <span className="text-xs text-muted-foreground">{hint}</span>} </label> ); } diff --git a/editor/app/settings/components/WorkersField.tsx b/editor/app/settings/components/WorkersField.tsx @@ -119,7 +119,7 @@ export function WorkersField({ initial, apps, name }: Props) { <div className="flex flex-col gap-3"> <input type="hidden" name={name} value={serialize(rows)} readOnly /> {rows.length === 0 && ( - <p className="text-xs text-red-700 dark:text-red-300"> + <p className="text-xs text-destructive"> No workers defined — add at least one so transcription can run. </p> )} @@ -142,14 +142,14 @@ export function WorkersField({ initial, apps, name }: Props) { <button type="button" onClick={() => update([...rows, blankLocal(defaultAppId)])} - className="px-2 py-1 rounded border border-zinc-300 dark:border-zinc-700 text-xs hover:bg-zinc-100 dark:hover:bg-zinc-800" + className="px-2 py-1 rounded border border-border text-xs hover:bg-muted" > + Local worker </button> <button type="button" onClick={() => update([...rows, blankRemote()])} - className="px-2 py-1 rounded border border-zinc-300 dark:border-zinc-700 text-xs hover:bg-zinc-100 dark:hover:bg-zinc-800" + className="px-2 py-1 rounded border border-border text-xs hover:bg-muted" > + Remote worker </button> @@ -188,10 +188,10 @@ function WorkerCard({ return ( <div aria-label={`worker ${index + 1}`} - className="flex flex-col gap-2 rounded border border-zinc-200 dark:border-zinc-800 p-3" + className="flex flex-col gap-2 rounded border border-border p-3" > <div className="flex flex-wrap items-center gap-2"> - <span className="text-xs text-zinc-500" title="priority (lower = preferred)"> + <span className="text-xs text-muted-foreground" title="priority (lower = preferred)"> #{index + 1} </span> <input @@ -200,7 +200,7 @@ function WorkerCard({ placeholder="Worker name" value={row.name} onChange={(e) => onPatch({ name: e.target.value })} - className="flex-1 min-w-40 rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm" + className="flex-1 min-w-40 rounded border border-border bg-card px-2 py-1 text-sm" /> <label className="flex items-center gap-1 text-xs"> <input @@ -217,7 +217,7 @@ function WorkerCard({ onClick={() => onMove(-1)} disabled={index === 0} aria-label={`move worker ${index + 1} up`} - className="px-1.5 py-0.5 rounded border border-zinc-300 dark:border-zinc-700 text-xs disabled:opacity-40" + className="px-1.5 py-0.5 rounded border border-border text-xs disabled:opacity-40" > ↑ </button> @@ -226,7 +226,7 @@ function WorkerCard({ onClick={() => onMove(1)} disabled={index === total - 1} aria-label={`move worker ${index + 1} down`} - className="px-1.5 py-0.5 rounded border border-zinc-300 dark:border-zinc-700 text-xs disabled:opacity-40" + className="px-1.5 py-0.5 rounded border border-border text-xs disabled:opacity-40" > ↓ </button> @@ -235,7 +235,7 @@ function WorkerCard({ onClick={onCopy} aria-label={`copy worker ${index + 1}`} title="Duplicate this worker into another slot" - className="px-1.5 py-0.5 rounded border border-zinc-300 dark:border-zinc-700 text-xs hover:bg-zinc-100 dark:hover:bg-zinc-800" + className="px-1.5 py-0.5 rounded border border-border text-xs hover:bg-muted" > Copy </button> @@ -243,7 +243,7 @@ function WorkerCard({ type="button" onClick={onRemove} aria-label={`remove worker ${index + 1}`} - className="px-1.5 py-0.5 rounded border border-red-300 dark:border-red-800 text-xs text-red-700 dark:text-red-300 hover:bg-red-50 dark:hover:bg-red-950" + className="px-1.5 py-0.5 rounded border border-destructive/30 text-xs text-destructive hover:bg-destructive-soft" > Remove </button> @@ -258,7 +258,7 @@ function WorkerCard({ value={row.appId ?? ""} onChange={(e) => onPatch({ appId: e.target.value })} aria-label={`worker ${index + 1} engine`} - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm" + className="rounded border border-border bg-card px-2 py-1 text-sm" > {apps.map((a) => ( <option key={a.id} value={a.id}> @@ -268,13 +268,13 @@ function WorkerCard({ </select> </label> )} - <span className="text-xs text-zinc-500"> + <span className="text-xs text-muted-foreground"> {row.kind === "local" ? "Local · one slot" : "Remote · one slot"} </span> </div> {row.kind === "local" ? ( - <div className="flex flex-col gap-2 border-t border-zinc-100 dark:border-zinc-800 pt-2"> + <div className="flex flex-col gap-2 border-t border-border pt-2"> <CardField label="Binary" value={cfg.bin ?? ""} @@ -337,9 +337,9 @@ function WorkerCard({ }) } rows={4} - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm font-mono" + className="rounded border border-border bg-card px-2 py-1 text-sm font-mono" /> - <span className="text-zinc-500"> + <span className="text-muted-foreground"> Blank = default. Placeholders: <code>{"{audioFile}"}</code>,{" "} <code>{"{outputBase}"}</code> (both required),{" "} <code>{"{model}"}</code>. @@ -348,7 +348,7 @@ function WorkerCard({ )} </div> ) : ( - <div className="flex flex-col gap-2 border-t border-zinc-100 dark:border-zinc-800 pt-2"> + <div className="flex flex-col gap-2 border-t border-border pt-2"> <CardField label="Base URL" value={remote.baseUrl} @@ -403,9 +403,9 @@ function CardField({ type={type} value={value} onChange={(e) => onChange(e.target.value)} - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm" + className="rounded border border-border bg-card px-2 py-1 text-sm" /> - {hint && <span className="text-zinc-500">{hint}</span>} + {hint && <span className="text-muted-foreground">{hint}</span>} </label> ); } diff --git a/editor/app/settings/page.tsx b/editor/app/settings/page.tsx @@ -31,7 +31,7 @@ export default function SettingsPage() { <section className="flex flex-col gap-3"> <div> <h2 className="text-lg font-semibold">Site settings</h2> - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> Persisted in <code>settings.json</code> at the monorepo root. Used by the export build to title the static site. </p> @@ -39,12 +39,12 @@ export default function SettingsPage() { <SettingsForm initial={settings} apps={listTranscriptionApps()} /> </section> - <section className="flex flex-col gap-3 border-t border-zinc-200 dark:border-zinc-800 pt-6"> + <section className="flex flex-col gap-3 border-t border-border pt-6"> <details> <summary className="cursor-pointer text-lg font-semibold"> System paths </summary> - <p className="text-sm text-zinc-500 mt-2"> + <p className="text-sm text-muted-foreground mt-2"> Read-only view of <code>getPaths()</code>. Set the matching env vars (TRANSCRIPTS_DIR, SETTINGS_FILE, EXPORT_PUBLIC_DIR, YTDLP_BIN, WHISPER_BIN, WHISPER_MODEL, CHOUGH_BIN, CHOUGH_URL, CHOUGH_MODEL) @@ -57,7 +57,7 @@ export default function SettingsPage() { {pathRows.map(([key, value]) => ( <tr key={key} - className="border-b border-zinc-200 dark:border-zinc-800" + className="border-b border-border" > <th className="text-left font-medium py-2 pr-4 align-top whitespace-nowrap"> {key} diff --git a/editor/app/sites/[siteId]/page.tsx b/editor/app/sites/[siteId]/page.tsx @@ -46,8 +46,8 @@ export default async function EditSitePage({ .map((s) => ({ siteId: s.siteId, siteTitle: s.siteTitle })); return ( <div className="flex flex-col gap-4"> - <div className="flex items-center gap-2 text-sm text-zinc-500"> - <Link href="/sites" className="underline hover:text-zinc-900 dark:hover:text-zinc-100"> + <div className="flex items-center gap-2 text-sm text-muted-foreground"> + <Link href="/sites" className="underline hover:text-foreground"> Sites </Link> <span>/</span> @@ -57,7 +57,7 @@ export default async function EditSitePage({ <h1 className="text-2xl font-semibold">{site.siteTitle || siteId}</h1> <Link href={`/charts?site=${siteId}`} - className="text-sm underline hover:text-zinc-900 dark:hover:text-zinc-100" + className="text-sm underline hover:text-foreground" > Edit charts dashboard → </Link> diff --git a/editor/app/sites/components/SiteForm.tsx b/editor/app/sites/components/SiteForm.tsx @@ -240,22 +240,22 @@ export function SiteForm({ initial, channels, allSites, isNew }: Props) { hint="Absolute URL this site is served at (e.g. https://jeralyzer.com). Used so other sites can link to it in their footer. Leave blank to omit this site from cross-site lists." /> - <fieldset className="flex flex-col gap-3 border border-zinc-200 dark:border-zinc-800 rounded p-3"> + <fieldset className="flex flex-col gap-3 border border-border rounded p-3"> <legend className="px-1 text-sm font-medium">Channels</legend> - <p className="text-xs text-zinc-500"> + <p className="text-xs text-muted-foreground"> Pick the channels this site exposes and the group each belongs to. Channels not checked here are excluded from this site's build. </p> <div className="flex flex-col gap-1 max-h-96 overflow-y-auto"> {channels.length === 0 && ( - <div className="text-sm text-zinc-500">No channels configured.</div> + <div className="text-sm text-muted-foreground">No channels configured.</div> )} {channels.map((c) => { const on = members.has(c.slug); return ( <div key={c.slug} - className="flex items-center gap-2 text-sm py-1 border-b border-zinc-100 dark:border-zinc-900" + className="flex items-center gap-2 text-sm py-1 border-b border-border" > <label className="flex items-center gap-2 flex-1 min-w-0"> <input @@ -266,7 +266,7 @@ export function SiteForm({ initial, channels, allSites, isNew }: Props) { /> <span className="truncate"> {c.name || c.slug}{" "} - <code className="text-xs text-zinc-500">{c.slug}</code> + <code className="text-xs text-muted-foreground">{c.slug}</code> </span> </label> {on && groupOptions.length > 0 && ( @@ -274,7 +274,7 @@ export function SiteForm({ initial, channels, allSites, isNew }: Props) { value={members.get(c.slug) ?? ""} onChange={(e) => setMemberGroup(c.slug, e.target.value)} aria-label={`Group for ${c.name || c.slug}`} - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-1.5 py-0.5 text-xs shrink-0" + className="rounded border border-border bg-card px-1.5 py-0.5 text-xs shrink-0" > <option value="">(default)</option> {groupOptions.map((g) => ( @@ -290,9 +290,9 @@ export function SiteForm({ initial, channels, allSites, isNew }: Props) { </div> </fieldset> - <fieldset className="flex flex-col gap-3 border border-zinc-200 dark:border-zinc-800 rounded p-3"> + <fieldset className="flex flex-col gap-3 border border-border rounded p-3"> <legend className="px-1 text-sm font-medium">Channel groups</legend> - <p className="text-xs text-zinc-500"> + <p className="text-xs text-muted-foreground"> Cosmetic groupings the export UI uses to organize this site's channel checkboxes. </p> @@ -302,7 +302,7 @@ export function SiteForm({ initial, channels, allSites, isNew }: Props) { return ( <div key={idx} - className="flex flex-col gap-2 border border-zinc-200 dark:border-zinc-800 rounded p-3 bg-zinc-50/50 dark:bg-zinc-900/40" + className="flex flex-col gap-2 border border-border rounded p-3 bg-card" > <div className="flex items-center justify-between gap-2"> <label className="flex items-center gap-1.5 text-xs select-none"> @@ -311,7 +311,7 @@ export function SiteForm({ initial, channels, allSites, isNew }: Props) { checked={isDefault} onChange={() => setDefaultGroupId(idTrim)} disabled={!idTrim} - className="accent-blue-600" + className="accent-brand" /> Default group </label> @@ -320,7 +320,7 @@ export function SiteForm({ initial, channels, allSites, isNew }: Props) { type="button" onClick={() => moveGroup(idx, -1)} disabled={idx === 0} - className="px-2 py-0.5 rounded border border-zinc-300 dark:border-zinc-700 text-xs disabled:opacity-40" + className="px-2 py-0.5 rounded border border-border text-xs disabled:opacity-40" aria-label="Move group up" > ↑ @@ -329,7 +329,7 @@ export function SiteForm({ initial, channels, allSites, isNew }: Props) { type="button" onClick={() => moveGroup(idx, 1)} disabled={idx === groups.length - 1} - className="px-2 py-0.5 rounded border border-zinc-300 dark:border-zinc-700 text-xs disabled:opacity-40" + className="px-2 py-0.5 rounded border border-border text-xs disabled:opacity-40" aria-label="Move group down" > ↓ @@ -337,7 +337,7 @@ export function SiteForm({ initial, channels, allSites, isNew }: Props) { <button type="button" onClick={() => removeGroup(idx)} - className="px-2 py-0.5 rounded border border-red-300 dark:border-red-800 text-xs text-red-700 dark:text-red-300" + className="px-2 py-0.5 rounded border border-destructive/30 text-xs text-destructive" > Remove </button> @@ -357,7 +357,7 @@ export function SiteForm({ initial, channels, allSites, isNew }: Props) { setDefaultGroupId(v.trim()); }} placeholder="kebab-case" - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm font-mono" + className="rounded border border-border bg-card px-2 py-1 text-sm font-mono" /> </label> <label className="flex flex-col gap-1 text-sm"> @@ -373,7 +373,7 @@ export function SiteForm({ initial, channels, allSites, isNew }: Props) { ) } placeholder="Display name (optional)" - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm" + className="rounded border border-border bg-card px-2 py-1 text-sm" /> </label> <label className="flex flex-col gap-1 text-sm"> @@ -390,7 +390,7 @@ export function SiteForm({ initial, channels, allSites, isNew }: Props) { ) } rows={2} - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm" + className="rounded border border-border bg-card px-2 py-1 text-sm" /> </label> <label className="flex items-center gap-2 text-sm"> @@ -406,7 +406,7 @@ export function SiteForm({ initial, channels, allSites, isNew }: Props) { ), ) } - className="accent-blue-600" + className="accent-brand" /> Selected by default </label> @@ -417,14 +417,14 @@ export function SiteForm({ initial, channels, allSites, isNew }: Props) { <button type="button" onClick={addGroup} - className="px-3 py-1.5 rounded border border-zinc-300 dark:border-zinc-700 text-sm hover:bg-zinc-50 dark:hover:bg-zinc-800" + className="px-3 py-1.5 rounded border border-border text-sm hover:bg-muted" > + Add group </button> </div> </fieldset> - <fieldset className="flex flex-col gap-3 border border-zinc-200 dark:border-zinc-800 rounded p-3"> + <fieldset className="flex flex-col gap-3 border border-border rounded p-3"> <legend className="px-1 text-sm font-medium">Social links</legend> <input type="hidden" @@ -436,12 +436,12 @@ export function SiteForm({ initial, channels, allSites, isNew }: Props) { type="checkbox" checked={inheritSocial} onChange={(e) => setInheritSocial(e.target.checked)} - className="accent-blue-600" + className="accent-brand" /> Use global default social links </label> {inheritSocial ? ( - <p className="text-xs text-zinc-500"> + <p className="text-xs text-muted-foreground"> This site's footer uses the social links set in{" "} <span className="font-medium">Settings</span>. Uncheck to give this site its own list (an empty list shows no links). @@ -455,16 +455,16 @@ export function SiteForm({ initial, channels, allSites, isNew }: Props) { )} </fieldset> - <fieldset className="flex flex-col gap-3 border border-zinc-200 dark:border-zinc-800 rounded p-3"> + <fieldset className="flex flex-col gap-3 border border-border rounded p-3"> <legend className="px-1 text-sm font-medium">Related sites</legend> - <p className="text-xs text-zinc-500"> + <p className="text-xs text-muted-foreground"> The footer links to every other site that has a Public URL. Add groups here to pull closely-related sites to the front under a heading; every site you don't feature falls into a trailing “Other sites” group automatically. </p> {allSites.length === 0 ? ( - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> No other sites to link to yet. </p> ) : ( @@ -472,7 +472,7 @@ export function SiteForm({ initial, channels, allSites, isNew }: Props) { {related.map((g, idx) => ( <div key={idx} - className="flex flex-col gap-2 border border-zinc-200 dark:border-zinc-800 rounded p-3 bg-zinc-50/50 dark:bg-zinc-900/40" + className="flex flex-col gap-2 border border-border rounded p-3 bg-card" > <div className="flex items-center justify-between gap-2"> <label className="flex flex-col gap-1 text-sm flex-1 min-w-0"> @@ -482,7 +482,7 @@ export function SiteForm({ initial, channels, allSites, isNew }: Props) { value={g.label} onChange={(e) => setRelatedLabel(idx, e.target.value)} placeholder="Heading (optional)" - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm" + className="rounded border border-border bg-card px-2 py-1 text-sm" /> </label> <div className="flex items-center gap-1 self-end"> @@ -490,7 +490,7 @@ export function SiteForm({ initial, channels, allSites, isNew }: Props) { type="button" onClick={() => moveRelatedGroup(idx, -1)} disabled={idx === 0} - className="px-2 py-0.5 rounded border border-zinc-300 dark:border-zinc-700 text-xs disabled:opacity-40" + className="px-2 py-0.5 rounded border border-border text-xs disabled:opacity-40" aria-label="Move related group up" > ↑ @@ -499,7 +499,7 @@ export function SiteForm({ initial, channels, allSites, isNew }: Props) { type="button" onClick={() => moveRelatedGroup(idx, 1)} disabled={idx === related.length - 1} - className="px-2 py-0.5 rounded border border-zinc-300 dark:border-zinc-700 text-xs disabled:opacity-40" + className="px-2 py-0.5 rounded border border-border text-xs disabled:opacity-40" aria-label="Move related group down" > ↓ @@ -507,7 +507,7 @@ export function SiteForm({ initial, channels, allSites, isNew }: Props) { <button type="button" onClick={() => removeRelatedGroup(idx)} - className="px-2 py-0.5 rounded border border-red-300 dark:border-red-800 text-xs text-red-700 dark:text-red-300" + className="px-2 py-0.5 rounded border border-destructive/30 text-xs text-destructive" > Remove </button> @@ -528,7 +528,7 @@ export function SiteForm({ initial, channels, allSites, isNew }: Props) { /> <span className="truncate"> {s.siteTitle || s.siteId}{" "} - <code className="text-xs text-zinc-500">{s.siteId}</code> + <code className="text-xs text-muted-foreground">{s.siteId}</code> </span> </label> ))} @@ -539,7 +539,7 @@ export function SiteForm({ initial, channels, allSites, isNew }: Props) { <button type="button" onClick={addRelatedGroup} - className="px-3 py-1.5 rounded border border-zinc-300 dark:border-zinc-700 text-sm hover:bg-zinc-50 dark:hover:bg-zinc-800" + className="px-3 py-1.5 rounded border border-border text-sm hover:bg-muted" > + Add related group </button> @@ -547,7 +547,7 @@ export function SiteForm({ initial, channels, allSites, isNew }: Props) { {related.some((g) => g.siteIds.some((id) => !siteTitleById.has(id)), ) && ( - <p className="text-xs text-amber-600 dark:text-amber-400"> + <p className="text-xs text-warning"> Some featured sites no longer exist and will be dropped on save. </p> )} @@ -558,17 +558,17 @@ export function SiteForm({ initial, channels, allSites, isNew }: Props) { <div className="flex items-center gap-3"> <button type="submit" - className="px-3 py-2 rounded-md bg-zinc-900 dark:bg-zinc-100 text-zinc-100 dark:text-zinc-900 text-sm font-medium hover:opacity-90" + className="px-3 py-2 rounded-md bg-primary text-primary-foreground text-sm font-medium hover:opacity-90" > {isNew ? "Create site" : "Save site"} </button> {state?.ok === true && ( - <span role="status" className="text-sm text-green-700 dark:text-green-300"> + <span role="status" className="text-sm text-success"> Saved. </span> )} {state && state.ok === false && ( - <span role="alert" className="text-sm text-red-700 dark:text-red-300"> + <span role="alert" className="text-sm text-destructive"> {state.error} </span> )} @@ -598,9 +598,9 @@ function Field({ name={name} defaultValue={defaultValue} required={required} - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm" + className="rounded border border-border bg-card px-2 py-1 text-sm" /> - {hint && <span className="text-xs text-zinc-500">{hint}</span>} + {hint && <span className="text-xs text-muted-foreground">{hint}</span>} </label> ); } diff --git a/editor/app/sites/components/SiteListActions.tsx b/editor/app/sites/components/SiteListActions.tsx @@ -21,11 +21,11 @@ export function DeleteSiteButton({ siteId }: { siteId: string }) { else router.refresh(); }); }} - className="px-2 py-1 rounded border border-red-300 dark:border-red-800 text-xs text-red-700 dark:text-red-300 disabled:opacity-50" + className="px-2 py-1 rounded border border-destructive/30 text-xs text-destructive disabled:opacity-50" > {pending ? "Deleting…" : "Delete"} </button> - {error && <span className="text-xs text-red-600">{error}</span>} + {error && <span className="text-xs text-destructive">{error}</span>} </span> ); } @@ -46,11 +46,11 @@ export function MigrateButton() { else router.refresh(); }) } - className="px-3 py-2 rounded-md bg-blue-600 text-white text-sm font-medium hover:opacity-90 disabled:opacity-50" + className="px-3 py-2 rounded-md bg-primary text-primary-foreground text-sm font-medium hover:opacity-90 disabled:opacity-50" > {pending ? "Migrating…" : "Migrate existing settings into a site"} </button> - {error && <span className="text-xs text-red-600">{error}</span>} + {error && <span className="text-xs text-destructive">{error}</span>} </span> ); } diff --git a/editor/app/sites/new/page.tsx b/editor/app/sites/new/page.tsx @@ -39,8 +39,8 @@ export default async function NewSitePage() { }; return ( <div className="flex flex-col gap-4"> - <div className="flex items-center gap-2 text-sm text-zinc-500"> - <Link href="/sites" className="underline hover:text-zinc-900 dark:hover:text-zinc-100"> + <div className="flex items-center gap-2 text-sm text-muted-foreground"> + <Link href="/sites" className="underline hover:text-foreground"> Sites </Link> <span>/</span> diff --git a/editor/app/sites/page.tsx b/editor/app/sites/page.tsx @@ -15,18 +15,18 @@ export default function SitesPage() { <h1 className="text-2xl font-semibold">Sites</h1> <Link href="/sites/new" - className="px-3 py-2 rounded-md bg-zinc-900 dark:bg-zinc-100 text-zinc-100 dark:text-zinc-900 text-sm font-medium hover:opacity-90" + className="px-3 py-2 rounded-md bg-primary text-primary-foreground text-sm font-medium hover:opacity-90" > + New site </Link> </div> - <p className="text-sm text-zinc-500 max-w-2xl"> + <p className="text-sm text-muted-foreground max-w-2xl"> Each site is a selection + branding over the shared channel pool. The same channel can appear on several sites; its downloads are stored once and reused. </p> {sites.length === 0 && ( - <div className="flex flex-col gap-3 rounded border border-zinc-200 dark:border-zinc-800 p-4"> + <div className="flex flex-col gap-3 rounded border border-border p-4"> <p className="text-sm"> No sites configured yet. If you're upgrading an existing single-site install, migrate your current branding, channel groups, @@ -35,7 +35,7 @@ export default function SitesPage() { <MigrateButton /> </div> )} - <ul className="flex flex-col divide-y divide-zinc-200 dark:divide-zinc-800"> + <ul className="flex flex-col divide-y divide-border"> {sites.map((s) => ( <li key={s.siteId} @@ -43,7 +43,7 @@ export default function SitesPage() { > <Link href={`/sites/${s.siteId}`} className="min-w-0"> <div className="font-medium">{s.siteTitle}</div> - <div className="text-xs text-zinc-500"> + <div className="text-xs text-muted-foreground"> <code>{s.siteId}</code> · {s.channels.length} channel {s.channels.length === 1 ? "" : "s"} · {s.groups.length} group {s.groups.length === 1 ? "" : "s"} diff --git a/editor/app/widget/builder/components/WidgetBuilder.tsx b/editor/app/widget/builder/components/WidgetBuilder.tsx @@ -148,7 +148,7 @@ export function WidgetBuilder() { value={config.channel ?? ""} onChange={(e) => patch({ channel: e.target.value || undefined })} placeholder="all channels" - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1" + className="rounded border border-border bg-card px-2 py-1" /> </label> @@ -163,7 +163,7 @@ export function WidgetBuilder() { pollSeconds: Math.max(1, Math.round(Number(e.target.value) || 1)), }) } - className="rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 w-24" + className="rounded border border-border bg-card px-2 py-1 w-24" /> </label> </form> @@ -178,24 +178,24 @@ export function WidgetBuilder() { aria-label="widget URL" value={fullUrl} onFocus={(e) => e.currentTarget.select()} - className="flex-1 min-w-0 rounded border border-zinc-300 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-900 px-2 py-1 font-mono text-xs" + className="flex-1 min-w-0 rounded border border-border bg-muted px-2 py-1 font-mono text-xs" /> <button type="button" onClick={copy} - className="px-3 py-1.5 rounded-md bg-blue-600 text-white text-sm font-medium hover:bg-blue-700 shrink-0" + className="px-3 py-1.5 rounded-md bg-primary text-primary-foreground text-sm font-medium hover:opacity-90 shrink-0" > {copied ? "Copied" : "Copy"} </button> <button type="button" onClick={openPopup} - className="px-3 py-1.5 rounded-md border border-zinc-300 dark:border-zinc-700 text-sm font-medium hover:bg-zinc-100 dark:hover:bg-zinc-800 shrink-0" + className="px-3 py-1.5 rounded-md border border-border text-sm font-medium hover:bg-muted shrink-0" > Open popup </button> </div> - <p className="text-xs text-zinc-500"> + <p className="text-xs text-muted-foreground"> Open this in a small pinned window, or embed it with an{" "} <code className="font-mono"><iframe></code>. It has no sidebar; by default it's read-only, but{" "} @@ -217,8 +217,8 @@ export function WidgetBuilder() { aria-pressed={size.label === p.label} className={`px-2 py-0.5 rounded text-xs border ${ size.label === p.label - ? "bg-zinc-900 text-white border-zinc-900 dark:bg-zinc-100 dark:text-zinc-900 dark:border-zinc-100" - : "border-zinc-300 dark:border-zinc-700 hover:bg-zinc-100 dark:hover:bg-zinc-800" + ? "bg-primary text-primary-foreground border-primary" + : "border-border hover:bg-muted" }`} > {p.label} @@ -226,7 +226,7 @@ export function WidgetBuilder() { ))} </div> </div> - <div className="inline-block rounded-lg border border-zinc-300 dark:border-zinc-700 overflow-hidden bg-white dark:bg-zinc-950 shadow-sm"> + <div className="inline-block rounded-lg border border-border overflow-hidden bg-card shadow-sm"> <iframe title="widget preview" src={relativeUrl} @@ -235,7 +235,7 @@ export function WidgetBuilder() { className="block" /> </div> - <p className="text-xs text-zinc-500"> + <p className="text-xs text-muted-foreground"> Previewing at {size.width}×{size.height}px. </p> </div> diff --git a/editor/app/widget/builder/page.tsx b/editor/app/widget/builder/page.tsx @@ -12,7 +12,7 @@ export default function WidgetBuilderPage() { <div className="flex items-center justify-between"> <h1 className="text-2xl font-semibold">Monitor widget</h1> </div> - <p className="text-sm text-zinc-500 max-w-2xl"> + <p className="text-sm text-muted-foreground max-w-2xl"> Build a read-only monitoring widget that shows worker status and active jobs with no sidebar or controls — sized to sit in a small pinned window or an embedded <code className="font-mono"><iframe></code>. Toggle diff --git a/editor/app/widget/components/MonitorWidget.tsx b/editor/app/widget/components/MonitorWidget.tsx @@ -118,14 +118,14 @@ export function MonitorWidget({ // controls also stay visible when idle (you may want to pause preemptively). if (config.hideIdle && nothingActive && !disk?.low && !config.controls) { return ( - <div className="p-2 text-xs text-zinc-500" aria-label="monitor idle"> + <div className="p-2 text-xs text-muted-foreground" aria-label="monitor idle"> Idle </div> ); } return ( - <div className="flex flex-col gap-3 p-2 text-zinc-900 dark:text-zinc-100"> + <div className="flex flex-col gap-3 p-2 text-foreground"> {config.controls && ( <WidgetControls paused={workersPayload?.paused ?? false} @@ -193,21 +193,21 @@ function WorkersStrip({ <section aria-label="Workers" className="flex flex-col gap-1.5"> {showTitle && ( <div className="flex items-baseline gap-2"> - <h2 className="text-xs font-semibold uppercase tracking-wide text-zinc-500"> + <h2 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground"> Workers </h2> - <span className="text-xs text-zinc-500"> + <span className="text-xs text-muted-foreground"> {busy}/{workers.length} busy </span> {paused && ( - <span className="text-[10px] px-1.5 py-0.5 rounded-full bg-amber-100 dark:bg-amber-950 text-amber-800 dark:text-amber-200 border border-amber-300 dark:border-amber-800"> + <span className="text-[10px] px-1.5 py-0.5 rounded-full bg-warning-soft text-warning border border-warning/30"> paused </span> )} </div> )} {workers.length === 0 ? ( - <p className="text-xs text-zinc-500">No workers configured.</p> + <p className="text-xs text-muted-foreground">No workers configured.</p> ) : ( <ul className="flex flex-wrap gap-1.5"> {workers.map((w) => ( @@ -217,7 +217,7 @@ function WorkersStrip({ aria-label={`worker ${w.name} ${workerStateLabel(w)}`} className={ showLabels - ? "flex items-center gap-1.5 rounded border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 px-1.5 py-0.5 text-xs" + ? "flex items-center gap-1.5 rounded border border-border bg-card px-1.5 py-0.5 text-xs" : "flex items-center" } > @@ -312,16 +312,16 @@ function ActiveJobsStrip({ <section aria-label="Active jobs" className="flex flex-col gap-1.5"> {showTitle && ( <div className="flex items-baseline gap-2"> - <h2 className="text-xs font-semibold uppercase tracking-wide text-zinc-500"> + <h2 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground"> Active </h2> - <span className="text-xs text-zinc-500"> + <span className="text-xs text-muted-foreground"> {running} running, {queued} queued </span> </div> )} {jobs.length === 0 ? ( - <p className="text-xs text-zinc-500">No active jobs.</p> + <p className="text-xs text-muted-foreground">No active jobs.</p> ) : ( <ul className="flex flex-col gap-2"> {jobs.map((j) => ( @@ -356,8 +356,8 @@ function JobRow({ }) { const statusClass = job.status === "running" - ? "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200" - : "bg-zinc-200 text-zinc-700 dark:bg-zinc-700 dark:text-zinc-200"; + ? "bg-info-soft text-info" + : "bg-muted text-muted-foreground"; return ( <div className="flex flex-col gap-1"> <div className="flex flex-wrap items-center gap-1.5 text-xs"> @@ -371,19 +371,19 @@ function JobRow({ </span> {job.background && ( <span - className="uppercase tracking-wide px-1 py-0.5 rounded text-[9px] bg-amber-100 text-amber-800 dark:bg-amber-900/60 dark:text-amber-200" + className="uppercase tracking-wide px-1 py-0.5 rounded text-[9px] bg-warning-soft text-warning" title="Background work — yields to a manual sync/download" > auto </span> )} {job.channelSlug && ( - <span className="font-mono text-zinc-500 truncate max-w-[12rem]"> + <span className="font-mono text-muted-foreground truncate max-w-[12rem]"> {job.channelSlug} </span> )} {headingProgress && job.progress && ( - <span className="font-mono text-zinc-500 shrink-0"> + <span className="font-mono text-muted-foreground shrink-0"> {jobProgressText(job.progress, eta)} </span> )} @@ -446,14 +446,14 @@ function JobProgressBar({ aria-valuemax={progress.target} aria-valuenow={progress.current} aria-label={label} - className="relative h-1.5 w-full overflow-hidden rounded bg-zinc-200 dark:bg-zinc-800" + className="relative h-1.5 w-full overflow-hidden rounded bg-muted" > <div - className="absolute inset-y-0 left-0 bg-emerald-500" + className="absolute inset-y-0 left-0 bg-success" style={{ width: `${progress.pct}%` }} /> </div> - <div className="flex items-baseline justify-between gap-2 text-[11px] text-zinc-500"> + <div className="flex items-baseline justify-between gap-2 text-[11px] text-muted-foreground"> <span>{label}</span> {etaText && <span className="shrink-0">{etaText}</span>} </div> @@ -490,11 +490,11 @@ function TaskBar({ const fillClass = probing ? "bg-violet-400 dark:bg-violet-500" : task.kind === "download" - ? "bg-emerald-300 dark:bg-emerald-700" - : "bg-emerald-500"; + ? "bg-success/60" + : "bg-success"; const pulseClass = probing ? "bg-violet-400 dark:bg-violet-500" - : "bg-amber-400 dark:bg-amber-500"; + : "bg-warning"; const elapsed = now === null ? null @@ -508,19 +508,19 @@ function TaskBar({ .filter(Boolean) .join(" · "); return ( - <div className="flex flex-col gap-0.5 pl-2 border-l-2 border-zinc-200 dark:border-zinc-800"> + <div className="flex flex-col gap-0.5 pl-2 border-l-2 border-border"> <div className="flex items-baseline justify-between gap-2 text-[11px]"> <span className="truncate"> - <span className="text-zinc-500">{verb} </span> + <span className="text-muted-foreground">{verb} </span> <span className="font-mono">{task.label}</span> </span> - <span className="font-mono text-zinc-500 shrink-0">{meta}</span> + <span className="font-mono text-muted-foreground shrink-0">{meta}</span> </div> <div role="progressbar" aria-valuenow={hasFraction ? pct : undefined} aria-label={`${task.label}`} - className="relative h-1 w-full overflow-hidden rounded bg-zinc-200 dark:bg-zinc-800" + className="relative h-1 w-full overflow-hidden rounded bg-muted" > {hasFraction ? ( <div diff --git a/editor/app/workers/components/WorkersView.tsx b/editor/app/workers/components/WorkersView.tsx @@ -122,7 +122,7 @@ export function WorkersView({ initial }: { initial: WorkersPayload }) { onChange={refetch} /> {paused && ( - <span role="status" className="text-sm text-amber-700 dark:text-amber-300"> + <span role="status" className="text-sm text-warning"> Transcriptions paused — batches are waiting; parakeet jobs stop after the current segment and resume on the next run. Resume restores each worker's previous state. @@ -132,7 +132,7 @@ export function WorkersView({ initial }: { initial: WorkersPayload }) { {savedDefault && ( <span role="status" - className="text-sm text-green-700 dark:text-green-300" + className="text-sm text-success" > Saved as default </span> @@ -142,7 +142,7 @@ export function WorkersView({ initial }: { initial: WorkersPayload }) { disabled={pending || workers.length === 0} onClick={saveDefault} title="Save the currently enabled workers as the launch default; on the next server start only these workers start enabled and all others start disabled" - className="px-3 py-1.5 rounded-md border border-zinc-300 dark:border-zinc-700 text-sm hover:bg-zinc-100 dark:hover:bg-zinc-800 disabled:opacity-50" + className="px-3 py-1.5 rounded-md border border-border text-sm hover:bg-muted disabled:opacity-50" > {defaultEnabledIds ? "Update default" : "Set as default"} </button> @@ -150,7 +150,7 @@ export function WorkersView({ initial }: { initial: WorkersPayload }) { </div> {workers.length === 0 ? ( - <p className="text-sm text-zinc-500 border border-dashed border-zinc-300 dark:border-zinc-700 rounded p-4"> + <p className="text-sm text-muted-foreground border border-dashed border-border rounded p-4"> No workers configured. Add one on the{" "} <a href="/settings" className="underline"> Settings @@ -219,7 +219,7 @@ function WorkerCard({ return ( <li aria-label={`worker ${w.name}`} - className="flex flex-col gap-2 rounded border border-zinc-200 dark:border-zinc-800 p-3 bg-white dark:bg-zinc-900" + className="flex flex-col gap-2 rounded border border-border p-3 bg-card" > <div className="flex flex-wrap items-center gap-2"> <span className="font-medium text-sm">{w.name}</span> @@ -228,11 +228,11 @@ function WorkerCard({ > {badge.label} </span> - <span className="text-xs text-zinc-500">{engine}</span> - <span className="text-xs text-zinc-400">priority {w.priority}</span> + <span className="text-xs text-muted-foreground">{engine}</span> + <span className="text-xs text-muted-foreground">priority {w.priority}</span> {inDefault && ( <span - className="text-[11px] px-1.5 py-0.5 rounded-full border bg-blue-100 dark:bg-blue-950 text-blue-800 dark:text-blue-200 border-blue-300 dark:border-blue-800" + className="text-[11px] px-1.5 py-0.5 rounded-full border bg-info-soft text-info border-info/30" title="Starts enabled on the next server launch (part of the saved default)" > default @@ -245,7 +245,7 @@ function WorkerCard({ disabled={pending} onClick={() => run(() => enableWorkerAction(w.id))} aria-label={`enable ${w.name}`} - className="px-2 py-1 rounded border border-green-300 dark:border-green-800 text-xs text-green-700 dark:text-green-300 hover:bg-green-50 dark:hover:bg-green-950 disabled:opacity-50" + className="px-2 py-1 rounded border border-success/30 text-xs text-success hover:bg-success-soft disabled:opacity-50" > Enable </button> @@ -257,7 +257,7 @@ function WorkerCard({ onClick={() => run(() => stopWorkerPartialAction(w.id))} aria-label={`stop ${w.name} keep progress`} title="Finish the current window, then stop and take the worker out of rotation (it ends disabled); completed windows are cached and resume on the next run" - className="px-2 py-1 rounded border border-amber-300 dark:border-amber-800 text-xs text-amber-700 dark:text-amber-300 hover:bg-amber-50 dark:hover:bg-amber-950 disabled:opacity-50" + className="px-2 py-1 rounded border border-warning/30 text-xs text-warning hover:bg-warning-soft disabled:opacity-50" > Stop & keep progress </button> @@ -270,7 +270,7 @@ function WorkerCard({ onClick={() => run(() => drainWorkerAction(w.id))} aria-label={`drain ${w.name}`} title="Stop taking new work; let in-flight transcriptions finish" - className="px-2 py-1 rounded border border-amber-300 dark:border-amber-800 text-xs text-amber-700 dark:text-amber-300 hover:bg-amber-50 dark:hover:bg-amber-950 disabled:opacity-50" + className="px-2 py-1 rounded border border-warning/30 text-xs text-warning hover:bg-warning-soft disabled:opacity-50" > Drain </button> @@ -280,7 +280,7 @@ function WorkerCard({ onClick={() => run(() => disableWorkerAction(w.id))} aria-label={`disable ${w.name}`} title="Stop immediately (in-flight transcriptions keep running)" - className="px-2 py-1 rounded border border-zinc-300 dark:border-zinc-700 text-xs hover:bg-zinc-100 dark:hover:bg-zinc-800 disabled:opacity-50" + className="px-2 py-1 rounded border border-border text-xs hover:bg-muted disabled:opacity-50" > Disable </button> @@ -292,7 +292,7 @@ function WorkerCard({ disabled={pending} onClick={() => run(() => disableWorkerAction(w.id))} aria-label={`force disable ${w.name}`} - className="px-2 py-1 rounded border border-zinc-300 dark:border-zinc-700 text-xs hover:bg-zinc-100 dark:hover:bg-zinc-800 disabled:opacity-50" + className="px-2 py-1 rounded border border-border text-xs hover:bg-muted disabled:opacity-50" > Disable now </button> @@ -300,7 +300,7 @@ function WorkerCard({ </span> </div> {w.degraded && ( - <p className="text-xs text-red-700 dark:text-red-300"> + <p className="text-xs text-destructive"> Auto-disabled — unreachable or repeated failures. Enable to retry. </p> )} @@ -333,11 +333,11 @@ function TaskRow({ task }: { task: WorkerTask }) { <div className="flex flex-col gap-0.5"> <div className="flex items-baseline justify-between gap-2 text-xs"> <span className="truncate"> - <span className="text-zinc-500">Transcribing </span> + <span className="text-muted-foreground">Transcribing </span> {task.channelSlug ? ( <Link href={`/channels/${task.channelSlug}/videos/${encodeURIComponent(task.label)}`} - className="font-mono underline hover:text-zinc-900 dark:hover:text-zinc-100" + className="font-mono underline hover:text-foreground" > {task.label} </Link> @@ -345,29 +345,29 @@ function TaskRow({ task }: { task: WorkerTask }) { <span className="font-mono">{task.label}</span> )} {task.channelSlug && ( - <span className="text-zinc-400"> + <span className="text-muted-foreground"> {" "} in <span className="font-mono">{task.channelSlug}</span> </span> )} </span> - <span className="font-mono text-zinc-500 shrink-0">{meta}</span> + <span className="font-mono text-muted-foreground shrink-0">{meta}</span> </div> <span role="progressbar" aria-label={`Transcribing ${task.label}`} aria-valuenow={hasFraction ? pct : undefined} - className="relative block h-1.5 w-full overflow-hidden rounded bg-zinc-200 dark:bg-zinc-800" + className="relative block h-1.5 w-full overflow-hidden rounded bg-muted" > {hasFraction ? ( <span - className="absolute inset-y-0 left-0 bg-blue-500" + className="absolute inset-y-0 left-0 bg-info" style={{ width: `${pct}%` }} /> ) : ( // Indeterminate (no parseable progress yet): a full-width amber pulse // reads as "working, no number" rather than a stalled ~33%. - <span className="absolute inset-0 animate-pulse bg-amber-400 dark:bg-amber-500" /> + <span className="absolute inset-0 animate-pulse bg-warning" /> )} </span> </div> diff --git a/editor/app/workers/page.tsx b/editor/app/workers/page.tsx @@ -17,7 +17,7 @@ export default async function WorkersPage() { Configure workers </Link> </div> - <p className="text-sm text-zinc-500"> + <p className="text-sm text-muted-foreground"> Transcription workers — one slot each. Enable, disable, or drain a worker to free up a CPU/GPU for other programs, then turn it back on when done. Disabling every worker pauses running batches (they wait for a worker) diff --git a/export/CHANGELOG.md b/export/CHANGELOG.md @@ -4,7 +4,7 @@ - **Fixed: some Odysee/Rumble/Twitch videos opened in a YouTube player.** A video you'd opened before multi-platform support was added could get stuck playing in the YouTube embed even after its data was corrected, because the browser's local transcript cache (IndexedDB) held an old record with no platform and was never invalidated. The cache now self-heals — stale records are discarded on load — so every video plays in its correct platform's player. As a safeguard, a video whose platform somehow can't be determined now shows an "Open on source site" link instead of silently loading the wrong player. - **Instant offline word-search, powered by a background FlexSearch index.** On the Offline page, each channel now has a **Build index** action: it constructs a FlexSearch full-text index for that channel in a **Web Worker** — entirely off the main thread, so indexing a large channel never freezes the page (a live progress % shows the work). Once built, a search box does **instant word/prefix search** across your indexed channels and returns the matching transcript lines (with channel + timestamp) — and because the index is built from the same shards the offline download caches, it works with no connection. The worker bundles into the static site (no server needed). This is a first, self-contained surface; the main search box will gain the same acceleration next. - **Install the archive as an app, and search it offline.** Each public site is now a PWA: it ships a web app manifest (named + themed with the site's accent color) and an icon set, so your browser can install it to your home screen or desktop and open it in its own window. A service worker caches the app so it loads instantly on repeat visits and still opens without a connection. A new **Offline** page (linked in the footer) lists the site's channels with a **Download** button each — pick the channels you want and their transcripts are saved to your device so you can **search them with no connection**; a live Online/Offline indicator and per-channel "Available offline" state show what's ready, and you can remove a channel to free space. Downloads are opt-in (a channel's transcripts can be large) and refresh automatically when the site is rebuilt. Everything still works online-only in browsers without service-worker support. -- **Two more themes + a theme picker.** A palette menu next to the light/dark toggle lets you switch the whole site's look between **Archive** (the warm reading room), **Terminal** (an ice-cyan signal terminal), **Swiss** (red/black/white editorial), and **Base** (neutral) — each in light or dark, and your choice is remembered. Switching is instant and purely cosmetic (just colors; the layout never changes). +- **Two more themes + a theme picker, and the themes now feel genuinely distinct.** A palette menu next to the light/dark toggle lets you switch the whole site's look between **Archive** (the warm reading room), **Selenized** (the Solarized successor — teal-slate on warm tan, with the calibrated Selenized accents), **Swiss** (red/black/white editorial), and **Base** (neutral) — each in light or dark, and your choice is remembered. Beyond color, each theme now carries its own **corner shape and typeface**: Swiss goes hard-cornered in a neo-grotesque, Selenized adopts a code voice (JetBrains Mono headings + IBM Plex Sans), and Archive keeps its soft-cornered serif reading room. Switching is instant and never changes the layout. (If you'd picked the old "Terminal" theme, it becomes "Selenized" automatically.) - **Optional per-site brand accent.** A site can set its own accent color (a hex, in the editor's site config) to replace the family brass everywhere on its public build — the wordmark mark, links, badges, and highlights. It's baked onto the page at build time so it's correct on the very first paint, and applies in both light and dark. Sites that don't set one keep the family brass. - **New look: the "Archive / Reading Room" identity.** The public site adopts the family design system — a serif display face (Source Serif 4) for headings/counts, clean sans for body, and mono for the query/power layer, over a warm paper (light) / ink (dark) palette with a brass accent. A light/dark/system theme toggle in the header replaces OS-only dark mode and persists your choice. Header, footer, and the whole search UI (query builder, filters, scopes, charts) are rebuilt on a shared component kit for a calmer, more consistent feel — the simple one-box search is unchanged; the power tools just read better. - **"New since your last visit."** A dismissible banner on the home page tells you how many transcripts were added since you were last here, and first-time visitors get a one-line nudge toward the power tools (layered queries, scopes, filters, charts). Entirely client-side — nothing about your visit leaves your browser. diff --git a/export/app/duplicates/DuplicatesClient.tsx b/export/app/duplicates/DuplicatesClient.tsx @@ -221,13 +221,13 @@ function DuplicatesView({ ); if (loading) { - return <p className="text-sm text-zinc-500">Loading duplicates…</p>; + return <p className="text-sm text-muted-foreground">Loading duplicates…</p>; } return ( <section aria-label="duplicate-shorts" className="flex flex-col gap-3"> {report && ( - <p className="text-xs text-zinc-500"> + <p className="text-xs text-muted-foreground"> {report.runConfig.thresholdSeconds === null ? "all durations" : `shorts ≤${report.runConfig.thresholdSeconds}s`} @@ -262,11 +262,11 @@ function DuplicatesView({ )} {clusters.length === 0 ? ( - <p className="text-sm text-zinc-500 border border-dashed border-zinc-300 dark:border-zinc-700 rounded p-4"> + <p className="text-sm text-muted-foreground border border-dashed border-border rounded p-4"> No duplicate shorts have been detected for this site yet. </p> ) : visible.length === 0 ? ( - <p className="text-sm text-zinc-500 border border-dashed border-zinc-300 dark:border-zinc-700 rounded p-4"> + <p className="text-sm text-muted-foreground border border-dashed border-border rounded p-4"> No duplicates match the current filters. </p> ) : ( @@ -320,19 +320,19 @@ function FilterPanel({ }) { const platformLabel = (n: number) => `platform${n === 1 ? "" : "s"}`; return ( - <details open className="text-sm text-zinc-600 dark:text-zinc-400"> + <details open className="text-sm text-muted-foreground"> <summary className="cursor-pointer select-none flex flex-wrap items-baseline gap-x-3 gap-y-1"> - <span className="text-xs uppercase tracking-wide text-zinc-500"> + <span className="text-xs uppercase tracking-wide text-muted-foreground"> Filters </span> - <span className="text-xs text-zinc-500"> + <span className="text-xs text-muted-foreground"> {platforms.size} of {presentPlatforms.length}{" "} {platformLabel(presentPlatforms.length)} </span> </summary> <div className="mt-2 flex flex-wrap items-center gap-x-4 gap-y-2"> <label className="flex items-center gap-1.5 select-none"> - <span className="text-xs uppercase tracking-wide text-zinc-500"> + <span className="text-xs uppercase tracking-wide text-muted-foreground"> Search </span> <input @@ -341,7 +341,7 @@ function FilterPanel({ placeholder="title or channel…" value={query} onChange={(e) => onQuery(e.target.value)} - className="rounded-md border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm" + className="rounded-md border border-border bg-card px-2 py-1 text-sm" /> </label> <FilterRow label="Platforms"> @@ -404,7 +404,7 @@ function FilterRow({ }) { return ( <div className="flex flex-wrap items-center gap-x-3 gap-y-1"> - <span className="text-xs uppercase tracking-wide text-zinc-500"> + <span className="text-xs uppercase tracking-wide text-muted-foreground"> {label} </span> {children} @@ -427,7 +427,7 @@ function Check({ type="checkbox" checked={checked} onChange={onChange} - className="accent-blue-600" + className="accent-brand" /> {children} </label> @@ -448,20 +448,20 @@ function DuplicateClusterCard({ return ( <li aria-label={`duplicate cluster ${cluster.clusterId}`} - className="border border-zinc-200 dark:border-zinc-800 rounded-lg overflow-hidden bg-white dark:bg-zinc-900" + className="border border-border rounded-lg overflow-hidden bg-card" > - <div className="flex items-center gap-2 flex-wrap px-4 py-1.5 bg-zinc-50 dark:bg-zinc-900/60 border-b border-zinc-200 dark:border-zinc-800 text-xs"> + <div className="flex items-center gap-2 flex-wrap px-4 py-1.5 bg-muted border-b border-border text-xs"> <Badge>{MATCH_LABEL[cluster.matchKind]}</Badge> {cluster.score !== null && <Badge>score {cluster.score.toFixed(2)}</Badge>} {cluster.contained && <Badge>clip-of-longer</Badge>} {cluster.crossPlatform && <Badge>cross-platform</Badge>} {cluster.crossChannel && <Badge>cross-channel</Badge>} {hasSameChannelDup(cluster) && <Badge>same-channel</Badge>} - <span className="ml-auto text-zinc-500 shrink-0"> + <span className="ml-auto text-muted-foreground shrink-0"> {cluster.videoRefs.length} videos · ~{cluster.durationBucket}s </span> </div> - <ul className="flex flex-col divide-y divide-zinc-200 dark:divide-zinc-800"> + <ul className="flex flex-col divide-y divide-border"> {cluster.videoRefs.map((ref) => ( <MemberRow key={ref.slug} member={ref} detail={details.get(ref.slug)} /> ))} @@ -483,7 +483,7 @@ function MemberRow({ const { openTranscript } = usePlayer(); const title = detail?.title || member.title || member.slug; return ( - <li className="flex flex-col gap-0.5 px-4 py-2 hover:bg-zinc-100 dark:hover:bg-zinc-800"> + <li className="flex flex-col gap-0.5 px-4 py-2 hover:bg-muted"> <div className="flex items-baseline gap-2"> <button type="button" @@ -492,7 +492,7 @@ function MemberRow({ > {title} </button> - <span className="text-xs text-zinc-500 shrink-0"> + <span className="text-xs text-muted-foreground shrink-0"> {member.channel || member.channelSlug} · {member.platform} ·{" "} {formatDate(member.uploadDate)} </span> @@ -502,7 +502,7 @@ function MemberRow({ href={detail.webpageUrl} target="_blank" rel="noopener noreferrer" - className="text-xs text-blue-600 dark:text-blue-400 hover:underline truncate font-mono" + className="text-xs text-info hover:underline truncate font-mono" aria-label={`Open "${title}" on ${member.platform}`} > {detail.webpageUrl} @@ -514,7 +514,7 @@ function MemberRow({ function Badge({ children }: { children: React.ReactNode }) { return ( - <span className="inline-flex items-center rounded-full bg-zinc-100 dark:bg-zinc-800 px-2 py-0.5 text-zinc-700 dark:text-zinc-300"> + <span className="inline-flex items-center rounded-full bg-muted px-2 py-0.5 text-muted-foreground"> {children} </span> ); diff --git a/export/app/duplicates/page.tsx b/export/app/duplicates/page.tsx @@ -11,7 +11,7 @@ export default function DuplicatesPage() { <div className="flex flex-col gap-4"> <div> <h1 className="text-2xl font-semibold">Duplicate shorts</h1> - <p className="mt-1 text-sm text-zinc-500"> + <p className="mt-1 text-sm text-muted-foreground"> Shorts whose transcripts match across channels and platforms — re-uploads, mirrors, and cross-posts of the same clip. Click any member to play it. diff --git a/export/e2e/theme-family.spec.ts b/export/e2e/theme-family.spec.ts @@ -25,10 +25,10 @@ test("theme-family picker switches + persists data-theme with no FOUC", async ({ page.getByRole("button", { name: "Choose theme" }), ).toBeVisible(); - await pickFamily(page, "Terminal"); + await pickFamily(page, "Selenized"); let s = await familyState(page); - expect(s.theme).toBe("terminal"); - expect(s.persisted).toBe("terminal"); + expect(s.theme).toBe("selenized"); + expect(s.persisted).toBe("selenized"); // Reload: the pre-paint script re-applies data-theme before React hydrates. await page.reload({ waitUntil: "commit" }); @@ -36,7 +36,7 @@ test("theme-family picker switches + persists data-theme with no FOUC", async ({ await page.evaluate(() => document.documentElement.getAttribute("data-theme"), ), - ).toBe("terminal"); + ).toBe("selenized"); await pickFamily(page, "Swiss"); s = await familyState(page); diff --git a/homepage/app/components/RecentAdditions.tsx b/homepage/app/components/RecentAdditions.tsx @@ -15,14 +15,14 @@ export function RecentAdditions({ items }: { items: HomepageRecentItem[] }) { if (items.length === 0) return null; return ( <section className="flex flex-col gap-3"> - <h2 className="text-lg font-semibold text-zinc-900 dark:text-zinc-100"> + <h2 className="text-lg font-semibold text-foreground"> Recent additions </h2> - <ul className="flex flex-col divide-y divide-zinc-200 dark:divide-zinc-800 rounded-xl border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900"> + <ul className="flex flex-col divide-y divide-border rounded-xl border border-border bg-card"> {items.map((item) => { const href = transcriptHref(item); const title = ( - <span className="font-medium text-zinc-900 dark:text-zinc-100 group-hover:text-blue-600 dark:group-hover:text-blue-400"> + <span className="font-medium text-foreground group-hover:text-info"> {item.title} </span> ); @@ -38,12 +38,12 @@ export function RecentAdditions({ items }: { items: HomepageRecentItem[] }) { > <div className="min-w-0"> {title} - <p className="truncate text-xs text-zinc-500"> + <p className="truncate text-xs text-muted-foreground"> {item.channel} </p> </div> - <div className="flex shrink-0 items-center gap-2 text-xs text-zinc-500"> - <span className="rounded-full bg-zinc-100 dark:bg-zinc-800 px-2 py-0.5 font-medium text-zinc-600 dark:text-zinc-300"> + <div className="flex shrink-0 items-center gap-2 text-xs text-muted-foreground"> + <span className="rounded-full bg-muted px-2 py-0.5 font-medium text-muted-foreground"> {item.siteTitle} </span> <time dateTime={item.transcribedDate}>