Glass Notification

PreviousNext

A customizable notification system with glass morphism styling, allowing for different types of messages and actions.

Docs
einuicomponent

Preview

Loading preview…
registry/innovative/glass-notification.tsx
"use client"

import * as React from "react"
import { X, CheckCircle, AlertCircle, AlertTriangle, Info } from "lucide-react"
import { cn } from "@/lib/utils"

type NotificationType = "success" | "error" | "warning" | "info"
type NotificationPosition = "top-right" | "top-left" | "bottom-right" | "bottom-left" | "top-center" | "bottom-center"

interface Notification {
  id: string
  type: NotificationType
  title: string
  description?: string
  duration?: number
}

interface NotificationContextType {
  notifications: Notification[]
  addNotification: (notification: Omit<Notification, "id">) => void
  removeNotification: (id: string) => void
}

const NotificationContext = React.createContext<NotificationContextType | null>(null)

export function useNotification() {
  const context = React.useContext(NotificationContext)
  if (!context) {
    throw new Error("useNotification must be used within a GlassNotificationProvider")
  }
  return context
}

export function GlassNotificationProvider({
  children,
  position = "bottom-right",
}: {
  children: React.ReactNode
  position?: NotificationPosition
}) {
  const [notifications, setNotifications] = React.useState<Notification[]>([])

  const addNotification = React.useCallback((notification: Omit<Notification, "id">) => {
    const id = Math.random().toString(36).substring(2, 9)
    setNotifications((prev) => [...prev, { ...notification, id }])

    if (notification.duration !== 0) {
      setTimeout(() => {
        setNotifications((prev) => prev.filter((n) => n.id !== id))
      }, notification.duration || 5000)
    }
  }, [])

  const removeNotification = React.useCallback((id: string) => {
    setNotifications((prev) => prev.filter((n) => n.id !== id))
  }, [])

  return (
    <NotificationContext.Provider value={{ notifications, addNotification, removeNotification }}>
      {children}
      <GlassNotificationContainer position={position} />
    </NotificationContext.Provider>
  )
}

const typeConfig = {
  success: {
    icon: CheckCircle,
    gradient: "from-emerald-500/30 to-green-500/30",
    border: "border-emerald-400/30",
    iconColor: "text-emerald-400",
  },
  error: {
    icon: AlertCircle,
    gradient: "from-red-500/30 to-rose-500/30",
    border: "border-red-400/30",
    iconColor: "text-red-400",
  },
  warning: {
    icon: AlertTriangle,
    gradient: "from-amber-500/30 to-yellow-500/30",
    border: "border-amber-400/30",
    iconColor: "text-amber-400",
  },
  info: {
    icon: Info,
    gradient: "from-cyan-500/30 to-blue-500/30",
    border: "border-cyan-400/30",
    iconColor: "text-cyan-400",
  },
}

const positionStyles: Record<NotificationPosition, { container: string; animation: string }> = {
  "top-right": {
    container: "top-4 right-4",
    animation: "slide-in-from-right-full",
  },
  "top-left": {
    container: "top-4 left-4",
    animation: "slide-in-from-left-full",
  },
  "bottom-right": {
    container: "bottom-4 right-4",
    animation: "slide-in-from-right-full",
  },
  "bottom-left": {
    container: "bottom-4 left-4",
    animation: "slide-in-from-left-full",
  },
  "top-center": {
    container: "top-4 left-1/2 -translate-x-1/2",
    animation: "slide-in-from-top-full",
  },
  "bottom-center": {
    container: "bottom-4 left-1/2 -translate-x-1/2",
    animation: "slide-in-from-bottom-full",
  },
}

function GlassNotificationContainer({ position = "bottom-right" }: { position?: NotificationPosition }) {
  const { notifications, removeNotification } = useNotification()
  const positionConfig = positionStyles[position]

  return (
    <div
      className={cn("fixed z-50 flex flex-col gap-3 max-w-sm w-full pointer-events-none", positionConfig.container)}
      role="region"
      aria-label="Notifications"
    >
      {notifications.map((notification, index) => (
        <GlassNotificationItem
          key={notification.id}
          notification={notification}
          onClose={() => removeNotification(notification.id)}
          animationClass={positionConfig.animation}
          style={{
            transform: `scale(${1 - index * 0.02})`,
            opacity: 1 - index * 0.1,
          }}
        />
      ))}
    </div>
  )
}

interface GlassNotificationItemProps {
  notification: Notification
  onClose: () => void
  style?: React.CSSProperties
  animationClass?: string
}

function GlassNotificationItem({
  notification,
  onClose,
  style,
  animationClass = "slide-in-from-right-full",
}: GlassNotificationItemProps) {
  const config = typeConfig[notification.type]
  const Icon = config.icon
  const [progress, setProgress] = React.useState(100)
  const duration = notification.duration || 5000

  React.useEffect(() => {
    if (duration === 0) return

    const interval = setInterval(() => {
      setProgress((prev) => {
        const newProgress = prev - 100 / (duration / 100)
        return newProgress <= 0 ? 0 : newProgress
      })
    }, 100)

    return () => clearInterval(interval)
  }, [duration])

  return (
    <div
      className={cn("pointer-events-auto animate-in fade-in duration-300", animationClass)}
      style={style}
      role="alert"
    >
      <div className="relative">
        <div className={cn("absolute -inset-1.5 rounded-xl bg-linear-to-r blur-xl opacity-60", config.gradient)} />

        {/* Main container with enhanced glass */}
        <div
          className={cn(
            "relative rounded-xl border",
            "bg-white/10 backdrop-blur-2xl",
            "shadow-[0_8px_32px_rgba(0,0,0,0.3),inset_0_1px_1px_rgba(255,255,255,0.15)]",
            "overflow-hidden",
            config.border,
          )}
        >
          {/* Glass highlight layers */}
          <div className="absolute inset-0 rounded-xl bg-linear-to-b from-white/15 to-transparent pointer-events-none" />
          <div className="absolute inset-0 rounded-xl bg-linear-to-tr from-transparent to-white/10 pointer-events-none" />

          <div className="relative p-4 flex items-start gap-3">
            <div
              className={cn(
                "flex items-center justify-center w-8 h-8 rounded-lg shrink-0",
                "border border-white/10",
                `bg-linear-to-br ${config.gradient}`,
              )}
            >
              <Icon className={cn("w-5 h-5", config.iconColor)} aria-hidden="true" />
            </div>

            <div className="flex-1 min-w-0">
              <h4 className="font-medium text-white">{notification.title}</h4>
              {notification.description && <p className="mt-1 text-sm text-white/60">{notification.description}</p>}
            </div>

            <button
              onClick={onClose}
              aria-label="Dismiss notification"
              className="shrink-0 p-1.5 rounded-lg text-white/40 hover:text-white hover:bg-white/10 transition-colors"
            >
              <X className="w-4 h-4" />
            </button>
          </div>

          {/* Progress bar */}
          {duration !== 0 && (
            <div className="h-1 bg-white/5">
              <div
                className={cn("h-full transition-all duration-100 ease-linear", `bg-linear-to-r ${config.gradient}`)}
                style={{ width: `${progress}%` }}
                role="progressbar"
                aria-valuenow={progress}
                aria-valuemin={0}
                aria-valuemax={100}
              />
            </div>
          )}
        </div>
      </div>
    </div>
  )
}

// Standalone notification component for demos
export function GlassNotification({
  type = "info",
  title,
  description,
  className,
}: {
  type?: NotificationType
  title: string
  description?: string
  className?: string
}) {
  const config = typeConfig[type]
  const Icon = config.icon

  return (
    <div className={cn("relative", className)}>
      <div className={cn("absolute -inset-1.5 rounded-xl bg-linear-to-r blur-xl opacity-60", config.gradient)} />
      <div
        className={cn(
          "relative rounded-xl border",
          "bg-white/10 backdrop-blur-2xl",
          "shadow-[0_8px_32px_rgba(0,0,0,0.3),inset_0_1px_1px_rgba(255,255,255,0.15)]",
          config.border,
        )}
      >
        <div className="absolute inset-0 rounded-xl bg-linear-to-b from-white/15 to-transparent pointer-events-none" />
        <div className="relative p-4 flex items-start gap-3">
          <div
            className={cn(
              "flex items-center justify-center w-8 h-8 rounded-lg shrink-0 border border-white/10",
              `bg-linear-to-br ${config.gradient}`,
            )}
          >
            <Icon className={cn("w-5 h-5", config.iconColor)} />
          </div>
          <div className="flex-1 min-w-0">
            <h4 className="font-medium text-white">{title}</h4>
            {description && <p className="mt-1 text-sm text-white/60">{description}</p>}
          </div>
        </div>
      </div>
    </div>
  )
}

export { GlassNotificationItem }
export type { NotificationPosition }

Installation

npx shadcn@latest add @einui/glass-notification

Usage

import { GlassNotification } from "@/components/glass-notification"
<GlassNotification />