Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,26 @@ export default async function RootLayout({ children }: { children: React.ReactNo

return (
<html lang={locale} suppressHydrationWarning>
<head>
<script
dangerouslySetInnerHTML={{
__html: `
(function() {
try {
var theme = localStorage.getItem('merchant-theme-preference');
var darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
var themeToApply = 'light';
if (theme === 'dark' || ((!theme || theme === 'system') && darkQuery.matches)) {
themeToApply = 'dark';
}
document.documentElement.classList.remove('light', 'dark');
document.documentElement.classList.add(themeToApply);
} catch (e) {}
})();
`,
}}
/>
</head>
<body className={`${spaceGrotesk.variable} ${spaceMono.variable} min-h-screen font-sans`}>
<NextIntlClientProvider locale={locale} messages={messages}>
<ThemeProvider>
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/ThemeProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ export default function ThemeProvider({ children }: { children: ReactNode }) {
return (
<NextThemesProvider
attribute="class"
defaultTheme="dark"
enableSystem={false}
defaultTheme="system"
enableSystem={true}
storageKey="merchant-theme-preference"
>
{children}
Expand Down
46 changes: 36 additions & 10 deletions frontend/src/components/ThemeToggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useTheme } from "next-themes";
import { useEffect, useState } from "react";

export default function ThemeToggle() {
const { resolvedTheme, setTheme } = useTheme();
const { theme, setTheme, resolvedTheme } = useTheme();
const [mounted, setMounted] = useState(false);

// Avoid hydration mismatch by only rendering after mount
Expand All @@ -14,34 +14,40 @@ export default function ThemeToggle() {

if (!mounted) {
return (
<button className="h-9 w-9 rounded-lg border border-white/10 bg-white/5" aria-label="Toggle theme">
<span className="sr-only">Loading theme toggle</span>
</button>
<button className="h-9 w-9 rounded-lg border border-white/10 bg-white/5" aria-label="Loading theme settings" />
);
}

const cycleTheme = () => {
const themes = ["light", "dark", "system"];
const currentIndex = themes.indexOf(theme || "system");
const nextIndex = (currentIndex + 1) % themes.length;
setTheme(themes[nextIndex]);
};

return (
<button
onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
className="flex h-9 w-9 items-center justify-center rounded-lg border border-white/10 bg-white/5 transition-colors hover:bg-white/10"
aria-label="Toggle theme"
onClick={cycleTheme}
className="flex h-9 w-9 items-center justify-center rounded-lg border border-white/10 bg-white/5 transition-all hover:bg-white/10 active:scale-95"
aria-label={`Switch theme (current: ${theme})`}
title={theme === 'system' ? `Theme: System (${resolvedTheme})` : `Theme: ${theme}`}
>
{resolvedTheme === "dark" ? (
{theme === "light" ? (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="h-5 w-5 text-mint"
className="h-5 w-5 text-amber-500"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z"
/>
</svg>
) : (
) : theme === "dark" ? (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
Expand All @@ -56,6 +62,26 @@ export default function ThemeToggle() {
d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z"
/>
</svg>
) : (
<div className="relative flex items-center justify-center">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="h-5 w-5 text-slate-400"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9 17.25v1.007a3 3 0 01-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0115 18.257V17.25m6-12V15a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 15V5.25m18 0A2.25 2.25 0 0018.75 3H5.25A2.25 2.25 0 003 5.25m18 0V12a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 12V5.25"
/>
</svg>
<div className="absolute -bottom-0.5 -right-0.5 flex h-2 w-2 items-center justify-center">
<div className={`h-1.5 w-1.5 rounded-full ${resolvedTheme === 'dark' ? 'bg-mint' : 'bg-amber-500'}`} />
</div>
</div>
)}
</button>
);
Expand Down
Loading