Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions packages/ui/src/foundations/colors.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { colors as colorUtils } from '../utils/colors';
import { colorOptionToThemedAlphaScale, colorOptionToThemedLightnessScale } from '../utils/colors/scales';
import { clerkCssVar } from '../utils/cssVariables';
import { lightDark } from '../utils/lightDark';

const whiteAlpha = Object.freeze({
whiteAlpha25: 'hsla(0, 0%, 100%, 0.02)',
Expand Down Expand Up @@ -34,14 +35,14 @@ type AlphaScale<T extends string> = NonNullable<ReturnType<typeof colorOptionToT
* Therefore, it's safe to assert these as NonNullable.
*/

const defaultColorNeutral = clerkCssVar('color-neutral', '#000000');
const defaultColorNeutral = clerkCssVar('color-neutral', lightDark('#000000', '#ffffff'));

const dangerScale = colorOptionToThemedLightnessScale(
clerkCssVar('color-danger', '#EF4444'),
'danger',
) as LightnessScale<'danger'>;
const primaryScale = colorOptionToThemedLightnessScale(
clerkCssVar('color-primary', '#2F3037'),
clerkCssVar('color-primary', lightDark('#2F3037', '#ffffff')),
'primary',
) as LightnessScale<'primary'>;
const successScale = colorOptionToThemedLightnessScale(
Expand All @@ -62,7 +63,7 @@ const neutralAlphaScale = colorOptionToThemedAlphaScale(
'neutralAlpha',
) as AlphaScale<'neutralAlpha'>;
const primaryAlphaScale = colorOptionToThemedAlphaScale(
clerkCssVar('color-primary', '#2F3037'),
clerkCssVar('color-primary', lightDark('#2F3037', '#ffffff')),
'primaryAlpha',
) as AlphaScale<'primaryAlpha'>;
const successAlphaScale = colorOptionToThemedAlphaScale(
Expand All @@ -79,7 +80,7 @@ const borderAlphaScale = colorOptionToThemedAlphaScale(
'borderAlpha',
) as AlphaScale<'borderAlpha'>;

const colorForeground = clerkCssVar('color-foreground', '#212126');
const colorForeground = clerkCssVar('color-foreground', lightDark('#212126', 'white'));
const colorMutedForeground = clerkCssVar(
'color-muted-foreground',
colorUtils.makeTransparent(colorForeground, 0.35) || '#747686',
Expand All @@ -92,17 +93,17 @@ const colors = Object.freeze({
'color-modal-backdrop',
colorUtils.makeTransparent(defaultColorNeutral, 0.27) || neutralAlphaScale.neutralAlpha700,
),
colorBackground: clerkCssVar('color-background', 'white'),
colorInput: clerkCssVar('color-input', 'white'),
colorBackground: clerkCssVar('color-background', lightDark('#ffffff', '#212126')),
colorInput: clerkCssVar('color-input', lightDark('white', '#26262B')),
colorForeground,
colorMutedForeground,
colorMuted: undefined,
colorRing: clerkCssVar(
'color-ring',
colorUtils.makeTransparent(defaultColorNeutral, 0.85) || neutralAlphaScale.neutralAlpha200,
),
colorInputForeground: clerkCssVar('color-input-foreground', '#131316'),
colorPrimaryForeground: clerkCssVar('color-primary-foreground', 'white'),
colorInputForeground: clerkCssVar('color-input-foreground', lightDark('#131316', 'white')),
colorPrimaryForeground: clerkCssVar('color-primary-foreground', lightDark('white', 'black')),
colorShimmer: clerkCssVar('color-shimmer', 'rgba(255, 255, 255, 0.36)'),
transparent: 'transparent',
white: 'white',
Expand Down
1 change: 0 additions & 1 deletion packages/ui/src/themes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,3 @@ export * from './dark';
export * from './shadesOfPurple';
export * from './neobrutalism';
export * from './shadcn';
export * from './lightDark';
22 changes: 0 additions & 22 deletions packages/ui/src/themes/lightDark.ts

This file was deleted.

111 changes: 97 additions & 14 deletions packages/ui/src/utils/__tests__/cssSupports.test.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,54 @@
import { afterAll, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';

import { clearCache, cssSupports } from '../cssSupports';
import { lightDark } from '../lightDark';

// Mock CSS.supports
const originalCSSSupports = CSS.supports;
// Store original CSS if it exists
const originalCSS = globalThis.CSS;

beforeAll(() => {
CSS.supports = vi.fn(feature => {
if (feature === 'color: hsl(from white h s l)') {
return true;
}
if (feature === 'color: color-mix(in srgb, white, black)') {
// Create mock CSS global for Node.js environment
globalThis.CSS = {
supports: vi.fn((feature: string) => {
if (feature === 'color: hsl(from white h s l)') {
return true;
}
if (feature === 'color: color-mix(in srgb, white, black)') {
return false;
}
if (feature === 'color: light-dark(white, black)') {
return true;
}
return false;
}
return false;
});
}),
} as unknown as typeof CSS;
});

afterAll(() => {
CSS.supports = originalCSSSupports;
// Restore original CSS or remove if it didn't exist
if (originalCSS) {
globalThis.CSS = originalCSS;
} else {
// @ts-expect-error - cleaning up mock
delete globalThis.CSS;
}
});

beforeEach(() => {
clearCache();
vi.mocked(CSS.supports).mockClear();
// Reset mock to default behavior
vi.mocked(globalThis.CSS.supports).mockImplementation((feature: string) => {
if (feature === 'color: hsl(from white h s l)') {
return true;
}
if (feature === 'color: color-mix(in srgb, white, black)') {
return false;
}
if (feature === 'color: light-dark(white, black)') {
return true;
}
return false;
});
});

describe('cssSupports', () => {
Expand All @@ -39,10 +64,68 @@ describe('cssSupports', () => {
expect(cssSupports.modernColor()).toBe(true);
});

test('lightDark should return true when supported', () => {
expect(cssSupports.lightDark()).toBe(true);
});

test('caching works correctly', () => {
const initialCallCount = vi.mocked(globalThis.CSS.supports).mock.calls.length;
cssSupports.relativeColorSyntax();
expect(CSS.supports).toHaveBeenCalledTimes(1);
expect(globalThis.CSS.supports).toHaveBeenCalledTimes(initialCallCount + 1);
cssSupports.relativeColorSyntax();
expect(CSS.supports).toHaveBeenCalledTimes(1); // Should not call again due to caching
expect(globalThis.CSS.supports).toHaveBeenCalledTimes(initialCallCount + 1); // Should not call again due to caching
});

test('lightDark caching works correctly', () => {
const initialCallCount = vi.mocked(globalThis.CSS.supports).mock.calls.length;
cssSupports.lightDark();
expect(globalThis.CSS.supports).toHaveBeenCalledTimes(initialCallCount + 1);
cssSupports.lightDark();
expect(globalThis.CSS.supports).toHaveBeenCalledTimes(initialCallCount + 1); // Should not call again due to caching
});
});

describe('lightDark utility', () => {
test('returns light-dark() when both lightDark and modernColor are supported', () => {
// In this test setup: lightDark=true, relativeColorSyntax=true (so modernColor=true)
const result = lightDark('#ffffff', '#000000');
expect(result).toBe('light-dark(#ffffff, #000000)');
});

test('returns light value when lightDark is not supported', () => {
// Override mock to return false for lightDark
vi.mocked(globalThis.CSS.supports).mockImplementation((feature: string) => {
if (feature === 'color: light-dark(white, black)') {
return false;
}
if (feature === 'color: hsl(from white h s l)') {
return true;
}
return false;
});
clearCache();

const result = lightDark('#ffffff', '#000000');
expect(result).toBe('#ffffff');
});

test('returns light value when modernColor is not supported', () => {
// Override mock to return true for lightDark but false for modern color features
vi.mocked(globalThis.CSS.supports).mockImplementation((feature: string) => {
if (feature === 'color: light-dark(white, black)') {
return true;
}
// Both relativeColorSyntax and colorMix return false
return false;
});
clearCache();

const result = lightDark('#ffffff', '#000000');
expect(result).toBe('#ffffff');
});

test('works with named colors', () => {
const result = lightDark('white', 'black');
expect(result).toBe('light-dark(white, black)');
});
});
18 changes: 18 additions & 0 deletions packages/ui/src/utils/cssSupports.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
const CSS_FEATURE_TESTS: Record<string, string> = {
relativeColorSyntax: 'color: hsl(from white h s l)',
colorMix: 'color: color-mix(in srgb, white, black)',
lightDark: 'color: light-dark(white, black)',
} as const;

let SUPPORTS_RELATIVE_COLOR: boolean | undefined;
let SUPPORTS_COLOR_MIX: boolean | undefined;
let SUPPORTS_MODERN_COLOR: boolean | undefined;
let SUPPORTS_LIGHT_DARK: boolean | undefined;

export const cssSupports = {
relativeColorSyntax: () => {
Expand Down Expand Up @@ -47,10 +49,26 @@ export const cssSupports = {

return SUPPORTS_MODERN_COLOR;
},
/**
* Returns true if the light-dark() CSS function is supported
*/
lightDark: () => {
if (SUPPORTS_LIGHT_DARK !== undefined) {
return SUPPORTS_LIGHT_DARK;
}
try {
SUPPORTS_LIGHT_DARK = CSS.supports(CSS_FEATURE_TESTS.lightDark);
} catch {
SUPPORTS_LIGHT_DARK = false;
}

return SUPPORTS_LIGHT_DARK;
},
};

export const clearCache = () => {
SUPPORTS_RELATIVE_COLOR = undefined;
SUPPORTS_COLOR_MIX = undefined;
SUPPORTS_MODERN_COLOR = undefined;
SUPPORTS_LIGHT_DARK = undefined;
};
19 changes: 19 additions & 0 deletions packages/ui/src/utils/lightDark.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { cssSupports } from './cssSupports';

/**
* Returns a light-dark() CSS function string when supported,
* otherwise returns the light value as a fallback.
*
* This ensures compatibility with the legacy color system which
* cannot parse light-dark() syntax.
*
* @param light - The color value for light mode
* @param dark - The color value for dark mode
* @returns CSS light-dark() function or the light value
*/
export function lightDark(light: string, dark: string): string {
if (cssSupports.lightDark() && cssSupports.modernColor()) {
return `light-dark(${light}, ${dark})`;
}
return light;
}