diff --git a/modules/rnotes/components/folk-notes-app.ts b/modules/rnotes/components/folk-notes-app.ts index ed9c952..8edcf2e 100644 --- a/modules/rnotes/components/folk-notes-app.ts +++ b/modules/rnotes/components/folk-notes-app.ts @@ -477,38 +477,39 @@ Gear: EUR 400 (10%)

Maya is tracking expenses in rF private demoSearchNotes(query: string) { if (!query.trim()) { this.searchResults = []; - this.render(); + this.renderNav(); return; } const q = query.toLowerCase(); - const all = this.demoNotebooks.flatMap(nb => nb.notes); - this.searchResults = all.filter(n => - n.title.toLowerCase().includes(q) || - n.content_plain.toLowerCase().includes(q) || - (n.tags && n.tags.some(t => t.toLowerCase().includes(q))) - ); - this.render(); - } - - private demoLoadNotebook(id: string) { - const nb = this.demoNotebooks.find(n => n.id === id); - if (nb) { - this.selectedNotebook = { ...nb }; - } else { - this.error = "Notebook not found"; + const results: Note[] = []; + for (const nb of this.demoNotebooks) { + for (const n of nb.notes) { + if (n.title.toLowerCase().includes(q) || + n.content_plain.toLowerCase().includes(q) || + (n.tags && n.tags.some(t => t.toLowerCase().includes(q)))) { + results.push(Object.assign({}, n, { notebook_id: nb.id }) as any); + } + } } - this.loading = false; - this.render(); + this.searchResults = results; + this.renderNav(); } private demoLoadNote(id: string) { - const allNotes = this.demoNotebooks.flatMap(nb => nb.notes); - this.selectedNote = allNotes.find(n => n.id === id) || null; - if (this.selectedNote) { - this.view = "note"; - this.renderNav(); - this.renderMeta(); - this.mountEditor(this.selectedNote); + // Find the note and its parent notebook + for (const nb of this.demoNotebooks) { + const note = nb.notes.find(n => n.id === id); + if (note) { + this.selectedNote = note; + this.selectedNotebook = { ...nb }; + if (!this.expandedNotebooks.has(nb.id)) { + this.expandedNotebooks.add(nb.id); + } + this.renderNav(); + this.renderMeta(); + this.mountEditor(this.selectedNote); + return; + } } } @@ -529,12 +530,11 @@ Gear: EUR 400 (10%)

Maya is tracking expenses in rF } as any; this.demoNotebooks.push(nb); this.notebooks = this.demoNotebooks.map(({ notes, ...rest }) => rest as Notebook); + this.notebookNotes.set(nbId, [newNote]); + this.expandedNotebooks.add(nbId); this.selectedNotebook = { ...nb }; - this.view = "notebook"; - this.render(); // Auto-open the note for editing this.selectedNote = newNote; - this.view = "note"; this.renderNav(); this.renderMeta(); this.mountEditor(newNote); @@ -555,15 +555,21 @@ Gear: EUR 400 (10%)

Maya is tracking expenses in rF duration: opts.duration ?? null, created_at: new Date(now).toISOString(), updated_at: new Date(now).toISOString(), }; - const demoNb = this.demoNotebooks.find(n => n.id === this.selectedNotebook!.id); + const nbId = this.selectedNotebook.id; + const demoNb = this.demoNotebooks.find(n => n.id === nbId); if (demoNb) { demoNb.notes.push(newNote); demoNb.note_count = String(demoNb.notes.length); } this.selectedNotebook.notes.push(newNote); this.selectedNotebook.note_count = String(this.selectedNotebook.notes.length); + // Update sidebar cache + const cached = this.notebookNotes.get(nbId) || []; + cached.unshift(newNote); + this.notebookNotes.set(nbId, cached); + const nbIdx = this.notebooks.findIndex(n => n.id === nbId); + if (nbIdx >= 0) this.notebooks[nbIdx].note_count = String(cached.length); this.selectedNote = newNote; - this.view = "note"; this.renderNav(); this.renderMeta(); this.mountEditor(newNote); @@ -671,8 +677,13 @@ Gear: EUR 400 (10%)

Maya is tracking expenses in rF notes, }; - // If viewing a note and editor is mounted, update editor content from remote - if (this.view === "note" && this.selectedNote && this.editor && this.editorNoteId === this.selectedNote.id) { + // Update sidebar note cache + this.notebookNotes.set(nb.id, notes); + const nbIdx = this.notebooks.findIndex(n => n.id === nb.id); + if (nbIdx >= 0) this.notebooks[nbIdx].note_count = String(notes.length); + + // If editor is mounted for a note, update editor content from remote + if (this.selectedNote && this.editor && this.editorNoteId === this.selectedNote.id) { const noteItem = items?.[this.selectedNote.id]; if (noteItem) { this.selectedNote = { @@ -733,8 +744,8 @@ Gear: EUR 400 (10%)

Maya is tracking expenses in rF } } - // If viewing a specific note without editor mounted, update selectedNote - if (this.view === "note" && this.selectedNote) { + // If a note is selected but editor not mounted yet, update selectedNote + if (this.selectedNote) { const noteItem = items?.[this.selectedNote.id]; if (noteItem) { this.selectedNote = { @@ -821,7 +832,6 @@ Gear: EUR 400 (10%)

Maya is tracking expenses in rF duration: opts.duration ?? null, created_at: new Date(now).toISOString(), updated_at: new Date(now).toISOString(), }; - this.view = "note"; this.renderNav(); this.renderMeta(); this.mountEditor(this.selectedNote); @@ -969,15 +979,13 @@ Gear: EUR 400 (10%)

Maya is tracking expenses in rF } private async loadNotebook(id: string) { - this.loading = true; - this.render(); - this.unsubscribeNotebook(); - this.subscribeNotebook(id); + await this.subscribeNotebook(id); this.broadcastPresence(); + // REST fallback if Automerge doc is empty after 5s setTimeout(() => { - if (this.loading && this.view === "notebook") { + if (this.subscribedDocId && (!this.doc?.items || Object.keys(this.doc.items).length === 0)) { this.loadNotebookREST(id); } }, 5000); @@ -987,7 +995,11 @@ Gear: EUR 400 (10%)

Maya is tracking expenses in rF try { const base = this.getApiBase(); const res = await fetch(`${base}/api/notebooks/${id}`, { headers: this.authHeaders() }); - this.selectedNotebook = await res.json(); + const data = await res.json(); + this.selectedNotebook = data; + if (data?.notes) { + this.notebookNotes.set(id, data.notes); + } } catch { this.error = "Failed to load notebook"; } @@ -995,6 +1007,100 @@ Gear: EUR 400 (10%)

Maya is tracking expenses in rF this.render(); } + /** Fetch notes for a notebook (sidebar display). */ + private async fetchNotebookNotes(id: string) { + try { + const base = this.getApiBase(); + const res = await fetch(`${base}/api/notebooks/${id}`, { headers: this.authHeaders() }); + const data = await res.json(); + if (data?.notes) { + this.notebookNotes.set(id, data.notes); + const nbIdx = this.notebooks.findIndex(n => n.id === id); + if (nbIdx >= 0) this.notebooks[nbIdx].note_count = String(data.notes.length); + } + } catch { + // Silently fail — sidebar stays empty for this notebook + } + this.renderNav(); + } + + /** Toggle notebook expansion in sidebar. */ + private expandNotebook(id: string) { + if (this.expandedNotebooks.has(id)) { + this.expandedNotebooks.delete(id); + this.renderNav(); + return; + } + this.expandedNotebooks.add(id); + if (!this.notebookNotes.has(id)) { + if (this.space === "demo") { + const nb = this.demoNotebooks.find(n => n.id === id); + if (nb) this.notebookNotes.set(id, nb.notes); + this.renderNav(); + } else { + this.fetchNotebookNotes(id); + } + } else { + this.renderNav(); + } + } + + /** Open a note for editing from the sidebar. */ + private async openNote(noteId: string, notebookId: string) { + const isDemo = this.space === "demo"; + + // Expand notebook if not expanded + if (!this.expandedNotebooks.has(notebookId)) { + this.expandedNotebooks.add(notebookId); + } + + if (isDemo) { + this.demoLoadNote(noteId); + return; + } + + // Set selected notebook + const nb = this.notebooks.find(n => n.id === notebookId); + if (nb) { + this.selectedNotebook = { ...nb, notes: this.notebookNotes.get(notebookId) || [] }; + } + + // Subscribe to Automerge if needed + const needSubscribe = !this.subscribedDocId || !this.subscribedDocId.endsWith(`:${notebookId}`); + if (needSubscribe) { + await this.loadNotebook(notebookId); + } + + this.loadNote(noteId); + } + + /** Add a new note to a notebook via the sidebar '+' button. */ + private async addNoteToNotebook(notebookId: string) { + const isDemo = this.space === "demo"; + + // Ensure expanded + if (!this.expandedNotebooks.has(notebookId)) { + this.expandedNotebooks.add(notebookId); + } + + // Set selected notebook + const nb = this.notebooks.find(n => n.id === notebookId); + if (!nb) return; + this.selectedNotebook = { ...nb, notes: this.notebookNotes.get(notebookId) || [] }; + + if (isDemo) { + this.demoCreateNote(); + return; + } + + // Ensure subscribed + const needSubscribe = !this.subscribedDocId || !this.subscribedDocId.endsWith(`:${notebookId}`); + if (needSubscribe) { + await this.loadNotebook(notebookId); + } + this.createNoteViaSync(); + } + private loadNote(id: string) { // Note is already in the Automerge doc if (this.doc?.items?.[id]) { @@ -1020,6 +1126,14 @@ Gear: EUR 400 (10%)

Maya is tracking expenses in rF this.selectedNote = this.selectedNotebook.notes.find(n => n.id === id) || null; } + // Fallback: try sidebar note cache + if (!this.selectedNote) { + for (const [, notes] of this.notebookNotes) { + const found = notes.find(n => n.id === id); + if (found) { this.selectedNote = found; break; } + } + } + if (this.selectedNote) { this.renderNav(); this.renderMeta(); @@ -1031,7 +1145,7 @@ Gear: EUR 400 (10%)

Maya is tracking expenses in rF private async searchNotes(query: string) { if (!query.trim()) { this.searchResults = []; - this.render(); + this.renderNav(); return; } try { @@ -1042,7 +1156,7 @@ Gear: EUR 400 (10%)

Maya is tracking expenses in rF } catch { this.searchResults = []; } - this.render(); + this.renderNav(); } private async createNotebook() { @@ -1055,8 +1169,10 @@ Gear: EUR 400 (10%)

Maya is tracking expenses in rF }); const nb = await res.json(); if (nb?.id) { - this.view = "notebook"; - await this.loadNotebook(nb.id); + await this.loadNotebooks(); // Refresh list + this.notebookNotes.set(nb.id, []); + this.expandedNotebooks.add(nb.id); + this.render(); } else { await this.loadNotebooks(); } @@ -1372,27 +1488,25 @@ Gear: EUR 400 (10%)

Maya is tracking expenses in rF }); } - /** Patch presence dots onto notebook cards and note items in the DOM. */ + /** Patch presence dots onto sidebar notebook headers and note items. */ private renderPresenceIndicators() { // Remove all existing presence dots this.shadow.querySelectorAll('.presence-dots').forEach(el => el.remove()); - // Notebook cards - this.shadow.querySelectorAll('.notebook-card[data-notebook]').forEach(card => { - const nbId = card.dataset.notebook; + // Notebook headers in sidebar + this.shadow.querySelectorAll('.sbt-notebook-header[data-toggle-notebook]').forEach(header => { + const nbId = header.dataset.toggleNotebook; const peers = Array.from(this._presencePeers.values()).filter(p => p.notebookId === nbId); if (peers.length === 0) return; - const footer = card.querySelector('.notebook-card__footer'); - if (footer) footer.appendChild(this.buildPresenceDots(peers)); + header.appendChild(this.buildPresenceDots(peers)); }); - // Note items - this.shadow.querySelectorAll('.note-item[data-note]').forEach(item => { + // Note items in sidebar + this.shadow.querySelectorAll('.sbt-note[data-note]').forEach(item => { const noteId = item.dataset.note; const peers = Array.from(this._presencePeers.values()).filter(p => p.noteId === noteId); if (peers.length === 0) return; - const meta = item.querySelector('.note-item__meta'); - if (meta) meta.appendChild(this.buildPresenceDots(peers)); + item.appendChild(this.buildPresenceDots(peers)); }); } @@ -2331,13 +2445,12 @@ Gear: EUR 400 (10%)

Maya is tracking expenses in rF private render() { this.renderNav(); - if (this.view === 'note' && this.editor && this.editorNoteId) { + if (this.selectedNote && this.editor && this.editorNoteId === this.selectedNote.id) { // Editor already mounted — don't touch contentZone } else { this.renderContent(); } this.renderMeta(); - this.attachListeners(); this.renderPresenceIndicators(); this._tour.renderOverlay(); } @@ -2345,112 +2458,84 @@ Gear: EUR 400 (10%)

Maya is tracking expenses in rF startTour() { this._tour.start(); } private renderNav() { - const isDemo = this.space === "demo"; + const isSearching = this.searchQuery.trim().length > 0; - if (this.view === "note" && this.selectedNote) { - // Nav is handled by mountEditor's content, or we just show back button - this.navZone.innerHTML = ` -

- ${this._history.canGoBack ? `` : ''} - -
`; - // Re-attach back listener - this.navZone.querySelectorAll('[data-back]').forEach((el) => { - el.addEventListener('click', (e) => { - e.stopPropagation(); - this.goBack(); - }); - }); - return; - } - - if (this.view === "notebook" && this.selectedNotebook) { - const nb = this.selectedNotebook; - const syncBadge = this.subscribedDocId - ? `` - : ""; - const filterTypes: { label: string; value: NoteType | '' }[] = [ - { label: 'All', value: '' }, - { label: 'Notes', value: 'NOTE' }, - { label: 'Code', value: 'CODE' }, - { label: 'Bookmarks', value: 'BOOKMARK' }, - { label: 'Clips', value: 'CLIP' }, - { label: 'Audio', value: 'AUDIO' }, - ]; - this.navZone.innerHTML = ` -
- ${this._history.canGoBack ? '' : ''} - ${this.esc(nb.title)}${syncBadge} - -
- - - -
- ${filterTypes.map(f => ``).join('')} -
`; - - // Wire type filter pills - this.navZone.querySelectorAll('[data-type-filter]').forEach(el => { - el.addEventListener('click', () => { - this.typeFilter = ((el as HTMLElement).dataset.typeFilter || '') as NoteType | ''; - this.renderNav(); - this.renderContent(); - this.attachListeners(); - }); - }); - - // Wire new note dropdown - const toggleBtn = this.navZone.querySelector('#new-note-dropdown-toggle'); - const dropdown = this.navZone.querySelector('#new-note-dropdown') as HTMLElement; - if (toggleBtn && dropdown) { - toggleBtn.addEventListener('click', (e) => { - e.stopPropagation(); - dropdown.style.display = dropdown.style.display === 'none' ? 'block' : 'none'; - }); - dropdown.querySelectorAll('[data-create-type]').forEach(el => { - el.addEventListener('click', () => { - const type = (el as HTMLElement).dataset.createType as NoteType; - dropdown.style.display = 'none'; - if (this.space === 'demo') { - this.demoCreateNote({ type }); - } else { - this.createNoteViaSync({ type }); - } - }); - }); - // Close dropdown on outside click - document.addEventListener('click', () => { dropdown.style.display = 'none'; }, { once: true }); - } - - return; + `; + }).join(''); } - // Notebooks view + // Preserve search input focus/cursor position + const prevInput = this.navZone.querySelector('#search-input') as HTMLInputElement; + const hadFocus = prevInput && (prevInput === this.shadow.activeElement); + const selStart = prevInput?.selectionStart; + const selEnd = prevInput?.selectionEnd; + this.navZone.innerHTML = ` -
- Notebooks - - - +
+ + + +
- `; + `; + + // Restore search focus + if (hadFocus) { + const newInput = this.navZone.querySelector('#search-input') as HTMLInputElement; + if (newInput) { + newInput.focus(); + if (selStart !== null && selEnd !== null) { + newInput.setSelectionRange(selStart, selEnd); + } + } + } + + this.attachSidebarListeners(); } private renderContent() { @@ -2463,46 +2548,34 @@ Gear: EUR 400 (10%)

Maya is tracking expenses in rF return; } - if (this.view === "notebook" && this.selectedNotebook) { - const nb = this.selectedNotebook; - const filtered = this.typeFilter - ? nb.notes.filter(n => n.type === this.typeFilter) - : nb.notes; - this.contentZone.innerHTML = filtered.length > 0 - ? filtered.map((n) => this.renderNoteItem(n)).join("") - : `

${this.typeFilter ? `No ${this.typeFilter.toLowerCase()} notes in this notebook.` : 'No notes in this notebook.'}
`; - return; - } + // Empty state — no note selected + this.contentZone.innerHTML = ` +
+ + + + + + +

Select a note from the sidebar

+ ${this.notebooks.length > 0 + ? '' + : ''} +
+ `; - // Notebooks view - let html = ''; - if (this.searchQuery && this.searchResults.length > 0) { - html += `
${this.searchResults.length} results for "${this.esc(this.searchQuery)}"
`; - html += this.searchResults.map((n) => this.renderNoteItem(n)).join(""); - } - if (!this.searchQuery) { - html += `
${this.notebooks.map((nb) => ` -
-
-
-
${this.esc(nb.title)}
-
${this.esc(nb.description || "")}
-
- -
- `).join("")}
`; - if (this.notebooks.length === 0) { - html += '
No notebooks yet. Create one to get started.
'; - } - } - this.contentZone.innerHTML = html; + // Wire CTA button + this.contentZone.querySelector('#btn-empty-new-note')?.addEventListener('click', () => { + const nbId = this.selectedNotebook?.id || (this.notebooks.length > 0 ? this.notebooks[0].id : null); + if (nbId) this.addNoteToNotebook(nbId); + }); + this.contentZone.querySelector('#btn-empty-new-nb')?.addEventListener('click', () => { + this.space === 'demo' ? this.demoCreateNotebook() : this.createNotebook(); + }); } private renderMeta() { - if (this.view === "note" && this.selectedNote) { + if (this.selectedNote) { const n = this.selectedNote; const isAutomerge = !!(this.doc?.items?.[n.id]); const isDemo = this.space === "demo"; @@ -2567,100 +2640,9 @@ Gear: EUR 400 (10%)

Maya is tracking expenses in rF } } - private renderNoteItem(n: Note): string { - let typeDetail = ''; - switch (n.type) { - case 'BOOKMARK': - case 'CLIP': { - if (n.url) { - try { typeDetail = `${new URL(n.url).hostname}`; } catch { typeDetail = ''; } - } - break; - } - case 'CODE': - if (n.language) typeDetail = `${this.esc(n.language)}`; - break; - case 'AUDIO': - if (n.duration) { - const m = Math.floor(n.duration / 60); - const s = String(n.duration % 60).padStart(2, '0'); - typeDetail = `${m}:${s}`; - } - break; - } - - const typeBorder = this.getTypeBorderColor(n.type); - - // Sync status indicator - let syncBadge = ''; - if (n.source_ref) { - const status = n.source_ref.syncStatus || 'synced'; - const statusColors: Record = { - synced: 'var(--rs-success, #22c55e)', - 'local-modified': '#facc15', - 'remote-modified': 'var(--rs-primary, #6366f1)', - conflict: 'var(--rs-error, #ef4444)', - }; - const color = statusColors[status] || 'var(--rs-text-muted, #888)'; - syncBadge = ``; - } - - return ` -

- ${this.getNoteIcon(n.type)} -
-
${n.is_pinned ? '\u{1F4CC} ' : ""}${this.esc(n.title)}${syncBadge}
-
${this.esc(n.content_plain || "")}
-
- ${this.formatDate(n.updated_at)} - ${typeDetail} - ${n.type} - ${n.tags ? n.tags.map((t) => `${this.esc(t)}`).join("") : ""} -
-
-
- `; - } - - private getTypeBorderColor(type: string): string { - switch (type) { - case 'NOTE': return 'var(--rs-primary, #6366f1)'; - case 'CODE': return '#10b981'; - case 'BOOKMARK': return '#f59e0b'; - case 'CLIP': return '#8b5cf6'; - case 'IMAGE': return '#ec4899'; - case 'AUDIO': return '#ef4444'; - case 'FILE': return '#6b7280'; - default: return 'var(--rs-border, #e5e7eb)'; - } - } - - private attachListeners() { + private attachSidebarListeners() { const isDemo = this.space === "demo"; - // Import / Export button - this.shadow.getElementById("btn-import-export")?.addEventListener("click", () => { - this.openImportExportDialog(); - }); - - // Create notebook - this.shadow.getElementById("create-notebook")?.addEventListener("click", () => { - isDemo ? this.demoCreateNotebook() : this.createNotebook(); - }); - - // Tour button - this.shadow.getElementById("btn-tour")?.addEventListener("click", () => this.startTour()); - - // Export notebook button (in notebook detail view) - this.shadow.getElementById("btn-export-notebook")?.addEventListener("click", () => { - this.openImportExportDialog('export'); - }); - - // Create note - this.shadow.getElementById("create-note")?.addEventListener("click", () => { - isDemo ? this.demoCreateNote() : this.createNoteViaSync(); - }); - // Search const searchInput = this.shadow.getElementById("search-input") as HTMLInputElement; let searchTimeout: any; @@ -2672,58 +2654,61 @@ Gear: EUR 400 (10%)

Maya is tracking expenses in rF }, 300); }); - // Notebook cards - this.shadow.querySelectorAll("[data-notebook]").forEach((el) => { - el.addEventListener("click", () => { - const id = (el as HTMLElement).dataset.notebook!; - this._history.push(this.view); - this._history.push("notebook", { notebookId: id }); - this.view = "notebook"; - isDemo ? this.demoLoadNotebook(id) : this.loadNotebook(id); + // Create notebook + this.shadow.getElementById("create-notebook")?.addEventListener("click", () => { + isDemo ? this.demoCreateNotebook() : this.createNotebook(); + }); + + // Import / Export + this.shadow.getElementById("btn-import-export")?.addEventListener("click", () => { + this.openImportExportDialog(); + }); + + // Tour + this.shadow.getElementById("btn-tour")?.addEventListener("click", () => this.startTour()); + + // Toggle notebooks + this.shadow.querySelectorAll("[data-toggle-notebook]").forEach(el => { + el.addEventListener("click", (e) => { + if ((e.target as HTMLElement).closest('[data-add-note]')) return; + const id = (el as HTMLElement).dataset.toggleNotebook!; + this.expandNotebook(id); }); }); - // Note items - this.shadow.querySelectorAll("[data-note]").forEach((el) => { - el.addEventListener("click", () => { - const id = (el as HTMLElement).dataset.note!; - this._history.push(this.view); - this._history.push("note", { noteId: id }); - this.view = "note"; - isDemo ? this.demoLoadNote(id) : this.loadNote(id); + // Add note to notebook + this.shadow.querySelectorAll("[data-add-note]").forEach(el => { + el.addEventListener("click", (e) => { + e.stopPropagation(); + const nbId = (el as HTMLElement).dataset.addNote!; + this.addNoteToNotebook(nbId); }); }); - // Make notes draggable for calendar reminders - makeDraggableAll(this.shadow, "[data-note]", (el) => { - const title = el.querySelector(".note-item__title")?.textContent?.replace("📌 ", "") || ""; + // Click note in tree + this.shadow.querySelectorAll(".sbt-note[data-note]").forEach(el => { + el.addEventListener("click", () => { + const noteId = (el as HTMLElement).dataset.note!; + const nbId = (el as HTMLElement).dataset.notebook!; + this.openNote(noteId, nbId); + }); + }); + + // Click search result + this.shadow.querySelectorAll(".sidebar-search-result[data-note]").forEach(el => { + el.addEventListener("click", () => { + const noteId = (el as HTMLElement).dataset.note!; + const nbId = (el as HTMLElement).dataset.notebook!; + this.openNote(noteId, nbId); + }); + }); + + // Make sidebar notes draggable + makeDraggableAll(this.shadow, ".sbt-note[data-note]", (el) => { + const title = el.querySelector(".sbt-note-title")?.textContent || ""; const id = el.dataset.note || ""; return title ? { title, module: "rnotes", entityId: id, label: "Note", color: "#f59e0b" } : null; }); - - // Back buttons (for notebook view) - this.navZone.querySelectorAll("[data-back]").forEach((el) => { - el.addEventListener("click", (e) => { - e.stopPropagation(); - this.goBack(); - }); - }); - } - - private goBack() { - this.destroyEditor(); - const prev = this._history.back(); - if (!prev) return; - this.view = prev.view; - if (prev.view === "notebooks") { - if (this.space !== "demo") this.unsubscribeNotebook(); - this.selectedNotebook = null; - this.selectedNote = null; - } else if (prev.view === "notebook") { - this.selectedNote = null; - } - this.render(); - this.broadcastPresence(); } private demoUpdateNoteField(noteId: string, field: string, value: string) { @@ -2806,109 +2791,141 @@ Gear: EUR 400 (10%)

Maya is tracking expenses in rF * { box-sizing: border-box; } button, a, input, select, textarea, [role="button"] { touch-action: manipulation; } + /* ── Sidebar Layout ── */ + #notes-layout { + display: grid; + grid-template-columns: 260px 1fr; + min-height: 400px; + height: calc(100vh - 120px); + } + .notes-sidebar { + display: flex; + flex-direction: column; + border-right: 1px solid var(--rs-border-subtle); + background: var(--rs-bg-surface); + overflow: hidden; + height: 100%; + } + .sidebar-header { padding: 12px 12px 8px; } + .sidebar-search { + width: 100%; padding: 8px 12px; border-radius: 6px; + border: 1px solid var(--rs-input-border); background: var(--rs-input-bg); + color: var(--rs-input-text); font-size: 13px; font-family: inherit; + transition: border-color 0.15s; + } + .sidebar-search:focus { border-color: var(--rs-primary); outline: none; } + .sidebar-tree { flex: 1; overflow-y: auto; padding: 4px 0; } + .sidebar-btn-new-nb { + display: flex; align-items: center; justify-content: center; gap: 6px; + width: calc(100% - 24px); margin: 0 12px 8px; padding: 7px; + border-radius: 6px; border: 1px dashed var(--rs-border); + background: transparent; color: var(--rs-text-secondary); + font-size: 13px; font-family: inherit; cursor: pointer; transition: all 0.15s; + } + .sidebar-btn-new-nb:hover { border-color: var(--rs-primary); color: var(--rs-primary); background: rgba(99, 102, 241, 0.05); } + + /* Notebook tree */ + .sbt-notebook-header { + display: flex; align-items: center; gap: 6px; + padding: 6px 12px; cursor: pointer; user-select: none; + transition: background 0.1s; font-size: 13px; + } + .sbt-notebook-header:hover { background: var(--rs-bg-hover); } + .sbt-toggle { + width: 16px; text-align: center; font-size: 10px; + color: var(--rs-text-muted); flex-shrink: 0; + transition: transform 0.15s; + } + .sbt-toggle.expanded { transform: rotate(90deg); } + .sbt-nb-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; } + .sbt-nb-title { + flex: 1; font-weight: 500; color: var(--rs-text-primary); + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + } + .sbt-nb-count { font-size: 11px; color: var(--rs-text-muted); flex-shrink: 0; } + .sbt-nb-add { + opacity: 0; border: none; background: none; + color: var(--rs-text-muted); cursor: pointer; + font-size: 16px; line-height: 1; padding: 0 4px; + border-radius: 3px; transition: all 0.15s; flex-shrink: 0; + } + .sbt-notebook-header:hover .sbt-nb-add { opacity: 1; } + .sbt-nb-add:hover { color: var(--rs-primary); background: var(--rs-bg-surface-raised); } + .sbt-notes { padding-left: 20px; } + .sbt-note { + display: flex; align-items: center; gap: 8px; + padding: 5px 12px 5px 8px; cursor: pointer; + font-size: 13px; color: var(--rs-text-secondary); + border-radius: 4px; margin: 1px 8px 1px 0; + transition: all 0.1s; overflow: hidden; + } + .sbt-note:hover { background: var(--rs-bg-hover); color: var(--rs-text-primary); } + .sbt-note.active { background: var(--rs-primary); color: #fff; } + .sbt-note-icon { font-size: 14px; flex-shrink: 0; } + .sbt-note-title { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .sbt-note-pin { font-size: 10px; flex-shrink: 0; } + + .sidebar-footer { + padding: 8px 12px; border-top: 1px solid var(--rs-border-subtle); + display: flex; gap: 6px; + } + .sidebar-footer-btn { + padding: 5px 10px; border-radius: 5px; + border: 1px solid var(--rs-border); background: transparent; + color: var(--rs-text-secondary); font-size: 11px; + font-family: inherit; cursor: pointer; transition: all 0.15s; + } + .sidebar-footer-btn:hover { border-color: var(--rs-border-strong); color: var(--rs-text-primary); } + + /* Sidebar search results */ + .sidebar-search-results { padding: 4px 0; } + .sidebar-search-result { + display: flex; align-items: center; gap: 8px; + padding: 6px 12px; cursor: pointer; font-size: 13px; + color: var(--rs-text-secondary); transition: background 0.1s; + } + .sidebar-search-result:hover { background: var(--rs-bg-hover); color: var(--rs-text-primary); } + .sidebar-search-result-nb { font-size: 10px; color: var(--rs-text-muted); margin-left: auto; } + + /* Right column */ + .notes-right-col { + display: flex; flex-direction: column; overflow: hidden; height: 100%; + } + .notes-right-col #content-zone { flex: 1; overflow-y: auto; padding: 20px; } + .notes-right-col #meta-zone { padding: 0 20px 12px; } + + /* Empty state */ + .editor-empty-state { + display: flex; flex-direction: column; align-items: center; + justify-content: center; height: 100%; min-height: 300px; + color: var(--rs-text-muted); gap: 12px; + } + .editor-empty-state svg { width: 48px; height: 48px; opacity: 0.4; } + .editor-empty-state p { font-size: 14px; } + + /* Mobile sidebar */ + .mobile-sidebar-toggle { + display: none; position: fixed; bottom: 20px; left: 20px; z-index: 198; + width: 44px; height: 44px; border-radius: 50%; border: none; + background: var(--rs-primary); color: #fff; font-size: 20px; + cursor: pointer; box-shadow: var(--rs-shadow-md); + align-items: center; justify-content: center; + } + .sidebar-overlay { + display: none; position: fixed; inset: 0; + background: rgba(0,0,0,0.4); z-index: 199; + } + .sidebar-overlay.open { display: block; } + /* ── Navigation ── */ - .rapp-nav { display: flex; gap: 8px; margin-bottom: 16px; align-items: center; min-height: 36px; } - .rapp-nav__back { padding: 4px 10px; border-radius: 6px; border: 1px solid var(--rs-border); background: transparent; color: var(--rs-text-secondary); cursor: pointer; font-size: 13px; transition: all 0.15s; } - .rapp-nav__back:hover { color: var(--rs-text-primary); border-color: var(--rs-border-strong); } - .rapp-nav__title { font-size: 15px; font-weight: 600; flex: 1; color: var(--rs-text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .rapp-nav__btn { padding: 6px 14px; border-radius: 6px; border: none; background: var(--rs-primary); color: #fff; font-weight: 600; cursor: pointer; font-size: 13px; transition: background 0.15s; display: flex; align-items: center; gap: 4px; } .rapp-nav__btn:hover { background: var(--rs-primary-hover); } - .rapp-nav__btn--secondary { background: transparent; border: 1px solid var(--rs-border); color: var(--rs-text-secondary); font-weight: 500; } - .rapp-nav__btn--secondary:hover { border-color: var(--rs-border-strong); color: var(--rs-text-primary); background: transparent; } - - /* ── Search ── */ - .search-bar { - width: 100%; padding: 10px 14px; border-radius: 8px; - border: 1px solid var(--rs-input-border); background: var(--rs-input-bg); color: var(--rs-input-text); - font-size: 14px; margin-bottom: 16px; transition: border-color 0.15s; - } - .search-bar:focus { border-color: var(--rs-primary); outline: none; } - .search-results-info { margin-bottom: 12px; font-size: 13px; color: var(--rs-text-secondary); } - - /* ── Notebook Grid ── */ - .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 14px; } - .notebook-card { - position: relative; overflow: hidden; - border-radius: 12px; padding: 16px 16px 16px 20px; cursor: pointer; - border: 1px solid var(--rs-card-border); background: var(--rs-card-bg); - transition: border-color 0.2s, box-shadow 0.2s, transform 0.15s; - min-height: 120px; display: flex; flex-direction: column; justify-content: space-between; - } - .notebook-card:hover { border-color: var(--rs-border); box-shadow: var(--rs-shadow-sm); transform: translateY(-1px); } - .notebook-card__accent { position: absolute; top: 0; left: 0; width: 4px; height: 100%; border-radius: 12px 0 0 12px; } - .notebook-card__body { flex: 1; } - .notebook-card__title { font-size: 15px; font-weight: 600; margin-bottom: 4px; color: var(--rs-text-primary); } - .notebook-card__desc { font-size: 12px; color: var(--rs-text-muted); line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } - .notebook-card__footer { display: flex; justify-content: space-between; font-size: 11px; color: var(--rs-text-muted); margin-top: 8px; } /* ── Presence Indicators ── */ .presence-dots { display: inline-flex; gap: 2px; align-items: center; margin-left: auto; } .presence-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; border: 1px solid var(--rs-bg-surface, #fff); flex-shrink: 0; } .presence-dot-more { font-size: 10px; color: var(--rs-text-muted); margin-left: 2px; } - /* ── Note Items ── */ - .note-item { - background: var(--rs-bg-surface); border: 1px solid var(--rs-border-subtle); border-radius: 10px; - padding: 14px 16px; margin-bottom: 8px; cursor: pointer; - transition: border-color 0.15s, box-shadow 0.15s; - display: flex; gap: 12px; align-items: flex-start; - } - .note-item:hover { border-color: var(--rs-border); box-shadow: var(--rs-shadow-sm); } - .note-item__icon { - font-size: 18px; flex-shrink: 0; width: 32px; height: 32px; - display: flex; align-items: center; justify-content: center; - background: var(--rs-bg-surface-raised); border-radius: 8px; - } - .note-item__body { flex: 1; min-width: 0; } - .note-item__title { font-size: 14px; font-weight: 600; color: var(--rs-text-primary); display: flex; align-items: center; gap: 6px; } - .note-item__pin { color: var(--rs-warning); } - .note-sync-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; } - .note-item__preview { - font-size: 12px; color: var(--rs-text-muted); margin-top: 3px; line-height: 1.4; - overflow: hidden; text-overflow: ellipsis; - display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; - } - .note-item__meta { font-size: 11px; color: var(--rs-text-muted); margin-top: 6px; display: flex; gap: 8px; align-items: center; } - .tag { display: inline-block; padding: 1px 6px; border-radius: 3px; background: var(--rs-bg-surface-raised); color: var(--rs-text-secondary); font-size: 10px; } - - /* ── Type Filter Bar ── */ - .type-filter-bar { display: flex; gap: 6px; margin-bottom: 12px; flex-wrap: wrap; } - .type-filter-pill { - padding: 4px 12px; border-radius: 16px; border: 1px solid var(--rs-border); - background: transparent; color: var(--rs-text-secondary); font-size: 12px; - cursor: pointer; transition: all 0.15s; font-family: inherit; - } - .type-filter-pill:hover { border-color: var(--rs-border-strong); color: var(--rs-text-primary); } - .type-filter-pill.active { background: var(--rs-primary); color: #fff; border-color: var(--rs-primary); } - - /* ── New Note Split Button ── */ - .new-note-split { display: flex; position: relative; } - .new-note-split .rapp-nav__btn:first-child { border-radius: 6px 0 0 6px; } - .new-note-dropdown-btn { - border-radius: 0 6px 6px 0 !important; padding: 6px 8px !important; - border-left: 1px solid rgba(255,255,255,0.2) !important; min-width: 28px; - } - .new-note-dropdown { - position: absolute; top: 100%; right: 0; margin-top: 4px; z-index: 50; - background: var(--rs-bg-surface); border: 1px solid var(--rs-border); - border-radius: 8px; box-shadow: var(--rs-shadow-md); min-width: 180px; - overflow: hidden; - } - .new-note-dropdown-item { - padding: 8px 14px; cursor: pointer; font-size: 13px; - color: var(--rs-text-primary); transition: background 0.1s; - } - .new-note-dropdown-item:hover { background: var(--rs-bg-hover); } - - /* ── Note Item Type Badges ── */ - .note-item__badge { - display: inline-block; padding: 1px 6px; border-radius: 3px; - font-size: 10px; font-weight: 500; - } - .badge-url { background: rgba(245, 158, 11, 0.15); color: #d97706; } - .badge-lang { background: rgba(16, 185, 129, 0.15); color: #059669; } - .badge-duration { background: rgba(239, 68, 68, 0.15); color: #dc2626; } - /* ── Code Editor ── */ .code-editor-controls { padding: 4px 12px; display: flex; gap: 8px; align-items: center; } .code-textarea { @@ -3176,21 +3193,19 @@ Gear: EUR 400 (10%)

Maya is tracking expenses in rF .tiptap-container .tiptap u { text-underline-offset: 3px; } @media (max-width: 768px) { - .grid { grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 10px; } - .notebook-card { min-height: 100px; padding: 12px 12px 12px 16px; } + #notes-layout { grid-template-columns: 1fr; } + .notes-sidebar { + position: fixed; left: 0; top: 0; bottom: 0; width: 280px; + z-index: 200; transform: translateX(-100%); + transition: transform 0.25s ease; box-shadow: var(--rs-shadow-lg); + } + .notes-sidebar.open { transform: translateX(0); } + .mobile-sidebar-toggle { display: flex !important; } .editor-wrapper .editable-title { padding: 12px 14px 0; } .tiptap-container .tiptap { padding: 14px 16px; } - .note-item { padding: 10px 12px; gap: 8px; } - } - @media (max-width: 600px) { - .grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); } } @media (max-width: 480px) { - .grid { grid-template-columns: 1fr; } - .rapp-nav { gap: 4px; } .rapp-nav__btn { padding: 5px 10px; font-size: 12px; } - .search-bar { font-size: 13px; padding: 8px 10px; } - .notebook-card__title { font-size: 14px; } .editable-title { font-size: 18px; } .tiptap-container .tiptap { font-size: 14px; padding: 12px 14px; min-height: 200px; } .editor-toolbar { padding: 3px 4px; gap: 1px; } diff --git a/modules/rnotes/components/notes.css b/modules/rnotes/components/notes.css index afdc266..3b41591 100644 --- a/modules/rnotes/components/notes.css +++ b/modules/rnotes/components/notes.css @@ -2,6 +2,6 @@ folk-notes-app { display: block; min-height: 400px; - padding: 20px; + padding: 0; position: relative; }