From 0ae2bb0b6008d86206e566a959f1275a850d1554 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 3 May 2026 21:39:44 -0500 Subject: [PATCH] Clean up search page visual design (PAP-3468) Drop the candy-colored Title/Comment/Doc chips that competed with the title and were redundant with the snippet field labels. Replace the all-caps snippet labels with subtle leading icons (MessageSquare, FileText, Quote) so each result reads as one tidy line of metadata under the title. Bump row padding, lighten the group section header treatment (no muted-bg slab), and switch row dividers to a softer border-border/50. Co-Authored-By: Paperclip --- ui/src/components/search/SearchResultRow.tsx | 154 +++++++++---------- ui/src/pages/Search.tsx | 19 ++- ui/storybook/stories/search.stories.tsx | 18 +-- 3 files changed, 91 insertions(+), 100 deletions(-) diff --git a/ui/src/components/search/SearchResultRow.tsx b/ui/src/components/search/SearchResultRow.tsx index 9a341bf022a..11bb0e3da33 100644 --- a/ui/src/components/search/SearchResultRow.tsx +++ b/ui/src/components/search/SearchResultRow.tsx @@ -1,37 +1,26 @@ -import { useMemo } from "react"; -import { Bot, Hexagon } from "lucide-react"; +import type { ComponentType, SVGProps } from "react"; +import { Bot, FileText, Hexagon, MessageSquare, Quote } from "lucide-react"; import type { Agent, CompanySearchResult, Project } from "@paperclipai/shared"; import { Link } from "@/lib/router"; import { cn } from "@/lib/utils"; import { StatusIcon } from "../StatusIcon"; import { StatusBadge } from "../StatusBadge"; import { Identity } from "../Identity"; -import { HighlightedText } from "./HighlightedText"; -import { MatchSourceChip, type MatchSourceChipKind } from "./MatchSourceChip"; +import { HighlightedText, type HighlightedTextProps } from "./HighlightedText"; -const SNIPPET_FIELD_LABEL: Record = { - comment: "COMMENT", - document: "DOC", - identifier: "IDENTIFIER", - description: "DESCRIPTION", - title: "TITLE", +type SnippetStyle = { + Icon: ComponentType>; + label: string; }; -function deriveChips(matchedFields: string[]): Array<{ kind: MatchSourceChipKind; count?: number }> { - const counts = new Map(); - for (const field of matchedFields) { - let kind: MatchSourceChipKind | null = null; - if (field === "title" || field === "description") kind = "title"; - else if (field === "identifier") kind = "identifier"; - else if (field === "comment") kind = "comment"; - else if (field === "document") kind = "document"; - if (!kind) continue; - counts.set(kind, (counts.get(kind) ?? 0) + 1); - } - const order: MatchSourceChipKind[] = ["title", "identifier", "comment", "document"]; - return order - .filter((kind) => counts.has(kind)) - .map((kind) => ({ kind, count: counts.get(kind) ?? 1 })); +const SNIPPET_STYLES: Record = { + comment: { Icon: MessageSquare, label: "Comment" }, + document: { Icon: FileText, label: "Doc" }, + description: { Icon: Quote, label: "Description" }, +}; + +function snippetStyle(field: string, fallbackLabel: string): SnippetStyle { + return SNIPPET_STYLES[field] ?? { Icon: Quote, label: fallbackLabel }; } function formatRelativeTime(input: string | null): string { @@ -70,20 +59,18 @@ export function SearchResultRow({ isActive, className, }: SearchResultRowProps) { - const chips = useMemo(() => deriveChips(result.matchedFields), [result.matchedFields]); - if (result.type === "agent") { return ( - +
@@ -91,16 +78,12 @@ export function SearchResultRow({ {result.title}
{result.snippet ? ( -
- - {result.sourceLabel ?? "Agent"} - - -
+ ) : null} @@ -112,7 +95,7 @@ export function SearchResultRow({ {result.title} {result.snippet ? ( -
- - {result.sourceLabel ?? "Project"} - - -
+ ) : null} @@ -146,72 +125,57 @@ export function SearchResultRow({ const projectName = issue.projectId ? projectsById?.get(issue.projectId)?.name ?? null : null; 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); return ( -
+
-
+
{issue.identifier ? ( - + {issue.identifier} ) : null} -
+
{assigneeName ? : null} {projectName ? ( - + {projectName} ) : null} {updated ? ( - {updated} + {updated} ) : null}
- {chips.length > 0 ? ( -
- {chips.map((chip) => ( - - ))} -
- ) : null} - {result.snippets - .filter((snippet) => snippet.field !== "title") - .slice(0, 2) - .map((snippet, index) => ( -
- - {SNIPPET_FIELD_LABEL[snippet.field] ?? snippet.label.toUpperCase()} - - -
- ))} -
+ {bodySnippets.map((snippet, index) => ( + + ))} +
{assigneeName ? {assigneeName} : null} {projectName ? · {projectName} : null} @@ -221,3 +185,25 @@ export function SearchResultRow({ ); } + +interface SnippetLineProps { + text: string; + highlights?: HighlightedTextProps["highlights"]; + field: string; + fallbackLabel: string; +} + +function SnippetLine({ text, highlights, field, fallbackLabel }: SnippetLineProps) { + const { Icon, label } = snippetStyle(field, fallbackLabel); + return ( +
+ + {label}: + +
+ ); +} diff --git a/ui/src/pages/Search.tsx b/ui/src/pages/Search.tsx index 458af4c24c6..1331345a56a 100644 --- a/ui/src/pages/Search.tsx +++ b/ui/src/pages/Search.tsx @@ -13,6 +13,7 @@ import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; import { Badge } from "@/components/ui/badge"; +import { cn } from "@/lib/utils"; import { useNavigate, useSearchParams } from "@/lib/router"; import { useCompany } from "../context/CompanyContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; @@ -593,16 +594,20 @@ function SearchTabContent({ return (
-
+
{totalResults === 1 ? "1 result" : `${totalResults} results`} · sorted by relevance - {isFetching ? Updating… : null} + {isFetching ? Updating… : null}
{scope === "all" ? ( - subgroups.map((group) => ( -
+ subgroups.map((group, groupIndex) => ( +
0 && "mt-3 border-t border-border")} + > } - className="border-b border-border bg-muted/30" + className="px-4 pt-2 pb-1 text-[11px] tracking-wider text-muted-foreground" /> -
+
{group.results.map((result) => ( )) ) : ( -
+
{subgroups .flatMap((group) => group.results) .map((result) => ( diff --git a/ui/storybook/stories/search.stories.tsx b/ui/storybook/stories/search.stories.tsx index 40b1bfb2aa8..3bf2507cdc7 100644 --- a/ui/storybook/stories/search.stories.tsx +++ b/ui/storybook/stories/search.stories.tsx @@ -263,7 +263,7 @@ function SearchPagePreview({ {state === "results" ? (
-
+
{response.results.length} results · sorted by relevance
@@ -274,9 +274,9 @@ function SearchPagePreview({ {fixtureResults.length} } - className="border-b border-border bg-muted/30" + className="px-4 pt-2 pb-1 text-[11px] tracking-wider text-muted-foreground" /> -
+
{fixtureResults.map((result) => (
-
+
} - className="border-b border-border bg-muted/30" + className="px-4 pt-2 pb-1 text-[11px] tracking-wider text-muted-foreground" /> -
+
{fixtureAgents.map((result) => ( ))}
-
+
} - className="border-b border-border bg-muted/30" + className="px-4 pt-2 pb-1 text-[11px] tracking-wider text-muted-foreground" /> -
+
{fixtureProjects.map((result) => ( ))}