mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-28 07:32:52 +00:00
146 lines
4.8 KiB
TypeScript
146 lines
4.8 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* One line of type, and the two tables behind it.
|
|
*
|
|
* This site sets type in exactly one place. It is not `components/pricing`'s
|
|
* and not `components/landing`'s — every kit imports it, because a second copy
|
|
* of a ramp is how two sections of one page come to disagree about what `text-sm`
|
|
* means.
|
|
*/
|
|
|
|
import type { TextProps } from '@hanzo/gui'
|
|
import type { ReactNode } from 'react'
|
|
import { Box, Text } from '@hanzo/ui'
|
|
|
|
/**
|
|
* The size/leading pairs the page already renders, rung for rung.
|
|
*
|
|
* gui's `$N` font sizes and the `text-*` utilities resolve to the SAME
|
|
* `--text-*` custom properties, so the sizes need no translation. The leadings
|
|
* do: gui pairs each rung with a length from its own table (20px at `$3` where
|
|
* the utility gives 21), and a pixel a line over a page of copy is a page that
|
|
* has quietly changed height. The ratio is stated here instead, as a STRING —
|
|
* gui appends px to a bare number in a prop and in `style` alike, so `1.5` would
|
|
* mean one and a half pixels.
|
|
*/
|
|
const SIZE = {
|
|
xs: { fontSize: '$1', lineHeight: '$1' },
|
|
sm: { fontSize: '$2', lineHeight: '$2' },
|
|
base: { fontSize: '$3', lineHeight: '$3' },
|
|
lg: { fontSize: '$4', lineHeight: '$4' },
|
|
xl: { fontSize: '$6', lineHeight: '$6' },
|
|
x2: { fontSize: '$7', lineHeight: '$7' },
|
|
x3: { fontSize: '$8', lineHeight: '$8' },
|
|
x4: { fontSize: '$10', lineHeight: '$10' },
|
|
} as const
|
|
|
|
export type Size = keyof typeof SIZE
|
|
|
|
/**
|
|
* The inks this page draws with.
|
|
*
|
|
* `soft`/`dim`/`faint` are the three strengths of the foreground the model
|
|
* badges already used — 80, 70 and 60 percent — kept apart because they are
|
|
* what tells a tier from the one above it.
|
|
*/
|
|
const TONE = {
|
|
plain: '$ink',
|
|
muted: '$soft',
|
|
soft: '$quiet',
|
|
dim: '$faint',
|
|
faint: '$dim',
|
|
good: 'var(--state-online)',
|
|
} as const
|
|
|
|
export type Tone = keyof typeof TONE
|
|
|
|
/**
|
|
* One line of type.
|
|
*
|
|
* `render` picks the element — `tag` is not a gui prop, and asking for one ships
|
|
* a page with no headings at all while passing every check.
|
|
*/
|
|
export function Line({
|
|
size,
|
|
px,
|
|
tone = 'plain',
|
|
weight,
|
|
caps,
|
|
lead,
|
|
render,
|
|
align,
|
|
children,
|
|
...rest
|
|
}: {
|
|
/**
|
|
* A rung of the ramp, WITH its leading.
|
|
*
|
|
* Omitting it is not a default — it means this line states no type at all and
|
|
* inherits both, which is what an element carrying only a colour class does.
|
|
* Stating one anyway is not free: the document's leading is 20px at 14px and
|
|
* the ramp pairs `base` with 21, so stamping the pair on a `<span>` that had
|
|
* no `text-*` class grows the page by a pixel per rendered line. Measured on
|
|
* the plan ladder: sixteen wrapped lines, sixteen pixels, on the one card that
|
|
* sets the row's height.
|
|
*/
|
|
size?: Size
|
|
tone?: Tone
|
|
weight?: '400' | '500' | '600' | '700'
|
|
/** Small caps with the wide tracking the utility pair gave them. */
|
|
caps?: boolean
|
|
/** A leading other than the one this rung is paired with. */
|
|
lead?: TextProps['lineHeight']
|
|
/** A size off the ramp, in px. The model badges are set at 10. */
|
|
px?: number
|
|
render?: 'h1' | 'h2' | 'h3' | 'h4' | 'p' | 'span' | 'div' | 'li' | 'code'
|
|
align?: 'center' | 'right'
|
|
children: ReactNode
|
|
[k: string]: unknown
|
|
}) {
|
|
const pair = size ? SIZE[size] : undefined
|
|
// Caps track wide in `em`, so the spacing follows the rung; gui types a
|
|
// tracking in pixels, so it rides the web object, merged with any the caller
|
|
// brings rather than replacing it.
|
|
const web = rest['$platform-web'] as Record<string, unknown> | undefined
|
|
// The leading rides gui's PROP, not `style`. An inline style outranks every
|
|
// class, including the one a `$md`/`$lg` object emits — so a heading that
|
|
// steps up a rung at a breakpoint kept the base ratio and grew looser the
|
|
// bigger it got. The class is emitted either way; only one of the two can win.
|
|
//
|
|
// A STRING, because gui appends px to a bare number in a prop and in `style`
|
|
// alike, and `1.5` would mean one and a half pixels.
|
|
return (
|
|
<Text
|
|
render={render}
|
|
fontSize={px ?? pair?.fontSize}
|
|
lineHeight={lead ?? pair?.lineHeight}
|
|
fontWeight={weight}
|
|
color={TONE[tone]}
|
|
text={align}
|
|
textTransform={caps ? 'uppercase' : undefined}
|
|
{...rest}
|
|
{...(caps ? { '$platform-web': { letterSpacing: '0.05em', ...web } } : null)}
|
|
>
|
|
{children}
|
|
</Text>
|
|
)
|
|
}
|
|
|
|
/** `leading-relaxed`, which several cards set on their body copy. */
|
|
export const RELAXED = '$5'
|
|
|
|
|
|
/**
|
|
* Words a phone leaves out: a clause of a lede that the heading above it, or
|
|
* the page under it, already says. Inline from `$sm`, where the line has room;
|
|
* below it the lede ends at the words before. Start the clause with its space.
|
|
*/
|
|
export function Spare({ children }: { children: ReactNode }) {
|
|
return (
|
|
<Box render="span" display="inline" $max-sm={{ display: 'none' }}>
|
|
{children}
|
|
</Box>
|
|
)
|
|
}
|