Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion tenant-dashboard/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,19 @@
@apply border-border;
}
body {
@apply bg-background text-foreground;
@apply bg-background text-foreground transition-colors duration-300;
}

/* Smooth theme transitions */
*, *::before, *::after {
transition-property: background-color, border-color, color, fill, stroke;
transition-duration: 150ms;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}

/* Disable transitions during theme changes to prevent flash */
.theme-transitioning * {
transition: none !important;
}
}

Expand Down
31 changes: 17 additions & 14 deletions tenant-dashboard/src/app/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { PerformanceProvider } from "@/components/performance/PerformanceProvide
import { AuthProvider } from "@/lib/auth/auth-context";
import { WebSocketProvider } from "@/lib/realtime/WebSocketProvider";
import { QueryProvider } from "@/components/providers/query-provider";
import { ThemeProvider } from "@/providers/ThemeProvider";

interface ProvidersProps {
children: React.ReactNode;
Expand All @@ -24,12 +25,13 @@ export function Providers({ children }: ProvidersProps) {
console.error("Root error boundary triggered:", error, errorInfo);
}}
>
<PerformanceProvider>
<AuthProvider>
<WebSocketProvider>
{/* Temporarily disabled: <PostHogProvider> */}
<QueryProvider>
{children}
<ThemeProvider>
<PerformanceProvider>
<AuthProvider>
<WebSocketProvider>
{/* Temporarily disabled: <PostHogProvider> */}
<QueryProvider>
{children}
<Toaster
position="top-right"
toastOptions={{
Expand All @@ -54,14 +56,15 @@ export function Providers({ children }: ProvidersProps) {
}}
/>

{/* PWA Components */}
<ConnectionStatus />
<PWAPrompts showInstallBanner={true} autoShowInstallDialog={false} />
</QueryProvider>
{/* Temporarily disabled: </PostHogProvider> */}
</WebSocketProvider>
</AuthProvider>
</PerformanceProvider>
{/* PWA Components */}
<ConnectionStatus />
<PWAPrompts showInstallBanner={true} autoShowInstallDialog={false} />
</QueryProvider>
{/* Temporarily disabled: </PostHogProvider> */}
</WebSocketProvider>
</AuthProvider>
</PerformanceProvider>
</ThemeProvider>
</ErrorBoundary>
);
}
40 changes: 13 additions & 27 deletions tenant-dashboard/src/components/layout/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import React from "react";
import Image from "next/image";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
Expand All @@ -19,22 +20,20 @@ import {
User,
LogOut,
HelpCircle,
Moon,
Sun,
Menu,
ChevronDown,
} from "lucide-react";
import { cn, getInitials } from "@/lib/utils";
import { CommandPalette, useCommandPalette } from "@/components/design-system";
import { useResponsive, responsive } from "@/hooks/useResponsive";
import { ThemeToggle } from "@/components/ui/ThemeToggle";

interface HeaderProps {
onMenuToggle?: () => void;
className?: string;
}

export function Header({ onMenuToggle, className }: HeaderProps) {
const [theme, setTheme] = React.useState<"light" | "dark">("light");
const { open, setOpen, CommandPalette: CommandPaletteComponent } = useCommandPalette();
const { isMobile, isTablet } = useResponsive();
const [notifications] = React.useState([
Expand Down Expand Up @@ -77,11 +76,6 @@ export function Header({ onMenuToggle, className }: HeaderProps) {
avatar: null,
};

const toggleTheme = () => {
setTheme(theme === "light" ? "dark" : "light");
// In real app, this would update the theme context
document.documentElement.classList.toggle("dark");
};

return (
<header className={cn(
Expand Down Expand Up @@ -167,19 +161,7 @@ export function Header({ onMenuToggle, className }: HeaderProps) {
</Button>

{/* Theme Toggle */}
<Button
variant="ghost"
size="icon"
onClick={toggleTheme}
className={cn("h-9 w-9", responsive.touchTarget)}
aria-label={`Switch to ${theme === "light" ? "dark" : "light"} mode`}
>
{theme === "light" ? (
<Moon className="h-4 w-4" />
) : (
<Sun className="h-4 w-4" />
)}
</Button>
<ThemeToggle />

{/* Notifications */}
<DropdownMenu>
Expand Down Expand Up @@ -302,13 +284,17 @@ export function Header({ onMenuToggle, className }: HeaderProps) {
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem className={cn("cursor-pointer", responsive.touchTarget)}>
<User className="mr-2 h-4 w-4" />
Profile Settings
<DropdownMenuItem asChild>
<Link href="/dashboard/profile" className={cn("cursor-pointer", responsive.touchTarget)}>
<User className="mr-2 h-4 w-4" />
Profile Settings
</Link>
</DropdownMenuItem>
<DropdownMenuItem className={cn("cursor-pointer", responsive.touchTarget)}>
<Settings className="mr-2 h-4 w-4" />
Account Settings
<DropdownMenuItem asChild>
<Link href="/settings" className={cn("cursor-pointer", responsive.touchTarget)}>
<Settings className="mr-2 h-4 w-4" />
Settings
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className={cn("cursor-pointer text-destructive", responsive.touchTarget)}>
Expand Down
78 changes: 78 additions & 0 deletions tenant-dashboard/src/components/settings/AppearanceSettings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
'use client';

import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator';
import { ThemeToggleInline } from '@/components/ui/ThemeToggle';
import { Palette, Monitor } from 'lucide-react';

export function AppearanceSettings() {
return (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Palette className="h-5 w-5" />
Theme
</CardTitle>
<CardDescription>
Choose how the interface appears to you. The system option will follow your device's theme preference.
</CardDescription>
</CardHeader>
<CardContent>
<ThemeToggleInline />
</CardContent>
</Card>

<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Monitor className="h-5 w-5" />
Display
</CardTitle>
<CardDescription>
Additional display preferences and accessibility options.
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<div className="text-sm font-medium">High contrast mode</div>
<div className="text-sm text-muted-foreground">
Increases color contrast for better readability
</div>
</div>
<div className="px-3 py-1 bg-muted rounded text-xs text-muted-foreground">
Coming soon
</div>
</div>
<Separator />
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<div className="text-sm font-medium">Compact mode</div>
<div className="text-sm text-muted-foreground">
Reduces spacing for a more compact interface
</div>
</div>
<div className="px-3 py-1 bg-muted rounded text-xs text-muted-foreground">
Coming soon
</div>
</div>
<Separator />
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<div className="text-sm font-medium">Motion preference</div>
<div className="text-sm text-muted-foreground">
Reduce motion and animations
</div>
</div>
<div className="px-3 py-1 bg-muted rounded text-xs text-muted-foreground">
Coming soon
</div>
</div>
</div>
</CardContent>
</Card>
</div>
);
}
145 changes: 145 additions & 0 deletions tenant-dashboard/src/components/ui/ThemeToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
'use client';

import * as React from 'react';
import { useTheme } from 'next-themes';
import { Moon, Sun, Monitor, Check } from 'lucide-react';

import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';

export function ThemeToggle() {
const { theme, setTheme, themes } = useTheme();
const [mounted, setMounted] = React.useState(false);

// useEffect only runs on the client, so now we can safely show the UI
React.useEffect(() => {
setMounted(true);
}, []);

if (!mounted) {
return (
<Button variant="ghost" size="sm" className="w-9 px-0">
<span className="sr-only">Toggle theme</span>
</Button>
);
}

const getThemeIcon = (themeName: string) => {
switch (themeName) {
case 'dark':
return <Moon className="h-4 w-4" />;
case 'light':
return <Sun className="h-4 w-4" />;
case 'system':
return <Monitor className="h-4 w-4" />;
default:
return <Monitor className="h-4 w-4" />;
}
};

const getThemeLabel = (themeName: string) => {
switch (themeName) {
case 'dark':
return 'Dark';
case 'light':
return 'Light';
case 'system':
return 'System';
default:
return 'System';
}
};

return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="w-9 px-0 transition-colors hover:bg-accent hover:text-accent-foreground"
>
{getThemeIcon(theme || 'system')}
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="min-w-[140px]">
{themes.map((themeName) => (
<DropdownMenuItem
key={themeName}
onClick={() => setTheme(themeName)}
className="flex items-center justify-between cursor-pointer"
>
<div className="flex items-center gap-2">
{getThemeIcon(themeName)}
<span className="capitalize">{getThemeLabel(themeName)}</span>
</div>
{theme === themeName && <Check className="h-4 w-4" />}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}

export function ThemeToggleInline() {
const { theme, setTheme } = useTheme();
const [mounted, setMounted] = React.useState(false);

React.useEffect(() => {
setMounted(true);
}, []);

if (!mounted) {
return (
<div className="flex items-center space-x-2 opacity-50">
<Monitor className="h-4 w-4" />
<span>Loading theme...</span>
</div>
);
}

const themes = [
{ value: 'light', label: 'Light', icon: Sun },
{ value: 'dark', label: 'Dark', icon: Moon },
{ value: 'system', label: 'System', icon: Monitor },
];

return (
<div className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-medium">Theme Preference</h4>
<p className="text-sm text-muted-foreground">
Choose how the interface looks. Select System to use your device preference.
</p>
</div>

<div className="grid grid-cols-3 gap-2">
{themes.map(({ value, label, icon: Icon }) => (
<button
key={value}
onClick={() => setTheme(value)}
className={`
flex flex-col items-center gap-2 p-3 rounded-lg border-2 transition-all
${
theme === value
? 'border-primary bg-primary/5 text-primary'
: 'border-border hover:border-border/80 hover:bg-accent/50'
}
`}
>
<Icon className="h-5 w-5" />
<span className="text-xs font-medium">{label}</span>
{theme === value && (
<Check className="h-3 w-3 absolute top-1 right-1 opacity-70" />
)}
</button>
))}
</div>
</div>
);
}
Loading
Loading