Narrow search column, add row spacing, render inline image previews (PAP-3468)

Tightens the /search results layout to a Google-style 680px-wide column
centered in the page, swaps the divider lines for `gap-y-1` + bumped row
padding so each result has visible breathing room, and renders an inline
image preview when a result references one.

Image previews: extracted server-side from the first markdown
`\![](url)` in description / commentSnippet / documentSnippet and surfaced
on `CompanySearchResult.previewImageUrl`. The row renders an 88×88 lazy
thumbnail when present.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
DottaandClaude Opus 4.7 committed 2026-05-05 11:05:07 -05:00
1 parent 5d92571313
commit f67f60d662
5 files changed
+53 -22

No files matched your search

+1
View File
@@ -41,6 +41,7 @@ export interface CompanySearchResult {
snippets: CompanySearchSnippet[];
issue?: CompanySearchIssueSummary;
updatedAt: string | null;
previewImageUrl: string | null;
}
export interface CompanySearchResponse {
+15
View File
@@ -109,6 +109,14 @@ function plainText(value: string | null | undefined) {
.trim();
}
const MARKDOWN_IMAGE_PATTERN = /!\[[^\]]*\]\(\s*([^)\s]+)(?:\s+"[^"]*")?\s*\)/;
function extractFirstImageUrl(value: string | null | undefined): string | null {
if (!value) return null;
const match = MARKDOWN_IMAGE_PATTERN.exec(value);
return match ? match[1] : null;
}
function findFirstMatchIndex(value: string, terms: string[]) {
const lower = value.toLowerCase();
let best = -1;
@@ -247,6 +255,10 @@ function issueResult(row: IssueSearchRow, prefix: string, normalizedQuery: strin
projectId: row.projectId,
updatedAt: iso(row.updatedAt)!,
};
const previewImageUrl =
extractFirstImageUrl(row.description) ??
extractFirstImageUrl(row.commentSnippet) ??
extractFirstImageUrl(row.documentSnippet);
return {
id: row.id,
type: "issue",
@@ -259,6 +271,7 @@ function issueResult(row: IssueSearchRow, prefix: string, normalizedQuery: strin
snippets,
issue,
updatedAt: issue.updatedAt,
previewImageUrl,
};
}
@@ -643,6 +656,7 @@ export function companySearchService(db: Db) {
snippet: snippet?.text ?? null,
snippets: snippet ? [snippet] : [],
updatedAt: iso(row.updatedAt),
previewImageUrl: null,
};
}),
...(projectRows as SimpleSearchRow[]).map((row) => {
@@ -659,6 +673,7 @@ export function companySearchService(db: Db) {
snippet: snippet?.text ?? null,
snippets: snippet ? [snippet] : [],
updatedAt: iso(row.updatedAt),
previewImageUrl: null,
};
}),
].sort((left, right) => {
+14 -4
View File
@@ -51,7 +51,7 @@ export interface SearchResultRowProps {
}
const ROW_BASE =
"group flex items-start gap-3 px-4 transition-colors no-underline text-inherit hover:bg-muted/40";
"group flex items-start gap-3 rounded-md px-3 transition-colors no-underline text-inherit hover:bg-muted/40";
function SearchResultRowImpl({
result,
@@ -63,7 +63,7 @@ function SearchResultRowImpl({
return (
<Link
to={result.href}
className={cn(ROW_BASE, "py-2.5", isActive && "bg-muted/40", className)}
className={cn(ROW_BASE, "py-3", isActive && "bg-muted/40", className)}
data-result-type="agent"
>
<span className="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground">
@@ -90,7 +90,7 @@ function SearchResultRowImpl({
return (
<Link
to={result.href}
className={cn(ROW_BASE, "py-2.5", isActive && "bg-muted/40", className)}
className={cn(ROW_BASE, "py-3", isActive && "bg-muted/40", className)}
data-result-type="project"
>
<Hexagon className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
@@ -117,12 +117,13 @@ function SearchResultRowImpl({
const updated = formatRelativeTime(result.updatedAt ?? issue.updatedAt);
const titleHighlights = result.snippets.find((snippet) => snippet.field === "title")?.highlights;
const bodySnippets = result.snippets.filter((snippet) => snippet.field !== "title").slice(0, 2);
const previewImageUrl = result.previewImageUrl;
return (
<Link
to={result.href}
disableIssueQuicklook
className={cn(ROW_BASE, "py-3", isActive && "bg-muted/40", className)}
className={cn(ROW_BASE, "py-4", isActive && "bg-muted/40", className)}
data-result-type="issue"
>
<div className="mt-1 shrink-0">
@@ -160,6 +161,15 @@ function SearchResultRowImpl({
{updated ? <span className="ml-auto tabular-nums">{updated}</span> : null}
</div>
</div>
{previewImageUrl ? (
<img
src={previewImageUrl}
alt=""
loading="lazy"
decoding="async"
className="ml-2 mt-0.5 h-[88px] w-[88px] shrink-0 rounded-md border border-border bg-muted object-cover"
/>
) : null}
</Link>
);
}
+7 -7
View File
@@ -577,20 +577,20 @@ function SearchTabContent({
if (!hasResults) return null;
return (
<div className="flex flex-col" data-testid="search-results">
<div className="flex items-center justify-between px-4 py-2 text-[11px] uppercase tracking-wide text-muted-foreground">
<div className="mx-auto flex w-full max-w-[680px] flex-col px-2 sm:px-4" data-testid="search-results">
<div className="flex items-center justify-between py-2 text-[11px] uppercase tracking-wide text-muted-foreground">
<span>
{totalResults === 1 ? "1 result" : `${totalResults} results`} · sorted by relevance
</span>
{isFetching ? <span aria-live="polite" className="normal-case tracking-normal">Updating…</span> : null}
</div>
<div className="flex flex-col">
<div className="flex flex-col pb-10">
{scope === "all" ? (
subgroups.map((group, groupIndex) => (
<section
key={group.key}
aria-label={SUBGROUP_LABELS[group.key]}
className={cn("flex flex-col", groupIndex > 0 && "mt-3 border-t border-border")}
className={cn("flex flex-col", groupIndex > 0 && "mt-6")}
>
<IssueGroupHeader
label={SUBGROUP_LABELS[group.key]}
@@ -599,9 +599,9 @@ function SearchTabContent({
{group.results.length}
</span>
}
className="px-4 pt-2 pb-1 text-[11px] tracking-wider text-muted-foreground"
className="pt-2 pb-1 text-[11px] tracking-wider text-muted-foreground"
/>
<div className="flex flex-col divide-y divide-border/50">
<div className="flex flex-col gap-y-1">
{group.results.map((result) => (
<SearchResultRow
key={`${result.type}:${result.id}:${result.href}`}
@@ -613,7 +613,7 @@ function SearchTabContent({
</section>
))
) : (
<div className="flex flex-col divide-y divide-border/50">
<div className="flex flex-col gap-y-1">
{subgroups
.flatMap((group) => group.results)
.map((result) => (
+16 -11
View File
@@ -62,6 +62,7 @@ function buildIssueResult(overrides: IssueResultOverrides): CompanySearchResult
snippets: overrides.snippets ?? [],
issue: baseIssue,
updatedAt: baseIssue.updatedAt,
previewImageUrl: overrides.previewImageUrl ?? null,
};
}
@@ -112,6 +113,8 @@ const fixtureResults: CompanySearchResult[] = [
highlights: [{ start: 12, end: 16 }, { start: 26, end: 30 }],
},
],
previewImageUrl:
"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect width='100' height='100' fill='%23a78bfa'/><text x='50' y='55' font-size='14' fill='white' text-anchor='middle' font-family='sans-serif'>chart</text></svg>",
}),
buildIssueResult({
id: "issue-3",
@@ -154,6 +157,7 @@ const fixtureAgents: CompanySearchResult[] = storybookAgents.slice(0, 1).map((ag
]
: [],
updatedAt: new Date().toISOString(),
previewImageUrl: null,
}));
const fixtureProjects: CompanySearchResult[] = storybookProjects.slice(0, 1).map((project) => ({
@@ -176,6 +180,7 @@ const fixtureProjects: CompanySearchResult[] = storybookProjects.slice(0, 1).map
]
: [],
updatedAt: new Date().toISOString(),
previewImageUrl: null,
}));
const fixtureResponse: CompanySearchResponse = {
@@ -261,8 +266,8 @@ function SearchPagePreview({
</div>
{state === "results" ? (
<div className="flex flex-col">
<div className="flex items-center justify-between px-4 py-2 text-[11px] uppercase tracking-wide text-muted-foreground">
<div className="mx-auto flex w-full max-w-[680px] flex-col px-2 sm:px-4">
<div className="flex items-center justify-between py-2 text-[11px] uppercase tracking-wide text-muted-foreground">
<span>{response.results.length} results · sorted by relevance</span>
</div>
<section aria-label="Issues" className="flex flex-col">
@@ -273,9 +278,9 @@ function SearchPagePreview({
{fixtureResults.length}
</span>
}
className="px-4 pt-2 pb-1 text-[11px] tracking-wider text-muted-foreground"
className="pt-2 pb-1 text-[11px] tracking-wider text-muted-foreground"
/>
<div className="flex flex-col divide-y divide-border/50">
<div className="flex flex-col gap-y-1">
{fixtureResults.map((result) => (
<SearchResultRow
key={result.id}
@@ -285,7 +290,7 @@ function SearchPagePreview({
))}
</div>
</section>
<section aria-label="Agents" className="mt-3 flex flex-col border-t border-border">
<section aria-label="Agents" className="mt-6 flex flex-col">
<IssueGroupHeader
label="Agents"
trailing={
@@ -293,15 +298,15 @@ function SearchPagePreview({
{fixtureAgents.length}
</span>
}
className="px-4 pt-2 pb-1 text-[11px] tracking-wider text-muted-foreground"
className="pt-2 pb-1 text-[11px] tracking-wider text-muted-foreground"
/>
<div className="flex flex-col divide-y divide-border/50">
<div className="flex flex-col gap-y-1">
{fixtureAgents.map((result) => (
<SearchResultRow key={result.id} result={result} />
))}
</div>
</section>
<section aria-label="Projects" className="mt-3 flex flex-col border-t border-border">
<section aria-label="Projects" className="mt-6 flex flex-col">
<IssueGroupHeader
label="Projects"
trailing={
@@ -309,9 +314,9 @@ function SearchPagePreview({
{fixtureProjects.length}
</span>
}
className="px-4 pt-2 pb-1 text-[11px] tracking-wider text-muted-foreground"
className="pt-2 pb-1 text-[11px] tracking-wider text-muted-foreground"
/>
<div className="flex flex-col divide-y divide-border/50">
<div className="flex flex-col gap-y-1">
{fixtureProjects.map((result) => (
<SearchResultRow key={result.id} result={result} />
))}
@@ -572,7 +577,7 @@ function SearchStories() {
<div className="paperclip-story__label">Search result row</div>
<h2 className="mt-1 text-lg font-semibold">Issue, agent, project rows</h2>
</div>
<div className="flex flex-col divide-y divide-border">
<div className="mx-auto flex w-full max-w-[680px] flex-col gap-y-1">
{fixtureResults.map((result) => (
<SearchResultRow
key={result.id}