Skip to content

Commit fd44884

Browse files
Add theme toggle functionality and update styles for dark mode
1 parent 26e9e71 commit fd44884

File tree

6 files changed

+180
-1
lines changed

6 files changed

+180
-1
lines changed

src/components/Hero.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Sparkle, ArrowRight, Code } from '@phosphor-icons/react'
2+
import { ModeToggle } from "@/components/mode-toggle"
23

34
export function Hero() {
45
const handleScroll = (e: React.MouseEvent<HTMLAnchorElement>, targetId: string) => {
@@ -16,6 +17,9 @@ export function Hero() {
1617

1718
return (
1819
<div className="relative border-b border-border bg-gradient-to-b from-muted/30 to-background">
20+
<div className="absolute top-6 right-6">
21+
<ModeToggle />
22+
</div>
1923
<div className="max-w-3xl mx-auto px-6 py-16 md:py-24">
2024
<div className="space-y-8">
2125
<div className="flex items-center gap-4">

src/components/mode-toggle.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { Moon, Sun } from "@phosphor-icons/react"
2+
import { Button } from "@/components/ui/button"
3+
import {
4+
DropdownMenu,
5+
DropdownMenuContent,
6+
DropdownMenuItem,
7+
DropdownMenuTrigger,
8+
} from "@/components/ui/dropdown-menu"
9+
import { useTheme } from "@/components/theme-provider"
10+
11+
export function ModeToggle() {
12+
const { setTheme } = useTheme()
13+
14+
return (
15+
<DropdownMenu>
16+
<DropdownMenuTrigger asChild>
17+
<Button variant="outline" size="icon">
18+
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
19+
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
20+
<span className="sr-only">Toggle theme</span>
21+
</Button>
22+
</DropdownMenuTrigger>
23+
<DropdownMenuContent align="end">
24+
<DropdownMenuItem onClick={() => setTheme("light")}>
25+
Light
26+
</DropdownMenuItem>
27+
<DropdownMenuItem onClick={() => setTheme("dark")}>
28+
Dark
29+
</DropdownMenuItem>
30+
<DropdownMenuItem onClick={() => setTheme("system")}>
31+
System
32+
</DropdownMenuItem>
33+
</DropdownMenuContent>
34+
</DropdownMenu>
35+
)
36+
}

src/components/theme-provider.tsx

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { createContext, useContext, useEffect, useState } from "react"
2+
3+
type Theme = "dark" | "light" | "system"
4+
5+
type ThemeProviderProps = {
6+
children: React.ReactNode
7+
defaultTheme?: Theme
8+
storageKey?: string
9+
}
10+
11+
type ThemeProviderState = {
12+
theme: Theme
13+
setTheme: (theme: Theme) => void
14+
}
15+
16+
const initialState: ThemeProviderState = {
17+
theme: "system",
18+
setTheme: () => null,
19+
}
20+
21+
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
22+
23+
export function ThemeProvider({
24+
children,
25+
defaultTheme = "system",
26+
storageKey = "vite-ui-theme",
27+
}: ThemeProviderProps) {
28+
const [theme, setTheme] = useState<Theme>(
29+
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
30+
)
31+
32+
useEffect(() => {
33+
const root = window.document.documentElement
34+
35+
root.classList.remove("light", "dark")
36+
37+
if (theme === "system") {
38+
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
39+
.matches
40+
? "dark"
41+
: "light"
42+
43+
root.classList.add(systemTheme)
44+
root.setAttribute("data-appearance", systemTheme)
45+
return
46+
}
47+
48+
root.classList.add(theme)
49+
root.setAttribute("data-appearance", theme)
50+
}, [theme])
51+
52+
const value = {
53+
theme,
54+
setTheme: (theme: Theme) => {
55+
localStorage.setItem(storageKey, theme)
56+
setTheme(theme)
57+
},
58+
}
59+
60+
return (
61+
<ThemeProviderContext.Provider value={value}>
62+
{children}
63+
</ThemeProviderContext.Provider>
64+
)
65+
}
66+
67+
export const useTheme = () => {
68+
const context = useContext(ThemeProviderContext)
69+
70+
if (context === undefined)
71+
throw new Error("useTheme must be used within a ThemeProvider")
72+
73+
return context
74+
}

src/index.css

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,67 @@
4848
--input: oklch(0.90 0.005 270);
4949
--ring: oklch(0.52 0.18 275);
5050

51+
--chart-1: oklch(0.646 0.222 41.116);
52+
--chart-2: oklch(0.6 0.118 184.704);
53+
--chart-3: oklch(0.398 0.07 227.392);
54+
--chart-4: oklch(0.828 0.189 84.429);
55+
--chart-5: oklch(0.769 0.188 70.08);
56+
--sidebar: oklch(0.985 0 0);
57+
--sidebar-foreground: oklch(0.145 0 0);
58+
--sidebar-primary: oklch(0.205 0 0);
59+
--sidebar-primary-foreground: oklch(0.985 0 0);
60+
--sidebar-accent: oklch(0.97 0 0);
61+
--sidebar-accent-foreground: oklch(0.205 0 0);
62+
--sidebar-border: oklch(0.922 0 0);
63+
--sidebar-ring: oklch(0.708 0 0);
64+
5165
--radius: 0.5rem;
5266
}
5367

68+
[data-appearance="dark"] {
69+
--background: oklch(0.14 0.005 275);
70+
--foreground: oklch(0.98 0.003 270);
71+
72+
--card: oklch(0.14 0.005 275);
73+
--card-foreground: oklch(0.98 0.003 270);
74+
75+
--popover: oklch(0.14 0.005 275);
76+
--popover-foreground: oklch(0.98 0.003 270);
77+
78+
--primary: oklch(0.52 0.18 275);
79+
--primary-foreground: oklch(0.99 0 0);
80+
81+
--secondary: oklch(0.25 0.02 270);
82+
--secondary-foreground: oklch(0.98 0.003 270);
83+
84+
--muted: oklch(0.20 0.005 270);
85+
--muted-foreground: oklch(0.65 0.02 275);
86+
87+
--accent: oklch(0.62 0.20 245);
88+
--accent-foreground: oklch(0.99 0 0);
89+
90+
--destructive: oklch(0.55 0.22 27);
91+
--destructive-foreground: oklch(0.99 0 0);
92+
93+
--border: oklch(0.25 0.005 270);
94+
--input: oklch(0.25 0.005 270);
95+
--ring: oklch(0.52 0.18 275);
96+
97+
--chart-1: oklch(0.488 0.243 264.376);
98+
--chart-2: oklch(0.696 0.17 162.48);
99+
--chart-3: oklch(0.769 0.188 70.08);
100+
--chart-4: oklch(0.627 0.265 303.9);
101+
--chart-5: oklch(0.645 0.246 16.439);
102+
--sidebar: oklch(0.205 0 0);
103+
--sidebar-foreground: oklch(0.985 0 0);
104+
--sidebar-primary: oklch(0.488 0.243 264.376);
105+
--sidebar-primary-foreground: oklch(0.985 0 0);
106+
--sidebar-accent: oklch(0.269 0 0);
107+
--sidebar-accent-foreground: oklch(0.985 0 0);
108+
--sidebar-border: oklch(1 0 0 / 10%);
109+
--sidebar-ring: oklch(0.556 0 0);
110+
}
111+
54112
@theme {
55113
--color-background: var(--background);
56114
--color-foreground: var(--foreground);

src/main.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
/*
3232
---break---
3333
*/
34+
/*
3435
:root {
3536
--radius: 0.625rem;
3637
--background: oklch(1 0 0);
@@ -65,10 +66,12 @@
6566
--sidebar-border: oklch(0.922 0 0);
6667
--sidebar-ring: oklch(0.708 0 0);
6768
}
69+
*/
6870

6971
/*
7072
---break---
7173
*/
74+
/*
7275
.dark {
7376
--background: oklch(0.145 0 0);
7477
--foreground: oklch(0.985 0 0);
@@ -102,6 +105,7 @@
102105
--sidebar-border: oklch(1 0 0 / 10%);
103106
--sidebar-ring: oklch(0.556 0 0);
104107
}
108+
*/
105109

106110
/*
107111
---break---

src/main.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@ import "@github/spark/spark"
44

55
import App from './App.tsx'
66
import { ErrorFallback } from './ErrorFallback.tsx'
7+
import { ThemeProvider } from "@/components/theme-provider"
78

89
import "./main.css"
910

1011
createRoot(document.getElementById('root')!).render(
1112
<ErrorBoundary FallbackComponent={ErrorFallback}>
12-
<App />
13+
<ThemeProvider defaultTheme="system" storageKey="vite-ui-theme">
14+
<App />
15+
</ThemeProvider>
1316
</ErrorBoundary>
1417
)

0 commit comments

Comments
 (0)