diff --git a/app/playground/notification-demo.tsx b/app/playground/notification-demo.tsx new file mode 100644 index 0000000..56a65a7 --- /dev/null +++ b/app/playground/notification-demo.tsx @@ -0,0 +1,76 @@ +'use client' + +import { useState } from 'react' +import { Terminal, TerminalCommand, TerminalOutput, TerminalNotification } from '@/components/terminal' + +export function NotificationDemo() { + const [notifications, setNotifications] = useState< + Array<{ id: number; variant: any; message: string }> + >([]) + const [nextId, setNextId] = useState(1) + + const addNotification = (variant: any, message: string) => { + const id = nextId + setNextId(nextId + 1) + setNotifications((prev) => [...prev, { id, variant, message }]) + } + + const removeNotification = (id: number) => { + setNotifications((prev) => prev.filter((n) => n.id !== id)) + } + + return ( + <> + + show-notifications + +
+ + + + + +
+
+
+ + {/* Render notifications */} + {notifications.map((notif) => ( + removeNotification(notif.id)} + /> + ))} + + ) +} diff --git a/app/playground/page.tsx b/app/playground/page.tsx index f6a7bde..fa9a7cb 100644 --- a/app/playground/page.tsx +++ b/app/playground/page.tsx @@ -5,6 +5,7 @@ import { LogDemo } from './log-demo' import { PromptDemo } from './prompt-demo' import { TreeDemo } from './tree-demo' import { TreeKeyboardDemo } from './tree-keyboard-demo' +import { NotificationDemo } from './notification-demo' export const metadata = { title: 'Playground', @@ -175,6 +176,16 @@ export default function PlaygroundPage() { + +
+

+ TerminalNotification +

+

+ Toast-style notifications with auto-dismiss. Click buttons to trigger different types. +

+ +
) } diff --git a/components/terminal-notification.tsx b/components/terminal-notification.tsx new file mode 100644 index 0000000..234f392 --- /dev/null +++ b/components/terminal-notification.tsx @@ -0,0 +1,183 @@ +'use client' + +import { ReactNode, useEffect, useState } from 'react' +import { X } from 'lucide-react' + +export interface TerminalNotificationProps { + /** Notification message */ + message: string | ReactNode + /** Notification type */ + variant?: 'neutral' | 'success' | 'error' | 'warning' | 'info' + /** Auto-dismiss duration in ms (default: 5000, 0 = no auto-dismiss) */ + duration?: number + /** Callback when notification is dismissed */ + onDismiss?: () => void + /** Position on screen (default: 'top-right') */ + position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' + /** Show close button (default: true) */ + closable?: boolean + /** Additional CSS classes */ + className?: string +} + +const variantConfig: Record< + NonNullable, + { icon: string; borderColor: string; bgColor: string } +> = { + neutral: { + icon: 'ℹ', + borderColor: 'var(--glass-border)', + bgColor: 'var(--term-bg-panel)', + }, + success: { + icon: '✓', + borderColor: 'var(--term-green)', + bgColor: 'color-mix(in oklab, var(--term-green) 8%, var(--term-bg-panel))', + }, + error: { + icon: '✗', + borderColor: 'var(--term-red)', + bgColor: 'color-mix(in oklab, var(--term-red) 8%, var(--term-bg-panel))', + }, + warning: { + icon: '⚠', + borderColor: 'var(--term-yellow)', + bgColor: 'color-mix(in oklab, var(--term-yellow) 8%, var(--term-bg-panel))', + }, + info: { + icon: 'ℹ', + borderColor: 'var(--term-blue)', + bgColor: 'color-mix(in oklab, var(--term-blue) 8%, var(--term-bg-panel))', + }, +} + +const positionClasses: Record, string> = { + 'top-right': 'top-4 right-4', + 'top-left': 'top-4 left-4', + 'bottom-right': 'bottom-4 right-4', + 'bottom-left': 'bottom-4 left-4', +} + +/** + * Toast-style notification component with auto-dismiss and animations. + * Displays temporary alerts in terminal aesthetic. + * + * @param message - Notification message content + * @param variant - Notification type (neutral, success, error, warning, info) + * @param duration - Auto-dismiss time in ms (0 = no auto-dismiss, default: 5000) + * @param onDismiss - Callback when notification is dismissed + * @param position - Screen position (default: 'top-right') + * @param closable - Show close button (default: true) + * @param className - Additional classes applied to the root element + * + * @example + * ```tsx + * console.log('dismissed')} + * /> + * + * + * ``` + */ +export function TerminalNotification({ + message, + variant = 'neutral', + duration = 5000, + onDismiss, + position = 'top-right', + closable = true, + className = '', +}: TerminalNotificationProps) { + const [visible, setVisible] = useState(true) + const [progress, setProgress] = useState(100) + + const config = variantConfig[variant] + const positionClass = positionClasses[position] + + useEffect(() => { + if (duration <= 0) return + + const startTime = Date.now() + const interval = setInterval(() => { + const elapsed = Date.now() - startTime + const remaining = Math.max(0, duration - elapsed) + const percentRemaining = (remaining / duration) * 100 + setProgress(percentRemaining) + + if (remaining === 0) { + clearInterval(interval) + handleDismiss() + } + }, 50) + + return () => clearInterval(interval) + }, [duration]) + + const handleDismiss = () => { + setVisible(false) + setTimeout(() => { + onDismiss?.() + }, 200) // Wait for fade-out animation + } + + if (!visible) return null + + return ( +
+
+ {/* Icon */} + + {config.icon} + + + {/* Message */} +
{message}
+ + {/* Close button */} + {closable && ( + + )} +
+ + {/* Progress bar */} + {duration > 0 && ( +
+
+
+ )} +
+ ) +} diff --git a/components/terminal.tsx b/components/terminal.tsx index 56c9e1f..5c3ab7e 100644 --- a/components/terminal.tsx +++ b/components/terminal.tsx @@ -264,3 +264,4 @@ export { TerminalAutocomplete, useAutocomplete, COMMON_COMMANDS, COMMON_FLAGS, f export { TerminalGhosttyTheme, GhosttyThemePicker } from './terminal-ghostty' export { ThemeSwitcher } from './theme-switcher' export { TerminalBadge } from './terminal-badge' +export { TerminalNotification } from './terminal-notification'