From 0535f63b40118bcebb6faed4b3d8a1aa3a09fb87 Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Tue, 3 Feb 2026 22:40:16 +0100 Subject: [PATCH] Align admin theme with Tamagui v2 --- .tamagui/tamagui.config.cjs | 4 + resources/js/admin/main.tsx | 7 +- .../mobile/__tests__/AdminTheme.test.tsx | 60 +++++++++++++++ resources/js/admin/mobile/theme.ts | 76 ++++++++----------- resources/js/guest-v2/App.tsx | 2 +- resources/js/setupTests.ts | 16 ++++ tamagui.config.ts | 4 + 7 files changed, 121 insertions(+), 48 deletions(-) create mode 100644 resources/js/admin/mobile/__tests__/AdminTheme.test.tsx diff --git a/.tamagui/tamagui.config.cjs b/.tamagui/tamagui.config.cjs index 4e0c77f..ecf059f 100644 --- a/.tamagui/tamagui.config.cjs +++ b/.tamagui/tamagui.config.cjs @@ -3749,6 +3749,10 @@ var fonts2 = { }; var config = (0, import_core2.createTamagui)({ ...defaultConfig, + settings: { + ...defaultConfig.settings ?? {}, + addThemeClassName: "html" + }, animations: createAnimations({ fast: "cubic-bezier(0.2, 0.7, 0.2, 1) 150ms", medium: "cubic-bezier(0.2, 0.7, 0.2, 1) 250ms", diff --git a/resources/js/admin/main.tsx b/resources/js/admin/main.tsx index 4b6dd4f..5ee2154 100644 --- a/resources/js/admin/main.tsx +++ b/resources/js/admin/main.tsx @@ -72,7 +72,7 @@ function AdminApp() { }, []); return ( - + @@ -86,7 +86,10 @@ function AdminApp() { )} > -
+
diff --git a/resources/js/admin/mobile/__tests__/AdminTheme.test.tsx b/resources/js/admin/mobile/__tests__/AdminTheme.test.tsx new file mode 100644 index 0000000..2d2c18c --- /dev/null +++ b/resources/js/admin/mobile/__tests__/AdminTheme.test.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { describe, expect, it } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { TamaguiProvider, Theme } from 'tamagui'; +import { useTheme } from '@tamagui/core'; +import tamaguiConfig from '../../../../../tamagui.config'; +import { useAdminTheme } from '../theme'; + +const ThemeProbe = () => { + const adminTheme = useAdminTheme(); + const theme = useTheme(); + + return ( +
+ ); +}; + +const renderWithTheme = (name: 'light' | 'dark') => + render( + + + + + + ); + +describe('useAdminTheme', () => { + it('tracks Tamagui theme values across light and dark modes', () => { + const { rerender } = renderWithTheme('light'); + const probe = screen.getByTestId('probe'); + const lightAdminBg = probe.getAttribute('data-admin-bg'); + const lightThemeBg = probe.getAttribute('data-theme-bg'); + const lightAdminText = probe.getAttribute('data-admin-text'); + const lightThemeText = probe.getAttribute('data-theme-text'); + + expect(lightAdminBg).toBe(lightThemeBg); + expect(lightAdminText).toBe(lightThemeText); + + rerender( + + + + + + ); + + const darkProbe = screen.getByTestId('probe'); + const darkAdminBg = darkProbe.getAttribute('data-admin-bg'); + const darkThemeBg = darkProbe.getAttribute('data-theme-bg'); + + expect(darkAdminBg).toBe(darkThemeBg); + expect(darkAdminBg).not.toBe(lightAdminBg); + }); +}); diff --git a/resources/js/admin/mobile/theme.ts b/resources/js/admin/mobile/theme.ts index dcec723..b4b2bc9 100644 --- a/resources/js/admin/mobile/theme.ts +++ b/resources/js/admin/mobile/theme.ts @@ -1,4 +1,4 @@ -import { useTheme } from '@tamagui/core'; +import { useTheme, useThemeName } from '@tamagui/core'; export const ADMIN_COLORS = { primary: '#4F46E5', // Indigo 600 @@ -85,54 +85,40 @@ export function withAlpha(color: string, alpha: number): string { return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha})`; } -function isDarkColor(color: string): boolean { - const rgb = parseRgb(color); - if (! rgb) { - return false; - } - - const luminance = (0.2126 * rgb.r + 0.7152 * rgb.g + 0.0722 * rgb.b) / 255; - return luminance < 0.5; -} - export function useAdminTheme() { const theme = useTheme(); + const themeName = useThemeName(); + const themeLabel = String(themeName ?? '').toLowerCase(); + const isDark = themeLabel.includes('dark') || themeLabel.includes('night'); - const cssValue = (name: string): string => { - if (typeof document === 'undefined') { - return ''; + const resolveThemeValue = (value: unknown, fallback: string): string => { + if (value === undefined || value === null || value === '') { + return fallback; } - return getComputedStyle(document.documentElement).getPropertyValue(name).trim(); + return String(value); }; - const cssBackground = cssValue('--background'); - const cssSurface = cssValue('--surface'); - const cssMuted = cssValue('--muted'); - const cssBorder = cssValue('--borderColor'); - const cssText = cssValue('--color'); - - const background = cssBackground || String(theme.background?.val ?? ADMIN_COLORS.surfaceMuted); - const isDark = isDarkColor(background); + const background = resolveThemeValue(theme.background?.val, ADMIN_COLORS.surfaceMuted); // Resolve core colors - const primary = String(theme.primary?.val ?? ADMIN_COLORS.primary); - const surface = cssSurface || String(theme.surface?.val ?? (isDark ? '#0F1B36' : ADMIN_COLORS.surface)); - const border = cssBorder || String(theme.borderColor?.val ?? (isDark ? '#1F2A4A' : ADMIN_COLORS.border)); - const text = cssText || String(theme.color?.val ?? (isDark ? '#F8FAFF' : ADMIN_COLORS.text)); + const primary = resolveThemeValue(theme.primary?.val, ADMIN_COLORS.primary); + const surface = resolveThemeValue(theme.surface?.val, isDark ? '#0F1B36' : ADMIN_COLORS.surface); + const border = resolveThemeValue(theme.borderColor?.val, isDark ? '#1F2A4A' : ADMIN_COLORS.border); + const text = resolveThemeValue(theme.color?.val, isDark ? '#F8FAFF' : ADMIN_COLORS.text); // Muted/Subtle should NOT use theme.muted (which is a background color in Tamagui standard) // Instead, we derive them from Text with opacity or use specific palette values if available // But safer is Alpha since it works in both Light (Dark Text) and Dark (Light Text) modes. const muted = withAlpha(text, 0.65); const subtle = withAlpha(text, 0.45); - const surfaceMuted = cssMuted || String(theme.muted?.val ?? (isDark ? '#121F3D' : ADMIN_COLORS.surfaceMuted)); + const surfaceMuted = resolveThemeValue(theme.muted?.val, isDark ? '#121F3D' : ADMIN_COLORS.surfaceMuted); const glassSurface = withAlpha(surface, isDark ? 0.90 : 0.85); const glassSurfaceStrong = withAlpha(surface, isDark ? 0.96 : 0.95); const glassBorder = withAlpha(border, 0.5); const glassShadow = isDark ? 'rgba(0, 0, 0, 0.55)' : 'rgba(15, 23, 42, 0.08)'; - const appBackground = isDark ? ADMIN_GRADIENTS.appBackgroundDark : ADMIN_GRADIENTS.appBackground; + const appBackground = `linear-gradient(180deg, ${background} 0%, ${isDark ? surface : surfaceMuted} 100%)`; return { theme, @@ -147,22 +133,22 @@ export function useAdminTheme() { muted, // Now properly derived from text color subtle, // Now properly derived from text color primary, - accent: String(theme.accent?.val ?? ADMIN_COLORS.accent), - accentSoft: String(theme.blue3?.val ?? ADMIN_COLORS.accentSoft), - accentStrong: String(theme.blue11?.val ?? ADMIN_COLORS.primaryStrong), - successBg: String(theme.backgroundStrong?.val ?? '#DCFCE7'), - successText: String(theme.green10?.val ?? ADMIN_COLORS.success), - dangerBg: String(theme.red3?.val ?? '#FEE2E2'), - dangerText: String(theme.red11?.val ?? ADMIN_COLORS.danger), - warningBg: String(theme.yellow3?.val ?? '#FEF3C7'), - warningBorder: String(theme.yellow6?.val ?? '#FCD34D'), - warningText: String(theme.yellow11?.val ?? ADMIN_COLORS.warning), - infoBg: String(theme.blue3?.val ?? ADMIN_COLORS.accentSoft), - infoText: String(theme.blue10?.val ?? ADMIN_COLORS.primaryStrong), - danger: String(theme.danger?.val ?? ADMIN_COLORS.danger), - backdrop: String(theme.backgroundStrong?.val ?? ADMIN_COLORS.backdrop), - overlay: withAlpha(ADMIN_COLORS.backdrop, 0.7), - shadow: String(theme.shadowColor?.val ?? 'rgba(15, 23, 42, 0.08)'), + accent: resolveThemeValue(theme.accent?.val, ADMIN_COLORS.accent), + accentSoft: resolveThemeValue(theme.blue3?.val, ADMIN_COLORS.accentSoft), + accentStrong: resolveThemeValue(theme.blue11?.val, ADMIN_COLORS.primaryStrong), + successBg: resolveThemeValue(theme.backgroundStrong?.val, '#DCFCE7'), + successText: resolveThemeValue(theme.green10?.val, ADMIN_COLORS.success), + dangerBg: resolveThemeValue(theme.red3?.val, '#FEE2E2'), + dangerText: resolveThemeValue(theme.red11?.val, ADMIN_COLORS.danger), + warningBg: resolveThemeValue(theme.yellow3?.val, '#FEF3C7'), + warningBorder: resolveThemeValue(theme.yellow6?.val, '#FCD34D'), + warningText: resolveThemeValue(theme.yellow11?.val, ADMIN_COLORS.warning), + infoBg: resolveThemeValue(theme.blue3?.val, ADMIN_COLORS.accentSoft), + infoText: resolveThemeValue(theme.blue10?.val, ADMIN_COLORS.primaryStrong), + danger: resolveThemeValue(theme.danger?.val, ADMIN_COLORS.danger), + backdrop: resolveThemeValue(theme.backgroundStrong?.val, ADMIN_COLORS.backdrop), + overlay: withAlpha(resolveThemeValue(theme.backgroundStrong?.val, ADMIN_COLORS.backdrop), 0.7), + shadow: resolveThemeValue(theme.shadowColor?.val, 'rgba(15, 23, 42, 0.08)'), glassSurface, glassSurfaceStrong, glassBorder, diff --git a/resources/js/guest-v2/App.tsx b/resources/js/guest-v2/App.tsx index d9cbce0..e15018b 100644 --- a/resources/js/guest-v2/App.tsx +++ b/resources/js/guest-v2/App.tsx @@ -10,7 +10,7 @@ import ToastHost from './components/ToastHost'; export default function App() { return ( - + diff --git a/resources/js/setupTests.ts b/resources/js/setupTests.ts index f6da0bb..ba7daeb 100644 --- a/resources/js/setupTests.ts +++ b/resources/js/setupTests.ts @@ -20,3 +20,19 @@ vi.mock('react-i18next', async () => { Trans: ({ children }: { children: React.ReactNode }) => children, }; }); + +if (typeof window !== 'undefined' && !window.matchMedia) { + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: (query: string) => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + }), + }); +} diff --git a/tamagui.config.ts b/tamagui.config.ts index 555e427..d4ae76f 100644 --- a/tamagui.config.ts +++ b/tamagui.config.ts @@ -178,6 +178,10 @@ const fonts = { const config = createTamagui({ ...defaultConfig, + settings: { + ...(defaultConfig.settings ?? {}), + addThemeClassName: 'html', + }, animations: createAnimations({ fast: 'cubic-bezier(0.2, 0.7, 0.2, 1) 150ms', medium: 'cubic-bezier(0.2, 0.7, 0.2, 1) 250ms',