mirror of
https://github.com/soconnor0919/beenvoice.git
synced 2025-12-13 01:24:44 -05:00
feat: Add comprehensive theme management with mode and color selectors, alongside new fonts.
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
33
src/app/dashboard/settings/_components/mode-switcher.tsx
Normal file
33
src/app/dashboard/settings/_components/mode-switcher.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user