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.
87 lines
3.4 KiB
TypeScript
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>
|
|
)
|
|
}
|