feat(search): detail chips on result cards; phones keep only Preview

Result cards show the backend's secondary field values (details) as
label/value chips instead of a run of text, so a record's key fields
read at a glance; screen readers still hear "Label: value".

On phones (below sm) the card keeps only its Preview button: New tab and
Copy link are in the preview sheet, and the snippet gets the width
instead of breaking mid-word.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
james.maesandClaude Opus 5.5 committed 2026-10-12 12:13:32 -05:00
1 parent 9ce3b81b04
commit 6c1e3a67b1
3 files changed
+20 -8

No files matched your search

@@ -136,6 +136,8 @@ describe('SearchPage', () => {
const card = within(accountsSection).getAllByRole('listitem')[0]
expect(card).toHaveTextContent('Domain:acme.com')
expect(card).toHaveTextContent('City: Austin')
const chips = card.querySelectorAll('[data-qqq-id="search-result-detail-chip"]')
expect(Array.from(chips).map((chip) => chip.textContent)).toEqual(['City: Austin'])
expect(card.querySelector('time')).toHaveAttribute('dateTime')
expect(Array.from(card.querySelectorAll('mark')).map((mark) => mark.textContent)).toEqual(['Acme', 'acme'])
expect(within(accountsSection).getByRole('link', { name: 'Show all' })).toHaveAttribute('href', '/app/search?q=acme&tables=account')
+15 -8
View File
@@ -98,7 +98,7 @@ async function copyLink(path: string): Promise<void> {
}
/** Classes of a quick action button. */
const ACTION_CLASS = 'inline-flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring'
const ACTION_CLASS = 'h-9 w-9 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring'
/**
* Renders one record result as a list item.
@@ -116,6 +116,9 @@ export function SearchResultCard({ result, tableLabel, icon, term, showTable = f
pressTimer.current = null
}
const path = recordSearchResultPath(result)
// On phones the preview sheet offers New tab and Copy link, so the card keeps only Preview
// and its text gets the width
const secondaryActionClass = cn(ACTION_CLASS, onPreview ? 'hidden sm:inline-flex' : 'inline-flex')
const highlights = (result.highlights ?? []).filter((highlight) => highlight.text.trim()).slice(0, 3)
const details = (result.details ?? []).filter((detail) => detail.displayValue.trim()).slice(0, 4)
@@ -176,11 +179,15 @@ export function SearchResultCard({ result, tableLabel, icon, term, showTable = f
</dl>
)}
{(details.length > 0 || result.updatedAt) && (
<div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-xs text-muted-foreground">
<div className="mt-1.5 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground">
{details.map((detail) => (
<span key={detail.fieldName} className="min-w-0 break-words">
{detail.fieldLabel && <span className="text-muted-foreground/80">{detail.fieldLabel}: </span>}
{detail.displayValue}
<span
key={detail.fieldName}
className="inline-flex min-w-0 max-w-full items-baseline gap-1 rounded-full border border-border bg-muted/40 px-2 py-0.5"
data-qqq-id="search-result-detail-chip"
>
{detail.fieldLabel && <span className="shrink-0 text-muted-foreground">{detail.fieldLabel}<span className="sr-only">: </span></span>}
<span className="min-w-0 break-words font-medium text-foreground">{detail.displayValue}</span>
</span>
))}
{result.updatedAt && (
@@ -197,7 +204,7 @@ export function SearchResultCard({ result, tableLabel, icon, term, showTable = f
<button
type="button"
onClick={onPreview}
className={ACTION_CLASS}
className={cn(ACTION_CLASS, 'inline-flex')}
aria-label={`Preview ${result.recordLabel}`}
aria-pressed={active}
title="Preview"
@@ -210,7 +217,7 @@ export function SearchResultCard({ result, tableLabel, icon, term, showTable = f
href={path}
target="_blank"
rel="noopener noreferrer"
className={ACTION_CLASS}
className={secondaryActionClass}
aria-label={`Open ${result.recordLabel} in a new tab`}
title="Open in new tab"
data-qqq-id="search-result-new-tab"
@@ -220,7 +227,7 @@ export function SearchResultCard({ result, tableLabel, icon, term, showTable = f
<button
type="button"
onClick={() => void copyLink(path)}
className={ACTION_CLASS}
className={secondaryActionClass}
aria-label={`Copy link to ${result.recordLabel}`}
title="Copy link"
data-qqq-id="search-result-copy-link"
+3
View File
@@ -257,8 +257,11 @@ test.describe('Search page', () => {
await page.goto('/app/search?q=alice')
await waitForAppReady(page)
// the card keeps only Preview on phones; the sheet offers New tab and Copy link
await expect(page.getByRole('link', { name: 'Open Alice Johnson in a new tab' })).toBeHidden()
await page.getByRole('button', { name: 'Preview Alice Johnson' }).click()
const sheet = page.getByRole('dialog', { name: 'Preview' })
await expect(sheet.getByRole('button', { name: 'Copy link' })).toBeVisible()
await expect(sheet.getByRole('heading', { name: 'Alice Johnson' })).toBeVisible()
await expect(sheet.locator('[data-qqq-id="search-preview-fields"]')).toContainText('alice@example.com')
await sheet.getByRole('button', { name: 'Close preview' }).click()