summaryrefslogtreecommitdiff
path: root/frontend/src/components/document/Toast.tsx
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-04-28 00:18:40 +0100
committerGitHub <noreply@github.com>2026-04-28 00:18:40 +0100
commitc8b169da8cb7eae0957e0ab5e7370b071093a224 (patch)
treec3f9720a8acfe863ac0b65df9439abf9a941323a /frontend/src/components/document/Toast.tsx
parent3679ceb3325033faa2f889ef3dfee5668ef7aeea (diff)
downloadsoryu-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.tsx97
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>
+ );
+}