Faq Searchable Accordion Workbench
blockmarketingSearchable FAQ workbench with real-time query filtering, category switcher, collapsible accordions, and community support links.
Installation
$pnpm dlx uipkge-ng@latest add faq-searchable-accordion-workbench$npx uipkge-ng@latest add faq-searchable-accordion-workbench$yarn dlx uipkge-ng@latest add faq-searchable-accordion-workbench$bunx uipkge-ng@latest add faq-searchable-accordion-workbenchManifest:
https://uipkge.dev/r/angular/faq-searchable-accordion-workbench.jsonInstalls to:src/app/components/blocks/faq-searchable-accordion-workbench/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.
FaqIteminterface FaqItem {
id: string
category: 'architecture' | 'parity' | 'licensing'
question: string
answer: string
tag: string
}npm dependencies
Files installed (2)
src/app/components/blocks/faq-searchable-accordion-workbench/faq-searchable-accordion-workbench.component.ts10.6 kB
// Searchable FAQ workbench with real-time query filtering, category switcher, collapsible // accordions, and community support links. Port of the React block 1:1. import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core' import { ChevronDown, HelpCircle, LucideAngularModule, MessageSquare, Search, Terminal } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent } from '@/ui/card/card.component' import { UiInputComponent } from '@/ui/input/input.component' type FaqCategory = 'all' | 'architecture' | 'parity' | 'licensing' interface FaqItem { id: string category: 'architecture' | 'parity' | 'licensing' question: string answer: string tag: string } const faqItems: FaqItem[] = [ { id: '1', category: 'architecture', question: 'How is UIPKGE different from standard component packages on npm?', answer: 'UIPKGE does not publish monolithic npm packages. When you run our CLI command, raw Vue 3.5 SFCs or React 19 TSX files are copied directly into your repository. You own 100% of the code, have zero semver breaking risks, and can customize any token, prop, or micro-interaction without waiting on maintainers.', tag: 'Unbundled AST', }, { id: '2', category: 'parity', question: 'How does dual-framework parity work between Vue and React?', answer: 'Every primitive and block in UIPKGE is authored with identical class tokens, CVA variant definitions, and OKLCH color palettes across both Vue (Reka UI) and React (Radix UI). Automated parity check scripts in CI ensure 100% token consistency across frameworks.', tag: 'Dual-Framework', }, { id: '3', category: 'architecture', question: 'Can I customize the Tailwind CSS v4 design tokens?', answer: 'Yes. All design tokens are declared using standard Tailwind CSS v4 `@theme inline` CSS custom properties in `styles/tailwind.css`. You can change primary colors, border radii, or spring curves globally in one file.', tag: 'Tailwind v4', }, { id: '4', category: 'licensing', question: 'What is the licensing model for commercial projects?', answer: 'All UIPKGE components and blocks are 100% MIT-licensed. You can use them freely in commercial applications, client projects, and internal tools without paying per-seat subscriptions or attribution fees.', tag: 'MIT License', }, { id: '5', category: 'parity', question: 'Which headless primitives power UIPKGE components?', answer: 'In Vue 3.5, primitives use Reka UI for keyboard ergonomics and ARIA compliance. In React 19, primitives utilize Radix UI and standard headless primitives, ensuring complete accessibility compliance (WCAG AA).', tag: 'Headless A11y', }, { id: '6', category: 'licensing', question: 'Do you offer custom enterprise component design engineering?', answer: 'Yes. Our enterprise tier includes dedicated engineering syncs, bespoke block authoring, private organizational registry hubs, and priority architectural audits.', tag: 'Enterprise SLA', }, ] @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-faq-searchable-accordion-workbench, [ui-faq-searchable-accordion-workbench]', standalone: true, // React renders the root <section> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiInputComponent], template: ` <section data-slot="faq-searchable-accordion-workbench" [class]="rootClass"> <div class="mx-auto max-w-4xl space-y-12"> <!-- Section Header --> <div class="space-y-4 text-center"> <span ui-badge variant="secondary" class="gap-1.5 px-3 py-1 font-mono text-xs shadow-xs"> <lucide-icon [img]="HelpCircle" class="text-primary size-3.5" /> Technical FAQ Workbench </span> <h2 class="text-foreground text-3xl font-bold tracking-tight sm:text-4xl"> Frequently asked architectural questions. </h2> <p class="text-muted-foreground text-base"> Everything you need to know about our unbundled registry distribution model. </p> <!-- Search & Filter Bar --> <div class="mx-auto max-w-xl space-y-3 pt-4"> <div class="relative"> <lucide-icon [img]="Search" class="text-muted-foreground pointer-events-none absolute top-1/2 left-3.5 size-4 -translate-y-1/2" /> <ui-input [value]="searchQuery()" (valueChange)="searchQuery.set($event)" placeholder="Search architecture, tokens, licensing, or parity..." class="bg-card border-border h-11 rounded-xl pl-10 font-mono text-xs shadow-sm" /> </div> <div class="flex flex-wrap items-center justify-center gap-1.5"> <button type="button" [class]="pillClass('all')" (click)="selectedCategory.set('all')"> All ({{ totalCount }}) </button> <button type="button" [class]="pillClass('architecture')" (click)="selectedCategory.set('architecture')"> Architecture </button> <button type="button" [class]="pillClass('parity')" (click)="selectedCategory.set('parity')"> Dual-Framework Parity </button> <button type="button" [class]="pillClass('licensing')" (click)="selectedCategory.set('licensing')"> Licensing & SLA </button> </div> </div> </div> <!-- FAQ Accordion List --> <div class="space-y-3"> @for (faq of filteredFaqs(); track faq.id) { <div [class]="cardClass(isOpen(faq.id))"> <button type="button" class="flex w-full items-center justify-between gap-4 p-5 text-left font-mono" (click)="toggleItem(faq.id)" [attr.aria-expanded]="isOpen(faq.id)" [attr.aria-controls]="'faq-answer-' + faq.id" > <div class="flex min-w-0 items-center gap-3"> <span ui-badge variant="outline" class="shrink-0 font-mono text-xs">{{ faq.tag }}</span> <h3 class="text-foreground truncate text-sm font-bold sm:text-base">{{ faq.question }}</h3> </div> <lucide-icon [img]="ChevronDown" [class]="chevronClass(isOpen(faq.id))" /> </button> @if (isOpen(faq.id)) { <div [id]="'faq-answer-' + faq.id" class="text-muted-foreground border-border/40 border-t px-5 pt-1 pb-5 text-xs leading-relaxed sm:text-sm" > {{ faq.answer }} </div> } </div> } @if (filteredFaqs().length === 0) { <div class="border-border rounded-2xl border border-dashed p-12 text-center"> <p class="text-muted-foreground font-mono text-xs"> No questions match your query "{{ searchQuery() }}". </p> </div> } </div> <!-- Live Support Footer Card --> <div ui-card class="border-border bg-card/50 flex flex-col items-center justify-between gap-4 rounded-2xl p-6 text-center sm:flex-row sm:text-left" > <div class="space-y-1"> <h4 class="text-foreground font-mono text-sm font-bold">Have a unique technical requirement?</h4> <p class="text-muted-foreground text-xs">Chat with the core design engineering team in real time.</p> </div> <div class="flex shrink-0 items-center gap-2"> <button ui-button size="sm" variant="outline" class="h-9 gap-1.5 font-mono text-xs"> <lucide-icon [img]="MessageSquare" class="size-3.5" /> <span>Join Discord</span> </button> <button ui-button size="sm" class="h-9 gap-1.5 font-mono text-xs"> <lucide-icon [img]="Terminal" class="size-3.5" /> <span>GitHub Discussions</span> </button> </div> </div> </div> </section> `, }) export class UiFaqSearchableAccordionWorkbenchComponent { protected readonly ChevronDown = ChevronDown protected readonly HelpCircle = HelpCircle protected readonly MessageSquare = MessageSquare protected readonly Search = Search protected readonly Terminal = Terminal protected readonly totalCount = faqItems.length @Input('class') className?: string @Input() set initialCategory(v: FaqCategory) { this.selectedCategory.set(v ?? 'all') } readonly searchQuery = signal('') readonly selectedCategory = signal<FaqCategory>('all') readonly openItemIds = signal<Record<string, boolean>>({ '1': true }) readonly filteredFaqs = computed(() => { const query = this.searchQuery().toLowerCase().trim() return faqItems.filter((item) => { const matchesCategory = this.selectedCategory() === 'all' || item.category === this.selectedCategory() const matchesQuery = query === '' || item.question.toLowerCase().includes(query) || item.answer.toLowerCase().includes(query) || item.tag.toLowerCase().includes(query) return matchesCategory && matchesQuery }) }) get rootClass(): string { return cn('bg-background relative overflow-hidden px-4 py-16 sm:px-6 sm:py-24 lg:px-8', this.className) } isOpen(id: string): boolean { return Boolean(this.openItemIds()[id]) } toggleItem(id: string): void { this.openItemIds.update((prev) => ({ ...prev, [id]: !prev[id] })) } pillClass(cat: FaqCategory): string { return cn( 'rounded-lg border px-3 py-1 font-mono text-xs transition-colors', this.selectedCategory() === cat ? 'border-primary bg-primary text-primary-foreground font-semibold shadow-xs' : 'border-border bg-card text-muted-foreground hover:text-foreground', ) } cardClass(open: boolean): string { return cn( 'border-border bg-card/95 overflow-hidden rounded-2xl border text-left shadow-xs transition-colors', open ? 'ring-primary/20 shadow-md ring-1' : 'hover:border-border/80', ) } chevronClass(open: boolean): string { return cn( 'text-muted-foreground size-4 shrink-0 transition-transform duration-200', open && 'text-primary rotate-180', ) } }src/app/components/blocks/faq-searchable-accordion-workbench/index.ts0.1 kB
export { UiFaqSearchableAccordionWorkbenchComponent } from './faq-searchable-accordion-workbench.component'
Raw manifest:https://uipkge.dev/r/angular/faq-searchable-accordion-workbench.json