feat(ui): phase 1 — layout, tokens, header, rail redesign

- 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
This commit is contained in:
2026-05-16 09:25:35 -04:00
parent f4e178e3ee
commit dda8426113
4 changed files with 495 additions and 208 deletions
+37
View File
@@ -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
+148 -51
View File
@@ -16,6 +16,8 @@
const PAGE_SIZE = 50; const PAGE_SIZE = 50;
const INTENT_HINTS = { grab: 'scan + copy', read: 'expand + study', fill: 'templates only' };
const state = { const state = {
view: 'stream', view: 'stream',
entities: [], entities: [],
@@ -24,6 +26,7 @@
activeTag: null, activeTag: null,
hasMore: false, hasMore: false,
activeMonth: null, activeMonth: null,
intent: 'grab',
}; };
const $ = (sel) => document.querySelector(sel); const $ = (sel) => document.querySelector(sel);
@@ -223,32 +226,128 @@
function formatDate(dateStr) { function formatDate(dateStr) {
const d = new Date(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(); return months[d.getMonth()] + ' ' + d.getDate();
} }
// ── Tag Rail ──
function renderTagRail() { function renderTagRail() {
const rail = $('#tag-rail'); const rail = $('#tag-rail');
const allItem = `<div class="tag-item ${!state.activeTag ? 'active' : ''}" data-tag=""> const total = state.tags.reduce((s, t) => s + t.count, 0);
<span class="tag-name" style="font-style: italic">all</span>
</div>`;
rail.innerHTML = allItem + state.tags.map(t => let html = `<div class="rail-head"><span class="rail-brand">nib</span></div>`;
`<div class="tag-item ${state.activeTag === t.tag ? 'active' : ''}" data-tag="${t.tag}"> html += '<div class="rail-scroll">';
<span class="tag-name">${t.tag}</span>
<span class="tag-count">${t.count}</span>
</div>`
).join('');
rail.querySelectorAll('.tag-item').forEach(el => { if (state.view === 'cards') {
html += '<div class="rail-sec">';
html += '<div class="rail-lbl">intent</div>';
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 += `<button class="rail-item${on}" data-intent="${k}">`;
html += `<span class="rail-arrow">${state.intent === k ? '▸' : ''}</span>`;
html += '<span class="rail-dot"></span>';
html += `<span class="rail-name">${k}</span>`;
html += `<span class="rail-count">${count}</span>`;
if (state.intent === k) html += `<span class="rail-hint">${INTENT_HINTS[k]}</span>`;
html += '</button>';
}
html += '</div>';
}
html += '<div class="rail-sec">';
html += '<div class="rail-lbl">tags</div>';
const allOn = !state.activeTag ? ' on' : '';
html += `<button class="rail-item${allOn}" data-tag="">`;
html += `<span class="rail-arrow">${!state.activeTag ? '▸' : ''}</span>`;
html += '<span class="rail-dot"></span>';
html += `<span class="rail-name">#all</span>`;
html += `<span class="rail-count">${total}</span>`;
html += '</button>';
for (const t of state.tags) {
const on = state.activeTag === t.tag ? ' on' : '';
html += `<button class="rail-item${on}" data-tag="${t.tag}">`;
html += `<span class="rail-arrow">${state.activeTag === t.tag ? '▸' : ''}</span>`;
html += '<span class="rail-dot"></span>';
html += `<span class="rail-name">#${t.tag}</span>`;
html += `<span class="rail-count">${t.count}</span>`;
html += '</button>';
}
html += '</div></div>';
rail.innerHTML = html;
rail.querySelectorAll('.rail-item[data-tag]').forEach(el => {
el.addEventListener('click', () => { el.addEventListener('click', () => {
state.activeTag = el.dataset.tag || null; state.activeTag = el.dataset.tag || null;
loadEntities(); loadEntities();
renderTagRail(); 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 = `
<div class="cap-row">
<span class="cap-prompt"></span>
<textarea id="capture-input" rows="1" placeholder="${placeholder}" spellcheck="false"></textarea>
<span class="cap-hint">⏎ save</span>
</div>
`;
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) { function groupByDate(entities) {
const groups = []; const groups = [];
let current = null; let current = null;
@@ -309,9 +408,11 @@
const glyph = displayGlyph(e); const glyph = displayGlyph(e);
const gc = glyphClass(e); const gc = glyphClass(e);
const selected = idx === state.selectedIndex ? ' selected' : ''; const selected = idx === state.selectedIndex ? ' selected' : '';
const tags = (e.tags || []).map(t => `<span class="entity-tag">${t}</span>`).join(''); const isCard = e.card_type ? ' is-card' : '';
const tags = (e.tags || []).slice(0, 2).map(t => `<span class="entity-tag">${t}</span>`).join('');
const time = e.time_anchor ? `<span class="entity-time">@${e.time_anchor}</span>` : ''; const time = e.time_anchor ? `<span class="entity-time">@${e.time_anchor}</span>` : '';
const useBadge = e.use_count > 0 ? `<span class="use-badge">${e.use_count}×</span>` : ''; const useBadge = e.use_count > 0 ? `<span class="use-badge">${e.use_count}×</span>` : '';
const cardBadge = e.card_type ? `<span class="card-badge">${e.card_type}</span>` : '';
let label; let label;
if (e.title) { if (e.title) {
@@ -321,11 +422,11 @@
label = `<span class="entity-body">${escHtml(e.body)}</span>`; label = `<span class="entity-body">${escHtml(e.body)}</span>`;
} }
return `<div class="entity-item ${selected}" data-index="${idx}" data-id="${e.id}"> return `<div class="entity-item${selected}${isCard}" data-index="${idx}" data-id="${e.id}">
<span class="entity-glyph ${gc}">${glyph}</span> <span class="entity-glyph ${gc}">${glyph}</span>
${label} ${label}
${time} ${time}
<span class="entity-tags">${tags}</span> <span class="entity-tags">${tags}${cardBadge}</span>
<span class="entity-meta">${useBadge}</span> <span class="entity-meta">${useBadge}</span>
</div>`; </div>`;
} }
@@ -351,10 +452,10 @@
if (e.card_type) { if (e.card_type) {
cardContent = renderCardContent(e); cardContent = renderCardContent(e);
actions += `<button class="action-btn" onclick="nibApp.copyEntity('${e.id}')">copy</button>`; 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>`; actions += `<button class="action-btn" onclick="nibApp.demoteEntity('${e.id}')">demote</button>`;
} else { } else {
actions += `<button class="action-btn primary" onclick="nibApp.showPromote('${e.id}')">promote</button>`; 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" onclick="nibApp.showAbsorb('${e.id}')">absorb</button>`;
} }
actions += `<button class="action-btn danger" onclick="nibApp.deleteEntity('${e.id}')">delete</button>`; actions += `<button class="action-btn danger" onclick="nibApp.deleteEntity('${e.id}')">delete</button>`;
@@ -363,6 +464,7 @@
const titleHtml = e.title ? `<h2 class="detail-title" data-id="${e.id}">${escHtml(e.title)}</h2>` : ''; const titleHtml = e.title ? `<h2 class="detail-title" data-id="${e.id}">${escHtml(e.title)}</h2>` : '';
pane.innerHTML = ` pane.innerHTML = `
<div class="detail-scroll">
<div class="detail-header"> <div class="detail-header">
<span class="detail-glyph ${gc}">${glyph}</span> <span class="detail-glyph ${gc}">${glyph}</span>
<span class="detail-id">${shortId}</span> <span class="detail-id">${shortId}</span>
@@ -374,6 +476,7 @@
${tags ? `<div class="detail-tags">${tags}</div>` : ''} ${tags ? `<div class="detail-tags">${tags}</div>` : ''}
${cardContent} ${cardContent}
<div class="detail-actions">${actions}</div> <div class="detail-actions">${actions}</div>
</div>
`; `;
const titleEl = pane.querySelector('.detail-title'); const titleEl = pane.querySelector('.detail-title');
@@ -560,13 +663,10 @@
<button class="month-nav-btn" id="month-prev">◂</button> <button class="month-nav-btn" id="month-prev">◂</button>
<span class="month-nav-label">${label}</span> <span class="month-nav-label">${label}</span>
<button class="month-nav-btn" id="month-next">▸</button> <button class="month-nav-btn" id="month-next">▸</button>
${state.activeMonth ? '<button class="month-nav-clear">clear</button>' : ''}
`; `;
$('#month-prev').addEventListener('click', () => shiftMonth(-1)); $('#month-prev').addEventListener('click', () => shiftMonth(-1));
$('#month-next').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) { function shiftMonth(dir) {
@@ -590,10 +690,25 @@
function switchView(view) { function switchView(view) {
state.view = view; state.view = view;
state.activeMonth = null; state.activeMonth = null;
state.selectedIndex = -1;
$$('.nav-btn').forEach(b => b.classList.toggle('active', b.dataset.view === view)); $$('.nav-btn').forEach(b => b.classList.toggle('active', b.dataset.view === view));
window.location.hash = view === 'cards' ? '/cards' : '/'; window.location.hash = view === 'cards' ? '/cards' : '/';
loadEntities(); loadEntities();
renderMonthNav(); 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) ========== // ========== Public API (for inline handlers) ==========
@@ -606,6 +721,7 @@
await navigator.clipboard.writeText(e.body); await navigator.clipboard.writeText(e.body);
await api.useEntity(id); await api.useEntity(id);
await loadEntities(); await loadEntities();
showToast('copied');
} catch (err) { } catch (err) {
console.error('clipboard:', err); console.error('clipboard:', err);
} }
@@ -630,12 +746,14 @@
await api.demoteEntity(id); await api.demoteEntity(id);
await loadEntities(); await loadEntities();
await loadTags(); await loadTags();
showToast('demoted');
}, },
async deleteEntity(id) { async deleteEntity(id) {
await api.deleteEntity(id); await api.deleteEntity(id);
await loadEntities(); await loadEntities();
await loadTags(); await loadTags();
showToast('deleted');
}, },
async resolveTemplate(id) { async resolveTemplate(id) {
@@ -651,6 +769,7 @@
await navigator.clipboard.writeText(resolved); await navigator.clipboard.writeText(resolved);
await api.useEntity(id); await api.useEntity(id);
await loadEntities(); await loadEntities();
showToast('copied');
} catch (err) { } catch (err) {
console.error('clipboard:', err); console.error('clipboard:', err);
} }
@@ -688,6 +807,7 @@
await loadTags(); await loadTags();
const idx = state.entities.findIndex(x => x.id === targetId); const idx = state.entities.findIndex(x => x.id === targetId);
if (idx >= 0) selectEntity(idx); 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 ========== // ========== Promote modal ==========
$$('.type-btn').forEach(btn => { $$('.type-btn').forEach(btn => {
@@ -745,6 +838,7 @@
await api.promoteEntity(id, btn.dataset.type); await api.promoteEntity(id, btn.dataset.type);
await loadEntities(); await loadEntities();
await loadTags(); await loadTags();
showToast('promoted → ' + btn.dataset.type);
}); });
}); });
@@ -761,12 +855,11 @@
// ========== Keyboard shortcuts ========== // ========== Keyboard shortcuts ==========
let lastDTime = 0; let lastDTime = 0;
const captureInput = $('#capture-input');
document.addEventListener('keydown', (ev) => { document.addEventListener('keydown', (ev) => {
if (document.activeElement === captureInput || const tag = (ev.target.tagName || '').toLowerCase();
document.activeElement.classList.contains('detail-body-edit')) { if (tag === 'input' || tag === 'textarea') {
if (ev.key === 'Escape') document.activeElement.blur(); if (ev.key === 'Escape') ev.target.blur();
return; return;
} }
@@ -789,7 +882,7 @@
break; break;
case 'n': case 'n':
ev.preventDefault(); ev.preventDefault();
captureInput.focus(); $('#capture-input').focus();
break; break;
case 'p': { case 'p': {
const e = state.entities[state.selectedIndex]; const e = state.entities[state.selectedIndex];
@@ -848,6 +941,9 @@
} }
$$('.nav-btn').forEach(b => b.classList.toggle('active', b.dataset.view === state.view)); $$('.nav-btn').forEach(b => b.classList.toggle('active', b.dataset.view === state.view));
loadEntities(); loadEntities();
renderMonthNav();
renderTagRail();
renderCaptureBar();
} }
window.addEventListener('hashchange', handleHash); window.addEventListener('hashchange', handleHash);
@@ -884,6 +980,7 @@
// ========== Init ========== // ========== Init ==========
async function init() { async function init() {
renderCaptureBar();
await Promise.all([loadEntities(), loadTags()]); await Promise.all([loadEntities(), loadTags()]);
handleHash(); handleHash();
renderMonthNav(); renderMonthNav();
+6 -11
View File
@@ -6,28 +6,22 @@
<title>nib</title> <title>nib</title>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/style.css"> <link rel="stylesheet" href="/style.css">
<style>
@font-face { font-family: 'Monaspace Neon'; font-weight: 300; src: url('https://cdn.jsdelivr.net/gh/githubnext/monaspace@v1.000/fonts/webfonts/MonaspaceNeon-Light.woff2') format('woff2'); }
@font-face { font-family: 'Monaspace Neon'; font-weight: 400; src: url('https://cdn.jsdelivr.net/gh/githubnext/monaspace@v1.000/fonts/webfonts/MonaspaceNeon-Regular.woff2') format('woff2'); }
@font-face { font-family: 'Monaspace Neon'; font-weight: 500; src: url('https://cdn.jsdelivr.net/gh/githubnext/monaspace@v1.000/fonts/webfonts/MonaspaceNeon-Medium.woff2') format('woff2'); }
@font-face { font-family: 'Monaspace Neon'; font-weight: 700; src: url('https://cdn.jsdelivr.net/gh/githubnext/monaspace@v1.000/fonts/webfonts/MonaspaceNeon-Bold.woff2') format('woff2'); }
</style>
</head> </head>
<body> <body>
<div id="app"> <div id="app">
<header> <header>
<div class="header-left"> <div class="header-left">
<h1 class="logo">nib</h1> <span class="logo">nib</span>
<nav> <nav>
<button data-view="stream" class="nav-btn active">stream</button> <button data-view="stream" class="nav-btn active">stream</button>
<button data-view="cards" class="nav-btn">cards</button> <button data-view="cards" class="nav-btn">cards</button>
</nav> </nav>
</div> </div>
<form id="capture-bar" autocomplete="off"> <div class="header-search">
<input type="text" id="capture-input" placeholder="capture — - todo # note * event" spellcheck="false"> <input type="text" id="search-input" placeholder="? search #tag" spellcheck="false">
</form> </div>
<button class="theme-toggle" id="theme-toggle" title="toggle theme"></button> <button class="theme-toggle" id="theme-toggle" title="toggle theme"></button>
</header> </header>
<main> <main>
@@ -35,6 +29,7 @@
<section id="entity-panel"> <section id="entity-panel">
<div id="month-nav"></div> <div id="month-nav"></div>
<div id="entity-list"></div> <div id="entity-list"></div>
<div id="capture-bar"></div>
</section> </section>
<aside id="detail-pane"> <aside id="detail-pane">
<div class="detail-empty">select an entity</div> <div class="detail-empty">select an entity</div>
+293 -135
View File
@@ -1,4 +1,4 @@
/* ── TOKENS ─────────────────────────────────────────── */ /* ── TOKENS (nib DS v2) ─────────────────────────────── */
:root { :root {
color-scheme: dark; color-scheme: dark;
--bg: #0c0b09; --bg: #0c0b09;
@@ -11,6 +11,7 @@
--dim: #504840; --dim: #504840;
--accent: #c8942a; --accent: #c8942a;
--a-bg: rgba(200,148,42,.09); --a-bg: rgba(200,148,42,.09);
--a-str: rgba(200,148,42,.22);
--todo: #d4a84b; --todo: #d4a84b;
--note: #6ab8b0; --note: #6ab8b0;
--event: #6898c8; --event: #6898c8;
@@ -18,9 +19,8 @@
--ok: #7aab72; --ok: #7aab72;
--danger: #b85858; --danger: #b85858;
--lineage: #9878bc; --lineage: #9878bc;
--pin: #c8942a;
--sans: 'Space Grotesk', system-ui, sans-serif; --sans: 'Space Grotesk', system-ui, sans-serif;
--mono: 'Monaspace Neon', ui-monospace, monospace; --mono: 'JetBrains Mono', ui-monospace, monospace;
--r1: 2px; --r1: 2px;
--r2: 4px; --r2: 4px;
--r3: 8px; --r3: 8px;
@@ -40,6 +40,7 @@
--dim: #a09080; --dim: #a09080;
--accent: #8a6018; --accent: #8a6018;
--a-bg: rgba(138,96,24,.08); --a-bg: rgba(138,96,24,.08);
--a-str: rgba(138,96,24,.18);
--todo: #7a5c00; --todo: #7a5c00;
--note: #1a7070; --note: #1a7070;
--event: #245890; --event: #245890;
@@ -47,14 +48,16 @@
--ok: #2a6828; --ok: #2a6828;
--danger: #882030; --danger: #882030;
--lineage: #5830a0; --lineage: #5830a0;
--pin: #8a6018;
} }
/* ── RESET ──────────────────────────────────────────── */ /* ── RESET ──────────────────────────────────────────── */
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; } *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; overflow: hidden; }
::-webkit-scrollbar { width: 3px; } ::-webkit-scrollbar { width: 3px; }
::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; } ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
button { background: none; border: none; cursor: pointer; font: inherit; color: inherit; }
input, textarea, select { font: inherit; color: inherit; }
body { body {
font-family: var(--sans); font-family: var(--sans);
@@ -62,96 +65,85 @@ body {
color: var(--text); color: var(--text);
font-size: 14px; font-size: 14px;
line-height: 1.5; line-height: 1.5;
height: 100vh;
overflow: hidden;
} }
#app { #app {
display: flex; display: grid;
flex-direction: column; grid-template-rows: 36px 1fr;
height: 100vh; height: 100vh;
background: var(--bg);
} }
/* ── HEADER ─────────────────────────────────────────── */ /* ── HEADER ─────────────────────────────────────────── */
header { header {
background: var(--surf);
border-bottom: 1px solid var(--border);
display: flex; display: flex;
align-items: center; align-items: center;
gap: 16px; gap: 14px;
padding: 0 20px; padding: 0 18px;
height: 36px;
border-bottom: 1px solid var(--border);
background: var(--surf);
flex-shrink: 0;
} }
.header-left { .header-left {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 16px; gap: 12px;
flex-shrink: 0; flex-shrink: 0;
} }
.logo { .logo {
font-family: var(--mono); font-family: var(--mono);
font-size: 15px; font-size: 16px;
font-weight: 300; font-weight: 700;
color: var(--accent); color: var(--accent);
letter-spacing: .3em; letter-spacing: -.02em;
} }
nav { nav { display: flex; gap: 2px; }
display: flex;
gap: 2px;
}
.nav-btn { .nav-btn {
background: none;
border: none;
color: var(--dim);
padding: 4px 8px;
border-radius: var(--r1);
cursor: pointer;
font-size: 11px;
font-family: var(--sans); font-family: var(--sans);
font-size: 11px;
font-weight: 500; font-weight: 500;
color: var(--dim);
padding: 3px 8px;
border-radius: var(--r1);
transition: color var(--t-fast), background var(--t-fast); transition: color var(--t-fast), background var(--t-fast);
} }
.nav-btn:hover { color: var(--muted); } .nav-btn:hover { color: var(--muted); }
.nav-btn.active { color: var(--accent); background: var(--a-bg); } .nav-btn.active { color: var(--accent); background: var(--a-bg); }
#capture-bar { .header-search {
flex: 1; flex: 1;
max-width: 600px; max-width: 400px;
} }
#capture-input { #search-input {
width: 100%; width: 100%;
background: var(--bg); background: var(--bg);
border: 1px solid var(--border); border: 1px solid var(--border);
color: var(--text);
padding: 4px 10px;
border-radius: var(--r2); border-radius: var(--r2);
padding: 4px 10px;
font-family: var(--mono); font-family: var(--mono);
font-size: 12px; font-size: 12px;
color: var(--text);
outline: none; outline: none;
transition: border-color var(--t-fast); transition: border-color var(--t-fast);
} }
#capture-input:hover { border-color: var(--muted); } #search-input:hover { border-color: var(--muted); }
#capture-input:focus { border-color: var(--accent); } #search-input:focus { border-color: var(--accent); }
#capture-input::placeholder { color: var(--dim); } #search-input::placeholder { color: var(--dim); }
.theme-toggle { .theme-toggle {
background: none; margin-left: auto;
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--r1); border-radius: var(--r1);
color: var(--dim); color: var(--dim);
font-family: var(--mono); font-family: var(--mono);
font-size: 13px; font-size: 12px;
padding: 2px 8px; padding: 2px 8px;
cursor: pointer;
flex-shrink: 0;
transition: color var(--t-fast), border-color var(--t-fast); transition: color var(--t-fast), border-color var(--t-fast);
} }
@@ -160,67 +152,132 @@ nav {
/* ── MAIN LAYOUT ────────────────────────────────────── */ /* ── MAIN LAYOUT ────────────────────────────────────── */
main { main {
display: grid; display: grid;
grid-template-columns: 180px 1fr 320px; grid-template-columns: 192px 1fr 400px;
flex: 1;
overflow: hidden; overflow: hidden;
} }
/* ── TAG RAIL ───────────────────────────────────────── */ /* ── TAG RAIL ───────────────────────────────────────── */
#tag-rail { #tag-rail {
border-right: 1px solid var(--border);
padding: 12px 0;
overflow-y: auto;
background: var(--surf); background: var(--surf);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow: hidden;
} }
.tag-item { .rail-head {
display: flex; display: flex;
justify-content: space-between; align-items: baseline;
gap: 5px;
padding: .85rem 1rem .75rem;
border-bottom: 1px solid var(--border);
}
.rail-brand {
font-family: var(--mono);
font-size: 14px;
font-weight: 700;
color: var(--accent);
}
.rail-scroll {
flex: 1;
overflow-y: auto;
padding: 6px 0;
}
.rail-sec { padding: 4px 0 10px; }
.rail-lbl {
font-family: var(--mono);
font-size: 9px;
text-transform: uppercase;
letter-spacing: .16em;
color: var(--dim);
padding: 4px 16px 6px;
}
.rail-item {
display: grid;
grid-template-columns: 8px 8px 1fr auto;
align-items: center;
gap: 5px;
width: 100%;
padding: 4px 16px; padding: 4px 16px;
cursor: pointer; text-align: left;
font-family: var(--mono);
font-size: 11px; font-size: 11px;
color: var(--muted); color: var(--muted);
transition: color var(--t-fast), background var(--t-fast); transition: color var(--t-fast), background var(--t-fast);
border-left: 2px solid transparent;
} }
.tag-item:hover { background: var(--raised); color: var(--text); } .rail-item:hover { color: var(--text); background: var(--raised); }
.tag-item.active { color: var(--accent); background: var(--a-bg); } .rail-item.on { color: var(--accent); background: var(--a-bg); border-left-color: var(--accent); }
.tag-name { font-family: var(--mono); font-size: 11px; } .rail-arrow {
.tag-name::before { content: '#'; color: var(--dim); } font-size: 9px;
.tag-count { color: var(--accent);
font-family: var(--mono); width: 8px;
font-size: 10px; }
.rail-item:not(.on) .rail-arrow { color: transparent; }
.rail-dot {
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--muted);
opacity: .4;
flex-shrink: 0;
}
.rail-item.on .rail-dot { background: var(--accent); opacity: 1; }
.rail-name { font-weight: 500; }
.rail-count {
font-size: 9px;
color: var(--dim); color: var(--dim);
min-width: 20px; font-variant-numeric: tabular-nums;
text-align: right;
} }
/* ── ENTITY PANEL ───────────────────────────────────── */ .rail-item.on .rail-count { color: var(--accent); opacity: .7; }
.rail-hint {
grid-column: 2 / 5;
font-size: 9px;
color: var(--dim);
font-family: var(--mono);
margin-top: 1px;
}
.rail-item:not(.on) .rail-hint { display: none; }
/* ── CENTER PANEL ───────────────────────────────────── */
#entity-panel { #entity-panel {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
background: var(--bg);
} }
#month-nav { #month-nav {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
padding: 6px 20px; padding: 5px 18px;
border-bottom: 1px solid var(--soft); border-bottom: 1px solid var(--soft);
background: var(--surf);
flex-shrink: 0; flex-shrink: 0;
} }
#month-nav:empty { display: none; } #month-nav:empty { display: none; }
.month-nav-btn { .month-nav-btn {
background: none;
border: none;
color: var(--dim);
font-family: var(--mono); font-family: var(--mono);
font-size: 11px; font-size: 10px;
cursor: pointer; color: var(--dim);
padding: 2px 6px; padding: 2px 6px;
border-radius: var(--r1); border-radius: var(--r1);
transition: color var(--t-fast), background var(--t-fast); transition: color var(--t-fast), background var(--t-fast);
@@ -231,38 +288,25 @@ main {
.month-nav-label { .month-nav-label {
font-family: var(--mono); font-family: var(--mono);
font-size: 11px; font-size: 11px;
color: var(--text); color: var(--muted);
min-width: 80px; flex: 1;
text-align: center; text-align: center;
} }
.month-nav-clear {
background: none;
border: none;
color: var(--dim);
font-family: var(--mono);
font-size: 10px;
cursor: pointer;
margin-left: auto;
transition: color var(--t-fast);
}
.month-nav-clear:hover { color: var(--text); }
/* ── ENTITY LIST ────────────────────────────────────── */ /* ── ENTITY LIST ────────────────────────────────────── */
#entity-list { #entity-list {
overflow-y: auto;
padding: 4px 0;
flex: 1; flex: 1;
overflow-y: auto;
padding: 4px 0 8px;
} }
.date-header { .date-header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: .6rem; gap: 8px;
padding: 8px 20px 4px; padding: 10px 20px 5px;
font-size: 10px;
font-family: var(--mono); font-family: var(--mono);
font-size: 9px;
color: var(--dim); color: var(--dim);
text-transform: uppercase; text-transform: uppercase;
letter-spacing: .2em; letter-spacing: .2em;
@@ -279,35 +323,44 @@ main {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
padding: 6px 20px; padding: 5px 16px 5px 20px;
cursor: pointer; cursor: pointer;
transition: background var(--t-fast);
border-left: 2px solid transparent; border-left: 2px solid transparent;
min-height: 32px;
transition: background var(--t-fast), border-left-color var(--t-fast);
} }
.entity-item:hover { background: var(--raised); } .entity-item:hover { background: var(--surf); }
.entity-item.selected { .entity-item.selected { background: var(--surf); border-left-color: var(--accent); }
.entity-item.is-card {
background: var(--surf); background: var(--surf);
border-left-color: var(--accent); margin: 2px 10px;
border-radius: var(--r2);
border: 1px solid var(--border);
border-left-width: 1px;
padding: 7px 12px;
} }
.entity-item.is-card:hover { border-color: var(--muted); }
.entity-item.is-card.selected { border-color: var(--accent); background: var(--a-bg); }
.entity-glyph { .entity-glyph {
font-family: var(--mono); font-family: var(--mono);
font-size: 12px; font-size: 12px;
width: 14px; width: 14px;
text-align: center; text-align: center;
flex-shrink: 0; flex-shrink: 0;
font-weight: 500;
} }
.glyph-note { color: var(--dim); } .glyph-note { color: var(--muted); }
.glyph-todo { color: var(--todo); } .glyph-todo { color: var(--todo); }
.glyph-event { color: var(--event); } .glyph-event { color: var(--event); }
.glyph-snippet { color: var(--accent); } .glyph-snippet { color: var(--accent); }
.glyph-template { color: var(--lineage); } .glyph-template { color: var(--lineage); }
.glyph-checklist { color: var(--remind); } .glyph-checklist { color: var(--remind); }
.glyph-decision { color: var(--note); } .glyph-decision { color: var(--note); }
.glyph-link { color: var(--danger); } .glyph-link { color: var(--event); }
.entity-title { .entity-title {
font-family: var(--sans); font-family: var(--sans);
@@ -355,7 +408,7 @@ main {
font-size: 9px; font-size: 9px;
color: var(--muted); color: var(--muted);
border: 1px solid var(--border); border: 1px solid var(--border);
padding: 1px 6px; padding: 1px 5px;
border-radius: var(--r1); border-radius: var(--r1);
} }
@@ -368,17 +421,86 @@ main {
gap: 8px; gap: 8px;
} }
.card-badge {
font-family: var(--mono);
font-size: 9px;
color: var(--accent);
border: 1px solid rgba(200,148,42,.4);
background: var(--a-bg);
padding: 1px 5px;
border-radius: var(--r1);
}
.use-badge { .use-badge {
color: var(--todo); color: var(--todo);
font-size: 10px; font-size: 9px;
}
/* ── CAPTURE BAR ────────────────────────────────────── */
#capture-bar {
border-top: 1px solid var(--border);
background: var(--surf);
flex-shrink: 0;
transition: border-top-color var(--t-base);
}
#capture-bar:focus-within { border-top-color: rgba(200,148,42,.35); }
.cap-row {
display: flex;
align-items: flex-end;
padding: 0 18px;
}
.cap-prompt {
font-family: var(--mono);
font-size: 14px;
color: var(--accent);
opacity: .4;
padding-bottom: 8px;
flex-shrink: 0;
transition: opacity var(--t-base);
}
#capture-bar:focus-within .cap-prompt { opacity: 1; }
#capture-input {
flex: 1;
background: transparent;
border: none;
outline: none;
padding: 8px 10px;
font-family: var(--mono);
font-size: 12px;
color: var(--text);
line-height: 1.5;
resize: none;
}
#capture-input::placeholder { color: var(--dim); }
.cap-hint {
font-family: var(--mono);
font-size: 9px;
color: var(--dim);
padding-bottom: 8px;
white-space: nowrap;
letter-spacing: .04em;
} }
/* ── DETAIL PANE ────────────────────────────────────── */ /* ── DETAIL PANE ────────────────────────────────────── */
#detail-pane { #detail-pane {
border-left: 1px solid var(--border);
padding: 20px;
overflow-y: auto;
background: var(--surf); background: var(--surf);
border-left: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow: hidden;
}
.detail-scroll {
flex: 1;
overflow-y: auto;
padding: 20px;
} }
.detail-empty { .detail-empty {
@@ -420,8 +542,8 @@ main {
.detail-title { .detail-title {
font-family: var(--sans); font-family: var(--sans);
font-size: 16px; font-size: 15px;
font-weight: 500; font-weight: 600;
margin-bottom: 12px; margin-bottom: 12px;
cursor: text; cursor: text;
padding: 2px 6px; padding: 2px 6px;
@@ -449,7 +571,7 @@ main {
.detail-body { .detail-body {
font-family: var(--mono); font-family: var(--mono);
font-size: 13px; font-size: 13px;
line-height: 1.7; line-height: 1.72;
margin-bottom: 16px; margin-bottom: 16px;
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-word; word-break: break-word;
@@ -468,7 +590,7 @@ main {
min-height: 80px; min-height: 80px;
font-family: var(--mono); font-family: var(--mono);
font-size: 13px; font-size: 13px;
line-height: 1.7; line-height: 1.72;
margin-bottom: 16px; margin-bottom: 16px;
padding: 6px 8px; padding: 6px 8px;
background: var(--bg); background: var(--bg);
@@ -490,40 +612,40 @@ main {
.detail-tag { .detail-tag {
font-family: var(--mono); font-family: var(--mono);
font-size: 11px; font-size: 10px;
color: var(--accent); color: var(--accent);
border: 1px solid currentColor; border: 1px solid rgba(200,148,42,.35);
border-color: color-mix(in srgb, var(--accent) 38%, transparent);
background: var(--a-bg); background: var(--a-bg);
padding: 2px 8px; padding: 2px 7px;
border-radius: var(--r1); border-radius: var(--r1);
} }
.detail-actions { .detail-actions {
display: flex; display: flex;
gap: 6px; gap: 5px;
flex-wrap: wrap; flex-wrap: wrap;
border-top: 1px solid var(--soft);
padding-top: 12px;
} }
.action-btn { .action-btn {
background: none; font-family: var(--sans);
font-size: 11px;
font-weight: 500;
border: 1px solid var(--border); border: 1px solid var(--border);
color: var(--muted); color: var(--muted);
padding: 4px 12px; padding: 4px 11px;
border-radius: var(--r1); border-radius: var(--r1);
cursor: pointer;
font-size: 11px;
font-family: var(--mono);
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 4px; gap: 5px;
transition: color var(--t-fast), border-color var(--t-fast); transition: color var(--t-fast), border-color var(--t-fast);
} }
.action-btn:hover { border-color: var(--accent); color: var(--accent); } .action-btn:hover { color: var(--accent); border-color: var(--accent); }
.action-btn.primary { border-color: var(--accent); color: var(--accent); background: var(--a-bg); } .action-btn.primary { color: var(--accent); border-color: var(--accent); background: var(--a-bg); }
.action-btn.danger { color: var(--danger); border-color: var(--danger); } .action-btn.danger { color: var(--danger); border-color: rgba(184,88,88,.4); }
.action-btn.danger:hover { background: color-mix(in srgb, var(--danger) 8%, transparent); } .action-btn.danger:hover { border-color: var(--danger); }
/* ── TEMPLATE SLOTS ─────────────────────────────────── */ /* ── TEMPLATE SLOTS ─────────────────────────────────── */
.slot-form { margin: 16px 0; } .slot-form { margin: 16px 0; }
@@ -591,7 +713,7 @@ main {
.modal-backdrop { .modal-backdrop {
position: fixed; position: fixed;
inset: 0; inset: 0;
background: rgba(0, 0, 0, 0.6); background: rgba(0,0,0,.65);
z-index: 100; z-index: 100;
} }
@@ -603,17 +725,28 @@ main {
background: var(--surf); background: var(--surf);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--r3); border-radius: var(--r3);
padding: 24px; padding: 22px;
z-index: 101; z-index: 101;
min-width: 320px; min-width: 300px;
box-shadow: 0 20px 60px rgba(0,0,0,.5);
} }
.modal-content h3 { .modal-content h3 {
font-family: var(--mono); font-family: var(--mono);
font-size: 13px; font-size: 12px;
color: var(--text);
margin-bottom: 16px;
font-weight: 500; font-weight: 500;
color: var(--text);
margin-bottom: 4px;
}
.modal-sub {
font-family: var(--mono);
font-size: 10px;
color: var(--muted);
margin-bottom: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
.type-picker { .type-picker {
@@ -625,34 +758,38 @@ main {
.type-btn { .type-btn {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 10px;
padding: 8px 14px; padding: 8px 12px;
background: var(--bg); background: var(--bg);
border: 1px solid var(--border); border: 1px solid var(--border);
color: var(--text);
border-radius: var(--r2); border-radius: var(--r2);
cursor: pointer; text-align: left;
font-size: 12px; font-size: 12px;
font-family: var(--mono); font-family: var(--mono);
color: var(--text);
transition: border-color var(--t-fast), background var(--t-fast); transition: border-color var(--t-fast), background var(--t-fast);
} }
.type-btn:hover { border-color: var(--accent); background: var(--raised); } .type-btn:hover { border-color: var(--accent); background: var(--raised); }
.type-btn.suggested { border-color: var(--accent); background: var(--a-bg); } .type-btn.suggested { border-color: var(--accent); background: var(--a-bg); }
.type-glyph { font-size: 14px; width: 20px; text-align: center; } .type-glyph { font-size: 13px; width: 16px; flex-shrink: 0; }
.type-hint {
font-family: var(--sans);
font-size: 11px;
color: var(--muted);
}
.modal-close { .modal-close {
display: block; display: block;
width: 100%; width: 100%;
margin-top: 12px; margin-top: 12px;
padding: 6px; padding: 5px;
background: none;
border: none;
color: var(--dim); color: var(--dim);
font-size: 10px; font-size: 9px;
cursor: pointer;
font-family: var(--mono); font-family: var(--mono);
text-align: center;
transition: color var(--t-fast); transition: color var(--t-fast);
} }
@@ -670,7 +807,6 @@ main {
color: var(--dim); color: var(--dim);
padding: 4px 20px; padding: 4px 20px;
border-radius: var(--r1); border-radius: var(--r1);
cursor: pointer;
font-family: var(--mono); font-family: var(--mono);
font-size: 11px; font-size: 11px;
transition: color var(--t-fast), border-color var(--t-fast); transition: color var(--t-fast), border-color var(--t-fast);
@@ -689,7 +825,6 @@ main {
align-items: center; align-items: center;
gap: 8px; gap: 8px;
padding: 6px 12px; padding: 6px 12px;
cursor: pointer;
border-radius: var(--r2); border-radius: var(--r2);
transition: background var(--t-fast); transition: background var(--t-fast);
} }
@@ -703,6 +838,29 @@ main {
text-overflow: ellipsis; text-overflow: ellipsis;
} }
/* ── TOAST ──────────────────────────────────────────── */
.toast {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: var(--raised);
border: 1px solid var(--border);
border-radius: var(--r2);
padding: 6px 16px;
font-family: var(--mono);
font-size: 11px;
color: var(--text);
z-index: 300;
box-shadow: 0 4px 16px rgba(0,0,0,.4);
animation: toast-in .16s ease-out;
}
@keyframes toast-in {
from { opacity: 0; transform: translate(-50%, 6px); }
to { opacity: 1; transform: translate(-50%, 0); }
}
/* ── RESPONSIVE ─────────────────────────────────────── */ /* ── RESPONSIVE ─────────────────────────────────────── */
@media (max-width: 900px) { @media (max-width: 900px) {
main { grid-template-columns: 1fr; } main { grid-template-columns: 1fr; }