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 5, 2024
1 parent 7d1e67f commit 07d1c79
Show file tree
Hide file tree
Showing 7 changed files with 231 additions and 5 deletions.
5 changes: 4 additions & 1 deletion apps/web/src/app/app-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { Avatar, Group, rem } from '@mantine/core'
import { UiHeader, UiLayout, UiMenu, UiThemeSwitch } from '@pubkey-ui/core'
import { IconSettings, IconUser, IconUserCog } from '@tabler/icons-react'
import { ReactNode } from 'react'
import { useDisclosure } from '@mantine/hooks'
import { AccountChecker } from './features/account/account-ui'
import { ClusterChecker, ClusterUiSelect } from './features/cluster/cluster-ui'
import { useDisclosure } from '@mantine/hooks'
import { AppThemeSelect } from './app-theme.provider'

export function AppLayout({ children }: { children: ReactNode }) {
const [opened, { toggle }] = useDisclosure(false)
Expand All @@ -19,10 +20,12 @@ export function AppLayout({ children }: { children: ReactNode }) {
{ label: 'Account', link: '/account' },
{ label: 'Demo', link: '/demo' },
{ label: 'Dev', link: '/dev' },
{ label: 'Themes', link: '/themes' },
]}
profile={
<Group>
<ClusterUiSelect />
<AppThemeSelect />
<UiThemeSwitch />
<UiMenu
position="bottom-end"
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/app/app-routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Link, Navigate, RouteObject, useRoutes } from 'react-router-dom'
import { DemoFeature } from './features'
import { DashboardFeature } from './features/dashboard/dashboard-feature'
import { DevFeature } from './features/dev/dev-feature'
import { ThemesFeature } from './features/themes/themes-feature'

const AccountList = lazy(() => import('./features/account/account-list-feature'))
const AccountDetail = lazy(() => import('./features/account/account-detail-feature'))
Expand All @@ -17,6 +18,7 @@ const routes: RouteObject[] = [
{ path: '/dashboard', element: <DashboardFeature /> },
{ path: '/demo/*', element: <DemoFeature /> },
{ path: '/dev', element: <DevFeature /> },
{ path: '/themes', element: <ThemesFeature /> },
{ path: '*', element: <UiNotFound /> },
]

Expand Down
122 changes: 122 additions & 0 deletions apps/web/src/app/app-theme.provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { createContext, ReactNode, useContext } from 'react'
import {
BACKGROUND_COLORS,
BackgroundColors,
defaultThemes,
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'

export interface AppTheme extends UiTheme {
active?: boolean
}

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'] } }) },
]

const initialThemes = appThemes
const initialTheme = appThemes[0]

const themeAtom = atomWithStorage<AppTheme>('pubkey-ui-app-theme', initialTheme, undefined, { getOnInit: true })
const themesAtom = atomWithStorage<AppTheme[]>('pubkey-ui-app-themes', initialThemes, undefined, { getOnInit: true })

const activeThemesAtom = atom<AppTheme[]>((get) => {
const themes = get(themesAtom)
const theme = get(themeAtom)
return themes.map((item) => ({
...item,
active: item.id === theme.id,
}))
})

const activeThemeAtom = atom<AppTheme>((get) => {
const themes = get(activeThemesAtom)

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
}

const Context = createContext<AppThemeProviderContext>({} as AppThemeProviderContext)

export function AppThemeProvider({ children }: { children: ReactNode }) {
const theme = useAtomValue(activeThemeAtom)
const themes = useAtomValue(activeThemesAtom)
const setTheme = useSetAtom(themeAtom)
const setThemes = useSetAtom(themesAtom)

const value: AppThemeProviderContext = {
theme,
themes,
addTheme: (color: MantineColor, dark?: BackgroundColors) => {
const id = `${color}-${dark ?? 'default'}`
// Make sure we don't add a theme with the same id
if (themes.find((item) => item.id === 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)
},
setTheme,
}

return (
<Context.Provider value={value}>
<UiThemeSelectProvider link={ThemeLink} theme={value.theme} themes={value.themes}>
{children}
</UiThemeSelectProvider>
</Context.Provider>
)
}

export function useAppTheme() {
return useContext(Context)
}

export function AppThemeSelect() {
const { themes, theme, setTheme } = useAppTheme()
return (
<Menu shadow="md" width={200}>
<Menu.Target>
<Button>{theme.id}</Button>
</Menu.Target>

<Menu.Dropdown>
{themes.map((item) => (
<Menu.Item key={item.id} onClick={() => setTheme(item)}>
{item.id}
</Menu.Item>
))}
</Menu.Dropdown>
</Menu>
)
}
8 changes: 4 additions & 4 deletions apps/web/src/app/app.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import { UiThemeProvider } from '@pubkey-ui/core'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { AppLayout } from './app-layout'
import { AppRoutes, ThemeLink } from './app-routes'
import { AppRoutes } from './app-routes'
import { ClusterProvider } from './features/cluster/cluster-data-access'
import { SolanaProvider } from './features/solana/solana-provider'
import { AppThemeProvider } from './app-theme.provider'

const client = new QueryClient()

export function App() {
return (
<QueryClientProvider client={client}>
<UiThemeProvider link={ThemeLink}>
<AppThemeProvider>
<ClusterProvider>
<SolanaProvider>
<AppLayout>
<AppRoutes />
</AppLayout>
</SolanaProvider>
</ClusterProvider>
</UiThemeProvider>
</AppThemeProvider>
</QueryClientProvider>
)
}
23 changes: 23 additions & 0 deletions apps/web/src/app/features/demo/demo-feature-theme-select.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Button, Group } from '@mantine/core'
import { UiCard, UiDebugModal, UiStack, UiThemeSelect, useUiThemeSelect } from '@pubkey-ui/core'

export function DemoFeatureThemeSelect() {
const { themes, selected, selectTheme } = useUiThemeSelect()
return (
<UiStack>
<UiCard title="ThemeSelect">
<UiThemeSelect />
</UiCard>
<UiCard title="ThemeSelect">
<Group>
{themes.map((item) => (
<Button disabled={selected.id === item.id} key={item.id} onClick={() => selectTheme(item.id)}>
{item.id}
</Button>
))}
</Group>
</UiCard>
<UiDebugModal data={{ themes, selected }} />
</UiStack>
)
}
2 changes: 2 additions & 0 deletions apps/web/src/app/features/demo/demo-feature.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { DemoFeaturePage } from './demo-feature-page'
import { DemoFeatureSearchInput } from './demo-feature-search-input'
import { DemoFeatureStack } from './demo-feature-stack'
import { DemoFeatureTabRoutes } from './demo-feature-tab-routes'
import { DemoFeatureThemeSelect } from './demo-feature-theme-select'
import { DemoFeatureTime } from './demo-feature-time'
import { DemoFeatureToast } from './demo-feature-toast'

Expand Down Expand Up @@ -47,6 +48,7 @@ export function DemoFeature() {
{ path: 'search-input', label: 'Search Input', element: <DemoFeatureSearchInput /> },
{ path: 'stack', label: 'Stack', element: <DemoFeatureStack /> },
{ path: 'tab-routes', label: 'Tab Routes', element: <DemoFeatureTabRoutes /> },
{ path: 'theme-select', label: 'Theme Select', element: <DemoFeatureThemeSelect /> },
{ path: 'time', label: 'Time', element: <DemoFeatureTime /> },
{ path: 'toast', label: 'Toast', element: <DemoFeatureToast /> },
]
Expand Down
74 changes: 74 additions & 0 deletions apps/web/src/app/features/themes/themes-feature.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import {
backgroundColorIds,
BackgroundColors,
mantineColorIds,
UiCard,
UiContainer,
UiDebug,
UiInfo,
UiStack,
useUiThemeSelect,
} from '@pubkey-ui/core'
import { useAppTheme } from '../../app-theme.provider'
import { Button, Group, MantineColor, Select } from '@mantine/core'
import { useState } from 'react'
export function ThemesFeature() {
const { themes, addTheme, setTheme, theme } = useAppTheme()
const { selected } = useUiThemeSelect()
return (
<UiContainer>
<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>
))}
</Group>
</UiCard>
<UiDebug data={{ selected: selected.id, theme: theme.id, themes }} open />
</UiStack>
</UiContainer>
)
}

export function ThemeForm({ add }: { add: (color: MantineColor, dark?: BackgroundColors) => void }) {
const [color, setColor] = useState<MantineColor>('blue')
const [dark, setDark] = useState<BackgroundColors | undefined>(undefined)

return (
<Group align="end">
<Select
label="Color"
description="Select the primary color"
required
data={mantineColorIds.map((id) => ({ label: id, value: id }))}
value={color}
onChange={(value) => (value ? setColor(value as MantineColor) : undefined)}
/>

<Select
label="Dark"
description="Select the dark color"
clearable
data={backgroundColorIds.map((id) => ({ label: id, value: id }))}
value={dark}
onChange={(value) => (value ? setDark(value as BackgroundColors) : undefined)}
/>
<Button
onClick={() => {
add(color, dark)
}}
>
Add
</Button>
</Group>
)
}

0 comments on commit 07d1c79

Please sign in to comment.