Skip to content

Commit 1467a56

Browse files
authored
feat: allow for theme color fallbacks for overrides (#456)
feat: update theme to use color defaults
1 parent ade135e commit 1467a56

File tree

5 files changed

+132
-76
lines changed

5 files changed

+132
-76
lines changed

src/components/Employee/OnboardingFlow/OnboardingFlow.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ describe('EmployeeOnboardingFlow', () => {
9090
)
9191
})
9292

93-
it('succeeds', { timeout: 20_000 }, async () => {
93+
it('succeeds', { timeout: 30_000 }, async () => {
9494
const user = userEvent.setup()
9595
render(
9696
<GustoProvider config={{ baseUrl: API_BASE_URL }}>

src/contexts/ThemeProvider/ThemeProvider.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type React from 'react'
22
import { useEffect, useRef } from 'react'
33
import { useTranslation } from 'react-i18next'
44
import { ThemeContext } from './useTheme'
5-
import { gustoSDKTheme, type GustoSDKTheme } from './theme'
5+
import { mergePartnerTheme, type GustoSDKTheme } from './theme'
66
import '@/styles/sdk.scss'
77

88
export interface ThemeProviderProps {
@@ -19,10 +19,7 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({
1919
const containerRef = useRef<HTMLElement>(null)
2020

2121
useEffect(() => {
22-
const gustoSDKThemeWithOverrides = {
23-
...gustoSDKTheme,
24-
...partnerThemeOverrides,
25-
}
22+
const gustoSDKThemeWithOverrides = mergePartnerTheme(partnerThemeOverrides)
2623

2724
if (GThemeVariables.current) {
2825
GThemeVariables.current.remove()

src/contexts/ThemeProvider/test-utils.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import type React from 'react'
22
import { I18nextProvider, useTranslation } from 'react-i18next'
33
import { ThemeProvider } from './ThemeProvider'
4-
import { gustoSDKTheme } from './theme'
4+
import { createTheme } from './theme'
55
import '@/i18n'
66

77
export const AllTheProviders = ({ children }: { children: React.ReactNode }) => {
88
const { i18n } = useTranslation()
9-
const theme = gustoSDKTheme
9+
const theme = createTheme()
1010
return (
1111
<ThemeProvider theme={theme}>
1212
<I18nextProvider i18n={i18n}>{children}</I18nextProvider>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { mergePartnerTheme } from './theme'
2+
3+
describe('theme', () => {
4+
describe('mergePartnerTheme', () => {
5+
it('should apply the default colors when no partner theme is provided', () => {
6+
const mergedTheme = mergePartnerTheme({})
7+
8+
expect(mergedTheme.colorBody).toBe('#FFFFFF')
9+
expect(mergedTheme.inputBackgroundColor).toBe('#FFFFFF')
10+
})
11+
12+
it('should apply custom colorBody to inputBackgroundColor when inputBackgroundColor is not specified', () => {
13+
const customColorBody = '#F0F0F0'
14+
const mergedTheme = mergePartnerTheme({ colorBody: customColorBody })
15+
16+
expect(mergedTheme.colorBody).toBe(customColorBody)
17+
expect(mergedTheme.inputBackgroundColor).toBe(customColorBody)
18+
})
19+
20+
it('should prioritize explicit inputBackgroundColor over colorBody fallback', () => {
21+
const customColorBody = '#F0F0F0'
22+
const customInputBackground = '#E0E0E0'
23+
const mergedTheme = mergePartnerTheme({
24+
colorBody: customColorBody,
25+
inputBackgroundColor: customInputBackground,
26+
})
27+
28+
expect(mergedTheme.colorBody).toBe(customColorBody)
29+
expect(mergedTheme.inputBackgroundColor).toBe(customInputBackground)
30+
})
31+
})
32+
})

src/contexts/ThemeProvider/theme.ts

Lines changed: 95 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import '@/styles/sdk.scss'
21
import { getRootFontSize, toRem } from '@/helpers/rem'
32

43
// Colors are for internal use in our theme currently
54
// We don't export them for partner use or overrides
6-
const colors = {
5+
const baseColors = {
76
neutral: {
87
100: '#FFFFFF',
98
200: '#FBFAFA',
@@ -40,71 +39,99 @@ const colors = {
4039
},
4140
}
4241

43-
export const gustoSDKTheme = {
44-
// Colors
45-
colorBody: colors.neutral[100],
46-
colorBodyContent: colors.neutral[1000],
47-
colorPrimary: colors.neutral[1000],
48-
colorPrimaryAccent: colors.neutral[900],
49-
colorPrimaryContent: colors.neutral[100],
50-
colorSecondary: colors.neutral[100],
51-
colorSecondaryAccent: colors.neutral[400],
52-
colorSecondaryContent: colors.neutral[1000],
53-
colorInfo: colors.info[100],
54-
colorInfoAccent: colors.info[500],
55-
colorInfoContent: colors.info[800],
56-
colorWarning: colors.warning[100],
57-
colorWarningAccent: colors.warning[500],
58-
colorWarningContent: colors.warning[800],
59-
colorError: colors.error[100],
60-
colorErrorAccent: colors.error[500],
61-
colorErrorContent: colors.error[800],
62-
colorSuccess: colors.success[100],
63-
colorSuccessAccent: colors.success[500],
64-
colorSuccessContent: colors.success[800],
65-
// Input Colors
66-
inputBackgroundColor: colors.neutral[100],
67-
inputBorderColor: colors.neutral[500],
68-
inputContentColor: colors.neutral[1000],
69-
inputBorderWidth: '1px',
70-
inputPlaceholderColor: colors.neutral[800],
71-
inputAdornmentColor: colors.neutral[800],
72-
inputDisabledBackgroundColor: colors.neutral[300],
73-
// Field Colors
74-
inputLabelColor: colors.neutral[1000],
75-
inputLabelFontSize: toRem(16),
76-
inputLabelFontWeight: '500',
77-
inputDescriptionColor: colors.neutral[900],
78-
inputErrorColor: colors.error[500],
79-
// Radius
80-
inputRadius: toRem(8),
81-
buttonRadius: toRem(8),
82-
badgeRadius: toRem(16),
83-
// Font
84-
fontSizeRoot: getRootFontSize(),
85-
fontFamily: 'Geist',
86-
fontLineHeight: toRem(24),
87-
fontSizeSmall: toRem(14),
88-
fontSizeRegular: toRem(16),
89-
fontSizeLarge: toRem(18),
90-
fontSizeHeading1: toRem(32),
91-
fontSizeHeading2: toRem(24),
92-
fontSizeHeading3: toRem(20),
93-
fontSizeHeading4: toRem(18),
94-
fontSizeHeading5: toRem(16),
95-
fontSizeHeading6: toRem(14),
96-
fontWeightRegular: '400',
97-
fontWeightMedium: '500',
98-
fontWeightSemibold: '600',
99-
fontWeightBold: '700',
100-
// Transitions
101-
transitionDuration: '200ms',
102-
// Shadows
103-
shadowResting: '0px 1px 2px 0px rgba(10, 13, 18, 0.05)',
104-
shadowTopmost: '0px 4px 6px 0px rgba(28, 28, 28, 0.05), 0px 10px 15px 0px rgba(28, 28, 28, 0.10)',
105-
// Focus
106-
focusRingColor: colors.neutral[1000],
107-
focusRingWidth: '2px',
42+
const defaultThemeColors = {
43+
colorBody: baseColors.neutral[100],
44+
colorBodyContent: baseColors.neutral[1000],
45+
colorPrimary: baseColors.neutral[1000],
46+
colorPrimaryAccent: baseColors.neutral[900],
47+
colorPrimaryContent: baseColors.neutral[100],
48+
colorSecondary: baseColors.neutral[100],
49+
colorSecondaryAccent: baseColors.neutral[400],
50+
colorSecondaryContent: baseColors.neutral[1000],
51+
colorInfo: baseColors.info[100],
52+
colorInfoAccent: baseColors.info[500],
53+
colorInfoContent: baseColors.info[800],
54+
colorWarning: baseColors.warning[100],
55+
colorWarningAccent: baseColors.warning[500],
56+
colorWarningContent: baseColors.warning[800],
57+
colorError: baseColors.error[100],
58+
colorErrorAccent: baseColors.error[500],
59+
colorErrorContent: baseColors.error[800],
60+
colorSuccess: baseColors.success[100],
61+
colorSuccessAccent: baseColors.success[500],
62+
colorSuccessContent: baseColors.success[800],
10863
}
10964

110-
export type GustoSDKTheme = Partial<typeof gustoSDKTheme>
65+
export type GustoSDKThemeColors = Partial<typeof defaultThemeColors>
66+
67+
export const createTheme = (colors: GustoSDKThemeColors = {}) => {
68+
return {
69+
// Colors
70+
...defaultThemeColors,
71+
...colors,
72+
// Input Colors
73+
inputBackgroundColor: colors.colorBody,
74+
inputBorderColor: baseColors.neutral[500],
75+
inputContentColor: colors.colorBodyContent,
76+
inputBorderWidth: '1px',
77+
inputPlaceholderColor: baseColors.neutral[800],
78+
inputAdornmentColor: baseColors.neutral[800],
79+
inputDisabledBackgroundColor: baseColors.neutral[300],
80+
// Field Colors
81+
inputLabelColor: colors.colorBodyContent,
82+
inputLabelFontSize: toRem(16),
83+
inputLabelFontWeight: '500',
84+
inputDescriptionColor: baseColors.neutral[900],
85+
inputErrorColor: colors.colorErrorAccent,
86+
// Radius
87+
inputRadius: toRem(8),
88+
buttonRadius: toRem(8),
89+
badgeRadius: toRem(16),
90+
// Font
91+
fontSizeRoot: getRootFontSize(),
92+
fontFamily: 'Geist',
93+
fontLineHeight: toRem(24),
94+
fontSizeSmall: toRem(14),
95+
fontSizeRegular: toRem(16),
96+
fontSizeLarge: toRem(18),
97+
fontSizeHeading1: toRem(32),
98+
fontSizeHeading2: toRem(24),
99+
fontSizeHeading3: toRem(20),
100+
fontSizeHeading4: toRem(18),
101+
fontSizeHeading5: toRem(16),
102+
fontSizeHeading6: toRem(14),
103+
fontWeightRegular: '400',
104+
fontWeightMedium: '500',
105+
fontWeightSemibold: '600',
106+
fontWeightBold: '700',
107+
// Transitions
108+
transitionDuration: '200ms',
109+
// Shadows
110+
shadowResting: '0px 1px 2px 0px rgba(10, 13, 18, 0.05)',
111+
shadowTopmost:
112+
'0px 4px 6px 0px rgba(28, 28, 28, 0.05), 0px 10px 15px 0px rgba(28, 28, 28, 0.10)',
113+
// Focus
114+
focusRingColor: colors.colorPrimary,
115+
focusRingWidth: '2px',
116+
}
117+
}
118+
119+
export type GustoSDKTheme = Partial<ReturnType<typeof createTheme>>
120+
121+
export const mergePartnerTheme = (partnerTheme: GustoSDKTheme): GustoSDKTheme => {
122+
const colors = Object.entries(defaultThemeColors).reduce(
123+
(acc: GustoSDKThemeColors, [key, value]) => {
124+
acc[key as keyof typeof defaultThemeColors] =
125+
partnerTheme[key as keyof typeof defaultThemeColors] || value
126+
return acc
127+
},
128+
{},
129+
)
130+
131+
const theme = createTheme(colors)
132+
133+
return {
134+
...theme,
135+
...partnerTheme,
136+
}
137+
}

0 commit comments

Comments
 (0)