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();
+
+
+
+
+
+ {{ ui.toast.message }}
+
+
+
+
+
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 };
});