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:
+167
-157
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user