/**
* Notes — Surface Entry Point (v0.1.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('' + listTag + '>'); inList = false; }
out.push('');
inCode = true;
}
continue;
}
if (inCode) {
out.push(escHtml(line) + '\n');
continue;
}
// blank line
if (!line.trim()) {
if (inList) { out.push('' + listTag + '>'); 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(''); inList = true; listTag = 'ul'; }
out.push('- ' + inline(line.replace(/^[\-*+]\s/, '')) + '
');
continue;
}
// ordered list
if (/^\d+\.\s/.test(line)) {
if (!inList || listTag !== 'ol') { closeList(); out.push(''); inList = true; listTag = 'ol'; }
out.push('- ' + inline(line.replace(/^\d+\.\s/, '')) + '
');
continue;
}
// paragraph
closeList();
out.push('' + inline(line) + '
');
}
if (inCode) out.push('
');
if (inList) out.push('' + listTag + '>');
return out.join('\n');
function closeList() { if (inList) { out.push('' + listTag + '>'); 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}
`}
`;
}
// ═══════════════════════════════════════════
// EditorPane — note editor + preview
// ═══════════════════════════════════════════
function EditorPane({ note, 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]);
if (!note) {
return html`Select a note or create a new one
`;
}
return html`
${preview
? html`
`
: html`
`
}
`;
}
// ═══════════════════════════════════════════
// NotesApp — root component
// ═══════════════════════════════════════════
function NotesApp() {
var [notes, setNotes] = useState([]);
var [loading, setLoading] = useState(true);
var [activeId, setActiveId] = useState(null);
var [activeNote, setActiveNote] = useState(null);
var [searchTerm, setSearchTerm] = useState('');
var [stats, setStats] = useState(null);
// ── Load note list ────────────────────────
var loadNotes = useCallback(async function() {
try {
var res;
if (searchTerm.trim()) {
res = await api.get('/search?q=' + encodeURIComponent(searchTerm.trim()));
} else {
res = await api.get('/notes');
}
var items = (res && res.data) || res || [];
if (!Array.isArray(items)) items = [];
// sort: pinned first, then by updated_at desc
items.sort(function(a, b) {
if (a.pinned !== b.pinned) return b.pinned - a.pinned;
return (b.updated_at || b.created_at || '').localeCompare(a.updated_at || a.created_at || '');
});
setNotes(items);
} catch (e) {
console.error('Load notes failed:', e);
} finally {
setLoading(false);
}
}, [searchTerm]);
// ── Load single note (full body) ──────────
var loadNote = useCallback(async function(id) {
if (!id) { setActiveNote(null); return; }
try {
var note = await api.get('/notes/' + id);
setActiveNote(note);
} catch (e) {
console.error('Load note failed:', e);
setActiveNote(null);
}
}, []);
// ── Load stats ────────────────────────────
var loadStats = useCallback(async function() {
try {
var s = await api.get('/stats');
setStats(s);
} catch (e) { /* ignore */ }
}, []);
useEffect(function() { loadNotes(); loadStats(); }, [loadNotes]);
// select note
function handleSelect(id) {
setActiveId(id);
loadNote(id);
}
// ── Create note ───────────────────────────
async function handleNew() {
try {
var note = await api.post('/notes', { title: 'Untitled', body: '' });
if (note && note.id) {
setActiveId(note.id);
loadNote(note.id);
loadNotes();
loadStats();
}
} catch (e) {
console.error('Create failed:', e);
}
}
// ── Refresh after save ────────────────────
function handleRefresh() {
loadNotes();
if (activeId) loadNote(activeId);
loadStats();
}
// ── After delete ──────────────────────────
function handleDelete() {
setActiveId(null);
setActiveNote(null);
loadNotes();
loadStats();
}
// ── Search ────────────────────────────────
function handleSearch(e) {
var val = e.target.value;
setSearchTerm(val);
debounce('search', function() { loadNotes(); }, 300);
}
return html`
<${Topbar} title="Notes">
<${Button} variant="primary" size="sm" onClick=${handleNew}>+ New Note/>
/>
<${EditorPane} note=${activeNote}
onSave=${handleRefresh}
onDelete=${handleDelete}
onRefresh=${handleRefresh} />
`;
}
// ── Mount ──────────────────────────────────
render(html`<${NotesApp} />`, mount);
})();