/** * Notes — Surface Entry Point (v0.2.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; } // ═══════════════════════════════════════════ // NoteCard — sidebar list item // ═══════════════════════════════════════════ function NoteCard({ note, active, onClick }) { var dateStr = note.updated_at || note.created_at || ''; if (dateStr) { try { dateStr = new Date(dateStr).toLocaleDateString(); } catch(e) { /* keep raw */ } } return html`
${note.pinned ? html`📌` : null} ${note.title || 'Untitled'}
${note.snippet && html`
${note.snippet}
`} ${dateStr && html`
${dateStr}
`}
`; } // ═══════════════════════════════════════════ // FolderNode — recursive tree node // ═══════════════════════════════════════════ function FolderNode({ node, depth, activeFolderId, onSelect, onRename, onDelete, onCreateSub }) { var [expanded, setExpanded] = useState(true); var [editing, setEditing] = useState(false); var [editName, setEditName] = useState(node.folder.name); 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(); var action = prompt('Type "rename" to rename, "delete" to delete, "subfolder" to add subfolder'); if (action === 'rename') { setEditing(true); setEditName(node.folder.name); } else if (action === 'delete') { onDelete(node.folder.id); } else if (action === 'subfolder') { onCreateSub(node.folder.id); } } return html`
${hasChildren ? (expanded ? '▼' : '▶') : '·'} ${isActive ? '📂' : '📁'} ${editing ? html`` : html`${node.folder.name}` }
${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} />`; })}
`; } // ═══════════════════════════════════════════ // FolderTree — navigation panel // ═══════════════════════════════════════════ function FolderTree({ folders, activeFolderId, showUnfiled, onSelectFolder, onSelectAll, onSelectUnfiled, onCreateFolder, onRenameFolder, onDeleteFolder }) { // 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]); 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} />`; })}
+ New Folder
`; } // ═══════════════════════════════════════════ // EditorPane — note editor + preview // ═══════════════════════════════════════════ function EditorPane({ note, folders, onSave, onDelete, onRefresh }) { var [title, setTitle] = useState(note ? note.title : ''); var [body, setBody] = useState(note ? note.body : ''); 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 || ''); 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 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')}
${preview ? html`
` : html`