ux(search): repaint autocomplete dropdown to magazine palette
The /search input opened a green/cyan terminal-style dropdown leftover from the dev-tool era — the only piece of the search experience still off-theme after the page redesign. - Switch matrix-green (#00ff9c) and cyan (#7fdbff) for gold (#e0c080) and cream throughout the dropdown. - Replace ASCII "⚡" with a Zap lucide icon and reorder the header so the Suggestions label leads. - Localise "documentos" / "trechos" / "autocomplete" via a locale prop threaded from SearchPanel; counts pluralise per language. - Document title now uses the display serif at 15px (was monospace small); passage excerpts in #cbd2dd to match the rest of body copy. - Passage row reorder: page · type · 🛸 · doc-id (was chunk_id first); page number gets gold accent so the reader can scan by page. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
927c1fc8f7
commit
0a4c319a14
2 changed files with 57 additions and 22 deletions
|
|
@ -7,9 +7,13 @@
|
||||||
* a two-section dropdown: matching documents (jump targets) and matching
|
* a two-section dropdown: matching documents (jump targets) and matching
|
||||||
* chunks (in-doc passages with excerpt). Sub-30ms target. Keyboard navigation
|
* chunks (in-doc passages with excerpt). Sub-30ms target. Keyboard navigation
|
||||||
* via Up/Down + Enter. Esc closes.
|
* via Up/Down + Enter. Esc closes.
|
||||||
|
*
|
||||||
|
* Magazine palette (gold/cream on dark) to match the rest of the site; the
|
||||||
|
* previous matrix-green/cyan styling was a leftover from the dev-tool era.
|
||||||
*/
|
*/
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { Zap } from "lucide-react";
|
||||||
|
|
||||||
interface DocSuggestion {
|
interface DocSuggestion {
|
||||||
doc_id: string;
|
doc_id: string;
|
||||||
|
|
@ -34,12 +38,38 @@ interface ApiResponse {
|
||||||
chunks: ChunkSuggestion[];
|
chunks: ChunkSuggestion[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SearchAutocomplete({ query, onPick }: { query: string; onPick?: () => void }) {
|
const COPY = {
|
||||||
|
"pt-br": {
|
||||||
|
autocomplete: "Sugestões",
|
||||||
|
documents: "Documentos",
|
||||||
|
passages: "Trechos",
|
||||||
|
docs_count: (n: number) => `${n} ${n === 1 ? "documento" : "documentos"}`,
|
||||||
|
chunks_count: (n: number) => `${n} ${n === 1 ? "trecho" : "trechos"}`,
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
autocomplete: "Suggestions",
|
||||||
|
documents: "Documents",
|
||||||
|
passages: "Passages",
|
||||||
|
docs_count: (n: number) => `${n} ${n === 1 ? "document" : "documents"}`,
|
||||||
|
chunks_count: (n: number) => `${n} ${n === 1 ? "passage" : "passages"}`,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function SearchAutocomplete({
|
||||||
|
query,
|
||||||
|
onPick,
|
||||||
|
locale = "pt-br",
|
||||||
|
}: {
|
||||||
|
query: string;
|
||||||
|
onPick?: () => void;
|
||||||
|
locale?: "pt-br" | "en";
|
||||||
|
}) {
|
||||||
const [data, setData] = useState<ApiResponse | null>(null);
|
const [data, setData] = useState<ApiResponse | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const abort = useRef<AbortController | null>(null);
|
const abort = useRef<AbortController | null>(null);
|
||||||
|
const t = COPY[locale];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const q = query.trim();
|
const q = query.trim();
|
||||||
|
|
@ -72,18 +102,23 @@ export function SearchAutocomplete({ query, onPick }: { query: string; onPick?:
|
||||||
if (!open || !data) return null;
|
if (!open || !data) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute z-30 left-0 right-0 mt-1 max-h-[60vh] overflow-y-auto bg-[#0a121e] border border-[#00ff9c] rounded shadow-lg">
|
<div className="absolute z-30 left-0 right-0 mt-2 max-h-[60vh] overflow-y-auto bg-[#0d1220] border border-[rgba(224,192,128,0.30)] rounded-xl shadow-2xl">
|
||||||
<div className="flex items-center justify-between px-3 py-1.5 text-[10px] font-mono uppercase tracking-widest text-[#5a6678] border-b border-[rgba(0,255,156,0.20)]">
|
<div className="flex items-center justify-between px-3 py-2 text-[10px] font-mono uppercase tracking-widest text-[#9aa6b8] border-b border-[rgba(224,192,128,0.18)]">
|
||||||
<span>
|
<span className="inline-flex items-center gap-1.5">
|
||||||
⚡ autocomplete · {data.documents.length} docs · {data.chunks.length} trechos
|
<Zap size={11} className="text-[#e0c080]" aria-hidden="true" />
|
||||||
|
<span className="text-[#e0c080]">{t.autocomplete}</span>
|
||||||
|
<span aria-hidden="true">·</span>
|
||||||
|
<span>{t.docs_count(data.documents.length)}</span>
|
||||||
|
<span aria-hidden="true">·</span>
|
||||||
|
<span>{t.chunks_count(data.chunks.length)}</span>
|
||||||
</span>
|
</span>
|
||||||
<span>{loading ? "…" : `${data.duration_ms ?? "?"}ms`}</span>
|
<span>{loading ? "…" : `${data.duration_ms ?? "?"}ms`}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{data.documents.length > 0 && (
|
{data.documents.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<div className="px-3 pt-2 pb-1 text-[10px] font-mono uppercase tracking-widest text-[#7fdbff]">
|
<div className="px-3 pt-2 pb-1 text-[10px] font-mono uppercase tracking-widest text-[#e0c080]">
|
||||||
documentos
|
{t.documents}
|
||||||
</div>
|
</div>
|
||||||
<ul>
|
<ul>
|
||||||
{data.documents.map((d) => (
|
{data.documents.map((d) => (
|
||||||
|
|
@ -91,12 +126,12 @@ export function SearchAutocomplete({ query, onPick }: { query: string; onPick?:
|
||||||
<Link
|
<Link
|
||||||
href={d.href}
|
href={d.href}
|
||||||
onClick={onPick}
|
onClick={onPick}
|
||||||
className="block px-3 py-2 hover:bg-[rgba(0,255,156,0.06)] border-l-2 border-transparent hover:border-[#00ff9c]"
|
className="block px-3 py-2 hover:bg-[rgba(224,192,128,0.06)] border-l-2 border-transparent hover:border-[#e0c080] transition-colors"
|
||||||
>
|
>
|
||||||
<div className="font-mono text-sm text-[#c8d4e6] truncate">{d.title}</div>
|
<div className="font-display text-[15px] text-[#e7ecf3] truncate">{d.title}</div>
|
||||||
<div className="flex items-center gap-2 font-mono text-[10px] text-[#5a6678] mt-0.5">
|
<div className="flex items-center gap-2 font-mono text-[10px] text-[#5a6678] mt-0.5">
|
||||||
<span>{d.doc_id}</span>
|
<span className="truncate">{d.doc_id}</span>
|
||||||
{d.collection && <span>· {d.collection}</span>}
|
{d.collection && <><span aria-hidden="true">·</span><span>{d.collection}</span></>}
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
|
|
@ -107,8 +142,8 @@ export function SearchAutocomplete({ query, onPick }: { query: string; onPick?:
|
||||||
|
|
||||||
{data.chunks.length > 0 && (
|
{data.chunks.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<div className="px-3 pt-2 pb-1 text-[10px] font-mono uppercase tracking-widest text-[#7fdbff]">
|
<div className="px-3 pt-2 pb-1 text-[10px] font-mono uppercase tracking-widest text-[#e0c080]">
|
||||||
trechos
|
{t.passages}
|
||||||
</div>
|
</div>
|
||||||
<ul>
|
<ul>
|
||||||
{data.chunks.map((c) => (
|
{data.chunks.map((c) => (
|
||||||
|
|
@ -116,16 +151,16 @@ export function SearchAutocomplete({ query, onPick }: { query: string; onPick?:
|
||||||
<Link
|
<Link
|
||||||
href={c.href}
|
href={c.href}
|
||||||
onClick={onPick}
|
onClick={onPick}
|
||||||
className="block px-3 py-2 hover:bg-[rgba(0,255,156,0.06)] border-l-2 border-transparent hover:border-[#00ff9c]"
|
className="block px-3 py-2 hover:bg-[rgba(224,192,128,0.06)] border-l-2 border-transparent hover:border-[#e0c080] transition-colors"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 font-mono text-[10px] text-[#5a6678] mb-0.5">
|
<div className="flex items-center gap-2 font-mono text-[10px] mb-1">
|
||||||
<span className="text-[#00ff9c]">{c.chunk_id}</span>
|
<span className="text-[#e0c080]">p{c.page}</span>
|
||||||
<span>p{c.page}</span>
|
<span className="text-[#5a6678]" aria-hidden="true">·</span>
|
||||||
<span>{c.type}</span>
|
<span className="text-[#9aa6b8]">{c.type}</span>
|
||||||
{c.ufo_anomaly && <span className="text-[#00ff9c]">🛸</span>}
|
{c.ufo_anomaly && <span aria-label="UAP anomaly">🛸</span>}
|
||||||
<span className="text-[#7fdbff] truncate">{c.doc_id}</span>
|
<span className="text-[#5a6678] truncate ml-1">{c.doc_id}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[13px] text-[#c8d4e6] line-clamp-2 leading-snug">{c.excerpt}</div>
|
<div className="text-[13px] text-[#cbd2dd] line-clamp-2 leading-snug">{c.excerpt}</div>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -174,7 +174,7 @@ export function SearchPanel({
|
||||||
className="w-full bg-[#0a0e1a] border border-[rgba(224,192,128,0.25)] focus:border-[#e0c080] rounded-full pl-10 pr-4 py-3 font-display text-[15px] md:text-[16px] text-[#e7ecf3] placeholder:text-[#5a6678] outline-none transition-colors"
|
className="w-full bg-[#0a0e1a] border border-[rgba(224,192,128,0.25)] focus:border-[#e0c080] rounded-full pl-10 pr-4 py-3 font-display text-[15px] md:text-[16px] text-[#e7ecf3] placeholder:text-[#5a6678] outline-none transition-colors"
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<SearchAutocomplete query={q} onPick={() => setQ("")} />
|
<SearchAutocomplete query={q} onPick={() => setQ("")} locale={locale} />
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue