/** * 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 `
${eye ? '' : ''} ${frame()}${reticle()}
${esc(sensorId(s))}${s.live ? `${eye ? `LIVE ` : 'LIVE'}` : s.kind === 'reconstruction' ? '4D' : ''}
${inner} ${strip ? `
${esc(telemetryOf(s))}
` : ''}
`; } 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}`)}
${frame()}
${bandLine(s, [s.kind === 'gnss' ? 'ADS-B' : '', ...distBits(s)])}
${esc(fmtVal(hl?.value))}${esc(hl?.unit || '')}
${esc(hl?.label || '')}
${d ? `
${esc(s.seriesLabel || 'trend')} · ${ser.length} samples
` : ''}
${esc(telemetryOf(s))}

${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}`)}
${frame()}${reticle()}
LIVE ${esc(sensorId(s))}
${Number.isFinite(hd) ? `` : ''}

${esc(s.title)}

${esc(s.place || '')}
${esc(telemetryOf(s))}${Number.isFinite(hd) ? ` · LOOKING ${cardinal(hd)} ${Math.round(hd)}°` : ''}
${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}`)}
${w.isVideo && !w.frame ? `` : ``} ${frame()}
${w.liveNow ? `LIVE ${icon('users')}${w.viewers || 0}` : `${icon(sealed ? 'sealFill' : 'upload')}`}
${esc(tele)}
${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')}
${frame()}
${icon('viewfinder')}
Inside · you
${esc(p.title)} ${esc(p.text)}
${esc(p.tele)}
${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)}` : ''}
${r.participants.slice(0, 5).map(avatar).join('')} ${r.participants.length} people · ${r.posts.length} posts${r.openQ.length ? ` · ${r.openQ.length} open question${r.openQ.length > 1 ? 's' : ''}` : ''} ${icon('chevronRight')}
`; } 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 };