HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
1import { scaleTime } from 'd3-scale';2import { fmtDate } from '@/lib/format';3import type { ChangeEvent } from '@/lib/types';4import { laneColor, laneOf, occurredAt, TIMELINE_LANES } from './lanes';56/**7 * Mobile counterpart of `TimelineLanes`: one lane, dots coloured by category and sized by importance (server-safe SVG,8 * no interaction — the list below is the interactive surface on small screens). Legend = the lanes present.9 */10export function MobileLaneStrip({ events, className }: { events: ChangeEvent[]; className?: string }) {11 const w = 360;12 const h = 58;13 const pad = { l: 8, r: 8, t: 8, b: 18 };14 const ts = events.map((e) => new Date(occurredAt(e)).getTime()).filter((t) => Number.isFinite(t));15 if (ts.length < 2) return null;16 const lo = Math.min(...ts);17 const hi = Math.max(...ts);18 const x = scaleTime()19 .domain([new Date(lo), new Date(hi === lo ? lo + 86400000 : hi)])20 .range([pad.l, w - pad.r]);21 const ticks = x.ticks(4);22 const present = new Set(events.map(laneOf));23 return (24 <figure className={className} data-mobile-lane-strip>25 <svg viewBox={`0 0 ${w} ${h}`} className="block w-full" role="img" aria-label="Events over time, coloured by category">26 <title>Events over time</title>27 <line x1={pad.l} x2={w - pad.r} y1={h / 2 - 4} y2={h / 2 - 4} stroke="var(--rule-strong)" />28 {ticks.map((t, i) => (29 <g key={i}>30 <line x1={x(t)} x2={x(t)} y1={pad.t} y2={h - pad.b} stroke="var(--rule)" strokeDasharray="2 3" />31 <text x={x(t)} y={h - 5} textAnchor="middle" fontSize={9} fill="var(--ink-3)">32 {t.toLocaleDateString('en-GB', { month: 'short', year: '2-digit', timeZone: 'UTC' })}33 </text>34 </g>35 ))}36 {events.map((e) => {37 const t = new Date(occurredAt(e)).getTime();38 if (!Number.isFinite(t)) return null;39 const jitter = ((e.id.charCodeAt(e.id.length - 1) + e.id.charCodeAt(e.id.length - 2)) % 7) - 3;40 return <circle key={e.id} cx={x(new Date(t))} cy={h / 2 - 4 + jitter} r={1.6 + Math.min(3, e.importance) * 0.9} fill={laneColor(laneOf(e))} fillOpacity={e.importance >= 3 ? 0.95 : 0.6} />;41 })}42 </svg>43 <figcaption className="mt-1 flex flex-wrap gap-x-3 gap-y-0.5 text-[11px] text-ink-3">44 {TIMELINE_LANES.filter((l) => present.has(l.key)).map((l) => (45 <span key={l.key} className="inline-flex items-center gap-1">46 <span className="inline-block size-2 rounded-full" style={{ background: l.color }} aria-hidden />47 {l.label}48 </span>49 ))}50 <span className="ml-auto tnum">51 {fmtDate(new Date(lo).toISOString())} → {fmtDate(new Date(hi).toISOString())}52 </span>53 </figcaption>54 </figure>55 );56}57