mirror of
https://github.com/hanzoai/id.git
synced 2026-10-16 18:38:15 +00:00
id: style from @hanzo/design tokens, and stop painting controls by ancestor
A component's surface must never depend on WHERE it is mounted. app.css painted
controls with the descendant selectors `form input {…}` and `.hanzo-id-btn, form
button {…}`. DeviceApproval — the screen a human hits to authorise the CLI — has
2 inputs and 0 <form> ancestors, so its device-code field fell out of the
stylesheet and rendered as raw UA chrome: 31px tall, #3b3b3b, a 2px inset bevel,
square corners, sitting beside correctly-styled 44px siblings. That is the mirror
image of a component library shipping utility class names with no CSS behind
them — bare elements instead of bare class names — and it was live on hanzo.id,
lux.id and pars.id at once. Every control now carries its own class
(.hanzo-id-input / .hanzo-id-btn / .hanzo-id-field / .hanzo-id-form) and no
element-descendant selector for surface is left in the file.
.hanzo-id-spinner had NO rule at all: the loading state measured 0px and was
invisible on every portal. It is a real 28px ring now.
Tokens come from @hanzo/design — the same token layer hanzoai/pay renders from,
so the two halves of one flow (sign in -> pay) agree on the page black (#000000,
--background; the page was painted with the --card value), the type ramp (h1 21px
on both), the radii and the greys. Nothing in this file invents a colour, radius
or size. @hanzo/gui was declared as a dependency and imported exactly zero times;
it is removed rather than left as decoration.
ONE button: .hanzo-id-social-btn and the .primary modifier are gone. .hanzo-id-btn
IS primary and .ghost is the secondary surface, so 13 treatments become 1
primitive with 1 modifier. ONE focus indicator: the file had a single focus rule
(`form input:focus`), so every button, link and social entry fell back to
Chrome's blue outline: auto on a monochrome surface. `font: inherit` on every
control: buttons and inputs rendered in Arial while headings rendered in the
platform face — two typefaces in one 432px card, including on the Sign in CTA.
Control borders are --white-40 on purpose. On --background the semantic --border
(#1f1f1f) measures 1.27:1 and --border-strong (#404040) 2.03:1; neither clears
the 3:1 a control boundary needs (WCAG 1.4.11), and --white-40 measures 3.66:1
and is on the ladder. Same reasoning for the focus ring: --ring (#333333)
measures 1.66:1 and cannot carry one, so the ring is --primary.
Fonts are deliberately NOT imported from the design package: tokens/fonts.css
pulls Geist from fonts.googleapis.com and the sign-in path loads no third-party
font. --font-sans resolves to the platform stack, the identical value hanzoai/pay
sets.
Measured in a real browser against the built bundle, fresh context and empty
storage: page #000000, every control 44px, one 6px radius, h1 21px, a white
2px focus ring, nothing under the touch floor at 390px, no horizontal overflow.
134 unit tests and every workspace typecheck green.
Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -1,5 +1,52 @@
|
||||
# LLM.md — Hanzo ID
|
||||
|
||||
## One token layer, and no control painted by its ancestor (0.2.12)
|
||||
|
||||
The portal is now styled from **@hanzo/design tokens** — the same token layer
|
||||
hanzoai/pay renders from, so the two halves of one flow (sign in → pay) agree on
|
||||
the page black, the type ramp, the radii and the greys. `apps/web/src/app.css`
|
||||
invents no colour, radius or size; `@hanzo/gui` was declared as a dependency but
|
||||
never imported once, and is removed rather than left as decoration.
|
||||
|
||||
- **Surface is keyed to a CLASS the component carries, never to where it is
|
||||
mounted.** The stylesheet used to paint controls with the descendant selectors
|
||||
`form input {…}` and `.hanzo-id-btn, form button {…}`. `DeviceApproval.tsx` —
|
||||
the screen a human hits to authorise the CLI — has 2 inputs and **0** `<form>`
|
||||
ancestors, so its device-code field fell out of the stylesheet and rendered as
|
||||
raw UA chrome: 31px tall, `#3b3b3b`, `2px inset` bevel, square corners, beside
|
||||
correctly-styled 44px siblings. Every control now carries `.hanzo-id-input`,
|
||||
`.hanzo-id-btn`, `.hanzo-id-field` or `.hanzo-id-form`, and there is not one
|
||||
element-descendant selector for surface left in the file. Same class of defect
|
||||
as a component library shipping utility class names with no CSS behind them —
|
||||
the mirror image, bare elements instead of bare class names.
|
||||
- **`.hanzo-id-spinner` had no rule at all.** The loading state measured 0px and
|
||||
was invisible on hanzo.id, lux.id and pars.id at once. It is a real 28px ring
|
||||
now, verified rendering and animating.
|
||||
- **ONE focus indicator.** The file had exactly one focus rule (`form
|
||||
input:focus`), so every button, link and social entry fell back to Chrome's
|
||||
`outline: auto` — a blue ring on a monochrome surface. `:focus-visible` is now
|
||||
global, 2px `--primary`. Note `--ring` (#333333) measures **1.66:1** on
|
||||
`--background` and cannot carry a focus indicator; that is a finding against
|
||||
@hanzo/design, not a licence to invent a value.
|
||||
- **ONE button.** `.hanzo-id-social-btn` and the `.primary` modifier are gone:
|
||||
`.hanzo-id-btn` IS primary, `.ghost` is the secondary surface (social sign-in,
|
||||
Skip/Back). 13 treatments → 1 primitive with 1 modifier.
|
||||
- **`font: inherit` on every control.** Buttons and inputs rendered in Arial
|
||||
while headings rendered in the platform face — two typefaces in one 432px card,
|
||||
including on the "Sign in" CTA.
|
||||
- **Control borders are `--white-40`, deliberately.** On `--background` the
|
||||
semantic `--border` (#1f1f1f) measures 1.27:1 and `--border-strong` (#404040)
|
||||
2.03:1 — neither clears the 3:1 a control boundary needs (WCAG 1.4.11).
|
||||
`--white-40` measures 3.66:1 and is on the ladder.
|
||||
- **Fonts are NOT imported from the design package.** `tokens/fonts.css` pulls
|
||||
Geist from fonts.googleapis.com; the sign-in path loads no third-party font.
|
||||
`--font-sans` resolves to the platform stack, the identical value hanzoai/pay
|
||||
sets. Self-hosting Geist would let both import `fonts.css` unchanged.
|
||||
|
||||
Measured in a real browser against the built bundle (fresh context, empty
|
||||
storage): page `#000000`, controls 44px, one 6px radius, h1 21px, white focus
|
||||
ring, no control under the touch floor at 390px, no horizontal overflow.
|
||||
|
||||
## One chain-agnostic "Connect Wallet" button — merge EVM + Solana entries (0.2.9)
|
||||
|
||||
The login page rendered ONE wallet button PER enabled chain (`SocialButtons`
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "@hanzo/id-web",
|
||||
"private": true,
|
||||
"version": "0.1.26",
|
||||
"description": "Hanzo ID \u2014 white-label login / signup / IDV portal. Vite + React 19 + @hanzo/gui. Same image serves hanzo.id / lux.id / zoo.id / pars.id.",
|
||||
"version": "0.1.27",
|
||||
"description": "Hanzo ID — white-label login / signup / IDV portal. Vite + React 19, styled from @hanzo/design tokens. Same image serves hanzo.id / lux.id / zoo.id / pars.id.",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -13,7 +13,7 @@
|
||||
"dependencies": {
|
||||
"@crossmarkio/sdk": "^0.4.0",
|
||||
"@hanzo/brand": "^1.3.0",
|
||||
"@hanzo/gui": "^7.2.4",
|
||||
"@hanzo/design": "^0.2.0",
|
||||
"@hanzo/iam": "^0.13.1",
|
||||
"@hanzo/id-auth": "workspace:*",
|
||||
"@hanzo/id-connect": "workspace:*",
|
||||
|
||||
+249
-147
@@ -1,23 +1,65 @@
|
||||
/* Hanzo ID — the styling layer for hanzo.id / lux.id / zoo.id / pars.id.
|
||||
*
|
||||
* TOKENS COME FROM @hanzo/design. Not one colour, radius or type size is
|
||||
* invented here; every value below resolves to a design token, so a token change
|
||||
* lands on all four brand portals AND on pay.hanzo.ai — the other half of the
|
||||
* same sign-in-then-pay flow — at once.
|
||||
*
|
||||
* ARCHITECTURE RULE, learned the hard way. A component's surface must never
|
||||
* depend on WHERE it is mounted. This file used to paint controls with the
|
||||
* descendant selectors `form input {…}` and `.hanzo-id-btn, form button {…}`,
|
||||
* so any control that escaped a <form> ancestor fell out of the stylesheet and
|
||||
* rendered as raw UA chrome: the device-approval screen (2 inputs, 0 forms)
|
||||
* showed a 31px beveled browser input next to correctly-styled 44px siblings.
|
||||
* That is the same class of defect as a distributed component shipping utility
|
||||
* class names with no CSS behind them. Every rule below is keyed to a CLASS the
|
||||
* component itself carries — `.hanzo-id-input`, `.hanzo-id-btn`,
|
||||
* `.hanzo-id-field`, `.hanzo-id-form` — and there are no element-descendant
|
||||
* selectors for surface anywhere in this file.
|
||||
*/
|
||||
|
||||
@import '@hanzo/design/tokens/colors.css';
|
||||
@import '@hanzo/design/tokens/radius.css';
|
||||
@import '@hanzo/design/tokens/typography.css';
|
||||
@import '@hanzo/design/tokens/motion.css';
|
||||
|
||||
:root {
|
||||
--brand: #ffffff;
|
||||
--bg: #0a0a0a;
|
||||
--fg: #fafafa;
|
||||
--muted: #a3a3a3;
|
||||
--border: #71717a;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
/* The design system's face is Geist, pulled from fonts.googleapis.com by
|
||||
@hanzo/design/tokens/fonts.css. The sign-in path deliberately loads NO
|
||||
third-party font, so --font-sans resolves to the platform stack instead.
|
||||
hanzoai/pay resolves it to the same value, so one flow renders in one
|
||||
typeface. Self-hosting Geist would let both import fonts.css unchanged. */
|
||||
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, ui-sans-serif, system-ui, sans-serif;
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
|
||||
font-family: var(--font-sans);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html, body, #root { height: 100%; margin: 0; }
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: var(--text-base);
|
||||
}
|
||||
|
||||
/* ONE focus indicator, on every focusable thing. There used to be exactly one
|
||||
focus rule in this file (`form input:focus`), so every button, link and social
|
||||
entry fell back to Chrome's `outline: auto` — a BLUE ring on a monochrome
|
||||
surface. --ring (#333333) measures 1.66:1 against --background and cannot
|
||||
carry a focus indicator (WCAG 2.4.13 wants 3:1), so the ring is --primary. */
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--primary);
|
||||
outline-offset: 2px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
/* ── Page shell ────────────────────────────────────────────────────── */
|
||||
|
||||
.hanzo-id-page {
|
||||
flex: 1;
|
||||
/* #root is not a flex container, so `flex: 1` alone never stretches this to
|
||||
@@ -31,16 +73,14 @@ body {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.hanzo-id-brand-header {
|
||||
padding: 16px 0 32px;
|
||||
}
|
||||
.hanzo-id-brand-header { padding: 16px 0 32px; }
|
||||
|
||||
/* Text fallback when a brand ships no logo asset (see BrandHeader). */
|
||||
.hanzo-id-wordmark {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--fg);
|
||||
font-size: var(--text-xl);
|
||||
font-weight: var(--weight-bold);
|
||||
letter-spacing: var(--tracking-tight);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.hanzo-id-page main {
|
||||
@@ -51,75 +91,183 @@ body {
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.hanzo-id-page h1 { margin: 0; font-size: 28px; }
|
||||
.hanzo-id-page .lede { color: var(--muted); margin: 0; }
|
||||
|
||||
form { display: flex; flex-direction: column; gap: 16px; }
|
||||
form label { display: flex; flex-direction: column; gap: 6px; font-size: 14px; color: var(--muted); }
|
||||
form input {
|
||||
background: #111;
|
||||
color: var(--fg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px;
|
||||
font-size: 16px;
|
||||
/* The type scale is the design scale, five rungs: xs / sm / base / xl / 2xl.
|
||||
`h1` at --text-2xl is the SAME 24px as pay.hanzo.ai's `text-2xl` headings, so
|
||||
a heading does not change size when the flow crosses between the two. */
|
||||
.hanzo-id-page h1 {
|
||||
margin: 0;
|
||||
font-size: var(--text-2xl);
|
||||
line-height: var(--leading-2xl);
|
||||
letter-spacing: var(--tracking-tight);
|
||||
}
|
||||
form input:focus { outline: 2px solid var(--brand); outline-offset: -1px; }
|
||||
.hanzo-id-page h2 { margin: 0; font-size: var(--text-xl); line-height: var(--leading-xl); }
|
||||
.hanzo-id-page .lede { color: var(--muted-foreground); margin: 0; font-size: var(--text-base); }
|
||||
|
||||
.hanzo-id-btn, form button {
|
||||
background: var(--fg);
|
||||
color: var(--bg);
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
/* 13px vertical padding -> 44px tall (>=44px touch target). */
|
||||
padding: 13px 16px;
|
||||
/* ── Field ─────────────────────────────────────────────────────────── */
|
||||
|
||||
.hanzo-id-form { display: flex; flex-direction: column; gap: 16px; }
|
||||
|
||||
.hanzo-id-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.hanzo-id-input {
|
||||
/* `font: inherit` first: a bare <input>/<button> otherwise renders in the UA
|
||||
face (Arial), which put two typefaces inside one 432px card — including on
|
||||
the primary CTA. */
|
||||
font: inherit;
|
||||
font-size: var(--text-base); /* <16px makes iOS Safari zoom on focus */
|
||||
background: var(--white-05);
|
||||
color: var(--foreground);
|
||||
/* A control boundary must clear 3:1 against the page (WCAG 1.4.11). On
|
||||
--background (#000) the semantic --border (#1f1f1f) measures 1.27:1 and
|
||||
--border-strong (#404040) 2.03:1 — neither can carry an input edge. The
|
||||
ladder rung that does is --white-40 (3.66:1). */
|
||||
border: 1px solid var(--white-40);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 0 14px;
|
||||
min-height: 44px; /* the touch-target floor */
|
||||
width: 100%;
|
||||
}
|
||||
.hanzo-id-input::placeholder { color: var(--text-disabled); }
|
||||
.hanzo-id-input:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
|
||||
/* Checkbox: the UA control, sized up from its 13px intrinsic box and tinted with
|
||||
the brand. Its touch target is the whole <label> row, which is why it is the
|
||||
one control here that is not 44px itself. */
|
||||
.hanzo-id-check {
|
||||
font: inherit;
|
||||
accent-color: var(--primary);
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
min-height: 0;
|
||||
margin-top: 2px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
/* ── Button — ONE primitive, two modifiers ─────────────────────────── */
|
||||
/* `.hanzo-id-btn` is filled (the primary action). `.ghost` is the secondary
|
||||
surface — social sign-in, org rows, Skip/Back. `.row` spreads content for a
|
||||
list row. There is no `.primary`: the base IS primary, so there is one and
|
||||
only one way to write the default button. */
|
||||
|
||||
.hanzo-id-btn {
|
||||
font: inherit;
|
||||
font-size: var(--text-base);
|
||||
font-weight: var(--weight-semibold);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
background: var(--primary);
|
||||
color: var(--primary-foreground);
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 10px 16px;
|
||||
min-height: 44px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
transition: background var(--duration-fast) var(--ease-out),
|
||||
border-color var(--duration-fast) var(--ease-out);
|
||||
}
|
||||
.hanzo-id-btn[aria-disabled='true'], form button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.hanzo-id-btn.primary { background: var(--brand); }
|
||||
.hanzo-id-btn:disabled,
|
||||
.hanzo-id-btn[aria-disabled='true'] { opacity: 0.5; cursor: not-allowed; }
|
||||
.hanzo-id-btn svg { flex: none; }
|
||||
|
||||
.hanzo-id-btn.ghost {
|
||||
background: var(--white-05);
|
||||
color: var(--foreground);
|
||||
border-color: var(--white-40);
|
||||
font-weight: var(--weight-medium);
|
||||
}
|
||||
.hanzo-id-btn.ghost:hover:not(:disabled) { background: var(--white-10); border-color: var(--white-60); }
|
||||
|
||||
.hanzo-id-btn.row { justify-content: space-between; text-align: left; width: 100%; }
|
||||
|
||||
.hanzo-id-cta-row { display: flex; gap: 12px; }
|
||||
.hanzo-id-footer-links { color: var(--muted); font-size: 14px; }
|
||||
.hanzo-id-footer-links a { color: var(--fg); }
|
||||
.hanzo-id-cta-row .hanzo-id-btn { flex: 1; }
|
||||
|
||||
.hanzo-id-linkbtn {
|
||||
font: inherit;
|
||||
background: none;
|
||||
border: 0;
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-sm);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
padding: 4px 0;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 4px;
|
||||
}
|
||||
|
||||
.hanzo-id-footer-links { color: var(--muted-foreground); font-size: var(--text-sm); }
|
||||
.hanzo-id-footer-links a { color: var(--text-primary); }
|
||||
|
||||
/* A2P SMS consent disclosure (shown on phone/SMS surfaces). */
|
||||
.hanzo-id-sms-consent { color: var(--muted); font-size: 12px; line-height: 1.5; }
|
||||
.hanzo-id-sms-consent { color: var(--muted-foreground); font-size: var(--text-xs); line-height: var(--leading-relaxed); }
|
||||
.hanzo-id-sms-consent p { margin: 0 0 6px; }
|
||||
.hanzo-id-sms-consent-links { margin: 0; }
|
||||
.hanzo-id-sms-consent a { color: var(--fg); }
|
||||
.hanzo-id-sms-consent a { color: var(--text-primary); }
|
||||
|
||||
.hanzo-id-error {
|
||||
background: #2d0a0a;
|
||||
color: #ff7878;
|
||||
border: 1px solid #5a1414;
|
||||
/* ── Inline message ────────────────────────────────────────────────── */
|
||||
/* Red is one of the two hues @hanzo/design permits, and these are the system's
|
||||
own state tokens — the same #fca5a5 pay.hanzo.ai renders errors in. The
|
||||
informational variant used to be a blue (#78b8ff) that exists nowhere in the
|
||||
system; on a monochrome surface "information" is a neutral card. */
|
||||
|
||||
.hanzo-id-error,
|
||||
.hanzo-id-info {
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: var(--text-sm);
|
||||
line-height: var(--leading-sm);
|
||||
margin: 0;
|
||||
}
|
||||
.hanzo-id-error {
|
||||
background: var(--state-error-bg);
|
||||
color: var(--state-error-text);
|
||||
border: 1px solid var(--state-error);
|
||||
}
|
||||
.hanzo-id-info {
|
||||
background: #0a1f2d;
|
||||
color: #78b8ff;
|
||||
border: 1px solid #14385a;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
background: var(--card);
|
||||
color: var(--text-secondary);
|
||||
border: 1px solid var(--white-10);
|
||||
}
|
||||
|
||||
/* ──Device-authorization approval ─────────────────────────────── */
|
||||
/* ── Loading ───────────────────────────────────────────────────────── */
|
||||
/* `.hanzo-id-spinner` used to be a class with NO rule behind it: the loading
|
||||
state measured 0px tall and was invisible on every portal. */
|
||||
|
||||
.hanzo-id-spinner {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin: 0 auto;
|
||||
border: 2px solid var(--white-15);
|
||||
border-top-color: var(--primary);
|
||||
border-radius: var(--radius-full);
|
||||
animation: hanzo-id-spin 700ms linear infinite;
|
||||
}
|
||||
@keyframes hanzo-id-spin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* ── Device-authorization approval ─────────────────────────────────── */
|
||||
|
||||
.hanzo-id-device main { gap: 18px; }
|
||||
.hanzo-id-device-prompt { color: var(--muted); font-size: 14px; line-height: 1.5; margin: 0; }
|
||||
.hanzo-id-device-prompt strong { color: var(--fg); }
|
||||
.hanzo-id-device-code-field { display: flex; flex-direction: column; gap: 6px; }
|
||||
.hanzo-id-device-code-field span { color: var(--muted); font-size: 13px; }
|
||||
.hanzo-id-device-prompt {
|
||||
color: var(--muted-foreground);
|
||||
font-size: var(--text-sm);
|
||||
line-height: var(--leading-relaxed);
|
||||
margin: 0;
|
||||
}
|
||||
.hanzo-id-device-prompt strong { color: var(--text-primary); }
|
||||
.hanzo-id-device-code {
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 22px;
|
||||
letter-spacing: 0.25em;
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-xl);
|
||||
letter-spacing: var(--tracking-widest);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.hanzo-id-device-confirm {
|
||||
@@ -127,79 +275,57 @@ form input:focus { outline: 2px solid var(--brand); outline-offset: -1px; }
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.hanzo-id-device-confirm input {
|
||||
width: auto;
|
||||
margin-top: 2px;
|
||||
flex: none;
|
||||
color: var(--muted-foreground);
|
||||
font-size: var(--text-sm);
|
||||
line-height: var(--leading-relaxed);
|
||||
}
|
||||
|
||||
/* ──Forced TOTP enrollment ────────────────────────────────────── */
|
||||
/* ── Forced TOTP enrollment ────────────────────────────────────────── */
|
||||
|
||||
.hanzo-id-mfa-enroll { display: flex; flex-direction: column; gap: 16px; }
|
||||
.hanzo-id-mfa-enroll h2 { margin: 0; font-size: 22px; }
|
||||
.hanzo-id-mfa-enroll h2 { margin: 0; font-size: var(--text-xl); }
|
||||
.hanzo-id-mfa-qr {
|
||||
align-self: center;
|
||||
background: #fff;
|
||||
background: var(--pure-white);
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
border-radius: var(--radius-lg);
|
||||
width: 220px;
|
||||
height: 220px;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
.hanzo-id-mfa-qr svg { width: 100%; height: 100%; display: block; }
|
||||
.hanzo-id-mfa-manual { font-size: 14px; color: var(--muted); }
|
||||
.hanzo-id-mfa-manual { font-size: var(--text-sm); color: var(--muted-foreground); }
|
||||
.hanzo-id-mfa-manual summary { cursor: pointer; }
|
||||
.hanzo-id-mfa-secret,
|
||||
.hanzo-id-mfa-recovery code {
|
||||
display: inline-block;
|
||||
margin-top: 8px;
|
||||
padding: 6px 10px;
|
||||
background: #111;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
letter-spacing: 2px;
|
||||
background: var(--white-05);
|
||||
border: 1px solid var(--white-10);
|
||||
border-radius: var(--radius-sm);
|
||||
font-family: var(--font-mono);
|
||||
letter-spacing: var(--tracking-widest);
|
||||
word-break: break-all;
|
||||
}
|
||||
.hanzo-id-mfa-recovery { font-size: 13px; color: var(--muted); line-height: 1.6; }
|
||||
.hanzo-id-mfa-recovery code { letter-spacing: normal; }
|
||||
.hanzo-id-mfa-recovery { font-size: var(--text-sm); color: var(--muted-foreground); line-height: var(--leading-relaxed); }
|
||||
.hanzo-id-mfa-recovery code { letter-spacing: var(--tracking-normal); }
|
||||
|
||||
/* ── Social / Web3 sign-in ─────────────────────────────────────────── */
|
||||
/* These are `.hanzo-id-btn.ghost` — there is no separate social-button surface. */
|
||||
|
||||
/* ── Social / Web3 sign-in buttons ─────────────────────────────── */
|
||||
.hanzo-id-social { display: flex; flex-direction: column; gap: 10px; }
|
||||
.hanzo-id-social-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
background: #111;
|
||||
color: var(--fg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 11px 14px;
|
||||
/* Flex-centered above, so min-height keeps the label centered at
|
||||
the >=44px touch target. */
|
||||
min-height: 44px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
.hanzo-id-social-btn:hover { border-color: #3a3a3a; background: #161616; }
|
||||
.hanzo-id-social-btn:disabled { opacity: 0.6; cursor: default; }
|
||||
.hanzo-id-social-btn svg { flex: none; }
|
||||
|
||||
/* Wallet chain chooser — revealed under the single "Connect Wallet" button when
|
||||
the injected chain is ambiguous. Indented so the EVM/Solana options read as
|
||||
children of the wallet entry, same monochrome buttons. */
|
||||
children of the wallet entry. */
|
||||
.hanzo-id-wallet-chains {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-left: 4px;
|
||||
padding-left: 12px;
|
||||
border-left: 1px solid var(--border);
|
||||
border-left: 1px solid var(--white-10);
|
||||
}
|
||||
|
||||
/* Labeled divider between social row and email form. */
|
||||
@@ -207,70 +333,46 @@ form input:focus { outline: 2px solid var(--brand); outline-offset: -1px; }
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
color: var(--muted-foreground);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
.hanzo-id-divider::before,
|
||||
.hanzo-id-divider::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: var(--border);
|
||||
background: var(--white-10);
|
||||
}
|
||||
.hanzo-id-divider span { padding: 0 12px; }
|
||||
|
||||
/* ── Onboarding flow ───────────────────────────────────────────── */
|
||||
/* ── Onboarding flow ───────────────────────────────────────────────── */
|
||||
|
||||
.hanzo-id-onboarding { display: flex; flex-direction: column; gap: 24px; }
|
||||
.hanzo-id-onboarding-head { display: flex; flex-direction: column; gap: 6px; }
|
||||
.hanzo-id-onboarding-head h1 { margin: 0; font-size: 26px; }
|
||||
.hanzo-id-onboarding-head h1,
|
||||
.hanzo-id-onboarding-done h1 { margin: 0; font-size: var(--text-2xl); letter-spacing: var(--tracking-tight); }
|
||||
.hanzo-id-onboarding-body { display: flex; flex-direction: column; gap: 16px; }
|
||||
.hanzo-id-onboarding-done { display: flex; flex-direction: column; gap: 16px; }
|
||||
.hanzo-id-onboarding-done h1 { margin: 0; font-size: 26px; }
|
||||
|
||||
.hanzo-id-stepdots { display: flex; gap: 8px; }
|
||||
.hanzo-id-stepdots span {
|
||||
flex: 1;
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
background: var(--border);
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--white-10);
|
||||
}
|
||||
.hanzo-id-stepdots span.on { background: var(--brand); }
|
||||
.hanzo-id-stepdots span.on { background: var(--primary); }
|
||||
|
||||
.hanzo-id-org-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
|
||||
.hanzo-id-org-row {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #111;
|
||||
color: var(--fg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 14px 16px;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
.hanzo-id-slug-preview {
|
||||
color: var(--muted-foreground);
|
||||
font-size: var(--text-sm);
|
||||
margin: -8px 0 0;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
.hanzo-id-org-row:hover { border-color: #3a3a3a; background: #161616; }
|
||||
.hanzo-id-org-slug { color: var(--muted); font-size: 13px; font-family: ui-monospace, monospace; }
|
||||
|
||||
.hanzo-id-linkbtn {
|
||||
background: none;
|
||||
border: 0;
|
||||
color: var(--fg);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
padding: 4px 0;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.hanzo-id-slug-preview { color: var(--muted); font-size: 13px; margin: -8px 0 0; font-family: ui-monospace, monospace; }
|
||||
|
||||
.hanzo-id-onboarding-actions { display: flex; gap: 10px; flex-wrap: wrap; }
|
||||
.hanzo-id-onboarding-actions .hanzo-id-btn { flex: 1; min-width: 120px; }
|
||||
.hanzo-id-btn.ghost { background: transparent; color: var(--fg); border: 1px solid var(--border); }
|
||||
.hanzo-id-btn.ghost:hover { border-color: #3a3a3a; }
|
||||
|
||||
.hanzo-id-summary { display: grid; grid-template-columns: auto 1fr; gap: 8px 16px; margin: 0; }
|
||||
.hanzo-id-summary dt { color: var(--muted); font-size: 14px; }
|
||||
.hanzo-id-summary dd { margin: 0; font-size: 14px; font-family: ui-monospace, monospace; }
|
||||
.hanzo-id-summary dt { color: var(--muted-foreground); font-size: var(--text-sm); }
|
||||
.hanzo-id-summary dd { margin: 0; font-size: var(--text-sm); font-family: var(--font-mono); }
|
||||
|
||||
@@ -156,7 +156,7 @@ export function DeviceApproval({ client, brand }: { client: AuthClient; brand: B
|
||||
ONLY if this code matches the one shown on that device.
|
||||
</p>
|
||||
|
||||
<label className="hanzo-id-device-code-field">
|
||||
<label className="hanzo-id-field">
|
||||
<span>Device code</span>
|
||||
<input
|
||||
type="text"
|
||||
@@ -166,7 +166,7 @@ export function DeviceApproval({ client, brand }: { client: AuthClient; brand: B
|
||||
spellCheck={false}
|
||||
autoComplete="one-time-code"
|
||||
aria-label="Device code"
|
||||
className="hanzo-id-device-code"
|
||||
className="hanzo-id-input hanzo-id-device-code"
|
||||
value={userCode}
|
||||
onChange={(e) => setUserCode(e.target.value)}
|
||||
placeholder="e.g. K7M4P2QH"
|
||||
@@ -183,6 +183,7 @@ export function DeviceApproval({ client, brand }: { client: AuthClient; brand: B
|
||||
|
||||
<label className="hanzo-id-device-confirm">
|
||||
<input
|
||||
className="hanzo-id-check"
|
||||
type="checkbox"
|
||||
checked={confirmed}
|
||||
onChange={(e) => setConfirmed(e.target.checked)}
|
||||
@@ -198,7 +199,7 @@ export function DeviceApproval({ client, brand }: { client: AuthClient; brand: B
|
||||
<div className="hanzo-id-cta-row">
|
||||
<button
|
||||
type="button"
|
||||
className="hanzo-id-btn primary"
|
||||
className="hanzo-id-btn"
|
||||
disabled={busy || userCode.trim().length === 0 || !confirmed}
|
||||
onClick={approve}
|
||||
>
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@hanzo/id",
|
||||
"private": true,
|
||||
"version": "0.2.11",
|
||||
"version": "0.2.12",
|
||||
"description": "Hanzo ID \u2014 white-label login + identity verification portal (Vite + @hanzo/gui)",
|
||||
"scripts": {
|
||||
"build": "pnpm -r build",
|
||||
|
||||
@@ -40,13 +40,13 @@ export function ForgotForm(props: ForgotFormProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmit} className="hanzo-id-forgot-form" aria-busy={busy}>
|
||||
<label>
|
||||
<form onSubmit={onSubmit} className="hanzo-id-form" aria-busy={busy}>
|
||||
<label className="hanzo-id-field">
|
||||
<span>Email</span>
|
||||
<input type="email" autoComplete="email" value={identifier} onChange={(e) => setIdentifier(e.target.value)} required />
|
||||
<input className="hanzo-id-input" type="email" autoComplete="email" value={identifier} onChange={(e) => setIdentifier(e.target.value)} required />
|
||||
</label>
|
||||
{error ? <p role="alert" className="hanzo-id-error">{error}</p> : null}
|
||||
<button type="submit" disabled={busy}>{busy ? 'Sending…' : 'Send reset link'}</button>
|
||||
<button type="submit" className="hanzo-id-btn" disabled={busy}>{busy ? 'Sending…' : 'Send reset link'}</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -99,10 +99,11 @@ export function LoginForm(props: LoginFormProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmit} className="hanzo-id-login-form" aria-busy={busy}>
|
||||
<label>
|
||||
<form onSubmit={onSubmit} className="hanzo-id-form" aria-busy={busy}>
|
||||
<label className="hanzo-id-field">
|
||||
<span>Email or username</span>
|
||||
<input
|
||||
className="hanzo-id-input"
|
||||
type="text"
|
||||
autoComplete="username"
|
||||
value={identifier}
|
||||
@@ -110,9 +111,10 @@ export function LoginForm(props: LoginFormProps) {
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<label className="hanzo-id-field">
|
||||
<span>Password</span>
|
||||
<input
|
||||
className="hanzo-id-input"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
value={password}
|
||||
@@ -121,7 +123,7 @@ export function LoginForm(props: LoginFormProps) {
|
||||
/>
|
||||
</label>
|
||||
{error ? <p role="alert" className="hanzo-id-error">{error}</p> : null}
|
||||
<button type="submit" disabled={busy}>{busy ? 'Signing in…' : 'Sign in'}</button>
|
||||
<button type="submit" className="hanzo-id-btn" disabled={busy}>{busy ? 'Signing in…' : 'Sign in'}</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -26,10 +26,11 @@ export function OTPForm(props: OTPFormProps) {
|
||||
const label = channel === 'sms' ? 'SMS code' : channel === 'email' ? 'Email code' : 'Authenticator code'
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmit} className="hanzo-id-otp-form" aria-busy={busy}>
|
||||
<label>
|
||||
<form onSubmit={onSubmit} className="hanzo-id-form" aria-busy={busy}>
|
||||
<label className="hanzo-id-field">
|
||||
<span>{label}</span>
|
||||
<input
|
||||
className="hanzo-id-input"
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern={`\\d{${length}}`}
|
||||
@@ -41,7 +42,7 @@ export function OTPForm(props: OTPFormProps) {
|
||||
/>
|
||||
</label>
|
||||
{channel === 'sms' ? <SmsConsentNotice /> : null}
|
||||
<button type="submit" disabled={busy || code.length !== length}>{busy ? 'Verifying…' : 'Verify'}</button>
|
||||
<button type="submit" className="hanzo-id-btn" disabled={busy || code.length !== length}>{busy ? 'Verifying…' : 'Verify'}</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -38,14 +38,15 @@ export function SignupForm(props: SignupFormProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmit} className="hanzo-id-signup-form" aria-busy={busy}>
|
||||
<label>
|
||||
<form onSubmit={onSubmit} className="hanzo-id-form" aria-busy={busy}>
|
||||
<label className="hanzo-id-field">
|
||||
<span>Email</span>
|
||||
<input type="email" autoComplete="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
|
||||
<input className="hanzo-id-input" type="email" autoComplete="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
|
||||
</label>
|
||||
<label>
|
||||
<label className="hanzo-id-field">
|
||||
<span>Password</span>
|
||||
<input
|
||||
className="hanzo-id-input"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
value={password}
|
||||
@@ -55,7 +56,7 @@ export function SignupForm(props: SignupFormProps) {
|
||||
/>
|
||||
</label>
|
||||
{error ? <p role="alert" className="hanzo-id-error">{error}</p> : null}
|
||||
<button type="submit" disabled={busy}>{busy ? 'Creating account…' : 'Create account'}</button>
|
||||
<button type="submit" className="hanzo-id-btn" disabled={busy}>{busy ? 'Creating account…' : 'Create account'}</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -267,7 +267,7 @@ export function SocialButtons({
|
||||
<Fragment key="web3">
|
||||
<button
|
||||
type="button"
|
||||
className="hanzo-id-social-btn"
|
||||
className="hanzo-id-btn ghost"
|
||||
data-provider="web3"
|
||||
data-wallet-connect="true"
|
||||
aria-expanded={walletMenu}
|
||||
@@ -287,7 +287,7 @@ export function SocialButtons({
|
||||
<button
|
||||
key={`web3-${chain}`}
|
||||
type="button"
|
||||
className="hanzo-id-social-btn hanzo-id-wallet-chain"
|
||||
className="hanzo-id-btn ghost"
|
||||
data-provider="web3"
|
||||
data-chain={chain}
|
||||
disabled={busyChain !== null}
|
||||
@@ -309,7 +309,7 @@ export function SocialButtons({
|
||||
<button
|
||||
key={k}
|
||||
type="button"
|
||||
className="hanzo-id-social-btn"
|
||||
className="hanzo-id-btn ghost"
|
||||
data-provider={k}
|
||||
onClick={() => startOAuth(provider)}
|
||||
>
|
||||
|
||||
@@ -159,10 +159,11 @@ function OrgStep({
|
||||
// by invitation, handled outside this flow.
|
||||
return (
|
||||
<div className="hanzo-id-onboarding-body">
|
||||
<form onSubmit={create} aria-busy={busy}>
|
||||
<label>
|
||||
<form onSubmit={create} className="hanzo-id-form" aria-busy={busy}>
|
||||
<label className="hanzo-id-field">
|
||||
<span>Organization name</span>
|
||||
<input
|
||||
className="hanzo-id-input"
|
||||
type="text"
|
||||
value={displayName}
|
||||
onChange={(e) => setDisplayName(e.target.value)}
|
||||
@@ -177,7 +178,7 @@ function OrgStep({
|
||||
<button type="button" className="hanzo-id-btn ghost" onClick={() => onNext({})} disabled={busy}>
|
||||
Skip for now
|
||||
</button>
|
||||
<button type="submit" className="hanzo-id-btn primary" disabled={busy}>
|
||||
<button type="submit" className="hanzo-id-btn" disabled={busy}>
|
||||
{busy ? 'Creating…' : 'Create organization'}
|
||||
</button>
|
||||
</div>
|
||||
@@ -236,7 +237,7 @@ function ProjectStep({
|
||||
Back
|
||||
</button>
|
||||
) : null}
|
||||
<button type="button" className="hanzo-id-btn primary" onClick={() => onNext({})}>
|
||||
<button type="button" className="hanzo-id-btn" onClick={() => onNext({})}>
|
||||
Continue
|
||||
</button>
|
||||
</div>
|
||||
@@ -246,10 +247,11 @@ function ProjectStep({
|
||||
|
||||
return (
|
||||
<div className="hanzo-id-onboarding-body">
|
||||
<form onSubmit={create} aria-busy={busy}>
|
||||
<label>
|
||||
<form onSubmit={create} className="hanzo-id-form" aria-busy={busy}>
|
||||
<label className="hanzo-id-field">
|
||||
<span>Project name</span>
|
||||
<input
|
||||
className="hanzo-id-input"
|
||||
type="text"
|
||||
value={displayName}
|
||||
onChange={(e) => setDisplayName(e.target.value)}
|
||||
@@ -268,7 +270,7 @@ function ProjectStep({
|
||||
<button type="button" className="hanzo-id-btn ghost" onClick={() => onNext({})} disabled={busy}>
|
||||
Skip
|
||||
</button>
|
||||
<button type="submit" className="hanzo-id-btn primary" disabled={busy}>
|
||||
<button type="submit" className="hanzo-id-btn" disabled={busy}>
|
||||
{busy ? 'Creating…' : 'Create project'}
|
||||
</button>
|
||||
</div>
|
||||
@@ -334,7 +336,7 @@ function WalletStep({
|
||||
Skip
|
||||
</button>
|
||||
{connectWallet ? (
|
||||
<button type="button" className="hanzo-id-btn primary" onClick={link} disabled={busy}>
|
||||
<button type="button" className="hanzo-id-btn" onClick={link} disabled={busy}>
|
||||
{busy ? 'Connecting…' : 'Connect wallet'}
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
Generated
+174
-6384
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user