import { useState } from 'react'; import { cn } from '@/lib/utils'; import digitaloceanLogo from '../../assets/logos/provider-digitalocean.svg'; import hetznerLogo from '../../assets/logos/provider-hetzner.svg'; import linodeLogo from '../../assets/logos/provider-linode.svg'; import scalewayLogo from '../../assets/logos/provider-scaleway.svg'; import vultrLogo from '../../assets/logos/provider-vultr.svg'; import { Section } from '../ui/section'; import perfData from './vendor-matrix-data.json'; /* * vibecarbon.com-SPECIFIC section. HomePreview is the Launch-UI rebuild of the * vibecarbon.com homepage (it will replace Home as the marketing site); it * ships with the carbon template on purpose — users are welcome to crib the * sections. Wire this section into HomePreview when it takes over as Home. * * Sovereignty / "deploy anywhere" section: an honest vendor × deploy-scenario * matrix where every supported cell carries its measured deploy time. The * numbers come from `vendor-matrix-data.json`, a generated byte-identical * copy of the root repo's `docs/perf-data.json` — refreshed automatically by * each green CI perf run, never hand-edited (the census walk in * tests/unit/metrics/readme-provider-coverage.test.ts enforces the sync). * * The `tiers` values mirror each provider's `SUPPORTED_TIERS` in the CLI * (src/lib/providers/*.js); the same census walk enforces that sync too, so * a tier flip in the CLI fails unit tests until this file follows. * * Cell language: a measured cell shows its timing; an applicable cell with no * number yet — an unbuilt scenario ('soon') or an unmeasured command on a * supported one (failover, which CI does not emit timings for yet) — shows * "Coming soon"; a cell that will never hold a number (failover on a non-HA * tier) is a dash. */ type Support = 'yes' | 'beta' | 'soon' | 'no'; const TIERS = [ { key: 'compose', label: 'Compose', note: 'single server' }, { key: 'compose-ha', label: 'Compose HA', note: '2 region replication' }, { key: 'k8s', label: 'Kubernetes', note: 'autoscaling' }, { key: 'k8s-ha', label: 'Kubernetes HA', note: 'autoscaling / multi-region' }, ] as const; type TierKey = (typeof TIERS)[number]['key']; interface ProviderRow { /** Provider registry key — must match src/lib/providers/*.js and perf-data. */ id: string; name: string; /** Official mark (Simple Icons, CC0), brand fill baked into the SVG. */ logo: string; badge?: { text: string; tone: 'success' | 'primary' }; tiers: Record; } const PROVIDERS: ProviderRow[] = [ { id: 'hetzner', name: 'Hetzner Cloud', logo: hetznerLogo, tiers: { compose: 'yes', 'compose-ha': 'yes', k8s: 'yes', 'k8s-ha': 'yes' }, }, { id: 'digitalocean', name: 'DigitalOcean', logo: digitaloceanLogo, tiers: { compose: 'yes', 'compose-ha': 'yes', k8s: 'yes', 'k8s-ha': 'yes' }, }, { id: 'linode', name: 'Linode', logo: linodeLogo, badge: { text: 'New', tone: 'success' }, tiers: { compose: 'yes', 'compose-ha': 'yes', k8s: 'soon', 'k8s-ha': 'soon' }, }, { id: 'vultr', name: 'Vultr', logo: vultrLogo, badge: { text: 'New', tone: 'success' }, tiers: { compose: 'yes', 'compose-ha': 'yes', k8s: 'soon', 'k8s-ha': 'soon' }, }, { id: 'scaleway', name: 'Scaleway', logo: scalewayLogo, badge: { text: 'New', tone: 'success' }, tiers: { compose: 'yes', 'compose-ha': 'yes', k8s: 'soon', 'k8s-ha': 'soon' }, }, ]; // --------------------------------------------------------------------------- // Measured timings — read from the generated data copy // --------------------------------------------------------------------------- interface PerfScenario { deploy?: number; 'warm-deploy'?: number; backup?: number; restore?: number; scale?: number; /** Not emitted by CI yet; the failover tab reads it the day it appears. */ failover?: number; destroy?: number; } const SCENARIOS: Record | undefined> = Object.fromEntries( Object.entries( perfData.providers as Record }> ).map(([id, entry]) => [id, entry.scenarios]) ); function formatDuration(ms: number): string { if (ms < 1_000) return `${Math.round(ms)}ms`; const totalSeconds = ms / 1_000; if (totalSeconds < 60) return `${totalSeconds.toFixed(1)}s`; const minutes = Math.floor(totalSeconds / 60); const seconds = Math.round(totalSeconds % 60); const carry = Math.floor(seconds / 60); return `${minutes + carry}m ${seconds % 60}s`; } // --------------------------------------------------------------------------- // Cells // --------------------------------------------------------------------------- /** The perf-tracked CLI commands; two tabs show timing pairs. */ type StepKey = 'deploy' | 'backup-restore' | 'scale' | 'failover' | 'destroy'; const STEP_TABS: Array<{ key: StepKey; label: string }> = [ { key: 'deploy', label: 'deploy' }, { key: 'backup-restore', label: 'backup / restore' }, { key: 'scale', label: 'scale' }, { key: 'failover', label: 'failover' }, { key: 'destroy', label: 'destroy' }, ]; /** Pair tabs: [top, bottom] timing keys, plus the row-key labels for each. */ const PAIRS: Partial> = { deploy: ['deploy', 'warm-deploy'], 'backup-restore': ['backup', 'restore'], }; const PAIR_LABELS: Partial> = { deploy: ['New Deploy', 'Redeploy'], 'backup-restore': ['Backup', 'Restore'], }; /** Failover is an HA capability; it never applies to the single-* tiers. */ const HA_TIERS: ReadonlySet = new Set(['compose-ha', 'k8s-ha']); function ComingSoon() { return ( Coming soon ); } function Cell({ support, timings, step, tier, }: { support: Support; timings?: PerfScenario; step: StepKey; tier: TierKey; }) { // A command that can never apply to this tier is a dash whatever the // support state — "coming soon" is reserved for cells that will one day // hold a number. if (support === 'no' || (step === 'failover' && !HA_TIERS.has(tier))) return ( — ); if (support === 'beta') { return ( Beta ); } if (support === 'soon') return ; // Supported: lead with the measured timing(s) when this scenario has a // green CI baseline; "coming soon" otherwise — the applicable-but-unmeasured // cell (e.g. failover, which CI does not emit timings for yet) makes the // same promise as an unbuilt scenario: a number is on its way. Pair tabs // (deploy, backup/restore) stack two times, labeled once per table by the // row key; the rest are a single time. const pair = PAIRS[step]; if (!pair) { const value = timings?.[step as keyof PerfScenario]; if (value == null) return ; return ( {formatDuration(value)} ); } const top = timings?.[pair[0]]; const bottom = timings?.[pair[1]]; if (top == null && bottom == null) return ; return ( {top != null ? formatDuration(top) : '—'} {bottom != null ? formatDuration(bottom) : '—'} ); } /** Once-per-table timing-pair key, aligned to the stacked pair in every cell. */ function RowKey({ labels }: { labels: [string, string] }) { return ( {labels[0]} {labels[1]} ); } // --------------------------------------------------------------------------- // Section // --------------------------------------------------------------------------- export default function VendorMatrix() { const [step, setStep] = useState('deploy'); return (
{/* Leads with the measurement, not the ideology. The Sovereign and Agnostic argument this used to open with now lives in the pillars section above, and making it twice weakened both. */}
Performance

Fast Automated DevOps.

Vibecarbon CLI commands are constantly tested and optimized for speed and reliability on real infrastructure.

{/* One tab per perf-tracked CLI command; the table below re-reads the same generated data for whichever command is selected. */}
{STEP_TABS.map((tab) => ( ))}
{/* Row-key column. Fixed width so the timing columns sit in the same place whether or not a tab renders the key. */} ))} {PROVIDERS.map((p, i) => ( {/* Every cell renders a fixed 44px-tall content box (the h-[44px] wrappers below), so the table keeps one height whichever tab is selected — stacked timing pairs, single values, chips, and dashes all occupy the same row. */} {TIERS.map((t) => ( ))} ))}
Provider {TIERS.map((t) => ( {t.label} {/* block + w-fit forces the pill onto its own line despite the th's whitespace-nowrap. */} {t.note}
{/* min-width spans the longest name so every row's badge starts at the same x. */} {p.name} {p.badge && ( {p.badge.text} )}
{/* The key reads once, on the top row; the alignment carries it for the rows below. Only the pair tabs need it. */} {i === 0 && PAIR_LABELS[step] && }
— Not applicable
); }