searchbar improvements
This commit is contained in:
@@ -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,9 +76,10 @@ 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"}
|
||||
>
|
||||
<div class="relative">
|
||||
{mode() === 'tasks' ? (
|
||||
<Search
|
||||
size={18}
|
||||
class={cn(
|
||||
@@ -87,6 +88,16 @@ export const FilterBar: Component<{ mode?: 'tasks' | 'notes', class?: string, tr
|
||||
hasActiveFilters() && "text-primary"
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<SlidersHorizontal
|
||||
size={18}
|
||||
class={cn(
|
||||
"transition-transform duration-300",
|
||||
isOpen() && "scale-110",
|
||||
hasActiveFilters() && "text-primary"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-[calc(100vw-2rem)] sm:w-[480px] p-0 overflow-hidden bg-card border-border shadow-2xl rounded-2xl animate-in fade-in zoom-in-95 duration-200 z-[100]">
|
||||
@@ -173,7 +184,7 @@ export const FilterBar: Component<{ mode?: 'tasks' | 'notes', class?: string, tr
|
||||
|
||||
{/* Controls */}
|
||||
<div class="p-4 space-y-6">
|
||||
{/* Search Input */}
|
||||
<Show when={mode() === 'tasks'}>
|
||||
<div class="space-y-2">
|
||||
<div class="relative group">
|
||||
<Search size={16} class="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground/50 group-focus-within:text-primary transition-colors" />
|
||||
@@ -182,7 +193,7 @@ export const FilterBar: Component<{ mode?: 'tasks' | 'notes', class?: string, tr
|
||||
if (el) setTimeout(() => el.focus(), 50);
|
||||
}}
|
||||
type="text"
|
||||
placeholder={mode() === 'tasks' ? "Search tasks, descriptions, tags..." : "Search notes, tags..."}
|
||||
placeholder="Search tasks, descriptions, tags..."
|
||||
value={currentFilter().query}
|
||||
onInput={(e) => currentSetFilter({ query: e.currentTarget.value })}
|
||||
onKeyDown={(e) => {
|
||||
@@ -195,6 +206,7 @@ export const FilterBar: Component<{ mode?: 'tasks' | 'notes', class?: string, tr
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={mode() === 'tasks'}>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
|
||||
@@ -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<LayoutProps> = (props) => {
|
||||
)}>
|
||||
<div class={cn(store.isNotepadMode ? "hidden" : "block")}>
|
||||
<Show when={props.currentView.toLowerCase() !== "settings"}>
|
||||
<FilterBar />
|
||||
<TaskSearchPopover />
|
||||
</Show>
|
||||
{props.children}
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<div ref={triggerRef} class="fixed top-4 right-5 sm:right-6 z-[60]">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class={cn(
|
||||
"bg-background/80 backdrop-blur-sm border border-border hover:bg-muted shadow-sm transition-all duration-300",
|
||||
hasActiveFilters() && "border-primary/50 ring-1 ring-primary/20",
|
||||
isOpen() && "bg-muted ring-1 ring-border"
|
||||
)}
|
||||
title="Search & Filter Tasks"
|
||||
onClick={() => {
|
||||
const next = !isOpen();
|
||||
setIsOpen(next);
|
||||
if (!next) {
|
||||
setIsExpanded(false);
|
||||
setIsSaving(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Search
|
||||
size={18}
|
||||
class={cn(
|
||||
"transition-transform duration-300",
|
||||
isOpen() && "scale-110",
|
||||
hasActiveFilters() && "text-primary"
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Show when={isOpen()}>
|
||||
<div ref={panelRef} class="fixed top-[4.25rem] left-1/2 -translate-x-1/2 z-[95] w-[min(calc(100vw-2rem),32rem)]">
|
||||
<div class="overflow-hidden rounded-2xl border border-border/70 bg-background/95 shadow-2xl backdrop-blur-md">
|
||||
<div class="flex items-center gap-2 p-2">
|
||||
<div class="relative flex-1 group">
|
||||
<Search size={16} class="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground/50 group-focus-within:text-primary transition-colors" />
|
||||
<input
|
||||
ref={(el) => {
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class={cn(
|
||||
"h-10 w-10 rounded-xl border border-border/50 bg-muted/30 hover:bg-muted/60 shrink-0",
|
||||
(isExpanded() || hasExpandedFilters()) && "border-primary/30 bg-primary/5 text-primary"
|
||||
)}
|
||||
title="More filters"
|
||||
onClick={() => setIsExpanded(!isExpanded())}
|
||||
>
|
||||
<SlidersHorizontal size={16} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-10 w-10 rounded-xl shrink-0"
|
||||
title="Close search"
|
||||
onClick={closePanel}
|
||||
>
|
||||
<X size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Show when={isExpanded()}>
|
||||
<>
|
||||
<div class="px-4 py-3 border-t border-border/50 bg-muted/20 flex flex-wrap items-center justify-between gap-3">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<h3 class="text-xs font-bold uppercase tracking-widest text-muted-foreground flex items-center gap-2 mr-1">
|
||||
<SlidersHorizontal size={12} />
|
||||
Filters
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setFilter({ editedToday: !store.filter.editedToday })}
|
||||
class={cn(
|
||||
"flex items-center gap-1.5 px-2 py-0.5 rounded-full text-[0.5625rem] font-black uppercase tracking-tight transition-all border",
|
||||
store.filter.editedToday
|
||||
? "bg-primary/10 text-primary border-primary/20"
|
||||
: "bg-muted/50 text-muted-foreground border-transparent hover:bg-muted"
|
||||
)}
|
||||
>
|
||||
<div class={cn("w-1.5 h-1.5 rounded-full", store.filter.editedToday ? "bg-primary" : "bg-muted-foreground/30")} />
|
||||
Edited Today
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setFilter({ ownedByMe: !store.filter.ownedByMe })}
|
||||
class={cn(
|
||||
"flex items-center gap-1.5 px-2 py-0.5 rounded-full text-[0.5625rem] font-black uppercase tracking-tight transition-all border",
|
||||
store.filter.ownedByMe
|
||||
? "bg-primary/10 text-primary border-primary/20"
|
||||
: "bg-muted/50 text-muted-foreground border-transparent hover:bg-muted"
|
||||
)}
|
||||
>
|
||||
<User size={10} class={cn(store.filter.ownedByMe ? "text-primary" : "text-muted-foreground/50")} />
|
||||
Mine
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setFilter({ starred: !store.filter.starred })}
|
||||
class={cn(
|
||||
"flex items-center gap-1.5 px-2 py-0.5 rounded-full text-[0.5625rem] font-black uppercase tracking-tight transition-all border",
|
||||
store.filter.starred
|
||||
? "bg-amber-500/10 text-amber-500 border-amber-500/20"
|
||||
: "bg-muted/50 text-muted-foreground border-transparent hover:bg-muted"
|
||||
)}
|
||||
>
|
||||
<Star size={10} class={cn(store.filter.starred ? "fill-amber-500 text-amber-500" : "text-muted-foreground/50")} />
|
||||
Starred
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-7 px-2 text-[0.625rem] font-bold uppercase tracking-wider text-muted-foreground hover:text-primary"
|
||||
onClick={() => {
|
||||
loadAllHistory();
|
||||
closePanel();
|
||||
}}
|
||||
title="Load all completed tasks"
|
||||
>
|
||||
Load All
|
||||
</Button>
|
||||
<Show when={hasActiveFilters()}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-7 px-2 text-[0.625rem] font-bold uppercase tracking-wider text-muted-foreground hover:text-destructive"
|
||||
onClick={clearFilter}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-h-[min(70vh,40rem)] overflow-y-auto">
|
||||
<div class="p-4 space-y-6">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
<div class="space-y-2.5">
|
||||
<label class="text-[0.625rem] font-black uppercase tracking-widest text-muted-foreground ml-1">Priority Range</label>
|
||||
<div class="flex items-center gap-2 bg-muted/20 p-1 rounded-xl border border-border/30">
|
||||
<Select
|
||||
value={store.filter.priorityMin.toString()}
|
||||
onChange={(val) => val && setFilter({ priorityMin: parseInt(val) })}
|
||||
options={["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]}
|
||||
itemComponent={(props) => <SelectItem item={props.item}>{props.item.rawValue}</SelectItem>}
|
||||
>
|
||||
<SelectTrigger class="h-9 flex-1 bg-transparent border-none shadow-none focus:ring-0 text-xs font-bold text-foreground">
|
||||
<div class="flex items-center gap-2 justify-center">
|
||||
<ArrowUpCircle size={14} class="text-muted-foreground/70" />
|
||||
<span>Min: {store.filter.priorityMin}</span>
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent />
|
||||
</Select>
|
||||
<div class="w-px h-4 bg-border/50" />
|
||||
<Select
|
||||
value={store.filter.priorityMax.toString()}
|
||||
onChange={(val) => val && setFilter({ priorityMax: parseInt(val) })}
|
||||
options={["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]}
|
||||
itemComponent={(props) => <SelectItem item={props.item}>{props.item.rawValue}</SelectItem>}
|
||||
>
|
||||
<SelectTrigger class="h-9 flex-1 bg-transparent border-none shadow-none focus:ring-0 text-xs font-bold text-foreground">
|
||||
<div class="flex items-center gap-2 justify-center">
|
||||
<span>Max: {store.filter.priorityMax}</span>
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent />
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2.5">
|
||||
<label class="text-[0.625rem] font-black uppercase tracking-widest text-muted-foreground ml-1">Urgency Range</label>
|
||||
<div class="flex items-center gap-2 bg-muted/20 p-1 rounded-xl border border-border/30">
|
||||
<Select
|
||||
value={store.filter.urgencyMin.toString()}
|
||||
onChange={(val) => val && setFilter({ urgencyMin: parseInt(val) })}
|
||||
options={["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]}
|
||||
itemComponent={(props) => <SelectItem item={props.item}>{props.item.rawValue}</SelectItem>}
|
||||
>
|
||||
<SelectTrigger class="h-9 flex-1 bg-transparent border-none shadow-none focus:ring-0 text-xs font-bold text-foreground">
|
||||
<div class="flex items-center gap-2 justify-center">
|
||||
<Clock size={14} class="text-muted-foreground/70" />
|
||||
<span>Min: {store.filter.urgencyMin}</span>
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent />
|
||||
</Select>
|
||||
<div class="w-px h-4 bg-border/50" />
|
||||
<Select
|
||||
value={store.filter.urgencyMax.toString()}
|
||||
onChange={(val) => val && setFilter({ urgencyMax: parseInt(val) })}
|
||||
options={["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]}
|
||||
itemComponent={(props) => <SelectItem item={props.item}>{props.item.rawValue}</SelectItem>}
|
||||
>
|
||||
<SelectTrigger class="h-9 flex-1 bg-transparent border-none shadow-none focus:ring-0 text-xs font-bold text-foreground">
|
||||
<div class="flex items-center gap-2 justify-center">
|
||||
<span>Max: {store.filter.urgencyMax}</span>
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent />
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2.5">
|
||||
<label class="text-[0.625rem] font-black uppercase tracking-widest text-muted-foreground ml-1 flex items-center justify-between">
|
||||
Filter by Tags
|
||||
<span class="text-[0.5625rem] lowercase font-medium opacity-50 tracking-normal text-right">
|
||||
Click tag name to toggle exclude<br />
|
||||
Click X to remove
|
||||
</span>
|
||||
</label>
|
||||
<div class="flex flex-wrap gap-2 min-h-[44px] p-2 bg-muted/10 border border-dashed border-border/60 rounded-xl">
|
||||
<For each={store.filter.tags}>
|
||||
{(tag) => (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
class={cn(
|
||||
"h-7 px-2.5 rounded-lg text-xs gap-2 transition-all border shadow-sm",
|
||||
tag.excluded
|
||||
? "bg-red-500/10 text-red-600 dark:text-red-400 border-red-500/20 hover:bg-red-500/20"
|
||||
: "bg-indigo-500/10 text-indigo-600 dark:text-indigo-300 border-indigo-500/20 hover:bg-indigo-500/20"
|
||||
)}
|
||||
style={!tag.excluded ? (() => {
|
||||
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`
|
||||
} : {};
|
||||
})() : {}}
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-2 cursor-pointer h-full"
|
||||
onClick={() => {
|
||||
setFilter({
|
||||
tags: store.filter.tags.map(t =>
|
||||
t.name === tag.name ? { ...t, excluded: !t.excluded } : t
|
||||
)
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Show when={tag.excluded} fallback={<Tag size={12} class="opacity-70" />}>
|
||||
<Minus size={12} class="opacity-100" />
|
||||
</Show>
|
||||
<span class={cn(tag.excluded && "line-through opacity-70")}>{tag.name}</span>
|
||||
</div>
|
||||
<button
|
||||
class="p-0.5 hover:bg-foreground/10 rounded transition-colors"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setFilter({ tags: store.filter.tags.filter(t => t.name !== tag.name) });
|
||||
}}
|
||||
>
|
||||
<X size={10} />
|
||||
</button>
|
||||
</Badge>
|
||||
)}
|
||||
</For>
|
||||
<Popover open={isTagOpen()} onOpenChange={setIsTagOpen}>
|
||||
<PopoverTrigger class="h-7 px-3 bg-muted/40 border-none shadow-none focus:ring-0 text-[0.625rem] font-bold uppercase tracking-wider rounded-lg hover:bg-muted/60 transition-colors">
|
||||
<span>+ Tag</span>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-[200px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search tags..."
|
||||
value={tagSearch()}
|
||||
onValueChange={setTagSearch}
|
||||
/>
|
||||
<CommandList>
|
||||
<Show when={allTags().filter(t => {
|
||||
if (t.startsWith("_")) return false;
|
||||
const search = tagSearch().toLowerCase();
|
||||
if (!search && (t.startsWith("@") || t.startsWith("#"))) return false;
|
||||
return t.toLowerCase().includes(search);
|
||||
}).length === 0}>
|
||||
<CommandEmpty>No tags found.</CommandEmpty>
|
||||
</Show>
|
||||
<CommandGroup>
|
||||
<For each={allTags().filter(t => {
|
||||
if (t.startsWith("_")) return false;
|
||||
const search = tagSearch().toLowerCase();
|
||||
if (!search && (t.startsWith("@") || t.startsWith("#"))) return false;
|
||||
return t.toLowerCase().includes(search);
|
||||
})}>
|
||||
{(tag) => (
|
||||
<CommandItem
|
||||
value={tag}
|
||||
onSelect={() => {
|
||||
if (!store.filter.tags.some(t => t.name === tag)) {
|
||||
setFilter({ tags: [...store.filter.tags, { name: tag, excluded: false }] });
|
||||
}
|
||||
setIsTagOpen(false);
|
||||
setTagSearch("");
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
</CommandItem>
|
||||
)}
|
||||
</For>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-4 pb-4 pt-4 border-t border-border/50 space-y-4 bg-muted/5">
|
||||
<div class="flex items-center justify-between ml-1">
|
||||
<label class="text-[0.625rem] font-black uppercase tracking-widest text-muted-foreground flex items-center gap-2">
|
||||
<Bookmark size={12} class="text-primary" />
|
||||
Saved Filters
|
||||
</label>
|
||||
<Show when={!isSaving() && hasActiveFilters()}>
|
||||
<button
|
||||
onClick={() => setIsSaving(true)}
|
||||
class="text-[0.5625rem] font-bold uppercase tracking-wider text-primary hover:underline flex items-center gap-1"
|
||||
>
|
||||
<Save size={10} />
|
||||
Save Current
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Show when={isSaving()}>
|
||||
<div class="flex items-center gap-2 animate-in fade-in slide-in-from-top-1">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Template name..."
|
||||
value={newTemplateName()}
|
||||
onInput={(e) => 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);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
class="h-8 px-3"
|
||||
disabled={!newTemplateName().trim()}
|
||||
onClick={() => {
|
||||
saveFilterTemplate(newTemplateName().trim());
|
||||
setNewTemplateName("");
|
||||
setIsSaving(false);
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
onClick={() => setIsSaving(false)}
|
||||
>
|
||||
<X size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
<For each={store.filterTemplates} fallback={
|
||||
<div class="col-span-2 py-4 text-center border border-dashed border-border/40 rounded-xl bg-muted/5">
|
||||
<p class="text-[0.625rem] text-muted-foreground uppercase tracking-widest">No saved filters yet</p>
|
||||
</div>
|
||||
}>
|
||||
{(template) => (
|
||||
<div class="group flex items-center justify-between p-2 rounded-xl bg-muted/20 border border-border/30 hover:bg-muted/40 transition-all">
|
||||
<button
|
||||
class="flex-1 text-left px-1 min-w-0"
|
||||
onClick={() => applyFilterTemplate(template.id)}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="text-[0.6875rem] font-bold text-foreground truncate">{template.name}</p>
|
||||
<div class="flex gap-1 opacity-60 shrink-0">
|
||||
<Show when={template.filter.query}>
|
||||
<div class="w-1 h-1 rounded-full bg-primary" />
|
||||
</Show>
|
||||
<Show when={template.filter.tags.length > 0}>
|
||||
<div class="w-1 h-1 rounded-full bg-indigo-500" />
|
||||
</Show>
|
||||
<Show when={template.filter.priorityMin > 1 || template.filter.priorityMax < 10}>
|
||||
<div class="w-1 h-1 rounded-full bg-amber-500" />
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
class="opacity-0 group-hover:opacity-100 p-1.5 hover:bg-destructive/10 hover:text-destructive rounded-lg transition-all"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
removeFilterTemplate(template.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user