From c8b169da8cb7eae0957e0ab5e7370b071093a224 Mon Sep 17 00:00:00 2001
From: soryu <soryu@soryu.co>
Date: Tue, 28 Apr 2026 00:18:40 +0100
Subject: 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
---
 frontend/src/components/document/Toast.tsx | 97 ++++++++++++++++++++++++++++++
 1 file changed, 97 insertions(+)
 create mode 100644 frontend/src/components/document/Toast.tsx

(limited to 'frontend/src/components/document/Toast.tsx')

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>
+  );
+}
-- 
cgit v1.2.3

