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.
Last updated
---
/**
* 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>
Show codeHide 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.