Partial Note Tabs Setup - NOT FOR PRODUCTION
This commit is contained in:
+120
-18
@@ -1,7 +1,7 @@
|
|||||||
import { type Component, createSignal, createMemo, For, Show } from "solid-js";
|
import { type Component, createSignal, createMemo, For, Show, onCleanup, createEffect } from "solid-js";
|
||||||
import { store } from "@/store";
|
import { store, setStore, syncPreferences } from "@/store";
|
||||||
import { pb } from "@/lib/pocketbase";
|
import { pb } from "@/lib/pocketbase";
|
||||||
import { Plus, Lock, Search } from "lucide-solid";
|
import { Plus, Lock, Search, Filter } from "lucide-solid";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { NOTES_COLLECTION } from "@/lib/constants";
|
import { NOTES_COLLECTION } from "@/lib/constants";
|
||||||
@@ -10,15 +10,66 @@ export const NotesSidebar: Component<{
|
|||||||
selectedNoteId: string | null;
|
selectedNoteId: string | null;
|
||||||
setSelectedNoteId: (id: string | null) => void;
|
setSelectedNoteId: (id: string | null) => void;
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
|
let containerRef: HTMLDivElement | undefined;
|
||||||
const [searchQuery, setSearchQuery] = createSignal("");
|
const [searchQuery, setSearchQuery] = createSignal("");
|
||||||
|
const [showFilters, setShowFilters] = createSignal(false);
|
||||||
const currentUserId = pb.authStore.model?.id;
|
const currentUserId = pb.authStore.model?.id;
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!showFilters()) return;
|
||||||
|
|
||||||
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
|
if (containerRef && !containerRef.contains(e.target as Node)) {
|
||||||
|
setShowFilters(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
onCleanup(() => document.removeEventListener("mousedown", handleClickOutside));
|
||||||
|
});
|
||||||
|
|
||||||
|
const availableTags = createMemo(() => {
|
||||||
|
const tags = new Set<string>();
|
||||||
|
store.notes.forEach(n => {
|
||||||
|
if (!n.deletedAt) n.tags.forEach(t => tags.add(t));
|
||||||
|
});
|
||||||
|
return Array.from(tags).sort();
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleTagFilter = (tag: string) => {
|
||||||
|
const current = store.noteFilter.tags;
|
||||||
|
const next = current.includes(tag) ? current.filter(t => t !== tag) : [...current, tag];
|
||||||
|
setStore("noteFilter", "tags", next);
|
||||||
|
syncPreferences();
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleOwnedByMe = () => {
|
||||||
|
setStore("noteFilter", "ownedByMe", !store.noteFilter.ownedByMe);
|
||||||
|
syncPreferences();
|
||||||
|
};
|
||||||
|
|
||||||
|
const isFilterActive = createMemo(() => {
|
||||||
|
return store.noteFilter.ownedByMe || store.noteFilter.tags.length > 0;
|
||||||
|
});
|
||||||
|
|
||||||
const filteredNotes = createMemo(() => {
|
const filteredNotes = createMemo(() => {
|
||||||
let n = store.notes.filter(note => !note.deletedAt);
|
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;
|
||||||
|
});
|
||||||
|
|
||||||
if (searchQuery()) {
|
if (searchQuery()) {
|
||||||
const q = searchQuery().toLowerCase();
|
const q = searchQuery().toLowerCase();
|
||||||
n = n.filter(note => note.title.toLowerCase().includes(q) || note.tags.some(t => t.toLowerCase().includes(q)));
|
n = n.filter(note => note.title.toLowerCase().includes(q) || note.tags.some(t => t.toLowerCase().includes(q)));
|
||||||
}
|
}
|
||||||
|
if (store.noteFilter.ownedByMe) {
|
||||||
|
n = n.filter(note => note.user === currentUserId);
|
||||||
|
}
|
||||||
|
if (store.noteFilter.tags.length > 0) {
|
||||||
|
n = n.filter(note => store.noteFilter.tags.some(t => note.tags.includes(t)));
|
||||||
|
}
|
||||||
return n;
|
return n;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -40,21 +91,72 @@ export const NotesSidebar: Component<{
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col h-full bg-card">
|
<div class="flex flex-col h-full bg-card">
|
||||||
{/* Header */}
|
<div ref={containerRef} class="z-20">
|
||||||
<div class="p-4 border-b border-border bg-card/50 backdrop-blur flex justify-between items-center gap-2">
|
{/* Header */}
|
||||||
<div class="relative flex-1">
|
<div class="p-4 border-b border-border bg-card/50 backdrop-blur flex justify-between items-center gap-2">
|
||||||
<Search size={14} class="absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground/50" />
|
<div class="relative flex-1">
|
||||||
<input
|
<Search size={14} class="absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground/50" />
|
||||||
type="text"
|
<input
|
||||||
placeholder="Search notes..."
|
type="text"
|
||||||
value={searchQuery()}
|
placeholder="Search notes..."
|
||||||
onInput={(e) => setSearchQuery(e.currentTarget.value)}
|
value={searchQuery()}
|
||||||
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"
|
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>
|
||||||
|
<Button onClick={() => setShowFilters(!showFilters())} size="sm" variant={showFilters() ? "secondary" : "ghost"} class="h-8 w-8 p-0 shrink-0 relative">
|
||||||
|
<Filter size={16} />
|
||||||
|
<Show when={isFilterActive()}>
|
||||||
|
<span class="absolute top-1 right-1 w-2 h-2 bg-primary rounded-full border-2 border-background animate-in fade-in zoom-in duration-300" />
|
||||||
|
</Show>
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleCreateNote} size="sm" variant="default" class="h-8 w-8 p-0 shrink-0 shadow-sm">
|
||||||
|
<Plus size={16} />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={handleCreateNote} size="sm" variant="default" class="h-8 w-8 p-0 shrink-0 shadow-sm">
|
|
||||||
<Plus size={16} />
|
<Show when={showFilters()}>
|
||||||
</Button>
|
<div class="p-4 border-b border-border bg-muted/20 space-y-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<label class="text-xs font-medium text-foreground">Owned by me</label>
|
||||||
|
<button
|
||||||
|
onClick={toggleOwnedByMe}
|
||||||
|
class={cn(
|
||||||
|
"w-9 h-5 rounded-full transition-colors relative",
|
||||||
|
store.noteFilter.ownedByMe ? "bg-primary" : "bg-muted-foreground/30"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div class={cn(
|
||||||
|
"absolute top-0.5 left-0.5 w-4 h-4 bg-background rounded-full transition-transform shadow-sm",
|
||||||
|
store.noteFilter.ownedByMe ? "translate-x-4" : "translate-x-0"
|
||||||
|
)}></div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-xs font-medium text-foreground">Tags (Any)</label>
|
||||||
|
<div class="flex flex-wrap gap-1.5">
|
||||||
|
<For each={availableTags()} fallback={<span class="text-xs text-muted-foreground">No tags found</span>}>
|
||||||
|
{(tag) => {
|
||||||
|
const isSelected = store.noteFilter.tags.includes(tag);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={() => toggleTagFilter(tag)}
|
||||||
|
class={cn(
|
||||||
|
"text-xs px-2 py-1 rounded-md border transition-colors",
|
||||||
|
isSelected
|
||||||
|
? "bg-primary/10 border-primary/50 text-primary font-medium"
|
||||||
|
: "bg-card border-border hover:bg-muted text-muted-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* List */}
|
{/* List */}
|
||||||
|
|||||||
+7
-3
@@ -269,6 +269,7 @@ interface TaskStore {
|
|||||||
notes: Note[];
|
notes: Note[];
|
||||||
isNotepadMode: boolean;
|
isNotepadMode: boolean;
|
||||||
quickloadTasks: string[];
|
quickloadTasks: string[];
|
||||||
|
noteFilter: { tags: string[]; ownedByMe: boolean };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initial empty state
|
// Initial empty state
|
||||||
@@ -295,7 +296,8 @@ export const [store, setStore] = createStore<TaskStore>({
|
|||||||
subscribedBuckets: [],
|
subscribedBuckets: [],
|
||||||
notes: [],
|
notes: [],
|
||||||
isNotepadMode: false,
|
isNotepadMode: false,
|
||||||
quickloadTasks: []
|
quickloadTasks: [],
|
||||||
|
noteFilter: { tags: [], ownedByMe: false }
|
||||||
});
|
});
|
||||||
|
|
||||||
export const matchesFilter = (task: Task) => {
|
export const matchesFilter = (task: Task) => {
|
||||||
@@ -1135,7 +1137,8 @@ export const initStore = async () => {
|
|||||||
matrixScaleDays: prefs.matrixScaleDays || 30,
|
matrixScaleDays: prefs.matrixScaleDays || 30,
|
||||||
prefId: userId,
|
prefId: userId,
|
||||||
subscribedBuckets: user.subscribedBuckets || [],
|
subscribedBuckets: user.subscribedBuckets || [],
|
||||||
quickloadTasks: prefs.quickloadTasks || []
|
quickloadTasks: prefs.quickloadTasks || [],
|
||||||
|
noteFilter: prefs.noteFilter || { tags: [], ownedByMe: false }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (prefErr) {
|
} catch (prefErr) {
|
||||||
@@ -2337,7 +2340,8 @@ export const syncPreferences = async () => {
|
|||||||
uWeight: store.uWeight,
|
uWeight: store.uWeight,
|
||||||
matrixScaleDays: store.matrixScaleDays,
|
matrixScaleDays: store.matrixScaleDays,
|
||||||
tagDefinitions: store.tagDefinitions,
|
tagDefinitions: store.tagDefinitions,
|
||||||
quickloadTasks: store.quickloadTasks
|
quickloadTasks: store.quickloadTasks,
|
||||||
|
noteFilter: store.noteFilter
|
||||||
};
|
};
|
||||||
|
|
||||||
await pb.collection('users').update(userId, {
|
await pb.collection('users').update(userId, {
|
||||||
|
|||||||
+458
-183
@@ -2,9 +2,10 @@ import { type Component, createSignal, createMemo, createEffect, onCleanup, For,
|
|||||||
import { store, renameTagDefinition, updateNote, removeNote, restoreNote, setActiveNoteId, cleanupNoteAttachments } from "@/store";
|
import { store, renameTagDefinition, updateNote, removeNote, restoreNote, setActiveNoteId, cleanupNoteAttachments } from "@/store";
|
||||||
import { pb } from "@/lib/pocketbase";
|
import { pb } from "@/lib/pocketbase";
|
||||||
import { type Note } from "@/store";
|
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, ArrowLeft, Plus } from "lucide-solid";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { NOTES_COLLECTION } from "@/lib/constants";
|
||||||
import { TaskEditor } from "@/components/TaskEditor";
|
import { TaskEditor } from "@/components/TaskEditor";
|
||||||
import { TaskCard } from "@/components/TaskCard";
|
import { TaskCard } from "@/components/TaskCard";
|
||||||
import { NotesSidebar } from "@/components/NotesSidebar";
|
import { NotesSidebar } from "@/components/NotesSidebar";
|
||||||
@@ -161,12 +162,75 @@ export const NotepadView: Component<{
|
|||||||
}).slice(0, 10);
|
}).slice(0, 10);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Tabs Logic ---
|
||||||
|
const childTabs = createMemo(() => {
|
||||||
|
const note = activeNote();
|
||||||
|
if (!note) return [];
|
||||||
|
const childTag = `child-of-${note.id}`;
|
||||||
|
return store.notes.filter(n => !n.deletedAt && n.tags?.includes(childTag));
|
||||||
|
});
|
||||||
|
|
||||||
|
const parentNote = createMemo(() => {
|
||||||
|
const note = activeNote();
|
||||||
|
if (!note) return null;
|
||||||
|
const parentTag = note.tags?.find(t => t.startsWith("child-of-"));
|
||||||
|
if (!parentTag) return null;
|
||||||
|
const parentId = parentTag.replace("child-of-", "");
|
||||||
|
return store.notes.find(n => n.id === parentId) || null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const siblingTabs = createMemo(() => {
|
||||||
|
const parent = parentNote();
|
||||||
|
if (!parent) return [];
|
||||||
|
const childTag = `child-of-${parent.id}`;
|
||||||
|
return store.notes.filter(n => !n.deletedAt && n.tags?.includes(childTag));
|
||||||
|
});
|
||||||
|
|
||||||
|
const outsideTabs = createMemo(() => parentNote() ? siblingTabs() : childTabs());
|
||||||
|
const insideTabs = createMemo(() => parentNote() ? childTabs() : []);
|
||||||
|
|
||||||
|
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 handleCreateTab = async (parentIdOverride?: string) => {
|
||||||
|
const note = activeNote();
|
||||||
|
if (!note || !currentUserId) return;
|
||||||
|
|
||||||
|
const parentId = parentIdOverride || note.id;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await pb.collection(NOTES_COLLECTION).create({
|
||||||
|
title: "New Tab",
|
||||||
|
content: "",
|
||||||
|
tags: [`child-of-${parentId}`],
|
||||||
|
isPrivate: note.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 = () => {
|
const openQuickEntry = () => {
|
||||||
window.dispatchEvent(new KeyboardEvent('keydown', { metaKey: true, key: 'k' }));
|
window.dispatchEvent(new KeyboardEvent('keydown', { metaKey: true, key: 'k' }));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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 */}
|
{/* Mobile Notes List */}
|
||||||
<Show when={!props.hideNavigation}>
|
<Show when={!props.hideNavigation}>
|
||||||
<div class={cn(
|
<div class={cn(
|
||||||
@@ -179,8 +243,8 @@ export const NotepadView: Component<{
|
|||||||
|
|
||||||
{/* Note Editor (Main Panel) */}
|
{/* Note Editor (Main Panel) */}
|
||||||
<div class={cn(
|
<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",
|
"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-muted/5" : "opacity-100 translate-x-0 flex flex-col md:flex-row"
|
!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"
|
||||||
)}>
|
)}>
|
||||||
<Show when={activeNote()} fallback={
|
<Show when={activeNote()} fallback={
|
||||||
<div class="text-center space-y-6 opacity-40 select-none flex flex-col items-center justify-center h-full">
|
<div class="text-center space-y-6 opacity-40 select-none flex flex-col items-center justify-center h-full">
|
||||||
@@ -205,197 +269,408 @@ export const NotepadView: Component<{
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Editor Area */}
|
{/* Desktop Style Tabs (Left Side) - Ribbon Bookmark Style */}
|
||||||
<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="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 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">
|
<Show when={parentNote()}>
|
||||||
<div class="flex items-start justify-between gap-4">
|
{(parent) => (
|
||||||
<textarea
|
<button
|
||||||
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"
|
class="py-3 px-3 flex flex-row items-center justify-start transition-all relative group rounded-l-xl shadow-[inset_0_-2px_4px_rgba(0,0,0,0.05)] text-left truncate flex-none bg-muted border-y border-l border-border hover:bg-muted/80 w-full z-0 mb-4"
|
||||||
value={note().title}
|
onClick={() => props.setSelectedNoteId(parent().id)}
|
||||||
placeholder="Note Title"
|
title={`Parent: ${parent().title || "Untitled"}`}
|
||||||
readOnly={!canEdit}
|
>
|
||||||
rows={1}
|
<ArrowLeft size={14} class="mr-2 opacity-50 shrink-0 text-muted-foreground" />
|
||||||
onBlur={(e) => {
|
<span class="text-xs font-bold truncate w-full text-foreground/80">
|
||||||
const val = e.currentTarget.value.trim() || 'Untitled';
|
{parent().title || "Untitled"}
|
||||||
if (val !== note().title) handleRenameNote(note().id, note().title, val);
|
</span>
|
||||||
}}
|
</button>
|
||||||
onInput={(e) => {
|
)}
|
||||||
e.currentTarget.style.height = 'auto';
|
</Show>
|
||||||
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>
|
|
||||||
|
|
||||||
</div>
|
<For each={outsideTabs()}>
|
||||||
|
{(tab) => (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
class={cn(
|
||||||
|
"py-2 px-3 flex flex-row items-center justify-start transition-all relative group rounded-l-xl shadow-sm text-left truncate flex-none",
|
||||||
|
props.selectedNoteId === tab.id
|
||||||
|
? "bg-background border-y border-l border-border text-primary font-semibold w-full z-30"
|
||||||
|
: "bg-muted/40 border-y border-l border-border/40 text-muted-foreground hover:bg-muted w-10/12 hover:w-full hover:border-border/80 z-0"
|
||||||
|
)}
|
||||||
|
onClick={() => props.setSelectedNoteId(tab.id)}
|
||||||
|
title={tab.title || "Untitled Tab"}
|
||||||
|
>
|
||||||
|
<span class="text-xs truncate w-full">
|
||||||
|
{tab.title || "Untitled Tab"}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
{/* Editor Instance */}
|
{/* Bubbles hanging off string (Inside Tabs on the outside) positioned beside */}
|
||||||
<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">
|
<Show when={props.selectedNoteId === tab.id && (insideTabs().length > 0 || true)}>
|
||||||
<TaskEditor
|
{/* absolute container shifted to the left, top aligned to the tab */}
|
||||||
content={note().content}
|
<div class="absolute right-[100%] top-0 ml-4 flex flex-col w-32 py-1 gap-1 before:absolute before:right-[-4px] before:top-4 before:bottom-3 before:w-[2px] before:bg-border/60 before:rounded-full before:z-10 group-hover:opacity-100 transition-opacity z-50">
|
||||||
onUpdate={(html) => handleUpdateContent(note().id, html)}
|
<For each={insideTabs()}>
|
||||||
editable={canEdit}
|
{(subTab) => (
|
||||||
/>
|
|
||||||
</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() && 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
|
<button
|
||||||
class="w-full text-left p-2 text-xs hover:bg-muted border-b border-border last:border-0 truncate"
|
class={cn(
|
||||||
onClick={() => handleLinkTask(t.id)}
|
"py-1.5 px-2.5 flex flex-row items-center justify-start transition-all relative group/sub rounded-xl shadow-sm text-left truncate flex-none text-[0.65rem] border mr-4",
|
||||||
|
props.selectedNoteId === subTab.id
|
||||||
|
? "bg-primary text-primary-foreground border-primary font-semibold z-20"
|
||||||
|
: "bg-card border-border text-muted-foreground hover:bg-muted/80 z-20 hover:mr-5"
|
||||||
|
)}
|
||||||
|
onClick={(e) => { e.stopPropagation(); props.setSelectedNoteId(subTab.id); }}
|
||||||
>
|
>
|
||||||
{t.title}
|
{/* connecting string dot (on the right side now) */}
|
||||||
|
<div class="absolute -right-[11px] top-1/2 -mt-[3px] w-[6px] h-[6px] rounded-full bg-border shadow-[0_0_2px_rgba(0,0,0,0.1)] z-30 group-hover/sub:bg-primary transition-colors"></div>
|
||||||
|
<span class="truncate w-full">{subTab.title || "Untitled Tab"}</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
<button
|
||||||
|
class="w-[calc(100%-1rem)] py-1.5 px-2.5 rounded-xl text-[0.65rem] font-medium border border-dashed border-border/80 text-muted-foreground hover:bg-muted hover:text-foreground transition-colors flex items-center gap-1.5 z-20 bg-card/50 mt-1 relative mr-4"
|
||||||
|
onClick={(e) => { e.stopPropagation(); handleCreateTab(); }}
|
||||||
|
>
|
||||||
|
{/* connecting string dot for Add button */}
|
||||||
|
<div class="absolute -right-[11px] top-1/2 -mt-[3px] w-[6px] h-[6px] rounded-full bg-border/60 z-30 transition-colors"></div>
|
||||||
|
<Plus size={10} /> Add Sub-Tab
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
|
||||||
|
<button
|
||||||
|
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"
|
||||||
|
onClick={() => handleCreateTab(parentNote()?.id)}
|
||||||
|
title="Add Tab"
|
||||||
|
>
|
||||||
|
<Plus size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Editor Area Container */}
|
||||||
|
<div class="flex-1 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-hidden z-20 relative shadow-[-4px_0_12px_rgba(0,0,0,0.02)]">
|
||||||
|
|
||||||
|
{/* 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 bg-background z-20">
|
||||||
|
{/* Mobile Back to Parent Breadcrumb */}
|
||||||
|
<Show when={parentNote()}>
|
||||||
|
{(parent) => (
|
||||||
|
<div class="md:hidden sticky top-0 z-40 bg-card/95 backdrop-blur-sm pt-2 -mx-4 px-4 pb-1">
|
||||||
|
<button
|
||||||
|
class="flex items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors py-1 px-2 -ml-2 rounded-lg hover:bg-muted"
|
||||||
|
onClick={() => props.setSelectedNoteId(parent().id)}
|
||||||
|
>
|
||||||
|
<ArrowLeft size={12} />
|
||||||
|
<span class="truncate max-w-[150px]">Back to {parent().title || "Parent"}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<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",
|
||||||
|
parentNote() ? "text-xl sm:text-2xl text-muted-foreground" : "text-3xl sm:text-4xl"
|
||||||
|
)}
|
||||||
|
value={root().title}
|
||||||
|
placeholder="Note Title"
|
||||||
|
readOnly={!canEdit || !!parentNote()}
|
||||||
|
rows={1}
|
||||||
|
onBlur={(e) => {
|
||||||
|
if (parentNote()) return; // Read-only via logic if tab
|
||||||
|
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>
|
||||||
|
<Show when={parentNote()}>
|
||||||
|
<input
|
||||||
|
class="text-lg font-semibold tracking-wide bg-transparent border-none outline-none focus:ring-0 px-0 min-w-0 w-full text-foreground/80 mt-1"
|
||||||
|
value={note().title}
|
||||||
|
placeholder="Tab Title"
|
||||||
|
readOnly={!canEdit}
|
||||||
|
onBlur={(e) => {
|
||||||
|
const val = e.currentTarget.value.trim() || 'Untitled Tab';
|
||||||
|
if (val !== note().title) handleRenameNote(note().id, note().title, val);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 shrink-0 pt-1">
|
||||||
|
{/* Mobile Tabs Dropdown */}
|
||||||
|
<div class="md:hidden flex items-center">
|
||||||
|
<Show when={outsideTabs().length > 0}>
|
||||||
|
<div class="relative group">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
class="h-8 px-2 text-xs gap-1 max-w-[120px]"
|
||||||
|
>
|
||||||
|
<FileText size={12} />
|
||||||
|
<span class="truncate">Tabs ({outsideTabs().length})</span>
|
||||||
|
<ChevronDown size={12} class="opacity-50" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div class="absolute right-0 top-full mt-1 w-64 bg-popover text-popover-foreground rounded-lg border border-border shadow-lg opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all z-50 overflow-hidden flex flex-col">
|
||||||
|
<Show when={parentNote()}>
|
||||||
|
{(parent) => (
|
||||||
|
<button
|
||||||
|
class="w-full text-left px-3 py-2 text-sm hover:bg-muted transition-colors border-b border-border bg-muted/30 font-semibold truncate flex items-center gap-2 text-foreground/80"
|
||||||
|
onClick={() => props.setSelectedNoteId(parent().id)}
|
||||||
|
>
|
||||||
|
<ArrowLeft size={12} class="opacity-50 shrink-0" /> <span class="truncate">{parent().title || "Untitled Parent"}</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
<For each={outsideTabs()}>
|
||||||
|
{(tab) => (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
class={cn(
|
||||||
|
"w-full text-left px-3 py-2 text-sm hover:bg-muted transition-colors border-b border-border/50 truncate flex-1",
|
||||||
|
props.selectedNoteId === tab.id && "bg-muted font-medium text-primary"
|
||||||
|
)}
|
||||||
|
onClick={() => props.setSelectedNoteId(tab.id)}
|
||||||
|
>
|
||||||
|
{tab.title || "Untitled Tab"}
|
||||||
|
</button>
|
||||||
|
<Show when={props.selectedNoteId === tab.id && (insideTabs().length > 0 || true)}>
|
||||||
|
{/* absolute container shifted to the right, top aligned to the tab */}
|
||||||
|
<div class="absolute left-full top-0 ml-1 flex flex-col w-48 py-1 border border-border/50 bg-popover rounded-lg shadow-xl z-50">
|
||||||
|
<div class="absolute left-2 top-0 bottom-0 w-[1px] bg-border/40"></div>
|
||||||
|
<For each={insideTabs()}>
|
||||||
|
{(subTab) => (
|
||||||
|
<button
|
||||||
|
class={cn(
|
||||||
|
"w-full text-left pl-5 pr-3 py-2 text-xs hover:bg-muted transition-colors truncate flex items-center relative",
|
||||||
|
props.selectedNoteId === subTab.id && "text-primary font-medium bg-muted/50"
|
||||||
|
)}
|
||||||
|
onClick={(e) => { e.stopPropagation(); props.setSelectedNoteId(subTab.id); }}
|
||||||
|
>
|
||||||
|
<div class="w-[5px] h-[5px] rounded-full bg-border shrink-0 absolute left-[6px]"></div>
|
||||||
|
<span class="truncate ml-2">{subTab.title || "Untitled Tab"}</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
<button
|
||||||
|
class="w-full text-left pl-5 pr-3 py-2 text-xs hover:bg-muted transition-colors text-muted-foreground font-medium flex items-center gap-1.5 relative border-t border-border/30 mt-1 pt-2"
|
||||||
|
onClick={(e) => { e.stopPropagation(); handleCreateTab(); }}
|
||||||
|
>
|
||||||
|
<div class="w-[5px] h-[5px] rounded-full bg-border/40 shrink-0 absolute left-[6px]"></div>
|
||||||
|
<Plus size={12} class="ml-2" /> Add Sub-Tab
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
<button
|
||||||
|
class="w-full text-left px-3 py-2 text-sm hover:bg-primary/10 text-primary font-medium transition-colors flex items-center gap-2"
|
||||||
|
onClick={() => handleCreateTab(parentNote()?.id)}
|
||||||
|
>
|
||||||
|
<Plus size={14} /> Add Tab
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<Show when={outsideTabs().length === 0}>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
class="h-8 w-8 p-0 text-muted-foreground hover:bg-muted hover:text-foreground rounded-xl"
|
||||||
|
onClick={() => handleCreateTab(parentNote()?.id)}
|
||||||
|
title="Add Tab"
|
||||||
|
>
|
||||||
|
<Plus size={16} />
|
||||||
|
</Button>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
</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>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</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>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</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>
|
</Show>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
{/* Sticky Footer Actions (Match TaskDetail style) */}
|
{/* 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">
|
<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">
|
||||||
|
|||||||
Reference in New Issue
Block a user