feat: add marketing site and expand Proxmox management features
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.astro/
|
||||
npm-debug.log*
|
||||
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'astro/config'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
export default defineConfig({
|
||||
output: 'static',
|
||||
vite: { plugins: [tailwindcss()] },
|
||||
})
|
||||
Generated
+7544
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "clustri-site",
|
||||
"type": "module",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource-variable/geist": "^5.3.0",
|
||||
"@fontsource-variable/jetbrains-mono": "^5.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
"astro": "^5.18.2",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"typescript": "^7.0.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<polygon
|
||||
points="30,16 23,28.124 9,28.124 2,16 9,3.876 23,3.876"
|
||||
fill="none"
|
||||
stroke="#e08a3c"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
<polygon
|
||||
points="23,16 19.5,22.062 12.5,22.062 9,16 12.5,9.938 19.5,9.938"
|
||||
fill="none"
|
||||
stroke="#e08a3c"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 350 B |
@@ -0,0 +1,482 @@
|
||||
---
|
||||
// Static data for the mockup — compiled away at build time, zero runtime JS.
|
||||
const navItems = [
|
||||
{ label: 'Dashboard', icon: 'grid', active: true },
|
||||
{ label: 'Nodes', icon: 'server', active: false },
|
||||
{ label: 'VMs', icon: 'box', active: false },
|
||||
{ label: 'Containers', icon: 'box', active: false },
|
||||
{ label: 'Storage', icon: 'hdd', active: false },
|
||||
{ label: 'Tasks', icon: 'todo', active: false },
|
||||
{ label: 'Backups', icon: 'shield', active: false },
|
||||
]
|
||||
|
||||
const clusterNodes = [
|
||||
{ name: 'pve1', dot: 'bg-success shadow-[0_0_6px] shadow-success/60', chip: 'primary' },
|
||||
{ name: 'pve2', dot: 'bg-warning', chip: null },
|
||||
{ name: 'pve3', dot: 'bg-success', chip: null },
|
||||
]
|
||||
|
||||
const nodeCards = [
|
||||
{
|
||||
name: 'pve1',
|
||||
dot: 'bg-success shadow-[0_0_6px] shadow-success/60',
|
||||
chip: { label: 'primary', cls: 'border-primary/30 bg-primary/10 text-primary' },
|
||||
statusLabel: null,
|
||||
fill: 'bg-primary',
|
||||
rows: [
|
||||
{ label: 'CPU', value: '42% · 3.4/8 c', width: '42%' },
|
||||
{ label: 'RAM', value: '10.2/32 GiB', width: '32%' },
|
||||
{ label: 'Disk', value: '187/500 GB', width: '37%' },
|
||||
],
|
||||
footer: ['up 42d 11h', '4 VMs'],
|
||||
},
|
||||
{
|
||||
name: 'pve2',
|
||||
dot: 'bg-warning',
|
||||
chip: null,
|
||||
statusLabel: 'failover',
|
||||
statusCls: 'text-warning',
|
||||
fill: 'bg-warning',
|
||||
rows: [
|
||||
{ label: 'CPU', value: '31% · 2.5/8 c', width: '31%' },
|
||||
{ label: 'RAM', value: '7.1/32 GiB', width: '22%' },
|
||||
{ label: 'Disk', value: '163/500 GB', width: '33%' },
|
||||
],
|
||||
footer: ['up 9d 4h', '2 VMs'],
|
||||
},
|
||||
{
|
||||
name: 'pve3',
|
||||
dot: 'bg-success shadow-[0_0_6px] shadow-success/60',
|
||||
chip: null,
|
||||
statusLabel: 'online',
|
||||
statusCls: 'text-success',
|
||||
fill: 'bg-primary',
|
||||
rows: [
|
||||
{ label: 'CPU', value: '18% · 1.4/8 c', width: '18%' },
|
||||
{ label: 'RAM', value: '5.8/16 GiB', width: '36%' },
|
||||
{ label: 'Disk', value: '121/240 GB', width: '50%' },
|
||||
],
|
||||
footer: ['up 117d 2h', '3 VMs'],
|
||||
},
|
||||
]
|
||||
|
||||
const tasks = [
|
||||
{
|
||||
chip: { label: 'RUNNING', cls: 'border-warning/25 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-border 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' },
|
||||
{ label: 'RAM', icon: 'mem', percent: '24%', width: '24%', detail: '23.1 / 96 GiB' },
|
||||
]
|
||||
|
||||
const quickActions = [
|
||||
{ label: 'Start', icon: 'play' },
|
||||
{ label: 'Stop', icon: 'stop' },
|
||||
{ label: 'Migrate', icon: 'migrate' },
|
||||
{ label: 'Console', icon: 'term' },
|
||||
]
|
||||
---
|
||||
|
||||
<div
|
||||
role="img"
|
||||
aria-label="Clustri desktop app showing the cluster dashboard"
|
||||
class="w-full select-none"
|
||||
>
|
||||
<!-- Inline icon library (rendered once, referenced via <use>) -->
|
||||
<svg class="absolute h-0 w-0 overflow-hidden" aria-hidden="true">
|
||||
<defs>
|
||||
<symbol id="i-hex" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<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" />
|
||||
</symbol>
|
||||
<symbol id="i-grid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect width="7" height="9" x="3" y="3" rx="1" />
|
||||
<rect width="7" height="5" x="14" y="3" rx="1" />
|
||||
<rect width="7" height="9" x="14" y="12" rx="1" />
|
||||
<rect width="7" height="5" x="3" y="16" rx="1" />
|
||||
</symbol>
|
||||
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect width="20" height="8" x="2" y="2" rx="2" />
|
||||
<rect width="20" height="8" x="2" y="14" rx="2" />
|
||||
<line x1="6" x2="6.01" y1="6" y2="6" />
|
||||
<line x1="6" x2="6.01" y1="18" y2="18" />
|
||||
</symbol>
|
||||
<symbol id="i-box" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 8a2 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" />
|
||||
<path d="m3.3 7 8.7 5 8.7-5" />
|
||||
<path d="M12 22V12" />
|
||||
</symbol>
|
||||
<symbol id="i-hdd" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="22" x2="2" y1="12" y2="12" />
|
||||
<path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" />
|
||||
<line x1="6" x2="6.01" y1="16" y2="16" />
|
||||
<line x1="10" x2="10.01" y1="16" y2="16" />
|
||||
</symbol>
|
||||
<symbol id="i-todo" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect width="6" height="6" x="3" y="5" rx="1" />
|
||||
<path d="m3 17 2 2 4-4" />
|
||||
<path d="M13 6h8" />
|
||||
<path d="M13 12h8" />
|
||||
<path d="M13 18h8" />
|
||||
</symbol>
|
||||
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1 1 0 0 1 1.52 0C14.5 3.8 17 5 19 5a1 1 0 0 1 1 1z" />
|
||||
</symbol>
|
||||
<symbol id="i-gear" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</symbol>
|
||||
<symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M5 12h14" />
|
||||
<path d="M12 5v14" />
|
||||
</symbol>
|
||||
<symbol id="i-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" />
|
||||
<path d="M12 9v4" />
|
||||
<path d="M12 17h.01" />
|
||||
</symbol>
|
||||
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect width="16" height="16" x="4" y="4" rx="2" />
|
||||
<rect width="6" height="6" x="9" y="9" rx="1" />
|
||||
<path d="M15 2v2" />
|
||||
<path d="M15 20v2" />
|
||||
<path d="M2 15h2" />
|
||||
<path d="M2 9h2" />
|
||||
<path d="M20 15h2" />
|
||||
<path d="M20 9h2" />
|
||||
<path d="M9 2v2" />
|
||||
<path d="M9 20v2" />
|
||||
</symbol>
|
||||
<symbol id="i-mem" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M6 19v-3" />
|
||||
<path d="M10 19v-3" />
|
||||
<path d="M14 19v-3" />
|
||||
<path d="M18 19v-3" />
|
||||
<path d="M8 11V9" />
|
||||
<path d="M16 11V9" />
|
||||
<path d="M12 11V9" />
|
||||
<path d="M2 15h20" />
|
||||
<path d="M2 7a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v1.1a2 2 0 0 0 0 3.837V17a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-5.1a2 2 0 0 0 0-3.837Z" />
|
||||
</symbol>
|
||||
<symbol id="i-activity" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M22 12h-4l-3 9L9 3l-3 9H2" />
|
||||
</symbol>
|
||||
<symbol id="i-play" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polygon points="6 3 20 12 6 21 6 3" />
|
||||
</symbol>
|
||||
<symbol id="i-stop" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect width="10" height="10" x="7" y="7" rx="1" />
|
||||
</symbol>
|
||||
<symbol id="i-migrate" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m16 3 4 4-4 4" />
|
||||
<path d="M20 7H4" />
|
||||
<path d="m8 21-4-4 4-4" />
|
||||
<path d="M4 17h16" />
|
||||
</symbol>
|
||||
<symbol id="i-term" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="4 17 10 11 4 5" />
|
||||
<line x1="12" x2="20" y1="19" y2="19" />
|
||||
</symbol>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<!-- Window frame -->
|
||||
<div class="overflow-hidden rounded-lg border border-border bg-card shadow-card">
|
||||
<!-- Title bar -->
|
||||
<div class="relative flex h-9 items-center border-b border-border 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-border bg-muted-foreground/40"></span>
|
||||
<span class="h-2.5 w-2.5 rounded-full border border-border bg-muted-foreground/40"></span>
|
||||
<span class="h-2.5 w-2.5 rounded-full border border-border 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>
|
||||
|
||||
<!-- Failover banner -->
|
||||
<div class="flex items-center gap-2 border-b border-warning/25 bg-warning/10 px-4 py-1.5">
|
||||
<span class="h-1.5 w-1.5 shrink-0 rounded-full bg-warning" aria-hidden="true"></span>
|
||||
<p class="shrink-0 font-mono text-[10px] text-warning">Operating on a fallback node</p>
|
||||
<p class="ml-auto min-w-0 truncate font-mono text-[10px] tabular-nums text-warning/80">
|
||||
https://192.168.1.12:8006
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="flex">
|
||||
<!-- Sidebar -->
|
||||
<aside class="hidden w-52 shrink-0 flex-col border-r border-border bg-card sm:flex">
|
||||
<!-- Brand header -->
|
||||
<div class="flex h-14 shrink-0 items-center gap-2.5 border-b border-border 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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation -->
|
||||
<div class="min-h-0 flex-1 p-2">
|
||||
<div class="space-y-1">
|
||||
<!-- Active connection -->
|
||||
<div class="flex items-center gap-2.5 rounded-md bg-accent px-3 py-2">
|
||||
<span
|
||||
class="h-1.5 w-1.5 shrink-0 rounded-full bg-success shadow-[0_0_6px] shadow-success/60"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span class="truncate font-mono text-[13px] font-medium text-accent-foreground">home-lab</span>
|
||||
</div>
|
||||
|
||||
<!-- Sub-nav -->
|
||||
<div class="ml-4 mt-1 space-y-0.5">
|
||||
{
|
||||
navItems.map((item) => (
|
||||
<button
|
||||
type="button"
|
||||
class:list={[
|
||||
'relative flex w-full items-center gap-2.5 rounded-md px-3 py-1.5 text-[12px] font-medium transition-colors',
|
||||
item.active
|
||||
? 'bg-primary/10 text-primary'
|
||||
: 'text-muted-foreground hover:bg-accent/60 hover:text-foreground',
|
||||
]}
|
||||
>
|
||||
{item.active && (
|
||||
<span
|
||||
class="absolute left-0 top-1/2 h-4 w-0.5 -translate-y-1/2 rounded-full bg-primary"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
)}
|
||||
<svg class="h-4 w-4 shrink-0" aria-hidden="true"><use href={`#i-${item.icon}`} /></svg>
|
||||
<span>{item.label}</span>
|
||||
</button>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Second connection (offline) -->
|
||||
<div class="mt-2">
|
||||
<div class="flex items-center gap-2.5 rounded-md px-3 py-2">
|
||||
<span class="h-1.5 w-1.5 shrink-0 rounded-full bg-muted-foreground/40" aria-hidden="true"></span>
|
||||
<span class="truncate font-mono text-[13px] font-medium text-muted-foreground">office-prod</span>
|
||||
</div>
|
||||
<p class="ml-7 font-mono text-[10px] text-muted-foreground">Offline</p>
|
||||
</div>
|
||||
|
||||
<!-- Cluster nodes -->
|
||||
<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="space-y-0.5">
|
||||
{
|
||||
clusterNodes.map((n) => (
|
||||
<div class="flex w-full items-center gap-2.5 rounded-md px-3 py-1.5">
|
||||
<span class={`h-1.5 w-1.5 shrink-0 rounded-full ${n.dot}`} aria-hidden="true"></span>
|
||||
<span class="min-w-0 flex-1 truncate font-mono text-[13px] font-medium text-muted-foreground">
|
||||
{n.name}
|
||||
</span>
|
||||
{n.chip && (
|
||||
<span class="shrink-0 rounded border border-primary/30 bg-primary/10 px-1 py-px font-mono text-[9px] uppercase text-primary">
|
||||
{n.chip}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar footer -->
|
||||
<div class="space-y-1 border-t border-border 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-border 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>
|
||||
</aside>
|
||||
|
||||
<!-- Main area -->
|
||||
<main class="min-w-0 flex-1 bg-background p-4">
|
||||
<div class="space-y-4">
|
||||
<!-- Header row -->
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<h2 class="text-base font-semibold tracking-tight">Overview</h2>
|
||||
<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>
|
||||
<span class="rounded border border-success/25 bg-success/10 px-1.5 py-0.5 font-mono text-[10px] text-success">
|
||||
Connected
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Node health grid -->
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
{
|
||||
nodeCards.map((node) => (
|
||||
<article class="rounded-md border border-border 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>
|
||||
<h3 class="truncate font-mono text-[13px] font-semibold tracking-tight text-foreground">
|
||||
{node.name}
|
||||
</h3>
|
||||
</div>
|
||||
{node.chip && (
|
||||
<span class={`shrink-0 rounded border px-1 py-px font-mono text-[9px] uppercase ${node.chip.cls}`}>
|
||||
{node.chip.label}
|
||||
</span>
|
||||
)}
|
||||
{node.statusLabel && (
|
||||
<span class={`shrink-0 font-mono text-[9px] font-medium uppercase ${node.statusCls}`}>
|
||||
{node.statusLabel}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div class="mt-3 space-y-2.5">
|
||||
{
|
||||
node.rows.map((row) => (
|
||||
<div>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="font-mono text-[9px] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
{row.label}
|
||||
</span>
|
||||
<span class="font-mono text-[10px] tabular-nums text-muted-foreground">
|
||||
{row.value}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-1 h-1 overflow-hidden rounded-full bg-muted">
|
||||
<div class={`h-full rounded-full ${node.fill}`} style={`width: ${row.width}`}></div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div class="mt-3 flex items-center justify-between border-t pt-2 font-mono text-[9px] tabular-nums text-muted-foreground">
|
||||
<span>{node.footer[0]}</span>
|
||||
<span>{node.footer[1]}</span>
|
||||
</div>
|
||||
</article>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Lower row: activity feed + resource gauges -->
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-5">
|
||||
<section class="rounded-md border border-border bg-card sm:col-span-3">
|
||||
<header class="flex items-center gap-2 border-b border-border 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>
|
||||
</header>
|
||||
<div class="divide-y divide-border">
|
||||
{
|
||||
tasks.map((task) => (
|
||||
<div class="flex items-center justify-between gap-3 px-3 py-2">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<span class={`shrink-0 rounded border px-1.5 py-0.5 font-mono text-[9px] font-medium ${task.chip.cls}`}>
|
||||
{task.chip.label}
|
||||
</span>
|
||||
<p class="truncate font-mono text-[11px] tabular-nums text-foreground">
|
||||
{task.text}
|
||||
<span class="text-muted-foreground"> · {task.node}</span>
|
||||
</p>
|
||||
</div>
|
||||
<span class="shrink-0 font-mono text-[10px] tabular-nums text-muted-foreground">
|
||||
{task.time}
|
||||
</span>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="space-y-3 sm:col-span-2">
|
||||
{
|
||||
gauges.map((gauge) => (
|
||||
<section class="rounded-md border border-border 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>
|
||||
</div>
|
||||
<div class="mt-2 flex items-end justify-between gap-2">
|
||||
<span class="font-mono text-2xl font-semibold leading-none tracking-tight tabular-nums">
|
||||
{gauge.percent}
|
||||
</span>
|
||||
<span class="font-mono text-[10px] tabular-nums text-muted-foreground">
|
||||
{gauge.detail}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-2 h-1 overflow-hidden rounded-full bg-muted">
|
||||
<div class="h-full rounded-full bg-primary" style={`width: ${gauge.width}`}></div>
|
||||
</div>
|
||||
</section>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick actions -->
|
||||
<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-border 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>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,152 @@
|
||||
<!--
|
||||
ArchitectureStrip — the request path from React to the Proxmox API.
|
||||
|
||||
Three sequential stages: React frontend → Rust backend → Proxmox API.
|
||||
The middle stage carries the brand accent (border-primary/30,
|
||||
bg-primary/10) so the pipeline reads left-to-right, not as three equal
|
||||
cards. Below lg the stages stack with ↓ connectors between them.
|
||||
|
||||
Design constraints (see src/styles/global.css):
|
||||
- Dark-only surfaces: bg-background / bg-card; muted-orange text-primary is
|
||||
the single brand accent.
|
||||
- Green / amber / red are reserved for status semantics (success /
|
||||
warning / destructive) only.
|
||||
- No gradients, no glows, no glassmorphism, no emoji, no fake metrics.
|
||||
-->
|
||||
|
||||
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
<section
|
||||
id="architecture"
|
||||
aria-labelledby="architecture-title"
|
||||
class="scroll-mt-24 py-16 sm:py-20"
|
||||
>
|
||||
<p class="font-mono text-[11px] uppercase tracking-[0.2em] text-muted-foreground">
|
||||
ARCHITECTURE
|
||||
</p>
|
||||
<h2
|
||||
id="architecture-title"
|
||||
class="mt-4 text-2xl font-semibold tracking-tighter text-foreground sm:text-3xl"
|
||||
>
|
||||
One proxy between you and the API.
|
||||
</h2>
|
||||
<p class="mt-3 max-w-[58ch] 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>
|
||||
|
||||
<!--
|
||||
3-stage pipeline: React frontend → Rust backend → Proxmox API.
|
||||
On lg the grid is 1fr / auto / 1fr / auto / 1fr with centered arrows;
|
||||
below lg the stages stack with ↓ connectors between them.
|
||||
-->
|
||||
<div class="mt-10 grid gap-3 lg:grid-cols-[1fr_auto_1fr_auto_1fr] lg:items-center">
|
||||
<!-- Stage 01 · React frontend -->
|
||||
<div class="rounded-lg border border-border bg-card p-5">
|
||||
<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-[9px] text-muted-foreground">01</span>
|
||||
</div>
|
||||
<div class="mt-1.5 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>
|
||||
|
||||
<!-- Arrow · tauri invoke -->
|
||||
<div class="hidden flex-col items-center justify-center gap-1 lg:flex" aria-hidden="true">
|
||||
<span class="text-xl 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-2 lg:hidden" aria-hidden="true">
|
||||
<span class="text-xl text-primary">↓</span>
|
||||
<span class="font-mono text-[9px] text-muted-foreground">tauri invoke</span>
|
||||
</div>
|
||||
|
||||
<!-- Stage 02 · Rust backend (emphasized core) -->
|
||||
<div class="rounded-lg border border-primary/30 bg-primary/10 p-5">
|
||||
<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-[9px] text-muted-foreground">02</span>
|
||||
</div>
|
||||
<div class="mt-1.5 space-y-1 font-mono text-[11px] text-primary/80">
|
||||
<p>ConnectionManager</p>
|
||||
<p>endpoint rotation · retry</p>
|
||||
<p>TOFU cert pinning</p>
|
||||
<p>credentials in OS keyring</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Arrow · HTTPS -->
|
||||
<div class="hidden flex-col items-center justify-center gap-1 lg:flex" aria-hidden="true">
|
||||
<span class="text-xl 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-2 lg:hidden" aria-hidden="true">
|
||||
<span class="text-xl text-primary">↓</span>
|
||||
<span class="font-mono text-[9px] text-muted-foreground">HTTPS</span>
|
||||
</div>
|
||||
|
||||
<!-- Stage 03 · Proxmox API -->
|
||||
<div class="rounded-lg border border-border bg-card p-5">
|
||||
<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-[9px] text-muted-foreground">03</span>
|
||||
</div>
|
||||
<div class="mt-1.5 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>
|
||||
|
||||
<!-- Request example + auth note, and resilience note -->
|
||||
<div class="mt-8 grid gap-6 lg:grid-cols-2 lg:items-stretch">
|
||||
<!-- Request example -->
|
||||
<div class="overflow-x-auto rounded-lg border border-border bg-card p-5">
|
||||
<p class="font-mono text-[10px] text-muted-foreground">
|
||||
POST /nodes/pve/qemu/100/status/start
|
||||
</p>
|
||||
<p class="mt-3 whitespace-nowrap 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 items-center gap-2">
|
||||
<span class="rounded border border-border bg-muted px-2 py-0.5 font-mono text-[10px] text-muted-foreground">
|
||||
PVEAPIToken
|
||||
</span>
|
||||
<span class="rounded border border-border bg-muted px-2 py-0.5 font-mono text-[10px] text-muted-foreground">
|
||||
PVEAuthCookie + CSRFPreventionToken
|
||||
</span>
|
||||
<span class="font-mono text-[10px] text-muted-foreground">
|
||||
Headers injected by the backend
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Resilience note -->
|
||||
<div class="lg:pt-5">
|
||||
<p class="font-mono text-[10px] text-muted-foreground">failover model</p>
|
||||
<p class="mt-3 text-sm leading-relaxed text-muted-foreground">
|
||||
Requests ride an ordered, deduplicated endpoint list: primary,
|
||||
fallbacks, then discovered cluster nodes. A connect, timeout, DNS, or
|
||||
transport failure retries on the next endpoint.
|
||||
</p>
|
||||
<div class="mt-4 flex flex-wrap gap-4 font-mono text-[10px]">
|
||||
<span class="flex items-center gap-1.5">
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-success" aria-hidden="true"></span>
|
||||
<span class="text-muted-foreground">connected</span>
|
||||
</span>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-warning" aria-hidden="true"></span>
|
||||
<span class="text-muted-foreground">failover</span>
|
||||
</span>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-destructive" aria-hidden="true"></span>
|
||||
<span class="text-muted-foreground">failed</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -0,0 +1,46 @@
|
||||
<!--
|
||||
DownloadCta — closing section, one centered column (the page's closing
|
||||
statement). Id anchors the Download link in the nav and footer.
|
||||
-->
|
||||
|
||||
<section id="download" class="scroll-mt-24 py-24 sm:py-32">
|
||||
<div class="mx-auto max-w-3xl px-6 text-center">
|
||||
<p class="font-mono text-[11px] uppercase tracking-[0.2em] text-muted-foreground">
|
||||
RELEASES
|
||||
</p>
|
||||
<h2 class="mt-4 text-3xl font-semibold tracking-tighter text-foreground sm:text-4xl">
|
||||
Releases coming soon.
|
||||
</h2>
|
||||
<p class="mx-auto mt-4 max-w-[46ch] text-[15px] leading-relaxed text-muted-foreground">
|
||||
Clustri is in active development. Packages for Linux, macOS, and Windows are on the way.
|
||||
</p>
|
||||
<div class="mt-8 flex flex-wrap items-center justify-center gap-3">
|
||||
<a
|
||||
href="#"
|
||||
class="rounded-md bg-primary px-5 py-2.5 text-sm font-semibold text-primary-foreground transition hover:brightness-110 active:scale-[0.98]"
|
||||
>
|
||||
Get Clustri
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
class="rounded-md border border-border bg-background px-5 py-2.5 text-sm font-semibold text-foreground transition hover:bg-accent/60"
|
||||
>
|
||||
Build from source
|
||||
</a>
|
||||
</div>
|
||||
<div class="mt-6 flex items-center justify-center gap-2">
|
||||
<span class="rounded border border-border bg-muted px-2.5 py-1 font-mono text-[11px] text-muted-foreground">
|
||||
Linux
|
||||
</span>
|
||||
<span class="rounded border border-border bg-muted px-2.5 py-1 font-mono text-[11px] text-muted-foreground">
|
||||
macOS
|
||||
</span>
|
||||
<span class="rounded border border-border bg-muted px-2.5 py-1 font-mono text-[11px] text-muted-foreground">
|
||||
Windows
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Close the section, consistent with the hero's closing hairline -->
|
||||
<div class="mt-16 border-b border-border" aria-hidden="true"></div>
|
||||
</section>
|
||||
@@ -0,0 +1,336 @@
|
||||
<!--
|
||||
FeatureBlocks — the five feature sections of the Clustri marketing site.
|
||||
|
||||
Design constraints (see src/styles/global.css):
|
||||
- Dark-only surfaces: bg-background / bg-card; muted-orange text-primary is
|
||||
the single brand accent.
|
||||
- Green / amber are reserved for status semantics (success / warning) only.
|
||||
- No gradients, no glows, no glassmorphism, no emoji, no fake metrics.
|
||||
- No uppercase eyebrows: section identity is the mono index (01–05) beside
|
||||
each heading.
|
||||
All visuals are hand-built schematic markup, never screenshots.
|
||||
-->
|
||||
|
||||
<div id="features" class="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
<!--
|
||||
01 · FAILOVER — split text/diagram.
|
||||
Below lg the grid collapses to a single column (copy first, stack after).
|
||||
-->
|
||||
<section
|
||||
id="failover"
|
||||
aria-labelledby="features-failover-title"
|
||||
class="scroll-mt-24 py-16 sm:py-20"
|
||||
>
|
||||
<div class="grid gap-10 lg:grid-cols-2 lg:items-center">
|
||||
<div>
|
||||
<div class="flex items-baseline gap-3">
|
||||
<span class="shrink-0 font-mono text-sm text-primary">01</span>
|
||||
<h2
|
||||
id="features-failover-title"
|
||||
class="text-2xl font-semibold tracking-tighter text-foreground sm:text-3xl"
|
||||
>
|
||||
Failover that happens on its own
|
||||
</h2>
|
||||
</div>
|
||||
<p class="mt-3 max-w-[58ch] text-[15px] leading-relaxed text-muted-foreground">
|
||||
Requests ride an ordered endpoint list: primary, fallbacks, then
|
||||
discovered cluster nodes. When one dies, the next takes over, and the
|
||||
status flips to failover so you always know which node answered.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
role="img"
|
||||
aria-label="Ordered endpoint stack: primary, fallback, then discovered cluster node, with a failover status bar"
|
||||
class="select-none rounded-lg border border-border bg-card p-5"
|
||||
>
|
||||
<p class="font-mono text-[10px] text-muted-foreground">
|
||||
request → endpoints[0] → retry on failure
|
||||
</p>
|
||||
<div class="mt-4">
|
||||
<div class="flex items-center justify-between gap-4 border-b border-border py-3">
|
||||
<span class="min-w-0 truncate font-mono text-[13px] tabular-nums text-foreground">
|
||||
192.168.1.10:8006
|
||||
</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>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-4 border-b border-border py-3">
|
||||
<span class="min-w-0 truncate font-mono text-[13px] tabular-nums text-foreground">
|
||||
192.168.1.11:8006
|
||||
</span>
|
||||
<span
|
||||
class="shrink-0 rounded bg-muted px-1.5 py-0.5 font-mono text-[10px] uppercase text-muted-foreground"
|
||||
>
|
||||
Fallback
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-4 border-b border-border py-3 last:border-0">
|
||||
<span class="min-w-0 truncate font-mono text-[13px] tabular-nums text-foreground">
|
||||
pve3.discovery
|
||||
</span>
|
||||
<span
|
||||
class="shrink-0 rounded bg-muted px-1.5 py-0.5 font-mono text-[10px] uppercase text-muted-foreground"
|
||||
>
|
||||
Cluster
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<span class="font-mono text-[10px] text-muted-foreground">
|
||||
status: failover
|
||||
</span>
|
||||
<div class="mt-1.5 h-1 overflow-hidden rounded-full bg-muted">
|
||||
<div class="h-full w-1/2 rounded-full bg-primary"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!--
|
||||
02 · SECURITY — full-width editorial band.
|
||||
Single column on every breakpoint; the fingerprint block is the visual
|
||||
and spans full width up to max-w-2xl.
|
||||
-->
|
||||
<section
|
||||
id="security"
|
||||
aria-labelledby="features-security-title"
|
||||
class="scroll-mt-24 py-16 sm:py-20"
|
||||
>
|
||||
<div class="flex items-baseline gap-3">
|
||||
<span class="shrink-0 font-mono text-sm text-primary">02</span>
|
||||
<h2
|
||||
id="features-security-title"
|
||||
class="text-2xl font-semibold tracking-tighter text-foreground sm:text-3xl"
|
||||
>
|
||||
Credentials never touch disk
|
||||
</h2>
|
||||
</div>
|
||||
<p class="mt-3 max-w-[58ch] text-[15px] leading-relaxed text-muted-foreground">
|
||||
API tokens live in the OS keyring. Self-signed certificates are pinned by
|
||||
SHA-256 fingerprint on first connect and verified on every connect after.
|
||||
</p>
|
||||
|
||||
<div class="mt-8 max-w-2xl rounded-lg border border-border bg-card p-5">
|
||||
<div class="flex flex-wrap items-center gap-x-3 gap-y-2">
|
||||
<span class="font-mono text-[10px] text-muted-foreground">
|
||||
SHA-256 fingerprint
|
||||
</span>
|
||||
<span
|
||||
class="rounded border border-success/25 bg-success/10 px-1.5 py-0.5 font-mono text-[10px] uppercase text-success"
|
||||
>
|
||||
Pinned
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-4 overflow-x-auto">
|
||||
<p class="whitespace-nowrap font-mono text-sm tracking-[0.18em] text-foreground">
|
||||
A3:0F:71:C9:52:B8:4D:E0:11:2A:7F:9C:63:D4:18:0B
|
||||
</p>
|
||||
</div>
|
||||
<p class="mt-3 font-mono text-[10px] text-muted-foreground">
|
||||
Example fingerprint · captured on first connect, verified on every connect after.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!--
|
||||
03 · VMS — asymmetric split (5/7).
|
||||
Below lg the grid collapses to a single column (copy first, list after).
|
||||
-->
|
||||
<section
|
||||
id="vms"
|
||||
aria-labelledby="features-vms-title"
|
||||
class="scroll-mt-24 py-16 sm:py-20"
|
||||
>
|
||||
<div class="grid gap-10 lg:grid-cols-12 lg:items-start">
|
||||
<div class="lg:col-span-5">
|
||||
<div class="flex items-baseline gap-3">
|
||||
<span class="shrink-0 font-mono text-sm text-primary">03</span>
|
||||
<h2
|
||||
id="features-vms-title"
|
||||
class="text-2xl font-semibold tracking-tighter text-foreground sm:text-3xl"
|
||||
>
|
||||
The full VM lifecycle in one place
|
||||
</h2>
|
||||
</div>
|
||||
<p class="mt-3 max-w-[58ch] text-[15px] leading-relaxed text-muted-foreground">
|
||||
Start, stop, shutdown, reboot, suspend, resume, migrate. Plus disks,
|
||||
NICs, snapshots, backup jobs, and restore.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="lg:col-span-7">
|
||||
<p class="mb-3 font-mono text-[10px] text-muted-foreground">
|
||||
migrate 102 <span class="text-muted-foreground/70">→</span> pve3
|
||||
</p>
|
||||
<div
|
||||
role="img"
|
||||
aria-label="VM list: VM 100 web-01 running, VM 101 db-01 running, VM 102 worker-01 stopped"
|
||||
class="select-none rounded-lg border border-border bg-card"
|
||||
>
|
||||
<div class="flex items-center justify-between border-b border-border px-4 py-2.5">
|
||||
<span class="font-mono text-[10px] uppercase text-muted-foreground">
|
||||
VM ID
|
||||
</span>
|
||||
<span class="font-mono text-[10px] uppercase text-muted-foreground">
|
||||
Status
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-4 border-b border-border px-4 py-2.5">
|
||||
<span class="min-w-0 font-mono text-[13px] tabular-nums text-foreground">
|
||||
100<span class="text-muted-foreground"> · web-01</span>
|
||||
</span>
|
||||
<span
|
||||
class="shrink-0 rounded border border-success/25 bg-success/10 px-1.5 py-0.5 font-mono text-[10px] uppercase text-success"
|
||||
>
|
||||
Running
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-4 border-b border-border px-4 py-2.5">
|
||||
<span class="min-w-0 font-mono text-[13px] tabular-nums text-foreground">
|
||||
101<span class="text-muted-foreground"> · db-01</span>
|
||||
</span>
|
||||
<span
|
||||
class="shrink-0 rounded border border-success/25 bg-success/10 px-1.5 py-0.5 font-mono text-[10px] uppercase text-success"
|
||||
>
|
||||
Running
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-4 border-b border-border px-4 py-2.5 last:border-0">
|
||||
<span class="min-w-0 font-mono text-[13px] tabular-nums text-foreground">
|
||||
102<span class="text-muted-foreground"> · worker-01</span>
|
||||
</span>
|
||||
<span
|
||||
class="shrink-0 rounded bg-muted px-1.5 py-0.5 font-mono text-[10px] uppercase text-muted-foreground"
|
||||
>
|
||||
Stopped
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!--
|
||||
04 · CONSOLE — full-width terminal.
|
||||
Single column on every breakpoint; the terminal window spans full width
|
||||
up to max-w-3xl.
|
||||
-->
|
||||
<section
|
||||
id="console"
|
||||
aria-labelledby="features-console-title"
|
||||
class="scroll-mt-24 py-16 sm:py-20"
|
||||
>
|
||||
<div class="flex items-baseline gap-3">
|
||||
<span class="shrink-0 font-mono text-sm text-primary">04</span>
|
||||
<h2
|
||||
id="features-console-title"
|
||||
class="text-2xl font-semibold tracking-tighter text-foreground sm:text-3xl"
|
||||
>
|
||||
Consoles that behave like the real thing
|
||||
</h2>
|
||||
</div>
|
||||
<p class="mt-3 max-w-[58ch] 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
|
||||
role="img"
|
||||
aria-label="Terminal session inside a container: pct enter 101, uptime, and its output"
|
||||
class="mt-8 max-w-3xl select-none overflow-hidden rounded-lg border border-border bg-card shadow-card"
|
||||
>
|
||||
<div class="relative flex items-center border-b border-border bg-background px-4 py-2">
|
||||
<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-border bg-muted-foreground/40"></span>
|
||||
<span class="h-2.5 w-2.5 rounded-full border border-border bg-muted-foreground/40"></span>
|
||||
<span class="h-2.5 w-2.5 rounded-full border border-border bg-muted-foreground/40"></span>
|
||||
</div>
|
||||
<p class="w-full text-center font-mono text-[10px] text-muted-foreground">
|
||||
root@pve1: ~ (xterm)
|
||||
</p>
|
||||
</div>
|
||||
<div class="overflow-x-auto bg-background px-4 py-4 font-mono text-[12px] leading-relaxed">
|
||||
<p>
|
||||
<span class="text-primary">root@pve1:~#</span>
|
||||
<span class="text-foreground">pct enter 101</span>
|
||||
</p>
|
||||
<p>
|
||||
<span class="text-primary">root@db-01:~#</span>
|
||||
<span class="text-foreground">uptime</span>
|
||||
</p>
|
||||
<!-- indentation survives Astro's whitespace trimming -->
|
||||
<p class="whitespace-pre text-muted-foreground"> 23:41:05 up 9 days, 4:21, 1 user, load average: 0.42, 0.38, 0.35</p>
|
||||
<p>
|
||||
<span class="text-primary">root@db-01:~#</span>
|
||||
<span class="animate-pulse text-foreground">█</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 flex justify-end">
|
||||
<kbd
|
||||
class="rounded border border-border bg-muted px-2 py-0.5 font-mono text-[10px] text-muted-foreground"
|
||||
>
|
||||
Ctrl+Alt+Del
|
||||
</kbd>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!--
|
||||
05 · POWER USER — shortcut sheet.
|
||||
Two columns on sm and up, single column below sm.
|
||||
-->
|
||||
<section
|
||||
id="power-user"
|
||||
aria-labelledby="features-power-user-title"
|
||||
class="scroll-mt-24 py-16 sm:py-20"
|
||||
>
|
||||
<div class="flex items-baseline gap-3">
|
||||
<span class="shrink-0 font-mono text-sm text-primary">05</span>
|
||||
<h2
|
||||
id="features-power-user-title"
|
||||
class="text-2xl font-semibold tracking-tighter text-foreground sm:text-3xl"
|
||||
>
|
||||
Built for the daily grind
|
||||
</h2>
|
||||
</div>
|
||||
<p class="mt-3 max-w-[58ch] text-[15px] leading-relaxed text-muted-foreground">
|
||||
Tasks and events stream over WebSocket, with polling as the fallback.
|
||||
Cmd/Ctrl+K opens the command palette. The tray menu keeps one-click
|
||||
actions one click away.
|
||||
</p>
|
||||
|
||||
<div class="mt-8 grid gap-x-12 gap-y-0 border-t border-border sm:grid-cols-2">
|
||||
<div class="flex items-center justify-between gap-4 border-b border-border py-4">
|
||||
<kbd
|
||||
class="shrink-0 rounded border border-border bg-muted px-2 py-0.5 font-mono text-[10px] text-muted-foreground"
|
||||
>
|
||||
Cmd/Ctrl + K
|
||||
</kbd>
|
||||
<p class="text-right text-sm text-muted-foreground">Command palette</p>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-4 border-b border-border py-4">
|
||||
<span class="shrink-0 font-mono text-[11px] text-primary">WebSocket</span>
|
||||
<p class="text-right text-sm text-muted-foreground">
|
||||
Live task & event stream
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-4 border-b border-border py-4">
|
||||
<span class="shrink-0 font-mono text-[11px] text-primary">Tray menu</span>
|
||||
<p class="text-right text-sm text-muted-foreground">
|
||||
Show / hide · connections · quit
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-4 border-b border-border py-4">
|
||||
<span class="shrink-0 font-mono text-[11px] text-primary">10s</span>
|
||||
<p class="text-right text-sm text-muted-foreground">
|
||||
Connection status polling
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
const year = new Date().getFullYear()
|
||||
---
|
||||
|
||||
<!--
|
||||
Footer — minimal, one row on desktop, stacked on mobile.
|
||||
-->
|
||||
|
||||
<footer class="border-t border-border">
|
||||
<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-8 w-8 items-center justify-center rounded-md bg-primary/15 text-primary">
|
||||
<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-4 w-4"
|
||||
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>
|
||||
</div>
|
||||
<p class="mt-3 font-mono text-[11px] text-muted-foreground">v0.1.0</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-8 sm:flex-row sm:gap-16">
|
||||
<nav aria-label="Navigate" class="space-y-2.5">
|
||||
<p class="text-[11px] font-mono uppercase tracking-[0.14em] text-muted-foreground">
|
||||
Navigate
|
||||
</p>
|
||||
<a href="#failover" class="block text-[13px] text-muted-foreground transition-colors hover:text-foreground">
|
||||
Features
|
||||
</a>
|
||||
<a href="#security" class="block text-[13px] text-muted-foreground transition-colors hover:text-foreground">
|
||||
Security
|
||||
</a>
|
||||
<a href="#console" class="block text-[13px] text-muted-foreground transition-colors hover:text-foreground">
|
||||
Console
|
||||
</a>
|
||||
<a href="#architecture" class="block text-[13px] text-muted-foreground transition-colors hover:text-foreground">
|
||||
Architecture
|
||||
</a>
|
||||
<a href="#download" class="block text-[13px] text-muted-foreground transition-colors hover:text-foreground">
|
||||
Download
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<nav aria-label="Source" class="space-y-2.5">
|
||||
<p class="text-[11px] font-mono uppercase tracking-[0.14em] text-muted-foreground">
|
||||
Source
|
||||
</p>
|
||||
<a href="#" class="block text-[13px] text-muted-foreground transition-colors hover:text-foreground">
|
||||
GitHub
|
||||
</a>
|
||||
<a href="#" class="block text-[13px] text-muted-foreground transition-colors hover:text-foreground">
|
||||
Build from source
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-10 flex flex-col gap-2 border-t border-border pt-6 md:flex-row md:items-center md:justify-between">
|
||||
<p class="text-[12px] text-muted-foreground">© {year} Clustri. MIT license.</p>
|
||||
<p class="text-[12px] text-muted-foreground">Not affiliated with Proxmox.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -0,0 +1,77 @@
|
||||
---
|
||||
import AppWindowMockup from './AppWindowMockup.astro'
|
||||
---
|
||||
|
||||
<section class="relative overflow-hidden">
|
||||
<!-- Copy block -->
|
||||
<div class="hero-copy mx-auto max-w-7xl px-6 pt-20 lg:px-8 lg:pt-24">
|
||||
<p class="font-mono text-[11px] uppercase tracking-[0.2em] text-muted-foreground">
|
||||
OPS CONSOLE
|
||||
</p>
|
||||
<h1 class="mt-4 max-w-3xl text-4xl font-semibold leading-[1.05] tracking-tighter text-foreground sm:text-5xl lg:text-6xl">
|
||||
One <span class="text-primary">console</span> for your Proxmox cluster.
|
||||
</h1>
|
||||
<p class="mt-5 max-w-2xl text-base leading-relaxed text-muted-foreground sm:text-lg">
|
||||
Manage Proxmox VE nodes, VMs, storage, and backups from a desktop app that keeps working when a node goes down.
|
||||
</p>
|
||||
<div class="mt-8 flex flex-wrap items-center gap-3">
|
||||
<a
|
||||
href="#"
|
||||
class="rounded-md bg-primary px-5 py-2.5 text-sm font-semibold text-primary-foreground transition hover:brightness-110 active:scale-[0.98]"
|
||||
>
|
||||
Get Clustri
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
class="rounded-md border border-border bg-background px-5 py-2.5 text-sm font-semibold text-foreground transition hover:bg-accent/60 active:scale-[0.98]"
|
||||
>
|
||||
Read the docs
|
||||
</a>
|
||||
</div>
|
||||
<p class="mt-8 font-mono text-[11px] tabular-nums text-muted-foreground">
|
||||
<span>v0.1.0</span><span aria-hidden="true"> · </span><span>Linux</span><span aria-hidden="true"> · </span><span>macOS</span><span aria-hidden="true"> · </span><span>Windows</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Product mockup, full-bleed below the copy -->
|
||||
<div class="hero-mockup mt-16 sm:mt-20">
|
||||
<div class="mx-auto max-w-6xl px-6 lg:px-8">
|
||||
<div class="pointer-events-none select-none [mask-image:linear-gradient(to_bottom,black_0%,black_78%,transparent_100%)]">
|
||||
<AppWindowMockup />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Close the hero -->
|
||||
<div class="mt-16 border-b border-border" aria-hidden="true"></div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
@keyframes hero-rise {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(12px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes hero-fade {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.hero-copy {
|
||||
animation: hero-rise 0.7s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
|
||||
.hero-mockup {
|
||||
animation: hero-fade 1s ease 0.15s both;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,52 @@
|
||||
<!--
|
||||
Nav — sticky site header. Renders on a single line at lg, height capped at
|
||||
64px (h-16). Links point at in-page feature anchors.
|
||||
-->
|
||||
|
||||
<header class="sticky top-0 z-50 border-b border-border bg-background/85 backdrop-blur-sm">
|
||||
<nav class="mx-auto flex h-16 max-w-7xl items-center justify-between gap-6 px-6 lg:px-8" aria-label="Primary">
|
||||
<a href="#" class="flex items-center gap-2.5">
|
||||
<span class="flex h-8 w-8 items-center justify-center rounded-md bg-primary/15 text-primary">
|
||||
<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-4 w-4"
|
||||
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>
|
||||
</a>
|
||||
|
||||
<div class="hidden items-center gap-6 md:flex">
|
||||
<a href="#failover" class="text-[13px] font-medium text-muted-foreground transition-colors hover:text-foreground">
|
||||
Features
|
||||
</a>
|
||||
<a href="#security" class="text-[13px] font-medium text-muted-foreground transition-colors hover:text-foreground">
|
||||
Security
|
||||
</a>
|
||||
<a href="#console" class="text-[13px] font-medium text-muted-foreground transition-colors hover:text-foreground">
|
||||
Console
|
||||
</a>
|
||||
<a href="#architecture" class="text-[13px] font-medium text-muted-foreground transition-colors hover:text-foreground">
|
||||
Architecture
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="#"
|
||||
class="rounded-md bg-primary px-4 py-2 text-[13px] font-semibold text-primary-foreground transition hover:brightness-110 active:scale-[0.98]"
|
||||
>
|
||||
Get Clustri
|
||||
</a>
|
||||
</nav>
|
||||
</header>
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
import '@fontsource-variable/geist'
|
||||
import '@fontsource-variable/jetbrains-mono'
|
||||
import '../styles/global.css'
|
||||
|
||||
interface Props { title?: string; description?: string }
|
||||
const { title = 'Clustri', description = 'A desktop client for managing Proxmox VE servers and clusters.' } = Astro.props
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description} />
|
||||
</head>
|
||||
<body>
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro'
|
||||
import Nav from '../components/Nav.astro'
|
||||
import Hero from '../components/Hero.astro'
|
||||
import FeatureBlocks from '../components/FeatureBlocks.astro'
|
||||
import ArchitectureStrip from '../components/ArchitectureStrip.astro'
|
||||
import DownloadCta from '../components/DownloadCta.astro'
|
||||
import Footer from '../components/Footer.astro'
|
||||
---
|
||||
|
||||
<Base
|
||||
title="Clustri — Ops Console for Proxmox VE"
|
||||
description="Clustri is a cross-platform desktop client for managing Proxmox VE servers and clusters. Failover, security, VMs, storage, backups, and consoles in one app."
|
||||
>
|
||||
<Nav />
|
||||
<main>
|
||||
<Hero />
|
||||
<FeatureBlocks />
|
||||
<ArchitectureStrip />
|
||||
<DownloadCta />
|
||||
</main>
|
||||
<Footer />
|
||||
</Base>
|
||||
@@ -0,0 +1,147 @@
|
||||
@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;
|
||||
--font-mono:
|
||||
'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;
|
||||
--radius-lg: 0.625rem;
|
||||
--radius-xl: 0.75rem;
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* Dark theme — blue-black ink surfaces, cool neutrals, muted orange.
|
||||
* The marketing site is dark-only: the app's `.dark` palette is the
|
||||
* single theme (no light/dark toggle by design).
|
||||
* ------------------------------------------------------------------ */
|
||||
--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-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-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-ring: oklch(0.72 0.15 60);
|
||||
|
||||
/* Semantic status colors — data semantics, not UI accents */
|
||||
--color-success: oklch(0.7 0.14 155);
|
||||
--color-warning: oklch(0.76 0.13 80);
|
||||
--color-info: oklch(0.68 0.11 245);
|
||||
|
||||
/* Tinted card shadow — carries the cool hue of the dark surface */
|
||||
--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);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
html {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
@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,
|
||||
pre {
|
||||
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;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: var(--color-border);
|
||||
border-radius: 999px;
|
||||
border: 3px solid transparent;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--color-muted-foreground);
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
/* Very subtle fixed grain overlay to break digital flatness */
|
||||
body::after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 60;
|
||||
pointer-events: none;
|
||||
opacity: 0.04;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict"
|
||||
}
|
||||
Reference in New Issue
Block a user