diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..a93ac00 --- /dev/null +++ b/TODO.md @@ -0,0 +1,37 @@ +# UI Redesign — Design Handoff Implementation + +## Phase 1: Layout + Tokens + Header + Rail +- [ ] Update CSS tokens (add --a-str, switch mono font to JetBrains Mono) +- [ ] Fix grid dimensions (192px rail, 400px peek) +- [ ] Move capture bar from header to bottom of center panel +- [ ] Add search bar to header (centered, max-width 400px) +- [ ] Redesign tag rail: grid layout (arrow ▸ + dot + name + count) +- [ ] Add intent section (grab/read/fill) for cards view in rail + +## Phase 2: Stream + Cards Views +- [ ] Stream rows: promoted entries get card-style border/radius + card-type badge +- [ ] Card rows: rich single-line with title — preview — affordance badges — tag pills — pin — use count +- [ ] Affordance detection client-side (fill, steps, decide, link, code) +- [ ] Affordance badge components +- [ ] Cards sub-header (scope label + card count + sort dropdown) +- [ ] Section labels (★ pinned, recent) +- [ ] Flash animation on copy +- [ ] Bottom capture bar styling per view (different placeholders) + +## Phase 3: Peek Pane + Modes +- [ ] Idle state with keyboard shortcuts display +- [ ] Stream entry peek: eyebrow, body, tags, context, actions +- [ ] Card peek: card container with eyebrow, title, desc, meta, content sections +- [ ] Code block with syntax highlighting +- [ ] Decision section display +- [ ] Steps section display +- [ ] Link section display +- [ ] Run mode (interactive checklist with progress bar) +- [ ] Fill mode (inline slot editor with tab navigation) +- [ ] Edit mode (form fields) +- [ ] Toast notifications + +## Phase 4: Polish +- [ ] Promote modal enhancement (add hint text per type) +- [ ] Remaining keyboard shortcuts (r=run, f=fill) +- [ ] Scroll behavior and edge cases diff --git a/web/app.js b/web/app.js index 10c1ec6..4b3970f 100644 --- a/web/app.js +++ b/web/app.js @@ -16,6 +16,8 @@ const PAGE_SIZE = 50; + const INTENT_HINTS = { grab: 'scan + copy', read: 'expand + study', fill: 'templates only' }; + const state = { view: 'stream', entities: [], @@ -24,6 +26,7 @@ activeTag: null, hasMore: false, activeMonth: null, + intent: 'grab', }; const $ = (sel) => document.querySelector(sel); @@ -223,32 +226,128 @@ function formatDate(dateStr) { const d = new Date(dateStr); - const months = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']; + 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 allItem = `
- all -
`; + const total = state.tags.reduce((s, t) => s + t.count, 0); - rail.innerHTML = allItem + state.tags.map(t => - `
- ${t.tag} - ${t.count} -
` - ).join(''); + let html = `
nib
`; + html += '
'; - rail.querySelectorAll('.tag-item').forEach(el => { + 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(); + }); + }); } + // ── 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'); + input.addEventListener('keydown', (ev) => { + if (ev.key === 'Enter' && !ev.shiftKey) { + ev.preventDefault(); + handleCapture(); + } + }); + } + + async function handleCapture() { + const input = $('#capture-input'); + const val = input.value.trim(); + if (!val) return; + + const parsed = parseInput(val); + if (!parsed) 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; + + await api.createEntity(data); + input.value = ''; + await loadEntities(); + await loadTags(); + showToast('captured'); + } + + // ── Entity List ── + function groupByDate(entities) { const groups = []; let current = null; @@ -308,10 +407,12 @@ function renderEntityItem(e, idx) { const glyph = displayGlyph(e); const gc = glyphClass(e); - const selected = idx === state.selectedIndex ? 'selected' : ''; - const tags = (e.tags || []).map(t => `${t}`).join(''); + 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; if (e.title) { @@ -321,11 +422,11 @@ label = `${escHtml(e.body)}`; } - return `
+ return `
${glyph} ${label} ${time} - ${tags} + ${tags}${cardBadge} ${useBadge}
`; } @@ -351,10 +452,10 @@ if (e.card_type) { cardContent = renderCardContent(e); - actions += ``; + actions += ``; actions += ``; } else { - actions += ``; + actions += ``; actions += ``; } actions += ``; @@ -363,17 +464,19 @@ const titleHtml = e.title ? `

${escHtml(e.title)}

` : ''; pane.innerHTML = ` -
- ${glyph} - ${shortId} - ${e.time_anchor ? `@${e.time_anchor}` : ''} +
+
+ ${glyph} + ${shortId} + ${e.time_anchor ? `@${e.time_anchor}` : ''} +
+ ${descHtml} + ${titleHtml} +
${escHtml(e.body)}
+ ${tags ? `
${tags}
` : ''} + ${cardContent} +
${actions}
- ${descHtml} - ${titleHtml} -
${escHtml(e.body)}
- ${tags ? `
${tags}
` : ''} - ${cardContent} -
${actions}
`; const titleEl = pane.querySelector('.detail-title'); @@ -560,13 +663,10 @@ ${label} - ${state.activeMonth ? '' : ''} `; $('#month-prev').addEventListener('click', () => shiftMonth(-1)); $('#month-next').addEventListener('click', () => shiftMonth(1)); - const clearBtn = nav.querySelector('.month-nav-clear'); - if (clearBtn) clearBtn.addEventListener('click', () => { state.activeMonth = null; loadEntities(); renderMonthNav(); }); } function shiftMonth(dir) { @@ -590,10 +690,25 @@ 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' : '/'; loadEntities(); renderMonthNav(); + renderTagRail(); + renderCaptureBar(); + } + + // ========== 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) ========== @@ -606,6 +721,7 @@ await navigator.clipboard.writeText(e.body); await api.useEntity(id); await loadEntities(); + showToast('copied'); } catch (err) { console.error('clipboard:', err); } @@ -630,12 +746,14 @@ 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) { @@ -651,6 +769,7 @@ await navigator.clipboard.writeText(resolved); await api.useEntity(id); await loadEntities(); + showToast('copied'); } catch (err) { console.error('clipboard:', err); } @@ -688,6 +807,7 @@ await loadTags(); const idx = state.entities.findIndex(x => x.id === targetId); if (idx >= 0) selectEntity(idx); + showToast('absorbed'); }); }); @@ -706,33 +826,6 @@ }, }; - // ========== Capture bar ========== - - $('#capture-bar').addEventListener('submit', async (ev) => { - ev.preventDefault(); - const input = $('#capture-input'); - const val = input.value.trim(); - if (!val) return; - - const parsed = parseInput(val); - if (!parsed) 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; - - await api.createEntity(data); - input.value = ''; - await loadEntities(); - await loadTags(); - }); - // ========== Promote modal ========== $$('.type-btn').forEach(btn => { @@ -745,6 +838,7 @@ await api.promoteEntity(id, btn.dataset.type); await loadEntities(); await loadTags(); + showToast('promoted → ' + btn.dataset.type); }); }); @@ -761,12 +855,11 @@ // ========== Keyboard shortcuts ========== let lastDTime = 0; - const captureInput = $('#capture-input'); document.addEventListener('keydown', (ev) => { - if (document.activeElement === captureInput || - document.activeElement.classList.contains('detail-body-edit')) { - if (ev.key === 'Escape') document.activeElement.blur(); + const tag = (ev.target.tagName || '').toLowerCase(); + if (tag === 'input' || tag === 'textarea') { + if (ev.key === 'Escape') ev.target.blur(); return; } @@ -789,7 +882,7 @@ break; case 'n': ev.preventDefault(); - captureInput.focus(); + $('#capture-input').focus(); break; case 'p': { const e = state.entities[state.selectedIndex]; @@ -848,6 +941,9 @@ } $$('.nav-btn').forEach(b => b.classList.toggle('active', b.dataset.view === state.view)); loadEntities(); + renderMonthNav(); + renderTagRail(); + renderCaptureBar(); } window.addEventListener('hashchange', handleHash); @@ -884,6 +980,7 @@ // ========== Init ========== async function init() { + renderCaptureBar(); await Promise.all([loadEntities(), loadTags()]); handleHash(); renderMonthNav(); diff --git a/web/index.html b/web/index.html index 0c8e0fd..c9ede77 100644 --- a/web/index.html +++ b/web/index.html @@ -6,28 +6,22 @@ nib - + -
-

nib

+
-
- -
+
@@ -35,6 +29,7 @@
+