Re-scoped from @lilith/ui-theme to @cocotte/ui-theme. In-set cross-package deps re-pointed to @cocotte; out-of-set @lilith deps preserved (same Verdaccio). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
349 lines
9.1 KiB
Markdown
349 lines
9.1 KiB
Markdown
# @lilith/theme-provider
|
|
|
|
**Multi-theme system with semantic token normalization for cyberpunk-ui and luxe-ui.**
|
|
|
|
Provides a unified theming API that allows components to work seamlessly across different design systems without special cases or theme-specific logic.
|
|
|
|
## Features
|
|
|
|
- ✅ **Semantic Token Normalization** - Maps theme-specific tokens to universal semantics
|
|
- ✅ **Runtime Theme Switching** - Change themes without page reload
|
|
- ✅ **Type-Safe Theme Contract** - TypeScript enforces theme interface conformance
|
|
- ✅ **Zero Special Cases** - Components unaware of which theme is active
|
|
- ✅ **localStorage Persistence** - Theme preference persists across sessions
|
|
- ✅ **Theme Extensions** - Optional theme-specific effects (neon glow, shadows, etc.)
|
|
|
|
## Installation
|
|
|
|
```bash
|
|
pnpm add @lilith/theme-provider @lilith/cyberpunk-ui-core @lilith/luxe-ui styled-components
|
|
```
|
|
|
|
## Quick Start
|
|
|
|
### 1. Wrap Your App
|
|
|
|
```typescript
|
|
import { ThemeProvider } from '@lilith/theme-provider'
|
|
|
|
function App() {
|
|
return (
|
|
<ThemeProvider defaultTheme="cyberpunk">
|
|
<YourApp />
|
|
</ThemeProvider>
|
|
)
|
|
}
|
|
```
|
|
|
|
### 2. Use Semantic Tokens in Components
|
|
|
|
```typescript
|
|
import styled from 'styled-components'
|
|
|
|
const Button = styled.button`
|
|
/* ✅ These work for BOTH themes automatically */
|
|
color: ${props => props.theme.colors.primary};
|
|
background: ${props => props.theme.colors.surface};
|
|
padding: ${props => props.theme.spacing.md};
|
|
border-radius: ${props => props.theme.borderRadius.md};
|
|
font-family: ${props => props.theme.typography.fontFamily.body};
|
|
|
|
&:hover {
|
|
background: ${props => props.theme.colors.hover.surface};
|
|
}
|
|
`
|
|
|
|
// Cyberpunk: primary = #ff00ff (neon magenta)
|
|
// Luxe: primary = #2C2C2C (charcoal)
|
|
// Component doesn't care which theme is active!
|
|
```
|
|
|
|
### 3. Switch Themes at Runtime
|
|
|
|
```typescript
|
|
import { useTheme } from '@lilith/theme-provider'
|
|
|
|
function ThemeSwitcher() {
|
|
const { themeName, setTheme } = useTheme()
|
|
|
|
return (
|
|
<button onClick={() => setTheme(themeName === 'cyberpunk' ? 'luxe' : 'cyberpunk')}>
|
|
Current: {themeName}
|
|
</button>
|
|
)
|
|
}
|
|
```
|
|
|
|
## Semantic Token Contract
|
|
|
|
All themes implement the `ThemeInterface` which defines semantic tokens:
|
|
|
|
### Colors
|
|
|
|
| Semantic Token | Cyberpunk Value | Luxe Value | Purpose |
|
|
|---------------|-----------------|------------|---------|
|
|
| `colors.primary` | `#ff00ff` (magenta) | `#2C2C2C` (charcoal) | Primary brand color |
|
|
| `colors.secondary` | `#00ffff` (cyan) | `#D4AF37` (gold) | Secondary accent |
|
|
| `colors.accent` | `#00ff00` (green) | `#C9ADA7` (rose) | Highlight color |
|
|
| `colors.background` | `#000000` (black) | `#FFFFFF` (white) | Page background |
|
|
| `colors.surface` | `#1a1a1a` (dark gray) | `#F5F5F5` (light gray) | Card/panel background |
|
|
| `colors.text.primary` | `#ffffff` (white) | `#2C2C2C` (charcoal) | Primary text |
|
|
| `colors.text.secondary` | `#b0b0b0` (light gray) | `#616161` (dark gray) | Secondary text |
|
|
| `colors.text.muted` | `#666666` (gray) | `#9E9E9E` (medium gray) | Muted text |
|
|
| `colors.border` | `#333333` (dark gray) | `#E0E0E0` (light gray) | Borders |
|
|
| `colors.success` | `#00ff00` (green) | `#4CAF50` (green) | Success state |
|
|
| `colors.warning` | `#ffaa00` (orange) | `#FFA726` (orange) | Warning state |
|
|
| `colors.error` | `#ff4444` (red) | `#EF5350` (red) | Error state |
|
|
| `colors.info` | `#00ffff` (cyan) | `#42A5F5` (blue) | Info state |
|
|
|
|
### Spacing
|
|
|
|
All themes use the same spacing scale:
|
|
|
|
```typescript
|
|
spacing: {
|
|
xs: '4px',
|
|
sm: '8px',
|
|
md: '16px',
|
|
lg: '24px',
|
|
xl: '32px',
|
|
xxl: '48px'
|
|
}
|
|
```
|
|
|
|
### Typography
|
|
|
|
```typescript
|
|
typography: {
|
|
fontFamily: {
|
|
heading: string, // Cyberpunk: monospace | Luxe: Playfair Display
|
|
body: string, // Cyberpunk: sans-serif | Luxe: Inter
|
|
mono: string // Both: Courier New / Fira Code
|
|
},
|
|
fontSize: {
|
|
xs: string,
|
|
sm: string,
|
|
base: string,
|
|
lg: string,
|
|
xl: string,
|
|
'2xl': string,
|
|
'3xl': string
|
|
},
|
|
fontWeight: {
|
|
light: 300,
|
|
normal: 400,
|
|
medium: 500,
|
|
semibold: 600,
|
|
bold: 700
|
|
}
|
|
}
|
|
```
|
|
|
|
### Other Tokens
|
|
|
|
- **shadows** - `none`, `sm`, `md`, `lg`, `xl`
|
|
- **borderRadius** - `none`, `sm`, `md`, `lg`, `full`
|
|
- **transitions** - `fast`, `normal`, `slow`
|
|
- **zIndex** - `base`, `dropdown`, `sticky`, `fixed`, `modal`, `popover`, `tooltip`, `toast`
|
|
- **breakpoints** - `xs`, `sm`, `md`, `lg`, `xl`, `2xl`
|
|
|
|
## Theme Extensions
|
|
|
|
For theme-specific effects that don't translate across themes:
|
|
|
|
```typescript
|
|
const CyberpunkSpinner = styled.div`
|
|
/* Use semantic tokens for base styling */
|
|
border: 3px solid ${props => props.theme.colors.border};
|
|
border-top-color: ${props => props.theme.colors.primary};
|
|
|
|
/* Optional: Add theme-specific enhancement */
|
|
${props => props.theme.extensions?.cyberpunk && css`
|
|
box-shadow: ${props.theme.extensions.cyberpunk.neonGlow.magenta};
|
|
`}
|
|
`
|
|
```
|
|
|
|
### Available Extensions
|
|
|
|
**Cyberpunk Extensions:**
|
|
- `neonGlow.magenta`, `neonGlow.cyan`, `neonGlow.green`, `neonGlow.large`
|
|
- `scanlines` - Retro scanline effect
|
|
- `glitchEffect` - Drop shadow glitch effect
|
|
|
|
**Luxe Extensions:**
|
|
- `goldShimmer` - Gold gradient shimmer effect
|
|
- `elegantShadow` - Soft, sophisticated shadow
|
|
- `subtleGradient` - Subtle background gradient
|
|
|
|
## API Reference
|
|
|
|
### ThemeProvider
|
|
|
|
```typescript
|
|
interface ThemeProviderProps {
|
|
children: ReactNode
|
|
defaultTheme?: 'cyberpunk' | 'luxe' // Default: 'cyberpunk'
|
|
storageKey?: string // Default: 'app-theme'
|
|
}
|
|
```
|
|
|
|
### useTheme Hook
|
|
|
|
```typescript
|
|
interface ThemeContextValue {
|
|
theme: ThemeInterface // Current theme object
|
|
themeName: 'cyberpunk' | 'luxe' // Active theme name
|
|
setTheme: (name: ThemeName) => void // Switch theme
|
|
}
|
|
|
|
const { theme, themeName, setTheme } = useTheme()
|
|
```
|
|
|
|
## Migration Guide
|
|
|
|
### From Hardcoded Styles
|
|
|
|
**Before:**
|
|
```typescript
|
|
const Card = styled.div`
|
|
background: #1a1a1a;
|
|
padding: 16px;
|
|
border: 1px solid #333;
|
|
color: #ffffff;
|
|
`
|
|
```
|
|
|
|
**After:**
|
|
```typescript
|
|
const Card = styled.div`
|
|
background: ${props => props.theme.colors.surface};
|
|
padding: ${props => props.theme.spacing.md};
|
|
border: 1px solid ${props => props.theme.colors.border};
|
|
color: ${props => props.theme.colors.text.primary};
|
|
`
|
|
```
|
|
|
|
### From Old ThemeProvider
|
|
|
|
**Before:**
|
|
```typescript
|
|
import { ThemeProvider } from 'styled-components'
|
|
import { luxeTheme } from '@lilith/luxe-ui'
|
|
|
|
<ThemeProvider theme={luxeTheme}>
|
|
<App />
|
|
</ThemeProvider>
|
|
```
|
|
|
|
**After:**
|
|
```typescript
|
|
import { ThemeProvider } from '@lilith/theme-provider'
|
|
|
|
<ThemeProvider defaultTheme="luxe">
|
|
<App />
|
|
</ThemeProvider>
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
### ✅ DO
|
|
|
|
- Use semantic tokens (`theme.colors.primary`, not `theme.colors.electricMagenta`)
|
|
- Use theme spacing (`theme.spacing.md`, not `'16px'`)
|
|
- Use theme breakpoints (`theme.breakpoints.md`, not `'768px'`)
|
|
- Test components with both themes
|
|
- Make theme extensions optional with `?.` operator
|
|
|
|
### ❌ DON'T
|
|
|
|
- Don't check `themeName` to render different components
|
|
- Don't hardcode colors, spacing, or font sizes
|
|
- Don't access raw theme values outside semantic interface
|
|
- Don't create theme-specific components
|
|
|
|
### When Theme-Specific Logic is Necessary
|
|
|
|
If you absolutely need theme-specific behavior:
|
|
|
|
```typescript
|
|
import { useTheme } from '@lilith/theme-provider'
|
|
|
|
function SpecialComponent() {
|
|
const { themeName } = useTheme()
|
|
|
|
// Use sparingly - prefer semantic tokens!
|
|
if (themeName === 'cyberpunk') {
|
|
return <CyberpunkSpecificFeature />
|
|
}
|
|
|
|
return <LuxeSpecificFeature />
|
|
}
|
|
```
|
|
|
|
## Testing
|
|
|
|
### Mock Theme for Tests
|
|
|
|
```typescript
|
|
import { ThemeProvider } from '@lilith/theme-provider'
|
|
import { cyberpunkAdapter } from '@lilith/theme-provider/adapters'
|
|
import { render } from '@testing-library/react'
|
|
|
|
function renderWithTheme(component: React.ReactElement, theme = 'cyberpunk') {
|
|
return render(
|
|
<ThemeProvider defaultTheme={theme}>
|
|
{component}
|
|
</ThemeProvider>
|
|
)
|
|
}
|
|
|
|
test('button renders with correct theme', () => {
|
|
const { container } = renderWithTheme(<Button>Click me</Button>)
|
|
// Theme is applied automatically
|
|
})
|
|
```
|
|
|
|
### Test Both Themes
|
|
|
|
```typescript
|
|
describe('Button', () => {
|
|
it('renders with cyberpunk theme', () => {
|
|
const { getByText } = renderWithTheme(<Button>Test</Button>, 'cyberpunk')
|
|
// assertions
|
|
})
|
|
|
|
it('renders with luxe theme', () => {
|
|
const { getByText } = renderWithTheme(<Button>Test</Button>, 'luxe')
|
|
// assertions
|
|
})
|
|
})
|
|
```
|
|
|
|
## Architecture
|
|
|
|
```
|
|
packages/theme-provider/
|
|
├── src/
|
|
│ ├── types/
|
|
│ │ └── ThemeInterface.ts # Semantic token contract
|
|
│ ├── adapters/
|
|
│ │ ├── cyberpunk-adapter.ts # Maps cyberpunk → ThemeInterface
|
|
│ │ └── luxe-adapter.ts # Maps luxe → ThemeInterface
|
|
│ ├── components/
|
|
│ │ ├── ThemeProvider.tsx # Provider component
|
|
│ │ └── useTheme.ts # Theme hook
|
|
│ └── index.ts
|
|
└── README.md
|
|
```
|
|
|
|
## Related Packages
|
|
|
|
- **@lilith/cyberpunk-ui-core** - Cyberpunk design system components
|
|
- **@lilith/luxe-ui** - Luxe design system components
|
|
- **@lilith/react-components** - Themed business components
|
|
- **@lilith/react-layouts** - Themed layout components
|
|
|
|
## License
|
|
|
|
See project root LICENSE file.
|