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:
2026-07-31 10:54:31 +07:00
parent 4905d63b97
commit b673363351
82 changed files with 3776 additions and 2578 deletions
+167 -157
View File
@@ -311,8 +311,10 @@ function Toggle({
return (
<label className="flex items-center justify-between gap-3 cursor-pointer group">
<div className="min-w-0">
<p className="text-sm text-gray-300 group-hover:text-gray-200 transition-colors">{label}</p>
{description && <p className="text-xs text-gray-500 mt-0.5">{description}</p>}
<p className="text-sm text-fg-secondary group-hover:text-fg-secondary transition-colors">
{label}
</p>
{description && <p className="text-xs text-fg-muted mt-0.5">{description}</p>}
</div>
<button
type="button"
@@ -320,7 +322,7 @@ function Toggle({
aria-checked={checked}
onClick={() => onChange(!checked)}
className={`relative inline-flex h-5 w-9 flex-shrink-0 rounded-full border-2 border-transparent transition-colors duration-200 ${
checked ? "bg-blue-500" : "bg-surface-4"
checked ? "bg-blue-600" : "bg-surface-4"
}`}
>
<span
@@ -394,7 +396,7 @@ function PricingInfoTooltip() {
onMouseLeave={() => setOpen(false)}
onFocus={() => setOpen(true)}
onBlur={() => setOpen(false)}
className="inline-flex items-center justify-center rounded-full p-0.5 text-gray-500 hover:text-gray-300 focus:outline-none focus:ring-1 focus:ring-accent/40"
className="inline-flex items-center justify-center rounded-full p-0.5 text-fg-muted hover:text-fg-secondary focus:outline-none focus:ring-1 focus:ring-accent/40"
>
<Info className="w-3.5 h-3.5" />
</button>
@@ -402,32 +404,36 @@ function PricingInfoTooltip() {
<div
ref={popoverRef}
role="tooltip"
className="fixed z-50 p-3 bg-[#12121f] border border-[#2a2a4a] rounded-lg shadow-2xl text-[11px] text-gray-300 pointer-events-none"
className="fixed z-50 p-3 bg-[#12121f] border border-[#2a2a4a] rounded-lg shadow-2xl text-[11px] text-fg-secondary pointer-events-none"
style={{ left: pos.left, top: pos.top, width: 320 }}
>
<p className="text-xs font-semibold text-gray-100 mb-2">{t("pricing.tooltip.title")}</p>
<p className="text-xs font-semibold text-fg-primary mb-2">{t("pricing.tooltip.title")}</p>
<p className="font-semibold text-gray-200 uppercase tracking-wider text-[9px] mb-1">
<p className="font-semibold text-fg-secondary uppercase tracking-wider text-[9px] mb-1">
{t("pricing.tooltip.howItWorks")}
</p>
<p className="text-gray-400 leading-snug mb-2.5">{t("pricing.tooltip.howItWorksBody")}</p>
<p className="text-fg-secondary leading-snug mb-2.5">
{t("pricing.tooltip.howItWorksBody")}
</p>
<p className="font-semibold text-gray-200 uppercase tracking-wider text-[9px] mb-1">
<p className="font-semibold text-fg-secondary uppercase tracking-wider text-[9px] mb-1">
{t("pricing.tooltip.patternsTitle")}
</p>
<p className="text-gray-400 leading-snug mb-2.5">{t("pricing.tooltip.patternsBody")}</p>
<p className="text-fg-secondary leading-snug mb-2.5">
{t("pricing.tooltip.patternsBody")}
</p>
<p className="font-semibold text-amber-300 uppercase tracking-wider text-[9px] mb-1">
<p className="font-semibold text-status-warning uppercase tracking-wider text-[9px] mb-1">
{t("pricing.tooltip.manualUpdates")}
</p>
<p className="text-gray-400 leading-snug mb-2.5">
<p className="text-fg-secondary leading-snug mb-2.5">
{t("pricing.tooltip.manualUpdatesBody")}
</p>
<p className="font-semibold text-amber-300 uppercase tracking-wider text-[9px] mb-1">
<p className="font-semibold text-status-warning uppercase tracking-wider text-[9px] mb-1">
{t("pricing.tooltip.apiPricing")}
</p>
<p className="text-gray-400 leading-snug">{t("pricing.tooltip.apiPricingBody")}</p>
<p className="text-fg-secondary leading-snug">{t("pricing.tooltip.apiPricingBody")}</p>
</div>
)}
</>
@@ -866,14 +872,14 @@ export function Settings() {
<button
onClick={saveEdit}
disabled={saving}
className="p-1.5 rounded-md text-emerald-400 hover:bg-emerald-500/10 transition-colors disabled:opacity-50"
className="p-1.5 rounded-md text-status-success hover:bg-status-success/10 transition-colors disabled:opacity-50"
title={t("common:save")}
>
<Check className="w-4 h-4" />
</button>
<button
onClick={cancelEdit}
className="p-1.5 rounded-md text-gray-400 hover:bg-surface-4 transition-colors"
className="p-1.5 rounded-md text-fg-secondary hover:bg-surface-4 transition-colors"
title={t("common:cancel")}
>
<X className="w-4 h-4" />
@@ -909,7 +915,7 @@ export function Settings() {
<span className="text-[11px] font-semibold text-violet-300 uppercase tracking-wider">
{t("pricing.introRatesTitle")}
</span>
<span className="text-[11px] text-gray-500">{t("pricing.introRatesHint")}</span>
<span className="text-[11px] text-fg-muted">{t("pricing.introRatesHint")}</span>
</div>
<div className="flex flex-wrap items-end gap-3">
{introField("intro_until", "pricing.introUntil", { date: true })}
@@ -931,8 +937,8 @@ export function Settings() {
<div
className={`px-3 py-2 rounded-lg text-xs ${
match.isError
? "bg-red-500/10 border border-red-500/20 text-red-400"
: "bg-emerald-500/10 border border-emerald-500/20 text-emerald-400"
? "bg-status-danger/10 border border-status-danger/20 text-status-danger"
: "bg-status-success/10 border border-status-success/20 text-status-success"
}`}
>
{match.message}
@@ -981,27 +987,27 @@ export function Settings() {
</div>
<div>
<div className="flex items-center gap-2">
<h1 className="text-lg font-semibold text-gray-100">{t("title")}</h1>
<h1 className="text-lg font-semibold text-fg-primary">{t("title")}</h1>
{wsConnected ? (
<span className="flex items-center gap-1.5 text-[11px] text-emerald-400 bg-emerald-500/10 border border-emerald-500/20 px-2 py-0.5 rounded-full">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse-dot" />
<span className="flex items-center gap-1.5 text-[11px] text-status-success bg-status-success/10 border border-status-success/20 px-2 py-0.5 rounded-full">
<span className="w-1.5 h-1.5 rounded-full bg-status-success animate-pulse-dot" />
{t("common:live")}
</span>
) : (
<span className="flex items-center gap-1.5 text-[11px] text-gray-400 bg-gray-500/10 border border-gray-500/20 px-2 py-0.5 rounded-full">
<span className="w-1.5 h-1.5 rounded-full bg-gray-400" />
<span className="flex items-center gap-1.5 text-[11px] text-fg-secondary bg-surface-4/10 border border-border-light/20 px-2 py-0.5 rounded-full">
<span className="w-1.5 h-1.5 rounded-full bg-surface-4" />
{t("common:offline")}
</span>
)}
</div>
<p className="text-xs text-gray-500">{t("subtitle")}</p>
<p className="text-xs text-fg-muted">{t("subtitle")}</p>
</div>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<a
href={api.settings.exportData()}
download
className="inline-flex items-center gap-1.5 text-xs px-3 py-1.5 rounded-md border border-border text-gray-400 hover:text-gray-200 hover:border-gray-500 transition-colors"
className="inline-flex items-center gap-1.5 text-xs px-3 py-1.5 rounded-md border border-border text-fg-secondary hover:text-fg-primary hover:border-border-light transition-colors"
>
<FileDown className="w-3.5 h-3.5" />
{t("exportData")}
@@ -1015,7 +1021,7 @@ export function Settings() {
{/* In-page section navigation - Settings is dense, so this TOC jumps to
and scroll-spies each section. */}
<nav className="sticky top-0 z-20 -mx-1 !mt-2 px-1 py-2 bg-surface-0/85 backdrop-blur border-b border-border/60 flex items-center gap-1.5">
<span className="flex-shrink-0 pl-1 text-[10px] font-semibold uppercase tracking-wider text-gray-500">
<span className="flex-shrink-0 pl-1 text-[10px] font-semibold uppercase tracking-wider text-fg-muted">
{t("jumpTo", "Jump to")}
</span>
{tocOverflow.left && (
@@ -1023,7 +1029,7 @@ export function Settings() {
type="button"
onClick={() => scrollTocBy(-180)}
aria-label="Scroll left"
className="flex-shrink-0 flex items-center justify-center w-6 h-7 rounded-md border border-border text-gray-400 hover:text-gray-200 hover:bg-surface-3 transition-colors"
className="flex-shrink-0 flex items-center justify-center w-6 h-7 rounded-md border border-border text-fg-secondary hover:text-fg-primary hover:bg-surface-3 transition-colors"
>
<ChevronLeft className="w-3.5 h-3.5" />
</button>
@@ -1043,7 +1049,7 @@ export function Settings() {
className={`inline-flex items-center gap-1.5 text-xs whitespace-nowrap px-2.5 py-1.5 rounded-lg border transition-colors flex-shrink-0 ${
active
? "bg-accent/15 border-accent/30 text-accent"
: "border-border text-gray-400 hover:text-gray-200 hover:bg-surface-3"
: "border-border text-fg-secondary hover:text-fg-primary hover:bg-surface-3"
}`}
>
<Icon className="w-3.5 h-3.5" />
@@ -1057,7 +1063,7 @@ export function Settings() {
type="button"
onClick={() => scrollTocBy(180)}
aria-label="Scroll right"
className="flex-shrink-0 flex items-center justify-center w-6 h-7 rounded-md border border-border text-gray-400 hover:text-gray-200 hover:bg-surface-3 transition-colors"
className="flex-shrink-0 flex items-center justify-center w-6 h-7 rounded-md border border-border text-fg-secondary hover:text-fg-primary hover:bg-surface-3 transition-colors"
>
<ChevronRight className="w-3.5 h-3.5" />
</button>
@@ -1068,12 +1074,12 @@ export function Settings() {
<div className="card p-6">
<div className="flex items-center justify-between flex-wrap gap-4">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-xl bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center">
<DollarSign className="w-6 h-6 text-emerald-400" />
<div className="w-12 h-12 rounded-xl bg-status-success/10 border border-status-success/20 flex items-center justify-center">
<DollarSign className="w-6 h-6 text-status-success" />
</div>
<div>
<p className="text-sm text-gray-500">{t("common:cost.totalEstimatedCost")}</p>
<p className="text-2xl font-semibold text-gray-100">
<p className="text-sm text-fg-muted">{t("common:cost.totalEstimatedCost")}</p>
<p className="text-2xl font-semibold text-fg-primary">
<Tip
raw={
totalCost !== null
@@ -1086,7 +1092,7 @@ export function Settings() {
</p>
</div>
</div>
<div className="text-right text-xs text-gray-500">
<div className="text-right text-xs text-fg-muted">
<p>{t("acrossSessions")}</p>
<p>{t("basedOnUsage")}</p>
</div>
@@ -1097,12 +1103,12 @@ export function Settings() {
<section id="pricing" className="scroll-mt-24">
<div className="flex flex-wrap items-center justify-between gap-3 mb-4">
<div>
<h3 className="text-sm font-medium text-gray-300 flex items-center gap-2">
<DollarSign className="w-4 h-4 text-gray-500" />
<h3 className="text-sm font-medium text-fg-secondary flex items-center gap-2">
<DollarSign className="w-4 h-4 text-fg-muted" />
{t("pricing.title")}
<PricingInfoTooltip />
</h3>
<p className="text-xs text-gray-500 mt-0.5">{t("pricing.description")}</p>
<p className="text-xs text-fg-muted mt-0.5">{t("pricing.description")}</p>
</div>
<div className="flex items-center gap-2">
<button
@@ -1114,8 +1120,8 @@ export function Settings() {
disabled={isEditing || actionLoading !== null}
className={`text-xs px-3 py-1.5 rounded-md transition-colors disabled:opacity-50 inline-flex items-center gap-1.5 ${
confirmAction === "reset-pricing"
? "bg-amber-500/20 text-amber-400 border border-amber-500/30"
: "text-gray-400 hover:text-gray-300 hover:bg-surface-4"
? "bg-status-warning/20 text-status-warning border border-status-warning/30"
: "text-fg-secondary hover:text-fg-primary hover:bg-surface-4"
}`}
>
<RotateCcw className="w-3 h-3" />
@@ -1134,7 +1140,7 @@ export function Settings() {
</div>
{error && (
<div className="mb-4 px-4 py-2.5 bg-red-500/10 border border-red-500/20 rounded-lg text-sm text-red-400">
<div className="mb-4 px-4 py-2.5 bg-status-danger/10 border border-status-danger/20 rounded-lg text-sm text-status-danger">
{error}
</div>
)}
@@ -1145,34 +1151,34 @@ export function Settings() {
<table className="w-full min-w-[1000px]">
<thead>
<tr className="border-b border-border text-left">
<th className="px-4 py-3 text-[11px] font-semibold text-gray-500 uppercase tracking-wider">
<th className="px-4 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider">
{t("pricing.pattern")}
</th>
<th className="px-4 py-3 text-[11px] font-semibold text-gray-500 uppercase tracking-wider">
<th className="px-4 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider">
{t("common:cost.model")}
</th>
<th className="px-4 py-3 text-[11px] font-semibold text-gray-500 uppercase tracking-wider text-right">
<th className="px-4 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider text-right">
{t("common:token.input")}
</th>
<th className="px-4 py-3 text-[11px] font-semibold text-gray-500 uppercase tracking-wider text-right">
<th className="px-4 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider text-right">
{t("common:token.output")}
</th>
<th className="px-4 py-3 text-[11px] font-semibold text-gray-500 uppercase tracking-wider text-right">
<th className="px-4 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider text-right">
{t("common:token.cacheRead")}
</th>
<th className="px-4 py-3 text-[11px] font-semibold text-gray-500 uppercase tracking-wider text-right">
<th className="px-4 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider text-right">
{t("pricing.cacheWrite5m")}
</th>
<th className="px-4 py-3 text-[11px] font-semibold text-gray-500 uppercase tracking-wider text-right">
<th className="px-4 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider text-right">
{t("pricing.cacheWrite1h")}
</th>
<th className="px-4 py-3 text-[11px] font-semibold text-gray-500 uppercase tracking-wider text-right">
<th className="px-4 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider text-right">
{t("pricing.fastInput")}
</th>
<th className="px-4 py-3 text-[11px] font-semibold text-gray-500 uppercase tracking-wider text-right">
<th className="px-4 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider text-right">
{t("pricing.fastOutput")}
</th>
<th className="w-24 px-4 py-3 text-[11px] font-semibold text-gray-500 uppercase tracking-wider">
<th className="w-24 px-4 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider">
{t("common:actions")}
</th>
</tr>
@@ -1189,10 +1195,10 @@ export function Settings() {
key={rule.model_pattern}
className="hover:bg-surface-4 transition-colors group"
>
<td className="px-4 py-3 text-sm font-mono text-gray-300">
<td className="px-4 py-3 text-sm font-mono text-fg-secondary">
{rule.model_pattern}
</td>
<td className="px-4 py-3 text-sm text-gray-300">
<td className="px-4 py-3 text-sm text-fg-secondary">
{rule.display_name}
{rule.intro_until && (
<div className="text-[11px] font-normal text-violet-400/80 mt-0.5">
@@ -1204,7 +1210,7 @@ export function Settings() {
</div>
)}
</td>
<td className="px-4 py-3 text-sm text-gray-400 text-right font-mono">
<td className="px-4 py-3 text-sm text-fg-secondary text-right font-mono">
${rule.input_per_mtok}
{rule.intro_until && (
<span className="block text-[11px] text-violet-400/80">
@@ -1212,7 +1218,7 @@ export function Settings() {
</span>
)}
</td>
<td className="px-4 py-3 text-sm text-gray-400 text-right font-mono">
<td className="px-4 py-3 text-sm text-fg-secondary text-right font-mono">
${rule.output_per_mtok}
{rule.intro_until && (
<span className="block text-[11px] text-violet-400/80">
@@ -1220,7 +1226,7 @@ export function Settings() {
</span>
)}
</td>
<td className="px-4 py-3 text-sm text-gray-400 text-right font-mono">
<td className="px-4 py-3 text-sm text-fg-secondary text-right font-mono">
${rule.cache_read_per_mtok}
{rule.intro_until && (
<span className="block text-[11px] text-violet-400/80">
@@ -1228,7 +1234,7 @@ export function Settings() {
</span>
)}
</td>
<td className="px-4 py-3 text-sm text-gray-400 text-right font-mono">
<td className="px-4 py-3 text-sm text-fg-secondary text-right font-mono">
${rule.cache_write_per_mtok}
{rule.intro_until && (
<span className="block text-[11px] text-violet-400/80">
@@ -1236,7 +1242,7 @@ export function Settings() {
</span>
)}
</td>
<td className="px-4 py-3 text-sm text-gray-400 text-right font-mono">
<td className="px-4 py-3 text-sm text-fg-secondary text-right font-mono">
${rule.cache_write_1h_per_mtok}
{rule.intro_until && (
<span className="block text-[11px] text-violet-400/80">
@@ -1244,10 +1250,10 @@ export function Settings() {
</span>
)}
</td>
<td className="px-4 py-3 text-sm text-gray-400 text-right font-mono">
<td className="px-4 py-3 text-sm text-fg-secondary text-right font-mono">
{rule.fast_input_per_mtok ? `$${rule.fast_input_per_mtok}` : "-"}
</td>
<td className="px-4 py-3 text-sm text-gray-400 text-right font-mono">
<td className="px-4 py-3 text-sm text-fg-secondary text-right font-mono">
{rule.fast_output_per_mtok ? `$${rule.fast_output_per_mtok}` : "-"}
</td>
<td className="px-4 py-3">
@@ -1255,7 +1261,7 @@ export function Settings() {
<button
onClick={() => startEdit(rule)}
disabled={isEditing}
className="p-1.5 rounded-md text-gray-400 hover:text-blue-400 hover:bg-blue-500/10 transition-colors disabled:opacity-30"
className="p-1.5 rounded-md text-fg-secondary hover:text-blue-500 hover:bg-blue-600/10 transition-colors disabled:opacity-30"
title={t("common:edit")}
>
<Pencil className="w-3.5 h-3.5" />
@@ -1263,7 +1269,7 @@ export function Settings() {
<button
onClick={() => deleteRule(rule.model_pattern)}
disabled={isEditing}
className="p-1.5 rounded-md text-gray-400 hover:text-red-400 hover:bg-red-500/10 transition-colors disabled:opacity-30"
className="p-1.5 rounded-md text-fg-secondary hover:text-status-danger hover:bg-status-danger/10 transition-colors disabled:opacity-30"
title={t("common:delete")}
>
<Trash2 className="w-3.5 h-3.5" />
@@ -1284,7 +1290,7 @@ export function Settings() {
</div>
{lastUpdated && (
<p className="text-xs text-gray-600 mt-3">
<p className="text-xs text-fg-muted mt-3">
{t("pricing.lastUpdated")}
{formatTimestamp(lastUpdated)}
</p>
@@ -1293,21 +1299,21 @@ export function Settings() {
{/* ─── HOOK CONFIGURATION ─── */}
<section id="hooks" className="scroll-mt-24">
<h3 className="text-sm font-medium text-gray-300 flex items-center gap-2 mb-1">
<Plug className="w-4 h-4 text-gray-500" />
<h3 className="text-sm font-medium text-fg-secondary flex items-center gap-2 mb-1">
<Plug className="w-4 h-4 text-fg-muted" />
{t("hooks.title")}
</h3>
<p className="text-xs text-gray-500 mb-4">{t("hooks.description")}</p>
<p className="text-xs text-fg-muted mb-4">{t("hooks.description")}</p>
<div className="card p-5 space-y-4">
<div className="flex items-center justify-between flex-wrap gap-3">
<div className="flex items-center gap-3">
{sysInfo?.hooks.installed ? (
<span className="inline-flex items-center gap-1.5 text-xs font-medium text-emerald-400 bg-emerald-500/10 border border-emerald-500/20 px-2.5 py-1 rounded-full">
<span className="inline-flex items-center gap-1.5 text-xs font-medium text-status-success bg-status-success/10 border border-status-success/20 px-2.5 py-1 rounded-full">
<CheckCircle className="w-3.5 h-3.5" /> {t("hooks.allInstalled")}
</span>
) : (
<span className="inline-flex items-center gap-1.5 text-xs font-medium text-amber-400 bg-amber-500/10 border border-amber-500/20 px-2.5 py-1 rounded-full">
<span className="inline-flex items-center gap-1.5 text-xs font-medium text-status-warning bg-status-warning/10 border border-status-warning/20 px-2.5 py-1 rounded-full">
<AlertTriangle className="w-3.5 h-3.5" /> {t("hooks.incomplete")}
</span>
)}
@@ -1337,15 +1343,15 @@ export function Settings() {
className="flex items-center gap-1.5 text-xs px-2.5 py-1.5 rounded-md bg-surface-2"
>
{active ? (
<CheckCircle className="w-3 h-3 text-emerald-400 flex-shrink-0" />
<CheckCircle className="w-3 h-3 text-status-success flex-shrink-0" />
) : (
<XCircle className="w-3 h-3 text-red-400 flex-shrink-0" />
<XCircle className="w-3 h-3 text-status-danger flex-shrink-0" />
)}
<span className="text-gray-400 truncate">{hook}</span>
<span className="text-fg-secondary truncate">{hook}</span>
</div>
))}
</div>
<p className="text-[11px] text-gray-600 font-mono truncate">{sysInfo.hooks.path}</p>
<p className="text-[11px] text-fg-muted font-mono truncate">{sysInfo.hooks.path}</p>
</>
)}
</div>
@@ -1353,12 +1359,12 @@ export function Settings() {
{/* ─── CLAUDE HOME ─── */}
<section id="claude-home" className="scroll-mt-24">
<h3 className="text-sm font-medium text-gray-300 flex items-center gap-2 mb-1">
<FolderOpen className="w-4 h-4 text-gray-500" />
<h3 className="text-sm font-medium text-fg-secondary flex items-center gap-2 mb-1">
<FolderOpen className="w-4 h-4 text-fg-muted" />
{t("claudeHome.title")}
</h3>
<p className="text-xs text-gray-500 mb-1">{t("claudeHome.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("claudeHome.description")}</p>
<p className="text-[11px] text-fg-muted italic mb-4 leading-snug">{t("cursorPathsNote")}</p>
<div className="card p-5 space-y-4">
<div className="flex items-center gap-3">
@@ -1369,7 +1375,7 @@ export function Settings() {
setClaudeHomeInput(e.target.value);
setClaudeHomeError(null);
}}
className="flex-1 bg-surface-4 border border-surface-3 rounded-lg px-3 py-2 text-sm text-gray-200 font-mono focus:outline-none focus:border-violet-500/50"
className="flex-1 bg-surface-4 border border-surface-3 rounded-lg px-3 py-2 text-sm text-fg-secondary font-mono focus:outline-none focus:border-violet-500/50"
placeholder={t("claudeHome.placeholder")}
/>
<button
@@ -1380,10 +1386,10 @@ export function Settings() {
{claudeHomeSaving ? t("claudeHome.saving") : t("claudeHome.save")}
</button>
</div>
{claudeHomeError && <p className="text-xs text-red-400">{claudeHomeError}</p>}
{claudeHomeError && <p className="text-xs text-status-danger">{claudeHomeError}</p>}
{claudeHome && (
<p className="text-xs text-gray-500">
{t("claudeHome.current")} <code className="text-gray-400">{claudeHome}</code>
<p className="text-xs text-fg-muted">
{t("claudeHome.current")} <code className="text-fg-secondary">{claudeHome}</code>
</p>
)}
</div>
@@ -1401,13 +1407,13 @@ export function Settings() {
{/* ─── TABBY COMPANION ─── */}
<section id="tabby" className="scroll-mt-24">
<h3 className="text-sm font-medium text-gray-300 flex items-center gap-2 mb-1">
<h3 className="text-sm font-medium text-fg-secondary flex items-center gap-2 mb-1">
<span className="text-base leading-none" aria-hidden>
🐾
</span>
{t("tabby.title", "Tabby companion")}
</h3>
<p className="text-xs text-gray-500 mb-4">
<p className="text-xs text-fg-muted mb-4">
{t("tabby.description", "A floating cat that reacts to your live sessions.")}
</p>
@@ -1416,7 +1422,7 @@ export function Settings() {
<div
className={`w-10 h-10 rounded-xl flex items-center justify-center transition-colors ${
tabbyEnabled
? "bg-blue-500/10 border border-blue-500/20"
? "bg-blue-600/10 border border-blue-600/20"
: "bg-surface-2 border border-border"
}`}
>
@@ -1439,11 +1445,11 @@ export function Settings() {
{/* ─── NOTIFICATIONS ─── */}
<section id="notifications" className="scroll-mt-24">
<h3 className="text-sm font-medium text-gray-300 flex items-center gap-2 mb-1">
<Bell className="w-4 h-4 text-gray-500" />
<h3 className="text-sm font-medium text-fg-secondary flex items-center gap-2 mb-1">
<Bell className="w-4 h-4 text-fg-muted" />
{t("notifications.title")}
</h3>
<p className="text-xs text-gray-500 mb-4">{t("notifications.description")}</p>
<p className="text-xs text-fg-muted mb-4">{t("notifications.description")}</p>
<div className="card p-5 space-y-5">
<div className="flex items-center justify-between flex-wrap gap-3">
@@ -1451,14 +1457,14 @@ export function Settings() {
<div
className={`w-10 h-10 rounded-xl flex items-center justify-center transition-colors ${
notifPrefs.enabled
? "bg-blue-500/10 border border-blue-500/20"
? "bg-blue-600/10 border border-blue-600/20"
: "bg-surface-2 border border-border"
}`}
>
{notifPrefs.enabled ? (
<BellRing className="w-5 h-5 text-blue-400" />
<BellRing className="w-5 h-5 text-blue-500" />
) : (
<BellOff className="w-5 h-5 text-gray-500" />
<BellOff className="w-5 h-5 text-fg-muted" />
)}
</div>
<Toggle
@@ -1483,10 +1489,10 @@ export function Settings() {
<span
className={`inline-flex items-center gap-1.5 text-xs font-medium px-2.5 py-1 rounded-full ${
Notification.permission === "granted"
? "text-emerald-400 bg-emerald-500/10 border border-emerald-500/20"
? "text-status-success bg-status-success/10 border border-status-success/20"
: Notification.permission === "denied"
? "text-red-400 bg-red-500/10 border border-red-500/20"
: "text-amber-400 bg-amber-500/10 border border-amber-500/20"
? "text-status-danger bg-status-danger/10 border border-status-danger/20"
: "text-status-warning bg-status-warning/10 border border-status-warning/20"
}`}
>
{Notification.permission === "granted" ? (
@@ -1507,12 +1513,12 @@ export function Settings() {
{notifPrefs.enabled && (
<div className="space-y-3 pt-4 border-t border-border">
<p className="text-xs text-gray-500 uppercase tracking-wider font-semibold">
<p className="text-xs text-fg-muted uppercase tracking-wider font-semibold">
{t("notifications.notifyWhen")}
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
<div className="flex items-center gap-3 bg-surface-2 rounded-lg px-3.5 py-3">
<Play className="w-4 h-4 text-emerald-400 flex-shrink-0" />
<Play className="w-4 h-4 text-status-success flex-shrink-0" />
<Toggle
checked={notifPrefs.onNewSession}
onChange={(v) => updateNotifPrefs({ onNewSession: v })}
@@ -1528,7 +1534,7 @@ export function Settings() {
/>
</div>
<div className="flex items-center gap-3 bg-surface-2 rounded-lg px-3.5 py-3">
<AlertCircle className="w-4 h-4 text-red-400 flex-shrink-0" />
<AlertCircle className="w-4 h-4 text-status-danger flex-shrink-0" />
<Toggle
checked={notifPrefs.onSessionError}
onChange={(v) => updateNotifPrefs({ onSessionError: v })}
@@ -1536,7 +1542,7 @@ export function Settings() {
/>
</div>
<div className="flex items-center gap-3 bg-surface-2 rounded-lg px-3.5 py-3">
<GitBranch className="w-4 h-4 text-blue-400 flex-shrink-0" />
<GitBranch className="w-4 h-4 text-blue-500 flex-shrink-0" />
<Toggle
checked={notifPrefs.onSubagentSpawn}
onChange={(v) => updateNotifPrefs({ onSubagentSpawn: v })}
@@ -1559,7 +1565,7 @@ export function Settings() {
}),
});
}}
className="inline-flex items-center gap-1.5 text-xs px-3 py-1.5 rounded-md text-gray-400 hover:text-gray-200 hover:bg-surface-4 border border-border transition-colors"
className="inline-flex items-center gap-1.5 text-xs px-3 py-1.5 rounded-md text-fg-secondary hover:text-fg-primary hover:bg-surface-4 border border-border transition-colors"
>
<Zap className="w-3 h-3" />
{t("notifications.sendTest")}
@@ -1569,7 +1575,7 @@ export function Settings() {
)}
{!notifPrefs.enabled && (
<div className="flex items-center gap-2 text-xs text-gray-500">
<div className="flex items-center gap-2 text-xs text-fg-muted">
<BellOff className="w-3.5 h-3.5" />
{t("notifications.disabledInfo")}
</div>
@@ -1579,30 +1585,30 @@ export function Settings() {
{/* ─── ALERTS ─── */}
<section id="alerts" className="scroll-mt-24">
<h3 className="text-sm font-medium text-gray-300 flex items-center gap-2 mb-1">
<BellRing className="w-4 h-4 text-gray-500" />
<h3 className="text-sm font-medium text-fg-secondary flex items-center gap-2 mb-1">
<BellRing className="w-4 h-4 text-fg-muted" />
{t("alertsHub.title")}
</h3>
<p className="text-xs text-gray-500 mb-4">{t("alertsHub.description")}</p>
<p className="text-xs text-fg-muted mb-4">{t("alertsHub.description")}</p>
<AlertsNotifications />
</section>
{/* ─── DATA MANAGEMENT ─── */}
<section id="data" className="scroll-mt-24">
<h3 className="text-sm font-medium text-gray-300 flex items-center gap-2 mb-1">
<Database className="w-4 h-4 text-gray-500" />
<h3 className="text-sm font-medium text-fg-secondary flex items-center gap-2 mb-1">
<Database className="w-4 h-4 text-fg-muted" />
{t("data.title")}
</h3>
<p className="text-xs text-gray-500 mb-4">{t("data.description")}</p>
<p className="text-xs text-fg-muted mb-4">{t("data.description")}</p>
<div className="space-y-4">
<div className="card p-5 space-y-4">
<div className="flex flex-wrap items-center justify-between gap-2">
<p className="text-xs text-gray-500 uppercase tracking-wider font-semibold flex-shrink-0">
<p className="text-xs text-fg-muted uppercase tracking-wider font-semibold flex-shrink-0">
{t("data.dbOverview")}
</p>
{sysInfo && (
<div className="flex items-center gap-1.5 text-[11px] text-gray-600 font-mono bg-surface-2 px-2.5 py-1 rounded-md min-w-0">
<div className="flex items-center gap-1.5 text-[11px] text-fg-muted font-mono bg-surface-2 px-2.5 py-1 rounded-md min-w-0">
<HardDrive className="w-3 h-3 flex-shrink-0" />
<span className="truncate">{sysInfo.db.path}</span>
</div>
@@ -1613,10 +1619,10 @@ export function Settings() {
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-2">
{(() => {
const tableIcons: Record<string, React.ReactNode> = {
sessions: <Layers className="w-4 h-4 text-blue-400" />,
agents: <Users className="w-4 h-4 text-emerald-400" />,
sessions: <Layers className="w-4 h-4 text-blue-500" />,
agents: <Users className="w-4 h-4 text-status-success" />,
events: <Activity className="w-4 h-4 text-violet-400" />,
token_usage: <Coins className="w-4 h-4 text-amber-400" />,
token_usage: <Coins className="w-4 h-4 text-status-warning" />,
model_pricing: <BarChart3 className="w-4 h-4 text-cyan-400" />,
};
const tableLabels: Record<string, string> = {
@@ -1627,24 +1633,24 @@ export function Settings() {
model_pricing: t("tables.pricingRules"),
};
const tableColors: Record<string, string> = {
sessions: "border-blue-500/20",
agents: "border-emerald-500/20",
sessions: "border-blue-600/20",
agents: "border-status-success/20",
events: "border-violet-500/20",
token_usage: "border-amber-500/20",
token_usage: "border-status-warning/20",
model_pricing: "border-cyan-500/20",
};
return Object.entries(sysInfo.db.counts).map(([table, count]) => (
<div
key={table}
className={`bg-surface-2 rounded-lg px-3 py-3 border-l-2 ${tableColors[table] || "border-gray-500/20"}`}
className={`bg-surface-2 rounded-lg px-3 py-3 border-l-2 ${tableColors[table] || "border-border-light/20"}`}
>
<div className="flex items-center gap-2 mb-1.5">
{tableIcons[table] || <Database className="w-4 h-4 text-gray-500" />}
<p className="text-[11px] text-gray-500 uppercase tracking-wider">
{tableIcons[table] || <Database className="w-4 h-4 text-fg-muted" />}
<p className="text-[11px] text-fg-muted uppercase tracking-wider">
{tableLabels[table] || table.replace(/_/g, " ")}
</p>
</div>
<p className="text-xl font-semibold text-gray-200">
<p className="text-xl font-semibold text-fg-secondary">
<Tip raw={count.toLocaleString()}>{fmt(count)}</Tip>
</p>
</div>
@@ -1653,35 +1659,37 @@ export function Settings() {
<div className="bg-surface-2 rounded-lg px-3 py-3 border-l-2 border-indigo-500/20">
<div className="flex items-center gap-2 mb-1.5">
<HardDrive className="w-4 h-4 text-indigo-400" />
<p className="text-[11px] text-gray-500 uppercase tracking-wider">
<p className="text-[11px] text-fg-muted uppercase tracking-wider">
{t("data.dbSize")}
</p>
</div>
<p className="text-xl font-semibold text-gray-200">
<p className="text-xl font-semibold text-fg-secondary">
{formatBytes(sysInfo.db.size)}
</p>
</div>
</div>
) : (
<p className="text-xs text-gray-500">{t("data.loadingDb")}</p>
<p className="text-xs text-fg-muted">{t("data.loadingDb")}</p>
)}
</div>
{/* Session Cleanup */}
<div className="card p-5 space-y-4">
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-lg bg-amber-500/10 border border-amber-500/20 flex items-center justify-center">
<Eraser className="w-4 h-4 text-amber-400" />
<div className="w-9 h-9 rounded-lg bg-status-warning/10 border border-status-warning/20 flex items-center justify-center">
<Eraser className="w-4 h-4 text-status-warning" />
</div>
<div>
<p className="text-sm font-medium text-gray-300">{t("data.sessionCleanup")}</p>
<p className="text-xs text-gray-500">{t("data.cleanupDesc")}</p>
<p className="text-sm font-medium text-fg-secondary">{t("data.sessionCleanup")}</p>
<p className="text-xs text-fg-muted">{t("data.cleanupDesc")}</p>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div className="bg-surface-2 rounded-lg px-4 py-3">
<label className="text-xs text-gray-400 block mb-2">{t("data.abandonAfter")}</label>
<label className="text-xs text-fg-secondary block mb-2">
{t("data.abandonAfter")}
</label>
<div className="flex items-center gap-2">
<input
type="number"
@@ -1690,11 +1698,13 @@ export function Settings() {
onChange={(e) => setAbandonHours(e.target.value)}
className="input w-20 text-sm text-right font-mono"
/>
<span className="text-xs text-gray-500">{t("common:hours")}</span>
<span className="text-xs text-fg-muted">{t("common:hours")}</span>
</div>
</div>
<div className="bg-surface-2 rounded-lg px-4 py-3">
<label className="text-xs text-gray-400 block mb-2">{t("data.purgeAfter")}</label>
<label className="text-xs text-fg-secondary block mb-2">
{t("data.purgeAfter")}
</label>
<div className="flex items-center gap-2">
<input
type="number"
@@ -1703,7 +1713,7 @@ export function Settings() {
onChange={(e) => setPurgeDays(e.target.value)}
className="input w-20 text-sm text-right font-mono"
/>
<span className="text-xs text-gray-500">{t("common:days")}</span>
<span className="text-xs text-fg-muted">{t("common:days")}</span>
</div>
</div>
</div>
@@ -1715,8 +1725,8 @@ export function Settings() {
disabled={actionLoading !== null}
className={`text-xs px-3 py-1.5 rounded-md transition-colors disabled:opacity-50 ${
confirmAction === "cleanup"
? "bg-amber-500/20 text-amber-400 border border-amber-500/30"
: "text-gray-400 hover:text-gray-300 hover:bg-surface-4 border border-border"
? "bg-status-warning/20 text-status-warning border border-status-warning/30"
: "text-fg-secondary hover:text-fg-primary hover:bg-surface-4 border border-border"
}`}
>
{actionLoading === "cleanup" ? (
@@ -1731,25 +1741,25 @@ export function Settings() {
</div>
{/* Danger zone */}
<div className="card p-5 space-y-4 border-red-500/10">
<div className="card p-5 space-y-4 border-status-danger/10">
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-lg bg-red-500/10 border border-red-500/20 flex items-center justify-center">
<AlertTriangle className="w-4 h-4 text-red-400" />
<div className="w-9 h-9 rounded-lg bg-status-danger/10 border border-status-danger/20 flex items-center justify-center">
<AlertTriangle className="w-4 h-4 text-status-danger" />
</div>
<div>
<p className="text-sm font-medium text-red-400">{t("danger.title")}</p>
<p className="text-xs text-gray-500">{t("danger.description")}</p>
<p className="text-sm font-medium text-status-danger">{t("danger.title")}</p>
<p className="text-xs text-fg-muted">{t("danger.description")}</p>
</div>
</div>
{confirmAction === "clear" ? (
<div className="bg-red-500/5 border border-red-500/20 rounded-lg px-4 py-3 flex items-center justify-between flex-wrap gap-3">
<span className="text-xs text-amber-400">{t("danger.warning")}</span>
<div className="bg-status-danger/5 border border-status-danger/20 rounded-lg px-4 py-3 flex items-center justify-between flex-wrap gap-3">
<span className="text-xs text-status-warning">{t("danger.warning")}</span>
<div className="flex items-center gap-2">
<button
onClick={handleClearData}
disabled={actionLoading !== null}
className="text-xs px-3 py-1.5 rounded-md bg-red-500/20 text-red-400 border border-red-500/30 hover:bg-red-500/30 transition-colors disabled:opacity-50"
className="text-xs px-3 py-1.5 rounded-md bg-status-danger/20 text-status-danger border border-status-danger/30 hover:bg-status-danger/30 transition-colors disabled:opacity-50"
>
{actionLoading === "clear" ? (
<RefreshCw className="w-3.5 h-3.5 animate-spin inline mr-1" />
@@ -1758,7 +1768,7 @@ export function Settings() {
</button>
<button
onClick={() => setConfirmAction(null)}
className="text-xs px-3 py-1.5 rounded-md text-gray-400 hover:bg-surface-4 transition-colors"
className="text-xs px-3 py-1.5 rounded-md text-fg-secondary hover:bg-surface-4 transition-colors"
>
{t("common:cancel")}
</button>
@@ -1768,7 +1778,7 @@ export function Settings() {
<button
onClick={() => setConfirmAction("clear")}
disabled={actionLoading !== null}
className="text-xs px-3 py-1.5 rounded-md text-red-400 hover:bg-red-500/10 border border-red-500/20 transition-colors disabled:opacity-50"
className="text-xs px-3 py-1.5 rounded-md text-status-danger hover:bg-status-danger/10 border border-status-danger/20 transition-colors disabled:opacity-50"
>
<AlertTriangle className="w-3.5 h-3.5 inline mr-1" />
{t("danger.clearAllData")}
@@ -1782,11 +1792,11 @@ export function Settings() {
{/* ─── ABOUT ─── */}
<section id="about" className="scroll-mt-24">
<h3 className="text-sm font-medium text-gray-300 flex items-center gap-2 mb-1">
<Server className="w-4 h-4 text-gray-500" />
<h3 className="text-sm font-medium text-fg-secondary flex items-center gap-2 mb-1">
<Server className="w-4 h-4 text-fg-muted" />
{t("about.title")}
</h3>
<p className="text-xs text-gray-500 mb-4">{t("about.description")}</p>
<p className="text-xs text-fg-muted mb-4">{t("about.description")}</p>
{sysInfo ? (
<div className="card p-5">
@@ -1794,65 +1804,65 @@ export function Settings() {
<div className="bg-surface-2 rounded-lg px-4 py-3">
<div className="flex items-center gap-2 mb-1.5">
<Server className="w-4 h-4 text-indigo-400" />
<p className="text-[11px] text-gray-500 uppercase tracking-wider">
<p className="text-[11px] text-fg-muted uppercase tracking-wider">
{t("about.release")}
</p>
</div>
<p className="text-sm font-semibold text-gray-200 font-mono">
<p className="text-sm font-semibold text-fg-secondary font-mono">
v{sysInfo.server.version}
</p>
{sysInfo.server.version !== __APP_VERSION__ && (
<p className="text-[10px] text-amber-400/90 mt-1">
<p className="text-[10px] text-status-warning/90 mt-1">
{t("about.uiBuild", { version: __APP_VERSION__ })}
</p>
)}
</div>
<div className="bg-surface-2 rounded-lg px-4 py-3">
<div className="flex items-center gap-2 mb-1.5">
<Clock className="w-4 h-4 text-blue-400" />
<p className="text-[11px] text-gray-500 uppercase tracking-wider">
<Clock className="w-4 h-4 text-blue-500" />
<p className="text-[11px] text-fg-muted uppercase tracking-wider">
{t("about.uptime")}
</p>
</div>
<p className="text-sm font-semibold text-gray-200">
<p className="text-sm font-semibold text-fg-secondary">
{formatUptime(sysInfo.server.uptime)}
</p>
</div>
<div className="bg-surface-2 rounded-lg px-4 py-3">
<div className="flex items-center gap-2 mb-1.5">
<Cpu className="w-4 h-4 text-emerald-400" />
<p className="text-[11px] text-gray-500 uppercase tracking-wider">
<Cpu className="w-4 h-4 text-status-success" />
<p className="text-[11px] text-fg-muted uppercase tracking-wider">
{t("about.nodejs")}
</p>
</div>
<p className="text-sm font-semibold text-gray-200 font-mono">
<p className="text-sm font-semibold text-fg-secondary font-mono">
{sysInfo.server.node_version}
</p>
</div>
<div className="bg-surface-2 rounded-lg px-4 py-3">
<div className="flex items-center gap-2 mb-1.5">
<Globe className="w-4 h-4 text-violet-400" />
<p className="text-[11px] text-gray-500 uppercase tracking-wider">
<p className="text-[11px] text-fg-muted uppercase tracking-wider">
{t("about.platform")}
</p>
</div>
<p className="text-sm font-semibold text-gray-200">{sysInfo.server.platform}</p>
<p className="text-sm font-semibold text-fg-secondary">{sysInfo.server.platform}</p>
</div>
<div className="bg-surface-2 rounded-lg px-4 py-3">
<div className="flex items-center gap-2 mb-1.5">
<Wifi className="w-4 h-4 text-amber-400" />
<p className="text-[11px] text-gray-500 uppercase tracking-wider">
<Wifi className="w-4 h-4 text-status-warning" />
<p className="text-[11px] text-fg-muted uppercase tracking-wider">
{t("about.wsClients")}
</p>
</div>
<p className="text-sm font-semibold text-gray-200">
<p className="text-sm font-semibold text-fg-secondary">
{sysInfo.server.ws_connections}
</p>
</div>
</div>
</div>
) : (
<p className="text-xs text-gray-500">{t("about.loadingInfo")}</p>
<p className="text-xs text-fg-muted">{t("about.loadingInfo")}</p>
)}
</section>
</div>