Files
hanzo.ai/components/ui/chrome-text.tsx
zeekay d6e029ab18 A badged heading centres its lines, light code reads at AA, and the Kai answer is never NaN
- ChromeText centres the stack when there is a badge, but the heading box
  is as wide as the stack once it wraps, so at 360px /playground's two-line
  heading started at the left, 76px off the axis its badge and lede share.
  It centres its text too.
- GitHub Light's orange token (#E36209) is 3.49:1 on its white, under AA
  since 11265a21 put every sample through shiki. A light token ink under
  4.5:1 is darkened, hue kept, until it clears it.
- useTween with `ms` 0 divided 0 by 0 when the frame's time was the effect's
  own, and a negative elapsed share eased to NaN: /kai's distribution drew
  `NaN` on some loads. The share is held to [0, 1] and 0 ms arrives at once.
2026-10-15 09:41:46 -07:00

57 lines
2.5 KiB
TypeScript

import type { ReactNode } from 'react'
import { YStack } from '@/components/ui'
import { Chip, Display, type RoleProps } from '@hanzo/ui/marketing'
/**
* The display heading, and the badge that can sit above it.
*
* SOLID INK. The heading used to be painted with a horizontal
* grey→white→grey gradient clipped to the glyphs, which cost more than it
* looked like it did:
*
* - the ends of every headline were `rgb(180,180,180)` while the middle was
* `rgb(240,240,240)`, so the last word of each one was its dimmest and the
* line read as a render that had not finished;
* - the gradient's offset tracked the POINTER, through a `mousemove`
* listener on `window` that called `setState` and `getBoundingClientRect`
* on every event — a React re-render and a forced layout per mouse move,
* on 33 surfaces, for an effect most readers never noticed;
* - the rule shipped inside the component, so a page with six headings put
* six identical `<style>` blocks in the DOM.
*
* Hanzo is monochrome and its ink is paper-white. A confident heading is that
* white, once, at a tight display leading — which is also the one treatment a
* reader can rely on meaning "this is the heading" rather than "this heading
* is decorated".
*
* The type is @hanzo/ui's `Display`, which owns the WHOLE of it — ink, leading
* and the scale at every width. A call site passes CONTENT and spacing, never
* type. Everything here comes through `components/ui`, the client boundary, so
* a Server Component can render it.
*
* The stack centres when there is a badge, because a badge that starts where
* the heading does reads as part of it; without one it starts at the left. A
* centred heading centres its lines too: a box centred in the stack is as wide
* as the stack once it wraps, so its lines would start at the left of it.
*/
type ChromeTextProps = Omit<RoleProps, 'render'> & {
children: ReactNode
as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'span' | 'p'
preHeading?: string
}
const ChromeText = ({ children, as = 'h1', preHeading, ...display }: ChromeTextProps) => (
<YStack items={preHeading ? 'center' : 'flex-start'}>
{preHeading ? (
<Chip mb={16} px={12} py={4} fontSize="$1" lineHeight="calc(1 / 0.75)" fontWeight="500" bg="var(--glass)">
{preHeading}
</Chip>
) : null}
<Display render={as} {...(preHeading ? { text: 'center' as const } : null)} {...display}>
{children}
</Display>
</YStack>
)
export default ChromeText