Files
hanzo.ai/components/app/Get.tsx
zeekay 6c65e3e941 @hanzo/app 0.2.0: the app SDK, renamed from @hanzo/rooms
Every import, the catalog pin and the docs name @hanzo/app and
github.com/hanzoai/app. lib/rooms.ts is lib/app.ts and
components/rooms.tsx is components/app.tsx.
2026-10-22 19:07:20 -07:00

161 lines
6.6 KiB
TypeScript

'use client'
/**
* hanzo.app: the three ways to get the Hanzo App — Download, Install CLI, Open
* in web — and nothing else. This export's /app, served as hanzo.app's root
* (lib/host.ts `GET`).
*
* Every choice is a thing a reader can take today. Download is the native build
* `data/downloads.ts` resolved against the real release (`NATIVE`), the reader's
* own platform first; Install CLI is the line the download page prints; Open in
* web is the chat at hanzo.ai/chat, which signs a stranger in first. A phone has
* no build to take, so its Download says to open the app in the browser instead.
*/
import { useState, type ReactNode } from 'react'
import { Anchor, Paragraph, SizableText, Text, XStack, YStack } from '@hanzo/gui'
import { Grid } from '@hanzo/ui/grid'
import { Check, Copy, Download, Globe, Terminal, type LucideIcon } from 'lucide-react'
import { PageHero } from '@/components/marketing/page-kit'
import { BuildRow } from '@/components/download/surface-card'
import { INSTALL, NATIVE, weigh, type Build } from '@/data/downloads'
import { useHydrated } from '@hanzo/app/lib/hydrated'
import { current, type Platform } from '@/lib/platform'
import { WEB } from '@hanzo/app/lib/host'
import { Action } from '@hanzo/ui/marketing'
/** Where every build and checksum is listed. */
const ALL = 'https://hanzo.ai/download'
const CLI = 'https://docs.hanzo.ai/docs/cli'
/** The words a build's name starts with, per platform that can take one. */
const NAMES: Partial<Record<Platform, string>> = { mac: 'macOS', windows: 'Windows', linux: 'Linux' }
/** The reader's own build, once the browser has said what it runs on. */
function mine(platform: Platform | null): Build | undefined {
const name = platform ? NAMES[platform] : undefined
return name ? NATIVE?.builds?.find((b) => b.name.startsWith(name)) : undefined
}
function Card({ icon: Icon, title, body, children }: { icon: LucideIcon; title: string; body: string; children: ReactNode }) {
return (
<YStack rounded={18} borderWidth={1} borderColor="$color04" bg="$color2" p="$5" gap="$4" $md={{ p: '$6' }} height="100%">
<XStack items="center" gap="$2.5">
<Icon size={18} strokeWidth={1.75} aria-hidden />
<SizableText render="h2" fontSize="$7" fontWeight="600" letterSpacing={-0.3} color="$color">
{title}
</SizableText>
</XStack>
<Paragraph fontSize="$3" color="$color11">
{body}
</Paragraph>
{children}
</YStack>
)
}
/** The install line, and the one control that puts it on the clipboard. */
function Line({ command }: { command: string }) {
const [copied, setCopied] = useState(false)
const copy = async () => {
try {
await navigator.clipboard.writeText(command)
setCopied(true)
window.setTimeout(() => setCopied(false), 2000)
} catch {
/* no clipboard here: the line stays on screen to select by hand */
}
}
return (
<XStack items="center" gap="$2" rounded="$4" borderWidth={1} borderColor="$color04" bg="$background" pl="$3.5" pr="$1.5" py="$1.5">
<SizableText fontFamily="$mono" fontSize="$2" color="$color" flex={1} minW={0} whiteSpace="nowrap" overflowX="auto">
{command}
</SizableText>
<Action render="button" type="button" onClick={copy} aria-label={copied ? 'Copied' : 'Copy the install command'} minH={36} px={14}>
{copied ? <Check size={14} aria-hidden /> : <Copy size={14} aria-hidden />}
{copied ? 'Copied' : 'Copy'}
</Action>
</XStack>
)
}
export function Get() {
// The export renders no guess; the browser says what it runs on once hydrated.
const platform = useHydrated() ? current() : null
const own = mine(platform)
const rest = NATIVE?.builds?.filter((b) => b !== own) ?? []
const phone = platform === 'ios' || platform === 'android'
return (
<>
<PageHero
eyebrow="Hanzo App"
title="Get the Hanzo App"
lede="Chat, build and ship with one account — on your computer, in your terminal, or right here in the browser."
/>
<YStack px="$4" pb="$12" $sm={{ px: '$8' }}>
<YStack width="100%" maxW={1120} mx="auto">
<Grid columns={{ min: 280, max: 3 }} gap={16}>
<Card
icon={Download}
title="Download"
body={
NATIVE?.id === 'desktop'
? 'The desktop app, with a global hotkey: ask from any window and put the answer back where you were typing.'
: 'The Hanzo binary for your machine: the same agent as the app, working in your own checkout, and the whole cloud from one command.'
}
>
{phone ? (
<Paragraph fontSize="$3" color="$color11">
There is no build for a phone. Open the app in this browser instead.
</Paragraph>
) : own ? (
<Action fill href={own.url} aria-label={`Download for ${own.name}`} self="flex-start">
<Download size={15} aria-hidden />
Download for {own.name}
<Text opacity={0.6} color="inherit">{weigh(own.size)}</Text>
</Action>
) : null}
{rest.length ? (
<YStack>
{rest.map((b) => (
<BuildRow key={b.name} build={b} surface={NATIVE?.name ?? 'Hanzo'} />
))}
</YStack>
) : null}
<Anchor href={ALL} self="flex-start">
<SizableText fontSize="$2" color="$color11" hoverStyle={{ color: '$color' }}>
All downloads and checksums →
</SizableText>
</Anchor>
</Card>
<Card
icon={Terminal}
title="Install CLI"
body="One line in your terminal. Then hanzo in any repository is a coding session, and every product of the cloud is a command."
>
{INSTALL ? <Line command={INSTALL} /> : null}
<Anchor href={CLI} self="flex-start">
<SizableText fontSize="$2" color="$color11" hoverStyle={{ color: '$color' }}>
Read the CLI docs →
</SizableText>
</Anchor>
</Card>
<Card
icon={Globe}
title="Open in web"
body="Nothing to install. Sign in and the app opens in this browser, Chat and Dev, on the same account as every build here."
>
<Action fill href={WEB} self="flex-start">
Open Hanzo
</Action>
</Card>
</Grid>
</YStack>
</YStack>
</>
)
}