- src/cli.ts: load config, fetch board, write dist/index.html, copy static - static/board.css: minimal raw CSS, system fonts, light/dark via prefers-color-scheme - static/board.js: client-side filter, drawer (lazy-fetch + marked@14 from esm.sh), toggle-archived, refresh-info; ESM module - npm run build → runs cli with --experimental-strip-types CSS/JS = [skip-tdd: visual]. CLI = plumbing wrapper (~30 LOC of pure FS+orchestration calling already-tested functions; over 10-line wrapper-skip threshold but no branching logic — would be tested via smoke / e2e, not unit). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
94 lines
3.1 KiB
JavaScript
94 lines
3.1 KiB
JavaScript
import { marked } from 'https://esm.sh/marked@14';
|
|
|
|
const recordsEl = document.getElementById('records');
|
|
const records = recordsEl ? JSON.parse(recordsEl.textContent || '[]') : [];
|
|
|
|
const filterInput = document.querySelector('.filter');
|
|
const toggleArchived = document.querySelector('.toggle-archived');
|
|
const toggleGrouping = document.querySelector('.toggle-grouping');
|
|
const drawer = document.querySelector('.drawer');
|
|
const drawerContent = drawer?.querySelector('.drawer-content');
|
|
const drawerClose = drawer?.querySelector('.drawer-close');
|
|
const refreshInfo = document.querySelector('.refresh-info');
|
|
const header = document.querySelector('header');
|
|
|
|
const generatedAt = header?.dataset.generated ? new Date(header.dataset.generated) : null;
|
|
const NEXT_TICK_MIN = 5;
|
|
|
|
function relativeMin(ms) {
|
|
return Math.max(0, Math.floor(ms / 60000));
|
|
}
|
|
|
|
function updateRefreshInfo() {
|
|
if (!refreshInfo || !generatedAt) return;
|
|
const ageMin = relativeMin(Date.now() - generatedAt.getTime());
|
|
const nextMin = Math.max(0, NEXT_TICK_MIN - ageMin);
|
|
refreshInfo.textContent = `refreshed ${ageMin}m ago · next in ${nextMin}m`;
|
|
}
|
|
updateRefreshInfo();
|
|
setInterval(updateRefreshInfo, 30_000);
|
|
|
|
function applyFilter(query) {
|
|
const q = query.trim().toLowerCase();
|
|
document.querySelectorAll('.card').forEach((card) => {
|
|
const slug = card.dataset.slug || '';
|
|
const project = card.querySelector('.badge.project')?.textContent || '';
|
|
const title = card.querySelector('.card-title')?.textContent || '';
|
|
const hay = `${slug} ${project} ${title}`.toLowerCase();
|
|
card.dataset.hidden = q === '' || hay.includes(q) ? 'false' : 'true';
|
|
});
|
|
}
|
|
|
|
filterInput?.addEventListener('input', (e) => {
|
|
applyFilter(e.target.value);
|
|
});
|
|
|
|
toggleArchived?.addEventListener('change', (e) => {
|
|
document.body.classList.toggle('show-archived', e.target.checked);
|
|
});
|
|
|
|
toggleGrouping?.addEventListener('change', (e) => {
|
|
document.body.classList.toggle('group-by-project', e.target.checked);
|
|
});
|
|
|
|
async function openDrawer(card) {
|
|
if (!drawer || !drawerContent) return;
|
|
const rawUrl = card.dataset.rawUrl;
|
|
const slug = card.dataset.slug;
|
|
if (!rawUrl) return;
|
|
drawerContent.innerHTML = `<h2>${slug}</h2><p>loading…</p>`;
|
|
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>`;
|
|
}
|
|
}
|
|
|
|
document.addEventListener('click', (e) => {
|
|
const card = e.target.closest?.('.card');
|
|
if (card) openDrawer(card);
|
|
});
|
|
|
|
drawerClose?.addEventListener('click', () => {
|
|
if (drawer) drawer.hidden = true;
|
|
});
|
|
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape' && drawer) drawer.hidden = true;
|
|
});
|
|
|
|
function escapeHtml(s) {
|
|
return s
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
console.log(`board-viewer: ${records.length} records loaded`);
|