Menu

Change language

GitHub repo

Pricing — Grouped Matrix

blockmarketing

Feature comparison matrix with collapsible capability groups, so a long plan comparison opens on the summary rows and expands only where the reader is looking.

Installation

$npx uipkge-ng@latest add pricing-matrix-grouped
Manifest:https://uipkge.dev/r/angular/pricing-matrix-grouped.jsonInstalls to:src/app/components/blocks/pricing-matrix-grouped/

Variants

Loading interactive previews…

Schema

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

PricingGroupedRow
interface PricingGroupedRow {
  feature: string
  values: (boolean | string)[]
}
PricingGroupedGroup
interface PricingGroupedGroup {
  id: string
  label: string
  summary: string
  rows: PricingGroupedRow[]
}

Used by

Files installed (2)

  • src/app/components/blocks/pricing-matrix-grouped/pricing-matrix-grouped.component.ts7.3 kB
    // Feature comparison matrix with collapsible capability groups, so a long plan
    // comparison opens on the summary rows and expands only where the reader is looking.
    // Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, Input, signal } from '@angular/core'
    import { Check, ChevronDown, LucideAngularModule, Minus } 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 } from '@/ui/card/card.component'
    import { UiSeparatorComponent } from '@/ui/separator/separator.component'
    import {
      UiTableBodyComponent,
      UiTableCellComponent,
      UiTableComponent,
      UiTableRowComponent,
    } from '@/ui/table/table.component'
    
    export interface PricingGroupedRow {
      feature: string
      values: (boolean | string)[]
    }
    
    export interface PricingGroupedGroup {
      id: string
      label: string
      summary: string
      rows: PricingGroupedRow[]
    }
    
    const plans = ['Team', 'Business', 'Enterprise']
    
    const groups: PricingGroupedGroup[] = [
      {
        id: 'modelling',
        label: 'Modelling',
        summary: 'Definitions, versioning, and review',
        rows: [
          { feature: 'Certified definitions', values: ['50', '500', 'Unlimited'] },
          { feature: 'Version history', values: ['30 days', 'Unlimited', 'Unlimited'] },
          { feature: 'Required review', values: [false, true, true] },
          { feature: 'Period locking', values: [false, true, true] },
        ],
      },
      {
        id: 'access',
        label: 'Access & identity',
        summary: 'Who sees which rows',
        rows: [
          { feature: 'SSO (SAML)', values: [true, true, true] },
          { feature: 'SCIM provisioning', values: [false, true, true] },
          { feature: 'Row-level scope', values: [false, true, true] },
          { feature: 'Customer-managed keys', values: [false, false, true] },
        ],
      },
      {
        id: 'ops',
        label: 'Operations',
        summary: 'Cost, alerting, and support',
        rows: [
          { feature: 'Query budgets', values: [false, true, true] },
          { feature: 'Drift alerting', values: [false, true, true] },
          { feature: 'Audit log export', values: [false, '90 days', 'Unlimited'] },
          { feature: 'Named support engineer', values: [false, false, true] },
        ],
      },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-pricing-matrix-grouped, [ui-pricing-matrix-grouped]',
      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,
        UiCardContentComponent,
        UiSeparatorComponent,
        UiTableBodyComponent,
        UiTableCellComponent,
        UiTableComponent,
        UiTableRowComponent,
      ],
      template: `
        <section data-slot="pricing-matrix-grouped" [class]="rootClass">
          <div class="mx-auto max-w-4xl px-6 py-20 lg:py-28">
            <span ui-badge variant="secondary">Compare plans</span>
            <h2 class="mt-4 text-3xl font-semibold tracking-tight sm:text-4xl">Grouped, so it stays readable</h2>
    
            <div ui-card class="mt-8">
              <div ui-card-content class="p-0">
                <div
                  class="text-muted-foreground grid grid-cols-[1fr_repeat(3,5rem)] gap-2 px-4 py-3 text-xs font-medium sm:grid-cols-[1fr_repeat(3,7rem)]"
                >
                  <span>Capability</span>
                  @for (plan of plans; track plan) {
                    <span class="text-center">{{ plan }}</span>
                  }
                </div>
                <ui-separator />
    
                @for (group of groups; track group.id) {
                  <div>
                    <button
                      type="button"
                      class="hover:bg-muted focus-visible:ring-ring flex w-full items-center gap-3 px-4 py-3 text-left transition-colors focus-visible:ring-2 focus-visible:outline-none"
                      [attr.aria-expanded]="isOpen(group.id)"
                      [attr.aria-controls]="'group-' + group.id"
                      (click)="toggle(group.id)"
                    >
                      <lucide-icon [img]="ChevronDown" [class]="chevronClass(group.id)" aria-hidden="true" />
                      <span class="min-w-0">
                        <span class="block text-sm font-medium">{{ group.label }}</span>
                        <span class="text-muted-foreground block text-xs">{{ group.summary }}</span>
                      </span>
                      <span class="text-muted-foreground ml-auto shrink-0 font-mono text-xs">
                        {{ group.rows.length }}
                      </span>
                    </button>
    
                    @if (isOpen(group.id)) {
                      <div [id]="'group-' + group.id">
                        <table ui-table>
                          <tbody ui-table-body>
                            @for (row of group.rows; track row.feature) {
                              <tr ui-table-row>
                                <td ui-table-cell class="pl-11 text-sm">{{ row.feature }}</td>
                                @for (value of row.values; track $index) {
                                  <td ui-table-cell class="w-20 text-center sm:w-28">
                                    @if (value === true) {
                                      <lucide-icon [img]="Check" class="text-success mx-auto size-4" aria-hidden="true" />
                                      <span class="sr-only">Included</span>
                                    } @else if (value === false) {
                                      <lucide-icon
                                        [img]="Minus"
                                        class="text-muted-foreground/50 mx-auto size-4"
                                        aria-hidden="true"
                                      />
                                      <span class="sr-only">Not included</span>
                                    } @else {
                                      <span class="font-mono text-xs">{{ value }}</span>
                                    }
                                  </td>
                                }
                              </tr>
                            }
                          </tbody>
                        </table>
                      </div>
                    }
                    <ui-separator />
                  </div>
                }
              </div>
            </div>
    
            <button ui-button class="mt-6">Start on Business</button>
          </div>
        </section>
      `,
    })
    export class UiPricingMatrixGroupedComponent {
      protected readonly Check = Check
      protected readonly ChevronDown = ChevronDown
      protected readonly Minus = Minus
    
      readonly plans = plans
      readonly groups = groups
    
      @Input() set initialOpen(v: string[] | undefined) {
        if (v) this.open.set([...v])
      }
      @Input('class') className?: string
    
      // Opens on the first group only: a fully expanded matrix is the thing readers
      // bounce off, and every group is one click from open.
      readonly open = signal<string[]>([groups[0].id])
    
      get rootClass(): string {
        return cn('bg-background', this.className)
      }
    
      isOpen(id: string): boolean {
        return this.open().includes(id)
      }
    
      chevronClass(id: string): string {
        return cn('text-muted-foreground size-4 shrink-0 transition-transform', !this.isOpen(id) && '-rotate-90')
      }
    
      toggle(id: string): void {
        this.open.update((current) => (current.includes(id) ? current.filter((entry) => entry !== id) : [...current, id]))
      }
    }
    
  • src/app/components/blocks/pricing-matrix-grouped/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/pricing-matrix-grouped.json