feat(ui): redesign to match design handoff prototype #9

Merged
lerko merged 5 commits from feat/ui-redesign into develop 2026-05-16 14:26:28 +00:00
2 changed files with 284 additions and 4 deletions
Showing only changes of commit 156ea6ea1c - Show all commits
+112 -4
View File
@@ -27,6 +27,7 @@
hasMore: false, hasMore: false,
activeMonth: null, activeMonth: null,
intent: 'grab', intent: 'grab',
flashId: null,
}; };
const $ = (sel) => document.querySelector(sel); const $ = (sel) => document.querySelector(sel);
@@ -214,6 +215,42 @@
return null; 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 `<span class="choice">▸ ${escHtml(data.chose)}</span>`;
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 `<span class="card-row-prog"><span style="width:${pct}%"></span></span> ${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 => `<span class="slot">\${${escHtml(s)}}</span>`).join(' ');
}
if (data.url) return `<span class="link">${escHtml(data.url.replace(/^https?:\/\//, ''))}</span>`;
const first = (entity.body || '').split('\n')[0] || '';
return escHtml(first.slice(0, 60));
}
// ========== Rendering ========== // ========== Rendering ==========
function displayGlyph(entity) { function displayGlyph(entity) {
@@ -368,11 +405,13 @@
if (state.entities.length === 0) { if (state.entities.length === 0) {
list.innerHTML = '<div class="detail-empty" style="margin-top:40px">no entities yet</div>'; list.innerHTML = '<div class="detail-empty" style="margin-top:40px">no entities yet</div>';
renderCardsHeader(false);
return; return;
} }
let html = ''; let html = '';
if (state.view === 'stream') { if (state.view === 'stream') {
renderCardsHeader(false);
const groups = groupByDate(state.entities); const groups = groupByDate(state.entities);
let idx = 0; let idx = 0;
for (const g of groups) { for (const g of groups) {
@@ -383,9 +422,22 @@
} }
} }
} else { } else {
state.entities.forEach((e, idx) => { renderCardsHeader(true);
html += renderEntityItem(e, idx); const pinned = state.entities.filter(e => e.pinned);
}); const rest = state.entities.filter(e => !e.pinned);
let idx = 0;
if (pinned.length) {
html += '<div class="list-sec-lbl">★ pinned</div>';
for (const e of pinned) {
html += renderCardRow(e, state.entities.indexOf(e));
}
}
if (rest.length) {
if (pinned.length) html += '<div class="list-sec-lbl">recent</div>';
for (const e of rest) {
html += renderCardRow(e, state.entities.indexOf(e));
}
}
} }
if (state.hasMore) { 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'); const loadMoreBtn = list.querySelector('.load-more-btn');
if (loadMoreBtn) loadMoreBtn.addEventListener('click', loadMore); 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 = `
<span class="cards-scope">${scope}</span>
<span class="cards-count">${state.entities.length} cards</span>
<select class="cards-sort"><option>newest</option><option>most used</option></select>
`;
}
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 => `<span class="tag-pill">#${t}</span>`).join('');
const affHtml = affs.map(a => `<span class="aff clickable ${AFF_CLASSES[a]}">${AFF_LABELS[a]}</span>`).join('');
return `<div class="card-row${selected}${pinCls}${flashCls}" data-index="${idx}" data-id="${e.id}">
<span class="card-row-title">${escHtml(title)}</span>
<span class="card-row-dash">—</span>
<span class="card-row-preview">${preview}</span>
<div class="card-row-right">
${affHtml}
${tags}
${e.pinned ? '<span class="card-row-pin">★</span>' : ''}
${e.use_count > 0 ? `<span class="card-row-use">${e.use_count}×</span>` : ''}
</div>
</div>`;
}
function renderEntityItem(e, idx) { function renderEntityItem(e, idx) {
const glyph = displayGlyph(e); const glyph = displayGlyph(e);
const gc = glyphClass(e); const gc = glyphClass(e);
@@ -652,7 +757,8 @@
function renderMonthNav() { function renderMonthNav() {
const nav = $('#month-nav'); 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 MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
const label = state.activeMonth const label = state.activeMonth
@@ -720,8 +826,10 @@
try { try {
await navigator.clipboard.writeText(e.body); await navigator.clipboard.writeText(e.body);
await api.useEntity(id); await api.useEntity(id);
state.flashId = id;
await loadEntities(); await loadEntities();
showToast('copied'); showToast('copied');
setTimeout(() => { state.flashId = null; renderEntityList(); }, 360);
} catch (err) { } catch (err) {
console.error('clipboard:', err); console.error('clipboard:', err);
} }
+172
View File
@@ -436,6 +436,178 @@ main {
font-size: 9px; 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 ────────────────────────────────────── */
#capture-bar { #capture-bar {
border-top: 1px solid var(--border); border-top: 1px solid var(--border);