feat: interactive site redesign with dot matrix effects and component polish
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
---
|
||||
import DotMatrixText from './DotMatrixText.astro'
|
||||
const navItems = [
|
||||
{ label: 'Dashboard', icon: 'grid', active: true },
|
||||
{ label: 'Nodes', icon: 'server', active: false },
|
||||
@@ -60,7 +61,7 @@ const tasks = [
|
||||
{ chip: { label: 'RUNNING', cls: 'border-warning/30 bg-warning/10 text-warning' }, text: 'start VM 100', node: 'pve1', time: '12s ago' },
|
||||
{ chip: { label: 'OK', cls: 'border-success/25 bg-success/10 text-success' }, text: 'backup VM 101 → local-lvm', node: 'pve2', time: '3m ago' },
|
||||
{ chip: { label: 'OK', cls: 'border-success/25 bg-success/10 text-success' }, text: 'migrate VM 102', node: 'pve1 → pve3', time: '18m ago' },
|
||||
{ chip: { label: 'DONE', cls: 'border-white/[0.06] bg-muted text-muted-foreground' }, text: 'update apt', node: 'pve3', time: '1h ago' },
|
||||
{ chip: { label: 'DONE', cls: 'border-dotted border-white/10 bg-muted text-muted-foreground' }, text: 'update apt', node: 'pve3', time: '1h ago' },
|
||||
]
|
||||
const gauges = [
|
||||
{ label: 'CPU', icon: 'cpu', percent: '30', width: '30', detail: '7.3 / 24 cores' },
|
||||
@@ -97,14 +98,14 @@ const quickActions = [
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<div class="overflow-hidden rounded-xl border border-white/[0.08] bg-card shadow-card">
|
||||
<div class="relative flex h-9 items-center border-b border-white/[0.06] bg-background">
|
||||
<div class="overflow-hidden rounded-xl border border-dotted border-white/10 bg-card shadow-card">
|
||||
<div class="relative flex h-9 items-center border-b border-dotted border-white/10 bg-background">
|
||||
<div class="absolute left-4 flex items-center gap-1.5" aria-hidden="true">
|
||||
<span class="h-2.5 w-2.5 rounded-full border border-white/[0.06] bg-muted-foreground/40"></span>
|
||||
<span class="h-2.5 w-2.5 rounded-full border border-white/[0.06] bg-muted-foreground/40"></span>
|
||||
<span class="h-2.5 w-2.5 rounded-full border border-white/[0.06] bg-muted-foreground/40"></span>
|
||||
<span class="h-2.5 w-2.5 rounded-full border border-dotted border-white/10 bg-muted-foreground/40"></span>
|
||||
<span class="h-2.5 w-2.5 rounded-full border border-dotted border-white/10 bg-muted-foreground/40"></span>
|
||||
<span class="h-2.5 w-2.5 rounded-full border border-dotted border-white/10 bg-muted-foreground/40"></span>
|
||||
</div>
|
||||
<p class="w-full text-center font-mono text-[10px] tabular-nums text-muted-foreground">Clustri — Ops Console</p>
|
||||
<div class="w-full flex justify-center"><DotMatrixText text="CLUSTRI" size="xs" dotClass="text-muted-foreground" /></div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 border-b border-warning/25 bg-warning/10 px-4 py-1.5">
|
||||
@@ -114,14 +115,13 @@ const quickActions = [
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<aside class="hidden w-52 shrink-0 flex-col border-r border-white/[0.06] bg-card sm:flex">
|
||||
<div class="flex h-14 shrink-0 items-center gap-2.5 border-b border-white/[0.06] px-4">
|
||||
<aside class="hidden w-52 shrink-0 flex-col border-r border-dotted border-white/10 bg-card sm:flex">
|
||||
<div class="flex h-14 shrink-0 items-center gap-2.5 border-b border-dotted border-white/10 px-4">
|
||||
<div class="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-primary/15 text-primary">
|
||||
<svg class="h-4 w-4" aria-hidden="true"><use href="#i-hex" /></svg>
|
||||
</div>
|
||||
<div class="min-w-0 leading-tight">
|
||||
<p class="truncate text-sm font-semibold tracking-tight text-foreground">Clustri</p>
|
||||
<p class="font-mono text-[9px] font-medium uppercase tracking-[0.14em] text-muted-foreground">Ops Console</p>
|
||||
<DotMatrixText text="CLUSTRI" size="xs" dotClass="text-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-h-0 flex-1 p-2">
|
||||
@@ -146,7 +146,7 @@ const quickActions = [
|
||||
</div>
|
||||
<p class="ml-7 font-mono text-[10px] text-muted-foreground">Offline</p>
|
||||
</div>
|
||||
<p class="px-3 pb-1 pt-3 font-mono text-[10px] font-medium uppercase tracking-[0.14em] text-muted-foreground">Cluster nodes</p>
|
||||
<div class="px-3 pb-1 pt-3"><DotMatrixText text="CLUSTER NODES" size="xs" dotClass="text-muted-foreground" /></div>
|
||||
<div class="space-y-0.5">
|
||||
{clusterNodes.map((n) => (
|
||||
<div class="flex w-full items-center gap-2.5 rounded-md px-3 py-1.5">
|
||||
@@ -158,11 +158,11 @@ const quickActions = [
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-1 border-t border-white/[0.06] p-2">
|
||||
<div class="space-y-1 border-t border-dotted border-white/10 p-2">
|
||||
<button type="button" class="flex w-full items-center gap-2.5 rounded-md px-3 py-1.5 text-[12px] font-medium text-muted-foreground transition-colors hover:bg-accent/60 hover:text-foreground">
|
||||
<svg class="h-4 w-4 shrink-0" aria-hidden="true"><use href="#i-gear" /></svg><span>Settings</span>
|
||||
</button>
|
||||
<button type="button" class="flex w-full items-center gap-2 rounded-md border border-white/[0.06] bg-background px-3 py-1.5 text-[12px] font-medium text-muted-foreground shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground">
|
||||
<button type="button" class="flex w-full items-center gap-2 rounded-md border border-dotted border-white/10 bg-background px-3 py-1.5 text-[12px] font-medium text-muted-foreground shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground">
|
||||
<svg class="h-3.5 w-3.5 shrink-0" aria-hidden="true"><use href="#i-plus" /></svg><span>Add Connection</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -171,7 +171,7 @@ const quickActions = [
|
||||
<main class="min-w-0 flex-1 bg-background p-4">
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<h2 class="text-base font-semibold tracking-tight">Overview</h2>
|
||||
<DotMatrixText text="OVERVIEW" size="sm" dotClass="text-foreground" />
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="font-mono text-[10px] tabular-nums text-muted-foreground">home-lab · 192.168.1.10:8006</span>
|
||||
<span class="font-mono text-[10px] tabular-nums text-muted-foreground">3 nodes · 2 online</span>
|
||||
@@ -181,7 +181,7 @@ const quickActions = [
|
||||
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
{nodeCards.map((node) => (
|
||||
<article class="rounded-md border border-white/[0.06] bg-card p-3">
|
||||
<article class="rounded-md border border-dotted border-white/10 bg-card p-3">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<span class={`h-1.5 w-1.5 shrink-0 rounded-full ${node.dot}`} aria-hidden="true"></span>
|
||||
@@ -216,8 +216,8 @@ const quickActions = [
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-5">
|
||||
<section class="rounded-md border border-white/[0.06] bg-card sm:col-span-3">
|
||||
<header class="flex items-center gap-2 border-b border-white/[0.06] px-3 py-2">
|
||||
<section class="rounded-md border border-dotted border-white/10 bg-card sm:col-span-3">
|
||||
<header class="flex items-center gap-2 border-b border-dotted border-white/10 px-3 py-2">
|
||||
<svg class="h-3.5 w-3.5 shrink-0 text-muted-foreground" aria-hidden="true"><use href="#i-activity" /></svg>
|
||||
<h3 class="font-mono text-[10px] font-medium uppercase tracking-[0.14em] text-muted-foreground">Task activity</h3>
|
||||
<span class="ml-auto h-1.5 w-1.5 animate-pulse rounded-full bg-success" aria-hidden="true"></span>
|
||||
@@ -237,7 +237,7 @@ const quickActions = [
|
||||
|
||||
<div class="space-y-3 sm:col-span-2">
|
||||
{gauges.map((gauge) => (
|
||||
<section class="rounded-md border border-white/[0.06] bg-card p-3">
|
||||
<section class="rounded-md border border-dotted border-white/10 bg-card p-3">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<svg class="h-3.5 w-3.5 text-muted-foreground" aria-hidden="true"><use href={`#i-${gauge.icon}`} /></svg>
|
||||
<span class="font-mono text-[9px] font-medium uppercase tracking-[0.14em] text-muted-foreground">{gauge.label}</span>
|
||||
@@ -256,7 +256,7 @@ const quickActions = [
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
{quickActions.map((action) => (
|
||||
<button type="button" class="flex items-center gap-1.5 rounded border border-white/[0.06] px-3 py-1.5 text-[11px] font-medium text-muted-foreground transition-colors hover:bg-accent/60 hover:text-foreground">
|
||||
<button type="button" class="flex items-center gap-1.5 rounded border border-dotted border-white/10 px-3 py-1.5 text-[11px] font-medium text-muted-foreground transition-colors hover:bg-accent/60 hover:text-foreground">
|
||||
<svg class="h-3 w-3" aria-hidden="true"><use href={`#i-${action.icon}`} /></svg><span>{action.label}</span>
|
||||
</button>
|
||||
))}
|
||||
|
||||
@@ -1,45 +1,48 @@
|
||||
<section id="architecture" aria-labelledby="arch-title" class="scroll-mt-24 border-t border-white/[0.06] py-20 sm:py-28">
|
||||
---
|
||||
import DotMatrixText from './DotMatrixText.astro'
|
||||
---
|
||||
<section id="architecture" aria-labelledby="arch-title" class="scroll-mt-24 border-t border-dotted border-white/10 py-20 sm:py-28">
|
||||
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
<div class="max-w-2xl" data-reveal>
|
||||
<p class="font-mono text-[11px] uppercase tracking-[0.18em] text-primary">Architecture</p>
|
||||
<DotMatrixText text="ARCHITECTURE" size="xs" dotClass="text-primary" />
|
||||
<h2 id="arch-title" class="mt-4 text-3xl font-semibold tracking-[-0.03em] text-foreground sm:text-4xl">One proxy between you and the API.</h2>
|
||||
<p class="mt-4 text-[15px] leading-relaxed text-muted-foreground">The frontend calls Rust commands over Tauri IPC. The backend proxies them to the Proxmox API over HTTPS.</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-10 grid gap-3 lg:grid-cols-[1fr_auto_1fr_auto_1fr] lg:items-stretch" data-stagger>
|
||||
<div class="rounded-xl border border-white/[0.06] bg-card p-5" data-stagger-child>
|
||||
<div class="flex items-start justify-between gap-4"><p class="text-sm font-semibold text-foreground">React frontend</p><span class="font-mono text-[10px] text-muted-foreground">01</span></div>
|
||||
<div class="rounded-xl border border-dotted border-white/10 bg-card p-5 dot-grid" data-stagger-child>
|
||||
<div class="flex items-start justify-between gap-4"><p class="text-sm font-semibold text-foreground">React frontend</p><DotMatrixText text="01" size="xs" dotClass="text-muted-foreground" /></div>
|
||||
<div class="mt-2 space-y-1 font-mono text-[11px] text-muted-foreground"><p>TanStack Query · Zustand</p><p>Geist · JetBrains Mono</p><p>React 19</p></div>
|
||||
</div>
|
||||
<div class="hidden flex-col items-center justify-center gap-1 lg:flex flow-arrow" aria-hidden="true"><span class="text-sm text-primary">→</span><span class="font-mono text-[9px] text-muted-foreground">tauri invoke</span></div>
|
||||
<div class="flex items-center justify-center gap-1.5 py-1 lg:hidden flow-arrow" aria-hidden="true"><span class="text-sm text-primary">↓</span><span class="font-mono text-[9px] text-muted-foreground">tauri invoke</span></div>
|
||||
|
||||
<div class="rounded-xl border border-primary/25 bg-primary/[0.07] p-5" data-stagger-child>
|
||||
<div class="flex items-start justify-between gap-4"><p class="text-sm font-semibold text-primary">Rust backend</p><span class="font-mono text-[10px] text-muted-foreground">02</span></div>
|
||||
<div class="rounded-xl border border-dotted border-primary/30 bg-primary/[0.07] p-5 dot-grid" data-stagger-child>
|
||||
<div class="flex items-start justify-between gap-4"><p class="text-sm font-semibold text-primary">Rust backend</p><DotMatrixText text="02" size="xs" dotClass="text-primary" /></div>
|
||||
<div class="mt-2 space-y-1 font-mono text-[11px] text-primary/75"><p>ConnectionManager</p><p>endpoint rotation · retry</p><p>TOFU cert pinning</p><p>OS keyring</p></div>
|
||||
</div>
|
||||
<div class="hidden flex-col items-center justify-center gap-1 lg:flex flow-arrow" aria-hidden="true"><span class="text-sm text-primary">→</span><span class="font-mono text-[9px] text-muted-foreground">HTTPS</span></div>
|
||||
<div class="flex items-center justify-center gap-1.5 py-1 lg:hidden flow-arrow" aria-hidden="true"><span class="text-sm text-primary">↓</span><span class="font-mono text-[9px] text-muted-foreground">HTTPS</span></div>
|
||||
|
||||
<div class="rounded-xl border border-white/[0.06] bg-card p-5" data-stagger-child>
|
||||
<div class="flex items-start justify-between gap-4"><p class="text-sm font-semibold text-foreground">Proxmox API</p><span class="font-mono text-[10px] text-muted-foreground">03</span></div>
|
||||
<div class="rounded-xl border border-dotted border-white/10 bg-card p-5 dot-grid" data-stagger-child>
|
||||
<div class="flex items-start justify-between gap-4"><p class="text-sm font-semibold text-foreground">Proxmox API</p><DotMatrixText text="03" size="xs" dotClass="text-muted-foreground" /></div>
|
||||
<div class="mt-2 space-y-1 font-mono text-[11px] text-muted-foreground"><p>/api2/json</p><p>nodes · cluster · vms</p><p>self-signed TLS</p></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 grid gap-6 lg:grid-cols-2">
|
||||
<div class="overflow-hidden rounded-xl border border-white/[0.06] bg-card" data-reveal>
|
||||
<div class="border-b border-white/[0.06] bg-background px-4 py-2.5 font-mono text-[10px] text-muted-foreground">POST /nodes/pve/qemu/100/status/start</div>
|
||||
<div class="overflow-hidden rounded-xl border border-dotted border-white/10 bg-card" data-reveal>
|
||||
<div class="border-b border-dotted border-white/10 bg-background px-4 py-2.5 font-mono text-[10px] text-muted-foreground">POST /nodes/pve/qemu/100/status/start</div>
|
||||
<div class="px-4 py-4">
|
||||
<p class="truncate font-mono text-[13px] tabular-nums text-foreground">https://192.168.1.10:8006/api2/json/nodes/pve/qemu/100/status/start</p>
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
<span class="rounded border border-white/[0.08] bg-muted px-2 py-1 font-mono text-[10px] text-muted-foreground">PVEAPIToken</span>
|
||||
<span class="rounded border border-white/[0.08] bg-muted px-2 py-1 font-mono text-[10px] text-muted-foreground">PVEAuthCookie + CSRFPreventionToken</span>
|
||||
<span class="rounded border border-dotted border-white/10 bg-muted px-2 py-1 font-mono text-[10px] text-muted-foreground">PVEAPIToken</span>
|
||||
<span class="rounded border border-dotted border-white/10 bg-muted px-2 py-1 font-mono text-[10px] text-muted-foreground">PVEAuthCookie + CSRFPreventionToken</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="py-2" data-reveal data-reveal-delay="100">
|
||||
<p class="font-mono text-[10px] uppercase tracking-[0.08em] text-muted-foreground">Failover model</p>
|
||||
<DotMatrixText text="FAILOVER MODEL" size="xs" dotClass="text-muted-foreground" />
|
||||
<p class="mt-3 text-sm leading-relaxed text-muted-foreground">Ordered, deduped endpoints: primary → fallbacks → discovered nodes. Connect / timeout / DNS / transport failure → retry next endpoint.</p>
|
||||
<div class="mt-4 flex gap-4 font-mono text-[10px]">
|
||||
<span class="inline-flex items-center gap-1.5"><span class="h-1.5 w-1.5 rounded-full bg-success"></span> connected</span>
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
---
|
||||
import DotMatrixText from './DotMatrixText.astro'
|
||||
---
|
||||
<div id="cmdk-overlay" class="cmdk-overlay fixed inset-0 z-[100] hidden items-start justify-center px-4 pt-[20vh]" role="dialog" aria-modal="true" aria-label="Command palette">
|
||||
<div id="cmdk-panel" class="cmdk-panel w-full max-w-[520px] overflow-hidden rounded-xl border border-white/[0.08] bg-card">
|
||||
<div class="flex items-center gap-3 border-b border-white/[0.06] px-4">
|
||||
<div id="cmdk-panel" class="cmdk-panel w-full max-w-[520px] overflow-hidden rounded-xl border border-dotted border-white/10 bg-card dot-grid">
|
||||
<div class="flex items-center gap-3 border-b border-dotted border-white/10 px-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="h-4 w-4 shrink-0 text-muted-foreground" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
|
||||
<input id="cmdk-input" type="text" placeholder="Type a command…" autocomplete="off" spellcheck="false" class="h-12 w-full bg-transparent font-mono text-sm text-foreground placeholder:text-muted-foreground/60 focus:outline-none" />
|
||||
<kbd class="hidden rounded border border-white/[0.08] bg-white/[0.04] px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground sm:inline">ESC</kbd>
|
||||
<kbd class="hidden rounded border border-dotted border-white/10 bg-white/[0.04] px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground sm:inline">ESC</kbd>
|
||||
</div>
|
||||
<div id="cmdk-list" class="max-h-[320px] overflow-auto p-2"></div>
|
||||
<div class="border-t border-white/[0.06] px-3 py-2 font-mono text-[10px] text-muted-foreground/60">↑↓ to navigate · ↵ to select · esc to close</div>
|
||||
<div class="border-t border-dotted border-white/10 px-3 py-2 flex items-center justify-between">
|
||||
<DotMatrixText text="UP/DN NAVIGATE / ENTER SELECT / ESC CLOSE" size="xs" dotClass="text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
import { GLYPH_COLS, GLYPH_ROWS, getGlyph } from '../lib/dotMatrixGlyphs'
|
||||
|
||||
interface Props {
|
||||
text: string
|
||||
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
||||
class?: string
|
||||
dotClass?: string
|
||||
}
|
||||
|
||||
const { text, size = 'sm', class: className, dotClass } = Astro.props as Props
|
||||
|
||||
const sizeMap = {
|
||||
xs: { dot: 1.8, gap: 1.3, charGap: 5 },
|
||||
sm: { dot: 2.2, gap: 1.5, charGap: 6 },
|
||||
md: { dot: 2.6, gap: 1.8, charGap: 7 },
|
||||
lg: { dot: 3.0, gap: 2.0, charGap: 9 },
|
||||
xl: { dot: 3.8, gap: 2.4, charGap: 12 },
|
||||
} as const
|
||||
|
||||
const { dot, gap, charGap } = sizeMap[size]
|
||||
const chars = Array.from(text.toUpperCase())
|
||||
---
|
||||
|
||||
<span aria-label={text} role="text" class:list={['inline-flex items-center leading-none', dotClass, className]} style={`gap:${charGap}px`}>
|
||||
{chars.map((ch) => {
|
||||
if (ch === ' ') return <span aria-hidden="true" style={`width:${charGap * 1.8}px`} />
|
||||
const glyph = getGlyph(ch)
|
||||
if (!glyph) return <span class="font-mono text-[0.7em] leading-none" aria-hidden="true">{ch}</span>
|
||||
const w = GLYPH_COLS * dot + (GLYPH_COLS - 1) * gap
|
||||
const h = GLYPH_ROWS * dot + (GLYPH_ROWS - 1) * gap
|
||||
const r = dot / 2
|
||||
return (
|
||||
<svg width={w} height={h} viewBox={`0 0 ${w} ${h}`} aria-hidden="true" class="shrink-0" shape-rendering="geometricPrecision">
|
||||
{glyph.map((row, y) =>
|
||||
Array.from(row).map((cell, x) => {
|
||||
if (cell === ' ') return null
|
||||
const cx = x * (dot + gap) + r
|
||||
const cy = y * (dot + gap) + r
|
||||
return <circle cx={cx} cy={cy} r={r} fill="currentColor" />
|
||||
}),
|
||||
)}
|
||||
</svg>
|
||||
)
|
||||
})}
|
||||
</span>
|
||||
@@ -1,26 +1,29 @@
|
||||
<section id="download" class="scroll-mt-24 border-t border-white/[0.06] py-20 sm:py-28">
|
||||
---
|
||||
import DotMatrixText from './DotMatrixText.astro'
|
||||
---
|
||||
<section id="download" class="scroll-mt-24 border-t border-dotted border-white/10 py-20 sm:py-28">
|
||||
<div class="mx-auto max-w-3xl px-6 text-center">
|
||||
<p class="font-mono text-[11px] uppercase tracking-[0.18em] text-primary" data-reveal>Releases</p>
|
||||
<div data-reveal><DotMatrixText text="RELEASES" size="xs" dotClass="text-primary" /></div>
|
||||
<h2 class="mt-4 text-3xl font-semibold tracking-[-0.03em] text-foreground sm:text-4xl" data-reveal data-reveal-delay="80">Coming soon.</h2>
|
||||
<p class="mx-auto mt-4 max-w-[46ch] text-[15px] leading-relaxed text-muted-foreground" data-reveal data-reveal-delay="140">
|
||||
Clustri is in active development. Packages for Linux, macOS, and Windows are on the way.
|
||||
</p>
|
||||
|
||||
<div class="relative mt-10 overflow-hidden rounded-xl border border-white/[0.07] bg-card p-6 sm:p-8 shadow-card" data-reveal data-reveal-delay="200">
|
||||
<div class="pointer-events-none absolute inset-0 grid-bg opacity-[0.12]" aria-hidden="true"></div>
|
||||
<div class="relative mt-10 overflow-hidden rounded-xl border border-dotted border-white/10 bg-card p-6 sm:p-8 shadow-card dot-grid" data-reveal data-reveal-delay="200">
|
||||
<div class="pointer-events-none absolute inset-0 dot-grid opacity-[0.10]" aria-hidden="true"></div>
|
||||
<div class="pointer-events-none absolute inset-x-0 top-0 h-px bg-primary/30" aria-hidden="true"></div>
|
||||
|
||||
<div class="relative">
|
||||
<div class="flex flex-wrap items-center justify-center gap-3">
|
||||
<a href="#" class="rounded-[10px] bg-primary px-6 py-3 text-sm font-semibold text-primary-foreground transition hover:bg-primary/90 active:scale-[0.98]">Get Clustri</a>
|
||||
<a href="#" class="rounded-[10px] border border-white/[0.08] bg-white/[0.03] px-6 py-3 text-sm font-semibold text-muted-foreground transition hover:border-white/15 hover:text-foreground active:scale-[0.98]">Build from source</a>
|
||||
<a href="#" class="rounded-[10px] border border-dotted border-white/10 bg-white/[0.03] px-6 py-3 text-sm font-semibold text-muted-foreground transition hover:border-white/15 hover:text-foreground active:scale-[0.98]">Build from source</a>
|
||||
</div>
|
||||
<div class="mt-6 flex flex-wrap items-center justify-center gap-2">
|
||||
<span class="rounded-md border border-white/[0.06] bg-background px-2.5 py-1 font-mono text-[11px] text-muted-foreground">Linux</span>
|
||||
<span class="rounded-md border border-white/[0.06] bg-background px-2.5 py-1 font-mono text-[11px] text-muted-foreground">macOS</span>
|
||||
<span class="rounded-md border border-white/[0.06] bg-background px-2.5 py-1 font-mono text-[11px] text-muted-foreground">Windows</span>
|
||||
<span class="rounded-md border border-dotted border-white/10 bg-background px-2.5 py-1 font-mono text-[11px] text-muted-foreground">Linux</span>
|
||||
<span class="rounded-md border border-dotted border-white/10 bg-background px-2.5 py-1 font-mono text-[11px] text-muted-foreground">macOS</span>
|
||||
<span class="rounded-md border border-dotted border-white/10 bg-background px-2.5 py-1 font-mono text-[11px] text-muted-foreground">Windows</span>
|
||||
</div>
|
||||
<p class="mt-4 font-mono text-[10px] text-muted-foreground/60">cargo test · 64 tests · MIT · v0.1.0</p>
|
||||
<div class="mt-4 flex justify-center"><DotMatrixText text="CARGO TEST / 64 TESTS / MIT / V0.1.0" size="xs" dotClass="text-muted-foreground" /></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<section id="failover-playground" aria-labelledby="fp-title" class="relative overflow-hidden border-y border-white/[0.06] bg-card/40">
|
||||
<div class="pointer-events-none absolute inset-0 grid-bg opacity-[0.18]" aria-hidden="true"></div>
|
||||
---
|
||||
import DotMatrixText from './DotMatrixText.astro'
|
||||
---
|
||||
<section id="failover-playground" aria-labelledby="fp-title" class="relative overflow-hidden border-y border-dotted border-white/10 bg-card/40">
|
||||
<div class="pointer-events-none absolute inset-0 dot-grid opacity-[0.12]" aria-hidden="true"></div>
|
||||
|
||||
<div class="relative mx-auto max-w-7xl px-6 py-20 lg:px-8 lg:py-28">
|
||||
<div class="grid gap-10 lg:grid-cols-12 lg:items-start">
|
||||
<!-- Copy + controls -->
|
||||
<div class="lg:col-span-5">
|
||||
<p class="font-mono text-[11px] uppercase tracking-[0.18em] text-primary" data-reveal>Failover — try it</p>
|
||||
<div data-reveal><DotMatrixText text="FAILOVER / TRY IT" size="xs" dotClass="text-primary" /></div>
|
||||
<h2 id="fp-title" class="mt-4 text-3xl font-semibold tracking-[-0.03em] text-foreground sm:text-4xl" data-reveal data-reveal-delay="80">
|
||||
One node dies.<br /><span class="text-muted-foreground">You don't notice.</span>
|
||||
</h2>
|
||||
@@ -14,9 +16,9 @@
|
||||
</p>
|
||||
|
||||
<div class="mt-8 flex flex-wrap gap-2" data-reveal data-reveal-delay="180">
|
||||
<button id="fp-kill-pve1" type="button" class="rounded-md border border-white/[0.08] bg-white/[0.04] px-3 py-2 font-mono text-[11px] font-medium text-foreground transition hover:border-white/15 hover:bg-white/[0.07]">Kill pve1</button>
|
||||
<button id="fp-kill-pve2" type="button" class="rounded-md border border-white/[0.08] bg-white/[0.04] px-3 py-2 font-mono text-[11px] font-medium text-foreground transition hover:border-white/15 hover:bg-white/[0.07]">Kill pve2</button>
|
||||
<button id="fp-kill-pve3" type="button" class="rounded-md border border-white/[0.08] bg-white/[0.04] px-3 py-2 font-mono text-[11px] font-medium text-foreground transition hover:border-white/15 hover:bg-white/[0.07]">Kill pve3</button>
|
||||
<button id="fp-kill-pve1" type="button" class="rounded-md border border-dotted border-white/10 bg-white/[0.04] px-3 py-2 font-mono text-[11px] font-medium text-foreground transition hover:border-white/15 hover:bg-white/[0.07]">Kill pve1</button>
|
||||
<button id="fp-kill-pve2" type="button" class="rounded-md border border-dotted border-white/10 bg-white/[0.04] px-3 py-2 font-mono text-[11px] font-medium text-foreground transition hover:border-white/15 hover:bg-white/[0.07]">Kill pve2</button>
|
||||
<button id="fp-kill-pve3" type="button" class="rounded-md border border-dotted border-white/10 bg-white/[0.04] px-3 py-2 font-mono text-[11px] font-medium text-foreground transition hover:border-white/15 hover:bg-white/[0.07]">Kill pve3</button>
|
||||
<button id="fp-heal" type="button" class="rounded-md bg-primary px-3 py-2 font-mono text-[11px] font-semibold text-primary-foreground transition hover:bg-primary/90">Heal all</button>
|
||||
</div>
|
||||
|
||||
@@ -27,19 +29,19 @@
|
||||
<span class="ml-1 text-muted-foreground">— tap a node or use the buttons</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 rounded-md border border-white/[0.06] bg-background px-3 py-2.5 font-mono text-[11px] leading-relaxed text-muted-foreground" data-reveal data-reveal-delay="260">
|
||||
<div class="mt-6 rounded-md border border-dotted border-white/10 bg-background px-3 py-2.5 font-mono text-[11px] leading-relaxed text-muted-foreground" data-reveal data-reveal-delay="260">
|
||||
<span class="text-foreground">How it works:</span> deduped order <span class="text-foreground">primary → fallbacks → discovered</span>. On connect / timeout / DNS / transport failure, retry the next endpoint. Status reflects which endpoint actually answered.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Diagram -->
|
||||
<div class="lg:col-span-7" data-reveal data-reveal-delay="120">
|
||||
<div class="relative overflow-hidden rounded-xl border border-white/[0.07] bg-card shadow-card">
|
||||
<div class="relative overflow-hidden rounded-xl border border-dotted border-white/10 bg-card shadow-card">
|
||||
<!-- top rule -->
|
||||
<div id="fp-top-rule" class="h-px w-full bg-primary/30 transition-colors" aria-hidden="true"></div>
|
||||
|
||||
<!-- header -->
|
||||
<div class="flex items-center justify-between gap-3 border-b border-white/[0.06] px-4 py-3">
|
||||
<div class="flex items-center justify-between gap-3 border-b border-dotted border-white/10 px-4 py-3">
|
||||
<p class="font-mono text-[10px] tracking-[0.06em] text-muted-foreground">request → endpoints[0] → retry on failure</p>
|
||||
<span id="fp-status-chip" class="rounded border border-success/25 bg-success/10 px-1.5 py-0.5 font-mono text-[10px] uppercase text-success">Connected</span>
|
||||
</div>
|
||||
@@ -47,7 +49,7 @@
|
||||
<!-- body: rail + nodes -->
|
||||
<div class="relative flex gap-0">
|
||||
<!-- packet rail -->
|
||||
<div class="relative w-[36px] shrink-0 border-r border-white/[0.04] bg-background/40">
|
||||
<div class="relative w-[36px] shrink-0 border-r border-dotted border-white/10 bg-background/40">
|
||||
<div class="absolute left-1/2 top-3 bottom-3 w-px -translate-x-1/2 bg-white/[0.08]" aria-hidden="true"></div>
|
||||
<!-- dots injected by JS -->
|
||||
<div id="fp-rail" class="absolute inset-0 overflow-hidden" aria-hidden="true"></div>
|
||||
@@ -56,7 +58,7 @@
|
||||
|
||||
<!-- node list -->
|
||||
<div class="min-w-0 flex-1">
|
||||
<button id="fp-node-0" type="button" class="flex w-full items-center justify-between gap-3 border-b border-white/[0.06] px-4 py-3.5 text-left transition hover:bg-white/[0.02]">
|
||||
<button id="fp-node-0" type="button" class="flex w-full items-center justify-between gap-3 border-b border-dotted border-white/10 px-4 py-3.5 text-left transition hover:bg-white/[0.02]">
|
||||
<span class="min-w-0">
|
||||
<span class="flex items-center gap-2">
|
||||
<span id="fp-dot-0" class="h-1.5 w-1.5 shrink-0 rounded-full bg-success shadow-[0_0_6px] shadow-success/50"></span>
|
||||
@@ -66,7 +68,7 @@
|
||||
</span>
|
||||
<span class="shrink-0 rounded border border-primary/30 bg-primary/10 px-1.5 py-0.5 font-mono text-[10px] uppercase text-primary">Primary</span>
|
||||
</button>
|
||||
<button id="fp-node-1" type="button" class="flex w-full items-center justify-between gap-3 border-b border-white/[0.06] px-4 py-3.5 text-left transition hover:bg-white/[0.02]">
|
||||
<button id="fp-node-1" type="button" class="flex w-full items-center justify-between gap-3 border-b border-dotted border-white/10 px-4 py-3.5 text-left transition hover:bg-white/[0.02]">
|
||||
<span class="min-w-0">
|
||||
<span class="flex items-center gap-2">
|
||||
<span id="fp-dot-1" class="h-1.5 w-1.5 shrink-0 rounded-full bg-success"></span>
|
||||
@@ -90,7 +92,7 @@
|
||||
</div>
|
||||
|
||||
<!-- status bar -->
|
||||
<div class="border-t border-white/[0.06] bg-background px-4 py-3">
|
||||
<div class="border-t border-dotted border-white/10 bg-background px-4 py-3">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<span id="fp-status-text" class="font-mono text-[11px] tabular-nums text-muted-foreground">status: connected — pve1 answered</span>
|
||||
<span id="fp-latency" class="font-mono text-[10px] tabular-nums text-muted-foreground">~18 ms</span>
|
||||
@@ -214,16 +216,20 @@
|
||||
document.getElementById('fp-heal')?.addEventListener('click', healAll)
|
||||
nodes.forEach((n, i) => n?.addEventListener('click', () => kill(i)))
|
||||
|
||||
const DOT_COUNT = 3
|
||||
const CYCLE_SPEED = 0.012
|
||||
|
||||
function createDots() {
|
||||
if (!rail) return
|
||||
rail.innerHTML = ''
|
||||
dotEls = []
|
||||
for (let k = 0; k < 2; k++) {
|
||||
for (let k = 0; k < DOT_COUNT; k++) {
|
||||
const d = document.createElement('div')
|
||||
d.className = 'absolute left-1/2 h-2 w-2 -translate-x-1/2 rounded-full bg-primary shadow-[0_0_8px] shadow-primary/60'
|
||||
d.style.top = '8px'
|
||||
d.className = 'absolute h-2 w-2 rounded-full bg-primary shadow-[0_0_8px] shadow-primary/60'
|
||||
d.style.left = '50%'
|
||||
d.style.top = '0px'
|
||||
// @ts-ignore
|
||||
d._offset = k * 0.5
|
||||
d._phase = k / DOT_COUNT
|
||||
rail.appendChild(d)
|
||||
dotEls.push(d)
|
||||
}
|
||||
@@ -232,33 +238,32 @@
|
||||
function restartDots() {
|
||||
if (prefersReduced) return
|
||||
cancelAnimationFrame(raf)
|
||||
t = 0
|
||||
createDots()
|
||||
animate()
|
||||
}
|
||||
|
||||
function targetYs() {
|
||||
function getTargetY() {
|
||||
const ai = activeIndex()
|
||||
if (ai === -1) return [0.88]
|
||||
// map active index to rail progress (0..1): pve1 ~0.26, pve2 ~0.55, pve3 ~0.84
|
||||
if (ai === -1) return 0.88
|
||||
const map = [0.28, 0.56, 0.86]
|
||||
return [map[ai]]
|
||||
return map[ai]
|
||||
}
|
||||
|
||||
function animate() {
|
||||
if (prefersReduced || !rail) return
|
||||
t += 0.018
|
||||
t += CYCLE_SPEED
|
||||
const h = rail.clientHeight || 160
|
||||
const targets = targetYs()
|
||||
const target = getTargetY()
|
||||
const ai = activeIndex()
|
||||
|
||||
dotEls.forEach((el) => {
|
||||
const phase = (t + el._offset) % 1
|
||||
// ease: move from 0 to target, then fade and reset
|
||||
const ty = phase * (targets[0] * h)
|
||||
const visible = phase < 0.92
|
||||
// @ts-ignore
|
||||
const phase = (t + el._phase) % 1
|
||||
const ty = phase * target * h
|
||||
const visible = phase < 0.88
|
||||
el.style.transform = `translate(-50%, ${ty}px)`
|
||||
el.style.opacity = visible ? '1' : '0'
|
||||
// if failed, make dot red and slower
|
||||
const ai = activeIndex()
|
||||
|
||||
if (ai === -1) {
|
||||
el.style.background = 'var(--color-destructive)'
|
||||
// @ts-ignore
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
---
|
||||
import DotMatrixText from './DotMatrixText.astro'
|
||||
---
|
||||
<!-- Security -->
|
||||
<section id="security" aria-labelledby="sec-title" class="scroll-mt-24 py-20 sm:py-28">
|
||||
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
<div class="grid gap-10 lg:grid-cols-12 lg:items-start">
|
||||
<div class="lg:col-span-6" data-reveal>
|
||||
<p class="font-mono text-[11px] uppercase tracking-[0.18em] text-primary">Security</p>
|
||||
<DotMatrixText text="SECURITY" size="xs" dotClass="text-primary" />
|
||||
<h2 id="sec-title" class="mt-4 text-3xl font-semibold tracking-[-0.03em] text-foreground sm:text-4xl">
|
||||
Your keys stay<br /><span class="text-muted-foreground">in your pocket.</span>
|
||||
</h2>
|
||||
@@ -18,14 +21,14 @@
|
||||
</div>
|
||||
|
||||
<div class="lg:col-span-6" data-reveal data-reveal-delay="120">
|
||||
<div class="rounded-xl border border-white/[0.07] bg-card p-5 shadow-card">
|
||||
<div class="rounded-xl border border-dotted border-white/10 bg-card p-5 shadow-card">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="h-2 w-2 rounded-full bg-success shadow-[0_0_6px] shadow-success/60" aria-hidden="true"></span>
|
||||
<span class="font-mono text-[10px] uppercase tracking-[0.08em] text-muted-foreground">SHA-256 fingerprint — pinned</span>
|
||||
<span class="ml-auto rounded border border-success/20 bg-success/10 px-1.5 py-0.5 font-mono text-[10px] uppercase text-success">Pinned</span>
|
||||
</div>
|
||||
<p class="mt-4 overflow-x-auto whitespace-nowrap font-mono text-[13px] tracking-[0.12em] text-foreground">A3:0F:71:C9:52:B8:4D:E0:11:2A:7F:9C:63:D4:18:0B</p>
|
||||
<div class="mt-4 flex flex-wrap gap-2 border-t border-white/[0.06] pt-4 font-mono text-[10px]">
|
||||
<div class="mt-4 flex flex-wrap gap-2 border-t border-dotted border-white/10 pt-4 font-mono text-[10px]">
|
||||
<span class="rounded bg-white/[0.04] px-2 py-1 text-muted-foreground">OS keyring</span>
|
||||
<span class="rounded bg-white/[0.04] px-2 py-1 text-muted-foreground">rustls · sha2 · x509-cert</span>
|
||||
<span class="rounded bg-white/[0.04] px-2 py-1 text-muted-foreground">no telemetry</span>
|
||||
@@ -34,15 +37,15 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid grid-cols-3 gap-3">
|
||||
<div class="rounded-lg border border-white/[0.06] bg-background p-3 text-center">
|
||||
<div class="rounded-lg border border-dotted border-white/10 bg-background p-3 text-center">
|
||||
<p class="font-mono text-[10px] uppercase tracking-[0.08em] text-muted-foreground">Tokens</p>
|
||||
<p class="mt-1 font-mono text-[11px] font-medium text-foreground">Keyring only</p>
|
||||
</div>
|
||||
<div class="rounded-lg border border-white/[0.06] bg-background p-3 text-center">
|
||||
<div class="rounded-lg border border-dotted border-white/10 bg-background p-3 text-center">
|
||||
<p class="font-mono text-[10px] uppercase tracking-[0.08em] text-muted-foreground">Certs</p>
|
||||
<p class="mt-1 font-mono text-[11px] font-medium text-foreground">TOFU SHA-256</p>
|
||||
</div>
|
||||
<div class="rounded-lg border border-white/[0.06] bg-background p-3 text-center">
|
||||
<div class="rounded-lg border border-dotted border-white/10 bg-background p-3 text-center">
|
||||
<p class="font-mono text-[10px] uppercase tracking-[0.08em] text-muted-foreground">Transport</p>
|
||||
<p class="mt-1 font-mono text-[11px] font-medium text-foreground">rustls</p>
|
||||
</div>
|
||||
@@ -53,11 +56,11 @@
|
||||
</section>
|
||||
|
||||
<!-- Bold statement — full bleed -->
|
||||
<section class="relative overflow-hidden border-y border-white/[0.06] bg-[#08080a]">
|
||||
<section class="relative overflow-hidden border-y border-dotted border-white/10 bg-[#08080a]">
|
||||
<div class="pointer-events-none absolute inset-0 grid-bg opacity-[0.22]" aria-hidden="true"></div>
|
||||
<div class="relative mx-auto max-w-7xl px-6 py-20 sm:py-28 lg:px-8">
|
||||
<div class="max-w-4xl" data-reveal>
|
||||
<p class="font-mono text-[11px] uppercase tracking-[0.18em] text-primary">The idea</p>
|
||||
<DotMatrixText text="THE IDEA" size="xs" dotClass="text-primary" />
|
||||
<p class="mt-6 text-3xl font-semibold leading-[0.95] tracking-[-0.04em] text-foreground sm:text-5xl lg:text-[3.4rem]">
|
||||
Proxmox is powerful.<br />
|
||||
<span class="text-muted-foreground">The web UI is not the point of failure.</span>
|
||||
@@ -75,27 +78,27 @@
|
||||
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
<div class="grid gap-10 lg:grid-cols-12">
|
||||
<div class="lg:col-span-5" data-reveal>
|
||||
<p class="font-mono text-[11px] uppercase tracking-[0.18em] text-primary">Console</p>
|
||||
<DotMatrixText text="CONSOLE" size="xs" dotClass="text-primary" />
|
||||
<h2 id="console-title" class="mt-4 text-3xl font-semibold tracking-[-0.03em] text-foreground sm:text-4xl">Type like you're there.</h2>
|
||||
<p class="mt-4 max-w-[44ch] text-[15px] leading-relaxed text-muted-foreground">
|
||||
noVNC for VMs, xterm.js for containers. Fullscreen, keyboard passthrough, and Ctrl+Alt+Del where you expect it.
|
||||
</p>
|
||||
<div class="mt-6 inline-flex gap-2">
|
||||
<span class="rounded border border-white/[0.06] bg-card px-2.5 py-1 font-mono text-[11px] text-muted-foreground">Fullscreen</span>
|
||||
<span class="rounded border border-white/[0.06] bg-card px-2.5 py-1 font-mono text-[11px] text-muted-foreground">Ctrl+Alt+Del</span>
|
||||
<span class="rounded border border-dotted border-white/10 bg-card px-2.5 py-1 font-mono text-[11px] text-muted-foreground">Fullscreen</span>
|
||||
<span class="rounded border border-dotted border-white/10 bg-card px-2.5 py-1 font-mono text-[11px] text-muted-foreground">Ctrl+Alt+Del</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lg:col-span-7" data-reveal data-reveal-delay="120">
|
||||
<div class="overflow-hidden rounded-xl border border-white/[0.07] bg-card shadow-card">
|
||||
<div class="flex items-center justify-between border-b border-white/[0.06] bg-background px-4 py-2.5">
|
||||
<div class="overflow-hidden rounded-xl border border-dotted border-white/10 bg-card shadow-card">
|
||||
<div class="flex items-center justify-between border-b border-dotted border-white/10 bg-background px-4 py-2.5">
|
||||
<div class="flex items-center gap-1.5" aria-hidden="true">
|
||||
<span class="h-2.5 w-2.5 rounded-full border border-white/[0.06] bg-white/10"></span>
|
||||
<span class="h-2.5 w-2.5 rounded-full border border-white/[0.06] bg-white/10"></span>
|
||||
<span class="h-2.5 w-2.5 rounded-full border border-white/[0.06] bg-white/10"></span>
|
||||
<span class="h-2.5 w-2.5 rounded-full border border-dotted border-white/10 bg-white/10"></span>
|
||||
<span class="h-2.5 w-2.5 rounded-full border border-dotted border-white/10 bg-white/10"></span>
|
||||
<span class="h-2.5 w-2.5 rounded-full border border-dotted border-white/10 bg-white/10"></span>
|
||||
</div>
|
||||
<p class="font-mono text-[10px] text-muted-foreground">root@pve1 · xterm</p>
|
||||
<kbd class="rounded border border-white/[0.08] bg-muted px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground">Ctrl+Alt+Del</kbd>
|
||||
<kbd class="rounded border border-dotted border-white/10 bg-muted px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground">Ctrl+Alt+Del</kbd>
|
||||
</div>
|
||||
<div class="bg-[#0a0a0a] px-4 py-4 font-mono text-[12px] leading-relaxed" data-terminal>
|
||||
<div data-line><span class="text-primary">root@pve1:~#</span> <span class="text-foreground">pct enter 101</span></div>
|
||||
@@ -110,11 +113,11 @@
|
||||
</section>
|
||||
|
||||
<!-- Power user -->
|
||||
<section id="power-user" aria-labelledby="power-title" class="scroll-mt-24 border-t border-white/[0.06] py-20 sm:py-28">
|
||||
<section id="power-user" aria-labelledby="power-title" class="scroll-mt-24 border-t border-dotted border-white/10 py-20 sm:py-28">
|
||||
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
<div class="flex flex-wrap items-end justify-between gap-6" data-reveal>
|
||||
<div>
|
||||
<p class="font-mono text-[11px] uppercase tracking-[0.18em] text-primary">Power user</p>
|
||||
<DotMatrixText text="POWER USER" size="xs" dotClass="text-primary" />
|
||||
<h2 id="power-title" class="mt-4 text-3xl font-semibold tracking-[-0.03em] text-foreground sm:text-4xl">Built for the people who keep the lights on.</h2>
|
||||
</div>
|
||||
<p class="max-w-[44ch] text-[15px] leading-relaxed text-muted-foreground">
|
||||
@@ -122,16 +125,16 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-10 grid gap-0 border-y border-white/[0.06] sm:grid-cols-2" data-stagger>
|
||||
<div class="flex items-center justify-between gap-4 border-b border-white/[0.06] px-1 py-4 sm:border-r sm:px-4" data-stagger-child>
|
||||
<kbd class="rounded border border-white/[0.08] bg-muted px-2 py-1 font-mono text-[10px] text-muted-foreground">Cmd / Ctrl + K</kbd>
|
||||
<div class="mt-10 grid gap-0 border-y border-dotted border-white/10 sm:grid-cols-2" data-stagger>
|
||||
<div class="flex items-center justify-between gap-4 border-b border-dotted border-white/10 px-1 py-4 sm:border-r sm:px-4" data-stagger-child>
|
||||
<kbd class="rounded border border-dotted border-white/10 bg-muted px-2 py-1 font-mono text-[10px] text-muted-foreground">Cmd / Ctrl + K</kbd>
|
||||
<span class="text-sm text-muted-foreground">Command palette</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-4 border-b border-white/[0.06] px-1 py-4 sm:px-4" data-stagger-child>
|
||||
<div class="flex items-center justify-between gap-4 border-b border-dotted border-white/10 px-1 py-4 sm:px-4" data-stagger-child>
|
||||
<span class="font-mono text-[11px] font-medium text-primary">WebSocket</span>
|
||||
<span class="text-sm text-muted-foreground">Live task & event stream</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-4 border-b border-white/[0.06] px-1 py-4 sm:border-r sm:px-4 sm:border-b-0" data-stagger-child>
|
||||
<div class="flex items-center justify-between gap-4 border-b border-dotted border-white/10 px-1 py-4 sm:border-r sm:px-4 sm:border-b-0" data-stagger-child>
|
||||
<span class="font-mono text-[11px] font-medium text-primary">Tray menu</span>
|
||||
<span class="text-sm text-muted-foreground">Show / hide · connections · quit</span>
|
||||
</div>
|
||||
|
||||
@@ -1,42 +1,47 @@
|
||||
---
|
||||
import DotMatrixText from './DotMatrixText.astro'
|
||||
const year = new Date().getFullYear()
|
||||
---
|
||||
|
||||
<footer class="border-t border-white/[0.06] bg-[#08080a]">
|
||||
<footer class="border-t border-dotted border-white/10 bg-[#08080a] dot-grid">
|
||||
<div class="mx-auto max-w-7xl px-6 py-10 lg:px-8">
|
||||
<div class="flex flex-col gap-8 md:flex-row md:items-start md:justify-between">
|
||||
<div>
|
||||
<div class="flex items-center gap-2.5">
|
||||
<span class="flex h-7 w-7 items-center justify-center rounded-[8px] bg-primary text-primary-foreground">
|
||||
<span class="flex h-7 w-7 items-center justify-center rounded-[8px] border border-dotted border-white/10 bg-primary text-primary-foreground">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-3.5 w-3.5" aria-hidden="true"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" /></svg>
|
||||
</span>
|
||||
<span class="text-sm font-semibold tracking-tight text-foreground">Clustri</span>
|
||||
<span class="hidden font-mono text-[9px] uppercase tracking-[0.14em] text-muted-foreground sm:inline">Ops Console</span>
|
||||
<DotMatrixText text="CLUSTRI" size="xs" dotClass="text-foreground" />
|
||||
<span class="hidden sm:inline"><DotMatrixText text="OPS CONSOLE" size="xs" dotClass="text-muted-foreground opacity-70" /></span>
|
||||
</div>
|
||||
<p class="mt-3 font-mono text-[11px] text-muted-foreground">v0.1.0 · MIT · Not affiliated with Proxmox.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-8 sm:flex-row sm:gap-16">
|
||||
<nav aria-label="Navigate" class="space-y-2">
|
||||
<p class="font-mono text-[10px] uppercase tracking-[0.14em] text-muted-foreground">Navigate</p>
|
||||
<a href="#failover-playground" class="block font-mono text-[12px] text-muted-foreground transition-colors hover:text-foreground">Failover demo</a>
|
||||
<a href="#security" class="block font-mono text-[12px] text-muted-foreground transition-colors hover:text-foreground">Security</a>
|
||||
<a href="#tour" class="block font-mono text-[12px] text-muted-foreground transition-colors hover:text-foreground">Tour</a>
|
||||
<a href="#console" class="block font-mono text-[12px] text-muted-foreground transition-colors hover:text-foreground">Console</a>
|
||||
<a href="#architecture" class="block font-mono text-[12px] text-muted-foreground transition-colors hover:text-foreground">Architecture</a>
|
||||
<a href="#download" class="block font-mono text-[12px] text-muted-foreground transition-colors hover:text-foreground">Download</a>
|
||||
<DotMatrixText text="NAVIGATE" size="xs" dotClass="text-muted-foreground" />
|
||||
<div class="mt-2 space-y-1">
|
||||
<a href="#failover-playground" class="block font-mono text-[12px] text-muted-foreground transition-colors hover:text-foreground">Failover demo</a>
|
||||
<a href="#security" class="block font-mono text-[12px] text-muted-foreground transition-colors hover:text-foreground">Security</a>
|
||||
<a href="#tour" class="block font-mono text-[12px] text-muted-foreground transition-colors hover:text-foreground">Tour</a>
|
||||
<a href="#console" class="block font-mono text-[12px] text-muted-foreground transition-colors hover:text-foreground">Console</a>
|
||||
<a href="#architecture" class="block font-mono text-[12px] text-muted-foreground transition-colors hover:text-foreground">Architecture</a>
|
||||
<a href="#download" class="block font-mono text-[12px] text-muted-foreground transition-colors hover:text-foreground">Download</a>
|
||||
</div>
|
||||
</nav>
|
||||
<nav aria-label="Source" class="space-y-2">
|
||||
<p class="font-mono text-[10px] uppercase tracking-[0.14em] text-muted-foreground">Source</p>
|
||||
<a href="#" class="block font-mono text-[12px] text-muted-foreground transition-colors hover:text-foreground">GitHub</a>
|
||||
<a href="#" class="block font-mono text-[12px] text-muted-foreground transition-colors hover:text-foreground">Build from source</a>
|
||||
<DotMatrixText text="SOURCE" size="xs" dotClass="text-muted-foreground" />
|
||||
<div class="mt-2 space-y-1">
|
||||
<a href="#" class="block font-mono text-[12px] text-muted-foreground transition-colors hover:text-foreground">GitHub</a>
|
||||
<a href="#" class="block font-mono text-[12px] text-muted-foreground transition-colors hover:text-foreground">Build from source</a>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-10 flex flex-col gap-2 border-t border-white/[0.06] pt-6 font-mono text-[11px] text-muted-foreground/60 md:flex-row md:items-center md:justify-between">
|
||||
<div class="mt-10 flex flex-col gap-2 border-t border-dotted border-white/10 pt-6 font-mono text-[11px] text-muted-foreground/60 md:flex-row md:items-center md:justify-between">
|
||||
<p>© {year} Clustri. MIT license.</p>
|
||||
<p class="flex items-center gap-2">Press <kbd class="rounded border border-white/[0.08] bg-white/[0.04] px-1.5 py-0.5 text-[10px]">⌘K</kbd> anywhere</p>
|
||||
<p class="flex items-center gap-2">Press <kbd class="rounded border border-dotted border-white/15 bg-white/[0.04] px-1.5 py-0.5 text-[10px]">⌘K</kbd> anywhere</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -1,38 +1,21 @@
|
||||
---
|
||||
import AppWindowMockup from './AppWindowMockup.astro'
|
||||
import DotMatrixText from './DotMatrixText.astro'
|
||||
---
|
||||
|
||||
<section class="relative overflow-hidden border-b border-white/[0.06]">
|
||||
<!-- Subtle grid -->
|
||||
<div class="pointer-events-none absolute inset-0 grid-bg opacity-[0.35] [mask-image:radial-gradient(ellipse_80%_60%_at_50%_0%,black_40%,transparent_85%)]" aria-hidden="true"></div>
|
||||
<section class="relative overflow-hidden border-b border-dotted border-white/10">
|
||||
<div class="pointer-events-none absolute inset-0 dot-grid opacity-[0.18] [mask-image:radial-gradient(ellipse_80%_60%_at_50%_0%,black_40%,transparent_85%)]" aria-hidden="true"></div>
|
||||
|
||||
<!-- Faint topology lines top-right -->
|
||||
<svg class="pointer-events-none absolute right-0 top-0 hidden h-[520px] w-[700px] opacity-[0.07] lg:block" viewBox="0 0 700 520" fill="none" aria-hidden="true">
|
||||
<circle cx="520" cy="120" r="3" fill="white" /><circle cx="620" cy="200" r="3" fill="white" /><circle cx="480" cy="260" r="3" fill="white" />
|
||||
<path d="M520 120 L620 200 M520 120 L480 260 M620 200 L480 260" stroke="white" stroke-width="1" stroke-dasharray="6 6" />
|
||||
<circle cx="520" cy="120" r="18" stroke="white" stroke-width="0.7" opacity="0.5" />
|
||||
<circle cx="620" cy="200" r="18" stroke="white" stroke-width="0.7" opacity="0.5" />
|
||||
<circle cx="480" cy="260" r="18" stroke="white" stroke-width="0.7" opacity="0.5" />
|
||||
<path d="M520 120 L620 200 M520 120 L480 260 M620 200 L480 260" stroke="white" stroke-width="1" stroke-dasharray="2 6" />
|
||||
<circle cx="520" cy="120" r="18" stroke="white" stroke-width="0.7" opacity="0.5" stroke-dasharray="1 4" />
|
||||
<circle cx="620" cy="200" r="18" stroke="white" stroke-width="0.7" opacity="0.5" stroke-dasharray="1 4" />
|
||||
<circle cx="480" cy="260" r="18" stroke="white" stroke-width="0.7" opacity="0.5" stroke-dasharray="1 4" />
|
||||
</svg>
|
||||
|
||||
<div class="relative mx-auto max-w-7xl px-6 lg:px-8">
|
||||
<!-- Copy -->
|
||||
<div class="pt-28 sm:pt-36 lg:pt-40">
|
||||
<div class="flex flex-wrap items-center gap-3 font-mono text-[11px]">
|
||||
<span class="inline-flex items-center gap-1.5 rounded-full border border-success/20 bg-success/10 px-2.5 py-1 text-success">
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-success shadow-[0_0_6px] shadow-success/60"></span>
|
||||
Live
|
||||
</span>
|
||||
<span class="text-muted-foreground">v0.1.0</span>
|
||||
<span class="text-white/20">·</span>
|
||||
<span class="text-muted-foreground">Linux · macOS · Windows</span>
|
||||
<span class="hidden items-center gap-1.5 text-muted-foreground/60 sm:inline-flex">
|
||||
<span class="text-white/10">—</span>
|
||||
<button data-cmdk-trigger class="rounded border border-white/10 px-1.5 py-0.5 text-[10px] hover:border-white/15 hover:text-foreground">⌘K</button>
|
||||
<span>to jump</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h1 class="mt-6 max-w-[14ch] text-[2.6rem] font-semibold leading-[0.92] tracking-[-0.04em] text-foreground sm:text-[3.5rem] lg:text-[4.6rem]">
|
||||
Your cluster,<br />
|
||||
<span class="text-muted-foreground">not your problem.</span>
|
||||
@@ -44,31 +27,22 @@ import AppWindowMockup from './AppWindowMockup.astro'
|
||||
|
||||
<div class="mt-8 flex flex-wrap items-center gap-3">
|
||||
<a href="#download" class="rounded-[10px] bg-primary px-6 py-3 text-sm font-semibold text-primary-foreground transition hover:bg-primary/90 active:scale-[0.98]">Get Clustri</a>
|
||||
<a href="#failover-playground" class="rounded-[10px] border border-white/[0.08] bg-white/[0.04] px-6 py-3 text-sm font-semibold text-foreground transition hover:border-white/15 hover:bg-white/[0.06] active:scale-[0.98]">Try the failover demo ↓</a>
|
||||
<a href="#failover-playground" class="rounded-[10px] border border-dotted border-white/15 bg-white/[0.04] px-6 py-3 text-sm font-semibold text-foreground transition hover:border-white/20 hover:bg-white/[0.06] active:scale-[0.98]">Try the failover demo ↓</a>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex flex-wrap gap-x-4 gap-y-1 font-mono text-[10px] uppercase tracking-[0.08em] text-muted-foreground/70">
|
||||
<span>Rust · Tauri 2</span><span class="text-white/10">/</span><span>React 19 · Tailwind v4</span><span class="text-white/10">/</span><span>MIT licensed</span>
|
||||
<div class="mt-6 flex flex-wrap gap-x-4 gap-y-1 font-mono text-[11px] uppercase tracking-[0.08em] text-muted-foreground">
|
||||
<DotMatrixText text="RUST" size="xs" dotClass="text-foreground" /><span class="text-muted-foreground">· Tauri 2</span><span class="text-white/10">/</span><DotMatrixText text="REACT" size="xs" dotClass="text-foreground" /><span class="text-muted-foreground">· Tailwind v4</span><span class="text-white/10">/</span><DotMatrixText text="MIT" size="xs" dotClass="text-foreground" /><span class="text-muted-foreground">licensed</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mockup -->
|
||||
<div class="mt-12 sm:mt-16">
|
||||
<div class="relative overflow-hidden rounded-xl border border-white/[0.08] bg-card shadow-card" data-parallax="0.06">
|
||||
<!-- thin orange top rule -->
|
||||
<div class="h-px w-full bg-primary/40" aria-hidden="true"></div>
|
||||
<div class="relative overflow-hidden rounded-xl border border-dotted border-white/15 bg-card shadow-card dot-grid" data-parallax="0.06">
|
||||
<div class="h-px w-full bg-primary/40" style="border-top: 1px dotted oklch(0.72 0.16 52 / 0.4)" aria-hidden="true"></div>
|
||||
<div class="p-2 sm:p-3">
|
||||
<AppWindowMockup />
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-3 text-center font-mono text-[10px] text-muted-foreground/60">Live demo — values tick, tasks stream. The real app looks like this.</p>
|
||||
<p class="mt-3 text-center font-mono text-[10px] text-muted-foreground">Live demo — values tick, tasks stream. The real app looks like this.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
@keyframes hero-in {
|
||||
from { opacity: 0; transform: translateY(14px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,37 +1,41 @@
|
||||
---
|
||||
import DotMatrixText from './DotMatrixText.astro'
|
||||
---
|
||||
|
||||
<header class="nav-header fixed top-0 left-0 right-0 z-50 px-4 pt-4">
|
||||
<nav
|
||||
class="nav-pill mx-auto flex h-[46px] max-w-6xl items-center justify-between gap-6 rounded-[12px] border border-white/[0.07] bg-[#0f0f14]/70 px-4 backdrop-blur-xl transition-all duration-400"
|
||||
class="nav-pill mx-auto flex h-[46px] max-w-6xl items-center justify-between rounded-[12px] border border-dotted border-white/15 bg-[#0f0f14]/70 px-5 backdrop-blur-xl transition-all duration-400 dot-grid"
|
||||
aria-label="Primary"
|
||||
>
|
||||
<a href="#" class="flex items-center gap-2.5">
|
||||
<span class="flex h-7 w-7 items-center justify-center rounded-[8px] bg-primary text-primary-foreground">
|
||||
<a href="#" class="group flex shrink-0 items-center gap-2.5">
|
||||
<span class="flex h-7 w-7 items-center justify-center rounded-[8px] border border-dotted border-white/10 bg-primary text-primary-foreground transition-colors group-hover:bg-primary/90">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-3.5 w-3.5" aria-hidden="true">
|
||||
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="text-sm font-semibold tracking-tight text-foreground">Clustri</span>
|
||||
<span class="hidden font-mono text-[9px] uppercase tracking-[0.14em] text-muted-foreground sm:inline">Ops Console</span>
|
||||
<DotMatrixText text="CLUSTRI" size="xs" dotClass="text-foreground group-hover:text-primary transition-colors" />
|
||||
</a>
|
||||
|
||||
<div class="hidden items-center gap-5 md:flex">
|
||||
<a href="#failover-playground" class="font-mono text-[11px] uppercase tracking-[0.08em] text-muted-foreground transition-colors hover:text-foreground">Failover</a>
|
||||
<a href="#security" class="font-mono text-[11px] uppercase tracking-[0.08em] text-muted-foreground transition-colors hover:text-foreground">Security</a>
|
||||
<a href="#tour" class="font-mono text-[11px] uppercase tracking-[0.08em] text-muted-foreground transition-colors hover:text-foreground">Tour</a>
|
||||
<a href="#architecture" class="font-mono text-[11px] uppercase tracking-[0.08em] text-muted-foreground transition-colors hover:text-foreground">Architecture</a>
|
||||
<div class="hidden items-center gap-6 md:flex">
|
||||
<span class="h-3 w-px bg-white/10" aria-hidden="true"></span>
|
||||
<a href="#failover-playground" class="group font-mono text-[11px] tracking-wide text-muted-foreground transition-colors hover:text-foreground">FAILOVER</a>
|
||||
<a href="#security" class="group font-mono text-[11px] tracking-wide text-muted-foreground transition-colors hover:text-foreground">SECURITY</a>
|
||||
<a href="#tour" class="group font-mono text-[11px] tracking-wide text-muted-foreground transition-colors hover:text-foreground">TOUR</a>
|
||||
<a href="#architecture" class="group font-mono text-[11px] tracking-wide text-muted-foreground transition-colors hover:text-foreground">ARCH</a>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<button data-cmdk-trigger class="hidden sm:flex items-center gap-1.5 rounded-md border border-white/[0.06] bg-white/[0.04] px-2.5 py-1.5 font-mono text-[11px] text-muted-foreground transition-colors hover:border-white/10 hover:text-foreground" aria-label="Open command palette">
|
||||
<div class="flex shrink-0 items-center gap-3">
|
||||
<button data-cmdk-trigger class="hidden sm:flex items-center gap-1.5 rounded-md border border-dotted border-white/10 bg-white/[0.04] px-2.5 py-1.5 font-mono text-[11px] text-muted-foreground transition-colors hover:border-white/15 hover:text-foreground" aria-label="Open command palette">
|
||||
<span>⌘K</span>
|
||||
</button>
|
||||
<a href="#download" class="rounded-[10px] bg-primary px-4 py-2 text-[13px] font-semibold text-primary-foreground transition-all hover:bg-primary/90 active:scale-[0.98]">Get Clustri</a>
|
||||
<a href="#download" class="rounded-[10px] border border-dotted border-primary/30 bg-primary px-4 py-2 text-[13px] font-semibold text-primary-foreground transition-all hover:bg-primary/90 active:scale-[0.98]">Get Clustri</a>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
.nav-scrolled .nav-pill {
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
background-color: oklch(0.11 0.008 275 / 0.85);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
background-color: oklch(0.145 0.012 250 / 0.85);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
---
|
||||
import DotMatrixText from './DotMatrixText.astro'
|
||||
---
|
||||
<section id="tour" aria-labelledby="tour-title" class="scroll-mt-24 py-20 sm:py-28">
|
||||
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
<div class="flex flex-wrap items-end justify-between gap-6">
|
||||
<div data-reveal>
|
||||
<p class="font-mono text-[11px] uppercase tracking-[0.18em] text-primary">Tour</p>
|
||||
<DotMatrixText text="TOUR" size="xs" dotClass="text-primary" />
|
||||
<h2 id="tour-title" class="mt-4 max-w-[14ch] text-3xl font-semibold tracking-[-0.03em] text-foreground sm:text-4xl">
|
||||
Feels like the Proxmox UI,<br /><span class="text-muted-foreground">without the browser.</span>
|
||||
</h2>
|
||||
@@ -12,7 +15,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="flex gap-1 rounded-lg border border-white/[0.06] bg-card p-1" role="tablist" aria-label="Screenshot tour" data-reveal data-reveal-delay="100">
|
||||
<div class="flex gap-1 rounded-lg border border-dotted border-white/10 bg-card p-1" role="tablist" aria-label="Screenshot tour" data-reveal data-reveal-delay="100">
|
||||
<button role="tab" aria-selected="true" data-tour-tab="vms" class="rounded-md bg-primary px-3.5 py-1.5 font-mono text-[11px] font-semibold text-primary-foreground">VMs</button>
|
||||
<button role="tab" aria-selected="false" data-tour-tab="console" class="rounded-md px-3.5 py-1.5 font-mono text-[11px] font-medium text-muted-foreground hover:text-foreground">Console</button>
|
||||
<button role="tab" aria-selected="false" data-tour-tab="backups" class="rounded-md px-3.5 py-1.5 font-mono text-[11px] font-medium text-muted-foreground hover:text-foreground">Backups</button>
|
||||
@@ -20,12 +23,12 @@
|
||||
</div>
|
||||
|
||||
<!-- Frame -->
|
||||
<div class="mt-10 overflow-hidden rounded-xl border border-white/[0.07] bg-card shadow-card" data-reveal data-reveal-delay="160">
|
||||
<div class="flex h-9 items-center justify-between border-b border-white/[0.06] bg-background px-4">
|
||||
<div class="mt-10 overflow-hidden rounded-xl border border-dotted border-white/10 bg-card shadow-card" data-reveal data-reveal-delay="160">
|
||||
<div class="flex h-9 items-center justify-between border-b border-dotted border-white/10 bg-background px-4">
|
||||
<div class="flex items-center gap-1.5" aria-hidden="true">
|
||||
<span class="h-2.5 w-2.5 rounded-full border border-white/[0.06] bg-white/10"></span>
|
||||
<span class="h-2.5 w-2.5 rounded-full border border-white/[0.06] bg-white/10"></span>
|
||||
<span class="h-2.5 w-2.5 rounded-full border border-white/[0.06] bg-white/10"></span>
|
||||
<span class="h-2.5 w-2.5 rounded-full border border-dotted border-white/10 bg-white/10"></span>
|
||||
<span class="h-2.5 w-2.5 rounded-full border border-dotted border-white/10 bg-white/10"></span>
|
||||
<span class="h-2.5 w-2.5 rounded-full border border-dotted border-white/10 bg-white/10"></span>
|
||||
</div>
|
||||
<p id="tour-frame-label" class="font-mono text-[10px] tabular-nums text-muted-foreground">Virtual Machines — 4 VMs across the cluster</p>
|
||||
<span class="hidden font-mono text-[10px] text-success sm:inline">● live</span>
|
||||
@@ -36,16 +39,16 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid gap-3 sm:grid-cols-3">
|
||||
<div class="rounded-lg border border-white/[0.06] bg-card p-4" data-reveal>
|
||||
<p class="font-mono text-[11px] font-medium uppercase tracking-[0.08em] text-foreground">VMs & Containers</p>
|
||||
<p class="mt-1.5 text-[13px] leading-relaxed text-muted-foreground">Filter by node, type, status. Start / stop / migrate / console in one click. QEMU and LXC.</p>
|
||||
<div class="rounded-lg border border-dotted border-white/10 bg-card p-4 dot-grid" data-reveal>
|
||||
<DotMatrixText text="VMS + CONTAINERS" size="xs" dotClass="text-foreground" />
|
||||
<p class="mt-2 text-[13px] leading-relaxed text-muted-foreground">Filter by node, type, status. Start / stop / migrate / console in one click. QEMU and LXC.</p>
|
||||
</div>
|
||||
<div class="rounded-lg border border-white/[0.06] bg-card p-4" data-reveal data-reveal-delay="80">
|
||||
<p class="font-mono text-[11px] font-medium uppercase tracking-[0.08em] text-foreground">Console</p>
|
||||
<p class="mt-1.5 text-[13px] leading-relaxed text-muted-foreground">noVNC for VMs, xterm.js for CTs. Fullscreen, resize, Ctrl+Alt+Del passthrough.</p>
|
||||
<div class="rounded-lg border border-dotted border-white/10 bg-card p-4 dot-grid" data-reveal data-reveal-delay="80">
|
||||
<DotMatrixText text="CONSOLE" size="xs" dotClass="text-foreground" />
|
||||
<p class="mt-2 text-[13px] leading-relaxed text-muted-foreground">noVNC for VMs, xterm.js for CTs. Fullscreen, resize, Ctrl+Alt+Del passthrough.</p>
|
||||
</div>
|
||||
<div class="rounded-lg border border-white/[0.06] bg-card p-4" data-reveal data-reveal-delay="160">
|
||||
<p class="font-mono text-[11px] font-medium uppercase tracking-[0.08em] text-foreground">Backups</p>
|
||||
<div class="rounded-lg border border-dotted border-white/10 bg-card p-4 dot-grid" data-reveal data-reveal-delay="160">
|
||||
<DotMatrixText text="BACKUPS" size="xs" dotClass="text-foreground" />
|
||||
<p class="mt-1.5 text-[13px] leading-relaxed text-muted-foreground">Jobs, groups, snapshots. Verify, prune (dry-run), GC, archive download via OS dialog.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,32 +1,34 @@
|
||||
<section aria-label="Facts" class="border-y border-white/[0.06] bg-[#08080a]">
|
||||
---
|
||||
import DotMatrixText from './DotMatrixText.astro'
|
||||
---
|
||||
<section aria-label="Facts" class="border-y border-dotted border-white/10 bg-[#08080a] dot-grid">
|
||||
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
<div class="grid grid-cols-2 gap-0 py-8 sm:grid-cols-4" data-stagger>
|
||||
<div class="flex flex-col items-center gap-1 border-r border-white/[0.06] px-2 py-6" data-stagger-child>
|
||||
<div class="flex flex-col items-center gap-2 border-r border-dotted border-white/10 px-2 py-6" data-stagger-child>
|
||||
<span class="font-mono text-3xl font-semibold tracking-[-0.03em] tabular-nums text-foreground sm:text-4xl" data-countup="0">0</span>
|
||||
<span class="font-mono text-[10px] uppercase tracking-[0.14em] text-muted-foreground">Telemetry</span>
|
||||
<span class="font-mono text-[10px] text-muted-foreground/60">ships with the app</span>
|
||||
<DotMatrixText text="TELEMETRY" size="xs" dotClass="text-muted-foreground" />
|
||||
<span class="font-mono text-[11px] text-muted-foreground">ships with the app</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-1 border-r border-white/[0.06] px-2 py-6 sm:border-r" data-stagger-child>
|
||||
<div class="flex flex-col items-center gap-2 border-r border-dotted border-white/10 px-2 py-6 sm:border-r" data-stagger-child>
|
||||
<span class="font-mono text-3xl font-semibold tracking-[-0.03em] tabular-nums text-foreground sm:text-4xl" data-countup="0">0</span>
|
||||
<span class="font-mono text-[10px] uppercase tracking-[0.14em] text-muted-foreground">Cloud dependency</span>
|
||||
<span class="font-mono text-[10px] text-muted-foreground/60">talks to your nodes</span>
|
||||
<DotMatrixText text="CLOUD" size="xs" dotClass="text-muted-foreground" />
|
||||
<span class="font-mono text-[11px] text-muted-foreground">talks to your nodes</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-1 border-r border-white/[0.06] px-2 py-6 max-sm:border-r-0" data-stagger-child>
|
||||
<div class="flex flex-col items-center gap-2 border-r border-dotted border-white/10 px-2 py-6 max-sm:border-r-0" data-stagger-child>
|
||||
<span class="font-mono text-3xl font-semibold tracking-[-0.03em] tabular-nums text-primary sm:text-4xl" data-countup="64">0</span>
|
||||
<span class="font-mono text-[10px] uppercase tracking-[0.14em] text-muted-foreground">Rust tests</span>
|
||||
<span class="font-mono text-[10px] text-muted-foreground/60">httpmock · rcgen · tempfile</span>
|
||||
<DotMatrixText text="RUST TESTS" size="xs" dotClass="text-muted-foreground" />
|
||||
<span class="font-mono text-[11px] text-muted-foreground">httpmock · rcgen · tempfile</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-1 px-2 py-6" data-stagger-child>
|
||||
<div class="flex flex-col items-center gap-2 px-2 py-6" data-stagger-child>
|
||||
<span class="font-mono text-3xl font-semibold tracking-[-0.03em] tabular-nums text-foreground sm:text-4xl" data-countup="3">0</span>
|
||||
<span class="font-mono text-[10px] uppercase tracking-[0.14em] text-muted-foreground">Platforms</span>
|
||||
<span class="font-mono text-[10px] text-muted-foreground/60">Linux · macOS · Windows</span>
|
||||
<DotMatrixText text="PLATFORMS" size="xs" dotClass="text-muted-foreground" />
|
||||
<span class="font-mono text-[11px] text-muted-foreground">Linux · macOS · Windows</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- mono ticker -->
|
||||
<div class="overflow-hidden border-t border-white/[0.06] py-3" aria-hidden="true">
|
||||
<div class="flex gap-6 whitespace-nowrap font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground/40">
|
||||
<span>start</span><span class="text-white/10">·</span><span>stop</span><span class="text-white/10">·</span><span>shutdown</span><span class="text-white/10">·</span><span>reboot</span><span class="text-white/10">·</span><span>suspend</span><span class="text-white/10">·</span><span>resume</span><span class="text-white/10">·</span><span>migrate</span><span class="text-white/10">·</span><span>snapshot</span><span class="text-white/10">·</span><span>backup</span><span class="text-white/10">·</span><span>restore</span><span class="text-white/10">·</span><span>prune</span><span class="text-white/10">·</span><span>GC</span><span class="text-white/10">·</span><span>noVNC</span><span class="text-white/10">·</span><span>xterm.js</span>
|
||||
<div class="overflow-hidden border-t border-dotted border-white/10 py-3" aria-hidden="true">
|
||||
<div class="flex gap-6 whitespace-nowrap font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground/50">
|
||||
<DotMatrixText text="START / STOP / SHUTDOWN / REBOOT / SUSPEND / RESUME / MIGRATE / SNAPSHOT / BACKUP / RESTORE / PRUNE / GC / NOVNC / XTERM.JS" size="xs" dotClass="text-muted-foreground/50" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
export type Glyph = readonly string[]
|
||||
|
||||
export const GLYPH_COLS = 5
|
||||
export const GLYPH_ROWS = 7
|
||||
|
||||
export const GLYPHS: Record<string, Glyph> = {
|
||||
' ': [' ', ' ', ' ', ' ', ' ', ' ', ' '],
|
||||
A: [' ### ', '# #', '# #', '#####', '# #', '# #', '# #'],
|
||||
B: ['#### ', '# #', '# #', '#### ', '# #', '# #', '#### '],
|
||||
C: [' ### ', '# #', '# ', '# ', '# ', '# #', ' ### '],
|
||||
D: ['#### ', '# #', '# #', '# #', '# #', '# #', '#### '],
|
||||
E: ['#####', '# ', '# ', '#### ', '# ', '# ', '#####'],
|
||||
F: ['#####', '# ', '# ', '#### ', '# ', '# ', '# '],
|
||||
G: [' ### ', '# #', '# ', '# ## ', '# #', '# #', ' ### '],
|
||||
H: ['# #', '# #', '# #', '#####', '# #', '# #', '# #'],
|
||||
I: ['#####', ' # ', ' # ', ' # ', ' # ', ' # ', '#####'],
|
||||
J: ['#####', ' # ', ' # ', ' # ', ' # ', '# # ', ' ## '],
|
||||
K: ['# #', '# # ', '# # ', '## ', '# # ', '# # ', '# #'],
|
||||
L: ['# ', '# ', '# ', '# ', '# ', '# ', '#####'],
|
||||
M: ['# #', '## ##', '# # #', '# # #', '# #', '# #', '# #'],
|
||||
N: ['# #', '## #', '# # #', '# # #', '# ##', '# #', '# #'],
|
||||
O: [' ### ', '# #', '# #', '# #', '# #', '# #', ' ### '],
|
||||
P: ['#### ', '# #', '# #', '#### ', '# ', '# ', '# '],
|
||||
Q: [' ### ', '# #', '# #', '# #', '# # #', '# # ', ' ## #'],
|
||||
R: ['#### ', '# #', '# #', '#### ', '# # ', '# # ', '# #'],
|
||||
S: [' ### ', '# #', '# ', ' ### ', ' #', '# #', ' ### '],
|
||||
T: ['#####', ' # ', ' # ', ' # ', ' # ', ' # ', ' # '],
|
||||
U: ['# #', '# #', '# #', '# #', '# #', '# #', ' ### '],
|
||||
V: ['# #', '# #', '# #', '# #', '# #', ' # # ', ' # '],
|
||||
W: ['# #', '# #', '# #', '# # #', '# # #', '## ##', '# #'],
|
||||
X: ['# #', '# #', ' # # ', ' # ', ' # # ', '# #', '# #'],
|
||||
Y: ['# #', '# #', ' # # ', ' # ', ' # ', ' # ', ' # '],
|
||||
Z: ['#####', ' #', ' # ', ' # ', ' # ', '# ', '#####'],
|
||||
'0': [' ### ', '# #', '# ##', '# # #', '## #', '# #', ' ### '],
|
||||
'1': [' # ', ' ## ', ' # ', ' # ', ' # ', ' # ', '#####'],
|
||||
'2': [' ### ', '# #', ' #', ' ## ', ' # ', '# ', '#####'],
|
||||
'3': ['#### ', ' #', ' #', ' ### ', ' #', ' #', '#### '],
|
||||
'4': [' # ', ' ## ', ' # # ', '# # ', '#####', ' # ', ' # '],
|
||||
'5': ['#####', '# ', '# ', '#### ', ' #', '# #', ' ### '],
|
||||
'6': [' ### ', '# #', '# ', '#### ', '# #', '# #', ' ### '],
|
||||
'7': ['#####', ' #', ' # ', ' # ', ' # ', ' # ', ' # '],
|
||||
'8': [' ### ', '# #', '# #', ' ### ', '# #', '# #', ' ### '],
|
||||
'9': [' ### ', '# #', '# #', ' ####', ' #', '# #', ' ### '],
|
||||
'-': [' ', ' ', ' ', '#####', ' ', ' ', ' '],
|
||||
'.': [' ', ' ', ' ', ' ', ' ', ' ## ', ' ## '],
|
||||
':': [' ', ' ## ', ' ## ', ' ', ' ## ', ' ## ', ' '],
|
||||
'/': [' #', ' #', ' # ', ' # ', ' # ', '# ', '# '],
|
||||
'%': ['## #', '## #', ' # ', ' # ', ' # ', '# ##', '# ##'],
|
||||
'(': [' ## ', ' # ', '# ', '# ', '# ', ' # ', ' ## '],
|
||||
')': [' ## ', ' # ', ' #', ' #', ' #', ' # ', ' ## '],
|
||||
'+': [' ', ' # ', ' # ', '#####', ' # ', ' # ', ' '],
|
||||
_: [' ', ' ', ' ', ' ', ' ', ' ', '#####'],
|
||||
'@': [' ### ', '# #', '# ## ', '# ## ', '# ## ', '# ', ' ### '],
|
||||
'#': [' # # ', ' # # ', '#####', ' # # ', '#####', ' # # ', ' # # '],
|
||||
'!': [' # ', ' # ', ' # ', ' # ', ' # ', ' ', ' # '],
|
||||
'?': [' ### ', '# #', ' #', ' ## ', ' # ', ' ', ' # '],
|
||||
'*': [' ', '# #', ' # # ', '#####', ' # # ', '# #', ' '],
|
||||
'=': [' ', ' ', '#####', ' ', '#####', ' ', ' '],
|
||||
',': [' ', ' ', ' ', ' ', ' ## ', ' ## ', ' # '],
|
||||
"'": [' # ', ' # ', ' # ', ' ', ' ', ' ', ' '],
|
||||
'"': [' # # ', ' # # ', ' ', ' ', ' ', ' ', ' '],
|
||||
'[': [' ### ', ' # ', ' # ', ' # ', ' # ', ' # ', ' ### '],
|
||||
']': [' ### ', ' # ', ' # ', ' # ', ' # ', ' # ', ' ### '],
|
||||
'<': [' # ', ' # ', ' # ', '# ', ' # ', ' # ', ' # '],
|
||||
'>': ['# ', ' # ', ' # ', ' # ', ' # ', ' # ', '# '],
|
||||
'|': [' # ', ' # ', ' # ', ' # ', ' # ', ' # ', ' # '],
|
||||
'&': [' ## ', '# # ', '# # ', ' ## ', '# # ', '# # ', ' ## #'],
|
||||
$: [' ### ', '# # ', '# ', ' ### ', ' #', ' # #', ' ### '],
|
||||
}
|
||||
|
||||
export function getGlyph(char: string): Glyph | null {
|
||||
const upper = char.toUpperCase()
|
||||
return GLYPHS[upper] ?? null
|
||||
}
|
||||
+107
-29
@@ -14,25 +14,25 @@
|
||||
--radius-lg: 0.625rem;
|
||||
--radius-xl: 0.75rem;
|
||||
|
||||
/* Dark — near-black with warm charcoal, not indigo wash */
|
||||
--color-background: oklch(0.11 0.008 275);
|
||||
--color-foreground: oklch(0.965 0.004 275);
|
||||
--color-card: oklch(0.155 0.012 275);
|
||||
--color-card-foreground: oklch(0.965 0.004 275);
|
||||
--color-popover: oklch(0.18 0.014 275);
|
||||
--color-popover-foreground: oklch(0.965 0.004 275);
|
||||
/* Dark paper — dark ink-paper, light ink, Proxmox orange ribbon */
|
||||
--color-background: oklch(0.145 0.012 250);
|
||||
--color-foreground: oklch(0.96 0.005 85);
|
||||
--color-card: oklch(0.182 0.014 250);
|
||||
--color-card-foreground: oklch(0.96 0.005 85);
|
||||
--color-popover: oklch(0.2 0.015 250);
|
||||
--color-popover-foreground: oklch(0.96 0.005 85);
|
||||
--color-primary: oklch(0.72 0.16 52);
|
||||
--color-primary-foreground: oklch(0.14 0.02 50);
|
||||
--color-secondary: oklch(0.19 0.012 275);
|
||||
--color-secondary-foreground: oklch(0.94 0.004 275);
|
||||
--color-muted: oklch(0.19 0.012 275);
|
||||
--color-muted-foreground: oklch(0.6 0.012 275);
|
||||
--color-accent: oklch(0.22 0.012 275);
|
||||
--color-accent-foreground: oklch(0.965 0.004 275);
|
||||
--color-secondary: oklch(0.22 0.014 250);
|
||||
--color-secondary-foreground: oklch(0.92 0.004 85);
|
||||
--color-muted: oklch(0.22 0.014 250);
|
||||
--color-muted-foreground: oklch(0.68 0.012 250);
|
||||
--color-accent: oklch(0.24 0.014 250);
|
||||
--color-accent-foreground: oklch(0.96 0.005 85);
|
||||
--color-destructive: oklch(0.62 0.18 25);
|
||||
--color-destructive-foreground: oklch(0.97 0.015 25);
|
||||
--color-border: oklch(0.21 0.01 275);
|
||||
--color-input: oklch(0.24 0.01 275);
|
||||
--color-border: oklch(0.26 0.014 250);
|
||||
--color-input: oklch(0.28 0.015 250);
|
||||
--color-ring: oklch(0.72 0.16 52);
|
||||
|
||||
--color-success: oklch(0.7 0.14 155);
|
||||
@@ -40,25 +40,103 @@
|
||||
--color-info: oklch(0.68 0.11 245);
|
||||
|
||||
--shadow-card:
|
||||
0 1px 0 0 oklch(1 0 0 / 0.04) inset, 0 1px 2px 0 oklch(0 0 0 / 0.4),
|
||||
0 1px 0 0 oklch(1 0 0 / 0.05) inset, 0 1px 2px 0 oklch(0 0 0 / 0.45),
|
||||
0 8px 24px -8px oklch(0 0 0 / 0.5);
|
||||
}
|
||||
|
||||
/* Grid texture — 1px hairline grid at very low opacity */
|
||||
.grid-bg {
|
||||
background-image:
|
||||
linear-gradient(to right, oklch(1 0 0 / 0.04) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, oklch(1 0 0 / 0.04) 1px, transparent 1px);
|
||||
background-size: 32px 32px;
|
||||
/* Dot-matrix texture utilities */
|
||||
.dot-grid {
|
||||
background-image: radial-gradient(
|
||||
circle,
|
||||
oklch(1 0 0 / 0.07) 1px,
|
||||
transparent 1px
|
||||
);
|
||||
background-size: 14px 14px;
|
||||
}
|
||||
.grid-bg-strong {
|
||||
background-image:
|
||||
linear-gradient(to right, oklch(1 0 0 / 0.06) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, oklch(1 0 0 / 0.06) 1px, transparent 1px);
|
||||
background-size: 24px 24px;
|
||||
.dot-grid-strong {
|
||||
background-image: radial-gradient(
|
||||
circle,
|
||||
oklch(1 0 0 / 0.1) 1.1px,
|
||||
transparent 1.1px
|
||||
);
|
||||
background-size: 10px 10px;
|
||||
}
|
||||
.dot-grid-paper {
|
||||
background-image: radial-gradient(
|
||||
circle,
|
||||
oklch(1 0 0 / 0.045) 1px,
|
||||
transparent 1px
|
||||
);
|
||||
background-size: 16px 16px;
|
||||
}
|
||||
.dot-rule {
|
||||
border: none;
|
||||
border-top: 1.5px dotted oklch(1 0 0 / 0.12);
|
||||
height: 0;
|
||||
}
|
||||
.perf-edge {
|
||||
position: relative;
|
||||
}
|
||||
.perf-edge::before,
|
||||
.perf-edge::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
bottom: 8px;
|
||||
width: 6px;
|
||||
background-image: radial-gradient(
|
||||
circle,
|
||||
oklch(1 0 0 / 0.1) 2px,
|
||||
transparent 2px
|
||||
);
|
||||
background-size: 6px 12px;
|
||||
background-repeat: repeat-y;
|
||||
pointer-events: none;
|
||||
}
|
||||
.perf-edge::before { left: 4px; }
|
||||
.perf-edge::after { right: 4px; }
|
||||
|
||||
.halftone-fill {
|
||||
position: relative;
|
||||
}
|
||||
.halftone-fill::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: radial-gradient(
|
||||
circle,
|
||||
oklch(1 0 0 / 0.35) 0.9px,
|
||||
transparent 0.9px
|
||||
);
|
||||
background-size: 5px 5px;
|
||||
opacity: 0.9;
|
||||
pointer-events: none;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.feed-rule {
|
||||
border-bottom: 1px dotted oklch(1 0 0 / 0.08);
|
||||
}
|
||||
.feed-rule:last-child { border-bottom: none; }
|
||||
|
||||
/* Legacy grid-bg kept as alias to dot-grid for compat */
|
||||
.grid-bg {
|
||||
background-image: radial-gradient(
|
||||
circle,
|
||||
oklch(1 0 0 / 0.07) 1px,
|
||||
transparent 1px
|
||||
);
|
||||
background-size: 14px 14px;
|
||||
}
|
||||
.grid-bg-strong {
|
||||
background-image: radial-gradient(
|
||||
circle,
|
||||
oklch(1 0 0 / 0.1) 1.1px,
|
||||
transparent 1.1px
|
||||
);
|
||||
background-size: 10px 10px;
|
||||
}
|
||||
|
||||
/* Hairline rule utility */
|
||||
.hairline {
|
||||
border-color: oklch(1 0 0 / 0.06);
|
||||
}
|
||||
@@ -193,7 +271,7 @@
|
||||
inset: 0;
|
||||
z-index: 60;
|
||||
pointer-events: none;
|
||||
opacity: 0.025;
|
||||
opacity: 0.03;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23n)'/%3E%3C/svg%3E");
|
||||
background-repeat: repeat;
|
||||
}
|
||||
|
||||
+11
-12
@@ -18,6 +18,7 @@ import { PbsDatastoreDetail } from '@/components/pbs/PbsDatastoreDetail'
|
||||
import { SettingsPage } from '@/components/settings/SettingsPage'
|
||||
import { ErrorBoundary } from '@/components/ErrorBoundary'
|
||||
import { ToastProvider, useToast } from '@/components/ui/toast'
|
||||
import { DotMatrixText } from '@/components/ui/dot-matrix'
|
||||
import { Server, AlertTriangle } from 'lucide-react'
|
||||
import { useConnectionStore } from '@/stores/connectionStore'
|
||||
import { useUIStore } from '@/stores/uiStore'
|
||||
@@ -236,15 +237,13 @@ function AppContent() {
|
||||
|
||||
if (!activeConnectionId) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center p-6">
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<div className="mb-4 flex h-12 w-12 items-center justify-center rounded-lg border border-border bg-card shadow-card">
|
||||
<div className="flex h-full items-center justify-center p-6 dot-grid">
|
||||
<div className="flex flex-col items-center text-center rounded-lg border border-dotted border-border bg-card p-8 shadow-card">
|
||||
<div className="mb-4 flex h-12 w-12 items-center justify-center rounded-sm border border-dotted border-border bg-muted/40">
|
||||
<Server className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold tracking-tight">
|
||||
No connection selected
|
||||
</h2>
|
||||
<p className="mt-1 max-w-sm text-sm text-muted-foreground">
|
||||
<DotMatrixText text="NO CONNECTION" size="sm" className="text-foreground" />
|
||||
<p className="mt-2 max-w-sm text-sm text-muted-foreground">
|
||||
Connect to a Proxmox server to get started
|
||||
</p>
|
||||
</div>
|
||||
@@ -375,11 +374,11 @@ function AppContent() {
|
||||
}, [activeConnectionId, activeServerType, view.type])
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col bg-background">
|
||||
<div className="flex h-screen flex-col bg-background dot-grid">
|
||||
{activeConnection?.status === 'failover' && activeConnection.currentEndpointUrl && (
|
||||
<div className="flex items-center gap-2 border-b border-warning/25 bg-warning/10 px-4 py-1.5">
|
||||
<div className="flex items-center gap-2 border-b border-dotted border-warning/30 bg-warning/10 px-4 py-1.5 dot-grid">
|
||||
<AlertTriangle className="h-3.5 w-3.5 shrink-0 text-warning" />
|
||||
<p className="text-xs font-medium text-warning">Operating on a fallback node</p>
|
||||
<DotMatrixText text="FALLBACK NODE" size="xs" className="text-warning" />
|
||||
<p className="truncate font-mono text-[11px] tabular-nums text-warning/80">
|
||||
{activeConnection.currentEndpointUrl}
|
||||
</p>
|
||||
@@ -391,9 +390,9 @@ function AppContent() {
|
||||
activeView={view.type}
|
||||
onNavigate={(v) => handleNavigate(v as View)}
|
||||
/>
|
||||
<main className="flex min-h-0 flex-1 flex-col">
|
||||
<main className="flex min-h-0 flex-1 flex-col bg-background/60">
|
||||
{activeConnectionId && (
|
||||
<div className="shrink-0 border-b px-3 py-1.5">
|
||||
<div className="shrink-0 border-b border-dotted border-border px-3 py-1.5 dot-grid">
|
||||
<TaskStatusBar
|
||||
connectionId={activeConnectionId}
|
||||
onClick={() => handleNavigate({ type: 'tasks' })}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Card, CardContent, CardHeader } from '@/components/ui/card'
|
||||
import { DotMatrixText } from '@/components/ui/dot-matrix'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Activity, Clock, Loader2 } from 'lucide-react'
|
||||
@@ -16,20 +17,20 @@ function isRunningTask(status?: string): boolean {
|
||||
function getStatusBadgeClass(status?: string): string {
|
||||
switch (status) {
|
||||
case 'OK':
|
||||
return 'border-success/25 bg-success/10 text-success'
|
||||
return 'border-dotted border-success/30 bg-success/10 text-success'
|
||||
case 'unknown':
|
||||
return 'border-warning/25 bg-warning/10 text-warning'
|
||||
return 'border-dotted border-warning/30 bg-warning/10 text-warning'
|
||||
default:
|
||||
if (!status || status === 'Running') {
|
||||
return 'border-info/25 bg-info/10 text-info'
|
||||
return 'border-dotted border-info/30 bg-info/10 text-info'
|
||||
}
|
||||
return 'border-destructive/25 bg-destructive/10 text-destructive'
|
||||
return 'border-dotted border-destructive/30 bg-destructive/10 text-destructive'
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusLabel(status?: string): string {
|
||||
if (!status || status === 'Running') return 'Running'
|
||||
return status
|
||||
if (!status || status === 'Running') return 'RUNNING'
|
||||
return status.toUpperCase()
|
||||
}
|
||||
|
||||
function formatDuration(starttime: number, endtime?: number): string {
|
||||
@@ -52,7 +53,6 @@ function formatTime(timestamp: number): string {
|
||||
}
|
||||
|
||||
function getTaskTypeLabel(type: string): string {
|
||||
// Capitalize and format Proxmox task types
|
||||
return type
|
||||
.replace(/qm/gi, 'VM')
|
||||
.replace(/vz/gi, 'CT')
|
||||
@@ -68,16 +68,16 @@ function getTaskTypeLabel(type: string): string {
|
||||
export function ActivityFeed({ tasks, isLoading }: ActivityFeedProps) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Card className="dot-grid">
|
||||
<CardHeader className="border-b border-dotted border-border/60">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="flex h-7 w-7 items-center justify-center rounded-md border border-border bg-muted/40">
|
||||
<div className="flex h-7 w-7 items-center justify-center rounded-sm border border-dotted border-border bg-muted/40">
|
||||
<Activity className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
<CardTitle className="text-sm font-semibold">Recent Activity</CardTitle>
|
||||
<DotMatrixText text="RECENT ACTIVITY" size="xs" className="text-foreground" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent className="pt-4">
|
||||
<div className="space-y-2">
|
||||
{Array.from({ length: 5 }, (_, i) => (
|
||||
<Skeleton key={i} className="h-12 w-full" />
|
||||
@@ -91,30 +91,30 @@ export function ActivityFeed({ tasks, isLoading }: ActivityFeedProps) {
|
||||
const recentTasks = tasks?.slice(0, 15) ?? []
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Card className="dot-grid">
|
||||
<CardHeader className="border-b border-dotted border-border/60">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="flex h-7 w-7 items-center justify-center rounded-md border border-border bg-muted/40">
|
||||
<div className="flex h-7 w-7 items-center justify-center rounded-sm border border-dotted border-border bg-muted/40">
|
||||
<Activity className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
<CardTitle className="text-sm font-semibold">Recent Activity</CardTitle>
|
||||
<DotMatrixText text="RECENT ACTIVITY" size="xs" className="text-foreground" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent className="pt-4">
|
||||
{recentTasks.length > 0 ? (
|
||||
<ScrollArea className="h-[300px]">
|
||||
<div className="space-y-1.5">
|
||||
<div className="space-y-0">
|
||||
{recentTasks.map((task) => (
|
||||
<div
|
||||
key={task.upid}
|
||||
className="flex items-center justify-between rounded-md border border-border/70 bg-background/30 px-3 py-2.5 transition-colors duration-150 hover:border-border hover:bg-accent/40"
|
||||
className="flex items-center justify-between px-3 py-2.5 transition-colors duration-150 hover:bg-accent/30 feed-rule"
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 items-center gap-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="truncate text-[13px] font-medium">{getTaskTypeLabel(task.type)}</p>
|
||||
<span
|
||||
className={`inline-flex shrink-0 items-center gap-1 rounded-sm border px-1.5 py-0.5 text-[11px] font-medium ${getStatusBadgeClass(task.status)}`}
|
||||
className={`inline-flex shrink-0 items-center gap-1 rounded-sm border border-dotted px-1.5 py-0.5 text-[10px] font-medium tracking-widest ${getStatusBadgeClass(task.status)}`}
|
||||
>
|
||||
{isRunningTask(task.status) && (
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useMemo } from 'react'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Card, CardContent, CardHeader } from '@/components/ui/card'
|
||||
import { DotMatrixText } from '@/components/ui/dot-matrix'
|
||||
import { Cpu, MemoryStick, HardDrive, Clock, Server, Box } from 'lucide-react'
|
||||
import type { ProxmoxNode, ProxmoxVM } from '@/types/proxmox'
|
||||
import { formatBytes, formatUptime } from '@/lib/format'
|
||||
@@ -20,30 +21,23 @@ function getStatusTextColor(status: 'online' | 'offline'): string {
|
||||
return status === 'online' ? 'text-success' : 'text-destructive'
|
||||
}
|
||||
|
||||
function getStatusLabel(status: 'online' | 'offline'): string {
|
||||
return status === 'online' ? 'Online' : 'Offline'
|
||||
}
|
||||
|
||||
function NodeCard({ node, vmCount }: { node: ProxmoxNode; vmCount: number }) {
|
||||
const cpuPercent = node.maxcpu > 0 ? node.cpu : 0
|
||||
const memPercent = node.maxmem > 0 ? node.mem / node.maxmem : 0
|
||||
const diskPercent = node.maxdisk > 0 ? node.disk / node.maxdisk : 0
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-3">
|
||||
<Card className="dot-grid">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-3 border-b border-dotted border-border/60">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span className={cn('h-2 w-2 rounded-full', getStatusColor(node.status))} />
|
||||
<CardTitle className="font-mono text-sm font-semibold tracking-tight">
|
||||
<span className="font-mono text-sm font-semibold tracking-tight text-foreground">
|
||||
{node.node}
|
||||
</CardTitle>
|
||||
</span>
|
||||
</div>
|
||||
<span className={cn('text-xs font-medium', getStatusTextColor(node.status))}>
|
||||
{getStatusLabel(node.status)}
|
||||
</span>
|
||||
<DotMatrixText text={node.status === 'online' ? 'ONLINE' : 'OFFLINE'} size="xs" className={getStatusTextColor(node.status)} />
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{/* Stats row */}
|
||||
<CardContent className="space-y-3 pt-3">
|
||||
<div className="grid grid-cols-3 gap-2 font-mono text-xs tabular-nums text-muted-foreground">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Cpu className="h-3 w-3 shrink-0" />
|
||||
@@ -58,9 +52,7 @@ function NodeCard({ node, vmCount }: { node: ProxmoxNode; vmCount: number }) {
|
||||
<span>{(diskPercent * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom info */}
|
||||
<div className="flex items-center justify-between border-t pt-2.5 font-mono text-xs tabular-nums text-muted-foreground">
|
||||
<div className="flex items-center justify-between border-t border-dotted border-border/60 pt-2.5 font-mono text-xs tabular-nums text-muted-foreground">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Clock className="h-3 w-3 shrink-0" />
|
||||
<span>{formatUptime(node.uptime)}</span>
|
||||
@@ -80,7 +72,6 @@ function NodeCard({ node, vmCount }: { node: ProxmoxNode; vmCount: number }) {
|
||||
}
|
||||
|
||||
export function NodeHealthGrid({ nodes, vms }: NodeHealthGridProps) {
|
||||
// Count VMs per node
|
||||
const vmCounts = useMemo(() => {
|
||||
const counts: Record<string, number> = {}
|
||||
vms?.forEach((vm) => {
|
||||
@@ -91,13 +82,13 @@ export function NodeHealthGrid({ nodes, vms }: NodeHealthGridProps) {
|
||||
|
||||
if (!nodes || nodes.length === 0) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Card className="dot-grid">
|
||||
<CardHeader className="border-b border-dotted border-border/60">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="flex h-7 w-7 items-center justify-center rounded-md border border-border bg-muted/40">
|
||||
<div className="flex h-7 w-7 items-center justify-center rounded-sm border border-dotted border-border bg-muted/40">
|
||||
<Server className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
<CardTitle className="text-sm font-semibold">Node Health</CardTitle>
|
||||
<DotMatrixText text="NODE HEALTH" size="xs" className="text-foreground" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -108,16 +99,16 @@ export function NodeHealthGrid({ nodes, vms }: NodeHealthGridProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Card className="dot-grid">
|
||||
<CardHeader className="border-b border-dotted border-border/60">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="flex h-7 w-7 items-center justify-center rounded-md border border-border bg-muted/40">
|
||||
<div className="flex h-7 w-7 items-center justify-center rounded-sm border border-dotted border-border bg-muted/40">
|
||||
<Server className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
<CardTitle className="text-sm font-semibold">Node Health</CardTitle>
|
||||
<DotMatrixText text="NODE HEALTH" size="xs" className="text-foreground" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent className="pt-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{nodes.map((node) => (
|
||||
<NodeCard key={node.node} node={node} vmCount={vmCounts[node.node] ?? 0} />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Card, CardContent, CardHeader } from '@/components/ui/card'
|
||||
import { DotMatrixText } from '@/components/ui/dot-matrix'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { RefreshCw, Server, Box, HardDrive, ListTodo, Shield, Zap } from 'lucide-react'
|
||||
|
||||
@@ -10,65 +11,65 @@ interface QuickActionsProps {
|
||||
|
||||
export function QuickActions({ onRefresh, isRefreshing, onNavigate }: QuickActionsProps) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0">
|
||||
<Card className="dot-grid">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 border-b border-dotted border-border/60">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="flex h-7 w-7 items-center justify-center rounded-md border border-border bg-muted/40">
|
||||
<div className="flex h-7 w-7 items-center justify-center rounded-sm border border-dotted border-border bg-muted/40">
|
||||
<Zap className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
<CardTitle className="text-sm font-semibold">Quick Actions</CardTitle>
|
||||
<DotMatrixText text="QUICK ACTIONS" size="xs" className="text-foreground" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent className="pt-4">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex h-auto flex-col items-center gap-1.5 px-2 py-3"
|
||||
className="flex h-auto flex-col items-center gap-1.5 border-dotted px-2 py-3"
|
||||
onClick={onRefresh}
|
||||
disabled={isRefreshing}
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 ${isRefreshing ? 'animate-spin' : ''}`} />
|
||||
<span className="text-xs font-medium">Refresh</span>
|
||||
<DotMatrixText text="REFRESH" size="xs" className="text-muted-foreground" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex h-auto flex-col items-center gap-1.5 px-2 py-3"
|
||||
className="flex h-auto flex-col items-center gap-1.5 border-dotted px-2 py-3"
|
||||
onClick={() => onNavigate?.('nodes')}
|
||||
>
|
||||
<Server className="h-4 w-4" />
|
||||
<span className="text-xs font-medium">Nodes</span>
|
||||
<DotMatrixText text="NODES" size="xs" className="text-muted-foreground" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex h-auto flex-col items-center gap-1.5 px-2 py-3"
|
||||
className="flex h-auto flex-col items-center gap-1.5 border-dotted px-2 py-3"
|
||||
onClick={() => onNavigate?.('vms')}
|
||||
>
|
||||
<Box className="h-4 w-4" />
|
||||
<span className="text-xs font-medium">VMs</span>
|
||||
<DotMatrixText text="VMS" size="xs" className="text-muted-foreground" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex h-auto flex-col items-center gap-1.5 px-2 py-3"
|
||||
className="flex h-auto flex-col items-center gap-1.5 border-dotted px-2 py-3"
|
||||
onClick={() => onNavigate?.('storage')}
|
||||
>
|
||||
<HardDrive className="h-4 w-4" />
|
||||
<span className="text-xs font-medium">Storage</span>
|
||||
<DotMatrixText text="STORAGE" size="xs" className="text-muted-foreground" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex h-auto flex-col items-center gap-1.5 px-2 py-3"
|
||||
className="flex h-auto flex-col items-center gap-1.5 border-dotted px-2 py-3"
|
||||
onClick={() => onNavigate?.('tasks')}
|
||||
>
|
||||
<ListTodo className="h-4 w-4" />
|
||||
<span className="text-xs font-medium">Tasks</span>
|
||||
<DotMatrixText text="TASKS" size="xs" className="text-muted-foreground" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex h-auto flex-col items-center gap-1.5 px-2 py-3"
|
||||
className="flex h-auto flex-col items-center gap-1.5 border-dotted px-2 py-3"
|
||||
onClick={() => onNavigate?.('backups')}
|
||||
>
|
||||
<Shield className="h-4 w-4" />
|
||||
<span className="text-xs font-medium">Backups</span>
|
||||
<DotMatrixText text="BACKUPS" size="xs" className="text-muted-foreground" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Card, CardContent, CardHeader } from '@/components/ui/card'
|
||||
import { DotMatrixText } from '@/components/ui/dot-matrix'
|
||||
import { Cpu, MemoryStick, HardDrive } from 'lucide-react'
|
||||
import { formatBytes } from '@/lib/format'
|
||||
|
||||
@@ -46,45 +47,40 @@ function getTrackColor(): string {
|
||||
export function ResourceGauge({ label, used, total, icon, formatValue }: ResourceGaugeProps) {
|
||||
const percent = total > 0 ? used / total : 0
|
||||
const clampedPercent = Math.min(Math.max(percent, 0), 1)
|
||||
const displayPercent = (clampedPercent * 100).toFixed(1)
|
||||
const displayPercent = `${(clampedPercent * 100).toFixed(1)}%`
|
||||
|
||||
const Icon = iconMap[icon]
|
||||
|
||||
// SVG circle parameters
|
||||
const radius = 40
|
||||
const strokeWidth = 8
|
||||
const circumference = 2 * Math.PI * radius
|
||||
const strokeDashoffset = circumference * (1 - clampedPercent)
|
||||
|
||||
const defaultFormat =
|
||||
icon === 'cpu'
|
||||
? formatCores
|
||||
: formatBytes
|
||||
|
||||
const defaultFormat = icon === 'cpu' ? formatCores : formatBytes
|
||||
const formatter = formatValue ?? defaultFormat
|
||||
const gaugeStyle = getGaugeStyle(clampedPercent)
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card className="dot-grid">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-3">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">{label}</CardTitle>
|
||||
<div className="flex h-7 w-7 items-center justify-center rounded-md border border-border bg-muted/40">
|
||||
<DotMatrixText text={label} size="xs" className="text-muted-foreground" />
|
||||
<div className="flex h-7 w-7 items-center justify-center rounded-sm border border-dotted border-border bg-muted/40">
|
||||
<Icon className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col items-center gap-3">
|
||||
<div className="relative">
|
||||
<svg width="108" height="108" viewBox="0 0 100 100">
|
||||
{/* Background track */}
|
||||
<circle
|
||||
cx="50"
|
||||
cy="50"
|
||||
r={radius}
|
||||
fill="none"
|
||||
strokeWidth={strokeWidth}
|
||||
strokeWidth={strokeWidth - 1}
|
||||
strokeDasharray="1 3"
|
||||
strokeLinecap="round"
|
||||
className={getTrackColor()}
|
||||
opacity="0.35"
|
||||
/>
|
||||
{/* Progress arc */}
|
||||
<circle
|
||||
cx="50"
|
||||
cy="50"
|
||||
@@ -92,7 +88,7 @@ export function ResourceGauge({ label, used, total, icon, formatValue }: Resourc
|
||||
fill="none"
|
||||
strokeWidth={strokeWidth}
|
||||
strokeLinecap="round"
|
||||
strokeDasharray={circumference}
|
||||
strokeDasharray={`${circumference * 0.018} ${circumference * 0.012}`}
|
||||
strokeDashoffset={strokeDashoffset}
|
||||
className={gaugeStyle.className}
|
||||
style={{
|
||||
@@ -104,9 +100,7 @@ export function ResourceGauge({ label, used, total, icon, formatValue }: Resourc
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
||||
<span className="font-mono text-xl font-semibold leading-none tracking-tight tabular-nums">
|
||||
{displayPercent}%
|
||||
</span>
|
||||
<DotMatrixText text={displayPercent} size="xs" className="text-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center font-mono text-xs tabular-nums text-muted-foreground">
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useNodes, useVMs, useTasks, queryKeys } from '@/hooks/useProxmox'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Card, CardContent, CardHeader } from '@/components/ui/card'
|
||||
import { DotMatrixText } from '@/components/ui/dot-matrix'
|
||||
import { Server, Cpu, HardDrive, MemoryStick, AlertCircle } from 'lucide-react'
|
||||
import { ResourceGauge } from '@/components/dashboard/ResourceGauge'
|
||||
import { NodeHealthGrid } from '@/components/dashboard/NodeHealthGrid'
|
||||
@@ -31,9 +32,6 @@ export function Dashboard({ connectionId, onNavigate }: DashboardProps) {
|
||||
|
||||
const formatPercent = (value: number) => `${(value * 100).toFixed(1)}%`
|
||||
|
||||
// Track actual refresh activity rather than piggybacking on tasksLoading,
|
||||
// which is true whenever the tasks query is fetching (e.g. background
|
||||
// polling) and would keep the Refresh button spinning continuously.
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
|
||||
const handleRefresh = useCallback(async () => {
|
||||
@@ -60,19 +58,19 @@ export function Dashboard({ connectionId, onNavigate }: DashboardProps) {
|
||||
const hasError = nodesError || vmsError || tasksError
|
||||
if (hasError) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center p-6">
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<div className="mb-4 flex h-12 w-12 items-center justify-center rounded-lg border border-destructive/25 bg-destructive/10">
|
||||
<div className="flex h-full items-center justify-center p-6 dot-grid">
|
||||
<div className="flex flex-col items-center text-center rounded-lg border border-dotted border-destructive/30 bg-card p-8 shadow-card">
|
||||
<div className="mb-4 flex h-12 w-12 items-center justify-center rounded-sm border border-dotted border-destructive/30 bg-destructive/10">
|
||||
<AlertCircle className="h-5 w-5 text-destructive" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold tracking-tight">Unable to load dashboard</h3>
|
||||
<p className="mt-1 max-w-sm text-sm text-muted-foreground">
|
||||
<DotMatrixText text="LOAD FAILED" size="sm" className="text-destructive" />
|
||||
<p className="mt-2 max-w-sm text-sm text-muted-foreground">
|
||||
{nodesError?.message || vmsError?.message || tasksError?.message || 'An error occurred while loading data'}
|
||||
</p>
|
||||
<p className="mt-1 max-w-sm text-sm text-muted-foreground">
|
||||
Check the connection to your Proxmox server and try again.
|
||||
</p>
|
||||
<Button variant="outline" className="mt-4" onClick={handleRetry}>
|
||||
<Button variant="outline" className="mt-4 border-dotted" onClick={handleRetry}>
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
@@ -81,80 +79,69 @@ export function Dashboard({ connectionId, onNavigate }: DashboardProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-auto p-6">
|
||||
<div className="h-full overflow-auto p-6 dot-grid">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-[1.625rem] font-semibold leading-tight tracking-[-0.02em]">
|
||||
Dashboard
|
||||
</h2>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
<DotMatrixText text="DASHBOARD" size="md" className="text-foreground" />
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Cluster overview and resource usage
|
||||
</p>
|
||||
<hr className="dot-rule mt-3" />
|
||||
</div>
|
||||
|
||||
{/* Summary Stats Row */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<Card>
|
||||
<Card className="dot-grid">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-3">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Nodes</CardTitle>
|
||||
<div className="flex h-7 w-7 items-center justify-center rounded-md border border-border bg-muted/40">
|
||||
<DotMatrixText text="NODES" size="xs" className="text-muted-foreground" />
|
||||
<div className="flex h-7 w-7 items-center justify-center rounded-sm border border-dotted border-border bg-muted/40">
|
||||
<Server className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="font-mono text-2xl font-semibold leading-none tracking-tight tabular-nums">
|
||||
{nodes?.filter((n) => n.status === 'online').length ?? 0}
|
||||
<span className="text-muted-foreground"> / {nodes?.length ?? 0}</span>
|
||||
</div>
|
||||
<p className="mt-1.5 text-xs text-muted-foreground">Online</p>
|
||||
<DotMatrixText text={`${nodes?.filter((n) => n.status === 'online').length ?? 0} / ${nodes?.length ?? 0}`} size="sm" className="text-foreground" />
|
||||
<p className="mt-1.5 text-xs tracking-widest text-muted-foreground">ONLINE</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Card className="dot-grid">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-3">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">CPU Usage</CardTitle>
|
||||
<div className="flex h-7 w-7 items-center justify-center rounded-md border border-border bg-muted/40">
|
||||
<DotMatrixText text="CPU USAGE" size="xs" className="text-muted-foreground" />
|
||||
<div className="flex h-7 w-7 items-center justify-center rounded-sm border border-dotted border-border bg-muted/40">
|
||||
<Cpu className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="font-mono text-2xl font-semibold leading-none tracking-tight tabular-nums">
|
||||
{totalCPU > 0 ? formatPercent(usedCPU / totalCPU) : '0%'}
|
||||
</div>
|
||||
<DotMatrixText text={totalCPU > 0 ? formatPercent(usedCPU / totalCPU) : '0%'} size="sm" className="text-foreground" />
|
||||
<p className="mt-1.5 font-mono text-xs tabular-nums text-muted-foreground">
|
||||
{usedCPU.toFixed(1)} / {totalCPU} cores
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Card className="dot-grid">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-3">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Memory</CardTitle>
|
||||
<div className="flex h-7 w-7 items-center justify-center rounded-md border border-border bg-muted/40">
|
||||
<DotMatrixText text="MEMORY" size="xs" className="text-muted-foreground" />
|
||||
<div className="flex h-7 w-7 items-center justify-center rounded-sm border border-dotted border-border bg-muted/40">
|
||||
<MemoryStick className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="font-mono text-2xl font-semibold leading-none tracking-tight tabular-nums">
|
||||
{totalMem > 0 ? formatPercent(usedMem / totalMem) : '0%'}
|
||||
</div>
|
||||
<DotMatrixText text={totalMem > 0 ? formatPercent(usedMem / totalMem) : '0%'} size="sm" className="text-foreground" />
|
||||
<p className="mt-1.5 font-mono text-xs tabular-nums text-muted-foreground">
|
||||
{formatBytes(usedMem)} / {formatBytes(totalMem)}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Card className="dot-grid">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-3">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Storage</CardTitle>
|
||||
<div className="flex h-7 w-7 items-center justify-center rounded-md border border-border bg-muted/40">
|
||||
<DotMatrixText text="STORAGE" size="xs" className="text-muted-foreground" />
|
||||
<div className="flex h-7 w-7 items-center justify-center rounded-sm border border-dotted border-border bg-muted/40">
|
||||
<HardDrive className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="font-mono text-2xl font-semibold leading-none tracking-tight tabular-nums">
|
||||
{totalDisk > 0 ? formatPercent(usedDisk / totalDisk) : '0%'}
|
||||
</div>
|
||||
<DotMatrixText text={totalDisk > 0 ? formatPercent(usedDisk / totalDisk) : '0%'} size="sm" className="text-foreground" />
|
||||
<p className="mt-1.5 font-mono text-xs tabular-nums text-muted-foreground">
|
||||
{formatBytes(usedDisk)} / {formatBytes(totalDisk)}
|
||||
</p>
|
||||
@@ -162,7 +149,6 @@ export function Dashboard({ connectionId, onNavigate }: DashboardProps) {
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Resource Gauges */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<ResourceGauge
|
||||
label="CPU"
|
||||
@@ -172,14 +158,14 @@ export function Dashboard({ connectionId, onNavigate }: DashboardProps) {
|
||||
formatValue={(v) => `${v.toFixed(1)} cores`}
|
||||
/>
|
||||
<ResourceGauge
|
||||
label="Memory"
|
||||
label="MEMORY"
|
||||
used={usedMem}
|
||||
total={totalMem}
|
||||
icon="memory"
|
||||
formatValue={formatBytes}
|
||||
/>
|
||||
<ResourceGauge
|
||||
label="Storage"
|
||||
label="STORAGE"
|
||||
used={usedDisk}
|
||||
total={totalDisk}
|
||||
icon="disk"
|
||||
@@ -187,10 +173,8 @@ export function Dashboard({ connectionId, onNavigate }: DashboardProps) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Node Health Grid */}
|
||||
<NodeHealthGrid nodes={nodes} vms={vms} />
|
||||
|
||||
{/* Activity Feed + Quick Actions */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||
<div className="lg:col-span-2">
|
||||
<ActivityFeed tasks={tasks} isLoading={tasksLoading} />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useConnectionStore } from '@/stores/connectionStore'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { DotMatrixText } from '@/components/ui/dot-matrix'
|
||||
import { Plus, Server, LayoutDashboard, HardDrive, Box, ListTodo, Shield, Settings } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
@@ -36,16 +37,13 @@ export function Sidebar({ onAddConnection, activeView, onNavigate }: SidebarProp
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex w-64 flex-col border-r border-border bg-card">
|
||||
<div className="flex h-14 shrink-0 items-center gap-2.5 border-b border-border px-4">
|
||||
<img src="/icon.png" alt="Clustri" className="h-7 w-7 rounded-md" />
|
||||
<div className="flex w-64 flex-col border-r border-dotted border-border bg-card dot-grid">
|
||||
<div className="flex h-14 shrink-0 items-center gap-2.5 border-b border-dotted border-border px-4 bg-card/80">
|
||||
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-sm border border-dotted border-border bg-primary/10 text-primary">
|
||||
<Server className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
<div className="min-w-0 leading-tight">
|
||||
<h1 className="truncate text-sm font-semibold tracking-tight text-foreground">
|
||||
Clustri
|
||||
</h1>
|
||||
<p className="text-[10px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
|
||||
Ops Console
|
||||
</p>
|
||||
<DotMatrixText text="CLUSTRI" size="xs" className="text-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
<ScrollArea className="flex-1">
|
||||
@@ -55,10 +53,10 @@ export function Sidebar({ onAddConnection, activeView, onNavigate }: SidebarProp
|
||||
<button
|
||||
onClick={() => setActiveConnection(connection.id)}
|
||||
className={cn(
|
||||
'flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm transition duration-150',
|
||||
'flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm transition duration-150 border border-transparent',
|
||||
activeConnectionId === connection.id
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'text-muted-foreground hover:bg-accent/60 hover:text-foreground'
|
||||
? 'bg-accent text-accent-foreground border-dotted border-border'
|
||||
: 'text-muted-foreground hover:bg-accent/60 hover:text-foreground hover:border-dotted hover:border-border/60',
|
||||
)}
|
||||
>
|
||||
<span className={cn('h-1.5 w-1.5 shrink-0 rounded-full', getStatusColor(connection.status))} />
|
||||
@@ -79,59 +77,60 @@ export function Sidebar({ onAddConnection, activeView, onNavigate }: SidebarProp
|
||||
<>
|
||||
<SidebarItem
|
||||
icon={LayoutDashboard}
|
||||
label="Overview"
|
||||
label="OVERVIEW"
|
||||
active={activeView === 'pbs-overview'}
|
||||
onClick={() => onNavigate?.({ type: 'pbs-overview' })}
|
||||
/>
|
||||
<SidebarItem
|
||||
icon={HardDrive}
|
||||
label="Datastores"
|
||||
label="DATASTORES"
|
||||
active={activeView === 'pbs-datastores' || activeView === 'pbs-datastore-detail'}
|
||||
onClick={() => onNavigate?.({ type: 'pbs-datastores' })}
|
||||
/>
|
||||
<SidebarItem icon={ListTodo} label="Tasks" active={activeView === 'tasks'} onClick={() => onNavigate?.({ type: 'tasks' })} />
|
||||
<SidebarItem icon={ListTodo} label="TASKS" active={activeView === 'tasks'} onClick={() => onNavigate?.({ type: 'tasks' })} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<SidebarItem
|
||||
icon={LayoutDashboard}
|
||||
label="Dashboard"
|
||||
label="DASHBOARD"
|
||||
active={activeView === 'dashboard'}
|
||||
onClick={() => onNavigate?.({ type: 'dashboard' })}
|
||||
/>
|
||||
<SidebarItem
|
||||
icon={Server}
|
||||
label="Nodes"
|
||||
label="NODES"
|
||||
active={activeView === 'nodes'}
|
||||
onClick={() => onNavigate?.({ type: 'nodes' })}
|
||||
/>
|
||||
<SidebarItem
|
||||
icon={Box}
|
||||
label="VMs"
|
||||
label="VMS"
|
||||
active={activeView === 'vms' || activeView === 'vm-detail'}
|
||||
onClick={() => onNavigate?.({ type: 'vms' })}
|
||||
/>
|
||||
<SidebarItem
|
||||
icon={Box}
|
||||
label="Containers"
|
||||
label="CONTAINERS"
|
||||
active={activeView === 'containers'}
|
||||
onClick={() => onNavigate?.({ type: 'containers' })}
|
||||
/>
|
||||
<SidebarItem icon={HardDrive} label="Storage" active={activeView === 'storage' || activeView === 'storage-detail'} onClick={() => onNavigate?.({ type: 'storage' })} />
|
||||
<SidebarItem icon={ListTodo} label="Tasks" active={activeView === 'tasks'} onClick={() => onNavigate?.({ type: 'tasks' })} />
|
||||
<SidebarItem icon={Shield} label="Backups" active={activeView === 'backups'} onClick={() => onNavigate?.({ type: 'backups' })} />
|
||||
<SidebarItem icon={HardDrive} label="STORAGE" active={activeView === 'storage' || activeView === 'storage-detail'} onClick={() => onNavigate?.({ type: 'storage' })} />
|
||||
<SidebarItem icon={ListTodo} label="TASKS" active={activeView === 'tasks'} onClick={() => onNavigate?.({ type: 'tasks' })} />
|
||||
<SidebarItem icon={Shield} label="BACKUPS" active={activeView === 'backups'} onClick={() => onNavigate?.({ type: 'backups' })} />
|
||||
</>
|
||||
)}
|
||||
{connection.serverType !== 'pbs' && connection.nodes && connection.nodes.length > 0 && (
|
||||
<>
|
||||
<p className="px-3 pb-1 pt-3 text-[10px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
|
||||
Cluster nodes
|
||||
</p>
|
||||
<div className="px-3 pb-1 pt-3">
|
||||
<DotMatrixText text="CLUSTER NODES" size="xs" className="text-muted-foreground opacity-60" />
|
||||
</div>
|
||||
<hr className="dot-rule mx-2" />
|
||||
{connection.nodes.map((node) => (
|
||||
<button
|
||||
key={node.url}
|
||||
onClick={() => onNavigate?.({ type: 'node-detail', nodeName: node.name })}
|
||||
className="flex w-full items-center gap-2.5 rounded-md px-3 py-1.5 text-[13px] font-medium text-muted-foreground transition duration-150 hover:bg-accent/60 hover:text-foreground"
|
||||
className="flex w-full items-center gap-2.5 rounded-md px-3 py-1.5 text-[13px] font-medium text-muted-foreground transition duration-150 hover:bg-accent/60 hover:text-foreground border border-transparent hover:border-dotted hover:border-border/50"
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
@@ -141,7 +140,7 @@ export function Sidebar({ onAddConnection, activeView, onNavigate }: SidebarProp
|
||||
/>
|
||||
<span className="min-w-0 flex-1 truncate font-mono">{node.name}</span>
|
||||
{node.isPrimary && (
|
||||
<span className="shrink-0 rounded border border-primary/30 bg-primary/10 px-1 py-px text-[10px] uppercase text-primary">
|
||||
<span className="shrink-0 rounded-sm border border-dotted border-primary/40 bg-primary/10 px-1 py-px text-[10px] uppercase tracking-widest text-primary">
|
||||
primary
|
||||
</span>
|
||||
)}
|
||||
@@ -161,16 +160,16 @@ export function Sidebar({ onAddConnection, activeView, onNavigate }: SidebarProp
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
<div className="space-y-1 border-t border-border p-2">
|
||||
<div className="space-y-1 border-t border-dotted border-border p-2 bg-card/60">
|
||||
<SidebarItem
|
||||
icon={Settings}
|
||||
label="Settings"
|
||||
label="SETTINGS"
|
||||
active={activeView === 'settings'}
|
||||
onClick={() => onNavigate?.({ type: 'settings' })}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-start gap-2"
|
||||
className="w-full justify-start gap-2 border-dotted"
|
||||
onClick={onAddConnection}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
@@ -199,19 +198,19 @@ function SidebarItem({
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
'relative flex w-full items-center gap-2.5 rounded-md px-3 py-1.5 text-[13px] font-medium transition duration-150',
|
||||
'relative flex w-full items-center gap-2.5 rounded-md px-3 py-1.5 text-[13px] font-medium transition duration-150 border border-transparent',
|
||||
disabled
|
||||
? 'pointer-events-none cursor-not-allowed opacity-50'
|
||||
: active
|
||||
? 'bg-primary/10 text-primary hover:bg-primary/15'
|
||||
: 'text-muted-foreground hover:bg-accent/60 hover:text-foreground'
|
||||
? 'bg-primary/10 text-primary border-dotted border-primary/20'
|
||||
: 'text-muted-foreground hover:bg-accent/60 hover:text-foreground hover:border-dotted hover:border-border/50',
|
||||
)}
|
||||
>
|
||||
{active && (
|
||||
<span className="absolute left-0 top-1/2 h-4 w-0.5 -translate-y-1/2 rounded-full bg-primary" />
|
||||
)}
|
||||
<Icon className="h-4 w-4 shrink-0" />
|
||||
<span>{label}</span>
|
||||
<DotMatrixText text={label} size="xs" className={active ? 'text-primary' : 'text-muted-foreground'} dotClassName={active ? 'text-primary' : ''} />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useMemo } from 'react'
|
||||
import { ListTodo, Loader2 } from 'lucide-react'
|
||||
import { DotMatrixText } from '@/components/ui/dot-matrix'
|
||||
import { useTasks } from '@/hooks/useProxmox'
|
||||
import type { ProxmoxTask } from '@/types/proxmox'
|
||||
|
||||
@@ -27,21 +28,19 @@ export function TaskStatusBar({ connectionId, onClick }: TaskStatusBarProps) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="w-full flex items-center gap-2 px-3 py-2 rounded-md text-sm transition-colors duration-150 hover:bg-accent/50 text-muted-foreground hover:text-accent-foreground"
|
||||
className="group flex w-full items-center gap-2 rounded-md border border-transparent px-3 py-2 text-sm transition-colors duration-150 hover:border-dotted hover:border-border/60 hover:bg-accent/50 text-muted-foreground hover:text-accent-foreground"
|
||||
>
|
||||
<ListTodo className="h-4 w-4" />
|
||||
<ListTodo className="h-4 w-4 transition-colors group-hover:text-foreground" />
|
||||
<span className="flex-1 text-left">
|
||||
{runningCount > 0 ? (
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="font-medium text-info">{runningCount} task{runningCount !== 1 ? 's' : ''} running</span>
|
||||
<DotMatrixText text={`${runningCount} TASK${runningCount !== 1 ? 'S' : ''} RUNNING`} size="xs" className="text-info group-hover:text-foreground transition-colors" />
|
||||
</span>
|
||||
) : (
|
||||
<span>No tasks running</span>
|
||||
<DotMatrixText text="NO TASKS RUNNING" size="xs" className="text-muted-foreground group-hover:text-foreground transition-colors" />
|
||||
)}
|
||||
</span>
|
||||
{runningCount > 0 && (
|
||||
<Loader2 className="h-3 w-3 text-info animate-spin" />
|
||||
)}
|
||||
{runningCount > 0 && <Loader2 className="h-3 w-3 text-info group-hover:text-foreground transition-colors animate-spin" />}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ const buttonVariants = cva(
|
||||
destructive:
|
||||
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
|
||||
outline:
|
||||
'border border-input bg-background shadow-sm hover:border-border hover:bg-accent hover:text-accent-foreground',
|
||||
'border border-dotted border-input bg-background shadow-sm hover:border-border hover:bg-accent hover:text-accent-foreground',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
|
||||
ghost:
|
||||
@@ -31,7 +31,7 @@ const buttonVariants = cva(
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
@@ -50,7 +50,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
Button.displayName = 'Button'
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ const Card = React.forwardRef<
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'rounded-lg border bg-card text-card-foreground shadow-card',
|
||||
className
|
||||
'rounded-lg border border-dotted bg-card text-card-foreground shadow-card',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -36,7 +36,7 @@ const CardTitle = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'font-semibold leading-none tracking-tight text-foreground',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -16,7 +16,7 @@ const DialogOverlay = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 bg-black/60 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -32,8 +32,8 @@ const DialogContent = React.forwardRef<
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-popover p-6 text-popover-foreground shadow-2xl shadow-black/20 duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
|
||||
className
|
||||
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-dotted bg-popover p-6 text-popover-foreground shadow-2xl shadow-black/20 duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg dot-grid',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -54,7 +54,7 @@ const DialogHeader = ({
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col space-y-1.5 text-center sm:text-left',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -68,7 +68,7 @@ const DialogFooter = ({
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -83,7 +83,7 @@ const DialogTitle = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'text-base font-semibold leading-none tracking-tight',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import { GLYPH_COLS, GLYPH_ROWS, getGlyph } from '@/lib/dotMatrixGlyphs'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
type DotMatrixSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
||||
|
||||
const sizeMap: Record<DotMatrixSize, { dot: number; gap: number; charGap: number }> = {
|
||||
xs: { dot: 1.8, gap: 1.3, charGap: 5 },
|
||||
sm: { dot: 2.2, gap: 1.5, charGap: 6 },
|
||||
md: { dot: 2.6, gap: 1.8, charGap: 7 },
|
||||
lg: { dot: 3.0, gap: 2.0, charGap: 9 },
|
||||
xl: { dot: 3.8, gap: 2.4, charGap: 12 },
|
||||
}
|
||||
|
||||
interface DotMatrixTextProps {
|
||||
text: string
|
||||
size?: DotMatrixSize
|
||||
className?: string
|
||||
dotClassName?: string
|
||||
'aria-label'?: string
|
||||
}
|
||||
|
||||
function CharSvg({
|
||||
glyph,
|
||||
dot,
|
||||
gap,
|
||||
}: {
|
||||
glyph: readonly string[]
|
||||
dot: number
|
||||
gap: number
|
||||
}) {
|
||||
const w = GLYPH_COLS * dot + (GLYPH_COLS - 1) * gap
|
||||
const h = GLYPH_ROWS * dot + (GLYPH_ROWS - 1) * gap
|
||||
const r = dot / 2
|
||||
return (
|
||||
<svg
|
||||
width={w}
|
||||
height={h}
|
||||
viewBox={`0 0 ${w} ${h}`}
|
||||
aria-hidden="true"
|
||||
className="shrink-0"
|
||||
shapeRendering="geometricPrecision"
|
||||
>
|
||||
{glyph.map((row, y) =>
|
||||
Array.from(row).map((cell, x) => {
|
||||
if (cell === ' ') return null
|
||||
const cx = x * (dot + gap) + r
|
||||
const cy = y * (dot + gap) + r
|
||||
return <circle key={`${x}-${y}`} cx={cx} cy={cy} r={r} fill="currentColor" />
|
||||
}),
|
||||
)}
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function DotMatrixText({
|
||||
text,
|
||||
size = 'sm',
|
||||
className,
|
||||
dotClassName,
|
||||
'aria-label': ariaLabel,
|
||||
}: DotMatrixTextProps) {
|
||||
const { dot, gap, charGap } = sizeMap[size]
|
||||
const chars = Array.from(text.toUpperCase())
|
||||
const label = ariaLabel ?? text
|
||||
return (
|
||||
<span
|
||||
aria-label={label}
|
||||
role="text"
|
||||
className={cn('inline-flex items-center leading-none', dotClassName, className)}
|
||||
style={{ gap: charGap }}
|
||||
>
|
||||
{chars.map((ch, i) => {
|
||||
if (ch === ' ') {
|
||||
return <span key={i} style={{ width: charGap * 1.8 }} aria-hidden="true" />
|
||||
}
|
||||
const glyph = getGlyph(ch)
|
||||
if (!glyph) {
|
||||
return (
|
||||
<span
|
||||
key={i}
|
||||
className="font-mono text-[0.7em] leading-none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{ch}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
return <CharSvg key={i} glyph={glyph} dot={dot} gap={gap} />
|
||||
})}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export function DotMatrixHeading({
|
||||
text,
|
||||
size = 'md',
|
||||
className,
|
||||
dotClassName,
|
||||
as: Tag = 'span',
|
||||
...rest
|
||||
}: DotMatrixTextProps & { as?: 'h1' | 'h2' | 'h3' | 'span' | 'p' | 'div' }) {
|
||||
return (
|
||||
<Tag className={cn('block', className)} {...rest}>
|
||||
<DotMatrixText text={text} size={size} dotClassName={dotClassName} />
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
import { DotMatrixText } from '@/components/ui/dot-matrix'
|
||||
|
||||
interface EmptyStateProps {
|
||||
icon: LucideIcon
|
||||
@@ -10,13 +11,13 @@ interface EmptyStateProps {
|
||||
|
||||
export function EmptyState({ icon: Icon, title, description, action }: EmptyStateProps) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center px-6 py-12 text-center">
|
||||
<div className="mb-3 flex h-12 w-12 items-center justify-center rounded-lg border border-border bg-muted/50">
|
||||
<div className="flex flex-col items-center justify-center px-6 py-12 text-center dot-grid">
|
||||
<div className="mb-3 flex h-12 w-12 items-center justify-center rounded-sm border border-dotted border-border bg-muted/50">
|
||||
<Icon className="h-5 w-5 text-muted-foreground" />
|
||||
</div>
|
||||
<p className="text-sm font-medium text-foreground">{title}</p>
|
||||
<DotMatrixText text={title.toUpperCase()} size="xs" className="text-foreground" />
|
||||
{description && (
|
||||
<p className="mt-1 max-w-sm text-sm text-muted-foreground">{description}</p>
|
||||
<p className="mt-2 max-w-sm text-sm text-muted-foreground">{description}</p>
|
||||
)}
|
||||
{action && <div className="mt-4">{action}</div>}
|
||||
</div>
|
||||
|
||||
@@ -7,14 +7,14 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
'flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className
|
||||
'flex h-9 w-full rounded-md border border-dotted border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
Input.displayName = 'Input'
|
||||
|
||||
|
||||
@@ -10,33 +10,33 @@ interface StatusBadgeProps {
|
||||
|
||||
const statusConfig: Record<BadgeStatus, { label: string; color: string; icon: React.ComponentType<{ className?: string }> }> = {
|
||||
running: {
|
||||
label: 'Running',
|
||||
color: 'border-success/25 bg-success/10 text-success',
|
||||
label: 'RUNNING',
|
||||
color: 'border-dotted border-success/30 bg-success/10 text-success',
|
||||
icon: CheckCircle,
|
||||
},
|
||||
stopped: {
|
||||
label: 'Stopped',
|
||||
color: 'border-border bg-muted/60 text-muted-foreground',
|
||||
label: 'STOPPED',
|
||||
color: 'border-dotted border-border bg-muted/60 text-muted-foreground',
|
||||
icon: Square,
|
||||
},
|
||||
paused: {
|
||||
label: 'Paused',
|
||||
color: 'border-warning/25 bg-warning/10 text-warning',
|
||||
label: 'PAUSED',
|
||||
color: 'border-dotted border-warning/30 bg-warning/10 text-warning',
|
||||
icon: Pause,
|
||||
},
|
||||
suspended: {
|
||||
label: 'Suspended',
|
||||
color: 'border-warning/25 bg-warning/10 text-warning',
|
||||
label: 'SUSPENDED',
|
||||
color: 'border-dotted border-warning/30 bg-warning/10 text-warning',
|
||||
icon: Clock,
|
||||
},
|
||||
online: {
|
||||
label: 'Online',
|
||||
color: 'border-success/25 bg-success/10 text-success',
|
||||
label: 'ONLINE',
|
||||
color: 'border-dotted border-success/30 bg-success/10 text-success',
|
||||
icon: CheckCircle,
|
||||
},
|
||||
offline: {
|
||||
label: 'Offline',
|
||||
color: 'border-destructive/25 bg-destructive/10 text-destructive',
|
||||
label: 'OFFLINE',
|
||||
color: 'border-dotted border-destructive/30 bg-destructive/10 text-destructive',
|
||||
icon: XCircle,
|
||||
},
|
||||
}
|
||||
@@ -48,9 +48,9 @@ export function StatusBadge({ status, className }: StatusBadgeProps) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1.5 rounded-sm border px-2 py-0.5 text-xs font-medium',
|
||||
'inline-flex items-center gap-1.5 rounded-sm border border-dotted px-2 py-0.5 text-[11px] font-medium tracking-widest',
|
||||
config.color,
|
||||
className
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<Icon className="h-3 w-3" />
|
||||
|
||||
@@ -11,8 +11,8 @@ const TabsList = React.forwardRef<
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'inline-flex h-9 items-center justify-center rounded-md bg-secondary p-1 text-muted-foreground',
|
||||
className
|
||||
'inline-flex h-9 items-center justify-center rounded-md border border-dotted border-border bg-secondary p-1 text-muted-foreground dot-grid',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -26,8 +26,8 @@ const TabsTrigger = React.forwardRef<
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1 text-sm font-medium ring-offset-background transition-all duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=inactive]:text-muted-foreground data-[state=inactive]:hover:text-foreground data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm',
|
||||
className
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded-sm border border-transparent px-3 py-1 text-sm font-medium tracking-wide ring-offset-background transition-all duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=inactive]:text-muted-foreground data-[state=inactive]:hover:text-foreground data-[state=active]:border-dotted data-[state=active]:border-border data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -42,7 +42,7 @@ const TabsContent = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -39,7 +39,7 @@ export function ToastProvider({ children }: { children: React.ReactNode }) {
|
||||
setToasts((prev) => [...prev, { id, message, variant }])
|
||||
setTimeout(() => removeToast(id), 5000)
|
||||
},
|
||||
[removeToast]
|
||||
[removeToast],
|
||||
)
|
||||
|
||||
return (
|
||||
@@ -104,8 +104,8 @@ function ToastItem({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-start gap-3 rounded-md border border-border border-l-2 bg-card p-3.5 text-sm text-foreground shadow-lg shadow-black/10 dark:shadow-black/40',
|
||||
config.bar
|
||||
'flex items-start gap-3 rounded-md border border-dotted border-border border-l-2 bg-card p-3.5 text-sm text-foreground shadow-lg shadow-black/10 dark:shadow-black/40 dot-grid',
|
||||
config.bar,
|
||||
)}
|
||||
style={{
|
||||
animation: 'toast-slide-in 0.3s ease-out',
|
||||
|
||||
+157
-47
@@ -1,9 +1,6 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
@theme {
|
||||
/* ------------------------------------------------------------------
|
||||
* Typography — offline-safe variable fonts (bundled via fontsource)
|
||||
* ------------------------------------------------------------------ */
|
||||
--font-sans:
|
||||
'Geist Variable', 'Geist', ui-sans-serif, system-ui, -apple-system,
|
||||
'Segoe UI', sans-serif;
|
||||
@@ -11,9 +8,6 @@
|
||||
'JetBrains Mono Variable', 'JetBrains Mono', ui-monospace, 'SF Mono',
|
||||
Menlo, monospace;
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* Radius scale — tighter on inner elements, softer on containers
|
||||
* ------------------------------------------------------------------ */
|
||||
--radius: 0.625rem;
|
||||
--radius-sm: 0.375rem;
|
||||
--radius-md: 0.5rem;
|
||||
@@ -21,60 +15,59 @@
|
||||
--radius-xl: 0.75rem;
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* Light theme (default) — warm paper neutrals, cool ink, Proxmox accent
|
||||
* Light — cream paper, ink black, Proxmox orange ribbon
|
||||
* ------------------------------------------------------------------ */
|
||||
--color-background: oklch(0.972 0.004 85);
|
||||
--color-foreground: oklch(0.24 0.012 250);
|
||||
--color-card: oklch(0.998 0.002 85);
|
||||
--color-card-foreground: oklch(0.24 0.012 250);
|
||||
--color-popover: oklch(1 0 0);
|
||||
--color-popover-foreground: oklch(0.24 0.012 250);
|
||||
--color-background: oklch(0.985 0.006 85);
|
||||
--color-foreground: oklch(0.18 0.012 250);
|
||||
--color-card: oklch(0.994 0.003 85);
|
||||
--color-card-foreground: oklch(0.18 0.012 250);
|
||||
--color-popover: oklch(0.998 0.002 85);
|
||||
--color-popover-foreground: oklch(0.18 0.012 250);
|
||||
--color-primary: oklch(0.62 0.15 55);
|
||||
--color-primary-foreground: oklch(0.985 0.008 60);
|
||||
--color-secondary: oklch(0.95 0.005 85);
|
||||
--color-secondary-foreground: oklch(0.3 0.012 250);
|
||||
--color-muted: oklch(0.95 0.005 85);
|
||||
--color-muted-foreground: oklch(0.51 0.014 250);
|
||||
--color-accent: oklch(0.935 0.008 85);
|
||||
--color-accent-foreground: oklch(0.28 0.012 250);
|
||||
--color-secondary: oklch(0.94 0.01 85);
|
||||
--color-secondary-foreground: oklch(0.22 0.012 250);
|
||||
--color-muted: oklch(0.94 0.01 85);
|
||||
--color-muted-foreground: oklch(0.56 0.012 250);
|
||||
--color-accent: oklch(0.93 0.012 85);
|
||||
--color-accent-foreground: oklch(0.22 0.012 250);
|
||||
--color-destructive: oklch(0.56 0.19 25);
|
||||
--color-destructive-foreground: oklch(0.98 0.008 25);
|
||||
--color-border: oklch(0.905 0.006 85);
|
||||
--color-input: oklch(0.885 0.007 85);
|
||||
--color-border: oklch(0.88 0.012 85);
|
||||
--color-input: oklch(0.86 0.014 85);
|
||||
--color-ring: oklch(0.62 0.15 55);
|
||||
|
||||
/* Semantic status colors — data semantics, not UI accents */
|
||||
--color-success: oklch(0.55 0.13 155);
|
||||
--color-warning: oklch(0.62 0.13 80);
|
||||
--color-info: oklch(0.55 0.11 245);
|
||||
|
||||
/* Tinted card shadow — carries the warm hue of the surface */
|
||||
--shadow-card:
|
||||
0 1px 2px 0 oklch(0.3 0.02 60 / 0.06), 0 2px 8px -2px oklch(0.3 0.02 60 / 0.05);
|
||||
0 1px 0 0 oklch(0.18 0.012 250 / 0.06) inset,
|
||||
0 1px 3px 0 oklch(0.18 0.012 250 / 0.07), 0 4px 12px -4px oklch(0.18 0.012 250 / 0.06);
|
||||
}
|
||||
|
||||
.dark {
|
||||
/* ------------------------------------------------------------------
|
||||
* Dark theme — blue-black ink surfaces, cool neutrals, muted orange
|
||||
* Dark — dark ink-paper, light ink, same orange ribbon
|
||||
* ------------------------------------------------------------------ */
|
||||
--color-background: oklch(0.16 0.011 250);
|
||||
--color-foreground: oklch(0.965 0.004 250);
|
||||
--color-card: oklch(0.19 0.012 250);
|
||||
--color-card-foreground: oklch(0.965 0.004 250);
|
||||
--color-popover: oklch(0.215 0.013 250);
|
||||
--color-popover-foreground: oklch(0.965 0.004 250);
|
||||
--color-background: oklch(0.145 0.012 250);
|
||||
--color-foreground: oklch(0.96 0.005 85);
|
||||
--color-card: oklch(0.182 0.014 250);
|
||||
--color-card-foreground: oklch(0.96 0.005 85);
|
||||
--color-popover: oklch(0.2 0.015 250);
|
||||
--color-popover-foreground: oklch(0.96 0.005 85);
|
||||
--color-primary: oklch(0.72 0.15 60);
|
||||
--color-primary-foreground: oklch(0.18 0.02 50);
|
||||
--color-secondary: oklch(0.235 0.011 250);
|
||||
--color-secondary-foreground: oklch(0.94 0.004 250);
|
||||
--color-muted: oklch(0.235 0.011 250);
|
||||
--color-muted-foreground: oklch(0.655 0.012 250);
|
||||
--color-accent: oklch(0.275 0.012 250);
|
||||
--color-accent-foreground: oklch(0.965 0.004 250);
|
||||
--color-secondary: oklch(0.22 0.014 250);
|
||||
--color-secondary-foreground: oklch(0.92 0.004 85);
|
||||
--color-muted: oklch(0.22 0.014 250);
|
||||
--color-muted-foreground: oklch(0.68 0.012 250);
|
||||
--color-accent: oklch(0.24 0.014 250);
|
||||
--color-accent-foreground: oklch(0.96 0.005 85);
|
||||
--color-destructive: oklch(0.645 0.18 25);
|
||||
--color-destructive-foreground: oklch(0.97 0.015 25);
|
||||
--color-border: oklch(0.255 0.012 250);
|
||||
--color-input: oklch(0.3 0.013 250);
|
||||
--color-border: oklch(0.26 0.014 250);
|
||||
--color-input: oklch(0.28 0.015 250);
|
||||
--color-ring: oklch(0.72 0.15 60);
|
||||
|
||||
--color-success: oklch(0.7 0.14 155);
|
||||
@@ -82,7 +75,118 @@
|
||||
--color-info: oklch(0.68 0.11 245);
|
||||
|
||||
--shadow-card:
|
||||
0 1px 2px 0 oklch(0.06 0.01 250 / 0.4), 0 2px 10px -2px oklch(0.06 0.01 250 / 0.35);
|
||||
0 1px 0 0 oklch(1 0 0 / 0.05) inset, 0 1px 2px 0 oklch(0 0 0 / 0.45),
|
||||
0 8px 24px -8px oklch(0 0 0 / 0.5);
|
||||
}
|
||||
|
||||
/* ── Dot-matrix texture utilities ───────────────────────────────── */
|
||||
.dot-grid {
|
||||
background-image: radial-gradient(
|
||||
circle,
|
||||
oklch(0.18 0.012 250 / 0.09) 1px,
|
||||
transparent 1px
|
||||
);
|
||||
background-size: 14px 14px;
|
||||
}
|
||||
.dark .dot-grid {
|
||||
background-image: radial-gradient(
|
||||
circle,
|
||||
oklch(1 0 0 / 0.07) 1px,
|
||||
transparent 1px
|
||||
);
|
||||
}
|
||||
.dot-grid-strong {
|
||||
background-image: radial-gradient(
|
||||
circle,
|
||||
oklch(0.18 0.012 250 / 0.13) 1.1px,
|
||||
transparent 1.1px
|
||||
);
|
||||
background-size: 10px 10px;
|
||||
}
|
||||
.dark .dot-grid-strong {
|
||||
background-image: radial-gradient(
|
||||
circle,
|
||||
oklch(1 0 0 / 0.1) 1.1px,
|
||||
transparent 1.1px
|
||||
);
|
||||
}
|
||||
|
||||
.dot-rule {
|
||||
border: none;
|
||||
border-top: 1.5px dotted oklch(0.18 0.012 250 / 0.18);
|
||||
height: 0;
|
||||
}
|
||||
.dark .dot-rule {
|
||||
border-top-color: oklch(1 0 0 / 0.12);
|
||||
}
|
||||
|
||||
.perf-edge {
|
||||
position: relative;
|
||||
}
|
||||
.perf-edge::before,
|
||||
.perf-edge::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
bottom: 8px;
|
||||
width: 6px;
|
||||
background-image: radial-gradient(
|
||||
circle,
|
||||
oklch(0.18 0.012 250 / 0.14) 2px,
|
||||
transparent 2px
|
||||
);
|
||||
background-size: 6px 12px;
|
||||
background-repeat: repeat-y;
|
||||
pointer-events: none;
|
||||
}
|
||||
.perf-edge::before {
|
||||
left: 4px;
|
||||
}
|
||||
.perf-edge::after {
|
||||
right: 4px;
|
||||
}
|
||||
.dark .perf-edge::before,
|
||||
.dark .perf-edge::after {
|
||||
background-image: radial-gradient(
|
||||
circle,
|
||||
oklch(1 0 0 / 0.1) 2px,
|
||||
transparent 2px
|
||||
);
|
||||
}
|
||||
|
||||
.halftone-fill {
|
||||
position: relative;
|
||||
}
|
||||
.halftone-fill::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: radial-gradient(
|
||||
circle,
|
||||
oklch(1 0 0 / 0.55) 0.9px,
|
||||
transparent 0.9px
|
||||
);
|
||||
background-size: 5px 5px;
|
||||
opacity: 0.9;
|
||||
pointer-events: none;
|
||||
border-radius: inherit;
|
||||
}
|
||||
.dark .halftone-fill::after {
|
||||
background-image: radial-gradient(
|
||||
circle,
|
||||
oklch(1 0 0 / 0.35) 0.9px,
|
||||
transparent 0.9px
|
||||
);
|
||||
}
|
||||
|
||||
.feed-rule {
|
||||
border-bottom: 1px dotted oklch(0.18 0.012 250 / 0.12);
|
||||
}
|
||||
.dark .feed-rule {
|
||||
border-bottom-color: oklch(1 0 0 / 0.08);
|
||||
}
|
||||
.feed-rule:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
@@ -102,12 +206,10 @@
|
||||
@apply bg-background font-sans text-foreground antialiased;
|
||||
}
|
||||
|
||||
/* Keep native form controls and webview widgets on theme */
|
||||
::selection {
|
||||
@apply bg-primary/20 text-foreground;
|
||||
}
|
||||
|
||||
/* Monospace and code contexts always use tabular numerals */
|
||||
code,
|
||||
kbd,
|
||||
samp,
|
||||
@@ -115,12 +217,10 @@
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* Consistent focus ring everywhere, including custom elements */
|
||||
:focus-visible {
|
||||
@apply outline-none ring-2 ring-ring ring-offset-2 ring-offset-background;
|
||||
}
|
||||
|
||||
/* Native scrollbars tuned to the surface language */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--color-border) transparent;
|
||||
@@ -147,20 +247,19 @@
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
/* Very subtle fixed grain overlay to break digital flatness */
|
||||
body::after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9999;
|
||||
pointer-events: none;
|
||||
opacity: 0.02;
|
||||
opacity: 0.018;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23n)'/%3E%3C/svg%3E");
|
||||
background-repeat: repeat;
|
||||
}
|
||||
|
||||
html.dark body::after {
|
||||
opacity: 0.04;
|
||||
opacity: 0.03;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,3 +273,14 @@
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dot-reveal {
|
||||
from {
|
||||
opacity: 0;
|
||||
filter: blur(1px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
filter: blur(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
export type Glyph = readonly string[]
|
||||
|
||||
export const GLYPH_COLS = 5
|
||||
export const GLYPH_ROWS = 7
|
||||
|
||||
export const GLYPHS: Record<string, Glyph> = {
|
||||
' ': [' ', ' ', ' ', ' ', ' ', ' ', ' '],
|
||||
A: [' ### ', '# #', '# #', '#####', '# #', '# #', '# #'],
|
||||
B: ['#### ', '# #', '# #', '#### ', '# #', '# #', '#### '],
|
||||
C: [' ### ', '# #', '# ', '# ', '# ', '# #', ' ### '],
|
||||
D: ['#### ', '# #', '# #', '# #', '# #', '# #', '#### '],
|
||||
E: ['#####', '# ', '# ', '#### ', '# ', '# ', '#####'],
|
||||
F: ['#####', '# ', '# ', '#### ', '# ', '# ', '# '],
|
||||
G: [' ### ', '# #', '# ', '# ## ', '# #', '# #', ' ### '],
|
||||
H: ['# #', '# #', '# #', '#####', '# #', '# #', '# #'],
|
||||
I: ['#####', ' # ', ' # ', ' # ', ' # ', ' # ', '#####'],
|
||||
J: ['#####', ' # ', ' # ', ' # ', ' # ', '# # ', ' ## '],
|
||||
K: ['# #', '# # ', '# # ', '## ', '# # ', '# # ', '# #'],
|
||||
L: ['# ', '# ', '# ', '# ', '# ', '# ', '#####'],
|
||||
M: ['# #', '## ##', '# # #', '# # #', '# #', '# #', '# #'],
|
||||
N: ['# #', '## #', '# # #', '# # #', '# ##', '# #', '# #'],
|
||||
O: [' ### ', '# #', '# #', '# #', '# #', '# #', ' ### '],
|
||||
P: ['#### ', '# #', '# #', '#### ', '# ', '# ', '# '],
|
||||
Q: [' ### ', '# #', '# #', '# #', '# # #', '# # ', ' ## #'],
|
||||
R: ['#### ', '# #', '# #', '#### ', '# # ', '# # ', '# #'],
|
||||
S: [' ### ', '# #', '# ', ' ### ', ' #', '# #', ' ### '],
|
||||
T: ['#####', ' # ', ' # ', ' # ', ' # ', ' # ', ' # '],
|
||||
U: ['# #', '# #', '# #', '# #', '# #', '# #', ' ### '],
|
||||
V: ['# #', '# #', '# #', '# #', '# #', ' # # ', ' # '],
|
||||
W: ['# #', '# #', '# #', '# # #', '# # #', '## ##', '# #'],
|
||||
X: ['# #', '# #', ' # # ', ' # ', ' # # ', '# #', '# #'],
|
||||
Y: ['# #', '# #', ' # # ', ' # ', ' # ', ' # ', ' # '],
|
||||
Z: ['#####', ' #', ' # ', ' # ', ' # ', '# ', '#####'],
|
||||
'0': [' ### ', '# #', '# ##', '# # #', '## #', '# #', ' ### '],
|
||||
'1': [' # ', ' ## ', ' # ', ' # ', ' # ', ' # ', '#####'],
|
||||
'2': [' ### ', '# #', ' #', ' ## ', ' # ', '# ', '#####'],
|
||||
'3': ['#### ', ' #', ' #', ' ### ', ' #', ' #', '#### '],
|
||||
'4': [' # ', ' ## ', ' # # ', '# # ', '#####', ' # ', ' # '],
|
||||
'5': ['#####', '# ', '# ', '#### ', ' #', '# #', ' ### '],
|
||||
'6': [' ### ', '# #', '# ', '#### ', '# #', '# #', ' ### '],
|
||||
'7': ['#####', ' #', ' # ', ' # ', ' # ', ' # ', ' # '],
|
||||
'8': [' ### ', '# #', '# #', ' ### ', '# #', '# #', ' ### '],
|
||||
'9': [' ### ', '# #', '# #', ' ####', ' #', '# #', ' ### '],
|
||||
'-': [' ', ' ', ' ', '#####', ' ', ' ', ' '],
|
||||
'.': [' ', ' ', ' ', ' ', ' ', ' ## ', ' ## '],
|
||||
':': [' ', ' ## ', ' ## ', ' ', ' ## ', ' ## ', ' '],
|
||||
'/': [' #', ' #', ' # ', ' # ', ' # ', '# ', '# '],
|
||||
'%': ['## #', '## #', ' # ', ' # ', ' # ', '# ##', '# ##'],
|
||||
'(': [' ## ', ' # ', '# ', '# ', '# ', ' # ', ' ## '],
|
||||
')': [' ## ', ' # ', ' #', ' #', ' #', ' # ', ' ## '],
|
||||
'+': [' ', ' # ', ' # ', '#####', ' # ', ' # ', ' '],
|
||||
_: [' ', ' ', ' ', ' ', ' ', ' ', '#####'],
|
||||
'@': [' ### ', '# #', '# ## ', '# ## ', '# ## ', '# ', ' ### '],
|
||||
'#': [' # # ', ' # # ', '#####', ' # # ', '#####', ' # # ', ' # # '],
|
||||
'!': [' # ', ' # ', ' # ', ' # ', ' # ', ' ', ' # '],
|
||||
'?': [' ### ', '# #', ' #', ' ## ', ' # ', ' ', ' # '],
|
||||
'*': [' ', '# #', ' # # ', '#####', ' # # ', '# #', ' '],
|
||||
'=': [' ', ' ', '#####', ' ', '#####', ' ', ' '],
|
||||
',': [' ', ' ', ' ', ' ', ' ## ', ' ## ', ' # '],
|
||||
"'": [' # ', ' # ', ' # ', ' ', ' ', ' ', ' '],
|
||||
'"': [' # # ', ' # # ', ' ', ' ', ' ', ' ', ' '],
|
||||
'[': [' ### ', ' # ', ' # ', ' # ', ' # ', ' # ', ' ### '],
|
||||
']': [' ### ', ' # ', ' # ', ' # ', ' # ', ' # ', ' ### '],
|
||||
'<': [' # ', ' # ', ' # ', '# ', ' # ', ' # ', ' # '],
|
||||
'>': ['# ', ' # ', ' # ', ' # ', ' # ', ' # ', '# '],
|
||||
'|': [' # ', ' # ', ' # ', ' # ', ' # ', ' # ', ' # '],
|
||||
'&': [' ## ', '# # ', '# # ', ' ## ', '# # ', '# # ', ' ## #'],
|
||||
$: [' ### ', '# # ', '# ', ' ### ', ' #', ' # #', ' ### '],
|
||||
}
|
||||
|
||||
export function getGlyph(char: string): Glyph | null {
|
||||
const upper = char.toUpperCase()
|
||||
return GLYPHS[upper] ?? null
|
||||
}
|
||||
Reference in New Issue
Block a user