Add a bounded relation graph and knowledge journeys.
CONTENT-WP-0004 keeps outgoing and incoming labels apart, limits the graph to two hops, and stores focus and detail in the URL. Assistant: grok Assistant-Session: 01a1531b-6698-7732-8587-f28ea604bcdf
This commit is contained in:
parent
749752df4e
commit
b5b6f6bc00
15 changed files with 1290 additions and 52 deletions
|
|
@ -1,15 +1,52 @@
|
|||
import { PageHeader } from "@/components/PageHeader";
|
||||
import type { EntityView } from "@/lib/catalog";
|
||||
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">
|
||||
|
|
@ -18,6 +55,28 @@ export function EntityDetail({ entity }: { entity: EntityView }) {
|
|||
))}
|
||||
</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 ? (
|
||||
|
|
@ -49,27 +108,47 @@ export function EntityDetail({ entity }: { entity: EntityView }) {
|
|||
</ul>
|
||||
)}
|
||||
</section>
|
||||
<section aria-labelledby="relations-heading">
|
||||
<h2 id="relations-heading" className="mb-3 text-lg">Relations</h2>
|
||||
{entity.relations.length === 0 ? (
|
||||
<p className="text-sm text-muted">No relations.</p>
|
||||
) : (
|
||||
<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.relations.map((relation) => (
|
||||
<li key={`${relation.direction}:${relation.wording}:${relation.otherTitle}`}>
|
||||
<span className="text-muted">{relation.wording}</span>
|
||||
{" · "}
|
||||
{relation.otherHref ? (
|
||||
<a className="underline" href={relation.otherHref}>{relation.otherTitle}</a>
|
||||
) : (
|
||||
relation.otherTitle
|
||||
)}
|
||||
{entity.related.map((item) => (
|
||||
<li key={item.id}>
|
||||
<RecordLink link={item} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
</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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue