diff --git a/src/stores/batch-store.ts b/src/stores/batch-store.ts new file mode 100644 index 0000000..193c784 --- /dev/null +++ b/src/stores/batch-store.ts @@ -0,0 +1,118 @@ +/** + * Batch Store - manages batch processing state. + * + * When active, presents items of the same type one at a time + * for rapid completion. Tracks progress and completion stats. + */ + +import { create } from "zustand"; +import type { FocusItem } from "@/lib/types"; + +export type BatchItemStatus = "pending" | "done" | "skipped"; + +export interface BatchState { + /** Whether batch mode is active */ + isActive: boolean; + /** Label for the batch (e.g., "CODE REVIEWS") */ + batchLabel: string; + /** Items in this batch */ + items: FocusItem[]; + /** Status of each item (indexed same as items) */ + statuses: BatchItemStatus[]; + /** Current item index */ + currentIndex: number; + /** When the batch started (for duration tracking) */ + startedAt: number | null; + + // -- Derived -- + + /** Number of items completed (done) */ + completedCount: () => number; + /** Number of items skipped */ + skippedCount: () => number; + /** Whether all items have been processed */ + isFinished: () => boolean; + + // -- Actions -- + + /** Start a batch with the given items and label */ + startBatch: (items: FocusItem[], label: string) => void; + /** Mark current item as done and advance */ + markDone: () => void; + /** Mark current item as skipped and advance */ + markSkipped: () => void; + /** Exit batch mode */ + exitBatch: () => void; +} + +export const useBatchStore = create((set, get) => ({ + isActive: false, + batchLabel: "", + items: [], + statuses: [], + currentIndex: 0, + startedAt: null, + + completedCount: () => get().statuses.filter((s) => s === "done").length, + skippedCount: () => get().statuses.filter((s) => s === "skipped").length, + isFinished: () => { + const { statuses } = get(); + return statuses.length > 0 && statuses.every((s) => s !== "pending"); + }, + + startBatch: (items, label) => { + set({ + isActive: true, + batchLabel: label, + items, + statuses: items.map(() => "pending" as BatchItemStatus), + currentIndex: 0, + startedAt: Date.now(), + }); + }, + + markDone: () => { + const { currentIndex, statuses, items } = get(); + if (currentIndex >= items.length) return; + + const updated = [...statuses]; + updated[currentIndex] = "done"; + + const nextIndex = findNextPending(updated, currentIndex); + set({ statuses: updated, currentIndex: nextIndex }); + }, + + markSkipped: () => { + const { currentIndex, statuses, items } = get(); + if (currentIndex >= items.length) return; + + const updated = [...statuses]; + updated[currentIndex] = "skipped"; + + const nextIndex = findNextPending(updated, currentIndex); + set({ statuses: updated, currentIndex: nextIndex }); + }, + + exitBatch: () => { + set({ + isActive: false, + batchLabel: "", + items: [], + statuses: [], + currentIndex: 0, + startedAt: null, + }); + }, +})); + +/** Find the next pending item after the given index, or return items.length if none. */ +function findNextPending( + statuses: BatchItemStatus[], + afterIndex: number +): number { + for (let i = afterIndex + 1; i < statuses.length; i++) { + if (statuses[i] === "pending") return i; + } + // All remaining are processed -- return length to signal finished + return statuses.length; +} diff --git a/src/stores/capture-store.ts b/src/stores/capture-store.ts new file mode 100644 index 0000000..d4c007f --- /dev/null +++ b/src/stores/capture-store.ts @@ -0,0 +1,55 @@ +/** + * Quick Capture store. + * + * Manages the overlay state for the global Cmd+Shift+C capture flow. + */ + +import { create } from "zustand"; + +interface CaptureState { + isOpen: boolean; + isSubmitting: boolean; + lastCapturedId: string | null; + error: string | null; +} + +interface CaptureActions { + open: () => void; + close: () => void; + setSubmitting: (submitting: boolean) => void; + captureSuccess: (beadId: string) => void; + captureError: (message: string) => void; +} + +export const useCaptureStore = create((set) => ({ + isOpen: false, + isSubmitting: false, + lastCapturedId: null, + error: null, + + open: () => set({ isOpen: true, error: null }), + + close: () => + set({ + isOpen: false, + isSubmitting: false, + error: null, + }), + + setSubmitting: (submitting) => set({ isSubmitting: submitting }), + + captureSuccess: (beadId) => + set({ + lastCapturedId: beadId, + isSubmitting: false, + isOpen: false, + error: null, + }), + + captureError: (message) => + set({ + error: message, + isSubmitting: false, + isOpen: true, + }), +})); diff --git a/src/stores/focus-store.ts b/src/stores/focus-store.ts new file mode 100644 index 0000000..1d42352 --- /dev/null +++ b/src/stores/focus-store.ts @@ -0,0 +1,105 @@ +/** + * Focus Store - manages THE ONE THING state. + * + * Tracks the currently focused item, the queue of remaining items, + * and dispatches actions (start, defer, skip) that advance through the queue. + */ + +import { create } from "zustand"; +import type { FocusAction, FocusItem } from "@/lib/types"; + +export interface FocusState { + /** The current ONE THING (null when queue is empty) */ + current: FocusItem | null; + /** Remaining items after the current one */ + queue: FocusItem[]; + /** Whether we're loading data from the backend */ + isLoading: boolean; + /** Last error message */ + error: string | null; + + // -- Actions -- + + /** Set the full list of items (called after sync). First item becomes focus. */ + setItems: (items: FocusItem[]) => void; + /** Perform an action on the current item and advance to next */ + act: (action: FocusAction, reason?: string) => FocusItem | null; + /** Manually set a specific item as THE ONE THING */ + setFocus: (itemId: string) => void; + /** Reorder queue by moving an item from one index to another */ + reorderQueue: (fromIndex: number, toIndex: number) => void; + /** Set loading state */ + setLoading: (loading: boolean) => void; + /** Set error state */ + setError: (error: string | null) => void; +} + +export const useFocusStore = create((set, get) => ({ + current: null, + queue: [], + isLoading: false, + error: null, + + setItems: (items) => { + const [first, ...rest] = items; + set({ + current: first ?? null, + queue: rest, + isLoading: false, + error: null, + }); + }, + + act: (action, _reason) => { + const { current, queue } = get(); + const [next, ...rest] = queue; + + // Log the decision (will be wired to backend decision log in Phase 7) + console.debug("[focus] act:", action, "on:", current?.id); + + const nextItem = next ?? null; + set({ + current: nextItem, + queue: rest, + }); + + return nextItem; + }, + + setFocus: (itemId) => { + const { current, queue } = get(); + const allItems = current ? [current, ...queue] : [...queue]; + const target = allItems.find((item) => item.id === itemId); + + if (!target) return; + + const remaining = allItems.filter((item) => item.id !== itemId); + set({ + current: target, + queue: remaining, + }); + }, + + reorderQueue: (fromIndex, toIndex) => { + const { queue } = get(); + if ( + fromIndex < 0 || + fromIndex >= queue.length || + toIndex < 0 || + toIndex >= queue.length || + fromIndex === toIndex + ) { + return; + } + + const updated = [...queue]; + const [moved] = updated.splice(fromIndex, 1); + updated.splice(toIndex, 0, moved); + + console.debug("[focus] reorder:", moved.id, "from", fromIndex, "to", toIndex); + set({ queue: updated }); + }, + + setLoading: (loading) => set({ isLoading: loading }), + setError: (error) => set({ error }), +})); diff --git a/src/stores/nav-store.ts b/src/stores/nav-store.ts new file mode 100644 index 0000000..df09987 --- /dev/null +++ b/src/stores/nav-store.ts @@ -0,0 +1,20 @@ +/** + * Navigation Store - tracks which view is active. + * + * Simple view routing for the desktop app. No URL-based routing needed + * since this is a native app with a fixed set of views. + */ + +import { create } from "zustand"; + +export type ViewId = "focus" | "queue" | "inbox"; + +export interface NavState { + activeView: ViewId; + setView: (view: ViewId) => void; +} + +export const useNavStore = create((set) => ({ + activeView: "focus", + setView: (view) => set({ activeView: view }), +}));