(function () { 'use strict'; const GLYPHS = { note: '—', todo: '○', event: '◇', reminder: '△', snippet: '◆', template: '◈', checklist: '☐', decision: '⚖', link: '↗', }; const GLYPH_CLASSES = { note: 'glyph-note', todo: 'glyph-todo', event: 'glyph-event', reminder: 'glyph-reminder', snippet: 'glyph-snippet', template: 'glyph-template', checklist: 'glyph-checklist', decision: 'glyph-decision', link: 'glyph-link', }; const PAGE_SIZE = 50; const INTENT_HINTS = { grab: 'scan + copy', read: 'expand + study', fill: 'templates only' }; const state = { view: 'stream', entities: [], tags: [], selectedIndex: -1, activeTag: null, hasMore: false, activeMonth: null, intent: 'grab', flashId: null, peekMode: 'preview', runChecked: new Set(), fillValues: {}, fillActive: 0, searchQuery: '', cardsSort: 'newest', }; const $ = (sel) => document.querySelector(sel); const $$ = (sel) => document.querySelectorAll(sel); // ========== API ========== const api = { async listEntities(params = {}) { const q = new URLSearchParams(); if (params.tag) q.set('tag', params.tag); if (params.date) q.set('date', params.date); if (params.from) q.set('from', params.from); if (params.to) q.set('to', params.to); if (params.cards_only) q.set('cards_only', 'true'); if (params.sort) q.set('sort', params.sort); if (params.order) q.set('order', params.order); if (params.limit) q.set('limit', String(params.limit)); if (params.offset) q.set('offset', String(params.offset)); const resp = await fetch('/api/entities?' + q); return resp.json(); }, async createEntity(data) { const resp = await fetch('/api/entities', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); return resp.json(); }, async getEntity(id) { const resp = await fetch('/api/entities/' + id); return resp.json(); }, async updateEntity(id, data) { const resp = await fetch('/api/entities/' + id, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); return resp.json(); }, async deleteEntity(id) { return fetch('/api/entities/' + id, { method: 'DELETE' }); }, async promoteEntity(id, cardType, cardData) { const resp = await fetch('/api/entities/' + id + '/promote', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ card_type: cardType, card_data: cardData }), }); return resp.json(); }, async demoteEntity(id) { const resp = await fetch('/api/entities/' + id + '/demote', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}), }); return resp.json(); }, async useEntity(id) { const resp = await fetch('/api/entities/' + id + '/use', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}), }); return resp.json(); }, async absorbEntity(targetId, sourceId) { const resp = await fetch('/api/entities/' + targetId + '/absorb', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ source_id: sourceId }), }); return resp.json(); }, async listTags(params = {}) { const q = new URLSearchParams(); if (params.cards_only) q.set('cards_only', 'true'); const resp = await fetch('/api/tags?' + q); return resp.json(); }, }; // ========== Grammar parser (mirrors Go parser) ========== const VALID_CARDS = { card: 'snippet', c: 'snippet', snippet: 'snippet', template: 'template', checklist: 'checklist', decision: 'decision', link: 'link' }; function validateTime(s) { const parts = s.split(':'); if (parts.length !== 2) return false; const h = parseInt(parts[0], 10), m = parseInt(parts[1], 10); return !isNaN(h) && !isNaN(m) && h >= 0 && h <= 23 && m >= 0 && m <= 59; } function parseInput(input) { input = input.trim(); if (!input) return null; let glyph = 'note'; let remaining = input; let timeAnchor = null, cardSuffix = null, pin = false, query = false; const tags = [], seenTags = {}, filterTags = []; // Step 1: Escape check — `\` prefix → thought, skip prefix detection if (remaining.startsWith('\\')) { remaining = remaining.slice(1); const result = extractModifiers(remaining, true); if (!result.body) return null; return { body: result.body, glyph: 'note', title: null, description: null, timeAnchor: result.timeAnchor, tags: result.tags, cardSuffix: result.cardSuffix, pin: result.pin, query: false, filterTags: [] }; } // Step 2: Query check — `?` prefix → search mode if (remaining.startsWith('?')) { remaining = remaining.slice(1).trim(); const tokens = remaining.split(/\s+/).filter(Boolean); const bodyParts = []; for (const tok of tokens) { if (tok.startsWith('#') && tok.length > 1 && !tok.startsWith('##')) { filterTags.push(tok.slice(1).toLowerCase()); } else { bodyParts.push(tok); } } return { body: bodyParts.join(' '), glyph: '', title: null, description: null, timeAnchor: null, tags: [], cardSuffix: null, pin: false, query: true, filterTags }; } // Step 3: Kind prefix — `-`, `@time`, `!time` if (remaining.startsWith('- ')) { glyph = 'todo'; remaining = remaining.slice(2).trim(); } else if (remaining === '-') { glyph = 'todo'; remaining = ''; } else if (remaining.startsWith('@')) { const afterAt = remaining.slice(1).trim(); const sp = afterAt.indexOf(' '); const timeTok = sp >= 0 ? afterAt.slice(0, sp) : afterAt; if (validateTime(timeTok)) { glyph = 'event'; timeAnchor = timeTok; remaining = sp >= 0 ? afterAt.slice(sp + 1).trim() : ''; } } else if (remaining.startsWith('!')) { const afterBang = remaining.slice(1).trim(); const firstWord = afterBang.split(/\s+/)[0] || ''; if (firstWord.toLowerCase() !== 'pin') { const sp = afterBang.indexOf(' '); const timeTok = sp >= 0 ? afterBang.slice(0, sp) : afterBang; if (validateTime(timeTok)) { glyph = 'reminder'; timeAnchor = timeTok; remaining = sp >= 0 ? afterBang.slice(sp + 1).trim() : ''; } } } // Steps 4-5: Title and description extraction let titleRaw = null, descRaw = null, hasTitle = false; const lines = remaining.split('\n'); const firstLine = (lines[0] || '').trim(); if (firstLine.startsWith('|')) { hasTitle = true; const titleContent = firstLine.slice(1); const descIdx = titleContent.indexOf(' // '); if (descIdx >= 0) { titleRaw = titleContent.slice(0, descIdx).trim(); descRaw = titleContent.slice(descIdx + 4).trim(); } else { titleRaw = titleContent.trim(); } remaining = lines.slice(1).join('\n'); } else { let descParts = [], startBody = 0; for (let i = 0; i < lines.length; i++) { const trimmed = lines[i].trim(); if (trimmed.startsWith('// ') || trimmed === '//') { descParts.push(trimmed.slice(2).trim()); startBody = i + 1; } else { break; } } if (descParts.length) { descRaw = descParts.join(' '); remaining = lines.slice(startBody).join('\n'); } else if (!firstLine.includes('://')) { const dIdx = firstLine.indexOf(' // '); if (dIdx >= 0) { descRaw = firstLine.slice(dIdx + 4).trim(); remaining = firstLine.slice(0, dIdx).trim(); if (lines.length > 1) remaining += '\n' + lines.slice(1).join('\n'); } } } // Steps 6-8: Extract flags, tags, time, card suffix function extractModifiers(text, handleFlags) { let localTime = timeAnchor, localPin = pin, localCard = cardSuffix; const localTags = [...tags]; const localSeen = { ...seenTags }; const outLines = []; for (const line of text.split('\n')) { const tokens = line.split(/[ \t]+/).filter(Boolean); const lineParts = []; for (const tok of tokens) { if (handleFlags && tok.toLowerCase() === '!pin') { localPin = true; } else if (tok.startsWith('##') && tok.length > 2) { lineParts.push('#' + tok.slice(2)); } else if (tok.startsWith('@') && tok.length > 1) { const ts = tok.slice(1); if (validateTime(ts) && localTime === null) { localTime = ts; } else { lineParts.push(tok); } } else if (tok.startsWith('#') && tok.length > 1) { const tag = tok.slice(1).toLowerCase(); if (!localSeen[tag]) { localTags.push(tag); localSeen[tag] = true; } } else if (tok.startsWith('^') && tok.length > 1) { const suffix = tok.slice(1); if (VALID_CARDS[suffix] && localCard === null) localCard = VALID_CARDS[suffix]; else lineParts.push(tok); } else { lineParts.push(tok); } } outLines.push(lineParts.join(' ')); } return { body: outLines.join('\n'), timeAnchor: localTime, tags: localTags, seen: localSeen, cardSuffix: localCard, pin: localPin }; } let title = null, description = null; if (hasTitle) { const r = extractModifiers(titleRaw || '', false); if (r.body) title = r.body; timeAnchor = r.timeAnchor; Object.assign(seenTags, r.seen); tags.length = 0; tags.push(...r.tags); cardSuffix = r.cardSuffix; pin = r.pin; } if (descRaw) { const r = extractModifiers(descRaw, false); if (r.body) description = r.body; timeAnchor = r.timeAnchor; Object.assign(seenTags, r.seen); tags.length = 0; tags.push(...r.tags); cardSuffix = r.cardSuffix; pin = r.pin; } const bodyResult = extractModifiers(remaining, true); const body = bodyResult.body; timeAnchor = bodyResult.timeAnchor; tags.length = 0; tags.push(...bodyResult.tags); cardSuffix = bodyResult.cardSuffix; pin = bodyResult.pin; if (!body && !title) return null; return { body, glyph, title, description, timeAnchor, tags, cardSuffix, pin, query: false, filterTags: [] }; } function detectCardType(body) { if (/\$\{.+\}/.test(body)) return 'template'; if (/\bchose:|why:/.test(body)) return 'decision'; if (/\[ \]|\d+\./.test(body)) return 'checklist'; if (/https?:\/\//.test(body)) return 'link'; return null; } function detectAffordances(entity) { const affs = []; const body = entity.body || ''; const data = entity.card_data ? (() => { try { return JSON.parse(entity.card_data); } catch { return {}; } })() : {}; if (data.lang || entity.card_type === 'snippet') affs.push('code'); if (/\$\{[^}]+\}/.test(body)) affs.push('fill'); if (data.steps && data.steps.length) affs.push('steps'); if (data.chose != null || entity.card_type === 'decision') affs.push('decide'); if (data.url || entity.card_type === 'link') affs.push('link'); return affs; } const AFF_LABELS = { code: 'code', fill: 'tpl', steps: 'steps', decide: 'dec', link: 'link' }; const AFF_CLASSES = { code: 'aff-code', fill: 'aff-fill', steps: 'aff-steps', decide: 'aff-decide', link: 'aff-link' }; function cardPreview(entity) { const data = entity.card_data ? (() => { try { return JSON.parse(entity.card_data); } catch { return {}; } })() : {}; if (data.chose) return `▸ ${escHtml(data.chose)}`; if (data.steps && data.steps.length) { const done = data.steps.filter(s => s.done).length; const total = data.steps.length; const pct = total > 0 ? Math.round(done / total * 100) : 0; return ` ${done}/${total} steps`; } if (/\$\{[^}]+\}/.test(entity.body || '')) { const slots = []; const re = /\$\{([^}]+)\}/g; let m; while ((m = re.exec(entity.body)) && slots.length < 2) slots.push(m[1]); return slots.map(s => `\${${escHtml(s)}}`).join(' '); } if (data.url) return `${escHtml(data.url.replace(/^https?:\/\//, ''))}`; const first = (entity.body || '').split('\n')[0] || ''; return escHtml(first.slice(0, 60)); } // ========== Rendering ========== function displayGlyph(entity) { return entity.card_type ? GLYPHS[entity.card_type] : GLYPHS[entity.glyph]; } function glyphClass(entity) { return entity.card_type ? GLYPH_CLASSES[entity.card_type] : GLYPH_CLASSES[entity.glyph]; } function formatDate(dateStr) { const d = new Date(dateStr); const months = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC']; return months[d.getMonth()] + ' ' + d.getDate(); } // ── Tag Rail ── function renderTagRail() { const rail = $('#tag-rail'); const total = state.tags.reduce((s, t) => s + t.count, 0); let html = `
nib
`; html += '
'; if (state.view === 'cards') { html += '
'; html += '
intent
'; for (const k of ['grab', 'read', 'fill']) { const on = state.intent === k ? ' on' : ''; const count = k === 'grab' ? state.entities.length : k === 'read' ? state.entities.filter(e => e.card_data).length : state.entities.filter(e => e.body && /\$\{.+\}/.test(e.body)).length; html += `'; } html += '
'; } html += '
'; html += '
tags
'; const allOn = !state.activeTag ? ' on' : ''; html += `'; for (const t of state.tags) { const on = state.activeTag === t.tag ? ' on' : ''; html += `'; } html += '
'; rail.innerHTML = html; rail.querySelectorAll('.rail-item[data-tag]').forEach(el => { el.addEventListener('click', () => { state.activeTag = el.dataset.tag || null; loadEntities(); renderTagRail(); }); }); rail.querySelectorAll('.rail-item[data-intent]').forEach(el => { el.addEventListener('click', () => { state.intent = el.dataset.intent; renderTagRail(); renderEntityList(); }); }); } // ── Capture Bar ── function renderCaptureBar() { const bar = $('#capture-bar'); const placeholder = state.view === 'stream' ? 'capture · - todo @time event !time reminder #tag |title' : '|title // desc #tag ${slot} 1. step'; bar.innerHTML = `
⏎ save
`; const input = $('#capture-input'); function autoResize() { input.style.height = 'auto'; input.style.height = input.scrollHeight + 'px'; } input.addEventListener('keydown', (ev) => { if (ev.key === 'Enter' && !ev.shiftKey) { ev.preventDefault(); handleCapture(); } }); input.addEventListener('input', () => { autoResize(); updateCapturePreview(input.value); }); } function updateCapturePreview(val) { const el = $('#cap-preview'); if (!el) return; val = val.trim(); if (!val) { el.innerHTML = ''; el.classList.remove('visible'); return; } const parsed = parseInput(val); if (!parsed) { el.innerHTML = ''; el.classList.remove('visible'); return; } const pills = []; if (parsed.query) { pills.push('search'); } else { pills.push(`${escHtml(parsed.glyph)}`); } if (parsed.title) pills.push(`|${escHtml(parsed.title)}`); if (parsed.description) pills.push(`${escHtml(parsed.description)}`); for (const t of (parsed.query ? parsed.filterTags : parsed.tags)) { pills.push(`#${escHtml(t)}`); } if (parsed.timeAnchor) pills.push(`@${escHtml(parsed.timeAnchor)}`); if (parsed.pin) pills.push('pin'); if (parsed.cardSuffix) pills.push(`^${escHtml(parsed.cardSuffix)}`); el.innerHTML = pills.join(''); el.classList.add('visible'); } async function handleCapture() { const input = $('#capture-input'); const val = input.value.trim(); if (!val) return; const parsed = parseInput(val); if (!parsed) return; // Query mode → switch to search if (parsed.query) { state.searchQuery = parsed.body; const searchInput = $('#search-input'); if (searchInput) searchInput.value = parsed.body + (parsed.filterTags.length ? ' ' + parsed.filterTags.map(t => '#' + t).join(' ') : ''); input.value = ''; input.style.height = 'auto'; updateCapturePreview(''); renderEntityList(); return; } const data = { body: parsed.body, glyph: parsed.glyph, tags: parsed.tags, }; if (parsed.title) data.title = parsed.title; if (parsed.description) data.description = parsed.description; if (parsed.timeAnchor) data.time_anchor = parsed.timeAnchor; if (parsed.cardSuffix) data.card_type = parsed.cardSuffix; if (parsed.pin) data.pinned = true; await api.createEntity(data); input.value = ''; input.style.height = 'auto'; updateCapturePreview(''); await loadEntities(); await loadTags(); showToast('captured'); } // ── Entity List ── function groupByDate(entities) { const groups = []; let current = null; for (const e of entities) { const label = formatDate(e.created_at); if (!current || current.label !== label) { if (current) groups.push(current); current = { label, entities: [] }; } current.entities.push(e); } if (current) groups.push(current); return groups; } function renderEntityList() { const list = $('#entity-list'); const filtered = filterBySearch(state.entities); if (filtered.length === 0) { list.innerHTML = `
${state.searchQuery ? 'no matches' : 'no entities yet'}
`; renderCardsHeader(state.view === 'cards'); return; } let html = ''; if (state.view === 'stream') { renderCardsHeader(false); const groups = groupByDate(filtered); let idx = 0; for (const g of groups) { html += `
${g.label}
`; for (const e of g.entities) { const realIdx = state.entities.indexOf(e); html += renderEntityItem(e, realIdx); idx++; } } } else { renderCardsHeader(true); const pinned = filtered.filter(e => e.pinned); const rest = filtered.filter(e => !e.pinned); if (pinned.length) { html += '
★ pinned
'; for (const e of pinned) { html += renderCardRow(e, state.entities.indexOf(e)); } } if (rest.length) { if (pinned.length) html += '
recent
'; for (const e of rest) { html += renderCardRow(e, state.entities.indexOf(e)); } } } if (state.hasMore) { html += '
'; } list.innerHTML = html; list.querySelectorAll('.entity-item').forEach(el => { el.addEventListener('click', () => { selectEntity(parseInt(el.dataset.index)); }); }); list.querySelectorAll('.card-row').forEach(el => { el.addEventListener('click', (ev) => { if (!ev.target.closest('.aff')) { selectEntity(parseInt(el.dataset.index)); } }); }); const loadMoreBtn = list.querySelector('.load-more-btn'); if (loadMoreBtn) loadMoreBtn.addEventListener('click', loadMore); } function renderCardsHeader(show) { let hdr = $('#cards-hdr'); if (!show) { if (hdr) hdr.remove(); return; } if (!hdr) { hdr = document.createElement('div'); hdr.id = 'cards-hdr'; hdr.className = 'cards-hdr'; const panel = $('#entity-panel'); const list = $('#entity-list'); panel.insertBefore(hdr, list); } const scope = state.activeTag ? `${state.intent} · #${state.activeTag}` : state.intent; const sorts = ['newest', 'oldest', 'most used']; const options = sorts.map(s => `${s}`).join(''); hdr.innerHTML = ` ${scope} ${state.entities.length} cards `; hdr.querySelector('.cards-sort').addEventListener('change', (ev) => { state.cardsSort = ev.target.value; loadEntities(); }); } function renderCardRow(e, idx) { const selected = idx === state.selectedIndex ? ' selected' : ''; const pinCls = e.pinned ? ' pinned' : ''; const flashCls = state.flashId === e.id ? ' flashing' : ''; const title = e.title || (e.body || '').split('\n')[0].slice(0, 50); const affs = detectAffordances(e); const preview = cardPreview(e); const tags = (e.tags || []).slice(0, 2).map(t => `#${t}`).join(''); const affHtml = affs.map(a => `${AFF_LABELS[a]}`).join(''); return `
${escHtml(title)} ${preview}
${affHtml} ${tags} ${e.pinned ? '' : ''} ${e.use_count > 0 ? `${e.use_count}×` : ''}
`; } function renderEntityItem(e, idx) { const glyph = displayGlyph(e); const gc = glyphClass(e); const selected = idx === state.selectedIndex ? ' selected' : ''; const isCard = e.card_type ? ' is-card' : ''; const tags = (e.tags || []).slice(0, 2).map(t => `${t}`).join(''); const time = e.time_anchor ? `@${e.time_anchor}` : ''; const useBadge = e.use_count > 0 ? `${e.use_count}×` : ''; const cardBadge = e.card_type ? `${e.card_type}` : ''; let label; const descSnip = e.description ? `${escHtml(e.description)}` : ''; if (e.title) { const preview = e.body ? `${escHtml(e.body)}` : ''; label = `${escHtml(e.title)}${descSnip}${preview}`; } else { label = `${escHtml(e.body)}${descSnip}`; } return `
${glyph} ${label} ${time} ${tags}${cardBadge} ${useBadge}
`; } function fmtDateLong(dateStr) { const d = new Date(dateStr); const months = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']; return `${months[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()} · ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`; } function renderDetailPane() { const pane = $('#detail-pane'); const e = state.entities[state.selectedIndex]; if (!e) { pane.innerHTML = renderPeekIdle(); pane.classList.remove('visible'); return; } pane.classList.add('visible'); const mobileBar = `
`; if (state.peekMode === 'edit') { pane.innerHTML = mobileBar + renderEditMode(e); } else if (state.view === 'stream' || !e.card_type) { pane.innerHTML = mobileBar + renderStreamPeek(e); } else if (state.peekMode === 'run') { pane.innerHTML = mobileBar + renderRunMode(e); } else if (state.peekMode === 'fill') { pane.innerHTML = mobileBar + renderFillMode(e); } else { pane.innerHTML = mobileBar + renderCardPeek(e); } bindPeekEvents(e); } function renderPeekIdle() { const v = state.view; return `
peek
Select ${v === 'cards' ? 'a card' : 'an entry'}.
${v === 'cards' ? 'Full detail lives here. Run checklists, fill templates, edit in place.' : 'Entry detail lives here. Promote any capture to a card when it earns a permanent home.'}
navigate
jknext / prev
12stream / cards
${v === 'stream' ? `
stream grammar
(bare text) = thought
- todo · @time event · !time reminder
#tag · |title · // desc · !pin
` : `
act
copy
rrun checklist
ffill template
eedit
ppin
`}
`; } function renderStreamPeek(e) { const kind = e.card_type || e.glyph; const glyph = displayGlyph(e); const gc = glyphClass(e); const kindLbl = { note: 'thought', todo: 'todo', event: 'event', snippet: 'snippet', template: 'template', checklist: 'checklist', decision: 'decision', link: 'link' }[kind] || kind; const tags = (e.tags || []).map(t => `#${t}`).join(''); let actions = ''; actions += ``; if (!e.card_type) { actions += ``; actions += ``; } if (e.card_type) { actions += ``; } else { actions += ``; } return `
${glyph} ${kindLbl} · ${e.id.slice(-10)} ${fmtDateLong(e.created_at)}
${e.title ? `
${escHtml(e.title)}
` : ''}
${renderMd(e.body)}
${tags ? `
tags
${tags}
` : ''}
context
created${fmtDateLong(e.created_at)} ${e.time_anchor ? `time@${e.time_anchor}` : ''} ${e.card_type ? `statuspromoted → ${e.card_type}` : ''}
${actions}
`; } function renderCardPeek(e) { const glyph = GLYPHS[e.card_type] || '◆'; const gc = GLYPH_CLASSES[e.card_type] || 'glyph-snippet'; const affs = detectAffordances(e); const data = e.card_data ? (() => { try { return JSON.parse(e.card_data); } catch { return {}; } })() : {}; const tags = (e.tags || []).map(t => `#${t}`).join(''); const affHtml = affs.map(a => `${AFF_LABELS[a]}`).join(''); const hasSteps = data.steps && data.steps.length; const hasDecision = data.chose != null; const hasFill = /\$\{[^}]+\}/.test(e.body || ''); const hasLink = !!data.url; let sections = ''; if (hasDecision) { const rejected = (data.rejected || []).map(r => `${escHtml(r)}`).join(''); sections += `
decision${data.status || 'decided'}
${escHtml(data.chose)}
why${escHtml(data.why || '')}
${rejected ? `
considered
${rejected}
` : ''}
`; } if (hasLink && !hasDecision) { sections += `
link
`; } if (hasSteps) { const steps = data.steps.map((s, i) => `
${escHtml(s.text || s)}
`).join(''); sections += `
steps · ${data.steps.length}
${steps}
`; } if (!hasDecision && e.body) { const lang = data.lang || ''; const isCode = lang || e.card_type === 'snippet'; const bodyHtml = isCode ? `
${escHtml(e.body)}
` : `
${renderMd(e.body)}
`; sections += `
content${lang ? `${lang}` : ''}${hasFill ? `` : ''}
${bodyHtml}
`; } let actions = ``; if (hasFill) actions += ``; if (hasSteps) actions += ``; actions += ``; actions += ``; actions += ``; return `
${glyph} ${e.card_type} · ${e.id.slice(-10)} ${e.use_count > 0 ? `${e.use_count}× used` : ''}
${escHtml(e.title || '')}
${e.description ? `
${escHtml(e.description)}
` : ''}
${affHtml}${tags}${e.pinned ? '' : ''}
${sections}
${actions}
`; } function renderRunMode(e) { const data = e.card_data ? (() => { try { return JSON.parse(e.card_data); } catch { return {}; } })() : {}; if (!data.steps) return renderCardPeek(e); const total = data.steps.length; const checked = state.runChecked || new Set(); const done = checked.size; const pct = total > 0 ? Math.round(done / total * 100) : 0; const steps = data.steps.map((s, i) => { const isDone = checked.has(i); const text = s.text || s; return `
${isDone ? '●' : '○'} ${escHtml(text)}
`; }).join(''); return `
▶ running ${done}/${total} done
${escHtml(e.title || '')}
${e.description ? `
${escHtml(e.description)}
` : ''}
${pct}%
${steps}
Space toggler resetEsc exit
`; } function renderFillMode(e) { const slots = []; const re = /\$\{([^}]+)\}/g; let m; const seen = new Set(); while ((m = re.exec(e.body || '')) !== null) { const name = m[1].trim(); if (!seen.has(name)) { seen.add(name); slots.push(name); } } if (!slots.length) return renderCardPeek(e); const fill = state.fillValues || {}; const active = state.fillActive || 0; let content = escHtml(e.body); for (const name of slots) { const val = fill[name] || ''; const idx = slots.indexOf(name); const cls = idx === active ? 'fill-slot active' : (val ? 'fill-slot filled' : 'fill-slot'); const width = Math.max(name.length, val.length, 4) * 8 + 16; content = content.replace(`\${${name}}`, ``); } const allFilled = slots.every(s => fill[s]); return `
⤓ filling slot ${active + 1} / ${slots.length}
${escHtml(e.title || '')}
${e.description ? `
${escHtml(e.description)}
` : ''}
${content}
Tab next⇧Tab prev copyEsc cancel
`; } function renderEditMode(e) { return `
✎ editing
${escHtml(e.title || 'untitled')}
⌘⏎ saveEsc cancel
`; } function bindPeekEvents(e) { const pane = $('#detail-pane'); if (!e) return; if (state.peekMode === 'run') { pane.querySelectorAll('.peek-run-step').forEach(el => { el.addEventListener('click', () => { const idx = parseInt(el.dataset.step); if (!state.runChecked) state.runChecked = new Set(); if (state.runChecked.has(idx)) state.runChecked.delete(idx); else state.runChecked.add(idx); renderDetailPane(); }); }); } if (state.peekMode === 'fill') { pane.querySelectorAll('.fill-slot input').forEach(input => { input.addEventListener('input', () => { if (!state.fillValues) state.fillValues = {}; state.fillValues[input.dataset.slot] = input.value; }); input.addEventListener('focus', () => { state.fillActive = parseInt(input.dataset.idx); }); input.addEventListener('keydown', (ev) => { if (ev.key === 'Tab') { ev.preventDefault(); const slots = pane.querySelectorAll('.fill-slot input'); const cur = parseInt(input.dataset.idx); const next = ev.shiftKey ? Math.max(0, cur - 1) : Math.min(slots.length - 1, cur + 1); state.fillActive = next; renderDetailPane(); setTimeout(() => { const el = pane.querySelector(`.fill-slot input[data-idx="${next}"]`); if (el) el.focus(); }, 0); } else if (ev.key === 'Enter' && !ev.shiftKey) { ev.preventDefault(); nibApp.completeFill(); } else if (ev.key === 'Escape') { ev.preventDefault(); nibApp.exitMode(); } }); }); setTimeout(() => { const el = pane.querySelector(`.fill-slot input[data-idx="${state.fillActive || 0}"]`); if (el) el.focus(); }, 0); } if (state.peekMode === 'edit') { const bodyTa = pane.querySelector('#edit-body'); if (bodyTa) { bodyTa.addEventListener('keydown', (ev) => { if (ev.key === 'Enter' && (ev.metaKey || ev.ctrlKey)) { ev.preventDefault(); nibApp.saveEdit(e.id); } if (ev.key === 'Escape') { ev.preventDefault(); nibApp.exitMode(); } }); } } // Double-click to edit (stream peek) const titleEl = pane.querySelector('.peek-title[data-id]'); if (titleEl) titleEl.addEventListener('dblclick', () => startEditField('title')); const bodyEl = pane.querySelector('.peek-body[data-id]'); if (bodyEl) bodyEl.addEventListener('dblclick', startEditBody); } // ========== Inline edit ========== function startEditBody() { const e = state.entities[state.selectedIndex]; if (!e) return; const el = $(`.peek-body[data-id="${e.id}"]`); if (!el || el.tagName === 'TEXTAREA') return; const ta = document.createElement('textarea'); ta.className = 'detail-body-edit'; ta.value = e.body; el.replaceWith(ta); ta.focus(); ta.setSelectionRange(ta.value.length, ta.value.length); async function save() { const newBody = ta.value.trim(); if (newBody && newBody !== e.body) { await api.updateEntity(e.id, { body: newBody }); await loadEntities(); const idx = state.entities.findIndex(x => x.id === e.id); if (idx >= 0) selectEntity(idx); } else { renderDetailPane(); } } ta.addEventListener('blur', save); ta.addEventListener('keydown', (ev) => { if (ev.key === 'Enter' && ev.ctrlKey) { ev.preventDefault(); ta.removeEventListener('blur', save); save(); } if (ev.key === 'Escape') { ev.preventDefault(); ta.removeEventListener('blur', save); renderDetailPane(); } }); } function startEditField(field) { const e = state.entities[state.selectedIndex]; if (!e) return; const el = $(`.peek-title[data-id="${e.id}"]`); if (!el || el.tagName === 'INPUT') return; const input = document.createElement('input'); input.type = 'text'; input.className = 'detail-field-edit'; input.value = e[field] || ''; input.placeholder = field; el.replaceWith(input); input.focus(); async function save() { const val = input.value.trim(); if (val !== (e[field] || '')) { await api.updateEntity(e.id, { [field]: val || null }); await loadEntities(); const idx = state.entities.findIndex(x => x.id === e.id); if (idx >= 0) selectEntity(idx); } else { renderDetailPane(); } } input.addEventListener('blur', save); input.addEventListener('keydown', (ev) => { if (ev.key === 'Enter') { ev.preventDefault(); input.removeEventListener('blur', save); save(); } if (ev.key === 'Escape') { ev.preventDefault(); input.removeEventListener('blur', save); renderDetailPane(); } }); } // ========== Actions ========== function selectEntity(idx) { state.selectedIndex = idx; state.peekMode = 'preview'; state.runChecked = new Set(); state.fillValues = {}; state.fillActive = 0; renderEntityList(); renderDetailPane(); } function buildListParams(offset) { const params = { limit: PAGE_SIZE, offset: offset || 0 }; if (state.activeTag) params.tag = state.activeTag; if (state.view === 'cards') { params.cards_only = true; if (state.cardsSort === 'oldest') { params.sort = 'created'; params.order = 'asc'; } else if (state.cardsSort === 'most used') { params.sort = 'use_count'; params.order = 'desc'; } else { params.sort = 'created'; params.order = 'desc'; } } else { params.sort = 'created'; params.order = 'desc'; } if (state.activeMonth) { const [y, m] = state.activeMonth.split('-').map(Number); params.from = state.activeMonth + '-01'; const last = new Date(y, m, 0).getDate(); params.to = state.activeMonth + '-' + String(last).padStart(2, '0'); } return params; } async function loadEntities() { const params = buildListParams(0); const results = await api.listEntities(params); state.entities = results; state.hasMore = results.length === PAGE_SIZE; state.selectedIndex = -1; renderEntityList(); renderDetailPane(); renderTagRail(); } async function loadMore() { const params = buildListParams(state.entities.length); const results = await api.listEntities(params); state.entities = state.entities.concat(results); state.hasMore = results.length === PAGE_SIZE; renderEntityList(); } function renderMonthNav() { const nav = $('#month-nav'); if (state.view !== 'stream') { nav.innerHTML = ''; nav.style.display = 'none'; return; } nav.style.display = ''; const MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']; const label = state.activeMonth ? (() => { const [y, m] = state.activeMonth.split('-'); return MONTHS[parseInt(m) - 1] + ' ' + y; })() : 'all time'; nav.innerHTML = ` ${label} `; $('#month-prev').addEventListener('click', () => shiftMonth(-1)); $('#month-next').addEventListener('click', () => shiftMonth(1)); } function shiftMonth(dir) { if (!state.activeMonth) { const now = new Date(); state.activeMonth = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0'); } else { const [y, m] = state.activeMonth.split('-').map(Number); const d = new Date(y, m - 1 + dir, 1); state.activeMonth = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0'); } loadEntities(); renderMonthNav(); } async function loadTags() { const params = {}; if (state.view === 'cards') params.cards_only = true; state.tags = await api.listTags(params); renderTagRail(); } function switchView(view) { state.view = view; state.activeMonth = null; state.selectedIndex = -1; $$('.nav-btn').forEach(b => b.classList.toggle('active', b.dataset.view === view)); window.location.hash = view === 'cards' ? '/cards' : '/'; renderMonthNav(); renderCaptureBar(); loadEntities(); loadTags(); } // ========== Toast ========== function showToast(msg) { let el = $('.toast'); if (el) el.remove(); el = document.createElement('div'); el.className = 'toast'; el.textContent = msg; document.body.appendChild(el); setTimeout(() => el.remove(), 1600); } // ========== Public API (for inline handlers) ========== window.nibApp = { async copyEntity(id) { const e = state.entities.find(x => x.id === id); if (!e) return; try { await navigator.clipboard.writeText(e.body); await api.useEntity(id); state.flashId = id; await loadEntities(); showToast('copied'); setTimeout(() => { state.flashId = null; renderEntityList(); }, 360); } catch (err) { console.error('clipboard:', err); } }, showPromote(id) { const e = state.entities.find(x => x.id === id); if (!e || e.card_type) return; const modal = $('#promote-modal'); modal.classList.remove('hidden'); modal.classList.add('visible'); modal.dataset.entityId = id; const sub = $('#promote-sub'); const label = (e.body || '').slice(0, 64) + ((e.body || '').length > 64 ? '…' : ''); sub.textContent = label; const suggested = detectCardType(e.body); $$('.type-btn').forEach(btn => { btn.classList.toggle('suggested', btn.dataset.type === suggested); }); }, async demoteEntity(id) { await api.demoteEntity(id); await loadEntities(); await loadTags(); showToast('demoted'); }, async deleteEntity(id) { await api.deleteEntity(id); await loadEntities(); await loadTags(); showToast('deleted'); }, async resolveTemplate(id) { const e = state.entities.find(x => x.id === id); if (!e) return; let resolved = e.body; $$('.slot-input').forEach(input => { const name = input.dataset.slot; const val = input.value || input.placeholder; resolved = resolved.replace(new RegExp('\\$\\{' + name + '\\}', 'g'), val); }); try { await navigator.clipboard.writeText(resolved); await api.useEntity(id); await loadEntities(); showToast('copied'); } catch (err) { console.error('clipboard:', err); } }, showAbsorb(targetId) { const target = state.entities.find(x => x.id === targetId); if (!target) return; if (target.card_type) return; const modal = $('#absorb-modal'); modal.dataset.targetId = targetId; const list = $('#absorb-source-list'); const sources = state.entities.filter(x => x.id !== targetId); if (!sources.length) { list.innerHTML = '
no other entities
'; } else { list.innerHTML = sources.map(e => { const g = displayGlyph(e); const gc = glyphClass(e); const rawLabel = e.title || (e.body || '').split('\n').find(l => l.trim()) || ''; const label = escHtml(rawLabel.length > 80 ? rawLabel.slice(0, 80) + '…' : rawLabel); return `
${g} ${label}
`; }).join(''); } list.querySelectorAll('.absorb-source-item').forEach(el => { el.addEventListener('click', async () => { modal.classList.add('hidden'); modal.classList.remove('visible'); await api.absorbEntity(targetId, el.dataset.id); await loadEntities(); await loadTags(); const idx = state.entities.findIndex(x => x.id === targetId); if (idx >= 0) selectEntity(idx); showToast('absorbed'); }); }); modal.classList.remove('hidden'); modal.classList.add('visible'); }, async toggleStep(id, stepIdx) { const e = state.entities.find(x => x.id === id); if (!e || !e.card_data) return; const data = JSON.parse(e.card_data); data.steps[stepIdx].done = !data.steps[stepIdx].done; await api.updateEntity(id, { card_data: JSON.stringify(data) }); await loadEntities(); selectEntity(state.entities.findIndex(x => x.id === id)); }, enterMode(mode) { state.peekMode = mode; if (mode === 'run') state.runChecked = new Set(); if (mode === 'fill') { state.fillValues = {}; state.fillActive = 0; } renderDetailPane(); }, exitMode() { state.peekMode = 'preview'; renderDetailPane(); }, resetRun() { state.runChecked = new Set(); renderDetailPane(); }, async completeFill() { const e = state.entities[state.selectedIndex]; if (!e) return; let resolved = e.body || ''; const fill = state.fillValues || {}; for (const [name, val] of Object.entries(fill)) { resolved = resolved.replace(new RegExp('\\$\\{' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\}', 'g'), val); } try { await navigator.clipboard.writeText(resolved); await api.useEntity(e.id); state.peekMode = 'preview'; await loadEntities(); showToast('copied resolved'); } catch (err) { console.error('clipboard:', err); } }, async saveEdit(id) { const title = ($('#edit-title') || {}).value || null; const desc = ($('#edit-desc') || {}).value || null; const body = ($('#edit-body') || {}).value || ''; const tagsStr = ($('#edit-tags') || {}).value || ''; const tags = tagsStr.split(/\s+/).filter(Boolean); await api.updateEntity(id, { body, title, description: desc, tags }); state.peekMode = 'preview'; await loadEntities(); await loadTags(); const idx = state.entities.findIndex(x => x.id === id); if (idx >= 0) selectEntity(idx); showToast('saved'); }, async togglePin(id) { const e = state.entities.find(x => x.id === id); if (!e) return; await api.updateEntity(id, { pinned: !e.pinned }); await loadEntities(); const idx = state.entities.findIndex(x => x.id === id); if (idx >= 0) { state.selectedIndex = idx; renderEntityList(); renderDetailPane(); } showToast(e.pinned ? 'unpinned' : 'pinned'); }, togglePeekFull() { const pane = $('#detail-pane'); pane.classList.toggle('peek-full'); const btn = pane.querySelector('.peek-mobile-btn'); if (btn) btn.textContent = pane.classList.contains('peek-full') ? '↓' : '↑'; }, dismissPeek() { const pane = $('#detail-pane'); pane.classList.remove('visible', 'peek-full'); state.selectedIndex = -1; renderEntityList(); }, }; // ========== Promote modal ========== $$('.type-btn').forEach(btn => { btn.addEventListener('click', async () => { const modal = $('#promote-modal'); const id = modal.dataset.entityId; modal.classList.add('hidden'); modal.classList.remove('visible'); await api.promoteEntity(id, btn.dataset.type); await loadEntities(); await loadTags(); showToast('promoted → ' + btn.dataset.type); }); }); $$('.modal-backdrop').forEach(el => el.addEventListener('click', closeModal)); $$('.modal-close').forEach(el => el.addEventListener('click', closeModal)); function closeModal() { $$('.modal.visible').forEach(m => { m.classList.add('hidden'); m.classList.remove('visible'); }); } // ========== Keyboard shortcuts ========== let lastDTime = 0; document.addEventListener('keydown', (ev) => { const tag = (ev.target.tagName || '').toLowerCase(); if (tag === 'input' || tag === 'textarea') { if (ev.key === 'Escape') ev.target.blur(); return; } if ($('#promote-modal').classList.contains('visible') || $('#absorb-modal').classList.contains('visible')) { if (ev.key === 'Escape') closeModal(); return; } if (state.peekMode !== 'preview' && ev.key === 'Escape') { nibApp.exitMode(); return; } if (ev.key === 'Escape') { const pane = $('#detail-pane'); if ($('main').classList.contains('focus-peek')) { exitFocusPeek(); } if (pane.classList.contains('visible')) { pane.classList.remove('visible', 'peek-full'); state.selectedIndex = -1; renderEntityList(); renderDetailPane(); return; } } const sel = state.entities[state.selectedIndex]; switch (ev.key) { case 'j': { ev.preventDefault(); const visible = filterBySearch(state.entities); const sel = state.entities[state.selectedIndex]; const curPos = sel ? visible.indexOf(sel) : -1; const nextPos = Math.min(curPos + 1, visible.length - 1); if (visible.length > 0 && nextPos >= 0) { selectEntity(state.entities.indexOf(visible[nextPos])); } scrollSelectedIntoView(); break; } case 'k': { ev.preventDefault(); const visible = filterBySearch(state.entities); const sel = state.entities[state.selectedIndex]; const curPos = sel ? visible.indexOf(sel) : -1; const prevPos = Math.max(curPos - 1, 0); if (visible.length > 0) { selectEntity(state.entities.indexOf(visible[prevPos])); } scrollSelectedIntoView(); break; } case 'n': ev.preventDefault(); $('#capture-input').focus(); break; case 'p': if (sel && sel.card_type && state.view === 'cards') { nibApp.togglePin(sel.id); } else if (sel && !sel.card_type) { nibApp.showPromote(sel.id); } break; case 'Enter': if (sel) nibApp.copyEntity(sel.id); break; case 'r': if (sel && sel.card_type && state.view === 'cards') nibApp.enterMode('run'); break; case 'f': if (sel && sel.card_type && state.view === 'cards') nibApp.enterMode('fill'); break; case 'e': if (sel) nibApp.enterMode('edit'); break; case 'd': { const now = Date.now(); if (now - lastDTime < 400) { if (sel) { if (sel.card_type) nibApp.demoteEntity(sel.id); else nibApp.deleteEntity(sel.id); } lastDTime = 0; } else { lastDTime = now; } break; } case 'a': if (sel && !sel.card_type) nibApp.showAbsorb(sel.id); break; case '1': switchView('stream'); break; case '2': switchView('cards'); break; case 'z': toggleZen(); break; case '[': togglePanel('rail'); break; case ']': togglePanel('peek'); break; } }); function togglePanel(panel) { const m = $('main'); const cls = panel === 'rail' ? 'hide-rail' : 'hide-peek'; m.classList.toggle(cls); localStorage.setItem('nib:' + cls, m.classList.contains(cls) ? '1' : ''); } function isMobileBreakpoint() { return window.matchMedia('(max-width: 900px)').matches; } function toggleZen() { if (isMobileBreakpoint()) { if (state.selectedIndex >= 0) nibApp.togglePeekFull(); return; } const m = $('main'); if (m.classList.contains('focus-peek')) { exitFocusPeek(); return; } if (state.selectedIndex >= 0) { m.classList.add('focus-peek'); return; } const isZen = m.classList.contains('hide-rail') && m.classList.contains('hide-peek'); if (isZen) { m.classList.remove('hide-rail', 'hide-peek'); localStorage.setItem('nib:hide-rail', ''); localStorage.setItem('nib:hide-peek', ''); } else { m.classList.add('hide-rail', 'hide-peek'); localStorage.setItem('nib:hide-rail', '1'); localStorage.setItem('nib:hide-peek', '1'); } } function exitFocusPeek() { $('main').classList.remove('focus-peek'); } (function restorePanels() { const m = $('main'); if (localStorage.getItem('nib:hide-rail')) m.classList.add('hide-rail'); if (localStorage.getItem('nib:hide-peek')) m.classList.add('hide-peek'); const railW = localStorage.getItem('nib:rail-w'); const peekW = localStorage.getItem('nib:peek-w'); if (railW) m.style.setProperty('--rail-w', railW + 'px'); if (peekW) m.style.setProperty('--peek-w', peekW + 'px'); })(); // ========== Resize handles ========== $$('.resize-handle').forEach(handle => { let startX, startW, panel; handle.addEventListener('mousedown', (ev) => { ev.preventDefault(); panel = handle.dataset.panel; startX = ev.clientX; const m = $('main'); m.classList.add('resizing'); handle.classList.add('active'); if (panel === 'rail') { startW = $('#tag-rail').offsetWidth; } else { startW = $('#detail-pane').offsetWidth; } document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); }); function onMove(ev) { const m = $('main'); const dx = ev.clientX - startX; let newW; if (panel === 'rail') { newW = Math.max(120, Math.min(360, startW + dx)); m.style.setProperty('--rail-w', newW + 'px'); } else { newW = Math.max(250, Math.min(700, startW - dx)); m.style.setProperty('--peek-w', newW + 'px'); } } function onUp() { const m = $('main'); m.classList.remove('resizing'); handle.classList.remove('active'); document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); if (panel === 'rail') { localStorage.setItem('nib:rail-w', $('#tag-rail').offsetWidth); } else { localStorage.setItem('nib:peek-w', $('#detail-pane').offsetWidth); } } }); function scrollSelectedIntoView() { const el = $(`.entity-item[data-index="${state.selectedIndex}"], .card-row[data-index="${state.selectedIndex}"]`); if (el) el.scrollIntoView({ block: 'nearest' }); } // ========== View nav buttons ========== $$('.nav-btn').forEach(btn => { btn.addEventListener('click', () => switchView(btn.dataset.view)); }); // ========== Hash routing ========== function handleHash() { const hash = window.location.hash; if (hash === '#/cards') { state.view = 'cards'; } else { state.view = 'stream'; } $$('.nav-btn').forEach(b => b.classList.toggle('active', b.dataset.view === state.view)); loadEntities(); renderMonthNav(); renderTagRail(); renderCaptureBar(); } window.addEventListener('hashchange', handleHash); // ========== Search ========== const searchInput = $('#search-input'); let searchDebounce = null; searchInput.addEventListener('input', () => { clearTimeout(searchDebounce); searchDebounce = setTimeout(() => { state.searchQuery = searchInput.value.trim().toLowerCase(); renderEntityList(); }, 150); }); searchInput.addEventListener('keydown', (ev) => { if (ev.key === 'Escape') { searchInput.value = ''; state.searchQuery = ''; renderEntityList(); searchInput.blur(); } }); function filterByIntent(entities) { if (state.view !== 'cards' || state.intent === 'grab') return entities; if (state.intent === 'read') return entities.filter(e => e.card_data); if (state.intent === 'fill') return entities.filter(e => e.body && /\$\{.+\}/.test(e.body)); return entities; } function filterBySearch(entities) { const intentFiltered = filterByIntent(entities); if (!state.searchQuery) return intentFiltered; let query = state.searchQuery; let filterTags = []; query = query.replace(/#(\S+)/g, (_, tag) => { filterTags.push(tag); return ''; }).trim(); return intentFiltered.filter(e => { if (filterTags.length) { const eTags = (e.tags || []).map(t => t.toLowerCase()); if (!filterTags.every(ft => eTags.includes(ft))) return false; } if (!query) return true; const haystack = ((e.body || '') + ' ' + (e.title || '') + ' ' + (e.description || '')).toLowerCase(); return haystack.includes(query); }); } // ========== Utils ========== function escHtml(s) { if (!s) return ''; return s.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } function escAttr(s) { return escHtml(s).replace(/'/g, '''); } function renderMd(s) { if (!s) return ''; if (typeof marked === 'undefined') return escHtml(s); return marked.parse(s, { breaks: true }); } function isSafeUrl(url) { return /^https?:\/\//i.test(url); } // ========== Theme ========== const THEMES = ['dark', 'paper', 'tinycard']; const THEME_ICONS = { dark: '◑', paper: '◐', tinycard: '◈' }; const themeToggle = $('#theme-toggle'); let nibTheme = localStorage.getItem('nib:theme') || 'dark'; if (!THEMES.includes(nibTheme)) nibTheme = 'dark'; document.documentElement.setAttribute('data-theme', nibTheme); themeToggle.textContent = THEME_ICONS[nibTheme]; themeToggle.addEventListener('click', () => { nibTheme = THEMES[(THEMES.indexOf(nibTheme) + 1) % THEMES.length]; document.documentElement.setAttribute('data-theme', nibTheme); localStorage.setItem('nib:theme', nibTheme); themeToggle.textContent = THEME_ICONS[nibTheme]; }); // ========== Init ========== async function init() { renderCaptureBar(); await Promise.all([loadEntities(), loadTags()]); handleHash(); renderMonthNav(); } init(); })();