From 1b8145a56ca639b5ae17d355cb6a0d9daad69ce1 Mon Sep 17 00:00:00 2001 From: Manavsaliya Date: Tue, 7 Oct 2025 22:29:13 +0530 Subject: [PATCH 1/3] Fix Theme Switching Issue & Two Factor QR Code for dark mode --- eslint.config.js | 2 +- .../js/components/two-factor-setup-modal.tsx | 8 ++ resources/js/hooks/use-appearance.tsx | 109 +++++++++++------- 3 files changed, 76 insertions(+), 43 deletions(-) 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; } From 3f336f967023d78127e2176afef5633a56994026 Mon Sep 17 00:00:00 2001 From: Manavsaliya Date: Tue, 21 Oct 2025 15:44:44 +0530 Subject: [PATCH 2/3] Replaced useState with useSyncExternalStore --- resources/js/hooks/use-appearance.tsx | 39 ++++++++++----------------- 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/resources/js/hooks/use-appearance.tsx b/resources/js/hooks/use-appearance.tsx index 98f8b283..8cbadfad 100644 --- a/resources/js/hooks/use-appearance.tsx +++ b/resources/js/hooks/use-appearance.tsx @@ -1,13 +1,11 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useMemo, useSyncExternalStore } from 'react'; export type ResolvedAppearance = 'light' | 'dark'; export type Appearance = ResolvedAppearance | 'system'; -// 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; @@ -30,11 +28,18 @@ const isDarkMode = (appearance: Appearance): boolean => { 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 subscribe = (callback: () => void) => { + listeners.add(callback); + return () => listeners.delete(callback); +}; + const notify = (): void => listeners.forEach((listener) => listener()); const mediaQuery = (): MediaQueryList | null => { @@ -50,15 +55,12 @@ const handleSystemThemeChange = (): void => { export function initializeTheme(): void { if (typeof window === 'undefined') return; - const storedAppearance = getStoredAppearance(); - - // Initialize default appearance if none exists if (!localStorage.getItem('appearance')) { localStorage.setItem('appearance', 'system'); setCookie('appearance', 'system'); } - currentAppearance = storedAppearance; + currentAppearance = getStoredAppearance(); applyTheme(currentAppearance); // Set up system theme change listener @@ -66,23 +68,11 @@ export function initializeTheme(): void { } export function useAppearance() { - const [appearance, setAppearance] = - useState(getStoredAppearance); - - 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 appearance: Appearance = useSyncExternalStore( + subscribe, + () => currentAppearance, + () => 'system', + ); const resolvedAppearance: ResolvedAppearance = useMemo( () => (isDarkMode(appearance) ? 'dark' : 'light'), @@ -91,7 +81,6 @@ export function useAppearance() { const updateAppearance = useCallback((mode: Appearance): void => { currentAppearance = mode; - setAppearance(mode); // Store in localStorage for client-side persistence... localStorage.setItem('appearance', mode); From 808de1d70633bec605d270a0899805b4d5e8ac33 Mon Sep 17 00:00:00 2001 From: Manavsaliya Date: Tue, 21 Oct 2025 15:51:27 +0530 Subject: [PATCH 3/3] Refactor ESLint ignores and update CSS import syntax --- eslint.config.js | 8 +------- resources/css/app.css | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 03866425..81c47509 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -30,13 +30,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/css/app.css b/resources/css/app.css index 468baf5b..7015d586 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -1,6 +1,6 @@ @import 'tailwindcss'; -@import "tw-animate-css"; +@import 'tw-animate-css'; @source '../views'; @source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';