mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
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.
210 lines
6.5 KiB
TypeScript
210 lines
6.5 KiB
TypeScript
'use client'
|
|
|
|
import { Anchor, Paragraph, SizableText, View, XStack, YStack } from '@hanzo/gui'
|
|
import { ArrowUpRight, Download } from 'lucide-react'
|
|
import { tap } from '@hanzo/app/lib/tap'
|
|
|
|
import { weigh, type Build, type Surface } from '@/data/downloads'
|
|
|
|
/**
|
|
* One surface, as a card: what it is, and every real way to get it.
|
|
*
|
|
* Every action is a real `<a>`. That is the whole focus story — `assets/globals.css`
|
|
* rings any `a[href]` at `var(--focus-ring)` and nothing here may weaken it, so a
|
|
* card gets a visible ring by being built out of links rather than by declaring one.
|
|
*/
|
|
|
|
/** A platform, and the artifact it takes. The row pattern, once. */
|
|
export function BuildRow({ build, surface }: { build: Build; surface: string }) {
|
|
const Icon = build.icon
|
|
return (
|
|
<XStack
|
|
items="center"
|
|
justify="space-between"
|
|
gap="$3"
|
|
py="$2.5"
|
|
borderBottomWidth={1}
|
|
borderColor="$color04"
|
|
>
|
|
<XStack items="center" gap="$2.5" flex={1} minW={0}>
|
|
{Icon ? <Icon size={16} strokeWidth={1.5} color="var(--text-tertiary)" aria-hidden /> : null}
|
|
<SizableText fontSize="$3" color="$color">
|
|
{build.name}
|
|
</SizableText>
|
|
</XStack>
|
|
<XStack items="center" gap="$3" shrink={0}>
|
|
<SizableText fontSize="$1" color="$color11">
|
|
{weigh(build.size)}
|
|
</SizableText>
|
|
<Anchor
|
|
{...tap}
|
|
href={build.url}
|
|
// The surface is in the name because the platform alone is not
|
|
// unique: the CLI and the desktop app both publish a Windows build,
|
|
// and two links reading "Download Windows" are two different files.
|
|
aria-label={`Download ${surface} for ${build.name}`}
|
|
display="flex"
|
|
items="center"
|
|
gap="$1.5"
|
|
rounded="$4"
|
|
borderWidth={1}
|
|
borderColor="$color06"
|
|
px="$3"
|
|
py="$1.5"
|
|
hoverStyle={{ bg: '$color04', borderColor: '$color' }}
|
|
>
|
|
<Download size={13} strokeWidth={2} aria-hidden />
|
|
<SizableText fontSize="$2" fontWeight="500" color="$color">
|
|
Download
|
|
</SizableText>
|
|
</Anchor>
|
|
</XStack>
|
|
</XStack>
|
|
)
|
|
}
|
|
|
|
/** The action a hosted surface carries: it opens, it does not download. */
|
|
function Open({ href, label }: { href: string; label: string }) {
|
|
return (
|
|
<Anchor
|
|
{...tap}
|
|
href={href}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
self="flex-start"
|
|
display="flex"
|
|
items="center"
|
|
gap="$2"
|
|
rounded="$4"
|
|
borderWidth={1}
|
|
borderColor="$color06"
|
|
px="$4"
|
|
py="$2.5"
|
|
hoverStyle={{ bg: '$color04', borderColor: '$color' }}
|
|
>
|
|
<SizableText fontSize="$3" fontWeight="500" color="$color">
|
|
{label}
|
|
</SizableText>
|
|
<ArrowUpRight size={15} strokeWidth={2} aria-hidden />
|
|
</Anchor>
|
|
)
|
|
}
|
|
|
|
export function SurfaceCard({ surface, lead = false }: { surface: Surface; lead?: boolean }) {
|
|
const { name, blurb, version, command, builds, open, all, visual } = surface
|
|
|
|
// The lead card lays its copy beside its picture from $lg up; the grid cards
|
|
// stack. One card, two arrangements — not two components.
|
|
// `flexGrow` + `flexBasis: auto`, never `flex={1}`: in gui's flex model
|
|
// `flex: 1` sets `flex-basis: 0`, and in a card that sizes to its contents a
|
|
// base size of 0 IS the size — the card collapses to its padding and the copy
|
|
// paints outside it.
|
|
const body = (
|
|
<YStack
|
|
grow={1}
|
|
flexBasis="auto"
|
|
minW={0}
|
|
gap="$4"
|
|
$lg={lead ? { flexBasis: 0 } : undefined}
|
|
>
|
|
<YStack gap="$2">
|
|
<XStack items="center" gap="$3">
|
|
<SizableText fontSize="$7" fontWeight="600" letterSpacing={-0.3} color="$color">
|
|
{name}
|
|
</SizableText>
|
|
{version ? (
|
|
<SizableText fontFamily="$mono" fontSize="$1" color="$color11">
|
|
{version}
|
|
</SizableText>
|
|
) : null}
|
|
</XStack>
|
|
<Paragraph fontSize="$3" color="$color11" maxW={52 * 8}>
|
|
{blurb}
|
|
</Paragraph>
|
|
</YStack>
|
|
|
|
{command ? (
|
|
<XStack
|
|
items="center"
|
|
gap="$3"
|
|
rounded="$4"
|
|
borderWidth={1}
|
|
borderColor="$color04"
|
|
bg="$color2"
|
|
px="$3.5"
|
|
py="$2.5"
|
|
>
|
|
<SizableText fontFamily="$mono" fontSize="$2" color="$color" flex={1} minW={0}>
|
|
{command}
|
|
</SizableText>
|
|
</XStack>
|
|
) : null}
|
|
|
|
{builds?.length ? (
|
|
<YStack>
|
|
{builds.map((b) => (
|
|
<BuildRow key={b.name} build={b} surface={name} />
|
|
))}
|
|
</YStack>
|
|
) : null}
|
|
|
|
{open ? <Open href={open.href} label={open.label} /> : null}
|
|
|
|
{all ? (
|
|
<Anchor
|
|
{...tap}
|
|
href={all}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
self="flex-start"
|
|
display="flex"
|
|
items="center"
|
|
>
|
|
<SizableText fontSize="$2" color="$color11" hoverStyle={{ color: '$color' }}>
|
|
All builds and checksums →
|
|
</SizableText>
|
|
</Anchor>
|
|
) : null}
|
|
</YStack>
|
|
)
|
|
|
|
return (
|
|
<YStack
|
|
width="100%"
|
|
rounded={18}
|
|
borderWidth={1}
|
|
borderColor="$color04"
|
|
bg="$color2"
|
|
p="$5"
|
|
gap="$5"
|
|
$md={{ p: '$6' }}
|
|
$lg={lead ? { flexDirection: 'row', items: 'center', p: '$7', gap: '$8' } : undefined}
|
|
>
|
|
{body}
|
|
{visual ? (
|
|
// `flexBasis: 0` at $lg ONLY, and the axis is the whole reason. From $lg
|
|
// a lead card is a ROW, so basis governs WIDTH: `auto` would take the
|
|
// capture's intrinsic 1920px as the basis and push the page wider than
|
|
// the viewport, and 0 lets the two halves split the row evenly. Below
|
|
// $lg the card is a COLUMN, where basis governs HEIGHT and 0 would
|
|
// flatten the picture to nothing — which is why this is not set at base.
|
|
<YStack
|
|
grow={1}
|
|
flexBasis="auto"
|
|
minW={0}
|
|
width="100%"
|
|
$lg={lead ? { flexBasis: 0 } : undefined}
|
|
rounded="$4"
|
|
overflow="hidden"
|
|
borderWidth={1}
|
|
borderColor="$color04"
|
|
>
|
|
{/* A real capture. `alt` carries what it shows, because that is the
|
|
one description a reader who cannot see it gets. */}
|
|
<View render={<img src={visual} alt={`${name} in use`} />} display="block" width="100%" maxW="100%" height="auto" />
|
|
</YStack>
|
|
) : null}
|
|
</YStack>
|
|
)
|
|
}
|