Set the app in Zen on hanzo.ai's palette, with pill buttons

The theme takes @hanzo/ui DESIGN.md's values: a true-black page, #0a0a0a
surfaces, #171717 raised, #ededf1 ink and white-alpha hairlines in dark;
white, #0a0a0a ink and black-alpha hairlines in light. Each is an opaque
triplet, so Tailwind's alpha modifiers still apply.

Type is Zen and Zen Mono from @hanzo/font, at Zen's own weights. Buttons
are pills, the primary in the ink colour; the option cards built from a
Button keep a card's corners, and the folder actions wrap on a phone
rather than breaking a pill over two lines.
This commit is contained in:
2026-10-20 12:47:20 -07:00
parent d8aa5b641e
commit 597cfae467
9 changed files with 83 additions and 70 deletions
+7 -19
View File
@@ -1,23 +1,7 @@
/* Zen and Zen Mono, the Hanzo family: @hanzo/font carries the faces and the @font-face. */
@import '@hanzo/font/css';
@import '@hanzo/esign-ui/styles/theme.css';
/* Inter Variable Fonts */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-variablefont_opsz,wght.ttf') format('truetype-variations');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
/* Inter Italic Variable Fonts */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-italic-variablefont_opsz,wght.ttf') format('truetype-variations');
font-weight: 100 900;
font-style: italic;
font-display: swap;
}
/* Caveat Variable Font */
@font-face {
font-family: 'Caveat';
@@ -64,7 +48,11 @@
@layer base {
:root {
--font-sans: 'Inter';
--font-sans:
Zen, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
sans-serif;
--font-mono:
Zen Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace;
--font-signature: 'Caveat';
--font-noto: 'Noto Sans', 'Noto Sans Korean', 'Noto Sans Japanese', 'Noto Sans Chinese';
}
@@ -10,11 +10,11 @@ import { useForm } from 'react-hook-form';
import { z } from 'zod';
import type { TRecipientAccessAuth } from '@hanzo/esign-lib/types/document-auth';
import { useZapMutation } from '@hanzo/esign-trpc/zap/react';
import type {
TAccessAuthRequest2FAEmailRequest,
TAccessAuthRequest2FAEmailResponse,
} from '@hanzo/esign-trpc/server/document-router/access-auth-request-2fa-email.types';
import { useZapMutation } from '@hanzo/esign-trpc/zap/react';
import { cn } from '@hanzo/esign-ui/lib/utils';
import { Alert } from '@hanzo/esign-ui/primitives/alert';
import { Button } from '@hanzo/esign-ui/primitives/button';
@@ -157,7 +157,7 @@ export const AccessAuth2FAForm = ({ onSubmit, token, error }: AccessAuth2FAFormP
<h3 className="text-lg font-semibold">
<Trans>Choose verification method</Trans>
</h3>
<p className="text-muted-foreground text-sm">
<p className="text-sm text-muted-foreground">
<Trans>Please select how you'd like to receive your verification code.</Trans>
</p>
</div>
@@ -172,7 +172,7 @@ export const AccessAuth2FAForm = ({ onSubmit, token, error }: AccessAuth2FAFormP
<Button
type="button"
variant="outline"
className="flex h-auto w-full justify-start gap-3 p-4"
className="flex h-auto w-full justify-start gap-3 rounded-2xl p-4"
onClick={async () => onMethodSelect('email')}
disabled={isRequesting2FAEmail}
>
@@ -181,7 +181,7 @@ export const AccessAuth2FAForm = ({ onSubmit, token, error }: AccessAuth2FAFormP
<div className="font-medium">
<Trans>Email verification</Trans>
</div>
<div className="text-muted-foreground text-sm">
<div className="text-sm text-muted-foreground">
<Trans>We'll send a 6-digit code to your email</Trans>
</div>
</div>
@@ -191,7 +191,7 @@ export const AccessAuth2FAForm = ({ onSubmit, token, error }: AccessAuth2FAFormP
<Button
type="button"
variant="outline"
className="flex h-auto w-full justify-start gap-3 p-4"
className="flex h-auto w-full justify-start gap-3 rounded-2xl p-4"
onClick={async () => onMethodSelect('authenticator')}
disabled={isRequesting2FAEmail}
>
@@ -200,7 +200,7 @@ export const AccessAuth2FAForm = ({ onSubmit, token, error }: AccessAuth2FAFormP
<div className="font-medium">
<Trans>Authenticator app</Trans>
</div>
<div className="text-muted-foreground text-sm">
<div className="text-sm text-muted-foreground">
<Trans>Use your authenticator app to generate a code</Trans>
</div>
</div>
@@ -222,7 +222,7 @@ export const AccessAuth2FAForm = ({ onSubmit, token, error }: AccessAuth2FAFormP
</h3>
</div>
<div className="text-muted-foreground text-sm">
<div className="text-sm text-muted-foreground">
{selectedMethod === 'email' ? (
<Trans>
We've sent a 6-digit verification code to your email. Please enter it below to
@@ -268,7 +268,7 @@ export const AccessAuth2FAForm = ({ onSubmit, token, error }: AccessAuth2FAFormP
{expiresAt && millisecondsRemaining !== null && (
<div
className={cn('text-muted-foreground mt-2 text-center text-sm', {
className={cn('mt-2 text-center text-sm text-muted-foreground', {
'text-destructive': millisecondsRemaining <= 0,
})}
>
@@ -123,7 +123,7 @@ export const DocumentSigningAuthDialog = ({
{/* Show chooser if no auth type is selected and there are multiple options */}
{!selectedAuthType && validAuthTypes.length > 1 && (
<div className="space-y-4">
<p className="text-muted-foreground text-sm">
<p className="text-sm text-muted-foreground">
<Trans>Choose your preferred authentication method:</Trans>
</p>
<div className="grid gap-2">
@@ -132,7 +132,7 @@ export const DocumentSigningAuthDialog = ({
key={authType}
type="button"
variant="outline"
className="h-auto justify-start p-4"
className="h-auto justify-start rounded-2xl p-4"
onClick={() => setSelectedAuthType(authType)}
>
<div className="text-left">
@@ -145,7 +145,7 @@ export const DocumentSigningAuthDialog = ({
.exhaustive()}
</div>
<div className="text-muted-foreground text-sm">
<div className="text-sm text-muted-foreground">
{match(authType)
.with(DocumentAuth.ACCOUNT, () => <Trans>Sign in to your account</Trans>)
.with(DocumentAuth.PASSKEY, () => (
@@ -100,7 +100,7 @@ export const FolderGrid = ({ type, parentId }: FolderGridProps) => {
)}
</div>
<div className="flex gap-4 sm:flex-row sm:justify-end">
<div className="flex flex-wrap gap-4 sm:flex-row sm:justify-end">
<EnvelopeUploadButton type={type} folderId={parentId || undefined} />
<EnvelopeImportMenu type={type} folderId={parentId || undefined} />
+1
View File
@@ -22,6 +22,7 @@
"@hanzo/esign-tailwind-config": "*",
"@hanzo/esign-trpc": "*",
"@hanzo/esign-ui": "*",
"@hanzo/font": "1.9.3",
"@zap-proto/zap": "^1.6.0",
"@zap-proto/web": "^1.0.0",
"ws": "^8.21.3",
+10
View File
@@ -123,6 +123,7 @@
"@hanzo/esign-tailwind-config": "*",
"@hanzo/esign-trpc": "*",
"@hanzo/esign-ui": "*",
"@hanzo/font": "1.9.3",
"@hanzo/insights": "1.358.2",
"@hanzo/insights-node": "5.26.2",
"@hono/node-server": "^2.1.1",
@@ -3552,6 +3553,15 @@
"resolved": "packages/ui",
"link": true
},
"node_modules/@hanzo/font": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/@hanzo/font/-/font-1.9.3.tgz",
"integrity": "sha512-PVCyIFMp/wBVfLsWczVWC3VVxbUzlTDwc/LBRgyT6Fizk09aACq8ByRmbJp0FGKVd1BbUDeekvfv0g1eoj4OvQ==",
"license": "SIL OPEN FONT LICENSE",
"peerDependencies": {
"next": ">=13.2.0"
}
},
"node_modules/@hanzo/insights": {
"version": "1.358.2",
"resolved": "https://registry.npmjs.org/@hanzo/insights/-/insights-1.358.2.tgz",
+8
View File
@@ -10,9 +10,17 @@ module.exports = {
extend: {
fontFamily: {
sans: ['var(--font-sans)', ...fontFamily.sans],
mono: ['var(--font-mono)', ...fontFamily.mono],
signature: ['var(--font-signature)'],
noto: ['var(--font-noto)'],
},
// Zen is variable; these are where it reads on a screen (@hanzo/ui DESIGN.md §1).
fontWeight: {
normal: '497',
medium: '606',
semibold: '680',
bold: '760',
},
zIndex: {
9999: '9999',
},
+5 -5
View File
@@ -7,8 +7,9 @@ import { Loader } from 'lucide-react';
import { cn } from '../lib/utils';
// Pills, as on hanzo.ai: the primary is the ink colour (white on black, black on white).
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background',
'inline-flex items-center justify-center rounded-full text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background',
{
variants: {
variant: {
@@ -23,8 +24,8 @@ const buttonVariants = cva(
},
size: {
default: 'h-10 py-2 px-4',
sm: 'h-9 px-3 rounded-md',
lg: 'h-11 px-8 rounded-md',
sm: 'h-9 px-3',
lg: 'h-11 px-8',
},
},
defaultVariants: {
@@ -48,8 +49,7 @@ const loaderVariants = cva('mr-2 animate-spin', {
});
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
asChild?: boolean;
/**
+40 -34
View File
@@ -3,45 +3,48 @@
@tailwind utilities;
@layer base {
/* Light: hanzo.ai's light palette (@hanzo/tokens `light`). A white page, #0a0a0a ink, and
hairlines that are DESIGN.md's black-alpha rungs composited on white: border 10%, a
control's edge 15%, hover 5%. Every value is opaque so Tailwind's `/alpha` modifiers apply. */
:root,
.dark-mode-disabled {
--background: 0 0% 100%;
--foreground: 0 0% 11%;
--foreground: 0 0% 4%;
--muted: 0 0% 96%;
--muted-foreground: 0 0% 45%;
--muted: 0 0% 95%;
--muted-foreground: 0 0% 32%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 11%;
--popover-foreground: 0 0% 4%;
--card: 0 0% 100%;
--card-border: 0 0% 91%;
--card-border: 0 0% 90%;
--card-border-tint: 115 115 115;
--card-foreground: 0 0% 11%;
--card-foreground: 0 0% 4%;
--field-card: 0 0% 96%;
--field-card-border: 0 0% 85%;
--field-card-foreground: 0 0% 11%;
--field-card-foreground: 0 0% 4%;
--widget: 0 0% 97%;
--widget-foreground: 0 0% 95%;
--border: 0 0% 91%;
--input: 0 0% 91%;
--border: 0 0% 90%;
--input: 0 0% 85%;
--primary: 0 0% 0%;
--primary-foreground: 0 0% 100%;
--primary: 0 0% 4%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96%;
--secondary-foreground: 0 0% 11%;
--secondary: 0 0% 95%;
--secondary-foreground: 0 0% 4%;
--accent: 0 0% 96%;
--accent-foreground: 0 0% 11%;
--accent: 0 0% 95%;
--accent-foreground: 0 0% 4%;
--destructive: 0 72% 51%;
--destructive-foreground: 0 0% 98%;
--ring: 0 0% 45%;
--ring: 0 0% 50%;
--radius: 0.5rem;
@@ -58,43 +61,45 @@
--envelope-editor-background: 0 0% 98%;
}
/* Black page. Depth is the ramp above it: surface 6-7%, hover 16%, edge 18%. No hue at any step. */
/* Dark, the house theme: @hanzo/ui DESIGN.md §4. A true-black page, #0a0a0a
surfaces, #171717 for hover and anything raised, and white-alpha hairlines composited
on black (border 10%, a control's edge 15%). Ink is #ededf1, never pure white on black. */
.dark:not(.dark-mode-disabled) {
--background: 0 0% 0%;
--foreground: 0 0% 97%;
--foreground: 240 11% 94%;
--muted: 0 0% 10%;
--muted-foreground: 0 0% 65%;
--muted: 0 0% 9%;
--muted-foreground: 240 5% 65%;
--popover: 0 0% 7%;
--popover-foreground: 0 0% 90%;
--popover: 0 0% 4%;
--popover-foreground: 240 11% 94%;
--card: 0 0% 6%;
--card-border: 0 0% 18%;
--card: 0 0% 4%;
--card-border: 0 0% 10%;
--card-border-tint: 115 115 115;
--card-foreground: 0 0% 95%;
--card-foreground: 240 11% 94%;
--widget: 0 0% 6%;
--widget-foreground: 0 0% 18%;
--widget: 0 0% 4%;
--widget-foreground: 0 0% 10%;
--border: 0 0% 18%;
--input: 0 0% 18%;
--border: 0 0% 10%;
--input: 0 0% 15%;
--field-border: 0 0% 91%;
--primary: 0 0% 100%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 0%;
--secondary: 0 0% 12%;
--secondary: 0 0% 9%;
--secondary-foreground: 0 0% 98%;
--accent: 0 0% 16%;
--accent: 0 0% 9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 87% 62%;
--destructive-foreground: 0 87% 19%;
--ring: 0 0% 45%;
--ring: 0 0% 40%;
--radius: 0.5rem;
@@ -109,8 +114,9 @@
@apply border-border;
}
/* Zen's book weight is 497, not the generic 400 (@hanzo/ui DESIGN.md §1). */
body {
@apply bg-background text-foreground;
@apply bg-background font-normal text-foreground;
font-feature-settings:
'rlig' 1,
'calt' 1;