feat(ui): phase 3 — peek pane redesign with modes

- Full peek pane rewrite: idle state, stream peek, card peek
- Idle state shows keyboard shortcuts per view
- Stream peek: eyebrow (glyph + kind + id + timestamp), body, tags, context
- Card peek: card container with eyebrow, title, desc, meta, content sections
- Decision section with choice/rationale/rejected display
- Steps section with run button
- Code section with content display
- Run mode: interactive checklist with progress bar + step toggling
- Fill mode: inline slot editor with tab navigation + copy resolved
- Edit mode: form fields for title/desc/body/tags
- Mode pills (running/filling/editing) with colored badges
- Pin/unpin action via keyboard (p) and button
- Escape exits any active mode
- Keyboard shortcuts: r=run, f=fill, e=edit, p=pin in cards view
This commit is contained in:
2026-05-16 09:35:06 -04:00
parent 156ea6ea1c
commit 1c95902e2b
2 changed files with 686 additions and 153 deletions
+417 -96
View File
@@ -28,6 +28,10 @@
activeMonth: null,
intent: 'grab',
flashId: null,
peekMode: 'preview',
runChecked: new Set(),
fillValues: {},
fillActive: 0,
};
const $ = (sel) => document.querySelector(sel);
@@ -536,109 +540,348 @@
</div>`;
}
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 = '<div class="detail-empty">select an entity</div>';
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 `<div class="peek-idle">
<div class="peek-idle-eyebrow">peek</div>
<div class="peek-idle-title">Select ${v === 'cards' ? 'a card' : 'an entry'}.</div>
<div class="peek-idle-sub">${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.'}</div>
<div class="peek-shortcuts">
<div class="peek-sc-sec">
<div class="peek-sc-lbl">navigate</div>
<div class="peek-sc-row"><kbd>j</kbd><kbd>k</kbd><span>next / prev</span></div>
<div class="peek-sc-row"><kbd>1</kbd><kbd>2</kbd><span>stream / cards</span></div>
</div>
${v === 'stream' ? `<div class="peek-sc-sec">
<div class="peek-sc-lbl">stream grammar</div>
<div class="peek-sc-code">(bare text) = thought</div>
<div class="peek-sc-hint">- todo · @time event · !time reminder</div>
<div class="peek-sc-hint">#tag · |title · // desc · !pin</div>
</div>` : `<div class="peek-sc-sec">
<div class="peek-sc-lbl">act</div>
<div class="peek-sc-row"><kbd>⏎</kbd><span>copy</span></div>
<div class="peek-sc-row"><kbd>r</kbd><span>run checklist</span></div>
<div class="peek-sc-row"><kbd>f</kbd><span>fill template</span></div>
<div class="peek-sc-row"><kbd>e</kbd><span>edit</span></div>
<div class="peek-sc-row"><kbd>p</kbd><span>pin</span></div>
</div>`}
</div>
</div>`;
}
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 => `<span class="detail-tag">#${t}</span>`).join('');
const shortId = e.id.slice(0, 12);
let cardContent = '';
let actions = '';
if (e.card_type) {
cardContent = renderCardContent(e);
actions += `<button class="action-btn primary" onclick="nibApp.copyEntity('${e.id}')">copy</button>`;
actions += `<button class="action-btn" onclick="nibApp.demoteEntity('${e.id}')">demote</button>`;
} else {
if (!e.card_type) {
actions += `<button class="action-btn primary" onclick="nibApp.showPromote('${e.id}')">promote →</button>`;
actions += `<button class="action-btn" onclick="nibApp.showAbsorb('${e.id}')">absorb</button>`;
}
actions += `<button class="action-btn danger" onclick="nibApp.deleteEntity('${e.id}')">delete</button>`;
const descHtml = e.description ? `<div class="detail-desc" data-id="${e.id}">${escHtml(e.description)}</div>` : '';
const titleHtml = e.title ? `<h2 class="detail-title" data-id="${e.id}">${escHtml(e.title)}</h2>` : '';
pane.innerHTML = `
<div class="detail-scroll">
<div class="detail-header">
<span class="detail-glyph ${gc}">${glyph}</span>
<span class="detail-id">${shortId}</span>
${e.time_anchor ? `<span class="entity-time">@${e.time_anchor}</span>` : ''}
</div>
${descHtml}
${titleHtml}
<div class="detail-body" data-id="${e.id}">${escHtml(e.body)}</div>
${tags ? `<div class="detail-tags">${tags}</div>` : ''}
${cardContent}
<div class="detail-actions">${actions}</div>
return `<div class="peek-scroll">
<div class="peek-brow">
<span class="peek-brow-g ${gc}">${glyph}</span>
<span class="peek-brow-kind">${kindLbl}</span>
<span class="peek-brow-sep">·</span>
<span class="peek-brow-id">${e.id.slice(-10)}</span>
<span class="peek-brow-ts">${fmtDateLong(e.created_at)}</span>
</div>
`;
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 ? `<div class="peek-title" data-id="${e.id}">${escHtml(e.title)}</div>` : ''}
<div class="peek-body" data-id="${e.id}">${escHtml(e.body)}</div>
${tags ? `<div class="peek-sec"><div class="peek-sec-lbl">tags</div><div class="peek-sec-inner tag-pills">${tags}</div></div>` : ''}
<div class="peek-sec">
<div class="peek-sec-lbl">context</div>
<div class="peek-sec-inner peek-ctx">
<span><span class="peek-ctx-lbl">created</span>${fmtDateLong(e.created_at)}</span>
${e.time_anchor ? `<span><span class="peek-ctx-lbl">time</span>@${e.time_anchor}</span>` : ''}
${e.card_type ? `<span><span class="peek-ctx-lbl">status</span><span class="peek-ctx-promoted">promoted → ${e.card_type}</span></span>` : ''}
</div>
</div>
<div class="peek-acts">${actions}</div>
</div>`;
}
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 => `<span class="detail-tag">#${t}</span>`).join('');
const affHtml = affs.map(a => `<span class="aff ${AFF_CLASSES[a]}">${AFF_LABELS[a]}</span>`).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 `<div class="slot-form">
${data.slots.map(s => `
<div class="slot-field">
<span class="slot-label">\${${s.name}}</span>
<input class="slot-input" data-slot="${s.name}" placeholder="${s.default || s.name}" value="${s.default || ''}">
</div>
`).join('')}
<button class="action-btn primary" onclick="nibApp.resolveTemplate('${e.id}')">resolve & copy</button>
</div>`;
let sections = '';
case 'checklist':
if (!data.steps || !data.steps.length) return '';
return `<div class="checklist">
${data.steps.map((s, i) => `
<div class="checklist-step ${s.done ? 'done' : ''}">
<input type="checkbox" ${s.done ? 'checked' : ''} onchange="nibApp.toggleStep('${e.id}', ${i})">
<span>${escHtml(s.text)}</span>
</div>
`).join('')}
</div>`;
case 'decision':
return `<div>
<div class="decision-field"><div class="decision-label">chose</div><div class="decision-value">${escHtml(data.chose || '—')}</div></div>
<div class="decision-field"><div class="decision-label">why</div><div class="decision-value">${escHtml(data.why || '—')}</div></div>
${data.rejected && data.rejected.length ? `<div class="decision-field"><div class="decision-label">rejected</div><div class="decision-value">${data.rejected.map(escHtml).join(', ') || '—'}</div></div>` : ''}
</div>`;
case 'link':
if (data.url && isSafeUrl(data.url)) {
return `<div style="margin-bottom:12px">
<button class="action-btn" onclick="window.open('${escAttr(data.url)}', '_blank')">open link</button>
</div>`;
}
return '';
default:
return '';
if (hasDecision) {
const rejected = (data.rejected || []).map(r => `<span class="peek-dec-rej">${escHtml(r)}</span>`).join('');
sections += `<div class="peek-sec">
<div class="peek-sec-lbl">decision<span class="peek-sec-status">${data.status || 'decided'}</span></div>
<div class="peek-sec-inner peek-decision">
<div class="peek-dec-choice">${escHtml(data.chose)}</div>
<div class="peek-dec-why"><span class="peek-dec-key">why</span>${escHtml(data.why || '')}</div>
${rejected ? `<div><span class="peek-dec-key">considered</span><div class="peek-dec-rejected">${rejected}</div></div>` : ''}
</div>
</div>`;
}
if (hasLink && !hasDecision) {
sections += `<div class="peek-sec">
<div class="peek-sec-lbl">link</div>
<div class="peek-sec-inner"><div class="peek-link-url">↗ ${escHtml(data.url)}</div></div>
</div>`;
}
if (hasSteps) {
const steps = data.steps.map((s, i) => `<div class="peek-step"><span class="peek-step-mark" style="color:var(--dim)">○</span><span class="peek-step-text">${escHtml(s.text || s)}</span></div>`).join('');
sections += `<div class="peek-sec">
<div class="peek-sec-lbl">steps · ${data.steps.length}<button class="peek-sec-run" onclick="nibApp.enterMode('run')">▶ run</button></div>
<div class="peek-sec-inner"><div class="peek-steps">${steps}</div></div>
</div>`;
}
if (!hasDecision && e.body) {
const lang = data.lang || '';
sections += `<div class="peek-sec">
<div class="peek-sec-lbl">content${lang ? `<span class="peek-sec-lang">${lang}</span>` : ''}${hasFill ? `<button class="peek-sec-run" onclick="nibApp.enterMode('fill')">⤓ fill</button>` : ''}</div>
<div class="peek-sec-inner"><div class="peek-code"><pre><code>${escHtml(e.body)}</code></pre></div></div>
</div>`;
}
let actions = `<button class="action-btn primary" onclick="nibApp.copyEntity('${e.id}')">copy <kbd>⏎</kbd></button>`;
if (hasFill) actions += `<button class="action-btn" onclick="nibApp.enterMode('fill')">fill <kbd>f</kbd></button>`;
if (hasSteps) actions += `<button class="action-btn" onclick="nibApp.enterMode('run')">run <kbd>r</kbd></button>`;
actions += `<button class="action-btn" onclick="nibApp.enterMode('edit')">edit <kbd>e</kbd></button>`;
actions += `<button class="action-btn" onclick="nibApp.togglePin('${e.id}')">${e.pinned ? 'unpin' : 'pin'} <kbd>p</kbd></button>`;
actions += `<button class="action-btn danger" onclick="nibApp.deleteEntity('${e.id}')">delete</button>`;
return `<div class="peek-scroll">
<div class="peek-card">
<div class="peek-card-head">
<div class="peek-brow" style="padding:14px 16px 0">
<span class="peek-brow-g ${gc}">${glyph}</span>
<span class="peek-brow-kind">${e.card_type}</span>
<span class="peek-brow-sep">·</span>
<span class="peek-brow-id">${e.id.slice(-10)}</span>
${e.use_count > 0 ? `<span class="peek-brow-ts">${e.use_count}× used</span>` : ''}
</div>
<div class="peek-title" style="padding:9px 16px 4px">${escHtml(e.title || '')}</div>
${e.description ? `<div class="peek-desc" style="padding:0 16px 10px">${escHtml(e.description)}</div>` : ''}
<div class="peek-meta" style="padding:0 16px 12px">${affHtml}${tags}${e.pinned ? '<span class="peek-pin">★</span>' : ''}</div>
</div>
${sections}
</div>
<div class="peek-acts">${actions}</div>
</div>`;
}
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 `<div class="peek-run-step${isDone ? ' done' : ''}" data-step="${i}">
<span class="peek-run-mark" style="color:${isDone ? 'var(--ok)' : 'var(--dim)'}">${isDone ? '●' : '○'}</span>
<span class="peek-run-text">${escHtml(text)}</span>
</div>`;
}).join('');
return `<div class="peek-scroll">
<div class="peek-brow">
<span class="peek-run-pill">▶ running</span>
<span class="peek-brow-ts">${done}/${total} done</span>
</div>
<div class="peek-title">${escHtml(e.title || '')}</div>
${e.description ? `<div class="peek-desc">${escHtml(e.description)}</div>` : ''}
<div class="peek-run-prog-wrap">
<div class="peek-run-prog-track"><div class="peek-run-prog" style="width:${pct}%"></div></div>
<span class="peek-run-pct">${pct}%</span>
</div>
<div class="peek-run-steps">${steps}</div>
<div class="peek-hints"><span><kbd>Space</kbd> toggle</span><span><kbd>r</kbd> reset</span><span><kbd>Esc</kbd> exit</span></div>
<div class="peek-acts">
<button class="action-btn primary" onclick="nibApp.exitMode()">done</button>
<button class="action-btn" onclick="nibApp.resetRun()">reset</button>
</div>
</div>`;
}
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}}`, `<span class="${cls}"><input type="text" data-slot="${name}" data-idx="${idx}" placeholder="${escHtml(name)}" value="${escHtml(val)}" style="width:${width}px"></span>`);
}
const allFilled = slots.every(s => fill[s]);
return `<div class="peek-scroll">
<div class="peek-brow">
<span class="peek-fill-pill">⤓ filling</span>
<span class="peek-brow-ts">slot ${active + 1} / ${slots.length}</span>
</div>
<div class="peek-title">${escHtml(e.title || '')}</div>
${e.description ? `<div class="peek-desc">${escHtml(e.description)}</div>` : ''}
<div class="peek-fill-canvas"><code>${content}</code></div>
<div class="peek-hints"><span><kbd>Tab</kbd> next</span><span><kbd>⇧Tab</kbd> prev</span><span><kbd>⏎</kbd> copy</span><span><kbd>Esc</kbd> cancel</span></div>
<div class="peek-acts">
<button class="action-btn primary${allFilled ? '' : ' dim'}" onclick="nibApp.completeFill()">copy resolved</button>
<button class="action-btn" onclick="nibApp.exitMode()">cancel</button>
</div>
</div>`;
}
function renderEditMode(e) {
return `<div class="peek-scroll">
<div class="peek-brow"><span class="peek-edit-pill">✎ editing</span></div>
<div class="peek-title" style="opacity:.45">${escHtml(e.title || 'untitled')}</div>
<div class="peek-edit-fields">
<div class="peek-edit-field"><label class="peek-edit-lbl">title</label>
<input class="peek-edit-in" id="edit-title" value="${escAttr(e.title || '')}"></div>
<div class="peek-edit-field"><label class="peek-edit-lbl">description</label>
<input class="peek-edit-in" id="edit-desc" value="${escAttr(e.description || '')}"></div>
<div class="peek-edit-field"><label class="peek-edit-lbl">content</label>
<textarea class="peek-edit-ta" id="edit-body" rows="7">${escHtml(e.body || '')}</textarea></div>
<div class="peek-edit-field"><label class="peek-edit-lbl">tags</label>
<input class="peek-edit-in" id="edit-tags" value="${escAttr((e.tags || []).join(' '))}" placeholder="space-separated"></div>
</div>
<div class="peek-hints"><span><kbd>⌘⏎</kbd> save</span><span><kbd>Esc</kbd> cancel</span></div>
<div class="peek-acts">
<button class="action-btn primary" onclick="nibApp.saveEdit('${e.id}')">save</button>
<button class="action-btn" onclick="nibApp.exitMode()">cancel</button>
</div>
</div>`;
}
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;
}