mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
The bar stood in the page's flow, clear at rest, so a first screen whose ground is a picture or a light started below it, and the bar read as a black band over the html's #000 ground: above the creative models' paintings, the kit pages' glow and the lit product heroes. It is glass now at rest and scrolled ($background/72 under the chrome blur; the scroll listener is gone), and such a first screen spreads `under(pt)` from components/shell/dock.ts: up by the bar's height, the same back on its top padding, so its ground starts at the top of the window behind the glass and no word moves. On Splash (the six creative models and Zen), PageHero, /edge, /identity, /functions, /playground, /operative, /realtime and /enso. On /beta the site's bar at rest keeps the glass and paints the focused pages' fixed glow on it (components/shell/focus.tsx), so the light runs on unbroken. 72% is kept: over Zen's painting the nav's ink holds 5.1:1 at .72 and falls to 3.7:1 at .6. The beta strip is polished curved steel in CSS gradients: a faint top line, a ridge 5px down, the flat dark under the words, a temper line and the bright 1px edge at the bottom; one `/` light at 70 degrees, resting mid-strip, moved by transform, brighter where it crosses the ridge and the edge than under the words. The 1px/3px grain that read as stripes is gone, and so is the root script that set --beta-tilt on <html> every pointer frame on every page, restyling the whole document and ignoring reduced motion. White words hold 5.4:1 against the brightest steel under them, where the live strip measured 4.35:1 at 1440 and 3.45:1 on a phone. Gates: e2e/gates/glass.spec.ts and e2e/gates/steel.spec.ts. The composer gate reads its rest once the theme's colours have eased in: read straight after the input appeared, the pill's border was a value on its way to its token (rgba(137…), rgba(175…) on two CI runs) and failed the check that the border keeps one value through focus. Claude-Session: https://claude.ai/code/session_01BLLMyBFSvAfCt7ZGqYuiEz
161 lines
6.6 KiB
TypeScript
161 lines
6.6 KiB
TypeScript
'use client'
|
|
|
|
import React, { type ReactNode } from "react";
|
|
import { motion } from "@/components/motion";
|
|
import { Activity, Radio, Zap, ArrowRight } from "lucide-react";
|
|
import { Pulse, View, XStack, YStack } from '@hanzo/ui'
|
|
import { Words } from '@/components/marketing/band'
|
|
import { Grid } from '@hanzo/ui/grid'
|
|
import { Line } from '@/components/type'
|
|
import { KEYS, DOCS } from '@/components/home/nav-data'
|
|
import { Action, Chip, Display, Lede } from '@hanzo/ui/marketing'
|
|
import { under } from '@/components/shell/dock'
|
|
|
|
/** The wash over the fold. */
|
|
const WASH = 'linear-gradient(to bottom, color-mix(in oklab, var(--pure-white) 20%, transparent), transparent)'
|
|
|
|
/**
|
|
* The badge's own dials. `Chip` owns the outline; `alignSelf` is what an
|
|
* inline-flex badge needs once the column around it is a flex container — an
|
|
* item with no alignment of its own stretches the full measure.
|
|
*/
|
|
const BADGE = { self: "flex-start", mb: 24, px: 12, py: 4, bg: "color-mix(in oklab, var(--primary) 10%, transparent)", borderColor: "color-mix(in oklab, var(--pure-white) 30%, transparent)" } as const
|
|
|
|
/** The live dot, at the two sizes this section draws it. `Pulse` is the breath. */
|
|
const DOT = { width: 12, height: 12, rounded: 'var(--radius-full)', background: 'color-mix(in oklab, var(--primary) 10%, transparent)' } as const
|
|
const TICK = { width: 8, height: 8, mb: 8, rounded: 'var(--radius-full)', bg: 'var(--primary)' } as const
|
|
|
|
/** `watch` is the subscription sample, highlighted by the server page. */
|
|
const HeroSection = ({ watch }: { watch: ReactNode }) => {
|
|
return (
|
|
<YStack
|
|
render="section"
|
|
position="relative"
|
|
overflow="hidden"
|
|
{...under(128)}
|
|
pb={64}
|
|
px={16}
|
|
$sm={{ px: 24 }}
|
|
$lg={{ px: 32 }}
|
|
>
|
|
<YStack position="absolute" t={0} r={0} b={0} l={0} background={WASH} />
|
|
|
|
<YStack width="100%" maxW={1280} mx="auto" position="relative" z="var(--z-raised)">
|
|
<XStack
|
|
flexDirection="column"
|
|
items="center"
|
|
justify="space-between"
|
|
gap={48}
|
|
$lg={{ flexDirection: 'row' }}
|
|
>
|
|
<YStack width="100%" $lg={{ width: '50%' }}>
|
|
<Chip
|
|
render={<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.5 }} />}
|
|
{...BADGE}
|
|
>
|
|
<Activity width={16} height={16} color="var(--foreground)" />
|
|
<span>Live record updates</span>
|
|
</Chip>
|
|
|
|
<Display
|
|
render={<motion.h1 initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5, delay: 0.1 }} />}
|
|
mb={24}
|
|
>
|
|
Your screen updates <br/>
|
|
when the data does
|
|
</Display>
|
|
|
|
<Lede
|
|
render={<motion.p initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5, delay: 0.2 }} />}
|
|
mb={32}
|
|
maxW={576}
|
|
>
|
|
Open one stream and records reach the browser as they change — created, updated, deleted — with no polling loop and no timer deciding how stale a page is allowed to get. A subscription is a collection, or a single record inside it, and the access rule already guarding that collection decides what any given subscriber is allowed to receive.
|
|
</Lede>
|
|
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.5, delay: 0.3 }}
|
|
>
|
|
<XStack flexWrap="wrap" gap={16}>
|
|
<Action href={KEYS} fill>
|
|
Start Building <ArrowRight width={16} height={16} />
|
|
</Action>
|
|
<Action href={DOCS}>
|
|
View Documentation
|
|
</Action>
|
|
</XStack>
|
|
</motion.div>
|
|
</YStack>
|
|
|
|
<YStack width="100%" $lg={{ width: '50%' }}>
|
|
<View
|
|
render={<motion.div initial={{ opacity: 0, scale: 0.9 }} animate={{ opacity: 1, scale: 1 }} transition={{ duration: 0.5, delay: 0.4 }} />}
|
|
display="block"
|
|
p={24}
|
|
rounded="var(--radius-xl)"
|
|
borderWidth={1}
|
|
borderColor="var(--neutral-800)"
|
|
background="color-mix(in oklab, var(--pure-black) 60%, transparent)"
|
|
>
|
|
<XStack justify="space-between" items="center" mb={16}>
|
|
<XStack items="center" gap={12}>
|
|
<Pulse {...DOT} />
|
|
<Line size="sm" tone="soft">Live connection</Line>
|
|
</XStack>
|
|
<XStack items="center" gap={8}>
|
|
<Radio width={16} height={16} color="var(--foreground)" />
|
|
<Line size="sm" tone="soft">One open stream</Line>
|
|
</XStack>
|
|
</XStack>
|
|
|
|
<YStack gap={16}>
|
|
<XStack
|
|
height={56}
|
|
items="center"
|
|
gap={12}
|
|
p={16}
|
|
rounded="var(--radius-lg)"
|
|
borderWidth={1}
|
|
borderColor="$rim"
|
|
background="linear-gradient(to right, color-mix(in oklab, var(--pure-white) 20%, transparent), color-mix(in oklab, var(--pure-white) 10%, transparent))"
|
|
>
|
|
<Zap width={20} height={20} color="var(--foreground)" />
|
|
<Words color="var(--text-secondary)">Records arrive as they change</Words>
|
|
</XStack>
|
|
|
|
<Grid columns={3} gap={16}>
|
|
{[1, 2, 3].map((i) => (
|
|
<YStack
|
|
key={i}
|
|
height={96}
|
|
justify="center"
|
|
items="center"
|
|
p={16}
|
|
rounded="var(--radius-lg)"
|
|
borderWidth={1}
|
|
borderColor="var(--neutral-800)"
|
|
background="linear-gradient(to bottom right, var(--neutral-900), var(--neutral-950))"
|
|
>
|
|
<Pulse {...TICK} />
|
|
<Line size="xs" tone="muted" align="center">Client {i}</Line>
|
|
<Line size="xs" tone="muted" align="center">Connected</Line>
|
|
</YStack>
|
|
))}
|
|
</Grid>
|
|
|
|
<View minH={144} overflow="hidden" rounded="var(--radius-lg)" borderWidth={1} borderColor="var(--neutral-800)">
|
|
{watch}
|
|
</View>
|
|
</YStack>
|
|
</View>
|
|
</YStack>
|
|
</XStack>
|
|
</YStack>
|
|
</YStack>
|
|
);
|
|
};
|
|
|
|
export default HeroSection;
|