Files
zeekay 0f104401e9 @hanzo/brand 1.5.0: title(id), what a host's wordmark reads
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.
2026-10-22 20:04:53 -07:00

143 lines
7.2 KiB
Markdown

# @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 via `MARK_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:
```ts
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
```bash
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/logo` is 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.json` are kept in sync by
test/brand-json-consistency.test.mjs.
- `pnpm lint` is currently a no-op error (no ESLint config in-repo); use
`type-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.md` are all symlinks to this file.