From 10e1ca83967b114888cd5a93442cf0e7f56f16c5 Mon Sep 17 00:00:00 2001 From: Sean O'Connor Date: Thu, 27 Nov 2025 23:31:10 -0500 Subject: [PATCH] feat: Add comprehensive theme management with mode and color selectors, alongside new fonts. --- .../_components/color-theme-selector.tsx | 54 +++++++++++++ .../settings/_components/mode-switcher.tsx | 33 ++++++++ .../settings/_components/settings-content.tsx | 8 +- src/app/layout.tsx | 23 +++++- src/components/providers/theme-provider.tsx | 2 +- src/styles/globals.css | 77 ++++++++++++++++++- 6 files changed, 188 insertions(+), 9 deletions(-) create mode 100644 src/app/dashboard/settings/_components/color-theme-selector.tsx create mode 100644 src/app/dashboard/settings/_components/mode-switcher.tsx diff --git a/src/app/dashboard/settings/_components/color-theme-selector.tsx b/src/app/dashboard/settings/_components/color-theme-selector.tsx new file mode 100644 index 0000000..ec0cb8a --- /dev/null +++ b/src/app/dashboard/settings/_components/color-theme-selector.tsx @@ -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 ( +
+
+ +

+ Select a color theme for the application. +

+
+ + + + + + {themes.map((t) => ( + setTheme(t.name)} + > + {t.label} + {theme === t.name && } + + ))} + + +
+ ); +} diff --git a/src/app/dashboard/settings/_components/mode-switcher.tsx b/src/app/dashboard/settings/_components/mode-switcher.tsx new file mode 100644 index 0000000..0cec1a5 --- /dev/null +++ b/src/app/dashboard/settings/_components/mode-switcher.tsx @@ -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 ( +
+
+ +

+ Select a light or dark mode, or sync with your system. +

+
+ + + + + + + + + + + + + +
+ ); +} diff --git a/src/app/dashboard/settings/_components/settings-content.tsx b/src/app/dashboard/settings/_components/settings-content.tsx index 1e5e926..8923ed6 100644 --- a/src/app/dashboard/settings/_components/settings-content.tsx +++ b/src/app/dashboard/settings/_components/settings-content.tsx @@ -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 - - + + + diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 17d5cd3..ff6e9cb 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -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 ( - + {/* Inline early animation preference script to avoid FOUC */}