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:
@@ -79,10 +79,13 @@ import {
|
||||
Gauge,
|
||||
ChevronUp,
|
||||
ChevronDown,
|
||||
Sun,
|
||||
Moon,
|
||||
} from "lucide-react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import { api } from "../lib/api";
|
||||
import { eventBus } from "../lib/eventBus";
|
||||
import { useTheme } from "../hooks/useTheme";
|
||||
import type { UpdateStatusPayload, WSMessage } from "../lib/types";
|
||||
|
||||
function isUpdatePayload(x: unknown): x is UpdateStatusPayload {
|
||||
@@ -180,6 +183,7 @@ interface SidebarProps {
|
||||
|
||||
export function Sidebar({ wsConnected, collapsed, onToggle }: SidebarProps) {
|
||||
const { t, i18n } = useTranslation();
|
||||
const { theme, setTheme, toggleTheme } = useTheme();
|
||||
// Track whether nav items are clipped by overflow so we can render
|
||||
// chevron affordances pointing toward the hidden items. Recomputed on
|
||||
// scroll, resize, and any structural change (e.g. collapse toggle).
|
||||
@@ -376,6 +380,8 @@ export function Sidebar({ wsConnected, collapsed, onToggle }: SidebarProps) {
|
||||
const switchLanguageTitle = t("nav:switchLanguage", {
|
||||
language: t(`nav:languageNames.${nextLanguage}`),
|
||||
});
|
||||
const nextTheme = theme === "dark" ? "light" : "dark";
|
||||
const switchThemeTitle = t("nav:switchTheme", { theme: t(`nav:themeNames.${nextTheme}`) });
|
||||
|
||||
const toggleLang = () => {
|
||||
i18n.changeLanguage(nextLanguage);
|
||||
@@ -401,8 +407,8 @@ export function Sidebar({ wsConnected, collapsed, onToggle }: SidebarProps) {
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-sm font-semibold text-gray-100 truncate">{t("nav:brand")}</h1>
|
||||
<p className="text-[11px] text-gray-500">{t("nav:brandSub")}</p>
|
||||
<h1 className="text-sm font-semibold text-fg-primary truncate">{t("nav:brand")}</h1>
|
||||
<p className="text-[11px] text-fg-muted">{t("nav:brandSub")}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -428,7 +434,7 @@ export function Sidebar({ wsConnected, collapsed, onToggle }: SidebarProps) {
|
||||
} ${
|
||||
isActive
|
||||
? "bg-accent/10 text-accent border border-accent/20"
|
||||
: "text-gray-400 hover:text-gray-200 hover:bg-surface-3 border border-transparent"
|
||||
: "text-fg-secondary hover:text-fg-primary hover:bg-surface-3 border border-transparent"
|
||||
}`
|
||||
}
|
||||
>
|
||||
@@ -444,7 +450,7 @@ export function Sidebar({ wsConnected, collapsed, onToggle }: SidebarProps) {
|
||||
onClick={() => scrollNavBy(-160)}
|
||||
aria-label={t("nav:scrollUp")}
|
||||
title={t("nav:scrollUp")}
|
||||
className="absolute top-1.5 right-[7px] z-10 inline-flex items-center justify-center w-6 h-6 rounded-md border border-border bg-surface-2/90 text-gray-300 hover:text-gray-50 hover:bg-surface-3 shadow-md backdrop-blur-sm transition-colors animate-fade-in"
|
||||
className="absolute top-1.5 right-[7px] z-10 inline-flex items-center justify-center w-6 h-6 rounded-md border border-border bg-surface-2/90 text-fg-secondary hover:text-fg-primary hover:bg-surface-3 shadow-md backdrop-blur-sm transition-colors animate-fade-in"
|
||||
>
|
||||
<ChevronUp className="w-3.5 h-3.5" aria-hidden />
|
||||
</button>
|
||||
@@ -455,52 +461,106 @@ export function Sidebar({ wsConnected, collapsed, onToggle }: SidebarProps) {
|
||||
onClick={() => scrollNavBy(160)}
|
||||
aria-label={t("nav:scrollDown")}
|
||||
title={t("nav:scrollDown")}
|
||||
className="absolute bottom-1.5 right-[7px] z-10 inline-flex items-center justify-center w-6 h-6 rounded-md border border-border bg-surface-2/90 text-gray-300 hover:text-gray-50 hover:bg-surface-3 shadow-md backdrop-blur-sm transition-colors animate-fade-in"
|
||||
className="absolute bottom-1.5 right-[7px] z-10 inline-flex items-center justify-center w-6 h-6 rounded-md border border-border bg-surface-2/90 text-fg-secondary hover:text-fg-primary hover:bg-surface-3 shadow-md backdrop-blur-sm transition-colors animate-fade-in"
|
||||
>
|
||||
<ChevronDown className="w-3.5 h-3.5" aria-hidden />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Language controls */}
|
||||
{/* Language + theme controls. Theme sits next to language: same icon
|
||||
row when collapsed, same row as the EN/VI buttons when expanded -
|
||||
both are 2-state click-to-flip toggles, so they read as one family
|
||||
of controls rather than two unrelated settings. */}
|
||||
<div className="px-2 pb-2 flex-shrink-0">
|
||||
{collapsed ? (
|
||||
<button
|
||||
onClick={toggleLang}
|
||||
className="w-full h-9 rounded-lg border border-border bg-surface-2 text-gray-300 hover:bg-surface-3 hover:text-gray-100 transition-colors flex flex-col items-center justify-center gap-0.5"
|
||||
title={switchLanguageTitle}
|
||||
aria-label={switchLanguageTitle}
|
||||
>
|
||||
<Languages className="w-3.5 h-3.5" />
|
||||
<span className="text-[10px] font-semibold leading-none">
|
||||
{t(`nav:languageShort.${currentLanguage}`)}
|
||||
</span>
|
||||
</button>
|
||||
<div className="flex flex-col gap-1">
|
||||
<button
|
||||
onClick={toggleLang}
|
||||
className="w-full h-9 rounded-lg border border-border bg-surface-2 text-fg-secondary hover:bg-surface-3 hover:text-fg-primary transition-colors flex flex-col items-center justify-center gap-0.5"
|
||||
title={switchLanguageTitle}
|
||||
aria-label={switchLanguageTitle}
|
||||
>
|
||||
<Languages className="w-3.5 h-3.5" />
|
||||
<span className="text-[10px] font-semibold leading-none">
|
||||
{t(`nav:languageShort.${currentLanguage}`)}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="w-full h-9 rounded-lg border border-border bg-surface-2 text-fg-secondary hover:bg-surface-3 hover:text-fg-primary transition-colors flex flex-col items-center justify-center gap-0.5"
|
||||
title={switchThemeTitle}
|
||||
aria-label={switchThemeTitle}
|
||||
>
|
||||
{theme === "dark" ? (
|
||||
<Moon className="w-3.5 h-3.5" />
|
||||
) : (
|
||||
<Sun className="w-3.5 h-3.5" />
|
||||
)}
|
||||
<span className="text-[10px] font-semibold leading-none">
|
||||
{t(`nav:themeNames.${theme}`).slice(0, 1)}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border border-border bg-surface-2 p-2">
|
||||
<p className="px-1 text-[10px] font-semibold uppercase tracking-wider text-gray-500">
|
||||
{t("nav:language")}
|
||||
</p>
|
||||
<div className="mt-2 grid grid-cols-4 gap-1">
|
||||
{SUPPORTED_LANGUAGES.map((language) => {
|
||||
const active = language === currentLanguage;
|
||||
return (
|
||||
<button
|
||||
key={language}
|
||||
onClick={() => changeLanguage(language)}
|
||||
aria-pressed={active}
|
||||
aria-label={t(`nav:languageNames.${language}`)}
|
||||
title={t(`nav:languageNames.${language}`)}
|
||||
className={`rounded-md px-2 py-1.5 text-[11px] font-semibold transition-colors ${
|
||||
active
|
||||
? "bg-accent/20 text-accent border border-accent/30"
|
||||
: "bg-surface-1 text-gray-400 border border-border hover:bg-surface-3 hover:text-gray-200"
|
||||
}`}
|
||||
>
|
||||
{t(`nav:languageShort.${language}`)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
<div className="rounded-lg border border-border bg-surface-2 p-2 space-y-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex-1">
|
||||
<p className="px-1 text-[10px] font-semibold uppercase tracking-wider text-fg-muted">
|
||||
{t("nav:language")}
|
||||
</p>
|
||||
<div className="mt-2 grid grid-cols-4 gap-1">
|
||||
{SUPPORTED_LANGUAGES.map((language) => {
|
||||
const active = language === currentLanguage;
|
||||
return (
|
||||
<button
|
||||
key={language}
|
||||
onClick={() => changeLanguage(language)}
|
||||
aria-pressed={active}
|
||||
aria-label={t(`nav:languageNames.${language}`)}
|
||||
title={t(`nav:languageNames.${language}`)}
|
||||
className={`rounded-md px-2 py-1.5 text-[11px] font-semibold transition-colors ${
|
||||
active
|
||||
? "bg-accent/20 text-accent border border-accent/30"
|
||||
: "bg-surface-1 text-fg-secondary border border-border hover:bg-surface-3 hover:text-fg-primary"
|
||||
}`}
|
||||
>
|
||||
{t(`nav:languageShort.${language}`)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="px-1 text-[10px] font-semibold uppercase tracking-wider text-fg-muted">
|
||||
{t("nav:theme")}
|
||||
</p>
|
||||
<div className="mt-2 grid grid-cols-2 gap-1">
|
||||
{(["dark", "light"] as const).map((option) => {
|
||||
const active = option === theme;
|
||||
return (
|
||||
<button
|
||||
key={option}
|
||||
onClick={() => setTheme(option)}
|
||||
aria-pressed={active}
|
||||
aria-label={t(`nav:themeNames.${option}`)}
|
||||
title={t(`nav:themeNames.${option}`)}
|
||||
className={`rounded-md px-2 py-1.5 text-[11px] font-semibold transition-colors flex items-center justify-center ${
|
||||
active
|
||||
? "bg-accent/20 text-accent border border-accent/30"
|
||||
: "bg-surface-1 text-fg-secondary border border-border hover:bg-surface-3 hover:text-fg-primary"
|
||||
}`}
|
||||
>
|
||||
{option === "dark" ? (
|
||||
<Moon className="w-3.5 h-3.5" />
|
||||
) : (
|
||||
<Sun className="w-3.5 h-3.5" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -512,8 +572,8 @@ export function Sidebar({ wsConnected, collapsed, onToggle }: SidebarProps) {
|
||||
onClick={onToggle}
|
||||
className={`w-full h-10 rounded-lg border border-border bg-surface-2 transition-colors ${
|
||||
collapsed
|
||||
? "flex items-center justify-center text-gray-400 hover:text-gray-200 hover:bg-surface-3"
|
||||
: "flex items-center gap-2.5 px-3 text-gray-300 hover:text-gray-100 hover:bg-surface-3"
|
||||
? "flex items-center justify-center text-fg-secondary hover:text-fg-primary hover:bg-surface-3"
|
||||
: "flex items-center gap-2.5 px-3 text-fg-secondary hover:text-fg-primary hover:bg-surface-3"
|
||||
}`}
|
||||
title={collapsed ? t("nav:expand") : t("nav:collapse")}
|
||||
aria-label={collapsed ? t("nav:expand") : t("nav:collapse")}
|
||||
@@ -551,7 +611,7 @@ export function Sidebar({ wsConnected, collapsed, onToggle }: SidebarProps) {
|
||||
>
|
||||
<span
|
||||
className={`inline-flex items-center gap-2 ${
|
||||
wsConnected ? "text-emerald-400" : "text-gray-500"
|
||||
wsConnected ? "text-status-success" : "text-fg-muted"
|
||||
}`}
|
||||
>
|
||||
{wsConnected ? (
|
||||
@@ -566,7 +626,7 @@ export function Sidebar({ wsConnected, collapsed, onToggle }: SidebarProps) {
|
||||
)}
|
||||
</span>
|
||||
{!collapsed && (
|
||||
<span className="text-[11px] font-medium text-gray-600">{`v${__APP_VERSION__}`}</span>
|
||||
<span className="text-[11px] font-medium text-fg-muted">{`v${__APP_VERSION__}`}</span>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
@@ -579,15 +639,15 @@ export function Sidebar({ wsConnected, collapsed, onToggle }: SidebarProps) {
|
||||
aria-label={checkTitle}
|
||||
className={`relative w-8 h-8 mx-auto flex items-center justify-center rounded-lg border bg-surface-2 transition-colors disabled:opacity-60 ${
|
||||
updateAvailable
|
||||
? "border-emerald-500/40 text-emerald-400 hover:bg-emerald-500/10"
|
||||
? "border-status-success/40 text-status-success hover:bg-status-success/10"
|
||||
: checkError
|
||||
? "border-amber-500/40 text-amber-300 hover:bg-amber-500/10"
|
||||
: "border-border text-gray-400 hover:text-gray-200 hover:bg-surface-3"
|
||||
? "border-status-warning/40 text-status-warning hover:bg-status-warning/10"
|
||||
: "border-border text-fg-secondary hover:text-fg-primary hover:bg-surface-3"
|
||||
}`}
|
||||
>
|
||||
<RefreshCw className={`w-3.5 h-3.5 ${checking ? "animate-spin" : ""}`} aria-hidden />
|
||||
{updateAvailable && !checking && (
|
||||
<span className="absolute top-1 right-1 w-1.5 h-1.5 rounded-full bg-emerald-400" />
|
||||
<span className="absolute top-1 right-1 w-1.5 h-1.5 rounded-full bg-status-success" />
|
||||
)}
|
||||
</button>
|
||||
) : (
|
||||
@@ -598,10 +658,10 @@ export function Sidebar({ wsConnected, collapsed, onToggle }: SidebarProps) {
|
||||
title={checkTitle}
|
||||
className={`w-full rounded-lg border bg-surface-2 px-2.5 py-2 text-xs transition-colors disabled:opacity-60 flex items-center justify-between gap-2 ${
|
||||
updateAvailable
|
||||
? "border-emerald-500/40 text-emerald-300 hover:bg-emerald-500/10"
|
||||
? "border-status-success/40 text-status-success hover:bg-status-success/10"
|
||||
: checkError
|
||||
? "border-amber-500/40 text-amber-300 hover:bg-amber-500/10"
|
||||
: "border-border text-gray-300 hover:text-gray-100 hover:bg-surface-3"
|
||||
? "border-status-warning/40 text-status-warning hover:bg-status-warning/10"
|
||||
: "border-border text-fg-secondary hover:text-fg-primary hover:bg-surface-3"
|
||||
}`}
|
||||
>
|
||||
<span className="inline-flex items-center gap-2 truncate">
|
||||
@@ -612,7 +672,7 @@ export function Sidebar({ wsConnected, collapsed, onToggle }: SidebarProps) {
|
||||
<span className="font-medium truncate">{checkTitle}</span>
|
||||
</span>
|
||||
{updateAvailable && !checking && (
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 flex-shrink-0" />
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-status-success flex-shrink-0" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
@@ -741,8 +801,8 @@ function ConnectionStatusModal({
|
||||
<div
|
||||
className={`w-9 h-9 rounded-lg border flex items-center justify-center flex-shrink-0 ${
|
||||
wsConnected
|
||||
? "bg-emerald-500/10 border-emerald-500/30 text-emerald-400"
|
||||
: "bg-surface-3 border-border text-gray-400"
|
||||
? "bg-status-success/10 border-status-success/30 text-status-success"
|
||||
: "bg-surface-3 border-border text-fg-secondary"
|
||||
}`}
|
||||
>
|
||||
{wsConnected ? (
|
||||
@@ -754,19 +814,19 @@ function ConnectionStatusModal({
|
||||
<div className="min-w-0">
|
||||
<h2
|
||||
id="connection-status-title"
|
||||
className="text-base font-semibold text-gray-50 truncate tracking-tight leading-tight"
|
||||
className="text-base font-semibold text-fg-primary truncate tracking-tight leading-tight"
|
||||
>
|
||||
{t("nav:connectionDetails")}
|
||||
</h2>
|
||||
<p
|
||||
className={`text-[11px] font-medium inline-flex items-center gap-1.5 leading-tight ${
|
||||
wsConnected ? "text-emerald-400" : "text-gray-500"
|
||||
wsConnected ? "text-status-success" : "text-fg-muted"
|
||||
}`}
|
||||
>
|
||||
{wsConnected && (
|
||||
<span className="relative flex w-1.5 h-1.5">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75" />
|
||||
<span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-emerald-400" />
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-status-success opacity-75" />
|
||||
<span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-status-success" />
|
||||
</span>
|
||||
)}
|
||||
{wsConnected ? t("nav:live") : t("nav:disconnected")}
|
||||
@@ -777,7 +837,7 @@ function ConnectionStatusModal({
|
||||
type="button"
|
||||
onClick={close}
|
||||
aria-label={t("nav:close")}
|
||||
className="p-1.5 -m-1 rounded-lg text-gray-500 hover:text-gray-200 hover:bg-surface-4 transition-colors flex-shrink-0"
|
||||
className="p-1.5 -m-1 rounded-lg text-fg-muted hover:text-fg-secondary hover:bg-surface-4 transition-colors flex-shrink-0"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
@@ -835,7 +895,7 @@ function ConnectionStatusModal({
|
||||
{/* Top event types */}
|
||||
<Section title={t("nav:topEventTypes")} icon={BarChart3}>
|
||||
{topTypes.length === 0 ? (
|
||||
<p className="text-xs text-gray-500 italic">{t("nav:noEventsYet")}</p>
|
||||
<p className="text-xs text-fg-muted italic">{t("nav:noEventsYet")}</p>
|
||||
) : (
|
||||
<div className="space-y-1.5">
|
||||
{topTypes.map(([type, count]) => (
|
||||
@@ -848,7 +908,7 @@ function ConnectionStatusModal({
|
||||
{/* Recent activity */}
|
||||
<Section title={t("nav:recentActivity")} icon={Clock}>
|
||||
{recentEvents.length === 0 ? (
|
||||
<p className="text-xs text-gray-500 italic">{t("nav:noEventsYet")}</p>
|
||||
<p className="text-xs text-fg-muted italic">{t("nav:noEventsYet")}</p>
|
||||
) : (
|
||||
<ul className="space-y-1">
|
||||
{recentEvents.map((evt, i) => (
|
||||
@@ -856,8 +916,8 @@ function ConnectionStatusModal({
|
||||
key={`${evt.at}-${i}`}
|
||||
className="flex items-center justify-between gap-3 text-[11px] font-mono px-2 py-1 rounded bg-surface-2/50"
|
||||
>
|
||||
<span className="text-gray-200 truncate">{evt.type}</span>
|
||||
<span className="text-gray-500 flex-shrink-0">{formatRelative(evt.at, t)}</span>
|
||||
<span className="text-fg-secondary truncate">{evt.type}</span>
|
||||
<span className="text-fg-muted flex-shrink-0">{formatRelative(evt.at, t)}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -866,11 +926,11 @@ function ConnectionStatusModal({
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-2 px-5 py-3 border-t border-border bg-surface-2/40">
|
||||
<span className="text-[10px] text-gray-500">{t("nav:statsPersisted")}</span>
|
||||
<span className="text-[10px] text-fg-muted">{t("nav:statsPersisted")}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onResetStats}
|
||||
className="text-[11px] font-medium text-gray-400 hover:text-gray-100 hover:bg-surface-3 px-2 py-1 rounded transition-colors"
|
||||
className="text-[11px] font-medium text-fg-secondary hover:text-fg-primary hover:bg-surface-3 px-2 py-1 rounded transition-colors"
|
||||
>
|
||||
{t("nav:resetStats")}
|
||||
</button>
|
||||
@@ -896,7 +956,7 @@ function Section({
|
||||
<span className="w-5 h-5 rounded-md bg-accent/15 border border-accent/25 flex items-center justify-center flex-shrink-0">
|
||||
<Icon className="w-3 h-3 text-accent" aria-hidden />
|
||||
</span>
|
||||
<h3 className="text-[13px] font-semibold text-gray-100 tracking-tight">{title}</h3>
|
||||
<h3 className="text-[13px] font-semibold text-fg-primary tracking-tight">{title}</h3>
|
||||
</div>
|
||||
{children}
|
||||
</section>
|
||||
@@ -906,12 +966,12 @@ function Section({
|
||||
function KpiTile({ label, value, unit }: { label: string; value: string; unit: string }) {
|
||||
return (
|
||||
<div className="rounded-lg border border-border bg-surface-2 px-2.5 py-2">
|
||||
<div className="text-[9px] font-semibold uppercase tracking-wider text-gray-500 truncate">
|
||||
<div className="text-[9px] font-semibold uppercase tracking-wider text-fg-muted truncate">
|
||||
{label}
|
||||
</div>
|
||||
<div className="mt-0.5 flex items-baseline gap-1 truncate">
|
||||
<span className="text-base font-semibold text-gray-100 font-mono">{value}</span>
|
||||
<span className="text-[10px] font-medium text-gray-500 truncate">{unit}</span>
|
||||
<span className="text-base font-semibold text-fg-primary font-mono">{value}</span>
|
||||
<span className="text-[10px] font-medium text-fg-muted truncate">{unit}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -920,10 +980,10 @@ function KpiTile({ label, value, unit }: { label: string; value: string; unit: s
|
||||
function DetailRow({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
|
||||
return (
|
||||
<div className="flex items-start justify-between gap-3 text-xs">
|
||||
<span className="text-gray-500 font-medium uppercase tracking-wider text-[10px] pt-0.5">
|
||||
<span className="text-fg-muted font-medium uppercase tracking-wider text-[10px] pt-0.5">
|
||||
{label}
|
||||
</span>
|
||||
<span className={`text-gray-200 text-right break-all min-w-0 ${mono ? "font-mono" : ""}`}>
|
||||
<span className={`text-fg-secondary text-right break-all min-w-0 ${mono ? "font-mono" : ""}`}>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
@@ -946,8 +1006,8 @@ function TypeBar({
|
||||
return (
|
||||
<div className="text-[11px]">
|
||||
<div className="flex items-center justify-between gap-2 mb-0.5">
|
||||
<span className="font-mono text-gray-200 truncate">{type}</span>
|
||||
<span className="text-gray-500 flex-shrink-0 font-mono">
|
||||
<span className="font-mono text-fg-secondary truncate">{type}</span>
|
||||
<span className="text-fg-muted flex-shrink-0 font-mono">
|
||||
{count} · {sharePct}%
|
||||
</span>
|
||||
</div>
|
||||
@@ -1005,7 +1065,7 @@ function Sparkline({
|
||||
/>
|
||||
)}
|
||||
</svg>
|
||||
<div className="flex items-center justify-between mt-1.5 text-[10px] text-gray-500 font-mono">
|
||||
<div className="flex items-center justify-between mt-1.5 text-[10px] text-fg-muted font-mono">
|
||||
<span>−60s</span>
|
||||
<span>{avgLabel}</span>
|
||||
<span>{"now"}</span>
|
||||
|
||||
Reference in New Issue
Block a user