A news pill, a centred heading with an underlined last word, a line and two buttons, over three product tiles: a chat, a coding terminal and an assistant thread.

Last updated

Open block
Show code
---
/**
 * Product tiles 01: A news pill, a centred heading with an underlined last word, a line and two buttons, over three product tiles: a chat, a coding terminal and an assistant thread.
 *
 * @block hero
 * @order 78
 */
import { ArrowRight, ChevronRight, Play } from '@lucide/astro'
import Section from '@/components/apogee/Section/Section.astro'
import Heading from '@/components/apogee/Heading/Heading.astro'
import Button from '@/components/apogee/Button/Button.astro'
import Badge from '@/components/apogee/Badge/Badge.astro'
import Link from '@/components/apogee/Link/Link.astro'
import { accentRuns, type Announcement, type HeroContent } from '@/components/apogee/sections/_shared/spine.ts'

/** @composable */
interface Props extends HeroContent {
  /** The pill above the heading; `detail` is the quieter text after the label. @category content */
  announcement?: (Announcement & { detail?: string }) | null
}

const {
  announcement = { badge: 'New', label: 'Meet Model 4.6', detail: 'Our newest model', href: '#' },
  heading = 'Frontier models for everything you *hear*.',
  description = 'Reasoning, code, voice, images and video, trained on one of the largest clusters in the world.',
  primaryAction = { label: 'Get API access', href: '#' },
  secondaryAction = { label: 'Read the docs', href: '#' },
} = Astro.props

const chat = [
  ['bot', 'Shorter blue light scatters more off the air than longer red light.'],
  ['me', 'How do black holes form?'],
  ['bot', 'A large star runs out of fuel and gravity pulls its core into a single point.'],
  ['me', 'What causes the northern lights?'],
]
const tasks = [['Audit auth middleware', 'running'], ['Design token rotation', 'running'], ['Find session references', 'done']]
const code = [
  ['42', 'export async function handler(req) {', ''],
  ['43', '  const token = readBearer(req);', 'add'],
  ['44', '  if (!token) return unauthorized();', 'add'],
  ['47', '  const session = await getSession(req);', 'del'],
  ['48', '  req.user = payload;', 'add'],
]
---

<Section spacing="large">
  <div class="mx-auto flex max-w-3xl flex-col items-center gap-6 text-center">
    {announcement && (
    <Link href={announcement.href} color="inherit" underline="none" class="inline-flex items-center gap-2 rounded-full! border border-neutral-faded py-1 ps-1 pe-2 text-sm">
      {announcement.badge && <Badge size="small" rounded variant="outline" color="primary">{announcement.badge}</Badge>}<span class="font-medium">{announcement.label}</span>{announcement.detail && <span class="text-neutral-faded">· {announcement.detail}</span>}<Play class="size-3.5 text-neutral-faded" aria-hidden="true" />
    </Link>
    )}
    <Heading as="h1" size="display" class="text-balance">{accentRuns(heading).map((r) => (r.accent ? <span class="underline decoration-primary decoration-4 underline-offset-[0.2em]">{r.text}</span> : r.text))}</Heading>
    {description && <p class="text-lg md:text-xl text-neutral-faded">{description}</p>}
    {(primaryAction || secondaryAction) && (
    <div class="flex flex-col gap-3 sm:flex-row">
      {primaryAction && <Button href={primaryAction.href} size="large" rounded>{primaryAction.label}<ChevronRight slot="end" class="rtl:-scale-x-100" aria-hidden="true" /></Button>}
      {secondaryAction && <Button href={secondaryAction.href} size="large" rounded variant="outline" color="neutral">{secondaryAction.label}</Button>}
    </div>
    )}
  </div>

  <ul class="mt-16 grid gap-4 text-sm md:mt-20 lg:grid-cols-3">
    <li class="flex h-72 flex-col overflow-hidden rounded-2xl border border-neutral-faded bg-neutral-faded p-4">
      <div class="flex flex-1 flex-col gap-3 overflow-hidden mask-b-from-70% text-xs" aria-hidden="true">
        {chat.map(([who, t]) => <p class:list={['max-w-[85%] rounded-xl border border-neutral-faded bg-page px-3 py-2', who === 'me' && 'self-end']}>{t}</p>)}
      </div>
      <a href="#" class="mt-3 flex items-center justify-between"><span class="font-medium">Chat</span><span class="flex items-center gap-1 text-neutral-faded">Explore<ArrowRight class="size-3.5 rtl:-scale-x-100" aria-hidden="true" /></span></a>
    </li>
    <li data-theme="dark" class="flex h-72 flex-col overflow-hidden rounded-2xl border border-neutral-faded bg-page p-4 text-neutral">
      <div class="flex flex-1 flex-col gap-1 overflow-hidden mask-b-from-75% font-mono text-xs" aria-hidden="true">
        <p class="mb-2 flex items-center gap-1.5 text-neutral-faded"><span class="size-2.5 rounded-full bg-critical"></span><span class="size-2.5 rounded-full bg-warning"></span><span class="size-2.5 rounded-full bg-positive"></span><span class="ms-2">projects/main</span><span class="ms-auto tabular-nums">16.15%</span></p>
        {tasks.map(([t, s]) => <p class="flex justify-between"><span>| {t}</span><span class="text-primary">[{s}]</span></p>)}
        <p class="mt-2 text-neutral-faded">◆ Thought for 4.1s</p>
        <p class="mt-1">◆ Edit src/middleware/auth.ts</p>
        {code.map(([n, l, k]) => <p class:list={['flex gap-3 whitespace-pre', k === 'add' && 'bg-positive-faded', k === 'del' && 'bg-critical-faded']}><span class="w-5 text-end text-neutral-faded tabular-nums">{n}</span>{l}</p>)}
      </div>
      <a href="#" class="mt-3 flex items-center justify-between"><span class="font-medium">Build</span><span class="flex items-center gap-1 text-neutral-faded">Explore<ArrowRight class="size-3.5 rtl:-scale-x-100" aria-hidden="true" /></span></a>
    </li>
    <li class="flex h-72 flex-col overflow-hidden rounded-2xl border border-neutral-faded bg-neutral-faded p-4">
      <div class="flex flex-1 flex-col justify-center gap-3" aria-hidden="true">
        <p class="max-w-[85%] rounded-xl bg-page px-3 py-2">The hotel charged $412 on the 12th and again on the 14th. Billed twice, or two nights?</p>
        <p class="max-w-[85%] self-end rounded-full bg-(--foreground-neutral) px-4 py-2 text-(--background-page)">Two nights, Mia stayed the second one</p>
        <p class="max-w-[85%] rounded-xl bg-page px-3 py-2">That clears it. Report filed: <b>9 receipts</b> matched, <span class="tabular-nums">$2,340</span> across 3 trips, nothing open.</p>
      </div>
      <a href="#" class="mt-3 flex items-center justify-between"><span class="font-medium">Assistant</span><span class="flex items-center gap-1 text-neutral-faded">Explore<ArrowRight class="size-3.5 rtl:-scale-x-100" aria-hidden="true" /></span></a>
    </li>
  </ul>
</Section>

Add

npx @ava-ui/apogee add hero/product-tiles-01

Every piece of copy is a prop, with the sample copy as its default. Pass yours, or null to hide a part; see copy is props.