fix(client): address theme env, dialog padding and locked button review feedback

Expose every IThemeRGB token through REACT_APP_THEME_* instead of the
hand-maintained subset that omitted the status, destructive, inverted and
fixed families.

Drop the padding OGDialogContent contributes to the Tool Library so the
header divider spans the panel again, and stop disabled:opacity-100 from
overriding the locked delete-account button's dimmed state.
This commit is contained in:
Marco Beretta 2026-08-03 01:36:28 +02:00
parent c1c3d67837
commit 25b949bbfa
No known key found for this signature in database
GPG key ID: D918033D8E74CC11
5 changed files with 126 additions and 62 deletions

View file

@ -183,10 +183,10 @@ const renderDeleteButton = (
<Button
variant="destructive"
className={cn(
'mt-4 w-full gap-0 bg-surface-tertiary text-text-primary transition-all duration-200 hover:bg-surface-tertiary disabled:opacity-100',
'mt-4 w-full gap-0 bg-surface-tertiary text-text-primary transition-all duration-200 hover:bg-surface-tertiary',
isLocked
? 'cursor-not-allowed opacity-30'
: 'bg-surface-destructive text-white hover:bg-surface-destructive-hover',
? 'cursor-not-allowed opacity-30 disabled:opacity-30'
: 'bg-surface-destructive text-white hover:bg-surface-destructive-hover disabled:opacity-100',
)}
onClick={handleDeleteUser}
disabled={isDeleting || isLocked}

View file

@ -157,7 +157,7 @@ function AssistantToolsDialog({
}}
>
<OGDialogContent
className="overflow-hidden overflow-y-auto bg-surface-secondary text-left max-sm:h-full sm:mx-7 sm:my-8 sm:max-w-2xl lg:max-w-5xl xl:max-w-7xl"
className="gap-0 overflow-hidden overflow-y-auto bg-surface-secondary p-0 text-left max-sm:h-full sm:mx-7 sm:my-8 sm:max-w-2xl lg:max-w-5xl xl:max-w-7xl"
style={{ minHeight: '610px' }}
>
<div>

View file

@ -1,66 +1,95 @@
/**
* Theme tokens configurable at deploy time. Each entry is the `IThemeRGB` key
* without its `rgb-` prefix, and maps to `REACT_APP_THEME_<UPPER_SNAKE>`.
* Keep this list in sync with `IThemeRGB` so every applied CSS variable stays
* reachable from the environment.
*/
const THEME_TOKENS = [
'text-primary',
'text-secondary',
'text-secondary-alt',
'text-tertiary',
'text-warning',
'text-destructive',
'link',
'link-hover',
'link-visited',
'accent-primary',
'accent-primary-hover',
'ring-primary',
'header-primary',
'header-hover',
'header-button-hover',
'surface-active',
'surface-active-alt',
'surface-hover',
'surface-hover-alt',
'surface-primary',
'surface-primary-alt',
'surface-primary-contrast',
'surface-secondary',
'surface-secondary-alt',
'surface-tertiary',
'surface-tertiary-alt',
'surface-dialog',
'surface-submit',
'surface-submit-hover',
'surface-destructive',
'surface-destructive-hover',
'surface-chat',
'surface-inverted',
'surface-inverted-hover',
'text-inverted',
'surface-fixed',
'surface-fixed-hover',
'text-fixed',
'border-light',
'border-medium',
'border-medium-alt',
'border-heavy',
'border-xheavy',
'border-destructive',
'status-success',
'status-success-subtle',
'status-success-border',
'status-info',
'status-info-subtle',
'status-info-border',
'status-warning',
'status-warning-subtle',
'status-warning-border',
'status-error',
'status-error-subtle',
'status-error-border',
'status-neutral',
'status-neutral-subtle',
'status-neutral-border',
'brand-purple',
'presentation',
];
const toEnvName = (token) => `REACT_APP_THEME_${token.toUpperCase().replace(/-/g, '_')}`;
/**
* Loads theme configuration from environment variables
* @returns {import('@librechat/client').IThemeRGB | undefined}
*/
export function getThemeFromEnv() {
// 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
}
// Build theme object from environment variables
const theme = {};
// Helper to get env value with prefix
const getEnv = (key) => process.env[`REACT_APP_THEME_${key}`];
// Text colors
if (getEnv('TEXT_PRIMARY')) theme['rgb-text-primary'] = getEnv('TEXT_PRIMARY');
if (getEnv('TEXT_SECONDARY')) theme['rgb-text-secondary'] = getEnv('TEXT_SECONDARY');
if (getEnv('TEXT_TERTIARY')) theme['rgb-text-tertiary'] = getEnv('TEXT_TERTIARY');
if (getEnv('TEXT_WARNING')) theme['rgb-text-warning'] = getEnv('TEXT_WARNING');
// Link and accent colors
if (getEnv('LINK')) theme['rgb-link'] = getEnv('LINK');
if (getEnv('LINK_HOVER')) theme['rgb-link-hover'] = getEnv('LINK_HOVER');
if (getEnv('LINK_VISITED')) theme['rgb-link-visited'] = getEnv('LINK_VISITED');
if (getEnv('ACCENT_PRIMARY')) theme['rgb-accent-primary'] = getEnv('ACCENT_PRIMARY');
if (getEnv('ACCENT_PRIMARY_HOVER'))
theme['rgb-accent-primary-hover'] = getEnv('ACCENT_PRIMARY_HOVER');
// Surface colors
if (getEnv('SURFACE_PRIMARY')) theme['rgb-surface-primary'] = getEnv('SURFACE_PRIMARY');
if (getEnv('SURFACE_SECONDARY')) theme['rgb-surface-secondary'] = getEnv('SURFACE_SECONDARY');
if (getEnv('SURFACE_TERTIARY')) theme['rgb-surface-tertiary'] = getEnv('SURFACE_TERTIARY');
if (getEnv('SURFACE_SUBMIT')) theme['rgb-surface-submit'] = getEnv('SURFACE_SUBMIT');
if (getEnv('SURFACE_SUBMIT_HOVER'))
theme['rgb-surface-submit-hover'] = getEnv('SURFACE_SUBMIT_HOVER');
if (getEnv('SURFACE_DESTRUCTIVE'))
theme['rgb-surface-destructive'] = getEnv('SURFACE_DESTRUCTIVE');
if (getEnv('SURFACE_DESTRUCTIVE_HOVER'))
theme['rgb-surface-destructive-hover'] = getEnv('SURFACE_DESTRUCTIVE_HOVER');
if (getEnv('SURFACE_DIALOG')) theme['rgb-surface-dialog'] = getEnv('SURFACE_DIALOG');
if (getEnv('SURFACE_CHAT')) theme['rgb-surface-chat'] = getEnv('SURFACE_CHAT');
// Border colors
if (getEnv('BORDER_LIGHT')) theme['rgb-border-light'] = getEnv('BORDER_LIGHT');
if (getEnv('BORDER_MEDIUM')) theme['rgb-border-medium'] = getEnv('BORDER_MEDIUM');
if (getEnv('BORDER_HEAVY')) theme['rgb-border-heavy'] = getEnv('BORDER_HEAVY');
if (getEnv('BORDER_XHEAVY')) theme['rgb-border-xheavy'] = getEnv('BORDER_XHEAVY');
// Brand colors
if (getEnv('BRAND_PURPLE')) theme['rgb-brand-purple'] = getEnv('BRAND_PURPLE');
// Header colors
if (getEnv('HEADER_PRIMARY')) theme['rgb-header-primary'] = getEnv('HEADER_PRIMARY');
if (getEnv('HEADER_HOVER')) theme['rgb-header-hover'] = getEnv('HEADER_HOVER');
// Presentation
if (getEnv('PRESENTATION')) theme['rgb-presentation'] = getEnv('PRESENTATION');
const theme = THEME_TOKENS.reduce((acc, token) => {
const value = process.env[toEnvName(token)];
if (value) {
acc[`rgb-${token}`] = value;
}
return acc;
}, {});
return Object.keys(theme).length > 0 ? theme : undefined;
}

View file

@ -33,4 +33,32 @@ describe('getThemeFromEnv', () => {
'rgb-accent-primary-hover': '13 14 15',
});
});
it('loads status, inverted, fixed and destructive colors', () => {
process.env.REACT_APP_THEME_STATUS_ERROR = '1 2 3';
process.env.REACT_APP_THEME_STATUS_SUCCESS_SUBTLE = '4 5 6';
process.env.REACT_APP_THEME_STATUS_NEUTRAL_BORDER = '7 8 9';
process.env.REACT_APP_THEME_TEXT_DESTRUCTIVE = '10 11 12';
process.env.REACT_APP_THEME_BORDER_DESTRUCTIVE = '13 14 15';
process.env.REACT_APP_THEME_SURFACE_INVERTED = '16 17 18';
process.env.REACT_APP_THEME_TEXT_INVERTED = '19 20 21';
process.env.REACT_APP_THEME_SURFACE_FIXED_HOVER = '22 23 24';
process.env.REACT_APP_THEME_TEXT_FIXED = '25 26 27';
expect(getThemeFromEnv()).toEqual({
'rgb-status-error': '1 2 3',
'rgb-status-success-subtle': '4 5 6',
'rgb-status-neutral-border': '7 8 9',
'rgb-text-destructive': '10 11 12',
'rgb-border-destructive': '13 14 15',
'rgb-surface-inverted': '16 17 18',
'rgb-text-inverted': '19 20 21',
'rgb-surface-fixed-hover': '22 23 24',
'rgb-text-fixed': '25 26 27',
});
});
it('returns undefined when no theme variables are set', () => {
expect(getThemeFromEnv()).toBeUndefined();
});
});

View file

@ -235,8 +235,15 @@ REACT_APP_THEME_SURFACE_PRIMARY=255 255 255
REACT_APP_THEME_SURFACE_SUBMIT=4 120 87
REACT_APP_THEME_LINK=37 99 235
REACT_APP_THEME_ACCENT_PRIMARY=18 110 107
REACT_APP_THEME_STATUS_ERROR=185 28 28
REACT_APP_THEME_STATUS_ERROR_SUBTLE=254 226 226
REACT_APP_THEME_STATUS_ERROR_BORDER=252 165 165
```
Every `IThemeRGB` key is configurable this way: drop the `rgb-` prefix and
upper-snake-case the rest, so `rgb-status-error-border` becomes
`REACT_APP_THEME_STATUS_ERROR_BORDER`.
### 2. Create a Theme Loader
```tsx