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 uipkge-ng@latest add search-results-page$npx uipkge-ng@latest add search-results-page$yarn dlx uipkge-ng@latest add search-results-page$bunx uipkge-ng@latest add search-results-pageManifest:
https://uipkge.dev/r/angular/search-results-page.jsonInstalls to:src/app/components/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: LucideIconData
title: 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[]
}FilterChipinterface FilterChip {
id: string
label: string
}Files installed (2)
src/app/components/blocks/search-results-page/search-results-page.component.ts10.7 kB
// Full search results page: pre-filled search input with result count, removable // filter chips with clear-all, a left facet sidebar, six result rows, and a // Previous/Next pagination footer. Port of the React block 1:1. import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core' import { BookOpen, FileText, GitBranch, LucideAngularModule, MessageSquare, Search, Users, Video, type LucideIconData, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCheckboxComponent } from '@/ui/checkbox/checkbox.component' import { UiChipComponent } from '@/ui/chip/chip.component' import { UiInputComponent } from '@/ui/input/input.component' interface SearchResult { id: string icon: LucideIconData title: 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 FilterChip { id: string label: string } const RESULTS: SearchResult[] = [ { id: 'r1', icon: BookOpen, title: '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, title: '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, title: '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, title: '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, title: '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, title: '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 @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-search-results-page, [ui-search-results-page]', standalone: true, // React renders the root <div> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCheckboxComponent, UiChipComponent, UiInputComponent, ], template: ` <div data-slot="search-results-page" [class]="rootClass"> <div class="space-y-3"> <div class="relative max-w-xl"> <lucide-icon [img]="Search" aria-hidden="true" class="text-muted-foreground pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2" /> <ui-input type="search" [value]="query()" (valueChange)="query.set($event)" placeholder="Search..." class="h-11 pl-9 text-base" /> </div> <p class="text-muted-foreground text-sm">{{ resultRange() }} of 42 results for “{{ query() }}”</p> </div> @if (chips().length > 0) { <div class="flex flex-wrap items-center gap-2"> @for (chip of chips(); track chip.id) { <span ui-chip variant="outline" size="sm" closable (close)="removeChip(chip.id)"> {{ chip.label }} </span> } <button ui-button variant="link" size="sm" class="text-muted-foreground h-auto min-h-6 px-2" (click)="clearAll()" > Clear all </button> </div> } <div class="flex gap-8"> <aside class="hidden w-48 shrink-0 space-y-6 lg:block" aria-label="Filters"> @for (group of facetGroups; track group.id) { <div> <h3 class="text-muted-foreground mb-3 text-xs font-medium tracking-wider uppercase"> {{ group.label }} </h3> <ul class="space-y-2.5"> @for (option of group.options; track option.id) { <li> <label [for]="group.id + '-' + option.id" class="flex cursor-pointer items-center gap-2 text-sm"> <ui-checkbox [id]="group.id + '-' + option.id" [checked]="activeFacets().has(option.id)" (checkedChange)="toggleFacet(option.id, !!$event)" /> <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"> @for (result of resultsList; track result.id) { <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" > <lucide-icon [img]="result.icon" aria-hidden="true" class="size-5" /> </div> <div class="min-w-0 space-y-1"> <p class="truncate font-medium">{{ result.title }}</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"> <span ui-badge variant="secondary">{{ result.type }}</span> <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"> <button ui-button variant="outline" size="sm" [disabled]="page() === 1" (click)="prevPage()"> Previous </button> <div class="flex items-center gap-1"> @for (n of pageNumbers; track n) { <button ui-button [variant]="n === page() ? 'outline' : 'ghost'" size="icon-sm" [attr.aria-current]="n === page() ? 'page' : null" (click)="page.set(n)" > {{ n }} </button> } </div> <button ui-button variant="outline" size="sm" [disabled]="page() === totalPages" (click)="nextPage()"> Next </button> </nav> </div> </div> </div> `, }) export class UiSearchResultsPageComponent { protected readonly Search = Search protected readonly facetGroups = FACET_GROUPS protected readonly resultsList = RESULTS protected readonly pageNumbers = Array.from({ length: TOTAL_PAGES }, (_, i) => i + 1) protected readonly totalPages = TOTAL_PAGES @Input() set initialQuery(v: string | undefined) { if (v !== undefined) this.query.set(v) } @Input('class') className?: string readonly query = signal('onboarding flows') readonly chips = signal<FilterChip[]>([ { id: 'type-docs', label: 'Type: Docs' }, { id: 'team-product', label: 'Team: Product' }, ]) readonly activeFacets = signal<Set<string>>(new Set(['type-docs', 'team-product'])) readonly page = signal(1) readonly resultRange = computed(() => { const start = (this.page() - 1) * this.resultsList.length + 1 return `${start}-${start + this.resultsList.length - 1}` }) get rootClass(): string { return cn('w-full space-y-6', this.className) } removeChip(id: string): void { this.chips.update((prev) => prev.filter((chip) => chip.id !== id)) } clearAll(): void { this.chips.set([]) } toggleFacet(id: string, checked: boolean): void { this.activeFacets.update((prev) => { const next = new Set(prev) if (checked) next.add(id) else next.delete(id) return next }) } prevPage(): void { this.page.update((p) => Math.max(1, p - 1)) } nextPage(): void { this.page.update((p) => Math.min(TOTAL_PAGES, p + 1)) } }src/app/components/blocks/search-results-page/index.ts0.1 kB
export { UiSearchResultsPageComponent } from './search-results-page.component'
Raw manifest:https://uipkge.dev/r/angular/search-results-page.json