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"
- />
+
+
-
+
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) => (
+
+
+
+
+ )}
+
+
+
+
+ >
+
+
+
+
+ >
+ );
+};