mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
Every word the landing, the header menus, the phone sheet, the footer,
the help corner and the way in (sign-in, sign-up, the reset, the
invitation, the callback, onboarding, the 404) write goes through
@hanzo/i18n's t(): one sentence is one message, its figures, names and
links arguments; copy written as data is phrase('…') where written and
t(label) where drawn. Plans, the chart, the API states and the lines
lib/kai and lib/enso build for the landing do the same.
Right to left: <html lang dir> is set before the first paint (the
package's bootScript first in the head); a figure, clock, range or
price outside a sentence is isolated; sides are logical gui props, the
layout's [data-join] corners logical, a bar grows from the start;
arrows along the line mirror (Onward for "words ->" links). The repo
dates read in the reader's language.
Gates select by slot, never by an English label (rtl, help). New gate
translated.spec.ts: in Arabic the landing at 1440 and 390, every menu
open, the phone sheet, sign-in, sign-up and the app signed in hold no
authored English, nothing scrolls sideways, every range is isolated;
French the same for English's commonest words. What another package
draws with no words prop is listed by name (@hanzo/ui's Footer label,
@hanzo/build's composer row and suggestions).
@hanzo/i18n 0.5.3 (the catalogs: 1,443 keys, every offered locale
complete), @hanzo/app 0.2.1 (the app's words). The link gate imports
the terms versions from @hanzo/app, the one SDK main now resolves.
178 lines
6.8 KiB
TypeScript
178 lines
6.8 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* The first run's parts, drawn as pay's (hanzo-inc/pay `.pay-cta`, `.pay-tier`):
|
|
* 56px pill buttons, and choices on pay's tile — the muted ground under a
|
|
* hairline, 24px corners. The page they stand on, its title and the line under
|
|
* it are the focused page's (components/shell/focus), as on every page of the
|
|
* way in. Every value is a design token, so light and dark are one rule.
|
|
*/
|
|
|
|
import type { ReactNode } from 'react'
|
|
import { Text, XStack, YStack } from '@hanzo/ui'
|
|
import { Action } from '@hanzo/ui/marketing'
|
|
import { Check, ChevronRight, type LucideIcon } from 'lucide-react'
|
|
import { LARGE } from '@/components/shell/focus'
|
|
|
|
/** gui types name custom properties only; a mix of two is still one. */
|
|
type Mix = `var(--${string})`
|
|
/** Ink at a rung of the ladder, over whatever the ground is: the same weight in both themes. */
|
|
const ink = (percent: number) => `color-mix(in srgb, var(--foreground) ${percent}%, transparent)` as Mix
|
|
|
|
/** A 56px pill: `fill` is the one loud control on the page, the rest are outlined. */
|
|
export function Pill({ fill = false, disabled, onPress, children }: { fill?: boolean; disabled?: boolean; onPress: () => void; children: ReactNode }) {
|
|
return (
|
|
<Action render="button" type="button" fill={fill} width="100%" disabled={disabled} onClick={onPress} {...LARGE}>
|
|
{children}
|
|
</Action>
|
|
)
|
|
}
|
|
|
|
/** The surface a choice or a form sits on: pay's tile (`--pay-tile` is `--muted`, its edge the hairline over it). */
|
|
export const TILE = {
|
|
bg: 'var(--muted)',
|
|
borderWidth: 1,
|
|
borderStyle: 'solid',
|
|
borderColor: ink(10),
|
|
rounded: 'var(--radius-2xl)',
|
|
} as const
|
|
|
|
/**
|
|
* One choice, as a link: its mark, its name and what it is for, and a chevron
|
|
* that says it goes on. A press first runs `onPick`, which may hold the page
|
|
* while it records the answer, and then follows the link. `data-ga`: the page
|
|
* counts the answer itself, so the site's click into pay is not counted as a
|
|
* checkout begun (app/providers.tsx `INTENT`); pay counts that at its card step.
|
|
*/
|
|
/**
|
|
* A side of the account as a square card, the way pay's plan cards stand: its
|
|
* mark, its name and who it is for, what its plans include, and the way on to
|
|
* them. The whole card is the link.
|
|
*/
|
|
export function Side({ href, icon: Icon, name, note, plans, points, see, onPick }: { href: string; icon: LucideIcon; name: string; note: string; plans: string; points: readonly string[]; see: string; onPick: (href: string) => void }) {
|
|
return (
|
|
<YStack
|
|
render={
|
|
<a
|
|
href={href}
|
|
data-ga=""
|
|
onClick={(e) => {
|
|
// A new tab or window is the reader's own; this tab follows the link once the answer is kept.
|
|
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) return
|
|
e.preventDefault()
|
|
onPick(href)
|
|
}}
|
|
/>
|
|
}
|
|
aria-label={`${name}: ${note}`}
|
|
// Side by side the two share the row; stacked on a phone each is as tall as
|
|
// what it holds. `flex: 1` there let the column squeeze a card to its 360px
|
|
// floor, and the way on stood over its edge.
|
|
$md={{ flex: 1 }}
|
|
minW={0}
|
|
gap="$4"
|
|
p="$6"
|
|
minH={360}
|
|
cursor="pointer"
|
|
{...TILE}
|
|
hoverStyle={{ borderColor: ink(30), bg: 'color-mix(in srgb, var(--foreground) 4%, var(--muted))' as Mix }}
|
|
focusVisibleStyle={{ outlineWidth: 2, outlineStyle: 'solid', outlineColor: 'var(--ring)', outlineOffset: 2 }}
|
|
$platform-web={{ textDecoration: 'none' }}
|
|
>
|
|
<YStack aria-hidden width={48} height={48} items="center" justify="center" rounded={14} borderWidth={1} borderStyle="solid" borderColor={ink(10)} bg="var(--background)">
|
|
<Icon size={22} strokeWidth={1.5} color="var(--foreground)" />
|
|
</YStack>
|
|
<YStack gap="$1">
|
|
<Text fontSize="$7" lineHeight="2rem" fontWeight="600" color="var(--foreground)">
|
|
{name}
|
|
</Text>
|
|
<Text fontSize="$4" lineHeight="1.375rem" color={ink(60)}>
|
|
{note}
|
|
</Text>
|
|
</YStack>
|
|
{/* Grows to push the button to the foot, and never shrinks under its own points. */}
|
|
<YStack gap="$2" grow={1} shrink={0}>
|
|
<Text fontFamily="$mono" fontSize="$1" color={ink(60)}>
|
|
{plans}
|
|
</Text>
|
|
{points.map((p) => (
|
|
<XStack key={p} gap="$2" items="flex-start">
|
|
<Text aria-hidden display="flex" pt={3} color="var(--foreground)">
|
|
<Check size={14} strokeWidth={2.2} />
|
|
</Text>
|
|
<Text fontSize="$3" lineHeight="1.375rem" color="var(--foreground)">
|
|
{p}
|
|
</Text>
|
|
</XStack>
|
|
))}
|
|
</YStack>
|
|
<XStack items="center" justify="center" gap="$2" height={44} rounded={9999} bg="var(--foreground)" $platform-web={{ borderRadius: '9999px' }}>
|
|
<Text fontSize="$3" fontWeight="600" color="var(--background)">
|
|
{see}
|
|
</Text>
|
|
<Text aria-hidden display="flex" color="var(--background)">
|
|
<ChevronRight size={16} />
|
|
</Text>
|
|
</XStack>
|
|
</YStack>
|
|
)
|
|
}
|
|
|
|
export function Choice({ href, icon: Icon, name, note, onPick }: { href: string; icon: LucideIcon; name: string; note: string; onPick: (href: string) => void }) {
|
|
return (
|
|
<XStack
|
|
render={
|
|
<a
|
|
href={href}
|
|
data-ga=""
|
|
onClick={(e) => {
|
|
// A new tab or window is the reader's own; this tab follows the link once the answer is kept.
|
|
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) return
|
|
e.preventDefault()
|
|
onPick(href)
|
|
}}
|
|
/>
|
|
}
|
|
aria-label={`${name}: ${note}`}
|
|
flex={1}
|
|
minW={0}
|
|
items="center"
|
|
gap="$4"
|
|
p="$5"
|
|
minH={96}
|
|
cursor="pointer"
|
|
{...TILE}
|
|
hoverStyle={{ borderColor: ink(20), bg: 'color-mix(in srgb, var(--foreground) 4%, var(--muted))' as Mix }}
|
|
focusVisibleStyle={{ outlineWidth: 2, outlineStyle: 'solid', outlineColor: 'var(--ring)', outlineOffset: 2 }}
|
|
$sm={{ p: '$6' }}
|
|
>
|
|
<YStack
|
|
aria-hidden
|
|
width={48}
|
|
height={48}
|
|
shrink={0}
|
|
items="center"
|
|
justify="center"
|
|
rounded={9999}
|
|
borderWidth={1}
|
|
borderStyle="solid"
|
|
borderColor={ink(10)}
|
|
bg="var(--background)"
|
|
>
|
|
<Icon size={22} strokeWidth={1.5} color="var(--foreground)" />
|
|
</YStack>
|
|
<YStack flex={1} minW={0} gap="$1">
|
|
<Text fontSize="$6" lineHeight="1.5rem" fontWeight="500" color="var(--foreground)">
|
|
{name}
|
|
</Text>
|
|
<Text fontSize="$4" lineHeight="1.375rem" color={ink(60)}>
|
|
{note}
|
|
</Text>
|
|
</YStack>
|
|
<Text aria-hidden display="flex" color={ink(60)}>
|
|
<ChevronRight size={20} />
|
|
</Text>
|
|
</XStack>
|
|
)
|
|
}
|