SPB Git forge

spb/ai-atlas

Public
41commits 1branches 0releases
4.6 MBsize
maindefault branch
12 days agolast push
HTML 77.2% TypeScript 10.5% Python 9.6% JavaScript 2.5%
2.7 KB · 57 lines tsx
Raw Blame History
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