Przejdź do treści
Wszystkie przykłady

Strona główna: bento

Strona główna tego serwisu. Hero wyrównane do lewej z poleceniem do skopiowania, siatka bento z żywymi komponentami, skalami kolorów, rolami typografii i terminalem, trzy kroki instalacji, pytania i końcowe wezwanie do działania. Własny nagłówek i stopka są opcjonalne.

Dodaj ten przykład do projektu z zainstalowanym prfct. Polecenie zapisuje poniższe pliki i instaluje komponenty, których używają.

$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/landing-bento.json
components/landing-bento/landing-bento.tsx
"use client"

import * as React from "react"
import Link from "next/link"
import { ArrowRightIcon, CheckIcon, CopyIcon } from "lucide-react"

import { PlanCard, TeamCard } from "./landing-showcase"
import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"
import { Button, buttonVariants } from "@/components/ui/button"
import { Kbd } from "@/components/ui/kbd"
import { cn } from "@/lib/utils"

const install = "pnpm dlx shadcn@latest add https://prfct.dev/r/prfct.json"

// Links that start with "/" are prfct.dev's own pages: point them at yours.
const navigation = [
  { label: "Features", href: "#features" },
  { label: "Install", href: "#install" },
  { label: "Questions", href: "#questions" },
]

/** Steps of each scale, light to dark, as the theme names them. */
const hues = ["gray", "blue", "green", "amber", "red"]
const shades = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]

const roles = [
  { name: "Display", className: "text-display-sm" },
  { name: "Heading", className: "text-heading-lg" },
  { name: "Body", className: "text-body-md" },
  { name: "Label", className: "text-label-sm" },
  { name: "Code", className: "font-mono text-sm" },
]

const brands = [
  "bg-foreground",
  "bg-blue-9",
  "bg-green-9",
  "bg-amber-9",
  "bg-orange-9",
  "bg-red-9",
  "bg-pink-9",
]

const examples = [
  { name: "Dashboard", href: "/examples/dashboard" },
  { name: "Mail", href: "/examples/mail" },
  { name: "Checkout", href: "/examples/checkout" },
  { name: "Booking", href: "/examples/booking" },
  { name: "Team", href: "/examples/team" },
]

const steps = [
  {
    title: "Start a project",
    text: "Create a Next.js app with shadcn. In an app you already have, leave out --template next.",
    command:
      "pnpm dlx shadcn@latest init --template next --base base --preset nova",
  },
  {
    title: "Add the theme",
    text: "Tokens, base styles and font faces arrive as files in your project. The guide shows the stylesheet import and the two font files to download.",
    command: install,
  },
  {
    title: "Add what you need",
    text: "A single component or a whole example app. Each one brings what it depends on.",
    command: "pnpm dlx shadcn@latest add https://prfct.dev/r/dashboard.json",
  },
]

const questions = [
  {
    question: "Is prfct free?",
    answer:
      "Yes. It is open source under the MIT license, for personal and commercial work.",
  },
  {
    question: "How is it different from shadcn?",
    answer:
      "prfct is built on shadcn's components and keeps their API. It adds generated tokens with tested contrast, a quieter look, documentation for every component and example apps.",
  },
  {
    question: "Do I need Next.js?",
    answer:
      "prfct is built and tested for Next.js. The components are React and Tailwind CSS, so other React frameworks may work, but nobody has tested them.",
  },
  {
    question: "Can I use my own brand color?",
    answer:
      "Yes. The theme builder regenerates every scale from one color and keeps text readable in both modes.",
  },
  {
    question: "How do I update a component?",
    answer:
      "Run the add command again. The CLI asks before it replaces a file, so you can look at the change first.",
  },
]

const footer = [
  {
    title: "Product",
    links: [
      { label: "Components", href: "/docs/components" },
      { label: "Foundations", href: "/docs/foundations" },
      { label: "Themes", href: "/themes" },
      { label: "Examples", href: "/examples" },
    ],
  },
  {
    title: "Resources",
    links: [
      { label: "Documentation", href: "/docs" },
      { label: "Installation", href: "/docs/installation" },
      { label: "Changelog", href: "/docs/resources/changelog" },
    ],
  },
]

/** One width and one gutter for every section. */
const container = "mx-auto w-full max-w-6xl px-4 @md:px-6"

/** Cells share hairlines: the grid's background shows through 1px gaps. */
const hairlines = "grid gap-px overflow-hidden rounded-2xl border bg-border"

const quietLink =
  "rounded-md text-sm text-muted-foreground focus-ring transition-colors duration-fast hover:text-foreground"

function Logo() {
  return (
    <Link href="/" className="flex items-center gap-2 rounded-md focus-ring">
      <svg viewBox="0 0 24 24" fill="currentColor" className="size-6">
        <path d="M3 21V3a18 18 0 0 1 18 18z" />
      </svg>
      <span className="font-semibold tracking-tight">prfct</span>
    </Link>
  )
}

/** A command people copy rather than retype. */
function Command({ children }: { children: string }) {
  const [copied, setCopied] = React.useState(false)

  // The check mark confirms the copy for a moment, then turns back.
  React.useEffect(() => {
    if (!copied) return
    const timer = setTimeout(() => setCopied(false), 2000)
    return () => clearTimeout(timer)
  }, [copied])

  async function copy() {
    try {
      await navigator.clipboard.writeText(children)
      setCopied(true)
    } catch {
      // No clipboard access (an insecure page, a denied permission): nothing
      // was copied, so nothing is confirmed.
    }
  }

  return (
    <div className="flex max-w-full items-center gap-1 rounded-xl border bg-surface py-1 pr-1 pl-4">
      <code className="truncate font-mono text-sm">
        <span className="text-muted-foreground select-none">$ </span>
        {children}
      </code>
      <Button
        variant="ghost"
        size="icon-sm"
        aria-label={copied ? "Copied" : "Copy command"}
        onClick={copy}
      >
        {copied ? <CheckIcon /> : <CopyIcon />}
      </Button>
    </div>
  )
}

/** One feature: what it is in a sentence, and the thing itself below. */
function Cell({
  title,
  text,
  className,
  children,
}: {
  title: string
  text: string
  className?: string
  children: React.ReactNode
}) {
  return (
    <div
      className={cn(
        "flex flex-col gap-8 bg-background p-6 @2xl:p-8",
        className
      )}
    >
      <div className="flex flex-col gap-1.5">
        <h3 className="text-heading-sm">{title}</h3>
        <p className="max-w-md text-sm leading-6 text-muted-foreground">
          {text}
        </p>
      </div>
      <div className="mt-auto">{children}</div>
    </div>
  )
}

function Header() {
  return (
    <header className="border-b">
      <div className={cn(container, "flex h-14 items-center gap-8")}>
        <Logo />
        <nav aria-label="Sections" className="flex gap-6 @max-2xl:hidden">
          {navigation.map((item) => (
            <a key={item.href} href={item.href} className={quietLink}>
              {item.label}
            </a>
          ))}
        </nav>
        <Link
          href="/docs/installation"
          className={buttonVariants({ size: "sm", className: "ml-auto" })}
        >
          Get started
        </Link>
      </div>
    </header>
  )
}

function Hero() {
  return (
    <section className={cn(container, "pt-14 pb-14 @2xl:pt-24 @2xl:pb-20")}>
      <Link
        href="/examples"
        className={cn(quietLink, "group inline-flex items-center gap-2")}
      >
        New: example apps you can install
        <ArrowRightIcon
          className="size-3.5 transition-transform duration-fast group-hover:translate-x-0.5"
          aria-hidden="true"
        />
      </Link>
      <h1 className="mt-5 text-display-sm @2xl:text-display-lg @5xl:text-display-xl">
        A design system for Next.js.
        <span className="block text-muted-foreground">
          Built on shadcn, yours to change.
        </span>
      </h1>
      <div className="mt-10 grid gap-8 @5xl:grid-cols-[minmax(0,1fr)_auto] @5xl:items-end">
        <p className="max-w-xl text-body-lg text-pretty text-muted-foreground">
          Tokens with tested contrast, accessible components and complete
          example apps. You install them with the shadcn CLI, and the code is
          yours.
        </p>
        <div className="flex min-w-0 flex-col gap-3 @5xl:items-end">
          <div className="flex flex-wrap gap-3">
            <Link
              href="/docs/installation"
              className={buttonVariants({ size: "lg" })}
            >
              Get started
              <ArrowRightIcon data-icon="inline-end" />
            </Link>
            <Link
              href="/docs/components"
              className={buttonVariants({ variant: "outline", size: "lg" })}
            >
              Browse components
            </Link>
          </div>
          <Command>{install}</Command>
        </div>
      </div>
    </section>
  )
}

function Features() {
  return (
    <section
      id="features"
      aria-labelledby="features-title"
      className={cn(container, "pb-20 @2xl:pb-28")}
    >
      <h2 id="features-title" className="sr-only">
        Features
      </h2>
      <div className={cn(hairlines, "@2xl:grid-cols-2 @5xl:grid-cols-6")}>
        <Cell
          className="@2xl:col-span-2 @5xl:col-span-4"
          title="Components that agree with each other"
          text="One radius, one spacing scale and one set of states, across every component."
        >
          {/* A picture of the system, not a form to fill in: nothing in it takes focus. */}
          <div
            inert
            className="grid items-start gap-4 rounded-xl bg-surface p-4 @2xl:grid-cols-2"
          >
            <TeamCard />
            <div className="@max-2xl:hidden">
              <PlanCard />
            </div>
          </div>
        </Cell>

        <Cell
          className="@5xl:col-span-2"
          title="Contrast you don't have to check"
          text="Every scale is generated, and its text steps are tested against WCAG AA on the fills they sit on, in light and dark."
        >
          <div className="flex flex-col gap-1.5" aria-hidden="true">
            {hues.map((hue) => (
              <div key={hue} className="flex gap-1.5">
                {shades.map((shade) => (
                  <span
                    key={shade}
                    className="h-8 flex-1 rounded-sm"
                    style={{ backgroundColor: `var(--${hue}-${shade})` }}
                  />
                ))}
              </div>
            ))}
          </div>
        </Cell>

        <Cell
          className="@5xl:col-span-2"
          title="Type with roles"
          text="Each role sets size, line height, weight and tracking together."
        >
          <ul className="flex flex-col gap-2">
            {roles.map((role) => (
              <li key={role.name} className={cn("truncate", role.className)}>
                {role.name}
              </li>
            ))}
          </ul>
        </Cell>

        <Cell
          className="@5xl:col-span-2"
          title="Usable without a mouse"
          text="Keyboard support, focus management and ARIA come with every component."
        >
          <div className="flex flex-col items-start gap-5" aria-hidden="true">
            <div className="flex flex-wrap gap-1.5">
              <Kbd>Tab</Kbd>
              <Kbd>Shift</Kbd>
              <Kbd>Enter</Kbd>
              <Kbd>Esc</Kbd>
              <Kbd>←</Kbd>
              <Kbd>→</Kbd>
            </div>
            {/* A button as it looks with keyboard focus. */}
            <span
              className={buttonVariants({
                variant: "outline",
                className: "outline-2 outline-offset-2 outline-ring",
              })}
            >
              Focused
            </span>
          </div>
        </Cell>

        <Cell
          className="@5xl:col-span-2"
          title="One theme, any brand"
          text="Pick a color in the theme builder. Every component follows it."
        >
          <div className="flex flex-col items-start gap-5">
            <div className="flex gap-2" aria-hidden="true">
              {brands.map((brand) => (
                <span
                  key={brand}
                  className={cn("size-6 rounded-full", brand)}
                />
              ))}
            </div>
            <Link
              href="/themes"
              className={buttonVariants({ variant: "outline" })}
            >
              Open the theme builder
            </Link>
          </div>
        </Cell>

        <Cell
          className="@5xl:col-span-3"
          title="Code you own"
          text="The shadcn CLI copies components into your project. Read them, change them, delete what you don't need."
        >
          <pre className="overflow-x-auto rounded-xl border bg-surface p-4 font-mono text-sm leading-6">
            <span className="text-muted-foreground">$ </span>
            shadcn add button{"\n"}
            <span className="text-muted-foreground">
              ✔ components/ui/button.tsx{"\n"}✔ components/ui/spinner.tsx
              {"\n"}✔ components/ui/locale.tsx{"\n"}✔ lib/utils.ts
            </span>
          </pre>
        </Cell>

        <Cell
          className="@5xl:col-span-3"
          title="Whole screens, not only parts"
          text="Example apps you install with one command and adapt."
        >
          <ul className="border-t">
            {examples.map((example) => (
              <li key={example.href} className="border-b">
                <Link
                  href={example.href}
                  className="group flex items-center justify-between rounded-md py-2.5 text-sm font-medium focus-ring"
                >
                  {example.name}
                  <ArrowRightIcon
                    className="size-4 text-muted-foreground transition-transform duration-fast group-hover:translate-x-0.5 group-hover:text-foreground"
                    aria-hidden="true"
                  />
                </Link>
              </li>
            ))}
          </ul>
        </Cell>
      </div>
    </section>
  )
}

function Install() {
  return (
    <section id="install" className="border-t bg-surface">
      <div className={cn(container, "py-20 @2xl:py-28")}>
        <h2 className="max-w-2xl text-heading-xl text-balance @2xl:text-display-sm">
          From an empty folder to a first screen
        </h2>
        <ol className={cn(hairlines, "mt-10")}>
          {steps.map((step, index) => (
            <li
              key={step.title}
              className="grid items-center gap-x-10 gap-y-4 bg-background p-6 @2xl:p-8 @5xl:grid-cols-[2rem_minmax(0,1fr)_auto]"
            >
              <span className="font-mono text-sm text-muted-foreground">
                0{index + 1}
              </span>
              <div className="flex flex-col gap-1">
                <h3 className="text-heading-sm">{step.title}</h3>
                <p className="text-sm leading-6 text-muted-foreground">
                  {step.text}
                </p>
              </div>
              <Command>{step.command}</Command>
            </li>
          ))}
        </ol>
      </div>
    </section>
  )
}

function Questions() {
  return (
    <section id="questions" className="border-t">
      <div
        className={cn(
          container,
          "grid gap-10 py-20 @2xl:py-28 @5xl:grid-cols-[2fr_3fr]"
        )}
      >
        <h2 className="text-heading-xl text-balance @2xl:text-display-sm">
          Questions people ask before they install
        </h2>
        <Accordion defaultValue={[questions[0].question]}>
          {questions.map((item) => (
            <AccordionItem key={item.question} value={item.question}>
              <AccordionTrigger>{item.question}</AccordionTrigger>
              <AccordionContent>{item.answer}</AccordionContent>
            </AccordionItem>
          ))}
        </Accordion>
      </div>
    </section>
  )
}

/** Dark in both themes, so the page ends on its one loud moment. */
function Closing() {
  return (
    <section className="dark border-t bg-background text-foreground">
      <div
        className={cn(
          container,
          "grid gap-10 py-20 @2xl:py-28 @5xl:grid-cols-[minmax(0,1fr)_auto] @5xl:items-end"
        )}
      >
        <h2 className="max-w-3xl text-display-sm text-balance @2xl:text-display-lg">
          Install the theme.{" "}
          <span className="text-muted-foreground">
            Build your first screen today.
          </span>
        </h2>
        <div className="flex min-w-0 flex-col gap-3 @5xl:items-end">
          <Link
            href="/docs/installation"
            className={buttonVariants({ size: "lg" })}
          >
            Read the installation guide
            <ArrowRightIcon data-icon="inline-end" />
          </Link>
          <Command>{install}</Command>
        </div>
      </div>
    </section>
  )
}

function Footer() {
  return (
    <footer className="border-t">
      <div
        className={cn(
          container,
          "grid gap-10 py-12 @2xl:grid-cols-[2fr_1fr_1fr]"
        )}
      >
        <div className="flex flex-col items-start gap-3">
          <Logo />
          <p className="max-w-xs text-sm text-muted-foreground">
            A design system for Next.js, built on shadcn. Released under the MIT
            license.
          </p>
        </div>
        {footer.map((group) => (
          <nav
            key={group.title}
            aria-label={group.title}
            className="flex flex-col gap-3"
          >
            <h2 className="text-sm font-medium">{group.title}</h2>
            <ul className="flex flex-col gap-2">
              {group.links.map((link) => (
                <li key={link.href}>
                  <Link href={link.href} className={quietLink}>
                    {link.label}
                  </Link>
                </li>
              ))}
            </ul>
          </nav>
        ))}
      </div>
    </footer>
  )
}

/**
 * A landing page built around a bento grid: every feature is shown with the
 * thing itself. `chrome` is its own header and footer: leave them out where
 * the page sits inside a site that has both, as it does on prfct.dev.
 */
export default function LandingBentoExample({
  chrome = true,
}: {
  chrome?: boolean
}) {
  return (
    <div className="@container bg-background text-foreground">
      {chrome && <Header />}
      <Hero />
      <Features />
      <Install />
      <Questions />
      <Closing />
      {chrome && <Footer />}
    </div>
  )
}
components/landing-bento/landing-showcase.tsx
import {
  CircleCheckIcon,
  GitBranchIcon,
  MinusIcon,
  PlusIcon,
} from "lucide-react"

import aurora from "./aurora.svg"
import ember from "./ember.svg"
import tide from "./tide.svg"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import { Checkbox } from "@/components/ui/checkbox"
import {
  Field,
  FieldContent,
  FieldDescription,
  FieldGroup,
  FieldLabel,
  FieldSeparator,
  FieldTitle,
} from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import {
  InputOTP,
  InputOTPGroup,
  InputOTPSeparator,
  InputOTPSlot,
} from "@/components/ui/input-otp"
import { Progress } from "@/components/ui/progress"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Slider } from "@/components/ui/slider"
import { Switch } from "@/components/ui/switch"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

const people = [
  { name: "Maya Chen", role: "Owner", image: aurora.src, owner: true },
  { name: "Jonah Okafor", role: "Admin", image: ember.src },
  { name: "Lena Park", role: "Member", image: tide.src },
]

const plans = [
  { value: "hobby", title: "Hobby", description: "One project", price: "$0" },
  {
    value: "pro",
    title: "Pro",
    description: "Unlimited projects",
    price: "$20",
  },
]

const alerts = [
  { id: "deploys", title: "Deployments", on: true },
  { id: "comments", title: "Comments", on: true },
  { id: "digest", title: "Weekly digest", on: false },
]

function AccountCard() {
  return (
    <Card size="sm">
      <CardHeader>
        <CardTitle>Create an account</CardTitle>
        <CardDescription>Start with your work email.</CardDescription>
      </CardHeader>
      <CardContent>
        <FieldGroup>
          <Field>
            <FieldLabel htmlFor="showcase-name">Name</FieldLabel>
            <Input id="showcase-name" defaultValue="Ada Lovelace" />
          </Field>
          <Field>
            <FieldLabel htmlFor="showcase-email">Email</FieldLabel>
            <Input id="showcase-email" placeholder="ada@example.com" />
          </Field>
          <Field orientation="horizontal">
            <Checkbox id="showcase-updates" defaultChecked />
            <FieldLabel htmlFor="showcase-updates" className="font-normal">
              Send me product updates
            </FieldLabel>
          </Field>
          <Button>Create account</Button>
          <FieldSeparator>or</FieldSeparator>
          <Button variant="outline">
            <GitBranchIcon data-icon="inline-start" />
            Continue with Git
          </Button>
        </FieldGroup>
      </CardContent>
    </Card>
  )
}

export function TeamCard() {
  return (
    <Card size="sm">
      <CardHeader>
        <CardTitle>Team</CardTitle>
        <CardDescription>Three people can open this project.</CardDescription>
      </CardHeader>
      <CardContent className="flex flex-col gap-3">
        {people.map((person) => (
          <div key={person.name} className="flex items-center gap-3">
            <Avatar>
              <AvatarImage src={person.image} alt="" />
              <AvatarFallback>{person.name[0]}</AvatarFallback>
            </Avatar>
            <span className="flex-1 truncate text-sm font-medium">
              {person.name}
            </span>
            <Badge variant={person.owner ? "default" : "outline"}>
              {person.role}
            </Badge>
          </div>
        ))}
      </CardContent>
    </Card>
  )
}

export function PlanCard() {
  return (
    <Card size="sm">
      <CardHeader>
        <CardTitle>Plan</CardTitle>
        <CardDescription>Change it whenever you like.</CardDescription>
      </CardHeader>
      <CardContent>
        <RadioGroup defaultValue="pro" aria-label="Plan">
          {plans.map((plan) => (
            <FieldLabel key={plan.value} htmlFor={`showcase-${plan.value}`}>
              <Field orientation="horizontal">
                <RadioGroupItem
                  value={plan.value}
                  id={`showcase-${plan.value}`}
                />
                <FieldContent>
                  <FieldTitle>{plan.title}</FieldTitle>
                  <FieldDescription>{plan.description}</FieldDescription>
                </FieldContent>
                <span className="text-sm font-semibold tabular-nums">
                  {plan.price}
                </span>
              </Field>
            </FieldLabel>
          ))}
        </RadioGroup>
      </CardContent>
    </Card>
  )
}

export function NotificationsCard() {
  return (
    <Card size="sm">
      <CardHeader>
        <CardTitle>Notifications</CardTitle>
        <CardDescription>Changes save as you switch.</CardDescription>
      </CardHeader>
      <CardContent>
        <FieldGroup className="gap-3">
          {alerts.map((alert) => (
            <Field key={alert.id} orientation="horizontal">
              <FieldLabel
                htmlFor={`showcase-${alert.id}`}
                className="flex-1 font-normal"
              >
                {alert.title}
              </FieldLabel>
              <Switch id={`showcase-${alert.id}`} defaultChecked={alert.on} />
            </Field>
          ))}
        </FieldGroup>
      </CardContent>
    </Card>
  )
}

function UsageCard() {
  return (
    <Card size="sm">
      <CardHeader>
        <CardTitle className="flex items-center gap-2">
          Usage <Badge variant="success">On track</Badge>
        </CardTitle>
        <CardDescription>Resets on the first of the month.</CardDescription>
      </CardHeader>
      <CardContent className="flex flex-col gap-2">
        <div className="flex justify-between text-sm">
          <span>Build minutes</span>
          <span className="text-muted-foreground tabular-nums">
            3,120 of 6,000
          </span>
        </div>
        <Progress value={52} aria-label="Build minutes used" />
      </CardContent>
      <CardFooter className="justify-end">
        <Button variant="outline" size="sm">
          Manage plan
        </Button>
      </CardFooter>
    </Card>
  )
}

function AppearanceCard() {
  return (
    <Card size="sm">
      <CardHeader>
        <CardTitle>Appearance</CardTitle>
        <CardDescription>How the editor looks on this device.</CardDescription>
      </CardHeader>
      <CardContent className="flex flex-col gap-5">
        {/* One choice of three with nothing to switch between: a toggle
            group, not tabs. */}
        <ToggleGroup
          variant="outline"
          spacing={0}
          defaultValue={["system"]}
          aria-label="Theme"
          className="w-full"
        >
          <ToggleGroupItem value="light" className="flex-1">
            Light
          </ToggleGroupItem>
          <ToggleGroupItem value="dark" className="flex-1">
            Dark
          </ToggleGroupItem>
          <ToggleGroupItem value="system" className="flex-1">
            System
          </ToggleGroupItem>
        </ToggleGroup>
        <div className="flex items-center gap-3 text-muted-foreground">
          <MinusIcon className="size-4 shrink-0" aria-hidden="true" />
          <Slider defaultValue={60} aria-label="Text size" />
          <PlusIcon className="size-4 shrink-0" aria-hidden="true" />
        </div>
      </CardContent>
    </Card>
  )
}

function VerifyCard() {
  return (
    <Card size="sm">
      <CardHeader>
        <CardTitle>Two-step verification</CardTitle>
        <CardDescription>
          Enter the code from your authenticator.
        </CardDescription>
      </CardHeader>
      <CardContent className="flex flex-col items-start gap-4">
        <InputOTP maxLength={6} aria-label="Verification code">
          <InputOTPGroup>
            <InputOTPSlot index={0} />
            <InputOTPSlot index={1} />
            <InputOTPSlot index={2} />
          </InputOTPGroup>
          <InputOTPSeparator />
          <InputOTPGroup>
            <InputOTPSlot index={3} />
            <InputOTPSlot index={4} />
            <InputOTPSlot index={5} />
          </InputOTPGroup>
        </InputOTP>
        <Button size="sm">Verify</Button>
      </CardContent>
    </Card>
  )
}

const column = "flex flex-col gap-4"

/**
 * A wall of components in their default theme: what the system looks like
 * before anyone has styled anything. One column on a phone, three on a desk.
 */
export default function LandingShowcase() {
  return (
    <div className="grid gap-4 @2xl:grid-cols-2 @5xl:grid-cols-3">
      <div className={column}>
        <AccountCard />
        <UsageCard />
      </div>
      <div className={`${column} @max-2xl:hidden`}>
        <TeamCard />
        <PlanCard />
        <Alert variant="success">
          <CircleCheckIcon />
          <AlertTitle>Deployment ready</AlertTitle>
          <AlertDescription>
            Production now serves the build from main.
          </AlertDescription>
        </Alert>
      </div>
      <div className={`${column} @max-5xl:hidden`}>
        <NotificationsCard />
        <AppearanceCard />
        <VerifyCard />
      </div>
    </div>
  )
}
components/landing-bento/aurora.svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80">
  <rect width="80" height="80" fill="#e4eaff"/>
  <circle cx="40" cy="31" r="14" fill="#8ea6ff"/>
  <path d="M12 80c0-15.5 12.5-26 28-26s28 10.5 28 26z" fill="#8ea6ff"/>
</svg>
components/landing-bento/ember.svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80">
  <rect width="80" height="80" fill="#fdebe0"/>
  <circle cx="40" cy="31" r="14" fill="#f0a072"/>
  <path d="M12 80c0-15.5 12.5-26 28-26s28 10.5 28 26z" fill="#f0a072"/>
</svg>
components/landing-bento/tide.svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80">
  <rect width="80" height="80" fill="#dcf2ef"/>
  <circle cx="40" cy="31" r="14" fill="#6cbdb3"/>
  <path d="M12 80c0-15.5 12.5-26 28-26s28 10.5 28 26z" fill="#6cbdb3"/>
</svg>
app/landing-bento/page.tsx
import type { Metadata } from "next"

import LandingBento from "@/components/landing-bento/landing-bento"

export const metadata: Metadata = {
  title: "Landing page: bento",
  description: "A two-tone headline and a bento grid where every feature is shown with the thing itself.",
}

export default function LandingBentoPage() {
  return <LandingBento />
}