@12nil/theme-registry-package
v0.1.5
Published
Theme registry and React provider utilities
Readme
Runtime Theme Registry
Runtime Theme Registry is a TypeScript-first theming runtime for React applications.
It provides:
- Runtime theme registration and switching
- React integration with ThemeProvider and useTheme
- Legacy and semantic token schemas
- Plugin contributions (icons, fonts, spacing, radii, typography)
- Brand governance with strict registration, runtime guardrails, and CI audits
- Composition layers, async loading, cache helpers, and hydration helpers
Table of Contents
- Installation
- Quick Start
- Theme Models
- React APIs
- Theme Switcher Components
- Registry APIs
- Brand Governance Toolkit
- CLI
- Tailwind v4 Integration
- Advanced Workflows
- API Overview
- Development
- Related Docs
Installation
npm install @12nil/theme-registry-packagePeer dependency:
- react >= 18
Quick Start
Create and register at least one theme before rendering ThemeProvider.
import { themeRegistry } from '@12nil/theme-registry-package'
themeRegistry.register({
name: 'default',
modes: {
light: {
primary: '#111827',
secondary: '#374151',
background: '#ffffff',
text: '#111827',
accent: '#2563eb',
muted: '#6b7280',
error: '#dc2626',
warning: '#f59e0b',
success: '#16a34a',
info: '#0ea5e9',
},
dark: {
primary: '#e5e7eb',
secondary: '#9ca3af',
background: '#030712',
text: '#e5e7eb',
accent: '#60a5fa',
muted: '#6b7280',
error: '#f87171',
warning: '#fbbf24',
success: '#4ade80',
info: '#38bdf8',
},
},
})
themeRegistry.setFallback({
themeName: 'default',
modeName: 'light',
})import React from 'react'
import { ThemeProvider, ThemeSwitcherStyled } from '@12nil/theme-registry-package'
export default function App() {
return (
<ThemeProvider>
<div style={{ padding: 24 }}>
<ThemeSwitcherStyled title="Theme Controls" subtitle="Choose theme and mode" />
<h1 style={{ color: 'var(--color-theme-text)' }}>Hello Theme</h1>
</div>
</ThemeProvider>
)
}Theme Models
Theme tokens support two schemas.
- Legacy palette
Required keys:
- primary
- secondary
- background
- text
- accent
- muted
- error
- warning
- success
- info
Optional key:
- tertiary
- Semantic tokens (v2)
Required categories:
- colors
- surface
- text
- border
Each category has required keys validated by the registry.
React APIs
ThemeProvider responsibilities:
- Loads registered themes from themeRegistry
- Restores persisted selection from localStorage
- Resolves invalid selections via fallback rules
- Applies resolved CSS variables to document root
Persistence keys:
- runtime-theme-registry-theme
- runtime-theme-registry-mode
ThemeProvider props:
- injectUtilityClasses?: boolean (default true)
useTheme returns:
- currentTheme
- currentMode
- themes
- availableModes
- setTheme(themeName, modeName)
- setMode(modeName)
- getThemeColors()
Theme Switcher Components
Exports:
- ThemeSwitcher
- ThemeSwitcherStyled
Common switcher props:
- themeLabel
- modeLabel
- showThemeSelector (default true)
- showModeSelector (default true)
- modeSelectorVariant: select | buttons
- modeIcons?: Partial<Record<string, React.ReactNode>>
- modeIconOnly?: boolean
- onThemeChanged
- onModeChanged
Mode-only icon example:
import { ThemeProvider, ThemeSwitcherStyled } from '@12nil/theme-registry-package'
import { Moon, Sun } from 'lucide-react'
export default function App() {
return (
<ThemeProvider>
<ThemeSwitcherStyled
showThemeSelector={false}
showModeSelector
modeSelectorVariant="buttons"
modeIconOnly
modeIcons={{
light: <Sun size={16} />,
dark: <Moon size={16} />,
}}
/>
</ThemeProvider>
)
}Registry APIs
Core registration and lookup:
- register, replace, merge, unregister
- get, getAll, has
- getTokens, getModes, getVariants
- setCurrent, getCurrent
- setFallback, resolveSelection
Dynamic theme extension:
- createTheme
- registerMode
- registerModes
Plugins:
- registerPlugin, unregisterPlugin
- getPlugin, getPlugins
- getPluginContributions, getMergedPluginContributions
Composition:
- registerCompositionLayer
- getCompositionLayer, getCompositionLayers
- compose
Loading and cache:
- load(source, options)
- clearLoadCache(cacheKey?)
Hydration helpers:
- getInitialThemeAttributes
- hydrateThemeOnDocument
Events:
- on(eventName, handler)
- onRegistered
- onThemeChanged
- onVariantAdded
- onLoaded
- onComposed
- onBrandViolation
- onDestroyed
Brand Governance Toolkit
This package supports three governance layers.
Phase 1: Contract Strictness at Registration
Define a BrandContract and enforce it during register and registerPlugin calls.
import { themeRegistry, type BrandContract } from '@12nil/theme-registry-package'
const contract: BrandContract = {
name: 'Acme Brand v1',
defaultLevel: 'error',
colors: {
requireCssVariable: true,
allowedByToken: {
'colors.primary': ['var(--brand-primary)'],
primary: ['var(--brand-primary)'],
},
},
fonts: {
allowedValues: ['Inter, sans-serif', 'Merriweather, serif'],
},
spacing: {
allowedValues: ['4px', '8px', '12px', '16px', '24px', '32px'],
},
borderRadius: {
allowedValues: ['0', '4px', '8px', '12px', '9999px'],
},
}
themeRegistry.setBrandContract(contract, { enforce: true })Severity behavior:
- error: throws and blocks registration
- warn: logs warning and allows registration
- off: skips the rule
Optional per-call override:
themeRegistry.register(theme, { enforceBrand: true })
themeRegistry.registerPlugin(plugin, { enforceBrand: true })Phase 2: Runtime Guardrails
Enable runtime checks in development to catch drift during load and theme application.
themeRegistry.setBrandRuntimeGuard({
enabled: true,
devOnly: true,
treatWarningsAsErrors: false,
onViolation: (payload) => {
console.log(payload.context, payload.issues)
},
})
const unsubscribe = themeRegistry.onBrandViolation((payload) => {
console.log('brand violation', payload)
})
// Call unsubscribe() when you no longer need the listener.Runtime guard checks run during:
- load
- compose
- setCurrent
- hydrateThemeOnDocument
Phase 3: CI Audit
Use the CLI to scan JSON themes and source files against the same contract.
npx runtime-theme-registry audit-brand ./brand-contract.json ./themesESLint Rule for Off-Brand Values
This package includes an ESLint rule that reports off-brand values in:
- style object literals (for example color, backgroundColor, spacing, borderRadius, fontFamily)
- className arbitrary color syntax (for example
bg-[#ff0000])
Rule id:
runtime-theme-registry/brand-contractruntime-theme-registry/tailwind-brand-classes
Flat config example (eslint.config.js):
import runtimeThemeRegistryEslint from '@12nil/theme-registry-package/eslint'
export default [
{
files: ['**/*.{js,jsx,ts,tsx}'],
plugins: {
'runtime-theme-registry': runtimeThemeRegistryEslint,
},
rules: {
'runtime-theme-registry/brand-contract': ['error', {
contractPath: './brand-contract.json',
}],
'runtime-theme-registry/tailwind-brand-classes': ['error', {
contractPath: './brand-contract.json',
}],
},
},
]Rule options:
contractPathpath to BrandContract JSON (default./brand-contract.json)rootDirbase path for resolvingcontractPath(default process cwd)
tailwind-brand-classes options:
allowedClassesextra Tailwind color utilities to allowallowArbitraryValuesallow classes likebg-[#ff0000](defaultfalse)
The tailwind-brand-classes rule enforces allowlisted color utility usage such as:
bg-primary,text-muted,border-border-subtlefill-primary,stroke-info
and reports non-approved classes such as:
bg-red-500text-blue-600border-emerald-400
Severity behavior:
- ESLint controls final severity (
errororwarn) based on your rule config. - Contract
warnanderrorlevels are preserved in the message prefix for context. - Contract
offdisables checks for that rule.
CLI
Inject utility classes into CSS
npx runtime-theme-registry inject-css ./src/app/globals.cssThis adds or updates a marked utility block.
Audit brand compliance
npx runtime-theme-registry audit-brand ./brand-contract.json ./themesFlags:
- --format json|text (default text)
- --fail-on error|warn (default error)
- --source-path
- --no-source-scan
- --source-scope all|strict (default all)
- --source-include
Strict source scope defaults:
- src
- app
- components
- pages
- styles
Examples:
# Fail only on error-level issues.
npx runtime-theme-registry audit-brand ./brand-contract.json ./themes --format text --fail-on error
# Fail on warnings too, output JSON for CI parsers.
npx runtime-theme-registry audit-brand ./brand-contract.json ./themes --format json --fail-on warn
# Audit theme JSON under ./themes but scan source files in ./src.
npx runtime-theme-registry audit-brand ./brand-contract.json ./themes --source-path ./src
# Strict source scope using defaults.
npx runtime-theme-registry audit-brand ./brand-contract.json . --source-scope strict
# Strict source scope with explicit roots.
npx runtime-theme-registry audit-brand ./brand-contract.json . --source-scope strict --source-include src,app,componentsAudit summary fields:
- filesScanned
- sourceFilesScanned
- themesScanned
- errorCount
- warningCount
- sourceScope
- sourceRoots
- status
Tailwind v4 Integration
ThemeProvider can inject utility classes at runtime.
If you prefer CSS-first mapping in global styles:
@import "tailwindcss";
@theme inline {
--color-primary: var(--theme-primary);
--color-secondary: var(--theme-secondary);
--color-background: var(--theme-background);
--color-text: var(--theme-text);
--color-accent: var(--theme-accent);
--color-muted: var(--theme-muted);
--color-error: var(--theme-error);
--color-warning: var(--theme-warning);
--color-success: var(--theme-success);
--color-info: var(--theme-info);
--color-surface-card: var(--surface-card);
--color-border-subtle: var(--border-subtle);
}Use mapped utilities:
<button className="bg-primary text-background border border-border-subtle">Save</button>
<p className="text-muted">Secondary text</p>
<div className="bg-surface-card">Card</div>If you inject utility classes at build-time via CLI, disable runtime injection:
<ThemeProvider injectUtilityClasses={false}>
<App />
</ThemeProvider>Advanced Workflows
Plugin registration and contributions
import { themeRegistry } from '@12nil/theme-registry-package'
themeRegistry.registerPlugin({
name: 'CRM',
version: '1.0.0',
themes: [
{
name: 'crm',
modes: {
light: {
primary: '#1d4ed8',
secondary: '#2563eb',
background: '#ffffff',
text: '#0f172a',
accent: '#1d4ed8',
muted: '#64748b',
error: '#dc2626',
warning: '#f59e0b',
success: '#16a34a',
info: '#0ea5e9',
},
},
},
],
fonts: {
body: 'Inter, sans-serif',
},
spacing: {
md: '16px',
},
radii: {
card: '12px',
},
})
const merged = themeRegistry.getMergedPluginContributions()
console.log(merged.fonts)Composition and loading
import { themeRegistry } from '@12nil/theme-registry-package'
themeRegistry.registerCompositionLayer({
type: 'brand',
name: 'fnp',
tokens: {
primary: '#031011',
secondary: '#153e46',
background: '#ffffff',
text: '#031011',
accent: '#378d93',
muted: '#667085',
error: '#DF1C41',
warning: '#F4C790',
success: '#27AE60',
info: '#378d93',
},
})
themeRegistry.registerCompositionLayer({
type: 'appearance',
name: 'dark',
tokens: {
background: '#031011',
text: '#c4e8ee',
secondary: '#378d93',
primary: '#c4e8ee',
accent: '#378d93',
muted: '#667085',
error: '#DF1C41',
warning: '#F4C790',
success: '#27AE60',
info: '#378d93',
},
})
const composed = themeRegistry.compose({ brand: 'fnp', appearance: 'dark' })
await themeRegistry.load('/api/themes', {
ifExists: 'merge',
cacheKey: 'customer-themes',
maxAgeMs: 5 * 60 * 1000,
})
const attrs = themeRegistry.getInitialThemeAttributes('fnp', 'dark')
const hydrated = themeRegistry.hydrateThemeOnDocument('fnp', 'dark')
themeRegistry.clearLoadCache('customer-themes')API Overview
Main exports include:
- ThemeProvider, useTheme
- ThemeSwitcher, ThemeSwitcherStyled
- themeRegistry
- validateTheme, isThemeTokensV2
- validateThemeAgainstBrand, validatePluginAgainstBrand
- paletteToCSSVariables, tokenSetToCSSVariables
- createTailwindThemeColorMap
- generateThemeCSS, generateAllThemesCSS, applyThemeToDocument
- Comprehensive types for themes, plugins, governance, composition, events, and loading
Development
npm.cmd install
npm.cmd run buildPublish checklist:
- Update package name/version in package.json.
- Run npm.cmd run build.
- Run npm.cmd pack --dry-run.
- Publish with npm.cmd publish --access public.
Related Docs
- Full reference: DOCUMENTATION.md
- Practical app setup: REAL_PROJECT_GUIDE.md
- Fast onboarding: QUICKSTART.md
- Roadmap: ROADMAP.md
