Updated note tabs - NOT READY FOR PRODUCTION

This commit is contained in:
2026-03-05 19:14:30 -06:00
parent f7b46b2017
commit 8c307db57a
+102 -73
View File
@@ -2,7 +2,7 @@ import { type Component, createSignal, createMemo, createEffect, onCleanup, For,
import { store, renameTagDefinition, updateNote, removeNote, restoreNote, setActiveNoteId, cleanupNoteAttachments } from "@/store";
import { pb } from "@/lib/pocketbase";
import { type Note } from "@/store";
import { Trash2, Lock, Unlock, Search, Link, X, ChevronRight, ChevronDown, FileText, ArrowLeft, Plus } from "lucide-solid";
import { Trash2, Lock, Unlock, Search, Link, X, ChevronRight, ChevronDown, FileText, Plus } from "lucide-solid";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { NOTES_COLLECTION } from "@/lib/constants";
@@ -186,7 +186,21 @@ export const NotepadView: Component<{
return store.notes.filter(n => !n.deletedAt && n.tags?.includes(childTag));
});
const outsideTabs = createMemo(() => parentNote() ? siblingTabs() : childTabs());
const outsideTabs = createMemo(() => {
const parent = parentNote();
if (parent) {
// We have a parent, so show the parent and its children (siblings of active)
return [parent, ...siblingTabs()];
}
// No parent, so we are at the root or no note selected
const active = activeNote();
if (active) {
// Show the root note as the first tab, followed by its children
return [active, ...childTabs()];
}
return [];
});
const insideTabs = createMemo(() => parentNote() ? childTabs() : []);
const rootParentNote = createMemo(() => {
@@ -271,38 +285,42 @@ export const NotepadView: Component<{
<>
{/* 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]">
<Show when={parentNote()}>
{(parent) => (
<button
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"
onClick={() => props.setSelectedNoteId(parent().id)}
title={`Parent: ${parent().title || "Untitled"}`}
>
<ArrowLeft size={14} class="mr-2 opacity-50 shrink-0 text-muted-foreground" />
<span class="text-xs font-bold truncate w-full text-foreground/80">
{parent().title || "Untitled"}
</span>
</button>
)}
</Show>
<For each={outsideTabs()}>
{(tab) => (
<>
<button
<div
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",
"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 cursor-pointer",
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)}
onClick={() => {
if (props.selectedNoteId !== tab.id) {
props.setSelectedNoteId(tab.id);
}
}}
title={tab.title || "Untitled Tab"}
>
<Show when={props.selectedNoteId === tab.id} fallback={
<span class="text-xs truncate w-full">
{tab.title || "Untitled Tab"}
</span>
</button>
}>
<input
class="text-xs bg-transparent border-none outline-none focus:ring-0 px-0 min-w-0 w-full truncate font-semibold"
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>
</div>
{/* Bubbles hanging off string (Inside Tabs on the outside) positioned beside */}
<Show when={props.selectedNoteId === tab.id && (insideTabs().length > 0 || true)}>
@@ -310,19 +328,36 @@ export const NotepadView: Component<{
<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">
<For each={insideTabs()}>
{(subTab) => (
<button
<div
class={cn(
"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",
"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 cursor-pointer",
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); }}
onClick={(e) => {
e.stopPropagation();
if (props.selectedNoteId !== subTab.id) props.setSelectedNoteId(subTab.id);
}}
>
{/* 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>
<Show when={props.selectedNoteId === subTab.id} fallback={
<span class="truncate w-full">{subTab.title || "Untitled Tab"}</span>
</button>
}>
<input
class="text-[0.65rem] bg-transparent border-none outline-none focus:ring-0 px-0 min-w-0 w-full truncate font-semibold placeholder:text-primary-foreground/50"
value={subTab.title}
placeholder="Untitled Tab"
readOnly={!canEdit}
onClick={(e) => e.stopPropagation()}
onBlur={(e) => {
const val = e.currentTarget.value.trim() || 'Untitled Tab';
if (val !== subTab.title) handleRenameNote(subTab.id, subTab.title, val);
}}
/>
</Show>
</div>
)}
</For>
<button
@@ -353,20 +388,7 @@ export const NotepadView: Component<{
{/* 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">
@@ -375,15 +397,13 @@ export const NotepadView: Component<{
{(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"
"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 || !!parentNote()}
readOnly={!canEdit}
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);
}}
@@ -401,18 +421,6 @@ export const NotepadView: Component<{
/>
)}
</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 */}
@@ -430,44 +438,65 @@ export const NotepadView: Component<{
</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
<div
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",
"w-full text-left px-3 py-2 text-sm hover:bg-muted transition-colors border-b border-border/50 truncate flex-1 cursor-pointer",
props.selectedNoteId === tab.id && "bg-muted font-medium text-primary"
)}
onClick={() => props.setSelectedNoteId(tab.id)}
onClick={() => {
if (props.selectedNoteId !== tab.id) props.setSelectedNoteId(tab.id);
}}
>
{tab.title || "Untitled Tab"}
</button>
<Show when={props.selectedNoteId === tab.id} fallback={tab.title || "Untitled Tab"}>
<input
class="text-sm bg-transparent border-none outline-none focus:ring-0 px-0 min-w-0 w-full truncate font-semibold"
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>
</div>
<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
<div
class={cn(
"w-full text-left pl-5 pr-3 py-2 text-xs hover:bg-muted transition-colors truncate flex items-center relative",
"w-full text-left pl-5 pr-3 py-2 text-xs hover:bg-muted transition-colors truncate flex items-center relative cursor-pointer",
props.selectedNoteId === subTab.id && "text-primary font-medium bg-muted/50"
)}
onClick={(e) => { e.stopPropagation(); props.setSelectedNoteId(subTab.id); }}
onClick={(e) => {
e.stopPropagation();
if (props.selectedNoteId !== subTab.id) props.setSelectedNoteId(subTab.id);
}}
>
<div class="w-[5px] h-[5px] rounded-full bg-border shrink-0 absolute left-[6px]"></div>
<Show when={props.selectedNoteId === subTab.id} fallback={
<span class="truncate ml-2">{subTab.title || "Untitled Tab"}</span>
</button>
}>
<input
class="text-xs bg-transparent border-none outline-none focus:ring-0 px-0 min-w-0 w-full truncate font-semibold ml-2"
value={subTab.title}
placeholder="Untitled Tab"
readOnly={!canEdit}
onClick={(e) => e.stopPropagation()}
onBlur={(e) => {
const val = e.currentTarget.value.trim() || 'Untitled Tab';
if (val !== subTab.title) handleRenameNote(subTab.id, subTab.title, val);
}}
/>
</Show>
</div>
)}
</For>
<button