diff --git a/.changeset/yummy-plants-jog.md b/.changeset/yummy-plants-jog.md new file mode 100644 index 00000000000..81fe3a05378 --- /dev/null +++ b/.changeset/yummy-plants-jog.md @@ -0,0 +1,12 @@ +--- +'@clerk/clerk-js': minor +'@clerk/types': minor +--- + +Refactor base theme approach to enable opting into simple theme. + +```tsx +appearance={{ + theme: 'simple' // removes Clerk base theme +}} +``` diff --git a/packages/clerk-js/sandbox/app.ts b/packages/clerk-js/sandbox/app.ts index c04981e2b77..cafc549ea1f 100644 --- a/packages/clerk-js/sandbox/app.ts +++ b/packages/clerk-js/sandbox/app.ts @@ -192,6 +192,8 @@ function appearanceVariableOptions() { const updateVariables = () => { void Clerk.__unstable__updateProps({ appearance: { + // Preserve existing appearance properties like baseTheme + ...Clerk.__internal_getOption('appearance'), variables: Object.fromEntries( Object.entries(variableInputs).map(([key, input]) => { sessionStorage.setItem(key, input.value); diff --git a/packages/clerk-js/src/ui/polishedAppearance.ts b/packages/clerk-js/src/ui/baseTheme.ts similarity index 95% rename from packages/clerk-js/src/ui/polishedAppearance.ts rename to packages/clerk-js/src/ui/baseTheme.ts index 57e71c21831..f1862b14902 100644 --- a/packages/clerk-js/src/ui/polishedAppearance.ts +++ b/packages/clerk-js/src/ui/baseTheme.ts @@ -93,7 +93,7 @@ const inputStyles = (theme: InternalTheme) => ({ }), }); -export const polishedAppearance: Appearance = { +const clerkTheme: Appearance = { elements: ({ theme }: { theme: InternalTheme }): Elements => { return { button: { @@ -266,3 +266,21 @@ export const polishedAppearance: Appearance = { }; }, } satisfies Appearance; + +const simpleTheme: Appearance = { + // @ts-expect-error Internal API for simple theme detection + simpleStyles: true, + elements: {}, +} satisfies Appearance; + +export const getBaseTheme = (theme: 'clerk' | 'simple' = 'clerk'): Appearance => { + switch (theme) { + case 'simple': + return simpleTheme; + case 'clerk': + default: + return clerkTheme; + } +}; + +export const baseTheme = clerkTheme; diff --git a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherPopover.tsx b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherPopover.tsx index 541546f0c6f..cc639569564 100644 --- a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherPopover.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherPopover.tsx @@ -117,6 +117,7 @@ export const OrganizationSwitcherPopover = React.forwardRef handleItemClick()} trailing={} + focusRing /> ); @@ -125,6 +126,7 @@ export const OrganizationSwitcherPopover = React.forwardRef router.navigate(runIfFunctionOrReturn(__unstable_manageBillingUrl))} + focusRing /> ); diff --git a/packages/clerk-js/src/ui/components/PricingTable/PricingTableDefault.tsx b/packages/clerk-js/src/ui/components/PricingTable/PricingTableDefault.tsx index 94519af5c42..9d2ea35b194 100644 --- a/packages/clerk-js/src/ui/components/PricingTable/PricingTableDefault.tsx +++ b/packages/clerk-js/src/ui/components/PricingTable/PricingTableDefault.tsx @@ -169,8 +169,7 @@ function Card(props: CardProps) { background: common.mutedBackground(t), borderWidth: t.borderWidths.$normal, borderStyle: t.borderStyles.$solid, - borderColor: t.colors.$borderAlpha100, - boxShadow: !isCompact ? t.shadows.$cardBoxShadow : t.shadows.$tableBodyShadow, + borderColor: t.colors.$borderAlpha150, borderRadius: t.radii.$xl, overflow: 'hidden', textAlign: 'left', @@ -205,7 +204,7 @@ function Card(props: CardProps) { backgroundColor: hasFeatures ? t.colors.$colorBackground : 'transparent', borderTopWidth: hasFeatures ? t.borderWidths.$normal : 0, borderTopStyle: t.borderStyles.$solid, - borderTopColor: t.colors.$borderAlpha100, + borderTopColor: t.colors.$borderAlpha150, })} data-variant={isCompact ? 'compact' : 'default'} > @@ -225,7 +224,7 @@ function Card(props: CardProps) { padding: isCompact ? t.space.$3 : t.space.$4, borderTopWidth: t.borderWidths.$normal, borderTopStyle: t.borderStyles.$solid, - borderTopColor: t.colors.$borderAlpha100, + borderTopColor: t.colors.$borderAlpha150, order: ctaPosition === 'top' ? -1 : undefined, })} > diff --git a/packages/clerk-js/src/ui/components/UserButton/SessionActions.tsx b/packages/clerk-js/src/ui/components/UserButton/SessionActions.tsx index b8d36543744..c7780d5b78f 100644 --- a/packages/clerk-js/src/ui/components/UserButton/SessionActions.tsx +++ b/packages/clerk-js/src/ui/components/UserButton/SessionActions.tsx @@ -182,6 +182,7 @@ export const MultiSessionActions = (props: MultiSessionActionsProps) => { icon={CogFilled} label={localizationKeys('userButton.action__manageAccount')} onClick={handleManageAccountClicked} + focusRing /> { icon={SignOut} label={localizationKeys('userButton.action__signOut')} onClick={handleSignOutSessionClicked(session)} + focusRing /> diff --git a/packages/clerk-js/src/ui/customizables/__tests__/parseAppearance.test.tsx b/packages/clerk-js/src/ui/customizables/__tests__/parseAppearance.test.tsx index 0ea3b198e88..022df997c8b 100644 --- a/packages/clerk-js/src/ui/customizables/__tests__/parseAppearance.test.tsx +++ b/packages/clerk-js/src/ui/customizables/__tests__/parseAppearance.test.tsx @@ -338,7 +338,7 @@ describe('AppearanceProvider layout flows', () => { expect(result.current.parsedLayout.socialButtonsVariant).toBe('blockButton'); }); - it('removes the polishedAppearance when simpleStyles is passed to globalAppearance', () => { + it('removes the baseTheme when simpleStyles is passed to globalAppearance', () => { const wrapper = ({ children }) => ( { expect(result.current.parsedElements[0]['alert'].backgroundColor).toBe(themeAColor); }); - it('removes the polishedAppearance when simpleStyles is passed to appearance', () => { + it('removes the baseTheme when simpleStyles is passed to appearance', () => { const wrapper = ({ children }) => ( { expect(result.current.parsedCaptcha.language).toBe(''); }); }); + +describe('AppearanceProvider theme flows', () => { + it('supports string-based theme property with "clerk" value', () => { + const wrapper = ({ children }) => ( + + {children} + + ); + + const { result } = renderHook(() => useAppearance(), { wrapper }); + // Should include clerk theme styles (baseTheme will be included) + expect(result.current.parsedElements.length).toBeGreaterThan(0); + }); + + it('supports string-based theme property with "simple" value', () => { + const wrapper = ({ children }) => ( + + {children} + + ); + + const { result } = renderHook(() => useAppearance(), { wrapper }); + // Should include both simple theme and base theme (2 elements total) + expect(result.current.parsedElements.length).toBe(2); + }); + + it('theme property takes precedence over deprecated baseTheme', () => { + const wrapper = ({ children }) => ( + + {children} + + ); + + const { result } = renderHook(() => useAppearance(), { wrapper }); + // Should include both simple theme and base theme (2 elements total) + expect(result.current.parsedElements.length).toBe(2); + }); + + it('maintains backward compatibility with baseTheme property', () => { + const wrapper = ({ children }) => ( + + {children} + + ); + + const { result } = renderHook(() => useAppearance(), { wrapper }); + // Should work the same as theme: 'simple' (2 elements total) + expect(result.current.parsedElements.length).toBe(2); + }); + + it('supports object-based themes with new theme property', () => { + const customTheme = { + elements: { + card: { backgroundColor: 'red' }, + }, + }; + + const wrapper = ({ children }) => ( + + {children} + + ); + + const { result } = renderHook(() => useAppearance(), { wrapper }); + // Should include base theme + custom theme + expect(result.current.parsedElements.length).toBeGreaterThan(1); + expect(result.current.parsedElements.some(el => el.card?.backgroundColor === 'red')).toBe(true); + }); + + it('supports array-based themes with new theme property', () => { + const themeA = { + elements: { card: { backgroundColor: 'red' } }, + }; + const themeB = { + elements: { card: { color: 'blue' } }, + }; + + const wrapper = ({ children }) => ( + + {children} + + ); + + const { result } = renderHook(() => useAppearance(), { wrapper }); + // Should include base theme + both custom themes + expect(result.current.parsedElements.length).toBeGreaterThan(2); + expect(result.current.parsedElements.some(el => el.card?.backgroundColor === 'red')).toBe(true); + expect(result.current.parsedElements.some(el => el.card?.color === 'blue')).toBe(true); + }); +}); diff --git a/packages/clerk-js/src/ui/customizables/parseAppearance.ts b/packages/clerk-js/src/ui/customizables/parseAppearance.ts index be3e2c45b01..c542f24c0b2 100644 --- a/packages/clerk-js/src/ui/customizables/parseAppearance.ts +++ b/packages/clerk-js/src/ui/customizables/parseAppearance.ts @@ -1,8 +1,8 @@ import { fastDeepMergeAndReplace } from '@clerk/shared/utils'; import type { Appearance, CaptchaAppearanceOptions, DeepPartial, Elements, Layout, Theme } from '@clerk/types'; +import { baseTheme, getBaseTheme } from '../baseTheme'; import { createInternalTheme, defaultInternalTheme } from '../foundations'; -import { polishedAppearance } from '../polishedAppearance'; import type { InternalTheme } from '../styledSystem'; import { createColorScales, @@ -21,7 +21,7 @@ export type ParsedCaptcha = Required; type PublicAppearanceTopLevelKey = keyof Omit< Appearance, - 'baseTheme' | 'elements' | 'layout' | 'variables' | 'captcha' | 'cssLayerName' + 'baseTheme' | 'theme' | 'elements' | 'layout' | 'variables' | 'captcha' | 'cssLayerName' >; export type AppearanceCascade = { @@ -83,7 +83,7 @@ export const parseAppearance = (cascade: AppearanceCascade): ParsedAppearance => return !!a.simpleStyles; }) ) { - appearanceList.unshift(polishedAppearance); + appearanceList.unshift(baseTheme); } const parsedElements = parseElements( @@ -104,9 +104,18 @@ const expand = (theme: Theme | undefined, cascade: any[]) => { return; } - (Array.isArray(theme.baseTheme) ? theme.baseTheme : [theme.baseTheme]).forEach(baseTheme => - expand(baseTheme as Theme, cascade), - ); + // Use new 'theme' property if available, otherwise fall back to deprecated 'baseTheme' + const themeProperty = theme.theme !== undefined ? theme.theme : theme.baseTheme; + + if (themeProperty !== undefined) { + (Array.isArray(themeProperty) ? themeProperty : [themeProperty]).forEach(baseTheme => { + if (typeof baseTheme === 'string') { + expand(getBaseTheme(baseTheme), cascade); + } else { + expand(baseTheme as Theme, cascade); + } + }); + } cascade.push(theme); }; @@ -122,7 +131,18 @@ const parseLayout = (appearanceList: Appearance[]) => { const parseCaptcha = (appearanceList: Appearance[]) => { return { ...defaultCaptchaOptions, - ...appearanceList.reduce((acc, appearance) => ({ ...acc, ...appearance.captcha }), {}), + ...appearanceList.reduce((acc, appearance) => { + if (appearance.captcha) { + const { theme: captchaTheme, size, language } = appearance.captcha; + return { + ...acc, + ...(captchaTheme && { theme: captchaTheme }), + ...(size && { size }), + ...(language && { language }), + }; + } + return acc; + }, {} as Partial), }; }; diff --git a/packages/clerk-js/src/ui/elements/Action/ActionCard.tsx b/packages/clerk-js/src/ui/elements/Action/ActionCard.tsx index 6bbbf3ed7ed..64ddca77aaa 100644 --- a/packages/clerk-js/src/ui/elements/Action/ActionCard.tsx +++ b/packages/clerk-js/src/ui/elements/Action/ActionCard.tsx @@ -24,13 +24,12 @@ export const ActionCard = (props: ActionCardProps) => { elementDescriptor={descriptors.actionCard} sx={[ t => ({ - boxShadow: t.shadows.$actionCardShadow, gap: t.space.$4, borderRadius: t.radii.$lg, padding: `${t.space.$4} ${t.space.$5}`, borderWidth: t.borderWidths.$normal, borderStyle: t.borderStyles.$solid, - borderColor: t.colors.$borderAlpha100, + borderColor: t.colors.$borderAlpha150, ...styles(t)[variant], }), sx, diff --git a/packages/clerk-js/src/ui/elements/Card/CardContent.tsx b/packages/clerk-js/src/ui/elements/Card/CardContent.tsx index 65fc71ace39..e49a6711f20 100644 --- a/packages/clerk-js/src/ui/elements/Card/CardContent.tsx +++ b/packages/clerk-js/src/ui/elements/Card/CardContent.tsx @@ -39,13 +39,14 @@ export const CardContent = React.forwardRef((p zIndex: t.zIndices.$card, borderWidth: t.borderWidths.$normal, borderStyle: t.borderStyles.$solid, - borderColor: t.colors.$borderAlpha50, - boxShadow: t.shadows.$cardContentShadow, + borderColor: t.colors.$borderAlpha150, borderRadius: t.radii.$lg, position: 'relative', padding: `${t.space.$8} ${t.space.$10}`, justifyContent: 'center', alignContent: 'center', + marginBlockStart: '-1px', + marginInline: '-1px', }), sx, ]} diff --git a/packages/clerk-js/src/ui/elements/Card/CardRoot.tsx b/packages/clerk-js/src/ui/elements/Card/CardRoot.tsx index 37e6fff1ced..e010d8cb759 100644 --- a/packages/clerk-js/src/ui/elements/Card/CardRoot.tsx +++ b/packages/clerk-js/src/ui/elements/Card/CardRoot.tsx @@ -40,8 +40,7 @@ export const CardRoot = React.forwardRef((props, width: t.sizes.$100, borderWidth: t.borderWidths.$normal, borderStyle: t.borderStyles.$solid, - borderColor: t.colors.$borderAlpha100, - boxShadow: t.shadows.$cardBoxShadow, + borderColor: t.colors.$borderAlpha150, borderRadius: t.radii.$xl, color: t.colors.$colorForeground, position: 'relative', diff --git a/packages/clerk-js/src/ui/elements/Drawer.tsx b/packages/clerk-js/src/ui/elements/Drawer.tsx index 6ba9d4d1172..4627dae89b4 100644 --- a/packages/clerk-js/src/ui/elements/Drawer.tsx +++ b/packages/clerk-js/src/ui/elements/Drawer.tsx @@ -264,8 +264,7 @@ const Content = React.forwardRef(({ children }, re borderStartEndRadius: strategy === 'fixed' ? t.radii.$lg : 0, borderWidth: t.borderWidths.$normal, borderStyle: t.borderStyles.$solid, - borderColor: t.colors.$borderAlpha100, - boxShadow: t.shadows.$cardBoxShadow, + borderColor: t.colors.$borderAlpha150, overflow: 'hidden', pointerEvents: 'auto', })} @@ -304,7 +303,7 @@ const Header = React.forwardRef(({ title, children, ), borderBlockEndWidth: t.borderWidths.$normal, borderBlockEndStyle: t.borderStyles.$solid, - borderBlockEndColor: t.colors.$borderAlpha100, + borderBlockEndColor: t.colors.$borderAlpha150, borderStartStartRadius: t.radii.$lg, borderStartEndRadius: t.radii.$lg, paddingBlock: title ? t.space.$3 : undefined, diff --git a/packages/clerk-js/src/ui/elements/Menu.tsx b/packages/clerk-js/src/ui/elements/Menu.tsx index fa5a1d0cba0..483a06495bc 100644 --- a/packages/clerk-js/src/ui/elements/Menu.tsx +++ b/packages/clerk-js/src/ui/elements/Menu.tsx @@ -130,7 +130,7 @@ export const MenuList = (props: MenuListProps) => { backgroundColor: colors.makeSolid(t.colors.$colorBackground), borderWidth: t.borderWidths.$normal, borderStyle: t.borderStyles.$solid, - borderColor: t.colors.$borderAlpha50, + borderColor: t.colors.$borderAlpha150, outline: 'none', borderRadius: t.radii.$md, padding: t.space.$0x5, @@ -138,7 +138,6 @@ export const MenuList = (props: MenuListProps) => { top: `calc(100% + ${t.space.$2})`, animation: `${animations.dropdownSlideInScaleAndFade} ${t.transitionDuration.$slower} ${t.transitionTiming.$slowBezier}`, transformOrigin: 'top center', - boxShadow: t.shadows.$menuShadow, zIndex: t.zIndices.$dropdown, gap: t.space.$0x5, }), diff --git a/packages/clerk-js/src/ui/elements/PopoverCard.tsx b/packages/clerk-js/src/ui/elements/PopoverCard.tsx index ef82c146b56..a05073c28fd 100644 --- a/packages/clerk-js/src/ui/elements/PopoverCard.tsx +++ b/packages/clerk-js/src/ui/elements/PopoverCard.tsx @@ -58,8 +58,9 @@ const PopoverCardContent = (props: PropsOfComponent) => { zIndex: t.zIndices.$card, borderWidth: t.borderWidths.$normal, borderStyle: t.borderStyles.$solid, - borderColor: t.colors.$borderAlpha50, - boxShadow: t.shadows.$cardContentShadow, + borderColor: t.colors.$borderAlpha150, + marginInline: '-1px', + marginBlockStart: '-1px', }), sx, ]} diff --git a/packages/clerk-js/src/ui/elements/ProfileCard/ProfileCardContent.tsx b/packages/clerk-js/src/ui/elements/ProfileCard/ProfileCardContent.tsx index 2e6890487c5..c498a07c7b8 100644 --- a/packages/clerk-js/src/ui/elements/ProfileCard/ProfileCardContent.tsx +++ b/packages/clerk-js/src/ui/elements/ProfileCard/ProfileCardContent.tsx @@ -38,12 +38,12 @@ export const ProfileCardContent = (props: ProfileCardContentProps) => { position: 'relative', borderRadius: t.radii.$lg, width: '100%', - height: '100%', overflow: 'hidden', borderWidth: t.borderWidths.$normal, borderStyle: t.borderStyles.$solid, - borderColor: t.colors.$borderAlpha50, - boxShadow: t.shadows.$cardContentShadow, + borderColor: t.colors.$borderAlpha150, + marginBlock: '-1px', + marginInlineEnd: '-1px', })} data-clerk-profile-scroll-box-root={scrollBoxId} > diff --git a/packages/clerk-js/src/ui/elements/SocialButtons.tsx b/packages/clerk-js/src/ui/elements/SocialButtons.tsx index a7960c383f7..8981677f971 100644 --- a/packages/clerk-js/src/ui/elements/SocialButtons.tsx +++ b/packages/clerk-js/src/ui/elements/SocialButtons.tsx @@ -211,6 +211,7 @@ const SocialButtonIcon = forwardRef((props: SocialButtonProps, ref: Ref ({ minHeight: t.sizes.$8, width: '100%', @@ -233,6 +234,7 @@ const SocialButtonBlock = forwardRef((props: SocialButtonProps, ref: Ref [ diff --git a/packages/clerk-js/src/ui/primitives/Button.tsx b/packages/clerk-js/src/ui/primitives/Button.tsx index 2c526ded34b..4db84359740 100644 --- a/packages/clerk-js/src/ui/primitives/Button.tsx +++ b/packages/clerk-js/src/ui/primitives/Button.tsx @@ -80,7 +80,6 @@ const { applyVariants, filterProps } = createVariants( solid: { backgroundColor: vars.accent, color: vars.accentContrast, - boxShadow: theme.shadows.$buttonShadow, borderWidth: theme.borderWidths.$normal, borderStyle: theme.borderStyles.$solid, borderColor: vars.accent, @@ -96,20 +95,23 @@ const { applyVariants, filterProps } = createVariants( outline: { borderWidth: theme.borderWidths.$normal, borderStyle: theme.borderStyles.$solid, - borderColor: theme.colors.$borderAlpha100, + borderColor: theme.colors.$borderAlpha150, color: theme.colors.$neutralAlpha600, '&:hover': { backgroundColor: theme.colors.$neutralAlpha50 }, - '&:focus': props.hoverAsFocus ? { backgroundColor: theme.colors.$neutralAlpha50 } : undefined, - boxShadow: theme.shadows.$outlineButtonShadow, + '&:focus': props.hoverAsFocus + ? { backgroundColor: theme.colors.$neutralAlpha50, borderColor: theme.colors.$borderAlpha300 } + : undefined, }, bordered: { borderWidth: theme.borderWidths.$normal, borderStyle: theme.borderStyles.$solid, - borderColor: theme.colors.$borderAlpha100, + borderColor: theme.colors.$borderAlpha150, color: vars.accentContrast, backgroundColor: vars.accent, '&:hover': { backgroundColor: vars.accentHover }, - '&:focus': props.hoverAsFocus ? { backgroundColor: vars.accentHover } : undefined, + '&:focus': props.hoverAsFocus + ? { backgroundColor: vars.accentHover, borderColor: theme.colors.$borderAlpha300 } + : undefined, }, ghost: { color: vars.accent, diff --git a/packages/clerk-js/src/ui/primitives/Table.tsx b/packages/clerk-js/src/ui/primitives/Table.tsx index 4dc2f43c7af..9ef257912dd 100644 --- a/packages/clerk-js/src/ui/primitives/Table.tsx +++ b/packages/clerk-js/src/ui/primitives/Table.tsx @@ -13,9 +13,8 @@ const { applyVariants, filterProps } = createVariants(theme => { borderCollapse: 'separate', borderWidth: theme.borderWidths.$normal, borderStyle: theme.borderStyles.$solid, - borderColor: theme.colors.$borderAlpha100, + borderColor: theme.colors.$borderAlpha150, borderRadius: theme.radii.$lg, - boxShadow: theme.shadows.$tableBodyShadow, width: '100%', '>:not([hidden])~:not([hidden])': { borderBottomWidth: '0px', @@ -23,7 +22,7 @@ const { applyVariants, filterProps } = createVariants(theme => { borderStyle: 'solid', borderLeftWidth: '0px', borderRightWidth: '0px', - borderColor: theme.colors.$borderAlpha100, + borderColor: theme.colors.$borderAlpha150, }, 'td:not(:first-of-type)': { paddingLeft: theme.space.$2, @@ -34,7 +33,7 @@ const { applyVariants, filterProps } = createVariants(theme => { 'tr > td': { borderTopWidth: theme.borderWidths.$normal, borderTopStyle: theme.borderStyles.$solid, - borderTopColor: theme.colors.$borderAlpha100, + borderTopColor: theme.colors.$borderAlpha150, paddingBottom: theme.space.$2, paddingTop: theme.space.$2, paddingLeft: theme.space.$4, @@ -46,7 +45,7 @@ const { applyVariants, filterProps } = createVariants(theme => { borderStyle: 'solid', borderLeftWidth: '0px', borderRightWidth: '0px', - borderColor: theme.colors.$borderAlpha100, + borderColor: theme.colors.$borderAlpha150, }, 'tr:hover td:first-of-type': { borderBottomLeftRadius: theme.radii.$lg, diff --git a/packages/clerk-js/src/utils/appearance.ts b/packages/clerk-js/src/utils/appearance.ts index 5aae032072b..84144c5f39b 100644 --- a/packages/clerk-js/src/utils/appearance.ts +++ b/packages/clerk-js/src/utils/appearance.ts @@ -1,36 +1,47 @@ import type { Appearance, BaseTheme } from '@clerk/types'; /** - * Extracts cssLayerName from baseTheme and moves it to appearance level. - * This is a pure function that can be tested independently. + * Extracts cssLayerName from theme/baseTheme and moves it to appearance level. + * Handles both the new 'theme' property and deprecated 'baseTheme' property. */ export function processCssLayerNameExtraction(appearance: Appearance | undefined): Appearance | undefined { - if (!appearance || typeof appearance !== 'object' || !('baseTheme' in appearance) || !appearance.baseTheme) { + if (!appearance || typeof appearance !== 'object') { return appearance; } - let cssLayerNameFromBaseTheme: string | undefined; + // Use new 'theme' property if available, otherwise fall back to deprecated 'baseTheme' + const themeProperty = appearance.theme !== undefined ? appearance.theme : appearance.baseTheme; + const isUsingNewThemeProperty = appearance.theme !== undefined; - if (Array.isArray(appearance.baseTheme)) { + if (!themeProperty) { + return appearance; + } + + let cssLayerNameFromTheme: string | undefined; + + if (Array.isArray(themeProperty)) { // Handle array of themes - extract cssLayerName from each and use the first one found - appearance.baseTheme.forEach((theme: BaseTheme) => { - if (!cssLayerNameFromBaseTheme && theme.cssLayerName) { - cssLayerNameFromBaseTheme = theme.cssLayerName; + themeProperty.forEach((theme: BaseTheme) => { + if (!cssLayerNameFromTheme && typeof theme === 'object' && theme.cssLayerName) { + cssLayerNameFromTheme = theme.cssLayerName; } }); - // Create array without cssLayerName properties - const processedBaseThemeArray = appearance.baseTheme.map((theme: BaseTheme) => { + // Create array without cssLayerName properties (only for object themes) + const processedThemeArray = themeProperty.map((theme: BaseTheme) => { + if (typeof theme === 'string') { + return theme; // String themes don't have cssLayerName + } const { cssLayerName, ...rest } = theme; return rest; }); - // Use existing cssLayerName at appearance level, or fall back to one from baseTheme(s) - const finalCssLayerName = appearance.cssLayerName || cssLayerNameFromBaseTheme; + // Use existing cssLayerName at appearance level, or fall back to one from theme(s) + const finalCssLayerName = appearance.cssLayerName || cssLayerNameFromTheme; const result = { ...appearance, - baseTheme: processedBaseThemeArray, + [isUsingNewThemeProperty ? 'theme' : 'baseTheme']: processedThemeArray, }; if (finalCssLayerName) { @@ -40,22 +51,28 @@ export function processCssLayerNameExtraction(appearance: Appearance | undefined return result; } else { // Handle single theme - const singleTheme = appearance.baseTheme; let cssLayerNameFromSingleTheme: string | undefined; - if (singleTheme.cssLayerName) { - cssLayerNameFromSingleTheme = singleTheme.cssLayerName; + // Only extract cssLayerName if it's an object theme + if (typeof themeProperty === 'object' && themeProperty.cssLayerName) { + cssLayerNameFromSingleTheme = themeProperty.cssLayerName; } - // Create new theme without cssLayerName - const { cssLayerName, ...processedBaseTheme } = singleTheme; + // Create new theme without cssLayerName (only for object themes) + const processedTheme = + typeof themeProperty === 'string' + ? themeProperty + : (() => { + const { cssLayerName, ...rest } = themeProperty; + return rest; + })(); - // Use existing cssLayerName at appearance level, or fall back to one from baseTheme + // Use existing cssLayerName at appearance level, or fall back to one from theme const finalCssLayerName = appearance.cssLayerName || cssLayerNameFromSingleTheme; const result = { ...appearance, - baseTheme: processedBaseTheme, + [isUsingNewThemeProperty ? 'theme' : 'baseTheme']: processedTheme, }; if (finalCssLayerName) { diff --git a/packages/types/src/appearance.ts b/packages/types/src/appearance.ts index e5333b5c7c5..e04f74b2598 100644 --- a/packages/types/src/appearance.ts +++ b/packages/types/src/appearance.ts @@ -806,10 +806,26 @@ export type Variables = { }; export type BaseThemeTaggedType = { __type: 'prebuilt_appearance' }; -export type BaseTheme = BaseThemeTaggedType & { cssLayerName?: string }; +export type BaseTheme = (BaseThemeTaggedType | 'clerk' | 'simple') & { cssLayerName?: string }; export type Theme = { /** + * A theme used as the base theme for the components. + * For further customisation, you can use the {@link Theme.layout}, {@link Theme.variables} and {@link Theme.elements} props. + * + * Supports both object-based themes and string-based themes: + * @example + * import { dark } from "@clerk/themes"; + * appearance={{ theme: dark }} + * + * @example + * // Use string-based theme + * appearance={{ theme: 'clerk' }} + * appearance={{ theme: 'simple' }} + */ + theme?: BaseTheme | BaseTheme[]; + /** + * @deprecated Use `theme` instead. This property will be removed in a future version. * A theme used as the base theme for the components. * For further customisation, you can use the {@link Theme.layout}, {@link Theme.variables} and {@link Theme.elements} props. * @example @@ -838,7 +854,7 @@ export type Theme = { /** * The appearance of the CAPTCHA widget. * This will be used to style the CAPTCHA widget. - * Eg: `theme: 'dark'` + * Eg: `captcha: { theme: 'dark' }` */ captcha?: CaptchaAppearanceOptions; };