refactor: unify Tailwind color tokens into a single source

Both the client SPA and @librechat/client Tailwind configs now consume one
createTailwindColors() map, eliminating config drift. Fixes the package-side
build along the way: shadcn tokens are wrapped in hsl(), the broken opacity
helper is removed, and text-destructive/border-destructive/switch-unchecked
plus the gray/green palettes are included.
This commit is contained in:
Marco Beretta 2026-06-19 17:18:32 +02:00
parent cdb60e74c2
commit 837ddedb95
No known key found for this signature in database
GPG key ID: D918033D8E74CC11
2 changed files with 121 additions and 162 deletions

View file

@ -1,4 +1,7 @@
// const { fontFamily } = require('tailwindcss/defaultTheme');
const {
createTailwindColors,
} = require('../packages/client/src/theme/utils/createTailwindColors.js');
/** @type {import('tailwindcss').Config} */
module.exports = {
@ -63,101 +66,7 @@ module.exports = {
'slide-out-right': 'slide-out-right 300ms cubic-bezier(0.25, 0.1, 0.25, 1)',
'shortcut-shake': 'shortcut-shake 0.25s ease-in-out',
},
colors: {
gray: {
20: '#ececf1',
50: '#f7f7f8',
100: '#ececec',
200: '#e3e3e3',
300: '#cdcdcd',
400: '#999696',
500: '#595959',
600: '#424242',
700: '#2f2f2f',
800: '#212121',
850: '#171717',
900: '#0d0d0d',
},
green: {
50: '#f1f9f7',
100: '#def2ed',
200: '#a6e5d6',
300: '#6dc8b9',
400: '#41a79d',
500: '#10a37f',
550: '#349072',
600: '#126e6b',
700: '#0a4f53',
800: '#06373e',
900: '#031f29',
},
'brand-purple': 'var(--brand-purple)',
presentation: 'var(--presentation)',
'text-primary': 'var(--text-primary)',
'text-secondary': 'var(--text-secondary)',
'text-secondary-alt': 'var(--text-secondary-alt)',
'text-tertiary': 'var(--text-tertiary)',
'text-warning': 'var(--text-warning)',
'text-destructive': 'var(--text-destructive)',
'ring-primary': 'var(--ring-primary)',
'header-primary': 'var(--header-primary)',
'header-hover': 'var(--header-hover)',
'header-button-hover': 'var(--header-button-hover)',
'surface-active': 'var(--surface-active)',
'surface-active-alt': 'var(--surface-active-alt)',
'surface-hover': 'var(--surface-hover)',
'surface-hover-alt': 'var(--surface-hover-alt)',
'surface-primary': 'var(--surface-primary)',
'surface-primary-alt': 'var(--surface-primary-alt)',
'surface-primary-contrast': 'var(--surface-primary-contrast)',
'surface-secondary': 'var(--surface-secondary)',
'surface-secondary-alt': 'var(--surface-secondary-alt)',
'surface-tertiary': 'var(--surface-tertiary)',
'surface-tertiary-alt': 'var(--surface-tertiary-alt)',
'surface-dialog': 'var(--surface-dialog)',
'surface-submit': 'var(--surface-submit)',
'surface-submit-hover': 'var(--surface-submit-hover)',
'surface-destructive': 'var(--surface-destructive)',
'surface-destructive-hover': 'var(--surface-destructive-hover)',
'surface-chat': 'var(--surface-chat)',
'border-light': 'var(--border-light)',
'border-medium': 'var(--border-medium)',
'border-medium-alt': 'var(--border-medium-alt)',
'border-heavy': 'var(--border-heavy)',
'border-xheavy': 'var(--border-xheavy)',
'border-destructive': 'var(--border-destructive)',
/* These are test styles */
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
['switch-unchecked']: 'hsl(var(--switch-unchecked))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
},
colors: createTailwindColors(),
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',

View file

@ -1,84 +1,134 @@
/**
* Helper function to create a color value that uses CSS variables with alpha support
* This is a CommonJS version for use in tailwind.config.js
* Single source of truth for the Tailwind semantic color palette.
*
* Both Tailwind configs consume this so the `client` SPA and the
* `@librechat/client` package generate identical color utilities and can
* never drift apart:
* - client/tailwind.config.cjs
* - packages/client/tailwind.config.js
*
* Semantic tokens resolve to CSS custom properties defined in
* client/src/style.css (`html`, `.dark`, `.gizmo` blocks). shadcn-compatible
* tokens hold HSL triplets, so they are wrapped in `hsl(...)`.
*
* Opacity modifiers (e.g. `bg-surface-primary/50`) are intentionally not
* supported yet: the underlying variables hold hex/`rgb()` values rather than
* bare channels. Migrating the variables to `R G B` triplets and switching to
* `rgb(var(--x) / <alpha-value>)` is tracked as a follow-up.
*/
function withOpacity(variableName) {
return ({ opacityValue }) => {
if (opacityValue !== undefined) {
// The CSS variable already contains rgb() so we need to extract the values
return `rgba(var(${variableName}), ${opacityValue})`.replace('rgb(', '').replace(')', '');
}
return `var(${variableName})`;
};
}
const palette = {
gray: {
20: '#ececf1',
50: '#f7f7f8',
100: '#ececec',
200: '#e3e3e3',
300: '#cdcdcd',
400: '#999696',
500: '#595959',
600: '#424242',
700: '#2f2f2f',
800: '#212121',
850: '#171717',
900: '#0d0d0d',
},
green: {
50: '#f1f9f7',
100: '#def2ed',
200: '#a6e5d6',
300: '#6dc8b9',
400: '#41a79d',
500: '#10a37f',
550: '#349072',
600: '#126e6b',
700: '#0a4f53',
800: '#06373e',
900: '#031f29',
},
};
const cssVar = (name) => `var(${name})`;
const hslVar = (name) => `hsl(var(${name}))`;
/**
* Creates Tailwind color configuration that uses CSS variables
* This allows dynamic theme switching by changing CSS variable values
* Creates the Tailwind `theme.extend.colors` object backed by CSS variables.
* This allows dynamic theme switching by changing CSS variable values at
* runtime (see ThemeProvider / applyTheme).
*/
function createTailwindColors() {
return {
'text-primary': withOpacity('--text-primary'),
'text-secondary': withOpacity('--text-secondary'),
'text-secondary-alt': withOpacity('--text-secondary-alt'),
'text-tertiary': withOpacity('--text-tertiary'),
'text-warning': withOpacity('--text-warning'),
'ring-primary': withOpacity('--ring-primary'),
'header-primary': withOpacity('--header-primary'),
'header-hover': withOpacity('--header-hover'),
'header-button-hover': withOpacity('--header-button-hover'),
'surface-active': withOpacity('--surface-active'),
'surface-active-alt': withOpacity('--surface-active-alt'),
'surface-hover': withOpacity('--surface-hover'),
'surface-hover-alt': withOpacity('--surface-hover-alt'),
'surface-primary': withOpacity('--surface-primary'),
'surface-primary-alt': withOpacity('--surface-primary-alt'),
'surface-primary-contrast': withOpacity('--surface-primary-contrast'),
'surface-secondary': withOpacity('--surface-secondary'),
'surface-secondary-alt': withOpacity('--surface-secondary-alt'),
'surface-tertiary': withOpacity('--surface-tertiary'),
'surface-tertiary-alt': withOpacity('--surface-tertiary-alt'),
'surface-dialog': withOpacity('--surface-dialog'),
'surface-submit': withOpacity('--surface-submit'),
'surface-submit-hover': withOpacity('--surface-submit-hover'),
'surface-destructive': withOpacity('--surface-destructive'),
'surface-destructive-hover': withOpacity('--surface-destructive-hover'),
'surface-chat': withOpacity('--surface-chat'),
'border-light': withOpacity('--border-light'),
'border-medium': withOpacity('--border-medium'),
'border-medium-alt': withOpacity('--border-medium-alt'),
'border-heavy': withOpacity('--border-heavy'),
'border-xheavy': withOpacity('--border-xheavy'),
'brand-purple': withOpacity('--brand-purple'),
presentation: withOpacity('--presentation'),
background: withOpacity('--background'),
foreground: withOpacity('--foreground'),
...palette,
'brand-purple': cssVar('--brand-purple'),
presentation: cssVar('--presentation'),
'text-primary': cssVar('--text-primary'),
'text-secondary': cssVar('--text-secondary'),
'text-secondary-alt': cssVar('--text-secondary-alt'),
'text-tertiary': cssVar('--text-tertiary'),
'text-warning': cssVar('--text-warning'),
'text-destructive': cssVar('--text-destructive'),
'ring-primary': cssVar('--ring-primary'),
'header-primary': cssVar('--header-primary'),
'header-hover': cssVar('--header-hover'),
'header-button-hover': cssVar('--header-button-hover'),
'surface-active': cssVar('--surface-active'),
'surface-active-alt': cssVar('--surface-active-alt'),
'surface-hover': cssVar('--surface-hover'),
'surface-hover-alt': cssVar('--surface-hover-alt'),
'surface-primary': cssVar('--surface-primary'),
'surface-primary-alt': cssVar('--surface-primary-alt'),
'surface-primary-contrast': cssVar('--surface-primary-contrast'),
'surface-secondary': cssVar('--surface-secondary'),
'surface-secondary-alt': cssVar('--surface-secondary-alt'),
'surface-tertiary': cssVar('--surface-tertiary'),
'surface-tertiary-alt': cssVar('--surface-tertiary-alt'),
'surface-dialog': cssVar('--surface-dialog'),
'surface-submit': cssVar('--surface-submit'),
'surface-submit-hover': cssVar('--surface-submit-hover'),
'surface-destructive': cssVar('--surface-destructive'),
'surface-destructive-hover': cssVar('--surface-destructive-hover'),
'surface-chat': cssVar('--surface-chat'),
'border-light': cssVar('--border-light'),
'border-medium': cssVar('--border-medium'),
'border-medium-alt': cssVar('--border-medium-alt'),
'border-heavy': cssVar('--border-heavy'),
'border-xheavy': cssVar('--border-xheavy'),
'border-destructive': cssVar('--border-destructive'),
border: hslVar('--border'),
input: hslVar('--input'),
'switch-unchecked': hslVar('--switch-unchecked'),
ring: hslVar('--ring'),
background: hslVar('--background'),
foreground: hslVar('--foreground'),
primary: {
DEFAULT: withOpacity('--primary'),
foreground: withOpacity('--primary-foreground'),
DEFAULT: hslVar('--primary'),
foreground: hslVar('--primary-foreground'),
},
secondary: {
DEFAULT: withOpacity('--secondary'),
foreground: withOpacity('--secondary-foreground'),
},
muted: {
DEFAULT: withOpacity('--muted'),
foreground: withOpacity('--muted-foreground'),
},
accent: {
DEFAULT: withOpacity('--accent'),
foreground: withOpacity('--accent-foreground'),
DEFAULT: hslVar('--secondary'),
foreground: hslVar('--secondary-foreground'),
},
destructive: {
DEFAULT: withOpacity('--surface-destructive'),
foreground: withOpacity('--destructive-foreground'),
DEFAULT: hslVar('--destructive'),
foreground: hslVar('--destructive-foreground'),
},
muted: {
DEFAULT: hslVar('--muted'),
foreground: hslVar('--muted-foreground'),
},
accent: {
DEFAULT: hslVar('--accent'),
foreground: hslVar('--accent-foreground'),
},
border: withOpacity('--border'),
input: withOpacity('--input'),
ring: withOpacity('--ring'),
card: {
DEFAULT: withOpacity('--card'),
foreground: withOpacity('--card-foreground'),
DEFAULT: hslVar('--card'),
foreground: hslVar('--card-foreground'),
},
};
}