SPB Git forge

spb/rareindex

Public
54commits 1branches 0releases
7.1 MBsize
maindefault branch
10 days agolast push
TypeScript 61.9% HTML 37.2% SQL 0.7%
4.5 KB · 81 lines tsx
Raw Blame History
1import Link from 'next/link';2import type { IndexRow } from '@/lib/queries/indices';3import { fmtNum, cn, fmtPct, deltaClass } from '@/lib/format';45/**6 * Live tape (§163, §194): a terminal-style ticker of the PUBLISHED indices only — value + daily change.7 * Indices that have not met their constituent minimum are not in the tape; they are listed in a secondary8 * compact "Indices in development" line with their constituent progress. Pauses on hover/focus; static under9 * reduced motion.10 */11export function IndexTape({ indices, className }: { indices: IndexRow[]; className?: string }) {12  if (!indices.length) return null;13  const published = indices.filter((i) => i.latest).sort((a, b) => Number(b.isFlagship) - Number(a.isFlagship) || a.ticker.localeCompare(b.ticker));14  const developing = indices.filter((i) => !i.latest).sort((a, b) => b.pricedAssets / Math.max(1, b.minConstituents) - a.pricedAssets / Math.max(1, a.minConstituents));15  const items = published.map((i) => {16    const up = (i.change1d ?? 0) > 0;17    const down = (i.change1d ?? 0) < 0;18    return (19      <Link key={i.ticker} href={`/rareindex/${i.ticker}`} className="mono-num flex h-9 shrink-0 items-center gap-2.5 border-r border-border px-4 text-[12px] hover:bg-inset" aria-label={`${i.ticker} ${fmtNum(i.latest!.value, { digits: 2 })} ${fmtPct(i.change1d)}`}>20        <span className="inline-block h-2 w-2 rounded-[2px]" style={{ background: i.color ?? 'var(--ri-index)' }} aria-hidden />21        <span className="font-semibold tracking-wide text-fg">{i.ticker}</span>22        <span className="text-muted">{fmtNum(i.latest!.value, { digits: 2 })}</span>23        <span className={cn('font-medium', deltaClass(i.change1d))}>24          {up ? '▲' : down ? '▼' : '■'} {fmtPct(i.change1d)}25        </span>26      </Link>27    );28  });29  return (30    <div className={className}>31      {items.length ? (32        <div className="relative overflow-hidden border-y border-border bg-elevated" aria-label="Index tape" role="marquee">33          <div className="pointer-events-none absolute inset-y-0 left-0 z-10 w-8 bg-gradient-to-r from-elevated to-transparent" aria-hidden />34          <div className="pointer-events-none absolute inset-y-0 right-0 z-10 w-8 bg-gradient-to-l from-elevated to-transparent" aria-hidden />35          <div className="tape-track flex w-max">36            {items}37            {items.map((it, k) => (38              <span key={`dup-${k}`} aria-hidden className="contents">39                {it}40              </span>41            ))}42          </div>43        </div>44      ) : (45        <div className="border-y border-border bg-elevated px-4 py-2 text-[12px] text-muted sm:px-6">No index has met its publication minimum yet — see the indices in development below.</div>46      )}47      {developing.length ? <DevelopingStrip items={developing} /> : null}48    </div>49  );50}5152/** Secondary line (§194): indices not yet published, with constituent progress. Compact, no marquee. */53function DevelopingStrip({ items }: { items: IndexRow[] }) {54  return (55    <details className="group border-b border-border bg-sunken px-4 text-[11px] text-muted sm:px-6">56      <summary className="flex h-7 cursor-pointer list-none items-center gap-2 [&::-webkit-details-marker]:hidden">57        <span aria-hidden className="inline-block text-subtle transition-transform group-open:rotate-90">▸</span>58        <span className="font-medium uppercase tracking-wider text-subtle">Indices in development</span>59        <span className="num text-subtle">{items.length}</span>60        <span className="hidden text-subtle sm:inline">· publish once each has its minimum number of priced constituents</span>61        <Link href="/methodology#indices" className="ml-auto text-subtle hover:text-fg">62          Methodology →63        </Link>64      </summary>65      <ul className="scrollbar-none -mx-4 flex gap-x-4 overflow-x-auto px-4 pb-2 sm:mx-0 sm:flex-wrap sm:px-0">66        {items.map((i) => (67          <li key={i.ticker} className="mono-num shrink-0">68            <Link href={`/rareindex/${i.ticker}`} className="inline-flex items-center gap-1.5 hover:text-fg" title={`${i.name}: ${i.pricedAssets} priced constituents of ${i.minConstituents} required`}>69              <span className="inline-block h-1.5 w-1.5 rounded-[2px]" style={{ background: i.color ?? 'var(--ri-index)' }} aria-hidden />70              <span className="text-fg">{i.ticker}</span>71              <span className="text-subtle">72                {i.pricedAssets}/{i.minConstituents}73              </span>74            </Link>75          </li>76        ))}77      </ul>78    </details>79  );80}81