mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
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.
254 lines
12 KiB
TypeScript
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 `'`: 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;
|