diff --git a/frontend/src/components/AuditLogView.tsx b/frontend/src/components/AuditLogView.tsx index 2a1419f3..75a15171 100644 --- a/frontend/src/components/AuditLogView.tsx +++ b/frontend/src/components/AuditLogView.tsx @@ -597,7 +597,7 @@ function StreamRow({ entry, now }: StreamRowProps) { ))} -
+
{entry.method} {entry.path.split('?')[0]}
diff --git a/frontend/src/components/AutoUpdateReadinessView.tsx b/frontend/src/components/AutoUpdateReadinessView.tsx index 6a0182b6..ba2a14e5 100644 --- a/frontend/src/components/AutoUpdateReadinessView.tsx +++ b/frontend/src/components/AutoUpdateReadinessView.tsx @@ -86,7 +86,7 @@ function formatClock(ts: number | null): string { function RiskBadge({ bump, blocked }: { bump: SemverBump; blocked: boolean }) { if (blocked || bump === 'major') { return ( - + Blocked · major @@ -94,7 +94,7 @@ function RiskBadge({ bump, blocked }: { bump: SemverBump; blocked: boolean }) { } if (bump === 'minor') { return ( - + Review · minor @@ -102,7 +102,7 @@ function RiskBadge({ bump, blocked }: { bump: SemverBump; blocked: boolean }) { } if (bump === 'patch') { return ( - + Safe · patch @@ -110,13 +110,13 @@ function RiskBadge({ bump, blocked }: { bump: SemverBump; blocked: boolean }) { } if (bump === 'unknown') { return ( - + Digest rebuild ); } return ( - + None ); @@ -164,7 +164,7 @@ function StackReadinessCard({
{!autoUpdateEnabled && ( - + Auto: Off @@ -188,7 +188,7 @@ function StackReadinessCard({ {p.summary.update_kind === 'digest' ? (
{p.summary.current_tag} - + Rebuild available
diff --git a/frontend/src/components/BashExecModal.tsx b/frontend/src/components/BashExecModal.tsx index 4fd3f0df..b5ff1da3 100644 --- a/frontend/src/components/BashExecModal.tsx +++ b/frontend/src/components/BashExecModal.tsx @@ -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, diff --git a/frontend/src/components/EditorLayout.tsx b/frontend/src/components/EditorLayout.tsx index 4df6b8b1..1f77e226 100644 --- a/frontend/src/components/EditorLayout.tsx +++ b/frontend/src/components/EditorLayout.tsx @@ -2680,7 +2680,7 @@ export default function EditorLayout() { const sparkStroke = health === 'unhealthy' ? 'var(--destructive)' : health === 'starting' ? 'var(--warning)' : 'var(--chart-1)'; return ( -
+
@@ -2771,7 +2771,7 @@ export default function EditorLayout() {
- cpu + cpu {stats?.cpu ?? '-'}
@@ -2780,7 +2780,7 @@ export default function EditorLayout() {
- mem + mem {stats?.ram ?? '-'}
@@ -2789,7 +2789,7 @@ export default function EditorLayout() {
- net i/o + net i/o {stats?.net ?? '-'}
@@ -2964,7 +2964,7 @@ export default function EditorLayout() { ) : ( <> {activeTab === 'env' && ( -
+
Variables defined here are automatically available for substitution in your compose.yaml (e.g., ${'{}'}VAR). To pass them directly into your container, you must add env_file: - .env to your service definition. diff --git a/frontend/src/components/FleetView.tsx b/frontend/src/components/FleetView.tsx index e4d015ae..279b0aac 100644 --- a/frontend/src/components/FleetView.tsx +++ b/frontend/src/components/FleetView.tsx @@ -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 ( - + Updating ); @@ -536,18 +536,18 @@ function NodeCard({ node, onNavigate, labelMap, updateStatus, onUpdate, updating {/* Container Stats */} {isOnline && node.stats && ( -
-
-
{node.stats.active}
-
Running
+
+
+
{node.stats.active}
+
Running
-
-
{node.stats.exited}
-
Stopped
+
+
{node.stats.exited}
+
Stopped
-
-
{node.stacks?.length ?? '-'}
-
Stacks
+
+
{node.stacks?.length ?? '-'}
+
Stacks
)} @@ -571,7 +571,7 @@ function NodeCard({ node, onNavigate, labelMap, updateStatus, onUpdate, updating {formatBytes(node.systemStats.memory.used)} / {formatBytes(node.systemStats.memory.total)}
- 80 ? 'bg-destructive/80' : memPercent > 60 ? 'bg-warning' : 'bg-info'} /> + 80 ? 'bg-destructive/80' : memPercent > 60 ? 'bg-warning' : 'bg-brand/60'} />
{node.systemStats.disk && (
@@ -1461,7 +1461,7 @@ export function FleetView({ onNavigateToNode }: FleetViewProps) {
{updating}
- Updating + Updating
@@ -1491,7 +1491,7 @@ export function FleetView({ onNavigateToNode }: FleetViewProps) {
{/* Table header */} -
+
Node Type Current @@ -1504,7 +1504,6 @@ export function FleetView({ onNavigateToNode }: FleetViewProps) {
{filtered.map(s => (
- {/* Node name */}
{s.type === 'local' diff --git a/frontend/src/components/HostConsole.tsx b/frontend/src/components/HostConsole.tsx index 4ded27f1..2d35df35 100644 --- a/frontend/src/components/HostConsole.tsx +++ b/frontend/src/components/HostConsole.tsx @@ -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, diff --git a/frontend/src/components/LogViewer.tsx b/frontend/src/components/LogViewer.tsx index 592d69ec..1c82ef11 100644 --- a/frontend/src/components/LogViewer.tsx +++ b/frontend/src/components/LogViewer.tsx @@ -67,7 +67,7 @@ export function LogViewer({ containerId, containerName, isOpen, onClose }: LogVi
{logs.length === 0 && !isConnected ? (
Connecting to container stream...
diff --git a/frontend/src/components/NodeManager.tsx b/frontend/src/components/NodeManager.tsx index eb758c67..2bd5e83d 100644 --- a/frontend/src/components/NodeManager.tsx +++ b/frontend/src/components/NodeManager.tsx @@ -285,7 +285,7 @@ export function NodeManager() { const getStatusBadge = (status: string) => { switch (status) { case 'online': - return Online; + return Online; case 'offline': return Offline; default: @@ -458,7 +458,7 @@ export function NodeManager() {

- + Generate Node Token

@@ -586,7 +586,7 @@ export function NodeManager() { return (

{summary?.auto_update_enabled ? ( - + Auto diff --git a/frontend/src/components/NodeSwitcher.tsx b/frontend/src/components/NodeSwitcher.tsx index bb2937a6..d937f996 100644 --- a/frontend/src/components/NodeSwitcher.tsx +++ b/frontend/src/components/NodeSwitcher.tsx @@ -65,7 +65,7 @@ export function NodeSwitcher({ onManageNodes }: NodeSwitcherProps) {
Node · {kickerType}
-
+
{activeNode?.name ?? (isLoading ? '—' : 'No node')}
@@ -104,7 +104,7 @@ export function NodeSwitcher({ onManageNodes }: NodeSwitcherProps) { Connected - + {nodes.length} nodes
diff --git a/frontend/src/components/NotificationPanel.tsx b/frontend/src/components/NotificationPanel.tsx index 65330f43..a7ce0d66 100644 --- a/frontend/src/components/NotificationPanel.tsx +++ b/frontend/src/components/NotificationPanel.tsx @@ -37,7 +37,7 @@ type LevelConfig = { }; const LEVEL_CONFIG: Record = { - 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 {unreadCount > 0 ? ( - + {unreadCount} unread ) : null} diff --git a/frontend/src/components/ResourcesView.tsx b/frontend/src/components/ResourcesView.tsx index aa6864a8..fbddcdf8 100644 --- a/frontend/src/components/ResourcesView.tsx +++ b/frontend/src/components/ResourcesView.tsx @@ -165,7 +165,7 @@ function FilterToggle({ value, onChange, counts }: FilterToggleProps) { {label} {count} @@ -214,7 +214,7 @@ function ManagedBadge({ status, managedBy }: { const SEVERITY_BADGE_CLASSES: Record = { 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 = { const SEVERITY_DOT_CLASSES: Record = { 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: ()
{summary.critical > 0 && {summary.critical}C} {summary.high > 0 && {summary.high}H} - {summary.medium > 0 && {summary.medium}M} + {summary.medium > 0 && {summary.medium}M} {summary.low > 0 && {summary.low}L}
)} @@ -752,7 +752,7 @@ export default function ResourcesView() { target="images" icon={} label="Prune Unused Images" - accentClass="text-info" + accentClass="text-brand" onManaged={() => setConfirmPrune({ target: 'images', scope: 'managed' })} onAll={() => setConfirmPrune({ target: 'images', scope: 'all' })} /> diff --git a/frontend/src/components/StackAlertSheet.tsx b/frontend/src/components/StackAlertSheet.tsx index 3865d9ba..617ad545 100644 --- a/frontend/src/components/StackAlertSheet.tsx +++ b/frontend/src/components/StackAlertSheet.tsx @@ -202,7 +202,7 @@ export function StackAlertSheet({ isOpen, onClose, stackName }: StackAlertSheetP const renderAgentStatusBanner = () => { if (agentStatus.loading) { return ( -
+
Checking notification channels...
@@ -211,10 +211,10 @@ export function StackAlertSheet({ isOpen, onClose, stackName }: StackAlertSheetP if (isRemote) { return ( -
- +
+
-

+

Remote node: {activeNode?.name}

diff --git a/frontend/src/components/Terminal.tsx b/frontend/src/components/Terminal.tsx index 5a25ee05..e9707519 100644 --- a/frontend/src/components/Terminal.tsx +++ b/frontend/src/components/Terminal.tsx @@ -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, diff --git a/frontend/src/components/UserProfileDropdown.tsx b/frontend/src/components/UserProfileDropdown.tsx index 6f9e2780..03a8c62e 100644 --- a/frontend/src/components/UserProfileDropdown.tsx +++ b/frontend/src/components/UserProfileDropdown.tsx @@ -80,7 +80,7 @@ export function UserProfileDropdown({ theme, setTheme, onOpenSettings }: UserPro - + {pill.label}

-
meshed
+
meshed
{meshedNodes}/{onlineNodes}
-
aliases
+
aliases
{totalAliases}
diff --git a/frontend/src/components/log-rendering/StructuredLogRow.tsx b/frontend/src/components/log-rendering/StructuredLogRow.tsx index 2a971ada..03c62255 100644 --- a/frontend/src/components/log-rendering/StructuredLogRow.tsx +++ b/frontend/src/components/log-rendering/StructuredLogRow.tsx @@ -7,8 +7,8 @@ interface StructuredLogRowProps { } const BADGE_CLASSES: Record = { - 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) { diff --git a/frontend/src/components/settings/AccountSection.tsx b/frontend/src/components/settings/AccountSection.tsx index c72d713c..14d8b392 100644 --- a/frontend/src/components/settings/AccountSection.tsx +++ b/frontend/src/components/settings/AccountSection.tsx @@ -232,7 +232,7 @@ export function AccountSection() { >
- + enrolled
diff --git a/frontend/src/components/settings/LabelsSection.tsx b/frontend/src/components/settings/LabelsSection.tsx index 5704ffb9..ddf8a3c9 100644 --- a/frontend/src/components/settings/LabelsSection.tsx +++ b/frontend/src/components/settings/LabelsSection.tsx @@ -166,7 +166,7 @@ export function LabelsSection({ onLabelsChanged }: LabelsSectionProps = {}) { {labels.map(label => (
- {label.name} + {label.name} {assignmentCounts[label.id] || 0} stack{(assignmentCounts[label.id] || 0) !== 1 ? 's' : ''} diff --git a/frontend/src/components/settings/LicenseSection.tsx b/frontend/src/components/settings/LicenseSection.tsx index cd4cee90..bcaba916 100644 --- a/frontend/src/components/settings/LicenseSection.tsx +++ b/frontend/src/components/settings/LicenseSection.tsx @@ -388,7 +388,7 @@ function UpgradeCard({ tone, icon, title, blurb, features, action }: UpgradeCard
{icon} - {title} + {title}

{blurb}

    diff --git a/frontend/src/components/settings/SettingsActions.tsx b/frontend/src/components/settings/SettingsActions.tsx index a669299a..cb326eeb 100644 --- a/frontend/src/components/settings/SettingsActions.tsx +++ b/frontend/src/components/settings/SettingsActions.tsx @@ -41,7 +41,7 @@ export const SettingsPrimaryButton = forwardRef( {...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 diff --git a/frontend/src/components/sidebar/SidebarBrand.tsx b/frontend/src/components/sidebar/SidebarBrand.tsx index d0adce90..351f9b6e 100644 --- a/frontend/src/components/sidebar/SidebarBrand.tsx +++ b/frontend/src/components/sidebar/SidebarBrand.tsx @@ -4,17 +4,17 @@ interface SidebarBrandProps { export function SidebarBrand({ isDarkMode }: SidebarBrandProps) { return ( -
    +
    Sencho Logo
    - + SENCHO · v{__APP_VERSION__} - Sencho + Sencho
    ); diff --git a/frontend/src/components/sidebar/StackContextMenu.tsx b/frontend/src/components/sidebar/StackContextMenu.tsx index e79be42c..7191b4f1 100644 --- a/frontend/src/components/sidebar/StackContextMenu.tsx +++ b/frontend/src/components/sidebar/StackContextMenu.tsx @@ -91,7 +91,7 @@ function renderItem(item: MenuItem, ctx: StackMenuCtx) { {item.label} {item.shortcut && ( - {item.shortcut} + {item.shortcut} )} ); diff --git a/frontend/src/components/sidebar/StackKebabMenu.tsx b/frontend/src/components/sidebar/StackKebabMenu.tsx index be938d44..b719922a 100644 --- a/frontend/src/components/sidebar/StackKebabMenu.tsx +++ b/frontend/src/components/sidebar/StackKebabMenu.tsx @@ -91,7 +91,7 @@ function renderItem(item: MenuItem, ctx: StackMenuCtx) { {item.label} {item.shortcut && ( - {item.shortcut} + {item.shortcut} )} ); diff --git a/frontend/src/components/sidebar/StackRow.tsx b/frontend/src/components/sidebar/StackRow.tsx index e6899e67..9fbb8adf 100644 --- a/frontend/src/components/sidebar/StackRow.tsx +++ b/frontend/src/components/sidebar/StackRow.tsx @@ -95,14 +95,14 @@ export function StackRow(props: StackRowProps) { {/* Stack name */} - {displayName} + {displayName} {/* Trailing: label dots (max 3 + overflow count) */} {visibleLabels.length > 0 && ( {visibleLabels.map(l => )} {overflowCount > 0 && ( - +{overflowCount} + +{overflowCount} )} )} diff --git a/frontend/src/components/ui/card.tsx b/frontend/src/components/ui/card.tsx index 9a017643..95babb7b 100644 --- a/frontend/src/components/ui/card.tsx +++ b/frontend/src/components/ui/card.tsx @@ -47,7 +47,7 @@ const CardDescription = React.forwardRef< >(({ className, ...props }, ref) => (
    )) diff --git a/frontend/src/index.css b/frontend/src/index.css index 4d674069..b05b0a3f 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -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); } /* ───────────────────────────────────────────────────────────── diff --git a/frontend/src/lib/terminalTheme.ts b/frontend/src/lib/terminalTheme.ts new file mode 100644 index 00000000..21151051 --- /dev/null +++ b/frontend/src/lib/terminalTheme.ts @@ -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'), + }; +}