Skip to content

Commit 65f0561

Browse files
committed
Adding light and dark mode toggle
1 parent 20045ce commit 65f0561

File tree

8 files changed

+239
-119
lines changed

8 files changed

+239
-119
lines changed

package-lock.json

Lines changed: 26 additions & 24 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"@radix-ui/react-avatar": "^1.1.2",
2727
"@radix-ui/react-collapsible": "^1.1.2",
2828
"@radix-ui/react-dialog": "^1.1.4",
29-
"@radix-ui/react-dropdown-menu": "^2.1.3",
29+
"@radix-ui/react-dropdown-menu": "^2.1.4",
3030
"@radix-ui/react-label": "^2.1.1",
3131
"@radix-ui/react-select": "^2.1.4",
3232
"@radix-ui/react-separator": "^1.1.1",
@@ -38,4 +38,4 @@
3838
"tailwind-merge": "^2.5.5",
3939
"tailwindcss-animate": "^1.0.7"
4040
}
41-
}
41+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { useState, useEffect, HTMLAttributes } from '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+
10+
type Appearance = 'light' | 'dark' | 'system';
11+
12+
declare global {
13+
interface Window {
14+
appearance: Appearance;
15+
}
16+
}
17+
18+
interface AppearanceToggleProps extends HTMLAttributes<HTMLDivElement> { }
19+
20+
export default function AppearanceToggle({ className = '', ...props }: AppearanceToggleProps) {
21+
const [appearance, setAppearance] = useState<Appearance>('system');
22+
23+
// Check if user prefers dark mode
24+
const prefersDark = () =>
25+
window.matchMedia('(prefers-color-scheme: dark)').matches;
26+
27+
// Apply theme to document body
28+
const applyTheme = (mode: Appearance) => {
29+
const isDark = mode === 'dark' || (mode === 'system' && prefersDark());
30+
document.body.classList.toggle('dark', isDark);
31+
};
32+
33+
// Update appearance state and sync with window/localStorage
34+
const updateAppearance = (mode: Appearance) => {
35+
setAppearance(mode);
36+
window.appearance = mode;
37+
localStorage.setItem('appearance', mode);
38+
applyTheme(mode);
39+
};
40+
41+
// Initialize theme from localStorage or default to system
42+
useEffect(() => {
43+
const savedAppearance = localStorage.getItem('appearance') as Appearance;
44+
const initialAppearance = savedAppearance || 'system';
45+
updateAppearance(initialAppearance);
46+
47+
// Listen for system theme changes
48+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
49+
const handleChange = () => {
50+
if (appearance === 'system') {
51+
applyTheme('system');
52+
}
53+
};
54+
55+
mediaQuery.addEventListener('change', handleChange);
56+
return () => mediaQuery.removeEventListener('change', handleChange);
57+
}, []);
58+
59+
return (
60+
<div className={className} {...props}>
61+
<DropdownMenu>
62+
<DropdownMenuTrigger asChild>
63+
<Button variant="ghost" size="icon" className="h-9 w-9 rounded-md">
64+
{appearance === 'dark' ? (
65+
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-moon"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg>
66+
) : appearance === 'light' ? (
67+
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-sun-medium"><circle cx="12" cy="12" r="4"/><path d="M12 3v1"/><path d="M12 20v1"/><path d="M3 12h1"/><path d="M20 12h1"/><path d="m18.364 5.636-.707.707"/><path d="m6.343 17.657-.707.707"/><path d="m5.636 5.636.707.707"/><path d="m17.657 17.657.707.707"/></svg>
68+
) : (
69+
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-monitor"><rect width="20" height="14" x="2" y="3" rx="2"/><line x1="8" x2="16" y1="21" y2="21"/><line x1="12" x2="12" y1="17" y2="21"/></svg>
70+
)}
71+
<span className="sr-only">Toggle theme</span>
72+
</Button>
73+
</DropdownMenuTrigger>
74+
<DropdownMenuContent align="end" rounded="xl">
75+
<DropdownMenuItem onClick={() => updateAppearance('light')}>
76+
<span className="flex items-center gap-2">
77+
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-sun-medium"><circle cx="12" cy="12" r="4"/><path d="M12 3v1"/><path d="M12 20v1"/><path d="M3 12h1"/><path d="M20 12h1"/><path d="m18.364 5.636-.707.707"/><path d="m6.343 17.657-.707.707"/><path d="m5.636 5.636.707.707"/><path d="m17.657 17.657.707.707"/></svg>
78+
Light
79+
</span>
80+
</DropdownMenuItem>
81+
<DropdownMenuItem onClick={() => updateAppearance('dark')}>
82+
<span className="flex items-center gap-2">
83+
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-moon"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg>
84+
Dark
85+
</span>
86+
</DropdownMenuItem>
87+
<DropdownMenuItem onClick={() => updateAppearance('system')}>
88+
<span className="flex items-center gap-2">
89+
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-monitor"><rect width="20" height="14" x="2" y="3" rx="2"/><line x1="8" x2="16" y1="21" y2="21"/><line x1="12" x2="12" y1="17" y2="21"/></svg>
90+
System
91+
</span>
92+
</DropdownMenuItem>
93+
</DropdownMenuContent>
94+
</DropdownMenu>
95+
</div>
96+
);
97+
}

resources/js/Layouts/AppLayout.tsx

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Fragment } from 'react'
2+
import AppearanceToggle from '@/Components/AppearanceToggle';
23
import { AppSidebar } from "@/Components/AppSidebar"
34
import {
45
Breadcrumb,
@@ -33,37 +34,40 @@ export default function App({
3334
<SidebarProvider>
3435
<AppSidebar />
3536
<SidebarInset>
36-
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4">
37-
<SidebarTrigger className="-ml-1" />
38-
{breadcrumbItems.length > 0 && (
39-
<>
40-
<Separator orientation="vertical" className="mr-2 h-4" />
41-
<Breadcrumb>
42-
<BreadcrumbList>
43-
{breadcrumbItems.map((item, index) => {
44-
const isLast = index === breadcrumbItems.length - 1;
37+
<header className="flex h-16 shrink-0 items-center w-full justify-between gap-2 border-b px-4">
38+
<div className="flex items-center gap-2">
39+
<SidebarTrigger className="-ml-1" />
40+
{breadcrumbItems.length > 0 && (
41+
<>
42+
<Separator orientation="vertical" className="mr-2 h-4" />
43+
<Breadcrumb>
44+
<BreadcrumbList>
45+
{breadcrumbItems.map((item, index) => {
46+
const isLast = index === breadcrumbItems.length - 1;
4547

46-
return (
47-
<Fragment key={index}>
48-
<BreadcrumbItem>
49-
{isLast ? (
50-
<BreadcrumbPage>{item.title}</BreadcrumbPage>
51-
) : (
52-
<BreadcrumbLink href={item.href}>
53-
{item.title}
54-
</BreadcrumbLink>
48+
return (
49+
<Fragment key={index}>
50+
<BreadcrumbItem>
51+
{isLast ? (
52+
<BreadcrumbPage>{item.title}</BreadcrumbPage>
53+
) : (
54+
<BreadcrumbLink href={item.href}>
55+
{item.title}
56+
</BreadcrumbLink>
57+
)}
58+
</BreadcrumbItem>
59+
{!isLast && (
60+
<BreadcrumbSeparator />
5561
)}
56-
</BreadcrumbItem>
57-
{!isLast && (
58-
<BreadcrumbSeparator />
59-
)}
60-
</Fragment>
61-
);
62-
})}
63-
</BreadcrumbList>
64-
</Breadcrumb>
65-
</>
66-
)}
62+
</Fragment>
63+
);
64+
})}
65+
</BreadcrumbList>
66+
</Breadcrumb>
67+
</>
68+
)}
69+
</div>
70+
<AppearanceToggle className="opacity-40 hover:opacity-100" />
6771
</header>
6872
{children}
6973
</SidebarInset>

resources/js/Layouts/Auth/AuthSimpleLayout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export default function AuthSimpleLayout({
2626
className="flex flex-col items-center gap-2 font-medium"
2727
>
2828
<div className="flex h-10 w-10 items-center justify-center rounded-md">
29-
<ApplicationLogo className="size-10 fill-current text-black" />
29+
<ApplicationLogo className="size-10 fill-current text-black dark:text-white" />
3030
</div>
3131
<span className="sr-only">{title}</span>
3232
</Link>

resources/js/Pages/Welcome.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import AppearanceToggle from '@/Components/AppearanceToggle';
12
import { PageProps } from '@/types';
23
import { Head, Link } from '@inertiajs/react';
34

@@ -43,6 +44,7 @@ export default function Welcome({
4344
</svg>
4445
</div>
4546
<nav className="-mx-3 flex flex-1 justify-end">
47+
<AppearanceToggle class="mr-2 translate-y-0.5" />
4648
{auth.user ? (
4749
<Link
4850
href={route('dashboard')}

resources/js/app.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,21 @@ import {
1414
const appName =
1515
import.meta.env.VITE_APP_NAME || 'Laravel';
1616

17+
// window.appearance = 'system';
18+
19+
// // Initialize appearance
20+
// if (typeof window !== 'undefined') {
21+
// const appearance = localStorage.getItem('appearance') || 'system';
22+
// window.appearance = appearance;
23+
24+
// const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
25+
// const shouldBeDark = appearance === 'dark' || (appearance === 'system' && prefersDark);
26+
27+
// if (shouldBeDark) {
28+
// document.body.classList.add('dark');
29+
// }
30+
// }
31+
1732
createInertiaApp({
1833
title: (title) => `${title} - ${appName}`,
1934
resolve: (name) =>

0 commit comments

Comments
 (0)