feat(viewer): MSK datetime + done-cutoff expander [v0.3.0]
Closes 2 more UX-round1 fixes (5/6 done; task-numbers remains design-first cross-repo, awaiting user decision). ux-full-datetime: formatMskDateTime(iso) → "YYYY-MM-DD HH:MM МСК". UTC+3 fixed (no-DST since 2011). Tooltip retains full ISO for copy-paste. Relative age stays in card-meta. Mirrored in board.js renderCard re-render path. ux-done-cutoff: source unchanged (render-side cutoff). Done column sorts by last_commit_iso desc, first 10 visible, rest get data-overflow="true" (CSS hides). Column bottom gets "show N more" expander; click toggles col.show-overflow class + button text "collapse". Non-done columns untouched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -210,6 +210,33 @@ body.show-archived .card[data-archived="true"] {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.card[data-overflow="true"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.col.show-overflow .card[data-overflow="true"] {
|
||||
display: block;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.done-expander {
|
||||
align-self: flex-start;
|
||||
margin-top: 0.4rem;
|
||||
font: inherit;
|
||||
font-size: 0.75rem;
|
||||
padding: 0.2rem 0.6rem;
|
||||
background: transparent;
|
||||
border: 1px dashed var(--border);
|
||||
border-radius: var(--radius);
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.done-expander:hover {
|
||||
border-style: solid;
|
||||
border-color: var(--muted);
|
||||
}
|
||||
|
||||
.card[data-hidden="true"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ const STATUS_EMOJI = {
|
||||
};
|
||||
|
||||
const ARCHIVE_DAYS = 14;
|
||||
const DONE_VISIBLE_LIMIT = 10;
|
||||
|
||||
const SOLO_OWNER = (() => {
|
||||
const owners = new Set();
|
||||
@@ -79,7 +80,14 @@ function truncate(s, max) {
|
||||
return s.length <= max ? s : s.slice(0, max) + '…';
|
||||
}
|
||||
|
||||
function renderCard(rec, now, mode) {
|
||||
function formatMskDateTime(iso) {
|
||||
const utc = new Date(iso);
|
||||
const msk = new Date(utc.getTime() + 3 * 60 * 60 * 1000);
|
||||
const pad = (n) => String(n).padStart(2, '0');
|
||||
return `${msk.getUTCFullYear()}-${pad(msk.getUTCMonth() + 1)}-${pad(msk.getUTCDate())} ${pad(msk.getUTCHours())}:${pad(msk.getUTCMinutes())} МСК`;
|
||||
}
|
||||
|
||||
function renderCard(rec, now, mode, overflow) {
|
||||
const age = formatAge(rec.last_commit_iso, now);
|
||||
const archived = isArchived(rec, now);
|
||||
const primary = mode === 'by-project'
|
||||
@@ -89,9 +97,9 @@ function renderCard(rec, now, mode) {
|
||||
? `<span class="badge owner">${escapeHtml(rec.project_owner)}</span>`
|
||||
: '';
|
||||
const dateSpan = rec.last_commit_iso
|
||||
? `<span class="date" title="${escapeHtml(rec.last_commit_iso)}">${escapeHtml(rec.last_commit_iso.slice(0, 10))}</span>`
|
||||
? `<span class="date" title="${escapeHtml(rec.last_commit_iso)}">${escapeHtml(formatMskDateTime(rec.last_commit_iso))}</span>`
|
||||
: '';
|
||||
return `<li class="card" data-slug="${escapeHtml(rec.slug)}" data-raw-url="${escapeHtml(rec.raw_url)}" data-archived="${archived ? 'true' : 'false'}">
|
||||
return `<li class="card" data-slug="${escapeHtml(rec.slug)}" data-raw-url="${escapeHtml(rec.raw_url)}" data-archived="${archived ? 'true' : 'false'}" data-overflow="${overflow ? 'true' : 'false'}">
|
||||
<div class="card-title">${escapeHtml(truncate(rec.title, 80))}</div>
|
||||
<code class="card-slug">${escapeHtml(rec.slug)}</code>
|
||||
<div class="card-meta">
|
||||
@@ -103,13 +111,32 @@ function renderCard(rec, now, mode) {
|
||||
</li>`;
|
||||
}
|
||||
|
||||
function compareByIsoDesc(a, b) {
|
||||
if (!a.last_commit_iso && !b.last_commit_iso) return 0;
|
||||
if (!a.last_commit_iso) return 1;
|
||||
if (!b.last_commit_iso) return -1;
|
||||
return b.last_commit_iso.localeCompare(a.last_commit_iso);
|
||||
}
|
||||
|
||||
function renderByStatus(now) {
|
||||
return STATUS_ORDER.map((status) => {
|
||||
const inCol = records.filter((r) => r.status === status);
|
||||
const cards = inCol.map((r) => renderCard(r, now, 'by-status')).join('');
|
||||
let inCol = records.filter((r) => r.status === status);
|
||||
let overflowStart = -1;
|
||||
if (status === 'done') {
|
||||
inCol = [...inCol].sort(compareByIsoDesc);
|
||||
if (inCol.length > DONE_VISIBLE_LIMIT) overflowStart = DONE_VISIBLE_LIMIT;
|
||||
}
|
||||
const cards = inCol
|
||||
.map((r, i) => renderCard(r, now, 'by-status', overflowStart !== -1 && i >= overflowStart))
|
||||
.join('');
|
||||
const overflowCount = overflowStart === -1 ? 0 : inCol.length - overflowStart;
|
||||
const expander =
|
||||
overflowCount > 0
|
||||
? `<button type="button" class="done-expander">show ${overflowCount} more</button>`
|
||||
: '';
|
||||
return `<section class="col" data-status="${status}">
|
||||
<h2>${STATUS_EMOJI[status]} ${STATUS_LABEL[status]} <span class="count">${inCol.length}</span></h2>
|
||||
<ul class="cards">${cards}</ul>
|
||||
<ul class="cards">${cards}</ul>${expander}
|
||||
</section>`;
|
||||
}).join('');
|
||||
}
|
||||
@@ -120,7 +147,7 @@ function renderByProject(now) {
|
||||
const inCol = records
|
||||
.filter((r) => r.project === project)
|
||||
.sort((a, b) => STATUS_ORDER.indexOf(a.status) - STATUS_ORDER.indexOf(b.status));
|
||||
const cards = inCol.map((r) => renderCard(r, now, 'by-project')).join('');
|
||||
const cards = inCol.map((r) => renderCard(r, now, 'by-project', false)).join('');
|
||||
return `<section class="col" data-project="${escapeHtml(project)}">
|
||||
<h2>${escapeHtml(project)} <span class="count">${inCol.length}</span></h2>
|
||||
<ul class="cards">${cards}</ul>
|
||||
@@ -247,6 +274,15 @@ function renderStatusBlock(rec) {
|
||||
}
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const expander = e.target.closest?.('.done-expander');
|
||||
if (expander) {
|
||||
const col = expander.closest('.col');
|
||||
if (!col) return;
|
||||
if (!expander.dataset.original) expander.dataset.original = expander.textContent;
|
||||
const expanded = col.classList.toggle('show-overflow');
|
||||
expander.textContent = expanded ? 'collapse' : expander.dataset.original;
|
||||
return;
|
||||
}
|
||||
const card = e.target.closest?.('.card');
|
||||
if (card) openDrawer(card);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user