diff --git a/frontend/src/components/AppShell.vue b/frontend/src/components/AppShell.vue index cc49789..b58d307 100644 --- a/frontend/src/components/AppShell.vue +++ b/frontend/src/components/AppShell.vue @@ -8,6 +8,7 @@ import { useUiStore } from "../stores/ui"; import CommandPalette from "./CommandPalette.vue"; import Icon from "./Icon.vue"; import LabelsModal from "./LabelsModal.vue"; +import ToastHost from "./ToastHost.vue"; import { NOTE_SWATCH_CLASSES, type NoteColor } from "../notes/colors"; const route = useRoute(); @@ -261,6 +262,8 @@ async function signOut() { + +
+import { useUiStore } from "../stores/ui"; + +const ui = useUiStore(); + + + diff --git a/frontend/src/stores/notes.ts b/frontend/src/stores/notes.ts index 49fe918..331b63f 100644 --- a/frontend/src/stores/notes.ts +++ b/frontend/src/stores/notes.ts @@ -1,6 +1,7 @@ import { defineStore } from "pinia"; import { ref } from "vue"; import { api } from "../api/client"; +import { useUiStore } from "./ui"; import type { NoteColor } from "../notes/colors"; export type NoteView = "active" | "archived" | "trash"; @@ -103,7 +104,10 @@ export const useNotesStore = defineStore("notes", () => { } const setPinned = (id: string, pinned: boolean) => mutate(id, { pinned }); - const setArchived = (id: string, archived: boolean) => mutate(id, { archived }); + const setArchived = async (id: string, archived: boolean): Promise => { + await mutate(id, { archived }); + if (archived) useUiStore().showToast("Note archived", "Undo", () => void setArchived(id, false)); + }; const setColor = (id: string, color: NoteColor) => mutate(id, { color }); const setKind = (id: string, kind: NoteKind) => mutate(id, { kind }); const setReminder = (id: string, remindAt: string | null) => mutate(id, { remind_at: remindAt }); @@ -172,6 +176,7 @@ export const useNotesStore = defineStore("notes", () => { async function trash(id: string): Promise { reconcile(await api.post(`/api/notes/${id}/trash`)); + useUiStore().showToast("Note moved to trash", "Undo", () => void restore(id)); } async function restore(id: string): Promise { diff --git a/frontend/src/stores/ui.ts b/frontend/src/stores/ui.ts index 0c49810..2e470a3 100644 --- a/frontend/src/stores/ui.ts +++ b/frontend/src/stores/ui.ts @@ -1,6 +1,13 @@ import { defineStore } from "pinia"; import { ref } from "vue"; +interface Toast { + id: number; + message: string; + actionLabel: string; + action: () => void; +} + // Cross-component UI signals that don't belong to any single view (e.g. a // global shortcut in the app shell asking the board to act). export const useUiStore = defineStore("ui", () => { @@ -10,5 +17,31 @@ export const useUiStore = defineStore("ui", () => { composeTick.value++; } - return { composeTick, requestCompose }; + // Transient undo toast (e.g. after trash/archive). + const toast = ref(null); + let toastTimer: ReturnType | undefined; + let toastSeq = 0; + + function showToast(message: string, actionLabel: string, action: () => void) { + toastSeq += 1; + const id = toastSeq; + toast.value = { id, message, actionLabel, action }; + clearTimeout(toastTimer); + toastTimer = setTimeout(() => { + if (toast.value?.id === id) toast.value = null; + }, 5000); + } + + function dismissToast() { + clearTimeout(toastTimer); + toast.value = null; + } + + function runToastAction() { + const action = toast.value?.action; + dismissToast(); + action?.(); + } + + return { composeTick, requestCompose, toast, showToast, dismissToast, runToastAction }; });