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:
vitya
2026-05-22 18:55:43 +03:00
parent fdca0b375a
commit f6a45f4987
6 changed files with 212 additions and 28 deletions

View File

@@ -225,6 +225,77 @@ describe('renderBoard', () => {
expect(html).not.toMatch(/<span class="badge owner"><\/span>/);
});
test('done column: shows only the 10 most recent by last_commit_iso, others get data-overflow', () => {
// 12 done records, distinct iso descending from 2026-05-22 backwards
const records = Array.from({ length: 12 }, (_, i) =>
rec({
slug: `done-${String(i).padStart(2, '0')}`,
status: 'done',
status_emoji: '🟢',
last_commit_iso: `2026-05-${String(22 - i).padStart(2, '0')}T12:00:00Z`,
}),
);
const html = renderBoard(records, { generatedAt: now });
// most-recent 10 (done-00..done-09) visible, last 2 (done-10, done-11) overflow
const overflowSlugs = [...html.matchAll(
/data-slug="(done-\d+)"[^>]*data-overflow="true"/g,
)].map((m) => m[1]);
expect(overflowSlugs.sort()).toEqual(['done-10', 'done-11']);
// first 10 don't carry data-overflow="true"
for (let i = 0; i < 10; i++) {
expect(html).toMatch(
new RegExp(`data-slug="done-${String(i).padStart(2, '0')}"[^>]*data-overflow="false"`),
);
}
});
test('done column with overflow renders a "show N more" expander button', () => {
const records = Array.from({ length: 12 }, (_, i) =>
rec({
slug: `d-${i}`,
status: 'done',
status_emoji: '🟢',
last_commit_iso: `2026-05-${String(22 - i).padStart(2, '0')}T12:00:00Z`,
}),
);
const html = renderBoard(records, { generatedAt: now });
expect(html).toMatch(
/<button[^>]*class="done-expander"[^>]*>[^<]*show 2 more[^<]*<\/button>/,
);
});
test('done column ≤10 records: no expander button rendered', () => {
const records = Array.from({ length: 8 }, (_, i) =>
rec({
slug: `d-${i}`,
status: 'done',
status_emoji: '🟢',
last_commit_iso: `2026-05-${String(22 - i).padStart(2, '0')}T12:00:00Z`,
}),
);
const html = renderBoard(records, { generatedAt: now });
expect(html).not.toContain('done-expander');
});
test('non-done columns are NOT subject to the cutoff (no overflow attr, no expander)', () => {
const records = Array.from({ length: 12 }, (_, i) =>
rec({
slug: `open-${i}`,
status: 'open',
status_emoji: '⚪',
last_commit_iso: `2026-05-${String(22 - i).padStart(2, '0')}T12:00:00Z`,
}),
);
const html = renderBoard(records, { generatedAt: now });
expect(html).not.toMatch(/data-overflow="true"/);
expect(html).not.toContain('done-expander');
});
test('renders slug as a visible <code class="card-slug"> on every card', () => {
const html = renderBoard(
[rec({ slug: 'board-viewer-pointers', title: 'pointers' })],
@@ -234,13 +305,26 @@ describe('renderBoard', () => {
expect(html).toMatch(/<code class="card-slug">board-viewer-pointers<\/code>/);
});
test('renders last_commit_iso as a short YYYY-MM-DD date span', () => {
test('renders last_commit_iso as YYYY-MM-DD HH:MM МСК (Moscow time, UTC+3, no DST)', () => {
const html = renderBoard(
[rec({ slug: 'x', last_commit_iso: '2026-05-19T08:30:00Z' })],
{ generatedAt: now },
);
expect(html).toMatch(/<span class="date"[^>]*>2026-05-19<\/span>/);
// 08:30 UTC + 3 = 11:30 МСК
expect(html).toMatch(/<span class="date"[^>]*>2026-05-19 11:30 МСК<\/span>/);
// tooltip retains full ISO for copy-paste
expect(html).toContain('title="2026-05-19T08:30:00Z"');
});
test('formats Moscow time correctly across midnight UTC rollover', () => {
const html = renderBoard(
[rec({ slug: 'x', last_commit_iso: '2026-05-19T22:00:00Z' })],
{ generatedAt: now },
);
// 22:00 UTC + 3 = 01:00 МСК next day
expect(html).toMatch(/2026-05-20 01:00 МСК/);
});
test('omits date span when last_commit_iso is null', () => {