refactor(design): align typography, colors, and card surfaces to DESIGN.md (#859)

* refactor(design): align surface tokens to DESIGN.md §2

* refactor(design): canonicalize tracked-mono kickers and display rungs

* refactor(design): collapse to five-slot palette and align card surfaces
This commit is contained in:
Anso
2026-05-01 03:01:00 -04:00
committed by GitHub
parent 7663f4cd8b
commit f62716f557
41 changed files with 248 additions and 181 deletions
+1 -1
View File
@@ -597,7 +597,7 @@ function StreamRow({ entry, now }: StreamRowProps) {
))}
</div>
</div>
<div className="text-[10px] font-mono uppercase tracking-wider text-stat-subtitle pt-1 whitespace-nowrap">
<div className="text-[10px] leading-3 font-mono uppercase tracking-[0.18em] text-stat-subtitle pt-1 whitespace-nowrap">
{entry.method} {entry.path.split('?')[0]}
</div>
</div>
@@ -86,7 +86,7 @@ function formatClock(ts: number | null): string {
function RiskBadge({ bump, blocked }: { bump: SemverBump; blocked: boolean }) {
if (blocked || bump === 'major') {
return (
<span className="inline-flex items-center gap-1.5 rounded-full border border-destructive/40 bg-destructive/10 px-2.5 py-0.5 font-mono text-[10px] uppercase tracking-[0.14em] text-destructive">
<span className="inline-flex items-center gap-1.5 rounded-full border border-destructive/40 bg-destructive/10 px-2.5 py-0.5 font-mono text-[10px] leading-3 uppercase tracking-[0.18em] text-destructive">
<ShieldAlert className="h-3 w-3" strokeWidth={1.5} />
Blocked · major
</span>
@@ -94,7 +94,7 @@ function RiskBadge({ bump, blocked }: { bump: SemverBump; blocked: boolean }) {
}
if (bump === 'minor') {
return (
<span className="inline-flex items-center gap-1.5 rounded-full border border-warning/40 bg-warning/10 px-2.5 py-0.5 font-mono text-[10px] uppercase tracking-[0.14em] text-warning">
<span className="inline-flex items-center gap-1.5 rounded-full border border-warning/40 bg-warning/10 px-2.5 py-0.5 font-mono text-[10px] leading-3 uppercase tracking-[0.18em] text-warning">
<AlertTriangle className="h-3 w-3" strokeWidth={1.5} />
Review · minor
</span>
@@ -102,7 +102,7 @@ function RiskBadge({ bump, blocked }: { bump: SemverBump; blocked: boolean }) {
}
if (bump === 'patch') {
return (
<span className="inline-flex items-center gap-1.5 rounded-full border border-success/40 bg-success/10 px-2.5 py-0.5 font-mono text-[10px] uppercase tracking-[0.14em] text-success">
<span className="inline-flex items-center gap-1.5 rounded-full border border-success/40 bg-success/10 px-2.5 py-0.5 font-mono text-[10px] leading-3 uppercase tracking-[0.18em] text-success">
<Shield className="h-3 w-3" strokeWidth={1.5} />
Safe · patch
</span>
@@ -110,13 +110,13 @@ function RiskBadge({ bump, blocked }: { bump: SemverBump; blocked: boolean }) {
}
if (bump === 'unknown') {
return (
<span className="inline-flex items-center gap-1.5 rounded-full border border-card-border bg-muted/30 px-2.5 py-0.5 font-mono text-[10px] uppercase tracking-[0.14em] text-stat-subtitle">
<span className="inline-flex items-center gap-1.5 rounded-full border border-card-border bg-muted/30 px-2.5 py-0.5 font-mono text-[10px] leading-3 uppercase tracking-[0.18em] text-stat-subtitle">
Digest rebuild
</span>
);
}
return (
<span className="inline-flex items-center gap-1.5 rounded-full border border-card-border bg-muted/30 px-2.5 py-0.5 font-mono text-[10px] uppercase tracking-[0.14em] text-stat-subtitle">
<span className="inline-flex items-center gap-1.5 rounded-full border border-card-border bg-muted/30 px-2.5 py-0.5 font-mono text-[10px] leading-3 uppercase tracking-[0.18em] text-stat-subtitle">
None
</span>
);
@@ -164,7 +164,7 @@ function StackReadinessCard({
</div>
<div className="flex items-center gap-2 shrink-0">
{!autoUpdateEnabled && (
<span className="inline-flex items-center gap-1.5 rounded-full border border-card-border bg-muted/30 px-2.5 py-0.5 font-mono text-[10px] uppercase tracking-[0.14em] text-stat-subtitle">
<span className="inline-flex items-center gap-1.5 rounded-full border border-card-border bg-muted/30 px-2.5 py-0.5 font-mono text-[10px] leading-3 uppercase tracking-[0.18em] text-stat-subtitle">
<CircleSlash className="h-3 w-3" strokeWidth={1.5} />
Auto: Off
</span>
@@ -188,7 +188,7 @@ function StackReadinessCard({
{p.summary.update_kind === 'digest' ? (
<div className="flex items-baseline gap-2 font-mono text-sm">
<span className="text-stat-subtitle">{p.summary.current_tag}</span>
<span className="text-brand text-[11px] uppercase tracking-[0.16em]">
<span className="text-brand text-[10px] leading-3 uppercase tracking-[0.18em]">
Rebuild available
</span>
</div>
+2 -10
View File
@@ -2,6 +2,7 @@ import { useEffect, useRef, useState, useCallback } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from './ui/dialog';
import { Terminal as TerminalIcon } from 'lucide-react';
import { loadXtermModules, type Terminal, type FitAddon, type XtermModules } from '@/lib/xtermLoader';
import { buildXtermMinimalTheme } from '@/lib/terminalTheme';
type TerminalContainer = HTMLDivElement & { __resizeObserver?: ResizeObserver };
@@ -91,17 +92,8 @@ export default function BashExecModal({ isOpen, onClose, containerId, containerN
});
function initTerminal(containerEl: HTMLDivElement, mods: XtermModules) {
// xterm.js requires literal color strings in its theme config; CSS variables
// and oklch() are not supported by the canvas renderer. These values are
// intentionally hardcoded to match the terminal well aesthetic.
const term = new mods.Terminal({
theme: {
background: '#0a0a0a',
foreground: '#d4d4d4',
cursor: '#ffffff',
cursorAccent: '#000000',
selectionBackground: 'rgba(255, 255, 255, 0.3)',
},
theme: buildXtermMinimalTheme(),
fontFamily: "'Geist Mono', monospace",
fontSize: 14,
cursorBlink: true,
+5 -5
View File
@@ -2680,7 +2680,7 @@ export default function EditorLayout() {
const sparkStroke = health === 'unhealthy' ? 'var(--destructive)' : health === 'starting' ? 'var(--warning)' : 'var(--chart-1)';
return (
<div key={container?.Id || Math.random()} className="rounded-lg border border-muted bg-muted/30 px-3 py-2.5">
<div key={container?.Id || Math.random()} className="rounded-lg border border-card-border border-t-card-border-top bg-card shadow-card-bevel px-3 py-2.5">
<div className="flex items-start justify-between gap-4">
<div className="flex items-start gap-3 min-w-0 flex-1">
<div className={cn('mt-1 inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-[10px] font-bold', badgeClass)}>
@@ -2771,7 +2771,7 @@ export default function EditorLayout() {
<div className="mt-2 grid grid-cols-3 gap-2">
<div className="flex items-center gap-2 rounded-md bg-background/60 px-2 py-1.5">
<div className="flex flex-col">
<span className="font-mono text-[9px] uppercase tracking-wide text-stat-subtitle">cpu</span>
<span className="font-mono text-[10px] leading-3 uppercase tracking-[0.18em] text-stat-subtitle">cpu</span>
<span className="font-mono text-xs tabular-nums text-foreground">{stats?.cpu ?? '-'}</span>
</div>
<div className="ml-auto h-5 w-16">
@@ -2780,7 +2780,7 @@ export default function EditorLayout() {
</div>
<div className="flex items-center gap-2 rounded-md bg-background/60 px-2 py-1.5">
<div className="flex flex-col">
<span className="font-mono text-[9px] uppercase tracking-wide text-stat-subtitle">mem</span>
<span className="font-mono text-[10px] leading-3 uppercase tracking-[0.18em] text-stat-subtitle">mem</span>
<span className="font-mono text-xs tabular-nums text-foreground">{stats?.ram ?? '-'}</span>
</div>
<div className="ml-auto h-5 w-16">
@@ -2789,7 +2789,7 @@ export default function EditorLayout() {
</div>
<div className="flex items-center gap-2 rounded-md bg-background/60 px-2 py-1.5">
<div className="flex flex-col">
<span className="font-mono text-[9px] uppercase tracking-wide text-stat-subtitle">net i/o</span>
<span className="font-mono text-[10px] leading-3 uppercase tracking-[0.18em] text-stat-subtitle">net i/o</span>
<span className="font-mono text-xs tabular-nums text-foreground">{stats?.net ?? '-'}</span>
</div>
<div className="ml-auto h-5 w-16">
@@ -2964,7 +2964,7 @@ export default function EditorLayout() {
) : (
<>
{activeTab === 'env' && (
<div className="bg-info-muted border-b border-info/20 px-4 py-2 flex items-center gap-2 text-xs text-info">
<div className="bg-brand/8 border-b border-brand/20 px-4 py-2 flex items-center gap-2 text-xs text-brand">
<span>
Variables defined here are automatically available for substitution in your compose.yaml (e.g., <code className="bg-background px-1 rounded text-[10px]">${'{}'}VAR</code>). To pass them directly into your container, you must add <code className="bg-background px-1 rounded text-[10px]">env_file: - .env</code> to your service definition.
</span>
+15 -16
View File
@@ -46,7 +46,7 @@ function labelPaletteKey(name: string, color: LabelColor): string {
return `${name.trim().toLowerCase()}|${color}`;
}
const FILTER_SECTION_LABEL_CLASS = 'text-[11px] font-medium uppercase tracking-wider text-muted-foreground';
const FILTER_SECTION_LABEL_CLASS = 'text-[10px] leading-3 font-mono uppercase tracking-[0.18em] text-stat-subtitle';
import { MultiSelectCombobox } from '@/components/ui/multi-select-combobox';
import { formatVersion } from '@/lib/version';
import { CursorProvider, Cursor, CursorFollow, CursorContainer } from '@/components/animate-ui/primitives/animate/cursor';
@@ -300,7 +300,7 @@ function UpdateStatusBadge({ status, error, onRetry, onDismiss }: {
onDismiss?: () => void;
}) {
if (status === 'updating') return (
<Badge className="text-[10px] px-1.5 py-0 h-4 bg-info/15 text-info border-info/30 shrink-0">
<Badge className="text-[10px] px-1.5 py-0 h-4 bg-brand/15 text-brand border-brand/30 shrink-0">
<Loader2 className="w-2.5 h-2.5 mr-0.5 animate-spin" /> Updating
</Badge>
);
@@ -536,18 +536,18 @@ function NodeCard({ node, onNavigate, labelMap, updateStatus, onUpdate, updating
{/* Container Stats */}
{isOnline && node.stats && (
<div className="grid grid-cols-3 gap-2 mb-3">
<div className="bg-muted/50 rounded-lg px-2.5 py-2 text-center">
<div className="text-lg font-medium leading-none tabular-nums">{node.stats.active}</div>
<div className="text-[10px] text-muted-foreground mt-1">Running</div>
<div className="grid grid-cols-3 mb-3 rounded-md border border-card-border overflow-hidden">
<div className="border-r border-card-border bg-card px-2.5 py-2 text-center">
<div className="text-lg font-medium leading-none tabular-nums text-stat-value">{node.stats.active}</div>
<div className="text-[10px] leading-3 font-mono uppercase tracking-[0.18em] text-stat-subtitle mt-1">Running</div>
</div>
<div className="bg-muted/50 rounded-lg px-2.5 py-2 text-center">
<div className="text-lg font-medium leading-none tabular-nums">{node.stats.exited}</div>
<div className="text-[10px] text-muted-foreground mt-1">Stopped</div>
<div className="border-r border-card-border bg-card px-2.5 py-2 text-center">
<div className="text-lg font-medium leading-none tabular-nums text-stat-value">{node.stats.exited}</div>
<div className="text-[10px] leading-3 font-mono uppercase tracking-[0.18em] text-stat-subtitle mt-1">Stopped</div>
</div>
<div className="bg-muted/50 rounded-lg px-2.5 py-2 text-center">
<div className="text-lg font-medium leading-none tabular-nums">{node.stacks?.length ?? '-'}</div>
<div className="text-[10px] text-muted-foreground mt-1">Stacks</div>
<div className="bg-card px-2.5 py-2 text-center">
<div className="text-lg font-medium leading-none tabular-nums text-stat-value">{node.stacks?.length ?? '-'}</div>
<div className="text-[10px] leading-3 font-mono uppercase tracking-[0.18em] text-stat-subtitle mt-1">Stacks</div>
</div>
</div>
)}
@@ -571,7 +571,7 @@ function NodeCard({ node, onNavigate, labelMap, updateStatus, onUpdate, updating
</span>
<span className="font-medium">{formatBytes(node.systemStats.memory.used)} / {formatBytes(node.systemStats.memory.total)}</span>
</div>
<UsageBar percent={memPercent} color={memPercent > 80 ? 'bg-destructive/80' : memPercent > 60 ? 'bg-warning' : 'bg-info'} />
<UsageBar percent={memPercent} color={memPercent > 80 ? 'bg-destructive/80' : memPercent > 60 ? 'bg-warning' : 'bg-brand/60'} />
</div>
{node.systemStats.disk && (
<div>
@@ -1461,7 +1461,7 @@ export function FleetView({ onNavigateToNode }: FleetViewProps) {
<div className="rounded-lg border border-card-border border-t-card-border-top bg-card shadow-card-bevel px-3 py-2 text-center">
<div className="text-lg font-medium tabular-nums tracking-tight text-stat-value">{updating}</div>
<div className="text-[10px] text-stat-subtitle flex items-center justify-center gap-1">
<Loader2 className="w-3 h-3 text-info" strokeWidth={1.5} /> Updating
<Loader2 className="w-3 h-3 text-brand" strokeWidth={1.5} /> Updating
</div>
</div>
<div className="rounded-lg border border-card-border border-t-card-border-top bg-card shadow-card-bevel px-3 py-2 text-center">
@@ -1491,7 +1491,7 @@ export function FleetView({ onNavigateToNode }: FleetViewProps) {
</div>
{/* Table header */}
<div className="grid grid-cols-[1fr_80px_100px_100px_120px] gap-2 px-3 text-[10px] text-muted-foreground uppercase tracking-wider">
<div className="grid grid-cols-[1fr_80px_100px_100px_120px] gap-2 px-3 text-[10px] leading-3 font-mono text-stat-subtitle uppercase tracking-[0.18em]">
<span>Node</span>
<span>Type</span>
<span>Current</span>
@@ -1504,7 +1504,6 @@ export function FleetView({ onNavigateToNode }: FleetViewProps) {
<div className="space-y-1">
{filtered.map(s => (
<div key={s.nodeId} className="grid grid-cols-[1fr_80px_100px_100px_120px] gap-2 items-center rounded-lg border border-card-border border-t-card-border-top bg-card shadow-card-bevel px-3 py-2">
{/* Node name */}
<div className="flex items-center gap-2.5 min-w-0">
<div className={`flex items-center justify-center w-6 h-6 rounded-md shrink-0 ${s.updateAvailable && !s.updateStatus ? 'bg-warning/10' : 'bg-muted'}`}>
{s.type === 'local'
+2 -12
View File
@@ -3,6 +3,7 @@ import { ArrowLeft, Copy, Trash2, Download, RefreshCw } from 'lucide-react';
import { Button } from './ui/button';
import { PageMasthead, type MastheadTone } from './ui/PageMasthead';
import { loadXtermModules, type Terminal, type FitAddon, type SerializeAddon } from '@/lib/xtermLoader';
import { buildXtermMinimalTheme } from '@/lib/terminalTheme';
import { useNodes } from '@/context/NodeContext';
import { copyToClipboard } from '@/lib/clipboard';
@@ -14,17 +15,6 @@ interface HostConsoleProps {
// Window considered "live" for the masthead pulsing dot.
const LIVE_WINDOW_MS = 5_000;
/** Build the xterm theme from CSS custom properties (resolved once per call). */
function getTerminalTheme() {
const s = getComputedStyle(document.documentElement);
return {
background: s.getPropertyValue('--terminal-bg').trim(),
foreground: s.getPropertyValue('--terminal-fg').trim(),
cursor: s.getPropertyValue('--terminal-cursor').trim(),
cursorAccent: s.getPropertyValue('--terminal-cursor-accent').trim(),
selectionBackground: s.getPropertyValue('--terminal-selection').trim(),
};
}
function formatUptime(ms: number): string {
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
@@ -75,7 +65,7 @@ export default function HostConsole({ stackName, onClose }: HostConsoleProps) {
if (!mounted) return;
const term = new mods.Terminal({
theme: getTerminalTheme(),
theme: buildXtermMinimalTheme(),
fontFamily: "'Geist Mono', monospace",
fontSize: 14,
cursorBlink: true,
+1 -1
View File
@@ -67,7 +67,7 @@ export function LogViewer({ containerId, containerName, isOpen, onClose }: LogVi
<div
ref={scrollRef}
className="flex-1 w-full bg-[#0c0c0c] text-green-400 p-4 rounded-md overflow-y-auto font-mono text-xs mt-2"
className="flex-1 w-full bg-[var(--terminal-bg)] text-success p-4 rounded-md overflow-y-auto font-mono text-xs mt-2"
>
{logs.length === 0 && !isConnected ? (
<div className="text-muted-foreground">Connecting to container stream...</div>
+3 -3
View File
@@ -285,7 +285,7 @@ export function NodeManager() {
const getStatusBadge = (status: string) => {
switch (status) {
case 'online':
return <Badge variant="default" className="bg-green-600 text-white gap-1"><Wifi className="w-3 h-3" /> Online</Badge>;
return <Badge variant="default" className="bg-success text-success-foreground gap-1"><Wifi className="w-3 h-3" /> Online</Badge>;
case 'offline':
return <Badge variant="destructive" className="gap-1"><WifiOff className="w-3 h-3" /> Offline</Badge>;
default:
@@ -458,7 +458,7 @@ export function NodeManager() {
<div className="flex items-start justify-between gap-4">
<div>
<p className="text-sm font-medium flex items-center gap-2">
<KeyRound className="w-4 h-4 text-blue-500" />
<KeyRound className="w-4 h-4 text-brand" />
Generate Node Token
</p>
<p className="text-xs text-muted-foreground mt-1">
@@ -586,7 +586,7 @@ export function NodeManager() {
return (
<div className="flex items-center gap-1.5">
{summary?.auto_update_enabled ? (
<Badge variant="outline" className="text-info border-info/30 gap-1 text-xs">
<Badge variant="outline" className="text-stat-subtitle border-card-border gap-1 text-xs">
<RefreshCw className="w-3 h-3" strokeWidth={1.5} />
Auto
</Badge>
+2 -2
View File
@@ -65,7 +65,7 @@ export function NodeSwitcher({ onManageNodes }: NodeSwitcherProps) {
<div className="font-mono text-[10px] uppercase tracking-[0.18em] text-stat-subtitle">
Node · {kickerType}
</div>
<div className="mt-0.5 truncate font-display text-base italic leading-none text-stat-value">
<div className="mt-0.5 truncate font-sans text-sm leading-none text-stat-value">
{activeNode?.name ?? (isLoading ? '—' : 'No node')}
</div>
</div>
@@ -104,7 +104,7 @@ export function NodeSwitcher({ onManageNodes }: NodeSwitcherProps) {
<span className="font-display text-xl italic leading-none text-stat-value">
Connected
</span>
<span className="font-mono text-[11px] uppercase tracking-[0.14em] tabular-nums text-brand">
<span className="font-mono text-[10px] leading-3 uppercase tracking-[0.18em] tabular-nums text-brand">
{nodes.length} nodes
</span>
</div>
@@ -37,7 +37,7 @@ type LevelConfig = {
};
const LEVEL_CONFIG: Record<NotificationItem['level'], LevelConfig> = {
info: { icon: Info, iconClass: 'text-info', railClass: 'bg-info' },
info: { icon: Info, iconClass: 'text-brand', railClass: 'bg-brand' },
warning: { icon: AlertTriangle, iconClass: 'text-warning', railClass: 'bg-warning' },
error: { icon: AlertOctagon, iconClass: 'text-destructive', railClass: 'bg-destructive' },
};
@@ -220,7 +220,7 @@ export function NotificationPanel({
Notifications
</span>
{unreadCount > 0 ? (
<span className="font-mono text-[11px] uppercase tracking-[0.14em] tabular-nums text-brand">
<span className="font-mono text-[10px] leading-3 uppercase tracking-[0.18em] tabular-nums text-brand">
{unreadCount} unread
</span>
) : null}
+5 -5
View File
@@ -165,7 +165,7 @@ function FilterToggle({ value, onChange, counts }: FilterToggleProps) {
{label}
<span className={cn(
'inline-flex items-center justify-center h-4 min-w-4 px-1 rounded-sm text-[10px] font-mono transition-colors duration-200',
value === key ? 'bg-muted text-foreground' : 'text-muted-foreground/70',
value === key ? 'bg-muted text-foreground' : 'text-stat-subtitle',
)}>
{count}
</span>
@@ -214,7 +214,7 @@ function ManagedBadge({ status, managedBy }: {
const SEVERITY_BADGE_CLASSES: Record<VulnSeverity | 'CLEAN', string> = {
CRITICAL: 'border-destructive/25 bg-destructive/8 text-destructive',
HIGH: 'border-warning/25 bg-warning/8 text-warning',
MEDIUM: 'border-info/25 bg-info/8 text-info',
MEDIUM: 'border-warning/25 bg-warning/8 text-warning',
LOW: 'border-border bg-muted/30 text-muted-foreground',
UNKNOWN: 'border-border bg-muted/20 text-muted-foreground',
CLEAN: 'border-success/25 bg-success/8 text-success',
@@ -223,7 +223,7 @@ const SEVERITY_BADGE_CLASSES: Record<VulnSeverity | 'CLEAN', string> = {
const SEVERITY_DOT_CLASSES: Record<VulnSeverity | 'CLEAN', string> = {
CRITICAL: 'bg-destructive',
HIGH: 'bg-warning',
MEDIUM: 'bg-info',
MEDIUM: 'bg-warning',
LOW: 'bg-muted-foreground/60',
UNKNOWN: 'bg-muted-foreground/40',
CLEAN: 'bg-success',
@@ -275,7 +275,7 @@ function SeverityBadge({ summary, onClick }: { summary: ScanSummary; onClick: ()
<div className="flex gap-3 mt-1">
{summary.critical > 0 && <span className="text-destructive">{summary.critical}C</span>}
{summary.high > 0 && <span className="text-warning">{summary.high}H</span>}
{summary.medium > 0 && <span className="text-info">{summary.medium}M</span>}
{summary.medium > 0 && <span className="text-warning">{summary.medium}M</span>}
{summary.low > 0 && <span className="text-muted-foreground">{summary.low}L</span>}
</div>
)}
@@ -752,7 +752,7 @@ export default function ResourcesView() {
target="images"
icon={<PackageMinus className="w-6 h-6" />}
label="Prune Unused Images"
accentClass="text-info"
accentClass="text-brand"
onManaged={() => setConfirmPrune({ target: 'images', scope: 'managed' })}
onAll={() => setConfirmPrune({ target: 'images', scope: 'all' })}
/>
+4 -4
View File
@@ -202,7 +202,7 @@ export function StackAlertSheet({ isOpen, onClose, stackName }: StackAlertSheetP
const renderAgentStatusBanner = () => {
if (agentStatus.loading) {
return (
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted/50 border text-sm text-muted-foreground">
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted/50 border text-sm text-stat-subtitle">
<Loader2 className="h-4 w-4 animate-spin shrink-0" />
<span>Checking notification channels...</span>
</div>
@@ -211,10 +211,10 @@ export function StackAlertSheet({ isOpen, onClose, stackName }: StackAlertSheetP
if (isRemote) {
return (
<div className="flex items-start gap-2 p-3 rounded-lg bg-info-muted border border-info/20 text-sm">
<Info className="h-4 w-4 text-info shrink-0 mt-0.5" strokeWidth={1.5} />
<div className="flex items-start gap-2 p-3 rounded-lg bg-brand/8 border border-brand/20 text-sm">
<Info className="h-4 w-4 text-brand shrink-0 mt-0.5" strokeWidth={1.5} />
<div className="space-y-0.5">
<p className="font-medium text-info">
<p className="font-medium text-brand">
Remote node: <span className="font-medium">{activeNode?.name}</span>
</p>
<p className="text-muted-foreground">
+2 -21
View File
@@ -3,6 +3,7 @@ import { Button } from './ui/button';
import { Input } from './ui/input';
import { Download, Search, ChevronUp, ChevronDown, X } from 'lucide-react';
import { loadXtermModules, type Terminal, type FitAddon, type SearchAddon, type SerializeAddon } from '@/lib/xtermLoader';
import { buildXtermTheme } from '@/lib/terminalTheme';
interface TerminalComponentProps {
stackName?: string;
@@ -60,27 +61,7 @@ export default function TerminalComponent({ stackName, onReady, onMessage }: Ter
cursorBlink: true,
convertEol: true,
allowProposedApi: true,
theme: {
background: '#0d1117',
foreground: '#e6edf3',
cursor: '#58a6ff',
black: '#484f58',
red: '#ff7b72',
green: '#3fb950',
yellow: '#d29922',
blue: '#58a6ff',
magenta: '#bc8cff',
cyan: '#39c5cf',
white: '#b1bac4',
brightBlack: '#6e7681',
brightRed: '#ffa198',
brightGreen: '#56d364',
brightYellow: '#e3b341',
brightBlue: '#79c0ff',
brightMagenta: '#d2a8ff',
brightCyan: '#56d4dd',
brightWhite: '#ffffff',
},
theme: buildXtermTheme(),
fontFamily: "'Geist Mono', monospace",
fontSize: 13,
scrollback: 10000,
@@ -80,7 +80,7 @@ export function UserProfileDropdown({ theme, setTheme, onOpenSettings }: UserPro
<Button
variant="outline"
size="icon"
className="h-9 w-9 rounded-full p-0 font-mono text-[11px] font-semibold uppercase tracking-wider"
className="h-9 w-9 rounded-full p-0 font-mono text-[10px] font-semibold uppercase tracking-[0.18em]"
title="Profile"
aria-label="Profile"
>
@@ -99,7 +99,7 @@ export function UserProfileDropdown({ theme, setTheme, onOpenSettings }: UserPro
<div className="relative flex items-center gap-3 px-[var(--density-row-x)] py-[var(--density-tile-y)]">
<div className="flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-full border border-brand/25 bg-brand/10">
{initials ? (
<span className="font-display text-lg leading-none text-brand">
<span className="font-mono text-sm leading-none text-brand">
{initials}
</span>
) : (
@@ -114,7 +114,7 @@ export function UserProfileDropdown({ theme, setTheme, onOpenSettings }: UserPro
{roleLabel ? (
<span
className={cn(
'rounded-sm px-1.5 py-0.5 font-mono text-[9px] font-semibold uppercase tracking-[0.14em]',
'rounded-sm px-1.5 py-0.5 font-mono text-[10px] leading-3 font-semibold uppercase tracking-[0.18em]',
isAdmin
? 'bg-brand/10 text-brand'
: 'bg-muted text-stat-subtitle',
@@ -81,9 +81,9 @@ const PAGE_SIZE = 25;
const SEVERITY_CLASSES: Record<VulnSeverity, string> = {
CRITICAL: 'text-destructive border-destructive/40 bg-destructive/10',
HIGH: 'text-warning border-warning/40 bg-warning/10',
MEDIUM: 'text-info border-info/40 bg-info/10',
LOW: 'text-muted-foreground border-border bg-muted/30',
UNKNOWN: 'text-muted-foreground border-border bg-muted/20',
MEDIUM: 'text-warning border-warning/40 bg-warning/10',
LOW: 'text-stat-subtitle border-border bg-muted/30',
UNKNOWN: 'text-stat-subtitle border-border bg-muted/20',
};
function SeverityChip({ severity }: { severity: VulnSeverity }) {
@@ -80,7 +80,7 @@ function SectionHeader({ icon: Icon, label }: { icon: typeof Bell; label: string
return (
<div className="flex items-center gap-1.5 pt-2 pb-0.5 first:pt-0">
<Icon className="h-3 w-3 text-stat-icon shrink-0" strokeWidth={1.5} />
<span className="text-[10px] font-mono tracking-widest uppercase text-stat-icon">{label}</span>
<span className="text-[10px] leading-3 font-mono tracking-[0.18em] uppercase text-stat-icon">{label}</span>
</div>
);
}
@@ -127,7 +127,7 @@ export function HealthStatusBar({
<span className={`font-display italic text-3xl leading-none tracking-tight ${config.textClass}`}>
{config.label}
</span>
<span className="font-mono text-[11px] uppercase tracking-[0.18em] text-stat-subtitle">
<span className="font-mono text-[10px] leading-3 uppercase tracking-[0.18em] text-stat-subtitle">
{metaLine}
</span>
{reasonsLine ? (
@@ -192,7 +192,7 @@ export function HealthStatusBar({
>
{unreadAlerts}
</span>
<span className="font-mono text-[11px] uppercase tracking-[0.18em] text-stat-subtitle">
<span className="font-mono text-[10px] leading-3 uppercase tracking-[0.18em] text-stat-subtitle">
{unreadAlerts === 1 ? 'alert' : 'alerts'}
</span>
</div>
@@ -20,9 +20,9 @@ type IconType = typeof Activity;
const CATEGORY_ICONS: Record<string, { icon: IconType; className: string }> = {
deploy_failure: { icon: ServerCrash, className: 'text-destructive' },
deploy_success: { icon: CheckCircle2, className: 'text-success' },
image_update_applied: { icon: RefreshCw, className: 'text-info' },
image_update_applied: { icon: RefreshCw, className: 'text-brand' },
image_update_available: { icon: RefreshCw, className: 'text-warning' },
auto_heal_restarted: { icon: RotateCcw, className: 'text-info' },
auto_heal_restarted: { icon: RotateCcw, className: 'text-brand' },
auto_heal_failed: { icon: AlertOctagon, className: 'text-destructive' },
auto_heal_policy_disabled: { icon: AlertTriangle, className: 'text-warning' },
scan_finding: { icon: Search, className: 'text-warning' },
@@ -31,7 +31,7 @@ const CATEGORY_ICONS: Record<string, { icon: IconType; className: string }> = {
};
const LEVEL_ICONS: Record<string, { icon: IconType; className: string }> = {
info: { icon: Info, className: 'text-info' },
info: { icon: Info, className: 'text-stat-subtitle' },
warning: { icon: AlertTriangle, className: 'text-warning' },
error: { icon: AlertOctagon, className: 'text-destructive' },
};
@@ -16,7 +16,7 @@ interface RecentAlertsProps {
const PAGE_SIZE = 8;
const levelConfig: Record<string, { icon: typeof Info; className: string }> = {
info: { icon: Info, className: 'text-info' },
info: { icon: Info, className: 'text-brand' },
warning: { icon: AlertTriangle, className: 'text-warning' },
error: { icon: AlertOctagon, className: 'text-destructive' },
};
@@ -210,7 +210,7 @@ export function StackHealthTable({
className={`grid ${GRID_TEMPLATE} cursor-pointer items-center gap-4 px-[var(--density-row-x)] py-[var(--density-row-y)] transition-colors hover:bg-accent/5 ${rowTint[row.state]}`}
>
<span className={`h-1.5 w-1.5 rounded-full justify-self-center ${stateDot[row.state]}`} aria-hidden="true" />
<span className="truncate font-mono text-[13px] text-stat-value">{row.name}</span>
<span className="truncate font-mono text-sm text-stat-value">{row.name}</span>
<span className="truncate font-mono text-xs text-stat-subtitle">{activeNodeName}</span>
<span className="text-right font-mono text-xs tabular-nums text-stat-subtitle">
{row.runningSince !== null
+1 -1
View File
@@ -253,7 +253,7 @@ export function FileViewer({
<span className="font-mono text-xs text-stat-subtitle truncate">{filename}</span>
<div className="flex items-center gap-2 shrink-0">
{readOnly && (
<span className="text-[10px] uppercase tracking-widest text-muted-foreground border border-border rounded px-1.5 py-0.5">
<span className="text-[10px] leading-3 uppercase tracking-[0.18em] text-stat-subtitle border border-border rounded px-1.5 py-0.5">
Read-only
</span>
)}
@@ -19,7 +19,7 @@ interface FleetNodeConfiguration {
function TierChip({ tier }: { tier: string }) {
const label = tier === 'admiral' ? 'Admiral' : 'Skipper';
return (
<span className="inline-flex items-center rounded-sm border border-warning/30 bg-warning/10 px-1.5 py-0.5 text-[10px] font-mono tracking-wide uppercase text-warning/80">
<span className="inline-flex items-center rounded-sm border border-warning/30 bg-warning/10 px-1.5 py-0.5 text-[10px] leading-3 font-mono tracking-[0.18em] uppercase text-warning/80">
{label}
</span>
);
@@ -120,7 +120,7 @@ export function FleetMasthead({
<span className={`font-display italic text-3xl leading-none tracking-tight ${config.textClass}`}>
{config.label}
</span>
<span className="font-mono text-[11px] uppercase tracking-[0.18em] text-stat-subtitle">
<span className="font-mono text-[10px] leading-3 uppercase tracking-[0.18em] text-stat-subtitle">
{metaLine}
</span>
{reasonsLine ? (
@@ -191,7 +191,7 @@ export function FleetMasthead({
>
{criticalCount}
</span>
<span className="font-mono text-[11px] uppercase tracking-[0.18em] text-stat-subtitle">
<span className="font-mono text-[10px] leading-3 uppercase tracking-[0.18em] text-stat-subtitle">
{criticalCount === 1 ? 'alert' : 'alerts'}
</span>
</div>
@@ -70,9 +70,9 @@ export function MeshActivitySheet({ open, onOpenChange }: Props) {
<div key={i} className="grid grid-cols-[80px_70px_120px_1fr] gap-2 text-[11px] font-mono py-1 border-b border-card-border/50">
<span className="text-stat-subtitle tabular-nums">{new Date(e.ts).toLocaleTimeString()}</span>
<span className={
e.level === 'error' ? 'text-destructive uppercase tracking-wide' :
e.level === 'warn' ? 'text-warning uppercase tracking-wide' :
'text-stat-subtitle uppercase tracking-wide'
e.level === 'error' ? 'text-destructive uppercase tracking-[0.18em]' :
e.level === 'warn' ? 'text-warning uppercase tracking-[0.18em]' :
'text-stat-subtitle uppercase tracking-[0.18em]'
}>{e.source}</span>
<span className="text-stat-value">{e.type}</span>
<span className="text-stat-value truncate" title={e.message}>{e.alias ? `[${e.alias}] ` : ''}{e.message}</span>
@@ -97,7 +97,7 @@ export function MeshDiagnosticsSheet({ open, onOpenChange, nodeId, nodeName }: P
</div>
<div>
<div className="text-[10px] tracking-wide uppercase text-stat-subtitle font-mono mb-2">Active streams</div>
<div className="text-[10px] tracking-[0.18em] uppercase text-stat-subtitle font-mono mb-2">Active streams</div>
{(!diag || diag.activeStreams.length === 0) && (
<div className="text-xs text-stat-subtitle">No active streams.</div>
)}
@@ -112,7 +112,7 @@ export function MeshDiagnosticsSheet({ open, onOpenChange, nodeId, nodeName }: P
</div>
<div>
<div className="text-[10px] tracking-wide uppercase text-stat-subtitle font-mono mb-2">Resolver cache</div>
<div className="text-[10px] tracking-[0.18em] uppercase text-stat-subtitle font-mono mb-2">Resolver cache</div>
{(!diag || diag.aliasCache.length === 0) && (
<div className="text-xs text-stat-subtitle">No aliases registered.</div>
)}
@@ -104,7 +104,7 @@ export function MeshOptInSheet({ open, onOpenChange, nodeId, nodeName, onChanged
</div>
{pendingStack === stack.name && <Loader2 className="w-3 h-3 animate-spin text-stat-subtitle" />}
{stack.optedIn && pendingStack !== stack.name && (
<span className="text-[10px] tracking-wide uppercase text-success/80 font-mono">in mesh</span>
<span className="text-[10px] leading-3 tracking-[0.18em] uppercase text-success/80 font-mono">in mesh</span>
)}
</div>
))}
@@ -72,7 +72,7 @@ export function MeshRouteDetailSheet({ open, onOpenChange, alias }: Props) {
<div className="mt-4 space-y-4">
<div className="flex items-center gap-2">
<span className={`inline-flex items-center px-2 py-0.5 rounded-sm border text-[10px] font-mono uppercase tracking-wide ${pill.toneClass}`}>
<span className={`inline-flex items-center px-2 py-0.5 rounded-sm border text-[10px] leading-3 font-mono uppercase tracking-[0.18em] ${pill.toneClass}`}>
{pill.label}
</span>
{diag?.lastProbeMs != null && (
@@ -95,7 +95,7 @@ export function MeshRouteDetailSheet({ open, onOpenChange, alias }: Props) {
{diag?.lastError && (
<div className="rounded border border-destructive/30 bg-destructive/10 p-3 text-xs">
<div className="text-destructive font-mono uppercase tracking-wide text-[10px] mb-1">last error</div>
<div className="text-destructive font-mono uppercase tracking-[0.18em] leading-3 text-[10px] mb-1">last error</div>
<div className="text-stat-value">{diag.lastError.message}</div>
<div className="text-[10px] text-stat-subtitle mt-1">{new Date(diag.lastError.ts).toLocaleString()}</div>
</div>
@@ -114,7 +114,7 @@ export function MeshRouteDetailSheet({ open, onOpenChange, alias }: Props) {
</div>
<div>
<div className="text-[10px] tracking-wide uppercase text-stat-subtitle font-mono mb-2">Recent activity</div>
<div className="text-[10px] leading-3 tracking-[0.18em] uppercase text-stat-subtitle font-mono mb-2">Recent activity</div>
<div className="space-y-1 max-h-72 overflow-auto">
{loading && <Loader2 className="w-4 h-4 animate-spin text-stat-subtitle" />}
{!loading && events.length === 0 && (
@@ -56,12 +56,12 @@ export function RoutingNodeCard({
<div className="flex items-center gap-2">
<span className="font-mono text-sm">{status.nodeName}</span>
{isLocal && (
<span className="inline-flex items-center px-1.5 py-0.5 rounded-sm border border-brand/40 bg-brand/10 text-[9px] font-mono uppercase tracking-wide text-brand">
<span className="inline-flex items-center px-1.5 py-0.5 rounded-sm border border-brand/40 bg-brand/10 text-[10px] leading-3 font-mono uppercase tracking-[0.18em] text-brand">
Local
</span>
)}
{!status.pilotConnected && status.nodeId !== -1 && !isLocal && (
<span className="inline-flex items-center px-1.5 py-0.5 rounded-sm border border-destructive/40 bg-destructive/10 text-[9px] font-mono uppercase tracking-wide text-destructive">
<span className="inline-flex items-center px-1.5 py-0.5 rounded-sm border border-destructive/40 bg-destructive/10 text-[10px] leading-3 font-mono uppercase tracking-[0.18em] text-destructive">
pilot offline
</span>
)}
@@ -112,7 +112,7 @@ export function RoutingNodeCard({
>
{a.host}:{a.port}
</button>
<span className={`shrink-0 px-1.5 py-0.5 rounded-sm border text-[9px] font-mono uppercase tracking-wide ${pill.toneClass}`}>
<span className={`shrink-0 px-1.5 py-0.5 rounded-sm border text-[10px] leading-3 font-mono uppercase tracking-[0.18em] ${pill.toneClass}`}>
{pill.label}
</span>
<Button
+2 -2
View File
@@ -160,11 +160,11 @@ function RoutingMasthead({ meshedNodes, onlineNodes, totalAliases, onShowActivit
<div className="font-display italic text-2xl">{stateWord}</div>
<div className="grid grid-cols-3 gap-4 text-xs">
<div>
<div className="text-[10px] tracking-wide uppercase text-stat-subtitle font-mono">meshed</div>
<div className="text-[10px] leading-3 tracking-[0.18em] uppercase text-stat-subtitle font-mono">meshed</div>
<div className="font-mono text-stat-value">{meshedNodes}/{onlineNodes}</div>
</div>
<div>
<div className="text-[10px] tracking-wide uppercase text-stat-subtitle font-mono">aliases</div>
<div className="text-[10px] leading-3 tracking-[0.18em] uppercase text-stat-subtitle font-mono">aliases</div>
<div className="font-mono text-stat-value">{totalAliases}</div>
</div>
</div>
@@ -7,8 +7,8 @@ interface StructuredLogRowProps {
}
const BADGE_CLASSES: Record<LogStage, string> = {
PULL: 'bg-blue-500/10 text-blue-400',
BUILD: 'bg-violet-500/10 text-violet-400',
PULL: 'bg-brand/10 text-brand',
BUILD: 'bg-warning/10 text-warning',
CREATE: 'bg-brand/10 text-brand',
START: 'bg-success/10 text-success',
STOP: 'bg-warning/10 text-warning',
@@ -35,7 +35,7 @@ function StructuredLogRowBase({ row }: StructuredLogRowProps) {
</span>
<span
className={cn(
'text-[10px] uppercase tracking-widest shrink-0 w-[52px] text-center rounded px-1 pt-[1px]',
'text-[10px] leading-3 uppercase tracking-[0.18em] shrink-0 w-[52px] text-center rounded px-1 pt-[1px]',
BADGE_CLASSES[row.stage],
)}
>
@@ -232,7 +232,7 @@ export function AccountSection() {
>
<div className="flex items-center gap-2">
<ShieldCheck className="h-4 w-4 text-brand" strokeWidth={1.5} />
<span className="font-mono text-[12px] uppercase tracking-[0.16em] text-stat-value">
<span className="font-mono text-[10px] leading-3 uppercase tracking-[0.18em] text-stat-value">
enrolled
</span>
</div>
@@ -166,7 +166,7 @@ export function LabelsSection({ onLabelsChanged }: LabelsSectionProps = {}) {
{labels.map(label => (
<div key={label.id} className="flex items-center gap-3 px-4 py-3 group transition-colors hover:bg-accent/5">
<LabelDot color={label.color} />
<span className="font-mono text-[13px] flex-1">{label.name}</span>
<span className="font-mono text-sm flex-1">{label.name}</span>
<span className="text-xs text-muted-foreground tabular-nums">
{assignmentCounts[label.id] || 0} stack{(assignmentCounts[label.id] || 0) !== 1 ? 's' : ''}
</span>
@@ -388,7 +388,7 @@ function UpgradeCard({ tone, icon, title, blurb, features, action }: UpgradeCard
<div className="border border-card-border rounded-md bg-card p-4 flex flex-col gap-3">
<div className="flex items-center gap-2">
<span className={iconClass}>{icon}</span>
<span className="font-display italic text-base text-stat-value">{title}</span>
<span className="font-sans text-base text-stat-value">{title}</span>
</div>
<p className="text-xs text-stat-subtitle">{blurb}</p>
<ul className="space-y-1.5 flex-1">
@@ -41,7 +41,7 @@ export const SettingsPrimaryButton = forwardRef<HTMLButtonElement, ButtonProps>(
{...props}
className={cn(
'bg-brand text-brand-foreground shadow-btn-glow hover:bg-brand/90',
'font-mono uppercase tracking-[0.16em] text-xs',
'font-mono uppercase tracking-[0.18em] text-[10px] leading-3',
className,
)}
/>
@@ -57,7 +57,7 @@ export const SettingsSecondaryButton = forwardRef<HTMLButtonElement, ButtonProps
size={size ?? 'sm'}
{...props}
className={cn(
'font-mono uppercase tracking-[0.16em] text-xs',
'font-mono uppercase tracking-[0.18em] text-[10px] leading-3',
className,
)}
/>
@@ -4,17 +4,17 @@ interface SidebarBrandProps {
export function SidebarBrand({ isDarkMode }: SidebarBrandProps) {
return (
<div className="flex items-center gap-2 px-4 py-2 border-b border-glass-border">
<div className="flex items-center gap-2 px-4 py-1.5 border-b border-glass-border">
<img
src={isDarkMode ? '/sencho-logo-dark.png' : '/sencho-logo-light.png'}
alt="Sencho Logo"
className="w-7 h-7 shrink-0"
/>
<div className="flex flex-col leading-none">
<span className="font-mono text-[9px] tracking-[0.22em] uppercase text-stat-subtitle">
<span className="font-mono text-[10px] leading-3 tracking-[0.18em] uppercase text-stat-subtitle">
SENCHO · v{__APP_VERSION__}
</span>
<span className="font-serif italic text-[17px] text-foreground mt-0.5">Sencho</span>
<span className="font-display italic text-[22px] leading-none text-foreground mt-0.5">Sencho</span>
</div>
</div>
);
@@ -91,7 +91,7 @@ function renderItem(item: MenuItem, ctx: StackMenuCtx) {
<item.icon className="h-4 w-4 mr-2" strokeWidth={1.5} />
<span className="flex-1">{item.label}</span>
{item.shortcut && (
<span className="ml-3 font-mono text-[10px] tracking-wider text-stat-subtitle">{item.shortcut}</span>
<span className="ml-3 font-mono text-[10px] leading-3 tracking-[0.18em] text-stat-subtitle">{item.shortcut}</span>
)}
</ContextMenuItem>
);
@@ -91,7 +91,7 @@ function renderItem(item: MenuItem, ctx: StackMenuCtx) {
<item.icon className="h-4 w-4 mr-2" strokeWidth={1.5} />
<span className="flex-1">{item.label}</span>
{item.shortcut && (
<span className="ml-3 font-mono text-[10px] tracking-wider text-stat-subtitle">{item.shortcut}</span>
<span className="ml-3 font-mono text-[10px] leading-3 tracking-[0.18em] text-stat-subtitle">{item.shortcut}</span>
)}
</DropdownMenuItem>
);
+2 -2
View File
@@ -95,14 +95,14 @@ export function StackRow(props: StackRowProps) {
</span>
{/* Stack name */}
<span className="flex-1 truncate font-mono text-[13px] min-w-0">{displayName}</span>
<span className="flex-1 truncate font-mono text-sm min-w-0">{displayName}</span>
{/* Trailing: label dots (max 3 + overflow count) */}
{visibleLabels.length > 0 && (
<span className="flex items-center gap-0.5 shrink-0">
{visibleLabels.map(l => <LabelDot key={l.id} color={l.color} />)}
{overflowCount > 0 && (
<span className="font-mono text-[8px] text-stat-icon leading-none">+{overflowCount}</span>
<span className="font-mono text-[10px] leading-3 text-stat-icon">+{overflowCount}</span>
)}
</span>
)}
+1 -1
View File
@@ -47,7 +47,7 @@ const CardDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
className={cn("text-sm text-stat-subtitle", className)}
{...props}
/>
))
+48 -44
View File
@@ -8,43 +8,46 @@
LIGHT THEME
*/
:root {
--background: oklch(0.97 0.005 80);
--foreground: oklch(0.15 0.005 80);
--card: oklch(1 0.003 80);
--card-foreground: oklch(0.15 0.005 80);
--popover: oklch(1 0.003 80 / 0.92);
--popover-foreground: oklch(0.15 0.005 80);
--primary: oklch(0.15 0.005 80);
--primary-foreground: oklch(1 0.003 80);
--background: oklch(0.97 0 0);
--foreground: oklch(0.15 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.15 0 0);
--popover: oklch(1 0 0 / 0.92);
--popover-foreground: oklch(0.15 0 0);
--primary: oklch(0.15 0 0);
--primary-foreground: oklch(1 0 0);
--secondary: oklch(0 0 0 / 0.05);
--secondary-foreground: oklch(0.15 0.005 80);
--secondary-foreground: oklch(0.15 0 0);
--muted: oklch(0 0 0 / 0.04);
--muted-foreground: oklch(0.50 0 0);
--accent: oklch(0 0 0 / 0.06);
--accent-foreground: oklch(0.15 0.005 80);
--accent-foreground: oklch(0.15 0 0);
--destructive: oklch(0.63 0.18 20);
--destructive-foreground: oklch(1 0.003 80);
--destructive-foreground: oklch(1 0 0);
--border: oklch(0 0 0 / 0.10);
--input: oklch(0.97 0.005 80);
--ring: oklch(0.50 0.11 195);
--input: oklch(0.97 0 0);
--ring: oklch(0.50 0.14 200);
/* Brand: technical cyan, the signature accent */
--brand: oklch(0.50 0.11 195);
--brand-foreground: oklch(1 0.003 80);
--brand-muted: oklch(0.50 0.11 195 / 0.12);
--brand: oklch(0.50 0.14 200);
--brand-foreground: oklch(1 0 0);
--brand-muted: oklch(0.50 0.14 200 / 0.12);
/* Glass: frosted surface system */
--glass: oklch(1 0.003 80);
--glass: oklch(1 0 0);
--glass-border: oklch(0 0 0 / 0.10);
--glass-highlight: oklch(0 0 0 / 0.06);
/* Semantic status colors */
--success: oklch(0.65 0.16 155);
--success-foreground: oklch(1 0.003 80);
--success-foreground: oklch(1 0 0);
--success-muted: oklch(0.65 0.16 155 / 0.12);
--warning: oklch(0.75 0.14 75);
--warning-foreground: oklch(0.20 0 0);
--warning-muted: oklch(0.75 0.14 75 / 0.12);
/* --info: TOAST USE ONLY. Not part of the §17 five-slot working palette.
New components must consume --brand, --success, --warning, --destructive,
or surface/ink tokens instead. */
--info: oklch(0.62 0.14 250);
--info-foreground: oklch(1 0 0);
--info-muted: oklch(0.62 0.14 250 / 0.12);
@@ -65,14 +68,14 @@
--data-peak: var(--warning);
/* Sidebar */
--sidebar: oklch(0.98 0.005 80 / 0.80);
--sidebar-foreground: oklch(0.15 0.005 80);
--sidebar-primary: oklch(0.15 0.005 80);
--sidebar-primary-foreground: oklch(1 0.003 80);
--sidebar: oklch(0.98 0 0 / 0.80);
--sidebar-foreground: oklch(0.15 0 0);
--sidebar-primary: oklch(0.15 0 0);
--sidebar-primary-foreground: oklch(1 0 0);
--sidebar-accent: oklch(0 0 0 / 0.06);
--sidebar-accent-foreground: oklch(0.15 0.005 80);
--sidebar-accent-foreground: oklch(0.15 0 0);
--sidebar-border: oklch(0 0 0 / 0.10);
--sidebar-ring: oklch(0.50 0.11 195);
--sidebar-ring: oklch(0.50 0.14 200);
/* Stat hierarchy: text brightness tiers */
--stat-value: oklch(0.15 0 0);
@@ -184,24 +187,24 @@
DARK THEME
*/
.dark {
--background: oklch(0.08 0.003 80);
--foreground: oklch(0.94 0.01 80);
--card: oklch(0.14 0.005 80);
--card-foreground: oklch(0.94 0.01 80);
--popover: oklch(0.16 0.005 80 / 0.82);
--popover-foreground: oklch(0.94 0.01 80);
--primary: oklch(0.98 0.01 80);
--background: oklch(0.08 0 0);
--foreground: oklch(0.94 0 0);
--card: oklch(0.12 0 0);
--card-foreground: oklch(0.94 0 0);
--popover: oklch(0.14 0 0 / 0.82);
--popover-foreground: oklch(0.94 0 0);
--primary: oklch(0.98 0 0);
--primary-foreground: oklch(0.10 0 0);
--secondary: oklch(0.14 0.005 80);
--secondary-foreground: oklch(0.94 0.01 80);
--muted: oklch(0.14 0.005 80);
--secondary: oklch(0.12 0 0);
--secondary-foreground: oklch(0.94 0 0);
--muted: oklch(0.12 0 0);
--muted-foreground: oklch(0.45 0 0);
--accent: oklch(1 0 0 / 0.06);
--accent-foreground: oklch(0.94 0.01 80);
--accent-foreground: oklch(0.94 0 0);
--destructive: oklch(0.72 0.18 20);
--destructive-foreground: oklch(0.94 0.01 80);
--destructive-foreground: oklch(0.94 0 0);
--border: oklch(1 0 0 / 0.06);
--input: oklch(0.14 0.005 80);
--input: oklch(0.12 0 0);
--ring: oklch(0.78 0.11 195);
/* Brand: desaturated teal accent */
@@ -210,7 +213,7 @@
--brand-muted: oklch(0.78 0.11 195 / 0.12);
/* Glass: solid surface system (no blur on static surfaces) */
--glass: oklch(0.14 0.005 80);
--glass: oklch(0.12 0 0);
--glass-border: oklch(1 0 0 / 0.06);
--glass-highlight: oklch(1 0 0 / 0.06);
@@ -221,6 +224,7 @@
--warning: oklch(0.82 0.14 75);
--warning-foreground: oklch(0.10 0 0);
--warning-muted: oklch(0.82 0.14 75 / 0.15);
/* --info: TOAST USE ONLY. Not part of the §17 five-slot working palette. */
--info: oklch(0.70 0.14 250);
--info-foreground: oklch(0.10 0 0);
--info-muted: oklch(0.70 0.14 250 / 0.15);
@@ -241,12 +245,12 @@
--data-peak: var(--warning);
/* Sidebar */
--sidebar: oklch(0.08 0.003 80 / 0.80);
--sidebar-foreground: oklch(0.94 0.01 80);
--sidebar-primary: oklch(0.98 0.01 80);
--sidebar: oklch(0.08 0 0 / 0.80);
--sidebar-foreground: oklch(0.94 0 0);
--sidebar-primary: oklch(0.98 0 0);
--sidebar-primary-foreground: oklch(0.10 0 0);
--sidebar-accent: oklch(1 0 0 / 0.07);
--sidebar-accent-foreground: oklch(0.94 0.01 80);
--sidebar-accent-foreground: oklch(0.94 0 0);
--sidebar-border: oklch(1 0 0 / 0.06);
--sidebar-ring: oklch(0.78 0.11 195);
@@ -533,10 +537,10 @@ body {
}
.glass-float {
background: oklch(0.14 0.005 80 / 0.82);
background: var(--popover);
backdrop-filter: blur(10px) saturate(1.15);
-webkit-backdrop-filter: blur(10px) saturate(1.15);
border: 1px solid oklch(1 0 0 / 0.07);
border: 1px solid var(--glass-border);
}
/*
+101
View File
@@ -0,0 +1,101 @@
/**
* Resolves design-system CSS tokens to concrete color strings for xterm.js.
*
* xterm's canvas renderer does not accept CSS variable references or oklch()
* values it needs resolved strings. Calling this function at component mount
* (after the DOM is ready) produces a theme object wired to the active color
* scheme without embedding any hex literals.
*/
function resolveToken(styles: CSSStyleDeclaration, token: string, fallback = 'oklch(0 0 0)'): string {
return styles.getPropertyValue(token).trim() || fallback;
}
export interface XtermTheme {
background: string;
foreground: string;
cursor: string;
cursorAccent: string;
selectionBackground: string;
black: string;
red: string;
green: string;
yellow: string;
blue: string;
magenta: string;
cyan: string;
white: string;
brightBlack: string;
brightRed: string;
brightGreen: string;
brightYellow: string;
brightBlue: string;
brightMagenta: string;
brightCyan: string;
brightWhite: string;
}
/**
* Builds the xterm theme from CSS custom properties. Must be called after the
* document has loaded so getComputedStyle returns the active theme values.
*
* ANSI slots are mapped to design-system semantic roles where applicable:
* cyan --brand (primary data color)
* green --success
* yellow --warning
* red --destructive
* Other ANSI slots use neutral grays derived from --terminal-fg/bg.
*/
export function buildXtermTheme(): XtermTheme {
const s = getComputedStyle(document.documentElement);
const bg = resolveToken(s, '--terminal-bg');
const fg = resolveToken(s, '--terminal-fg');
const brand = resolveToken(s, '--brand');
const success = resolveToken(s, '--success');
const warning = resolveToken(s, '--warning');
const destructive = resolveToken(s, '--destructive');
return {
background: bg,
foreground: fg,
cursor: resolveToken(s, '--terminal-cursor'),
cursorAccent: resolveToken(s, '--terminal-cursor-accent'),
selectionBackground: resolveToken(s, '--terminal-selection'),
// ANSI palette aligned to semantic roles
black: 'oklch(0.30 0 0)',
red: destructive,
green: success,
yellow: warning,
blue: brand,
magenta: 'oklch(0.70 0.15 300)',
cyan: brand,
white: 'oklch(0.70 0 0)',
brightBlack: 'oklch(0.43 0 0)',
brightRed: 'oklch(0.76 0.18 20)',
brightGreen: 'oklch(0.83 0.16 155)',
brightYellow: 'oklch(0.87 0.14 75)',
brightBlue: brand,
brightMagenta: 'oklch(0.80 0.12 300)',
brightCyan: brand,
brightWhite: fg,
};
}
/**
* Minimal xterm theme for modal/exec contexts that only override the
* background, foreground, cursor, and selection. The ANSI palette inherits
* defaults, which is fine for command output that rarely uses ANSI colors.
*/
export function buildXtermMinimalTheme(): Pick<
XtermTheme,
'background' | 'foreground' | 'cursor' | 'cursorAccent' | 'selectionBackground'
> {
const s = getComputedStyle(document.documentElement);
return {
background: resolveToken(s, '--terminal-bg'),
foreground: resolveToken(s, '--terminal-fg'),
cursor: resolveToken(s, '--terminal-cursor'),
cursorAccent: resolveToken(s, '--terminal-cursor-accent'),
selectionBackground: resolveToken(s, '--terminal-selection'),
};
}