/**
* Feed card archetypes (DESIGN.md §11.2): Sensor, Eye, Track, Number,
* Witness, Room. Every image card carries sensor framing (corner brackets,
* a hairline reticle) and one monospace telemetry strip burned into the
* bottom of the frame. Restraint: one strip per card, never a full HUD.
*/
import { icon } from './icons.js';
import { esc, fmtNum, ago, sparkPath } from './dom.js';
import {
BAND_NAME,
bandOf,
archetypeOf,
telemetryOf,
headlineOf,
realSeries,
sensorId,
cardinal,
fmtKm,
utc,
} from './bands.js';
import { KIND } from './rooms.js';
export const frame = () =>
'';
const reticle = () => '';
function fmtVal(v) {
if (typeof v === 'number') return fmtNum(v, v % 1 ? 1 : 0);
return String(v ?? '');
}
function bandLine(s, extra = []) {
const band = bandOf(s);
const parts = [BAND_NAME[band], ...extra].filter(Boolean);
return `
${parts.map(esc).join('·')}
`;
}
function distBits(s) {
return Number.isFinite(s.distanceKm) ? [`${fmtKm(s.distanceKm)} away`] : [];
}
function liveBit(s) {
return s.live
? ''
: `${s.kind === 'reconstruction' ? '4D · archive' : 'stale'}`;
}
function body(s, { cta = '' } = {}) {
return `
${bandLine(s, [...distBits(s), ago(s.updatedAt)])}
${esc(s.title)}
${esc(s.place || '')}${liveBit(s)}
${cta}
`;
}
/** The shared image frame for a situation card. */
function media(s, ar, { strip = true, inner = '', eye = false } = {}) {
return ``;
}
const hit = (label) =>
``;
/* ---------------- Situation archetypes ---------------- */
function sensorCard(s, ar) {
return `
${hit(`Open ${s.title}, ${s.place || ''}`)}
${media(s, ar)}
${body(s)}
`;
}
function trackCard(s, ar) {
const hl = headlineOf(s);
const harm = /squawk/.test(s.kind) && /7[5-7]00/.test(String(hl?.value));
return `
${hit(`Open ${s.title}`)}
${media(s, ar, {
inner: hl
? `${esc(fmtVal(hl.value))}${hl.unit ? `${esc(hl.unit)}` : ''}${esc(hl.label || '')}
`
: '',
})}
${body(s, { cta: `${icon('ride')}Ride along` })}
`;
}
function numberCard(s, ar) {
const hl = headlineOf(s);
const ser = realSeries(s);
const W = 200;
const H = 54;
const d = ser.length >= 3 ? sparkPath(ser, W, H, 3) : '';
return `
${hit(`Open ${s.title}`)}
${esc(s.title)}
${esc(s.place || '')} · ${esc(ago(s.updatedAt))}
`;
}
function eyeCard(s) {
const hd = s.media?.headingDeg;
return `
${hit(`Open live camera, ${s.title}`)}
${bandLine(s, [...distBits(s), 'Public camera'])}
${esc(s.media?.provider || s.source || '')}
`;
}
/* ---------------- Witness (local captures & live sessions) ---------------- */
function witnessCard(w, ar) {
const sealed = w.sealed;
const live = w.type === 'live' || w.liveNow;
const tele = [
live
? 'WITNESS · LIVE'
: w.unverified
? 'UPLOAD · UNVERIFIED'
: 'WITNESS · SEALED',
w.seq ? `#${w.seq}` : '',
Number.isFinite(w.acc) ? `±${Math.round(w.acc)} M` : '',
Number.isFinite(w.km) && w.situation
? `${fmtKm(w.km).toUpperCase()} FROM EVENT`
: '',
]
.filter(Boolean)
.join(' · ');
return `
${hit(w.liveNow ? `Watch ${w.handle}'s live broadcast` : `Open your ${w.type}`)}
${esc(w.liveNow ? 'Live from a person' : w.unverified ? 'Your upload' : 'Your witness')}·${esc(ago(w.at))}
${esc(w.title)}
${esc(w.sub || '')}
`;
}
function promptCard(p, ar) {
return `
${hit('Open Capture')}
${icon('viewfinder')}Open Capture
`;
}
/* ---------------- Room ---------------- */
function avatar(a, i) {
const h =
[...(a.handle || '?')].reduce((x, c) => x * 31 + c.charCodeAt(0), 7) >>> 0;
const hue = a.self ? 42 : h % 360;
return `${esc((a.handle || '?').replace(/^@/, '')[0].toUpperCase())}`;
}
function roomCard(r, watching = 0) {
const s = r.situation;
const top = r.top;
const k = top ? KIND[top.kind] : null;
return `
${hit(`Join the room for ${s.title}`)}
Room·${esc(s.title)}
${top?.example ? '
Example conversation' : `
${watching > 1 ? `${watching} here` : ''}`}
${top ? `
${esc(top.text)}
` : ''}
${icon(r.status.id === 'disputed' ? 'dispute' : r.status.id === 'open' ? 'question' : 'sealFill')}${esc(r.status.label)}
${top ? `${icon(k?.icon || 'note')}${esc(k?.name || '')} · ${esc(top.author.handle)}` : ''}
`;
}
export function cardHtml(item, ar) {
if (item.type === 'room') return roomCard(item.room, item.watching);
if (item.type === 'witness') return witnessCard(item.w, ar);
if (item.type === 'prompt') return promptCard(item.p, ar);
const s = item.s;
const a = item.archetype || archetypeOf(s);
if (a === 'eye') return eyeCard(s);
if (a === 'track') return trackCard(s, ar);
if (a === 'number') return numberCard(s, ar);
return sensorCard(s, ar);
}
export { avatar, utc };