Align admin theme with Tamagui v2
This commit is contained in:
@@ -3749,6 +3749,10 @@ var fonts2 = {
|
|||||||
};
|
};
|
||||||
var config = (0, import_core2.createTamagui)({
|
var config = (0, import_core2.createTamagui)({
|
||||||
...defaultConfig,
|
...defaultConfig,
|
||||||
|
settings: {
|
||||||
|
...defaultConfig.settings ?? {},
|
||||||
|
addThemeClassName: "html"
|
||||||
|
},
|
||||||
animations: createAnimations({
|
animations: createAnimations({
|
||||||
fast: "cubic-bezier(0.2, 0.7, 0.2, 1) 150ms",
|
fast: "cubic-bezier(0.2, 0.7, 0.2, 1) 150ms",
|
||||||
medium: "cubic-bezier(0.2, 0.7, 0.2, 1) 250ms",
|
medium: "cubic-bezier(0.2, 0.7, 0.2, 1) 250ms",
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ function AdminApp() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TamaguiProvider config={tamaguiConfig} defaultTheme={themeName} themeClassNameOnRoot>
|
<TamaguiProvider config={tamaguiConfig} defaultTheme={themeName}>
|
||||||
<Theme name={themeName}>
|
<Theme name={themeName}>
|
||||||
<ConsentProvider>
|
<ConsentProvider>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
@@ -86,7 +86,10 @@ function AdminApp() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="bg-[#FFF1E8] font-[Manrope] text-[14px] font-normal leading-[1.6] text-[#0B132B] dark:bg-[#0B132B] dark:text-slate-100">
|
<div
|
||||||
|
className="font-[Manrope] text-[14px] font-normal leading-[1.6]"
|
||||||
|
style={{ backgroundColor: 'var(--background)', color: 'var(--color)' }}
|
||||||
|
>
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</div>
|
</div>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
60
resources/js/admin/mobile/__tests__/AdminTheme.test.tsx
Normal file
60
resources/js/admin/mobile/__tests__/AdminTheme.test.tsx
Normal file
@@ -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 (
|
||||||
|
<div
|
||||||
|
data-testid="probe"
|
||||||
|
data-admin-bg={adminTheme.background}
|
||||||
|
data-theme-bg={String(theme.background?.val ?? '')}
|
||||||
|
data-admin-text={adminTheme.text}
|
||||||
|
data-theme-text={String(theme.color?.val ?? '')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderWithTheme = (name: 'light' | 'dark') =>
|
||||||
|
render(
|
||||||
|
<TamaguiProvider config={tamaguiConfig} defaultTheme={name}>
|
||||||
|
<Theme name={name}>
|
||||||
|
<ThemeProbe />
|
||||||
|
</Theme>
|
||||||
|
</TamaguiProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<TamaguiProvider config={tamaguiConfig} defaultTheme="dark">
|
||||||
|
<Theme name="dark">
|
||||||
|
<ThemeProbe />
|
||||||
|
</Theme>
|
||||||
|
</TamaguiProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useTheme } from '@tamagui/core';
|
import { useTheme, useThemeName } from '@tamagui/core';
|
||||||
|
|
||||||
export const ADMIN_COLORS = {
|
export const ADMIN_COLORS = {
|
||||||
primary: '#4F46E5', // Indigo 600
|
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})`;
|
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() {
|
export function useAdminTheme() {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const themeName = useThemeName();
|
||||||
|
const themeLabel = String(themeName ?? '').toLowerCase();
|
||||||
|
const isDark = themeLabel.includes('dark') || themeLabel.includes('night');
|
||||||
|
|
||||||
const cssValue = (name: string): string => {
|
const resolveThemeValue = (value: unknown, fallback: string): string => {
|
||||||
if (typeof document === 'undefined') {
|
if (value === undefined || value === null || value === '') {
|
||||||
return '';
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
return String(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const cssBackground = cssValue('--background');
|
const background = resolveThemeValue(theme.background?.val, ADMIN_COLORS.surfaceMuted);
|
||||||
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);
|
|
||||||
|
|
||||||
// Resolve core colors
|
// Resolve core colors
|
||||||
const primary = String(theme.primary?.val ?? ADMIN_COLORS.primary);
|
const primary = resolveThemeValue(theme.primary?.val, ADMIN_COLORS.primary);
|
||||||
const surface = cssSurface || String(theme.surface?.val ?? (isDark ? '#0F1B36' : ADMIN_COLORS.surface));
|
const surface = resolveThemeValue(theme.surface?.val, isDark ? '#0F1B36' : ADMIN_COLORS.surface);
|
||||||
const border = cssBorder || String(theme.borderColor?.val ?? (isDark ? '#1F2A4A' : ADMIN_COLORS.border));
|
const border = resolveThemeValue(theme.borderColor?.val, isDark ? '#1F2A4A' : ADMIN_COLORS.border);
|
||||||
const text = cssText || String(theme.color?.val ?? (isDark ? '#F8FAFF' : ADMIN_COLORS.text));
|
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)
|
// 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
|
// 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.
|
// But safer is Alpha since it works in both Light (Dark Text) and Dark (Light Text) modes.
|
||||||
const muted = withAlpha(text, 0.65);
|
const muted = withAlpha(text, 0.65);
|
||||||
const subtle = withAlpha(text, 0.45);
|
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 glassSurface = withAlpha(surface, isDark ? 0.90 : 0.85);
|
||||||
const glassSurfaceStrong = withAlpha(surface, isDark ? 0.96 : 0.95);
|
const glassSurfaceStrong = withAlpha(surface, isDark ? 0.96 : 0.95);
|
||||||
const glassBorder = withAlpha(border, 0.5);
|
const glassBorder = withAlpha(border, 0.5);
|
||||||
const glassShadow = isDark ? 'rgba(0, 0, 0, 0.55)' : 'rgba(15, 23, 42, 0.08)';
|
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 {
|
return {
|
||||||
theme,
|
theme,
|
||||||
@@ -147,22 +133,22 @@ export function useAdminTheme() {
|
|||||||
muted, // Now properly derived from text color
|
muted, // Now properly derived from text color
|
||||||
subtle, // Now properly derived from text color
|
subtle, // Now properly derived from text color
|
||||||
primary,
|
primary,
|
||||||
accent: String(theme.accent?.val ?? ADMIN_COLORS.accent),
|
accent: resolveThemeValue(theme.accent?.val, ADMIN_COLORS.accent),
|
||||||
accentSoft: String(theme.blue3?.val ?? ADMIN_COLORS.accentSoft),
|
accentSoft: resolveThemeValue(theme.blue3?.val, ADMIN_COLORS.accentSoft),
|
||||||
accentStrong: String(theme.blue11?.val ?? ADMIN_COLORS.primaryStrong),
|
accentStrong: resolveThemeValue(theme.blue11?.val, ADMIN_COLORS.primaryStrong),
|
||||||
successBg: String(theme.backgroundStrong?.val ?? '#DCFCE7'),
|
successBg: resolveThemeValue(theme.backgroundStrong?.val, '#DCFCE7'),
|
||||||
successText: String(theme.green10?.val ?? ADMIN_COLORS.success),
|
successText: resolveThemeValue(theme.green10?.val, ADMIN_COLORS.success),
|
||||||
dangerBg: String(theme.red3?.val ?? '#FEE2E2'),
|
dangerBg: resolveThemeValue(theme.red3?.val, '#FEE2E2'),
|
||||||
dangerText: String(theme.red11?.val ?? ADMIN_COLORS.danger),
|
dangerText: resolveThemeValue(theme.red11?.val, ADMIN_COLORS.danger),
|
||||||
warningBg: String(theme.yellow3?.val ?? '#FEF3C7'),
|
warningBg: resolveThemeValue(theme.yellow3?.val, '#FEF3C7'),
|
||||||
warningBorder: String(theme.yellow6?.val ?? '#FCD34D'),
|
warningBorder: resolveThemeValue(theme.yellow6?.val, '#FCD34D'),
|
||||||
warningText: String(theme.yellow11?.val ?? ADMIN_COLORS.warning),
|
warningText: resolveThemeValue(theme.yellow11?.val, ADMIN_COLORS.warning),
|
||||||
infoBg: String(theme.blue3?.val ?? ADMIN_COLORS.accentSoft),
|
infoBg: resolveThemeValue(theme.blue3?.val, ADMIN_COLORS.accentSoft),
|
||||||
infoText: String(theme.blue10?.val ?? ADMIN_COLORS.primaryStrong),
|
infoText: resolveThemeValue(theme.blue10?.val, ADMIN_COLORS.primaryStrong),
|
||||||
danger: String(theme.danger?.val ?? ADMIN_COLORS.danger),
|
danger: resolveThemeValue(theme.danger?.val, ADMIN_COLORS.danger),
|
||||||
backdrop: String(theme.backgroundStrong?.val ?? ADMIN_COLORS.backdrop),
|
backdrop: resolveThemeValue(theme.backgroundStrong?.val, ADMIN_COLORS.backdrop),
|
||||||
overlay: withAlpha(ADMIN_COLORS.backdrop, 0.7),
|
overlay: withAlpha(resolveThemeValue(theme.backgroundStrong?.val, ADMIN_COLORS.backdrop), 0.7),
|
||||||
shadow: String(theme.shadowColor?.val ?? 'rgba(15, 23, 42, 0.08)'),
|
shadow: resolveThemeValue(theme.shadowColor?.val, 'rgba(15, 23, 42, 0.08)'),
|
||||||
glassSurface,
|
glassSurface,
|
||||||
glassSurfaceStrong,
|
glassSurfaceStrong,
|
||||||
glassBorder,
|
glassBorder,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import ToastHost from './components/ToastHost';
|
|||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<TamaguiProvider config={tamaguiConfig} defaultTheme="guestLight" themeClassNameOnRoot>
|
<TamaguiProvider config={tamaguiConfig} defaultTheme="guestLight">
|
||||||
<AppearanceProvider>
|
<AppearanceProvider>
|
||||||
<ConsentProvider>
|
<ConsentProvider>
|
||||||
<AppThemeRouter />
|
<AppThemeRouter />
|
||||||
|
|||||||
@@ -20,3 +20,19 @@ vi.mock('react-i18next', async () => {
|
|||||||
Trans: ({ children }: { children: React.ReactNode }) => children,
|
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(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -178,6 +178,10 @@ const fonts = {
|
|||||||
|
|
||||||
const config = createTamagui({
|
const config = createTamagui({
|
||||||
...defaultConfig,
|
...defaultConfig,
|
||||||
|
settings: {
|
||||||
|
...(defaultConfig.settings ?? {}),
|
||||||
|
addThemeClassName: 'html',
|
||||||
|
},
|
||||||
animations: createAnimations({
|
animations: createAnimations({
|
||||||
fast: 'cubic-bezier(0.2, 0.7, 0.2, 1) 150ms',
|
fast: 'cubic-bezier(0.2, 0.7, 0.2, 1) 150ms',
|
||||||
medium: 'cubic-bezier(0.2, 0.7, 0.2, 1) 250ms',
|
medium: 'cubic-bezier(0.2, 0.7, 0.2, 1) 250ms',
|
||||||
|
|||||||
Reference in New Issue
Block a user