mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
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.
340 lines
12 KiB
TypeScript
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>
|
|
)
|
|
}
|
|
|