Skip to content

Commit

Permalink
Allowing for theme overrides to be set on the ThemeProvider
Browse files Browse the repository at this point in the history
  • Loading branch information
corbanbrook committed Nov 21, 2023
1 parent bfe8319 commit 874c7b9
Show file tree
Hide file tree
Showing 5 changed files with 211 additions and 246 deletions.
13 changes: 11 additions & 2 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,27 @@ export const globalTypes = {
title: 'Theme',
icon: 'moon',
items: [
{ value: 'dark', icon: 'moon', title: 'Dark (default)' },
{ value: 'light', icon: 'sun', title: 'Light' },
{ value: 'dark', icon: 'moon', title: 'Dark' },
{ value: 'custom', icon: 'paintbrush', title: 'Custom' },
],
},
},
}

const CUSTOM_THEME = {
text100: 'rgba(255, 255, 255, 1)',
text80: 'rgba(200, 200, 255, 1)',
text50: 'rgba(150, 150, 200, 1)',
backgroundPrimary: 'pink',
backgroundSecondary: 'navy',
}

const withTheme: Decorator = (StoryFn, context) => {
const { theme } = context.globals

return (
<ThemeProvider theme={theme}>
<ThemeProvider theme={theme === 'custom' ? CUSTOM_THEME : theme}>
<StoryFn />
</ThemeProvider>
)
Expand Down
30 changes: 30 additions & 0 deletions src/components/ThemeProvider/ThemeProvider.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,36 @@ export const Nested = () => {
</Card>
</ThemeProvider>
</div>

<div id="app3">
<ThemeProvider
root="#app3"
scope="application3"
theme={{
text100: 'rgba(255, 255, 255, 1)',
text80: 'rgba(200, 200, 255, 1)',
text50: 'rgba(150, 150, 200, 1)',
backgroundPrimary: 'pink',
backgroundSecondary: 'navy',
}}
>
<Card background="backgroundPrimary" marginTop="4">
<Collapsible label="Nested Application 3">
<Text variant="normal" color="text100">
Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore
magna aliqua. Ut enim ad minim veniam, quis nostrud
exercitation ullamco laboris nisi ut aliquip ex ea
commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu
fugiat nulla pariatur. Excepteur sint occaecat cupidatat
non proident, sunt in culpa qui officia deserunt mollit
anim id est laborum.
</Text>
</Collapsible>
</Card>
</ThemeProvider>
</div>
</Collapsible>
</Card>
</ThemeProvider>
Expand Down
38 changes: 30 additions & 8 deletions src/components/ThemeProvider/ThemeProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { setElementVars } from '@vanilla-extract/dynamic'
import {
createContext,
PropsWithChildren,
Expand All @@ -7,6 +8,9 @@ import {
useState,
} from 'react'

import { colorSchemeVars } from '~/css/vars.css'
import { colors, ColorTokens } from '~/tokens/color'

const THEMES = ['dark', 'light'] as const

export type Theme = (typeof THEMES)[number]
Expand All @@ -15,25 +19,34 @@ const DEFAULT_THEME = 'dark'
const THEME_ATTR = 'data-theme'
const STORAGE_KEY = '@sequence.theme'

const isTheme = (theme: any): theme is Theme => THEMES.includes(theme as any)

type ThemeOverrides = Partial<ColorTokens>

const isThemeOverrides = (theme: any): theme is ThemeOverrides =>
typeof theme === 'object' && theme !== null && !Array.isArray(theme)

const getStorageKey = (scope?: string) =>
scope ? `${STORAGE_KEY}.${scope}` : STORAGE_KEY

interface ThemeContextValue {
theme: Theme
theme: Theme | ThemeOverrides
root?: string
setTheme: (mode: Theme) => void
}

interface ThemeProviderProps {
theme?: Theme
theme?: Theme | ThemeOverrides
root?: string
scope?: string
}

const getTheme = (scope?: string): Theme => {
const persistedTheme = localStorage.getItem(getStorageKey(scope)) as Theme
const persistedTheme = localStorage.getItem(
getStorageKey(scope)
) as Theme | null

if (THEMES.includes(persistedTheme)) {
if (persistedTheme && THEMES.includes(persistedTheme)) {
return persistedTheme
}

Expand All @@ -49,7 +62,7 @@ const getTheme = (scope?: string): Theme => {
const ThemeContext = createContext<ThemeContextValue | null>(null)

export const ThemeProvider = (props: PropsWithChildren<ThemeProviderProps>) => {
const [theme, setTheme] = useState<Theme>(
const [theme, setTheme] = useState<Theme | ThemeOverrides>(
props.theme || getTheme(props.scope)
)

Expand All @@ -61,17 +74,26 @@ export const ThemeProvider = (props: PropsWithChildren<ThemeProviderProps>) => {

// Allow prop theme override
useEffect(() => {
if (props.theme && THEMES.includes(props.theme)) {
if (props.theme) {
setTheme(props.theme)
}
}, [props.theme])

// Set the data-theme attribtute on the document root element
useEffect(() => {
const rootEl = document.querySelector(props.root || ':root')
const rootEl = document.querySelector(props.root || ':root') as HTMLElement

if (rootEl) {
rootEl.setAttribute(THEME_ATTR, theme)
if (isTheme(theme)) {
rootEl.setAttribute(THEME_ATTR, theme)
setElementVars(rootEl, colorSchemeVars, {
colors: colors[theme],
})
} else if (isThemeOverrides(theme)) {
setElementVars(rootEl, colorSchemeVars, {
colors: theme as ColorTokens,
})
}
}
}, [theme, props.root])

Expand Down
61 changes: 11 additions & 50 deletions src/css/vars.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,60 +3,19 @@ import {
createGlobalThemeContract,
} from '@vanilla-extract/css'

import { capitalize } from '~/helpers'
import { ColorScheme, tokens } from '~/tokens'

import { mapVarName } from './utils'

type MapTokens<P extends string, T> = {
[K in keyof T & string as `${P}${Capitalize<K>}`]: string
}

const mapTokens = <P extends string, T extends {}>(
prefix: P,
tokens: T
): MapTokens<P, T> => {
return Object.entries(tokens).reduce((acc, [key, value]) => {
return { ...acc, [`${prefix}${capitalize(key)}`]: value }
}, {}) as MapTokens<P, T>
}

type NetworkColors = typeof tokens.colors.network

const mapNetworkColors = <
T extends NetworkColors,
K extends keyof NetworkColors,
>(
networkColors: T
) => {
return Object.entries(networkColors).reduce(
(acc, [key, value]) => {
return { ...acc, ...mapTokens(key.toLowerCase(), value) }
},
{} as MapTokens<Lowercase<K>, T[K]>
)
}

const { colors, ...baseTokens } = tokens

export const baseVars = createGlobalThemeContract(baseTokens, mapVarName)

createGlobalTheme(':root', baseVars, baseTokens)

const makeColorScheme = (mode: ColorScheme = 'light') => {
const colorSchemeTokens = colors.colorSchemes[mode]

const makeColorScheme = (mode: ColorScheme = 'dark') => {
return {
colors: {
...colors.base,
...colors.context,
...mapTokens('gradient', colors.gradient),
...mapTokens('background', colorSchemeTokens.background),
...mapTokens('border', colorSchemeTokens.border),
...mapTokens('button', colorSchemeTokens.button),
...mapTokens('text', colorSchemeTokens.text),
...mapNetworkColors(colors.network),
},
colors: { ...colors[mode] },
}
}

Expand All @@ -65,13 +24,15 @@ export const colorSchemeVars = createGlobalThemeContract(
mapVarName
)

for (const colorScheme of Object.keys(colors.colorSchemes) as ColorScheme[]) {
createGlobalTheme(
`[data-theme="${colorScheme}"]`,
colorSchemeVars,
makeColorScheme(colorScheme)
)
}
createGlobalTheme(':root', colorSchemeVars, makeColorScheme())

// for (const colorScheme of Object.keys(colors) as ColorScheme[]) {
// createGlobalTheme(
// `[data-theme="${colorScheme}"]`,
// colorSchemeVars,
// makeColorScheme(colorScheme)
// )
// }

export const vars = { ...baseVars, ...colorSchemeVars }
export type ThemeVars = typeof vars
Loading

0 comments on commit 874c7b9

Please sign in to comment.