@uniweb/theming
v0.1.1
Published
Theming utilities for the Uniweb platform — shade generation, CSS generation, theme processing
Readme
@uniweb/theming
Theming engine for Uniweb — generates color palettes, semantic CSS tokens, and context classes from a declarative theme.yml configuration.
Overview
Uniweb sites separate theming from components. Content authors set theme: light|medium|dark per section in frontmatter; the runtime applies context classes; components use semantic CSS tokens (var(--heading), var(--link), var(--section)) that resolve automatically. This package is the engine behind that system.
It handles three concerns:
- Shade generation — Expand a single hex color into 11 perceptually uniform shades (50–950) using the OKLCH color space
- Theme processing — Validate and merge
theme.ymlconfiguration with foundation defaults - CSS generation — Produce complete CSS with palette variables, context classes, font imports, and foundation-specific custom properties
Installation
npm install @uniweb/themingQuick Start
import { buildTheme } from '@uniweb/theming'
// Process theme.yml and generate CSS in one step
const { css, config, errors, warnings } = buildTheme({
colors: {
primary: '#3b82f6',
neutral: 'stone', // Named preset (warm gray)
},
fonts: {
heading: '"Inter", sans-serif',
},
})
// css → complete stylesheet with palettes, contexts, fonts
// config → processed configuration for runtime useShade Generation
Generate Tailwind-compatible shade scales from any CSS color. Uses OKLCH for perceptually uniform lightness steps with automatic sRGB gamut mapping.
import { generateShades } from '@uniweb/theming'
// Default: shade 500 = exact input color, others redistributed proportionally
const shades = generateShades('#3b82f6')
// { 50: 'oklch(...)', 100: '...', ..., 500: 'oklch(...)', ..., 950: '...' }
// Hex output
generateShades('#3b82f6', { format: 'hex' })
// { 50: '#eff6ff', ..., 500: '#3b82f6', ..., 950: '#172554' }
// Fixed lightness scale (shade 500 may differ from input)
generateShades('#3b82f6', { exactMatch: false })
// Generation modes
generateShades('#e35d25', { mode: 'natural' }) // Temperature-aware hue shifts
generateShades('#e35d25', { mode: 'vivid' }) // Higher saturation
generateShades('#e35d25', { mode: 'fixed' }) // Default — constant hueMultiple Palettes
import { generatePalettes } from '@uniweb/theming'
const palettes = generatePalettes({
primary: '#3b82f6',
secondary: '#64748b',
accent: { base: '#8b5cf6', mode: 'vivid' }, // Per-color options
})
// { primary: { 50: ..., 950: ... }, secondary: { ... }, accent: { ... } }Color Parsing
Accepts hex, RGB, HSL, and OKLCH formats:
import { parseColor, isValidColor } from '@uniweb/theming'
parseColor('#3b82f6') // { l: 0.623, c: 0.214, h: 259.8 }
parseColor('rgb(59, 130, 246)') // Same result
parseColor('hsl(217, 91%, 60%)') // Same result
parseColor('oklch(62.3% 0.214 259.8)') // Same result
isValidColor('#3b82f6') // true
isValidColor('not-a-color') // falseUtility Exports
import { formatOklch, formatHex, getShadeLevels } from '@uniweb/theming'
formatOklch(0.55, 0.2, 250) // 'oklch(55.0% 0.2000 250.0)'
formatHex(59, 130, 246) // '#3b82f6'
getShadeLevels() // [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]Theme Processing
Validate and process raw theme.yml into a complete configuration, merging with defaults and resolving named presets.
import { processTheme } from '@uniweb/theming'
const { config, errors, warnings } = processTheme({
colors: {
primary: '#e35d25',
neutral: 'stone', // Resolves to #78716c
},
contexts: {
dark: {
primary: 'primary-400', // Bare palette ref → var(--primary-400)
link: '#60a5fa', // Hex passes through
},
},
fonts: {
body: '"Inter", sans-serif',
import: [{ url: 'https://fonts.googleapis.com/css2?family=Inter' }],
},
appearance: 'light', // or { default: 'dark', allowToggle: true }
vars: {
'header-height': '5rem', // Override foundation variable
},
}, {
foundationVars: { // Declared by foundation
'header-height': { default: '4rem' },
'sidebar-width': { default: '280px' },
},
})Named Neutral Presets
The neutral color accepts preset names that map to Tailwind gray families:
| Preset | Hex | Character |
|---|---|---|
| stone | #78716c | Warm (default) |
| zinc | #71717a | Cool blue-gray |
| gray | #6b7280 | True gray |
| slate | #64748b | Cool with blue tint |
| neutral | #737373 | Pure gray |
Context Token Resolution
Content authors write bare palette references in theme.yml contexts:
contexts:
dark:
primary: primary-400
link: primary-300The processor resolves primary-400 to var(--primary-400). Plain CSS values (hex, var(), named colors) pass through unchanged.
Validation
import { validateThemeConfig } from '@uniweb/theming'
const { valid, errors } = validateThemeConfig({
colors: { primary: 'not-a-color' },
})
// valid: false
// errors: ['Color "primary" has invalid value: not-a-color']CSS Generation
Generate complete CSS from a processed theme configuration.
import { generateThemeCSS } from '@uniweb/theming'
const css = generateThemeCSS(config)The output includes (in order):
- Typography —
@importrules and--font-body,--font-heading,--font-monovariables - Color palettes —
--primary-50through--primary-950(and secondary, accent, neutral) on:root - Default semantic tokens —
--heading,--body,--link,--border, etc. on:root - Context classes —
.context-light,.context-medium,.context-darkwith full token sets - Foundation variables — Custom
--var-nameproperties from foundation defaults + site overrides - Dark scheme —
.scheme-darkclass and optionalprefers-color-schememedia query - Site background —
body { background: ... }if specified - Inline text styles —
span[accent],span[muted]for markdown inline styling
Semantic Tokens
Each context class sets these CSS custom properties:
| Token | Purpose |
|---|---|
| --section | Section background |
| --card | Card/surface background |
| --muted | Muted/disabled background |
| --body | Body text |
| --heading | Heading text |
| --subtle | Secondary/muted text |
| --border | Borders |
| --link / --link-hover | Link colors |
| --primary / --primary-foreground / --primary-hover / --primary-border | Primary button |
| --secondary / --secondary-foreground / --secondary-hover / --secondary-border | Secondary button |
| --success / --warning / --error / --info | Status colors |
| --ring | Focus ring |
Inspecting Defaults
import { getDefaultContextTokens, getDefaultColors } from '@uniweb/theming'
getDefaultColors()
// { primary: '#3b82f6', secondary: '#64748b', accent: '#8b5cf6', neutral: '#78716c' }
getDefaultContextTokens()
// { light: { section: 'var(--neutral-50)', ... }, medium: { ... }, dark: { ... } }Foundation Integration
Foundations declare customizable variables; sites set values in theme.yml. This package handles the merge.
import { extractFoundationVars, foundationHasVars } from '@uniweb/theming'
// Check if a foundation declares theme variables
foundationHasVars(schemaJson) // true/false
// Extract vars from a foundation module
const vars = extractFoundationVars(await import('./foundation/vars.js'))How It Fits in Uniweb
theme.yml → processTheme() → generateThemeCSS() → CSS injected at build time
↓
:root { --primary-500: ...; --heading: ...; }
.context-light { --section: var(--neutral-50); ... }
.context-dark { --section: var(--neutral-900); ... }
↓
Runtime applies .context-{theme} per section
↓
Components use var(--heading), var(--link), etc.The site controls the theme. The foundation declares what's customizable. Components use semantic tokens and adapt to any context automatically.
License
Apache-2.0
