Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,19 @@ textarea:focus-visible {
border-radius: 4px;
}

/* cmdk command palette input — opts out of the universal focus ring.
The dialog already provides visual focus context (modal backdrop +
centered card); a hard accent outline on the input itself reads as
a validation error rather than a focus state, which is the look the
designer flagged as "pas pro". The input still receives focus and
keyboard nav, just without the harsh outline. */
[cmdk-input]:focus,
[cmdk-input]:focus-visible {
outline: none !important;
outline-offset: 0;
box-shadow: none !important;
}

/* Skip-link target. Hidden until keyboard focus brings it onto the
page, then snaps to the top-left so a screen-reader / keyboard
user can bypass the nav. */
Expand Down
97 changes: 71 additions & 26 deletions src/components/search/search-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { Command } from "cmdk";
import Fuse from "fuse.js";
import { Search, X } from "lucide-react";
import { ArrowRight, Search } from "lucide-react";
import { useRouter } from "next/navigation";
import { useEffect, useMemo, useRef, useState } from "react";
import { useSearch } from "@/components/search/search-provider";
Expand All @@ -18,6 +18,16 @@ const KIND_ORDER: SearchKind[] = [
"Page",
];

const KIND_LABEL: Record<SearchKind, string> = {
Benchmark: "Benchmarks",
Product: "Products",
Compare: "Compare",
Alternative: "Alternatives",
Answer: "Answers",
Chain: "Chains",
Page: "Pages",
};

/**
* Hardcoded "popular benches" shown when the query is empty. No
* analytics involved, just an editorial pick of high-traffic specs
Expand All @@ -30,6 +40,14 @@ const POPULAR_BENCH_SLUGS = [
"rpc-capabilities",
];

function Kbd({ children }: { children: React.ReactNode }) {
return (
<kbd className="inline-flex items-center justify-center min-w-[20px] h-5 px-1.5 rounded border border-rule bg-paper-soft text-[10px] font-medium text-ink-muted font-mono">
{children}
</kbd>
);
}

export default function SearchDialog() {
const { items, close: onClose } = useSearch();
const router = useRouter();
Expand Down Expand Up @@ -83,7 +101,7 @@ export default function SearchDialog() {
.filter((it): it is SearchItem => Boolean(it));
return popular;
}
return fuse.search(q, { limit: 8 }).map((r) => r.item);
return fuse.search(q, { limit: 12 }).map((r) => r.item);
}, [query, fuse, items]);

const grouped = useMemo(() => {
Expand All @@ -109,89 +127,116 @@ export default function SearchDialog() {

return (
<div
className="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-ink/40 backdrop-blur-sm p-4 sm:p-8"
className="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-ink/50 backdrop-blur-md p-4 sm:p-8 animate-in fade-in duration-150"
onClick={onClose}
role="dialog"
aria-modal="true"
aria-label="Search"
>
<div
className="relative w-full max-w-xl mx-auto mt-[10vh] rounded-md border border-rule bg-paper shadow-2xl font-sans"
className="relative w-full max-w-2xl mx-auto mt-[8vh] rounded-xl border border-rule-strong/60 bg-surface shadow-[0_24px_60px_-12px_rgba(0,0,0,0.45)] font-sans overflow-hidden"
onClick={(e) => e.stopPropagation()}
>
<Command
label="Site search"
shouldFilter={false}
className="flex flex-col"
>
<div className="flex items-center gap-2.5 border-b border-rule px-4 py-3">
<Search size={15} className="text-ink-faint shrink-0" aria-hidden />
{/* Input row — no border on the input itself, focus ring removed,
search icon left, ESC kbd right. */}
<div className="flex items-center gap-3 px-5 h-14 border-b border-rule">
<Search size={18} className="text-ink-faint shrink-0" aria-hidden />
<Command.Input
ref={inputRef}
value={query}
onValueChange={setQuery}
placeholder="Search benchmarks, products, chains, answers…"
className="flex-1 bg-transparent text-sm text-ink placeholder:text-ink-faint outline-none border-0"
className="flex-1 bg-transparent text-[15px] text-ink placeholder:text-ink-faint outline-none border-0 ring-0 focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0"
/>
<button
type="button"
onClick={onClose}
aria-label="Close search"
className="text-ink-muted hover:text-ink transition-colors"
className="hidden sm:inline-flex"
>
<X size={16} strokeWidth={2} />
<Kbd>esc</Kbd>
</button>
</div>

<Command.List className="max-h-[60vh] overflow-y-auto py-2">
<Command.List className="max-h-[60vh] overflow-y-auto px-2 py-2">
{headerLabel && (
<div className="px-4 pt-1 pb-2 text-[10px] font-medium uppercase tracking-[0.18em] text-ink-faint">
<div className="px-3 pt-3 pb-1 text-[10px] font-medium uppercase tracking-[0.14em] text-ink-faint">
{headerLabel}
</div>
)}

{showEmpty && (
<Command.Empty className="px-4 py-6 text-sm text-ink-muted leading-relaxed">
No results for &quot;{trimmed}&quot;. Try searching by bench name,
provider, chain, or keyword.
<Command.Empty className="px-4 py-10 text-center text-sm text-ink-muted leading-relaxed">
No results for &ldquo;{trimmed}&rdquo;.
<br />
<span className="text-xs text-ink-faint">
Try a bench name, provider, or chain.
</span>
</Command.Empty>
)}

{grouped.map(({ kind, list }) => (
<Command.Group
key={kind}
heading={trimmed.length > 0 ? kind : undefined}
className="px-2 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:pt-2 [&_[cmdk-group-heading]]:pb-1 [&_[cmdk-group-heading]]:text-[10px] [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:uppercase [&_[cmdk-group-heading]]:tracking-[0.18em] [&_[cmdk-group-heading]]:text-ink-faint"
heading={trimmed.length > 0 ? KIND_LABEL[kind] : undefined}
className="[&_[cmdk-group-heading]]:px-3 [&_[cmdk-group-heading]]:pt-3 [&_[cmdk-group-heading]]:pb-1 [&_[cmdk-group-heading]]:text-[10px] [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:uppercase [&_[cmdk-group-heading]]:tracking-[0.14em] [&_[cmdk-group-heading]]:text-ink-faint"
>
{list.map((it) => (
<Command.Item
key={it.id}
value={`${it.kind}|${it.title}|${it.id}`}
onSelect={() => go(it.url)}
className="flex items-start gap-3 rounded px-2 py-2 cursor-pointer text-sm aria-selected:bg-paper-soft"
className="group flex items-center gap-3 rounded-lg px-3 py-2.5 cursor-pointer text-sm aria-selected:bg-paper-soft transition-colors"
>
<span className="mt-[3px] inline-flex items-center justify-center min-w-[64px] shrink-0 rounded border border-rule px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-[0.14em] text-ink-muted">
{it.kind}
</span>
<span className="min-w-0 flex-1">
<span className="block font-semibold text-ink truncate">
{it.title}
<span className="flex items-center gap-2">
<span className="text-[10px] font-medium uppercase tracking-[0.1em] text-ink-faint shrink-0">
{KIND_LABEL[it.kind].replace(/s$/, "")}
</span>
<span className="text-rule-strong">·</span>
<span className="font-medium text-ink truncate">
{it.title}
</span>
</span>
{it.description && (
<span className="block text-xs text-ink-muted truncate">
<span className="block mt-0.5 text-[12px] text-ink-muted truncate">
{it.description}
</span>
)}
</span>
<ArrowRight
size={14}
className="text-ink-faint shrink-0 opacity-0 group-aria-selected:opacity-100 transition-opacity"
aria-hidden
/>
</Command.Item>
))}
</Command.Group>
))}
</Command.List>

<div className="flex items-center justify-between border-t border-rule px-4 py-2 text-[10px] font-medium uppercase tracking-[0.16em] text-ink-faint">
<span>Enter to go</span>
<span>Esc to close</span>
{/* Footer — subtle separator, kbd-driven hints. */}
<div className="flex items-center justify-between gap-4 border-t border-rule px-4 py-2.5 text-[11px] text-ink-faint">
<div className="flex items-center gap-3">
<span className="inline-flex items-center gap-1.5">
<Kbd>↑</Kbd>
<Kbd>↓</Kbd>
<span>navigate</span>
</span>
<span className="inline-flex items-center gap-1.5">
<Kbd>↵</Kbd>
<span>open</span>
</span>
</div>
<span className="inline-flex items-center gap-1.5">
<Kbd>esc</Kbd>
<span>close</span>
</span>
</div>
</Command>
</div>
Expand Down
20 changes: 18 additions & 2 deletions src/lib/search/buildIndex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,19 @@ const STATIC_PAGES: Array<{ url: string; title: string; description: string; tag
},
];

/** Strip `{{best_name}}` / `{{best_p50}}` / `{{p50:slug}}` etc. tokens
* from raw YAML editorial copy. The search index is built at build time
* with no Prom data, so template tokens can't be resolved here. Leaving
* them raw ships `{{best_name}} leads ...` into the search dialog
* description preview, which reads as broken. */
function stripTemplates(s: string): string {
return s.replace(/\{\{[^}]+\}\}/g, "").replace(/\s+/g, " ").trim();
}

function truncate(s: string | undefined, n = 150): string | undefined {
if (!s) return undefined;
const clean = s.replace(/\s+/g, " ").trim();
const clean = stripTemplates(s);
if (!clean) return undefined;
if (clean.length <= n) return clean;
return `${clean.slice(0, n - 1).trimEnd()}…`;
}
Expand Down Expand Up @@ -146,12 +156,18 @@ export const buildSearchIndex = cache(async function buildSearchIndex(): Promise
// ── Benchmarks ──────────────────────────────────────────────────
for (const spec of specs) {
if (spec.status === "draft") continue;
// Prefer subtitle over seo_description when the latter contains
// template tokens. The index is built without Prom data so we can't
// resolve `{{best_name}}` here, and showing the YAML with the tokens
// stripped leaves a sentence with gaps ("leads ... at (p50, 24h)").
const hasTokens = spec.seo_description?.includes("{{");
const description = hasTokens ? spec.subtitle : (spec.seo_description ?? spec.subtitle);
items.push({
id: `bench:${spec.slug}`,
kind: "Benchmark",
title: spec.title,
url: `/benchmarks/${spec.slug}`,
description: truncate(spec.seo_description ?? spec.subtitle),
description: truncate(description),
tags: [spec.category, spec.slug],
});
}
Expand Down
Loading