/** * Notes — Surface Entry Point (v0.3.0) * * Markdown notes surface using the SDK: * sw.api.ext('notes') — scoped API client * sw.ui.* — primitive components * sw.shell.Topbar — navigation bar */ (async function () { 'use strict'; var mount = document.getElementById('extension-mount'); if (!mount) return; var base = window.__BASE__ || ''; var ver = window.__VERSION__ || '0'; // ── 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; } 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'); 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
`; } // ═══════════════════════════════════════════ // EditorPane — note editor + preview + tags // ═══════════════════════════════════════════ function EditorPane({ note, folders, allTags, onSave, onDelete, onRefresh, onTagsChanged }) { 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 textareaRef = useRef(null); // sync when note changes useEffect(function() { if (note) { setTitle(note.title || ''); setBody(note.body || ''); setNoteTags(note.tags || []); setDirty(false); setPreview(false); } }, [note ? note.id : null]); function handleTitleChange(e) { setTitle(e.target.value); setDirty(true); debounce('save', function() { doSave(e.target.value, body); }, 1000); } function handleBodyChange(e) { setBody(e.target.value); setDirty(true); debounce('save', function() { doSave(title, 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); 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 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); 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); } } var previewHtml = useMemo(function() { return renderMarkdown(body); }, [body]); // 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
`; } return html`
${saving ? 'Saving…' : (dirty ? 'Unsaved' : 'Saved')}
<${TagInput} tags=${noteTags} allTags=${allTags} onChange=${handleTagsChange} />
${preview ? html`
` : html`