/** * Notes — Surface Entry Point (v0.5.0) * * Markdown notes surface using the SDK: * sw.api.ext('notes') — scoped API client * sw.ui.* — primitive components * sw.shell.Topbar — navigation bar * window.CM — CodeMirror 6 bundle (optional) */ (async function () { 'use strict'; var mount = document.getElementById('extension-mount'); if (!mount) return; var base = window.__BASE__ || ''; var ver = window.__VERSION__ || '0'; // ── Dynamic Script Loader ──────────────────── function _loadScript(src) { return new Promise(function(resolve, reject) { if (document.querySelector('script[src*="' + src.split('?')[0] + '"]')) { resolve(); return; } var s = document.createElement('script'); s.src = base + src; s.onload = resolve; s.onerror = function() { resolve(); }; // Non-fatal document.head.appendChild(s); }); } // ── Boot SDK ─────────────────────────────── try { if (!window.preact) { var { h, render } = await import(base + '/js/sw/vendor/preact.module.js'); var hooksModule = await import(base + '/js/sw/vendor/hooks.module.js'); var htmModule = await import(base + '/js/sw/vendor/htm.module.js'); window.preact = { h, render }; window.hooks = hooksModule; window.html = htmModule.default.bind(h); } var sdk = await import(base + '/js/sw/sdk/index.js?v=' + ver); await sdk.boot(); } catch (e) { mount.innerHTML = '

SDK boot failed: ' + e.message + '

'; return; } // ── Load CM6 bundle (optional — falls back to textarea) ── await _loadScript('/vendor/codemirror/codemirror.bundle.js?v=' + ver); var hasCM = !!(window.CM && window.CM.noteEditor); var { html } = window; var { useState, useEffect, useCallback, useRef, useMemo } = hooks; var { render } = preact; // ── SDK modules ──────────────────────────── var api = sw.api.ext('notes'); var { Button, Spinner } = sw.ui; var Topbar = sw.shell.Topbar; // ── Debounce helper ──────────────────────── var _timers = {}; function debounce(key, fn, ms) { clearTimeout(_timers[key]); _timers[key] = setTimeout(fn, ms); } // ═══════════════════════════════════════════ // Markdown renderer (inline, no deps) // ═══════════════════════════════════════════ function renderMarkdown(src) { if (!src) return ''; var lines = src.split('\n'); var out = []; var inCode = false; var inList = false; var listTag = ''; for (var i = 0; i < lines.length; i++) { var line = lines[i]; // fenced code blocks if (line.trim().startsWith('```')) { if (inCode) { out.push(''); inCode = false; } else { if (inList) { out.push(''); inList = false; } out.push('
');
          inCode = true;
        }
        continue;
      }
      if (inCode) {
        out.push(escHtml(line) + '\n');
        continue;
      }

      // blank line
      if (!line.trim()) {
        if (inList) { out.push(''); inList = false; }
        continue;
      }

      // headings
      if (line.startsWith('### ')) { closeList(); out.push('

' + inline(line.slice(4)) + '

'); continue; } if (line.startsWith('## ')) { closeList(); out.push('

' + inline(line.slice(3)) + '

'); continue; } if (line.startsWith('# ')) { closeList(); out.push('

' + inline(line.slice(2)) + '

'); continue; } // hr if (/^[-*_]{3,}\s*$/.test(line)) { closeList(); out.push('
'); continue; } // blockquote if (line.startsWith('> ')) { closeList(); out.push('

' + inline(line.slice(2)) + '

'); continue; } // unordered list if (/^[\-*+]\s/.test(line)) { if (!inList || listTag !== 'ul') { closeList(); out.push('
'); if (inList) out.push(''); return out.join('\n'); function closeList() { if (inList) { out.push(''); inList = false; } } } function escHtml(s) { return s.replace(/&/g, '&').replace(//g, '>'); } function inline(s) { s = escHtml(s); // inline code s = s.replace(/`([^`]+)`/g, '$1'); // bold s = s.replace(/\*\*(.+?)\*\*/g, '$1'); s = s.replace(/__(.+?)__/g, '$1'); // italic s = s.replace(/\*(.+?)\*/g, '$1'); s = s.replace(/_(.+?)_/g, '$1'); // links s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); // wikilinks: [[Note Title]] → clickable internal link s = s.replace(/\[\[([^\]]+)\]\]/g, function(match, linkText) { return '' + linkText.trim() + ''; }); return s; } // ═══════════════════════════════════════════ // TagInput — editor tag management // ═══════════════════════════════════════════ function TagInput({ tags, allTags, onChange }) { var [input, setInput] = useState(''); var [showAC, setShowAC] = useState(false); var inputRef = useRef(null); var suggestions = useMemo(function() { if (!input.trim()) return []; var q = input.trim().toLowerCase(); return (allTags || []).filter(function(t) { return t.toLowerCase().indexOf(q) !== -1 && (tags || []).indexOf(t) === -1; }).slice(0, 8); }, [input, allTags, tags]); function addTag(tag) { var t = tag.trim().toLowerCase(); if (!t) return; var current = tags || []; if (current.indexOf(t) === -1) { onChange(current.concat([t])); } setInput(''); setShowAC(false); if (inputRef.current) inputRef.current.focus(); } function removeTag(tag) { onChange((tags || []).filter(function(t) { return t !== tag; })); } function handleKeyDown(e) { if (e.key === 'Enter' || e.key === ',') { e.preventDefault(); if (input.trim()) addTag(input); } if (e.key === 'Backspace' && !input && tags && tags.length > 0) { removeTag(tags[tags.length - 1]); } if (e.key === 'Escape') { setShowAC(false); } } function handleInput(e) { var val = e.target.value.replace(',', ''); setInput(val); setShowAC(val.trim().length > 0); } return html`
${(tags || []).map(function(tag) { return html` ${tag} × `; })} ${showAC && suggestions.length > 0 && html`
${suggestions.map(function(s) { return html`
${s}
`; })}
`}
`; } // ═══════════════════════════════════════════ // TagFilter — sidebar tag filter // ═══════════════════════════════════════════ function TagFilter({ tags, activeTag, onSelect }) { if (!tags || tags.length === 0) return null; return html`
Tags ${activeTag && html` `}
${tags.map(function(tag) { return html` ${tag} `; })}
`; } // ═══════════════════════════════════════════ // NoteCard — sidebar list item (draggable) // ═══════════════════════════════════════════ function NoteCard({ note, active, onClick }) { var [dragging, setDragging] = useState(false); var dateStr = note.updated_at || note.created_at || ''; if (dateStr) { try { dateStr = new Date(dateStr).toLocaleDateString(); } catch(e) { /* keep raw */ } } var tags = note.tags || []; var showTags = tags.slice(0, 3); var overflow = tags.length - 3; function handleDragStart(e) { e.dataTransfer.setData('text/plain', note.id); e.dataTransfer.effectAllowed = 'move'; setDragging(true); } function handleDragEnd() { setDragging(false); } return html`
${note.pinned ? html`📌` : null} ${note.title || 'Untitled'}
${note.snippet && html`
${note.snippet}
`} ${showTags.length > 0 && html`
${showTags.map(function(t) { return html`${t}`; })} ${overflow > 0 && html`+${overflow}`}
`} ${dateStr && html`
${dateStr}
`}
`; } // ═══════════════════════════════════════════ // FolderContextMenu — right-click menu // ═══════════════════════════════════════════ function FolderContextMenu({ x, y, onRename, onDelete, onAddSub, onClose }) { var menuRef = useRef(null); useEffect(function() { function handleClick(e) { if (menuRef.current && !menuRef.current.contains(e.target)) onClose(); } document.addEventListener('mousedown', handleClick); return function() { document.removeEventListener('mousedown', handleClick); }; }, [onClose]); return html`
Add subfolder
Rename
Delete
`; } // ═══════════════════════════════════════════ // FolderNode — recursive tree node (drop target) // ═══════════════════════════════════════════ function FolderNode({ node, depth, activeFolderId, onSelect, onRename, onDelete, onCreateSub, onDropNote }) { var [expanded, setExpanded] = useState(true); var [editing, setEditing] = useState(false); var [editName, setEditName] = useState(node.folder.name); var [dropOver, setDropOver] = useState(false); var [menu, setMenu] = useState(null); var hasChildren = node.children.length > 0; var isActive = activeFolderId === node.folder.id; var indent = 20 + depth * 16; function handleToggle(e) { e.stopPropagation(); setExpanded(!expanded); } function handleRename() { if (editName.trim() && editName !== node.folder.name) { onRename(node.folder.id, editName.trim()); } setEditing(false); } function handleContext(e) { e.preventDefault(); e.stopPropagation(); setMenu({ x: e.clientX, y: e.clientY }); } function handleDragOver(e) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; setDropOver(true); } function handleDragLeave() { setDropOver(false); } function handleDrop(e) { e.preventDefault(); setDropOver(false); var noteId = e.dataTransfer.getData('text/plain'); if (noteId && onDropNote) onDropNote(noteId, node.folder.id); } return html`
${hasChildren ? (expanded ? '▼' : '▶') : '·'} ${isActive ? '📂' : '📁'} ${editing ? html`` : html`${node.folder.name}` }
${menu && html`<${FolderContextMenu} x=${menu.x} y=${menu.y} onRename=${function() { setMenu(null); setEditing(true); setEditName(node.folder.name); }} onDelete=${function() { setMenu(null); onDelete(node.folder.id); }} onAddSub=${function() { setMenu(null); onCreateSub(node.folder.id); }} onClose=${function() { setMenu(null); }} />`} ${expanded && node.children.map(function(child) { return html`<${FolderNode} key=${child.folder.id} node=${child} depth=${depth + 1} activeFolderId=${activeFolderId} onSelect=${onSelect} onRename=${onRename} onDelete=${onDelete} onCreateSub=${onCreateSub} onDropNote=${onDropNote} />`; })}
`; } // ═══════════════════════════════════════════ // FolderTree — navigation panel (drop targets) // ═══════════════════════════════════════════ function FolderTree({ folders, activeFolderId, showUnfiled, onSelectFolder, onSelectAll, onSelectUnfiled, onCreateFolder, onRenameFolder, onDeleteFolder, onDropNote }) { var [dropAll, setDropAll] = useState(false); var [dropUnfiled, setDropUnfiled] = useState(false); // build tree from flat list var tree = useMemo(function() { var byId = {}; var roots = []; for (var i = 0; i < folders.length; i++) { byId[folders[i].id] = { folder: folders[i], children: [] }; } for (var i = 0; i < folders.length; i++) { var f = folders[i]; var node = byId[f.id]; if (f.parent_id && byId[f.parent_id]) { byId[f.parent_id].children.push(node); } else { roots.push(node); } } function sortNodes(arr) { arr.sort(function(a, b) { if (a.folder.sort_order !== b.folder.sort_order) return a.folder.sort_order - b.folder.sort_order; return (a.folder.name || '').localeCompare(b.folder.name || ''); }); for (var j = 0; j < arr.length; j++) sortNodes(arr[j].children); } sortNodes(roots); return roots; }, [folders]); // drop handlers for "All Notes" / "Unfiled" (move to unfiled = folder_id "") function makeDropHandlers(setOver) { return { onDragOver: function(e) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; setOver(true); }, onDragLeave: function() { setOver(false); }, onDrop: function(e) { e.preventDefault(); setOver(false); var noteId = e.dataTransfer.getData('text/plain'); if (noteId && onDropNote) onDropNote(noteId, ''); } }; } var allDrop = makeDropHandlers(setDropAll); var unfiledDrop = makeDropHandlers(setDropUnfiled); return html`
📋 All Notes
📄 Unfiled
${tree.map(function(node) { return html`<${FolderNode} key=${node.folder.id} node=${node} depth=${0} activeFolderId=${activeFolderId} onSelect=${onSelectFolder} onRename=${onRenameFolder} onDelete=${onDeleteFolder} onCreateSub=${onCreateFolder} onDropNote=${onDropNote} />`; })}
+ New Folder
`; } // ═══════════════════════════════════════════ // BacklinksPanel — incoming wikilinks // ═══════════════════════════════════════════ function BacklinksPanel({ noteId, onNavigate }) { var [backlinks, setBacklinks] = useState([]); var [collapsed, setCollapsed] = useState(false); useEffect(function() { if (!noteId) { setBacklinks([]); return; } api.get('/backlinks/' + noteId).then(function(res) { var items = (res && res.data) || res || []; setBacklinks(Array.isArray(items) ? items : []); }).catch(function() { setBacklinks([]); }); }, [noteId]); if (!backlinks.length) return null; return html` `; } // ═══════════════════════════════════════════ // EditorPane — note editor + preview + tags // ═══════════════════════════════════════════ // ═══════════════════════════════════════════ // Export / Import helpers // ═══════════════════════════════════════════ function exportNoteAsMarkdown(note, tags) { var parts = ['---']; parts.push('title: ' + (note.title || 'Untitled')); if (tags && tags.length) parts.push('tags: [' + tags.join(', ') + ']'); if (note.created_at) parts.push('created: ' + note.created_at); parts.push('---'); parts.push(''); parts.push(note.body || ''); var text = parts.join('\n'); var blob = new Blob([text], { type: 'text/markdown;charset=utf-8' }); var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = (note.title || 'Untitled').replace(/[^a-zA-Z0-9_\- ]/g, '').trim() + '.md'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function parseFrontmatter(text) { var result = { title: '', tags: [], body: text }; if (!text.startsWith('---')) return result; var end = text.indexOf('\n---', 3); if (end === -1) return result; var fm = text.substring(3, end).trim(); var bodyStart = end + 4; // skip leading newline after frontmatter if (text[bodyStart] === '\n') bodyStart++; result.body = text.substring(bodyStart); var lines = fm.split('\n'); for (var i = 0; i < lines.length; i++) { var line = lines[i].trim(); if (line.startsWith('title:')) { result.title = line.substring(6).trim().replace(/^["']|["']$/g, ''); } else if (line.startsWith('tags:')) { var tagStr = line.substring(5).trim(); // parse [tag1, tag2] or tag1, tag2 tagStr = tagStr.replace(/^\[|\]$/g, ''); result.tags = tagStr.split(',').map(function(t) { return t.trim().toLowerCase(); }).filter(Boolean); } } return result; } function EditorPane({ note, folders, allTags, onSave, onDelete, onRefresh, onTagsChanged, onNavigate, onNavigateToTitle }) { var [title, setTitle] = useState(note ? note.title : ''); var [body, setBody] = useState(note ? note.body : ''); var [noteTags, setNoteTags] = useState([]); var [preview, setPreview] = useState(false); var [dirty, setDirty] = useState(false); var [saving, setSaving] = useState(false); var [saveCount, setSaveCount] = useState(0); var textareaRef = useRef(null); var cmContainerRef = useRef(null); var cmEditorRef = useRef(null); // keep mutable refs for the latest title/body so CM6 callbacks see current values var titleRef = useRef(title); var bodyRef = useRef(body); titleRef.current = title; bodyRef.current = body; // sync when note changes useEffect(function() { if (note) { setTitle(note.title || ''); setBody(note.body || ''); titleRef.current = note.title || ''; bodyRef.current = note.body || ''; setNoteTags(note.tags || []); setDirty(false); setPreview(false); // update CM6 content if editor exists if (cmEditorRef.current) { cmEditorRef.current.setValue(note.body || ''); } } }, [note ? note.id : null]); // ── CM6 lifecycle ───────────────────────── useEffect(function() { if (!hasCM || preview || !note) return; var el = cmContainerRef.current; if (!el) return; // destroy previous if (cmEditorRef.current) { cmEditorRef.current.destroy(); cmEditorRef.current = null; } var editor = CM.noteEditor(el, { value: bodyRef.current, darkMode: document.documentElement.getAttribute('data-theme') !== 'light', onChange: function(text) { bodyRef.current = text; setBody(text); setDirty(true); debounce('save', function() { doSave(titleRef.current, text); }, 1000); }, onLink: function(linkTitle) { if (onNavigateToTitle) onNavigateToTitle(linkTitle); }, linkCompleter: async function(query) { try { var res = await api.get('/search?q=' + encodeURIComponent(query)); var items = (res && res.data) || res || []; if (!Array.isArray(items)) items = []; return items.map(function(n) { return { label: n.title, id: n.id }; }); } catch (e) { return []; } }, }); cmEditorRef.current = editor; // Ctrl/Cmd+S via CM6 keymap injection var view = editor.getView(); if (view) { var handleSaveKey = function(e) { if (e.key === 's' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); doSave(titleRef.current, bodyRef.current); } }; view.dom.addEventListener('keydown', handleSaveKey); } editor.focus(); return function() { if (cmEditorRef.current) { cmEditorRef.current.destroy(); cmEditorRef.current = null; } }; }, [note ? note.id : null, preview]); function handleTitleChange(e) { setTitle(e.target.value); titleRef.current = e.target.value; setDirty(true); debounce('save', function() { doSave(e.target.value, bodyRef.current); }, 1000); } function handleBodyChange(e) { setBody(e.target.value); bodyRef.current = e.target.value; setDirty(true); debounce('save', function() { doSave(titleRef.current, e.target.value); }, 1000); } async function doSave(t, b) { if (!note) return; setSaving(true); try { await api.put('/notes/' + note.id, { title: t || 'Untitled', body: b }); setDirty(false); setSaveCount(function(c) { return c + 1; }); onRefresh(); } catch (e) { console.error('Save failed:', e); } finally { setSaving(false); } } async function handleTagsChange(newTags) { if (!note) return; setNoteTags(newTags); try { await api.put('/tags/' + note.id, { tags: newTags }); if (onTagsChanged) onTagsChanged(); } catch (e) { console.error('Save tags failed:', e); } } async function handlePin() { if (!note) return; await api.put('/notes/' + note.id, { pinned: note.pinned ? 0 : 1 }); onRefresh(); } async function handleArchive() { if (!note || !confirm('Archive this note?')) return; await api.del('/notes/' + note.id); onDelete(); } function handleExport() { if (!note) return; exportNoteAsMarkdown(note, noteTags); } function handleKeyDown(e) { // Tab inserts two spaces if (e.key === 'Tab') { e.preventDefault(); var ta = e.target; var start = ta.selectionStart; var end = ta.selectionEnd; var val = ta.value; ta.value = val.substring(0, start) + ' ' + val.substring(end); ta.selectionStart = ta.selectionEnd = start + 2; setBody(ta.value); bodyRef.current = ta.value; setDirty(true); debounce('save', function() { doSave(title, ta.value); }, 1000); } // Ctrl/Cmd+S force save if (e.key === 's' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); doSave(title, body); } } // ── Outgoing links for resolved/unresolved styling ── var [outgoingLinks, setOutgoingLinks] = useState([]); useEffect(function() { if (!note) { setOutgoingLinks([]); return; } api.get('/links/' + note.id).then(function(res) { var items = (res && res.data) || res || []; setOutgoingLinks(Array.isArray(items) ? items : []); }).catch(function() { setOutgoingLinks([]); }); }, [note ? note.id : null, saveCount]); var previewHtml = useMemo(function() { var h = renderMarkdown(body); // mark unresolved wikilinks for (var i = 0; i < outgoingLinks.length; i++) { var link = outgoingLinks[i]; if (!link.target_id) { var search = 'class="wikilink" data-wikilink="' + escHtml(link.link_text) + '"'; var replace = 'class="wikilink wikilink--unresolved" data-wikilink="' + escHtml(link.link_text) + '"'; h = h.split(search).join(replace); } } return h; }, [body, outgoingLinks]); // ── Wikilink click handling in preview ── var previewRef = useRef(null); useEffect(function() { var el = previewRef.current; if (!el || !preview) return; function handleClick(e) { var target = e.target; if (target.classList && target.classList.contains('wikilink')) { e.preventDefault(); var linkText = target.getAttribute('data-wikilink'); if (linkText && onNavigateToTitle) onNavigateToTitle(linkText); } } el.addEventListener('click', handleClick); return function() { el.removeEventListener('click', handleClick); }; }, [preview, onNavigateToTitle]); // build indented folder list for the select dropdown var folderOptions = useMemo(function() { var list = folders || []; var byId = {}; var roots = []; for (var i = 0; i < list.length; i++) { byId[list[i].id] = { folder: list[i], children: [] }; } for (var i = 0; i < list.length; i++) { var f = list[i]; if (f.parent_id && byId[f.parent_id]) { byId[f.parent_id].children.push(byId[f.id]); } else { roots.push(byId[f.id]); } } var flat = []; function walk(nodes, depth) { nodes.sort(function(a, b) { if (a.folder.sort_order !== b.folder.sort_order) return a.folder.sort_order - b.folder.sort_order; return (a.folder.name || '').localeCompare(b.folder.name || ''); }); for (var j = 0; j < nodes.length; j++) { var prefix = ''; for (var k = 0; k < depth; k++) prefix += '\u00A0\u00A0'; if (depth > 0) prefix += '\u2514 '; flat.push({ id: nodes[j].folder.id, label: prefix + nodes[j].folder.name }); walk(nodes[j].children, depth + 1); } } walk(roots, 0); return flat; }, [folders]); if (!note) { return html`
Select a note or create a new one
`; } // ── Render editor body ───────────────────── function renderEditorBody() { if (preview) { return html`
`; } if (hasCM) { return html`
`; } // fallback textarea return html`