diff --git a/src/components/FilterBar.tsx b/src/components/FilterBar.tsx index a7d8211..bff32de 100644 --- a/src/components/FilterBar.tsx +++ b/src/components/FilterBar.tsx @@ -1,5 +1,5 @@ import { type Component, createSignal, For, Show } from "solid-js"; -import { Search, X, Tag, ArrowUpCircle, Clock, Minus, Save, Trash2, Bookmark, User, Star } from "lucide-solid"; +import { Search, X, Tag, ArrowUpCircle, Clock, Minus, Save, Trash2, Bookmark, User, Star, SlidersHorizontal } from "lucide-solid"; import { store, setFilter, clearFilter, saveFilterTemplate, applyFilterTemplate, removeFilterTemplate, loadAllHistory, setNoteFilter, clearNoteFilter, saveNoteFilterTemplate, applyNoteFilterTemplate, removeNoteFilterTemplate, type Filter } from "@/store"; import { Button } from "./ui/button"; import { cn } from "@/lib/utils"; @@ -76,10 +76,20 @@ export const FilterBar: Component<{ mode?: 'tasks' | 'notes', class?: string, tr isOpen() && "bg-muted ring-1 ring-border", props.triggerClass )} - title="Filters & Search" + title={mode() === 'tasks' ? "Filters & Search" : "Filters"} >
- + ) : ( + + )}
@@ -173,28 +184,29 @@ export const FilterBar: Component<{ mode?: 'tasks' | 'notes', class?: string, tr {/* Controls */}
- {/* Search Input */} -
-
- - { - if (el) setTimeout(() => el.focus(), 50); - }} - type="text" - placeholder={mode() === 'tasks' ? "Search tasks, descriptions, tags..." : "Search notes, tags..."} - value={currentFilter().query} - onInput={(e) => currentSetFilter({ query: e.currentTarget.value })} - onKeyDown={(e) => { - if (e.key === 'Enter') { - e.preventDefault(); - setIsOpen(false); - } - }} - class="w-full bg-muted/30 border border-border/50 rounded-xl pl-10 pr-4 py-2.5 text-sm outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary/40 transition-all placeholder:text-muted-foreground/30" - /> + +
+
+ + { + if (el) setTimeout(() => el.focus(), 50); + }} + type="text" + placeholder="Search tasks, descriptions, tags..." + value={currentFilter().query} + onInput={(e) => currentSetFilter({ query: e.currentTarget.value })} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + setIsOpen(false); + } + }} + class="w-full bg-muted/30 border border-border/50 rounded-xl pl-10 pr-4 py-2.5 text-sm outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary/40 transition-all placeholder:text-muted-foreground/30" + /> +
-
+
diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index c5f7b69..0831554 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -9,7 +9,7 @@ const NotepadView = lazy(() => import("../views/NotepadView").then(m => ({ defau import { NotesSidebar } from "./NotesSidebar"; import { Button } from "./ui/button"; import { cn } from "@/lib/utils"; -import { FilterBar } from "./FilterBar"; +import { TaskSearchPopover } from "./TaskSearchPopover"; interface LayoutProps { children: JSX.Element; @@ -212,7 +212,7 @@ export const Layout: Component = (props) => { )}>
- + {props.children}
diff --git a/src/components/TaskSearchPopover.tsx b/src/components/TaskSearchPopover.tsx new file mode 100644 index 0000000..a4fd041 --- /dev/null +++ b/src/components/TaskSearchPopover.tsx @@ -0,0 +1,506 @@ +import { type Component, createEffect, createSignal, For, onCleanup, Show } from "solid-js"; +import { Search, X, Tag, ArrowUpCircle, Clock, Minus, Save, Trash2, Bookmark, User, Star, SlidersHorizontal } from "lucide-solid"; +import { store, setFilter, clearFilter, saveFilterTemplate, applyFilterTemplate, removeFilterTemplate, loadAllHistory } from "@/store"; +import { Button } from "./ui/button"; +import { cn } from "@/lib/utils"; +import { Badge } from "./ui/badge"; +import { Select, SelectContent, SelectItem, SelectTrigger } from "./ui/select"; +import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem } from "./ui/command"; +import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; +import { useTheme } from "./ThemeProvider"; +import { getThemeAdjustedColor } from "@/lib/colors"; + +export const TaskSearchPopover: Component = () => { + const { resolvedTheme } = useTheme(); + const [isOpen, setIsOpen] = createSignal(false); + const [isExpanded, setIsExpanded] = createSignal(false); + const [isTagOpen, setIsTagOpen] = createSignal(false); + const [tagSearch, setTagSearch] = createSignal(""); + const [newTemplateName, setNewTemplateName] = createSignal(""); + const [isSaving, setIsSaving] = createSignal(false); + let triggerRef: HTMLDivElement | undefined; + let panelRef: HTMLDivElement | undefined; + + const hasActiveFilters = () => { + const f = store.filter; + return (f.query || "") !== "" + || (f.tags || []).length > 0 + || f.editedToday + || f.ownedByMe + || f.starred + || f.priorityMin !== 1 + || f.priorityMax !== 10 + || f.urgencyMin !== 1 + || f.urgencyMax !== 10; + }; + + const hasExpandedFilters = () => { + const f = store.filter; + return (f.tags || []).length > 0 + || f.editedToday + || f.ownedByMe + || f.starred + || f.priorityMin !== 1 + || f.priorityMax !== 10 + || f.urgencyMin !== 1 + || f.urgencyMax !== 10; + }; + + const allTags = () => store.tagDefinitions.map(d => d.name).sort(); + + const closePanel = () => { + setIsOpen(false); + setIsExpanded(false); + setIsSaving(false); + }; + + createEffect(() => { + if (!isOpen()) return; + + const handlePointerDown = (event: PointerEvent) => { + const target = event.target as Node | null; + if (!target) return; + if (triggerRef?.contains(target)) return; + if (panelRef?.contains(target)) return; + closePanel(); + }; + + document.addEventListener("pointerdown", handlePointerDown); + onCleanup(() => document.removeEventListener("pointerdown", handlePointerDown)); + }); + + return ( + <> +
+ +
+ + +
+
+
+
+ + { + if (el) setTimeout(() => el.focus(), 50); + }} + type="text" + placeholder="Search tasks, descriptions, tags..." + value={store.filter.query} + onInput={(e) => setFilter({ query: e.currentTarget.value })} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + closePanel(); + } + }} + class="w-full bg-muted/30 border border-border/50 rounded-xl pl-10 pr-4 py-2.5 text-sm outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary/40 transition-all placeholder:text-muted-foreground/30" + /> +
+ + +
+ + + <> +
+
+

+ + Filters +

+ + + +
+
+ + + + +
+
+ +
+
+
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+ +
+
+
+
+ +
+ + {(tag) => ( + { + const def = store.tagDefinitions.find(d => d.name === tag.name); + const color = getThemeAdjustedColor(def?.color, def?.theme, resolvedTheme()); + return color ? { + "background-color": `${color}15`, + "color": color, + "border-color": `${color}30` + } : {}; + })() : {}} + > +
{ + setFilter({ + tags: store.filter.tags.map(t => + t.name === tag.name ? { ...t, excluded: !t.excluded } : t + ) + }); + }} + > + }> + + + {tag.name} +
+ +
+ )} +
+ + + + Tag + + + + + + { + if (t.startsWith("_")) return false; + const search = tagSearch().toLowerCase(); + if (!search && (t.startsWith("@") || t.startsWith("#"))) return false; + return t.toLowerCase().includes(search); + }).length === 0}> + No tags found. + + + { + if (t.startsWith("_")) return false; + const search = tagSearch().toLowerCase(); + if (!search && (t.startsWith("@") || t.startsWith("#"))) return false; + return t.toLowerCase().includes(search); + })}> + {(tag) => ( + { + if (!store.filter.tags.some(t => t.name === tag)) { + setFilter({ tags: [...store.filter.tags, { name: tag, excluded: false }] }); + } + setIsTagOpen(false); + setTagSearch(""); + }} + > + {tag} + + )} + + + + + + +
+
+
+ +
+
+ + + + +
+ + +
+ setNewTemplateName(e.currentTarget.value)} + class="flex-1 bg-muted/30 border border-primary/20 rounded-lg px-3 py-1.5 text-xs outline-none focus:ring-1 focus:ring-primary/30" + onKeyDown={(e) => { + if (e.key === "Enter" && newTemplateName().trim()) { + saveFilterTemplate(newTemplateName().trim()); + setNewTemplateName(""); + setIsSaving(false); + } else if (e.key === "Escape") { + setIsSaving(false); + } + }} + /> + + +
+
+ +
+ +

No saved filters yet

+
+ }> + {(template) => ( +
+ + +
+ )} + +
+
+
+ + +
+
+ + + ); +};