Files
zeekay 44626a40c3 chrome: the bar is glass on every page, and the beta strip is blade steel
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
2026-10-22 12:02:12 -07:00

314 lines
14 KiB
TypeScript

'use client'
/**
* /identity — the sign-in a reader sees, in front of Hanzo IAM.
*
* NO TAILWIND. The four section frames are `components/marketing/band`'s `Band`
* and `Head`; @hanzo/ui's marketing components carry the type and the controls
* (`Display`, `Lede`, `Title`, `Chip`, `Action`, `Line`); everything left is
* gui props over @hanzo/design's tokens. The three hero tiles, the six capability
* cards and the four enterprise cards are DATA — they were the same markup
* written thirteen times.
*
* Three things the swap moves, each deliberate, each the shared shape winning
* over this page's own copy of it:
*
* - the section headings take `Title quiet` and stop stating
* `text-3xl font-bold`, so they step 3xl -> 4xl at 640 and are CENTRED.
* They were `ChromeText` inside a `text-center` block, and ChromeText's own
* `flex flex-col items-start` wrapper shrink-wrapped each one to the LEFT
* while the paragraph under it centred.
* - the closing rule takes `Band`'s `--neutral-900` hairline rather than this
* page's `--neutral-800`, and its gutter steps at 640/1024 like every other
* band on the site.
* - `Action` is the one button shape at its two weights.
*/
import React from 'react';
import { motion } from "@/components/motion";
import { ArrowRight, User, UserCheck, Shield, Lock, UserCog, Key, Fingerprint, History } from "lucide-react";
import { Box, XStack, YStack } from '@hanzo/ui'
import { Grid } from '@hanzo/ui/grid'
import { Band, Head } from '@/components/marketing/band'
import { DOCS } from '@/components/home/nav-data'
import { Action, Chip, Display, Lede, Line, type LineProps, Title } from '@hanzo/ui/marketing'
import { under } from '@/components/shell/dock'
/** How narrow a column may get before a grid drops one, per shape. */
const TRIO = { min: 260, max: 3 } as const
const DECK = { min: 304, max: 3 } as const
const DUO = { min: 320, max: 2 } as const
/** The two inks this page draws body copy and marks in, and its one wash. */
const SOFT = 'color-mix(in oklab, var(--foreground) 80%, transparent)'
const DIM = 'color-mix(in oklab, var(--foreground) 70%, transparent)'
const WASH = 'color-mix(in oklab, var(--primary) 10%, transparent)'
/** A `color-mix()` ink is not a value gui's colour props type, so a frame's
* wash rides `$platform-web`, which gui still writes as a class. */
const CARD = { p: 24, rounded: 'var(--radius-xl)', borderWidth: 1, borderColor: 'var(--border)', '$platform-web': { backgroundColor: WASH } } as const
const PANEL = {
p: 32,
rounded: 'var(--radius-xl)',
borderWidth: 1,
borderColor: 'var(--border)',
'$platform-web': { backgroundColor: 'color-mix(in oklab, var(--primary) 5%, transparent)' },
} as const
/** A card's heading, at the ink a heading takes from the base sheet. */
const Heading = (p: LineProps) => <Line render="h3" display="block" fontWeight="700" color="var(--text-primary)" {...p} />
/** A paragraph in the page's soft ink. */
const Body = (p: LineProps) => <Line render="p" display="block" color={SOFT} {...p} />
/** The disc a hero mark sits on. */
const DISC = { items: 'center', justify: 'center', width: 80, height: 80, rounded: 'var(--radius-full)', '$platform-web': { backgroundColor: WASH } } as const
/** The two controls at the foot of a section, and the pair in the hero. */
const CENTER = { justify: "center" } as const
const OUTLINE = { justify: "center", borderColor: "var(--border)" } as const
/** The three tiles under the hero — what a sign-in is, in three sentences. */
const STEPS = [
{ icon: User, title: 'Signing in', body: 'A password, a passkey, or a Google or GitHub account. A second factor when the organization asks for one.' },
{ icon: Shield, title: 'What the token carries', body: 'Who signed in, which organizations they belong to, and the role they hold where they asked to act.' },
{ icon: Lock, title: 'One sign-in, every app', body: 'Sign in once and the session is good across every app in the organization, including ours.' },
] as const
const FEATURES = [
// The apostrophe is a plain U+0027 here rather than `&apos;`: it is a STRING,
// not JSX text, so the entity would be published literally.
{ icon: UserCheck, title: 'A second factor', body: "An authenticator app, a code by SMS or email, or a passkey. Enrolment sends the material and then demands it back before it writes anything, and the destination is never taken from the request — an enrolment that let the caller name the phone would enrol the attacker's." },
{ icon: UserCog, title: 'Accounts and membership', body: 'Create people, put them in an organization, give them a role there. SCIM 2.0 does the same thing from a directory you already run, so joiners and leavers arrive without a script of your own.' },
{ icon: Key, title: 'Single sign-on', body: 'Every application registers under its own client id and redirects to the same authorize endpoint, so a person who is already signed in comes straight back with a code and never sees a form twice.' },
{ icon: Shield, title: 'Roles that travel', body: 'A role is granted at a place — an organization, a workspace under it, a single project. The token carries the one that applies to the request, so your service decides without asking us anything.' },
{ icon: Fingerprint, title: 'Passkeys', body: 'A WebAuthn credential registered to the account, unlocked by whatever the device already uses — a fingerprint, a face, a PIN. Nothing that can be phished, because there is nothing to type.' },
{ icon: History, title: 'A record of what happened', body: 'Actions are written once to an append-only log scoped to the organization they happened in. The write path exists; there is no edit path for normal operation, which is the point of keeping one.' },
] as const
const ENTERPRISE = [
{
title: 'Where the data sits',
body: 'An organization is a tenancy boundary, and the boundary is enforced by the code that reads the token rather than by a column somebody has to remember to filter on.',
points: [
'Enterprise security with GDPR and privacy controls',
'Self-host it, in your own region, on your own cluster',
'One Go binary, embedded SQLite by default — nothing to run beside it',
],
},
{
title: 'Bring the identity you already have',
body: 'Point Identity at your own issuer and it becomes the entry point without becoming the record.',
points: [
'Federate to any OpenID Connect issuer by naming its discovery URL',
'SCIM 2.0 provisioning, so joiners and leavers arrive from your directory',
'White-labelled by hostname — your mark on your domain, never ours',
],
},
{
title: 'Signing up your customers',
body: 'An organization can hold its own customers as sub-organizations, so a business you serve manages its people without any of it reaching yours.',
points: [
'Sign in with Google or GitHub, or with any issuer you add',
'A consent screen the person has to pass before an app gets a token',
'Org admins manage their own members without platform access',
],
},
{
title: 'What you write',
body: 'Almost nothing. Nobody should be hand-rolling OAuth in 2026, and with this you do not.',
points: [
'One SDK with an entry point per runtime — server, React, browser, Next.js, Passport',
'An OpenAPI 3.1 description generated from the handlers themselves',
'The sign-in page is hosted and already built — you send people to it',
],
},
] as const
const Identity = () => {
return (
<Box minH="100vh" $platform-web={{ color: 'var(--foreground)' }}>
{/* Hero Section */}
<Box render="section" position="relative" {...under(80)} pb={80} $lg={{ pt: under(128).pt, pb: 128 }}>
<Box
position="absolute"
t={0}
r={0}
b={0}
l={0}
backgroundImage="linear-gradient(to bottom in oklab, color-mix(in oklab, var(--pure-white) 20%, transparent), transparent)"
/>
<Box
position="relative"
z="var(--z-raised)"
width="100%"
maxW={1280}
mx="auto"
px={16}
$sm={{ px: 24 }}
$lg={{ px: 32 }}
>
<Box width="100%" maxW={768} mx="auto" mb={64} $platform-web={{ textAlign: 'center' }}>
<Chip
mb={16}
px={16}
py={4}
borderColor="var(--border)"
bg="color-mix(in oklab, var(--primary) 5%, transparent)"
>
<Line fontWeight="500" color={DIM}>Sign-in</Line>
</Chip>
<Display
mb={24}
>
Hanzo Identity
</Display>
<Lede mb={32}>
The sign-in your users actually see. Hanzo IAM is the service; Identity is what stands in front of
it — the sign-in page, the sign-up, the account screen, and the SDK that puts them in your app.
Your application redirects, a person signs in, and your code gets back a signed token. It never
sees the password.
</Lede>
<YStack gap={16} justify="center" $sm={{ flexDirection: 'row' }}>
<Action href="/login" fill {...CENTER}>
Try Hanzo
</Action>
<Action href={`${DOCS}/docs/openapi/iam`} {...OUTLINE}>
View Documentation
</Action>
</YStack>
</Box>
{/* Hero Visual */}
<Grid columns={TRIO} gap={24}>
{STEPS.map(({ icon: Icon, title, body }) => (
<YStack
key={title}
items="center"
p={24}
rounded="var(--radius-xl)"
borderWidth={1}
borderColor="var(--border)"
$platform-web={{ backgroundColor: WASH }}
>
<XStack mb={16} {...DISC}>
<Icon width={40} height={40} color={DIM} />
</XStack>
<Heading mb={8} fontSize="$4" lineHeight="1.5556">
{title}
</Heading>
<Body text="center">{body}</Body>
</YStack>
))}
</Grid>
</Box>
</Box>
{/* Features */}
<Band rule={false} pad="$11" measure={1280}>
<Head
title="What it does"
lede="Standards, not a proprietary protocol. If your framework can read an OpenID Connect discovery document, it can already talk to this."
measure={768}
/>
<Grid columns={DECK} gap={32}>
{FEATURES.map(({ icon: Icon, title, body }, index) => (
<Box
key={title}
render={<motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} transition={{ duration: 0.5, delay: index * 0.1 }} viewport={{ once: true }} />}
{...CARD}
>
<Box mb={16}>
<Icon width={40} height={40} color={DIM} />
</Box>
<Heading mb={8} fontSize="$6" lineHeight="1.4">
{title}
</Heading>
<Body>{body}</Body>
</Box>
))}
</Grid>
</Band>
{/* Enterprise Section */}
<Band
rule={false}
pad="$11"
measure={1280}
ground="linear-gradient(to bottom in oklab, var(--background), color-mix(in oklab, var(--pure-white) 10%, transparent))"
>
<Head
title="Running it for a large organization"
lede="The same service, wearing your name, on hardware you choose."
measure={768}
/>
<Grid columns={DUO} gap={32}>
{ENTERPRISE.map(({ title, body, points }) => (
<Box key={title} {...PANEL}>
<Heading mb={16} fontSize="$7" lineHeight="1.3333">
{title}
</Heading>
<Body mb={16}>{body}</Body>
<YStack render="ul" gap={8}>
{points.map((point) => (
<XStack key={point} render="li" items="flex-start">
<Line mr={8} color={DIM}>•</Line>
<Line color={SOFT}>{point}</Line>
</XStack>
))}
</YStack>
</Box>
))}
</Grid>
</Band>
{/* CTA Section */}
<Band rule={false} pad="$11" measure={1024}>
<Box
p={32}
$md={{ p: 48 }}
rounded="var(--radius-xl)"
borderWidth={1}
borderColor="var(--border)"
backgroundImage="linear-gradient(to right in oklab, color-mix(in oklab, var(--pure-white) 20%, transparent), color-mix(in oklab, var(--pure-white) 10%, transparent))"
>
<Box $platform-web={{ textAlign: 'center' }}>
<Title quiet mb={16}>Add sign-in</Title>
<Lede mb={32} width="100%" maxW={768} mx="auto" mt="$3.5">
Register the app, set the callback your framework already uses, install the SDK. That is the
whole integration.
</Lede>
<YStack gap={16} justify="center" $sm={{ flexDirection: 'row' }}>
<Action href="/login" fill {...CENTER}>
Try Hanzo
</Action>
<Action href={`${DOCS}/docs/openapi/iam`} {...OUTLINE}>
Read Documentation
</Action>
</YStack>
</Box>
</Box>
</Band>
<Band pad="$10" measure={768}>
<Box $platform-web={{ textAlign: 'center' }}>
<Title quiet mb={16}>Get started with Identity</Title>
<YStack gap={12} justify="center" $sm={{ flexDirection: 'row' }}>
<Action href="https://docs.hanzo.ai/docs/projects/hanzoai/iam" target="_blank" rel="noopener noreferrer" fill {...CENTER}>
Read the docs <ArrowRight width={16} height={16} />
</Action>
<Action href="https://github.com/hanzoai" target="_blank" rel="noopener noreferrer" {...OUTLINE}>
View on GitHub
</Action>
</YStack>
</Box>
</Band>
</Box>
);
};
export default Identity;