diff --git a/web/app.js b/web/app.js
index 4b3970f..40ed2b0 100644
--- a/web/app.js
+++ b/web/app.js
@@ -27,6 +27,7 @@
hasMore: false,
activeMonth: null,
intent: 'grab',
+ flashId: null,
};
const $ = (sel) => document.querySelector(sel);
@@ -214,6 +215,42 @@
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) {
@@ -368,11 +405,13 @@
if (state.entities.length === 0) {
list.innerHTML = '
no entities yet
';
+ renderCardsHeader(false);
return;
}
let html = '';
if (state.view === 'stream') {
+ renderCardsHeader(false);
const groups = groupByDate(state.entities);
let idx = 0;
for (const g of groups) {
@@ -383,9 +422,22 @@
}
}
} else {
- state.entities.forEach((e, idx) => {
- html += renderEntityItem(e, idx);
- });
+ renderCardsHeader(true);
+ const pinned = state.entities.filter(e => e.pinned);
+ const rest = state.entities.filter(e => !e.pinned);
+ let idx = 0;
+ 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) {
@@ -400,10 +452,63 @@
});
});
+ 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;
+ hdr.innerHTML = `
+ ${scope}
+ ${state.entities.length} cards
+
+ `;
+ }
+
+ 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);
@@ -652,7 +757,8 @@
function renderMonthNav() {
const nav = $('#month-nav');
- if (state.view !== 'stream') { nav.innerHTML = ''; return; }
+ 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
@@ -720,8 +826,10 @@
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);
}
diff --git a/web/style.css b/web/style.css
index e9f73fa..9f37905 100644
--- a/web/style.css
+++ b/web/style.css
@@ -436,6 +436,178 @@ main {
font-size: 9px;
}
+/* ── CARDS SUB-HEADER ──────────────────────────────── */
+.cards-hdr {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 7px 14px;
+ border-bottom: 1px solid var(--soft);
+ background: var(--surf);
+ flex-shrink: 0;
+}
+
+.cards-scope {
+ font-family: var(--sans);
+ font-size: 12px;
+ font-weight: 500;
+ color: var(--text);
+}
+
+.cards-count {
+ font-family: var(--mono);
+ font-size: 10px;
+ color: var(--dim);
+ margin-left: auto;
+}
+
+.cards-sort {
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: var(--r2);
+ padding: 3px 6px;
+ font-family: var(--mono);
+ font-size: 11px;
+ color: var(--muted);
+ outline: none;
+}
+
+/* ── CARD ROWS ──────────────────────────────────────── */
+.card-row {
+ display: flex;
+ align-items: center;
+ gap: 7px;
+ padding: 9px 12px 9px 14px;
+ margin: 2px 10px;
+ background: var(--surf);
+ border: 1px solid var(--border);
+ border-radius: var(--r2);
+ cursor: pointer;
+ min-height: 40px;
+ position: relative;
+ transition: border-color var(--t-fast), background var(--t-fast);
+}
+
+.card-row:hover { border-color: var(--muted); }
+.card-row.selected { border-color: var(--accent); background: var(--a-bg); }
+.card-row.pinned { border-left: 2px solid var(--accent); }
+
+@keyframes card-flash { 0%,100%{} 50%{ background: var(--a-str); } }
+.card-row.flashing { animation: card-flash .3s ease; }
+
+.card-row-title {
+ font-family: var(--sans);
+ font-size: 12px;
+ font-weight: 500;
+ color: var(--text);
+ flex-shrink: 0;
+ max-width: 145px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.card-row-dash {
+ color: var(--dim);
+ font-family: var(--mono);
+ font-size: 10px;
+ flex-shrink: 0;
+}
+
+.card-row-preview {
+ flex: 1;
+ min-width: 0;
+ font-family: var(--mono);
+ font-size: 11px;
+ color: var(--muted);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.card-row-preview .slot { color: var(--lineage); }
+.card-row-preview .link { color: var(--event); }
+.card-row-preview .choice { color: var(--text); }
+
+.card-row-right {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ flex-shrink: 0;
+ margin-left: auto;
+}
+
+.card-row-pin {
+ color: var(--accent);
+ font-size: 10px;
+}
+
+.card-row-use {
+ font-family: var(--mono);
+ font-size: 9px;
+ color: var(--todo);
+}
+
+/* progress bar in card row */
+.card-row-prog {
+ display: inline-block;
+ width: 36px;
+ height: 3px;
+ background: var(--border);
+ border-radius: 2px;
+ overflow: hidden;
+ vertical-align: middle;
+}
+
+.card-row-prog span {
+ display: block;
+ height: 100%;
+ background: var(--ok);
+ border-radius: 2px;
+}
+
+/* ── SECTION LABELS ─────────────────────────────────── */
+.list-sec-lbl {
+ font-family: var(--mono);
+ font-size: 9px;
+ text-transform: uppercase;
+ letter-spacing: .16em;
+ color: var(--dim);
+ padding: 8px 14px 4px;
+}
+
+/* ── AFFORDANCE BADGES ──────────────────────────────── */
+.aff {
+ font-family: var(--mono);
+ font-size: 9px;
+ letter-spacing: .04em;
+ padding: 1px 6px;
+ border-radius: var(--r1);
+ border: 1px solid;
+ flex-shrink: 0;
+}
+
+.aff.clickable { cursor: pointer; transition: opacity var(--t-fast); }
+.aff.clickable:hover { opacity: .7; }
+
+.aff-code { color: var(--accent); border-color: rgba(200,148,42,.38); background: var(--a-bg); }
+.aff-fill { color: var(--lineage); border-color: rgba(152,120,188,.38); background: rgba(152,120,188,.06); }
+.aff-steps { color: var(--ok); border-color: rgba(122,171,114,.38); background: rgba(122,171,114,.06); }
+.aff-decide { color: var(--note); border-color: rgba(106,184,176,.38); background: rgba(106,184,176,.06); }
+.aff-link { color: var(--event); border-color: rgba(104,152,200,.38); background: rgba(104,152,200,.06); }
+
+/* ── TAG PILLS ──────────────────────────────────────── */
+.tag-pill {
+ font-family: var(--mono);
+ font-size: 10px;
+ color: var(--accent);
+ border: 1px solid rgba(200,148,42,.35);
+ background: var(--a-bg);
+ padding: 2px 7px;
+ border-radius: var(--r1);
+ flex-shrink: 0;
+}
+
/* ── CAPTURE BAR ────────────────────────────────────── */
#capture-bar {
border-top: 1px solid var(--border);