From bf5e9ff8b641db2268a5195eaa431e28a5b64eb9 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 15 Feb 2026 18:41:27 +0200 Subject: [PATCH] feat: add Zustand stores with undo/redo and debounced persistence - App store: theme, view routing, board list, settings - Board store: all card/column/label/checklist/attachment mutations - zundo temporal middleware for undo/redo (50 step limit) - Debounced saves (500ms) with immediate flush on close --- src/stores/app-store.ts | 69 ++++++ src/stores/board-store.ts | 469 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 538 insertions(+) create mode 100644 src/stores/app-store.ts create mode 100644 src/stores/board-store.ts diff --git a/src/stores/app-store.ts b/src/stores/app-store.ts new file mode 100644 index 0000000..3f2e7c5 --- /dev/null +++ b/src/stores/app-store.ts @@ -0,0 +1,69 @@ +import { create } from "zustand"; +import type { AppSettings } from "@/types/settings"; +import type { BoardMeta } from "@/types/board"; +import { loadSettings, saveSettings, listBoards, ensureDataDirs } from "@/lib/storage"; + +export type View = { type: "board-list" } | { type: "board"; boardId: string }; + +interface AppState { + settings: AppSettings; + boards: BoardMeta[]; + view: View; + initialized: boolean; + + init: () => Promise; + setTheme: (theme: AppSettings["theme"]) => void; + setView: (view: View) => void; + refreshBoards: () => Promise; + addRecentBoard: (boardId: string) => void; +} + +function applyTheme(theme: AppSettings["theme"]): void { + const root = document.documentElement; + if (theme === "system") { + const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; + root.classList.toggle("dark", prefersDark); + } else { + root.classList.toggle("dark", theme === "dark"); + } +} + +export const useAppStore = create((set, get) => ({ + settings: { theme: "system", dataDirectory: null, recentBoardIds: [] }, + boards: [], + view: { type: "board-list" }, + initialized: false, + + init: async () => { + await ensureDataDirs(); + const settings = await loadSettings(); + const boards = await listBoards(); + set({ settings, boards, initialized: true }); + applyTheme(settings.theme); + }, + + setTheme: (theme) => { + const settings = { ...get().settings, theme }; + set({ settings }); + saveSettings(settings); + applyTheme(theme); + }, + + setView: (view) => set({ view }), + + refreshBoards: async () => { + const boards = await listBoards(); + set({ boards }); + }, + + addRecentBoard: (boardId) => { + const settings = get().settings; + const recent = [ + boardId, + ...settings.recentBoardIds.filter((id) => id !== boardId), + ].slice(0, 10); + const updated = { ...settings, recentBoardIds: recent }; + set({ settings: updated }); + saveSettings(updated); + }, +})); diff --git a/src/stores/board-store.ts b/src/stores/board-store.ts new file mode 100644 index 0000000..9cbcdee --- /dev/null +++ b/src/stores/board-store.ts @@ -0,0 +1,469 @@ +import { create } from "zustand"; +import { temporal } from "zundo"; +import { ulid } from "ulid"; +import type { + Board, + Card, + Label, + ChecklistItem, + Attachment, + ColumnWidth, +} from "@/types/board"; +import { saveBoard, loadBoard } from "@/lib/storage"; + +interface BoardState { + board: Board | null; + saving: boolean; + lastSaved: number | null; +} + +interface BoardActions { + openBoard: (boardId: string) => Promise; + closeBoard: () => void; + + addColumn: (title: string) => void; + updateColumnTitle: (columnId: string, title: string) => void; + deleteColumn: (columnId: string) => void; + moveColumn: (fromIndex: number, toIndex: number) => void; + setColumnWidth: (columnId: string, width: ColumnWidth) => void; + + addCard: (columnId: string, title: string) => string; + updateCard: (cardId: string, updates: Partial) => void; + deleteCard: (cardId: string) => void; + moveCard: ( + cardId: string, + fromColumnId: string, + toColumnId: string, + toIndex: number + ) => void; + + addLabel: (name: string, color: string) => void; + updateLabel: (labelId: string, updates: Partial