-
Notifications
You must be signed in to change notification settings - Fork 419
feat(clerk-js): Add cssLayerName option to experimental_createTheme
#6344
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b2a7398
5c3b26c
dacd692
88a9d1a
cdf3faa
e853821
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,173 @@ | ||
| import type { Appearance, BaseTheme } from '@clerk/types'; | ||
| import { describe, expect, it } from 'vitest'; | ||
|
|
||
| import { processCssLayerNameExtraction } from '../appearance'; | ||
|
|
||
| describe('processCssLayerNameExtraction', () => { | ||
| it('extracts cssLayerName from single baseTheme and moves it to appearance level', () => { | ||
| const appearance: Appearance = { | ||
| baseTheme: { | ||
| __type: 'prebuilt_appearance' as const, | ||
| cssLayerName: 'theme-layer', | ||
| }, | ||
| }; | ||
|
|
||
| const result = processCssLayerNameExtraction(appearance); | ||
|
|
||
| expect(result?.cssLayerName).toBe('theme-layer'); | ||
| expect(result?.baseTheme).toBeDefined(); | ||
| if (result?.baseTheme && !Array.isArray(result.baseTheme)) { | ||
| expect((result.baseTheme as BaseTheme & { cssLayerName?: string }).cssLayerName).toBeUndefined(); | ||
| expect(result.baseTheme.__type).toBe('prebuilt_appearance'); | ||
| } | ||
| }); | ||
|
|
||
| it('preserves appearance-level cssLayerName over baseTheme cssLayerName', () => { | ||
| const appearance: Appearance = { | ||
| cssLayerName: 'appearance-layer', | ||
| baseTheme: { | ||
| __type: 'prebuilt_appearance' as const, | ||
| cssLayerName: 'theme-layer', | ||
| }, | ||
| }; | ||
|
|
||
| const result = processCssLayerNameExtraction(appearance); | ||
|
|
||
| expect(result?.cssLayerName).toBe('appearance-layer'); | ||
| if (result?.baseTheme && !Array.isArray(result.baseTheme)) { | ||
| expect((result.baseTheme as BaseTheme & { cssLayerName?: string }).cssLayerName).toBeUndefined(); | ||
| } | ||
| }); | ||
|
|
||
| it('extracts cssLayerName from first theme in array that has one', () => { | ||
| const appearance: Appearance = { | ||
| baseTheme: [ | ||
| { | ||
| __type: 'prebuilt_appearance' as const, | ||
| }, | ||
| { | ||
| __type: 'prebuilt_appearance' as const, | ||
| cssLayerName: 'first-layer', | ||
| }, | ||
| { | ||
| __type: 'prebuilt_appearance' as const, | ||
| cssLayerName: 'second-layer', | ||
| }, | ||
| ], | ||
| }; | ||
|
|
||
| const result = processCssLayerNameExtraction(appearance); | ||
|
|
||
| expect(result?.cssLayerName).toBe('first-layer'); | ||
| expect(result?.baseTheme).toBeDefined(); | ||
| if (result?.baseTheme && Array.isArray(result.baseTheme)) { | ||
| expect(result.baseTheme).toHaveLength(3); | ||
| expect((result.baseTheme[0] as BaseTheme & { cssLayerName?: string }).cssLayerName).toBeUndefined(); | ||
| expect((result.baseTheme[1] as BaseTheme & { cssLayerName?: string }).cssLayerName).toBeUndefined(); | ||
| expect((result.baseTheme[2] as BaseTheme & { cssLayerName?: string }).cssLayerName).toBeUndefined(); | ||
| result.baseTheme.forEach(theme => { | ||
| expect(theme.__type).toBe('prebuilt_appearance'); | ||
| }); | ||
| } | ||
| }); | ||
|
|
||
| it('preserves appearance-level cssLayerName over array baseTheme cssLayerName', () => { | ||
| const appearance: Appearance = { | ||
| cssLayerName: 'appearance-layer', | ||
| baseTheme: [ | ||
| { | ||
| __type: 'prebuilt_appearance' as const, | ||
| cssLayerName: 'theme1-layer', | ||
| }, | ||
| { | ||
| __type: 'prebuilt_appearance' as const, | ||
| cssLayerName: 'theme2-layer', | ||
| }, | ||
| ], | ||
| }; | ||
|
|
||
| const result = processCssLayerNameExtraction(appearance); | ||
|
|
||
| expect(result?.cssLayerName).toBe('appearance-layer'); | ||
| if (result?.baseTheme && Array.isArray(result.baseTheme)) { | ||
| result.baseTheme.forEach(theme => { | ||
| expect((theme as BaseTheme & { cssLayerName?: string }).cssLayerName).toBeUndefined(); | ||
| }); | ||
| } | ||
| }); | ||
|
|
||
| it('handles single baseTheme without cssLayerName', () => { | ||
| const appearance: Appearance = { | ||
| baseTheme: { | ||
| __type: 'prebuilt_appearance' as const, | ||
| }, | ||
| }; | ||
|
|
||
| const result = processCssLayerNameExtraction(appearance); | ||
|
|
||
| expect(result?.cssLayerName).toBeUndefined(); | ||
| if (result?.baseTheme && !Array.isArray(result.baseTheme)) { | ||
| expect(result.baseTheme.__type).toBe('prebuilt_appearance'); | ||
| expect((result.baseTheme as BaseTheme & { cssLayerName?: string }).cssLayerName).toBeUndefined(); | ||
| } | ||
| }); | ||
|
|
||
| it('handles array of baseThemes without any cssLayerName', () => { | ||
| const appearance: Appearance = { | ||
| baseTheme: [ | ||
| { | ||
| __type: 'prebuilt_appearance' as const, | ||
| }, | ||
| { | ||
| __type: 'prebuilt_appearance' as const, | ||
| }, | ||
| ], | ||
| }; | ||
|
|
||
| const result = processCssLayerNameExtraction(appearance); | ||
|
|
||
| expect(result?.cssLayerName).toBeUndefined(); | ||
| if (result?.baseTheme && Array.isArray(result.baseTheme)) { | ||
| expect(result.baseTheme).toHaveLength(2); | ||
| result.baseTheme.forEach(theme => { | ||
| expect(theme.__type).toBe('prebuilt_appearance'); | ||
| expect((theme as BaseTheme & { cssLayerName?: string }).cssLayerName).toBeUndefined(); | ||
| }); | ||
| } | ||
| }); | ||
|
|
||
| it('handles no baseTheme provided', () => { | ||
| const appearance: Appearance = { | ||
| cssLayerName: 'standalone-layer', | ||
| }; | ||
|
|
||
| const result = processCssLayerNameExtraction(appearance); | ||
|
|
||
| expect(result?.cssLayerName).toBe('standalone-layer'); | ||
| expect(result?.baseTheme).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('handles undefined appearance', () => { | ||
| const result = processCssLayerNameExtraction(undefined); | ||
|
|
||
| expect(result).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('preserves other appearance properties', () => { | ||
| const appearance: Appearance = { | ||
| variables: { colorPrimary: 'blue' }, | ||
| baseTheme: { | ||
| __type: 'prebuilt_appearance' as const, | ||
| cssLayerName: 'theme-layer', | ||
| }, | ||
| }; | ||
|
|
||
| const result = processCssLayerNameExtraction(appearance); | ||
|
|
||
| expect(result?.cssLayerName).toBe('theme-layer'); | ||
| expect(result?.variables?.colorPrimary).toBe('blue'); | ||
| if (result?.baseTheme && !Array.isArray(result.baseTheme)) { | ||
| expect((result.baseTheme as BaseTheme & { cssLayerName?: string }).cssLayerName).toBeUndefined(); | ||
| } | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| 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. | ||
| */ | ||
| export function processCssLayerNameExtraction(appearance: Appearance | undefined): Appearance | undefined { | ||
| if (!appearance || typeof appearance !== 'object' || !('baseTheme' in appearance) || !appearance.baseTheme) { | ||
| return appearance; | ||
| } | ||
|
|
||
| let cssLayerNameFromBaseTheme: string | undefined; | ||
|
|
||
| if (Array.isArray(appearance.baseTheme)) { | ||
| // 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; | ||
| } | ||
| }); | ||
|
|
||
| // Create array without cssLayerName properties | ||
| const processedBaseThemeArray = appearance.baseTheme.map((theme: BaseTheme) => { | ||
| 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; | ||
|
|
||
| const result = { | ||
| ...appearance, | ||
| baseTheme: processedBaseThemeArray, | ||
| }; | ||
|
|
||
| if (finalCssLayerName) { | ||
| result.cssLayerName = finalCssLayerName; | ||
| } | ||
|
|
||
| return result; | ||
| } else { | ||
| // Handle single theme | ||
| const singleTheme = appearance.baseTheme; | ||
| let cssLayerNameFromSingleTheme: string | undefined; | ||
|
|
||
| if (singleTheme.cssLayerName) { | ||
| cssLayerNameFromSingleTheme = singleTheme.cssLayerName; | ||
| } | ||
|
|
||
| // Create new theme without cssLayerName | ||
| const { cssLayerName, ...processedBaseTheme } = singleTheme; | ||
|
|
||
| // Use existing cssLayerName at appearance level, or fall back to one from baseTheme | ||
| const finalCssLayerName = appearance.cssLayerName || cssLayerNameFromSingleTheme; | ||
|
|
||
| const result = { | ||
| ...appearance, | ||
| baseTheme: processedBaseTheme, | ||
| }; | ||
|
|
||
| if (finalCssLayerName) { | ||
| result.cssLayerName = finalCssLayerName; | ||
| } | ||
|
|
||
| return result; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,6 +1,7 @@ | ||||||
| import { experimental_createTheme } from '../createTheme'; | ||||||
|
|
||||||
| export const shadcn = experimental_createTheme({ | ||||||
| cssLayerName: 'components', | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need this to be |
||||||
| variables: { | ||||||
| colorBackground: 'var(--card)', | ||||||
| colorDanger: 'var(--destructive)', | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we know shadcn is using TW, we can specify the cssLayerName to be defined within components, relying on TW layer order specified.