diff --git a/bun.lock b/bun.lock index b8db0d9..1c735cf 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], diff --git a/package.json b/package.json index 3e99c2f..0f66e96 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/views/NotepadView.tsx b/src/views/NotepadView.tsx index f1ce944..2e22180 100644 --- a/src/views/NotepadView.tsx +++ b/src/views/NotepadView.tsx @@ -4,6 +4,7 @@ import { pb } from "@/lib/pocketbase"; import { type Note } from "@/store"; import { Trash2, Lock, Unlock, Search, Link, X, ChevronRight, ChevronDown, FileText, Plus } from "lucide-solid"; import { Button } from "@/components/ui/button"; +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"; @@ -19,6 +20,7 @@ export const NotepadView: Component<{ const [isLinking, setIsLinking] = createSignal(false); const [linkSearchQuery, setLinkSearchQuery] = createSignal(""); const [isLinkedTasksOpen, setIsLinkedTasksOpen] = createSignal(true); + const [isDragging, setIsDragging] = createSignal(false); const currentUserId = pb.authStore.model?.id; @@ -163,46 +165,6 @@ export const NotepadView: Component<{ }); // --- 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(() => { let current = activeNote(); let count = 0; @@ -218,18 +180,34 @@ export const NotepadView: Component<{ return current; }); - const handleCreateTab = async (parentIdOverride?: string) => { - const note = activeNote(); - if (!note || !currentUserId) return; + const childTabs = createMemo(() => { + const root = rootParentNote(); + 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 { const result = await pb.collection(NOTES_COLLECTION).create({ title: "New Tab", content: "", - tags: [`child-of-${parentId}`], - isPrivate: note.isPrivate, // inherit privacy + tags: [`child-of-${root.id}`], + isPrivate: root.isPrivate, // inherit privacy user: currentUserId, }); props.setSelectedNoteId(result.id); @@ -243,6 +221,77 @@ export const NotepadView: Component<{ 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 ( +
{ + if (props.selectedNoteId !== sProps.tab.id) { + props.setSelectedNoteId(sProps.tab.id); + } + }} + title={sProps.tab.title || "Untitled Tab"} + > + {sProps.tab.title || "Untitled Tab"} + }> + 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); + }} + /> + +
+ ); + }; + return (
{/* Mobile Notes List */} @@ -286,101 +335,50 @@ export const NotepadView: Component<{ {/* Desktop Style Tabs (Left Side) - Ribbon Bookmark Style */}
{/* Editor Area Container */} @@ -425,7 +423,7 @@ export const NotepadView: Component<{
{/* Mobile Tabs Dropdown */}
- 0}> +
- +