Compare commits

...

6 Commits

Author SHA1 Message Date
tcardoza 3c23e2109a Note tabs and favoriting ready for production
CI / build (push) Has been skipped
CI / deploy (push) Successful in 56s
2026-03-06 14:18:18 -06:00
tcardoza 09557d49a4 favoriting 2026-03-06 13:53:26 -06:00
tcardoza 10d76eb3c1 Improved filtering and layout 2026-03-06 12:29:56 -06:00
tcardoza e406adf9c5 Note Tabs Reordering and Stable 2026-03-06 10:05:04 -06:00
tcardoza 8c307db57a Updated note tabs - NOT READY FOR PRODUCTION 2026-03-05 19:14:30 -06:00
tcardoza f7b46b2017 Partial Note Tabs Setup - NOT FOR PRODUCTION 2026-03-05 19:06:45 -06:00
10 changed files with 948 additions and 356 deletions
+3
View File
@@ -8,6 +8,7 @@
"@formkit/auto-animate": "^0.9.0",
"@kobalte/core": "^0.13.11",
"@tailwindcss/vite": "^4.1.18",
"@thisbeyond/solid-dnd": "^0.7.5",
"@tiptap/core": "^3.18.0",
"@tiptap/extension-bubble-menu": "^3.20.0",
"@tiptap/extension-dropcursor": "^3.20.0",
@@ -460,6 +461,8 @@
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="],
"@thisbeyond/solid-dnd": ["@thisbeyond/solid-dnd@0.7.5", "", { "peerDependencies": { "solid-js": "^1.5" } }, "sha512-DfI5ff+yYGpK9M21LhYwIPlbP2msKxN2ARwuu6GF8tT1GgNVDTI8VCQvH4TJFoVApP9d44izmAcTh/iTCH2UUw=="],
"@tiptap/core": ["@tiptap/core@3.18.0", "", { "peerDependencies": { "@tiptap/pm": "^3.18.0" } }, "sha512-Gczd4GbK1DNgy/QUPElMVozoa0GW9mW8E31VIi7Q4a9PHHz8PcrxPmuWwtJ2q0PF8MWpOSLuBXoQTWaXZRPRnQ=="],
"@tiptap/extension-blockquote": ["@tiptap/extension-blockquote@3.18.0", "", { "peerDependencies": { "@tiptap/core": "^3.18.0" } }, "sha512-1HjEoM5vZDfFnq2OodNpW13s56a9pbl7jolUv1V9FrE3X5s7n0HCfDzIVpT7z1HgTdPtlN5oSt5uVyBwuwSUfA=="],
+1
View File
@@ -18,6 +18,7 @@
"@formkit/auto-animate": "^0.9.0",
"@kobalte/core": "^0.13.11",
"@tailwindcss/vite": "^4.1.18",
"@thisbeyond/solid-dnd": "^0.7.5",
"@tiptap/core": "^3.18.0",
"@tiptap/extension-bubble-menu": "^3.20.0",
"@tiptap/extension-dropcursor": "^3.20.0",
+147 -105
View File
@@ -1,6 +1,6 @@
import { type Component, createSignal, For, Show } from "solid-js";
import { Search, X, Tag, ArrowUpCircle, Clock, Minus, Save, Trash2, Bookmark } from "lucide-solid";
import { store, setFilter, clearFilter, saveFilterTemplate, applyFilterTemplate, removeFilterTemplate, loadAllHistory } from "@/store";
import { Search, X, Tag, ArrowUpCircle, Clock, Minus, Save, Trash2, Bookmark, User, Star } 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";
import { Badge } from "./ui/badge";
@@ -9,21 +9,34 @@ import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
import { useTheme } from "./ThemeProvider";
import { getThemeAdjustedColor } from "@/lib/colors";
export const FilterBar: Component = () => {
export const FilterBar: Component<{ mode?: 'tasks' | 'notes', class?: string, triggerClass?: string }> = (props) => {
const { resolvedTheme } = useTheme();
const [isOpen, setIsOpen] = createSignal(false);
const [newTemplateName, setNewTemplateName] = createSignal("");
const [isSaving, setIsSaving] = createSignal(false);
const mode = () => props.mode || 'tasks';
const currentFilter = () => mode() === 'tasks' ? store.filter : store.noteFilter;
const currentSetFilter = (val: Partial<Filter>) => mode() === 'tasks' ? setFilter(val) : setNoteFilter(val);
const currentClearFilter = () => mode() === 'tasks' ? clearFilter() : clearNoteFilter();
const currentSaveTemplate = (name: string) => mode() === 'tasks' ? saveFilterTemplate(name) : saveNoteFilterTemplate(name);
const currentApplyTemplate = (id: string) => mode() === 'tasks' ? applyFilterTemplate(id) : applyNoteFilterTemplate(id);
const currentRemoveTemplate = (id: string) => mode() === 'tasks' ? removeFilterTemplate(id) : removeNoteFilterTemplate(id);
const hasActiveFilters = () => {
const f = store.filter;
return f.query !== "" || f.tags.length > 0 || f.priorityMin !== 1 || f.priorityMax !== 10 || f.urgencyMin !== 1 || f.urgencyMax !== 10 || f.editedToday;
const f = currentFilter();
const baseActive = (f.query || "") !== "" || (f.tags || []).length > 0 || f.editedToday || f.ownedByMe || f.starred;
if (mode() === 'tasks') {
return baseActive || f.priorityMin !== 1 || f.priorityMax !== 10 || f.urgencyMin !== 1 || f.urgencyMax !== 10;
}
return baseActive;
};
const allTags = () => store.tagDefinitions.map(d => d.name).sort();
return (
<div class="fixed top-4 right-5 sm:right-6 z-[60]">
<div class={cn(mode() === 'tasks' ? "fixed top-4 right-5 sm:right-6 z-[60]" : "relative", props.class)}>
<Popover open={isOpen()} onOpenChange={setIsOpen}>
<PopoverTrigger
as={Button}
@@ -32,7 +45,8 @@ export const FilterBar: Component = () => {
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"
isOpen() && "bg-muted ring-1 ring-border",
props.triggerClass
)}
title="Filters & Search"
>
@@ -43,7 +57,7 @@ export const FilterBar: Component = () => {
)}
</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">
<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]">
{/* Header */}
<div class="px-4 py-3 border-b border-border/50 bg-muted/20 flex items-center justify-between">
<div class="flex items-center gap-4">
@@ -53,37 +67,63 @@ export const FilterBar: Component = () => {
</h3>
<div class="h-4 w-px bg-border/50" />
<button
onClick={() => setFilter({ editedToday: !store.filter.editedToday })}
onClick={() => currentSetFilter({ editedToday: !currentFilter().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
currentFilter().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")} />
<div class={cn("w-1.5 h-1.5 rounded-full", currentFilter().editedToday ? "bg-primary" : "bg-muted-foreground/30")} />
Edited Today
</button>
<button
onClick={() => currentSetFilter({ ownedByMe: !currentFilter().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",
currentFilter().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(currentFilter().ownedByMe ? "text-primary" : "text-muted-foreground/50")} />
Mine
</button>
<button
onClick={() => currentSetFilter({ starred: !currentFilter().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",
currentFilter().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(currentFilter().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();
setIsOpen(false);
}}
title="Load all completed tasks"
>
Load All
</Button>
<Show when={mode() === 'tasks'}>
<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();
setIsOpen(false);
}}
title="Load all completed tasks"
>
Load All
</Button>
</Show>
<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}
onClick={currentClearFilter}
>
Clear
</Button>
@@ -110,9 +150,9 @@ export const FilterBar: Component = () => {
if (el) setTimeout(() => el.focus(), 50);
}}
type="text"
placeholder="Search tasks, descriptions, tags..."
value={store.filter.query}
onInput={(e) => setFilter({ query: e.currentTarget.value })}
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();
@@ -124,77 +164,79 @@ export const FilterBar: Component = () => {
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
{/* Priority Range */}
<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>
<Show when={mode() === 'tasks'}>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
{/* Priority Range */}
<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>
{/* Urgency Range */}
<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>
{/* Urgency Range */}
<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>
</Show>
{/* Tags Multi-select */}
<div class="space-y-2.5">
@@ -206,7 +248,7 @@ export const FilterBar: Component = () => {
</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}>
<For each={currentFilter().tags}>
{(tag) => (
<Badge
variant="secondary"
@@ -229,10 +271,10 @@ export const FilterBar: Component = () => {
<div
class="flex items-center gap-2 cursor-pointer h-full"
onClick={() => {
const newTags = store.filter.tags.map(t =>
const newTags = currentFilter().tags.map(t =>
t.name === tag.name ? { ...t, excluded: !t.excluded } : t
);
setFilter({ tags: newTags });
currentSetFilter({ tags: newTags });
}}
>
<Show when={tag.excluded} fallback={<Tag size={12} class="opacity-70" />}>
@@ -244,7 +286,7 @@ export const FilterBar: Component = () => {
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) });
currentSetFilter({ tags: currentFilter().tags.filter(t => t.name !== tag.name) });
}}
>
<X size={10} />
@@ -255,8 +297,8 @@ export const FilterBar: Component = () => {
<Select
value=""
onChange={(val) => {
if (val && !store.filter.tags.some(t => t.name === val)) {
setFilter({ tags: [...store.filter.tags, { name: val, excluded: false }] });
if (val && !currentFilter().tags.some(t => t.name === val)) {
currentSetFilter({ tags: [...currentFilter().tags, { name: val, excluded: false }] });
}
}}
options={allTags()}
@@ -301,7 +343,7 @@ export const FilterBar: Component = () => {
onKeyDown={(e) => {
if (e.key === 'Enter') {
if (newTemplateName().trim()) {
saveFilterTemplate(newTemplateName().trim());
currentSaveTemplate(newTemplateName().trim());
setNewTemplateName("");
setIsSaving(false);
}
@@ -315,7 +357,7 @@ export const FilterBar: Component = () => {
class="h-8 px-3"
disabled={!newTemplateName().trim()}
onClick={() => {
saveFilterTemplate(newTemplateName().trim());
currentSaveTemplate(newTemplateName().trim());
setNewTemplateName("");
setIsSaving(false);
}}
@@ -343,7 +385,7 @@ export const FilterBar: Component = () => {
<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)}
onClick={() => currentApplyTemplate(template.id)}
>
<div class="flex items-center gap-2">
<p class="text-[0.6875rem] font-bold text-foreground truncate">{template.name}</p>
@@ -354,7 +396,7 @@ export const FilterBar: Component = () => {
<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}>
<Show when={mode() === 'tasks' && (template.filter.priorityMin > 1 || template.filter.priorityMax < 10)}>
<div class="w-1 h-1 rounded-full bg-amber-500" />
</Show>
</div>
@@ -364,7 +406,7 @@ export const FilterBar: Component = () => {
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);
currentRemoveTemplate(template.id);
}}
>
<Trash2 size={12} />
+76 -27
View File
@@ -1,24 +1,69 @@
import { type Component, createSignal, createMemo, For, Show } from "solid-js";
import { store } from "@/store";
import { type Component, createMemo, For, Show } from "solid-js";
import { store, getFavoriteTag } from "@/store";
import { pb } from "@/lib/pocketbase";
import { Plus, Lock, Search } from "lucide-solid";
import { Plus, Lock, Star } from "lucide-solid";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { NOTES_COLLECTION } from "@/lib/constants";
import { FilterBar } from "./FilterBar";
export const NotesSidebar: Component<{
selectedNoteId: string | null;
setSelectedNoteId: (id: string | null) => void;
}> = (props) => {
const [searchQuery, setSearchQuery] = createSignal("");
const currentUserId = pb.authStore.model?.id;
const filteredNotes = createMemo(() => {
let n = store.notes.filter(note => !note.deletedAt);
if (searchQuery()) {
const q = searchQuery().toLowerCase();
n = n.filter(note => note.title.toLowerCase().includes(q) || note.tags.some(t => t.toLowerCase().includes(q)));
const filter = store.noteFilter;
let n = store.notes.filter(note => {
if (note.deletedAt) return false;
// Hide child notes from the main list
if (note.tags && note.tags.some(t => t.startsWith("child-of-"))) return false;
return true;
});
// Query Filter
if (filter.query) {
const q = filter.query.toLowerCase();
n = n.filter(note =>
note.title.toLowerCase().includes(q) ||
note.tags.some(t => t.toLowerCase().includes(q))
);
}
// Tags Filter
if (filter.tags.length > 0) {
const required = filter.tags.filter(t => !t.excluded).map(t => t.name);
const excluded = filter.tags.filter(t => t.excluded).map(t => t.name);
if (required.length > 0) {
n = n.filter(note => required.some(r => note.tags.includes(r)));
}
if (excluded.length > 0) {
n = n.filter(note => !excluded.some(e => note.tags.includes(e)));
}
}
// Edited Today Filter
if (filter.editedToday) {
const today = new Date();
today.setHours(0, 0, 0, 0);
n = n.filter(note => {
const updated = new Date(note.updated);
return updated >= today;
});
}
// Owned By Me Filter
if (filter.ownedByMe) {
n = n.filter(note => note.user === currentUserId);
}
// Starred Filter
if (filter.starred) {
n = n.filter(note => note.tags?.includes(getFavoriteTag()));
}
return n;
});
@@ -40,21 +85,20 @@ export const NotesSidebar: Component<{
return (
<div class="flex flex-col h-full bg-card">
{/* Header */}
<div class="p-4 border-b border-border bg-card/50 backdrop-blur flex justify-between items-center gap-2">
<div class="relative flex-1">
<Search size={14} class="absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground/50" />
<input
type="text"
placeholder="Search notes..."
value={searchQuery()}
onInput={(e) => setSearchQuery(e.currentTarget.value)}
class="w-full bg-muted/50 border border-border/50 rounded-lg pl-8 pr-3 py-1.5 text-xs outline-none focus:ring-1 focus:ring-primary/30"
/>
<div class="z-20">
{/* Header */}
<div class="p-4 border-b border-border bg-card/50 backdrop-blur flex justify-between items-center gap-2">
<div class="flex-1">
<FilterBar
mode="notes"
class="relative top-0 right-0 z-0"
triggerClass="w-full justify-start gap-2 px-3 h-9 bg-muted/40 hover:bg-muted/60 border-border/40"
/>
</div>
<Button onClick={handleCreateNote} size="sm" variant="default" class="h-9 w-9 p-0 shrink-0 shadow-sm rounded-xl">
<Plus size={18} />
</Button>
</div>
<Button onClick={handleCreateNote} size="sm" variant="default" class="h-8 w-8 p-0 shrink-0 shadow-sm">
<Plus size={16} />
</Button>
</div>
{/* List */}
@@ -71,23 +115,28 @@ export const NotesSidebar: Component<{
)}
>
<div class="font-semibold text-sm truncate flex items-center justify-between">
<span>{note.title || "Untitled"}</span>
<div class="flex items-center gap-1.5 truncate">
<Show when={note.tags?.includes(getFavoriteTag())}>
<Star size={12} class="fill-amber-500 text-amber-500 shrink-0" />
</Show>
<span class="truncate">{note.title || "Untitled"}</span>
</div>
<Show when={note.isPrivate}>
<Lock size={12} class="text-orange-500 shrink-0 ml-2" />
</Show>
</div>
<Show when={note.tags && note.tags.length > 0}>
<Show when={note.tags && note.tags.some(t => !t.endsWith("_favorite__"))}>
<div class="flex gap-1 mt-2 flex-wrap">
<For each={note.tags.slice(0, 3)}>
<For each={note.tags.filter(t => !t.endsWith("_favorite__")).slice(0, 3)}>
{(t) => (
<span class="text-[0.6rem] px-1.5 py-0.5 bg-muted rounded-md text-muted-foreground whitespace-nowrap border border-border/50">
{t}
</span>
)}
</For>
<Show when={note.tags.length > 3}>
<Show when={note.tags.filter(t => !t.endsWith("_favorite__")).length > 3}>
<span class="text-[0.6rem] px-1.5 py-0.5 text-muted-foreground whitespace-nowrap shrink-0">
+{note.tags.length - 3}
+{note.tags.filter(t => !t.endsWith("_favorite__")).length - 3}
</span>
</Show>
</div>
+1 -1
View File
@@ -345,7 +345,7 @@ export const QuickEntryForm: Component<QuickEntryFormProps> = (props) => {
<div class="flex flex-col md:flex-row items-stretch md:items-center px-4 py-3 gap-4 bg-muted/10 border-b border-border">
<div class="flex-1 flex flex-wrap gap-2 items-center">
<TagPicker selectedTags={tags()} onTagsChange={setTags} />
<For each={tags()}>
<For each={tags().filter(t => !t.endsWith("_favorite__"))}>
{(tag) => (
<Badge
variant="secondary"
+1 -1
View File
@@ -23,7 +23,7 @@ export const TagPicker: Component<TagPickerProps> = (props) => {
};
const allTagNames = () => {
const definedTags = store.tagDefinitions.map(d => d.name);
const definedTags = store.tagDefinitions.filter(d => !d.name.endsWith("_favorite__")).map(d => d.name);
const bucketTags = store.buckets.map(b => `@${b.name}`);
const noteTags = store.notes.map(n => `#${n.title}`);
return [...new Set([...definedTags, ...bucketTags, ...noteTags])].sort();
+25 -6
View File
@@ -1,7 +1,7 @@
import { type Component, createMemo, For, Show } from "solid-js";
import { type Task, calculateUrgencyFromDate, setActiveTaskId, store, copyTask, updateTask, currentTaskContext, now } from "@/store";
import { type Task, calculateUrgencyFromDate, setActiveTaskId, store, copyTask, updateTask, currentTaskContext, now, getFavoriteTag } from "@/store";
import { cn } from "@/lib/utils";
import { Clock, ArrowUpCircle, Copy, Users2 } from "lucide-solid";
import { Clock, ArrowUpCircle, Copy, Users2, Star } from "lucide-solid";
import { Badge } from "@/components/ui/badge";
import { useTheme } from "./ThemeProvider";
import { getThemeAdjustedColor } from "@/lib/colors";
@@ -35,10 +35,10 @@ export const TaskCard: Component<{ task: Task, isShaking?: boolean }> = (props)
// We are in a bucket view. Hide the tag that matches this bucket's name.
// Buckets are now tagged with '@' + name.
const bucketTagName = `@${(ctx as { name: string }).name.toLowerCase()}`;
return tags.filter(t => t.toLowerCase() !== bucketTagName);
return tags.filter(t => t.toLowerCase() !== bucketTagName && !t.endsWith("_favorite__"));
}
return tags;
return tags.filter(t => !t.endsWith("_favorite__"));
});
const isDueWithin12Hours = createMemo(() => {
@@ -218,9 +218,9 @@ export const TaskCard: Component<{ task: Task, isShaking?: boolean }> = (props)
</div>
</div>
<div class="ml-2 sm:ml-4 opacity-100 sm:opacity-0 group-hover:opacity-100 transition-opacity shrink-0">
<div class="ml-2 sm:ml-4 shrink-0 flex items-center gap-1">
<button
class="p-1.5 hover:bg-muted rounded-full transition-colors text-muted-foreground hover:text-primary"
class="p-1.5 hover:bg-muted rounded-full transition-all text-muted-foreground hover:text-primary opacity-0 group-hover:opacity-100"
onClick={(e) => {
e.stopPropagation();
copyTask(props.task.id);
@@ -229,6 +229,25 @@ export const TaskCard: Component<{ task: Task, isShaking?: boolean }> = (props)
>
<Copy size={16} />
</button>
<button
class={cn(
"p-1.5 rounded-full transition-all",
props.task.tags?.includes(getFavoriteTag())
? "text-amber-500 bg-amber-500/10 hover:bg-amber-500/20 opacity-100"
: "text-muted-foreground hover:text-amber-500 hover:bg-muted opacity-0 group-hover:opacity-100"
)}
onClick={(e) => {
e.stopPropagation();
const favTag = getFavoriteTag();
const currentTags = props.task.tags || [];
const isFav = currentTags.includes(favTag);
const newTags = isFav ? currentTags.filter(t => t !== favTag) : [...currentTags, favTag];
updateTask(props.task.id, { tags: newTags });
}}
title="Toggle Favorite"
>
<Star size={16} class={cn(props.task.tags?.includes(getFavoriteTag()) && "fill-amber-500")} />
</button>
</div>
</div>
);
+24 -4
View File
@@ -1,9 +1,9 @@
import { type Component, createEffect, createSignal, For, createMemo, onCleanup } from "solid-js";
import { Sheet, SheetContent } from "@/components/ui/sheet";
import { type Task, removeTask, restoreTask, updateTask, saveTaskAsTemplate, shareTask, revokeShare, splitTask, loadTaskContent, currentTaskContext, cleanupTaskAttachments } from "@/store";
import { type Task, removeTask, restoreTask, updateTask, saveTaskAsTemplate, shareTask, revokeShare, splitTask, loadTaskContent, currentTaskContext, cleanupTaskAttachments, getFavoriteTag } from "@/store";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { ArrowUpCircle, Clock, Calendar, Type, Trash2, X, Copy, MoreHorizontal, Gauge, Share2, UserMinus, GitBranch, Tag, Settings } from "lucide-solid";
import { ArrowUpCircle, Clock, Calendar, Type, Trash2, X, Copy, MoreHorizontal, Gauge, Share2, UserMinus, GitBranch, Tag, Settings, Star } from "lucide-solid";
import { StatusCircle } from "./StatusCircle";
import { calculateDateFromUrgency, calculateUrgencyFromDate } from "@/store";
import { Button } from "./ui/button";
@@ -201,10 +201,10 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
// We are in a bucket view. Hide the tag that matches this bucket's name.
// Buckets are now tagged with '@' + name.
const bucketTagName = `@${(ctx as { name: string }).name.toLowerCase()}`;
return tags.filter(t => t.toLowerCase() !== bucketTagName);
return tags.filter(t => t.toLowerCase() !== bucketTagName && !t.endsWith("_favorite__"));
}
return tags;
return tags.filter(t => !t.endsWith("_favorite__"));
});
const statusOptions = createMemo(() => getStatusOptionsForTask(props.task.recurrence));
@@ -361,6 +361,26 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
</Select>
</div>
{/* Favorite */}
<div class="flex items-center gap-1 group shrink-0">
<span class="text-[0.625rem] uppercase font-bold tracking-wider text-muted-foreground/70 hidden sm:inline">Favorite</span>
<Button
variant="ghost"
size="sm"
class={cn("h-8 px-1 sm:px-2 text-[0.625rem] font-bold uppercase tracking-wider transition-colors shrink-0", props.task.tags?.includes(getFavoriteTag()) ? "text-amber-500 hover:bg-amber-500/10" : "text-muted-foreground hover:bg-muted/50 hover:text-foreground")}
onClick={() => {
const favTag = getFavoriteTag();
const currentTags = props.task.tags || [];
const isFav = currentTags.includes(favTag);
const newTags = isFav ? currentTags.filter(t => t !== favTag) : [...currentTags, favTag];
updateTags(newTags);
}}
>
<Star size={14} class={cn("opacity-70 group-hover:opacity-100 transition-opacity shrink-0", props.task.tags?.includes(getFavoriteTag()) && "fill-amber-500")} />
<span class="hidden sm:inline sm:ml-1.5">{props.task.tags?.includes(getFavoriteTag()) ? "Starred" : "Star"}</span>
</Button>
</div>
{/* Commands Shortcut */}
<div class="flex items-center gap-1 shrink-0">
<Button
+134 -5
View File
@@ -4,6 +4,12 @@ import { pb } from "@/lib/pocketbase";
import { toast } from "solid-sonner";
import { TASGRID_COLLECTION, TAGS_COLLECTION, SHARE_RULES_COLLECTION, BUCKETS_COLLECTION, NOTES_COLLECTION, URGENCY_HOURS, SIZE_HOURS } from "@/lib/constants";
// Helper to get the current user's personalized favorite tag
export const getFavoriteTag = () => {
const userId = pb.authStore.model?.id;
return userId ? `__${userId}_favorite__` : "__favorite__";
};
const LIST_FIELDS = 'id,collectionId,collectionName,created,updated,title,startDate,dueDate,priority,status,completed,tags,user,size,recurrence,sharedWith,deletedAt';
const getStorageKey = () => {
@@ -163,6 +169,8 @@ export interface Filter {
urgencyMin: number;
urgencyMax: number;
editedToday: boolean;
ownedByMe: boolean;
starred: boolean;
}
// Background sync for Context Switcher (polling fallback for realtime)
@@ -269,6 +277,7 @@ interface TaskStore {
notes: Note[];
isNotepadMode: boolean;
quickloadTasks: string[];
noteFilter: Filter;
}
// Initial empty state
@@ -287,7 +296,9 @@ export const [store, setStore] = createStore<TaskStore>({
priorityMax: 10,
urgencyMin: 1,
urgencyMax: 10,
editedToday: false
editedToday: false,
ownedByMe: false,
starred: false
},
shareRules: [],
filterTemplates: [],
@@ -295,7 +306,18 @@ export const [store, setStore] = createStore<TaskStore>({
subscribedBuckets: [],
notes: [],
isNotepadMode: false,
quickloadTasks: []
quickloadTasks: [],
noteFilter: {
query: "",
tags: [],
priorityMin: 1,
priorityMax: 10,
urgencyMin: 1,
urgencyMax: 10,
editedToday: false,
ownedByMe: false,
starred: false
}
});
export const matchesFilter = (task: Task) => {
@@ -393,6 +415,16 @@ export const matchesFilter = (task: Task) => {
if (today !== updated) return false;
}
// Owned By Me
if (f.ownedByMe && task.ownerId !== currentUserId) {
return false;
}
// Starred
if (f.starred && !task.tags?.includes(getFavoriteTag())) {
return false;
}
return true;
};
@@ -1135,7 +1167,18 @@ export const initStore = async () => {
matrixScaleDays: prefs.matrixScaleDays || 30,
prefId: userId,
subscribedBuckets: user.subscribedBuckets || [],
quickloadTasks: prefs.quickloadTasks || []
quickloadTasks: prefs.quickloadTasks || [],
noteFilter: prefs.noteFilter || {
query: "",
tags: [],
priorityMin: 1,
priorityMax: 10,
urgencyMin: 1,
urgencyMax: 10,
editedToday: false,
ownedByMe: false,
starred: false
}
});
}
} catch (prefErr) {
@@ -2337,7 +2380,8 @@ export const syncPreferences = async () => {
uWeight: store.uWeight,
matrixScaleDays: store.matrixScaleDays,
tagDefinitions: store.tagDefinitions,
quickloadTasks: store.quickloadTasks
quickloadTasks: store.quickloadTasks,
noteFilter: store.noteFilter
};
await pb.collection('users').update(userId, {
@@ -2684,10 +2728,95 @@ export const clearFilter = () => {
priorityMax: 10,
urgencyMin: 1,
urgencyMax: 10,
editedToday: false
editedToday: false,
ownedByMe: false,
starred: false
});
};
export const setNoteFilter = (update: Partial<Filter>) => {
setStore("noteFilter", (f) => ({ ...f, ...update }));
syncPreferences();
};
const NOTE_FILTER_TEMPLATES_COLLECTION = 'TasGrid_NoteFilterTemplates';
export const loadNoteFilterTemplates = async () => {
if (!pb.authStore.isValid) return;
try {
const records = await pb.collection(NOTE_FILTER_TEMPLATES_COLLECTION).getFullList({
filter: `user = "${pb.authStore.model?.id}"`,
requestKey: null
});
const templates: FilterTemplate[] = records.map(r => ({
id: r.id,
name: r.name,
filter: r.filter as Filter
}));
setStore("filterTemplates", templates);
} catch (err) {
console.error("Failed to load note filter templates:", err);
}
};
export const saveNoteFilterTemplate = async (name: string) => {
if (!pb.authStore.isValid) return;
try {
const record = await pb.collection(NOTE_FILTER_TEMPLATES_COLLECTION).create({
user: pb.authStore.model?.id,
name,
filter: store.noteFilter
}, { requestKey: null });
const newTemplate: FilterTemplate = {
id: record.id,
name: record.name,
filter: record.filter as Filter
};
setStore("filterTemplates", prev => [...prev, newTemplate]);
toast.success(`Note filter template "${name}" saved`);
} catch (err) {
console.error("Failed to save note filter template:", err);
toast.error("Failed to save note filter template.");
}
};
export const applyNoteFilterTemplate = (id: string) => {
const template = store.filterTemplates.find(t => t.id === id);
if (template) {
setNoteFilter(template.filter);
toast.success(`Applied note filter: ${template.name}`);
}
};
export const removeNoteFilterTemplate = async (id: string) => {
if (!pb.authStore.isValid) return;
try {
await pb.collection(NOTE_FILTER_TEMPLATES_COLLECTION).delete(id);
setStore("filterTemplates", prev => prev.filter(t => t.id !== id));
toast.success("Note filter template deleted");
} catch (err) {
console.error("Failed to delete note filter template:", err);
toast.error("Failed to delete note filter template.");
}
};
export const clearNoteFilter = () => {
setStore("noteFilter", {
query: "",
tags: [],
priorityMin: 1,
priorityMax: 10,
urgencyMin: 1,
urgencyMax: 10,
editedToday: false,
ownedByMe: false,
starred: false
});
syncPreferences();
};
// Legacy cleanup - We don't need local storage persistence setup anymore
export const setupPersistence = () => {
// Moved logic to initStore which is called on auth.
+536 -207
View File
@@ -1,10 +1,13 @@
import { type Component, createSignal, createMemo, createEffect, onCleanup, For, Show } from "solid-js";
import { store, renameTagDefinition, updateNote, removeNote, restoreNote, setActiveNoteId, cleanupNoteAttachments } from "@/store";
import { store, renameTagDefinition, updateNote, removeNote, restoreNote, setActiveNoteId, cleanupNoteAttachments, getFavoriteTag } from "@/store";
import { pb } from "@/lib/pocketbase";
import { type Note } from "@/store";
import { Trash2, Lock, Unlock, Search, Link, X, ChevronRight } from "lucide-solid";
import { Trash2, Lock, Unlock, Search, Link, X, ChevronRight, ChevronDown, FileText, Plus, Star, MoreHorizontal } from "lucide-solid";
import { Button } from "@/components/ui/button";
import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover";
import { DragDropProvider, DragDropSensors, SortableProvider, createSortable, closestCenter } from "@thisbeyond/solid-dnd";
import { cn } from "@/lib/utils";
import { NOTES_COLLECTION } from "@/lib/constants";
import { TaskEditor } from "@/components/TaskEditor";
import { TaskCard } from "@/components/TaskCard";
import { NotesSidebar } from "@/components/NotesSidebar";
@@ -17,7 +20,8 @@ export const NotepadView: Component<{
}> = (props) => {
const [isLinking, setIsLinking] = createSignal(false);
const [linkSearchQuery, setLinkSearchQuery] = createSignal("");
const [isLinkedTasksOpen, setIsLinkedTasksOpen] = createSignal(true);
const [isLinkedTasksOpen, setIsLinkedTasksOpen] = createSignal(false);
const [isDragging, setIsDragging] = createSignal(false);
const currentUserId = pb.authStore.model?.id;
@@ -161,12 +165,136 @@ export const NotepadView: Component<{
}).slice(0, 10);
});
// --- Tabs Logic ---
const rootParentNote = createMemo(() => {
let current = activeNote();
let count = 0;
while (current && count < 10) {
const parentTag = current.tags?.find(t => t.startsWith("child-of-"));
if (!parentTag) break;
const parentId = parentTag.replace("child-of-", "");
const parent = store.notes.find(n => n.id === parentId);
if (!parent) break;
current = parent;
count++;
}
return current;
});
const childTabs = createMemo(() => {
const root = rootParentNote();
if (!root) return [];
const childTag = `child-of-${root.id}`;
const tabs = store.notes.filter(n => !n.deletedAt && n.tags?.includes(childTag));
tabs.sort((a, b) => {
const getOrder = (tags: string[]) => {
const orderTag = (tags || []).find(t => t.startsWith('taborder:'));
return orderTag ? parseInt(orderTag.split(':')[1]) : 999;
};
return getOrder(a.tags) - getOrder(b.tags);
});
return tabs;
});
const handleCreateTab = async () => {
const root = rootParentNote();
if (!root || !currentUserId) return;
try {
const result = await pb.collection(NOTES_COLLECTION).create({
title: "New Tab",
content: "",
tags: [`child-of-${root.id}`],
isPrivate: root.isPrivate, // inherit privacy
user: currentUserId,
});
props.setSelectedNoteId(result.id);
} catch (e) {
console.error("Failed to create tab", e);
toast.error("Failed to create tab");
}
};
const openQuickEntry = () => {
window.dispatchEvent(new KeyboardEvent('keydown', { metaKey: true, key: 'k' }));
};
const handleDragStart = () => {
setIsDragging(true);
};
const handleDragEnd = (event: any) => {
setIsDragging(false);
const { draggable, droppable } = event;
if (draggable && droppable && draggable.id !== droppable.id) {
const newTabs = [...childTabs()];
const oldIndex = newTabs.findIndex(t => t.id === draggable.id);
const newIndex = newTabs.findIndex(t => t.id === droppable.id);
if (oldIndex !== -1 && newIndex !== -1) {
const [item] = newTabs.splice(oldIndex, 1);
newTabs.splice(newIndex, 0, item);
// Fire off updates
newTabs.forEach((tab, index) => {
const cleanedTags = (tab.tags || []).filter(t => !t.startsWith('taborder:'));
cleanedTags.push(`taborder:${index}`);
handleUpdateNote(tab.id, { tags: cleanedTags });
});
}
}
};
// Sub-component for sortable tab
const SortableDesktopTab: Component<{ tab: Note, isSelected: boolean, canEdit: boolean }> = (sProps) => {
const sortable = createSortable(sProps.tab.id);
return (
<div
ref={sortable.ref}
{...(sortable.dragActivators as object)}
style={sortable.transform ? `transform: translate3d(${sortable.transform.x}px, ${sortable.transform.y}px, 0); z-index: 50;` : undefined}
class={cn(
"py-2 px-3 flex flex-row items-center justify-start relative group rounded-l-xl shadow-sm text-left truncate flex-none cursor-pointer select-none",
!isDragging() && "transition-all",
sProps.isSelected
? "bg-background border-y border-l border-border text-primary font-semibold w-full z-30"
: cn(
"bg-muted/40 border-y border-l border-border/40 text-muted-foreground z-0 w-10/12",
!isDragging() && "hover:bg-muted hover:w-full hover:border-border/80"
),
sortable.isActiveDraggable && "opacity-50 ring-2 ring-primary"
)}
onClick={() => {
if (props.selectedNoteId !== sProps.tab.id) {
props.setSelectedNoteId(sProps.tab.id);
}
}}
title={sProps.tab.title || "Untitled Tab"}
>
<Show when={sProps.isSelected} fallback={
<span class="text-xs truncate w-full">{sProps.tab.title || "Untitled Tab"}</span>
}>
<input
class="text-xs bg-transparent border-none outline-none focus:ring-0 px-0 min-w-0 w-full truncate font-semibold"
value={sProps.tab.title}
placeholder="Untitled Tab"
readOnly={!sProps.canEdit}
onClick={(e) => e.stopPropagation()}
onBlur={(e) => {
const val = e.currentTarget.value.trim() || 'Untitled Tab';
if (val !== sProps.tab.title) handleRenameNote(sProps.tab.id, sProps.tab.title, val);
}}
/>
</Show>
</div>
);
};
return (
<div class="flex flex-col md:flex-row h-full bg-background md:bg-card md:border md:border-border md:rounded-xl md:shadow-sm overflow-hidden relative">
<div class="flex flex-col md:flex-row h-full bg-background md:bg-transparent overflow-visible relative">
{/* Mobile Notes List */}
<Show when={!props.hideNavigation}>
<div class={cn(
@@ -179,8 +307,8 @@ export const NotepadView: Component<{
{/* Note Editor (Main Panel) */}
<div class={cn(
"flex-1 bg-card min-w-0 transition-all duration-200 ease-out h-full overflow-y-auto md:overflow-hidden absolute md:relative inset-0 z-20 md:z-0",
!props.selectedNoteId ? "opacity-0 pointer-events-none md:pointer-events-auto md:opacity-100 translate-x-8 md:translate-x-0 md:flex items-center justify-center bg-muted/5" : "opacity-100 translate-x-0 flex flex-col md:flex-row"
"flex-1 min-w-0 transition-all duration-200 ease-out h-full overflow-y-auto md:overflow-visible absolute md:relative inset-0 z-20 md:z-0",
!props.selectedNoteId ? "opacity-0 pointer-events-none md:pointer-events-auto md:opacity-100 translate-x-8 md:translate-x-0 md:flex items-center justify-center bg-card md:bg-muted/5 md:border md:border-border md:rounded-xl md:shadow-sm overflow-hidden" : "opacity-100 translate-x-0 flex flex-col md:flex-row bg-transparent md:justify-center"
)}>
<Show when={activeNote()} fallback={
<div class="text-center space-y-6 opacity-40 select-none flex flex-col items-center justify-center h-full">
@@ -205,225 +333,426 @@ export const NotepadView: Component<{
return (
<>
{/* Editor Area */}
<div class="flex-1 flex flex-col min-w-0 md:h-full overflow-visible md:overflow-y-auto relative px-4 md:px-6 pb-4 md:pb-6 space-y-4 md:space-y-6 scroll-smooth pt-0">
<div class="space-y-4 sticky top-0 z-30 bg-card/95 backdrop-blur-sm pt-4 pb-2 -mx-4 px-4 md:pt-6 md:-mx-6 md:px-6 md:pb-2">
<div class="flex items-start justify-between gap-4">
<textarea
class="text-3xl sm:text-4xl font-black tracking-tight bg-transparent border-none outline-none focus:ring-0 flex-1 placeholder:text-muted-foreground/30 px-0 min-w-0 resize-none overflow-hidden h-auto"
value={note().title}
placeholder="Note Title"
readOnly={!canEdit}
rows={1}
onBlur={(e) => {
const val = e.currentTarget.value.trim() || 'Untitled';
if (val !== note().title) handleRenameNote(note().id, note().title, val);
}}
onInput={(e) => {
e.currentTarget.style.height = 'auto';
e.currentTarget.style.height = e.currentTarget.scrollHeight + 'px';
}}
ref={(el) => {
createEffect(() => {
note().title;
el.style.height = 'auto';
el.style.height = el.scrollHeight + 'px';
});
}}
/>
<div class="flex items-center gap-2 shrink-0 pt-1">
<Show when={isOwner}>
<Button
variant="ghost"
size="sm"
class="hidden md:flex h-8 w-8 p-0 hover:bg-destructive/10 hover:text-destructive text-muted-foreground transition-all rounded-xl border border-border/40 shadow-sm"
onClick={() => handleDeleteNote(note().id)}
title="Move to Trash"
>
<Trash2 size={14} />
</Button>
<Button
variant="ghost"
size="sm"
class={cn("h-8 text-[0.625rem] font-bold uppercase tracking-widest", note().isPrivate ? "text-orange-500 hover:bg-orange-500/10" : "text-muted-foreground")}
onClick={() => handleUpdateNote(note().id, { isPrivate: !note().isPrivate })}
>
<Show when={note().isPrivate} fallback={<><Unlock size={12} class="mr-1.5" /> Public</>}>
<><Lock size={12} class="mr-1.5" /> Private</>
</Show>
</Button>
</Show>
</div>
</div>
{/* Desktop Style Tabs (Left Side) - Ribbon Bookmark Style */}
<div class="hidden md:flex flex-col w-32 shrink-0 bg-transparent pt-12 items-end gap-1.5 z-30 transition-all relative border-0 -mr-[1px]">
</div>
<DragDropProvider onDragStart={handleDragStart} onDragEnd={handleDragEnd} collisionDetector={closestCenter}>
<DragDropSensors />
{/* Editor Instance */}
<div class="grow shrink-0 flex flex-col min-h-[300px] border border-border/50 rounded-xl p-4 bg-background shadow-sm mb-4">
<TaskEditor
content={note().content}
onUpdate={(html) => handleUpdateContent(note().id, html)}
editable={canEdit}
/>
<Show when={rootParentNote()}>
{(root) => (
<div
class={cn(
"py-2 px-3 flex flex-row items-center justify-start relative group rounded-l-xl shadow-sm text-left truncate flex-none cursor-pointer",
!isDragging() && "transition-all",
props.selectedNoteId === root().id
? "bg-background border-y border-l border-border text-primary font-semibold w-full z-30"
: cn(
"bg-muted/40 border-y border-l border-border/40 text-muted-foreground z-0 w-10/12",
!isDragging() && "hover:bg-muted hover:w-full hover:border-border/80"
)
)}
onClick={() => {
if (props.selectedNoteId !== root().id) props.setSelectedNoteId(root().id);
}}
>
<span class="text-xs truncate w-full">General</span>
</div>
)}
</Show>
<SortableProvider ids={childTabs().map(t => t.id)}>
<For each={childTabs()}>
{(tab) => (
<SortableDesktopTab
tab={tab}
isSelected={props.selectedNoteId === tab.id}
canEdit={canEdit}
/>
)}
</For>
</SortableProvider>
</DragDropProvider>
<div class="w-8 h-8 rounded-xl bg-transparent hover:bg-muted border border-dashed border-border flex items-center justify-center text-muted-foreground transition-colors mt-2 mr-3 cursor-pointer"
onClick={() => handleCreateTab()}
title="Add Tab"
>
<Plus size={16} />
</div>
<div class="shrink-0 h-16 md:h-8" />
</div>
{/* Linked Tasks Sidebar */}
<div class={cn(
"border-t md:border-t-0 md:border-l border-border bg-muted/5 flex flex-col shrink-0 transition-all duration-300 ease-in-out overflow-hidden",
isLinkedTasksOpen() ? "w-full md:w-80 lg:w-96 h-auto md:h-full" : "w-full md:w-12 h-12 md:h-full cursor-pointer hover:bg-muted/10"
)}
onClick={() => {
if (!isLinkedTasksOpen()) {
setIsLinkedTasksOpen(true);
}
}}>
<div class={cn("p-4 border-b border-border bg-card/50 flex flex-col gap-3", !isLinkedTasksOpen() && "items-center justify-center p-0 h-full md:py-4")}>
<div
class="flex items-center justify-between cursor-pointer group"
onClick={(e) => {
e.stopPropagation();
setIsLinkedTasksOpen(!isLinkedTasksOpen());
}}
>
<div class={cn("flex items-center gap-2", !isLinkedTasksOpen() && "md:flex-col")}>
<ChevronRight size={14} class={cn("text-muted-foreground transition-all duration-300 group-hover:text-foreground", !isLinkedTasksOpen() ? "-rotate-90 md:rotate-180" : "rotate-90 md:rotate-0")} />
<Show when={isLinkedTasksOpen()} fallback={
<div class="hidden md:flex items-center justify-center -rotate-90 origin-center whitespace-nowrap mt-8 text-[0.625rem] font-bold uppercase tracking-widest text-muted-foreground/50">
<Link size={10} class="mr-2 rotate-90" />
Linked Tasks
</div>
}>
<h3 class="text-[0.625rem] font-bold uppercase tracking-widest text-muted-foreground flex items-center gap-2">
<Link size={12} />
Linked Tasks
</h3>
</Show>
<Show when={!isLinkedTasksOpen()}>
<div class="md:hidden flex items-center justify-center text-[0.625rem] font-bold uppercase tracking-widest text-muted-foreground">
<Link size={12} class="mr-2" />
Linked Tasks
{/* Editor Area Container */}
<div class="flex-1 md:flex-initial md:shrink w-full md:w-auto flex flex-col md:flex-row min-w-0 md:h-full md:bg-card md:border md:border-border md:rounded-xl md:shadow-[0_4px_12px_rgba(0,0,0,0.02)] overflow-visible md:overflow-hidden z-20 relative shadow-[-4px_0_12px_rgba(0,0,0,0.02)] transition-all duration-300">
{/* Editor Area */}
<div class="flex-1 md:flex-initial md:shrink flex flex-col min-w-0 md:w-[960px] lg:w-[1120px] md:h-full overflow-visible md:overflow-y-auto relative px-4 md:px-6 pb-4 md:pb-6 space-y-4 md:space-y-6 scroll-smooth pt-0 bg-background z-20">
<div class="space-y-4 sticky top-0 z-30 bg-card/95 backdrop-blur-sm pt-4 pb-2 -mx-4 px-4 md:pt-6 md:-mx-6 md:px-6 md:pb-2">
<div class="flex items-start justify-between gap-4">
<div class="flex flex-col min-w-0 flex-1 gap-1">
<Show when={rootParentNote()}>
{(root) => (
<textarea
class={cn(
"font-black tracking-tight bg-transparent border-none outline-none focus:ring-0 flex-1 placeholder:text-muted-foreground/30 px-0 min-w-0 resize-none overflow-hidden h-auto text-3xl sm:text-4xl w-full text-foreground"
)}
value={root().title}
placeholder="Note Title"
readOnly={!canEdit}
rows={1}
onBlur={(e) => {
const val = e.currentTarget.value.trim() || 'Untitled';
if (val !== root().title) handleRenameNote(root().id, root().title, val);
}}
onInput={(e) => {
e.currentTarget.style.height = 'auto';
e.currentTarget.style.height = e.currentTarget.scrollHeight + 'px';
}}
ref={(el) => {
createEffect(() => {
root().title;
el.style.height = 'auto';
el.style.height = el.scrollHeight + 'px';
});
}}
/>
)}
</Show>
</div>
<div class="flex items-center gap-2 shrink-0 pt-1">
{/* Mobile Tabs Dropdown Moved to Footer */}
<Button
variant="ghost"
size="sm"
class={cn(
"h-8 w-8 p-0 transition-all rounded-xl border border-border/40 shadow-sm",
note().tags?.includes(getFavoriteTag()) ? "bg-amber-500/10 text-amber-500 hover:bg-amber-500/20" : "text-muted-foreground hover:bg-muted"
)}
onClick={async (e) => {
e.stopPropagation();
const favTag = getFavoriteTag();
const currentTags = note().tags || [];
const isFav = currentTags.includes(favTag);
const newTags = isFav ? currentTags.filter(t => t !== favTag) : [...currentTags, favTag];
await handleUpdateNote(note().id, { tags: newTags });
}}
title="Toggle Favorite"
>
<Star size={14} class={cn(note().tags?.includes(getFavoriteTag()) && "fill-amber-500")} />
</Button>
<Show when={isOwner}>
<Button
variant="ghost"
size="sm"
class="hidden md:flex h-8 w-8 p-0 hover:bg-destructive/10 hover:text-destructive text-muted-foreground transition-all rounded-xl border border-border/40 shadow-sm"
onClick={() => handleDeleteNote(note().id)}
title="Move to Trash"
>
<Trash2 size={14} />
</Button>
<Button
variant="ghost"
size="sm"
class={cn("hidden md:flex h-8 text-[0.625rem] font-bold uppercase tracking-widest", note().isPrivate ? "text-orange-500 hover:bg-orange-500/10" : "text-muted-foreground")}
onClick={() => handleUpdateNote(note().id, { isPrivate: !note().isPrivate })}
>
<Show when={note().isPrivate} fallback={<><Unlock size={12} class="mr-1.5" /> Public</>}>
<><Lock size={12} class="mr-1.5" /> Private</>
</Show>
</Button>
</Show>
</div>
</div>
</div>
{/* Inside Tabs Section Removed: Sub-tabs are now displayed on the outside as hanging bubbles */}
{/* Editor Instance */}
<div class="grow shrink-0 flex flex-col min-h-[300px] border border-border/50 rounded-xl p-4 bg-background shadow-sm mb-4">
<TaskEditor
content={note().content}
onUpdate={(html) => handleUpdateContent(note().id, html)}
editable={canEdit}
/>
</div>
<div class="shrink-0 h-16 md:h-8" />
</div>
{/* Linked Tasks Sidebar */}
<div class={cn(
"border-t md:border-t-0 md:border-l border-border bg-muted/5 flex flex-col shrink-0 transition-all duration-300 ease-in-out overflow-hidden",
isLinkedTasksOpen() ? "w-full md:w-80 lg:w-96 h-auto md:h-full" : "w-full md:w-12 h-12 md:h-full cursor-pointer hover:bg-muted/10"
)}
onClick={() => {
if (!isLinkedTasksOpen()) {
setIsLinkedTasksOpen(true);
}
}}>
<div class={cn("p-4 border-b border-border bg-card/50 flex flex-col gap-3", !isLinkedTasksOpen() && "items-center justify-center p-0 h-full md:py-4")}>
<div
class="flex items-center justify-between cursor-pointer group"
onClick={(e) => {
e.stopPropagation();
setIsLinkedTasksOpen(!isLinkedTasksOpen());
}}
>
<div class={cn("flex items-center gap-2", !isLinkedTasksOpen() && "md:flex-col")}>
<ChevronRight size={14} class={cn("text-muted-foreground transition-all duration-300 group-hover:text-foreground", !isLinkedTasksOpen() ? "-rotate-90 md:rotate-180" : "rotate-90 md:rotate-0")} />
<Show when={isLinkedTasksOpen()} fallback={
<div class="hidden md:flex items-center justify-center -rotate-90 origin-center whitespace-nowrap mt-8 text-[0.625rem] font-bold uppercase tracking-widest text-muted-foreground/50">
<Link size={10} class="mr-2 rotate-90" />
Linked Tasks
</div>
}>
<h3 class="text-[0.625rem] font-bold uppercase tracking-widest text-muted-foreground flex items-center gap-2">
<Link size={12} />
Linked Tasks
</h3>
</Show>
<Show when={!isLinkedTasksOpen()}>
<div class="md:hidden flex items-center justify-center text-[0.625rem] font-bold uppercase tracking-widest text-muted-foreground">
<Link size={12} class="mr-2" />
Linked Tasks
</div>
</Show>
</div>
<Show when={isLinkedTasksOpen()}>
<div class="flex items-center gap-1">
<Button
size="icon"
variant="ghost"
class="h-7 w-7 rounded-lg text-muted-foreground hover:text-primary hover:bg-primary/10"
onClick={(e) => {
e.stopPropagation();
setIsLinking(!isLinking());
}}
title="Link Existing Task"
>
<Link size={14} />
</Button>
<Button
size="sm"
variant="ghost"
class="h-7 px-2 text-[0.625rem] font-bold uppercase tracking-wider text-primary hover:bg-primary/10 rounded-lg"
onClick={(e) => {
e.stopPropagation();
openQuickEntry();
}}
title="Create Task via Quick Entry"
>
+ Task
</Button>
</div>
</Show>
</div>
<Show when={isLinkedTasksOpen()}>
<div class="flex items-center gap-1">
<Button
size="icon"
variant="ghost"
class="h-7 w-7 rounded-lg text-muted-foreground hover:text-primary hover:bg-primary/10"
onClick={(e) => {
e.stopPropagation();
setIsLinking(!isLinking());
}}
title="Link Existing Task"
>
<Link size={14} />
</Button>
<Button
size="sm"
variant="ghost"
class="h-7 px-2 text-[0.625rem] font-bold uppercase tracking-wider text-primary hover:bg-primary/10 rounded-lg"
onClick={(e) => {
e.stopPropagation();
openQuickEntry();
}}
title="Create Task via Quick Entry"
>
+ Task
</Button>
<Show when={isLinkedTasksOpen() && isLinking()}>
<div class="animate-in fade-in slide-in-from-top-2 space-y-2">
<div class="relative">
<Search size={14} class="absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground/50" />
<input
type="text"
placeholder="Search tasks to link..."
value={linkSearchQuery()}
onInput={(e) => setLinkSearchQuery(e.currentTarget.value)}
class="w-full bg-background border border-border/50 rounded-lg pl-8 pr-3 py-1.5 text-xs outline-none focus:ring-1 focus:ring-primary/30 shadow-sm"
/>
</div>
<Show when={linkSearchQuery() && unlinkedTasks().length > 0}>
<div class="bg-background border border-border rounded-lg shadow-xl overflow-hidden max-h-48 overflow-y-auto">
<For each={unlinkedTasks()}>
{(t) => (
<button
class="w-full text-left p-2 text-xs hover:bg-muted border-b border-border last:border-0 truncate"
onClick={() => handleLinkTask(t.id)}
>
{t.title}
</button>
)}
</For>
</div>
</Show>
</div>
</Show>
</div>
<Show when={isLinkedTasksOpen() && isLinking()}>
<div class="animate-in fade-in slide-in-from-top-2 space-y-2">
<div class="relative">
<Search size={14} class="absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground/50" />
<input
type="text"
placeholder="Search tasks to link..."
value={linkSearchQuery()}
onInput={(e) => setLinkSearchQuery(e.currentTarget.value)}
class="w-full bg-background border border-border/50 rounded-lg pl-8 pr-3 py-1.5 text-xs outline-none focus:ring-1 focus:ring-primary/30 shadow-sm"
/>
</div>
<Show when={linkSearchQuery() && unlinkedTasks().length > 0}>
<div class="bg-background border border-border rounded-lg shadow-xl overflow-hidden max-h-48 overflow-y-auto">
<For each={unlinkedTasks()}>
{(t) => (
<button
class="w-full text-left p-2 text-xs hover:bg-muted border-b border-border last:border-0 truncate"
onClick={() => handleLinkTask(t.id)}
>
{t.title}
</button>
)}
</For>
<Show when={isLinkedTasksOpen()}>
<div class="flex-1 overflow-y-auto p-4 space-y-4">
<For each={linkedTasks()} fallback={
<div class="text-center text-xs text-muted-foreground p-6 bg-muted/10 border border-dashed border-border/50 rounded-xl flex flex-col items-center gap-2">
<Link size={24} class="opacity-20" />
<p>No tasks linked yet.</p>
<p class="text-[0.6rem] opacity-70 max-w-[200px]">Link an existing task, create a new one, or add the tag #{note().title} to a task.</p>
</div>
</Show>
}>
{(task) => (
<div class="relative group">
<TaskCard task={task} />
{/* If explicitly linked, allow unlinking. If linked by tag, they should remove the tag from the task, which we can't easily do here unless we implement it. */}
<Show when={note().tasks && note().tasks.includes(task.id)}>
<button
class="absolute -top-2 -right-2 bg-destructive text-destructive-foreground rounded-full p-1 opacity-0 group-hover:opacity-100 transition-opacity shadow-sm hover:scale-110"
onClick={(e) => {
e.stopPropagation();
handleUnlinkTask(task.id);
}}
title="Unlink Task"
>
<X size={12} />
</button>
</Show>
</div>
)}
</For>
</div>
</Show>
</div>
<Show when={isLinkedTasksOpen()}>
<div class="flex-1 overflow-y-auto p-4 space-y-4">
<For each={linkedTasks()} fallback={
<div class="text-center text-xs text-muted-foreground p-6 bg-muted/10 border border-dashed border-border/50 rounded-xl flex flex-col items-center gap-2">
<Link size={24} class="opacity-20" />
<p>No tasks linked yet.</p>
<p class="text-[0.6rem] opacity-70 max-w-[200px]">Link an existing task, create a new one, or add the tag #{note().title} to a task.</p>
</div>
}>
{(task) => (
<div class="relative group">
<TaskCard task={task} />
{/* If explicitly linked, allow unlinking. If linked by tag, they should remove the tag from the task, which we can't easily do here unless we implement it. */}
<Show when={note().tasks && note().tasks.includes(task.id)}>
<button
class="absolute -top-2 -right-2 bg-destructive text-destructive-foreground rounded-full p-1 opacity-0 group-hover:opacity-100 transition-opacity shadow-sm hover:scale-110"
onClick={(e) => {
e.stopPropagation();
handleUnlinkTask(task.id);
}}
title="Unlink Task"
>
<X size={12} />
</button>
</Show>
</div>
)}
</For>
</div>
</Show>
</div>
{/* Sticky Footer Actions (Match TaskDetail style) */}
<div class="px-6 py-4 border-t border-border/50 md:hidden flex items-center justify-between shrink-0 bg-background/95 backdrop-blur-sm z-40 w-full sticky bottom-0">
{/* Delete button (persistent for owner) */}
<Show when={isOwner}>
<Button
variant="ghost"
size="sm"
class="h-9 px-3 gap-2 hover:bg-destructive/10 hover:text-destructive text-muted-foreground transition-all rounded-xl border border-border/40"
onClick={() => handleDeleteNote(note().id)}
>
<Trash2 size={16} />
<span class="text-xs font-bold uppercase tracking-wider">Delete</span>
</Button>
</Show>
<div class="px-4 py-3 border-t border-border/50 md:hidden flex items-center justify-between shrink-0 bg-background/95 backdrop-blur-sm z-40 w-full sticky bottom-0">
<div class="flex items-center gap-2 w-16 shrink-0">
{/* "..." popover menu containing Public/Private toggle and Delete */}
<Show when={isOwner}>
<Popover>
<PopoverTrigger
as={Button}
variant="ghost"
size="sm"
class="h-9 w-9 p-0 hover:bg-muted text-muted-foreground transition-all rounded-xl border border-border/40 shrink-0"
>
<MoreHorizontal size={16} />
</PopoverTrigger>
<PopoverContent class="w-48 p-2" align="start">
<div class="flex flex-col gap-1">
<Button
variant="ghost"
size="sm"
class={cn("w-full justify-start text-[0.625rem] font-bold uppercase tracking-widest gap-2", note().isPrivate ? "text-orange-500 hover:bg-orange-500/10" : "text-muted-foreground hover:bg-muted")}
onClick={(e) => {
e.stopPropagation();
handleUpdateNote(note().id, { isPrivate: !note().isPrivate });
}}
>
<Show when={note().isPrivate} fallback={<><Unlock size={14} /> Public</>}>
<><Lock size={14} /> Private</>
</Show>
</Button>
<Button
variant="ghost"
size="sm"
class="w-full justify-start text-[0.625rem] font-bold uppercase tracking-widest gap-2 hover:bg-destructive/10 hover:text-destructive text-muted-foreground transition-all"
onClick={(e) => {
e.stopPropagation();
handleDeleteNote(note().id);
}}
>
<Trash2 size={14} />
Delete
</Button>
</div>
</PopoverContent>
</Popover>
</Show>
</div>
<Show when={!props.hideNavigation}>
{/* Close button (Mobile only) */}
<Button
variant="ghost"
size="sm"
class="md:hidden h-9 px-3 gap-2 hover:bg-muted text-muted-foreground transition-all rounded-xl border border-border/40"
onClick={() => props.setSelectedNoteId(null)}
>
<X size={16} />
<span class="text-xs font-bold uppercase tracking-wider">Close</span>
</Button>
</Show>
{/* Tab dropdown moved to middle */}
<div class="flex-1 flex justify-center px-2 min-w-0">
<Show when={rootParentNote()}>
<Popover>
<PopoverTrigger
as={Button}
variant="ghost"
size="sm"
class="h-9 px-3 text-xs gap-1 max-w-[150px] bg-muted/30 hover:bg-muted/50 rounded-xl w-full"
>
<FileText size={14} class="shrink-0" />
<span class="truncate">Tabs ({childTabs().length + (rootParentNote() ? 1 : 0)})</span>
<ChevronDown size={14} class="opacity-50 shrink-0" />
</PopoverTrigger>
<PopoverContent class="w-[200px] p-2 bg-popover text-popover-foreground rounded-xl shadow-lg border-border" align="center">
<div class="flex flex-col gap-1 max-h-60 overflow-y-auto">
<Show when={rootParentNote()}>
{(root) => (
<Button
variant="ghost"
class={cn(
"w-full justify-start px-3 py-2 text-sm hover:bg-muted border-border/50 truncate flex items-center h-auto font-normal rounded-lg",
props.selectedNoteId === root().id && "bg-muted font-medium text-primary border"
)}
onClick={() => props.setSelectedNoteId(root().id)}
>
<span class="truncate">General</span>
</Button>
)}
</Show>
<For each={childTabs()}>
{(tab) => (
<Button
variant="ghost"
class={cn(
"w-full justify-start px-3 py-2 text-sm hover:bg-muted border-border/50 truncate flex items-center h-auto font-normal rounded-lg group",
props.selectedNoteId === tab.id && "bg-muted font-medium text-primary border"
)}
onClick={() => props.setSelectedNoteId(tab.id)}
>
<Show when={props.selectedNoteId === tab.id} fallback={
<span class="truncate">{tab.title || "Untitled Tab"}</span>
}>
<input
class="text-sm bg-transparent border-none outline-none focus:ring-0 px-0 min-w-0 w-full truncate font-semibold cursor-text"
value={tab.title}
placeholder="Untitled Tab"
readOnly={!canEdit}
onClick={(e) => e.stopPropagation()}
onBlur={(e) => {
const val = e.currentTarget.value.trim() || 'Untitled Tab';
if (val !== tab.title) handleRenameNote(tab.id, tab.title, val);
}}
/>
</Show>
</Button>
)}
</For>
<Button
variant="ghost"
class="w-full justify-start px-3 py-2 text-sm hover:bg-primary/10 text-primary font-medium transition-colors gap-2 h-auto rounded-lg mt-1"
onClick={handleCreateTab}
>
<Plus size={14} /> Add Tab
</Button>
</div>
</PopoverContent>
</Popover>
</Show>
<Show when={childTabs().length === 0 && !rootParentNote()}>
<Button
variant="ghost"
size="sm"
class="h-9 px-3 text-xs gap-1 bg-muted/30 hover:bg-muted/50 rounded-xl"
onClick={() => handleCreateTab()}
>
<Plus size={14} /> Add Tab
</Button>
</Show>
</div>
<div class="flex items-center gap-2 w-16 justify-end shrink-0">
<Show when={!props.hideNavigation}>
{/* Close button (Mobile only) */}
<Button
variant="ghost"
size="sm"
class="md:hidden h-9 px-3 gap-2 hover:bg-muted text-muted-foreground transition-all rounded-xl border border-border/40 shrink-0"
onClick={() => props.setSelectedNoteId(null)}
>
<X size={16} />
<span class="text-xs font-bold uppercase tracking-wider">Close</span>
</Button>
</Show>
</div>
</div>
</>
);