title() answers a brand's title or a Hanzo product's own name: PRODUCTS
holds the one product that has one, team → "Hanzo Team" (hanzo.team),
so @hanzo/app's app({ brand }) reads a name from here instead of keeping
its own brand() and wordmark(). An id that names neither throws.
The package publishes from CI now. Its gate could not pass on a fresh
checkout — pnpm gen read @hanzo/tokens from a sibling ../ui checkout, and
the declaration build died on TypeScript 6's baseUrl deprecation — so it
went out from a laptop. @hanzo/tokens is a dev dependency, tsconfig states
its types and the deprecation, and publish.yml reads the npm token from
KMS. styles/variables.css is regenerated from @hanzo/tokens 1.1.1, which
is what every build already wrote.
7.2 KiB
@hanzo/brand
Official brand assets, design system, AND the fleet white-label registry for
Hanzo. Published as @hanzo/brand on npm. Exports the multi-brand registry +
host resolver (hanzo / lux / zoo / pars + white-label tenants), colors,
typography, themes, design tokens, logo components, and CSS variables.
How this ships
One way, and it runs on our own stack:
push -> github.com/hanzoai/brand (a mirror)
-> git.hanzo.ai/hanzoai/brand CANONICAL
.hanzo/workflows/sync-from-github.yml pulls the mirror, every 10m
.hanzo/workflows/publish.yml publishes @hanzo/brand to npm
.hanzo/workflows/deploy.yml builds ghcr.io/hanzoai/brand
-> hanzoai/universe crs/brand.yaml names the tag that is live
-> hanzoai/operator reconciles the App
-> hanzoai/static behind hanzoai/ingress serves brand.hanzo.ai
git.hanzo.ai is canonical; GitHub is a mirror. .github/workflows/ is empty.
Every build, check, publish and the sync itself lives under .hanzo/workflows/,
which the forge reads. It uses GitHub Actions syntax, so a workflow moves between
the two by changing directory and nothing else.
The sync is a PULL: the forge runner reaches both ends, so nothing needs a credential aimed into the forge, and a public repo needs none at all. It fast-forwards only — a divergence fails loudly instead of force-pushing a side.
publish.yml is the SOLE publisher of @hanzo/brand. It fires when version in
package.json changes on main, skips a version already on the registry, and
runs pnpm test first — one command that builds dist/ and then gates on the
suite. dist/ is gitignored, so publishing without it ships a package whose every
subpath 404s. Needs NPM_TOKEN as a forge secret.
The site
index.html is the whole site: one self-contained page, inline CSS, no scripts
and no subresources. Dockerfile copies it into ghcr.io/hanzoai/static — no
build stage, because there is nothing to build. No GitHub Pages and no Cloudflare
Pages: the repo used to push this page to a gh-pages branch, Pages never built
it, and brand.hanzo.ai has answered 404 from our own ingress ever since.
A build never deploys itself. deploy.yml publishes
ghcr.io/hanzoai/brand:<sha>; a human then sets spec.image.tag in
hanzoai/universe infra/k8s/operator/crs/brand.yaml and adds - brand.yaml to
that directory's kustomization.yaml. The CR is inert until both are done, which
is deliberate: an App promoted with an empty tag takes the host down instead of
leaving it alone.
Order: publish an image -> set the tag -> add the line -> confirm the pod is Running.
Stack
- TypeScript 5, tsup (CJS + ESM + DTS output)
- Runtime dep:
@hanzo/logo(the ONE home of the Hanzo block-H geometry — consumed viaMARK_PATHS/MARK_VIEWBOX; never re-typed here) - React is an OPTIONAL peer (only the logo components need it)
- Outputs:
dist/index.{js,mjs,d.ts}, plus./loader,./hero
White-label registry (the multi-brand layer)
ONE home for "who is this brand?" across every surface. A lux.network /
zoo.ngo / pars.network host resolves to ITS brand + mark — NEVER Hanzo's.
That non-cross-contamination is the white-label invariant (test/registry.test.mjs).
src/brand-id.ts # BrandId + HOST_BRANDS suffix table + brandFromHost(host)
# the pure resolver (no React, no data). Exact suffix /
# dot-boundary match — lookalike hosts can't impersonate.
src/marks.ts # BRAND_MARKS: per-brand inline mark geometry. Hanzo mark =
# @hanzo/logo MARK_PATHS. renderMarkSVG(id) string helper.
src/registry.ts # BrandIdentity + BRANDS registry + getBrand(host) +
# toBrandContract() (projects to @hanzo/id's BrandContract).
Key API:
import { getBrand, brandFromHost, BRANDS, BrandLogo, renderMarkSVG, toBrandContract } from '@hanzo/brand'
getBrand('lux.network') // -> BrandIdentity { id:'lux', name:'Lux', mark, ... }
brandFromHost('zoo.ngo') // -> 'zoo'
<BrandLogo /> // React: auto-resolves the mark by window.location.hostname
<BrandLogo brand="lux" /> // React: explicit brand mark
renderMarkSVG('pars') // framework-agnostic inline SVG string
toBrandContract(BRANDS.zoo) // -> the @hanzo/id runtime BrandContract shape
What a wordmark reads: title
title(id) (@hanzo/brand/registry, also the root) is the one answer to "what
does this host's wordmark say": a brand's title ("Hanzo AI", "Lux Network",
"Zoo"), or a Hanzo product's that carries a name of its own (PRODUCTS:
team → "Hanzo Team", hanzo.team). @hanzo/app's app({ brand }) reads it.
A product names the brand it belongs to and never repeats a brand's title; an
id that names neither throws, so a host's typo never prints another brand.
Release
.github/workflows/publish.yml publishes when package.json is ahead of npm,
with NPM_TOKEN read from KMS through KMS_CLIENT_ID/KMS_CLIENT_SECRET. The
gate is the release: pnpm install --frozen-lockfile, pnpm type-check,
pnpm test (which builds and regenerates styles/variables.css from
@hanzo/tokens, a dev dependency). No laptop publishes.
Structure
src/
index.ts # Main entry - re-exports all modules, auto-applies theme in browser
brand-id.ts # BrandId, HOST_BRANDS, brandFromHost, isKnownBrandHost, normalizeHost
marks.ts # BRAND_MARKS (hanzo/lux/zoo/pars marks), renderMarkSVG
registry.ts # BrandIdentity, BRANDS, getBrand, toBrandContract, BrandContract
logos.tsx # HanzoLogo/HanzoLogoSVG/HanzoFavicon (consume @hanzo/logo) + BrandLogo
colors.ts # Monochrome palette: Hanzo Black (#0a0a0b) ↔ Hanzo White (#fff), neutral scale, status semantics
typography.ts # Font families (Zen, Zen Mono), sizes, weights, line heights
themes.ts # Light/dark theme definitions, applyTheme() helper
tokens.ts # Design tokens (spacing, radius, shadows, transitions)
config.ts # Company info, social links, domains, SEO defaults
hero.ts, cards.ts, cli.ts # README hero + social-card toolkit + `hanzo-brand` CLI
utils.ts, docs.ts
brand.json # Hanzo runtime BrandContract (id portal fetches this; kept in
# sync with the registry Hanzo entry by a consistency test)
test/ # node:test suites run against the BUILT dist (pnpm test)
styles/variables.css
Commands
pnpm install # install dev deps
pnpm build # tsup (CJS + ESM + DTS)
pnpm type-check # tsc --noEmit (the real static gate)
pnpm test # build + node --test (19 tests: white-label invariant, suffix
# and homoglyph confusion, brand.json consistency)
Notes
@hanzo/logois the single source of the Hanzo mark; this package NEVER re-types the block-H path data (marks.ts / logos.tsx both consume it).- The registry's Hanzo entry and
brand.jsonare kept in sync by test/brand-json-consistency.test.mjs. pnpm lintis currently a no-op error (no ESLint config in-repo); usetype-check.- Auto-applies dark/light theme on import in browser environments.
- Published with public access to npm registry.
CLAUDE.md,AGENTS.md,QWEN.md,GEMINI.mdare all symlinks to this file.