Skip to content

Commit d4d2612

Browse files
refactor(clerk-js,types): Refactor base theme approach (#6371)
Co-authored-by: Tom Milewski <me@tm.codes>
1 parent e404456 commit d4d2612

File tree

20 files changed

+273
-63
lines changed

20 files changed

+273
-63
lines changed

.changeset/yummy-plants-jog.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@clerk/clerk-js': minor
3+
'@clerk/types': minor
4+
---
5+
6+
Refactor base theme approach to enable opting into simple theme.
7+
8+
```tsx
9+
appearance={{
10+
theme: 'simple' // removes Clerk base theme
11+
}}
12+
```

packages/clerk-js/sandbox/app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,8 @@ function appearanceVariableOptions() {
192192
const updateVariables = () => {
193193
void Clerk.__unstable__updateProps({
194194
appearance: {
195+
// Preserve existing appearance properties like baseTheme
196+
...Clerk.__internal_getOption('appearance'),
195197
variables: Object.fromEntries(
196198
Object.entries(variableInputs).map(([key, input]) => {
197199
sessionStorage.setItem(key, input.value);

packages/clerk-js/src/ui/polishedAppearance.ts renamed to packages/clerk-js/src/ui/baseTheme.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ const inputStyles = (theme: InternalTheme) => ({
9393
}),
9494
});
9595

96-
export const polishedAppearance: Appearance = {
96+
const clerkTheme: Appearance = {
9797
elements: ({ theme }: { theme: InternalTheme }): Elements => {
9898
return {
9999
button: {
@@ -266,3 +266,21 @@ export const polishedAppearance: Appearance = {
266266
};
267267
},
268268
} satisfies Appearance;
269+
270+
const simpleTheme: Appearance = {
271+
// @ts-expect-error Internal API for simple theme detection
272+
simpleStyles: true,
273+
elements: {},
274+
} satisfies Appearance;
275+
276+
export const getBaseTheme = (theme: 'clerk' | 'simple' = 'clerk'): Appearance => {
277+
switch (theme) {
278+
case 'simple':
279+
return simpleTheme;
280+
case 'clerk':
281+
default:
282+
return clerkTheme;
283+
}
284+
};
285+
286+
export const baseTheme = clerkTheme;

packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherPopover.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ export const OrganizationSwitcherPopover = React.forwardRef<HTMLDivElement, Orga
117117
label={localizationKeys('organizationSwitcher.action__manageOrganization')}
118118
onClick={() => handleItemClick()}
119119
trailing={<NotificationCountBadgeManageButton />}
120+
focusRing
120121
/>
121122
);
122123

@@ -125,6 +126,7 @@ export const OrganizationSwitcherPopover = React.forwardRef<HTMLDivElement, Orga
125126
icon={Billing}
126127
label={runIfFunctionOrReturn(__unstable_manageBillingLabel) || 'Upgrade'}
127128
onClick={() => router.navigate(runIfFunctionOrReturn(__unstable_manageBillingUrl))}
129+
focusRing
128130
/>
129131
);
130132

packages/clerk-js/src/ui/components/PricingTable/PricingTableDefault.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,7 @@ function Card(props: CardProps) {
169169
background: common.mutedBackground(t),
170170
borderWidth: t.borderWidths.$normal,
171171
borderStyle: t.borderStyles.$solid,
172-
borderColor: t.colors.$borderAlpha100,
173-
boxShadow: !isCompact ? t.shadows.$cardBoxShadow : t.shadows.$tableBodyShadow,
172+
borderColor: t.colors.$borderAlpha150,
174173
borderRadius: t.radii.$xl,
175174
overflow: 'hidden',
176175
textAlign: 'left',
@@ -205,7 +204,7 @@ function Card(props: CardProps) {
205204
backgroundColor: hasFeatures ? t.colors.$colorBackground : 'transparent',
206205
borderTopWidth: hasFeatures ? t.borderWidths.$normal : 0,
207206
borderTopStyle: t.borderStyles.$solid,
208-
borderTopColor: t.colors.$borderAlpha100,
207+
borderTopColor: t.colors.$borderAlpha150,
209208
})}
210209
data-variant={isCompact ? 'compact' : 'default'}
211210
>
@@ -225,7 +224,7 @@ function Card(props: CardProps) {
225224
padding: isCompact ? t.space.$3 : t.space.$4,
226225
borderTopWidth: t.borderWidths.$normal,
227226
borderTopStyle: t.borderStyles.$solid,
228-
borderTopColor: t.colors.$borderAlpha100,
227+
borderTopColor: t.colors.$borderAlpha150,
229228
order: ctaPosition === 'top' ? -1 : undefined,
230229
})}
231230
>

packages/clerk-js/src/ui/components/UserButton/SessionActions.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ export const MultiSessionActions = (props: MultiSessionActionsProps) => {
182182
icon={CogFilled}
183183
label={localizationKeys('userButton.action__manageAccount')}
184184
onClick={handleManageAccountClicked}
185+
focusRing
185186
/>
186187
<SmallAction
187188
elementDescriptor={descriptors.userButtonPopoverActionButton}
@@ -193,6 +194,7 @@ export const MultiSessionActions = (props: MultiSessionActionsProps) => {
193194
icon={SignOut}
194195
label={localizationKeys('userButton.action__signOut')}
195196
onClick={handleSignOutSessionClicked(session)}
197+
focusRing
196198
/>
197199
</Flex>
198200
</SmallActions>

packages/clerk-js/src/ui/customizables/__tests__/parseAppearance.test.tsx

Lines changed: 123 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ describe('AppearanceProvider layout flows', () => {
338338
expect(result.current.parsedLayout.socialButtonsVariant).toBe('blockButton');
339339
});
340340

341-
it('removes the polishedAppearance when simpleStyles is passed to globalAppearance', () => {
341+
it('removes the baseTheme when simpleStyles is passed to globalAppearance', () => {
342342
const wrapper = ({ children }) => (
343343
<AppearanceProvider
344344
appearanceKey='signIn'
@@ -359,7 +359,7 @@ describe('AppearanceProvider layout flows', () => {
359359
expect(result.current.parsedElements[0]['alert'].backgroundColor).toBe(themeAColor);
360360
});
361361

362-
it('removes the polishedAppearance when simpleStyles is passed to appearance', () => {
362+
it('removes the baseTheme when simpleStyles is passed to appearance', () => {
363363
const wrapper = ({ children }) => (
364364
<AppearanceProvider
365365
appearanceKey='signIn'
@@ -464,3 +464,124 @@ describe('AppearanceProvider captcha', () => {
464464
expect(result.current.parsedCaptcha.language).toBe('');
465465
});
466466
});
467+
468+
describe('AppearanceProvider theme flows', () => {
469+
it('supports string-based theme property with "clerk" value', () => {
470+
const wrapper = ({ children }) => (
471+
<AppearanceProvider
472+
appearanceKey='signIn'
473+
appearance={{
474+
theme: 'clerk',
475+
}}
476+
>
477+
{children}
478+
</AppearanceProvider>
479+
);
480+
481+
const { result } = renderHook(() => useAppearance(), { wrapper });
482+
// Should include clerk theme styles (baseTheme will be included)
483+
expect(result.current.parsedElements.length).toBeGreaterThan(0);
484+
});
485+
486+
it('supports string-based theme property with "simple" value', () => {
487+
const wrapper = ({ children }) => (
488+
<AppearanceProvider
489+
appearanceKey='signIn'
490+
appearance={{
491+
theme: 'simple',
492+
}}
493+
>
494+
{children}
495+
</AppearanceProvider>
496+
);
497+
498+
const { result } = renderHook(() => useAppearance(), { wrapper });
499+
// Should include both simple theme and base theme (2 elements total)
500+
expect(result.current.parsedElements.length).toBe(2);
501+
});
502+
503+
it('theme property takes precedence over deprecated baseTheme', () => {
504+
const wrapper = ({ children }) => (
505+
<AppearanceProvider
506+
appearanceKey='signIn'
507+
appearance={{
508+
theme: 'simple',
509+
baseTheme: 'clerk', // This should be ignored
510+
}}
511+
>
512+
{children}
513+
</AppearanceProvider>
514+
);
515+
516+
const { result } = renderHook(() => useAppearance(), { wrapper });
517+
// Should include both simple theme and base theme (2 elements total)
518+
expect(result.current.parsedElements.length).toBe(2);
519+
});
520+
521+
it('maintains backward compatibility with baseTheme property', () => {
522+
const wrapper = ({ children }) => (
523+
<AppearanceProvider
524+
appearanceKey='signIn'
525+
appearance={{
526+
baseTheme: 'simple',
527+
}}
528+
>
529+
{children}
530+
</AppearanceProvider>
531+
);
532+
533+
const { result } = renderHook(() => useAppearance(), { wrapper });
534+
// Should work the same as theme: 'simple' (2 elements total)
535+
expect(result.current.parsedElements.length).toBe(2);
536+
});
537+
538+
it('supports object-based themes with new theme property', () => {
539+
const customTheme = {
540+
elements: {
541+
card: { backgroundColor: 'red' },
542+
},
543+
};
544+
545+
const wrapper = ({ children }) => (
546+
<AppearanceProvider
547+
appearanceKey='signIn'
548+
appearance={{
549+
theme: customTheme,
550+
}}
551+
>
552+
{children}
553+
</AppearanceProvider>
554+
);
555+
556+
const { result } = renderHook(() => useAppearance(), { wrapper });
557+
// Should include base theme + custom theme
558+
expect(result.current.parsedElements.length).toBeGreaterThan(1);
559+
expect(result.current.parsedElements.some(el => el.card?.backgroundColor === 'red')).toBe(true);
560+
});
561+
562+
it('supports array-based themes with new theme property', () => {
563+
const themeA = {
564+
elements: { card: { backgroundColor: 'red' } },
565+
};
566+
const themeB = {
567+
elements: { card: { color: 'blue' } },
568+
};
569+
570+
const wrapper = ({ children }) => (
571+
<AppearanceProvider
572+
appearanceKey='signIn'
573+
appearance={{
574+
theme: [themeA, themeB],
575+
}}
576+
>
577+
{children}
578+
</AppearanceProvider>
579+
);
580+
581+
const { result } = renderHook(() => useAppearance(), { wrapper });
582+
// Should include base theme + both custom themes
583+
expect(result.current.parsedElements.length).toBeGreaterThan(2);
584+
expect(result.current.parsedElements.some(el => el.card?.backgroundColor === 'red')).toBe(true);
585+
expect(result.current.parsedElements.some(el => el.card?.color === 'blue')).toBe(true);
586+
});
587+
});

packages/clerk-js/src/ui/customizables/parseAppearance.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { fastDeepMergeAndReplace } from '@clerk/shared/utils';
22
import type { Appearance, CaptchaAppearanceOptions, DeepPartial, Elements, Layout, Theme } from '@clerk/types';
33

4+
import { baseTheme, getBaseTheme } from '../baseTheme';
45
import { createInternalTheme, defaultInternalTheme } from '../foundations';
5-
import { polishedAppearance } from '../polishedAppearance';
66
import type { InternalTheme } from '../styledSystem';
77
import {
88
createColorScales,
@@ -21,7 +21,7 @@ export type ParsedCaptcha = Required<CaptchaAppearanceOptions>;
2121

2222
type PublicAppearanceTopLevelKey = keyof Omit<
2323
Appearance,
24-
'baseTheme' | 'elements' | 'layout' | 'variables' | 'captcha' | 'cssLayerName'
24+
'baseTheme' | 'theme' | 'elements' | 'layout' | 'variables' | 'captcha' | 'cssLayerName'
2525
>;
2626

2727
export type AppearanceCascade = {
@@ -83,7 +83,7 @@ export const parseAppearance = (cascade: AppearanceCascade): ParsedAppearance =>
8383
return !!a.simpleStyles;
8484
})
8585
) {
86-
appearanceList.unshift(polishedAppearance);
86+
appearanceList.unshift(baseTheme);
8787
}
8888

8989
const parsedElements = parseElements(
@@ -104,9 +104,18 @@ const expand = (theme: Theme | undefined, cascade: any[]) => {
104104
return;
105105
}
106106

107-
(Array.isArray(theme.baseTheme) ? theme.baseTheme : [theme.baseTheme]).forEach(baseTheme =>
108-
expand(baseTheme as Theme, cascade),
109-
);
107+
// Use new 'theme' property if available, otherwise fall back to deprecated 'baseTheme'
108+
const themeProperty = theme.theme !== undefined ? theme.theme : theme.baseTheme;
109+
110+
if (themeProperty !== undefined) {
111+
(Array.isArray(themeProperty) ? themeProperty : [themeProperty]).forEach(baseTheme => {
112+
if (typeof baseTheme === 'string') {
113+
expand(getBaseTheme(baseTheme), cascade);
114+
} else {
115+
expand(baseTheme as Theme, cascade);
116+
}
117+
});
118+
}
110119

111120
cascade.push(theme);
112121
};
@@ -122,7 +131,18 @@ const parseLayout = (appearanceList: Appearance[]) => {
122131
const parseCaptcha = (appearanceList: Appearance[]) => {
123132
return {
124133
...defaultCaptchaOptions,
125-
...appearanceList.reduce((acc, appearance) => ({ ...acc, ...appearance.captcha }), {}),
134+
...appearanceList.reduce((acc, appearance) => {
135+
if (appearance.captcha) {
136+
const { theme: captchaTheme, size, language } = appearance.captcha;
137+
return {
138+
...acc,
139+
...(captchaTheme && { theme: captchaTheme }),
140+
...(size && { size }),
141+
...(language && { language }),
142+
};
143+
}
144+
return acc;
145+
}, {} as Partial<CaptchaAppearanceOptions>),
126146
};
127147
};
128148

packages/clerk-js/src/ui/elements/Action/ActionCard.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,12 @@ export const ActionCard = (props: ActionCardProps) => {
2424
elementDescriptor={descriptors.actionCard}
2525
sx={[
2626
t => ({
27-
boxShadow: t.shadows.$actionCardShadow,
2827
gap: t.space.$4,
2928
borderRadius: t.radii.$lg,
3029
padding: `${t.space.$4} ${t.space.$5}`,
3130
borderWidth: t.borderWidths.$normal,
3231
borderStyle: t.borderStyles.$solid,
33-
borderColor: t.colors.$borderAlpha100,
32+
borderColor: t.colors.$borderAlpha150,
3433
...styles(t)[variant],
3534
}),
3635
sx,

packages/clerk-js/src/ui/elements/Card/CardContent.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,14 @@ export const CardContent = React.forwardRef<HTMLDivElement, CardContentProps>((p
3939
zIndex: t.zIndices.$card,
4040
borderWidth: t.borderWidths.$normal,
4141
borderStyle: t.borderStyles.$solid,
42-
borderColor: t.colors.$borderAlpha50,
43-
boxShadow: t.shadows.$cardContentShadow,
42+
borderColor: t.colors.$borderAlpha150,
4443
borderRadius: t.radii.$lg,
4544
position: 'relative',
4645
padding: `${t.space.$8} ${t.space.$10}`,
4746
justifyContent: 'center',
4847
alignContent: 'center',
48+
marginBlockStart: '-1px',
49+
marginInline: '-1px',
4950
}),
5051
sx,
5152
]}

0 commit comments

Comments
 (0)