feat(viewer): bundle md + slug + conditional owner pill [v0.2.0]
Closes 3 of 6 UX-round1 fixes in one TDD cycle (49→56 tests). drawer-bundle-md: TaskRecord.md_content fetched in build-time; drawer reads recordsBySlug instead of fetch — 0 runtime hits to git.kzntsv.site/raw (private repos 401'd browser fetches without token). Fallback dl-block when md=null. ux-show-slug: <code class="card-slug"> rendered under card-title in both render.ts and board.js renderCard (re-render path). ux-owner-conditional: distinctOwner(records) pre-scan. Single owner → pill omitted on cards, header gets <span class="owner-summary"> Owner: X</span>. Multi-owner → pill restored, no header summary. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -287,3 +287,44 @@ aside.drawer[hidden] {
|
||||
color: var(--muted);
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.card-slug {
|
||||
display: block;
|
||||
font: 0.7rem/1.3 ui-monospace, SFMono-Regular, Consolas, monospace;
|
||||
color: var(--muted);
|
||||
margin-bottom: 0.35rem;
|
||||
word-break: break-all;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.owner-summary {
|
||||
font-size: 0.8rem;
|
||||
color: var(--muted);
|
||||
border: 1px solid var(--border);
|
||||
padding: 0.1rem 0.55rem;
|
||||
border-radius: 999px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.drawer-empty {
|
||||
color: var(--muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.status-block {
|
||||
display: grid;
|
||||
grid-template-columns: max-content 1fr;
|
||||
gap: 0.3rem 0.8rem;
|
||||
font-size: 0.85rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.status-block dt {
|
||||
color: var(--muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-block dd {
|
||||
margin: 0;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,12 @@ const STATUS_EMOJI = {
|
||||
|
||||
const ARCHIVE_DAYS = 14;
|
||||
|
||||
const SOLO_OWNER = (() => {
|
||||
const owners = new Set();
|
||||
for (const r of records) if (r.project_owner) owners.add(r.project_owner);
|
||||
return owners.size === 1 ? [...owners][0] : null;
|
||||
})();
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s)
|
||||
.replace(/&/g, '&')
|
||||
@@ -79,7 +85,7 @@ function renderCard(rec, now, mode) {
|
||||
const primary = mode === 'by-project'
|
||||
? `<span class="badge status">${STATUS_EMOJI[rec.status]} ${STATUS_LABEL[rec.status]}</span>`
|
||||
: `<span class="badge project">${escapeHtml(rec.project)}</span>`;
|
||||
const ownerPill = rec.project_owner
|
||||
const ownerPill = rec.project_owner && SOLO_OWNER === null
|
||||
? `<span class="badge owner">${escapeHtml(rec.project_owner)}</span>`
|
||||
: '';
|
||||
const dateSpan = rec.last_commit_iso
|
||||
@@ -87,6 +93,7 @@ function renderCard(rec, now, mode) {
|
||||
: '';
|
||||
return `<li class="card" data-slug="${escapeHtml(rec.slug)}" data-raw-url="${escapeHtml(rec.raw_url)}" data-archived="${archived ? 'true' : 'false'}">
|
||||
<div class="card-title">${escapeHtml(truncate(rec.title, 80))}</div>
|
||||
<code class="card-slug">${escapeHtml(rec.slug)}</code>
|
||||
<div class="card-meta">
|
||||
${primary}
|
||||
${ownerPill}
|
||||
@@ -197,21 +204,46 @@ toggleGrouping?.addEventListener('change', () => {
|
||||
rerender();
|
||||
});
|
||||
|
||||
async function openDrawer(card) {
|
||||
function openDrawer(card) {
|
||||
if (!drawer || !drawerContent) return;
|
||||
const rawUrl = card.dataset.rawUrl;
|
||||
const slug = card.dataset.slug;
|
||||
if (!rawUrl) return;
|
||||
drawerContent.innerHTML = `<h2>${escapeHtml(slug || '')}</h2><p>loading…</p>`;
|
||||
const slug = card.dataset.slug || '';
|
||||
const rec = recordsBySlug.get(slug);
|
||||
drawer.hidden = false;
|
||||
try {
|
||||
const res = await fetch(rawUrl, { credentials: 'omit' });
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const md = await res.text();
|
||||
drawerContent.innerHTML = `<h2>${escapeHtml(slug || '')}</h2>` + marked.parse(md);
|
||||
} catch (err) {
|
||||
drawerContent.innerHTML = `<h2>${escapeHtml(slug || '')}</h2><p>Failed to load: ${escapeHtml(String(err))}</p>`;
|
||||
if (!rec) {
|
||||
drawerContent.innerHTML = `<h2>${escapeHtml(slug)}</h2><p>record not found.</p>`;
|
||||
return;
|
||||
}
|
||||
const md = rec.md_content;
|
||||
if (md === null || md === undefined) {
|
||||
drawerContent.innerHTML =
|
||||
`<h2>${escapeHtml(slug)}</h2>` +
|
||||
`<p class="drawer-empty">No per-task <code>.md</code> file in repo — status block only.</p>` +
|
||||
renderStatusBlock(rec);
|
||||
return;
|
||||
}
|
||||
drawerContent.innerHTML = `<h2>${escapeHtml(slug)}</h2>` + marked.parse(md);
|
||||
}
|
||||
|
||||
function renderStatusBlock(rec) {
|
||||
const rows = [
|
||||
['Status', `${STATUS_EMOJI[rec.status]} ${STATUS_LABEL[rec.status]}`],
|
||||
['Title', rec.title],
|
||||
['Where stopped', rec.where_stopped],
|
||||
['Next action', rec.next_action],
|
||||
['Blocker', rec.blocker],
|
||||
['Branch', rec.branch],
|
||||
];
|
||||
return (
|
||||
'<dl class="status-block">' +
|
||||
rows
|
||||
.filter(([, v]) => v !== null && v !== undefined && v !== '')
|
||||
.map(
|
||||
([k, v]) =>
|
||||
`<dt>${escapeHtml(k)}</dt><dd>${escapeHtml(String(v))}</dd>`,
|
||||
)
|
||||
.join('') +
|
||||
'</dl>'
|
||||
);
|
||||
}
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
|
||||
Reference in New Issue
Block a user