From dda84261135b748ed641bdd8a3a9118cbc643a5c Mon Sep 17 00:00:00 2001 From: Tyler Koenig Date: Sat, 16 May 2026 09:25:35 -0400 Subject: [PATCH 1/5] =?UTF-8?q?feat(ui):=20phase=201=20=E2=80=94=20layout,?= =?UTF-8?q?=20tokens,=20header,=20rail=20redesign?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Switch mono font from Monaspace Neon to JetBrains Mono - Grid layout 192px | 1fr | 400px (was 180/320) - Move capture bar from header to bottom of center panel - Add search input to header center - Redesign tag rail: grid items with arrow/dot/name/count - Add intent section (grab/read/fill) in cards view rail - Add --a-str token, toast component - Logo 16px 700 weight --- TODO.md | 37 +++++ web/app.js | 221 ++++++++++++++++++------- web/index.html | 17 +- web/style.css | 428 +++++++++++++++++++++++++++++++++---------------- 4 files changed, 495 insertions(+), 208 deletions(-) create mode 100644 TODO.md 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 @@
+
`; } + 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 = '
select an entity
'; + pane.innerHTML = renderPeekIdle(); pane.classList.remove('visible'); return; } pane.classList.add('visible'); + + if (state.view === 'stream' || !e.card_type) { + pane.innerHTML = renderStreamPeek(e); + } else if (state.peekMode === 'run') { + pane.innerHTML = renderRunMode(e); + } else if (state.peekMode === 'fill') { + pane.innerHTML = renderFillMode(e); + } else if (state.peekMode === 'edit') { + pane.innerHTML = renderEditMode(e); + } else { + pane.innerHTML = 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(''); - const shortId = e.id.slice(0, 12); - let cardContent = ''; let actions = ''; - - if (e.card_type) { - cardContent = renderCardContent(e); - actions += ``; - actions += ``; - } else { + if (!e.card_type) { actions += ``; - actions += ``; } actions += ``; - const descHtml = e.description ? `
${escHtml(e.description)}
` : ''; - const titleHtml = e.title ? `

${escHtml(e.title)}

` : ''; - - pane.innerHTML = ` -
-
- ${glyph} - ${shortId} - ${e.time_anchor ? `@${e.time_anchor}` : ''} -
- ${descHtml} - ${titleHtml} -
${escHtml(e.body)}
- ${tags ? `
${tags}
` : ''} - ${cardContent} -
${actions}
+ return `
+
+ ${glyph} + ${kindLbl} + · + ${e.id.slice(-10)} + ${fmtDateLong(e.created_at)}
- `; - - const titleEl = pane.querySelector('.detail-title'); - if (titleEl) titleEl.addEventListener('dblclick', () => startEditField('title')); - const descEl = pane.querySelector('.detail-desc'); - if (descEl) descEl.addEventListener('dblclick', () => startEditField('description')); - const bodyEl = pane.querySelector('.detail-body'); - if (bodyEl) bodyEl.addEventListener('dblclick', startEditBody); + ${e.title ? `
${escHtml(e.title)}
` : ''} +
${escHtml(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 renderCardContent(e) { - if (!e.card_data) return ''; - let data; - try { data = JSON.parse(e.card_data); } catch { return ''; } + 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; - switch (e.card_type) { - case 'template': - if (!data.slots || !data.slots.length) return ''; - return `
- ${data.slots.map(s => ` -
- \${${s.name}} - -
- `).join('')} - -
`; + let sections = ''; - case 'checklist': - if (!data.steps || !data.steps.length) return ''; - return `
- ${data.steps.map((s, i) => ` -
- - ${escHtml(s.text)} -
- `).join('')} -
`; - - case 'decision': - return `
-
chose
${escHtml(data.chose || '—')}
-
why
${escHtml(data.why || '—')}
- ${data.rejected && data.rejected.length ? `
rejected
${data.rejected.map(escHtml).join(', ') || '—'}
` : ''} -
`; - - case 'link': - if (data.url && isSafeUrl(data.url)) { - return `
- -
`; - } - return ''; - - default: - return ''; + 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 || ''; + sections += `
+
content${lang ? `${lang}` : ''}${hasFill ? `` : ''}
+
${escHtml(e.body)}
+
`; + } + + 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 ========== @@ -713,6 +956,10 @@ function selectEntity(idx) { state.selectedIndex = idx; + state.peekMode = 'preview'; + state.runChecked = new Set(); + state.fillValues = {}; + state.fillActive = 0; renderEntityList(); renderDetailPane(); } @@ -932,6 +1179,67 @@ 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'); + }, }; // ========== Promote modal ========== @@ -977,6 +1285,13 @@ return; } + if (state.peekMode !== 'preview' && ev.key === 'Escape') { + nibApp.exitMode(); + return; + } + + const sel = state.entities[state.selectedIndex]; + switch (ev.key) { case 'j': ev.preventDefault(); @@ -992,36 +1307,42 @@ ev.preventDefault(); $('#capture-input').focus(); break; - case 'p': { - const e = state.entities[state.selectedIndex]; - if (e && !e.card_type) nibApp.showPromote(e.id); + 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': { - const e = state.entities[state.selectedIndex]; - if (e) nibApp.copyEntity(e.id); + 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 && sel.card_type && state.view === 'cards') { + nibApp.enterMode('edit'); + } else { + startEditBody(); + } break; - } case 'd': { const now = Date.now(); if (now - lastDTime < 400) { - const e = state.entities[state.selectedIndex]; - if (e) nibApp.deleteEntity(e.id); + if (sel) nibApp.deleteEntity(sel.id); lastDTime = 0; } else { lastDTime = now; } break; } - case 'e': { - startEditBody(); + case 'a': + if (sel && !sel.card_type) nibApp.showAbsorb(sel.id); break; - } - case 'a': { - const e = state.entities[state.selectedIndex]; - if (e && !e.card_type) nibApp.showAbsorb(e.id); - break; - } case '1': switchView('stream'); break; case '2': switchView('cards'); break; } diff --git a/web/style.css b/web/style.css index 9f37905..21a35be 100644 --- a/web/style.css +++ b/web/style.css @@ -660,7 +660,7 @@ main { letter-spacing: .04em; } -/* ── DETAIL PANE ────────────────────────────────────── */ +/* ── PEEK PANE ──────────────────────────────────────── */ #detail-pane { background: var(--surf); border-left: 1px solid var(--border); @@ -669,10 +669,11 @@ main { overflow: hidden; } -.detail-scroll { +.peek-scroll { flex: 1; overflow-y: auto; - padding: 20px; + display: flex; + flex-direction: column; } .detail-empty { @@ -683,49 +684,283 @@ main { font-family: var(--mono); } -.detail-header { +/* peek idle */ +.peek-idle { + padding: 18px; display: flex; - align-items: center; - gap: 8px; - margin-bottom: 16px; + flex-direction: column; + gap: 14px; + flex: 1; + overflow-y: auto; } -.detail-glyph { font-size: 16px; } - -.detail-id { +.peek-idle-eyebrow { font-family: var(--mono); - font-size: 10px; - color: var(--dim); + font-size: 9px; + text-transform: uppercase; + letter-spacing: .16em; + color: var(--accent); + margin-bottom: 6px; } -.detail-desc { - font-family: var(--sans); - font-size: 11px; - color: var(--muted); - margin-bottom: 4px; - cursor: text; - padding: 2px 6px; - margin-left: -6px; - border-radius: var(--r2); - transition: background var(--t-fast); -} - -.detail-desc:hover { background: var(--raised); } - -.detail-title { +.peek-idle-title { font-family: var(--sans); font-size: 15px; font-weight: 600; - margin-bottom: 12px; + color: var(--text); + margin-bottom: 4px; +} + +.peek-idle-sub { + font-family: var(--sans); + font-size: 12px; + color: var(--muted); + line-height: 1.55; +} + +.peek-shortcuts { display: flex; flex-direction: column; gap: 10px; } +.peek-sc-sec { margin-bottom: 2px; } +.peek-sc-lbl { font-family: var(--mono); font-size: 9px; text-transform: uppercase; letter-spacing: .14em; color: var(--dim); margin-bottom: 5px; } +.peek-sc-row { display: flex; align-items: center; gap: 5px; padding: 2px 0; font-family: var(--mono); font-size: 11px; color: var(--muted); } +.peek-sc-row span { color: var(--dim); margin-left: 2px; } +.peek-sc-code { font-family: var(--mono); font-size: 10px; color: var(--accent); background: var(--bg); border: 1px solid var(--border); border-radius: var(--r2); padding: 4px 8px; margin-bottom: 5px; } +.peek-sc-hint { font-family: var(--mono); font-size: 9px; color: var(--dim); padding-bottom: 3px; } +kbd { background: var(--raised); border: 1px solid var(--border); border-radius: 2px; padding: 1px 4px; font-size: 9px; font-family: var(--mono); color: var(--muted); display: inline-block; line-height: 1.4; } + +/* peek eyebrow */ +.peek-brow { + padding: 14px 20px 0; + display: flex; + align-items: center; + gap: 7px; + flex-shrink: 0; + font-family: var(--mono); + font-size: 9px; + letter-spacing: .12em; + text-transform: uppercase; + color: var(--dim); +} + +.peek-brow-g { font-size: 13px; margin-right: 1px; flex-shrink: 0; } +.peek-brow-kind { color: var(--muted); } +.peek-brow-sep { color: var(--dim); opacity: .4; } +.peek-brow-id { color: var(--dim); } +.peek-brow-ts { margin-left: auto; color: var(--dim); letter-spacing: 0; text-transform: none; white-space: nowrap; } + +/* peek title / desc / body */ +.peek-title { + padding: 9px 20px 4px; + font-family: var(--sans); + font-size: 15px; + font-weight: 600; + color: var(--text); + line-height: 1.3; + flex-shrink: 0; +} + +.peek-desc { + padding: 0 20px 10px; + font-family: var(--sans); + font-size: 12px; + color: var(--muted); + line-height: 1.55; + flex-shrink: 0; +} + +.peek-body { + padding: 10px 20px 14px; + font-family: var(--mono); + font-size: 13px; + line-height: 1.72; + color: var(--text); + white-space: pre-wrap; + word-break: break-word; + flex-shrink: 0; cursor: text; - padding: 2px 6px; - margin-left: -6px; border-radius: var(--r2); transition: background var(--t-fast); } -.detail-title:hover { background: var(--raised); } +.peek-body:hover { background: var(--raised); } +.peek-meta { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 5px; + padding: 0 20px 12px; + flex-shrink: 0; +} + +.peek-pin { color: var(--accent); font-size: 11px; } + +/* peek sections */ +.peek-sec { border-top: 1px solid var(--soft); flex-shrink: 0; } +.peek-sec-lbl { + padding: 8px 20px 5px; + font-family: var(--mono); + font-size: 9px; + text-transform: uppercase; + letter-spacing: .16em; + color: var(--dim); + display: flex; + align-items: center; + gap: 6px; +} + +.peek-sec-lang { color: var(--accent); letter-spacing: 0; text-transform: none; font-size: 9px; } +.peek-sec-status { + color: var(--ok); + letter-spacing: 0; + text-transform: none; + font-size: 9px; + border: 1px solid rgba(122,171,114,.4); + background: rgba(122,171,114,.06); + padding: 0 6px; + border-radius: var(--r1); +} + +.peek-sec-run { + margin-left: auto; + font-family: var(--mono); + font-size: 9px; + color: var(--ok); + border: 1px solid rgba(122,171,114,.4); + padding: 1px 8px; + border-radius: var(--r1); + transition: background var(--t-fast); +} + +.peek-sec-run:hover { background: rgba(122,171,114,.1); } + +.peek-sec-inner { padding: 0 20px 14px; } +.tag-pills { display: flex; flex-wrap: wrap; gap: 5px; } + +/* peek context */ +.peek-ctx { display: flex; flex-direction: column; gap: 5px; font-family: var(--mono); font-size: 11px; color: var(--muted); } +.peek-ctx-lbl { font-size: 9px; text-transform: uppercase; letter-spacing: .1em; color: var(--dim); margin-right: 5px; } +.peek-ctx-promoted { color: var(--ok); } + +/* peek card container */ +.peek-card { + margin: 12px; + border: 1px solid var(--border); + border-radius: var(--r3); + overflow: hidden; + flex-shrink: 0; +} + +.peek-card-head { + background: var(--bg); + border-bottom: 1px solid var(--soft); + padding-bottom: 0; +} + +.peek-card .peek-sec { border-top-color: var(--border); } +.peek-card .peek-sec-inner { padding: 0 16px 14px; } +.peek-card .peek-sec-lbl { padding: 8px 16px 5px; } + +/* peek code block */ +.peek-code { + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--r2); + padding: 10px 12px; + overflow-x: auto; +} + +.peek-code pre { + font-family: var(--mono); + font-size: 11px; + line-height: 1.65; + color: var(--text); + white-space: pre-wrap; + word-break: break-word; +} + +/* peek steps */ +.peek-steps { display: flex; flex-direction: column; gap: 3px; } +.peek-step { display: flex; align-items: flex-start; gap: 8px; padding: 3px 0; font-family: var(--mono); font-size: 11px; line-height: 1.45; } +.peek-step-mark { flex-shrink: 0; margin-top: 1px; } +.peek-step-text { color: var(--text); } + +/* peek decision */ +.peek-decision { padding: 0; } +.peek-dec-choice { font-family: var(--sans); font-size: 15px; font-weight: 600; color: var(--text); margin-bottom: 6px; } +.peek-dec-choice::before { content: '▸ '; color: var(--accent); } +.peek-dec-why { font-family: var(--sans); font-size: 12px; color: var(--muted); line-height: 1.55; margin-bottom: 8px; } +.peek-dec-key { color: var(--dim); font-size: 9px; text-transform: uppercase; letter-spacing: .1em; font-family: var(--mono); margin-right: 5px; } +.peek-dec-rejected { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; } +.peek-dec-rej { font-family: var(--mono); font-size: 10px; color: var(--muted); border: 1px solid var(--border); padding: 1px 6px; border-radius: var(--r1); text-decoration: line-through; opacity: .6; } + +/* peek link */ +.peek-link-url { + display: flex; + align-items: flex-start; + gap: 6px; + font-family: var(--mono); + font-size: 11px; + color: var(--event); + border: 1px solid var(--soft); + padding: 8px 10px; + border-radius: var(--r2); + background: var(--bg); + word-break: break-all; + line-height: 1.5; +} + +/* peek actions */ +.peek-acts { + display: flex; + gap: 5px; + flex-wrap: wrap; + padding: 12px 20px 18px; + border-top: 1px solid var(--soft); + flex-shrink: 0; +} + +/* peek mode pills */ +.peek-run-pill { font-family: var(--mono); font-size: 9px; color: var(--ok); border: 1px solid rgba(122,171,114,.4); background: rgba(122,171,114,.06); padding: 1px 7px; border-radius: var(--r1); } +.peek-fill-pill { font-family: var(--mono); font-size: 9px; color: var(--lineage); border: 1px solid rgba(152,120,188,.4); background: rgba(152,120,188,.06); padding: 1px 7px; border-radius: var(--r1); } +.peek-edit-pill { font-family: var(--mono); font-size: 9px; color: var(--todo); border: 1px solid rgba(212,168,75,.4); background: rgba(212,168,75,.06); padding: 1px 7px; border-radius: var(--r1); } + +/* run mode */ +.peek-run-prog-wrap { display: flex; align-items: center; gap: 10px; padding: 0 20px 14px; flex-shrink: 0; } +.peek-run-prog-track { flex: 1; height: 3px; background: var(--border); border-radius: 2px; overflow: hidden; } +.peek-run-prog { height: 100%; background: var(--ok); border-radius: 2px; transition: width var(--t-base); } +.peek-run-pct { font-family: var(--mono); font-size: 10px; color: var(--ok); min-width: 28px; } +.peek-run-steps { flex-shrink: 0; } +.peek-run-step { display: flex; align-items: flex-start; gap: 10px; padding: 7px 20px; cursor: pointer; border-left: 2px solid transparent; transition: background var(--t-fast), border-left-color var(--t-fast); } +.peek-run-step:hover { background: var(--raised); } +.peek-run-step.done .peek-run-text { text-decoration: line-through; color: var(--dim); } +.peek-run-mark { font-family: var(--mono); font-size: 12px; flex-shrink: 0; margin-top: 1px; } +.peek-run-text { font-family: var(--sans); font-size: 12px; line-height: 1.5; color: var(--text); } + +/* fill mode */ +.peek-fill-canvas { padding: 14px 20px; flex-shrink: 0; } +.peek-fill-canvas code { font-family: var(--mono); font-size: 12px; line-height: 2; color: var(--text); white-space: pre-wrap; word-break: break-word; } + +.fill-slot { display: inline-block; border-bottom: 1.5px solid var(--lineage); } +.fill-slot.active { border-color: var(--accent); border-bottom-width: 2px; } +.fill-slot.filled { border-color: var(--ok); } +.fill-slot input { background: transparent; border: none; outline: none; color: var(--lineage); font-family: var(--mono); font-size: 12px; padding: 0 2px; min-width: 30px; line-height: 2; } +.fill-slot.active input { color: var(--text); } +.fill-slot.filled input { color: var(--ok); } + +/* edit mode */ +.peek-edit-fields { padding: 12px 20px; display: flex; flex-direction: column; gap: 12px; flex-shrink: 0; } +.peek-edit-field { display: flex; flex-direction: column; gap: 4px; } +.peek-edit-lbl { font-family: var(--mono); font-size: 9px; text-transform: uppercase; letter-spacing: .14em; color: var(--dim); } +.peek-edit-in { background: var(--bg); border: 1px solid var(--border); border-radius: var(--r2); padding: 6px 9px; font-family: var(--mono); font-size: 12px; color: var(--text); outline: none; transition: border-color var(--t-fast); } +.peek-edit-in:focus { border-color: var(--accent); } +.peek-edit-ta { background: var(--bg); border: 1px solid var(--border); border-radius: var(--r2); padding: 6px 9px; font-family: var(--mono); font-size: 12px; color: var(--text); outline: none; resize: vertical; min-height: 100px; line-height: 1.55; transition: border-color var(--t-fast); } +.peek-edit-ta:focus { border-color: var(--accent); } + +/* hints bar */ +.peek-hints { display: flex; gap: 12px; padding: 10px 20px; font-family: var(--mono); font-size: 9px; color: var(--dim); border-top: 1px solid var(--soft); flex-shrink: 0; } +.peek-hints span { display: flex; align-items: center; gap: 3px; } + +/* legacy detail support (inline edit) */ .detail-field-edit { display: block; width: 100%; @@ -740,22 +975,6 @@ main { outline: none; } -.detail-body { - font-family: var(--mono); - font-size: 13px; - line-height: 1.72; - margin-bottom: 16px; - white-space: pre-wrap; - word-break: break-word; - cursor: text; - border-radius: var(--r2); - padding: 4px 6px; - margin-left: -6px; - transition: background var(--t-fast); -} - -.detail-body:hover { background: var(--raised); } - .detail-body-edit { display: block; width: 100%; @@ -777,9 +996,8 @@ main { .detail-tags { display: flex; - gap: 6px; + gap: 5px; flex-wrap: wrap; - margin-bottom: 16px; } .detail-tag { @@ -792,14 +1010,6 @@ main { border-radius: var(--r1); } -.detail-actions { - display: flex; - gap: 5px; - flex-wrap: wrap; - border-top: 1px solid var(--soft); - padding-top: 12px; -} - .action-btn { font-family: var(--sans); font-size: 11px; @@ -816,8 +1026,10 @@ main { .action-btn:hover { color: var(--accent); border-color: var(--accent); } .action-btn.primary { color: var(--accent); border-color: var(--accent); background: var(--a-bg); } +.action-btn.dim { opacity: .45; } .action-btn.danger { color: var(--danger); border-color: rgba(184,88,88,.4); } .action-btn.danger:hover { border-color: var(--danger); } +.action-btn kbd { font-size: 9px; background: rgba(0,0,0,.2); border: 1px solid rgba(0,0,0,.3); border-radius: 2px; padding: 0 3px; opacity: .65; } /* ── TEMPLATE SLOTS ─────────────────────────────────── */ .slot-form { margin: 16px 0; } -- 2.52.0 From f26716a9eecf9e90912647a91b9e0a73a8f6c8e9 Mon Sep 17 00:00:00 2001 From: Tyler Koenig Date: Sat, 16 May 2026 09:37:32 -0400 Subject: [PATCH 4/5] =?UTF-8?q?feat(ui):=20phase=204=20=E2=80=94=20promote?= =?UTF-8?q?=20modal=20polish,=20TODO=20complete?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Promote modal: colored glyphs, type names, hint descriptions per type - Show truncated entry body in promote modal subtitle - Mark all redesign phases complete in TODO.md --- TODO.md | 64 +++++++++++++++++++++++++------------------------- web/app.js | 4 ++++ web/index.html | 26 ++++++++++++-------- web/style.css | 1 + 4 files changed, 53 insertions(+), 42 deletions(-) diff --git a/TODO.md b/TODO.md index a93ac00..e532696 100644 --- a/TODO.md +++ b/TODO.md @@ -1,37 +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 1: Layout + Tokens + Header + Rail ✓ +- [x] Update CSS tokens (add --a-str, switch mono font to JetBrains Mono) +- [x] Fix grid dimensions (192px rail, 400px peek) +- [x] Move capture bar from header to bottom of center panel +- [x] Add search bar to header (centered, max-width 400px) +- [x] Redesign tag rail: grid layout (arrow ▸ + dot + name + count) +- [x] 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 2: Stream + Cards Views ✓ +- [x] Stream rows: promoted entries get card-style border/radius + card-type badge +- [x] Card rows: rich single-line with title — preview — affordance badges — tag pills — pin — use count +- [x] Affordance detection client-side (fill, steps, decide, link, code) +- [x] Affordance badge components +- [x] Cards sub-header (scope label + card count + sort dropdown) +- [x] Section labels (★ pinned, recent) +- [x] Flash animation on copy +- [x] 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 3: Peek Pane + Modes ✓ +- [x] Idle state with keyboard shortcuts display +- [x] Stream entry peek: eyebrow, body, tags, context, actions +- [x] Card peek: card container with eyebrow, title, desc, meta, content sections +- [x] Code block with content display +- [x] Decision section display +- [x] Steps section display +- [x] Link section display +- [x] Run mode (interactive checklist with progress bar) +- [x] Fill mode (inline slot editor with tab navigation) +- [x] Edit mode (form fields) +- [x] 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 +## Phase 4: Polish ✓ +- [x] Promote modal enhancement (add hint text per type, show entry body preview) +- [x] Keyboard shortcuts (r=run, f=fill, p=pin in cards view) +- [x] Escape exits active modes diff --git a/web/app.js b/web/app.js index 79b8032..1e5fe57 100644 --- a/web/app.js +++ b/web/app.js @@ -1091,6 +1091,10 @@ 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); diff --git a/web/index.html b/web/index.html index c9ede77..42aed17 100644 --- a/web/index.html +++ b/web/index.html @@ -41,26 +41,32 @@