Skip to content

Commit 27451e7

Browse files
committed
Add dark mode toggle
1 parent 6172633 commit 27451e7

File tree

9 files changed

+626
-304
lines changed

9 files changed

+626
-304
lines changed

app/layout.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import "./globals.css";
33
import React from "react";
44
import { Toaster } from "@/components/ui/sonner";
55
import { Analytics } from "@vercel/analytics/react";
6+
import { ThemeProvider } from "@/components/theme-provider";
67

78
export const metadata: Metadata = {
89
title: "临时邮箱 - 匿名的一次性邮箱",
@@ -26,7 +27,10 @@ export const viewport: Viewport = {
2627
width: "device-width",
2728
initialScale: 1,
2829
maximumScale: 1,
29-
themeColor: "#fff",
30+
themeColor: [
31+
{ media: "(prefers-color-scheme: light)", color: "#fff" },
32+
{ media: "(prefers-color-scheme: dark)", color: "#09090B" },
33+
],
3034
};
3135

3236
export default function RootLayout({
@@ -37,8 +41,15 @@ export default function RootLayout({
3741
return (
3842
<html lang="zh">
3943
<body>
40-
{children}
41-
<Toaster richColors position="top-right" />
44+
<ThemeProvider
45+
attribute="class"
46+
defaultTheme="system"
47+
enableSystem
48+
disableTransitionOnChange
49+
>
50+
{children}
51+
<Toaster richColors position="top-right" />
52+
</ThemeProvider>
4253
{process.env.VERCEL && <Analytics />}
4354
</body>
4455
</html>

components/actions.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,23 +89,23 @@ function Actions() {
8989
</div>
9090

9191
{!edited && (
92-
<Button variant="outline" size="sm" onClick={() => setEdited(true)}>
92+
<Button variant="outline" onClick={() => setEdited(true)}>
9393
<PenLine size={14} className="mr-1" />
9494
编辑
9595
</Button>
9696
)}
9797
{edited && (
98-
<Button variant="outline" size="sm" onClick={onSave}>
98+
<Button variant="outline" onClick={onSave}>
9999
<CheckCircle size={14} className="mr-1" />
100100
保存
101101
</Button>
102102
)}
103-
<Button variant="outline" size="sm" onClick={onRandom}>
103+
<Button variant="outline" onClick={onRandom}>
104104
<Shuffle size={14} className="mr-1" />
105105
随机
106106
</Button>
107107
<MailHistory onChange={onChange}>
108-
<Button variant="outline" size="sm">
108+
<Button variant="outline">
109109
<History size={14} className="mr-1" />
110110
历史
111111
</Button>

components/header.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from "react";
22
import { Github, ShieldCheck } from "lucide-react";
33
import { VERSION } from "@/lib/constant";
4+
import { ModeToggle } from "@/components/mode-toggle";
45

56
function Header() {
67
return (
@@ -9,10 +10,11 @@ function Header() {
910
<ShieldCheck strokeWidth={1.8} />
1011
<span className="ml-1 font-medium">临时邮箱</span>
1112
<span className="flex-1" />
13+
<ModeToggle />
1214
<a
1315
href="https://github.com/sunls24/temporary-mail"
1416
target="_blank"
15-
className="flex items-center rounded-md p-1 transition-colors hover:bg-secondary"
17+
className="ml-1 flex items-center rounded-md p-1 transition-colors hover:bg-secondary"
1618
>
1719
<Github size={18} strokeWidth={1.8} />
1820
<span className="ml-1 text-sm font-medium text-muted-foreground underline underline-offset-4">

components/mode-toggle.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"use client";
2+
import * as React from "react";
3+
import { useTheme } from "next-themes";
4+
5+
import { Button } from "@/components/ui/button";
6+
import {
7+
DropdownMenu,
8+
DropdownMenuContent,
9+
DropdownMenuItem,
10+
DropdownMenuTrigger,
11+
} from "@/components/ui/dropdown-menu";
12+
import { Laptop2, MoonStar, Sun } from "lucide-react";
13+
14+
export function ModeToggle() {
15+
const { setTheme } = useTheme();
16+
17+
return (
18+
<DropdownMenu>
19+
<DropdownMenuTrigger asChild>
20+
<Button variant="ghost" size="icon">
21+
<Sun size={22} strokeWidth={1.5} className="dark:hidden" />
22+
<MoonStar size={22} strokeWidth={1.5} className="hidden dark:block" />
23+
</Button>
24+
</DropdownMenuTrigger>
25+
<DropdownMenuContent align="start">
26+
<DropdownMenuItem onClick={() => setTheme("light")}>
27+
<Sun size={20} strokeWidth={1.5} className="mr-2" />
28+
Light
29+
</DropdownMenuItem>
30+
<DropdownMenuItem onClick={() => setTheme("dark")}>
31+
<MoonStar size={20} strokeWidth={1.5} className="mr-2" />
32+
Dark
33+
</DropdownMenuItem>
34+
<DropdownMenuItem onClick={() => setTheme("system")}>
35+
<Laptop2 size={20} strokeWidth={1.5} className="mr-2" /> System
36+
</DropdownMenuItem>
37+
</DropdownMenuContent>
38+
</DropdownMenu>
39+
);
40+
}

components/theme-provider.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"use client";
2+
3+
import * as React from "react";
4+
import { ThemeProvider as NextThemesProvider } from "next-themes";
5+
import { type ThemeProviderProps } from "next-themes/dist/types";
6+
7+
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
8+
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
9+
}

components/ui/dropdown-menu.tsx

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

lib/constant.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ export const DOMAIN_LIST = ["@isco.eu.org", "@sliu.eu.org"];
33
export const REFRESH_SECONDS =
44
process.env.NODE_ENV === "development" ? 100 : 10;
55

6-
export const VERSION = "1.0.1";
6+
export const VERSION = "1.0.2";

0 commit comments

Comments
 (0)