Visual redesign: design system, refined ops console aesthetic

- Full component library (Button, Input, Label, Select, Modal, Table, Badge, Card, etc.)
- Tailwind design tokens: IBM Plex Sans + JetBrains Mono, teal accent, semantic status colors
- NavBar with logo, responsive hamburger menu, real logout
- All pages redesigned: Login, Dashboard (KPI cards), Machines, SyncPairs, JobHistory, JobDetail, SSHKeys, Settings
- Fixed: hover:bg-gray-750 dead class, window.location.href navigation bug
- Replaced alert()/confirm() with sonner toasts and accessible modals
- Added ErrorBoundary, skip link, accessible modal dialogs (Radix)
- Icons: lucide-react throughout, copy/download buttons
- 1.0.5 → 1.0.6
This commit is contained in:
2026-07-07 23:12:34 -04:00
parent 93c22e844e
commit 9cef7173cb
36 changed files with 4069 additions and 652 deletions
+56
View File
@@ -0,0 +1,56 @@
import { cva, type VariantProps } from 'class-variance-authority'
export const badgeVariants = cva(
'inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors',
{
variants: {
variant: {
running: 'bg-emerald-500/15 text-emerald-400 border border-emerald-500/30',
pending: 'bg-amber-500/15 text-amber-400 border border-amber-500/30',
waking: 'bg-amber-500/15 text-amber-400 border border-amber-500/30',
success: 'bg-emerald-500/15 text-emerald-400 border border-emerald-500/30',
error: 'bg-rose-500/15 text-rose-400 border border-rose-500/30',
info: 'bg-sky-500/15 text-sky-400 border border-sky-500/30',
neutral: 'bg-zinc-500/15 text-zinc-400 border border-zinc-500/30',
},
},
defaultVariants: {
variant: 'neutral',
},
}
)
export type BadgeVariant = VariantProps<typeof badgeVariants>['variant']
const STATUS_TO_VARIANT: Record<string, BadgeVariant> = {
running: 'running',
pending: 'pending',
waking: 'waking',
success: 'success',
error: 'error',
failed: 'error',
completed: 'success',
cancelled: 'neutral',
info: 'info',
unknown: 'neutral',
}
export function statusVariant(status: string): BadgeVariant {
return STATUS_TO_VARIANT[status.toLowerCase()] ?? 'neutral'
}
export function statusLabel(status: string): string {
const labels: Record<string, string> = {
running: 'Running',
pending: 'Pending',
waking: 'Waking',
success: 'Success',
error: 'Error',
failed: 'Failed',
completed: 'Completed',
cancelled: 'Cancelled',
info: 'Info',
unknown: 'Unknown',
}
return labels[status.toLowerCase()] ?? status
}
+33
View File
@@ -0,0 +1,33 @@
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
export function formatDuration(ms: number): string {
if (ms < 1000) return `${ms}ms`
const seconds = Math.floor(ms / 1000)
if (seconds < 60) return `${seconds}s`
const minutes = Math.floor(seconds / 60)
const remainingSeconds = seconds % 60
if (minutes < 60) return `${minutes}m ${remainingSeconds}s`
const hours = Math.floor(minutes / 60)
const remainingMinutes = minutes % 60
return `${hours}h ${remainingMinutes}m`
}
export function formatRelativeTime(date: Date | string): string {
const d = typeof date === 'string' ? new Date(date) : date
const now = new Date()
const diff = now.getTime() - d.getTime()
const seconds = Math.floor(diff / 1000)
if (seconds < 60) return 'just now'
const minutes = Math.floor(seconds / 60)
if (minutes < 60) return `${minutes}m ago`
const hours = Math.floor(minutes / 60)
if (hours < 24) return `${hours}h ago`
const days = Math.floor(hours / 24)
if (days < 7) return `${days}d ago`
return d.toLocaleDateString()
}