78 lines
2.8 KiB
TypeScript
78 lines
2.8 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { LANGUAGES } from "@/lib/languages";
|
|
import { Search } from "lucide-react";
|
|
|
|
/**
|
|
* Searchable language dropdown used by the per-message translator and the
|
|
* composer's translate-before-send flow. Positions itself above or below the
|
|
* trigger depending on available space (like a tooltip).
|
|
*/
|
|
export function LanguageMenu({
|
|
onPick,
|
|
onClose,
|
|
align = "left",
|
|
}: {
|
|
onPick: (langName: string) => void;
|
|
onClose: () => void;
|
|
align?: "left" | "right";
|
|
}) {
|
|
const [q, setQ] = useState("");
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
const [vpos, setVpos] = useState<"up" | "down">("down");
|
|
|
|
useEffect(() => {
|
|
const parent = ref.current?.offsetParent as HTMLElement | null;
|
|
const r = (parent ?? ref.current)?.getBoundingClientRect();
|
|
if (r) {
|
|
const H = 320;
|
|
setVpos(r.bottom + H > window.innerHeight && r.top > H ? "up" : "down");
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const onDoc = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) onClose(); };
|
|
const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") { e.stopPropagation(); onClose(); } };
|
|
document.addEventListener("mousedown", onDoc);
|
|
document.addEventListener("keydown", onKey);
|
|
return () => { document.removeEventListener("mousedown", onDoc); document.removeEventListener("keydown", onKey); };
|
|
}, [onClose]);
|
|
|
|
const list = LANGUAGES.filter(
|
|
(l) => l.name.toLowerCase().includes(q.toLowerCase()) || l.native.toLowerCase().includes(q.toLowerCase()),
|
|
);
|
|
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={`absolute z-50 w-64 overflow-hidden rounded-control border border-border bg-elevated shadow-pop animate-slide-up ${vpos === "up" ? "bottom-9" : "top-9"} ${align === "right" ? "right-0" : "left-0"}`}
|
|
>
|
|
<div className="flex items-center gap-2 border-b border-border px-2.5 py-2">
|
|
<Search size={14} className="text-dim" />
|
|
<input
|
|
autoFocus
|
|
value={q}
|
|
onChange={(e) => setQ(e.target.value)}
|
|
placeholder="Search language…"
|
|
className="w-full bg-transparent text-[13px] outline-none placeholder:text-dim"
|
|
/>
|
|
</div>
|
|
<div className="max-h-64 overflow-y-auto py-1">
|
|
{list.map((l) => (
|
|
<button
|
|
key={l.name}
|
|
onClick={() => onPick(l.name)}
|
|
className="flex w-full items-center gap-2.5 px-3 py-1.5 text-left text-[13px] hover:bg-hover"
|
|
>
|
|
<span className="text-base">{l.flag}</span>
|
|
<span className="font-medium">{l.name}</span>
|
|
<span className="ml-auto text-[11.5px] text-dim">{l.native}</span>
|
|
</button>
|
|
))}
|
|
{list.length === 0 && <div className="px-3 py-4 text-center text-[12.5px] text-muted">No match.</div>}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|