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

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 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:

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/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.