import type { Save } from "../mirror/queries.ts"; import { type Html, html } from "./html.ts"; import { formatDate } from "./layout.ts"; const TIP_CHARS = 120; /** One dot of the drifting timeline, as kepr.js reads it from `//timeline`. */ export type TimelineSave = { id: string; t: number; message: string; meta: string }; /** * §21.4's dotted timeline as SVG, so it draws without JavaScript: one dot per save on a dashed * line, x by time (as a percentage, so it scales without squashing dots), brighter and fuller where * neighbours crowd within a fortieth of the span. kepr.js swaps it for Kepr 2's drifting canvas, * fetching each save's words from `data-src` after the page has drawn: carried as tooltips in the * page, they were most of a big repo's page. */ export function timelineSvg(repo: string, saves: Save[]): Html { const times = saves.map((save) => save.created_at); // Replayed saves keep their own times, so the line is not in time order. const sorted = times.toSorted((a, b) => a - b); const first = sorted[0] ?? 0; const span = (sorted.at(-1) ?? first) - first; const reach = Math.max(span / 40, 3600); const dots = times.map((t) => { const x = span === 0 ? 50 : 1 + (98 * (t - first)) / span; const near = countBetween(sorted, t - reach, t + reach); const level = near >= 8 ? 3 : near >= 4 ? 2 : near >= 2 ? 1 : 0; return html``; }); return html` ${dots} `; } /** Each save's ID, time, first line (cut to fit the 340 px tip) and when, who and in what session. */ export function timelineSaves(saves: Save[]): TimelineSave[] { return saves.map((save) => { const line = save.message.split("\n")[0] as string; return { id: save.id, t: save.created_at, message: line.length > TIP_CHARS ? `${line.slice(0, TIP_CHARS - 1)}…` : line, meta: [formatDate(save.created_at), save.agent, save.session].filter(Boolean).join(" · "), }; }); } // How many of the sorted times fall within [low, high]. function countBetween(sorted: number[], low: number, high: number): number { // The index of the first time that `before` no longer holds for. const first = (before: (time: number) => boolean) => { let [lo, hi] = [0, sorted.length]; while (lo < hi) { const mid = (lo + hi) >> 1; if (before(sorted[mid] as number)) lo = mid + 1; else hi = mid; } return lo; }; return first((t) => t <= high) - first((t) => t < low); }