feat(ui): redesign to match design handoff prototype #9
+417
-96
@@ -28,6 +28,10 @@
|
||||
activeMonth: null,
|
||||
intent: 'grab',
|
||||
flashId: null,
|
||||
peekMode: 'preview',
|
||||
runChecked: new Set(),
|
||||
fillValues: {},
|
||||
fillActive: 0,
|
||||
};
|
||||
|
||||
const $ = (sel) => document.querySelector(sel);
|
||||
@@ -536,111 +540,350 @@
|
||||
</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>` : ''}
|
||||
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>
|
||||
${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>
|
||||
${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>`;
|
||||
}
|
||||
|
||||
const titleEl = pane.querySelector('.detail-title');
|
||||
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;
|
||||
|
||||
let sections = '';
|
||||
|
||||
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 descEl = pane.querySelector('.detail-desc');
|
||||
if (descEl) descEl.addEventListener('dblclick', () => startEditField('description'));
|
||||
const bodyEl = pane.querySelector('.detail-body');
|
||||
const bodyEl = pane.querySelector('.peek-body[data-id]');
|
||||
if (bodyEl) bodyEl.addEventListener('dblclick', startEditBody);
|
||||
}
|
||||
|
||||
function renderCardContent(e) {
|
||||
if (!e.card_data) return '';
|
||||
let data;
|
||||
try { data = JSON.parse(e.card_data); } catch { return ''; }
|
||||
|
||||
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>`;
|
||||
|
||||
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 '';
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Inline edit ==========
|
||||
|
||||
function startEditBody() {
|
||||
@@ -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);
|
||||
break;
|
||||
case 'p':
|
||||
if (sel && sel.card_type && state.view === 'cards') {
|
||||
nibApp.togglePin(sel.id);
|
||||
} else if (sel && !sel.card_type) {
|
||||
nibApp.showPromote(sel.id);
|
||||
}
|
||||
case 'Enter': {
|
||||
const e = state.entities[state.selectedIndex];
|
||||
if (e) nibApp.copyEntity(e.id);
|
||||
break;
|
||||
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;
|
||||
}
|
||||
|
||||
+269
-57
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user