feat(theme): dark/light mode with a Radix Colors-based palette
Adds a working Dark/Light toggle (next to the language switcher, same row
as EN/VI) and re-themes the whole dashboard, not just the handful of
components that already used semantic tokens.
- Tailwind darkMode:"class" + CSS-variable color tokens (client/src/index.css,
tailwind.config.js): surface.0-5, border/border-light, accent/accent-hover,
fg.primary/secondary/muted, status.success/danger/warning. One class flip
on <html> re-themes everything — no per-element dark: variant pairs.
- useTheme() hook: localStorage-persisted, defaults to dark, no
prefers-color-scheme fallback (client/src/hooks/useTheme.ts).
- Mechanical, table-driven migration (scripts/migrate-color-tokens.mjs,
scripts/tokenize-status-colors.mjs, scripts/darken-status-colors.mjs) of
every raw neutral/gray/slate + emerald/red/amber Tailwind utility across
client/src onto the new tokens, so every badge/button/component pulls the
same shade per status/role instead of each picking its own.
- Palette values are the literal Radix Colors (radix-ui.com/colors) scale
constants — slate/blue/green/red/amber steps 1-12 — adopted after three
rounds of hand-picked values that kept overshooting (flat, then too dark,
then glaring); see docs/superpowers/specs/2026-07-31-color-redesign-
dark-light-mode-design.md for the full history and role mapping.
- PipelineMap: done/current/failed/passed-no-evidence/detected share one
visual language (border + text + translucent wash of the same status
color); `current` alone stays a solid accent fill, the one state that
gets to look bolder ("you are here").
- LaneCard: removed the stage/kind/auto-stage chips that duplicated the
Workspace lane-detail header already showing them.
Categorical/decorative hues (violet, indigo, cyan, teal, sky, rose, pink,
orange, yellow, and blue where it plays a role-coloring part e.g. message
bubbles) are deliberately out of scope — collapsing those onto shared
tokens would erase the distinction between different kinds of thing, not
a status.
This commit is contained in:
@@ -285,12 +285,12 @@ export function ImportHistory() {
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h3 className="text-sm font-medium text-gray-300 flex items-center gap-2 mb-1">
|
||||
<History className="w-4 h-4 text-gray-500" />
|
||||
<h3 className="text-sm font-medium text-fg-secondary flex items-center gap-2 mb-1">
|
||||
<History className="w-4 h-4 text-fg-muted" />
|
||||
{t("import.title")}
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 mb-1">{t("import.description")}</p>
|
||||
<p className="text-[11px] text-gray-600 italic mb-4 leading-snug">{t("cursorPathsNote")}</p>
|
||||
<p className="text-xs text-fg-muted mb-1">{t("import.description")}</p>
|
||||
<p className="text-[11px] text-fg-muted italic mb-4 leading-snug">{t("cursorPathsNote")}</p>
|
||||
|
||||
<div className="card p-5 space-y-5">
|
||||
{/* Step-by-step instructions */}
|
||||
@@ -299,33 +299,33 @@ export function ImportHistory() {
|
||||
onClick={() => setInstructionsOpen((v) => !v)}
|
||||
className="w-full flex items-center justify-between px-4 py-3 bg-surface-2 hover:bg-surface-3 transition-colors"
|
||||
>
|
||||
<span className="flex items-center gap-2 text-xs font-semibold text-gray-300 uppercase tracking-wider">
|
||||
<ListChecks className="w-3.5 h-3.5 text-blue-400" />
|
||||
<span className="flex items-center gap-2 text-xs font-semibold text-fg-secondary uppercase tracking-wider">
|
||||
<ListChecks className="w-3.5 h-3.5 text-blue-500" />
|
||||
{t("import.instructions")}
|
||||
</span>
|
||||
<span className="text-[11px] text-gray-500">{instructionsOpen ? "▾" : "▸"}</span>
|
||||
<span className="text-[11px] text-fg-muted">{instructionsOpen ? "▾" : "▸"}</span>
|
||||
</button>
|
||||
{instructionsOpen && (
|
||||
<div className="px-4 py-4 space-y-4 text-sm text-gray-300 bg-surface-1">
|
||||
<div className="px-4 py-4 space-y-4 text-sm text-fg-secondary bg-surface-1">
|
||||
{/* Default location card */}
|
||||
{guide && (
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs bg-surface-2 border border-border rounded-md px-3 py-2">
|
||||
<HardDrive className="w-3.5 h-3.5 text-gray-500 flex-shrink-0" />
|
||||
<span className="text-gray-400">{t("import.defaultLocation")}:</span>
|
||||
<code className="font-mono text-gray-200 truncate">
|
||||
<HardDrive className="w-3.5 h-3.5 text-fg-muted flex-shrink-0" />
|
||||
<span className="text-fg-secondary">{t("import.defaultLocation")}:</span>
|
||||
<code className="font-mono text-fg-secondary truncate">
|
||||
{guide.default_projects_dir_display}
|
||||
</code>
|
||||
{guide.default_projects_dir_exists ? (
|
||||
<span className="inline-flex items-center gap-1 text-emerald-400 bg-emerald-500/10 border border-emerald-500/20 px-2 py-0.5 rounded-full">
|
||||
<span className="inline-flex items-center gap-1 text-status-success bg-status-success/10 border border-status-success/20 px-2 py-0.5 rounded-full">
|
||||
<CheckCircle2 className="w-3 h-3" />
|
||||
{t("import.locationFound")}
|
||||
<span className="text-gray-500 ml-1">
|
||||
<span className="text-fg-muted ml-1">
|
||||
· {guide.default_projects_dir_stats.projects} {t("import.projectsLabel")},{" "}
|
||||
{guide.default_projects_dir_stats.jsonl_files} {t("import.jsonlLabel")}
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1 text-amber-400 bg-amber-500/10 border border-amber-500/20 px-2 py-0.5 rounded-full">
|
||||
<span className="inline-flex items-center gap-1 text-status-warning bg-status-warning/10 border border-status-warning/20 px-2 py-0.5 rounded-full">
|
||||
<AlertTriangle className="w-3 h-3" />
|
||||
{t("import.locationMissing")}
|
||||
</span>
|
||||
@@ -339,13 +339,13 @@ export function ImportHistory() {
|
||||
<Step title={t("import.stepArchive")} body={t("import.stepArchiveBody")}>
|
||||
{guide && (
|
||||
<div className="mt-2 flex items-center gap-2 bg-surface-2 border border-border rounded-md px-3 py-2">
|
||||
<Terminal className="w-3.5 h-3.5 text-gray-500 flex-shrink-0" />
|
||||
<code className="flex-1 text-xs font-mono text-gray-200 truncate">
|
||||
<Terminal className="w-3.5 h-3.5 text-fg-muted flex-shrink-0" />
|
||||
<code className="flex-1 text-xs font-mono text-fg-secondary truncate">
|
||||
{guide.archive_command}
|
||||
</code>
|
||||
<button
|
||||
onClick={copyArchiveCmd}
|
||||
className="text-xs text-gray-400 hover:text-gray-200 flex items-center gap-1 flex-shrink-0"
|
||||
className="text-xs text-fg-secondary hover:text-fg-primary flex items-center gap-1 flex-shrink-0"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
@@ -364,7 +364,7 @@ export function ImportHistory() {
|
||||
<Step title={t("import.stepVerify")} body={t("import.stepVerifyBody")} />
|
||||
</div>
|
||||
|
||||
<div className="text-[11px] text-gray-500 flex items-start gap-2 pt-2 border-t border-border">
|
||||
<div className="text-[11px] text-fg-muted flex items-start gap-2 pt-2 border-t border-border">
|
||||
<Info className="w-3 h-3 mt-0.5 flex-shrink-0" />
|
||||
<span>{t("import.accuracyNote")}</span>
|
||||
</div>
|
||||
@@ -409,8 +409,8 @@ export function ImportHistory() {
|
||||
{mode === "rescan" && (
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<FolderOpen className="w-4 h-4 text-gray-500 flex-shrink-0" />
|
||||
<code className="font-mono text-xs text-gray-300 truncate">
|
||||
<FolderOpen className="w-4 h-4 text-fg-muted flex-shrink-0" />
|
||||
<code className="font-mono text-xs text-fg-secondary truncate">
|
||||
{guide?.default_projects_dir_display || "~/.claude/projects"}
|
||||
</code>
|
||||
</div>
|
||||
@@ -440,7 +440,7 @@ export function ImportHistory() {
|
||||
className="input w-full text-sm font-mono"
|
||||
spellCheck={false}
|
||||
/>
|
||||
<p className="text-[11px] text-gray-500 mt-1.5">{t("import.folderHelper")}</p>
|
||||
<p className="text-[11px] text-fg-muted mt-1.5">{t("import.folderHelper")}</p>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
@@ -475,13 +475,13 @@ export function ImportHistory() {
|
||||
}}
|
||||
className={`border-2 border-dashed rounded-lg px-4 py-8 text-center cursor-pointer transition-colors ${
|
||||
dragging
|
||||
? "border-blue-400 bg-blue-500/5"
|
||||
: "border-border hover:border-gray-500 bg-surface-1"
|
||||
? "border-blue-500 bg-blue-600/5"
|
||||
: "border-border hover:border-border-light bg-surface-1"
|
||||
}`}
|
||||
>
|
||||
<UploadCloud className="w-6 h-6 text-gray-500 mx-auto mb-2" />
|
||||
<p className="text-sm text-gray-300">{t("import.dropzoneHint")}</p>
|
||||
<p className="text-[11px] text-gray-500 mt-1">{t("import.dropzoneSub")}</p>
|
||||
<UploadCloud className="w-6 h-6 text-fg-muted mx-auto mb-2" />
|
||||
<p className="text-sm text-fg-secondary">{t("import.dropzoneHint")}</p>
|
||||
<p className="text-[11px] text-fg-muted mt-1">{t("import.dropzoneSub")}</p>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
@@ -493,17 +493,17 @@ export function ImportHistory() {
|
||||
</div>
|
||||
{files.length > 0 && (
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 text-xs bg-surface-3 rounded-md px-3 py-2">
|
||||
<span className="text-gray-400">
|
||||
<FileArchive className="w-3.5 h-3.5 inline mr-1.5 text-gray-500" />
|
||||
<span className="text-fg-secondary">
|
||||
<FileArchive className="w-3.5 h-3.5 inline mr-1.5 text-fg-muted" />
|
||||
{t("import.filesSelected", { count: files.length })}
|
||||
<span className="text-gray-600 ml-2">({formatBytes(totalSize)})</span>
|
||||
<span className="text-fg-muted ml-2">({formatBytes(totalSize)})</span>
|
||||
</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
setFiles([]);
|
||||
if (fileInputRef.current) fileInputRef.current.value = "";
|
||||
}}
|
||||
className="text-gray-500 hover:text-gray-300 text-[11px]"
|
||||
className="text-fg-muted hover:text-fg-secondary text-[11px]"
|
||||
>
|
||||
{t("import.clearSelection")}
|
||||
</button>
|
||||
@@ -543,13 +543,13 @@ export function ImportHistory() {
|
||||
}}
|
||||
className={`border-2 border-dashed rounded-lg px-4 py-8 text-center cursor-pointer transition-colors ${
|
||||
dragging
|
||||
? "border-blue-400 bg-blue-500/5"
|
||||
: "border-border hover:border-gray-500 bg-surface-1"
|
||||
? "border-blue-500 bg-blue-600/5"
|
||||
: "border-border hover:border-border-light bg-surface-1"
|
||||
}`}
|
||||
>
|
||||
<DatabaseBackup className="w-6 h-6 text-gray-500 mx-auto mb-2" />
|
||||
<p className="text-sm text-gray-300">{t("import.backupHint")}</p>
|
||||
<p className="text-[11px] text-gray-500 mt-1">{t("import.backupSub")}</p>
|
||||
<DatabaseBackup className="w-6 h-6 text-fg-muted mx-auto mb-2" />
|
||||
<p className="text-sm text-fg-secondary">{t("import.backupHint")}</p>
|
||||
<p className="text-[11px] text-fg-muted mt-1">{t("import.backupSub")}</p>
|
||||
<input
|
||||
ref={backupInputRef}
|
||||
type="file"
|
||||
@@ -560,17 +560,17 @@ export function ImportHistory() {
|
||||
</div>
|
||||
{backupFile && (
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 text-xs bg-surface-3 rounded-md px-3 py-2">
|
||||
<span className="text-gray-400 min-w-0">
|
||||
<FileArchive className="w-3.5 h-3.5 inline mr-1.5 text-gray-500" />
|
||||
<span className="text-fg-secondary min-w-0">
|
||||
<FileArchive className="w-3.5 h-3.5 inline mr-1.5 text-fg-muted" />
|
||||
<span className="font-mono truncate">{backupFile.name}</span>
|
||||
<span className="text-gray-600 ml-2">({formatBytes(backupFile.size)})</span>
|
||||
<span className="text-fg-muted ml-2">({formatBytes(backupFile.size)})</span>
|
||||
</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
setBackupFile(null);
|
||||
if (backupInputRef.current) backupInputRef.current.value = "";
|
||||
}}
|
||||
className="text-gray-500 hover:text-gray-300 text-[11px]"
|
||||
className="text-fg-muted hover:text-fg-secondary text-[11px]"
|
||||
>
|
||||
{t("import.clearSelection")}
|
||||
</button>
|
||||
@@ -596,11 +596,11 @@ export function ImportHistory() {
|
||||
|
||||
{/* In-flight progress */}
|
||||
{running && progressText && (
|
||||
<div className="flex items-center gap-2 text-xs text-gray-400 bg-surface-2 border border-border rounded-md px-3 py-2">
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin text-blue-400 flex-shrink-0" />
|
||||
<div className="flex items-center gap-2 text-xs text-fg-secondary bg-surface-2 border border-border rounded-md px-3 py-2">
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin text-blue-500 flex-shrink-0" />
|
||||
<span className="truncate">{progressText}</span>
|
||||
{progress?.current && (
|
||||
<code className="font-mono text-[11px] text-gray-600 truncate">
|
||||
<code className="font-mono text-[11px] text-fg-muted truncate">
|
||||
· {progress.current.split("/").slice(-2).join("/")}
|
||||
</code>
|
||||
)}
|
||||
@@ -609,7 +609,7 @@ export function ImportHistory() {
|
||||
|
||||
{/* Errors */}
|
||||
{errorMsg && (
|
||||
<div className="flex items-start gap-2 text-xs text-red-400 bg-red-500/10 border border-red-500/20 rounded-md px-3 py-2">
|
||||
<div className="flex items-start gap-2 text-xs text-status-danger bg-status-danger/10 border border-status-danger/20 rounded-md px-3 py-2">
|
||||
<XCircle className="w-3.5 h-3.5 mt-0.5 flex-shrink-0" />
|
||||
<span>{errorMsg}</span>
|
||||
</div>
|
||||
@@ -617,8 +617,8 @@ export function ImportHistory() {
|
||||
|
||||
{/* Result summary */}
|
||||
{result && !running && (
|
||||
<div className="border border-emerald-500/20 bg-emerald-500/5 rounded-lg px-4 py-3 space-y-2">
|
||||
<div className="flex items-center gap-2 text-xs font-semibold text-emerald-400 uppercase tracking-wider">
|
||||
<div className="border border-status-success/20 bg-status-success/5 rounded-lg px-4 py-3 space-y-2">
|
||||
<div className="flex items-center gap-2 text-xs font-semibold text-status-success uppercase tracking-wider">
|
||||
<CheckCircle2 className="w-3.5 h-3.5" />
|
||||
{t("import.result.title")}
|
||||
</div>
|
||||
@@ -626,26 +626,26 @@ export function ImportHistory() {
|
||||
<ResultStat
|
||||
label={t("import.result.imported", { count: result.imported })}
|
||||
value={result.imported}
|
||||
color="text-emerald-300"
|
||||
color="text-status-success"
|
||||
/>
|
||||
<ResultStat
|
||||
label={t("import.result.backfilled", { count: result.backfilled ?? 0 })}
|
||||
value={result.backfilled ?? 0}
|
||||
color="text-blue-300"
|
||||
color="text-blue-400"
|
||||
/>
|
||||
<ResultStat
|
||||
label={t("import.result.skipped", { count: result.skipped })}
|
||||
value={result.skipped}
|
||||
color="text-gray-400"
|
||||
color="text-fg-secondary"
|
||||
/>
|
||||
<ResultStat
|
||||
label={t("import.result.errors", { count: result.errors })}
|
||||
value={result.errors}
|
||||
color={result.errors > 0 ? "text-red-300" : "text-gray-500"}
|
||||
color={result.errors > 0 ? "text-status-danger" : "text-fg-muted"}
|
||||
/>
|
||||
</div>
|
||||
{typeof result.files_scanned === "number" && (
|
||||
<p className="text-[11px] text-gray-500">
|
||||
<p className="text-[11px] text-fg-muted">
|
||||
{t("import.result.filesScanned", { count: result.files_scanned })}
|
||||
{result.path ? ` · ${result.path}` : ""}
|
||||
</p>
|
||||
@@ -655,8 +655,8 @@ export function ImportHistory() {
|
||||
|
||||
{/* Restore-from-backup result summary */}
|
||||
{backupResult && !running && (
|
||||
<div className="border border-emerald-500/20 bg-emerald-500/5 rounded-lg px-4 py-3 space-y-2">
|
||||
<div className="flex items-center gap-2 text-xs font-semibold text-emerald-400 uppercase tracking-wider">
|
||||
<div className="border border-status-success/20 bg-status-success/5 rounded-lg px-4 py-3 space-y-2">
|
||||
<div className="flex items-center gap-2 text-xs font-semibold text-status-success uppercase tracking-wider">
|
||||
<CheckCircle2 className="w-3.5 h-3.5" />
|
||||
{t("import.backupResult.title")}
|
||||
</div>
|
||||
@@ -666,14 +666,14 @@ export function ImportHistory() {
|
||||
count: backupResult.sessions_imported,
|
||||
})}
|
||||
value={backupResult.sessions_imported}
|
||||
color="text-emerald-300"
|
||||
color="text-status-success"
|
||||
/>
|
||||
<ResultStat
|
||||
label={t("import.backupResult.sessionsSkipped", {
|
||||
count: backupResult.sessions_skipped,
|
||||
})}
|
||||
value={backupResult.sessions_skipped}
|
||||
color="text-gray-400"
|
||||
color="text-fg-secondary"
|
||||
/>
|
||||
<ResultStat
|
||||
label={t("import.backupResult.events", { count: backupResult.events })}
|
||||
@@ -686,7 +686,7 @@ export function ImportHistory() {
|
||||
color="text-cyan-300"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-[11px] text-gray-500">
|
||||
<p className="text-[11px] text-fg-muted">
|
||||
{t("import.backupResult.detail", {
|
||||
agents: backupResult.agents,
|
||||
workflows: backupResult.workflows,
|
||||
@@ -712,8 +712,8 @@ function Step({
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-200">{title}</p>
|
||||
<p className="text-xs text-gray-400 mt-1 whitespace-pre-line">{body}</p>
|
||||
<p className="text-sm font-medium text-fg-secondary">{title}</p>
|
||||
<p className="text-xs text-fg-secondary mt-1 whitespace-pre-line">{body}</p>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
@@ -737,19 +737,19 @@ function ModeButton({
|
||||
onClick={onClick}
|
||||
className={`text-left p-3 rounded-lg border transition-colors ${
|
||||
active
|
||||
? "border-blue-500/40 bg-blue-500/10"
|
||||
? "border-blue-600/40 bg-blue-600/10"
|
||||
: "border-border bg-surface-2 hover:bg-surface-3"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`flex items-center gap-1.5 text-xs font-medium mb-1 ${
|
||||
active ? "text-blue-300" : "text-gray-300"
|
||||
active ? "text-blue-400" : "text-fg-secondary"
|
||||
}`}
|
||||
>
|
||||
{icon}
|
||||
{title}
|
||||
</div>
|
||||
<p className="text-[11px] text-gray-500 leading-snug">{desc}</p>
|
||||
<p className="text-[11px] text-fg-muted leading-snug">{desc}</p>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -758,7 +758,7 @@ function ResultStat({ label, value, color }: { label: string; value: number; col
|
||||
return (
|
||||
<div className="bg-surface-2 rounded-md px-2.5 py-2">
|
||||
<p className={`text-sm font-semibold ${color}`}>{value.toLocaleString()}</p>
|
||||
<p className="text-[10px] text-gray-500 uppercase tracking-wider mt-0.5">{label}</p>
|
||||
<p className="text-[10px] text-fg-muted uppercase tracking-wider mt-0.5">{label}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user