diff --git a/packages/admin/index.html b/packages/admin/index.html index 74ed41f..cc7e081 100644 --- a/packages/admin/index.html +++ b/packages/admin/index.html @@ -6,6 +6,14 @@ Forte Admin +
diff --git a/packages/admin/src/app.css b/packages/admin/src/app.css index ce76e3c..c483157 100644 --- a/packages/admin/src/app.css +++ b/packages/admin/src/app.css @@ -1,94 +1,29 @@ @import "tailwindcss"; -@custom-variant dark (&:is(.dark *)); +@variant dark (&:is(.dark, .dark *)); @theme { - --color-background: hsl(0 0% 100%); - --color-foreground: hsl(240 10% 3.9%); + /* Defaults — overridden at runtime by theme store */ + --color-background: hsl(210 20% 97%); + --color-foreground: hsl(222 47% 11%); --color-card: hsl(0 0% 100%); - --color-card-foreground: hsl(240 10% 3.9%); + --color-card-foreground: hsl(222 47% 11%); --color-popover: hsl(0 0% 100%); - --color-popover-foreground: hsl(240 10% 3.9%); - --color-primary: hsl(240 5.9% 10%); - --color-primary-foreground: hsl(0 0% 98%); - --color-secondary: hsl(240 4.8% 95.9%); - --color-secondary-foreground: hsl(240 5.9% 10%); - --color-muted: hsl(240 4.8% 95.9%); - --color-muted-foreground: hsl(240 3.8% 46.1%); - --color-accent: hsl(240 4.8% 95.9%); - --color-accent-foreground: hsl(240 5.9% 10%); - --color-destructive: hsl(0 84.2% 60.2%); - --color-destructive-foreground: hsl(0 0% 98%); - --color-border: hsl(240 5.9% 90%); - --color-input: hsl(240 5.9% 90%); - --color-ring: hsl(240 5.9% 10%); - --color-sidebar: hsl(0 0% 98%); - --color-sidebar-foreground: hsl(240 5.3% 26.1%); - --color-sidebar-primary: hsl(240 5.9% 10%); - --color-sidebar-primary-foreground: hsl(0 0% 98%); - --color-sidebar-accent: hsl(240 4.8% 95.9%); - --color-sidebar-accent-foreground: hsl(240 5.9% 10%); - --color-sidebar-border: hsl(220 13% 91%); - --color-sidebar-ring: hsl(217.2 91.2% 59.8%); - --radius: 0.625rem; -} - -.dark { - --color-background: hsl(240 10% 3.9%); - --color-foreground: hsl(0 0% 98%); - --color-card: hsl(240 10% 3.9%); - --color-card-foreground: hsl(0 0% 98%); - --color-popover: hsl(240 10% 3.9%); - --color-popover-foreground: hsl(0 0% 98%); - --color-primary: hsl(0 0% 98%); - --color-primary-foreground: hsl(240 5.9% 10%); - --color-secondary: hsl(240 3.7% 15.9%); - --color-secondary-foreground: hsl(0 0% 98%); - --color-muted: hsl(240 3.7% 15.9%); - --color-muted-foreground: hsl(240 5% 64.9%); - --color-accent: hsl(240 3.7% 15.9%); - --color-accent-foreground: hsl(0 0% 98%); - --color-destructive: hsl(0 62.8% 30.6%); - --color-destructive-foreground: hsl(0 0% 98%); - --color-border: hsl(240 3.7% 15.9%); - --color-input: hsl(240 3.7% 15.9%); - --color-ring: hsl(240 4.9% 83.9%); - --color-sidebar: hsl(240 5.9% 10%); - --color-sidebar-foreground: hsl(240 4.8% 95.9%); - --color-sidebar-primary: hsl(224.3 76.3% 48%); - --color-sidebar-primary-foreground: hsl(0 0% 100%); - --color-sidebar-accent: hsl(240 3.7% 15.9%); - --color-sidebar-accent-foreground: hsl(240 4.8% 95.9%); - --color-sidebar-border: hsl(240 3.7% 15.9%); - --color-sidebar-ring: hsl(217.2 91.2% 59.8%); - --sidebar: hsl(240 5.9% 10%); - --sidebar-foreground: hsl(240 4.8% 95.9%); - --sidebar-primary: hsl(224.3 76.3% 48%); - --sidebar-primary-foreground: hsl(0 0% 100%); - --sidebar-accent: hsl(240 3.7% 15.9%); - --sidebar-accent-foreground: hsl(240 4.8% 95.9%); - --sidebar-border: hsl(240 3.7% 15.9%); - --sidebar-ring: hsl(217.2 91.2% 59.8%); -} - -body { - font-family: - "Inter", - ui-sans-serif, - system-ui, - -apple-system, - sans-serif; -} - -:root { - --sidebar: hsl(0 0% 98%); - --sidebar-foreground: hsl(240 5.3% 26.1%); - --sidebar-primary: hsl(240 5.9% 10%); - --sidebar-primary-foreground: hsl(0 0% 98%); - --sidebar-accent: hsl(240 4.8% 95.9%); - --sidebar-accent-foreground: hsl(240 5.9% 10%); - --sidebar-border: hsl(220 13% 91%); - --sidebar-ring: hsl(217.2 91.2% 59.8%); + --color-popover-foreground: hsl(222 47% 11%); + --color-primary: hsl(215 25% 27%); + --color-primary-foreground: hsl(210 40% 98%); + --color-secondary: hsl(210 40% 94%); + --color-secondary-foreground: hsl(222 47% 11%); + --color-muted: hsl(210 40% 94%); + --color-muted-foreground: hsl(215 16% 47%); + --color-accent: hsl(210 40% 94%); + --color-accent-foreground: hsl(222 47% 11%); + --color-destructive: hsl(0 72% 51%); + --color-destructive-foreground: hsl(210 40% 98%); + --color-border: hsl(214 32% 89%); + --color-input: hsl(214 32% 89%); + --color-ring: hsl(215 25% 27%); + --radius: 0.5rem; } @theme inline { @@ -101,3 +36,38 @@ body { --color-sidebar-border: var(--sidebar-border); --color-sidebar-ring: var(--sidebar-ring); } + +:root { + --sidebar: hsl(210 25% 95%); + --sidebar-foreground: hsl(215 16% 47%); + --sidebar-primary: hsl(215 25% 27%); + --sidebar-primary-foreground: hsl(210 40% 98%); + --sidebar-accent: hsl(210 40% 92%); + --sidebar-accent-foreground: hsl(222 47% 11%); + --sidebar-border: hsl(214 32% 89%); + --sidebar-ring: hsl(215 25% 27%); +} + +body { + font-family: + "Inter", + ui-sans-serif, + system-ui, + -apple-system, + sans-serif; +} + +.login-input { + background-color: #1a2740 !important; + border-color: #2a3a52 !important; + color: #d0d8e0 !important; +} + +.login-input:-webkit-autofill, +.login-input:-webkit-autofill:hover, +.login-input:-webkit-autofill:focus { + -webkit-box-shadow: 0 0 0 1000px #1a2740 inset !important; + -webkit-text-fill-color: #d0d8e0 !important; + border-color: #2a3a52 !important; + transition: background-color 5000s ease-in-out 0s; +} diff --git a/packages/admin/src/components/ui/button.tsx b/packages/admin/src/components/ui/button.tsx index 4d38506..508a878 100644 --- a/packages/admin/src/components/ui/button.tsx +++ b/packages/admin/src/components/ui/button.tsx @@ -9,7 +9,7 @@ const buttonVariants = cva( { variants: { variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/90", + default: "bg-primary text-primary-foreground hover:bg-primary/90 dark:bg-transparent dark:border dark:border-primary dark:text-primary dark:hover:bg-primary/10", destructive: "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40", outline: diff --git a/packages/admin/src/components/ui/input.tsx b/packages/admin/src/components/ui/input.tsx index f1124ae..f5a6f1d 100644 --- a/packages/admin/src/components/ui/input.tsx +++ b/packages/admin/src/components/ui/input.tsx @@ -8,9 +8,9 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) { type={type} data-slot="input" className={cn( - "h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30", + "h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base text-foreground shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50", - "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40", + "aria-invalid:border-destructive aria-invalid:ring-destructive/20", className )} {...props} diff --git a/packages/admin/src/lib/api-client.ts b/packages/admin/src/lib/api-client.ts index 0fde8ce..be530e4 100644 --- a/packages/admin/src/lib/api-client.ts +++ b/packages/admin/src/lib/api-client.ts @@ -31,7 +31,8 @@ async function request(method: string, path: string, body?: unknown): Promise if (res.status === 401) { useAuthStore.getState().logout() - window.location.href = '/login' + // Don't use window.location — that causes a full reload and flash + // The router's beforeLoad guard will redirect to /login on next navigation throw new ApiError('Unauthorized', 401) } diff --git a/packages/admin/src/lib/themes.ts b/packages/admin/src/lib/themes.ts new file mode 100644 index 0000000..0f986fe --- /dev/null +++ b/packages/admin/src/lib/themes.ts @@ -0,0 +1,379 @@ +export interface ThemeColors { + background: string + foreground: string + card: string + cardForeground: string + popover: string + popoverForeground: string + primary: string + primaryForeground: string + secondary: string + secondaryForeground: string + muted: string + mutedForeground: string + accent: string + accentForeground: string + destructive: string + destructiveForeground: string + border: string + input: string + ring: string + sidebar: string + sidebarForeground: string + sidebarPrimary: string + sidebarPrimaryForeground: string + sidebarAccent: string + sidebarAccentForeground: string + sidebarBorder: string +} + +export interface ThemePreset { + name: string + label: string + light: ThemeColors + dark: ThemeColors +} + +export const themes: ThemePreset[] = [ + { + name: 'slate', + label: 'Slate', + light: { + background: '210 20% 97%', + foreground: '222 47% 11%', + card: '0 0% 100%', + cardForeground: '222 47% 11%', + popover: '0 0% 100%', + popoverForeground: '222 47% 11%', + primary: '215 25% 27%', + primaryForeground: '210 40% 98%', + secondary: '210 40% 94%', + secondaryForeground: '222 47% 11%', + muted: '210 40% 94%', + mutedForeground: '215 16% 47%', + accent: '210 40% 94%', + accentForeground: '222 47% 11%', + destructive: '0 72% 51%', + destructiveForeground: '210 40% 98%', + border: '214 32% 89%', + input: '214 32% 89%', + ring: '215 25% 27%', + sidebar: '210 25% 95%', + sidebarForeground: '215 16% 47%', + sidebarPrimary: '215 25% 27%', + sidebarPrimaryForeground: '210 40% 98%', + sidebarAccent: '210 40% 92%', + sidebarAccentForeground: '222 47% 11%', + sidebarBorder: '214 32% 89%', + }, + dark: { + background: '222 30% 8%', + foreground: '210 20% 86%', + card: '222 30% 10%', + cardForeground: '210 20% 86%', + popover: '222 30% 10%', + popoverForeground: '210 20% 86%', + primary: '215 20% 72%', + primaryForeground: '222 30% 8%', + secondary: '217 20% 15%', + secondaryForeground: '210 20% 86%', + muted: '217 20% 15%', + mutedForeground: '215 15% 55%', + accent: '217 20% 15%', + accentForeground: '210 20% 86%', + destructive: '0 55% 48%', + destructiveForeground: '210 20% 92%', + border: '217 20% 17%', + input: '217 20% 17%', + ring: '215 20% 55%', + sidebar: '222 30% 7%', + sidebarForeground: '215 15% 65%', + sidebarPrimary: '215 20% 72%', + sidebarPrimaryForeground: '0 0% 100%', + sidebarAccent: '217 20% 13%', + sidebarAccentForeground: '210 20% 86%', + sidebarBorder: '217 20% 15%', + }, + }, + { + name: 'emerald', + label: 'Emerald', + light: { + background: '150 20% 97%', + foreground: '150 40% 10%', + card: '0 0% 100%', + cardForeground: '150 40% 10%', + popover: '0 0% 100%', + popoverForeground: '150 40% 10%', + primary: '160 84% 29%', + primaryForeground: '150 20% 98%', + secondary: '150 20% 93%', + secondaryForeground: '150 40% 10%', + muted: '150 20% 93%', + mutedForeground: '150 10% 45%', + accent: '150 20% 93%', + accentForeground: '150 40% 10%', + destructive: '0 72% 51%', + destructiveForeground: '150 20% 98%', + border: '150 15% 88%', + input: '150 15% 88%', + ring: '160 84% 29%', + sidebar: '150 20% 95%', + sidebarForeground: '150 10% 45%', + sidebarPrimary: '160 84% 29%', + sidebarPrimaryForeground: '150 20% 98%', + sidebarAccent: '150 20% 91%', + sidebarAccentForeground: '150 40% 10%', + sidebarBorder: '150 15% 88%', + }, + dark: { + background: '155 30% 7%', + foreground: '150 15% 85%', + card: '155 30% 9%', + cardForeground: '150 15% 85%', + popover: '155 30% 9%', + popoverForeground: '150 15% 85%', + primary: '160 72% 42%', + primaryForeground: '155 30% 7%', + secondary: '155 20% 14%', + secondaryForeground: '150 15% 85%', + muted: '155 20% 14%', + mutedForeground: '150 10% 52%', + accent: '155 20% 14%', + accentForeground: '150 15% 85%', + destructive: '0 55% 48%', + destructiveForeground: '150 15% 92%', + border: '155 20% 16%', + input: '155 20% 16%', + ring: '160 72% 42%', + sidebar: '155 30% 6%', + sidebarForeground: '150 10% 62%', + sidebarPrimary: '160 72% 42%', + sidebarPrimaryForeground: '0 0% 100%', + sidebarAccent: '155 20% 12%', + sidebarAccentForeground: '150 15% 85%', + sidebarBorder: '155 20% 14%', + }, + }, + { + name: 'violet', + label: 'Violet', + light: { + background: '260 15% 97%', + foreground: '260 40% 11%', + card: '0 0% 100%', + cardForeground: '260 40% 11%', + popover: '0 0% 100%', + popoverForeground: '260 40% 11%', + primary: '262 83% 58%', + primaryForeground: '260 15% 98%', + secondary: '260 20% 94%', + secondaryForeground: '260 40% 11%', + muted: '260 20% 94%', + mutedForeground: '260 10% 47%', + accent: '260 20% 94%', + accentForeground: '260 40% 11%', + destructive: '0 72% 51%', + destructiveForeground: '260 15% 98%', + border: '260 15% 89%', + input: '260 15% 89%', + ring: '262 83% 58%', + sidebar: '260 18% 95%', + sidebarForeground: '260 10% 47%', + sidebarPrimary: '262 83% 58%', + sidebarPrimaryForeground: '260 15% 98%', + sidebarAccent: '260 20% 92%', + sidebarAccentForeground: '260 40% 11%', + sidebarBorder: '260 15% 89%', + }, + dark: { + background: '260 30% 8%', + foreground: '260 15% 86%', + card: '260 30% 10%', + cardForeground: '260 15% 86%', + popover: '260 30% 10%', + popoverForeground: '260 15% 86%', + primary: '262 83% 65%', + primaryForeground: '260 30% 8%', + secondary: '260 20% 15%', + secondaryForeground: '260 15% 86%', + muted: '260 20% 15%', + mutedForeground: '260 10% 54%', + accent: '260 20% 15%', + accentForeground: '260 15% 86%', + destructive: '0 55% 48%', + destructiveForeground: '260 15% 92%', + border: '260 20% 17%', + input: '260 20% 17%', + ring: '262 83% 65%', + sidebar: '260 30% 7%', + sidebarForeground: '260 10% 64%', + sidebarPrimary: '262 83% 65%', + sidebarPrimaryForeground: '0 0% 100%', + sidebarAccent: '260 20% 13%', + sidebarAccentForeground: '260 15% 86%', + sidebarBorder: '260 20% 15%', + }, + }, + { + name: 'amber', + label: 'Amber', + light: { + background: '36 20% 97%', + foreground: '28 40% 10%', + card: '0 0% 100%', + cardForeground: '28 40% 10%', + popover: '0 0% 100%', + popoverForeground: '28 40% 10%', + primary: '25 95% 40%', + primaryForeground: '36 20% 98%', + secondary: '36 25% 93%', + secondaryForeground: '28 40% 10%', + muted: '36 25% 93%', + mutedForeground: '28 10% 47%', + accent: '36 25% 93%', + accentForeground: '28 40% 10%', + destructive: '0 72% 51%', + destructiveForeground: '36 20% 98%', + border: '36 20% 88%', + input: '36 20% 88%', + ring: '25 95% 40%', + sidebar: '36 22% 95%', + sidebarForeground: '28 10% 47%', + sidebarPrimary: '25 95% 40%', + sidebarPrimaryForeground: '36 20% 98%', + sidebarAccent: '36 25% 91%', + sidebarAccentForeground: '28 40% 10%', + sidebarBorder: '36 20% 88%', + }, + dark: { + background: '28 25% 7%', + foreground: '36 15% 85%', + card: '28 25% 9%', + cardForeground: '36 15% 85%', + popover: '28 25% 9%', + popoverForeground: '36 15% 85%', + primary: '30 90% 52%', + primaryForeground: '28 25% 7%', + secondary: '28 18% 14%', + secondaryForeground: '36 15% 85%', + muted: '28 18% 14%', + mutedForeground: '28 10% 52%', + accent: '28 18% 14%', + accentForeground: '36 15% 85%', + destructive: '0 55% 48%', + destructiveForeground: '36 15% 92%', + border: '28 18% 16%', + input: '28 18% 16%', + ring: '30 90% 52%', + sidebar: '28 25% 6%', + sidebarForeground: '28 10% 62%', + sidebarPrimary: '30 90% 52%', + sidebarPrimaryForeground: '0 0% 100%', + sidebarAccent: '28 18% 12%', + sidebarAccentForeground: '36 15% 85%', + sidebarBorder: '28 18% 14%', + }, + }, + { + name: 'rose', + label: 'Rose', + light: { + background: '350 15% 97%', + foreground: '350 40% 10%', + card: '0 0% 100%', + cardForeground: '350 40% 10%', + popover: '0 0% 100%', + popoverForeground: '350 40% 10%', + primary: '347 77% 50%', + primaryForeground: '350 15% 98%', + secondary: '350 20% 94%', + secondaryForeground: '350 40% 10%', + muted: '350 20% 94%', + mutedForeground: '350 10% 47%', + accent: '350 20% 94%', + accentForeground: '350 40% 10%', + destructive: '0 72% 51%', + destructiveForeground: '350 15% 98%', + border: '350 15% 89%', + input: '350 15% 89%', + ring: '347 77% 50%', + sidebar: '350 18% 95%', + sidebarForeground: '350 10% 47%', + sidebarPrimary: '347 77% 50%', + sidebarPrimaryForeground: '350 15% 98%', + sidebarAccent: '350 20% 92%', + sidebarAccentForeground: '350 40% 10%', + sidebarBorder: '350 15% 89%', + }, + dark: { + background: '350 25% 7%', + foreground: '350 12% 86%', + card: '350 25% 9%', + cardForeground: '350 12% 86%', + popover: '350 25% 9%', + popoverForeground: '350 12% 86%', + primary: '347 77% 58%', + primaryForeground: '350 25% 7%', + secondary: '350 18% 14%', + secondaryForeground: '350 12% 86%', + muted: '350 18% 14%', + mutedForeground: '350 10% 54%', + accent: '350 18% 14%', + accentForeground: '350 12% 86%', + destructive: '0 55% 48%', + destructiveForeground: '350 12% 92%', + border: '350 18% 16%', + input: '350 18% 16%', + ring: '347 77% 58%', + sidebar: '350 25% 6%', + sidebarForeground: '350 10% 62%', + sidebarPrimary: '347 77% 58%', + sidebarPrimaryForeground: '0 0% 100%', + sidebarAccent: '350 18% 12%', + sidebarAccentForeground: '350 12% 86%', + sidebarBorder: '350 18% 14%', + }, + }, +] + +const CSS_VAR_MAP: Record = { + background: '--color-background', + foreground: '--color-foreground', + card: '--color-card', + cardForeground: '--color-card-foreground', + popover: '--color-popover', + popoverForeground: '--color-popover-foreground', + primary: '--color-primary', + primaryForeground: '--color-primary-foreground', + secondary: '--color-secondary', + secondaryForeground: '--color-secondary-foreground', + muted: '--color-muted', + mutedForeground: '--color-muted-foreground', + accent: '--color-accent', + accentForeground: '--color-accent-foreground', + destructive: '--color-destructive', + destructiveForeground: '--color-destructive-foreground', + border: '--color-border', + input: '--color-input', + ring: '--color-ring', + sidebar: '--sidebar', + sidebarForeground: '--sidebar-foreground', + sidebarPrimary: '--sidebar-primary', + sidebarPrimaryForeground: '--sidebar-primary-foreground', + sidebarAccent: '--sidebar-accent', + sidebarAccentForeground: '--sidebar-accent-foreground', + sidebarBorder: '--sidebar-border', +} + +export function applyThemeColors(colors: ThemeColors) { + const root = document.documentElement + for (const [key, cssVar] of Object.entries(CSS_VAR_MAP)) { + const value = colors[key as keyof ThemeColors] + root.style.setProperty(cssVar, `hsl(${value})`) + } +} + +export function getThemeByName(name: string): ThemePreset { + return themes.find((t) => t.name === name) ?? themes[0] +} diff --git a/packages/admin/src/routes/_authenticated.tsx b/packages/admin/src/routes/_authenticated.tsx index 9d25a12..61e49c5 100644 --- a/packages/admin/src/routes/_authenticated.tsx +++ b/packages/admin/src/routes/_authenticated.tsx @@ -1,5 +1,20 @@ -import { createFileRoute, Outlet, redirect } from '@tanstack/react-router' +import { createFileRoute, Outlet, Link, redirect, useRouter } from '@tanstack/react-router' import { useAuthStore } from '@/stores/auth.store' +import { useThemeStore } from '@/stores/theme.store' +import { themes } from '@/lib/themes' +import { Button } from '@/components/ui/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +} from '@/components/ui/dropdown-menu' +import { Users, Sun, Moon, Monitor, LogOut, User, Palette } from 'lucide-react' export const Route = createFileRoute('/_authenticated')({ beforeLoad: () => { @@ -11,15 +26,102 @@ export const Route = createFileRoute('/_authenticated')({ component: AuthenticatedLayout, }) +function ModeIcon() { + const mode = useThemeStore((s) => s.mode) + if (mode === 'dark') return + if (mode === 'light') return + return +} + function AuthenticatedLayout() { + const router = useRouter() + const user = useAuthStore((s) => s.user) + const logout = useAuthStore((s) => s.logout) + const { mode, colorTheme, setMode, setColorTheme } = useThemeStore() + + function handleLogout() { + logout() + router.navigate({ to: '/login', replace: true }) + } + return (
-