smithery/syedaashnaghazanfar

notification-ui

Renders notification bell icon with unread count badge, pulse animations for new notifications, and dropdown panel (320px × max 400px) displaying notification items with purple theme for VERY IMPORTANT tasks. Includes mark-as-read interaction and empty state handling.

Installation

$ npx skills add smithery/syedaashnaghazanfar --skill notification-ui

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery/syedaashnaghazanfar.

npx skills add smithery/syedaashnaghazanfar

Browse all from smithery/syedaashnaghazanfar

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 13,417 B
  • docs SUMMARY.md 292 B

History

  1. First recorded snapshot · 0 installs

SKILL.md

Notification UI Skill

Overview

The notification UI skill provides the visual interface for displaying notifications, including a bell icon with badge, animated indicators, and an interactive notification panel.

When to Apply

Apply this skill:

  • When rendering the app header (bell icon)
  • When displaying unread notification count
  • When showing notification panel/dropdown
  • When animating new notification arrivals
  • When user interacts with notifications (click to mark as read)
  • When displaying empty state (no notifications)

Bell Icon Specifications

Icon Size and Position

  • Size: 24px × 24px
  • Position: Top-right header
  • Color: Default #374151 (gray), hover #1F2937 (dark gray)
function BellIcon({ unreadCount, onClick }) {
  return (
    <button
      className="bell-icon-button"
      onClick={onClick}
      aria-label={`Notifications (${unreadCount} unread)`}
    >
      <svg
        width="24"
        height="24"
        viewBox="0 0 24 24"
        fill="none"
        xmlns="http://www.w3.org/2000/svg"
        className="bell-icon"
      >
        <path
          d="M15 17H20L18.5951 15.5951C18.2141 15.2141 18 14.6973 18 14.1585V11C18 8.38757 16.3304 6.16509 14 5.34142V5C14 3.89543 13.1046 3 12 3C10.8954 3 10 3.89543 10 5V5.34142C7.66962 6.16509 6 8.38757 6 11V14.1585C6 14.6973 5.78595 15.2141 5.40493 15.5951L4 17H9M15 17V18C15 19.6569 13.6569 21 12 21C10.3431 21 9 19.6569 9 18V17M15 17H9"
          stroke="currentColor"
          strokeWidth="2"
          strokeLinecap="round"
          strokeLinejoin="round"
        />
      </svg>
      {unreadCount > 0 && (
        <span className="badge">{unreadCount > 99 ? '99+' : unreadCount}</span>
      )}
    </button>
  );
}

Bell Icon Styling

.bell-icon-button {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 8px;
  background: transparent;
  border: none;
  cursor: pointer;
  border-radius: 6px;
  transition: background-color 0.2s ease;
}

.bell-icon-button:hover {
  background-color: #F3F4F6;
}

.bell-icon {
  color: #374151;
  transition: color 0.2s ease;
}

.bell-icon-button:hover .bell-icon {
  color: #1F2937;
}

Unread Count Badge

Badge Specifications

  • Shape: Circular
  • Background: #DC2626 (red)
  • Text: White (#FFFFFF)
  • Display: Count up to 99, then "99+"
  • Position: Top-right corner of bell icon
.badge {
  position: absolute;
  top: 4px;
  right: 4px;
  min-width: 18px;
  height: 18px;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: #DC2626; /* Red */
  color: #FFFFFF; /* White */
  font-size: 10px;
  font-weight: 700;
  border-radius: 50%;
  padding: 0 4px;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}

Badge Display Logic

function getBadgeText(count) {
  if (count === 0) return null; // No badge
  if (count > 99) return '99+';
  return count.toString();
}

Pulse Animation

Animation Specifications

  • Duration: 2 seconds
  • Effect: Purple glow (#8B5CF6)
  • Trigger: On new notification arrival
  • Iterations: 3 times, then stop
@keyframes pulse {
  0%, 100% {
    box-shadow: 0 0 0 0 rgba(139, 92, 246, 0.7);
  }
  50% {
    box-shadow: 0 0 0 8px rgba(139, 92, 246, 0);
  }
}

.bell-icon-button.pulsing {
  animation: pulse 2s ease-in-out 3;
}

Animation Trigger

function NotificationBell({ notifications }) {
  const [isPulsing, setIsPulsing] = useState(false);
  const prevCountRef = useRef(notifications.length);

  useEffect(() => {
    // Detect new notification
    if (notifications.length > prevCountRef.current) {
      setIsPulsing(true);

      // Stop animation after 6 seconds (3 iterations × 2s)
      setTimeout(() => setIsPulsing(false), 6000);
    }

    prevCountRef.current = notifications.length;
  }, [notifications.length]);

  return (
    <BellIcon
      className={isPulsing ? 'pulsing' : ''}
      unreadCount={notifications.filter(n => !n.read).length}
    />
  );
}

Notification Dropdown Panel

Panel Specifications

  • Width: 320px
  • Max Height: 400px
  • Scrollable: Yes (overflow-y: auto)
  • Position: Dropdown below bell icon
  • Elevation: Shadow for depth
.notification-dropdown {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  width: 320px;
  max-height: 400px;
  overflow-y: auto;
  background-color: #FFFFFF;
  border: 1px solid #E5E7EB;
  border-radius: 8px;
  box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
              0 4px 6px -2px rgba(0, 0, 0, 0.05);
  z-index: 1000;
}

.notification-dropdown-header {
  padding: 16px;
  border-bottom: 1px solid #E5E7EB;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.notification-dropdown-title {
  font-size: 16px;
  font-weight: 600;
  color: #111827;
}

.mark-all-read {
  font-size: 12px;
  color: #8B5CF6;
  background: none;
  border: none;
  cursor: pointer;
  font-weight: 600;
}

.mark-all-read:hover {
  color: #7C3AED;
  text-decoration: underline;
}

Notification Item Styling

Item Specifications

  • Padding: 16px
  • Border: Bottom separator (1px solid #F3F4F6)
  • Hover: Background change to #F9FAFB
  • Purple Theme: VERY IMPORTANT notifications use #8B5CF6 accent
.notification-item {
  padding: 16px;
  border-bottom: 1px solid #F3F4F6;
  cursor: pointer;
  transition: background-color 0.15s ease;
}

.notification-item:hover {
  background-color: #F9FAFB;
}

.notification-item:last-child {
  border-bottom: none;
}

.notification-item.unread {
  background-color: #FAF5FF; /* Very light purple */
}

.notification-item.unread:hover {
  background-color: #F3E8FF; /* Light purple */
}

.notification-content {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.notification-header {
  display: flex;
  align-items: flex-start;
  gap: 8px;
}

.notification-priority-indicator {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background-color: #8B5CF6; /* Purple for VERY IMPORTANT */
  margin-top: 6px;
  flex-shrink: 0;
}

.notification-message {
  font-size: 14px;
  color: #374151;
  line-height: 1.5;
  flex: 1;
}

.notification-item.unread .notification-message {
  font-weight: 600;
  color: #111827;
}

.notification-time {
  font-size: 12px;
  color: #9CA3AF;
  display: flex;
  align-items: center;
  gap: 4px;
}

.unread-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background-color: #8B5CF6; /* Purple */
}

Complete Notification Item Component

function NotificationItem({ notification, onMarkAsRead }) {
  const handleClick = () => {
    if (!notification.read) {
      onMarkAsRead(notification.id);
    }
  };

  const relativeTime = formatRelativeTime(notification.timestamp);

  return (
    <div
      className={`notification-item ${notification.read ? '' : 'unread'}`}
      onClick={handleClick}
    >
      <div className="notification-content">
        <div className="notification-header">
          <div className="notification-priority-indicator" />
          <p className="notification-message">{notification.message}</p>
        </div>
        <div className="notification-time">
          {!notification.read && <span className="unread-dot" />}
          <span>{relativeTime}</span>
        </div>
      </div>
    </div>
  );
}

function formatRelativeTime(timestamp) {
  const now = Date.now();
  const diff = now - timestamp;
  const minutes = Math.floor(diff / (1000 * 60));
  const hours = Math.floor(diff / (1000 * 60 * 60));
  const days = Math.floor(diff / (1000 * 60 * 60 * 24));

  if (minutes < 1) return 'Just now';
  if (minutes < 60) return `${minutes}m ago`;
  if (hours < 24) return `${hours}h ago`;
  return `${days}d ago`;
}

Mark-as-Read Interaction

Click to Mark Read

Clicking an unread notification marks it as read:

function handleNotificationClick(notification) {
  if (!notification.read) {
    // Mark as read
    notificationPersistence.markAsRead(notification.id);

    // Update UI state
    setNotifications(notificationPersistence.getAll());
    setUnreadCount(notificationPersistence.getUnreadCount());

    // Optional: Navigate to task
    navigateToTask(notification.taskId);
  }
}

Visual Feedback

function NotificationItem({ notification, onMarkAsRead }) {
  const [isMarking, setIsMarking] = useState(false);

  const handleClick = async () => {
    if (!notification.read && !isMarking) {
      setIsMarking(true);
      await onMarkAsRead(notification.id);
      setIsMarking(false);
    }
  };

  return (
    <div
      className={`notification-item ${notification.read ? '' : 'unread'} ${isMarking ? 'marking' : ''}`}
      onClick={handleClick}
    >
      {/* ... */}
    </div>
  );
}

Empty State

Empty State Component

function EmptyState() {
  return (
    <div className="notification-empty-state">
      <div className="empty-icon">🔔</div>
      <p className="empty-title">No notifications</p>
      <p className="empty-description">
        You're all caught up! Notifications for VERY IMPORTANT tasks will appear here.
      </p>
    </div>
  );
}

Empty State Styling

.notification-empty-state {
  padding: 48px 24px;
  text-align: center;
  color: #6B7280;
}

.empty-icon {
  font-size: 48px;
  margin-bottom: 16px;
  opacity: 0.4;
}

.empty-title {
  font-size: 16px;
  font-weight: 600;
  color: #374151;
  margin-bottom: 8px;
}

.empty-description {
  font-size: 14px;
  color: #9CA3AF;
  line-height: 1.5;
}

Complete Notification System

function NotificationSystem() {
  const [isOpen, setIsOpen] = useState(false);
  const [notifications, setNotifications] = useState([]);
  const [unreadCount, setUnreadCount] = useState(0);
  const dropdownRef = useRef(null);

  // Load notifications
  useEffect(() => {
    const loaded = notificationPersistence.getAll();
    setNotifications(loaded);
    setUnreadCount(notificationPersistence.getUnreadCount());
  }, []);

  // Close dropdown when clicking outside
  useEffect(() => {
    function handleClickOutside(event) {
      if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
        setIsOpen(false);
      }
    }

    document.addEventListener('mousedown', handleClickOutside);
    return () => document.removeEventListener('mousedown', handleClickOutside);
  }, []);

  const handleMarkAsRead = (id) => {
    notificationPersistence.markAsRead(id);
    setNotifications(notificationPersistence.getAll());
    setUnreadCount(notificationPersistence.getUnreadCount());
  };

  const handleMarkAllAsRead = () => {
    notificationPersistence.markAllAsRead();
    setNotifications(notificationPersistence.getAll());
    setUnreadCount(0);
  };

  return (
    <div className="notification-system" ref={dropdownRef}>
      <NotificationBell
        unreadCount={unreadCount}
        onClick={() => setIsOpen(!isOpen)}
        notifications={notifications}
      />

      {isOpen && (
        <div className="notification-dropdown">
          <div className="notification-dropdown-header">
            <h3 className="notification-dropdown-title">Notifications</h3>
            {unreadCount > 0 && (
              <button className="mark-all-read" onClick={handleMarkAllAsRead}>
                Mark all as read
              </button>
            )}
          </div>

          {notifications.length === 0 ? (
            <EmptyState />
          ) : (
            <div className="notification-list">
              {notifications.map(notification => (
                <NotificationItem
                  key={notification.id}
                  notification={notification}
                  onMarkAsRead={handleMarkAsRead}
                />
              ))}
            </div>
          )}
        </div>
      )}
    </div>
  );
}

Responsive Behavior

Mobile Adjustments

@media (max-width: 640px) {
  .notification-dropdown {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    width: 100%;
    max-height: 100%;
    border-radius: 0;
  }

  .notification-dropdown-header {
    position: sticky;
    top: 0;
    background-color: #FFFFFF;
    z-index: 1;
  }
}

Integration Points

This skill integrates with:

  • Notification Persistence Skill: Reads notification data
  • Notification Trigger Skill: Displays triggered notifications
  • Priority Classification Skill: Uses purple theme for VERY IMPORTANT
  • Notification Experience Agent: Coordinates UI interactions

Accessibility

  • Bell icon button has aria-label with unread count
  • Keyboard navigation supported (Enter/Space to open/close)
  • Focus management in dropdown
  • Screen reader announcements for new notifications
  • Sufficient color contrast ratios (WCAG AA)

Performance Considerations

  • Dropdown renders only when open (conditional rendering)
  • Virtual scrolling for large notification lists (>50)
  • Debounced mark-as-read operations
  • Memoized components to prevent unnecessary re-renders