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%
3.3 KB · 61 lines tsx
Raw Blame History
1import Link from 'next/link';2import type { ReactNode } from 'react';3import { cn } from '@/lib/format';45export interface ExplainerRow {6  /** component name, e.g. "Sales per month" */7  label: ReactNode;8  /** the asset's actual input, or null when RareIndex does not have it (rendered "not available") */9  value: ReactNode | null;10  /** weight in the formula, e.g. "40 %" */11  weight?: ReactNode;12  /** how the input maps to 0–1, e.g. "30 / month → 1" */13  note?: ReactNode;14}1516/**17 * §204 "Explain every score": a native <details> (no client JS) that opens the components of a score18 * with the asset's real inputs and links to the matching Methodology section. Inputs RareIndex does19 * not store are shown as "not available" — a component is never filled with a guessed value.20 */21export function ScoreExplainer({ summary, title, anchor, formula, rows, footnote, className }: { summary?: ReactNode; title: ReactNode; anchor: string; formula?: ReactNode; rows: ExplainerRow[]; footnote?: ReactNode; className?: string }) {22  return (23    <details className={cn('group text-[11px]', className)}>24      <summary className="inline-flex cursor-pointer list-none items-center gap-1 text-subtle underline-offset-2 hover:text-fg hover:underline [&::-webkit-details-marker]:hidden">25        <span aria-hidden className="inline-block transition-transform group-open:rotate-90">▸</span>26        {summary ?? 'How is this computed?'}27      </summary>28      <div className="mt-2 rounded-sm border border-border bg-sunken p-2.5">29        <div className="flex items-baseline justify-between gap-2">30          <p className="font-semibold text-fg">{title}</p>31          <Link href={`/methodology#${anchor}`} className="shrink-0 text-subtle hover:text-fg hover:underline">32            Methodology →33          </Link>34        </div>35        {formula ? <p className="mono-num mt-1 text-[10.5px] leading-snug text-muted">{formula}</p> : null}36        <table className="mt-2 w-full border-collapse">37          <thead>38            <tr className="text-left text-[10px] uppercase tracking-wider text-subtle">39              <th className="py-0.5 pr-2 font-medium">Component</th>40              <th className="py-0.5 pr-2 text-right font-medium">This asset</th>41              {rows.some((r) => r.weight !== undefined) ? <th className="py-0.5 pr-2 text-right font-medium">Weight</th> : null}42              {rows.some((r) => r.note !== undefined) ? <th className="py-0.5 font-medium">Scale</th> : null}43            </tr>44          </thead>45          <tbody className="divide-y divide-border">46            {rows.map((r, i) => (47              <tr key={i}>48                <td className="py-1 pr-2 text-muted">{r.label}</td>49                <td className="num py-1 pr-2 text-right text-fg">{r.value === null || r.value === undefined ? <span className="text-subtle">not available</span> : r.value}</td>50                {rows.some((x) => x.weight !== undefined) ? <td className="num py-1 pr-2 text-right text-muted">{r.weight ?? ''}</td> : null}51                {rows.some((x) => x.note !== undefined) ? <td className="py-1 text-subtle">{r.note ?? ''}</td> : null}52              </tr>53            ))}54          </tbody>55        </table>56        {footnote ? <p className="mt-2 leading-snug text-subtle">{footnote}</p> : null}57      </div>58    </details>59  );60}61