Files
ESPDocs/components/SearchDialog.jsx
T
Lucas RayanandClaude Sonnet 5.5 71c5708726
CI / Lint e auditorias (push) Successful in 46s
CI / Trivy (dependências e segredos) (push) Successful in 10s
CI / Imagem Docker (linux/arm64) (push) Successful in 3m50s
Release / Plano (reaproveitar a imagem?) (push) Successful in 5s
Release / ci (push) Skipped
Release / Publica a imagem (linux/arm64) (push) Successful in 23s
Release / Cria a release no Gitea (push) Successful in 5s
feat(busca): pin-number queries in every language, pin answer card
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-02 00:43:23 -03:00

555 lines
23 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState, useEffect, useRef, useMemo, useCallback } from "react";
import { createPortal } from "react-dom";
import { useRouter, usePathname } from "next/navigation";
import {
Search,
X,
CornerDownLeft,
ArrowUp,
ArrowDown,
FileText,
Cpu,
Terminal,
BookOpen,
AlertTriangle,
Box,
Sparkles,
Plug,
} from "lucide-react";
import { preparar, buscar, EXEMPLOS_DE_BUSCA } from "@/lib/searchEngine";
import { normalizar } from "@/lib/searchTopics";
const TIPOS_PT = {
resposta: { label: "Resposta", Icon: Sparkles, cor: "bg-accent text-accent-contrast" },
pino: { label: "Pinos", Icon: Plug, cor: "bg-accent text-accent-contrast" },
pagina: { label: "Página", Icon: FileText, cor: "bg-surface-2 text-ink-soft" },
serie: { label: "Série", Icon: Cpu, cor: "bg-accent-soft text-accent" },
framework: { label: "Framework", Icon: Terminal, cor: "bg-teal-soft text-teal" },
glossario: { label: "Glossário", Icon: BookOpen, cor: "bg-copper-soft text-copper" },
erro: { label: "Erro", Icon: AlertTriangle, cor: "bg-gold-soft text-gold" },
componente: { label: "Componente", Icon: Box, cor: "bg-surface-2 text-ink-soft" },
};
const TIPOS_EN = {
resposta: { label: "Answer", Icon: Sparkles, cor: "bg-accent text-accent-contrast" },
pino: { label: "Pins", Icon: Plug, cor: "bg-accent text-accent-contrast" },
pagina: { label: "Page", Icon: FileText, cor: "bg-surface-2 text-ink-soft" },
serie: { label: "Series", Icon: Cpu, cor: "bg-accent-soft text-accent" },
framework: { label: "Framework", Icon: Terminal, cor: "bg-teal-soft text-teal" },
glossario: { label: "Glossary", Icon: BookOpen, cor: "bg-copper-soft text-copper" },
erro: { label: "Error", Icon: AlertTriangle, cor: "bg-gold-soft text-gold" },
componente: { label: "Component", Icon: Box, cor: "bg-surface-2 text-ink-soft" },
};
const TIPOS_ES = {
resposta: { label: "Respuesta", Icon: Sparkles, cor: "bg-accent text-accent-contrast" },
pino: { label: "Pines", Icon: Plug, cor: "bg-accent text-accent-contrast" },
pagina: { label: "Página", Icon: FileText, cor: "bg-surface-2 text-ink-soft" },
serie: { label: "Serie", Icon: Cpu, cor: "bg-accent-soft text-accent" },
framework: { label: "Framework", Icon: Terminal, cor: "bg-teal-soft text-teal" },
glossario: { label: "Glosario", Icon: BookOpen, cor: "bg-copper-soft text-copper" },
erro: { label: "Error", Icon: AlertTriangle, cor: "bg-gold-soft text-gold" },
componente: { label: "Componente", Icon: Box, cor: "bg-surface-2 text-ink-soft" },
};
const TIPOS_FR = {
resposta: { label: "Réponse", Icon: Sparkles, cor: "bg-accent text-accent-contrast" },
pino: { label: "Broches", Icon: Plug, cor: "bg-accent text-accent-contrast" },
pagina: { label: "Page", Icon: FileText, cor: "bg-surface-2 text-ink-soft" },
serie: { label: "Série", Icon: Cpu, cor: "bg-accent-soft text-accent" },
framework: { label: "Framework", Icon: Terminal, cor: "bg-teal-soft text-teal" },
glossario: { label: "Glossaire", Icon: BookOpen, cor: "bg-copper-soft text-copper" },
erro: { label: "Erreur", Icon: AlertTriangle, cor: "bg-gold-soft text-gold" },
componente: { label: "Composant", Icon: Box, cor: "bg-surface-2 text-ink-soft" },
};
const TIPOS_DE = {
resposta: { label: "Antwort", Icon: Sparkles, cor: "bg-accent text-accent-contrast" },
pino: { label: "Pins", Icon: Plug, cor: "bg-accent text-accent-contrast" },
pagina: { label: "Seite", Icon: FileText, cor: "bg-surface-2 text-ink-soft" },
serie: { label: "Serie", Icon: Cpu, cor: "bg-accent-soft text-accent" },
framework: { label: "Framework", Icon: Terminal, cor: "bg-teal-soft text-teal" },
glossario: { label: "Glossar", Icon: BookOpen, cor: "bg-copper-soft text-copper" },
erro: { label: "Fehler", Icon: AlertTriangle, cor: "bg-gold-soft text-gold" },
componente: { label: "Komponente", Icon: Box, cor: "bg-surface-2 text-ink-soft" },
};
const TIPOS_ZH = {
resposta: { label: "答案", Icon: Sparkles, cor: "bg-accent text-accent-contrast" },
pino: { label: "引脚", Icon: Plug, cor: "bg-accent text-accent-contrast" },
pagina: { label: "页面", Icon: FileText, cor: "bg-surface-2 text-ink-soft" },
serie: { label: "系列", Icon: Cpu, cor: "bg-accent-soft text-accent" },
framework: { label: "框架", Icon: Terminal, cor: "bg-teal-soft text-teal" },
glossario: { label: "术语", Icon: BookOpen, cor: "bg-copper-soft text-copper" },
erro: { label: "错误", Icon: AlertTriangle, cor: "bg-gold-soft text-gold" },
componente: { label: "组件", Icon: Box, cor: "bg-surface-2 text-ink-soft" },
};
const INTELIGENTES = new Set(["resposta", "pino"]);
const EXEMPLOS_EN = ["flash s3", "usb pins c6", "pin 32 esp32-s3", "gpio0 esp32", "brownout", "LVGL"];
const EXEMPLOS_ES = ["flash s3", "pines usb c6", "pin 32 esp32-s3", "gpio0 esp32", "brownout", "LVGL"];
const EXEMPLOS_FR = ["flash s3", "broches usb c6", "broche 32 esp32-s3", "gpio0 esp32", "brownout", "LVGL"];
const EXEMPLOS_DE = ["flash s3", "usb pins c6", "pin 32 esp32-s3", "gpio0 esp32", "brownout", "LVGL"];
const EXEMPLOS_ZH = ["s3 闪存", "c6 usb 引脚", "esp32-s3 32号引脚", "esp32 gpio0", "brownout", "LVGL"];
function Destaque({ texto, termos }) {
if (!termos.length) return texto;
const norm = normalizar(texto);
if (norm.length !== texto.length) return texto;
const marcas = new Array(texto.length).fill(false);
for (const t of termos) {
let i = norm.indexOf(t);
while (i !== -1) {
for (let k = i; k < i + t.length; k++) marcas[k] = true;
i = norm.indexOf(t, i + t.length);
}
}
const partes = [];
let atual = "";
let marcado = false;
for (let i = 0; i < texto.length; i++) {
if (marcas[i] !== marcado && atual) {
partes.push({ t: atual, m: marcado });
atual = "";
}
marcado = marcas[i];
atual += texto[i];
}
if (atual) partes.push({ t: atual, m: marcado });
return partes.map((p, i) =>
p.m ? (
<mark key={i} className="rounded-sm bg-accent-soft px-0.5 text-accent">
{p.t}
</mark>
) : (
<span key={i}>{p.t}</span>
)
);
}
function Pilula({ children, onClick, forte = false }) {
const base = "rounded-full px-2.5 py-1 text-xs font-semibold";
if (!onClick) return <span className={`${base} ${forte ? "bg-accent text-accent-contrast" : "bg-accent-soft text-accent"}`}>{children}</span>;
return (
<button
type="button"
onClick={onClick}
className={`${base} cursor-pointer border border-line bg-surface text-ink-soft transition-colors hover:border-accent hover:text-accent`}
>
{children}
</button>
);
}
export default function SearchDialog() {
const pathname = usePathname() || "";
const locale = pathname === "/en" || pathname.startsWith("/en/")
? "en"
: pathname === "/es" || pathname.startsWith("/es/")
? "es"
: pathname === "/fr" || pathname.startsWith("/fr/")
? "fr"
: pathname === "/de" || pathname.startsWith("/de/")
? "de"
: pathname === "/zh-CN" || pathname.startsWith("/zh-CN/")
? "zh-CN"
: "pt";
const isEn = locale === "en";
const isEs = locale === "es";
const isFr = locale === "fr";
const isDe = locale === "de";
const isZh = locale === "zh-CN";
const TIPOS = isEn ? TIPOS_EN : isEs ? TIPOS_ES : isFr ? TIPOS_FR : isZh ? TIPOS_ZH : isDe ? TIPOS_DE : TIPOS_PT;
const EXEMPLOS = isEn ? EXEMPLOS_EN : isEs ? EXEMPLOS_ES : isFr ? EXEMPLOS_FR : isZh ? EXEMPLOS_ZH : isDe ? EXEMPLOS_DE : EXEMPLOS_DE_BUSCA;
const [aberto, setAberto] = useState(false);
const [montado, setMontado] = useState(false);
const [consulta, setConsulta] = useState("");
const [indice, setIndice] = useState(null);
const [erro, setErro] = useState(false);
const [ativo, setAtivo] = useState(0);
const inputRef = useRef(null);
const listaRef = useRef(null);
const router = useRouter();
useEffect(() => setMontado(true), []);
const fechar = useCallback(() => {
setAberto(false);
setConsulta("");
setAtivo(0);
}, []);
// Atalhos globais: Ctrl/⌘+K abre; "/" abre fora de campos de texto.
useEffect(() => {
function onKey(e) {
const emCampo = /^(INPUT|TEXTAREA|SELECT)$/.test(e.target?.tagName) || e.target?.isContentEditable;
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") {
e.preventDefault();
setAberto((v) => !v);
} else if (e.key === "/" && !emCampo && !e.ctrlKey && !e.metaKey && !e.altKey) {
e.preventDefault();
setAberto(true);
} else if (e.key === "Escape") {
fechar();
}
}
document.addEventListener("keydown", onKey);
return () => document.removeEventListener("keydown", onKey);
}, [fechar]);
const carregarIndice = useCallback(() => {
setErro(false);
let cancelado = false;
const url = isEn
? "/api/busca/en?v=2"
: isEs
? "/api/busca/es?v=2"
: isFr
? "/api/busca/fr?v=2"
: isZh ? "/api/busca/zh-CN?v=2" : isDe
? "/api/busca/de?v=2"
: "/api/busca?v=2";
fetch(url, { cache: "no-cache" })
.then((r) => {
if (!r.ok) {
// Fallback para /api/busca caso a rota dinâmica do locale falhe
return fetch("/api/busca?v=2", { cache: "no-cache" }).then((r2) => {
if (!r2.ok) throw new Error(`HTTP ${r2.status}`);
return r2.json();
});
}
return r.json();
})
.then((dados) => {
if (!cancelado) {
setIndice(preparar(dados));
}
})
.catch((err) => {
console.error("Erro ao carregar índice de busca:", err);
if (!cancelado) setErro(true);
});
return () => {
cancelado = true;
};
}, [isEn, isEs, isFr, isDe, isZh]);
// Carrega o índice (textos + especificações + pinos) na primeira abertura.
useEffect(() => {
if (!aberto || indice) return;
return carregarIndice();
}, [aberto, indice, carregarIndice]);
useEffect(() => {
if (aberto) {
document.body.style.overflow = "hidden";
inputRef.current?.focus();
}
return () => {
document.body.style.overflow = "";
};
}, [aberto]);
const termos = useMemo(() => normalizar(consulta).split(/\s+/).filter((t) => t.length > 1), [consulta]);
const busca = useMemo(() => {
if (!indice) return { resultados: [], interpretacao: { chips: [], topicos: [] }, sugestoes: [], totalRespostas: 0 };
if (!consulta.trim()) {
return { resultados: indice.itens.filter((i) => i.tipo === "pagina"), interpretacao: { chips: [], topicos: [] }, sugestoes: [], totalRespostas: 0 };
}
return buscar(indice, consulta);
}, [indice, consulta]);
const { resultados, interpretacao, sugestoes } = busca;
const vazia = !consulta.trim();
useEffect(() => setAtivo(0), [consulta]);
useEffect(() => {
listaRef.current?.querySelector(`[data-idx="${ativo}"]`)?.scrollIntoView({ block: "nearest" });
}, [ativo]);
function ir(item) {
if (!item) return;
fechar();
router.push(item.href);
if (item.href.includes("#")) setTimeout(() => window.dispatchEvent(new HashChangeEvent("hashchange")), 80);
}
function anexar(termo) {
setConsulta((c) => `${c.trim()} ${termo}`.trim());
inputRef.current?.focus();
}
function onInputKey(e) {
if (e.key === "ArrowDown") {
e.preventDefault();
setAtivo((i) => Math.min(i + 1, resultados.length - 1));
} else if (e.key === "ArrowUp") {
e.preventDefault();
setAtivo((i) => Math.max(i - 1, 0));
} else if (e.key === "Enter") {
e.preventDefault();
ir(resultados[ativo]);
}
}
const entendeu = interpretacao.chips.length > 0 || interpretacao.topicos.length > 0 || (interpretacao.pinos?.length ?? 0) > 0;
return (
<>
<button
type="button"
id="botao-busca"
onClick={() => setAberto(true)}
className="flex h-11 min-w-11 shrink-0 cursor-pointer items-center justify-center gap-2 rounded-full border border-line px-3 text-ink transition-colors hover:bg-surface-2 xl:px-4"
aria-label={isEn ? "Search ESPDocs (Ctrl+K)" : isEs ? "Buscar en ESPDocs (Ctrl+K)" : isFr ? "Rechercher sur ESPDocs (Ctrl+K)" : isZh ? "在 ESPDocs 中搜索(Ctrl+K)" : isDe ? "ESPDocs durchsuchen (Strg+K)" : "Buscar no ESPDocs (Ctrl+K)"}
>
<Search className="h-4 w-4" aria-hidden="true" />
<span className="hidden text-sm font-medium xl:inline">{isEn ? "Search" : isEs ? "Buscar" : isFr ? "Rechercher" : isZh ? "搜索" : isDe ? "Suchen" : "Buscar"}</span>
<kbd className="hidden rounded-md border border-line bg-surface-2 px-1.5 py-0.5 font-mono text-xs font-semibold text-muted xl:inline">{isZh ? "Ctrl K" : isDe ? "Strg K" : "Ctrl K"}</kbd>
</button>
{montado &&
aberto &&
createPortal(
<div className="fixed inset-0 z-[120] flex items-start justify-center px-4 pt-[8vh]" role="dialog" aria-modal="true" aria-label={isEn ? "Search ESPDocs" : isEs ? "Buscar en ESPDocs" : isFr ? "Rechercher sur ESPDocs" : isZh ? "在 ESPDocs 中搜索" : isDe ? "ESPDocs durchsuchen" : "Buscar no ESPDocs"}>
<div className="dialog-scrim absolute inset-0 bg-black/45 backdrop-blur-sm" onClick={fechar} />
<div className="dialog-panel relative flex max-h-[80vh] w-full max-w-2xl flex-col overflow-hidden rounded-3xl border border-line bg-canvas shadow-2xl">
<div className="flex items-center gap-3 border-b border-line px-5">
<Search className="h-5 w-5 shrink-0 text-accent" aria-hidden="true" />
<input
ref={inputRef}
id="campo-busca"
type="text"
value={consulta}
onChange={(e) => setConsulta(e.target.value)}
onKeyDown={onInputKey}
placeholder={
isEn
? "Ask ESPDocs: “flash s3”, “usb pins c6”, “gpio0 esp32”…"
: isEs
? "Pregunte a ESPDocs: “flash s3”, “pines usb c6”, “gpio0 esp32”…"
: isFr
? "Demandez à ESPDocs : « flash s3 », « broches usb c6 », « gpio0 esp32 »…"
: isZh ? "向 ESPDocs 提问:“s3 闪存”、“c6 usb 引脚”、“esp32 gpio0”…" : isDe
? "ESPDocs fragen: „flash s3“, „usb pins c6“, „gpio0 esp32“… "
: "Pergunte ao ESPDocs: “flash s3”, “pinos usb c6”, “gpio0 esp32”…"
}
className="flex-1 bg-transparent py-5 text-base text-ink outline-none placeholder:text-muted"
autoComplete="off"
spellCheck={false}
aria-label={isEn ? "Search query" : isEs ? "Término de búsqueda" : isFr ? "Requête de recherche" : isZh ? "搜索词" : isDe ? "Suchbegriff" : "Termo de busca"}
/>
<button
type="button"
onClick={fechar}
className="grid h-9 w-9 cursor-pointer place-items-center rounded-full text-muted transition-colors hover:bg-surface-2 hover:text-ink"
aria-label={isEn ? "Close search" : isEs ? "Cerrar búsqueda" : isFr ? "Fermer la recherche" : isZh ? "关闭搜索" : isDe ? "Suche schließen" : "Fechar busca"}
>
<X className="h-4 w-4" />
</button>
</div>
{entendeu && (
<div className="flex flex-wrap items-center gap-2 border-b border-line bg-surface px-5 py-2.5">
<span className="text-xs font-semibold text-muted">{isEn ? "Understood:" : isEs ? "Entendido:" : isFr ? "Compris :" : isZh ? "理解为:" : isDe ? "Erkannt:" : "Entendi:"}</span>
{interpretacao.chips.map((c) => (
<Pilula key={c.chave} forte>{c.nome}</Pilula>
))}
{interpretacao.pinos?.map((p) => (
<Pilula key={p}>{p}</Pilula>
))}
{interpretacao.topicos.map((t) => (
<Pilula key={t.id}>{t.nome}</Pilula>
))}
</div>
)}
{sugestoes.length > 0 && (
<div className="flex flex-wrap items-center gap-2 border-b border-line px-5 py-2.5">
<span className="text-xs font-semibold text-muted">
{interpretacao.chips.length
? (isEn ? "Ask about:" : isEs ? "Preguntar sobre:" : isFr ? "Poser une question sur :" : isZh ? "查询:" : isDe ? "Frage zu:" : "Perguntar sobre:")
: (isEn ? "Which chip?" : isEs ? "¿Para qué chip?" : isFr ? "Pour quelle puce ?" : isZh ? "针对哪款芯片?" : isDe ? "Für welchen Chip?" : "Para qual chip?")}
</span>
{sugestoes.map((s) => (
<Pilula key={s.rotulo} onClick={() => anexar(s.anexar)}>
{s.rotulo}
</Pilula>
))}
</div>
)}
<div ref={listaRef} className="flex-1 overflow-y-auto p-2" role="listbox">
{erro && (
<div className="p-6 text-center">
<p className="text-sm font-semibold text-danger">
{isEn
? "Could not load search index."
: isEs
? "No se pudo cargar el índice de búsqueda."
: isFr
? "Impossible de charger l'index de recherche."
: isZh ? "无法加载搜索索引。" : isDe
? "Suchindex konnte nicht geladen werden."
: "Não foi possível carregar o índice de busca."}
</p>
<button
type="button"
onClick={carregarIndice}
className="btn btn-primary btn-sm mt-3"
>
{isEn ? "Try again" : isEs ? "Reintentar" : isFr ? "Réessayer" : isZh ? "重试" : isDe ? "Erneut versuchen" : "Tentar novamente"}
</button>
</div>
)}
{!erro && !indice && (
<p className="p-6 text-center text-sm text-muted">
{isEn ? "Loading…" : isEs ? "Cargando…" : isFr ? "Chargement…" : isZh ? "加载中…" : isDe ? "Wird geladen…" : "Carregando…"}
</p>
)}
{indice && vazia && (
<div className="px-3 pb-3 pt-1">
<p className="text-xs font-semibold text-muted">
{isEn ? "Try asking" : isEs ? "Pruebe a preguntar" : isFr ? "Essayez de demander" : isZh ? "试着问问" : isDe ? "Versuchen Sie" : "Experimente perguntar"}
</p>
<div className="mt-2 flex flex-wrap gap-2">
{EXEMPLOS.map((ex) => (
<Pilula key={ex} onClick={() => setConsulta(ex)}>
{ex}
</Pilula>
))}
</div>
<p className="mt-4 text-xs font-semibold text-muted">
{isEn ? "Go to" : isEs ? "Ir a" : isFr ? "Aller à" : isZh ? "前往" : isDe ? "Gehe zu" : "Ir para"}
</p>
</div>
)}
{indice && !vazia && resultados.length === 0 && (
<div className="p-8 text-center">
<p className="font-display text-lg font-semibold text-ink">
{isEn
? `No results for “${consulta}”`
: isEs
? `Sin resultados para “${consulta}”`
: isFr
? `Aucun résultat pour « ${consulta} »`
: isZh ? `未找到“${consulta}”的结果` : isDe
? `Keine Ergebnisse für „${consulta}“`
: `Nenhum resultado para “${consulta}”`}
</p>
<p className="mt-1 text-sm text-muted">
{isEn
? "Try “brownout”, “flash s3”, “usb pins c6” or “LVGL”."
: isEs
? "Pruebe “brownout”, “flash s3”, “pines usb c6” o “LVGL”."
: isFr
? "Essayez « brownout », « flash s3 », « broches usb c6 » ou « LVGL »."
: isZh ? "试试“brownout”、“s3 闪存”、“c6 usb 引脚”或“LVGL”。" : isDe
? "Versuchen Sie „brownout“, „flash s3“, „usb pins c6“ oder „LVGL“."
: "Tente “brownout”, “flash s3”, “pinos usb c6” ou “LVGL”."}
</p>
</div>
)}
{resultados.map((item, idx) => {
const { Icon, label, cor } = TIPOS[item.tipo] ?? TIPOS.pagina;
const sel = idx === ativo;
const inteligente = INTELIGENTES.has(item.tipo);
return (
<button
key={`${item.id}-${idx}`}
type="button"
role="option"
aria-selected={sel}
data-idx={idx}
onMouseMove={() => setAtivo(idx)}
onClick={() => ir(item)}
className={`flex w-full cursor-pointer items-start gap-3 rounded-2xl p-3 text-left transition-colors ${
sel ? "bg-surface-2" : ""
} ${inteligente ? "my-1 border border-line" : ""}`}
>
<div className={`grid h-9 w-9 shrink-0 place-items-center rounded-xl ${cor}`}>
<Icon className="h-4 w-4" aria-hidden="true" />
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="truncate text-sm font-semibold text-ink">
{inteligente ? item.titulo : <Destaque texto={item.titulo} termos={termos} />}
</span>
<span className={`shrink-0 rounded-full px-2 py-0.5 text-xs font-semibold ${inteligente ? "bg-accent-soft text-accent" : cor}`}>{label}</span>
</div>
<p className="mt-0.5 line-clamp-2 text-xs leading-relaxed text-muted">
{inteligente ? item.descricao : <Destaque texto={item.descricao} termos={termos} />}
</p>
{item.linhas && (
<dl className="mt-2 grid gap-x-4 gap-y-1 text-xs sm:grid-cols-[auto_1fr]">
{item.linhas.map((l, i) => (
<div key={`${l.k}-${i}`} className="contents">
<dt className="font-semibold text-ink-soft">{l.k}</dt>
<dd className="text-ink">{l.v}</dd>
</div>
))}
</dl>
)}
{item.chips && (
<div className="mt-2 flex flex-wrap gap-1.5">
{item.chips.map((c) => (
<span key={c} className="rounded-md border border-line bg-surface px-1.5 py-0.5 font-mono text-xs text-ink-soft">
{c}
</span>
))}
</div>
)}
</div>
{sel && <CornerDownLeft className="mt-2 h-4 w-4 shrink-0 text-accent" aria-hidden="true" />}
</button>
);
})}
</div>
<div className="hidden items-center gap-4 border-t border-line bg-surface px-4 py-2.5 text-xs text-muted md:flex">
<span className="flex items-center gap-1"><ArrowUp className="h-3 w-3" /><ArrowDown className="h-3 w-3" /> {isEn ? "navigate" : isEs ? "navegar" : isFr ? "naviguer" : isZh ? "导航" : isDe ? "navigieren" : "navegar"}</span>
<span className="flex items-center gap-1"><CornerDownLeft className="h-3 w-3" /> {isEn ? "open" : isEs ? "abrir" : isFr ? "ouvrir" : isZh ? "打开" : isDe ? "öffnen" : "abrir"}</span>
<span><kbd className="font-mono font-semibold">Esc</kbd> {isEn ? "close" : isEs ? "cerrar" : isFr ? "fermer" : isZh ? "关闭" : isDe ? "schließen" : "fechar"}</span>
<span className="ml-auto">
{vazia
? (isEn
? "Tip: press / on any page"
: isEs
? "Consejo: presione / en cualquier página"
: isFr
? "Astuce : appuyez sur / sur n'importe quelle page"
: isZh ? "提示:在任意页面按 / 键" : isDe
? "Tipp: Drücken Sie / auf einer beliebigen Seite"
: "Dica: pressione / em qualquer página")
: `${resultados.length} ${
isEn
? "result(s)"
: isEs
? "resultado(s)"
: isFr
? "résultat(s)"
: isZh ? "条结果" : isDe
? "Ergebnis(se)"
: "resultado(s)"
}`}
</span>
</div>
</div>
</div>,
document.body
)}
</>
);
}