feat(poweruser): saved views + editor slash commands + status bar

- Saved views store (Zustand + localStorage) with CRUD
- Save/apply/delete views in tasks filter bar
- TipTap slash command menu (headings, lists, code, divider, wikilink)
- Wikilink autocomplete with [[ trigger and note search
- StatusBar: SSE status, active domain, ⌘K hint, collapsible
- StatusBar + QuickCapture mounted in _app layout
This commit is contained in:
2026-09-09 01:32:01 +00:00
parent c98e1c69f2
commit 27995dd7a4
9 changed files with 508 additions and 6 deletions
+83 -1
View File
@@ -9,7 +9,7 @@ import { useOpenCreateDialog } from "@/hooks/use-open-create-dialog";
import { DndContext, DragOverlay, closestCorners, KeyboardSensor, PointerSensor, useSensor, useSensors, useDroppable, type DragEndEvent, type DragStartEvent } from "@dnd-kit/core";
import { SortableContext, verticalListSortingStrategy, useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { Plus, GripVertical, Pencil, Trash2, Calendar, ListTodo, Layout as LayoutIcon, Search, MoreHorizontal } from "lucide-react";
import { Plus, GripVertical, Pencil, Trash2, Calendar, ListTodo, Layout as LayoutIcon, Search, MoreHorizontal, Bookmark, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
@@ -29,6 +29,7 @@ import { PRIORITY } from "@/lib/status-colors";
import type { Task, State, StateGroup, PaginatedResponse } from "@/lib/types";
import { cn } from "@/lib/utils";
import { parseTaskInput } from "@/lib/nlp";
import { useSavedViewsStore } from "@/lib/stores/use-saved-views-store";
import { RecurrencePicker } from "@/components/tasks/recurrence-picker";
const STATE_GROUP_COLUMNS: { id: StateGroup; label: string; colorClass: string }[] = [
@@ -261,6 +262,11 @@ function TasksPage() {
const projects = projectsData?.items || [];
const [filterProjectId, setFilterProjectId] = useState("");
const [saveViewOpen, setSaveViewOpen] = useState(false);
const [viewName, setViewName] = useState("");
const savedViews = useSavedViewsStore((s) => s.views);
const addSavedView = useSavedViewsStore((s) => s.addView);
const removeSavedView = useSavedViewsStore((s) => s.removeView);
const { data: statesData } = useApiQuery<{ items: State[] }>(
["states", filterProjectId],
@@ -505,6 +511,82 @@ function TasksPage() {
</SelectContent>
</Select>
)}
{/* Saved views */}
{savedViews.length > 0 && (
<Select
value=""
onValueChange={(id) => {
const view = savedViews.find((v) => v.id === id);
if (view) {
setSearch(view.filters.search);
setFilterProjectId(view.filters.projectId);
setSelectedStateId(view.filters.stateId);
}
}}
>
<SelectTrigger className="w-44"><SelectValue placeholder="Saved views" /></SelectTrigger>
<SelectContent>
{savedViews.map((v) => (
<SelectItem key={v.id} value={v.id} className="flex items-center justify-between">
<span className="flex items-center gap-2">
<Bookmark className="h-3 w-3" />
{v.name}
</span>
<button
className="ml-auto hover:text-destructive"
onClick={(e) => {
e.stopPropagation();
removeSavedView(v.id);
}}
>
<X className="h-3 w-3" />
</button>
</SelectItem>
))}
</SelectContent>
</Select>
)}
<Dialog open={saveViewOpen} onOpenChange={setSaveViewOpen}>
<DialogTrigger asChild>
<Button variant="outline" size="sm" className="gap-1">
<Bookmark className="h-3.5 w-3.5" /> Save View
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-sm">
<DialogHeader>
<DialogTitle>Save Current Filters</DialogTitle>
</DialogHeader>
<div className="space-y-3">
<Input
placeholder="View name"
value={viewName}
onChange={(e) => setViewName(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && viewName.trim()) {
addSavedView(viewName.trim(), { search, projectId: filterProjectId, stateId: selectedStateId, priority: "" });
setViewName("");
setSaveViewOpen(false);
}
}}
/>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setSaveViewOpen(false)}>Cancel</Button>
<Button
disabled={!viewName.trim()}
onClick={() => {
addSavedView(viewName.trim(), { search, projectId: filterProjectId, stateId: selectedStateId, priority: "" });
setViewName("");
setSaveViewOpen(false);
}}
>
Save
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
{isLoading ? (