Knowledge Base Hub
blockmediaSelf-serve help center and customer documentation hub with hero search, quick topic pills, category cards grid, trending articles, and community support banner.
Installation
$pnpm dlx uipkge-ng@latest add knowledge-base-hub$npx uipkge-ng@latest add knowledge-base-hub$yarn dlx uipkge-ng@latest add knowledge-base-hub$bunx uipkge-ng@latest add knowledge-base-hubManifest:
https://uipkge.dev/r/angular/knowledge-base-hub.jsonInstalls to:src/app/components/blocks/knowledge-base-hub/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.
Categoryinterface Category {
id: string
title: string
description: string
articlesCount: number
icon: unknown
featuredTopics: string[]
}Articleinterface Article {
id: string
title: string
description: string
category: string
readTime: string
viewCount: string
}npm dependencies
Files installed (2)
src/app/components/blocks/knowledge-base-hub/knowledge-base-hub.component.ts17.4 kB
// Self-serve help center and customer documentation hub with hero search, quick topic pills, // category cards grid, trending articles, and community support banner. Port of the React block 1:1. import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core' import { ArrowRight, ArrowUpRight, Blocks, BookOpen, Clock, Code, CreditCard, Eye, HelpCircle, LucideAngularModule, MessageSquare, Rocket, Search, Shield, Users, X, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardFooterComponent, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiInputComponent } from '@/ui/input/input.component' interface Category { id: string title: string description: string articlesCount: number icon: unknown featuredTopics: string[] } interface Article { id: string title: string description: string category: string readTime: string viewCount: string } const defaultCategories: Category[] = [ { id: 'getting-started', title: 'Getting Started', icon: Rocket, articlesCount: 14, description: 'Quick start guides, installation walkthroughs, and core architecture concepts.', featuredTopics: ['Quickstart Guide', 'Project Setup', 'First Deployment'], }, { id: 'account-billing', title: 'Account & Billing', icon: CreditCard, articlesCount: 22, description: 'Subscription plans, payment methods, invoice management, and seat licensing.', featuredTopics: ['Upgrade Plan', 'Payment Methods', 'Invoices'], }, { id: 'api-sdks', title: 'API & Developer SDKs', icon: Code, articlesCount: 35, description: 'REST & GraphQL endpoints, SDK reference libraries, auth tokens, and rate limits.', featuredTopics: ['Authentication', 'Rate Limits', 'SDK Libraries'], }, { id: 'security-compliance', title: 'Security & Compliance', icon: Shield, articlesCount: 18, description: 'SOC 2 certification, SSO/SAML configuration, audit logs, and data encryption.', featuredTopics: ['SAML SSO', 'Audit Logs', 'Data Encryption'], }, { id: 'integrations-webhooks', title: 'Integrations & Webhooks', icon: Blocks, articlesCount: 28, description: 'Connecting third-party apps, custom webhooks, payload schemas, and event retries.', featuredTopics: ['Webhooks Setup', 'Slack App', 'Retry Policies'], }, { id: 'troubleshooting-faq', title: 'Troubleshooting & FAQ', icon: HelpCircle, articlesCount: 40, description: 'Common error codes, debugging workflows, known limits, and frequent questions.', featuredTopics: ['Error Codes', 'Performance', 'Account Recovery'], }, ] const defaultArticles: Article[] = [ { id: 'art-1', title: 'Setting up SAML 2.0 Single Sign-On (SSO)', description: 'Step-by-step instructions for configuring Okta, Azure AD, and Google Workspace SSO.', category: 'Security', readTime: '6 min read', viewCount: '14.2k views', }, { id: 'art-2', title: 'Migrating from v1 REST API to v2 GraphQL', description: 'Breaking changes, schema transformations, and backward-compatibility guidelines.', category: 'API & SDKs', readTime: '8 min read', viewCount: '11.8k views', }, { id: 'art-3', title: 'Configuring Custom Domains & SSL Certificates', description: 'DNS record configuration, automatic TLS provisioning, and apex domain routing.', category: 'Getting Started', readTime: '4 min read', viewCount: '9.5k views', }, { id: 'art-4', title: 'Webhook Signatures & Replay Attack Prevention', description: 'Verify HMAC-SHA256 signatures and implement idempotency keys for event handling.', category: 'Integrations', readTime: '5 min read', viewCount: '8.9k views', }, { id: 'art-5', title: 'Managing Team Roles & Granular Permissions', description: 'Assigning RBAC policies, custom permission sets, and project-level scopes.', category: 'Account', readTime: '5 min read', viewCount: '7.4k views', }, { id: 'art-6', title: 'Debugging 429 Rate Limit Errors & Backoff Strategies', description: 'Understanding quota tiers, exponential backoff algorithms, and jitter implementations.', category: 'Troubleshooting', readTime: '7 min read', viewCount: '12.1k views', }, ] const quickTopics = ['Authentication', 'Billing', 'Webhooks', 'Custom Domains'] @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-knowledge-base-hub, [ui-knowledge-base-hub]', 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, UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardFooterComponent, UiCardHeaderComponent, UiCardTitleComponent, UiInputComponent, ], template: ` <div data-slot="knowledge-base-hub" [class]="rootClass"> <!-- Hero Header --> <section class="mx-auto max-w-3xl space-y-6 text-center"> <div class="space-y-3"> <span ui-badge variant="secondary" class="gap-1.5 px-3 py-1 font-medium"> <lucide-icon [img]="BookOpen" class="text-primary size-3.5" /> Help Center & Documentation </span> <h1 class="text-3xl font-bold tracking-tight sm:text-4xl lg:text-5xl">How can we help you today?</h1> <p class="text-muted-foreground mx-auto max-w-xl text-base sm:text-lg"> Search our knowledge base for guides, API references, troubleshooting tips, and common workflows. </p> </div> <div class="relative mx-auto max-w-2xl"> <lucide-icon [img]="Search" class="text-muted-foreground pointer-events-none absolute top-1/2 left-4 size-5 -translate-y-1/2" /> <ui-input [value]="searchQuery()" (valueChange)="searchQuery.set($event)" type="search" placeholder="Search for answers, guides, and error codes..." class="bg-card h-12 rounded-xl pr-14 pl-11 text-base shadow-xs" /> <div class="pointer-events-none absolute top-1/2 right-3 -translate-y-1/2"> <kbd class="bg-muted text-muted-foreground border-border hidden h-6 items-center rounded border px-2 font-mono text-xs select-none sm:inline-flex" > ⌘K </kbd> </div> </div> <div class="flex flex-wrap items-center justify-center gap-2"> <span class="text-muted-foreground text-xs font-medium">Quick topics:</span> @for (topic of topics; track topic) { <button type="button" [class]="topicPillClass(topic)" (click)="selectTopic(topic)"> {{ topic }} </button> } @if (searchQuery()) { <button aria-label="Clear search" type="button" class="text-muted-foreground hover:text-foreground inline-flex items-center gap-1 px-1.5 py-1 text-xs font-medium transition-colors" (click)="searchQuery.set('')" > <lucide-icon [img]="X" class="size-3" /> Clear search </button> } </div> </section> <!-- Empty Search State --> @if (filteredCategories().length === 0 && filteredArticles().length === 0) { <div class="border-border bg-card/40 rounded-xl border border-dashed p-10 text-center"> <div class="bg-muted text-muted-foreground mx-auto flex size-12 items-center justify-center rounded-full"> <lucide-icon [img]="Search" class="size-6" /> </div> <h3 class="mt-4 text-base font-semibold">No matching articles found</h3> <p class="text-muted-foreground mt-1 text-sm"> No guides or categories matched “{{ searchQuery() }}”. Try searching for another topic. </p> <button ui-button aria-label="Clear search" variant="outline" size="sm" class="mt-4" (click)="searchQuery.set('')" > Reset search </button> </div> } <!-- Category Cards Grid --> @if (filteredCategories().length > 0) { <section class="space-y-4"> <div class="flex items-center justify-between"> <h2 class="text-xl font-semibold tracking-tight sm:text-2xl">Browse by Category</h2> <span class="text-muted-foreground text-xs tabular-nums">{{ filteredCategories().length }} categories</span> </div> <div class="grid gap-5 sm:grid-cols-2 lg:grid-cols-3"> @for (category of filteredCategories(); track category.id) { <div ui-card class="group/card hover:border-ring/50 relative flex flex-col justify-between overflow-hidden transition-colors duration-200 hover:shadow-sm" > <div ui-card-header class="pb-3"> <div class="flex items-start justify-between gap-3"> <div class="bg-primary/10 text-primary flex size-11 items-center justify-center rounded-lg"> <lucide-icon [img]="categoryIcon(category)" class="size-5" /> </div> <span ui-badge variant="secondary" class="text-xs font-normal"> {{ category.articlesCount }} articles </span> </div> <h3 ui-card-title class="mt-3 text-lg font-semibold tracking-tight">{{ category.title }}</h3> <p ui-card-description class="text-muted-foreground text-sm leading-relaxed"> {{ category.description }} </p> </div> <div ui-card-content class="pt-0"> <div class="border-border flex flex-wrap gap-1.5 border-t pt-3"> @for (topic of category.featuredTopics; track topic) { <span class="bg-muted/60 text-muted-foreground rounded-md px-2 py-0.5 text-xs font-medium"> {{ topic }} </span> } </div> </div> <div ui-card-footer class="border-border bg-muted/20 flex items-center justify-between border-t py-3"> <a href="#" class="text-primary group-hover/card:text-primary/80 flex min-h-6 items-center gap-1 text-xs font-medium transition-colors" > <span>Explore category</span> <lucide-icon [img]="ArrowRight" class="size-3.5 transition-transform duration-200 group-hover/card:translate-x-1" /> </a> </div> </div> } </div> </section> } <!-- Popular & Trending Articles Section --> @if (filteredArticles().length > 0) { <section class="space-y-4"> <div class="flex flex-col justify-between gap-2 sm:flex-row sm:items-end"> <div> <h2 class="text-xl font-semibold tracking-tight sm:text-2xl">Popular & Trending Articles</h2> <p class="text-muted-foreground mt-1 text-sm"> Frequently read guides and developer documentation this week. </p> </div> <a href="#" class="text-primary hover:text-primary/80 inline-flex min-h-6 items-center gap-1 text-xs font-medium transition-colors" > <span>View all documentation</span> <lucide-icon [img]="ArrowRight" class="size-3.5" /> </a> </div> <div class="grid gap-4 md:grid-cols-2"> @for (article of filteredArticles(); track article.id) { <div ui-card class="group/article hover:border-ring/50 relative flex flex-col justify-between p-5 transition-colors duration-200 hover:shadow-sm" > <div class="space-y-2.5"> <div class="flex items-center justify-between gap-2"> <span ui-badge variant="outline" class="text-xs font-medium">{{ article.category }}</span> <div class="text-muted-foreground flex items-center gap-3 text-xs"> <span class="inline-flex items-center gap-1"> <lucide-icon [img]="Clock" class="size-3.5" /> {{ article.readTime }} </span> <span class="inline-flex items-center gap-1"> <lucide-icon [img]="Eye" class="size-3.5" /> {{ article.viewCount }} </span> </div> </div> <div> <a href="#" class="text-foreground group-hover/article:text-primary flex items-start justify-between gap-2 text-base font-semibold tracking-tight transition-colors" > <span>{{ article.title }}</span> <lucide-icon [img]="ArrowUpRight" class="text-muted-foreground group-hover/article:text-primary size-4 shrink-0 transition-transform duration-200 group-hover/article:translate-x-0.5 group-hover/article:-translate-y-0.5" /> </a> <p class="text-muted-foreground mt-1 line-clamp-2 text-sm leading-relaxed"> {{ article.description }} </p> </div> </div> </div> } </div> </section> } <!-- Community & Live Support Footer Banner --> <div ui-card class="border-border bg-card/60 overflow-hidden shadow-xs"> <div ui-card-content class="flex flex-col items-center justify-between gap-6 p-8 text-center sm:p-10 md:flex-row md:text-left" > <div class="max-w-xl space-y-2"> <h3 class="text-xl font-semibold tracking-tight sm:text-2xl"> Can't find what you're looking for? </h3> <p class="text-muted-foreground text-sm leading-relaxed"> Our support engineers are available 24/7 to help resolve technical issues, or join our Discord community to connect with other developers. </p> </div> <div class="flex shrink-0 flex-wrap items-center justify-center gap-3"> <button ui-button class="gap-2"> <lucide-icon [img]="MessageSquare" class="size-4" /> <span>Contact Support</span> </button> <button ui-button variant="outline" class="gap-2"> <lucide-icon [img]="Users" class="size-4" /> <span>Join Discord Community</span> </button> </div> </div> </div> </div> `, }) export class UiKnowledgeBaseHubComponent { protected readonly ArrowRight = ArrowRight protected readonly ArrowUpRight = ArrowUpRight protected readonly BookOpen = BookOpen protected readonly Clock = Clock protected readonly Eye = Eye protected readonly MessageSquare = MessageSquare protected readonly Search = Search protected readonly Users = Users protected readonly X = X protected readonly topics = quickTopics @Input() set initialSearch(v: string) { this.searchQuery.set(v ?? '') } @Input('class') className?: string readonly categories = signal<Category[]>([...defaultCategories]) readonly articles = signal<Article[]>([...defaultArticles]) readonly searchQuery = signal('') readonly filteredCategories = computed(() => { const q = this.searchQuery().trim().toLowerCase() if (!q) return this.categories() return this.categories().filter( (c) => c.title.toLowerCase().includes(q) || c.description.toLowerCase().includes(q) || c.featuredTopics.some((t) => t.toLowerCase().includes(q)), ) }) readonly filteredArticles = computed(() => { const q = this.searchQuery().trim().toLowerCase() if (!q) return this.articles() return this.articles().filter( (a) => a.title.toLowerCase().includes(q) || a.description.toLowerCase().includes(q) || a.category.toLowerCase().includes(q), ) }) get rootClass(): string { return cn('w-full space-y-12 py-6', this.className) } categoryIcon(category: Category): unknown { return category.icon } topicPillClass(topic: string): string { const active = this.searchQuery() === topic return cn( 'focus-visible:ring-ring inline-flex items-center rounded-full border px-3 py-1 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none', active ? 'bg-primary text-primary-foreground border-primary' : 'bg-muted/50 text-muted-foreground hover:bg-muted hover:text-foreground border-transparent', ) } selectTopic(topic: string): void { this.searchQuery.set(this.searchQuery() === topic ? '' : topic) } }src/app/components/blocks/knowledge-base-hub/index.ts0.1 kB
export { UiKnowledgeBaseHubComponent } from './knowledge-base-hub.component'
Raw manifest:https://uipkge.dev/r/angular/knowledge-base-hub.json