Search Results Page
blocklayoutFull search results page: large pre-filled search input with result count, removable filter chips with clear-all, a left facet sidebar with Type and Team checkbox groups (hidden below lg), six result rows with icon tiles, titles, paths, snippets, and type/updated meta, plus a Previous/Next pagination footer. Chips and facets are interactive; swap the stub rows for your search source.
Installation
$pnpm dlx shadcn@latest add https://uipkge.dev/r/lit/search-results-page.json$npx shadcn@latest add https://uipkge.dev/r/lit/search-results-page.json$yarn dlx shadcn@latest add https://uipkge.dev/r/lit/search-results-page.json$bunx shadcn@latest add https://uipkge.dev/r/lit/search-results-page.jsonManifest:
https://uipkge.dev/r/lit/search-results-page.jsonInstalls to:src/uip/blocks/search-results-page/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
SearchResultinterface SearchResult {
id: string
icon: IconNode
iconName: string
heading: string
path: string
snippet: string
type: string
updated: string
}FacetOptioninterface FacetOption {
id: string
label: string
count: number
}FacetGroupinterface FacetGroup {
id: string
label: string
options: FacetOption[]
}Chipinterface Chip {
id: string
label: string
}HTMLElementTagNameMapinterface HTMLElementTagNameMap {
'uip-search-results-page': UipSearchResultsPage
}
}Files installed (1)
src/uip/blocks/search-results-page/search-results-page.ts11.7 kB
import { LitElement, css, html, nothing } from 'lit' import { BookOpen, FileText, GitBranch, MessageSquare, Search, Users, Video, type IconNode } from 'lucide' import { cn } from '../../lib/utils' import { tailwind } from '../../lib/styles' import { ThemeController } from '../../lib/theme' import { icon } from '../../lib/icon' // Side-effect imports: register the composed elements for standalone use // (consumers who install only this block still get working primitives). import '../../components/badge/badge' import '../../components/button/button' import '../../components/checkbox/checkbox' import '../../components/chip/chip' import '../../components/input/input' import type { UipInput } from '../../components/input/input' interface SearchResult { id: string icon: IconNode iconName: string heading: string path: string snippet: string type: string updated: string } interface FacetOption { id: string label: string count: number } interface FacetGroup { id: string label: string options: FacetOption[] } interface Chip { id: string label: string } const RESULTS: SearchResult[] = [ { id: 'r1', icon: BookOpen, iconName: 'book-open', heading: 'Onboarding flows: design your first-run experience', path: 'docs.acme.dev / guides / onboarding-flows', snippet: 'Learn how to compose onboarding flows from checklists, tours, and progressive disclosure so new users reach their first aha moment faster.', type: 'Docs', updated: '2 days ago', }, { id: 'r2', icon: FileText, iconName: 'file-text', heading: 'Checklist API reference for onboarding flows', path: 'docs.acme.dev / api / checklist', snippet: 'Full reference for the Checklist primitive used to drive onboarding flows, including step completion events, persistence, and theming.', type: 'Reference', updated: '5 hours ago', }, { id: 'r3', icon: Video, iconName: 'video', heading: 'Video walkthrough: building onboarding flows in 20 minutes', path: 'learn.acme.dev / videos / onboarding-flows-walkthrough', snippet: 'A hands-on screencast that builds a complete onboarding flow with branching steps, skippable tours, and analytics instrumentation.', type: 'Video', updated: '1 week ago', }, { id: 'r4', icon: MessageSquare, iconName: 'message-square', heading: 'How do you measure completion of onboarding flows?', path: 'community.acme.dev / discussions / 4821', snippet: 'Community thread comparing activation-rate funnels, step-drop-off charts, and qualitative surveys for measuring onboarding flow success.', type: 'Discussion', updated: '3 days ago', }, { id: 'r5', icon: GitBranch, iconName: 'git-branch', heading: 'Example repo: onboarding flows with A/B-tested variants', path: 'github.com / acme / examples / onboarding-ab-test', snippet: 'Runnable example that splits new signups into two onboarding flow variants and reports which one converts better to first project creation.', type: 'Example', updated: '2 weeks ago', }, { id: 'r6', icon: Users, iconName: 'users', heading: 'Playbook: personalizing onboarding flows by team role', path: 'docs.acme.dev / playbooks / role-based-onboarding', snippet: 'Patterns for tailoring onboarding flows to admins, editors, and viewers, with sample copy, suggested defaults, and rollout checklists.', type: 'Guide', updated: '4 days ago', }, ] const FACET_GROUPS: FacetGroup[] = [ { id: 'type', label: 'Type', options: [ { id: 'type-docs', label: 'Docs', count: 18 }, { id: 'type-guides', label: 'Guides', count: 11 }, { id: 'type-video', label: 'Video', count: 7 }, { id: 'type-discussion', label: 'Discussion', count: 6 }, ], }, { id: 'team', label: 'Team', options: [ { id: 'team-product', label: 'Product', count: 14 }, { id: 'team-engineering', label: 'Engineering', count: 16 }, { id: 'team-design', label: 'Design', count: 8 }, { id: 'team-support', label: 'Support', count: 4 }, ], }, ] const TOTAL_PAGES = 3 /** * <uip-search-results-page> — the registry SearchResultsPage block as a web * component. * * Class strings are React's SearchResultsPage verbatim. React state (query, * chips, facets, page) becomes @state fields. No props, no events: swap the * stub rows for your search source. * * Deviations: result `title` is `heading` in the internal row type only (no * public prop, no rename needed). The search field icon moves from React's * absolute overlay into <uip-input>'s prefix slot (same visual result; the * input's `pl-9` is dropped since the slot provides the gutter). */ export class UipSearchResultsPage extends LitElement { static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true } // :host display can't be a utility class (the host has no template of its own). static styles = [tailwind, css`:host { display: block; }`] static properties = { query: { state: true }, chips: { state: true }, activeFacets: { state: true }, page: { state: true }, } private query = 'onboarding flows' private chips: Chip[] = [ { id: 'type-docs', label: 'Type: Docs' }, { id: 'team-product', label: 'Team: Product' }, ] private activeFacets: Set<string> = new Set(['type-docs', 'team-product']) private page = 1 constructor() { super() new ThemeController(this) } connectedCallback() { super.connectedCallback() this.setAttribute('data-uipkge', '') this.setAttribute('data-slot', 'search-results-page') } private removeChip(id: string) { this.chips = this.chips.filter((chip) => chip.id !== id) } private toggleFacet(id: string, checked: boolean) { const next = new Set(this.activeFacets) if (checked) next.add(id) else next.delete(id) this.activeFacets = next } render() { const start = (this.page - 1) * RESULTS.length + 1 const resultRange = `${start}-${start + RESULTS.length - 1}` return html` <div data-slot="search-results-page" class=${cn('w-full space-y-6')}> <div class="space-y-3"> <div class="relative max-w-xl"> <uip-input type="search" class="[&::part(control)]:h-11 text-base" .value=${this.query} placeholder="Search..." @input=${(e: Event) => { this.query = (e.target as UipInput).value }} > <span slot="prefix">${icon(Search, 'search', 'size-4')}</span> </uip-input> </div> <p class="text-muted-foreground text-sm">${resultRange} of 42 results for “${this.query}”</p> </div> ${this.chips.length > 0 ? html`<div class="flex flex-wrap items-center gap-2"> ${this.chips.map( (chip) => html` <uip-chip variant="outline" size="sm" closable @close=${() => this.removeChip(chip.id)}> ${chip.label} </uip-chip> `, )} <uip-button variant="link" size="sm" class="text-muted-foreground h-auto min-h-6 px-2" @click=${() => (this.chips = [])} > Clear all </uip-button> </div>` : nothing} <div class="flex gap-8"> <aside class="hidden w-48 shrink-0 space-y-6 lg:block" aria-label="Filters"> ${FACET_GROUPS.map( (group) => html` <div> <h3 class="text-muted-foreground mb-3 text-xs font-medium tracking-wider uppercase"> ${group.label} </h3> <ul class="space-y-2.5"> ${group.options.map( (option) => html` <li> <label for=${`${group.id}-${option.id}`} class="flex cursor-pointer items-center gap-2 text-sm" > <uip-checkbox id=${`${group.id}-${option.id}`} .checked=${this.activeFacets.has(option.id)} @checked-change=${(e: CustomEvent<{ checked: boolean }>) => this.toggleFacet(option.id, e.detail.checked)} ></uip-checkbox> <span>${option.label}</span> <span class="text-muted-foreground ml-auto text-xs tabular-nums">${option.count}</span> </label> </li> `, )} </ul> </div> `, )} </aside> <div class="min-w-0 flex-1"> <ol class="divide-border divide-y"> ${RESULTS.map( (result) => html` <li> <a href="#" class="hover:bg-muted/50 focus-visible:ring-ring block rounded-lg p-4 transition-colors focus-visible:ring-2 focus-visible:outline-none" > <div class="flex gap-4"> <div class="bg-muted/50 text-muted-foreground flex size-10 shrink-0 items-center justify-center rounded-md border" > ${icon(result.icon, result.iconName, 'size-5')} </div> <div class="min-w-0 space-y-1"> <p class="truncate font-medium">${result.heading}</p> <p class="text-muted-foreground truncate text-xs">${result.path}</p> <p class="text-muted-foreground line-clamp-2 text-sm">${result.snippet}</p> <div class="flex items-center gap-3 pt-1"> <uip-badge variant="secondary">${result.type}</uip-badge> <span class="text-muted-foreground text-xs">Updated ${result.updated}</span> </div> </div> </div> </a> </li> `, )} </ol> <nav class="flex items-center justify-between pt-4" aria-label="Pagination"> <uip-button variant="outline" size="sm" ?disabled=${this.page === 1} @click=${() => (this.page -= 1)}> Previous </uip-button> <div class="flex items-center gap-1"> ${Array.from({ length: TOTAL_PAGES }, (_, i) => i + 1).map( (n) => html` <uip-button variant=${n === this.page ? 'outline' : 'ghost'} size="icon-sm" aria-current=${n === this.page ? 'page' : nothing} @click=${() => (this.page = n)} > ${n} </uip-button> `, )} </div> <uip-button variant="outline" size="sm" ?disabled=${this.page === TOTAL_PAGES} @click=${() => (this.page += 1)} > Next </uip-button> </nav> </div> </div> </div> ` } } customElements.get('uip-search-results-page') || customElements.define('uip-search-results-page', UipSearchResultsPage) declare global { interface HTMLElementTagNameMap { 'uip-search-results-page': UipSearchResultsPage } }
Raw manifest:https://uipkge.dev/r/lit/search-results-page.json