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]>
8.7 KiB
@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 },
})