All checks were successful
synthetic / suite (push) Successful in 56s
The single-operator pilot keeps static projection. Local OnCo use requires a CC BY-NC origin notice, and the slice stays out of public CI. Assistant: grok Assistant-Session: 01a15378-f601-7fe2-a143-8e1500a1d59c
167 lines
6.8 KiB
TypeScript
167 lines
6.8 KiB
TypeScript
import { PageHeader } from "@/components/PageHeader";
|
|
import { RecordLink } from "@/components/RecordLink";
|
|
import type { EntityView, RelationItem } from "@/lib/catalog";
|
|
|
|
/**
|
|
* Generic record page: title, summary, sources, claims, and neighbouring relations.
|
|
* OnCo EntityDetail was not copied. That file special-cases domain panels.
|
|
*/
|
|
|
|
function RelationBlock({
|
|
headingId,
|
|
title,
|
|
relations,
|
|
}: {
|
|
headingId: string;
|
|
title: string;
|
|
relations: RelationItem[];
|
|
}) {
|
|
return (
|
|
<section aria-labelledby={headingId}>
|
|
<h2 id={headingId} className="mb-3 text-lg">{title}</h2>
|
|
{relations.length === 0 ? (
|
|
<p className="text-sm text-muted">No {title.toLowerCase()} relations.</p>
|
|
) : (
|
|
<ul className="space-y-2 text-sm">
|
|
{relations.map((relation) => (
|
|
<li key={relation.id} data-relation={relation.id} data-direction={relation.direction}>
|
|
<span className="text-muted">{relation.wording}</span>
|
|
{" · "}
|
|
<RecordLink link={relation.other} />
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|
|
|
|
export function EntityDetail({ entity }: { entity: EntityView }) {
|
|
const currentIndex = entity.readingPath.findIndex((step) => step.id === entity.id);
|
|
return (
|
|
<>
|
|
<PageHeader kicker={entity.kindLabel} title={entity.title} lede={entity.tldr} />
|
|
<div className="mx-auto max-w-7xl space-y-10 px-4 py-10 sm:px-6">
|
|
{entity.glossary ? (
|
|
<p className="glossary" data-glossary={entity.glossary}>
|
|
<span className="glossary-tip" role="tooltip">{entity.glossary}</span>
|
|
</p>
|
|
) : null}
|
|
<p className="max-w-3xl text-body leading-relaxed">{entity.summary}</p>
|
|
{entity.tags.length > 0 ? (
|
|
<ul className="flex flex-wrap gap-2">
|
|
{entity.tags.map((tag) => (
|
|
<li key={tag} className="chip">{tag}</li>
|
|
))}
|
|
</ul>
|
|
) : null}
|
|
{entity.readingPath.length > 0 ? (
|
|
<nav aria-label="Reading path">
|
|
<ol className="flex flex-wrap gap-2 text-sm">
|
|
{entity.readingPath.map((step, index) => {
|
|
const current = step.id === entity.id;
|
|
const earlier = currentIndex >= 0 && index < currentIndex;
|
|
return (
|
|
<li key={`${step.id}:${index}`}>
|
|
{index > 0 ? <span className="text-muted"> / </span> : null}
|
|
{current ? (
|
|
<span aria-current="page">{step.title}</span>
|
|
) : earlier || step.href ? (
|
|
<a className="underline" href={step.href ?? "#"}>{step.title}</a>
|
|
) : (
|
|
<span>{step.title}</span>
|
|
)}
|
|
</li>
|
|
);
|
|
})}
|
|
</ol>
|
|
</nav>
|
|
) : null}
|
|
<section aria-labelledby="sources-heading">
|
|
<h2 id="sources-heading" className="mb-3 text-lg">Sources</h2>
|
|
{entity.sources.length === 0 ? (
|
|
<p className="text-sm text-muted">No sources.</p>
|
|
) : (
|
|
<ul className="space-y-2 text-sm">
|
|
{entity.sources.map((source) => (
|
|
<li key={source.title}>
|
|
{source.title}
|
|
{source.license ? <span className="text-muted"> · {source.license}</span> : null}
|
|
{source.attribution ? (
|
|
<p className="mt-1 text-muted">
|
|
{source.uri ? <a className="underline" href={source.uri}>{source.attribution}</a> : source.attribution}
|
|
{source.licenseUrl ? <> · <a className="underline" href={source.licenseUrl}>{source.license}</a></> : null}
|
|
{source.retrievedAt ? <> · retrieved {source.retrievedAt}</> : null}
|
|
{source.originSha ? <> · origin {source.originSha}</> : null}
|
|
{source.license === "CC-BY-NC-4.0" ? <> · NonCommercial</> : null}
|
|
{source.modificationNotice ? <> · {source.modificationNotice}</> : null}
|
|
</p>
|
|
) : null}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</section>
|
|
<section aria-labelledby="claims-heading">
|
|
<h2 id="claims-heading" className="mb-3 text-lg">Claims</h2>
|
|
{entity.claims.length === 0 ? (
|
|
<p className="text-sm text-muted">No claims.</p>
|
|
) : (
|
|
<ul className="space-y-2 text-sm">
|
|
{entity.claims.map((claim) => (
|
|
<li key={`${claim.key}:${claim.status}:${claim.value ?? ""}`}>
|
|
<span className="font-medium">{claim.key}</span>
|
|
<span className="text-muted"> · {claim.status}</span>
|
|
{claim.value ? <span> · {claim.value}</span> : null}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</section>
|
|
<section aria-labelledby="relations-heading" className="space-y-6">
|
|
<h2 id="relations-heading" className="text-lg">Relations</h2>
|
|
<RelationBlock headingId="outgoing-relations" title="Outgoing" relations={entity.outgoing} />
|
|
<RelationBlock headingId="incoming-relations" title="Incoming" relations={entity.incoming} />
|
|
{entity.relationRemainder > 0 ? (
|
|
<p data-relation-remainder={entity.relationRemainder}>
|
|
{entity.relationRemainder} more relations are not listed.
|
|
</p>
|
|
) : null}
|
|
</section>
|
|
{entity.collection ? (
|
|
<section aria-labelledby="collection-heading">
|
|
<h2 id="collection-heading" className="mb-3 text-lg">{entity.collection.title}</h2>
|
|
{entity.collection.members.length === 0 ? (
|
|
<p className="text-sm text-muted">No members.</p>
|
|
) : (
|
|
<ul className="space-y-2 text-sm">
|
|
{entity.collection.members.map((member) => (
|
|
<li key={member.id}>
|
|
<RecordLink link={member} />
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</section>
|
|
) : null}
|
|
{entity.related.length > 0 ? (
|
|
<section aria-labelledby="related-heading">
|
|
<h2 id="related-heading" className="mb-3 text-lg">Related</h2>
|
|
<ul className="space-y-2 text-sm">
|
|
{entity.related.map((item) => (
|
|
<li key={item.id}>
|
|
<RecordLink link={item} />
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</section>
|
|
) : null}
|
|
<p className="text-sm">
|
|
<a className="underline" href={entity.graphHref}>Open graph</a>
|
|
{" · "}
|
|
<a className="underline" href={entity.kindHref}>Back to {entity.kindLabel}</a>
|
|
</p>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|