diff --git a/client/tailwind.config.cjs b/client/tailwind.config.cjs index 4cdace6d3d..3120cc47ab 100644 --- a/client/tailwind.config.cjs +++ b/client/tailwind.config.cjs @@ -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)', diff --git a/packages/client/src/theme/utils/createTailwindColors.js b/packages/client/src/theme/utils/createTailwindColors.js index de110ac18e..dc4ddc76ab 100644 --- a/packages/client/src/theme/utils/createTailwindColors.js +++ b/packages/client/src/theme/utils/createTailwindColors.js @@ -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) / )` 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'), }, }; }