Skip to content

Commit 17c6785

Browse files
feat(ui): Make lightDark theme the default (#7571)
1 parent 0501847 commit 17c6785

File tree

6 files changed

+143
-45
lines changed

6 files changed

+143
-45
lines changed

packages/ui/src/foundations/colors.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { colors as colorUtils } from '../utils/colors';
22
import { colorOptionToThemedAlphaScale, colorOptionToThemedLightnessScale } from '../utils/colors/scales';
33
import { clerkCssVar } from '../utils/cssVariables';
4+
import { lightDark } from '../utils/lightDark';
45

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

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

3940
const dangerScale = colorOptionToThemedLightnessScale(
4041
clerkCssVar('color-danger', '#EF4444'),
4142
'danger',
4243
) as LightnessScale<'danger'>;
4344
const primaryScale = colorOptionToThemedLightnessScale(
44-
clerkCssVar('color-primary', '#2F3037'),
45+
clerkCssVar('color-primary', lightDark('#2F3037', '#ffffff')),
4546
'primary',
4647
) as LightnessScale<'primary'>;
4748
const successScale = colorOptionToThemedLightnessScale(
@@ -62,7 +63,7 @@ const neutralAlphaScale = colorOptionToThemedAlphaScale(
6263
'neutralAlpha',
6364
) as AlphaScale<'neutralAlpha'>;
6465
const primaryAlphaScale = colorOptionToThemedAlphaScale(
65-
clerkCssVar('color-primary', '#2F3037'),
66+
clerkCssVar('color-primary', lightDark('#2F3037', '#ffffff')),
6667
'primaryAlpha',
6768
) as AlphaScale<'primaryAlpha'>;
6869
const successAlphaScale = colorOptionToThemedAlphaScale(
@@ -79,7 +80,7 @@ const borderAlphaScale = colorOptionToThemedAlphaScale(
7980
'borderAlpha',
8081
) as AlphaScale<'borderAlpha'>;
8182

82-
const colorForeground = clerkCssVar('color-foreground', '#212126');
83+
const colorForeground = clerkCssVar('color-foreground', lightDark('#212126', 'white'));
8384
const colorMutedForeground = clerkCssVar(
8485
'color-muted-foreground',
8586
colorUtils.makeTransparent(colorForeground, 0.35) || '#747686',
@@ -92,17 +93,17 @@ const colors = Object.freeze({
9293
'color-modal-backdrop',
9394
colorUtils.makeTransparent(defaultColorNeutral, 0.27) || neutralAlphaScale.neutralAlpha700,
9495
),
95-
colorBackground: clerkCssVar('color-background', 'white'),
96-
colorInput: clerkCssVar('color-input', 'white'),
96+
colorBackground: clerkCssVar('color-background', lightDark('#ffffff', '#212126')),
97+
colorInput: clerkCssVar('color-input', lightDark('white', '#26262B')),
9798
colorForeground,
9899
colorMutedForeground,
99100
colorMuted: undefined,
100101
colorRing: clerkCssVar(
101102
'color-ring',
102103
colorUtils.makeTransparent(defaultColorNeutral, 0.85) || neutralAlphaScale.neutralAlpha200,
103104
),
104-
colorInputForeground: clerkCssVar('color-input-foreground', '#131316'),
105-
colorPrimaryForeground: clerkCssVar('color-primary-foreground', 'white'),
105+
colorInputForeground: clerkCssVar('color-input-foreground', lightDark('#131316', 'white')),
106+
colorPrimaryForeground: clerkCssVar('color-primary-foreground', lightDark('white', 'black')),
106107
colorShimmer: clerkCssVar('color-shimmer', 'rgba(255, 255, 255, 0.36)'),
107108
transparent: 'transparent',
108109
white: 'white',

packages/ui/src/themes/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,3 @@ export * from './dark';
22
export * from './shadesOfPurple';
33
export * from './neobrutalism';
44
export * from './shadcn';
5-
export * from './lightDark';

packages/ui/src/themes/lightDark.ts

Lines changed: 0 additions & 22 deletions
This file was deleted.
Lines changed: 97 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,54 @@
11
import { afterAll, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';
22

33
import { clearCache, cssSupports } from '../cssSupports';
4+
import { lightDark } from '../lightDark';
45

5-
// Mock CSS.supports
6-
const originalCSSSupports = CSS.supports;
6+
// Store original CSS if it exists
7+
const originalCSS = globalThis.CSS;
78

89
beforeAll(() => {
9-
CSS.supports = vi.fn(feature => {
10-
if (feature === 'color: hsl(from white h s l)') {
11-
return true;
12-
}
13-
if (feature === 'color: color-mix(in srgb, white, black)') {
10+
// Create mock CSS global for Node.js environment
11+
globalThis.CSS = {
12+
supports: vi.fn((feature: string) => {
13+
if (feature === 'color: hsl(from white h s l)') {
14+
return true;
15+
}
16+
if (feature === 'color: color-mix(in srgb, white, black)') {
17+
return false;
18+
}
19+
if (feature === 'color: light-dark(white, black)') {
20+
return true;
21+
}
1422
return false;
15-
}
16-
return false;
17-
});
23+
}),
24+
} as unknown as typeof CSS;
1825
});
1926

2027
afterAll(() => {
21-
CSS.supports = originalCSSSupports;
28+
// Restore original CSS or remove if it didn't exist
29+
if (originalCSS) {
30+
globalThis.CSS = originalCSS;
31+
} else {
32+
// @ts-expect-error - cleaning up mock
33+
delete globalThis.CSS;
34+
}
2235
});
2336

2437
beforeEach(() => {
2538
clearCache();
26-
vi.mocked(CSS.supports).mockClear();
39+
// Reset mock to default behavior
40+
vi.mocked(globalThis.CSS.supports).mockImplementation((feature: string) => {
41+
if (feature === 'color: hsl(from white h s l)') {
42+
return true;
43+
}
44+
if (feature === 'color: color-mix(in srgb, white, black)') {
45+
return false;
46+
}
47+
if (feature === 'color: light-dark(white, black)') {
48+
return true;
49+
}
50+
return false;
51+
});
2752
});
2853

2954
describe('cssSupports', () => {
@@ -39,10 +64,68 @@ describe('cssSupports', () => {
3964
expect(cssSupports.modernColor()).toBe(true);
4065
});
4166

67+
test('lightDark should return true when supported', () => {
68+
expect(cssSupports.lightDark()).toBe(true);
69+
});
70+
4271
test('caching works correctly', () => {
72+
const initialCallCount = vi.mocked(globalThis.CSS.supports).mock.calls.length;
4373
cssSupports.relativeColorSyntax();
44-
expect(CSS.supports).toHaveBeenCalledTimes(1);
74+
expect(globalThis.CSS.supports).toHaveBeenCalledTimes(initialCallCount + 1);
4575
cssSupports.relativeColorSyntax();
46-
expect(CSS.supports).toHaveBeenCalledTimes(1); // Should not call again due to caching
76+
expect(globalThis.CSS.supports).toHaveBeenCalledTimes(initialCallCount + 1); // Should not call again due to caching
77+
});
78+
79+
test('lightDark caching works correctly', () => {
80+
const initialCallCount = vi.mocked(globalThis.CSS.supports).mock.calls.length;
81+
cssSupports.lightDark();
82+
expect(globalThis.CSS.supports).toHaveBeenCalledTimes(initialCallCount + 1);
83+
cssSupports.lightDark();
84+
expect(globalThis.CSS.supports).toHaveBeenCalledTimes(initialCallCount + 1); // Should not call again due to caching
85+
});
86+
});
87+
88+
describe('lightDark utility', () => {
89+
test('returns light-dark() when both lightDark and modernColor are supported', () => {
90+
// In this test setup: lightDark=true, relativeColorSyntax=true (so modernColor=true)
91+
const result = lightDark('#ffffff', '#000000');
92+
expect(result).toBe('light-dark(#ffffff, #000000)');
93+
});
94+
95+
test('returns light value when lightDark is not supported', () => {
96+
// Override mock to return false for lightDark
97+
vi.mocked(globalThis.CSS.supports).mockImplementation((feature: string) => {
98+
if (feature === 'color: light-dark(white, black)') {
99+
return false;
100+
}
101+
if (feature === 'color: hsl(from white h s l)') {
102+
return true;
103+
}
104+
return false;
105+
});
106+
clearCache();
107+
108+
const result = lightDark('#ffffff', '#000000');
109+
expect(result).toBe('#ffffff');
110+
});
111+
112+
test('returns light value when modernColor is not supported', () => {
113+
// Override mock to return true for lightDark but false for modern color features
114+
vi.mocked(globalThis.CSS.supports).mockImplementation((feature: string) => {
115+
if (feature === 'color: light-dark(white, black)') {
116+
return true;
117+
}
118+
// Both relativeColorSyntax and colorMix return false
119+
return false;
120+
});
121+
clearCache();
122+
123+
const result = lightDark('#ffffff', '#000000');
124+
expect(result).toBe('#ffffff');
125+
});
126+
127+
test('works with named colors', () => {
128+
const result = lightDark('white', 'black');
129+
expect(result).toBe('light-dark(white, black)');
47130
});
48131
});

packages/ui/src/utils/cssSupports.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
const CSS_FEATURE_TESTS: Record<string, string> = {
22
relativeColorSyntax: 'color: hsl(from white h s l)',
33
colorMix: 'color: color-mix(in srgb, white, black)',
4+
lightDark: 'color: light-dark(white, black)',
45
} as const;
56

67
let SUPPORTS_RELATIVE_COLOR: boolean | undefined;
78
let SUPPORTS_COLOR_MIX: boolean | undefined;
89
let SUPPORTS_MODERN_COLOR: boolean | undefined;
10+
let SUPPORTS_LIGHT_DARK: boolean | undefined;
911

1012
export const cssSupports = {
1113
relativeColorSyntax: () => {
@@ -47,10 +49,26 @@ export const cssSupports = {
4749

4850
return SUPPORTS_MODERN_COLOR;
4951
},
52+
/**
53+
* Returns true if the light-dark() CSS function is supported
54+
*/
55+
lightDark: () => {
56+
if (SUPPORTS_LIGHT_DARK !== undefined) {
57+
return SUPPORTS_LIGHT_DARK;
58+
}
59+
try {
60+
SUPPORTS_LIGHT_DARK = CSS.supports(CSS_FEATURE_TESTS.lightDark);
61+
} catch {
62+
SUPPORTS_LIGHT_DARK = false;
63+
}
64+
65+
return SUPPORTS_LIGHT_DARK;
66+
},
5067
};
5168

5269
export const clearCache = () => {
5370
SUPPORTS_RELATIVE_COLOR = undefined;
5471
SUPPORTS_COLOR_MIX = undefined;
5572
SUPPORTS_MODERN_COLOR = undefined;
73+
SUPPORTS_LIGHT_DARK = undefined;
5674
};

packages/ui/src/utils/lightDark.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { cssSupports } from './cssSupports';
2+
3+
/**
4+
* Returns a light-dark() CSS function string when supported,
5+
* otherwise returns the light value as a fallback.
6+
*
7+
* This ensures compatibility with the legacy color system which
8+
* cannot parse light-dark() syntax.
9+
*
10+
* @param light - The color value for light mode
11+
* @param dark - The color value for dark mode
12+
* @returns CSS light-dark() function or the light value
13+
*/
14+
export function lightDark(light: string, dark: string): string {
15+
if (cssSupports.lightDark() && cssSupports.modernColor()) {
16+
return `light-dark(${light}, ${dark})`;
17+
}
18+
return light;
19+
}

0 commit comments

Comments
 (0)