Files
hanzo.ai/components/models/kit.tsx
zeekay 6c65e3e941 @hanzo/app 0.2.0: the app SDK, renamed from @hanzo/rooms
Every import, the catalog pin and the docs name @hanzo/app and
github.com/hanzoai/app. lib/rooms.ts is lib/app.ts and
components/rooms.tsx is components/app.tsx.
2026-10-22 19:07:20 -07:00

340 lines
12 KiB
TypeScript

/**
* The catalogue's own shapes — the pieces every /models page is stacked out of.
*
* It is `kit.tsx` and not `catalog.tsx`, which is the name `components/pricing`
* already gave the same idea: a kit holds the SHAPES a section is drawn with,
* and lowercase is what this repo calls a kit — `band.tsx`, `pricing/kit.tsx`,
* `type.tsx` — while a PascalCase file is one component. Under the old name the
* catalogue itself had nowhere to live: `Catalog.tsx` beside `catalog.tsx` is
* ONE FILE on the case-insensitive checkouts half this team works on, so the two
* would have silently overwritten each other.
*
* Five things were drawn more than once across /models, /models/<lab>,
* /models/<lab>/<model> and /models/enso. Each was written out
* again in utilities at every site, and each had already drifted, so collapsing
* them is a visible change rather than a translation and each one is named:
*
* - THE TRAIL. Four copies in two greys with two hovers (`hover:text-white`
* and `hover:text-foreground`). It is `More` now — the site's quiet-link
* role, which is where a hover has to live. The gap under it settles at
* 24px; two of the four had 32.
* - THE MODALITY. Three copies at two sizes. `MODALITY_STYLES` in lib/models
* answered `bg-white/10 text-white/70` for every one of its seven keys and
* the pages' own `?? { bg: 'bg-secondary', text: 'text-muted-foreground' }`
* for every key it did not have — a table of identical rows deciding one
* value, and getting a DIFFERENT one off its end. The value is stated here
* once and the table is deleted.
* - THE TILE. Three copies of one link-card, in two real sizes. Two sizes is
* what it keeps: `lg` is the flagship card /models draws (marked, with the
* API id under the name) and `sm` is the one a lab page and a related row
* draw. One knob, two rungs — the shape `Title quiet` already has.
* - THE SAMPLE. Three copies of one code frame, one of which drew its three
* window dots in a single grey instead of three, and set its type two rungs
* larger than the other two.
* - THE HERO. /models/enso opens on it — trail, badge, heading, lede, two
* actions.
*
* Type is not a parameter. The roles are @hanzo/ui/marketing's; a call site
* passes WORDS. Every line here is a `Line` and every box a gui frame, so the
* kit states no style object: a cut is `numberOfLines`, the light is a frame's
* gradient and blur, a tracking is the prop.
*/
import { Fragment, type ReactNode } from 'react'
import { Box, View, XStack, YStack } from '@/components/ui'
import { Chip, Display, Lede, Line, More, Leaf } from '@hanzo/ui/marketing'
import { mix } from '@hanzo/app/lib/mix'
/** The ink every quiet line in the catalogue is set in. */
export const DIM = 'var(--muted-foreground)'
/**
* The two small rungs the catalogue sets, each with its leading as a ratio of
* the size. gui's own `$1`/`$2` leadings are 16px and 18px; these are 14.67 and
* 20, which is what every line here has always stood on.
*/
export const XS = { fontSize: '$1', lineHeight: 'calc(1 / 0.75)' } as const
export const SM = { fontSize: '$2', lineHeight: 'calc(1.25 / 0.875)' } as const
/* ── the trail ─────────────────────────────────────────────────────────────── */
export interface Step {
label: string
/** Absent on the last step: you are already there. */
href?: string
}
export function Crumbs({ trail, center }: { trail: Step[]; center?: boolean }) {
return (
<XStack render="nav" flexWrap="wrap" items="center" justify={center ? 'center' : 'flex-start'} gap="$2" mb="$5">
{trail.map((step, i) => (
<Fragment key={step.label}>
{i > 0 ? (
<Line {...SM} color={DIM}>
/
</Line>
) : null}
{step.href ? (
<More href={step.href}>{step.label}</More>
) : (
<Line {...SM} color="var(--foreground)">
{step.label}
</Line>
)}
</Fragment>
))}
</XStack>
)
}
/* ── a capability ──────────────────────────────────────────────────────────── */
/**
* One modality, as a chip. Monochrome by design — the catalogue distinguishes
* these by their label and never by hue, which is what made the seven-row
* lookup table they used to come from answer one value seven times.
*/
export function Modality({ name }: { name: string }) {
return (
<Line
display="inline-flex"
items="center"
px={8}
py={2}
rounded="var(--radius-full)"
bg="var(--glass-strong)"
color="var(--text-secondary)"
fontSize="var(--text-floor)"
lineHeight="1.6"
fontWeight="500"
textTransform="uppercase"
letterSpacing="0.025em"
>
{name}
</Line>
)
}
/* ── a model, as a tile ────────────────────────────────────────────────────── */
/**
* The site's hairline tile as a `Leaf`, so the edge answers a pointer and
* reduced motion is honoured once — in the stylesheet — rather than per call
* site.
*
* Two rungs. `lg` is the flagship card, which has room for the lab's mark and
* the exact API id under the name; `sm` is the card a lab index and a related
* row draw, where the name is the whole of it.
*
* Without `href` the tile is a model nobody can call yet: not a link, its name
* muted, its edge still, and its children say why and where to ask.
*/
const RUNG = {
lg: { pad: 24, gap: 16, radius: 'var(--radius-2xl)', name: '$3', lead: '1.5', blurb: '$2' },
sm: { pad: 16, gap: 12, radius: 'var(--radius-xl)', name: '$2', lead: 'calc(1.25 / 0.875)', blurb: '$1' },
} as const
export function Tile({
href,
size = 'sm',
mark,
name,
id,
ctx,
blurb,
children,
}: {
href?: string
size?: keyof typeof RUNG
/** The lab's mark, which only the flagship card has room for. */
mark?: ReactNode
name: string
/** The exact API id, set in mono under the name. */
id?: string
/** The context window, already formatted. */
ctx?: string
blurb?: string
/** The modality row, or anything else that closes the card. */
children?: ReactNode
}) {
const rung = RUNG[size]
const frame = {
p: rung.pad,
borderColor: 'var(--border)',
hoverStyle: { borderColor: href ? 'var(--border-strong)' : 'var(--border)' },
display: 'flex',
flexDirection: 'column',
gap: rung.gap,
rounded: rung.radius,
bg: mix('var(--secondary)', 30),
...(href ? null : { color: DIM }),
} as const
const body = (
<>
<XStack items="flex-start" justify="space-between" gap="$3">
{/* gui gives a View `flex-shrink: 0`, so the name column is told it may
give up room — without it the ellipsis below never has anything to
cut and the row runs past the card. */}
<XStack items="center" gap="$3" shrink={1}>
{mark}
<YStack shrink={1}>
{/* A model id is one token, and a second line of it is noise beside
the name it belongs to: both cut with an ellipsis. */}
<Line numberOfLines={1} fontSize={rung.name} lineHeight={rung.lead} fontWeight="500" letterSpacing="var(--tracking-tight)" color="inherit">
{name}
</Line>
{id ? (
<Line numberOfLines={1} fontFamily="$mono" {...XS} color={DIM}>
{id}
</Line>
) : null}
</YStack>
</XStack>
{/* The context window, as a chip that never gives up room to the name. */}
{ctx ? (
<Line shrink={0} rounded="var(--radius-full)" bg="var(--secondary)" px={8} py={2} {...XS} color={DIM}>
{ctx}
</Line>
) : null}
</XStack>
{/* Two lines of the catalogue's own words, then an ellipsis. */}
{blurb ? (
<Line render="p" numberOfLines={2} fontSize={rung.blurb} lineHeight="var(--leading-relaxed)" color={DIM}>
{blurb}
</Line>
) : null}
{children}
</>
)
return href ? (
<Leaf href={href} {...frame}>
{body}
</Leaf>
) : (
<Leaf data-access="research" {...frame}>
{body}
</Leaf>
)
}
/* ── a code sample ─────────────────────────────────────────────────────────── */
/**
* The chrome above a sample: three window rings and the file's language. The
* sample itself is the child — a `<Code>` (components/code), highlighted where
* the page is built, which a server page renders and a client one is handed.
*/
export function Sample({ lang, children }: { lang: string; children: ReactNode }) {
return (
<Box
overflow="hidden"
rounded="var(--radius-xl)"
borderWidth={1}
borderColor="var(--border)"
bg="$background"
>
<XStack
items="center"
gap="$2"
borderBottomWidth={1}
borderColor="var(--border)"
bg="$panel"
px="$4"
py="$2"
>
{/* A ring, not a lit dot. The three traffic-light hues are another
vendor's mark, and the shape alone already says "window" — the same
three rings `Lights` draws (components/frame/Lights.tsx). */}
{[0, 1, 2].map((i) => (
<View key={i} render="span" width={10} height={10} rounded="var(--radius-full)" borderWidth={1} borderColor="var(--border)" />
))}
<Line ml={8} fontFamily="$mono" {...XS} color={DIM}>
{lang}
</Line>
</XStack>
{children}
</Box>
)
}
/* ── the top of a family page ──────────────────────────────────────────────── */
/**
* The opening of a model-family page: where you are, what was measured, the
* name, the sentence and the two things to do.
*
* The light behind it is blurred past its own edge and centred on the page by
* gui's `x`. It draws NO rule beneath it. The hairline that used to close both
* of these heroes is the first `Band`'s own, one element down — same line,
* stated by the frame that owns every other boundary on the page rather than by
* the hero.
*/
export function Hero({
crumb,
badge,
title,
lede,
children,
}: {
/** The last step of the trail — the page you are on. */
crumb: string
/** The line beside the live dot. */
badge: string
title: string
lede: ReactNode
/** The actions. */
children: ReactNode
}) {
return (
<Box
render="section"
position="relative"
overflow="hidden"
pt={112}
pb="$10"
px="$4"
$sm={{ px: '$5' }}
$lg={{ px: '$6' }}
$platform-web={{ textAlign: 'center' }}
>
<View
aria-hidden
position="absolute"
t={0}
l="50%"
x="-50%"
width={720}
height={420}
pointerEvents="none"
backgroundImage="radial-gradient(circle, var(--white-10) 0%, transparent 68%)"
filter="blur(100px)"
/>
<Box position="relative" z="var(--z-raised)" width="100%" maxW={768} mx="auto">
<Crumbs center trail={[{ label: 'Models', href: '/models' }, { label: crumb }]} />
{/* `Chip` is the site's hairline badge — the outline, the radius, the
gap and the ink. What it does not state is how big this one is. */}
<Chip
mb={24}
px={16}
py={8}
fontSize="$1"
lineHeight="calc(1 / 0.75)"
>
<View render="span" width={6} height={6} rounded="var(--radius-full)" bg="var(--foreground)" />
{badge}
</Chip>
<Display mb={20}>
{title}
</Display>
<Lede mb={32} width="100%" maxW={672} mx="auto">
{lede}
</Lede>
<XStack flexWrap="wrap" justify="center" gap="$3">
{children}
</XStack>
</Box>
</Box>
)
}