ThemeSettings.tsx129 lines · main
1import { LOCAL_STORAGE_KEYS } from 'common'
2import { useTheme } from 'next-themes'
3import { useEffect, useState } from 'react'
4import SVG from 'react-inlinesvg'
5import {
6 Card,
7 CardContent,
8 Label,
9 RadioGroup,
10 RadioGroupLargeItem,
11 Select,
12 SelectContent,
13 SelectItem,
14 SelectTrigger,
15 SelectValue,
16 Separator,
17 singleThemes,
18 Theme,
19} from 'ui'
20import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
21import {
22 PageSection,
23 PageSectionContent,
24 PageSectionDescription,
25 PageSectionMeta,
26 PageSectionSummary,
27 PageSectionTitle,
28} from 'ui-patterns/PageSection'
29
30import { DEFAULT_SIDEBAR_BEHAVIOR } from '@/components/interfaces/Sidebar'
31import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
32import { BASE_PATH } from '@/lib/constants'
33
34export const ThemeSettings = () => {
35 const [mounted, setMounted] = useState(false)
36 const { theme, setTheme } = useTheme()
37
38 const [sidebarBehaviour, setSidebarBehaviour] = useLocalStorageQuery(
39 LOCAL_STORAGE_KEYS.SIDEBAR_BEHAVIOR,
40 DEFAULT_SIDEBAR_BEHAVIOR
41 )
42 /**
43 * Avoid Hydration Mismatch
44 * https://github.com/pacocoursey/next-themes?tab=readme-ov-file#avoid-hydration-mismatch
45 */
46 // useEffect only runs on the client, so now we can safely show the UI
47 useEffect(() => setMounted(true), [])
48
49 if (!mounted) return null
50
51 function SingleThemeSelection() {
52 return (
53 <RadioGroup
54 name="theme"
55 onValueChange={setTheme}
56 aria-label="Choose a theme"
57 defaultValue={theme}
58 value={theme}
59 className="grid grid-cols-2 gap-4"
60 >
61 {singleThemes.map((theme: Theme) => (
62 <RadioGroupLargeItem
63 className="p-3 w-full"
64 key={theme.value}
65 value={theme.value}
66 label={theme.name}
67 >
68 <SVG src={`${BASE_PATH}/img/themes/${theme.value}.svg?v=2`} />
69 </RadioGroupLargeItem>
70 ))}
71 </RadioGroup>
72 )
73 }
74
75 return (
76 <PageSection>
77 <PageSectionMeta>
78 <PageSectionSummary>
79 <PageSectionTitle>Appearance</PageSectionTitle>
80 <PageSectionDescription>
81 Choose how Briven looks and behaves in the dashboard.
82 </PageSectionDescription>
83 </PageSectionSummary>
84 </PageSectionMeta>
85 <PageSectionContent>
86 <Card>
87 <CardContent className="grid grid-cols-12 gap-6">
88 <div className="col-span-full md:col-span-4 flex flex-col gap-2">
89 <Label htmlFor="theme" className="text-foreground">
90 Theme mode
91 </Label>
92 <p className="text-sm text-foreground-light">
93 Choose how Briven looks to you. Select a single theme, or sync with your system.
94 </p>
95 </div>
96
97 <div className="col-span-full md:col-span-8 flex flex-col gap-4">
98 <SingleThemeSelection />
99 </div>
100 </CardContent>
101 <Separator />
102 <CardContent>
103 <FormItemLayout
104 isReactForm={false}
105 label="Sidebar behavior"
106 layout="flex-row-reverse"
107 description="Choose your preferred sidebar behavior: open, closed, or expand on hover."
108 >
109 <Select
110 value={sidebarBehaviour}
111 onValueChange={setSidebarBehaviour}
112 aria-label="Select an option"
113 >
114 <SelectTrigger>
115 <SelectValue placeholder="Choose an option" />
116 </SelectTrigger>
117 <SelectContent>
118 <SelectItem value="open">Expanded</SelectItem>
119 <SelectItem value="closed">Collapsed</SelectItem>
120 <SelectItem value="expandable">Expand on hover</SelectItem>
121 </SelectContent>
122 </Select>
123 </FormItemLayout>
124 </CardContent>
125 </Card>
126 </PageSectionContent>
127 </PageSection>
128 )
129}