From 07d1c79140bef8f642c11a5fa1438ba2d9d603bf Mon Sep 17 00:00:00 2001 From: Bram Borggreve Date: Tue, 5 Mar 2024 18:02:01 +0000 Subject: [PATCH] feat: add app themes --- apps/web/src/app/app-layout.tsx | 5 +- apps/web/src/app/app-routes.tsx | 2 + apps/web/src/app/app-theme.provider.tsx | 122 ++++++++++++++++++ apps/web/src/app/app.tsx | 8 +- .../demo/demo-feature-theme-select.tsx | 23 ++++ .../src/app/features/demo/demo-feature.tsx | 2 + .../app/features/themes/themes-feature.tsx | 74 +++++++++++ 7 files changed, 231 insertions(+), 5 deletions(-) create mode 100644 apps/web/src/app/app-theme.provider.tsx create mode 100644 apps/web/src/app/features/demo/demo-feature-theme-select.tsx create mode 100644 apps/web/src/app/features/themes/themes-feature.tsx diff --git a/apps/web/src/app/app-layout.tsx b/apps/web/src/app/app-layout.tsx index 9a40718..a6c69cb 100644 --- a/apps/web/src/app/app-layout.tsx +++ b/apps/web/src/app/app-layout.tsx @@ -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) @@ -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={ + import('./features/account/account-list-feature')) const AccountDetail = lazy(() => import('./features/account/account-detail-feature')) @@ -17,6 +18,7 @@ const routes: RouteObject[] = [ { path: '/dashboard', element: }, { path: '/demo/*', element: }, { path: '/dev', element: }, + { path: '/themes', element: }, { path: '*', element: }, ] diff --git a/apps/web/src/app/app-theme.provider.tsx b/apps/web/src/app/app-theme.provider.tsx new file mode 100644 index 0000000..b961b6d --- /dev/null +++ b/apps/web/src/app/app-theme.provider.tsx @@ -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('pubkey-ui-app-theme', initialTheme, undefined, { getOnInit: true }) +const themesAtom = atomWithStorage('pubkey-ui-app-themes', initialThemes, undefined, { getOnInit: true }) + +const activeThemesAtom = atom((get) => { + const themes = get(themesAtom) + const theme = get(themeAtom) + return themes.map((item) => ({ + ...item, + active: item.id === theme.id, + })) +}) + +const activeThemeAtom = atom((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({} 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 ( + + + {children} + + + ) +} + +export function useAppTheme() { + return useContext(Context) +} + +export function AppThemeSelect() { + const { themes, theme, setTheme } = useAppTheme() + return ( + + + + + + + {themes.map((item) => ( + setTheme(item)}> + {item.id} + + ))} + + + ) +} diff --git a/apps/web/src/app/app.tsx b/apps/web/src/app/app.tsx index 543aa3b..2c0fddf 100644 --- a/apps/web/src/app/app.tsx +++ b/apps/web/src/app/app.tsx @@ -1,16 +1,16 @@ -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 ( - + @@ -18,7 +18,7 @@ export function App() { - + ) } diff --git a/apps/web/src/app/features/demo/demo-feature-theme-select.tsx b/apps/web/src/app/features/demo/demo-feature-theme-select.tsx new file mode 100644 index 0000000..56f5640 --- /dev/null +++ b/apps/web/src/app/features/demo/demo-feature-theme-select.tsx @@ -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 ( + + + + + + + {themes.map((item) => ( + + ))} + + + + + ) +} diff --git a/apps/web/src/app/features/demo/demo-feature.tsx b/apps/web/src/app/features/demo/demo-feature.tsx index 7076176..99f1de2 100644 --- a/apps/web/src/app/features/demo/demo-feature.tsx +++ b/apps/web/src/app/features/demo/demo-feature.tsx @@ -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' @@ -47,6 +48,7 @@ export function DemoFeature() { { path: 'search-input', label: 'Search Input', element: }, { path: 'stack', label: 'Stack', element: }, { path: 'tab-routes', label: 'Tab Routes', element: }, + { path: 'theme-select', label: 'Theme Select', element: }, { path: 'time', label: 'Time', element: }, { path: 'toast', label: 'Toast', element: }, ] diff --git a/apps/web/src/app/features/themes/themes-feature.tsx b/apps/web/src/app/features/themes/themes-feature.tsx new file mode 100644 index 0000000..77591ce --- /dev/null +++ b/apps/web/src/app/features/themes/themes-feature.tsx @@ -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 ( + + + + + + + + + + + {themes.map((item) => ( + + ))} + + + + + + ) +} + +export function ThemeForm({ add }: { add: (color: MantineColor, dark?: BackgroundColors) => void }) { + const [color, setColor] = useState('blue') + const [dark, setDark] = useState(undefined) + + return ( + + ({ label: id, value: id }))} + value={dark} + onChange={(value) => (value ? setDark(value as BackgroundColors) : undefined)} + /> + + + ) +}