Files
zeekayandHanzo Dev 6370fee218 Converge on Hanzo 8.x: @hanzo/gui, no Tailwind, one compiler
The 8.x refactor happened upstream; this package was still pinned to the
102.0.0-rc line and importing the pre-rename `hanzogui`. Two of its barrels
exported names that no longer existed (`createHanzoguiConfig`,
`HanzoguiConfigOptions`) and `src/tg/index.ts` re-exported files that were not
in its directory, so `tsc` could not have passed on `main`.

Packages
  [email protected] → @hanzo/gui@^8.0.1, and the four @hanzogui/* peers to
  ^8.0.0. Dropped @hanzogui/config-default and @hanzogui/animations-react-native
  entirely: the config package was consulted for exactly three static maps
  (tokens.radius, tokens.zIndex, media) and dragged react-native into a web
  theming package to get them. Those maps now live beside the size and space
  scales they belong with, in src/gui/defaults/. Verified against @hanzo/gui
  8.0.1 that the produced config is unchanged — radius $4 = 9, zIndex $3 = 300,
  all fourteen media keys, 280 themes.

Tailwind, shadcn, Radix
  The generated stylesheets emitted `@theme` and `@theme inline` blocks to
  register colors as Tailwind utilities. Those blocks are gone; what remains is
  `:root` / `.dark` custom properties, the same shape and the same dark selector
  as @hanzo/ui/theme.css. The semantic token map had drifted into two homes — a
  hand-edited shadcn-semantic-colors.css and a TS map that disagreed with it
  about `--primary` (step 9 vs step 10) — so the CSS file is deleted and
  dist/css/semantic.css is emitted from the map at build time. The dark block
  now carries only the roles whose step actually changes.

TypeScript 7
  tsgo for both typecheck and emit (~0.5s). It needs types: ["node", "react"]
  spelled out in tsconfig or it fails to find @types/node. tsup is gone with it:
  it and tsc were both writing dist/css/generate-palettes.js, and tsc's own ESM
  output is a perfectly good bin. One compiler, one output format — the package
  is now "type": "module" with explicit .js extensions, so Node resolves it
  rather than relying on a bundler to forgive extensionless ESM.

Also
  Export paths follow the rename (/gui, /gui/components, /gui/defaults, /css,
  /semantic.css) and a new root export carries the dependency-free color math.
  LLM.md was a tracked symlink to a gitignored file, dangling in every clone;
  it is a real file now. README described a differently-named package with
  export paths that did not exist.

Verified: pnpm build green, tsgo and tsc 5.9 both clean, CLI exercised on a real
brand.json, all six export subpaths resolved and imported from a linked dist.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-03 17:55:59 -07:00

8.7 KiB
Raw Permalink Blame History

@hanzo/theming

A theming toolkit for white-label apps on @hanzo/gui. Provide seed colors, get a complete design system: 12-step palettes, a @hanzo/gui config, themed components, and plain CSS custom properties.

No Tailwind, no shadcn, no Radix. The stylesheets this package emits are ordinary :root / .dark custom properties — any renderer can read them.

How it works

Every color in the system — backgrounds, text, borders, button states — derives from a 12-step palette, which you configure with a one-color "seed". Each seed produces both a light and a dark 12-step scale. The system calls these palettes "themes" and uses seven: neutral, primary, secondary, info, success, warning, danger. Any or all can fall back to the defaults.

Org-specific brand packages (@my-org/brand) supply only seeds (or explicit 12-step palettes) and assets; all the generation logic lives here.

Install

pnpm add @hanzo/theming

Quick start

The @hanzo/gui config

In the org's brand module — assemble and export a config from your seeds:

// @my-org/brand/src/gui-config.ts
import { createGuiConfig } from '@hanzo/theming/gui'
import brandJson from './brand.json'

export const guiConfig = createGuiConfig({
  themes: brandJson.themes,
  // omitted fields (fonts, size, space) fall back to defaults
})

In the org's apps — wrap the app with GuiProvider:

// @my-org/<app>/src/main.tsx
import { guiConfig } from '@my-org/brand'
import { GuiProvider } from '@hanzo/gui'

<GuiProvider config={guiConfig}>
  <App />
</GuiProvider>

The stylesheets

Two layers, loaded in this order:

@import './brand-palettes.css';        /* --palette-{theme}-{1..12}, generated per org */
@import '@hanzo/theming/semantic.css'; /* --background, --primary, … → the palette steps */

Order matters: brand-palettes.css defines the palette variables, semantic.css references them. Both declare their light values in :root and their dark values under .dark, so toggling that one class on <html> flips the whole system — the same convention @hanzo/ui/theme.css uses.

Palette system

A single hex seed produces a 12-step palette for both light and dark schemes:

Steps Role Examples
1–3 Backgrounds base, raised, subtle
4–6 Borders, separators subtle, medium, strong
7–9 Interactive states, quiet text muted, secondary, quiet
10–12 Foreground text primary, strong, darkest

Light palettes run lightest (1) to darkest (12). Dark palettes run darkest (1) to lightest (12). Saturation tapers at the extremes for a natural feel.

Theme seeds

Seven named themes, all optional. Omitted themes get these defaults:

Theme Default Purpose
neutral #808080 Greyscale canvas — backgrounds, text, borders
primary #3B82F6 Primary actions — buttons, links, focus rings
secondary #721be4 Secondary accent
info #eab308 Informational callouts, tips
success #16a34a Success states, confirmations
warning #fb923c Warning states, caution
danger #dc2626 Errors, destructive actions

Each theme can be specified as:

// A seed — generates both light and dark palettes
{ seed: '#1a2744' }

// An explicit 12-step palette — used for both schemes (dark is reversed)
['#f0f3f8', '#dfe5f0', ..., '#0c1322']

// Per-scheme — mix seeds and explicit palettes
{ light: { seed: '#1a2744' }, dark: ['#0c1322', '#131d34', ..., '#f0f3f8'] }

createGuiConfig

Produces a complete @hanzo/gui config from your supplied values:

import { createGuiConfig } from '@hanzo/theming/gui'

const config = createGuiConfig({
  themes: {
    primary: { seed: '#1a2744' },
    // neutral, secondary, info, success, warning, danger — pick up defaults
  },
  fonts: {
    body:    { family: '"Geist", sans-serif', size: { ... }, ... },
    heading: { family: '"Geist", sans-serif', size: { ... }, ... },
    mono:    { family: '"Geist Mono", monospace', size: { ... }, ... },
  },
  // pick up defaults for 'size' and 'space'
})
Option Type Default
themes ThemesConfig The seven default seeds above
fonts.body FontDef System sans-serif stack
fonts.heading FontDef System sans-serif stack
fonts.mono FontDef System monospace stack
size Record<string, number> Non-linear component size scale (20–144px)
space Record<string, number> Linear 4px grid (0–96px)

Radius, z-index and media scales are not options — they come from @hanzo/theming/gui/defaults, which also exposes every default above.

Each theme seed becomes a @hanzo/gui children theme. Components use standard theme wrapping:

<Theme name="primary">
  <Button>Submit</Button>     {/* picks up primary palette automatically */}
</Theme>

<Theme name="danger">
  <Card>Error occurred</Card> {/* danger palette for bg, border, text */}
</Theme>

Light/dark is handled structurally — the active scheme is inherited from the root, and each children theme has both variants.

The generate-palettes CLI

Reads an org's brand.json and writes its palette stylesheet:

# Defaults: reads src/brand.json, writes src/brand-palettes.css
npx generate-palettes

# Custom paths
npx generate-palettes --brandFile src/our-brand.json --outFile src/our-brand-palettes.css

Normally run as a prebuild step in the org brand package:

"prebuild": "generate-palettes"

Output:

:root {
  --palette-neutral-1: #fcfcfc;
  --palette-neutral-2: #f7f7f7;
  /* … through 12, for all 7 themes */
}
.dark {
  --palette-neutral-1: #111111;
  /* … */
}

Only the palettes are org-specific. The semantic layer maps roles onto palette steps and never varies, so it ships prebuilt as @hanzo/theming/semantic.css. @hanzo/theming/css exports the same two generators (generatePaletteCss, generateSemanticCss) for programmatic use.

Components

Themed @hanzo/gui components: ThemedButton, GhostButton, OutlineButton, StatusBox, ToggleSwitch, StyledCard.

See COMPONENTS.md for usage and examples.

Package structure

src/
├── types.ts              # Palette12, ThemeSeed, ThemeDesc, ThemesConfig
├── palette-utils.ts      # generateNeutralPalette, generateAccentPalette, resolveThemeDesc
├── index.ts              # dependency-free core: the two files above
├── css/                  # stylesheets — plain custom properties
│   ├── palettes.ts       # generatePaletteCss  → --palette-{theme}-{1..12}
│   ├── semantic.ts       # generateSemanticCss → --background, --primary, …
│   ├── generate-palettes.ts  # CLI: brand.json → brand-palettes.css
│   └── index.ts          # barrel
└── gui/                  # @hanzo/gui bindings
    ├── types.ts          # FontDef, GuiConfigOptions
    ├── create-config.ts  # createGuiConfig(options?)
    ├── index.ts          # barrel
    ├── defaults/         # seeds, size, space, radius, zIndex, media, fonts
    └── components/       # see COMPONENTS.md

Export paths

Path What
@hanzo/theming Palette types and generators — no dependencies
@hanzo/theming/css generatePaletteCss, generateSemanticCss, DARK_SELECTOR
@hanzo/theming/semantic.css Prebuilt semantic stylesheet
@hanzo/theming/gui createGuiConfig and its types
@hanzo/theming/gui/components Themed @hanzo/gui components
@hanzo/theming/gui/defaults Defaults (seeds, size, space, radius, zIndex, media, fonts)

Example use in @my-org/brand

An org brand package depends on @hanzo/theming and provides just data:

@my-org/brand/
├── src/
│   ├── brand.json           # org identity, URLs, and palettes in the 'themes' field
│   ├── brand-palettes.css   # generated by prebuild (gitignored)
│   ├── my-org.css           # bundle: palettes + semantic + org additions
│   ├── fonts.ts             # (optional) org-specific font defs
│   ├── gui-config.ts        # calls createGuiConfig
│   ├── types.ts             # BrandIdentity, OrgConfig (define the rest of brand.json)
│   └── index.ts             # re-exports
└── assets/                  # logos, etc
// gui-config.ts
import { createGuiConfig } from '@hanzo/theming/gui'
import brandJson from './brand.json'
import { bodyFont, headingFont, monoFont } from './fonts'

export const guiConfig = createGuiConfig({
  themes: brandJson.themes,
  fonts: { body: bodyFont, heading: headingFont, mono: monoFont },
})