Files
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

87 lines
3.4 KiB
TypeScript

'use client'
import { Text, XStack, YStack } from '@hanzo/ui'
import { Lights } from '@hanzo/app/bots/Lights'
/**
* A screen of the shipped product, in a window that says where it was taken.
*
* The picture is a capture of the room itself with sample names in it, so the
* chrome, the type and the controls are the product's own. The three rings are
* the same ones every window on this site wears.
*
* THE PICTURE FOLLOWS THE PAGE, and it is the only thing here that could not.
* The window below is drawn in tokens and retunes with the theme; a screenshot
* is pixels and cannot. One capture per room left the frame and its contents on
* opposite grounds — the chat plate was photographed light and glared out of a
* dark page, the rest were dark rectangles in a light one. So a room is
* photographed in both (`scripts/room-shots.mjs`) and `src` names the PAIR:
* `/shots/bots-1440`, from which `-light.webp` and `-dark.webp` follow.
*
* THE CHOICE IS CSS, gui's `$theme-light`, off the `t_light` class
* @hanzo/appearance paints onto <html> before the first
* paint — so the plate is settled in the first style resolution rather than
* after hydration, and there is no frame in which the wrong one shows. It has to be
* that class and not `prefers-color-scheme`: the install's
* default is dark, so a reader on a light desktop still gets a dark page,
* and the dock in the corner lets anyone overrule the system outright.
*
* ONE PLATE IS FETCHED. The one the theme did not pick has `display: none` and
* so has no box, and a lazy image with no box is never near the viewport —
* which is the thing that starts the request. Measured in Chromium, Firefox and
* WebKit: one image per load, either way round. That costs the hero its place
* in the preload scan, which is what `eager` now pays for with a priority
* rather than with a second download.
*/
/** The two grounds a room is photographed on. Dark is the page's own default. */
const GROUNDS = ['dark', 'light'] as const
export function Shot({
src,
at,
alt,
width,
height,
eager = false,
}: {
/** The pair, without its ground or extension: `/shots/bots-1440`. */
src: string
at: string
alt: string
width: number
height: number
eager?: boolean
}) {
return (
<YStack rounded="var(--radius-xl)" borderWidth={1} borderColor="$edge" bg="$panel" overflow="hidden">
<XStack items="center" gap={10} py={10} px={14} borderBottomWidth={1} borderColor="$edge" bg="var(--surface-card-quiet)">
<Lights />
<Text fontFamily="$mono" fontSize="$1" color="var(--text-tertiary)">
{at}
</Text>
</XStack>
{/* Both carry the alt, and only one is ever in the accessibility tree:
`display: none` takes the other out of it as surely as it takes it out
of the layout, so the screen is announced once. */}
{GROUNDS.map((ground) => (
// A flex box, so the picture is a block and fills the window's width.
<YStack
key={ground}
display={ground === 'dark' ? 'flex' : 'none'}
$theme-light={{ display: ground === 'dark' ? 'none' : 'flex' }}
>
<img
src={`${src}-${ground}.webp`}
alt={alt}
width={width}
height={height}
loading="lazy"
fetchPriority={eager ? 'high' : undefined}
decoding="async"
/>
</YStack>
))}
</YStack>
)
}