Improve mobile navigation, responsive layout, and theme styling

This commit is contained in:
Rami Bitar
2026-09-16 12:09:39 -04:00
parent f9550116d7
commit 55ae7f2e42
10 changed files with 187 additions and 31 deletions
+1 -1
View File
@@ -169,7 +169,7 @@
background-size: 64px 100%; background-size: 64px 100%;
} }
html { html {
@apply font-sans; @apply scroll-pt-20 font-sans;
} }
} }
+2 -2
View File
@@ -11,7 +11,7 @@ import { TestimonialsSection } from "@/components/sections/testimonials-section"
export default function Home() { export default function Home() {
return ( return (
<div className="min-h-screen bg-background text-foreground selection:bg-blue-600 selection:text-white"> <div className="min-h-screen overflow-x-clip bg-background text-foreground selection:bg-blue-600 selection:text-white">
<SiteHeader /> <SiteHeader />
<main id="top" className="mx-auto max-w-[1280px] border-x"> <main id="top" className="mx-auto max-w-[1280px] border-x">
<HeroSection /> <HeroSection />
@@ -22,8 +22,8 @@ export default function Home() {
<PricingSection /> <PricingSection />
<TestimonialsSection /> <TestimonialsSection />
<CtaSection /> <CtaSection />
<SiteFooter />
</main> </main>
<SiteFooter />
</div> </div>
) )
} }
+123
View File
@@ -0,0 +1,123 @@
"use client"
import { useEffect, useRef, useState } from "react"
import { RiArrowRightLine, RiCloseLine, RiMenu3Line } from "@remixicon/react"
import { Dialog as DialogPrimitive } from "radix-ui"
import { Brand } from "@/components/shared/brand"
import {
Dialog,
DialogClose,
DialogDescription,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
type NavigationItem = {
label: string
href: string
description: string
}
export function MobileNavigation({ items }: { items: NavigationItem[] }) {
const [open, setOpen] = useState(false)
const navigating = useRef(false)
useEffect(() => {
const desktop = window.matchMedia("(min-width: 768px)")
const closeOnDesktop = () => {
if (desktop.matches) setOpen(false)
}
desktop.addEventListener("change", closeOnDesktop)
return () => desktop.removeEventListener("change", closeOnDesktop)
}, [])
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<button
type="button"
aria-label="Open navigation menu"
className="inline-flex size-11 items-center justify-center rounded-sm border bg-background/80 text-foreground transition-colors hover:border-blue-500/40 hover:bg-blue-600/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring md:hidden"
>
<RiMenu3Line className="size-5" aria-hidden="true" />
</button>
</DialogTrigger>
<DialogPortal>
<DialogOverlay className="z-[60] bg-black/45 duration-200 supports-backdrop-filter:backdrop-blur-md md:hidden" />
<DialogPrimitive.Content
className="fixed inset-x-3 top-3 z-[61] mx-auto flex max-h-[calc(100dvh-1.5rem)] max-w-lg flex-col overflow-hidden rounded-2xl border bg-background text-foreground shadow-[0_24px_100px_rgba(0,0,0,.35)] outline-none duration-200 data-open:animate-in data-open:fade-in-0 data-open:slide-in-from-top-4 data-closed:animate-out data-closed:fade-out-0 data-closed:slide-out-to-top-4 md:hidden"
onCloseAutoFocus={(event) => {
// Let the anchor move focus to its section without the trigger
// interrupting the page's smooth scroll when the dialog closes.
if (navigating.current) event.preventDefault()
navigating.current = false
}}
>
<div className="flex shrink-0 items-center justify-between border-b bg-linear-to-r from-blue-600/10 via-blue-600/5 to-transparent px-5 py-3">
<Brand />
<DialogClose asChild>
<button
type="button"
aria-label="Close navigation menu"
className="inline-flex size-11 items-center justify-center rounded-full border bg-background/70 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<RiCloseLine className="size-5" aria-hidden="true" />
</button>
</DialogClose>
</div>
<div className="min-h-0 overflow-y-auto overscroll-contain p-3">
<DialogTitle className="px-3 pb-2 pt-3 font-mono text-[10px] font-medium tracking-[0.18em] text-muted-foreground uppercase">
Explore Vertex
</DialogTitle>
<DialogDescription className="sr-only">
Explore our products, models, features, pricing, and developer documentation.
</DialogDescription>
<nav aria-label="Mobile navigation">
<ul className="space-y-1">
{items.map(({ label, href, description }) => (
<li key={label}>
<DialogClose asChild>
<a
href={href}
onClick={() => { navigating.current = true }}
className="group flex items-center justify-between gap-4 rounded-xl px-3 py-3 transition-colors hover:bg-blue-600/8 focus-visible:bg-blue-600/8 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
>
<span className="min-w-0">
<span className="block text-xl font-medium tracking-tight">{label}</span>
<span className="mt-0.5 block text-xs leading-5 text-muted-foreground">{description}</span>
</span>
<span className="flex size-8 shrink-0 items-center justify-center rounded-full border text-muted-foreground transition-colors group-hover:border-blue-500/30 group-hover:bg-blue-600/10 group-hover:text-blue-600 dark:group-hover:text-blue-400">
<RiArrowRightLine className="size-4" aria-hidden="true" />
</span>
</a>
</DialogClose>
</li>
))}
</ul>
</nav>
</div>
<div className="shrink-0 border-t bg-muted/35 p-5">
<p className="mb-3 text-xs text-muted-foreground">One API. Every AI model.</p>
<DialogClose asChild>
<a
href="#pricing"
onClick={() => { navigating.current = true }}
className="flex h-12 items-center justify-between rounded-lg bg-blue-600 px-4 text-sm font-medium text-white shadow-[0_6px_24px_rgba(23,61,166,.2)] transition-colors hover:bg-blue-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
>
Get started
<RiArrowRightLine className="size-4" aria-hidden="true" />
</a>
</DialogClose>
</div>
</DialogPrimitive.Content>
</DialogPortal>
</Dialog>
)
}
+1 -1
View File
@@ -5,7 +5,7 @@ import { Button } from "@/components/ui/button"
export function SiteFooter() { export function SiteFooter() {
return ( return (
<footer className="px-6 py-10 sm:px-10"> <footer className="mx-auto max-w-[1280px] border-x px-6 py-10 sm:px-10">
<div className="flex flex-col gap-8 sm:flex-row sm:items-center sm:justify-between"> <div className="flex flex-col gap-8 sm:flex-row sm:items-center sm:justify-between">
<div> <div>
<Brand /> <Brand />
+14 -8
View File
@@ -1,23 +1,28 @@
import { RiArrowDownSLine, RiArrowRightSLine } from "@remixicon/react" import { RiArrowDownSLine, RiArrowRightSLine } from "@remixicon/react"
import { MobileNavigation } from "@/components/layout/mobile-navigation"
import { ThemeToggle } from "@/components/layout/theme-toggle" import { ThemeToggle } from "@/components/layout/theme-toggle"
import { Brand } from "@/components/shared/brand" import { Brand } from "@/components/shared/brand"
import { FrameCrosses } from "@/components/shared/frame-crosses"
const navigationItems = [
{ label: "Products", href: "#features", description: "One gateway for your AI stack" },
{ label: "Models", href: "#models", description: "Explore the models you can build with" },
{ label: "Features", href: "#features", description: "Routing, insights, and reliability" },
{ label: "Pricing", href: "#pricing", description: "Start free. Scale at your pace." },
{ label: "Docs", href: "#developers", description: "Your first request starts here" },
]
export function SiteHeader() { export function SiteHeader() {
return ( return (
<header className="sticky top-0 z-50 w-full border-b bg-background/86 backdrop-blur-xl supports-[backdrop-filter]:bg-background/74"> <header className="sticky top-0 z-50 w-full border-b bg-background/86 backdrop-blur-xl supports-[backdrop-filter]:bg-background/74">
<div className="mx-auto flex h-16 max-w-[1280px] items-center justify-between px-5 sm:px-8"> <div className="relative mx-auto flex h-16 max-w-[1280px] items-center justify-between border-x px-5 sm:px-8">
<FrameCrosses edges="bottom" />
<a href="#top" aria-label="Vertex home"> <a href="#top" aria-label="Vertex home">
<Brand /> <Brand />
</a> </a>
<nav aria-label="Main navigation" className="hidden items-center gap-8 md:flex lg:gap-10"> <nav aria-label="Main navigation" className="hidden items-center gap-8 md:flex lg:gap-10">
{[ {navigationItems.map(({ label, href }) => (
["Products", "#features"],
["Models", "#models"],
["Features", "#features"],
["Pricing", "#pricing"],
["Docs", "#developers"],
].map(([label, href]) => (
<a <a
key={label} key={label}
href={href} href={href}
@@ -36,6 +41,7 @@ export function SiteHeader() {
> >
Get started <RiArrowRightSLine className="size-3.5" /> Get started <RiArrowRightSLine className="size-3.5" />
</a> </a>
<MobileNavigation items={navigationItems} />
</div> </div>
</div> </div>
</header> </header>
+7 -7
View File
@@ -15,8 +15,8 @@ export function FeaturesSection() {
description="Everything you need to move quickly across models without giving up reliability, control, or visibility." description="Everything you need to move quickly across models without giving up reliability, control, or visibility."
/> />
<div className="grid lg:grid-cols-12"> <div className="grid grid-cols-1 lg:grid-cols-12">
<article className="flex min-h-[520px] flex-col border-b lg:col-span-7 lg:border-r"> <article className="flex min-h-[520px] min-w-0 flex-col border-b lg:col-span-7 lg:border-r">
<ApiTerminal /> <ApiTerminal />
<FeatureCopy <FeatureCopy
title="One compatible endpoint" title="One compatible endpoint"
@@ -24,7 +24,7 @@ export function FeaturesSection() {
/> />
</article> </article>
<article className="flex min-h-[520px] flex-col border-b lg:col-span-5"> <article className="flex min-h-[520px] min-w-0 flex-col border-b lg:col-span-5">
<IntegrationOrbit /> <IntegrationOrbit />
<FeatureCopy <FeatureCopy
title="Seamless integrations" title="Seamless integrations"
@@ -32,7 +32,7 @@ export function FeaturesSection() {
/> />
</article> </article>
<article className="flex min-h-[430px] flex-col border-b lg:col-span-4 lg:border-b-0 lg:border-r"> <article className="flex min-h-[430px] min-w-0 flex-col border-b lg:col-span-4 lg:border-b-0 lg:border-r">
<RoutingActivity /> <RoutingActivity />
<FeatureCopy <FeatureCopy
title="Automatic failover" title="Automatic failover"
@@ -40,8 +40,8 @@ export function FeaturesSection() {
/> />
</article> </article>
<article className="flex min-h-[430px] flex-col border-b lg:col-span-4 lg:border-b-0 lg:border-r"> <article className="flex min-h-[430px] min-w-0 flex-col border-b lg:col-span-4 lg:border-b-0 lg:border-r">
<div className="relative h-[360px] shrink-0 overflow-hidden bg-background"> <div className="relative h-[360px] w-full min-w-0 shrink-0 overflow-hidden bg-background">
<UsageChart /> <UsageChart />
</div> </div>
<FeatureCopy <FeatureCopy
@@ -50,7 +50,7 @@ export function FeaturesSection() {
/> />
</article> </article>
<article className="flex min-h-[430px] flex-col lg:col-span-4"> <article className="flex min-h-[430px] min-w-0 flex-col lg:col-span-4">
<GlobalGlobe /> <GlobalGlobe />
<FeatureCopy <FeatureCopy
title="Global inference gateway" title="Global inference gateway"
+20 -5
View File
@@ -1,8 +1,11 @@
"use client" "use client"
import { type COBEOptions } from "cobe"
import { useTheme } from "next-themes"
import { Globe } from "@/components/ui/globe" import { Globe } from "@/components/ui/globe"
const globeConfig = { const darkGlobeConfig: COBEOptions = {
width: 800, width: 800,
height: 800, height: 800,
onRender: () => {}, onRender: () => {},
@@ -28,13 +31,25 @@ const globeConfig = {
], ],
} }
const lightGlobeConfig: COBEOptions = {
...darkGlobeConfig,
dark: 0,
diffuse: 0.4,
mapBrightness: 1.2,
baseColor: [0.9, 0.94, 1],
markerColor: [0.09, 0.24, 0.65],
glowColor: [0.85, 0.9, 1],
}
export function GlobalGlobe() { export function GlobalGlobe() {
const { resolvedTheme } = useTheme()
return ( return (
<div className="relative h-[360px] shrink-0 overflow-hidden bg-background"> <div className="relative h-[360px] shrink-0 overflow-hidden bg-background">
<Globe className="top-6 max-w-[360px]" config={globeConfig} /> <Globe
<div className="absolute left-5 top-5 flex items-center gap-2 rounded-full border bg-background/85 px-3 py-1.5 font-mono text-[10px] shadow-sm backdrop-blur"> className="top-6 max-w-[360px]"
<span className="size-1.5 animate-pulse rounded-full bg-emerald-500" /> 8 regions online config={resolvedTheme === "light" ? lightGlobeConfig : darkGlobeConfig}
</div> />
</div> </div>
) )
} }
+1 -1
View File
@@ -62,7 +62,7 @@ export function UsageChart() {
const fillId = useId() const fillId = useId()
return ( return (
<ChartContainer config={chartConfig} className="h-full min-h-[270px] w-full aspect-auto"> <ChartContainer config={chartConfig} className="h-full min-h-[270px] w-full min-w-0 aspect-auto">
<AreaChart <AreaChart
accessibilityLayer accessibilityLayer
aria-label="Example model usage: requests increase from 2,350 to 5,680 over the day. Use the arrow keys to explore." aria-label="Example model usage: requests increase from 2,350 to 5,680 over the day. Use the arrow keys to explore."
+1 -1
View File
@@ -5,7 +5,7 @@ import { FrameCrosses } from "@/components/shared/frame-crosses"
export function HeroSection() { export function HeroSection() {
return ( return (
<section className="hero-gradient relative border-b px-6 pb-14 pt-24 text-center sm:px-10 sm:pb-20 sm:pt-28 lg:pt-32"> <section className="hero-gradient relative border-b px-6 pb-14 pt-24 text-center sm:px-10 sm:pb-20 sm:pt-28 lg:pt-32">
<FrameCrosses /> <FrameCrosses edges="bottom" />
<div className="relative z-10 mx-auto max-w-4xl"> <div className="relative z-10 mx-auto max-w-4xl">
<a <a
href="#features" href="#features"
+17 -5
View File
@@ -1,10 +1,22 @@
export function FrameCrosses() { export function FrameCrosses({
edges = "all",
}: {
edges?: "all" | "top" | "bottom"
}) {
return ( return (
<> <>
<span className="frame-cross left-0 top-0" aria-hidden="true" /> {edges !== "bottom" && (
<span className="frame-cross right-0 top-0" aria-hidden="true" /> <>
<span className="frame-cross bottom-0 left-0" aria-hidden="true" /> <span className="frame-cross left-0 top-0" aria-hidden="true" />
<span className="frame-cross bottom-0 right-0" aria-hidden="true" /> <span className="frame-cross right-0 top-0" aria-hidden="true" />
</>
)}
{edges !== "top" && (
<>
<span className="frame-cross bottom-0 left-0" aria-hidden="true" />
<span className="frame-cross bottom-0 right-0" aria-hidden="true" />
</>
)}
</> </>
) )
} }