first commit
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user