hazo_theme
v1.0.1
Published
Shared design tokens, Tailwind v4 theme, and theming runtime (dark/light + brand theme-sets + whitelabel) for all hazo_* packages and apps.
Downloads
139
Readme
hazo_theme
Shared design tokens, Tailwind v4 theme, and theming runtime (dark/light + brand theme-sets + whitelabel) for all hazo_* packages and apps.
Installation
npm install hazo_themeQuick start
This is the pattern used by test-app/app/layout.tsx in this package — copy it into a Next.js
App Router root layout.
1. Import the CSS in your global stylesheet (after Tailwind, before anything else):
/* app/globals.css */
@import "tailwindcss";
@import "hazo_theme/theme.css";
@import "hazo_theme/all-themes.css"; /* optional: bundles every shipped theme-set for runtime switching */
@source "../node_modules/hazo_theme/dist";If you only ship one brand palette, import just that theme-set instead of the full bundle:
@import "hazo_theme/themes/indigo-sunset.css";2. Wrap the root layout in <HazoThemeProvider> and render <ThemeScript> in <head> (the
script runs before first paint so there is no flash of the wrong mode/palette):
// app/layout.tsx
import './globals.css';
import { HazoThemeProvider, ThemeScript } from 'hazo_theme/client';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<ThemeScript storageKey="theme" defaultThemeSet="default" />
</head>
<body className="bg-background text-foreground">
<HazoThemeProvider defaultMode="system" defaultThemeSet="default">
{children}
</HazoThemeProvider>
</body>
</html>
);
}storageKey/defaultThemeSet passed to <ThemeScript> must match the values passed to
<HazoThemeProvider> — they read/write the same localStorage keys.
3. Use token classes and the prebuilt controls anywhere in the tree:
import { ThemeToggle, ThemeSetPicker } from 'hazo_theme/client';
function Toolbar() {
return (
<header className="flex items-center justify-between border-b border-border bg-background px-4 py-2 text-foreground">
<span>My App</span>
<div className="flex items-center gap-2">
<ThemeSetPicker />
<ThemeToggle />
</div>
</header>
);
}API
Styles (CSS exports)
| Import | Contents |
|---|---|
| hazo_theme/tokens.css | Tier-1 primitives, Tier-2 semantic tokens, Tier-3 chrome/dialog tokens, .dark overrides. Pulled in automatically by theme.css. |
| hazo_theme/theme.css | The Tailwind v4 @theme inline entrypoint — maps every token to a utility class (bg-primary, text-navbar-fg, bg-toolbar-hover, …). Import this, not tokens.css directly. |
| hazo_theme/themes/<name>.css | One brand theme-set ([data-theme="<name>"] override block). Currently shipped: default (implicit, no import needed), indigo-sunset. |
| hazo_theme/all-themes.css | Convenience bundle importing every shipped themes/*.css. |
Chrome tokens (footer / toolbar / dialog)
New in 0.2.0, alongside the BrandIdentity contract above:
footer-public-*—footer-public-bg,footer-public-fg,footer-public-fg-muted,footer-public-border. For a tall, multi-column public-site footer (e.g. hazo_ui's<AppFooter variant="public">).footer-app-*—footer-app-bg,footer-app-fg,footer-app-fg-muted,footer-app-border. For a thin in-app footer strip (e.g.<AppFooter variant="app">). The legacyfooter-*family (no-public/-appsuffix) is retained unchanged for existing consumers.--toolbar-hover-fg— foreground companion to the existing--toolbar-hoverbackground token; fixes toolbar hover text that was previously unreadable (no matching foreground token existed).--dialog-header-bar-bg/--dialog-header-bar-fg— the dialog header-bar background/foreground, consumed by hazo_ui'sHazoUiDialogin place of a hardcoded dark-slate color pair.
hazo_theme (server-safe entry)
SEMANTIC_TOKENS— the frozen Tier-2 token name list ("background","primary","chart-1","sidebar", …). Adding a name is a minor release; renaming/removing is a major.CHROME_TOKENS— the Tier-3 layout-chrome token name list ("navbar-bg","footer-fg","toolbar-hover", …).SHIPPED_THEME_SETS—["default", "indigo-sunset"], the theme-set names shipped with the package (drivesThemeSetPicker's options).BrandIdentity— the per-tenant whitelabel + chrome-identity contract:{ primary?, primaryForeground?, accent?, accentForeground?, fontDisplay?, fontBody?, themeSet?, logoUrl?, logoUrlDark?, appTitle?, footer? }. Colour fields are raw HSL channels (e.g."222 47% 11%", nohsl()wrapper). ThelogoUrl/logoUrlDark/appTitle/footerfields are identity data, not CSS variables —resolveTenantVars()ignores them; they're consumed directly by chrome components (e.g. hazo_ui'sLogo/AppHeader/AppFooter).TenantBrandingis kept as a back-compat type alias forBrandIdentity.FooterConfig—{ columns?: FooterColumn[]; copyright?: string; social?: FooterSocial }, the shape ofBrandIdentity.footer.public/.app.FooterColumnis{ heading?: string; links: FooterLink[] };FooterLinkis{ label: string; href: string };FooterSocialis{ github?, twitter?, linkedin?, website? }(all optional URLs).resolveTenantVars(branding: TenantBranding): string— turns aBrandIdentity/TenantBrandingrecord's colour/font fields into an inline CSS-variable declaration string, for whitelabel overrides scoped to any element. Identity-data fields (logoUrl,appTitle,footer, …) are not emitted — pass those straight to a chrome component instead.
hazo_theme/client (React runtime; re-exports everything above too)
HazoThemeProvider({ children, defaultMode?, defaultThemeSet?, storageKey? })— provides theme context.defaultMode?: "light" | "dark" | "system"— default"system".defaultThemeSet?: string— default"default".storageKey?: string—localStoragekey prefix, default"hazo-theme"(stores${storageKey}-modeand${storageKey}-set).
useTheme()— must be called under<HazoThemeProvider>; throws otherwise. Returns:mode: "light" | "dark" | "system"setMode(mode): voidresolvedMode: "light" | "dark"—modewith"system"resolved against the OS preference.toggleMode(): void— flips between light/dark (always sets an explicit mode, never"system").themeSet: string— active brand palette.setThemeSet(name: string): void
ThemeScript({ storageKey?, defaultThemeSet? })— no-flash inline<script>. Render it in<head>, before<body>. Reads the samelocalStoragekeys as the provider and sets.dark/data-themeon<html>before hydration. Wrapped in try/catch — never throws.ThemeToggle—React.forwardRef<HTMLButtonElement, { className? } & ButtonHTMLAttributes>. Prebuilt light/dark toggle button (Sun/Moon icons vialucide-react, optional peer dep). Must be rendered under<HazoThemeProvider>.ThemeSetPicker—React.forwardRef<HTMLSelectElement, { className? } & SelectHTMLAttributes>. Prebuilt<select>bound touseTheme().themeSet/setThemeSet, populated fromSHIPPED_THEME_SETS(plus the current value if it's a consumer-registered custom set not in that list).cn(...inputs: ClassValue[]): string—clsx+tailwind-merge, the shared class-merge helper.useLocalStorage<T>(key, initialValue)/useMediaQuery(query)— the small SSR-safe hooks the provider is built on; exported for consumers who need the same primitives.
Whitelabel snippet
resolveTenantVars() returns a "--var: value; --var2: value2;" inline-declaration string. React
passes hyphen-prefixed (custom-property) keys straight through the style prop without
camelCasing, so split the string into a plain object and apply it to the element you want scoped —
typically <html>/<body> for a whole-app tenant, or any wrapper for a scoped subtree (see
test-app/app/whitelabel/page.tsx for the full working version, including a live edit form):
import { resolveTenantVars, type TenantBranding } from 'hazo_theme';
function tenantVarsToStyle(cssText: string): React.CSSProperties {
const style: Record<string, string> = {};
for (const decl of cssText.split(';')) {
const [key, value] = decl.split(':').map((s) => s.trim());
if (key && value) style[key] = value;
}
return style as React.CSSProperties;
}
const branding: TenantBranding = {
primary: '204 94% 40%',
primaryForeground: '0 0% 100%',
};
function TenantScope({ children }: { children: React.ReactNode }) {
return <div style={tenantVarsToStyle(resolveTenantVars(branding))}>{children}</div>;
}It sits on top of whichever themeSet/dark-mode state is already active; an empty
TenantBranding resolves to "" and changes nothing.
Tailwind v4 (@source required)
If this package renders UI, add the following to your app's CSS entry:
@source "../node_modules/hazo_theme/dist";License
MIT
