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:
@@ -403,8 +403,8 @@ export function AlertsNotifications() {
|
||||
onClick={() => setTab(tb.key)}
|
||||
className={`inline-flex items-center gap-2 text-xs font-medium px-3.5 py-2 rounded-lg transition-colors ${
|
||||
active
|
||||
? "bg-surface-4 text-gray-100 shadow-sm"
|
||||
: "text-gray-500 hover:text-gray-300 hover:bg-surface-3"
|
||||
? "bg-surface-4 text-fg-primary shadow-sm"
|
||||
: "text-fg-muted hover:text-fg-secondary hover:bg-surface-3"
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-3.5 h-3.5" />
|
||||
@@ -413,10 +413,10 @@ export function AlertsNotifications() {
|
||||
<span
|
||||
className={`text-[10px] font-semibold rounded-full px-1.5 min-w-[18px] text-center ${
|
||||
tb.key === "activity"
|
||||
? "text-amber-300 bg-amber-500/15"
|
||||
? "text-status-warning bg-status-warning/15"
|
||||
: active
|
||||
? "text-accent bg-accent/15"
|
||||
: "text-gray-400 bg-surface-2"
|
||||
: "text-fg-secondary bg-surface-2"
|
||||
}`}
|
||||
>
|
||||
{tb.badge}
|
||||
@@ -432,8 +432,8 @@ export function AlertsNotifications() {
|
||||
<div className="card p-4">
|
||||
<div className="flex items-center justify-between gap-3 mb-3">
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-gray-200">{t("rules.title")}</h4>
|
||||
<p className="text-xs text-gray-500 mt-0.5">{ts("alertsHub.rulesHint")}</p>
|
||||
<h4 className="text-sm font-semibold text-fg-secondary">{t("rules.title")}</h4>
|
||||
<p className="text-xs text-fg-muted mt-0.5">{ts("alertsHub.rulesHint")}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
@@ -450,7 +450,7 @@ export function AlertsNotifications() {
|
||||
{formOpen && (
|
||||
<div className="rounded-lg border border-border bg-surface-2 p-3 mb-3 space-y-3">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<label className="block text-xs text-gray-400">
|
||||
<label className="block text-xs text-fg-secondary">
|
||||
<span className="inline-flex items-center gap-1">
|
||||
{t("rules.form.name")}
|
||||
<FieldHelp description={t("rules.help.name")} />
|
||||
@@ -463,7 +463,7 @@ export function AlertsNotifications() {
|
||||
className="input mt-1 w-full"
|
||||
/>
|
||||
</label>
|
||||
<label className="block text-xs text-gray-400">
|
||||
<label className="block text-xs text-fg-secondary">
|
||||
<span className="inline-flex items-center gap-1">
|
||||
{t("rules.form.type")}
|
||||
<FieldHelp title={t("rules.form.type")} description={t("rules.help.type")} />
|
||||
@@ -480,16 +480,16 @@ export function AlertsNotifications() {
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<ChevronDown className="w-3.5 h-3.5 absolute right-2.5 top-1/2 -translate-y-1/2 text-gray-500 pointer-events-none" />
|
||||
<ChevronDown className="w-3.5 h-3.5 absolute right-2.5 top-1/2 -translate-y-1/2 text-fg-muted pointer-events-none" />
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<p className="text-[11px] text-gray-500">{t(`ruleTypeHints.${form.rule_type}`)}</p>
|
||||
<p className="text-[11px] text-fg-muted">{t(`ruleTypeHints.${form.rule_type}`)}</p>
|
||||
|
||||
{form.rule_type === "event_pattern" && (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<label className="block text-xs text-gray-400">
|
||||
<label className="block text-xs text-fg-secondary">
|
||||
<span className="inline-flex items-center gap-1">
|
||||
{t("rules.form.eventType")}
|
||||
<FieldHelp
|
||||
@@ -506,7 +506,7 @@ export function AlertsNotifications() {
|
||||
className="input mt-1 w-full"
|
||||
/>
|
||||
</label>
|
||||
<label className="block text-xs text-gray-400">
|
||||
<label className="block text-xs text-fg-secondary">
|
||||
<span className="inline-flex items-center gap-1">
|
||||
{t("rules.form.toolName")}
|
||||
<FieldHelp
|
||||
@@ -523,7 +523,7 @@ export function AlertsNotifications() {
|
||||
className="input mt-1 w-full"
|
||||
/>
|
||||
</label>
|
||||
<label className="block text-xs text-gray-400">
|
||||
<label className="block text-xs text-fg-secondary">
|
||||
<span className="inline-flex items-center gap-1">
|
||||
{t("rules.form.summaryContains")}
|
||||
<FieldHelp
|
||||
@@ -540,7 +540,7 @@ export function AlertsNotifications() {
|
||||
className="input mt-1 w-full"
|
||||
/>
|
||||
</label>
|
||||
<label className="block text-xs text-gray-400">
|
||||
<label className="block text-xs text-fg-secondary">
|
||||
<span className="inline-flex items-center gap-1">
|
||||
{t("rules.form.count")}
|
||||
<FieldHelp description={t("rules.help.count")} />
|
||||
@@ -554,7 +554,7 @@ export function AlertsNotifications() {
|
||||
/>
|
||||
</label>
|
||||
{parseInt(form.count, 10) > 1 && (
|
||||
<label className="block text-xs text-gray-400">
|
||||
<label className="block text-xs text-fg-secondary">
|
||||
<span className="inline-flex items-center gap-1">
|
||||
{t("rules.form.windowMinutes")}
|
||||
<FieldHelp description={t("rules.help.window")} />
|
||||
@@ -574,7 +574,7 @@ export function AlertsNotifications() {
|
||||
{(form.rule_type === "inactivity" || form.rule_type === "status_duration") && (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
{form.rule_type === "status_duration" && (
|
||||
<label className="block text-xs text-gray-400">
|
||||
<label className="block text-xs text-fg-secondary">
|
||||
<span className="inline-flex items-center gap-1">
|
||||
{t("rules.form.agentStatus")}
|
||||
<FieldHelp description={t("rules.help.status")} />
|
||||
@@ -588,11 +588,11 @@ export function AlertsNotifications() {
|
||||
<option value="working">working</option>
|
||||
<option value="waiting">waiting</option>
|
||||
</select>
|
||||
<ChevronDown className="w-3.5 h-3.5 absolute right-2.5 top-1/2 -translate-y-1/2 text-gray-500 pointer-events-none" />
|
||||
<ChevronDown className="w-3.5 h-3.5 absolute right-2.5 top-1/2 -translate-y-1/2 text-fg-muted pointer-events-none" />
|
||||
</div>
|
||||
</label>
|
||||
)}
|
||||
<label className="block text-xs text-gray-400">
|
||||
<label className="block text-xs text-fg-secondary">
|
||||
<span className="inline-flex items-center gap-1">
|
||||
{t("rules.form.minutes")}
|
||||
<FieldHelp
|
||||
@@ -616,7 +616,7 @@ export function AlertsNotifications() {
|
||||
|
||||
{form.rule_type === "token_threshold" && (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<label className="block text-xs text-gray-400">
|
||||
<label className="block text-xs text-fg-secondary">
|
||||
<span className="inline-flex items-center gap-1">
|
||||
{t("rules.form.totalTokens")}
|
||||
<FieldHelp description={t("rules.help.totalTokens")} />
|
||||
@@ -633,7 +633,7 @@ export function AlertsNotifications() {
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap items-end justify-between gap-3">
|
||||
<label className="block text-xs text-gray-400">
|
||||
<label className="block text-xs text-fg-secondary">
|
||||
<span className="mb-1.5 flex items-center gap-1">
|
||||
{t("rules.form.cooldown")}
|
||||
<FieldHelp description={t("rules.help.cooldown")} />
|
||||
@@ -655,7 +655,7 @@ export function AlertsNotifications() {
|
||||
{saving ? t("rules.saving") : t("rules.create")}
|
||||
</button>
|
||||
</div>
|
||||
{formError && <p className="text-xs text-red-400">{formError}</p>}
|
||||
{formError && <p className="text-xs text-status-danger">{formError}</p>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -680,7 +680,7 @@ export function AlertsNotifications() {
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`text-sm font-medium truncate ${rule.enabled ? "text-gray-200" : "text-gray-500 line-through"}`}
|
||||
className={`text-sm font-medium truncate ${rule.enabled ? "text-fg-secondary" : "text-fg-muted line-through"}`}
|
||||
>
|
||||
{rule.name}
|
||||
</span>
|
||||
@@ -688,7 +688,7 @@ export function AlertsNotifications() {
|
||||
{t(`ruleTypes.${rule.rule_type}`)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-0.5 truncate">
|
||||
<p className="text-xs text-fg-muted mt-0.5 truncate">
|
||||
{describeRule(rule, t)} ·{" "}
|
||||
{t("rules.cooldown", { seconds: rule.cooldown_seconds })}
|
||||
</p>
|
||||
@@ -698,8 +698,8 @@ export function AlertsNotifications() {
|
||||
onClick={() => onToggleRule(rule)}
|
||||
className={`text-xs px-2.5 py-1.5 rounded-md border transition-colors ${
|
||||
rule.enabled
|
||||
? "border-emerald-500/30 text-emerald-400 hover:bg-emerald-500/10"
|
||||
: "border-border text-gray-500 hover:text-gray-300 hover:bg-surface-3"
|
||||
? "border-status-success/30 text-status-success hover:bg-status-success/10"
|
||||
: "border-border text-fg-muted hover:text-fg-secondary hover:bg-surface-3"
|
||||
}`}
|
||||
title={rule.enabled ? t("rules.disable") : t("rules.enable")}
|
||||
>
|
||||
@@ -707,7 +707,7 @@ export function AlertsNotifications() {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setConfirmRule(rule)}
|
||||
className="p-1.5 rounded-md text-gray-500 hover:text-red-400 hover:bg-red-500/10 transition-colors"
|
||||
className="p-1.5 rounded-md text-fg-muted hover:text-status-danger hover:bg-status-danger/10 transition-colors"
|
||||
title={t("rules.delete")}
|
||||
aria-label={t("rules.delete")}
|
||||
>
|
||||
@@ -728,10 +728,10 @@ export function AlertsNotifications() {
|
||||
{tab === "activity" && (
|
||||
<div className="card p-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 mb-3">
|
||||
<h4 className="text-sm font-semibold text-gray-200">
|
||||
<h4 className="text-sm font-semibold text-fg-secondary">
|
||||
{t("feed.title")}
|
||||
{unacked > 0 && (
|
||||
<span className="ml-2 text-[10px] font-semibold text-amber-300 bg-amber-500/10 border border-amber-500/30 rounded-full px-2 py-0.5">
|
||||
<span className="ml-2 text-[10px] font-semibold text-status-warning bg-status-warning/10 border border-status-warning/30 rounded-full px-2 py-0.5">
|
||||
{t("feed.unackedCount", { count: unacked })}
|
||||
</span>
|
||||
)}
|
||||
@@ -782,17 +782,17 @@ export function AlertsNotifications() {
|
||||
className={`flex flex-wrap items-center justify-between gap-3 rounded-lg border px-3 py-2.5 ${
|
||||
alert.acknowledged_at
|
||||
? "border-border bg-surface-2 opacity-70"
|
||||
: "border-amber-500/30 bg-amber-500/5"
|
||||
: "border-status-warning/30 bg-status-warning/5"
|
||||
}`}
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<BellRing
|
||||
className={`w-3.5 h-3.5 flex-shrink-0 ${alert.acknowledged_at ? "text-gray-500" : "text-amber-400"}`}
|
||||
className={`w-3.5 h-3.5 flex-shrink-0 ${alert.acknowledged_at ? "text-fg-muted" : "text-status-warning"}`}
|
||||
/>
|
||||
<span className="text-sm text-gray-200 truncate">{alert.message}</span>
|
||||
<span className="text-sm text-fg-secondary truncate">{alert.message}</span>
|
||||
</div>
|
||||
<p className="text-[11px] text-gray-500 mt-0.5 font-mono">
|
||||
<p className="text-[11px] text-fg-muted mt-0.5 font-mono">
|
||||
{timeAgo(alert.triggered_at)} · {alert.rule_name}
|
||||
{alert.session_id && (
|
||||
<>
|
||||
@@ -810,7 +810,7 @@ export function AlertsNotifications() {
|
||||
{!alert.acknowledged_at && (
|
||||
<button
|
||||
onClick={() => onAck(alert.id)}
|
||||
className="inline-flex items-center gap-1.5 text-xs px-2.5 py-1.5 rounded-md border border-border text-gray-300 hover:text-gray-100 hover:bg-surface-3 transition-colors flex-shrink-0"
|
||||
className="inline-flex items-center gap-1.5 text-xs px-2.5 py-1.5 rounded-md border border-border text-fg-secondary hover:text-fg-primary hover:bg-surface-3 transition-colors flex-shrink-0"
|
||||
>
|
||||
<Check className="w-3.5 h-3.5" />
|
||||
{t("feed.ack")}
|
||||
|
||||
Reference in New Issue
Block a user