Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions app/playground/notification-demo.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<Terminal title="notification-demo.sh">
<TerminalCommand>show-notifications</TerminalCommand>
<TerminalOutput type="info">
<div className="flex flex-wrap gap-2">
<button
onClick={() => addNotification('success', 'Build completed successfully!')}
className="rounded border border-[var(--term-green)] bg-[color-mix(in_oklab,var(--term-green)_12%,transparent)] px-3 py-1.5 font-mono text-xs text-[var(--term-green)] transition-colors hover:bg-[color-mix(in_oklab,var(--term-green)_20%,transparent)]"
>
Success
</button>
<button
onClick={() => addNotification('error', 'Connection failed. Please try again.')}
className="rounded border border-[var(--term-red)] bg-[color-mix(in_oklab,var(--term-red)_12%,transparent)] px-3 py-1.5 font-mono text-xs text-[var(--term-red)] transition-colors hover:bg-[color-mix(in_oklab,var(--term-red)_20%,transparent)]"
>
Error
</button>
<button
onClick={() =>
addNotification('warning', 'Disk space running low (15% remaining)')
}
className="rounded border border-[var(--term-yellow)] bg-[color-mix(in_oklab,var(--term-yellow)_12%,transparent)] px-3 py-1.5 font-mono text-xs text-[var(--term-yellow)] transition-colors hover:bg-[color-mix(in_oklab,var(--term-yellow)_20%,transparent)]"
>
Warning
</button>
<button
onClick={() => addNotification('info', 'New version 2.0 available')}
className="rounded border border-[var(--term-blue)] bg-[color-mix(in_oklab,var(--term-blue)_12%,transparent)] px-3 py-1.5 font-mono text-xs text-[var(--term-blue)] transition-colors hover:bg-[color-mix(in_oklab,var(--term-blue)_20%,transparent)]"
>
Info
</button>
<button
onClick={() => addNotification('neutral', 'System maintenance scheduled')}
className="rounded border border-[var(--glass-border)] bg-[var(--glass-bg)] px-3 py-1.5 font-mono text-xs text-[var(--term-fg)] transition-colors hover:bg-[color-mix(in_oklab,white_8%,var(--glass-bg))]"
>
Neutral
</button>
</div>
</TerminalOutput>
</Terminal>

{/* Render notifications */}
{notifications.map((notif) => (
<TerminalNotification
key={notif.id}
message={notif.message}
variant={notif.variant}
duration={5000}
onDismiss={() => removeNotification(notif.id)}
/>
))}
</>
)
}
11 changes: 11 additions & 0 deletions app/playground/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -175,6 +176,16 @@ export default function PlaygroundPage() {
</TerminalOutput>
</Terminal>
</section>

<section className="flex flex-col gap-2">
<h2 className="text-lg font-semibold font-mono text-[var(--term-fg)]">
TerminalNotification
</h2>
<p className="text-sm text-[var(--term-fg-dim)] font-mono">
Toast-style notifications with auto-dismiss. Click buttons to trigger different types.
</p>
<NotificationDemo />
</section>
</main>
)
}
183 changes: 183 additions & 0 deletions components/terminal-notification.tsx
Original file line number Diff line number Diff line change
@@ -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<TerminalNotificationProps['variant']>,
{ 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<NonNullable<TerminalNotificationProps['position']>, 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
* <TerminalNotification
* message="Build successful!"
* variant="success"
* duration={3000}
* onDismiss={() => console.log('dismissed')}
* />
*
* <TerminalNotification
* message="Error: Connection failed"
* variant="error"
* duration={0}
* position="bottom-right"
* />
* ```
*/
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 (
<div
className={`fixed ${positionClass} z-50 animate-in fade-in slide-in-from-top-2 ${
visible ? '' : 'animate-out fade-out'
} ${className}`.trim()}
role="alert"
aria-live="polite"
>
<div
className="flex min-w-[280px] max-w-[400px] items-start gap-3 rounded-lg border p-4 font-mono text-sm shadow-lg backdrop-blur-sm"
style={{
borderColor: config.borderColor,
backgroundColor: config.bgColor,
}}
>
{/* Icon */}
<span className="flex-shrink-0 text-lg" style={{ color: config.borderColor }}>
{config.icon}
</span>

{/* Message */}
<div className="flex-1 text-[var(--term-fg)]">{message}</div>

{/* Close button */}
{closable && (
<button
type="button"
onClick={handleDismiss}
className="flex-shrink-0 text-[var(--term-fg-dim)] hover:text-[var(--term-fg)] transition-colors"
aria-label="Close notification"
>
<X size={16} />
</button>
)}
</div>

{/* Progress bar */}
{duration > 0 && (
<div className="mt-1 h-1 overflow-hidden rounded-full bg-[var(--glass-bg)]">
<div
className="h-full transition-all duration-100 ease-linear"
style={{
width: `${progress}%`,
backgroundColor: config.borderColor,
}}
/>
</div>
)}
</div>
)
}
1 change: 1 addition & 0 deletions components/terminal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'