diff --git a/eslint.config.js b/eslint.config.js
index 7c86a6ea..aa03c972 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -38,7 +38,7 @@ export default [
},
},
{
- ignores: ['vendor', 'node_modules', 'public', 'bootstrap/ssr', 'tailwind.config.js'],
+ ignores: ['vendor', 'node_modules', 'public', 'bootstrap/ssr'],
},
prettier, // Turn off all rules that might conflict with Prettier
];
diff --git a/resources/js/components/two-factor-setup-modal.tsx b/resources/js/components/two-factor-setup-modal.tsx
index 317149ea..006547d0 100644
--- a/resources/js/components/two-factor-setup-modal.tsx
+++ b/resources/js/components/two-factor-setup-modal.tsx
@@ -12,6 +12,7 @@ import {
InputOTPGroup,
InputOTPSlot,
} from '@/components/ui/input-otp';
+import { useAppearance } from '@/hooks/use-appearance';
import { useClipboard } from '@/hooks/use-clipboard';
import { OTP_MAX_LENGTH } from '@/hooks/use-two-factor-auth';
import { confirm } from '@/routes/two-factor';
@@ -60,6 +61,7 @@ function TwoFactorSetupStep({
onNextStep: () => void;
errors: string[];
}) {
+ const { resolvedAppearance } = useAppearance();
const [copiedText, copy] = useClipboard();
const IconComponent = copiedText === manualSetupKey ? Check : Copy;
@@ -77,6 +79,12 @@ function TwoFactorSetupStep({
dangerouslySetInnerHTML={{
__html: qrCodeSvg,
}}
+ style={{
+ filter:
+ resolvedAppearance === 'dark'
+ ? 'invert(1) brightness(1.5)'
+ : undefined,
+ }}
/>
) : (
diff --git a/resources/js/hooks/use-appearance.tsx b/resources/js/hooks/use-appearance.tsx
index 56f5bcc5..98f8b283 100644
--- a/resources/js/hooks/use-appearance.tsx
+++ b/resources/js/hooks/use-appearance.tsx
@@ -1,59 +1,96 @@
-import { useCallback, useEffect, useState } from 'react';
+import { useCallback, useEffect, useMemo, useState } from 'react';
-export type Appearance = 'light' | 'dark' | 'system';
+export type ResolvedAppearance = 'light' | 'dark';
+export type Appearance = ResolvedAppearance | 'system';
-const prefersDark = () => {
- if (typeof window === 'undefined') {
- return false;
- }
+// Global state management
+const listeners = new Set<() => void>();
+let currentAppearance: Appearance = 'system';
+// Utility functions
+const prefersDark = (): boolean => {
+ if (typeof window === 'undefined') return false;
return window.matchMedia('(prefers-color-scheme: dark)').matches;
};
-const setCookie = (name: string, value: string, days = 365) => {
- if (typeof document === 'undefined') {
- return;
- }
-
+const setCookie = (name: string, value: string, days = 365): void => {
+ if (typeof document === 'undefined') return;
const maxAge = days * 24 * 60 * 60;
document.cookie = `${name}=${value};path=/;max-age=${maxAge};SameSite=Lax`;
};
-const applyTheme = (appearance: Appearance) => {
- const isDark =
- appearance === 'dark' || (appearance === 'system' && prefersDark());
+const getStoredAppearance = (): Appearance => {
+ if (typeof window === 'undefined') return 'system';
+ return (localStorage.getItem('appearance') as Appearance) || 'system';
+};
+
+const isDarkMode = (appearance: Appearance): boolean => {
+ return appearance === 'dark' || (appearance === 'system' && prefersDark());
+};
+const applyTheme = (appearance: Appearance): void => {
+ if (typeof document === 'undefined') return;
+ const isDark = isDarkMode(appearance);
document.documentElement.classList.toggle('dark', isDark);
document.documentElement.style.colorScheme = isDark ? 'dark' : 'light';
};
-const mediaQuery = () => {
- if (typeof window === 'undefined') {
- return null;
- }
+const notify = (): void => listeners.forEach((listener) => listener());
+const mediaQuery = (): MediaQueryList | null => {
+ if (typeof window === 'undefined') return null;
return window.matchMedia('(prefers-color-scheme: dark)');
};
-const handleSystemThemeChange = () => {
- const currentAppearance = localStorage.getItem('appearance') as Appearance;
- applyTheme(currentAppearance || 'system');
+const handleSystemThemeChange = (): void => {
+ applyTheme(currentAppearance);
+ notify();
};
-export function initializeTheme() {
- const savedAppearance =
- (localStorage.getItem('appearance') as Appearance) || 'system';
+export function initializeTheme(): void {
+ if (typeof window === 'undefined') return;
- applyTheme(savedAppearance);
+ const storedAppearance = getStoredAppearance();
- // Add the event listener for system theme changes...
+ // Initialize default appearance if none exists
+ if (!localStorage.getItem('appearance')) {
+ localStorage.setItem('appearance', 'system');
+ setCookie('appearance', 'system');
+ }
+
+ currentAppearance = storedAppearance;
+ applyTheme(currentAppearance);
+
+ // Set up system theme change listener
mediaQuery()?.addEventListener('change', handleSystemThemeChange);
}
export function useAppearance() {
- const [appearance, setAppearance] = useState('system');
+ const [appearance, setAppearance] =
+ useState(getStoredAppearance);
- const updateAppearance = useCallback((mode: Appearance) => {
+ useEffect(() => {
+ const handleChange = (): void => {
+ const newAppearance = getStoredAppearance();
+ setAppearance(newAppearance);
+ };
+
+ listeners.add(handleChange);
+ mediaQuery()?.addEventListener('change', handleChange);
+
+ return () => {
+ listeners.delete(handleChange);
+ mediaQuery()?.removeEventListener('change', handleChange);
+ };
+ }, []);
+
+ const resolvedAppearance: ResolvedAppearance = useMemo(
+ () => (isDarkMode(appearance) ? 'dark' : 'light'),
+ [appearance],
+ );
+
+ const updateAppearance = useCallback((mode: Appearance): void => {
+ currentAppearance = mode;
setAppearance(mode);
// Store in localStorage for client-side persistence...
@@ -63,20 +100,8 @@ export function useAppearance() {
setCookie('appearance', mode);
applyTheme(mode);
+ notify();
}, []);
- useEffect(() => {
- const savedAppearance = localStorage.getItem(
- 'appearance',
- ) as Appearance | null;
- updateAppearance(savedAppearance || 'system');
-
- return () =>
- mediaQuery()?.removeEventListener(
- 'change',
- handleSystemThemeChange,
- );
- }, [updateAppearance]);
-
- return { appearance, updateAppearance } as const;
+ return { appearance, resolvedAppearance, updateAppearance } as const;
}