TypeScript 61.9%
HTML 37.2%
SQL 0.7%
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