Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 44 additions & 54 deletions site/src/components/parameters/ParameterDetailPage.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
import { useMemo, useEffect, useState } from 'react';
import { getRichParameter, getAllParameters, type RichParameter } from '../../data/loader';
import { getProvenance, type ProvEntry } from '../../data/provenance';
import { Card, CardHeader, CardContent, CardFooter } from '../../ui/card';
import { Card, CardContent } from '../../ui/card';
import { Badge } from '../../ui/badge';
import { Button } from '../../ui/button';
import { RangeBar } from './RangeBar';
import { categoryColors, categoryLabels } from '../../styles/category-colors';
import { ProvenanceBadges } from './provenance/ProvenanceBadges';
import { SourcesExplorer } from './provenance/SourcesExplorer';
import { CorrelationMiniMatrix } from './provenance/CorrelationMiniMatrix';
import { linkifyCitations } from '../../lib/doi';

function nameToSlug(name: string): string {
return name.toLowerCase().replace(/\s+/g, '_').replace(/[()\/]/g, '').replace(/:/, '');
}
import { RelatedParametersList } from './RelatedParametersList';
import { ReferencesList } from './ReferencesList';
import { humanize, categoryKey, paramNameToSlug } from '../../lib/humanize';

interface ParameterDetailPageProps {
paramId: string;
Expand All @@ -38,10 +35,10 @@ function formatList(val: unknown): string[] {
const result: string[] = [];
for (const [k, v] of Object.entries(obj)) {
if (Array.isArray(v)) {
result.push(`**${k.charAt(0).toUpperCase() + k.slice(1)}**:`);
result.push(`**${humanize(k)}**:`);
result.push(...formatList(v).map(s => ` ${s}`));
} else {
result.push(`**${k}**: ${String(v)}`);
result.push(`**${humanize(k)}**: ${String(v)}`);
}
}
return result;
Expand Down Expand Up @@ -99,7 +96,7 @@ function BulletList({ items }: { items: string[] }) {
function InfoRow({ label, value, mono }: { label: string; value: React.ReactNode; mono?: boolean }) {
return (
<div className="flex items-baseline gap-3 py-2 border-b border-gray-100 last:border-0">
<span className="text-xs text-mes-text-muted uppercase tracking-wider w-36 flex-shrink-0">{label}</span>
<span className="text-xs text-mes-text-muted uppercase tracking-wider w-32 flex-shrink-0">{label}</span>
<span className={`text-sm text-mes-text-primary ${mono ? 'font-mono' : ''}`}>{value}</span>
</div>
);
Expand Down Expand Up @@ -144,14 +141,15 @@ export function ParameterDetailPage({ paramId }: ParameterDetailPageProps) {
}

const p = richParam as RichParameter;
const catId = p.category.toLowerCase().replace(/_/g, '-');
const catLabel = categoryLabels[catId] || p.category.replace(/_/g, ' ');
const catId = categoryKey(p.category);
const catLabel = categoryLabels[catId] || humanize(p.category);
const catColor = categoryColors[catId] || '#737373';
const subcatLabel = humanize(p.subcategory ?? '');
const dataTypeLabel = humanize(p.data_type ?? '');

const typicalItems = formatList(p.typical_values);
const methodItems = formatList(p.measurement_methods);
const factorItems = formatList(p.affecting_factors);
const relatedItems = formatList(p.related_parameters);

const issueBase = `https://github.com/${REPO}/issues/new`;
const issueBody = encodeURIComponent(`**Parameter:** ${p.name}\n**Category:** ${catLabel}\n\n`);
Expand All @@ -175,18 +173,25 @@ export function ParameterDetailPage({ paramId }: ParameterDetailPageProps) {

{/* Header */}
<div className="flex flex-col md:flex-row md:items-start md:justify-between gap-4">
<div>
<div className="min-w-0">
<div
className="inline-block w-10 h-1 mb-3"
style={{ backgroundColor: catColor }}
aria-hidden
/>
<h1 className="text-3xl font-serif font-bold text-mes-text-primary">{p.name}</h1>
<p className="mt-2 text-mes-text-secondary text-sm max-w-2xl">
{p.description}
</p>
{p.description && (
<p className="mt-2 text-mes-text-secondary text-sm max-w-2xl">
{p.description}
</p>
)}
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<div className="flex items-center gap-2 flex-shrink-0 flex-wrap">
<Badge variant="outline" size="lg" style={{ borderColor: catColor, color: catColor }}>
{catLabel}
</Badge>
{p.subcategory && (
<Badge variant="gray" size="sm">{p.subcategory}</Badge>
{subcatLabel && (
<Badge variant="gray" size="sm">{subcatLabel}</Badge>
)}
</div>
</div>
Expand All @@ -207,7 +212,7 @@ export function ParameterDetailPage({ paramId }: ParameterDetailPageProps) {
<Card padding="sm" className="text-center">
<span className="text-xs text-mes-text-muted uppercase tracking-wider block">Type</span>
<span className="text-lg font-medium text-mes-text-primary mt-1 block">
{(p.data_type || '').toLowerCase()}
{dataTypeLabel || '--'}
</span>
</Card>
<Card padding="sm" className="text-center">
Expand Down Expand Up @@ -243,7 +248,7 @@ export function ParameterDetailPage({ paramId }: ParameterDetailPageProps) {
</Card>
)}

<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 items-start">
{/* Main content — left 2 columns */}
<div className="lg:col-span-2 space-y-6">
{/* Definition */}
Expand Down Expand Up @@ -344,7 +349,7 @@ export function ParameterDetailPage({ paramId }: ParameterDetailPageProps) {
<Section title="Correlated Parameters">
<CorrelationMiniMatrix
correlations={prov.correlations}
paramNameToSlug={nameToSlug}
paramNameToSlug={paramNameToSlug}
/>
</Section>
</CardContent>
Expand Down Expand Up @@ -392,18 +397,29 @@ export function ParameterDetailPage({ paramId }: ParameterDetailPageProps) {
</CardContent>
</Card>
)}

{/* References */}
{p.references && (
<Card padding="sm">
<CardContent>
<Section title="References">
<ReferencesList text={p.references} />
</Section>
</CardContent>
</Card>
)}
</div>

{/* Sidebar — right column */}
<div className="space-y-6">
<div className="space-y-6 lg:sticky lg:top-6 lg:self-start">
{/* Properties */}
<Card padding="sm">
<CardContent>
<h3 className="text-sm font-semibold text-mes-text-primary mb-3">Properties</h3>
<InfoRow label="Category" value={catLabel} />
{p.subcategory && <InfoRow label="Subcategory" value={p.subcategory} />}
{subcatLabel && <InfoRow label="Subcategory" value={subcatLabel} />}
<InfoRow label="Unit" value={p.unit || 'Dimensionless'} mono />
<InfoRow label="Data type" value={(p.data_type || '').toLowerCase()} />
<InfoRow label="Data type" value={dataTypeLabel || '—'} />
{p.min_value != null && <InfoRow label="Minimum" value={`${p.min_value} ${p.unit || ''}`} mono />}
{p.max_value != null && <InfoRow label="Maximum" value={`${p.max_value} ${p.unit || ''}`} mono />}
<InfoRow label="Papers" value={p.usage_count > 0 ? p.usage_count.toLocaleString() : 'None yet'} />
Expand All @@ -421,37 +437,11 @@ export function ParameterDetailPage({ paramId }: ParameterDetailPageProps) {
)}

{/* Related Parameters */}
{relatedItems.length > 0 && (
{!!p.related_parameters && (
<Card padding="sm">
<CardContent>
<h3 className="text-sm font-semibold text-mes-text-primary mb-3">Related Parameters</h3>
<BulletList items={relatedItems} />
</CardContent>
</Card>
)}

{/* References */}
{p.references && (
<Card padding="sm">
<CardContent>
<h3 className="text-sm font-semibold text-mes-text-primary mb-3">References</h3>
<div className="text-xs text-mes-text-secondary leading-relaxed whitespace-pre-line">
{linkifyCitations(p.references).map((seg, i) =>
seg.href ? (
<a
key={i}
href={seg.href}
target="_blank"
rel="noopener noreferrer"
className="text-mes-text-link hover:underline break-all"
>
{seg.text}
</a>
) : (
<span key={i}>{seg.text}</span>
)
)}
</div>
<RelatedParametersList related={p.related_parameters} />
</CardContent>
</Card>
)}
Expand Down
7 changes: 4 additions & 3 deletions site/src/components/parameters/ParameterTable.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useState } from 'react';
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '../../ui/table';
import { Badge } from '../../ui/badge';
import { categoryColors } from '../../styles/category-colors';
import { categoryColors, categoryLabels } from '../../styles/category-colors';
import { humanize } from '../../lib/humanize';
import type { Parameter } from '../../data/types';

type SortKey = 'name' | 'unit' | 'category' | 'subcategory' | 'typical';
Expand Down Expand Up @@ -106,11 +107,11 @@ function ParameterTableRow({
size="sm"
style={{ borderColor: categoryColors[p.category] || '#737373', color: categoryColors[p.category] || '#737373' }}
>
{p.category}
{categoryLabels[p.category] || humanize(p.category)}
</Badge>
</TableCell>
<TableCell className="hidden lg:table-cell text-mes-text-muted text-xs">
{p.subcategory}
{humanize(p.subcategory)}
</TableCell>
<TableCell className="hidden sm:table-cell">
{p.range?.typical != null ? `${p.range.typical}` : '-'}
Expand Down
90 changes: 90 additions & 0 deletions site/src/components/parameters/ReferencesList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { useMemo } from 'react';
import { parseReferences, referenceHref, linkifyCitations } from '../../lib/citations';

interface Props {
text: string | null | undefined;
}

// Render the citation body. Embedded DOIs and URLs are linkified in place
// so the original citation typography (authors, year, journal) reads as it
// was authored.
function Inline({ text }: { text: string }) {
const segs = linkifyCitations(text);
return (
<>
{segs.map((seg, i) =>
seg.href ? (
<a
key={i}
href={seg.href}
target="_blank"
rel="noopener noreferrer"
className="text-mes-text-link hover:underline break-words"
>
{seg.text}
</a>
) : (
<span key={i}>{seg.text}</span>
)
)}
</>
);
}

// Drop markdown emphasis so the rendered citation isn't littered with
// asterisks/underscores. Bolding the journal/title isn't useful here.
function stripMd(s: string): string {
return s
.replace(/\*\*(.+?)\*\*/g, '$1')
.replace(/__(.+?)__/g, '$1')
.replace(/\*(.+?)\*/g, '$1')
.replace(/_(.+?)_/g, '$1');
}

export function ReferencesList({ text }: Props) {
const refs = useMemo(() => parseReferences(text), [text]);

if (refs.length === 0) return null;

return (
<ol className="space-y-3 list-none pl-0">
{refs.map((ref, i) => {
const href = referenceHref(ref);
const cleaned = stripMd(ref.raw);
const action = ref.doi
? { label: 'Open paper (DOI)', href: href! }
: ref.url
? { label: 'Open source', href: href! }
: href
? { label: 'Search on Google Scholar', href }
: null;
return (
<li
key={i}
className="grid grid-cols-[1.5rem,1fr] gap-2 text-xs text-mes-text-secondary leading-relaxed"
>
<span className="font-mono text-mes-text-muted tabular-nums select-none pt-px">
{i + 1}.
</span>
<div className="space-y-1 min-w-0">
<div className="whitespace-pre-line break-words">
<Inline text={cleaned} />
</div>
{action && (
<a
href={action.href}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-[11px] text-mes-text-link hover:underline"
>
{action.label}
<span aria-hidden>→</span>
</a>
)}
</div>
</li>
);
})}
</ol>
);
}
Loading
Loading