Show the brand as the Hanzo eSign wordmark alone, under hanzo.ai's bar

The wordmark was an SVG that guessed its own width, and clipped the name
to "Hanzo Sigı". It is now text: NEXT_PUBLIC_APP_NAME, "Hanzo eSign" by
default, measured by the browser and sized by the caller, with no mark in
front of it anywhere. The mobile menu and the public profile header drew
the upstream Documenso logo; the emails and the sealed PDF's certificate
and audit log drew it too, from static/logo.png, which is now the
wordmark set in Zen.

The app header is hanzo.ai's: the wordmark at the left, quiet nav, pill
controls at the right, a hairline under it. A visitor without a session
sees the same bar with the wordmark alone, and a footer with the
copyright and hanzo.ai's terms and privacy policy, on sign-in, the other
unauthenticated pages, a recipient's completion and result pages and
public profiles. The signing pages keep their own bar, which now shows
the wordmark at every width.
This commit is contained in:
2026-10-20 12:47:20 -07:00
parent 597cfae467
commit 9d66337dec
29 changed files with 184 additions and 312 deletions
+6 -14
View File
@@ -1,22 +1,14 @@
import { env } from '@hanzo/esign-lib/utils/env';
/**
* The tenant's wordmark, read at runtime so one image serves every tenant.
* The tenant's wordmark and company, read at runtime so one image serves every tenant.
*
* NEXT_PUBLIC_APP_NAME whole wordmark, e.g. "Hanzo Sign" | "Acme Sign"
* NEXT_PUBLIC_APP_NAME_PRIMARY first word (defaults to the first word of APP_NAME)
* NEXT_PUBLIC_APP_NAME_SUFFIX the rest (defaults to the rest of APP_NAME)
* NEXT_PUBLIC_APP_NAME the wordmark, e.g. "Hanzo eSign" | "Acme Sign"
* NEXT_PUBLIC_COMPANY_NAME the holder of the copyright line, e.g. "Hanzo AI, Inc."
*
* `env` reads `window.__ENV__` in the browser, so these are deployment values,
* never baked into the bundle. This is the one place the wordmark is resolved.
* never baked into the bundle. This is the one place they are resolved.
*/
export const brand = (): { name: string; primary: string; suffix: string } => {
const name = env('NEXT_PUBLIC_APP_NAME') || 'Hanzo Sign';
const [firstWord, ...rest] = name.split(' ');
export const appName = () => env('NEXT_PUBLIC_APP_NAME') || 'Hanzo eSign';
return {
name,
primary: env('NEXT_PUBLIC_APP_NAME_PRIMARY') || firstWord || name,
suffix: env('NEXT_PUBLIC_APP_NAME_SUFFIX') || rest.join(' '),
};
};
export const companyName = () => env('NEXT_PUBLIC_COMPANY_NAME') || 'Hanzo AI, Inc.';
@@ -1,54 +0,0 @@
// Canonical Hanzo mark — single-source SVG, mono via currentColor so it
// adopts whatever text color its parent sets. Same vector shipped in
// @hanzo/logo's docs/assets/hanzo-logo.svg, repainted to use
// currentColor instead of #000/#666 so it composes cleanly into any
// theme.
//
// White-label rule: this component renders the MARK only. The wordmark
// (brand name + suffix) is rendered separately by the consumer using
// NEXT_PUBLIC_APP_NAME so tenants can drop in their own brand without
// forking the component.
import type { SVGProps } from 'react';
import { cn } from '@hanzo/esign-ui/lib/utils';
export type HanzoMarkProps = SVGProps<SVGSVGElement> & {
size?: number;
};
/**
* The mark's geometry on a 67x67 grid, painted in `currentColor`.
*
* Exported so every lockup draws the same vector — the icon, the wordmark and
* this component share it rather than each carrying a copy that can drift.
*/
export const MarkPaths = () => (
<>
<path d="M22.21 67V44.6369H0V67H22.21Z" fill="currentColor" />
<path d="M0 44.6369L22.21 46.8285V44.6369H0Z" fill="currentColor" opacity="0.4" />
<path
d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z"
fill="currentColor"
/>
<path d="M22.21 0H0V22.3184H22.21V0Z" fill="currentColor" />
<path d="M66.7198 0H44.5098V22.3184H66.7198V0Z" fill="currentColor" />
<path d="M66.6753 22.3185L44.5098 20.0822V22.3185H66.6753Z" fill="currentColor" opacity="0.4" />
<path d="M66.7198 67V44.6369H44.5098V67H66.7198Z" fill="currentColor" />
</>
);
export const HanzoMark = ({ size = 24, className, ...props }: HanzoMarkProps) => {
return (
<svg
viewBox="0 0 67 67"
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
aria-label="Hanzo"
className={cn('text-current', className)}
{...props}
>
<MarkPaths />
</svg>
);
};
@@ -14,7 +14,7 @@ export const EmbedAuthenticationRequired = ({ returnTo }: EmbedAuthenticationReq
return (
<div className="flex min-h-[100dvh] w-full items-center justify-center">
<div className="flex w-full max-w-md flex-col">
<BrandingLogo className="h-8" />
<BrandingLogo className="text-2xl" />
<Alert className="mt-8" variant="warning">
<AlertDescription>
@@ -20,6 +20,7 @@ import { useThrottleFn } from '@hanzo/esign-lib/client-only/hooks/use-throttle-f
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@hanzo/esign-lib/constants/date-formats';
import { PDF_VIEWER_PAGE_SELECTOR } from '@hanzo/esign-lib/constants/pdf-viewer';
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@hanzo/esign-lib/constants/time-zones';
import type { TCreateDocumentFromDirectTemplateResponse } from '@hanzo/esign-lib/server-only/template/create-document-from-direct-template';
import { ZDirectTemplateEmbedDataSchema } from '@hanzo/esign-lib/types/embed-direct-template-schema';
import {
isFieldUnsignedAndRequired,
@@ -27,7 +28,6 @@ import {
} from '@hanzo/esign-lib/utils/advanced-fields-helpers';
import { getDocumentDataUrlForPdfViewer } from '@hanzo/esign-lib/utils/envelope-download';
import { sortFieldsByPosition, validateFieldsInserted } from '@hanzo/esign-lib/utils/fields';
import type { TCreateDocumentFromDirectTemplateResponse } from '@hanzo/esign-lib/server-only/template/create-document-from-direct-template';
import { isSignatureFieldType } from '@hanzo/esign-prisma/guards/is-signature-field';
import type {
TRemovedSignedFieldWithTokenMutationSchema,
@@ -512,7 +512,7 @@ export const EmbedDirectTemplateClientPage = ({
<span>
<Trans>Powered by</Trans>
</span>
<BrandingLogo className="ml-2 inline-block h-[14px]" />
<BrandingLogo className="ml-1" />
</div>
)}
</div>
@@ -523,7 +523,7 @@ export const EmbedSignDocumentV1ClientPage = ({
<span>
<Trans>Powered by</Trans>
</span>
<BrandingLogo className="ml-2 inline-block h-[14px]" />
<BrandingLogo className="ml-1" />
</div>
)}
</div>
+1 -6
View File
@@ -54,12 +54,7 @@ export const SignInForm = ({ className, returnTo }: SignInFormProps) => {
return (
<div className={cn('flex w-full flex-col gap-y-4', className)}>
<Button
type="button"
size="lg"
className="bg-white font-normal text-black hover:bg-zinc-200"
onClick={onSignInClick}
>
<Button type="button" size="lg" className="font-normal" onClick={onSignInClick}>
Sign in with <span className="ml-1 font-bold">{providerName}</span>
</Button>
</div>
@@ -0,0 +1,40 @@
import type { HTMLAttributes } from 'react';
import { Trans } from '@lingui/react/macro';
import { cn } from '@hanzo/esign-ui/lib/utils';
import { companyName } from '~/components/branding/brand';
import { BrandingLogo } from '~/components/general/branding-logo';
export type AppFooterProps = HTMLAttributes<HTMLElement>;
/**
* The foot of a public page, as hanzo.ai ends its own: the wordmark and the copyright on
* one side, the terms and the privacy policy on the other, under a hairline.
*/
export const AppFooter = ({ className, ...props }: AppFooterProps) => {
return (
<footer className={cn('border-t border-border', className)} {...props}>
<div className="mx-auto flex w-full max-w-screen-xl flex-col gap-y-3 px-4 py-8 text-[13px] text-muted-foreground sm:flex-row sm:items-center sm:justify-between md:px-8">
<div className="flex items-center gap-x-5">
<BrandingLogo className="text-[17px] text-foreground" />
<span>
© {new Date().getFullYear()} {companyName()}
</span>
</div>
<nav className="flex items-center gap-x-4">
<a href="https://hanzo.ai/terms" className="transition-colors hover:text-foreground">
<Trans>Terms</Trans>
</a>
<a href="https://hanzo.ai/privacy" className="transition-colors hover:text-foreground">
<Trans>Privacy</Trans>
</a>
</nav>
</div>
</footer>
);
};
@@ -1,4 +1,4 @@
import { type HTMLAttributes, useEffect, useState } from 'react';
import { type HTMLAttributes, useState } from 'react';
import { useLingui } from '@lingui/react/macro';
import { ReadStatus } from '@prisma/client';
@@ -23,6 +23,29 @@ import { OrgMenuSwitcher } from './org-menu-switcher';
export type HeaderProps = HTMLAttributes<HTMLDivElement>;
// hanzo.ai's bar: the wordmark at the left edge, a hairline under it, the page through a blur.
const HEADER_CLASS_NAME =
'supports-backdrop-blur:bg-background/60 sticky top-0 z-[60] flex h-16 w-full items-center border-b border-border bg-background/80 backdrop-blur';
const WORDMARK_LINK_CLASS_NAME =
'rounded-md ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2';
/**
* The bar a visitor without a session sees: the wordmark and nothing else, so a recipient
* or a sign-in reads as the same product as the app behind it.
*/
export const PublicHeader = ({ className, ...props }: HeaderProps) => {
return (
<header className={cn(HEADER_CLASS_NAME, className)} {...props}>
<div className="mx-auto flex w-full max-w-screen-xl items-center px-4 md:px-8">
<Link to="/" className={WORDMARK_LINK_CLASS_NAME}>
<BrandingLogo className="text-[19px]" />
</Link>
</div>
</header>
);
};
export const Header = ({ className, ...props }: HeaderProps) => {
const { t } = useLingui();
const params = useParams();
@@ -31,7 +54,6 @@ export const Header = ({ className, ...props }: HeaderProps) => {
const [isCommandMenuOpen, setIsCommandMenuOpen] = useState(false);
const [isHamburgerMenuOpen, setIsHamburgerMenuOpen] = useState(false);
const [scrollY, setScrollY] = useState(0);
const { data: unreadCountData } = useZapQuery<TGetInboxCountResponse>(
'document.inbox.getCount',
@@ -43,38 +65,18 @@ export const Header = ({ className, ...props }: HeaderProps) => {
},
);
useEffect(() => {
const onScroll = () => {
setScrollY(window.scrollY);
};
window.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll);
}, []);
return (
<header
className={cn(
'supports-backdrop-blur:bg-background/60 sticky top-0 z-[60] flex h-16 w-full items-center border-b border-b-transparent bg-background/95 backdrop-blur duration-200',
scrollY > 5 && 'border-b-border',
className,
)}
{...props}
>
<div className="mx-auto flex w-full max-w-screen-xl items-center justify-between gap-x-4 px-4 md:justify-normal md:px-8">
<Link
to={getRootHref(params)}
className="hidden rounded-md ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 md:inline"
>
<BrandingLogo className="h-6 w-auto" />
<header className={cn(HEADER_CLASS_NAME, className)} {...props}>
<div className="mx-auto flex w-full max-w-screen-xl items-center gap-x-4 px-4 md:px-8">
<Link to={getRootHref(params)} className={WORDMARK_LINK_CLASS_NAME}>
<BrandingLogo className="text-[19px]" />
</Link>
<AppNavDesktop setIsCommandMenuOpen={setIsCommandMenuOpen} />
<Button asChild variant="outline" className="relative hidden h-10 w-10 rounded-lg md:flex">
<Link to="/inbox" className="relative block h-10 w-10">
<InboxIcon className="h-5 w-5 flex-shrink-0 text-muted-foreground transition-colors hover:text-foreground" />
<Button asChild variant="outline" className="relative hidden h-9 w-9 md:flex">
<Link to="/inbox" aria-label={t`Inbox`} className="relative block h-9 w-9">
<InboxIcon className="h-4 w-4 flex-shrink-0 text-muted-foreground transition-colors hover:text-foreground" />
{unreadCountData && unreadCountData.count > 0 && (
<span className="absolute -right-1.5 -top-1.5 flex h-5 w-5 items-center justify-center rounded-full bg-primary text-[10px] font-semibold text-primary-foreground">
@@ -84,17 +86,17 @@ export const Header = ({ className, ...props }: HeaderProps) => {
</Link>
</Button>
<div className="md:ml-4">
<div className="ml-auto md:ml-2">
{isPersonalLayout(organisations) ? <MenuSwitcher /> : <OrgMenuSwitcher />}
</div>
<div className="flex flex-row items-center space-x-4 md:hidden">
<button aria-label={t`Search`} onClick={() => setIsCommandMenuOpen(true)}>
<SearchIcon className="h-6 w-6 text-muted-foreground" />
<SearchIcon className="h-5 w-5 text-muted-foreground" />
</button>
<button aria-label={t`Menu`} onClick={() => setIsHamburgerMenuOpen(true)}>
<MenuIcon className="h-6 w-6 text-muted-foreground" />
<MenuIcon className="h-5 w-5 text-muted-foreground" />
</button>
<AppCommandMenu open={isCommandMenuOpen} onOpenChange={setIsCommandMenuOpen} />
@@ -86,9 +86,9 @@ export const AppNavDesktop = ({
key={href}
to={href}
className={cn(
'text-muted-foreground dark:text-muted-foreground/60 focus-visible:ring-ring ring-offset-background rounded-md font-medium leading-5 hover:opacity-80 focus-visible:outline-none focus-visible:ring-2',
'rounded-full text-[13px] font-medium leading-5 text-muted-foreground ring-offset-background transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
{
'text-foreground dark:text-muted-foreground': pathname?.startsWith(href),
'text-foreground': pathname?.startsWith(href),
},
)}
>
@@ -102,16 +102,16 @@ export const AppNavDesktop = ({
<Button
variant="outline"
className="text-muted-foreground flex w-full max-w-96 items-center justify-between rounded-lg"
className="flex h-9 w-full max-w-80 items-center justify-between pl-3 pr-1.5 font-normal text-muted-foreground"
onClick={() => setIsCommandMenuOpen(true)}
>
<div className="flex items-center">
<Search className="mr-2 h-5 w-5" />
<div className="flex items-center text-[13px]">
<Search className="mr-2 h-4 w-4" />
<Trans>Search</Trans>
</div>
<div>
<div className="text-muted-foreground bg-muted flex items-center rounded-md px-1.5 py-0.5 text-xs tracking-wider">
<div className="flex items-center rounded-full bg-muted px-2 py-0.5 text-xs tracking-wider text-muted-foreground">
{modifierKey}+K
</div>
</div>
@@ -5,7 +5,6 @@ import { Trans } from '@lingui/react/macro';
import { ReadStatus } from '@prisma/client';
import { Link } from 'react-router';
import LogoImage from '@hanzo/esign-assets/logo.png';
import { authClient } from '@hanzo/esign-auth/client';
import { useSession } from '@hanzo/esign-lib/client-only/providers/session';
import { isPersonalLayout } from '@hanzo/esign-lib/utils/organisations';
@@ -14,6 +13,8 @@ import { useZapQuery } from '@hanzo/esign-trpc/zap/react';
import { Sheet, SheetContent } from '@hanzo/esign-ui/primitives/sheet';
import { ThemeSwitcher } from '@hanzo/esign-ui/primitives/theme-switcher';
import { companyName } from '~/components/branding/brand';
import { BrandingLogo } from '~/components/general/branding-logo';
import { useOptionalCurrentTeam } from '~/providers/team';
export type AppNavMobileProps = {
@@ -85,14 +86,8 @@ export const AppNavMobile = ({ isMenuOpen, onMenuOpenChange }: AppNavMobileProps
return (
<Sheet open={isMenuOpen} onOpenChange={onMenuOpenChange}>
<SheetContent className="flex w-full max-w-[350px] flex-col">
<Link to="/" onClick={handleMenuItemClick}>
<img
src={LogoImage}
alt="Hanzo Sign Logo"
className="dark:invert"
width={170}
height={25}
/>
<Link to="/" onClick={handleMenuItemClick} className="self-start">
<BrandingLogo className="text-[19px]" />
</Link>
<div className="mt-8 flex w-full flex-col items-start gap-y-4">
@@ -126,7 +121,7 @@ export const AppNavMobile = ({ isMenuOpen, onMenuOpenChange }: AppNavMobileProps
</div>
<p className="text-sm text-muted-foreground">
© {new Date().getFullYear()} Hanzo
© {new Date().getFullYear()} {companyName()}
<br />
<Trans>All rights reserved.</Trans>
</p>
@@ -1,15 +0,0 @@
import type { SVGAttributes } from 'react';
import { MarkPaths } from '~/components/branding/hanzo-mark';
export type LogoProps = SVGAttributes<SVGSVGElement>;
// The house mark on its own, mono via currentColor — the compact form of the
// wordmark for tight surfaces (the mobile header, embed strips).
export const BrandingLogoIcon = ({ ...props }: LogoProps) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 67 67" {...props}>
<MarkPaths />
</svg>
);
};
@@ -1,51 +1,24 @@
import type { SVGAttributes } from 'react';
import type { HTMLAttributes } from 'react';
import { brand } from '~/components/branding/brand';
import { MarkPaths } from '~/components/branding/hanzo-mark';
import { cn } from '@hanzo/esign-ui/lib/utils';
export type LogoProps = SVGAttributes<SVGSVGElement>;
import { appName } from '~/components/branding/brand';
// Mark plus wordmark, locked up as ONE svg so the pair scales as a unit
// wherever it mounts — the app header, the embed footers and the generated PDF
// audit trail all size it by height alone.
//
// The name comes from NEXT_PUBLIC_APP_NAME at runtime, so a tenant rebrands by
// setting env rather than forking this file. Per-org logo upload already exists
// for documents (team/org `brandingLogo`); extending it to the app shell is the
// next step and would replace the mark here.
export const BrandingLogo = ({ ...props }: LogoProps) => {
const { name, primary, suffix } = brand();
// The wordmark starts after the 67-wide mark plus a gap. Advance width is
// estimated per glyph (~0.55em at 44px) because SVG cannot measure text — the
// viewBox has to be right before layout, and `w-auto` callers size by height.
const textX = 86;
const width = textX + Math.ceil(name.length * 24);
export type BrandingLogoProps = HTMLAttributes<HTMLSpanElement>;
/**
* The wordmark, and nothing in front of it: the app's name set as text, as hanzo.ai sets
* its own. Text rather than an SVG so it is measured by the browser and never clipped,
* whatever the tenant's name (NEXT_PUBLIC_APP_NAME). It takes its colour from the parent
* and its size from the caller's text-* class.
*/
export const BrandingLogo = ({ className, ...props }: BrandingLogoProps) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox={`0 0 ${width} 67`}
aria-label={name}
<span
className={cn('whitespace-nowrap font-semibold tracking-[-0.02em]', className)}
{...props}
>
<MarkPaths />
{/* `dx` is the word space: SVG collapses a space written between tspans. */}
<text
x={textX}
y="48"
fontFamily="ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif"
fontSize="44"
fontWeight="600"
letterSpacing="-1.5"
>
<tspan className="fill-foreground">{primary}</tspan>
{suffix && (
<tspan className="fill-muted-foreground" dx="11">
{suffix}
</tspan>
)}
</text>
</svg>
{appName()}
</span>
);
};
@@ -125,7 +125,7 @@ export const DocumentSigningMobileWidget = () => {
<span>
<Trans>Powered by</Trans>
</span>
<BrandingLogo className="ml-2 inline-block h-[14px]" />
<BrandingLogo className="ml-1" />
</div>
)}
</div>
@@ -71,7 +71,7 @@ export const DocumentSigningPageViewV2 = () => {
}, [recipientFieldsRemaining, selectedAssistantRecipientFields, currentEnvelopeItem]);
return (
<div className="min-h-screen w-screen bg-gray-50 dark:bg-background">
<div className="min-h-screen w-screen bg-envelope-editor-background">
<SignFieldEmailDialog.Root />
<SignFieldTextDialog.Root />
<SignFieldNumberDialog.Root />
@@ -259,7 +259,7 @@ export const DocumentSigningPageViewV2 = () => {
<span>
<Trans>Powered by</Trans>
</span>
<BrandingLogo className="ml-2 inline-block h-[14px]" />
<BrandingLogo className="ml-1" />
</a>
)}
</div>
+3 -8
View File
@@ -2,28 +2,23 @@ import type { HTMLAttributes, ReactNode } from 'react';
import { cn } from '@hanzo/esign-ui/lib/utils';
import { BrandingLogoIcon } from '~/components/general/branding-logo-icon';
export type EmptyProps = Omit<HTMLAttributes<HTMLDivElement>, 'title'> & {
title?: ReactNode;
};
/**
* What a list draws when it holds nothing: the house mark, mono and quiet,
* above the reason it is empty. The mark is the vector the header draws, so a
* tenant's brand carries through.
* What a list draws when it holds nothing: the reason it is empty, set quietly in
* the space the list would fill.
*/
export const Empty = ({ title, children, className, ...props }: EmptyProps) => {
return (
<div
className={cn(
'flex h-60 flex-col items-center justify-center gap-y-4 text-muted-foreground',
'flex h-60 flex-col items-center justify-center text-muted-foreground',
className,
)}
{...props}
>
<BrandingLogoIcon className="h-10 w-10 text-muted-foreground/40" />
<div className="text-center">
{title && <h3 className="text-lg font-semibold text-foreground">{title}</h3>}
@@ -73,7 +73,7 @@ export default function EnvelopeEditorHeader() {
/>
) : (
<Link to="/">
<BrandingLogo className="h-6 w-auto" />
<BrandingLogo className="text-lg" />
</Link>
)}
<Separator orientation="vertical" className="h-6" />
@@ -19,7 +19,6 @@ import { EnvelopeDownloadDialog } from '~/components/dialogs/envelope-download-d
import { useEmbedSigningContext } from '~/components/embed/embed-signing-context';
import { BrandingLogo } from '~/components/general/branding-logo';
import { BrandingLogoIcon } from '../branding-logo-icon';
import { DocumentSigningRejectDialog } from '../document-signing/document-signing-reject-dialog';
import { useRequiredEnvelopeSigningContext } from '../document-signing/envelope-signing-provider';
import { EnvelopeSignerCompleteDialog } from './envelope-signing-complete-dialog';
@@ -31,9 +30,9 @@ export const EnvelopeSignerHeader = () => {
const isEmbedSigning = useEmbedSigningContext() !== null;
return (
<nav className="embed--DocumentWidgetHeader max-w-screen flex flex-row justify-between border-b border-border bg-background px-4 py-3 md:px-6">
<nav className="embed--DocumentWidgetHeader max-w-screen flex h-16 flex-row items-center justify-between border-b border-border bg-background px-4 md:px-6">
{/* Left side - Logo and title */}
<div className="flex min-w-0 flex-1 items-center space-x-2 md:w-auto md:flex-none">
<div className="flex min-w-0 flex-1 items-center space-x-3 md:w-auto md:flex-none">
{!isEmbedSigning && (
<Link to="/" className="flex-shrink-0">
{envelopeData.settings.brandingEnabled && envelopeData.settings.brandingLogo ? (
@@ -43,23 +42,20 @@ export const EnvelopeSignerHeader = () => {
className="h-6 w-auto"
/>
) : (
<>
<BrandingLogo className="hidden h-6 w-auto md:block" />
<BrandingLogoIcon className="h-6 w-auto md:hidden" />
</>
<BrandingLogo className="text-base md:text-[19px]" />
)}
</Link>
)}
{!isEmbedSigning && <Separator orientation="vertical" className="h-6" />}
<h1
title={envelope.title}
className="min-w-0 truncate text-base font-semibold text-foreground md:hidden"
className="min-w-0 truncate text-sm font-medium text-foreground md:hidden"
>
{envelope.title}
</h1>
{!isEmbedSigning && <Separator orientation="vertical" className="hidden h-6 md:block" />}
<div className="hidden items-center space-x-2 md:flex">
<h1 className="whitespace-nowrap text-sm font-medium text-foreground">
{envelope.title}
@@ -8,7 +8,6 @@ export type PageHeaderProps = {
/** Rendered at the trailing edge of the title row. */
children?: ReactNode;
className?: string;
descriptionClassName?: string;
};
/**
@@ -16,23 +15,13 @@ export type PageHeaderProps = {
* heading and the line explaining it are one unit, so they sit closer to each
* other than to anything else on the page.
*/
export const PageHeader = ({
title,
description,
children,
className,
descriptionClassName,
}: PageHeaderProps) => {
export const PageHeader = ({ title, description, children, className }: PageHeaderProps) => {
return (
<div className={cn('flex flex-row items-center justify-between gap-x-4', className)}>
<div className="space-y-1.5">
<h1 className="text-3xl font-semibold tracking-tight">{title}</h1>
{description && (
<p className={cn('text-balance text-sm text-muted-foreground', descriptionClassName)}>
{description}
</p>
)}
{description && <p className="text-balance text-sm text-muted-foreground">{description}</p>}
</div>
{children}
@@ -195,7 +195,7 @@ export default function AuditLog({ loaderData }: Route.ComponentProps) {
{!hidePoweredBy && (
<div className="my-8 flex-row-reverse">
<div className="flex items-end justify-end gap-x-4">
<BrandingLogo className="max-h-6 print:max-h-4" />
<BrandingLogo className="text-lg print:text-sm" />
</div>
</div>
)}
@@ -404,7 +404,7 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
<p className="flex-shrink-0 text-sm font-medium print:text-xs">
{_(msg`Signing certificate provided by`)}:
</p>
<BrandingLogo className="max-h-6 print:max-h-4" />
<BrandingLogo className="text-lg print:text-sm" />
</div>
</div>
)}
+7 -47
View File
@@ -1,17 +1,13 @@
import { useEffect, useState } from 'react';
import { msg } from '@lingui/core/macro';
import { Trans } from '@lingui/react/macro';
import { ChevronLeft } from 'lucide-react';
import { Link, Outlet, isRouteErrorResponse } from 'react-router';
import LogoIcon from '@hanzo/esign-assets/logo_icon.png';
import { useOptionalSession } from '@hanzo/esign-lib/client-only/providers/session';
import { cn } from '@hanzo/esign-ui/lib/utils';
import { Button } from '@hanzo/esign-ui/primitives/button';
import { Header as AuthenticatedHeader } from '~/components/general/app-header';
import { BrandingLogo } from '~/components/general/branding-logo';
import { AppFooter } from '~/components/general/app-footer';
import { Header as AuthenticatedHeader, PublicHeader } from '~/components/general/app-header';
import { GenericErrorLayout } from '~/components/general/generic-error-layout';
import { appMetaTags } from '~/utils/meta';
@@ -24,51 +20,15 @@ export function meta() {
export default function PublicProfileLayout() {
const { sessionData } = useOptionalSession();
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const onScroll = () => {
setScrollY(window.scrollY);
};
window.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll);
}, []);
return (
<div className="min-h-screen">
{sessionData ? (
<AuthenticatedHeader />
) : (
<header
className={cn(
'supports-backdrop-blur:bg-background/60 sticky top-0 z-[60] flex h-16 w-full items-center border-b border-b-transparent bg-background/95 backdrop-blur duration-200',
scrollY > 5 && 'border-b-border',
)}
>
<div className="mx-auto flex w-full max-w-screen-xl items-center justify-between gap-x-4 px-4 md:px-8">
<Link
to="/"
className="rounded-md ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 md:inline"
>
<BrandingLogo className="hidden h-6 w-auto sm:block" />
<div className="flex min-h-screen flex-col">
{sessionData ? <AuthenticatedHeader /> : <PublicHeader />}
<img
src={LogoIcon}
alt="Hanzo Sign Logo"
width={48}
height={48}
className="h-10 w-auto sm:hidden dark:invert"
/>
</Link>
</div>
</header>
)}
<main className="my-8 px-4 md:my-12 md:px-8">
<main className="my-8 flex-1 px-4 md:my-12 md:px-8">
<Outlet />
</main>
<AppFooter />
</div>
);
}
+19 -11
View File
@@ -3,10 +3,10 @@ import { ChevronLeft } from 'lucide-react';
import { Link, Outlet, isRouteErrorResponse } from 'react-router';
import { useOptionalSession } from '@hanzo/esign-lib/client-only/providers/session';
import { cn } from '@hanzo/esign-ui/lib/utils';
import { Button } from '@hanzo/esign-ui/primitives/button';
import { Header as AuthenticatedHeader } from '~/components/general/app-header';
import { AppFooter } from '~/components/general/app-footer';
import { Header as AuthenticatedHeader, PublicHeader } from '~/components/general/app-header';
import { GenericErrorLayout } from '~/components/general/generic-error-layout';
import type { Route } from './+types/_layout';
@@ -20,24 +20,32 @@ import type { Route } from './+types/_layout';
export default function RecipientLayout({ matches }: Route.ComponentProps) {
const { sessionData } = useOptionalSession();
// Hide the header for signing routes.
// Hide the header and footer for signing routes: the signer draws its own bar.
const hideHeader = matches.some(
(match) =>
match?.id === 'routes/_recipient+/sign.$token+/_index' ||
match?.id === 'routes/_recipient+/d.$token+/_index',
);
return (
<div className="min-h-screen">
{!hideHeader && sessionData?.user && <AuthenticatedHeader />}
if (hideHeader) {
return (
<div className="min-h-screen">
<main>
<Outlet />
</main>
</div>
);
}
<main
className={cn({
'mb-8 mt-8 px-4 md:mb-12 md:mt-12 md:px-8': !hideHeader,
})}
>
return (
<div className="flex min-h-screen flex-col">
{sessionData?.user ? <AuthenticatedHeader /> : <PublicHeader />}
<main className="mb-8 mt-8 flex-1 px-4 md:mb-12 md:mt-12 md:px-8">
<Outlet />
</main>
<AppFooter />
</div>
);
}
@@ -1,9 +1,18 @@
import { Outlet } from 'react-router';
import { AppFooter } from '~/components/general/app-footer';
import { PublicHeader } from '~/components/general/app-header';
export default function Layout() {
return (
<main className="flex min-h-screen w-full flex-col bg-black text-white">
<Outlet />
</main>
<div className="flex min-h-screen w-full flex-col">
<PublicHeader />
<main className="flex w-full flex-1 flex-col items-center justify-center px-4 py-12 md:py-16">
<Outlet />
</main>
<AppFooter />
</div>
);
}
@@ -5,8 +5,6 @@ import { getOptionalSession } from '@hanzo/esign-auth/server/lib/utils/get-sessi
import { SignInRefusal } from '@hanzo/esign-lib/constants/auth';
import { isValidReturnTo, normalizeReturnTo } from '@hanzo/esign-lib/utils/is-valid-return-to';
import { brand } from '~/components/branding/brand';
import { HanzoMark } from '~/components/branding/hanzo-mark';
import { SignInForm } from '~/components/forms/signin';
import { PageHeader } from '~/components/general/page-header';
import { appMetaTags } from '~/utils/meta';
@@ -70,34 +68,21 @@ const Refusal = ({ refusal }: { refusal: SignInRefusal }) => {
export default function SignIn({ loaderData }: Route.ComponentProps) {
const { returnTo, refusal } = loaderData;
const { primary, suffix } = brand();
return (
<div className="flex min-h-screen w-full items-center justify-center bg-black px-6 text-white">
<div className="w-full max-w-sm">
<div className="mb-12 flex items-center gap-3">
<HanzoMark size={28} className="text-white" />
<span className="text-base font-medium tracking-tight">
{primary}
{suffix && <span className="text-zinc-400"> {suffix}</span>}
</span>
</div>
<div className="w-full max-w-sm rounded-3xl border border-border bg-card p-8">
<PageHeader
title={<Trans>Sign in to your account</Trans>}
description={<Trans>Welcome back, we are lucky to have you.</Trans>}
/>
<PageHeader
title={<Trans>Sign in to your account</Trans>}
description={<Trans>Welcome back, we are lucky to have you.</Trans>}
descriptionClassName="text-zinc-400"
/>
{refusal && (
<p role="alert" className="mt-6 text-sm text-destructive">
<Refusal refusal={refusal} />
</p>
)}
{refusal && (
<p role="alert" className="mt-6 text-sm text-red-400">
<Refusal refusal={refusal} />
</p>
)}
<div className="mt-8">
<SignInForm returnTo={returnTo} />
</div>
<div className="mt-8">
<SignInForm returnTo={returnTo} />
</div>
</div>
);
@@ -287,7 +287,7 @@ export default function MultisignPage() {
<span>
<Trans>Powered by</Trans>
</span>
<BrandingLogo className="ml-2 inline-block h-[14px]" />
<BrandingLogo className="ml-1" />
</div>
)}
</div>
@@ -304,7 +304,7 @@ export default function MultisignPage() {
<span>
<Trans>Powered by</Trans>
</span>
<BrandingLogo className="ml-2 inline-block h-[14px]" />
<BrandingLogo className="ml-1" />
</div>
)}
</div>
+4 -2
View File
@@ -1,8 +1,10 @@
import { NEXT_PUBLIC_WEBAPP_URL } from '@hanzo/esign-lib/constants/app';
import { env } from '@hanzo/esign-lib/utils/env';
const APP_NAME = env('NEXT_PUBLIC_APP_NAME') || 'Hanzo Sign';
const COMPANY_NAME = env('NEXT_PUBLIC_COMPANY_NAME') || 'Hanzo AI, Inc.';
import { appName, companyName } from '~/components/branding/brand';
const APP_NAME = appName();
const COMPANY_NAME = companyName();
const TWITTER_HANDLE = env('NEXT_PUBLIC_TWITTER_HANDLE') || '@hanzoai';
export const appMetaTags = (title?: string) => {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 19 KiB