Files

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>
)
}