feat: Add comprehensive theme management with mode and color selectors, alongside new fonts.

This commit is contained in:
2025-11-27 23:31:10 -05:00
parent 0809f75673
commit 10e1ca8396
6 changed files with 188 additions and 9 deletions

View File

@@ -0,0 +1,54 @@
"use client";
import { useTheme } from "next-themes";
import { Check, Palette } from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "~/components/ui/dropdown-menu";
import { Button } from "~/components/ui/button";
export function ColorThemeSelector() {
const { theme, setTheme } = useTheme();
const themes = [
{ name: "theme-ocean", label: "Ocean" },
{ name: "theme-sunset", label: "Sunset" },
{ name: "theme-forest", label: "Forest" },
];
const currentTheme = themes.find((t) => t.name === theme)?.label ?? "Ocean";
return (
<div className="flex items-center justify-between">
<div className="space-y-1.5">
<label className="font-medium">Color Theme</label>
<p className="text-muted-foreground text-xs leading-snug">
Select a color theme for the application.
</p>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="w-40 justify-between">
<span>{currentTheme}</span>
<Palette className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
{themes.map((t) => (
<DropdownMenuItem
key={t.name}
className="flex justify-between"
onClick={() => setTheme(t.name)}
>
<span>{t.label}</span>
{theme === t.name && <Check className="h-4 w-4" />}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}

View File

@@ -0,0 +1,33 @@
"use client";
import { useTheme } from "next-themes";
import { Sun, Moon, Laptop } from "lucide-react";
import { Tabs, TabsList, TabsTrigger } from "~/components/ui/tabs";
export function ModeSwitcher() {
const { theme, setTheme } = useTheme();
return (
<div className="flex items-center justify-between">
<div className="space-y-1.5">
<label className="font-medium">Appearance</label>
<p className="text-muted-foreground text-xs leading-snug">
Select a light or dark mode, or sync with your system.
</p>
</div>
<Tabs defaultValue={theme} onValueChange={setTheme} className="w-auto">
<TabsList>
<TabsTrigger value="light">
<Sun className="h-4 w-4" />
</TabsTrigger>
<TabsTrigger value="dark">
<Moon className="h-4 w-4" />
</TabsTrigger>
<TabsTrigger value="system">
<Laptop className="h-4 w-4" />
</TabsTrigger>
</TabsList>
</Tabs>
</div>
);
}

View File

@@ -62,7 +62,8 @@ import { Textarea } from "~/components/ui/textarea";
import { api } from "~/trpc/react";
import { Switch } from "~/components/ui/switch";
import { Slider } from "~/components/ui/slider";
import { ThemeSelector } from "./theme-selector";
import { ModeSwitcher } from "./mode-switcher";
import { ColorThemeSelector } from "./color-theme-selector";
import { useAnimationPreferences } from "~/components/providers/animation-preferences-provider";
export function SettingsContent() {
@@ -628,8 +629,9 @@ export function SettingsContent() {
Customize the look and feel of the application
</CardDescription>
</CardHeader>
<CardContent>
<ThemeSelector />
<CardContent className="space-y-6">
<ModeSwitcher />
<ColorThemeSelector />
</CardContent>
</Card>

View File

@@ -2,7 +2,7 @@ import "~/styles/globals.css";
import { Analytics } from "@vercel/analytics/next";
import { type Metadata } from "next";
import { Geist_Mono } from "next/font/google";
import { Geist, Geist_Mono, Instrument_Serif } from "next/font/google";
import { TRPCReactProvider } from "~/trpc/react";
import { Toaster } from "~/components/ui/sonner";
@@ -17,17 +17,34 @@ export const metadata: Metadata = {
icons: [{ rel: "icon", url: "/favicon.ico" }],
};
const geistSans = Geist({
subsets: ["latin"],
variable: "--font-geist-sans",
display: "swap",
});
const geistMono = Geist_Mono({
subsets: ["latin"],
variable: "--font-geist-mono",
display: "swap",
});
const instrumentSerif = Instrument_Serif({
subsets: ["latin"],
variable: "--font-serif",
display: "swap",
weight: "400",
});
export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html suppressHydrationWarning lang="en" className={geistMono.variable}>
<html
suppressHydrationWarning
lang="en"
className={`${geistSans.variable} ${geistMono.variable} ${instrumentSerif.variable}`}
>
<head>
{/* Inline early animation preference script to avoid FOUC */}
<script
@@ -40,7 +57,7 @@ export default function RootLayout({
<body className="bg-background text-foreground relative min-h-screen overflow-x-hidden font-sans antialiased">
<ThemeProvider
attribute="class"
defaultTheme="system"
defaultTheme="theme-ocean"
enableSystem
disableTransitionOnChange
>

View File

@@ -8,7 +8,7 @@ export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return (
<NextThemesProvider
{...props}
themes={["light", "dark", "theme-sunset", "theme-forest"]}
themes={["light", "dark", "theme-ocean", "theme-sunset", "theme-forest"]}
>
{children}
</NextThemesProvider>

View File

@@ -1,8 +1,7 @@
@import "tailwindcss";
@import "tw-animate-css";
:root {
--background: oklch(0.98 0.01 230);
.theme-ocean {
--foreground: oklch(0.2 0.03 230);
--card: oklch(1 0 0);
--card-foreground: oklch(0.2 0.03 230);
@@ -240,6 +239,80 @@
--navbar-border: oklch(0.88 0.02 140);
}
.dark .theme-sunset {
--background: oklch(0.15 0.05 40);
--foreground: oklch(0.9 0.05 40);
--card: oklch(0.2 0.05 40);
--card-foreground: oklch(0.9 0.05 40);
--popover: oklch(0.22 0.05 40);
--popover-foreground: oklch(0.9 0.05 40);
--primary: oklch(0.7 0.2 50);
--primary-foreground: oklch(0.1 0.05 40);
--secondary: oklch(0.25 0.05 40);
--secondary-foreground: oklch(0.9 0.05 40);
--muted: oklch(0.25 0.05 40);
--muted-foreground: oklch(0.7 0.05 40);
--accent: oklch(0.3 0.05 40);
--accent-foreground: oklch(0.9 0.05 40);
--destructive: oklch(0.7 0.19 22);
--destructive-foreground: oklch(0.2 0.05 40);
--success: oklch(0.6 0.15 142);
--success-foreground: oklch(0.98 0.01 140);
--warning: oklch(0.7 0.15 38);
--warning-foreground: oklch(0.2 0.05 40);
--border: oklch(0.28 0.05 40);
--input: oklch(0.35 0.05 40);
--ring: oklch(0.7 0.2 50);
--sidebar: oklch(0.1 0.05 40);
--sidebar-foreground: oklch(0.9 0.05 40);
--sidebar-primary: oklch(0.9 0.05 40);
--sidebar-primary-foreground: oklch(0.1 0.05 40);
--sidebar-accent: oklch(0.2 0.05 40);
--sidebar-accent-foreground: oklch(0.9 0.05 40);
--sidebar-border: oklch(0.25 0.05 40);
--sidebar-ring: oklch(0.35 0.05 40);
--navbar: oklch(0.1 0.05 40);
--navbar-foreground: oklch(0.9 0.05 40);
--navbar-border: oklch(0.25 0.05 40);
}
.dark .theme-forest {
--background: oklch(0.15 0.05 140);
--foreground: oklch(0.9 0.05 140);
--card: oklch(0.2 0.05 140);
--card-foreground: oklch(0.9 0.05 140);
--popover: oklch(0.22 0.05 140);
--popover-foreground: oklch(0.9 0.05 140);
--primary: oklch(0.5 0.1 150);
--primary-foreground: oklch(0.1 0.05 140);
--secondary: oklch(0.25 0.05 140);
--secondary-foreground: oklch(0.9 0.05 140);
--muted: oklch(0.25 0.05 140);
--muted-foreground: oklch(0.7 0.05 140);
--accent: oklch(0.3 0.05 140);
--accent-foreground: oklch(0.9 0.05 140);
--destructive: oklch(0.7 0.19 22);
--destructive-foreground: oklch(0.2 0.05 140);
--success: oklch(0.6 0.15 142);
--success-foreground: oklch(0.98 0.01 140);
--warning: oklch(0.7 0.15 38);
--warning-foreground: oklch(0.2 0.05 140);
--border: oklch(0.28 0.05 140);
--input: oklch(0.35 0.05 140);
--ring: oklch(0.5 0.1 150);
--sidebar: oklch(0.1 0.05 140);
--sidebar-foreground: oklch(0.9 0.05 140);
--sidebar-primary: oklch(0.9 0.05 140);
--sidebar-primary-foreground: oklch(0.1 0.05 140);
--sidebar-accent: oklch(0.2 0.05 140);
--sidebar-accent-foreground: oklch(0.9 0.05 140);
--sidebar-border: oklch(0.25 0.05 140);
--sidebar-ring: oklch(0.35 0.05 140);
--navbar: oklch(0.1 0.05 140);
--navbar-foreground: oklch(0.9 0.05 140);
--navbar-border: oklch(0.25 0.05 140);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);