Accordion FAQ
blockmarketingInteractive architecture knowledge base and FAQ section with real-time keyword search, category topic filtering, and helpfulness voting.
Installation
$pnpm dlx shadcn@latest add https://uipkge.dev/r/lit/faq-01.json$npx shadcn@latest add https://uipkge.dev/r/lit/faq-01.json$yarn dlx shadcn@latest add https://uipkge.dev/r/lit/faq-01.json$bunx shadcn@latest add https://uipkge.dev/r/lit/faq-01.jsonManifest:
https://uipkge.dev/r/lit/faq-01.jsonInstalls to:src/uip/blocks/faq-01/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
question: string
answer: string
category: 'ownership' | 'security' | 'architecture' | 'pricing'
tags: string[]
helpfulCount: number
}HTMLElementTagNameMapinterface HTMLElementTagNameMap {
'uip-faq-01': UipFaq01
}
}Files installed (1)
src/uip/blocks/faq-01/faq-01.ts16.6 kB
import { LitElement, css, html } from 'lit' import { ChevronDown, ExternalLink, FileQuestion, HelpCircle, Mail, MessageSquare, Search, ThumbsDown, ThumbsUp, } 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/input/input' import type { UipInput } from '../../components/input/input' interface FaqItem { id: string question: string answer: string category: 'ownership' | 'security' | 'architecture' | 'pricing' tags: string[] helpfulCount: number } const faqs: FaqItem[] = [ { id: 'ownership', question: 'Do I actually own the component code after running the add command?', answer: 'Yes, 100%. UIPKGE follows the unbundled registry architecture pioneered by shadcn. When you run `npx shadcn-vue add` or `npx shadcn add`, the raw TypeScript, SFC, and variant files are copied directly into your repository. You are never bound to semver release cycles or rigid third-party package internals.', category: 'ownership', tags: ['Ownership', 'Zero Lock-in', 'MIT License'], helpfulCount: 342, }, { id: 'security', question: 'How do you ensure zero supply-chain security risks without npm packages?', answer: 'Every registry manifest and code payload is statically generated and cryptographically verifiable. Because source files live in your project tree, your static analysis tools (SonarQube, Snyk, ESLint, TypeScript compiler) inspect every single line of code during your existing CI pipeline with zero runtime black boxes.', category: 'security', tags: ['SOC 2', 'Zero Black Box', 'Static Audit'], helpfulCount: 289, }, { id: 'architecture', question: 'How does dual-framework parity work between Vue 3 and React 19?', answer: 'Both Vue and React registry trees are built against canonical shared Tailwind v4 design tokens and CVA variants in `packages/shared/`. Vue components leverage Reka UI primitives, while React components leverage Radix UI primitives, ensuring identical DOM contracts, keyboard navigation, and accessibility semantics.', category: 'architecture', tags: ['Vue 3.5', 'React 19', 'Tailwind v4', 'Reka UI'], helpfulCount: 215, }, { id: 'migration', question: 'Can we integrate these blocks into an existing Tailwind v4 or Nuxt 3 project?', answer: 'Absolutely. You only need to run `npx shadcn-vue add @uipkge/init` to configure the baseline `@theme` tokens and `cn()` utility in your `tailwind.css`. From there, individual blocks and primitives can be added incrementally without rewriting your existing styles.', category: 'architecture', tags: ['Nuxt 3', 'Tailwind v4', 'Vite', 'Next.js'], helpfulCount: 198, }, { id: 'pricing', question: 'What is the pricing model for commercial applications and vertical SaaS?', answer: 'The UIPKGE registry is 100% open source under the permissive MIT license. You can use all primitives and blocks in personal projects, commercial SaaS products, and internal client applications with zero licensing fees or seat royalties.', category: 'pricing', tags: ['MIT License', 'Commercial Use', 'Free Forever'], helpfulCount: 456, }, { id: 'updates', question: 'How do we pull updates or improvements to components we already copied?', answer: 'Because you own the code, you can inspect diffs using Git. If you want to re-pull the newest upstream implementation of a component or block, simply run `npx shadcn-vue add <name> --overwrite` and review the git diff in your IDE before committing.', category: 'ownership', tags: ['Git Diff', 'Custom Overwrites', 'Upgrades'], helpfulCount: 167, }, ] // Disclosure chrome resolved from the registry Accordion (default variant) + // the block's per-item className, the way cn() would merge them. <uip-accordion> // renders its own fixed item chrome, so the card-style items (rounded-xl, // open-state border/background) are native buttons/regions here instead — // same classes, same data-state, same slots in the DOM. const itemClass = 'border-border bg-card/60 data-[state=open]:bg-card data-[state=open]:border-primary/40 rounded-xl border px-5 transition-colors data-[state=open]:shadow-xs' const triggerClass = 'group/accordion-trigger flex w-full items-center justify-between gap-3 text-sm font-medium text-foreground outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring/40 hover:text-primary disabled:pointer-events-none disabled:opacity-50 py-4 text-left hover:no-underline' const contentClass = 'text-muted-foreground overflow-hidden data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down duration-200 ease-[cubic-bezier(0.32,0.72,0,1)] motion-reduce:animate-none border-border/50 space-y-4 border-t pt-1 pb-5 text-xs leading-relaxed sm:text-sm' /** * <uip-faq-01> — the registry Faq01 block as a web component. * * Class strings are React's Faq01 verbatim. React state (useState) becomes * @state fields. The search icon moves from React's absolute overlay into * <uip-input>'s prefix slot (same visual result), so the input keeps * `h-9 font-sans text-xs` and drops the overlay's `pl-9`. */ export class UipFaq01 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 = { searchQuery: { state: true }, activeCategory: { state: true }, votedMap: { state: true }, openIds: { state: true }, } private searchQuery = '' private activeCategory: 'all' | 'ownership' | 'security' | 'architecture' | 'pricing' = 'all' private votedMap: Record<string, 'up' | 'down'> = {} private openIds: string[] = [] constructor() { super() new ThemeController(this) } connectedCallback() { super.connectedCallback() this.setAttribute('data-uipkge', '') this.setAttribute('data-slot', 'faq-01') } private vote(id: string, dir: 'up' | 'down') { const next = { ...this.votedMap } if (next[id] === dir) delete next[id] else next[id] = dir this.votedMap = next } private toggle(id: string) { this.openIds = this.openIds.includes(id) ? this.openIds.filter((v) => v !== id) : [...this.openIds, id] } private get filteredFaqs() { return faqs.filter((item) => { const matchesCategory = this.activeCategory === 'all' || item.category === this.activeCategory const query = this.searchQuery.toLowerCase().trim() const matchesSearch = query === '' || item.question.toLowerCase().includes(query) || item.answer.toLowerCase().includes(query) || item.tags.some((t) => t.toLowerCase().includes(query)) return matchesCategory && matchesSearch }) } render() { const filtered = this.filteredFaqs return html` <section data-slot="faq-01" class="bg-background border-border relative w-full border-y py-16 lg:py-24"> <div class="mx-auto max-w-5xl space-y-12 px-4 sm:px-6 lg:px-8"> <div class="mx-auto max-w-2xl space-y-4 text-center"> <div class="inline-flex items-center gap-2"> <uip-badge variant="outline" class="border-primary/30 text-primary bg-primary/5 gap-1.5 px-2.5 py-1 font-mono text-xs tracking-wide uppercase" > ${icon(HelpCircle, 'circle-question-mark', 'size-3.5')} Knowledge Base </uip-badge> <span class="text-muted-foreground font-mono text-xs">Architecture & Licensing</span> </div> <h2 class="text-foreground text-3xl font-bold tracking-tight sm:text-4xl"> Frequently Answered Architecture Questions. </h2> <p class="text-muted-foreground text-base leading-relaxed sm:text-lg"> Everything you need to know about component ownership, security verification, and dual-framework integration. </p> </div> <div class="bg-card border-border flex flex-col items-center justify-between gap-4 rounded-xl border p-3 shadow-xs sm:flex-row" > <uip-input class="w-full sm:w-80 h-9 font-sans text-xs" .value=${this.searchQuery} @input=${(e: Event) => { this.searchQuery = (e.target as UipInput).value }} placeholder="Search questions or keywords..." > <span slot="prefix">${icon(Search, 'search', 'size-4')}</span> </uip-input> <div class="flex w-full flex-wrap items-center gap-1.5 sm:w-auto"> ${[ { id: 'all', label: 'All Topics' }, { id: 'ownership', label: 'Ownership' }, { id: 'security', label: 'Security' }, { id: 'architecture', label: 'Architecture' }, { id: 'pricing', label: 'Licensing' }, ].map( (cat) => html`<button type="button" class=${cn( 'rounded-md px-2.5 py-1.5 text-xs font-medium transition-colors', this.activeCategory === cat.id ? 'bg-primary text-primary-foreground font-semibold shadow-xs' : 'text-muted-foreground hover:text-foreground hover:bg-muted', )} @click=${() => { this.activeCategory = cat.id as 'all' | 'ownership' | 'security' | 'architecture' | 'pricing' }} > ${cat.label} </button>`, )} </div> </div> <div class="space-y-4"> ${filtered.length === 0 ? html`<div class="space-y-3 py-12 text-center"> ${icon(FileQuestion, 'file-question', 'text-muted-foreground mx-auto size-10')} <p class="text-foreground text-sm font-medium">No matching questions found</p> <p class="text-muted-foreground text-xs">Try adjusting your search query or topic filter.</p> </div>` : html`<div data-uipkge="" data-slot="accordion" data-variant="default" class="w-full space-y-3"> ${filtered.map((item) => { const isOpen = this.openIds.includes(item.id) const state = isOpen ? 'open' : 'closed' return html`<div data-uipkge="" data-slot="accordion-item" data-state=${state} class=${itemClass}> <button type="button" data-uipkge="" data-slot="accordion-trigger" data-state=${state} aria-expanded=${isOpen ? 'true' : 'false'} aria-controls=${`faq-${item.id}-panel`} id=${`faq-${item.id}-trigger`} class=${triggerClass} @click=${() => this.toggle(item.id)} > <div class="flex items-center gap-3 pr-4"> <span class="text-foreground text-sm font-semibold tracking-tight sm:text-base"> ${item.question} </span> </div> ${icon( ChevronDown, 'chevron-down', 'text-muted-foreground size-4 shrink-0 transition-transform duration-200 ease-[cubic-bezier(0.32,0.72,0,1)] group-data-[state=open]/accordion-trigger:rotate-180 motion-reduce:transition-none', )} </button> <div data-uipkge="" data-slot="accordion-content" data-state=${state} data-orientation="vertical" role="region" aria-labelledby=${`faq-${item.id}-trigger`} id=${`faq-${item.id}-panel`} ?hidden=${!isOpen} class=${contentClass} > <div class="pt-0 pb-4"> <p>${item.answer}</p> <div class="flex flex-wrap items-center justify-between gap-3 pt-2"> <div class="flex flex-wrap items-center gap-1.5"> ${item.tags.map( (tag) => html`<uip-badge variant="secondary" class="font-mono text-xs" >${tag}</uip-badge >`, )} </div> <div class="text-muted-foreground flex items-center gap-2 text-xs"> <span>Helpful?</span> <div class="flex items-center gap-1"> <button type="button" class=${cn( 'hover:bg-muted inline-flex items-center gap-1 rounded p-1 text-xs transition-colors', this.votedMap[item.id] === 'up' && 'text-success font-semibold', )} @click=${() => this.vote(item.id, 'up')} > ${icon(ThumbsUp, 'thumbs-up', 'size-3.5')} <span>${item.helpfulCount + (this.votedMap[item.id] === 'up' ? 1 : 0)}</span> </button> <button type="button" class=${cn( 'hover:bg-muted inline-flex items-center gap-1 rounded p-1 text-xs transition-colors', this.votedMap[item.id] === 'down' && 'text-destructive font-semibold', )} @click=${() => this.vote(item.id, 'down')} > ${icon(ThumbsDown, 'thumbs-down', 'size-3.5')} </button> </div> </div> </div> </div> </div> </div>` })} </div>`} </div> <div data-uipkge="" data-slot="card" class="bg-card text-card-foreground rounded-xl border shadow-sm transition-colors duration-150 overflow-hidden bg-muted/30 border-border flex flex-col items-center justify-between gap-4 p-6 sm:flex-row" > <div class="flex items-center gap-3.5 text-left"> <div class="bg-primary/10 text-primary border-primary/20 flex size-10 shrink-0 items-center justify-center rounded-lg border" > ${icon(MessageSquare, 'message-square', 'size-5')} </div> <div> <h4 class="text-foreground text-sm font-semibold">Have an edge-case or enterprise question?</h4> <p class="text-muted-foreground mt-0.5 text-xs"> Join our Discord community or open an architecture RFC on GitHub. </p> </div> </div> <div class="flex shrink-0 items-center gap-2.5"> <uip-button variant="outline" size="sm" class="h-8 gap-1.5 text-xs" href="https://github.com/uday-a/uipkge-registry/issues" target="_blank" > ${icon(ExternalLink, 'external-link', 'size-3.5')} Open GitHub RFC </uip-button> <uip-button size="sm" class="h-8 gap-1.5 text-xs" href="mailto:[email protected]"> ${icon(Mail, 'mail', 'size-3.5')} Contact Architecture Team </uip-button> </div> </div> </div> </section> ` } } customElements.get('uip-faq-01') || customElements.define('uip-faq-01', UipFaq01) declare global { interface HTMLElementTagNameMap { 'uip-faq-01': UipFaq01 } }
Raw manifest:https://uipkge.dev/r/lit/faq-01.json