Files
zeekay ac64d5d7dd Folds say one line: every first screen keeps its heading and a single lede
The owner's spec for a landing page is the models bento's: monochrome, the
words left and the art right, no extra prose. The folds carried two to four
sentences under each heading, and some a claim, a lede and a list besides.
Each now says one line: ProductLanding the tagline where a page states one
(the lede still describes it to search), a claim alone where the claim is
the line (/payments, /treasury, /zap, the extension), and elsewhere the
sentence that says what the thing is. /philosophy's two points, /dns' claim
that repeated its title, /calculator's capabilities line and the
Spare clauses on /research and /learn go.
2026-10-30 06:42:40 -07:00

254 lines
12 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, Lede, Line, type LineProps, Title } from '@hanzo/ui/marketing'
import { Fold } from '@/components/marketing/fold'
import { Steps } from '@/components/preview/steps'
import { phrase } from '@hanzo/i18n'
import { useI18n } from '@hanzo/i18n/react'
/** How narrow a column may get before a grid drops one, per shape. */
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 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: phrase('Signing in'), body: phrase('A password, a passkey, or a Google or GitHub account. A second factor when the organization asks for one.') },
{ icon: Shield, title: phrase('What the token carries'), body: phrase('Who signed in, which organizations they belong to, and the role they hold where they asked to act.') },
{ icon: Lock, title: phrase('One sign-in, every app'), body: phrase('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: phrase('A second factor'), body: phrase("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: phrase('Accounts and membership'), body: phrase('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: phrase('Single sign-on'), body: phrase('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: phrase('Roles that travel'), body: phrase('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: phrase('Passkeys'), body: phrase('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: phrase('A record of what happened'), body: phrase('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: phrase('Where the data sits'),
body: phrase('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: [
phrase('Enterprise security with GDPR and privacy controls'),
phrase('Self-host it, in your own region, on your own cluster'),
phrase('One Go binary, embedded SQLite by default — nothing to run beside it'),
],
},
{
title: phrase('Bring the identity you already have'),
body: phrase('Point Identity at your own issuer and it becomes the entry point without becoming the record.'),
points: [
phrase('Federate to any OpenID Connect issuer by naming its discovery URL'),
phrase('SCIM 2.0 provisioning, so joiners and leavers arrive from your directory'),
phrase('White-labelled by hostname — your mark on your domain, never ours'),
],
},
{
title: phrase('Signing up your customers'),
body: phrase('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: [
phrase('Sign in with Google or GitHub, or with any issuer you add'),
phrase('A consent screen the person has to pass before an app gets a token'),
phrase('Org admins manage their own members without platform access'),
],
},
{
title: phrase('What you write'),
body: phrase('Almost nothing. Nobody should be hand-rolling OAuth in 2026, and with this you do not.'),
points: [
phrase('One SDK with an entry point per runtime — server, React, browser, Next.js, Passport'),
phrase('An OpenAPI 3.1 description generated from the handlers themselves'),
phrase('The sign-in page is hosted and already built — you send people to it'),
],
},
] as const
const Identity = () => {
const { t } = useI18n()
return (
<Box minH="100vh" $platform-web={{ color: 'var(--foreground)' }}>
<Fold
label="Sign-in"
title="Hanzo Identity"
lede="The sign-in your users actually see."
actions={
<>
<Action href="/login" fill {...CENTER}>
{t('Try Hanzo')}
</Action>
<Action href={`${DOCS}/docs/openapi/iam`} {...OUTLINE}>
{t('View Documentation')}
</Action>
</>
}
preview={<Steps title="hanzo.id" steps={STEPS.map((x) => ({ title: x.title, body: x.body }))} />}
/>
{/* Features */}
<Band rule={false} pad="$11" measure={1280}>
<Head
title={t('What it does')}
lede={t('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">
{t(title)}
</Heading>
<Body>{t(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={t('Running it for a large organization')}
lede={t('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">
{t(title)}
</Heading>
<Body mb={16}>{t(body)}</Body>
<YStack render="ul" gap={8}>
{points.map((point) => (
<XStack key={point} render="li" items="flex-start">
<Line marginInlineEnd={8} color={DIM}>•</Line>
<Line color={SOFT}>{t(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}>{t('Add sign-in')}</Title>
<Lede mb={32} width="100%" maxW={768} mx="auto" mt="$3.5">
{t('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}>
{t('Try Hanzo')}
</Action>
<Action href={`${DOCS}/docs/openapi/iam`} {...OUTLINE}>
{t('Read Documentation')}
</Action>
</YStack>
</Box>
</Box>
</Band>
<Band pad="$10" measure={768}>
<Box $platform-web={{ textAlign: 'center' }}>
<Title quiet mb={16}>{t('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}>
{t('Read the docs')} <ArrowRight width={16} height={16} data-i18n-mirror />
</Action>
<Action href="https://github.com/hanzoai" target="_blank" rel="noopener noreferrer" {...OUTLINE}>
{t('View on GitHub')}
</Action>
</YStack>
</Box>
</Band>
</Box>
);
};
export default Identity;