diff --git a/src/App.tsx b/src/App.tsx
index 372008e..11bc6ae 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -7,6 +7,7 @@ import { BoardList } from "@/components/boards/BoardList";
import { BoardView } from "@/components/board/BoardView";
import { CommandPalette } from "@/components/command-palette/CommandPalette";
import { SettingsDialog } from "@/components/settings/SettingsDialog";
+import { ToastContainer } from "@/components/toast/ToastContainer";
import { useKeyboardShortcuts } from "@/hooks/useKeyboardShortcuts";
export default function App() {
@@ -64,6 +65,7 @@ export default function App() {
+
>
);
}
diff --git a/src/components/boards/BoardCard.tsx b/src/components/boards/BoardCard.tsx
index 20b9686..e8f2ee4 100644
--- a/src/components/boards/BoardCard.tsx
+++ b/src/components/boards/BoardCard.tsx
@@ -21,6 +21,7 @@ import { Button } from "@/components/ui/button";
import type { BoardMeta } from "@/types/board";
import { useAppStore } from "@/stores/app-store";
import { useBoardStore } from "@/stores/board-store";
+import { useToastStore } from "@/stores/toast-store";
import { deleteBoard, loadBoard, saveBoard } from "@/lib/storage";
interface BoardCardProps {
@@ -31,6 +32,7 @@ interface BoardCardProps {
export function BoardCard({ board, index = 0 }: BoardCardProps) {
const [confirmDelete, setConfirmDelete] = useState(false);
const prefersReducedMotion = useReducedMotion();
+ const addToast = useToastStore((s) => s.addToast);
const setView = useAppStore((s) => s.setView);
const addRecentBoard = useAppStore((s) => s.addRecentBoard);
@@ -51,6 +53,7 @@ export function BoardCard({ board, index = 0 }: BoardCardProps) {
await deleteBoard(board.id);
await refreshBoards();
setConfirmDelete(false);
+ addToast(`"${board.title}" deleted`, "info");
}
async function handleDuplicate() {
@@ -66,6 +69,7 @@ export function BoardCard({ board, index = 0 }: BoardCardProps) {
};
await saveBoard(duplicated);
await refreshBoards();
+ addToast(`"${board.title}" duplicated`, "success");
}
return (
diff --git a/src/components/import-export/ImportExportButtons.tsx b/src/components/import-export/ImportExportButtons.tsx
index a9a6caf..c0e15ab 100644
--- a/src/components/import-export/ImportExportButtons.tsx
+++ b/src/components/import-export/ImportExportButtons.tsx
@@ -9,6 +9,7 @@ import {
} from "@/components/ui/dropdown-menu";
import { useAppStore } from "@/stores/app-store";
import { useBoardStore } from "@/stores/board-store";
+import { useToastStore } from "@/stores/toast-store";
import { saveBoard } from "@/lib/storage";
import {
exportBoardAsJson,
@@ -31,6 +32,7 @@ function downloadBlob(content: string, filename: string, mimeType: string) {
export function ImportExportButtons() {
const fileInputRef = useRef(null);
+ const addToast = useToastStore((s) => s.addToast);
const board = useBoardStore((s) => s.board);
const refreshBoards = useAppStore((s) => s.refreshBoards);
const setView = useAppStore((s) => s.setView);
@@ -42,6 +44,7 @@ export function ImportExportButtons() {
const json = exportBoardAsJson(board);
const safeName = board.title.replace(/[^a-zA-Z0-9-_]/g, "_");
downloadBlob(json, `${safeName}.json`, "application/json");
+ addToast("Board exported as JSON", "success");
}
function handleExportCsv() {
@@ -49,6 +52,7 @@ export function ImportExportButtons() {
const csv = exportBoardAsCsv(board);
const safeName = board.title.replace(/[^a-zA-Z0-9-_]/g, "_");
downloadBlob(csv, `${safeName}.csv`, "text/csv");
+ addToast("Board exported as CSV", "success");
}
function handleImportClick() {
@@ -77,9 +81,10 @@ export function ImportExportButtons() {
await openBoard(imported.id);
setView({ type: "board", boardId: imported.id });
addRecentBoard(imported.id);
+ addToast("Board imported successfully", "success");
} catch (err) {
console.error("Import failed:", err);
- // Could show a toast here in the future
+ addToast("Import failed — check file format", "error");
}
// Reset the input so the same file can be re-imported
diff --git a/src/components/toast/ToastContainer.tsx b/src/components/toast/ToastContainer.tsx
new file mode 100644
index 0000000..5fac598
--- /dev/null
+++ b/src/components/toast/ToastContainer.tsx
@@ -0,0 +1,31 @@
+import { AnimatePresence, motion } from "framer-motion";
+import { useToastStore } from "@/stores/toast-store";
+
+const TYPE_STYLES = {
+ success: "bg-pylon-accent/10 text-pylon-accent border-pylon-accent/20",
+ error: "bg-pylon-danger/10 text-pylon-danger border-pylon-danger/20",
+ info: "bg-pylon-surface text-pylon-text border-border",
+} as const;
+
+export function ToastContainer() {
+ const toasts = useToastStore((s) => s.toasts);
+
+ return (
+
+
+ {toasts.map((toast) => (
+
+ {toast.message}
+
+ ))}
+
+
+ );
+}
diff --git a/src/stores/toast-store.ts b/src/stores/toast-store.ts
new file mode 100644
index 0000000..f918f39
--- /dev/null
+++ b/src/stores/toast-store.ts
@@ -0,0 +1,33 @@
+import { create } from "zustand";
+
+export type ToastType = "success" | "error" | "info";
+
+interface Toast {
+ id: string;
+ message: string;
+ type: ToastType;
+}
+
+interface ToastState {
+ toasts: Toast[];
+ addToast: (message: string, type?: ToastType) => void;
+ removeToast: (id: string) => void;
+}
+
+let nextId = 0;
+
+export const useToastStore = create((set) => ({
+ toasts: [],
+
+ addToast: (message, type = "info") => {
+ const id = String(++nextId);
+ set((s) => ({ toasts: [...s.toasts, { id, message, type }] }));
+ setTimeout(() => {
+ set((s) => ({ toasts: s.toasts.filter((t) => t.id !== id) }));
+ }, 3000);
+ },
+
+ removeToast: (id) => {
+ set((s) => ({ toasts: s.toasts.filter((t) => t.id !== id) }));
+ },
+}));