Skip to content

Commit f324a31

Browse files
add theme-provider and prettier
1 parent 8bc24ad commit f324a31

File tree

9 files changed

+334
-9
lines changed

9 files changed

+334
-9
lines changed

.prettierrc.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"trailingComma": "es5",
3+
"tabWidth": 2,
4+
"semi": false,
5+
"singleQuote": true,
6+
"printWidth": 120,
7+
"plugins": ["prettier-plugin-tailwindcss"]
8+
}

bun.lockb

56.7 KB
Binary file not shown.

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"prod": "serve -s dist"
1212
},
1313
"dependencies": {
14+
"@radix-ui/react-dropdown-menu": "^2.1.1",
1415
"@radix-ui/react-slot": "^1.1.0",
1516
"class-variance-authority": "^0.7.0",
1617
"clsx": "^2.1.1",
@@ -29,9 +30,14 @@
2930
"@vitejs/plugin-react-swc": "^3.5.0",
3031
"autoprefixer": "^10.4.19",
3132
"eslint": "^8.57.0",
33+
"eslint-config-prettier": "^9.1.0",
34+
"eslint-plugin-import": "^2.29.1",
35+
"eslint-plugin-prettier": "^5.1.3",
3236
"eslint-plugin-react-hooks": "^4.6.2",
3337
"eslint-plugin-react-refresh": "^0.4.7",
3438
"postcss": "^8.4.39",
39+
"prettier": "^3.3.2",
40+
"prettier-plugin-tailwindcss": "^0.6.5",
3541
"tailwindcss": "^3.4.4",
3642
"typescript": "^5.2.2",
3743
"vite": "^5.3.1"

src/App.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { useState } from 'react'
22
import reactLogo from './assets/react.svg'
33
import viteLogo from '/vite.svg'
44
import '@/App.css'
5-
import {Button} from "@/components/ui/button.tsx";
5+
import { Button } from '@/components/ui/button.tsx'
6+
import { ModeToggle } from '@/components/custom/mode-toggle.tsx'
67

78
function App() {
89
const [count, setCount] = useState(0)
@@ -19,16 +20,13 @@ function App() {
1920
</div>
2021
<h1>Vite + React</h1>
2122
<div className="card">
22-
<Button onClick={() => setCount((count) => count + 1)}>
23-
count is {count}
24-
</Button>
23+
<Button onClick={() => setCount((count) => count + 1)}>count is {count}</Button>
2524
<p>
2625
Edit <code>src/App.tsx</code> and save to test HMR
2726
</p>
2827
</div>
29-
<p className="read-the-docs">
30-
Click on the Vite and React logos to learn more
31-
</p>
28+
<p className="read-the-docs">Click on the Vite and React logos to learn more</p>
29+
<ModeToggle />
3230
</>
3331
)
3432
}

src/components/custom/mode-toggle.tsx

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

src/components/theme-provider.tsx

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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+
...props
28+
}: ThemeProviderProps) {
29+
const [theme, setTheme] = useState<Theme>(
30+
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
31+
)
32+
33+
useEffect(() => {
34+
const root = window.document.documentElement
35+
36+
root.classList.remove("light", "dark")
37+
38+
if (theme === "system") {
39+
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
40+
.matches
41+
? "dark"
42+
: "light"
43+
44+
root.classList.add(systemTheme)
45+
return
46+
}
47+
48+
root.classList.add(theme)
49+
}, [theme])
50+
51+
const value = {
52+
theme,
53+
setTheme: (theme: Theme) => {
54+
localStorage.setItem(storageKey, theme)
55+
setTheme(theme)
56+
},
57+
}
58+
59+
return (
60+
<ThemeProviderContext.Provider {...props} value={value}>
61+
{children}
62+
</ThemeProviderContext.Provider>
63+
)
64+
}
65+
66+
export const useTheme = () => {
67+
const context = useContext(ThemeProviderContext)
68+
69+
if (context === undefined)
70+
throw new Error("useTheme must be used within a ThemeProvider")
71+
72+
return context
73+
}

src/components/ui/dropdown-menu.tsx

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import * as React from "react"
2+
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
3+
import { Check, ChevronRight, Circle } from "lucide-react"
4+
5+
import { cn } from "@/lib/utils"
6+
7+
const DropdownMenu = DropdownMenuPrimitive.Root
8+
9+
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
10+
11+
const DropdownMenuGroup = DropdownMenuPrimitive.Group
12+
13+
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
14+
15+
const DropdownMenuSub = DropdownMenuPrimitive.Sub
16+
17+
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
18+
19+
const DropdownMenuSubTrigger = React.forwardRef<
20+
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
21+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
22+
inset?: boolean
23+
}
24+
>(({ className, inset, children, ...props }, ref) => (
25+
<DropdownMenuPrimitive.SubTrigger
26+
ref={ref}
27+
className={cn(
28+
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
29+
inset && "pl-8",
30+
className
31+
)}
32+
{...props}
33+
>
34+
{children}
35+
<ChevronRight className="ml-auto h-4 w-4" />
36+
</DropdownMenuPrimitive.SubTrigger>
37+
))
38+
DropdownMenuSubTrigger.displayName =
39+
DropdownMenuPrimitive.SubTrigger.displayName
40+
41+
const DropdownMenuSubContent = React.forwardRef<
42+
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
43+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
44+
>(({ className, ...props }, ref) => (
45+
<DropdownMenuPrimitive.SubContent
46+
ref={ref}
47+
className={cn(
48+
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
49+
className
50+
)}
51+
{...props}
52+
/>
53+
))
54+
DropdownMenuSubContent.displayName =
55+
DropdownMenuPrimitive.SubContent.displayName
56+
57+
const DropdownMenuContent = React.forwardRef<
58+
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
59+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
60+
>(({ className, sideOffset = 4, ...props }, ref) => (
61+
<DropdownMenuPrimitive.Portal>
62+
<DropdownMenuPrimitive.Content
63+
ref={ref}
64+
sideOffset={sideOffset}
65+
className={cn(
66+
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
67+
className
68+
)}
69+
{...props}
70+
/>
71+
</DropdownMenuPrimitive.Portal>
72+
))
73+
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
74+
75+
const DropdownMenuItem = React.forwardRef<
76+
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
77+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
78+
inset?: boolean
79+
}
80+
>(({ className, inset, ...props }, ref) => (
81+
<DropdownMenuPrimitive.Item
82+
ref={ref}
83+
className={cn(
84+
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
85+
inset && "pl-8",
86+
className
87+
)}
88+
{...props}
89+
/>
90+
))
91+
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
92+
93+
const DropdownMenuCheckboxItem = React.forwardRef<
94+
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
95+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
96+
>(({ className, children, checked, ...props }, ref) => (
97+
<DropdownMenuPrimitive.CheckboxItem
98+
ref={ref}
99+
className={cn(
100+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
101+
className
102+
)}
103+
checked={checked}
104+
{...props}
105+
>
106+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
107+
<DropdownMenuPrimitive.ItemIndicator>
108+
<Check className="h-4 w-4" />
109+
</DropdownMenuPrimitive.ItemIndicator>
110+
</span>
111+
{children}
112+
</DropdownMenuPrimitive.CheckboxItem>
113+
))
114+
DropdownMenuCheckboxItem.displayName =
115+
DropdownMenuPrimitive.CheckboxItem.displayName
116+
117+
const DropdownMenuRadioItem = React.forwardRef<
118+
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
119+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
120+
>(({ className, children, ...props }, ref) => (
121+
<DropdownMenuPrimitive.RadioItem
122+
ref={ref}
123+
className={cn(
124+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
125+
className
126+
)}
127+
{...props}
128+
>
129+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
130+
<DropdownMenuPrimitive.ItemIndicator>
131+
<Circle className="h-2 w-2 fill-current" />
132+
</DropdownMenuPrimitive.ItemIndicator>
133+
</span>
134+
{children}
135+
</DropdownMenuPrimitive.RadioItem>
136+
))
137+
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
138+
139+
const DropdownMenuLabel = React.forwardRef<
140+
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
141+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
142+
inset?: boolean
143+
}
144+
>(({ className, inset, ...props }, ref) => (
145+
<DropdownMenuPrimitive.Label
146+
ref={ref}
147+
className={cn(
148+
"px-2 py-1.5 text-sm font-semibold",
149+
inset && "pl-8",
150+
className
151+
)}
152+
{...props}
153+
/>
154+
))
155+
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
156+
157+
const DropdownMenuSeparator = React.forwardRef<
158+
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
159+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
160+
>(({ className, ...props }, ref) => (
161+
<DropdownMenuPrimitive.Separator
162+
ref={ref}
163+
className={cn("-mx-1 my-1 h-px bg-muted", className)}
164+
{...props}
165+
/>
166+
))
167+
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
168+
169+
const DropdownMenuShortcut = ({
170+
className,
171+
...props
172+
}: React.HTMLAttributes<HTMLSpanElement>) => {
173+
return (
174+
<span
175+
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
176+
{...props}
177+
/>
178+
)
179+
}
180+
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
181+
182+
export {
183+
DropdownMenu,
184+
DropdownMenuTrigger,
185+
DropdownMenuContent,
186+
DropdownMenuItem,
187+
DropdownMenuCheckboxItem,
188+
DropdownMenuRadioItem,
189+
DropdownMenuLabel,
190+
DropdownMenuSeparator,
191+
DropdownMenuShortcut,
192+
DropdownMenuGroup,
193+
DropdownMenuPortal,
194+
DropdownMenuSub,
195+
DropdownMenuSubContent,
196+
DropdownMenuSubTrigger,
197+
DropdownMenuRadioGroup,
198+
}

src/main.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import React from 'react'
22
import ReactDOM from 'react-dom/client'
33
import App from '@/App.tsx'
4+
import { ThemeProvider } from '@/components/theme-provider.tsx'
45

56
ReactDOM.createRoot(document.getElementById('root')!).render(
67
<React.StrictMode>
7-
<App />
8-
</React.StrictMode>,
8+
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
9+
<App />
10+
</ThemeProvider>
11+
</React.StrictMode>
912
)

0 commit comments

Comments
 (0)