import { useEffect, useId, useRef, Fragment } from "react"; import type { GraphNode, SuggestHit } from "./types"; export type DefinitionSense = { pos: string; glosses: string[] }; export type DefinitionPayload = { term: string; lang?: string; matched_code?: string | null; senses?: DefinitionSense[]; fallback?: boolean; url?: string; error?: string | null; }; type Detail = Record | null; type CognateSet = { id: number; language_count: number; word_count: number; words: { term?: string; lang?: string }[]; }; type WalsRow = { feature_id: string; feature_name: string; value_label: string }; type PhonemeRow = { phoneme: string; tone?: number }; export function WordModal({ open, node, detail, definition, definitionLoading, onClose, onDescend, onLeafLang, onPickEtymon, }: { open: boolean; node: GraphNode | null; detail: Detail; definition: DefinitionPayload | null; definitionLoading: boolean; onClose: () => void; onDescend: () => void; onLeafLang: () => void; onPickEtymon: (term: string, lang: string) => void; }) { const titleId = useId(); const closeRef = useRef(null); useEffect(() => { if (!open) return; const prev = document.activeElement as HTMLElement | null; closeRef.current?.focus(); const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; document.addEventListener("keydown", onKey); const overflow = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.removeEventListener("keydown", onKey); document.body.style.overflow = overflow; prev?.focus?.(); }; }, [open, onClose]); if (!open || !node) return null; const etymons = ((detail?.etymons as SuggestHit[]) || []).slice(0, 8); const cognates = ((detail?.cognate_sets as CognateSet[]) || []).slice(0, 3); const childRels = (detail?.child_relations as Record) || {}; const wals = ((detail?.wals as WalsRow[]) || []).slice(0, 6); const phonemes = (detail?.phonemes as PhonemeRow[]) || []; const wiki = (detail?.wiktionary as string) || definition?.url; const senses = definition?.senses || []; return (

Definition

{definitionLoading ? (

Looking up Wiktionary…

) : senses.length ? ( <> {definition?.fallback ? (

No exact language match — showing Wiktionary glosses for [{definition.matched_code}].

) : null}
{senses.map((s) => (
{s.pos}
    {s.glosses.map((g) => (
  1. {g}
  2. ))}
))}
) : (

No Wiktionary gloss found for this spelling {definition?.error ? ` (${definition.error})` : ""}. {wiki ? ( <> {" "} Open Wiktionary ) : null}

)}

In this graph

Depth
{node.depth}
Children
{node.child_count ?? "—"}
Area
{node.macroarea || "—"}
Status
{node.status || "—"}
Glottocode
{node.glottocode || "—"}
ISO 639-3
{node.iso_639_3 || "—"}
{!!Object.keys(childRels).length && (

{Object.entries(childRels).map(([k, v]) => ( {k.replace("_", " ")} {v} ))}

)}
{!!etymons.length && (

Comes from

{etymons.map((e, i) => ( ))}
)} {!!cognates.length && (

Cognate sets

{cognates.map((c) => (

{c.language_count} languages · {(c.words || []).slice(0, 8).map((w) => w.term).filter(Boolean).join(", ")} {(c.words || []).length > 8 ? "…" : ""}

))}
)} {!!wals.length && (

Typology (WALS sample)

{wals.map((w) => (
{w.feature_id}
{w.value_label}
))}
)} {!!phonemes.length && (

Phonemes

{phonemes.length} segments {phonemes.some((p) => p.tone) ? " · has tone" : ""} {" · "} {phonemes .slice(0, 24) .map((p) => p.phoneme) .join(" ")} {phonemes.length > 24 ? " …" : ""}

)}
); }