mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-07 01:14:14 +00:00
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:
@@ -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,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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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' })}
|
||||
/>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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'),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user