Note Tabs Reordering and Stable
This commit is contained in:
@@ -8,6 +8,7 @@
|
|||||||
"@formkit/auto-animate": "^0.9.0",
|
"@formkit/auto-animate": "^0.9.0",
|
||||||
"@kobalte/core": "^0.13.11",
|
"@kobalte/core": "^0.13.11",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
|
"@thisbeyond/solid-dnd": "^0.7.5",
|
||||||
"@tiptap/core": "^3.18.0",
|
"@tiptap/core": "^3.18.0",
|
||||||
"@tiptap/extension-bubble-menu": "^3.20.0",
|
"@tiptap/extension-bubble-menu": "^3.20.0",
|
||||||
"@tiptap/extension-dropcursor": "^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=="],
|
"@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/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=="],
|
"@tiptap/extension-blockquote": ["@tiptap/extension-blockquote@3.18.0", "", { "peerDependencies": { "@tiptap/core": "^3.18.0" } }, "sha512-1HjEoM5vZDfFnq2OodNpW13s56a9pbl7jolUv1V9FrE3X5s7n0HCfDzIVpT7z1HgTdPtlN5oSt5uVyBwuwSUfA=="],
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"@formkit/auto-animate": "^0.9.0",
|
"@formkit/auto-animate": "^0.9.0",
|
||||||
"@kobalte/core": "^0.13.11",
|
"@kobalte/core": "^0.13.11",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
|
"@thisbeyond/solid-dnd": "^0.7.5",
|
||||||
"@tiptap/core": "^3.18.0",
|
"@tiptap/core": "^3.18.0",
|
||||||
"@tiptap/extension-bubble-menu": "^3.20.0",
|
"@tiptap/extension-bubble-menu": "^3.20.0",
|
||||||
"@tiptap/extension-dropcursor": "^3.20.0",
|
"@tiptap/extension-dropcursor": "^3.20.0",
|
||||||
|
|||||||
+151
-179
@@ -4,6 +4,7 @@ import { pb } from "@/lib/pocketbase";
|
|||||||
import { type Note } from "@/store";
|
import { type Note } from "@/store";
|
||||||
import { Trash2, Lock, Unlock, Search, Link, X, ChevronRight, ChevronDown, FileText, 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 { Button } from "@/components/ui/button";
|
||||||
|
import { DragDropProvider, DragDropSensors, SortableProvider, createSortable, closestCenter } from "@thisbeyond/solid-dnd";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { NOTES_COLLECTION } from "@/lib/constants";
|
import { NOTES_COLLECTION } from "@/lib/constants";
|
||||||
import { TaskEditor } from "@/components/TaskEditor";
|
import { TaskEditor } from "@/components/TaskEditor";
|
||||||
@@ -19,6 +20,7 @@ export const NotepadView: Component<{
|
|||||||
const [isLinking, setIsLinking] = createSignal(false);
|
const [isLinking, setIsLinking] = createSignal(false);
|
||||||
const [linkSearchQuery, setLinkSearchQuery] = createSignal("");
|
const [linkSearchQuery, setLinkSearchQuery] = createSignal("");
|
||||||
const [isLinkedTasksOpen, setIsLinkedTasksOpen] = createSignal(true);
|
const [isLinkedTasksOpen, setIsLinkedTasksOpen] = createSignal(true);
|
||||||
|
const [isDragging, setIsDragging] = createSignal(false);
|
||||||
|
|
||||||
const currentUserId = pb.authStore.model?.id;
|
const currentUserId = pb.authStore.model?.id;
|
||||||
|
|
||||||
@@ -163,46 +165,6 @@ export const NotepadView: Component<{
|
|||||||
});
|
});
|
||||||
|
|
||||||
// --- Tabs Logic ---
|
// --- 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(() => {
|
|
||||||
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(() => {
|
const rootParentNote = createMemo(() => {
|
||||||
let current = activeNote();
|
let current = activeNote();
|
||||||
let count = 0;
|
let count = 0;
|
||||||
@@ -218,18 +180,34 @@ export const NotepadView: Component<{
|
|||||||
return current;
|
return current;
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleCreateTab = async (parentIdOverride?: string) => {
|
const childTabs = createMemo(() => {
|
||||||
const note = activeNote();
|
const root = rootParentNote();
|
||||||
if (!note || !currentUserId) return;
|
if (!root) return [];
|
||||||
|
const childTag = `child-of-${root.id}`;
|
||||||
|
|
||||||
const parentId = parentIdOverride || note.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 {
|
try {
|
||||||
const result = await pb.collection(NOTES_COLLECTION).create({
|
const result = await pb.collection(NOTES_COLLECTION).create({
|
||||||
title: "New Tab",
|
title: "New Tab",
|
||||||
content: "",
|
content: "",
|
||||||
tags: [`child-of-${parentId}`],
|
tags: [`child-of-${root.id}`],
|
||||||
isPrivate: note.isPrivate, // inherit privacy
|
isPrivate: root.isPrivate, // inherit privacy
|
||||||
user: currentUserId,
|
user: currentUserId,
|
||||||
});
|
});
|
||||||
props.setSelectedNoteId(result.id);
|
props.setSelectedNoteId(result.id);
|
||||||
@@ -243,6 +221,77 @@ export const NotepadView: Component<{
|
|||||||
window.dispatchEvent(new KeyboardEvent('keydown', { metaKey: true, key: 'k' }));
|
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 (
|
return (
|
||||||
<div class="flex flex-col md:flex-row h-full bg-background md:bg-transparent overflow-visible 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 */}
|
||||||
@@ -286,101 +335,50 @@ export const NotepadView: Component<{
|
|||||||
{/* Desktop Style Tabs (Left Side) - Ribbon Bookmark Style */}
|
{/* 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 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]">
|
||||||
|
|
||||||
<For each={outsideTabs()}>
|
<DragDropProvider onDragStart={handleDragStart} onDragEnd={handleDragEnd} collisionDetector={closestCenter}>
|
||||||
{(tab) => (
|
<DragDropSensors />
|
||||||
<>
|
|
||||||
|
<Show when={rootParentNote()}>
|
||||||
|
{(root) => (
|
||||||
<div
|
<div
|
||||||
class={cn(
|
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 cursor-pointer",
|
"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",
|
||||||
props.selectedNoteId === tab.id
|
!isDragging() && "transition-all",
|
||||||
|
props.selectedNoteId === root().id
|
||||||
? "bg-background border-y border-l border-border text-primary font-semibold w-full z-30"
|
? "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"
|
: 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={() => {
|
onClick={() => {
|
||||||
if (props.selectedNoteId !== tab.id) {
|
if (props.selectedNoteId !== root().id) props.setSelectedNoteId(root().id);
|
||||||
props.setSelectedNoteId(tab.id);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
title={tab.title || "Untitled Tab"}
|
|
||||||
>
|
>
|
||||||
<Show when={props.selectedNoteId === tab.id} fallback={
|
<span class="text-xs truncate w-full">General</span>
|
||||||
<span class="text-xs truncate w-full">
|
|
||||||
{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={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>
|
</div>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
|
||||||
{/* Bubbles hanging off string (Inside Tabs on the outside) positioned beside */}
|
<SortableProvider ids={childTabs().map(t => t.id)}>
|
||||||
<Show when={props.selectedNoteId === tab.id && (insideTabs().length > 0 || true)}>
|
<For each={childTabs()}>
|
||||||
{/* absolute container shifted to the left, top aligned to the tab */}
|
{(tab) => (
|
||||||
<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">
|
<SortableDesktopTab
|
||||||
<For each={insideTabs()}>
|
tab={tab}
|
||||||
{(subTab) => (
|
isSelected={props.selectedNoteId === tab.id}
|
||||||
<div
|
canEdit={canEdit}
|
||||||
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 cursor-pointer",
|
)}
|
||||||
props.selectedNoteId === subTab.id
|
</For>
|
||||||
? "bg-primary text-primary-foreground border-primary font-semibold z-20"
|
</SortableProvider>
|
||||||
: "bg-card border-border text-muted-foreground hover:bg-muted/80 z-20 hover:mr-5"
|
</DragDropProvider>
|
||||||
)}
|
|
||||||
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>
|
|
||||||
}>
|
|
||||||
<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
|
|
||||||
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
|
<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"
|
||||||
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()}
|
||||||
onClick={() => handleCreateTab(parentNote()?.id)}
|
|
||||||
title="Add Tab"
|
title="Add Tab"
|
||||||
>
|
>
|
||||||
<Plus size={16} />
|
<Plus size={16} />
|
||||||
</button>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Editor Area Container */}
|
{/* Editor Area Container */}
|
||||||
@@ -425,7 +423,7 @@ export const NotepadView: Component<{
|
|||||||
<div class="flex items-center gap-2 shrink-0 pt-1">
|
<div class="flex items-center gap-2 shrink-0 pt-1">
|
||||||
{/* Mobile Tabs Dropdown */}
|
{/* Mobile Tabs Dropdown */}
|
||||||
<div class="md:hidden flex items-center">
|
<div class="md:hidden flex items-center">
|
||||||
<Show when={outsideTabs().length > 0}>
|
<Show when={rootParentNote()}>
|
||||||
<div class="relative group">
|
<div class="relative group">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -433,12 +431,27 @@ export const NotepadView: Component<{
|
|||||||
class="h-8 px-2 text-xs gap-1 max-w-[120px]"
|
class="h-8 px-2 text-xs gap-1 max-w-[120px]"
|
||||||
>
|
>
|
||||||
<FileText size={12} />
|
<FileText size={12} />
|
||||||
<span class="truncate">Tabs ({outsideTabs().length})</span>
|
<span class="truncate">Tabs ({childTabs().length + (rootParentNote() ? 1 : 0)})</span>
|
||||||
<ChevronDown size={12} class="opacity-50" />
|
<ChevronDown size={12} class="opacity-50" />
|
||||||
</Button>
|
</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">
|
<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">
|
||||||
<For each={outsideTabs()}>
|
<Show when={rootParentNote()}>
|
||||||
|
{(root) => (
|
||||||
|
<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 cursor-pointer",
|
||||||
|
props.selectedNoteId === root().id && "bg-muted font-medium text-primary"
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
if (props.selectedNoteId !== root().id) props.setSelectedNoteId(root().id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span class="truncate">General</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
<For each={childTabs()}>
|
||||||
{(tab) => (
|
{(tab) => (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
@@ -450,7 +463,9 @@ export const NotepadView: Component<{
|
|||||||
if (props.selectedNoteId !== tab.id) props.setSelectedNoteId(tab.id);
|
if (props.selectedNoteId !== tab.id) props.setSelectedNoteId(tab.id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Show when={props.selectedNoteId === tab.id} fallback={tab.title || "Untitled Tab"}>
|
<Show when={props.selectedNoteId === tab.id} fallback={
|
||||||
|
<span class="truncate">{tab.title || "Untitled Tab"}</span>
|
||||||
|
}>
|
||||||
<input
|
<input
|
||||||
class="text-sm bg-transparent border-none outline-none focus:ring-0 px-0 min-w-0 w-full truncate font-semibold"
|
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}
|
value={tab.title}
|
||||||
@@ -464,68 +479,25 @@ export const NotepadView: Component<{
|
|||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</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) => (
|
|
||||||
<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 cursor-pointer",
|
|
||||||
props.selectedNoteId === subTab.id && "text-primary font-medium bg-muted/50"
|
|
||||||
)}
|
|
||||||
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>
|
|
||||||
}>
|
|
||||||
<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
|
|
||||||
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>
|
</For>
|
||||||
<button
|
<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"
|
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)}
|
onClick={() => handleCreateTab()}
|
||||||
>
|
>
|
||||||
<Plus size={14} /> Add Tab
|
<Plus size={14} /> Add Tab
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={outsideTabs().length === 0}>
|
<Show when={childTabs().length === 0}>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
class="h-8 w-8 p-0 text-muted-foreground hover:bg-muted hover:text-foreground rounded-xl"
|
class="h-8 w-8 p-0 text-muted-foreground hover:bg-muted hover:text-foreground rounded-xl"
|
||||||
onClick={() => handleCreateTab(parentNote()?.id)}
|
onClick={() => handleCreateTab()}
|
||||||
title="Add Tab"
|
title="Add Tab"
|
||||||
>
|
>
|
||||||
<Plus size={16} />
|
<Plus size={16} />
|
||||||
|
|||||||
Reference in New Issue
Block a user