## Signal Messaging Integration - Added SignalService for sending messages to drivers via Signal - SignalMessage model for tracking message history - Driver chat modal for real-time messaging - Send schedule via Signal (ICS + PDF attachments) ## AI Copilot - Natural language interface for VIP Coordinator - Capabilities: create VIPs, schedule events, assign drivers - Help and guidance for users - Floating copilot button in UI ## Theme System - Dark/light/system theme support - Color scheme selection (blue, green, purple, orange, red) - ThemeContext for global state - AppearanceMenu in header ## PDF Schedule Export - VIPSchedulePDF component for schedule generation - PDF settings (header, footer, branding) - Preview PDF in browser - Settings stored in database ## Database Migrations - add_signal_messages: SignalMessage model - add_pdf_settings: Settings model for PDF config - add_reminder_tracking: lastReminderSent for events - make_driver_phone_optional: phone field nullable ## Event Management - Event status service for automated updates - IN_PROGRESS/COMPLETED status tracking - Reminder tracking for notifications ## UI/UX Improvements - Driver schedule modal - Improved My Schedule page - Better error handling and loading states - Responsive design improvements ## Other Changes - AGENT_TEAM.md documentation - Seed data improvements - Ability factory updates - Driver profile page Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
66 lines
2.3 KiB
TypeScript
66 lines
2.3 KiB
TypeScript
import { useState, useRef, useEffect } from 'react';
|
|
import { Sun, Moon, Monitor } from 'lucide-react';
|
|
import { useTheme, ThemeMode } from '@/hooks/useTheme';
|
|
|
|
const modes: { value: ThemeMode; label: string; icon: typeof Sun }[] = [
|
|
{ value: 'light', label: 'Light', icon: Sun },
|
|
{ value: 'dark', label: 'Dark', icon: Moon },
|
|
{ value: 'system', label: 'System', icon: Monitor },
|
|
];
|
|
|
|
export function ThemeToggle() {
|
|
const { mode, resolvedTheme, setMode } = useTheme();
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
|
|
// Close dropdown when clicking outside
|
|
useEffect(() => {
|
|
function handleClickOutside(event: MouseEvent) {
|
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
|
setIsOpen(false);
|
|
}
|
|
}
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
}, []);
|
|
|
|
// Get current icon based on resolved theme
|
|
const CurrentIcon = resolvedTheme === 'dark' ? Moon : Sun;
|
|
|
|
return (
|
|
<div className="relative" ref={dropdownRef}>
|
|
<button
|
|
onClick={() => setIsOpen(!isOpen)}
|
|
className="flex items-center justify-center w-9 h-9 rounded-lg bg-muted hover:bg-accent transition-colors focus-ring"
|
|
aria-label={`Current theme: ${mode}. Click to change.`}
|
|
aria-expanded={isOpen}
|
|
aria-haspopup="true"
|
|
>
|
|
<CurrentIcon className="h-5 w-5 text-foreground" />
|
|
</button>
|
|
|
|
{isOpen && (
|
|
<div className="absolute right-0 mt-2 w-36 rounded-lg bg-popover border border-border shadow-elevated z-50 overflow-hidden animate-in fade-in slide-in-from-top-2 duration-150">
|
|
{modes.map(({ value, label, icon: Icon }) => (
|
|
<button
|
|
key={value}
|
|
onClick={() => {
|
|
setMode(value);
|
|
setIsOpen(false);
|
|
}}
|
|
className={`flex items-center gap-3 w-full px-3 py-2.5 text-sm transition-colors ${
|
|
mode === value
|
|
? 'bg-primary/10 text-primary font-medium'
|
|
: 'text-popover-foreground hover:bg-accent'
|
|
}`}
|
|
>
|
|
<Icon className="h-4 w-4" />
|
|
{label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|