mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
256 lines
7.9 KiB
TypeScript
256 lines
7.9 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* The picture kit — the one vocabulary every section on this page draws in.
|
|
*
|
|
* `Surfaces` invented all of this and held it privately: a card, an inset plate,
|
|
* a mono row, a verdict chip, a caption. Seven sections below it render nothing
|
|
* but type, and each one that grew a picture would have re-invented the same
|
|
* five things slightly differently — which is how a page comes to have four
|
|
* greys and three border radii and look assembled rather than designed.
|
|
*
|
|
* THE RULE THE PICTURES FOLLOW, and it is what makes them worth drawing: a plate
|
|
* shows the PRODUCT'S OWN OUTPUT. The primitives plate is an ops count, the
|
|
* policy plate is the decision log, the ledger plate is a bill. None of them is
|
|
* an illustration of a concept — each is a thing the platform really emits, in
|
|
* the shape it really emits it. A section whose claim has no artefact behind it
|
|
* should stay type rather than grow a drawing that implies one exists.
|
|
*
|
|
* Every part is a COMPONENT on gui props over @hanzo/design's tokens, so a plate
|
|
* retunes with the theme and never carries a colour of its own. Components and
|
|
* not shared style values, because this file is `'use client'`: a Server
|
|
* Component holding a client VALUE holds a reference, and a spread of one
|
|
* yields `{}` — the frame silently gone. A component crosses that boundary
|
|
* whole.
|
|
*/
|
|
import type { ComponentProps, ReactNode } from 'react'
|
|
import { Text, XStack, YStack } from '@hanzo/ui'
|
|
import { Grid } from '@hanzo/ui/grid'
|
|
|
|
type TextProps = ComponentProps<typeof Text>
|
|
|
|
/** The mono face every index and log line is set in: 11px on a 1.9 line. */
|
|
const MONO = { fontFamily: '$mono', fontSize: '$1', lineHeight: '1.3rem' } as const
|
|
|
|
/** A run of mono type: a value, a path, a line of a log. */
|
|
export function Mono({ children, ...rest }: TextProps) {
|
|
return (
|
|
<Text display="block" {...MONO} {...rest}>
|
|
{children}
|
|
</Text>
|
|
)
|
|
}
|
|
|
|
/** The quiet part of a line — an annotation beside the value it explains. */
|
|
export function Dim({ children }: { children: ReactNode }) {
|
|
return <Text color="var(--text-tertiary)">{children}</Text>
|
|
}
|
|
|
|
/** The line under a picture that says what the picture is. */
|
|
export function Caption({ children, ...rest }: TextProps) {
|
|
return (
|
|
<Text render="p" display="block" pt={4} fontFamily="$mono" fontSize="$1" color="var(--muted-foreground)" {...rest}>
|
|
{children}
|
|
</Text>
|
|
)
|
|
}
|
|
|
|
/** The word over a pane that says what the pane holds. */
|
|
export function Label({ children, ...rest }: TextProps) {
|
|
return (
|
|
<Text fontFamily="$mono" fontSize="$1" letterSpacing={1.54} textTransform="uppercase" color="var(--text-tertiary)" {...rest}>
|
|
{children}
|
|
</Text>
|
|
)
|
|
}
|
|
|
|
/** The line a plate opens on: the host every row under it hangs off. */
|
|
export function Host({ children }: { children: ReactNode }) {
|
|
return (
|
|
<Mono color="var(--muted-foreground)" pb={8} borderBottomWidth={1} borderColor="$edge">
|
|
{children}
|
|
</Mono>
|
|
)
|
|
}
|
|
|
|
/** The line that divides a stack from what cuts across it. */
|
|
export function Rule({ children }: { children: ReactNode }) {
|
|
return (
|
|
<Mono color="var(--text-tertiary)" pt={8} borderTopWidth={1} borderColor="$edge">
|
|
{children}
|
|
</Mono>
|
|
)
|
|
}
|
|
|
|
/**
|
|
* A verdict, as the platform words it.
|
|
*
|
|
* `strong` is the one that carries weight — a refusal, a hold, a failure — and it
|
|
* is the FILLED one. A control that only ever says yes is not a control, so a
|
|
* page showing this vocabulary has to be willing to show the loud state, and the
|
|
* loud state has to be the one that is legible at a glance.
|
|
*
|
|
* The quiet state is `--muted-foreground` rather than a step darker: an 11px
|
|
* word on the plate's own ground measured 4.04:1, under the 4.5:1 a reader is
|
|
* owed for text this size.
|
|
*/
|
|
export function Chip({ children, strong = false }: { children: ReactNode; strong?: boolean }) {
|
|
return (
|
|
<Text
|
|
shrink={0}
|
|
rounded={4}
|
|
borderWidth={1}
|
|
borderColor="$edge"
|
|
px={6}
|
|
fontSize="$1"
|
|
color={strong ? 'var(--pure-white)' : 'var(--muted-foreground)'}
|
|
bg={strong ? 'var(--neutral-800)' : undefined}
|
|
>
|
|
{children}
|
|
</Text>
|
|
)
|
|
}
|
|
|
|
/**
|
|
* One line of a log: a fixed left, a flexible middle, a verdict.
|
|
*
|
|
* NOWRAP on the row, and the middle is the only thing allowed to truncate. A log
|
|
* line is read left to right and its shape IS the information — let it wrap and
|
|
* one event breaks into stacked fragments that read as several.
|
|
*/
|
|
export function Row({
|
|
lead,
|
|
children,
|
|
verdict,
|
|
strong = false,
|
|
struck = false,
|
|
}: {
|
|
lead: string
|
|
children: ReactNode
|
|
verdict?: ReactNode
|
|
strong?: boolean
|
|
struck?: boolean
|
|
}) {
|
|
return (
|
|
<XStack items="center" gap={8}>
|
|
<Mono shrink={0} whiteSpace="nowrap" color="var(--text-tertiary)">
|
|
{lead}
|
|
</Mono>
|
|
<Mono
|
|
flex={1}
|
|
minW={0}
|
|
whiteSpace="nowrap"
|
|
color={struck ? 'var(--neutral-600)' : 'var(--neutral-400)'}
|
|
textDecorationLine={struck ? 'line-through' : 'none'}
|
|
>
|
|
{children}
|
|
</Mono>
|
|
{verdict ? <Chip strong={strong}>{verdict}</Chip> : null}
|
|
</XStack>
|
|
)
|
|
}
|
|
|
|
/**
|
|
* A row of an index that is itself a link: quiet ink at rest, the whole row
|
|
* coming up to full ink together under a pointer or a key, and a thumb-sized
|
|
* box on a coarse pointer, since a row that navigates is a control and not a
|
|
* link in a sentence.
|
|
*/
|
|
export function LinkRow({ href, children }: { href: string; children: ReactNode }) {
|
|
return (
|
|
<XStack render={<a href={href} />} group items="center" gap={8} $touchable={{ minH: 44 }}>
|
|
{children}
|
|
</XStack>
|
|
)
|
|
}
|
|
|
|
/** A cell of a `LinkRow`: muted at rest, full ink when its row is reached. */
|
|
export function Cell({ children, ...rest }: TextProps) {
|
|
return (
|
|
<Mono
|
|
whiteSpace="nowrap"
|
|
color="var(--muted-foreground)"
|
|
transition="quickest"
|
|
$group-hover={{ color: '$ink' }}
|
|
$group-focusVisible={{ color: '$ink' }}
|
|
{...rest}
|
|
>
|
|
{children}
|
|
</Mono>
|
|
)
|
|
}
|
|
|
|
/** A plate of rows. The one container every picture opens with. */
|
|
export function Plate({ children, gap = 6 }: { children: ReactNode; gap?: number }) {
|
|
return (
|
|
<YStack rounded="var(--radius-lg)" borderWidth={1} borderColor="$edge" bg="var(--surface-card-quiet)" p={12} overflow="hidden">
|
|
<Grid columns={1} gap={gap}>
|
|
{children}
|
|
</Grid>
|
|
</YStack>
|
|
)
|
|
}
|
|
|
|
/** A card's frame: one hairline, one radius, one inset. */
|
|
const FRAME = {
|
|
rounded: 'var(--radius-xl)',
|
|
borderWidth: 1,
|
|
borderColor: '$edge',
|
|
bg: '$panel',
|
|
p: 20,
|
|
gap: 12,
|
|
} as const
|
|
|
|
/** A card's two lines: what it is, then what it does. */
|
|
export function CardTitle({ children, render }: { children: ReactNode; render?: 'h3' | 'div' }) {
|
|
return (
|
|
<Text render={render} display="flex" items="center" gap={8} fontSize="$3" fontWeight="500" color="$ink">
|
|
{children}
|
|
</Text>
|
|
)
|
|
}
|
|
|
|
export function CardBody({ children }: { children: ReactNode }) {
|
|
return (
|
|
<Text render="p" display="block" fontSize="$2" lineHeight="1.3203rem" color="var(--muted-foreground)">
|
|
{children}
|
|
</Text>
|
|
)
|
|
}
|
|
|
|
/** The same card, when the whole card is the link. */
|
|
export function CardLink({ href, children }: { href: string; children: ReactNode }) {
|
|
return (
|
|
<YStack render={<a href={href} />} {...FRAME} gap={0}>
|
|
{children}
|
|
</YStack>
|
|
)
|
|
}
|
|
|
|
/** A titled card with a picture inside it and a caption under it. */
|
|
export function Card({
|
|
icon: Icon,
|
|
title,
|
|
body,
|
|
children,
|
|
caption,
|
|
}: {
|
|
icon?: (p: { size?: number; strokeWidth?: number; 'aria-hidden'?: boolean }) => ReactNode
|
|
title: string
|
|
body?: string
|
|
children?: ReactNode
|
|
caption?: string
|
|
}) {
|
|
return (
|
|
<YStack {...FRAME}>
|
|
<CardTitle render="div">
|
|
{Icon ? <Icon size={16} strokeWidth={1.5} aria-hidden /> : null}
|
|
{title}
|
|
</CardTitle>
|
|
{body ? <CardBody>{body}</CardBody> : null}
|
|
{children}
|
|
{caption ? <Caption>{caption}</Caption> : null}
|
|
</YStack>
|
|
)
|
|
}
|