LibreChat/packages/client/src/theme
Danny Avila 79197454f8
📦 feat: Move Shared Components to @librechat/client (#8685)
* feat: init @librechat/client

* feat: Add common types and interfaces for accessibility, agents, artifacts, assistants, and tools

* feat: Add jotai as a peer dependency

* fix build client package

* feat: cleanup unused types from common/index.ts

- Remove 104 unused type exports from packages/client/src/common/index.ts
- Keep only 7 actually used exports (93% reduction)
- Add cleanup script with enhanced import pattern detection
- Support both named imports and namespace imports (* as t)
- Create automatic backups and comprehensive documentation
- Maintain type safety with build verification
- No breaking changes to existing code

Kept exports:
- TShowToast, Option, OptionWithIcon, DropdownValueSetter
- MentionOption, NotificationSeverity, MenuItemProps

Scripts: cleanup-common-types-safe.js, README-CLEANUP.md

* fix: cleanup

* fix: package; refactor: tsconfig

* feat: add back `recoil`

* fix: move dependencies to peerDependencies in client package

* feat: add @librechat/client as a dependency in package.json and package-lock.json

* feat: update client package configuration and dependencies

- Added new dependencies for Rollup plugins and updated existing ones in package.json and package-lock.json.
- Introduced a new Rollup configuration file for building the client package.
- Refactored build scripts to include a dedicated build command for the client.
- Updated TypeScript configuration for improved module resolution and type declaration output.
- Integrated a Toast component from the client package into the main App component.

* feat: enhance Rollup configuration for client package

- Updated terser plugin settings to preserve directives like 'use client'.
- Added custom warning handler to ignore "use client" directive warnings during the build process.

* chore: rename package/client build script command

* feat: update client package dependencies and Rollup configuration

- Added rollup-plugin-postcss to package.json and updated package-lock.json.
- Enhanced Rollup configuration to include postcss plugin for CSS handling.
- Updated index.ts to export all components from the components directory for better modularity.

* feat: add client package directory to update configuration

- Included the 'client' package directory in the update.js configuration to ensure it is recognized during updates.

* feat: export Toast component in client package

- Added export for the Toast component in index.ts to enhance modularity and accessibility of components.

* feat: /client transition to @librechat/client

* chore: fixed formatting issues

* fix: update peer dependencies in @librechat/client to prevent bundling them

* fix: correct useSprings implementation in SplitText component

* fix: circular dependencies in DataTable

* fix: add remaining peer dependencies and match actual versions previously used in `client/package.json`

* fix: correct frontend:ci script to include client package build

* chore: enhance unused package detection for @librechat/client and improve dependency extraction

* fix: add missing peer dependency for @radix-ui/react-collapsible

* chore: include "packages/client" in unused i18next keys detection

* test: update AgentFooter tests to use document.querySelector for spinner checks
test: mock window.matchMedia in setupTests.js for consistent test environment

* feat: add react-hook-form dependency and update FormInput component to use its types

* chore: linting

* refactor: remove unused defaultSelectedValues prop from MCPSelect and MultiSelect components

* chore: linting

* feat: update GitHub Actions workflow to publish @librechat/client

* chore: update GitHub Actions workflow to install and build data-provider and client dependencies

* chore: add missing @testing-library/react dependency to client package

* chore: update tsconfig.json to exclude additional test files

* chore: fix build issues, resolve latest LC changes

* chore: move MCP components outside of `~/components/ui`

* feat: implement dynamic theme system with environment variable support and Tailwind CSS integration

* chore: remove unnecessary logging of sttExternal and ttsExternal in Speech component

* chore: squashed cleanup commits

chore: move @tanstack/react-virtual to dependencies and remove recoil from package.json

chore: move dependencies to peerDependencies in package.json

feat: update package.json and rollup.config.js to include jotai and enhance bundling configuration

feat: update package.json and rollup.config.js to include jotai and enhance bundling configuration

refactor: reorganize exports in index.ts for improved clarity

refactor: remove unused types and interfaces from common files

refactor: update peer dependencies and improve component typings

- Removed duplicate peer dependencies from package.json and organized them.
- Updated rollup.config.js to disable TypeScript checking during the build process.
- Modified AnimatedTabs component to use React.ReactNode for label and content types, and added TypeScript workarounds for compatibility.
- Enhanced Label and Separator components to accept an optional className prop and improved prop spreading.
- Updated Slider component to include an optional className prop and refined prop handling for better type safety.

refactor: clean up client workflow and update package dependencies

refactor: update package dependencies and improve PostCSS and Rollup configurations

chore: bump version to 0.1.2 in package.json

chore: bump client version to 0.1.2 in package-lock.json

chore: bump client version to 0.1.3 and update dependencies

chore: bump client version to 0.1.4 and update @react-spring dependencies

chore: update package version to 0.1.5 and adjust peer dependencies

- Bump version in package.json from 0.1.4 to 0.1.5.
- Update peer dependency for @tanstack/react-query to allow version 5.0.0.
- Add @tanstack/react-table and @tanstack/react-virtual as dependencies.
- Update various dependencies to their latest compatible versions.
- Simplify postcss.config.js by removing unnecessary options.
- Clean up rollup.config.js by removing ignored PostCSS warnings.
- Update CheckboxButton component to cast icon as React JSX element.
- Adjust Combobox component's class names for better styling.
- Change DropdownPopup component to use React's namespace import.
- Modify InputOTP component to use 'any' type for OTPInputContext.
- Ensure displayLabel and value in ModelParameters are converted to strings.
- Update MultiSearch component's placeholder to ensure it's a string.
- Cast selectIcon in MultiSelect as React JSX element for consistency.
- Update OGDialogTemplate to cast selectText as React JSX element.
- Initialize animationRef in PixelCard with undefined for clarity.
- Add TypeScript ignore comments in Select and SelectDropDown components for Radix UI type conflicts.
- Ensure title in SelectDropDown is a string and adjust rendering of options.
- Update useLocalize hook to cast options as any for compatibility.

refactor: code structure; chore: translations cleanup

chore: remove unused imports and clean up code in NewChat component

refactor: enhance Menu component to support custom render functions for menu items

style: update itemClassName in ToolsDropdown for improved UI consistency

fix: merge conflicts

chore: update @radix-ui/react-accordion to version 1.2.11

* refactor: remove unnecessary TypeScript type assertions in AnimatedTabs, Label, Separator, and Slider components

* feat: enhance theme system with localStorage persistence and new theme atoms

* chore: bump version of @librechat/client to 0.1.7

* chore: fix ci/cd warnings/errors related to linting and unused localization keys

* chore: update dependencies for class-variance-authority, clsx, and match-sorter

* chore: bump @librechat/client to v0.1.8

* feat: add utility colors for theme customization and remove unused tailwindConfig

* v0.1.9

---------

Co-authored-by: Marco Beretta <81851188+berry-13@users.noreply.github.com>
2025-07-27 12:19:01 -04:00
..
atoms 📦 feat: Move Shared Components to @librechat/client (#8685) 2025-07-27 12:19:01 -04:00
context 📦 feat: Move Shared Components to @librechat/client (#8685) 2025-07-27 12:19:01 -04:00
themes 📦 feat: Move Shared Components to @librechat/client (#8685) 2025-07-27 12:19:01 -04:00
types 📦 feat: Move Shared Components to @librechat/client (#8685) 2025-07-27 12:19:01 -04:00
utils 📦 feat: Move Shared Components to @librechat/client (#8685) 2025-07-27 12:19:01 -04:00
index.ts 📦 feat: Move Shared Components to @librechat/client (#8685) 2025-07-27 12:19:01 -04:00
README.md 📦 feat: Move Shared Components to @librechat/client (#8685) 2025-07-27 12:19:01 -04:00

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

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

npm install @librechat/client

2. Wrap Your App with ThemeProvider

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:

/* 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:

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

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

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

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.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

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

<ThemeProvider 
  initialTheme="system"
  themeRGB={getThemeFromEnv()}
>
  <App />
</ThemeProvider>

Dark/Light Mode

The ThemeProvider handles dark/light mode automatically:

Using the Theme Hook

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:

import { ThemeContext, ThemeProvider } from '~/hooks/ThemeContext';

After:

import { ThemeContext, ThemeProvider } from '@librechat/client';

2. Update ThemeProvider Usage

The new ThemeProvider is backward compatible but adds new capabilities:

<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:

// 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:
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

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

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:

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.