142 lines
3.6 KiB
JavaScript
142 lines
3.6 KiB
JavaScript
/**
|
||
* Toast Component - Minimal confirmations
|
||
*
|
||
* DESIGN:
|
||
* - Copy/applied/saved/reverted appear as brief toasts
|
||
* - Don't add to transcript (displayed separately)
|
||
* - Auto-dismiss after timeout
|
||
*/
|
||
|
||
import React, { useState, useEffect } from 'react';
|
||
import { Box, Text } from 'ink';
|
||
import { colors } from '../../tui-theme.mjs';
|
||
import { icon } from '../../icons.mjs';
|
||
import { getCapabilities } from '../../terminal-profile.mjs';
|
||
|
||
const h = React.createElement;
|
||
|
||
/**
|
||
* Toast - Single toast notification
|
||
*/
|
||
const Toast = ({
|
||
message,
|
||
type = 'info', // info, success, warning, error
|
||
duration = 3000,
|
||
onDismiss = null
|
||
}) => {
|
||
const caps = getCapabilities();
|
||
const [visible, setVisible] = useState(true);
|
||
|
||
useEffect(() => {
|
||
const timer = setTimeout(() => {
|
||
setVisible(false);
|
||
onDismiss?.();
|
||
}, duration);
|
||
return () => clearTimeout(timer);
|
||
}, [duration, onDismiss]);
|
||
|
||
if (!visible) return null;
|
||
|
||
const typeConfig = {
|
||
info: { color: colors.accent, icon: caps.unicodeOK ? 'ℹ' : 'i' },
|
||
success: { color: colors.success, icon: caps.unicodeOK ? '✓' : '+' },
|
||
warning: { color: colors.warning, icon: caps.unicodeOK ? '⚠' : '!' },
|
||
error: { color: colors.error, icon: caps.unicodeOK ? '✗' : 'X' }
|
||
};
|
||
|
||
const config = typeConfig[type] || typeConfig.info;
|
||
|
||
return h(Box, {
|
||
flexDirection: 'row',
|
||
justifyContent: 'flex-end',
|
||
paddingX: 1
|
||
},
|
||
h(Text, { color: config.color }, config.icon + ' '),
|
||
h(Text, { color: config.color }, message)
|
||
);
|
||
};
|
||
|
||
/**
|
||
* ToastContainer - Manages multiple toasts
|
||
*/
|
||
const ToastContainer = ({ toasts = [], onDismiss }) => {
|
||
if (toasts.length === 0) return null;
|
||
|
||
return h(Box, {
|
||
flexDirection: 'column',
|
||
position: 'absolute',
|
||
right: 0,
|
||
top: 0
|
||
},
|
||
...toasts.map((toast, i) =>
|
||
h(Toast, {
|
||
key: toast.id || i,
|
||
message: toast.message,
|
||
type: toast.type,
|
||
duration: toast.duration,
|
||
onDismiss: () => onDismiss?.(toast.id || i)
|
||
})
|
||
)
|
||
);
|
||
};
|
||
|
||
/**
|
||
* useToasts - Hook for managing toasts
|
||
*/
|
||
const createToastManager = () => {
|
||
let toasts = [];
|
||
let listeners = [];
|
||
let nextId = 0;
|
||
|
||
const subscribe = (listener) => {
|
||
listeners.push(listener);
|
||
return () => {
|
||
listeners = listeners.filter(l => l !== listener);
|
||
};
|
||
};
|
||
|
||
const notify = () => {
|
||
listeners.forEach(l => l(toasts));
|
||
};
|
||
|
||
const add = (message, type = 'info', duration = 3000) => {
|
||
const id = nextId++;
|
||
toasts = [...toasts, { id, message, type, duration }];
|
||
notify();
|
||
|
||
setTimeout(() => {
|
||
toasts = toasts.filter(t => t.id !== id);
|
||
notify();
|
||
}, duration);
|
||
|
||
return id;
|
||
};
|
||
|
||
const dismiss = (id) => {
|
||
toasts = toasts.filter(t => t.id !== id);
|
||
notify();
|
||
};
|
||
|
||
return { subscribe, add, dismiss, get: () => toasts };
|
||
};
|
||
|
||
// Global toast manager (singleton)
|
||
const toastManager = createToastManager();
|
||
|
||
// Convenience methods
|
||
const showToast = (message, type, duration) => toastManager.add(message, type, duration);
|
||
const showSuccess = (message) => showToast(message, 'success', 2000);
|
||
const showError = (message) => showToast(message, 'error', 4000);
|
||
const showInfo = (message) => showToast(message, 'info', 3000);
|
||
|
||
export default Toast;
|
||
export {
|
||
Toast,
|
||
ToastContainer,
|
||
toastManager,
|
||
showToast,
|
||
showSuccess,
|
||
showError,
|
||
showInfo
|
||
};
|