Menu

Change language

GitHub repo

FAQ — Categorized Tabs

blockmarketing

Question set grouped behind category tabs, each panel holding its own accordion with a count badge, for support pages where one flat list would run past thirty entries.

Installation

$npx uipkge-ng@latest add faq-categorized-tabs
Manifest:https://uipkge.dev/r/angular/faq-categorized-tabs.jsonInstalls to:src/app/components/blocks/faq-categorized-tabs/

Variants

Loading interactive previews…

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

FaqEntry
interface FaqEntry {
  q: string
  a: string
}
FaqCategory
interface FaqCategory {
  id: string
  label: string
  entries: FaqEntry[]
}

Files installed (2)

  • src/app/components/blocks/faq-categorized-tabs/faq-categorized-tabs.component.ts5 kB
    // Question set grouped behind category tabs, each panel holding its own accordion with a
    // count badge. Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, Input, signal } from '@angular/core'
    import { cn } from '@/lib/utils'
    import {
      UiAccordionComponent,
      UiAccordionContentComponent,
      UiAccordionItemComponent,
      UiAccordionTriggerComponent,
    } from '@/ui/accordion/accordion.component'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import {
      UiTabsComponent,
      UiTabsContentComponent,
      UiTabsListComponent,
      UiTabsTriggerComponent,
    } from '@/ui/tabs/tabs.component'
    
    interface FaqEntry {
      q: string
      a: string
    }
    
    interface FaqCategory {
      id: string
      label: string
      entries: FaqEntry[]
    }
    
    const categories: FaqCategory[] = [
      {
        id: 'security',
        label: 'Security',
        entries: [
          {
            q: 'Where is our data stored?',
            a: 'Definitions and query metadata sit in your chosen region. Warehouse rows are never copied out.',
          },
          {
            q: 'Are you SOC 2 audited?',
            a: 'Type II, audited annually. The report and the subprocessor list are on the trust centre.',
          },
          { q: 'Can we bring our own keys?', a: 'Yes, on the enterprise plan, with rotation handled through your KMS.' },
        ],
      },
      {
        id: 'billing',
        label: 'Billing',
        entries: [
          {
            q: 'How is usage counted?',
            a: 'By query, not by seat. Materialised reads are not billed twice within their freshness window.',
          },
          {
            q: 'Can we cap spend?',
            a: 'Per-team query budgets are enforced at run time, so a runaway dashboard cannot outspend its ceiling.',
          },
          { q: 'Do you offer annual terms?', a: 'Yes, with a discount and an invoiced option above a threshold.' },
        ],
      },
      {
        id: 'setup',
        label: 'Setup',
        entries: [
          {
            q: 'What do you need from us?',
            a: 'A read-only role, your identity provider, and an hour with whoever owns the warehouse.',
          },
          {
            q: 'How long until first value?',
            a: 'Most teams publish a first certified metric on day one and finish the rollout in five weeks.',
          },
        ],
      },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-faq-categorized-tabs, [ui-faq-categorized-tabs]',
      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: [
        UiAccordionComponent,
        UiAccordionContentComponent,
        UiAccordionItemComponent,
        UiAccordionTriggerComponent,
        UiBadgeComponent,
        UiButtonComponent,
        UiTabsComponent,
        UiTabsContentComponent,
        UiTabsListComponent,
        UiTabsTriggerComponent,
      ],
      template: `
        <section data-slot="faq-categorized-tabs" [class]="rootClass">
          <div class="mx-auto max-w-4xl px-6 py-20 lg:py-28">
            <div class="max-w-2xl">
              <span ui-badge variant="secondary">Support</span>
              <h2 class="mt-4 text-3xl font-semibold tracking-tight sm:text-4xl">Answers by topic</h2>
              <p class="text-muted-foreground mt-3 text-lg">
                Grouped rather than listed, because one flat list of thirty questions is a list nobody reads.
              </p>
            </div>
    
            <ui-tabs [value]="activeTab()" (valueChange)="activeTab.set($event)" class="mt-10">
              <ui-tabs-list variant="pill" class="flex-nowrap overflow-x-auto">
                @for (category of categories; track category.id) {
                  <button ui-tabs-trigger [value]="category.id" variant="pill">
                    {{ category.label }}
                    <span class="text-muted-foreground ml-1.5 font-mono text-xs">{{ category.entries.length }}</span>
                  </button>
                }
              </ui-tabs-list>
    
              @for (category of categories; track category.id) {
                <ui-tabs-content [value]="category.id" class="mt-6">
                  <ui-accordion type="single" collapsible class="w-full">
                    @for (entry of category.entries; track entry.q) {
                      <ui-accordion-item [value]="entry.q">
                        <button ui-accordion-trigger class="text-left font-medium">{{ entry.q }}</button>
                        <ui-accordion-content class="text-muted-foreground leading-relaxed">{{
                          entry.a
                        }}</ui-accordion-content>
                      </ui-accordion-item>
                    }
                  </ui-accordion>
                </ui-tabs-content>
              }
            </ui-tabs>
    
            <button ui-button variant="ghost" class="mt-8">Browse the full help centre</button>
          </div>
        </section>
      `,
    })
    export class UiFaqCategorizedTabsComponent {
      protected readonly categories = categories
    
      @Input('class') className?: string
      @Input() set defaultTab(v: string) {
        if (v) this.activeTab.set(v)
      }
    
      readonly activeTab = signal(categories[0].id)
    
      get rootClass(): string {
        return cn('bg-background', this.className)
      }
    }
    
  • src/app/components/blocks/faq-categorized-tabs/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/faq-categorized-tabs.json