tangled
alpha
login
or
join now
roost.moe
/
recipes.blue
2
fork
atom
The recipes.blue monorepo
recipes.blue
recipes
appview
atproto
2
fork
atom
overview
issues
1
pulls
pipelines
feat: dark mode(!!!)
Hayden Young
1 year ago
1496e0dc
952c5518
+143
-26
6 changed files
expand all
collapse all
unified
split
apps
web
src
components
app-sidebar.tsx
mode-toggle.tsx
sidebar-title.tsx
theme-provider.tsx
index.css
main.tsx
+3
-1
apps/web/src/components/app-sidebar.tsx
···
18
18
SidebarRail,
19
19
} from "@/components/ui/sidebar"
20
20
import { NavUserOpts } from "./nav-user-opts"
21
21
+
import { ModeToggle } from "./mode-toggle"
21
22
22
23
const data = {
23
24
navMain: [
···
61
62
<Sidebar collapsible="icon" {...props}>
62
63
<SidebarHeader>
63
64
<SidebarMenu>
64
64
-
<SidebarMenuItem>
65
65
+
<SidebarMenuItem className="flex items-center justify-between gap-x-2">
65
66
<SidebarMenuButton size="lg" asChild>
66
67
<a href="#">
67
68
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
···
72
73
</div>
73
74
</a>
74
75
</SidebarMenuButton>
76
76
+
<ModeToggle />
75
77
</SidebarMenuItem>
76
78
</SidebarMenu>
77
79
</SidebarHeader>
+37
apps/web/src/components/mode-toggle.tsx
···
1
1
+
import { Moon, Sun } from "lucide-react"
2
2
+
3
3
+
import { Button } from "@/components/ui/button"
4
4
+
import {
5
5
+
DropdownMenu,
6
6
+
DropdownMenuContent,
7
7
+
DropdownMenuItem,
8
8
+
DropdownMenuTrigger,
9
9
+
} from "@/components/ui/dropdown-menu"
10
10
+
import { useTheme } from "@/components/theme-provider"
11
11
+
12
12
+
export function ModeToggle() {
13
13
+
const { setTheme } = useTheme()
14
14
+
15
15
+
return (
16
16
+
<DropdownMenu>
17
17
+
<DropdownMenuTrigger asChild>
18
18
+
<Button variant="outline" size="icon" className="size-8 aspect-square flex items-center justify-center rounded-lg">
19
19
+
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
20
20
+
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
21
21
+
<span className="sr-only">Toggle theme</span>
22
22
+
</Button>
23
23
+
</DropdownMenuTrigger>
24
24
+
<DropdownMenuContent align="end">
25
25
+
<DropdownMenuItem onClick={() => setTheme("light")}>
26
26
+
Light
27
27
+
</DropdownMenuItem>
28
28
+
<DropdownMenuItem onClick={() => setTheme("dark")}>
29
29
+
Dark
30
30
+
</DropdownMenuItem>
31
31
+
<DropdownMenuItem onClick={() => setTheme("system")}>
32
32
+
System
33
33
+
</DropdownMenuItem>
34
34
+
</DropdownMenuContent>
35
35
+
</DropdownMenu>
36
36
+
)
37
37
+
}
+7
-5
apps/web/src/components/sidebar-title.tsx
···
4
4
SidebarMenu,
5
5
SidebarMenuItem,
6
6
} from "@/components/ui/sidebar"
7
7
+
import { ModeToggle } from "./mode-toggle"
7
8
8
9
export function SidebarTitle() {
9
10
return (
10
11
<SidebarMenu>
11
12
<SidebarMenuItem>
12
12
-
<div className="flex items-center gap-2 p-2">
13
13
-
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
14
14
-
<CookingPot className="size-4" />
15
15
-
</div>
16
16
-
<span className="font-semibold text-sm flex-1 text-left leading-tight">Recipes</span>
13
13
+
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
14
14
+
<CookingPot className="size-4" />
17
15
</div>
16
16
+
<div className="flex flex-col gap-0.5 leading-none">
17
17
+
<span className="font-semibold">Recipes</span>
18
18
+
</div>
19
19
+
<ModeToggle />
18
20
</SidebarMenuItem>
19
21
</SidebarMenu>
20
22
)
+73
apps/web/src/components/theme-provider.tsx
···
1
1
+
import { createContext, useContext, useEffect, useState } from "react"
2
2
+
3
3
+
type Theme = "dark" | "light" | "system"
4
4
+
5
5
+
type ThemeProviderProps = {
6
6
+
children: React.ReactNode
7
7
+
defaultTheme?: Theme
8
8
+
storageKey?: string
9
9
+
}
10
10
+
11
11
+
type ThemeProviderState = {
12
12
+
theme: Theme
13
13
+
setTheme: (theme: Theme) => void
14
14
+
}
15
15
+
16
16
+
const initialState: ThemeProviderState = {
17
17
+
theme: "system",
18
18
+
setTheme: () => null,
19
19
+
}
20
20
+
21
21
+
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
22
22
+
23
23
+
export function ThemeProvider({
24
24
+
children,
25
25
+
defaultTheme = "system",
26
26
+
storageKey = "vite-ui-theme",
27
27
+
...props
28
28
+
}: ThemeProviderProps) {
29
29
+
const [theme, setTheme] = useState<Theme>(
30
30
+
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
31
31
+
)
32
32
+
33
33
+
useEffect(() => {
34
34
+
const root = window.document.documentElement
35
35
+
36
36
+
root.classList.remove("light", "dark")
37
37
+
38
38
+
if (theme === "system") {
39
39
+
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
40
40
+
.matches
41
41
+
? "dark"
42
42
+
: "light"
43
43
+
44
44
+
root.classList.add(systemTheme)
45
45
+
return
46
46
+
}
47
47
+
48
48
+
root.classList.add(theme)
49
49
+
}, [theme])
50
50
+
51
51
+
const value = {
52
52
+
theme,
53
53
+
setTheme: (theme: Theme) => {
54
54
+
localStorage.setItem(storageKey, theme)
55
55
+
setTheme(theme)
56
56
+
},
57
57
+
}
58
58
+
59
59
+
return (
60
60
+
<ThemeProviderContext.Provider {...props} value={value}>
61
61
+
{children}
62
62
+
</ThemeProviderContext.Provider>
63
63
+
)
64
64
+
}
65
65
+
66
66
+
export const useTheme = () => {
67
67
+
const context = useContext(ThemeProviderContext)
68
68
+
69
69
+
if (context === undefined)
70
70
+
throw new Error("useTheme must be used within a ThemeProvider")
71
71
+
72
72
+
return context
73
73
+
}
+14
-14
apps/web/src/index.css
···
45
45
--card-foreground: 0 0% 98%;
46
46
--popover: 0 0% 3.9%;
47
47
--popover-foreground: 0 0% 98%;
48
48
-
--primary: 0 72.2% 50.6%;
49
49
-
--primary-foreground: 0 85.7% 97.3%;
50
50
-
--secondary: 0 0% 14.9%;
51
51
-
--secondary-foreground: 0 0% 98%;
52
52
-
--muted: 0 0% 14.9%;
53
53
-
--muted-foreground: 0 0% 63.9%;
54
54
-
--accent: 0 0% 14.9%;
55
55
-
--accent-foreground: 0 0% 98%;
48
48
+
--primary: 217.2 91.2% 59.8%;
49
49
+
--primary-foreground: 222.2 47.4% 11.2%;
50
50
+
--secondary: 217.2 32.6% 17.5%;
51
51
+
--secondary-foreground: 210 40% 98%;
52
52
+
--muted: 217.2 32.6% 17.5%;
53
53
+
--muted-foreground: 215 20.2% 65.1%;
54
54
+
--accent: 217.2 32.6% 17.5%;
55
55
+
--accent-foreground: 210 40% 98%;
56
56
--destructive: 0 62.8% 30.6%;
57
57
-
--destructive-foreground: 0 0% 98%;
58
58
-
--border: 0 0% 14.9%;
59
59
-
--input: 0 0% 14.9%;
60
60
-
--ring: 0 72.2% 50.6%;
57
57
+
--destructive-foreground: 210 40% 98%;
58
58
+
--border: 217.2 32.6% 17.5%;
59
59
+
--input: 217.2 32.6% 17.5%;
60
60
+
--ring: 224.3 76.3% 48%;
61
61
--chart-1: 220 70% 50%;
62
62
--chart-2: 160 60% 45%;
63
63
--chart-3: 30 80% 55%;
···
65
65
--chart-5: 340 75% 55%;
66
66
--sidebar-background: 240 5.9% 10%;
67
67
--sidebar-foreground: 240 4.8% 95.9%;
68
68
-
--sidebar-primary: 224.3 76.3% 48%;
68
68
+
--sidebar-primary: 217.2 91.2% 59.8%;
69
69
--sidebar-primary-foreground: 0 0% 100%;
70
70
-
--sidebar-accent: 240 3.7% 15.9%;
70
70
+
--sidebar-accent: 217.2 32.6% 17.5%;
71
71
--sidebar-accent-foreground: 240 4.8% 95.9%;
72
72
--sidebar-border: 240 3.7% 15.9%;
73
73
--sidebar-ring: 217.2 91.2% 59.8%;
+9
-6
apps/web/src/main.tsx
···
7
7
import { configureOAuth } from '@atcute/oauth-browser-client';
8
8
import './index.css'
9
9
import { AuthProvider, useAuth } from './state/auth';
10
10
+
import { ThemeProvider } from './components/theme-provider';
10
11
11
12
const router = createRouter({
12
13
routeTree,
···
45
46
46
47
createRoot(document.getElementById('root')!).render(
47
48
<StrictMode>
48
48
-
<AuthProvider>
49
49
-
<QueryClientProvider client={queryClient}>
50
50
-
<InnerApp />
51
51
-
<ReactQueryDevtools initialIsOpen={false} />
52
52
-
</QueryClientProvider>
53
53
-
</AuthProvider>
49
49
+
<ThemeProvider defaultTheme="dark" storageKey="recipes-theme">
50
50
+
<AuthProvider>
51
51
+
<QueryClientProvider client={queryClient}>
52
52
+
<InnerApp />
53
53
+
<ReactQueryDevtools initialIsOpen={false} />
54
54
+
</QueryClientProvider>
55
55
+
</AuthProvider>
56
56
+
</ThemeProvider>
54
57
</StrictMode>,
55
58
)