diff --git a/app/globals.css b/app/globals.css index 665a1fc..8dc85fb 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,5 +1,3 @@ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); - @tailwind base; @tailwind components; @tailwind utilities; @@ -20,7 +18,7 @@ html, body { background: #0A0A0A; color: #F5F5F5; - font-family: 'Inter', Arial, sans-serif; + font-family: var(--font-geist-sans), system-ui, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } @@ -107,3 +105,105 @@ body { .animate-marquee { animation: marquee 22s linear infinite; } + +/* ─── Linear/Vercel-grade surface system ───────────────────────────────────── */ + +/* Faint architectural grid, fades out toward edges */ +.grid-lines { + background-image: + linear-gradient(to right, rgba(255, 255, 255, 0.035) 1px, transparent 1px), + linear-gradient(to bottom, rgba(255, 255, 255, 0.035) 1px, transparent 1px); + background-size: 64px 64px; +} +.grid-fade { + -webkit-mask-image: radial-gradient(ellipse 80% 60% at 50% 0%, #000 20%, transparent 75%); + mask-image: radial-gradient(ellipse 80% 60% at 50% 0%, #000 20%, transparent 75%); +} + +/* Hairline surface — flat translucent fill + 1px border (no gradient). */ +.hairline { + background: rgba(255, 255, 255, 0.021); + border: 1px solid rgba(255, 255, 255, 0.08); +} +.hairline-hover { + transition: border-color 0.3s ease, background 0.3s ease; +} +.hairline-hover:hover { + border-color: rgba(255, 255, 255, 0.16); + background: rgba(255, 255, 255, 0.035); +} + +/* Solid headline colors (no gradient) */ +.gradient-text { color: #f4f4f5; } +.gradient-accent { color: #57a5f2; } + +/* Top-edge highlight line used on chrome / hero frame */ +.edge-light { + position: relative; +} +.edge-light::before { + content: ""; + position: absolute; + inset: 0 0 auto 0; + height: 1px; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.35), transparent); +} + +/* Soft vertical rise-in used sparingly */ +@keyframes float-y { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-6px); } +} +.animate-float-y { animation: float-y 6s ease-in-out infinite; } + +/* Live blink for status dots */ +@keyframes live-blink { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.35; } +} +.animate-live { animation: live-blink 1.6s ease-in-out infinite; } + +/* Scanning sweep for the hero chart line */ +@keyframes sweep { + from { transform: translateX(-100%); } + to { transform: translateX(100%); } +} +.animate-sweep { animation: sweep 3.2s ease-in-out infinite; } + +/* Conic border spin (announcement pill) */ +@keyframes spin-slow { + to { transform: rotate(1turn); } +} +.animate-spin-slow { animation: spin-slow 6s linear infinite; } + +/* Radar — concentric "ping" rings expanding from the hero center */ +@keyframes radar-ping { + 0% { transform: scale(0.18); opacity: 0; } + 12% { opacity: 0.5; } + 100% { transform: scale(1); opacity: 0; } +} +.radar-ping { + position: absolute; + inset: 0; + border-radius: 9999px; + border: 1px solid rgba(255, 255, 255, 0.35); + animation: radar-ping 6s cubic-bezier(0.16, 0.84, 0.44, 1) infinite; +} +/* Slow rotation for the radar sweep arm */ +@keyframes radar-spin { to { transform: rotate(360deg); } } +.radar-spin { animation: radar-spin 14s linear infinite; } + +/* Slow shimmer sweep across product frames */ +@keyframes frame-shine { + 0% { transform: translateX(-120%) skewX(-12deg); opacity: 0; } + 12% { opacity: 0.5; } + 30% { transform: translateX(220%) skewX(-12deg); opacity: 0; } + 100% { transform: translateX(220%) skewX(-12deg); opacity: 0; } +} +.animate-frame-shine { animation: frame-shine 7s ease-in-out infinite; } + +@media (prefers-reduced-motion: reduce) { + .radar-ping, .radar-spin, + .animate-frame-shine, .animate-drift, .animate-marquee, + .animate-float-y, .animate-sweep, .animate-live { animation: none !important; } +} diff --git a/app/layout.tsx b/app/layout.tsx index de5ea21..6c8e28a 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,15 +1,16 @@ import type { Metadata } from "next"; -import { Space_Grotesk, Inter } from "next/font/google"; +import { GeistSans } from "geist/font/sans"; +import { GeistMono } from "geist/font/mono"; +import { Sora } from "next/font/google"; import "./globals.css"; -import { SessionProvider } from "@/components/providers/session-provider"; -import { ProjectProvider } from "@/components/providers/project-provider"; -const inter = Inter({ subsets: ["latin"], variable: "--font-inter" }); -const spaceGrotesk = Space_Grotesk({ +const sora = Sora({ subsets: ["latin"], - weight: ["700"], - variable: "--font-space-grotesk", + weight: ["500", "600", "700"], + variable: "--font-sora", }); +import { SessionProvider } from "@/components/providers/session-provider"; +import { ProjectProvider } from "@/components/providers/project-provider"; export const metadata: Metadata = { title: "Liveboard — API Observability", @@ -18,7 +19,7 @@ export const metadata: Metadata = { export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - + diff --git a/app/page.tsx b/app/page.tsx index 48534dc..2d71cde 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,10 +1,11 @@ import { redirect } from "next/navigation"; import { auth } from "@/auth"; import { LandingNavbar } from "@/components/landing/navbar"; +import { ScrollProgress } from "@/components/landing/scroll-progress"; import { Hero } from "@/components/landing/hero"; import { IntegrationsStrip } from "@/components/landing/integrations-strip"; +import { Showcase } from "@/components/landing/showcase"; import { FeatureGrid } from "@/components/landing/feature-grid"; -import { InsightsBento } from "@/components/landing/insights-bento"; import { HowItWorks } from "@/components/landing/how-it-works"; import { CtaSection } from "@/components/landing/cta-section"; import { LandingFooter } from "@/components/landing/footer"; @@ -14,26 +15,72 @@ export default async function Home() { if (session) redirect("/overview"); return ( -
-
-
-
- -
- - -
-
- +
+ + + + + + + + + + - + + + diff --git a/components/landing/browser-frame.tsx b/components/landing/browser-frame.tsx new file mode 100644 index 0000000..5bb0be6 --- /dev/null +++ b/components/landing/browser-frame.tsx @@ -0,0 +1,46 @@ +import Image from "next/image"; + +interface BrowserFrameProps { + src: string; + alt: string; + url?: string; + width: number; + height: number; + priority?: boolean; + live?: boolean; +} + +export function BrowserFrame({ src, alt, url = "app.liveboard.dev", width, height, priority, live }: BrowserFrameProps) { + return ( +
+
+
+ + + +
+
+ + + + + {url} +
+ {live && ( +
+ + Live +
+ )} +
+ {alt} +
+ ); +} diff --git a/components/landing/code-snippet.tsx b/components/landing/code-snippet.tsx new file mode 100644 index 0000000..daa9ec0 --- /dev/null +++ b/components/landing/code-snippet.tsx @@ -0,0 +1,63 @@ +import { cn } from "@/lib/utils"; + +/* Lightweight, hand-tokenized syntax highlighting for the handful of code + snippets on the landing page. Palette tuned to the dark theme. */ + +export const tok = { + fn: "text-[#6aa9f0]", // functions / methods / commands + str: "text-[#5ac98a]", // strings, packages, urls + prop: "text-[#c795f0]", // properties / identifiers of note + txt: "text-[#d4d4dc]", // plain identifiers + punc: "text-[#5f5f68]", // punctuation / brackets + prompt: "text-[#4a4a52] select-none", // shell prompt / arrow +}; + +export function CodeSnippet({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) { + return ( + {children} + ); +} + +// ── Prebuilt, reused snippets ──────────────────────────────────────────────── + +export function InstallSnippet({ prompt = true }: { prompt?: boolean }) { + return ( + + {prompt && $ } + npm + install + liveboard-sdk + + ); +} + +export function MiddlewareSnippet() { + return ( + + app + .use + ( + liveboard + .middleware + ({"{ "} + apiKey + {" }"}) + ) + + ); +} + +export function DashboardUrlSnippet() { + return ( + + → + liveboard.dev/overview + + ); +} diff --git a/components/landing/count-up.tsx b/components/landing/count-up.tsx new file mode 100644 index 0000000..6515755 --- /dev/null +++ b/components/landing/count-up.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { animate, useInView } from "framer-motion"; + +interface CountUpProps { + to: number; + decimals?: number; + prefix?: string; + suffix?: string; + duration?: number; +} + +export function CountUp({ to, decimals = 0, prefix = "", suffix = "", duration = 1.6 }: CountUpProps) { + const ref = useRef(null); + const inView = useInView(ref, { once: true, margin: "-80px" }); + const [value, setValue] = useState(0); + + useEffect(() => { + if (!inView) return; + const controls = animate(0, to, { + duration, + ease: [0.22, 1, 0.36, 1], + onUpdate: (v) => setValue(v), + }); + return () => controls.stop(); + }, [inView, to, duration]); + + return ( + + {prefix} + {value.toFixed(decimals)} + {suffix} + + ); +} diff --git a/components/landing/cta-section.tsx b/components/landing/cta-section.tsx index 6ce1bae..b44295d 100644 --- a/components/landing/cta-section.tsx +++ b/components/landing/cta-section.tsx @@ -6,16 +6,18 @@ import { Reveal } from "@/components/landing/reveal"; export function CtaSection() { return ( -
-
+
+ {/* subtle monochrome focus + faint radar echo */} +
+
+
+
+
-

- Ship with confidence. +

+ Ship with confidence.

-

+

Open source, self-hostable, and free to start. Find out about the incident before your customers do.

diff --git a/components/landing/feature-grid.tsx b/components/landing/feature-grid.tsx index c549ce5..3d07015 100644 --- a/components/landing/feature-grid.tsx +++ b/components/landing/feature-grid.tsx @@ -1,17 +1,9 @@ -import { Sparkles, Zap } from "lucide-react"; -import { Badge } from "@/components/ui/badge"; +import { Bell, Gauge, Sparkles, Users, Zap } from "lucide-react"; import { Reveal } from "@/components/landing/reveal"; import { SpotlightCard } from "@/components/landing/spotlight-card"; +import { InstallSnippet } from "@/components/landing/code-snippet"; -const FLAME_SPANS = [ - { service: "gateway", width: "100%", color: "#3B82F6", indent: 0 }, - { service: "auth", width: "22%", color: "#A855F7", indent: 1 }, - { service: "orders-api", width: "74%", color: "#06B6D4", indent: 1 }, - { service: "postgres", width: "58%", color: "#F59E0B", indent: 2 }, - { service: "payments", width: "31%", color: "#F43F5E", indent: 2 }, -]; - -const UPTIME_DAYS = Array.from({ length: 48 }, (_, i) => (i === 31 ? "yellow" : i === 32 ? "red" : "green")); +const UPTIME_DAYS = Array.from({ length: 44 }, (_, i) => (i === 29 ? "yellow" : i === 30 ? "red" : "green")); const TOP_USERS = [ { id: "u_2291", reqs: "12.4k", color: "#3B82F6" }, @@ -19,55 +11,71 @@ const TOP_USERS = [ { id: "u_3387", reqs: "7.1k", color: "#06B6D4" }, ]; +function Eyebrow({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + ); +} + +function cardShell(extra = "") { + return `h-full rounded-2xl hairline hairline-hover p-6 ${extra}`; +} + export function FeatureGrid() { return ( -
- - Platform -

- Everything you need to know what your API is doing +
+ + Platform +

+ Everything you need to know +
what your API is doing

-

- Not another generic APM dashboard — built around the questions you actually - ask at 2am. +

+ Not another generic APM dashboard — built around the questions you actually ask at 2am.

-
- {/* Onboarding — small */} +
+ {/* Onboarding */} - -
- -

60-second onboarding

-

+ +

+ +

60-second onboarding

+

One line of middleware. No agents, no sidecars, no config files to maintain.

-
- $ npm install liveboard-sdk +
+
- {/* Distributed tracing — small with mock flame graph */} + {/* Endpoints / health */} - -
- - - -

Distributed tracing

-

- Trace IDs propagate across every service automatically. See exactly where time went. + +

+ +

Per-endpoint health

+

+ p50 / p95 / p99, error rate and a rolling health score for every route you serve.

-
- {FLAME_SPANS.map((span, i) => ( -
-
+
+ {[ + { r: "/checkout", p: 92, c: "#22C55E" }, + { r: "/orders", p: 68, c: "#F59E0B" }, + { r: "/search", p: 34, c: "#EF4444" }, + ].map((e) => ( +
+ {e.r} +
+
+
+ {e.p}
))}
@@ -75,57 +83,61 @@ export function FeatureGrid() { - {/* AI incident summaries — featured, spans 2 rows on md+ */} + {/* AI incident summaries — featured, tall */} - -
- -

AI incident summaries

-

- When error rate or p99 latency breaks 3 standard deviations from baseline, - an LLM reads the surrounding traffic and writes the postmortem before you do. + +

+
+ + + AI + +
+

AI incident summaries

+

+ When error rate or p99 latency breaks 3σ from baseline, an LLM reads the surrounding + traffic and writes the postmortem before you do.

-
+
- Critical - 2m ago + + Critical + + 2m ago
-

- p99 latency spike on /api/checkout +

+ p99 latency spike on /api/checkout

-

- Latency jumped 6.4x starting 14:02 UTC, correlated with a downstream timeout - from the payments provider. 312 requests affected. Recommend adding a circuit - breaker around the payment client. +

+ Latency jumped 6.4× starting 14:02 UTC, correlated with a downstream timeout from the + payments provider. 312 requests affected. Recommend a circuit breaker around the + payment client.

- - Resolved in 4m + + Resolved in 4m
- {/* User-centric — small */} + {/* User-centric */} - -
- - - - -

User-centric observability

-

+ +

+ +

User-centric observability

+

Every event carries a user ID. Know exactly who an incident hits, not just which route.

{TOP_USERS.map((u) => (
- - {u.id} - {u.reqs} req + + {u.id} + {u.reqs} req
))}
@@ -133,22 +145,19 @@ export function FeatureGrid() { - {/* Status page — small */} + {/* Alerts + status page combined-ish */} - -
- - - - -

Auto-generated status page

-

- A public, branded status page ships for free — fed by the same data, zero extra setup. + +

+ +

Alerts + status page

+

+ Route anomalies to Slack, PagerDuty or a webhook — and ship a public status page for free.

-
- 99.97% uptime - 48 days +
+ 99.97% uptime + 44 days
{UPTIME_DAYS.map((color, i) => ( @@ -157,7 +166,7 @@ export function FeatureGrid() { className="h-4 flex-1 rounded-[1px]" style={{ backgroundColor: color === "green" ? "#22C55E" : color === "yellow" ? "#F59E0B" : "#EF4444", - opacity: color === "green" ? 0.5 : 0.9, + opacity: color === "green" ? 0.45 : 0.9, }} /> ))} diff --git a/components/landing/footer.tsx b/components/landing/footer.tsx index f6989e4..6c33896 100644 --- a/components/landing/footer.tsx +++ b/components/landing/footer.tsx @@ -36,8 +36,8 @@ const COLUMNS: { title: string; links: FooterLink[] }[] = [ export function LandingFooter() { return ( -