Skip to content

Commit

Permalink
feat: add app themes
Browse files Browse the repository at this point in the history
  • Loading branch information
beeman committed Mar 9, 2024
1 parent c872331 commit 2ed40b3
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 35 deletions.
2 changes: 1 addition & 1 deletion apps/web/src/app/app-routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const routes: RouteObject[] = [
{ path: '/dashboard', element: <DashboardFeature /> },
{ path: '/demo/*', element: <DemoFeature /> },
{ path: '/dev', element: <DevFeature /> },
{ path: '/themes', element: <ThemesFeature /> },
{ path: '/themes/*', element: <ThemesFeature /> },
{ path: '*', element: <UiNotFound /> },
]

Expand Down
80 changes: 57 additions & 23 deletions apps/web/src/app/app-theme.provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,63 @@ import {
BACKGROUND_COLORS,
BackgroundColors,
defaultThemes,
mantineColorIds,
themeWithBrand,
UiTheme,
UiThemeSelectProvider,
} from '@pubkey-ui/core'
import { ThemeLink } from './app-routes'
import { atomWithStorage } from 'jotai/utils'
import { atom, useAtomValue, useSetAtom } from 'jotai/index'
import { Button, MantineColor, Menu } from '@mantine/core'
import { Button, Divider, MantineColor, Menu } from '@mantine/core'
import { Link } from 'react-router-dom'

export interface AppTheme extends UiTheme {
active?: boolean
}

function createAppTheme(color: MantineColor, dark?: BackgroundColors) {
const id = `${color}-${dark ?? 'default'}`
// Make sure `color` and `dark` are valid values
if (dark && !Object.keys(BACKGROUND_COLORS).includes(dark ?? 'default')) {
throw new Error(`Invalid value for dark: ${dark}`)
}
if (!mantineColorIds.includes(color)) {
console.log(`Invalid color: ${color}`)
throw new Error(`Invalid value for color: ${color}`)
}

return {
id,
theme: themeWithBrand(color, {
components: {
Input: {
styles: {
root: {
// backgroundColor: 'transparent',
},
},
},
},
colors: { dark: dark ? BACKGROUND_COLORS[dark] : undefined },
}),
}
}

const appThemes: AppTheme[] = [
...defaultThemes,
{ id: 'gray-pink', theme: themeWithBrand('pink', { colors: { dark: BACKGROUND_COLORS['gray'] } }) },
{ id: 'zinc-pink', theme: themeWithBrand('pink', { colors: { dark: BACKGROUND_COLORS['zinc'] } }) },
{ id: 'neutral-pink', theme: themeWithBrand('pink', { colors: { dark: BACKGROUND_COLORS['neutral'] } }) },
{ id: 'slate-pink', theme: themeWithBrand('pink', { colors: { dark: BACKGROUND_COLORS['slate'] } }) },
{ id: 'stone-pink', theme: themeWithBrand('pink', { colors: { dark: BACKGROUND_COLORS['stone'] } }) },
{ id: 'gray-blue', theme: themeWithBrand('blue', { colors: { dark: BACKGROUND_COLORS['gray'] } }) },
{ id: 'zinc-blue', theme: themeWithBrand('blue', { colors: { dark: BACKGROUND_COLORS['zinc'] } }) },
{ id: 'neutral-blue', theme: themeWithBrand('blue', { colors: { dark: BACKGROUND_COLORS['neutral'] } }) },
{ id: 'slate-blue', theme: themeWithBrand('blue', { colors: { dark: BACKGROUND_COLORS['slate'] } }) },
{ id: 'stone-blue', theme: themeWithBrand('blue', { colors: { dark: BACKGROUND_COLORS['stone'] } }) },
createAppTheme('blue'),
createAppTheme('red'),
createAppTheme('pink'),
createAppTheme('grape'),
createAppTheme('violet'),
createAppTheme('indigo'),
createAppTheme('cyan'),
createAppTheme('green'),
createAppTheme('lime'),
createAppTheme('yellow'),
createAppTheme('orange'),
createAppTheme('teal'),
]

const initialThemes = appThemes
Expand All @@ -39,7 +71,7 @@ const themesAtom = atomWithStorage<AppTheme[]>('pubkey-ui-app-themes', initialTh
const activeThemesAtom = atom<AppTheme[]>((get) => {
const themes = get(themesAtom)
const theme = get(themeAtom)
return themes.map((item) => ({
return themes?.map((item) => ({
...item,
active: item.id === theme.id,
}))
Expand All @@ -48,15 +80,15 @@ const activeThemesAtom = atom<AppTheme[]>((get) => {
const activeThemeAtom = atom<AppTheme>((get) => {
const themes = get(activeThemesAtom)

return themes.find((item) => item.active) || themes[0]
return themes?.find((item) => item.active) || themes[0]
})

export interface AppThemeProviderContext {
theme: AppTheme
themes: AppTheme[]
addTheme: (color: MantineColor, dark?: BackgroundColors) => void
setTheme: (theme: AppTheme) => void
resetThemes: () => void
resetThemes: () => Promise<void>
}

const Context = createContext<AppThemeProviderContext>({} as AppThemeProviderContext)
Expand All @@ -71,27 +103,25 @@ export function AppThemeProvider({ children }: { children: ReactNode }) {
theme,
themes,
addTheme: (color: MantineColor, dark?: BackgroundColors) => {
const id = `${color}-${dark ?? 'default'}`
const theme = createAppTheme(color, dark)
// Make sure we don't add a theme with the same id
if (themes.find((item) => item.id === id)) {
if (themes.find((item) => item.id === theme.id)) {
return
}
const theme: AppTheme = {
id,
theme: themeWithBrand(color, { colors: { dark: dark ? BACKGROUND_COLORS[dark] : undefined } }),
}

setThemes((prev) => [...prev, theme])
setTheme(theme)
},
resetThemes: () => {
setThemes(initialThemes)
resetThemes: async () => {
setTheme({ ...initialTheme })
setThemes(() => [...initialThemes])
},
setTheme,
}

return (
<Context.Provider value={value}>
<UiThemeSelectProvider link={ThemeLink} theme={value.theme} themes={value.themes}>
<UiThemeSelectProvider link={ThemeLink} theme={value.theme ?? undefined} themes={value.themes ?? []}>
{children}
</UiThemeSelectProvider>
</Context.Provider>
Expand All @@ -116,6 +146,10 @@ export function AppThemeSelect() {
{item.id}
</Menu.Item>
))}
<Divider />
<Menu.Item component={Link} to="/themes">
App Themes
</Menu.Item>
</Menu.Dropdown>
</Menu>
)
Expand Down
86 changes: 75 additions & 11 deletions apps/web/src/app/features/themes/themes-feature.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,65 @@ import {
UiCard,
UiContainer,
UiDebug,
UiDebugModal,
UiInfo,
UiStack,
useUiThemeSelect,
} from '@pubkey-ui/core'
import { useAppTheme } from '../../app-theme.provider'
import { Button, Group, MantineColor, Select } from '@mantine/core'
import { Button, Grid, Group, MantineColor, Select, Text } from '@mantine/core'
import { useState } from 'react'
import { DemoFeatureTabRoutes } from '../demo/demo-feature-tab-routes'
import { DemoFeatureLoader } from '../demo/demo-feature-loader'
import { DemoFeatureAlerts } from '../demo/demo-feature-alerts'
import { DemoFeatureAnchor } from '../demo/demo-feature-anchor'
import { DemoFeatureBack } from '../demo/demo-feature-back'
import { DemoFeatureCopy } from '../demo/demo-feature-copy'
import { DemoFeatureForm } from '../demo/demo-feature-form'
import { DemoFeatureGridRoutes } from '../demo/demo-feature-grid-routes'
import { DemoFeatureHeader } from '../demo/demo-feature-header'
import { DemoFeatureMenu } from '../demo/demo-feature-menu'
import { DemoFeatureNotFound } from '../demo/demo-feature-not-found'
import { DemoFeaturePage } from '../demo/demo-feature-page'
import { DemoFeatureSearchInput } from '../demo/demo-feature-search-input'
export function ThemesFeature() {
const { themes, addTheme, setTheme, theme } = useAppTheme()
const { themes, addTheme, resetThemes, setTheme, theme } = useAppTheme()
const { selected } = useUiThemeSelect()
return (
<UiContainer>
<UiStack>
<UiInfo
variant="outline"
message={
<UiStack>
<Text>These are some local themes that are stored in your browser.</Text>
<Group justify="flex-end">
<UiDebugModal data={{ selected: selected.id, theme: theme.id, themes }} />
<Button size="xs" variant={'light'} onClick={resetThemes}>
Reset to Default
</Button>
</Group>
</UiStack>
}
/>
<UiCard title="Add App Theme">
<UiStack>
<UiInfo variant="outline" message="These are some local themes that are stored in your browser." />
<ThemeForm add={addTheme} />
</UiStack>
</UiCard>
<UiCard title="ThemeSelect">
<Group>
{themes.map((item) => (
<Button disabled={theme.id === item.id} key={item.id} onClick={() => setTheme(item)}>
{item.id}
</Button>
))}
{themes
.sort((a, b) => a.id.localeCompare(b.id))
.map((item) => (
<Button disabled={theme.id === item.id} key={item.id} onClick={() => setTheme(item)}>
{item.id}
</Button>
))}
</Group>
</UiCard>
<UiDebug data={{ selected: selected.id, theme: theme.id, themes }} open />
<AppThemeUiDemo />
<UiDebug data={{ selected: selected.id, theme: theme.id, themes }} />
</UiStack>
</UiContainer>
)
Expand All @@ -49,7 +79,7 @@ export function ThemeForm({ add }: { add: (color: MantineColor, dark?: Backgroun
label="Color"
description="Select the primary color"
required
data={mantineColorIds.map((id) => ({ label: id, value: id }))}
data={mantineColorIds.sort((a, b) => a.localeCompare(b)).map((id) => ({ label: id, value: id }))}
value={color}
onChange={(value) => (value ? setColor(value as MantineColor) : undefined)}
/>
Expand All @@ -58,7 +88,7 @@ export function ThemeForm({ add }: { add: (color: MantineColor, dark?: Backgroun
label="Dark"
description="Select the dark color"
clearable
data={backgroundColorIds.map((id) => ({ label: id, value: id }))}
data={backgroundColorIds.sort((a, b) => a.localeCompare(b)).map((id) => ({ label: id, value: id }))}
value={dark}
onChange={(value) => (value ? setDark(value as BackgroundColors) : undefined)}
/>
Expand All @@ -72,3 +102,37 @@ export function ThemeForm({ add }: { add: (color: MantineColor, dark?: Backgroun
</Group>
)
}

export function AppThemeUiDemo() {
return (
<UiStack>
<Grid>
<Grid.Col span={12}>
<DemoFeatureAlerts />
</Grid.Col>
<Grid.Col span={4}>
<DemoFeatureAnchor />
</Grid.Col>
<Grid.Col span={4}>
<DemoFeatureLoader />
</Grid.Col>
<Grid.Col span={4}>
<DemoFeatureMenu />
</Grid.Col>
<Grid.Col span={4}>
<DemoFeatureBack />
</Grid.Col>
<Grid.Col span={4}>
<DemoFeatureCopy />
</Grid.Col>
</Grid>
<DemoFeatureGridRoutes basePath="/themes" />
<DemoFeatureTabRoutes basePath="/themes" />
<DemoFeatureForm />
<DemoFeatureHeader />
<DemoFeaturePage />
<DemoFeatureSearchInput />
<DemoFeatureNotFound />
</UiStack>
)
}

0 comments on commit 2ed40b3

Please sign in to comment.