feat: Implement dynamic accent color selection and refactor appearance settings
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { ModeSwitcher } from "./mode-switcher";
|
||||
import { ThemeSelector } from "./theme-selector";
|
||||
|
||||
export function AppearanceSettings() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<ModeSwitcher />
|
||||
<ThemeSelector />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { useTheme } from "~/components/providers/theme-provider";
|
||||
import { Sun, Moon, Laptop } from "lucide-react";
|
||||
import { Tabs, TabsList, TabsTrigger } from "~/components/ui/tabs";
|
||||
|
||||
@@ -9,13 +9,19 @@ export function ModeSwitcher() {
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1.5">
|
||||
<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.
|
||||
{theme === "system"
|
||||
? "Follows system preference"
|
||||
: `Currently in ${theme} mode`}
|
||||
</p>
|
||||
</div>
|
||||
<Tabs defaultValue={theme} onValueChange={setTheme} className="w-auto">
|
||||
<Tabs
|
||||
value={theme}
|
||||
onValueChange={(value) => setTheme(value as "light" | "dark" | "system")}
|
||||
className="w-auto"
|
||||
>
|
||||
<TabsList>
|
||||
<TabsTrigger value="light">
|
||||
<Sun className="h-4 w-4" />
|
||||
|
||||
@@ -62,8 +62,7 @@ import { Textarea } from "~/components/ui/textarea";
|
||||
import { api } from "~/trpc/react";
|
||||
import { Switch } from "~/components/ui/switch";
|
||||
import { Slider } from "~/components/ui/slider";
|
||||
import { ModeSwitcher } from "./mode-switcher";
|
||||
import { ColorThemeSelector } from "./color-theme-selector";
|
||||
import { AppearanceSettings } from "./appearance-settings";
|
||||
import { useAnimationPreferences } from "~/components/providers/animation-preferences-provider";
|
||||
|
||||
export function SettingsContent() {
|
||||
@@ -630,8 +629,7 @@ export function SettingsContent() {
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<ModeSwitcher />
|
||||
<ColorThemeSelector />
|
||||
<AppearanceSettings />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -1,55 +1,61 @@
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { Check, Palette } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { Check } from "lucide-react";
|
||||
import { cn } from "~/lib/utils";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "~/components/ui/dropdown-menu";
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "~/components/ui/tooltip";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { useColorTheme } from "~/components/providers/color-theme-provider";
|
||||
|
||||
const themes = [
|
||||
{ name: "slate", hex: "#64748b" },
|
||||
{ name: "blue", hex: "#3b82f6" },
|
||||
{ name: "green", hex: "#22c55e" },
|
||||
{ name: "rose", hex: "#be123c" },
|
||||
{ name: "orange", hex: "#ea580c" },
|
||||
];
|
||||
|
||||
export function ThemeSelector() {
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
const themes = [
|
||||
{ name: "light", label: "Light" },
|
||||
{ name: "dark", label: "Dark" },
|
||||
{ name: "theme-sunset", label: "Sunset" },
|
||||
{ name: "theme-forest", label: "Forest" },
|
||||
];
|
||||
const { colorTheme, setColorTheme } = useColorTheme();
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1.5">
|
||||
<label className="font-medium">Theme</label>
|
||||
<p className="text-muted-foreground text-xs leading-snug">
|
||||
Select a theme for the application.
|
||||
</p>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className="w-40 justify-between">
|
||||
<span>
|
||||
{themes.find((t) => t.name === theme)?.label ?? "Light"}
|
||||
</span>
|
||||
<Palette className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<div className="space-y-1.5">
|
||||
<label className="font-medium">Theme</label>
|
||||
<p className="text-muted-foreground text-xs leading-snug">
|
||||
Select a theme for the application.
|
||||
</p>
|
||||
<div className="flex items-center gap-2 pt-2">
|
||||
<TooltipProvider>
|
||||
{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>
|
||||
<Tooltip key={t.name}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className={cn(
|
||||
"h-8 w-8 rounded-full border-2",
|
||||
colorTheme === t.name && "border-primary",
|
||||
)}
|
||||
onClick={() => setColorTheme(t.name as any)}
|
||||
style={{ backgroundColor: t.hex }}
|
||||
>
|
||||
{colorTheme === t.name && (
|
||||
<Check className="h-4 w-4 text-white" />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className="capitalize">{t.name}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+94
-14
@@ -9,6 +9,7 @@ import { Toaster } from "~/components/ui/sonner";
|
||||
import { AnimationPreferencesProvider } from "~/components/providers/animation-preferences-provider";
|
||||
|
||||
import { ThemeProvider } from "~/components/providers/theme-provider";
|
||||
import { ColorThemeProvider } from "~/components/providers/color-theme-provider";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "beenvoice - Invoicing Made Simple",
|
||||
@@ -46,27 +47,106 @@ export default function RootLayout({
|
||||
className={`${geistSans.variable} ${geistMono.variable} ${instrumentSerif.variable}`}
|
||||
>
|
||||
<head>
|
||||
{/* Inline early animation preference script to avoid FOUC */}
|
||||
{/* Inline early theme and animation preference script to avoid FOUC */}
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `(function(){try{var STORAGE_KEY='bv.animation.prefs';var raw=localStorage.getItem(STORAGE_KEY);var prefersReduced=false;var speed=1;if(raw){try{var parsed=JSON.parse(raw);if(typeof parsed.prefersReducedMotion==='boolean'){prefersReduced=parsed.prefersReducedMotion;}else{prefersReduced=window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches;}if(typeof parsed.animationSpeedMultiplier==='number'){speed=parsed.animationSpeedMultiplier;if(isNaN(speed)||speed<0.25||speed>4)speed=1;}}catch(e){}}else{prefersReduced=window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches;}var root=document.documentElement;if(prefersReduced)root.classList.add('user-reduce-motion');function apply(fast,normal,slow){root.style.setProperty('--animation-speed-fast',fast+'s');root.style.setProperty('--animation-speed-normal',normal+'s');root.style.setProperty('--animation-speed-slow',slow+'s');}if(prefersReduced){apply(0.01,0.01,0.01);}else{var fast=(0.15/speed).toFixed(4);var normal=(0.30/speed).toFixed(4);var slow=(0.50/speed).toFixed(4);apply(fast,normal,slow);}}catch(_e){}})();`,
|
||||
__html: `(function(){
|
||||
try {
|
||||
var root = document.documentElement;
|
||||
|
||||
// Mode theme persistence (light/dark/system)
|
||||
var modeTheme = localStorage.getItem('theme');
|
||||
var systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
|
||||
root.classList.remove('light', 'dark');
|
||||
|
||||
if (modeTheme === 'dark' || modeTheme === 'light') {
|
||||
root.classList.add(modeTheme);
|
||||
} else {
|
||||
// Default to system if no preference or 'system'
|
||||
root.classList.add(systemTheme);
|
||||
}
|
||||
|
||||
// Color theme persistence (custom accent colors)
|
||||
var customColor = localStorage.getItem('customThemeColor');
|
||||
var isCustom = localStorage.getItem('isCustomTheme') === 'true';
|
||||
|
||||
if (isCustom && customColor) {
|
||||
try {
|
||||
var themeData = JSON.parse(customColor);
|
||||
if (themeData && themeData.colors && themeData.colors.light) {
|
||||
// Apply saved colors directly
|
||||
for (var key in themeData.colors.light) {
|
||||
if (themeData.colors.light.hasOwnProperty(key)) {
|
||||
root.style.setProperty(key, themeData.colors.light[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback logic omitted for brevity, relying on provider for full recovery
|
||||
}
|
||||
} else {
|
||||
// Apply preset color theme
|
||||
var colorTheme = localStorage.getItem('color-theme');
|
||||
if (colorTheme) {
|
||||
root.classList.add(colorTheme);
|
||||
} else {
|
||||
root.classList.add('slate'); // Default
|
||||
}
|
||||
}
|
||||
|
||||
// Animation preferences script (existing)
|
||||
var STORAGE_KEY='bv.animation.prefs';
|
||||
var raw=localStorage.getItem(STORAGE_KEY);
|
||||
var prefersReduced=false;
|
||||
var speed=1;
|
||||
if(raw){
|
||||
try{
|
||||
var parsed=JSON.parse(raw);
|
||||
if(typeof parsed.prefersReducedMotion==='boolean'){
|
||||
prefersReduced=parsed.prefersReducedMotion;
|
||||
}else{
|
||||
prefersReduced=window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
}
|
||||
if(typeof parsed.animationSpeedMultiplier==='number'){
|
||||
speed=parsed.animationSpeedMultiplier;
|
||||
if(isNaN(speed)||speed<0.25||speed>4)speed=1;
|
||||
}
|
||||
}catch(e){}
|
||||
}else{
|
||||
prefersReduced=window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
}
|
||||
|
||||
if(prefersReduced)root.classList.add('user-reduce-motion');
|
||||
function apply(fast,normal,slow){
|
||||
root.style.setProperty('--animation-speed-fast',fast+'s');
|
||||
root.style.setProperty('--animation-speed-normal',normal+'s');
|
||||
root.style.setProperty('--animation-speed-slow',slow+'s');
|
||||
}
|
||||
if(prefersReduced){
|
||||
apply(0.01,0.01,0.01);
|
||||
}else{
|
||||
var fast=(0.15/speed).toFixed(4);
|
||||
var normal=(0.30/speed).toFixed(4);
|
||||
var slow=(0.50/speed).toFixed(4);
|
||||
apply(fast,normal,slow);
|
||||
}
|
||||
} catch(_e) {}
|
||||
})();`,
|
||||
}}
|
||||
/>
|
||||
</head>
|
||||
<Analytics />
|
||||
<body className="bg-background text-foreground relative min-h-screen overflow-x-hidden font-sans antialiased">
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="theme-ocean"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<TRPCReactProvider>
|
||||
<AnimationPreferencesProvider>
|
||||
{children}
|
||||
</AnimationPreferencesProvider>
|
||||
<Toaster />
|
||||
</TRPCReactProvider>
|
||||
<ThemeProvider>
|
||||
<ColorThemeProvider>
|
||||
<TRPCReactProvider>
|
||||
<AnimationPreferencesProvider>
|
||||
{children}
|
||||
</AnimationPreferencesProvider>
|
||||
<Toaster />
|
||||
</TRPCReactProvider>
|
||||
</ColorThemeProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user