disclosure-bureau/web/components/fm/wikilink.tsx

109 lines
4.2 KiB
TypeScript
Raw Normal View History

/**
* Renders a single `[[wiki-link]]` string as a colored Link.
* Accepts either the raw `[[id]]` form OR an already-stripped `id`.
*/
import Link from "next/link";
const CLASS_MAP: Record<string, string> = {
people: "people", person: "people",
org: "organizations", organization: "organizations", organizations: "organizations",
loc: "locations", location: "locations", locations: "locations",
event: "events", events: "events",
uap: "uap-objects", "uap-object": "uap-objects", "uap-objects": "uap-objects",
vehicle: "vehicles", vehicles: "vehicles",
op: "operations", operation: "operations", operations: "operations",
concept: "concepts", concepts: "concepts",
};
const COLOR_BY_CLASS: Record<string, string> = {
people: "text-[#ff6ec7] border-[rgba(255,110,199,0.35)] hover:bg-[rgba(255,110,199,0.08)]",
organizations: "text-[#ff8a4d] border-[rgba(255,138,77,0.35)] hover:bg-[rgba(255,138,77,0.08)]",
locations: "text-[#3fde6a] border-[rgba(63,222,106,0.35)] hover:bg-[rgba(63,222,106,0.08)]",
events: "text-[#ffa500] border-[rgba(255,165,0,0.35)] hover:bg-[rgba(255,165,0,0.08)]",
"uap-objects": "text-[#ff3344] border-[rgba(255,51,68,0.45)] hover:bg-[rgba(255,51,68,0.08)] font-semibold",
vehicles: "text-[#5b9bd5] border-[rgba(91,155,213,0.35)] hover:bg-[rgba(91,155,213,0.08)]",
operations: "text-[#9b5de5] border-[rgba(155,93,229,0.35)] hover:bg-[rgba(155,93,229,0.08)]",
concepts: "text-[#06d6a0] border-[rgba(6,214,160,0.35)] hover:bg-[rgba(6,214,160,0.08)]",
document: "text-[#f5c542] border-[rgba(245,197,66,0.35)] hover:bg-[rgba(245,197,66,0.08)]",
page: "text-[#7fdbff] border-[rgba(127,219,255,0.35)] hover:bg-[rgba(127,219,255,0.08)]",
table: "text-[#1e9eb5] border-[rgba(30,158,181,0.35)]",
image: "text-[#ffeb99] border-[rgba(255,235,153,0.35)]",
unknown: "text-[#c8d4e6] border-[rgba(127,219,255,0.18)]",
};
export interface WikiLinkResolved {
href: string;
cls: keyof typeof COLOR_BY_CLASS;
display: string;
}
/** Parses `[[X]]`, `[[X|alias]]`, or bare `X` into a route + display. */
export function resolveWikiLink(input: string): WikiLinkResolved {
let target = input.trim();
let alias: string | undefined;
const m = target.match(/^\[\[(.+?)\]\]$/);
if (m) target = m[1];
if (target.includes("|")) {
const [t, a] = target.split("|");
target = t;
alias = a;
}
const display = (alias ?? target).trim();
if (target.startsWith("table/")) return { href: `/t/${target.slice(6)}`, cls: "table", display };
if (target.startsWith("image/")) return { href: `/i/${target.slice(6)}`, cls: "image", display };
// doc-id/pNNN
const pageMatch = target.match(/^([a-z0-9-]+)\/p\d{3}$/);
if (pageMatch) return { href: `/d/${target}`, cls: "page", display };
// class/id
const slash = target.match(/^([a-z-]+)\/([A-Za-z0-9._-]+)$/);
if (slash) {
const cls = CLASS_MAP[slash[1]];
if (cls) return { href: `/e/${cls}/${slash[2]}`, cls: cls as keyof typeof COLOR_BY_CLASS, display };
}
// bare doc-id
return { href: `/d/${target}`, cls: "document", display };
}
export function FmWikiLink({ target, size = "sm" }: { target: string; size?: "xs" | "sm" | "md" }) {
const { href, cls, display } = resolveWikiLink(target);
const sz =
size === "xs" ? "text-[10px] px-1.5 py-0.5" :
size === "md" ? "text-sm px-2.5 py-1" :
"text-xs px-2 py-0.5";
return (
<Link
href={href}
className={`inline-flex items-center font-mono border rounded transition-colors ${sz} ${COLOR_BY_CLASS[cls]}`}
>
{display}
</Link>
);
}
export function FmWikiLinkList({
items,
size = "sm",
emptyLabel = "—",
max = 64,
}: {
items: string[] | undefined;
size?: "xs" | "sm" | "md";
emptyLabel?: string;
max?: number;
}) {
if (!items || items.length === 0) return <span className="text-[#5a6678] text-xs italic">{emptyLabel}</span>;
const shown = items.slice(0, max);
const overflow = items.length - shown.length;
return (
<div className="flex flex-wrap gap-1">
{shown.map((t, i) => <FmWikiLink key={i} target={t} size={size} />)}
{overflow > 0 && (
<span className="font-mono text-[10px] text-[#5a6678] px-2 py-0.5">+{overflow}</span>
)}
</div>
);
}