Skip to content

hanzoai/theming

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

62 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@hanzo/theming

A theming and component toolkit for building white-label apps using hanzogui.

It also provides generation of Tailwind color tokens expected by shadcn for compatibility with systems that use @hanzo/ui.

How it works

Every color in the system — backgrounds, text, borders, button states — is derived from a 12-step palette, and sometimes an additional neutral palette. These can be configured with a simple one-color "seed." Doing so generates both light and dark versions of the palette. Hanzogui calls these palettes "themes," and our system uses 7 of them: neutral, primary, secondary, info, success, warning, and danger. These themes can be used with all @hanzogui components, as well as all componenents in @hanzo/theming/themed-components.

They are also translated into a Tailwind CSS theme that can be used with systems based on @hanzo/ui (which uses shadcn).

Typically, org-specific brand packages (@my-org/brand) just provide seeds (or 12-step palettes) and assets, and then invoke the generation logic that lives here.

Install

pnpm add @hanzo/theming

Quick start

Hanzogui config and org's brand module

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

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

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

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

// @my-org/<app>/src/main.tsx
import { hanzoguiConfig } from '@my-org/brand'
import { HanzoguiProvider } from 'hanzogui'

<HanzoguiProvider config={hanzoguiConfig}>
  <App />
</HanzoguiProvider>

Tailwind / shadcn

For use with shadcn-based code, token mappings from Hanzogui 'themes' (palettes) are provided by @hanzo/theming. Only the palettes are org-specific, and they are generated at build time by the generate-palettes CLI (see For org brand packages).

For convenience, an org's brand package should likely export a single CSS file that assembles the pieces in the right order.

// App.tsx
@import '@my-org/brand/tw-my-org.css';

This file would contain...

@import 'tailwindcss';
@import './my-org-tw-additions.css';            /* (optional) org-specific non-color tokens */
@import './brand-palettes.css';                 /* --color-{theme}-{1..12}, generated by prebuild*/
@import '@hanzo/theming/shadcn-semantic-tw-colors.css';  /* shadcn semantic refs derived from the above file */

NOTE: Order matters here. brand-palettes.css define the vars, shadcn-semantic-tw-colors.css references and maps them for shadcn.

Palette system

Follows the Radix Colors semantic scale. Every seed produces a 12-step palette with fixed semantic roles:

Step Role Token
1 App background $color1 / $grey1
2 Subtle background $color2 / $grey2
3 UI element background $color3 / $grey3
4 Hovered UI element bg $color4 / $grey4
5 Active / pressed UI bg $color5 / $grey5
6 Subtle borders $color6 / $grey6
7 UI borders / focus rings $color7 / $grey7
8 Hovered borders $color8 / $grey8
9 Solid background (= seed) $color9 / $grey9
10 Hovered solid $color10 / $grey10
11 Low-contrast text $color11 / $grey11
12 High-contrast text $color12 / $grey12

Two palette types

Neutral palette — a grey ramp generated from the neutral seed. Injected into every Hanzogui theme as $grey1$grey12. Used for surfaces, borders, and body text regardless of which accent theme is active.

Accent palette — a full 12-step scale from an accent seed (primary, danger, etc.). Steps 1–8 use the accent hue at progressively increasing saturation; step 9 is the literal seed; steps 10–12 blend toward the scheme foreground. Mapped to $color1$color12 by Hanzogui's default template.

An additional $solidText token is computed per-theme based on the luminance of step 9 — white text for dark fills, dark text for light fills.

Component convention

Within any <Theme> wrapper, both palettes are available:

  • $grey — surfaces, borders, body text (neutral)
  • $color — accent fills, hover, press, accent-colored text
  • $solidText — text on solid action surfaces (auto-contrasted)
<Theme name="primary">
  {/* Card surface — grey */}
  <YStack bg="$grey2" bc="$grey7">
    <Text color="$grey12">Body text</Text>
    {/* Button — accent */}
    <XStack bg="$color9" hoverStyle={{ bg: '$color10' }}>
      <Text color="$solidText">Submit</Text>
    </XStack>
  </YStack>
</Theme>

This means a single <Theme name="primary"> gives components access to both neutral surfaces and vivid accent fills — no nested theme wrappers needed.

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'] }

Hanzogui config

createHanzoguiConfig() produces a complete Hanzogui config from your supplied values:

import { createHanzoguiConfig } from '@hanzo/theming/hanzogui-config'

const config = createHanzoguiConfig({
  themes: {
    primary: { seed: '#1a2744' },
    // neutral, secondary, info, success, warning, danger — pick up defaults
  },
  fonts: {
    body:    { family: '"Inter", sans-serif', size: { ... }, ... },
    heading: { family: '"Inter", sans-serif', size: { ... }, ... },
    mono:    { family: '"JetBrains Mono", monospace', size: { ... }, ... },
  },
  // pick up defaults for 'size' and 'space'
})
Option Type Default
themes ThemesConfig Default color seeds for the 7 themes specified 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)

Each theme seed becomes a Hanzogui children theme with both $color (accent) and $grey (neutral) tokens available:

<Theme name="primary">
  {/* Grey for surfaces, accent for actions */}
  <YStack bg="$grey2" bc="$grey7">
    <Button bg="$color9">Submit</Button>
  </YStack>
</Theme>

<Theme name="danger">
  {/* Accent-tinted surface for alerts */}
  <StatusBox bg="$color2" bc="$color6">
    <Text color="$color11">Error occurred</Text>
  </StatusBox>
</Theme>

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

Tailwind / shadcn details

Brand palettes as brand-palettes.css

  • auto-generated within an org's <org>/brand from its ThemesConfig (which generally lives in brand.json)

  • generateTwThemePalettesCss(config?): Produces --color-{theme}-{1..12} for all 7 themes/palettes in :root and [data-color-scheme='dark'].

Shadcn semantic colors are derived in shadcn-semantic-tw-colors.css

  • @hanzo/theming/shadcn-semantic-tw-colors.css — Derives shadcn's expected color tokens (--background, --primary, --destructive...) from the palettes as css vars (var(--color-neutral-1), var(--color-primary-10)...) that were generated from the org's ThemesConfig. Simply maps, so does not vary. Just needs to be included in the app's css file after brand-palettes.css.

The generate-palettes CLI

@hanzo/theming includes a generate-palettes CLI that reads an org's brand.json and writes the palettes CSS file:

# 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 this is not run manually, but as a prebuild step in an org brand package's package.json:

"prebuild": "generate-palettes"

Output: brand-palettes.css

:root {
  --color-neutral-1: #fafafa;
  --color-neutral-2: #f5f5f5;
  /* ... through 12, for all 7 themes */
}
[data-color-scheme='dark'] {
  --color-neutral-1: #0d0d0d;
  /* ... */
}

Components

Themed Hanzogui 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
├── hanzogui/                             # Hanzogui-specific
│   ├── types.ts                          # FontDef, HanzoguiConfigOptions
│   ├── create-config.ts                  # createHanzoguiConfig(options?)
│   ├── index.ts                          # (internal barrel)
│   ├── defaults/
│   │   ├── themes.ts                     # default seed colors
│   │   ├── spacing.ts                    # default size + space scales
│   │   ├── fonts.ts                      # default system font definitions
│   │   └── index.ts                      # barrel (seeds, size, space, fonts)
│   └── components/                       # see COMPONENTS.md
└── hanzo-ui/                             # Tailwind / shadcn
    ├── utils.ts                          # generateTwThemePalettesCss
    ├── generate-palettes.ts              # CLI: reads brand.json, writes brand-palettes.css
    ├── index.ts                          # (internal barrel)
    └── shadcn-semantic-colors.css        # static — shadcn semantic color mappings

Export paths

Path What
@hanzo/theming/hanzogui-config createHanzoguiConfig and types
@hanzo/theming/themed-components Hanzogui components
@hanzo/theming/hanzogui-config-defaults Defaults (seeds, size, space, fonts)
@hanzo/theming/shadcn-semantic-tw-colors.css Static CSS — maps 12-step palettes to semantic color values expected by shadcn

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 Hanzogui palettes in 'themes' field
│   ├── brand-palettes.css        # generated by prebuild (gitignored)
│   ├── my-org-tw-additions.css   # (optional) org-specific tw tokens
│   ├── tw-my-org.css             # bundle: tailwind + additions + palettes + semantic
│   ├── fonts.ts                  # (optional) org-specific font defs 
│   ├── hanzogui-config.ts         # calls createHanzoguiConfig
│   ├── types.ts                  # BrandIdentity, OrgConfig (define the rest of brand.json)
│   └── index.ts                  # re-exports
└── assets/                       # logos, etc

The hanzogui-config.ts calls createHanzoguiConfig with the org's seeds and fonts:

import { createHanzoguiConfig } from '@hanzo/theming/hanzogui-config'
import brandJson from './brand.json'
import { bodyFont, headingFont, monoFont } from './fonts'

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

The package.json has a prebuild script that runs the palette generator:

"scripts": {
  "prebuild": "generate-palettes",
  "build": "tsup src/index.ts --format cjs,esm --dts --clean"
}

generate-palettes is included in @hanzo/theming as a bin. By default it reads src/brand.json and writes src/brand-palettes.css.

About

brand engine that defines types and utility for use in individual org 'brand' modules

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors