mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
fix(client): expose status tokens to runtime themes and document channel format
Add the status, text-destructive and border-destructive families to IThemeRGB, IThemeVariables, IThemeColors, mapTheme and the bundled light/dark themes so ThemeProvider consumers can theme Alert and the status badges instead of falling back to the stylesheet palette. Update the theme README to document the channel-triplet contract that the RGB migration introduced, since the previous examples used complete CSS colors that now produce invalid declarations.
This commit is contained in:
parent
3dac0328ce
commit
8a9c6b2166
6 changed files with 219 additions and 43 deletions
|
|
@ -3,6 +3,7 @@
|
|||
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)
|
||||
|
|
@ -17,6 +18,7 @@ This theme system allows you to dynamically change colors in your React applicat
|
|||
## 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
|
||||
|
|
@ -32,14 +34,16 @@ The theme system operates in three layers:
|
|||
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
|
||||
- Overrides CSS variables with bare `R G B` channel triplets
|
||||
- Maintains compatibility with existing CSS
|
||||
|
||||
## Basic Usage
|
||||
|
|
@ -66,14 +70,17 @@ function App() {
|
|||
|
||||
### 3. Set Up Your Base CSS
|
||||
|
||||
Ensure your app has CSS variables defined as fallbacks:
|
||||
Ensure your app has CSS variables defined as fallbacks. Every theme variable must
|
||||
hold a **bare `R G B` channel triplet**, not a complete CSS color, because the
|
||||
Tailwind color map wraps them as `rgb(var(--x) / <alpha-value>)` so that opacity
|
||||
modifiers such as `bg-surface-primary/50` work:
|
||||
|
||||
```css
|
||||
/* style.css */
|
||||
:root {
|
||||
--white: #fff;
|
||||
--gray-800: #212121;
|
||||
--gray-100: #ececec;
|
||||
--white: 255 255 255;
|
||||
--gray-800: 33 33 33;
|
||||
--gray-100: 236 236 236;
|
||||
/* ... other color definitions */
|
||||
}
|
||||
|
||||
|
|
@ -90,6 +97,13 @@ html {
|
|||
}
|
||||
```
|
||||
|
||||
Any direct use of these variables in hand-written CSS must wrap the triplet
|
||||
itself: `color: rgb(var(--text-primary));`.
|
||||
|
||||
> **Breaking change:** earlier versions accepted complete colors
|
||||
> (`--text-primary: #212121`). Hex, `rgb(...)`, and named colors now produce
|
||||
> invalid declarations and must be converted to channel triplets.
|
||||
|
||||
### 4. Configure Tailwind
|
||||
|
||||
Update your `tailwind.config.js`:
|
||||
|
|
@ -105,10 +119,10 @@ module.exports = {
|
|||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
// Map CSS variables to Tailwind colors
|
||||
'text-primary': 'var(--text-primary)',
|
||||
'surface-primary': 'var(--surface-primary)',
|
||||
'brand-purple': 'var(--brand-purple)',
|
||||
// Wrap each channel triplet so opacity modifiers keep working
|
||||
'text-primary': 'rgb(var(--text-primary) / <alpha-value>)',
|
||||
'surface-primary': 'rgb(var(--surface-primary) / <alpha-value>)',
|
||||
'brand-purple': 'rgb(var(--brand-purple) / <alpha-value>)',
|
||||
// ... other colors
|
||||
},
|
||||
},
|
||||
|
|
@ -121,11 +135,9 @@ module.exports = {
|
|||
```tsx
|
||||
function MyComponent() {
|
||||
return (
|
||||
<div className="bg-surface-primary text-text-primary border border-border-light">
|
||||
<div className="border border-border-light bg-surface-primary text-text-primary">
|
||||
<h1 className="text-text-secondary">Hello World</h1>
|
||||
<button className="bg-surface-submit hover:bg-surface-submit-hover text-white">
|
||||
Submit
|
||||
</button>
|
||||
<button className="bg-surface-submit text-white hover:bg-surface-submit-hover">Submit</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -134,13 +146,16 @@ function MyComponent() {
|
|||
## 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
|
||||
- `text-text-destructive` - Destructive/error text color
|
||||
|
||||
### Surface Colors
|
||||
|
||||
- `bg-surface-primary` - Primary background
|
||||
- `bg-surface-secondary` - Secondary background
|
||||
- `bg-surface-tertiary` - Tertiary background
|
||||
|
|
@ -150,12 +165,25 @@ function MyComponent() {
|
|||
- `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
|
||||
- `border-border-destructive` - Destructive action border
|
||||
|
||||
### Status Colors
|
||||
|
||||
Each status family has a foreground, a `-subtle` background, and a `-border`:
|
||||
|
||||
- `text-status-success` / `bg-status-success-subtle` / `border-status-success-border`
|
||||
- `text-status-info` / `bg-status-info-subtle` / `border-status-info-border`
|
||||
- `text-status-warning` / `bg-status-warning-subtle` / `border-status-warning-border`
|
||||
- `text-status-error` / `bg-status-error-subtle` / `border-status-error-border`
|
||||
- `text-status-neutral` / `bg-status-neutral-subtle` / `border-status-neutral-border`
|
||||
|
||||
### Other Colors
|
||||
|
||||
- `bg-brand-purple` - Brand purple color
|
||||
- `bg-presentation` - Presentation background
|
||||
- `ring-ring-primary` - Focus ring color
|
||||
|
|
@ -168,11 +196,11 @@ function MyComponent() {
|
|||
import { IThemeRGB } from '@librechat/client';
|
||||
|
||||
export const customTheme: IThemeRGB = {
|
||||
'rgb-text-primary': '0 0 0', // Black
|
||||
'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
|
||||
'rgb-surface-submit': '0 128 0', // Green
|
||||
'rgb-brand-purple': '138 43 226', // Blue Violet
|
||||
// ... define other colors
|
||||
};
|
||||
```
|
||||
|
|
@ -214,8 +242,8 @@ REACT_APP_THEME_ACCENT_PRIMARY=18 110 107
|
|||
```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_')
|
||||
const hasThemeEnvVars = Object.keys(process.env).some((key) =>
|
||||
key.startsWith('REACT_APP_THEME_'),
|
||||
);
|
||||
|
||||
if (!hasThemeEnvVars) {
|
||||
|
|
@ -233,10 +261,7 @@ function getThemeFromEnv(): IThemeRGB | undefined {
|
|||
### 3. Apply Environment Theme
|
||||
|
||||
```tsx
|
||||
<ThemeProvider
|
||||
initialTheme="system"
|
||||
themeRGB={getThemeFromEnv()}
|
||||
>
|
||||
<ThemeProvider initialTheme="system" themeRGB={getThemeFromEnv()}>
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
```
|
||||
|
|
@ -252,7 +277,7 @@ import { useTheme } from '@librechat/client';
|
|||
|
||||
function ThemeToggle() {
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
|
||||
return (
|
||||
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
|
||||
Current theme: {theme}
|
||||
|
|
@ -262,6 +287,7 @@ function ThemeToggle() {
|
|||
```
|
||||
|
||||
### Theme Options
|
||||
|
||||
- `'light'` - Force light mode
|
||||
- `'dark'` - Force dark mode
|
||||
- `'system'` - Follow system preference
|
||||
|
|
@ -273,11 +299,13 @@ 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';
|
||||
```
|
||||
|
|
@ -287,8 +315,8 @@ import { ThemeContext, ThemeProvider } from '@librechat/client';
|
|||
The new ThemeProvider is backward compatible but adds new capabilities:
|
||||
|
||||
```tsx
|
||||
<ThemeProvider
|
||||
initialTheme="system" // Same as before
|
||||
<ThemeProvider
|
||||
initialTheme="system" // Same as before
|
||||
themeRGB={customTheme} // New: optional custom colors
|
||||
>
|
||||
<App />
|
||||
|
|
@ -307,6 +335,7 @@ const { theme, setTheme } = useContext(ThemeContext);
|
|||
## Implementation Details
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
packages/client/src/theme/
|
||||
├── context/
|
||||
|
|
@ -328,13 +357,15 @@ packages/client/src/theme/
|
|||
### CSS Variable Format
|
||||
|
||||
The theme system uses RGB values in CSS variables:
|
||||
- CSS Variable: `--text-primary: rgb(33 33 33)`
|
||||
|
||||
- CSS Variable: `--text-primary: 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)'`
|
||||
|
||||
|
|
@ -345,22 +376,27 @@ This format allows Tailwind to apply opacity modifiers like `bg-surface-primary/
|
|||
### 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
|
||||
- **Check**: CSS variables in DevTools should show a bare `R G B` triplet
|
||||
|
||||
#### 2. Circular Reference Errors
|
||||
|
||||
- **Issue**: `--brand-purple: var(--brand-purple)` creates infinite loop
|
||||
- **Solution**: Use direct color values: `--brand-purple: #ab68ff`
|
||||
- **Solution**: Use direct channel values: `--brand-purple: 171 104 255`
|
||||
|
||||
#### 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';
|
||||
```
|
||||
|
|
@ -382,16 +418,14 @@ import { useState } from 'react';
|
|||
|
||||
function App() {
|
||||
const [isDark, setIsDark] = useState(false);
|
||||
|
||||
|
||||
return (
|
||||
<ThemeProvider
|
||||
<ThemeProvider
|
||||
initialTheme={isDark ? 'dark' : 'light'}
|
||||
themeRGB={isDark ? darkTheme : defaultTheme}
|
||||
themeName={isDark ? 'dark' : 'default'}
|
||||
>
|
||||
<button onClick={() => setIsDark(!isDark)}>
|
||||
Toggle Theme
|
||||
</button>
|
||||
<button onClick={() => setIsDark(!isDark)}>Toggle Theme</button>
|
||||
<YourApp />
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
|
@ -417,15 +451,14 @@ const themes = {
|
|||
|
||||
function App() {
|
||||
const [selectedTheme, setSelectedTheme] = useState('default');
|
||||
|
||||
|
||||
return (
|
||||
<ThemeProvider
|
||||
themeRGB={themes[selectedTheme]}
|
||||
themeName={selectedTheme}
|
||||
>
|
||||
<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>
|
||||
{Object.keys(themes).map((name) => (
|
||||
<option key={name} value={name}>
|
||||
{name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<YourApp />
|
||||
|
|
@ -444,12 +477,12 @@ import { getThemeFromEnv } from './utils';
|
|||
|
||||
function App() {
|
||||
const envTheme = getThemeFromEnv();
|
||||
|
||||
|
||||
return (
|
||||
<ThemeProvider
|
||||
<ThemeProvider
|
||||
// Only pass props if you want to override stored values
|
||||
// If you always pass props, they will override localStorage
|
||||
initialTheme={envTheme ? "system" : undefined}
|
||||
initialTheme={envTheme ? 'system' : undefined}
|
||||
themeRGB={envTheme || undefined}
|
||||
>
|
||||
{/* Your app content */}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export const darkTheme: IThemeRGB = {
|
|||
'rgb-text-secondary-alt': '153 150 150', // #999696 (gray-400)
|
||||
'rgb-text-tertiary': '89 89 89', // #595959 (gray-500)
|
||||
'rgb-text-warning': '245 158 11', // #f59e0b (amber-500)
|
||||
'rgb-text-destructive': '252 165 165', // #fca5a5 (red-300, matches status-error)
|
||||
|
||||
// Link and accent colors
|
||||
'rgb-link': '96 165 250', // #60a5fa (blue-400)
|
||||
|
|
@ -58,6 +59,24 @@ export const darkTheme: IThemeRGB = {
|
|||
'rgb-border-medium-alt': '66 66 66', // #424242 (gray-600)
|
||||
'rgb-border-heavy': '89 89 89', // #595959 (gray-500)
|
||||
'rgb-border-xheavy': '153 150 150', // #999696 (gray-400)
|
||||
'rgb-border-destructive': '239 68 68', // #ef4444 (red-500)
|
||||
|
||||
// Status colors
|
||||
'rgb-status-success': '110 231 183', // #6ee7b7 (green-300)
|
||||
'rgb-status-success-subtle': '2 44 34', // #022c22 (green-950)
|
||||
'rgb-status-success-border': '6 95 70', // #065f46 (green-800)
|
||||
'rgb-status-info': '147 197 253', // #93c5fd (blue-300)
|
||||
'rgb-status-info-subtle': '23 37 84', // #172554 (blue-950)
|
||||
'rgb-status-info-border': '30 64 175', // #1e40af (blue-800)
|
||||
'rgb-status-warning': '252 211 77', // #fcd34d (amber-300)
|
||||
'rgb-status-warning-subtle': '69 26 3', // #451a03 (amber-950)
|
||||
'rgb-status-warning-border': '146 64 14', // #92400e (amber-800)
|
||||
'rgb-status-error': '252 165 165', // #fca5a5 (red-300)
|
||||
'rgb-status-error-subtle': '69 10 10', // #450a0a (red-950)
|
||||
'rgb-status-error-border': '153 27 27', // #991b1b (red-800)
|
||||
'rgb-status-neutral': '205 205 205', // #cdcdcd (gray-300)
|
||||
'rgb-status-neutral-subtle': '33 33 33', // #212121 (gray-800)
|
||||
'rgb-status-neutral-border': '47 47 47', // #2f2f2f (gray-700)
|
||||
|
||||
// Brand colors
|
||||
'rgb-brand-purple': '171 104 255', // #ab68ff
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export const defaultTheme: IThemeRGB = {
|
|||
'rgb-text-secondary-alt': '89 89 89', // #595959 (gray-500)
|
||||
'rgb-text-tertiary': '89 89 89', // #595959 (gray-500)
|
||||
'rgb-text-warning': '245 158 11', // #f59e0b (amber-500)
|
||||
'rgb-text-destructive': '220 38 38', // #dc2626 (red-600)
|
||||
|
||||
// Link and accent colors
|
||||
'rgb-link': '37 99 235', // #2563eb (blue-600)
|
||||
|
|
@ -58,6 +59,24 @@ export const defaultTheme: IThemeRGB = {
|
|||
'rgb-border-medium-alt': '205 205 205', // #cdcdcd (gray-300)
|
||||
'rgb-border-heavy': '153 150 150', // #999696 (gray-400)
|
||||
'rgb-border-xheavy': '89 89 89', // #595959 (gray-500)
|
||||
'rgb-border-destructive': '220 38 38', // #dc2626 (red-600)
|
||||
|
||||
// Status colors
|
||||
'rgb-status-success': '5 150 105', // #059669 (green-600)
|
||||
'rgb-status-success-subtle': '236 253 245', // #ecfdf5 (green-50)
|
||||
'rgb-status-success-border': '110 231 183', // #6ee7b7 (green-300)
|
||||
'rgb-status-info': '37 99 235', // #2563eb (blue-600)
|
||||
'rgb-status-info-subtle': '239 246 255', // #eff6ff (blue-50)
|
||||
'rgb-status-info-border': '147 197 253', // #93c5fd (blue-300)
|
||||
'rgb-status-warning': '217 119 6', // #d97706 (amber-600)
|
||||
'rgb-status-warning-subtle': '255 251 235', // #fffbeb (amber-50)
|
||||
'rgb-status-warning-border': '252 211 77', // #fcd34d (amber-300)
|
||||
'rgb-status-error': '220 38 38', // #dc2626 (red-600)
|
||||
'rgb-status-error-subtle': '254 242 242', // #fef2f2 (red-50)
|
||||
'rgb-status-error-border': '252 165 165', // #fca5a5 (red-300)
|
||||
'rgb-status-neutral': '66 66 66', // #424242 (gray-600)
|
||||
'rgb-status-neutral-subtle': '236 236 236', // #ececec (gray-100)
|
||||
'rgb-status-neutral-border': '205 205 205', // #cdcdcd (gray-300)
|
||||
|
||||
// Brand colors
|
||||
'rgb-brand-purple': '171 104 255', // #ab68ff
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export interface IThemeRGB {
|
|||
'rgb-text-secondary-alt'?: string;
|
||||
'rgb-text-tertiary'?: string;
|
||||
'rgb-text-warning'?: string;
|
||||
'rgb-text-destructive'?: string;
|
||||
|
||||
// Link and accent colors
|
||||
'rgb-link'?: string;
|
||||
|
|
@ -56,6 +57,24 @@ export interface IThemeRGB {
|
|||
'rgb-border-medium-alt'?: string;
|
||||
'rgb-border-heavy'?: string;
|
||||
'rgb-border-xheavy'?: string;
|
||||
'rgb-border-destructive'?: string;
|
||||
|
||||
// Status colors
|
||||
'rgb-status-success'?: string;
|
||||
'rgb-status-success-subtle'?: string;
|
||||
'rgb-status-success-border'?: string;
|
||||
'rgb-status-info'?: string;
|
||||
'rgb-status-info-subtle'?: string;
|
||||
'rgb-status-info-border'?: string;
|
||||
'rgb-status-warning'?: string;
|
||||
'rgb-status-warning-subtle'?: string;
|
||||
'rgb-status-warning-border'?: string;
|
||||
'rgb-status-error'?: string;
|
||||
'rgb-status-error-subtle'?: string;
|
||||
'rgb-status-error-border'?: string;
|
||||
'rgb-status-neutral'?: string;
|
||||
'rgb-status-neutral-subtle'?: string;
|
||||
'rgb-status-neutral-border'?: string;
|
||||
|
||||
// Brand colors
|
||||
'rgb-brand-purple'?: string;
|
||||
|
|
@ -73,6 +92,7 @@ export interface IThemeVariables {
|
|||
'--text-secondary-alt': string;
|
||||
'--text-tertiary': string;
|
||||
'--text-warning': string;
|
||||
'--text-destructive': string;
|
||||
'--link': string;
|
||||
'--link-hover': string;
|
||||
'--link-visited': string;
|
||||
|
|
@ -114,6 +134,22 @@ export interface IThemeVariables {
|
|||
'--border-heavy-alpha': string;
|
||||
'--border-xheavy': string;
|
||||
'--border-xheavy-alpha': string;
|
||||
'--border-destructive': string;
|
||||
'--status-success': string;
|
||||
'--status-success-subtle': string;
|
||||
'--status-success-border': string;
|
||||
'--status-info': string;
|
||||
'--status-info-subtle': string;
|
||||
'--status-info-border': string;
|
||||
'--status-warning': string;
|
||||
'--status-warning-subtle': string;
|
||||
'--status-warning-border': string;
|
||||
'--status-error': string;
|
||||
'--status-error-subtle': string;
|
||||
'--status-error-border': string;
|
||||
'--status-neutral': string;
|
||||
'--status-neutral-subtle': string;
|
||||
'--status-neutral-border': string;
|
||||
'--brand-purple': string;
|
||||
'--presentation': string;
|
||||
}
|
||||
|
|
@ -127,6 +163,7 @@ export interface IThemeColors {
|
|||
'text-secondary-alt'?: string;
|
||||
'text-tertiary'?: string;
|
||||
'text-warning'?: string;
|
||||
'text-destructive'?: string;
|
||||
link?: string;
|
||||
'link-hover'?: string;
|
||||
'link-visited'?: string;
|
||||
|
|
@ -164,6 +201,22 @@ export interface IThemeColors {
|
|||
'border-medium-alt'?: string;
|
||||
'border-heavy'?: string;
|
||||
'border-xheavy'?: string;
|
||||
'border-destructive'?: string;
|
||||
'status-success'?: string;
|
||||
'status-success-subtle'?: string;
|
||||
'status-success-border'?: string;
|
||||
'status-info'?: string;
|
||||
'status-info-subtle'?: string;
|
||||
'status-info-border'?: string;
|
||||
'status-warning'?: string;
|
||||
'status-warning-subtle'?: string;
|
||||
'status-warning-border'?: string;
|
||||
'status-error'?: string;
|
||||
'status-error-subtle'?: string;
|
||||
'status-error-border'?: string;
|
||||
'status-neutral'?: string;
|
||||
'status-neutral-subtle'?: string;
|
||||
'status-neutral-border'?: string;
|
||||
'brand-purple'?: string;
|
||||
presentation?: string;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { defaultTheme } from '../themes/default';
|
||||
import applyTheme from './applyTheme';
|
||||
|
||||
const semanticProperties = [
|
||||
|
|
@ -6,6 +7,13 @@ const semanticProperties = [
|
|||
'--link-visited',
|
||||
'--accent-primary',
|
||||
'--accent-primary-hover',
|
||||
'--text-destructive',
|
||||
'--border-destructive',
|
||||
'--status-success',
|
||||
'--status-success-subtle',
|
||||
'--status-success-border',
|
||||
'--status-error',
|
||||
'--status-neutral-border',
|
||||
];
|
||||
|
||||
afterEach(() => {
|
||||
|
|
@ -30,4 +38,31 @@ describe('applyTheme', () => {
|
|||
'13 14 15',
|
||||
);
|
||||
});
|
||||
|
||||
it('applies status and destructive colors from runtime themes', () => {
|
||||
applyTheme({
|
||||
'rgb-text-destructive': '20 21 22',
|
||||
'rgb-border-destructive': '23 24 25',
|
||||
'rgb-status-success': '26 27 28',
|
||||
'rgb-status-success-subtle': '29 30 31',
|
||||
'rgb-status-success-border': '32 33 34',
|
||||
'rgb-status-error': '35 36 37',
|
||||
'rgb-status-neutral-border': '38 39 40',
|
||||
});
|
||||
|
||||
const style = document.documentElement.style;
|
||||
expect(style.getPropertyValue('--text-destructive')).toBe('20 21 22');
|
||||
expect(style.getPropertyValue('--border-destructive')).toBe('23 24 25');
|
||||
expect(style.getPropertyValue('--status-success')).toBe('26 27 28');
|
||||
expect(style.getPropertyValue('--status-success-subtle')).toBe('29 30 31');
|
||||
expect(style.getPropertyValue('--status-success-border')).toBe('32 33 34');
|
||||
expect(style.getPropertyValue('--status-error')).toBe('35 36 37');
|
||||
expect(style.getPropertyValue('--status-neutral-border')).toBe('38 39 40');
|
||||
});
|
||||
|
||||
it('ships status tokens in the bundled themes', () => {
|
||||
applyTheme(defaultTheme);
|
||||
|
||||
expect(document.documentElement.style.getPropertyValue('--status-error')).toBe('220 38 38');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ function mapTheme(rgb: IThemeRGB): Partial<IThemeVariables> {
|
|||
'rgb-text-secondary-alt': '--text-secondary-alt',
|
||||
'rgb-text-tertiary': '--text-tertiary',
|
||||
'rgb-text-warning': '--text-warning',
|
||||
'rgb-text-destructive': '--text-destructive',
|
||||
'rgb-link': '--link',
|
||||
'rgb-link-hover': '--link-hover',
|
||||
'rgb-link-visited': '--link-visited',
|
||||
|
|
@ -68,6 +69,22 @@ function mapTheme(rgb: IThemeRGB): Partial<IThemeVariables> {
|
|||
'rgb-border-medium-alt': '--border-medium-alt',
|
||||
'rgb-border-heavy': '--border-heavy',
|
||||
'rgb-border-xheavy': '--border-xheavy',
|
||||
'rgb-border-destructive': '--border-destructive',
|
||||
'rgb-status-success': '--status-success',
|
||||
'rgb-status-success-subtle': '--status-success-subtle',
|
||||
'rgb-status-success-border': '--status-success-border',
|
||||
'rgb-status-info': '--status-info',
|
||||
'rgb-status-info-subtle': '--status-info-subtle',
|
||||
'rgb-status-info-border': '--status-info-border',
|
||||
'rgb-status-warning': '--status-warning',
|
||||
'rgb-status-warning-subtle': '--status-warning-subtle',
|
||||
'rgb-status-warning-border': '--status-warning-border',
|
||||
'rgb-status-error': '--status-error',
|
||||
'rgb-status-error-subtle': '--status-error-subtle',
|
||||
'rgb-status-error-border': '--status-error-border',
|
||||
'rgb-status-neutral': '--status-neutral',
|
||||
'rgb-status-neutral-subtle': '--status-neutral-subtle',
|
||||
'rgb-status-neutral-border': '--status-neutral-border',
|
||||
'rgb-brand-purple': '--brand-purple',
|
||||
'rgb-presentation': '--presentation',
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue