Files
zeekay ac64d5d7dd Folds say one line: every first screen keeps its heading and a single lede
The owner's spec for a landing page is the models bento's: monochrome, the
words left and the art right, no extra prose. The folds carried two to four
sentences under each heading, and some a claim, a lede and a list besides.
Each now says one line: ProductLanding the tagline where a page states one
(the lede still describes it to search), a claim alone where the claim is
the line (/payments, /treasury, /zap, the extension), and elsewhere the
sentence that says what the thing is. /philosophy's two points, /dns' claim
that repeated its title, /calculator's capabilities line and the
Spare clauses on /research and /learn go.
2026-10-30 06:42:40 -07:00

876 lines
34 KiB
TypeScript

'use client'
/**
* ProductLanding — the ONE shared kit every featured Hanzo product page renders
* through, so the detailed pages that the apex nav now links into (on hanzo.ai
* itself) look first-class and identical in style to the flagship Enso landing.
*
* Content is data — each page passes ONE typed props object; this file owns the
* layout. Honest by construction: pass only real copy, real links, and real
* (measured) stats — never fabricated benchmarks.
*
* Every word in that object is drawn through `t` (`useSay`), so a page marks
* each literal where it writes it — `title: phrase('…')` (@hanzo/i18n) — and
* the reader gets it in their language. Markup passes as the page wrote it.
*
* TWO BLOCKS ARE THE KIT'S RATHER THAN A PAGE'S: `AgentPrompt` under the hero
* and `Related` last. Both take the product `lib/product` resolves from
* the ROUTE, so no page states them, none can state them differently, and the
* command and the docs link each carries are the audited ones or nothing.
*
* NO CLASSES AND NO STYLE OBJECTS. The page shape comes from
* `components/marketing/band`'s `Band`, `Head` and `Card` — the rule, the
* rhythm, the gutter, the centred head and the tile — the type and the
* controls from @hanzo/ui's marketing components, and everything left is gui props
* over @hanzo/design's tokens.
*
* RADIUS MAPS BY NAME: `rounded-lg` IS `var(--radius-lg)`, `rounded-2xl` IS
* `var(--radius-2xl)`. The utility compiles to a bare alias of the token, and
* the VALUE behind that token is @hanzo/design's rather than Tailwind's —
* `app/globals.css` imports `tokens/radius.css` with no `layer(...)`, so its
* `:root` beats Tailwind's `@layer theme` defaults at any specificity and
* regardless of order. Both declare `--radius-2xl`; design's 1.5rem wins over
* Tailwind's 1rem.
*
* The `/* rounded-md *\/`-style comments in `radius.css` say which Tailwind
* name a rung was DESIGNED to serve. They are a design note, not the emitted
* mapping — reading them as the mapping is what produces an off-by-one that
* shrinks every corner on fifteen pages.
*
* `rounded-3xl` is the one exception, and only because @hanzo/design does not
* define `--radius-3xl` at all: that one really does fall through to Tailwind's
* theme, at 1.5rem.
*/
import type { ComponentProps, ReactNode } from 'react'
import { motion } from '@/components/motion'
import { ArrowRight, ArrowUpRight, Check } from 'lucide-react'
import type { LucideIcon } from 'lucide-react'
import Link from 'next/link'
import { useRoute } from '@/components/shell/route'
import { AgentPrompt } from '@/components/product/AgentPrompt'
import { ProductJsonLd } from '@/components/product/ProductJsonLd'
import { Related } from '@/components/product/Related'
import { Surface } from '@/components/product/Surface'
import { Fold } from '@/components/marketing/fold'
import { Steps } from '@/components/preview/steps'
import { at } from '@/lib/product'
import { getPrimitive, getPrimitiveCategory } from '@/lib/data/cloud-primitives'
import { Box, Text, View, XStack } from '@hanzo/ui'
/* Under another name because this file already EXPORTS a type called `Grid` —
the titled set of cards — and that name is caller-visible. */
import { Grid as Columns } from '@hanzo/ui/grid'
import { Band, Card, Head } from '@/components/marketing/band'
import { Action, Chip, Eyebrow, Lede, More, Title } from '@hanzo/ui/marketing'
import { phrase } from '@hanzo/i18n'
import { useI18n } from '@hanzo/i18n/react'
import { useSay } from '@/components/marketing/say'
/** The marks that point somewhere, which turn over in a right-to-left page. */
const POINTS = new Set<LucideIcon>([ArrowRight, ArrowUpRight])
const mirror = (icon: LucideIcon) => (POINTS.has(icon) ? '' : undefined)
const fade = {
initial: { opacity: 0, y: 20 },
whileInView: { opacity: 1, y: 0 },
viewport: { once: true },
}
export interface CTA {
label: string
href: string
icon?: LucideIcon
}
export interface Pillar {
icon: LucideIcon
title: string
body: string
}
/**
* One card. The icon, the body, the list and the link are each optional because
* pages genuinely differ on them — a chain list is a title and its members, a
* use case is a title and a paragraph, an exchange product is all four — and a
* second card component for each combination is how a kit stops being one.
*/
export interface Feature {
icon?: LucideIcon
title: string
/** Markup where the sentence is markup: a command belongs in `code`. */
body?: ReactNode
/** What the card holds, one per line. */
points?: string[]
/**
* Where the card goes. WITH a `label` that is a link inside the card, in
* words the reader can see; WITHOUT one the whole card is the link and the
* page gains no words it did not have. The arity is the difference, so
* there is one field rather than one per treatment.
*/
link?: { href: string; label?: string; icon?: LucideIcon }
}
export interface Stat {
icon?: LucideIcon
value: string
label: string
sub?: string
}
export interface Step {
n: string
title: string
/** Markup, for the same reason a Feature's body is: a step can BE a command. */
body: ReactNode
icon?: LucideIcon
}
/**
* A plan, as a rate card states one. `price` is a STRING because "Custom" is a
* price a reader understands and a number is not: the moment this takes a
* number it has to grow a way to say the thing that is not one.
*/
export interface Plan {
name: string
price: string
/** "mo" — rendered after a slash, and absent on a plan that is not periodic. */
period?: string
points: string[]
cta: CTA
/** The one plan the page recommends. */
pick?: boolean
}
export interface SectionCopy {
/** Optional: a section that never had one is not given one here. */
eyebrow?: string
/** A heading may be markup: one names a CLI verb and sets it in code. */
title?: ReactNode
sub?: string
/** One link under the section — "View all X", where the cards are a sample. */
link?: CTA
}
/** A titled grid of cards. A page has as many as it has things to say. */
export type Grid = SectionCopy & { items: Feature[] }
export interface ProductLandingProps {
/** Hero */
badge?: string
badgeIcon?: LucideIcon
title: ReactNode
/**
* One line between the title and the lede — what the product IS, where the
* lede says what it does. Optional: most products say it in the lede.
*/
tagline?: string
lede: ReactNode
/** First CTA renders as the solid white primary; the rest as neutral outlines. */
ctas: CTA[]
note?: { icon?: LucideIcon; text: string }
availableThrough?: string[]
/**
* Catalog slug of the product whose surface this page draws.
*
* ONE STRING, because the catalog already answers everything else: the name
* in the titlebar, the route in the footer, and the corner of the console the
* product sits in. A slug the catalog has dropped draws nothing rather than
* an empty window.
*/
surface?: string
/**
* The product itself, playing, beside the words on the first screen
* (components/marketing/fold): /bot's is BotPreview. A page with one draws no
* surface, so the product is pictured once. Absent, the first screen draws the product's
* surface: the slug named above, else the route's own, when the catalog lists
* it; with neither, it walks the page's pillars (`what`). A page with none of
* them keeps its opening at its own height.
*/
preview?: ReactNode
/** The preview is an app's demo: on a phone it is the first screen, the words after it (Fold `stage`). */
stage?: boolean
/** What the product does, three or four short lines under the lede (Fold `points`). */
points?: string[]
/**
* What-it-is — numbered pillar grid. Optional: a page whose first section is
* a plain card grid has no pillars, and numbering its cards would be writing
* content it does not have.
*/
what?: SectionCopy & { pillars: Pillar[] }
/**
* The titled card grids, in order. One field with an arity — a page saying one
* thing passes the grid, a page saying five passes five — rather than a second
* name for the same section repeated.
*/
features?: Grid | Grid[]
/** How-it-works — numbered steps. */
steps?: SectionCopy & { items: Step[] }
/**
* A set of short names. A grid of cards over-dresses a list of chain names or
* of site kinds, and a card per word is how a page comes to look like it is
* saying more than it is.
*/
chips?: SectionCopy & { items: string[] }
/** The rate card. */
plans?: SectionCopy & { items: Plan[]; footnote?: string }
/**
* Measured stats row. `hero` puts it under the lede, which is where a page
* whose numbers ARE the promise wants it; otherwise it is its own section.
*/
stats?: Partial<SectionCopy> & { items: Stat[]; footnote?: string; hero?: boolean }
/** Terminal-style code block. */
code?: {
/** An anchor, where a control above sends the reader here ("#install"). */
id?: string
head?: SectionCopy
/** The language, shown in the terminal chrome when no file is named. */
lang?: string
/** A file name, which is what these pages label the frame with. */
name?: string
/** The sample, highlighted at build: a `<Code>` (components/code) from the
server page. A transcript passes its own markup in a `<Session>`. */
source: ReactNode
ctas?: CTA[]
}
/**
* The card that closes a page, where the page has a close of its own to make.
*
* OPTIONAL, because `Related` and the site's closing call render under it: a
* page whose card said only "read the docs" and "get a key" was saying what
* the blocks below it say, twice. Pass one only where it
* carries something that block does not — a link into the product itself, or
* a sentence the page needs last. `sub` is optional too: several pages' card
* is a heading and its buttons.
*/
finalCta?: {
/** A word above the heading — usually the product's own name. */
eyebrow?: string
title: string
sub?: ReactNode
buttons: CTA[]
icon?: LucideIcon
}
/**
* Sections only one product has, rendered after the shared ones and before
* the final CTA.
*
* The kit describes what every product page shares; it is not a ceiling. A
* product with more to say than the shared shape holds — Team has an AI
* coworker gallery, the people who lead it, and a compliance surface — used
* to have to stop using the kit to say it, which is how a page ends up
* looking like it belongs to a different site. Handing those sections in
* here keeps one standard for the parts that ARE shared.
*
* It goes before the final CTA rather than after because a call to action
* that is not last is not final: anything following it reads as content the
* reader was asked to leave for.
*/
children?: ReactNode
}
/* ── The card, and the two ways this page draws one ───────────────────────────
A card is `components/marketing/band`'s `Card` on this page's ground —
the muted surface at half strength (neutral-900 on dark, a pale grey on light),
a token mix gui's colour type does not take, so
it rides `$platform-web`. Every card is the animated child of a stagger: the
motion element carries the entrance and nothing else, and the card inside it
fills the cell. */
const GROUND = 'color-mix(in oklab, var(--muted) 50%, transparent)'
function Tile({ children, ...p }: ComponentProps<typeof Card>) {
return (
<Card height="100%" bg={undefined} {...p} $platform-web={{ backgroundColor: GROUND, ...p['$platform-web'] }}>
{children}
</Card>
)
}
/** The link an address names: the router's for a path on this site, a plain anchor anywhere else. */
function to(href: string) {
return href.startsWith('/') ? <Link href={href} /> : <a href={href} />
}
/**
* A card that IS a link: the hover is the whole reason it is a component — the
* edge brightens on approach. The corner is the unlinked card's, so the two
* halves of one row round alike.
*/
function LinkTile({ href, children }: { href: string; children: ReactNode }) {
return (
<Box
render={to(href)}
display="block"
height="100%"
p={28}
borderWidth={1}
borderColor="var(--border)"
rounded="var(--radius-2xl)"
hoverStyle={{ borderColor: 'var(--border-strong)' }}
$platform-web={{ backgroundColor: GROUND, textDecoration: 'none' }}
>
{children}
</Box>
)
}
/** A card's own heading and its paragraph — not a section head, so not a role. `panel` is one rung up, where a pillar or a step names itself. */
function CardTitle({ panel, children }: { panel?: boolean; children: ReactNode }) {
return (
<Text
render="h3"
display="block"
mb={8}
fontSize={panel ? '$4' : '$3'}
lineHeight={panel ? '1.4583rem' : '1.3125rem'}
fontWeight="600"
color="var(--foreground)"
>
{children}
</Text>
)
}
function CardBody({ children }: { children: ReactNode }) {
return (
<Text render="p" display="block" fontSize="$2" lineHeight="1.3203rem" color="var(--muted-foreground)">
{children}
</Text>
)
}
/** How narrow a column may get before a grid drops one, per shape.
*
* `Fit` is ONE number and a cap, so it cannot reproduce a pair of breakpoints
* exactly: `md:grid-cols-2 lg:grid-cols-3` asks for 348px columns at 768 and
* 304px ones at 1024, and no single floor is both. Each value below is chosen
* to land the WIDEST step on the breakpoint it had — 3-up at 1024, 3-up at 768
* — so a desktop is unchanged and the narrower steps arrive a little earlier. */
const DECK = { min: 304, max: 3 } as const
const TRIO = { min: 224, max: 3 } as const
const ROW = { min: 200, max: 4 } as const
/** A section with nothing to say above its cards renders no head at all. */
function SectionHead({ eyebrow, title, sub }: SectionCopy) {
return <Head eyebrow={eyebrow} title={title} lede={sub} />
}
/** The link that follows a set of cards, when the cards are a sample of more. */
function SectionLink({ cta }: { cta: CTA }) {
const say = useSay()
const Icon = cta.icon ?? ArrowRight
return (
<motion.div {...fade} transition={{ duration: 0.5 }}>
<XStack mt={32} justify="center">
<More href={cta.href}>
{say(cta.label)} <Icon width={16} height={16} data-i18n-mirror={mirror(Icon)} />
</More>
</XStack>
</motion.div>
)
}
/**
* The plate a card's mark sits on. `Disc` in components/marketing/band is the
* same idea as a circle; these pages draw a rounded square, at two sizes.
*/
function Plate({ size, mark: Mark, mb }: { size: number; mark: LucideIcon; mb?: number }) {
/* Half the plate, which is the proportion both sizes already drew: 20 in 40,
24 in 44 was 22 rounded to the even pixel a 24-unit viewBox renders on. */
const mark = size === 40 ? 20 : 24
return (
<XStack
width={size}
height={size}
mb={mb}
items="center"
justify="center"
rounded="var(--radius-lg)"
bg="$raised"
>
<Mark width={mark} height={mark} color="var(--foreground)" />
</XStack>
)
}
/**
* One titled grid. The card carries whichever of icon/body/points/link it was
* given — there is one card on this site, and it has optional parts.
*/
export function Cards({ grid, index = 0 }: { grid: Grid; index?: number }) {
const say = useSay()
return (
<Band key={index}>
<SectionHead eyebrow={grid.eyebrow} title={grid.title} sub={grid.sub} />
<Columns columns={DECK} gap={24}>
{grid.items.map((f, i) => {
const LinkIcon = f.link?.icon ?? ArrowRight
const card = (
<>
{f.icon && <Plate size={40} mark={f.icon} mb={16} />}
<CardTitle>{say(f.title)}</CardTitle>
{f.body && <CardBody>{say(f.body)}</CardBody>}
{f.points && f.points.length > 0 && (
<Box mt={16}>
<CheckList items={f.points} muted />
</Box>
)}
{f.link?.label && (
<More href={f.link.href} mt={16}>
{say(f.link.label)} <LinkIcon width={16} height={16} data-i18n-mirror={mirror(LinkIcon)} />
</More>
)}
</>
)
return (
<motion.div key={f.title} {...fade} transition={{ duration: 0.5, delay: i * 0.05 }}>
{f.link && !f.link.label ? <LinkTile href={f.link.href}>{card}</LinkTile> : <Tile>{card}</Tile>}
</motion.div>
)
})}
</Columns>
{grid.link && <SectionLink cta={grid.link} />}
</Band>
)
}
/** A row of short names, each in an outline on the page's ground. */
function Chips({ copy }: { copy: SectionCopy & { items: string[] } }) {
const say = useSay()
return (
<Band measure={896}>
<SectionHead eyebrow={copy.eyebrow} title={copy.title} sub={copy.sub} />
<motion.div {...fade} transition={{ duration: 0.5 }}>
<XStack flexWrap="wrap" justify="center" gap={12}>
{copy.items.map((s) => (
<Chip
key={s}
px={16}
py={8}
bg={undefined}
$theme-light={{ borderColor: 'var(--border)', color: 'var(--muted-foreground)' }}
$platform-web={{ backgroundColor: GROUND }}
>
{say(s)}
</Chip>
))}
</XStack>
</motion.div>
{copy.link && <SectionLink cta={copy.link} />}
</Band>
)
}
/** A footnote under a set of cards: centred, a measure wide. `quiet` is the stats' smaller, dimmer one. */
function Footnote({ quiet, children }: { quiet?: boolean; children: ReactNode }) {
return (
<Text
render="p"
display="block"
mt={32}
mx="auto"
maxW={672}
text="center"
fontSize={quiet ? '$1' : '$2'}
lineHeight={quiet ? '0.9167rem' : '1.1607rem'}
color={quiet ? 'var(--text-tertiary)' : 'var(--muted-foreground)'}
>
{children}
</Text>
)
}
/** The rate card. The picked plan is marked by its BORDER, not by a louder fill. */
function Plans({ copy }: { copy: SectionCopy & { items: Plan[]; footnote?: string } }) {
const say = useSay()
return (
<Band measure={1024}>
<SectionHead eyebrow={copy.eyebrow} title={copy.title} sub={copy.sub} />
<Columns columns={TRIO} gap={24}>
{copy.items.map((p, i) => (
<motion.div key={p.name} {...fade} transition={{ duration: 0.5, delay: i * 0.06 }}>
<Tile display="flex" flexDirection="column" borderWidth={p.pick ? 2 : 1} borderColor={p.pick ? 'var(--border-selected)' : 'var(--border)'}>
<Text render="h3" display="block" fontSize="$4" lineHeight="1.4583rem" fontWeight="600" color="var(--foreground)">
{say(p.name)}
</Text>
<Text render="div" display="block" mt={4} mb={24} fontSize="$3" lineHeight="1.25rem" color="var(--muted-foreground)">
<Text fontSize="$8" lineHeight="1.95rem" fontWeight="700" color="var(--foreground)">
{say(p.price)}
</Text>
{p.period && <Text color="inherit">/{say(p.period)}</Text>}
</Text>
<Box mb={32} flex={1}>
<CheckList items={p.points} muted />
</Box>
<Action href={p.cta.href} fill={p.pick}>
{say(p.cta.label)} <ArrowRight width={16} height={16} data-i18n-mirror />
</Action>
</Tile>
</motion.div>
))}
</Columns>
{copy.footnote && <Footnote>{say(copy.footnote)}</Footnote>}
</Band>
)
}
/** The stat row, in the hero or in its own section — one grid, two placements. */
function StatGrid({ items }: { items: Stat[] }) {
const say = useSay()
return (
<Columns columns={ROW} gap={24}>
{items.map((s, i) => (
<motion.div key={s.label} {...fade} transition={{ duration: 0.5, delay: i * 0.05 }}>
<Tile $platform-web={{ textAlign: 'center' }}>
{s.icon && (
<XStack justify="center" mb={16}>
<s.icon width={24} height={24} color="var(--neutral-400)" />
</XStack>
)}
<Text display="block" fontSize="$10" lineHeight="2.2222rem" fontWeight="700" color="var(--foreground)">
{say(s.value)}
</Text>
<Text display="block" mt={4} fontSize="$2" lineHeight="1.1607rem" fontWeight="500" color="var(--text-secondary)">
{say(s.label)}
</Text>
{s.sub && (
<Text display="block" mt={2} fontSize="$1" lineHeight="0.9167rem" color="var(--muted-foreground)">
{say(s.sub)}
</Text>
)}
</Tile>
</motion.div>
))}
</Columns>
)
}
function PrimaryButton({ cta }: { cta: CTA }) {
const say = useSay()
const Icon = cta.icon ?? ArrowRight
return (
<Action href={cta.href} fill>
{say(cta.label)} <Icon width={16} height={16} data-i18n-mirror={mirror(Icon)} />
</Action>
)
}
function OutlineButton({ cta }: { cta: CTA }) {
const say = useSay()
const Icon = cta.icon
return (
<Action href={cta.href}>
{Icon && <Icon width={16} height={16} data-i18n-mirror={mirror(Icon)} />} {say(cta.label)}
</Action>
)
}
function CTARow({ ctas }: { ctas: CTA[] }) {
return (
<XStack flexWrap="wrap" justify="center" gap={16}>
{ctas.map((c, i) => (i === 0 ? <PrimaryButton key={c.label} cta={c} /> : <OutlineButton key={c.label} cta={c} />))}
</XStack>
)
}
/** A line of muted small type under the hero's actions. */
const SMALL = { fontSize: '$2', lineHeight: '1.1607rem', color: 'var(--muted-foreground)' } as const
/* ── The terminal frame ────────────────────────────────────────────────────── */
function Frame({ name, children }: { name: string; children: ReactNode }) {
return (
<Box
overflow="hidden"
rounded="var(--radius-2xl)"
borderWidth={1}
borderColor="var(--border)"
$platform-web={{ backgroundColor: 'color-mix(in oklab, var(--muted) 60%, transparent)' }}
>
<XStack
items="center"
gap={8}
borderBottomWidth={1}
borderColor="var(--border)"
px={16}
py={10}
$platform-web={{ backgroundColor: 'color-mix(in oklab, var(--muted) 80%, transparent)' }}
>
{[0, 1, 2].map((k) => (
<View key={k} render="span" width={10} height={10} rounded="var(--radius-full)" bg="var(--neutral-700)" />
))}
<Text ml={8} fontFamily="$mono" fontSize="$1" lineHeight="0.9167rem" color="var(--muted-foreground)">
{name}
</Text>
</XStack>
{children}
</Box>
)
}
/**
* A terminal session drawn in its own markup — a prompt, a command and what it
* printed — for the pages whose sample is a transcript rather than source. Code
* is a `<Code>` (components/code) and needs none of this.
*
* `whiteSpace` is stated rather than left to the UA: this is the one place on
* the page where a line break is content.
*/
export function Session({ children }: { children: ReactNode }) {
return (
<Text
render="pre"
display="block"
overflowX="auto"
p={20}
my={13}
fontFamily="$mono"
fontSize="$2"
lineHeight="1.3203rem"
whiteSpace="pre"
color="var(--text-secondary)"
>
<code>{children}</code>
</Text>
)
}
/* ── The final card ───────────────────────────────────────────────────────── */
/** One of the two soft lights in the final card's corners. */
function Wash({ corner }: { corner: 'tr' | 'bl' }) {
return (
<Box
position="absolute"
width={256}
height={256}
rounded="var(--radius-full)"
filter="blur(64px)"
{...(corner === 'tr' ? { r: -96, t: -96 } : { b: -96, l: -96 })}
$platform-web={{ backgroundColor: 'color-mix(in oklab, var(--pure-white) 5%, transparent)' }}
/>
)
}
/** What the catalog says about the product a page draws the surface of. */
function surfaceOf(slug?: string) {
if (!slug) return null
const product = getPrimitive(slug)
const category = getPrimitiveCategory(slug)
if (!product || !category) return null
return { title: product.title, api: product.api, rail: category.id, slug }
}
export function ProductLanding(p: ProductLandingProps) {
const Badge = p.badgeIcon
const NoteIcon = p.note?.icon
const FinalIcon = p.finalCta?.icon
const grids: Grid[] = p.features ? (Array.isArray(p.features) ? p.features : [p.features]) : []
const heroStats = p.stats?.hero ? p.stats.items : null
// The catalog answers everything the drawn surface needs, so a page states a
// slug and nothing else. A slug the catalog has dropped draws nothing.
const route = useRoute()
// Which product this is, off the route — so the shared blocks below reach
// every page through the kit rather than through sixty call sites.
const product = at(route)
const drawn = surfaceOf(p.surface ?? product?.slug)
// A page with no surface of its own walks what it is on the first screen, and
// that band is then not drawn again further down.
const walked = !p.preview && !drawn && p.what ? p.what : null
const { t } = useI18n()
const say = useSay()
return (
<Box bg="var(--background)" $platform-web={{ color: 'var(--foreground)' }}>
{product && <ProductJsonLd product={product} path={route} description={typeof p.lede === 'string' ? p.lede : undefined} />}
{/* ── The first screen: the words beside the product, running ────────── */}
<Fold
label={p.badge}
mark={Badge && <Badge width={12} height={12} color="currentColor" />}
title={p.title}
// One line under the title: the tagline where the page states one,
// the lede where it does not. The lede still describes the page to
// search and to agents (ProductJsonLd above).
claim={p.tagline}
lede={p.tagline ? undefined : p.lede}
points={p.points}
stage={p.stage}
actions={p.ctas.map((c, i) => (i === 0 ? <PrimaryButton key={c.label} cta={c} /> : <OutlineButton key={c.label} cta={c} />))}
below={
p.note || p.availableThrough?.length ? (
<>
{p.note && (
<XStack maxW={576} items="center" gap={8}>
{NoteIcon && <NoteIcon width={16} height={16} color="var(--muted-foreground)" />}
<Text render="p" {...SMALL}>
{say(p.note.text)}
</Text>
</XStack>
)}
{p.availableThrough && p.availableThrough.length > 0 && (
<XStack flexWrap="wrap" items="center" columnGap={24} rowGap={8}>
<Text {...SMALL}>{t('Available through')}</Text>
{p.availableThrough.map((s) => (
<Text key={s} {...SMALL} color="var(--text-secondary)">
{say(s)}
</Text>
))}
</XStack>
)}
</>
) : undefined
}
preview={
p.preview ??
(drawn ? (
<Surface name={drawn.title} api={drawn.api} rail={drawn.rail} here={drawn.slug} />
) : walked ? (
<Steps title={typeof walked.title === 'string' ? walked.title : (p.badge ?? phrase('What it is'))} steps={walked.pillars.map((pl) => ({ title: pl.title, body: say(pl.body) }))} />
) : undefined)
}
>
{heroStats ? (
<Box width="100%" maxW={896} mx="auto">
<StatGrid items={heroStats} />
</Box>
) : undefined}
</Fold>
{/* ── The prompt a reader hands to an agent ───────────────────────────── */}
{product && <AgentPrompt product={product} />}
{/* ── What it is ─────────────────────────────────────────────────────── */}
{p.what && !walked && (
<Band>
<SectionHead eyebrow={p.what.eyebrow} title={p.what.title} sub={p.what.sub} />
<Columns columns={TRIO} gap={24}>
{p.what.pillars.map((pl, i) => (
<motion.div key={pl.title} {...fade} transition={{ duration: 0.5, delay: i * 0.06 }}>
<Tile>
<XStack mb={20} items="center" justify="space-between">
<Plate size={44} mark={pl.icon} />
<Text fontSize="$2" lineHeight="1.1607rem" fontWeight="600" color="var(--text-tertiary)">
{String(i + 1).padStart(2, '0')}
</Text>
</XStack>
<CardTitle panel>{say(pl.title)}</CardTitle>
<CardBody>{say(pl.body)}</CardBody>
</Tile>
</motion.div>
))}
</Columns>
</Band>
)}
{/* ── The card grids, in the order the page states them ───────────────── */}
{grids.map((g, i) => (
<Cards key={i} grid={g} index={i} />
))}
{/* ── How it works ───────────────────────────────────────────────────── */}
{p.steps && (
<Band measure={1024}>
<SectionHead eyebrow={p.steps.eyebrow} title={p.steps.title} sub={p.steps.sub} />
<Columns columns={TRIO} gap={24}>
{p.steps.items.map((s, i) => (
<motion.div key={s.n} {...fade} transition={{ duration: 0.5, delay: i * 0.06 }}>
<Tile>
{s.icon && <Plate size={44} mark={s.icon} mb={16} />}
<Text display="block" mb={16} fontSize="$8" lineHeight="1.95rem" fontWeight="700" color="var(--neutral-700)">
{s.n}
</Text>
<CardTitle panel>{say(s.title)}</CardTitle>
<CardBody>{say(s.body)}</CardBody>
</Tile>
</motion.div>
))}
</Columns>
</Band>
)}
{p.chips && <Chips copy={p.chips} />}
{p.plans && <Plans copy={p.plans} />}
{/* ── Stats ──────────────────────────────────────────────────────────── */}
{p.stats && !p.stats.hero && (
<Band>
{p.stats.title && <SectionHead eyebrow={p.stats.eyebrow} title={p.stats.title} sub={p.stats.sub} />}
<StatGrid items={p.stats.items} />
{p.stats.footnote && <Footnote quiet>{say(p.stats.footnote)}</Footnote>}
</Band>
)}
{/* ── Code ───────────────────────────────────────────────────────────── */}
{p.code && (
<Band measure={768} id={p.code.id}>
{p.code.head && <SectionHead eyebrow={p.code.head.eyebrow} title={p.code.head.title} sub={p.code.head.sub} />}
<motion.div {...fade} transition={{ duration: 0.5 }}>
<Frame name={p.code.name ?? p.code.lang}>{p.code.source}</Frame>
</motion.div>
{p.code.ctas && p.code.ctas.length > 0 && (
<Box mt={32}>
<CTARow ctas={p.code.ctas} />
</Box>
)}
</Band>
)}
{p.children}
{/* ── Final CTA ──────────────────────────────────────────────────────── */}
{p.finalCta && (
<Band measure={896}>
<motion.div {...fade} transition={{ duration: 0.5 }}>
<Tile position="relative" overflow="hidden" p={0} $platform-web={{ textAlign: 'center' }}>
<Box position="absolute" t={0} r={0} b={0} l={0} pointerEvents="none">
<Wash corner="tr" />
<Wash corner="bl" />
</Box>
{/* The room inside the card lives here rather than on the card, so
it can step up at 768 — the wash above is `inset: 0` either way. */}
<Box position="relative" z="var(--z-raised)" p={40} $md={{ p: 56 }}>
{FinalIcon && (
<XStack justify="center" mb={16}>
<FinalIcon width={40} height={40} color="var(--foreground)" />
</XStack>
)}
{p.finalCta.eyebrow && <Eyebrow mb={12}>{say(p.finalCta.eyebrow)}</Eyebrow>}
<Title quiet mt={p.finalCta.eyebrow ? '$3.5' : 0}>
{say(p.finalCta.title)}
</Title>
{p.finalCta.sub && (
<Lede mx="auto" mt={16} maxW={576}>
{say(p.finalCta.sub)}
</Lede>
)}
<Box mt={32}>
<CTARow ctas={p.finalCta.buttons} />
</Box>
</Box>
</Tile>
</motion.div>
</Band>
)}
{/* ── The docs, and what sits beside this product ─────────────────────── */}
{product && <Related product={product} />}
</Box>
)
}
/** Small inline helper for pages that want a checklist inside custom sections. */
export function CheckList({ items, muted }: { items: string[]; muted?: boolean }) {
const say = useSay()
return (
<View render="ul" display="grid" gap={8} my={13} pl={40}>
{items.map((x) => (
<XStack key={x} render="li" items="center" gap={8}>
<Check width={16} height={16} color={muted ? 'var(--muted-foreground)' : 'var(--foreground)'} />
<Text fontSize="$2" lineHeight="1.1607rem" color="var(--text-secondary)">
{say(x)}
</Text>
</XStack>
))}
</View>
)
}
export { fade, SectionHead, ArrowUpRight }