diff options
| author | soryu <soryu@soryu.co> | 2026-04-28 00:18:40 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-04-28 00:18:40 +0100 |
| commit | c8b169da8cb7eae0957e0ab5e7370b071093a224 (patch) | |
| tree | c3f9720a8acfe863ac0b65df9439abf9a941323a /frontend/src/components/document/Toast.tsx | |
| parent | 3679ceb3325033faa2f889ef3dfee5668ef7aeea (diff) | |
| download | soryu-c8b169da8cb7eae0957e0ab5e7370b071093a224.tar.gz soryu-c8b169da8cb7eae0957e0ab5e7370b071093a224.zip | |
feat: Document UI for directive orchestration with Lexical editor (#93)
* WIP: heartbeat checkpoint
* feat: soryu-co/soryu - makima: Save previous goal on update and include history in re-planning prompt
* feat: soryu-co/soryu - makima: Install Lexical and create base document editor component
* feat: soryu-co/soryu - makima: Create directive file system sidebar and document layout
* feat: soryu-co/soryu - makima: Create custom Lexical step diagram block
* feat: soryu-co/soryu - makima: Add context menu and goal auto-update integration
* WIP: heartbeat checkpoint
Diffstat (limited to 'frontend/src/components/document/Toast.tsx')
| -rw-r--r-- | frontend/src/components/document/Toast.tsx | 97 |
1 files changed, 97 insertions, 0 deletions
diff --git a/frontend/src/components/document/Toast.tsx b/frontend/src/components/document/Toast.tsx new file mode 100644 index 0000000..653db8f --- /dev/null +++ b/frontend/src/components/document/Toast.tsx @@ -0,0 +1,97 @@ +import { + createContext, + useCallback, + useContext, + useEffect, + useRef, + useState, + type ReactNode, +} from 'react'; +import './Toast.css'; + +// -- Types ------------------------------------------------------------------- + +export type ToastType = 'success' | 'error' | 'info'; + +interface ToastItem { + id: number; + message: string; + type: ToastType; +} + +interface ToastContextValue { + addToast: (message: string, type?: ToastType) => void; +} + +// -- Context ----------------------------------------------------------------- + +const ToastContext = createContext<ToastContextValue | null>(null); + +export function useToast(): ToastContextValue { + const ctx = useContext(ToastContext); + if (!ctx) throw new Error('useToast must be used within a ToastProvider'); + return ctx; +} + +// -- Provider ---------------------------------------------------------------- + +const DISMISS_MS = 3000; + +export function ToastProvider({ children }: { children: ReactNode }) { + const [toasts, setToasts] = useState<ToastItem[]>([]); + const nextId = useRef(0); + + const addToast = useCallback((message: string, type: ToastType = 'info') => { + const id = nextId.current++; + setToasts((prev) => [...prev, { id, message, type }]); + }, []); + + const removeToast = useCallback((id: number) => { + setToasts((prev) => prev.filter((t) => t.id !== id)); + }, []); + + return ( + <ToastContext.Provider value={{ addToast }}> + {children} + <div className="toast-container"> + {toasts.map((t) => ( + <ToastItem key={t.id} toast={t} onDismiss={removeToast} /> + ))} + </div> + </ToastContext.Provider> + ); +} + +// -- Single toast ------------------------------------------------------------ + +function ToastItem({ + toast, + onDismiss, +}: { + toast: ToastItem; + onDismiss: (id: number) => void; +}) { + const [exiting, setExiting] = useState(false); + + useEffect(() => { + const timer = setTimeout(() => setExiting(true), DISMISS_MS - 300); + const remove = setTimeout(() => onDismiss(toast.id), DISMISS_MS); + return () => { + clearTimeout(timer); + clearTimeout(remove); + }; + }, [toast.id, onDismiss]); + + const icon = + toast.type === 'success' ? '\u2713' : toast.type === 'error' ? '\u2717' : '\u2139'; + + return ( + <div + className={`toast-item toast-${toast.type} ${exiting ? 'toast-exit' : 'toast-enter'}`} + role="status" + > + <span className="toast-icon">{icon}</span> + <span className="toast-message">{toast.message}</span> + </div> + ); +} |
