- Created automated script (scripts/align-8px-grid.py) to align all spacing to 8px grid - Replaced non-8px-aligned spacing: gap-3/p-3/m-3 (12px) → gap-4/p-4/m-4 (16px), gap-5/p-5/m-5 (20px) → gap-6/p-6/m-6 (24px), gap-10/p-10/m-10 (40px) → gap-12/p-12/m-12 (48px), gap-20/p-20/m-20 (80px) → gap-24/p-24/m-24 (96px) - Preserved: 4px values (gap-1, p-1, m-1) as they may be intentional fine-tuning, responsive breakpoints (sm:, md:, lg:), test files, documentation - Modified files across all components to ensure consistent 8px grid alignment - Action 11.2.1.3: Align all elements to 8px grid - COMPLETE
107 lines
3.6 KiB
TypeScript
107 lines
3.6 KiB
TypeScript
import React, { useState, useRef, useEffect } from 'react';
|
|
import { Bell, Check } from 'lucide-react';
|
|
import { Button } from '../ui/button';
|
|
import { Notification } from '../../types';
|
|
import { NotificationItem } from './NotificationItem';
|
|
|
|
interface NotificationBellProps {
|
|
notifications: Notification[];
|
|
onMarkAllRead: () => void;
|
|
onRead: (id: string) => void;
|
|
onViewAll: () => void;
|
|
}
|
|
|
|
export const NotificationBell: React.FC<NotificationBellProps> = ({
|
|
notifications,
|
|
onMarkAllRead,
|
|
onRead,
|
|
onViewAll,
|
|
}) => {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
|
|
const unreadCount = notifications.filter((n) => !n.read).length;
|
|
|
|
useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (
|
|
wrapperRef.current &&
|
|
!wrapperRef.current.contains(event.target as Node)
|
|
) {
|
|
setIsOpen(false);
|
|
}
|
|
};
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
}, []);
|
|
|
|
return (
|
|
<div ref={wrapperRef} className="relative z-50">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className={`relative text-kodo-secondary hover:text-kodo-primary ${isOpen ? 'text-kodo-primary bg-white/5' : ''}`}
|
|
onClick={() => setIsOpen(!isOpen)}
|
|
>
|
|
<Bell className="w-5 h-5" />
|
|
{unreadCount > 0 && (
|
|
<span className="absolute top-1.5 right-2 w-2 h-2 bg-kodo-red rounded-full border-2 border-kodo-void animate-pulse"></span>
|
|
)}
|
|
</Button>
|
|
|
|
{isOpen && (
|
|
<div className="absolute top-full right-0 mt-4 w-96 bg-kodo-graphite border border-kodo-steel rounded-xl shadow-2xl overflow-hidden animate-fadeIn origin-top-right ring-1 ring-white/5 flex flex-col max-h-[80vh]">
|
|
<div className="p-4 border-b border-kodo-steel/50 flex justify-between items-center bg-kodo-ink">
|
|
<div>
|
|
<h3 className="font-bold text-white">Notifications</h3>
|
|
<p className="text-xs text-kodo-content-dim">{unreadCount} unread</p>
|
|
</div>
|
|
{unreadCount > 0 && (
|
|
<button
|
|
onClick={onMarkAllRead}
|
|
className="text-xs text-kodo-steel hover:underline flex items-center gap-1"
|
|
>
|
|
<Check className="w-3 h-3" /> Mark all read
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-y-auto custom-scrollbar p-2 space-y-1">
|
|
{notifications.length === 0 ? (
|
|
<div className="text-center py-12 text-kodo-content-dim">
|
|
<Bell className="w-8 h-8 mx-auto mb-2 opacity-50" />
|
|
<p className="text-sm">No new notifications</p>
|
|
</div>
|
|
) : (
|
|
notifications.slice(0, 5).map((n) => (
|
|
<NotificationItem
|
|
key={n.id}
|
|
notification={n}
|
|
onRead={onRead}
|
|
onAction={() => {
|
|
setIsOpen(false);
|
|
onViewAll();
|
|
}}
|
|
/>
|
|
))
|
|
)}
|
|
</div>
|
|
|
|
<div className="p-2 border-t border-kodo-steel/30 bg-kodo-ink/50">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="w-full text-xs"
|
|
onClick={() => {
|
|
setIsOpen(false);
|
|
onViewAll();
|
|
}}
|
|
>
|
|
View All Notifications
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|