| # Dynamic Theme System for @librechat/client | |
| This theme system allows you to dynamically change colors in your React application using CSS variables and Tailwind CSS. It combines dark/light mode switching with dynamic color theming capabilities. | |
| ## Table of Contents | |
| - [Overview](#overview) | |
| - [How It Works](#how-it-works) | |
| - [Basic Usage](#basic-usage) | |
| - [Available Theme Colors](#available-theme-colors) | |
| - [Creating Custom Themes](#creating-custom-themes) | |
| - [Environment Variable Themes](#environment-variable-themes) | |
| - [Dark/Light Mode](#darklight-mode) | |
| - [Migration Guide](#migration-guide) | |
| - [Implementation Details](#implementation-details) | |
| - [Troubleshooting](#troubleshooting) | |
| ## Overview | |
| The theme system provides: | |
| 1. **Dark/Light Mode Switching** - Automatic theme switching based on user preference | |
| 2. **Dynamic Color Theming** - Change colors at runtime without recompiling CSS | |
| 3. **CSS Variable Based** - Uses CSS custom properties for performance | |
| 4. **Tailwind Integration** - Works seamlessly with Tailwind CSS utilities | |
| 5. **TypeScript Support** - Full type safety for theme definitions | |
| ## How It Works | |
| The theme system operates in three layers: | |
| 1. **CSS Variables Layer**: Default colors defined in your app's CSS | |
| 2. **ThemeProvider Layer**: React context that manages theme state and applies CSS variables | |
| 3. **Tailwind Layer**: Maps CSS variables to Tailwind utility classes | |
| ### Default Behavior (No Custom Theme) | |
| - CSS variables cascade from your app's `style.css` definitions | |
| - Light mode uses variables under `html` selector | |
| - Dark mode uses variables under `.dark` selector | |
| - No JavaScript intervention in color values | |
| ### Custom Theme Behavior | |
| - Only applies when `themeRGB` prop is provided | |
| - Overrides CSS variables with `rgb()` formatted values | |
| - Maintains compatibility with existing CSS | |
| ## Basic Usage | |
| ### 1. Install the Component Library | |
| ```bash | |
| npm install @librechat/client | |
| ``` | |
| ### 2. Wrap Your App with ThemeProvider | |
| ```tsx | |
| import { ThemeProvider } from '@librechat/client'; | |
| function App() { | |
| return ( | |
| <ThemeProvider initialTheme="system"> | |
| <YourApp /> | |
| </ThemeProvider> | |
| ); | |
| } | |
| ``` | |
| ### 3. Set Up Your Base CSS | |
| Ensure your app has CSS variables defined as fallbacks: | |
| ```css | |
| /* style.css */ | |
| :root { | |
| --white: #fff; | |
| --gray-800: #212121; | |
| --gray-100: #ececec; | |
| /* ... other color definitions */ | |
| } | |
| html { | |
| --text-primary: var(--gray-800); | |
| --surface-primary: var(--white); | |
| /* ... other theme variables */ | |
| } | |
| .dark { | |
| --text-primary: var(--gray-100); | |
| --surface-primary: var(--gray-900); | |
| /* ... other dark theme variables */ | |
| } | |
| ``` | |
| ### 4. Configure Tailwind | |
| Update your `tailwind.config.js`: | |
| ```js | |
| module.exports = { | |
| content: [ | |
| './src/**/*.{js,jsx,ts,tsx}', | |
| // Include component library files | |
| './node_modules/@librechat/client/dist/**/*.js', | |
| ], | |
| darkMode: ['class'], | |
| theme: { | |
| extend: { | |
| colors: { | |
| // Map CSS variables to Tailwind colors | |
| 'text-primary': 'var(--text-primary)', | |
| 'surface-primary': 'var(--surface-primary)', | |
| 'brand-purple': 'var(--brand-purple)', | |
| // ... other colors | |
| }, | |
| }, | |
| }, | |
| }; | |
| ``` | |
| ### 5. Use Theme Colors in Components | |
| ```tsx | |
| function MyComponent() { | |
| return ( | |
| <div className="bg-surface-primary text-text-primary border border-border-light"> | |
| <h1 className="text-text-secondary">Hello World</h1> | |
| <button className="bg-surface-submit hover:bg-surface-submit-hover text-white"> | |
| Submit | |
| </button> | |
| </div> | |
| ); | |
| } | |
| ``` | |
| ## Available Theme Colors | |
| ### Text Colors | |
| - `text-text-primary` - Primary text color | |
| - `text-text-secondary` - Secondary text color | |
| - `text-text-secondary-alt` - Alternative secondary text | |
| - `text-text-tertiary` - Tertiary text color | |
| - `text-text-warning` - Warning text color | |
| ### Surface Colors | |
| - `bg-surface-primary` - Primary background | |
| - `bg-surface-secondary` - Secondary background | |
| - `bg-surface-tertiary` - Tertiary background | |
| - `bg-surface-submit` - Submit button background | |
| - `bg-surface-destructive` - Destructive action background | |
| - `bg-surface-dialog` - Dialog/modal background | |
| - `bg-surface-chat` - Chat interface background | |
| ### Border Colors | |
| - `border-border-light` - Light border | |
| - `border-border-medium` - Medium border | |
| - `border-border-heavy` - Heavy border | |
| - `border-border-xheavy` - Extra heavy border | |
| ### Other Colors | |
| - `bg-brand-purple` - Brand purple color | |
| - `bg-presentation` - Presentation background | |
| - `ring-ring-primary` - Focus ring color | |
| ## Creating Custom Themes | |
| ### 1. Define Your Theme | |
| ```tsx | |
| import { IThemeRGB } from '@librechat/client'; | |
| export const customTheme: IThemeRGB = { | |
| 'rgb-text-primary': '0 0 0', // Black | |
| 'rgb-text-secondary': '100 100 100', // Gray | |
| 'rgb-surface-primary': '255 255 255', // White | |
| 'rgb-surface-submit': '0 128 0', // Green | |
| 'rgb-brand-purple': '138 43 226', // Blue Violet | |
| // ... define other colors | |
| }; | |
| ``` | |
| ### 2. Use Your Custom Theme | |
| ```tsx | |
| import { ThemeProvider } from '@librechat/client'; | |
| import { customTheme } from './themes/custom'; | |
| function App() { | |
| return ( | |
| <ThemeProvider themeRGB={customTheme} themeName="custom"> | |
| <YourApp /> | |
| </ThemeProvider> | |
| ); | |
| } | |
| ``` | |
| ## Environment Variable Themes | |
| Load theme colors from environment variables: | |
| ### 1. Create Environment Variables | |
| ```env | |
| # .env.local | |
| REACT_APP_THEME_BRAND_PURPLE=171 104 255 | |
| REACT_APP_THEME_TEXT_PRIMARY=33 33 33 | |
| REACT_APP_THEME_TEXT_SECONDARY=66 66 66 | |
| REACT_APP_THEME_SURFACE_PRIMARY=255 255 255 | |
| REACT_APP_THEME_SURFACE_SUBMIT=4 120 87 | |
| ``` | |
| ### 2. Create a Theme Loader | |
| ```tsx | |
| function getThemeFromEnv(): IThemeRGB | undefined { | |
| // Check if any theme environment variables are set | |
| const hasThemeEnvVars = Object.keys(process.env).some(key => | |
| key.startsWith('REACT_APP_THEME_') | |
| ); | |
| if (!hasThemeEnvVars) { | |
| return undefined; // Use default themes | |
| } | |
| return { | |
| 'rgb-text-primary': process.env.REACT_APP_THEME_TEXT_PRIMARY || '33 33 33', | |
| 'rgb-brand-purple': process.env.REACT_APP_THEME_BRAND_PURPLE || '171 104 255', | |
| // ... other colors | |
| }; | |
| } | |
| ``` | |
| ### 3. Apply Environment Theme | |
| ```tsx | |
| <ThemeProvider | |
| initialTheme="system" | |
| themeRGB={getThemeFromEnv()} | |
| > | |
| <App /> | |
| </ThemeProvider> | |
| ``` | |
| ## Dark/Light Mode | |
| The ThemeProvider handles dark/light mode automatically: | |
| ### Using the Theme Hook | |
| ```tsx | |
| import { useTheme } from '@librechat/client'; | |
| function ThemeToggle() { | |
| const { theme, setTheme } = useTheme(); | |
| return ( | |
| <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}> | |
| Current theme: {theme} | |
| </button> | |
| ); | |
| } | |
| ``` | |
| ### Theme Options | |
| - `'light'` - Force light mode | |
| - `'dark'` - Force dark mode | |
| - `'system'` - Follow system preference | |
| ## Migration Guide | |
| If you're migrating from an older theme system: | |
| ### 1. Update Imports | |
| **Before:** | |
| ```tsx | |
| import { ThemeContext, ThemeProvider } from '~/hooks/ThemeContext'; | |
| ``` | |
| **After:** | |
| ```tsx | |
| import { ThemeContext, ThemeProvider } from '@librechat/client'; | |
| ``` | |
| ### 2. Update ThemeProvider Usage | |
| The new ThemeProvider is backward compatible but adds new capabilities: | |
| ```tsx | |
| <ThemeProvider | |
| initialTheme="system" // Same as before | |
| themeRGB={customTheme} // New: optional custom colors | |
| > | |
| <App /> | |
| </ThemeProvider> | |
| ``` | |
| ### 3. Existing Components | |
| Components using ThemeContext continue to work without changes: | |
| ```tsx | |
| // This still works! | |
| const { theme, setTheme } = useContext(ThemeContext); | |
| ``` | |
| ## Implementation Details | |
| ### File Structure | |
| ``` | |
| packages/client/src/theme/ | |
| ├── context/ | |
| │ └── ThemeProvider.tsx # Main theme provider | |
| ├── types/ | |
| │ └── index.ts # TypeScript interfaces | |
| ├── themes/ | |
| │ ├── default.ts # Light theme colors | |
| │ ├── dark.ts # Dark theme colors | |
| │ └── index.ts # Theme exports | |
| ├── utils/ | |
| │ ├── applyTheme.ts # Apply CSS variables | |
| │ ├── tailwindConfig.ts # Tailwind helpers | |
| │ └── createTailwindColors.js | |
| ├── README.md # This documentation | |
| └── index.ts # Main exports | |
| ``` | |
| ### CSS Variable Format | |
| The theme system uses RGB values in CSS variables: | |
| - CSS Variable: `--text-primary: rgb(33 33 33)` | |
| - Theme Definition: `'rgb-text-primary': '33 33 33'` | |
| - Tailwind Usage: `text-text-primary` | |
| ### RGB Format Requirements | |
| All color values must be in space-separated RGB format: | |
| - ✅ Correct: `'255 255 255'` | |
| - ❌ Incorrect: `'#ffffff'` or `'rgb(255, 255, 255)'` | |
| This format allows Tailwind to apply opacity modifiers like `bg-surface-primary/50`. | |
| ## Troubleshooting | |
| ### Common Issues | |
| #### 1. Colors Not Applying | |
| - **Issue**: Custom theme colors aren't showing | |
| - **Solution**: Ensure you're passing the `themeRGB` prop to ThemeProvider | |
| - **Check**: CSS variables in DevTools should show `rgb(R G B)` format | |
| #### 2. Circular Reference Errors | |
| - **Issue**: `--brand-purple: var(--brand-purple)` creates infinite loop | |
| - **Solution**: Use direct color values: `--brand-purple: #ab68ff` | |
| #### 3. Dark Mode Not Working | |
| - **Issue**: Dark mode doesn't switch | |
| - **Solution**: Ensure `darkMode: ['class']` is in your Tailwind config | |
| - **Check**: The `<html>` element should have `class="dark"` in dark mode | |
| #### 4. TypeScript Errors | |
| - **Issue**: Type errors when defining themes | |
| - **Solution**: Import and use the `IThemeRGB` interface: | |
| ```tsx | |
| import { IThemeRGB } from '@librechat/client'; | |
| ``` | |
| ### Debugging Tips | |
| 1. **Check CSS Variables**: Use browser DevTools to inspect computed CSS variables | |
| 2. **Verify Theme Application**: Look for inline styles on the root element | |
| 3. **Console Errors**: Check for validation errors in the console | |
| 4. **Test Isolation**: Try a minimal theme to isolate issues | |
| ## Examples | |
| ### Dynamic Theme Switching | |
| ```tsx | |
| import { ThemeProvider, defaultTheme, darkTheme } from '@librechat/client'; | |
| import { useState } from 'react'; | |
| function App() { | |
| const [isDark, setIsDark] = useState(false); | |
| return ( | |
| <ThemeProvider | |
| initialTheme={isDark ? 'dark' : 'light'} | |
| themeRGB={isDark ? darkTheme : defaultTheme} | |
| themeName={isDark ? 'dark' : 'default'} | |
| > | |
| <button onClick={() => setIsDark(!isDark)}> | |
| Toggle Theme | |
| </button> | |
| <YourApp /> | |
| </ThemeProvider> | |
| ); | |
| } | |
| ``` | |
| ### Multi-Theme Selector | |
| ```tsx | |
| const themes = { | |
| default: undefined, // Use CSS defaults | |
| ocean: { | |
| 'rgb-brand-purple': '0 119 190', | |
| 'rgb-surface-primary': '240 248 255', | |
| // ... ocean theme colors | |
| }, | |
| forest: { | |
| 'rgb-brand-purple': '34 139 34', | |
| 'rgb-surface-primary': '245 255 250', | |
| // ... forest theme colors | |
| }, | |
| }; | |
| function App() { | |
| const [selectedTheme, setSelectedTheme] = useState('default'); | |
| return ( | |
| <ThemeProvider | |
| themeRGB={themes[selectedTheme]} | |
| themeName={selectedTheme} | |
| > | |
| <select onChange={(e) => setSelectedTheme(e.target.value)}> | |
| {Object.keys(themes).map(name => ( | |
| <option key={name} value={name}>{name}</option> | |
| ))} | |
| </select> | |
| <YourApp /> | |
| </ThemeProvider> | |
| ); | |
| } | |
| ``` | |
| ### Using with the Main Application | |
| When using the ThemeProvider in your main application with localStorage persistence: | |
| ```tsx | |
| import { ThemeProvider } from '@librechat/client'; | |
| import { getThemeFromEnv } from './utils'; | |
| function App() { | |
| const envTheme = getThemeFromEnv(); | |
| return ( | |
| <ThemeProvider | |
| // Only pass props if you want to override stored values | |
| // If you always pass props, they will override localStorage | |
| initialTheme={envTheme ? "system" : undefined} | |
| themeRGB={envTheme || undefined} | |
| > | |
| {/* Your app content */} | |
| </ThemeProvider> | |
| ); | |
| } | |
| ``` | |
| **Important**: Props passed to ThemeProvider will override stored values on initial mount. Only pass props when you explicitly want to override the user's saved preferences. | |
| ## Contributing | |
| When adding new theme colors: | |
| 1. Add the type definition in `types/index.ts` | |
| 2. Add the color to default and dark themes | |
| 3. Update the applyTheme mapping | |
| 4. Add to Tailwind configuration | |
| 5. Document in this README | |
| ## License | |
| This theme system is part of the @librechat/client package. |