mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
fix(client): address semantic theme review feedback
This commit is contained in:
parent
be0ece2fa7
commit
f1b287fdb9
18 changed files with 181 additions and 31 deletions
|
|
@ -32,8 +32,8 @@ const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit, startupConfig, error,
|
|||
const validTheme = isDark(theme) ? 'dark' : 'light';
|
||||
const requireCaptcha = Boolean(startupConfig.turnstile?.siteKey);
|
||||
const authInputClassName =
|
||||
'webkit-dark-styles transition-color peer w-full rounded-2xl border border-border-light bg-surface-primary px-3.5 pb-2.5 pt-3 text-text-primary duration-200 hover:border-border-light focus:border-accent-primary focus:outline-none focus-visible:border-accent-primary';
|
||||
const authSecretInputClassName = `${authInputClassName} h-auto pr-12`;
|
||||
'webkit-dark-styles transition-color peer h-auto w-full rounded-2xl border border-border-light bg-surface-primary px-3.5 pb-2.5 pt-3 text-text-primary duration-200 hover:border-border-light focus:border-accent-primary focus:outline-none focus-visible:border-accent-primary';
|
||||
const authSecretInputClassName = `${authInputClassName} pr-12`;
|
||||
const authLabelClassName =
|
||||
'absolute start-3 top-1.5 z-10 origin-[0] -translate-y-4 scale-75 transform bg-surface-primary px-2 text-sm text-text-secondary-alt duration-200 peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100 peer-focus:top-1.5 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-2 peer-focus:text-accent-primary rtl:peer-focus:left-auto rtl:peer-focus:translate-x-1/4';
|
||||
const authSecretButtonClassName =
|
||||
|
|
|
|||
|
|
@ -37,8 +37,8 @@ const Registration: React.FC = () => {
|
|||
// only require captcha if we have a siteKey
|
||||
const requireCaptcha = Boolean(startupConfig?.turnstile?.siteKey);
|
||||
const authInputClassName =
|
||||
'webkit-dark-styles transition-color peer w-full rounded-2xl border border-border-light bg-surface-primary px-3.5 pb-2.5 pt-3 text-text-primary duration-200 hover:border-border-light focus:border-accent-primary focus:outline-none focus-visible:border-accent-primary';
|
||||
const authSecretInputClassName = `${authInputClassName} h-auto pr-12`;
|
||||
'webkit-dark-styles transition-color peer h-auto w-full rounded-2xl border border-border-light bg-surface-primary px-3.5 pb-2.5 pt-3 text-text-primary duration-200 hover:border-border-light focus:border-accent-primary focus:outline-none focus-visible:border-accent-primary';
|
||||
const authSecretInputClassName = `${authInputClassName} pr-12`;
|
||||
const authLabelClassName =
|
||||
'absolute start-3 top-1.5 z-10 origin-[0] -translate-y-4 scale-75 transform bg-surface-primary px-2 text-sm text-text-secondary-alt duration-200 peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100 peer-focus:top-1.5 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-2 peer-focus:text-accent-primary rtl:peer-focus:left-auto rtl:peer-focus:translate-x-1/4';
|
||||
const authSecretButtonClassName =
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ function RequestPasswordReset() {
|
|||
},
|
||||
})}
|
||||
aria-invalid={!!errors.email}
|
||||
className="webkit-dark-styles transition-color peer w-full rounded-2xl border border-border-light bg-surface-primary px-3.5 pb-2.5 pt-3 text-text-primary duration-200 focus:border-accent-primary focus:outline-none"
|
||||
className="webkit-dark-styles transition-color peer h-auto w-full rounded-2xl border border-border-light bg-surface-primary px-3.5 pb-2.5 pt-3 text-text-primary duration-200 focus:border-accent-primary focus:outline-none"
|
||||
placeholder=" "
|
||||
/>
|
||||
<label
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ test('renders login form', () => {
|
|||
setError={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(getByLabelText(/email/i)).toBeInTheDocument();
|
||||
expect(getByLabelText(/email/i)).toHaveClass('h-auto');
|
||||
expect(getByLabelText(/password/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import reactRouter from 'react-router-dom';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { render, waitFor, screen } from 'test/layout-test-utils';
|
||||
import * as mockDataProvider from 'librechat-data-provider/react-query';
|
||||
import type { TStartupConfig } from 'librechat-data-provider';
|
||||
import * as miscDataProvider from '~/data-provider/Misc/queries';
|
||||
import * as endpointQueries from '~/data-provider/Endpoints/queries';
|
||||
import { render, waitFor, screen } from 'test/layout-test-utils';
|
||||
import * as miscDataProvider from '~/data-provider/Misc/queries';
|
||||
import * as authMutations from '~/data-provider/Auth/mutations';
|
||||
import * as authQueries from '~/data-provider/Auth/queries';
|
||||
import Registration from '~/components/Auth/Registration';
|
||||
|
|
@ -83,7 +83,7 @@ const setup = ({
|
|||
const mockUseOutletContext = jest.spyOn(reactRouter, 'useOutletContext').mockReturnValue({
|
||||
startupConfig: useGetStartupConfigReturnValue.data,
|
||||
});
|
||||
const mockUseGetBannerQuery = jest
|
||||
jest
|
||||
.spyOn(miscDataProvider, 'useGetBannerQuery')
|
||||
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
|
||||
.mockReturnValue(useGetBannerQueryReturnValue);
|
||||
|
|
@ -120,7 +120,7 @@ jest.mock('react-router-dom', () => ({
|
|||
test('renders registration form', () => {
|
||||
const { getByText, getByTestId, getByRole } = setup();
|
||||
expect(getByText(/Create your account/i)).toBeInTheDocument();
|
||||
expect(getByRole('textbox', { name: /Full name/i })).toBeInTheDocument();
|
||||
expect(getByRole('textbox', { name: /Full name/i })).toHaveClass('h-auto');
|
||||
expect(getByRole('form', { name: /Registration form/i })).toBeVisible();
|
||||
expect(getByRole('textbox', { name: /Username/i })).toBeInTheDocument();
|
||||
expect(getByRole('textbox', { name: /Email/i })).toBeInTheDocument();
|
||||
|
|
|
|||
|
|
@ -102,8 +102,8 @@
|
|||
.t-icon-swap .t-icon {
|
||||
grid-area: 1 / 1;
|
||||
transition:
|
||||
opacity var(--icon-swap-dur) var(--icon-swap-ease),
|
||||
filter var(--icon-swap-dur) var(--icon-swap-ease),
|
||||
opacity var(--icon-swap-dur) var(--icon-swap-ease),
|
||||
filter var(--icon-swap-dur) var(--icon-swap-ease),
|
||||
transform var(--icon-swap-dur) var(--icon-swap-ease);
|
||||
will-change: opacity, filter, transform;
|
||||
}
|
||||
|
|
@ -180,10 +180,14 @@ html {
|
|||
--surface-destructive-hover: var(--red-800);
|
||||
--surface-chat: var(--white);
|
||||
--border-light: var(--gray-200);
|
||||
--border-light-alpha: 1;
|
||||
--border-medium-alt: var(--gray-300);
|
||||
--border-medium: var(--gray-300);
|
||||
--border-medium-alpha: 1;
|
||||
--border-heavy: var(--gray-400);
|
||||
--border-heavy-alpha: 1;
|
||||
--border-xheavy: var(--gray-500);
|
||||
--border-xheavy-alpha: 1;
|
||||
--border-destructive: var(--red-600);
|
||||
--status-success: var(--green-600);
|
||||
--status-success-subtle: var(--green-50);
|
||||
|
|
@ -218,7 +222,7 @@ html {
|
|||
--text-secondary-alt: var(--gray-400);
|
||||
--text-tertiary: var(--gray-500);
|
||||
--text-warning: var(--amber-500);
|
||||
--text-destructive: var(--red-600);
|
||||
--text-destructive: var(--status-error);
|
||||
--link: var(--blue-400);
|
||||
--link-hover: var(--blue-300);
|
||||
--link-visited: 192 132 252;
|
||||
|
|
@ -282,9 +286,13 @@ html {
|
|||
--surface-secondary: var(--gray-50);
|
||||
--surface-tertiary: var(--gray-100);
|
||||
--border-light: 0 0 0;
|
||||
--border-light-alpha: 0.1;
|
||||
--border-medium: 0 0 0;
|
||||
--border-medium-alpha: 0.15;
|
||||
--border-heavy: 0 0 0;
|
||||
--border-heavy-alpha: 0.2;
|
||||
--border-xheavy: 0 0 0;
|
||||
--border-xheavy-alpha: 0.25;
|
||||
}
|
||||
.gizmo.dark {
|
||||
--text-primary: var(--gray-100);
|
||||
|
|
@ -343,12 +351,12 @@ html {
|
|||
}
|
||||
.border-token-border-light {
|
||||
border-color: #ececf1;
|
||||
border-color: rgb(var(--border-light));
|
||||
border-color: rgb(var(--border-light) / var(--border-light-alpha, 1));
|
||||
}
|
||||
|
||||
.border-token-border-medium {
|
||||
border-color: #d9d9e3;
|
||||
border-color: rgb(var(--border-medium));
|
||||
border-color: rgb(var(--border-medium) / var(--border-medium-alpha, 1));
|
||||
}
|
||||
|
||||
.bg-token-surface-secondary {
|
||||
|
|
@ -1278,7 +1286,7 @@ button {
|
|||
overflow-x: auto;
|
||||
overscroll-behavior-x: contain;
|
||||
padding-bottom: 0.25rem;
|
||||
scrollbar-color: rgb(var(--border-medium)) transparent;
|
||||
scrollbar-color: rgb(var(--border-medium) / var(--border-medium-alpha, 1)) transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
|
|
@ -1287,11 +1295,11 @@ button {
|
|||
}
|
||||
|
||||
.markdown-table-wrapper::-webkit-scrollbar-thumb {
|
||||
background-color: rgb(var(--border-medium));
|
||||
background-color: rgb(var(--border-medium) / var(--border-medium-alpha, 1));
|
||||
}
|
||||
|
||||
.markdown-table-wrapper::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgb(var(--border-heavy));
|
||||
background-color: rgb(var(--border-heavy) / var(--border-heavy-alpha, 1));
|
||||
}
|
||||
|
||||
/* Show scrollbar only on hover */
|
||||
|
|
@ -1301,7 +1309,7 @@ button {
|
|||
}
|
||||
|
||||
.scrollbar-hover:hover {
|
||||
scrollbar-color: rgb(var(--border-medium)) transparent;
|
||||
scrollbar-color: rgb(var(--border-medium) / var(--border-medium-alpha, 1)) transparent;
|
||||
}
|
||||
|
||||
.scrollbar-hover::-webkit-scrollbar-thumb {
|
||||
|
|
@ -1310,7 +1318,7 @@ button {
|
|||
}
|
||||
|
||||
.scrollbar-hover:hover::-webkit-scrollbar-thumb {
|
||||
background-color: rgb(var(--border-medium));
|
||||
background-color: rgb(var(--border-medium) / var(--border-medium-alpha, 1));
|
||||
transition-delay: 0s;
|
||||
}
|
||||
|
||||
|
|
@ -2172,7 +2180,7 @@ html {
|
|||
border-radius: 0.7rem;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: rgb(var(--border-light));
|
||||
border-color: rgb(var(--border-light) / var(--border-light-alpha, 1));
|
||||
background-color: rgb(var(--surface-primary));
|
||||
padding: 0.5rem;
|
||||
color: rgb(var(--text-primary));
|
||||
|
|
|
|||
|
|
@ -24,6 +24,14 @@ export function getThemeFromEnv() {
|
|||
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');
|
||||
|
|
|
|||
36
client/src/utils/getThemeFromEnv.spec.js
Normal file
36
client/src/utils/getThemeFromEnv.spec.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { getThemeFromEnv } from './getThemeFromEnv';
|
||||
|
||||
const originalThemeEnv = Object.fromEntries(
|
||||
Object.entries(process.env).filter(([key]) => key.startsWith('REACT_APP_THEME_')),
|
||||
);
|
||||
|
||||
const clearThemeEnv = () => {
|
||||
Object.keys(process.env)
|
||||
.filter((key) => key.startsWith('REACT_APP_THEME_'))
|
||||
.forEach((key) => delete process.env[key]);
|
||||
};
|
||||
|
||||
beforeEach(clearThemeEnv);
|
||||
|
||||
afterAll(() => {
|
||||
clearThemeEnv();
|
||||
Object.assign(process.env, originalThemeEnv);
|
||||
});
|
||||
|
||||
describe('getThemeFromEnv', () => {
|
||||
it('loads link and accent colors', () => {
|
||||
process.env.REACT_APP_THEME_LINK = '1 2 3';
|
||||
process.env.REACT_APP_THEME_LINK_HOVER = '4 5 6';
|
||||
process.env.REACT_APP_THEME_LINK_VISITED = '7 8 9';
|
||||
process.env.REACT_APP_THEME_ACCENT_PRIMARY = '10 11 12';
|
||||
process.env.REACT_APP_THEME_ACCENT_PRIMARY_HOVER = '13 14 15';
|
||||
|
||||
expect(getThemeFromEnv()).toEqual({
|
||||
'rgb-link': '1 2 3',
|
||||
'rgb-link-hover': '4 5 6',
|
||||
'rgb-link-visited': '7 8 9',
|
||||
'rgb-accent-primary': '10 11 12',
|
||||
'rgb-accent-primary-hover': '13 14 15',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
border-radius: 1rem;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: rgb(var(--border-light));
|
||||
border-color: rgb(var(--border-light) / var(--border-light-alpha, 1));
|
||||
background-color: rgb(var(--surface-primary));
|
||||
padding: 0.5rem;
|
||||
color: rgb(var(--text-primary));
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
attribute is absent and the ring always shows — the safe fallback. */
|
||||
html[data-input-modality='pointer'] .lc-field:focus,
|
||||
html[data-input-modality='pointer'] .lc-field:focus-visible {
|
||||
border-color: rgb(var(--border-light));
|
||||
border-color: rgb(var(--border-light) / var(--border-light-alpha, 1));
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -205,6 +205,8 @@ 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
|
||||
REACT_APP_THEME_LINK=37 99 235
|
||||
REACT_APP_THEME_ACCENT_PRIMARY=18 110 107
|
||||
```
|
||||
|
||||
### 2. Create a Theme Loader
|
||||
|
|
@ -470,4 +472,4 @@ When adding new theme colors:
|
|||
|
||||
## License
|
||||
|
||||
This theme system is part of the @librechat/client package.
|
||||
This theme system is part of the @librechat/client package.
|
||||
|
|
|
|||
|
|
@ -12,6 +12,13 @@ export const darkTheme: IThemeRGB = {
|
|||
'rgb-text-tertiary': '89 89 89', // #595959 (gray-500)
|
||||
'rgb-text-warning': '245 158 11', // #f59e0b (amber-500)
|
||||
|
||||
// Link and accent colors
|
||||
'rgb-link': '96 165 250', // #60a5fa (blue-400)
|
||||
'rgb-link-hover': '147 197 253', // #93c5fd (blue-300)
|
||||
'rgb-link-visited': '192 132 252', // #c084fc (purple-400)
|
||||
'rgb-accent-primary': '65 167 157', // #41a79d
|
||||
'rgb-accent-primary-hover': '109 200 185', // #6dc8b9
|
||||
|
||||
// Ring colors (not defined in dark mode, using default)
|
||||
'rgb-ring-primary': '89 89 89', // #595959 (gray-500)
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,13 @@ export const defaultTheme: IThemeRGB = {
|
|||
'rgb-text-tertiary': '89 89 89', // #595959 (gray-500)
|
||||
'rgb-text-warning': '245 158 11', // #f59e0b (amber-500)
|
||||
|
||||
// Link and accent colors
|
||||
'rgb-link': '37 99 235', // #2563eb (blue-600)
|
||||
'rgb-link-hover': '29 78 216', // #1d4ed8 (blue-700)
|
||||
'rgb-link-visited': '147 51 234', // #9333ea (purple-600)
|
||||
'rgb-accent-primary': '18 110 107', // #126e6b
|
||||
'rgb-accent-primary-hover': '10 79 83', // #0a4f53
|
||||
|
||||
// Ring colors
|
||||
'rgb-ring-primary': '89 89 89', // #595959 (gray-500)
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,13 @@ export interface IThemeRGB {
|
|||
'rgb-text-tertiary'?: string;
|
||||
'rgb-text-warning'?: string;
|
||||
|
||||
// Link and accent colors
|
||||
'rgb-link'?: string;
|
||||
'rgb-link-hover'?: string;
|
||||
'rgb-link-visited'?: string;
|
||||
'rgb-accent-primary'?: string;
|
||||
'rgb-accent-primary-hover'?: string;
|
||||
|
||||
// Ring colors
|
||||
'rgb-ring-primary'?: string;
|
||||
|
||||
|
|
@ -66,6 +73,11 @@ export interface IThemeVariables {
|
|||
'--text-secondary-alt': string;
|
||||
'--text-tertiary': string;
|
||||
'--text-warning': string;
|
||||
'--link': string;
|
||||
'--link-hover': string;
|
||||
'--link-visited': string;
|
||||
'--accent-primary': string;
|
||||
'--accent-primary-hover': string;
|
||||
'--ring-primary': string;
|
||||
'--header-primary': string;
|
||||
'--header-hover': string;
|
||||
|
|
@ -94,10 +106,14 @@ export interface IThemeVariables {
|
|||
'--surface-fixed-hover': string;
|
||||
'--text-fixed': string;
|
||||
'--border-light': string;
|
||||
'--border-light-alpha': string;
|
||||
'--border-medium': string;
|
||||
'--border-medium-alpha': string;
|
||||
'--border-medium-alt': string;
|
||||
'--border-heavy': string;
|
||||
'--border-heavy-alpha': string;
|
||||
'--border-xheavy': string;
|
||||
'--border-xheavy-alpha': string;
|
||||
'--brand-purple': string;
|
||||
'--presentation': string;
|
||||
}
|
||||
|
|
@ -111,6 +127,11 @@ export interface IThemeColors {
|
|||
'text-secondary-alt'?: string;
|
||||
'text-tertiary'?: string;
|
||||
'text-warning'?: string;
|
||||
link?: string;
|
||||
'link-hover'?: string;
|
||||
'link-visited'?: string;
|
||||
'accent-primary'?: string;
|
||||
'accent-primary-hover'?: string;
|
||||
'ring-primary'?: string;
|
||||
'header-primary'?: string;
|
||||
'header-hover'?: string;
|
||||
|
|
|
|||
33
packages/client/src/theme/utils/applyTheme.spec.ts
Normal file
33
packages/client/src/theme/utils/applyTheme.spec.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import applyTheme from './applyTheme';
|
||||
|
||||
const semanticProperties = [
|
||||
'--link',
|
||||
'--link-hover',
|
||||
'--link-visited',
|
||||
'--accent-primary',
|
||||
'--accent-primary-hover',
|
||||
];
|
||||
|
||||
afterEach(() => {
|
||||
semanticProperties.forEach((property) => document.documentElement.style.removeProperty(property));
|
||||
});
|
||||
|
||||
describe('applyTheme', () => {
|
||||
it('applies link and accent colors from runtime themes', () => {
|
||||
applyTheme({
|
||||
'rgb-link': '1 2 3',
|
||||
'rgb-link-hover': '4 5 6',
|
||||
'rgb-link-visited': '7 8 9',
|
||||
'rgb-accent-primary': '10 11 12',
|
||||
'rgb-accent-primary-hover': '13 14 15',
|
||||
});
|
||||
|
||||
expect(document.documentElement.style.getPropertyValue('--link')).toBe('1 2 3');
|
||||
expect(document.documentElement.style.getPropertyValue('--link-hover')).toBe('4 5 6');
|
||||
expect(document.documentElement.style.getPropertyValue('--link-visited')).toBe('7 8 9');
|
||||
expect(document.documentElement.style.getPropertyValue('--accent-primary')).toBe('10 11 12');
|
||||
expect(document.documentElement.style.getPropertyValue('--accent-primary-hover')).toBe(
|
||||
'13 14 15',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -31,6 +31,11 @@ function mapTheme(rgb: IThemeRGB): Partial<IThemeVariables> {
|
|||
'rgb-text-secondary-alt': '--text-secondary-alt',
|
||||
'rgb-text-tertiary': '--text-tertiary',
|
||||
'rgb-text-warning': '--text-warning',
|
||||
'rgb-link': '--link',
|
||||
'rgb-link-hover': '--link-hover',
|
||||
'rgb-link-visited': '--link-visited',
|
||||
'rgb-accent-primary': '--accent-primary',
|
||||
'rgb-accent-primary-hover': '--accent-primary-hover',
|
||||
'rgb-ring-primary': '--ring-primary',
|
||||
'rgb-header-primary': '--header-primary',
|
||||
'rgb-header-hover': '--header-hover',
|
||||
|
|
|
|||
|
|
@ -11,9 +11,11 @@
|
|||
* client/src/style.css (`html`, `.dark`, `.gizmo` blocks). Those variables hold
|
||||
* bare `R G B` channel triplets, so `cssVar` wraps them as
|
||||
* `rgb(var(--x) / <alpha-value>)`. This makes opacity modifiers work, e.g.
|
||||
* `bg-surface-primary/50`. shadcn-compatible tokens still hold HSL triplets, so
|
||||
* they are wrapped in `hsl(...)`; any direct `var(--token)` usage in plain CSS
|
||||
* must wrap the channel triplet itself, e.g. `color: rgb(var(--text-primary))`.
|
||||
* `bg-surface-primary/50`. Tokens with an intrinsic alpha use a companion
|
||||
* `--x-alpha` variable that is multiplied by Tailwind's opacity modifier.
|
||||
* shadcn-compatible tokens still hold HSL triplets, so they are wrapped in
|
||||
* `hsl(...)`; any direct `var(--token)` usage in plain CSS must wrap the channel
|
||||
* triplet itself, e.g. `color: rgb(var(--text-primary))`.
|
||||
*/
|
||||
|
||||
const palette = {
|
||||
|
|
@ -47,6 +49,7 @@ const palette = {
|
|||
};
|
||||
|
||||
const cssVar = (name) => `rgb(var(${name}) / <alpha-value>)`;
|
||||
const cssVarWithAlpha = (name) => `rgb(var(${name}) / calc(var(${name}-alpha, 1) * <alpha-value>))`;
|
||||
const hslVar = (name) => `hsl(var(${name}))`;
|
||||
|
||||
/**
|
||||
|
|
@ -105,11 +108,11 @@ function createTailwindColors() {
|
|||
'surface-fixed-hover': cssVar('--surface-fixed-hover'),
|
||||
'text-fixed': cssVar('--text-fixed'),
|
||||
|
||||
'border-light': cssVar('--border-light'),
|
||||
'border-medium': cssVar('--border-medium'),
|
||||
'border-light': cssVarWithAlpha('--border-light'),
|
||||
'border-medium': cssVarWithAlpha('--border-medium'),
|
||||
'border-medium-alt': cssVar('--border-medium-alt'),
|
||||
'border-heavy': cssVar('--border-heavy'),
|
||||
'border-xheavy': cssVar('--border-xheavy'),
|
||||
'border-heavy': cssVarWithAlpha('--border-heavy'),
|
||||
'border-xheavy': cssVarWithAlpha('--border-xheavy'),
|
||||
'border-destructive': cssVar('--border-destructive'),
|
||||
|
||||
'status-success': cssVar('--status-success'),
|
||||
|
|
|
|||
20
packages/client/src/theme/utils/createTailwindColors.spec.js
Normal file
20
packages/client/src/theme/utils/createTailwindColors.spec.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
const { createTailwindColors } = require('./createTailwindColors');
|
||||
|
||||
describe('createTailwindColors', () => {
|
||||
it('combines intrinsic border alpha with Tailwind opacity modifiers', () => {
|
||||
const colors = createTailwindColors();
|
||||
|
||||
expect(colors['border-light']).toBe(
|
||||
'rgb(var(--border-light) / calc(var(--border-light-alpha, 1) * <alpha-value>))',
|
||||
);
|
||||
expect(colors['border-medium']).toBe(
|
||||
'rgb(var(--border-medium) / calc(var(--border-medium-alpha, 1) * <alpha-value>))',
|
||||
);
|
||||
expect(colors['border-heavy']).toBe(
|
||||
'rgb(var(--border-heavy) / calc(var(--border-heavy-alpha, 1) * <alpha-value>))',
|
||||
);
|
||||
expect(colors['border-xheavy']).toBe(
|
||||
'rgb(var(--border-xheavy) / calc(var(--border-xheavy-alpha, 1) * <alpha-value>))',
|
||||
);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue