Menu

Change language

GitHub repo

Feature Matrix Table Dense

blockfeature

Dense Linear-style technical specification matrix table with collapsible sections and search filtering.

Installation

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

Variants

Loading interactive previews…

Schema

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

MatrixFeature
interface MatrixFeature {
  name: string
  description: string
  uipkge: boolean | string
  npmMonolith: boolean | string
  inHouse: boolean | string
}
MatrixCategory
interface MatrixCategory {
  id: string
  title: string
  features: MatrixFeature[]
}

npm dependencies

Files installed (2)

  • src/app/components/blocks/feature-matrix-table-dense/feature-matrix-table-dense.component.ts15.5 kB
    // Dense Linear-style technical specification matrix: collapsible category sections,
    // capability search, and a "differences only" toggle across UIPKGE / NPM monolith /
    // in-house columns. Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core'
    import {
      ArrowRight,
      Check,
      CheckCircle2,
      ChevronDown,
      ChevronRight,
      Filter,
      LucideAngularModule,
      Minus,
      Search,
    } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    
    export interface MatrixFeature {
      name: string
      description: string
      uipkge: boolean | string
      npmMonolith: boolean | string
      inHouse: boolean | string
    }
    
    export interface MatrixCategory {
      id: string
      title: string
      features: MatrixFeature[]
    }
    
    const DEFAULT_CATEGORIES: MatrixCategory[] = [
      {
        id: 'architecture',
        title: 'Architecture & Runtime Ownership',
        features: [
          {
            name: 'Zero NPM Runtime Dependency',
            description: 'Source code copied directly into your repository with 100% ownership.',
            uipkge: true,
            npmMonolith: false,
            inHouse: true,
          },
          {
            name: 'Dual Framework 1:1 Parity',
            description: 'Synchronized Vue 3.5 and React 19 primitives with identical token bindings.',
            uipkge: true,
            npmMonolith: false,
            inHouse: 'High Cost',
          },
          {
            name: 'Deterministic Tree-Shaking',
            description: 'Zero dead code in final production bundles; only imported components compile.',
            uipkge: true,
            npmMonolith: 'Partial',
            inHouse: true,
          },
          {
            name: 'Tailwind CSS v4 Native Tokens',
            description: 'Direct @theme inline OKLCH bindings without legacy tailwind.config.js overhead.',
            uipkge: true,
            npmMonolith: false,
            inHouse: 'Manual',
          },
        ],
      },
      {
        id: 'developer-experience',
        title: 'Developer Experience & Tooling',
        features: [
          {
            name: 'Headless Primitive Foundation',
            description: 'Powered by battle-tested Reka UI and Radix UI accessible state machines.',
            uipkge: true,
            npmMonolith: 'Proprietary',
            inHouse: 'Custom',
          },
          {
            name: 'CLI Direct Add Workflow',
            description: 'Install via npx shadcn add @uipkge/<name> with automated dependency resolution.',
            uipkge: true,
            npmMonolith: false,
            inHouse: false,
          },
          {
            name: 'Automated Parity Check Suite',
            description: 'Built-in AST parity verification scripts guarding against framework drift.',
            uipkge: true,
            npmMonolith: false,
            inHouse: false,
          },
        ],
      },
      {
        id: 'enterprise',
        title: 'Enterprise & Security Compliance',
        features: [
          {
            name: 'Zero Supply-Chain Vulnerabilities',
            description: 'No third-party registry packages or compromised transitive semver updates.',
            uipkge: true,
            npmMonolith: false,
            inHouse: true,
          },
          {
            name: 'Full Customization Freedom',
            description: 'Modify any CSS class, DOM attribute, or internal logic without breaking upstream.',
            uipkge: true,
            npmMonolith: 'Limited',
            inHouse: true,
          },
          {
            name: 'WCAG 2.1 AA Accessibility',
            description: 'Keyboard ergonomics, focus rings, ARIA roles, and screen-reader audited.',
            uipkge: true,
            npmMonolith: 'Partial',
            inHouse: 'Manual',
          },
        ],
      },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-feature-matrix-table-dense, [ui-feature-matrix-table-dense]',
      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],
      template: `
        <section data-slot="feature-matrix-table-dense" [class]="rootClass">
          <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
            <!-- Section Header -->
            <div class="mx-auto max-w-3xl space-y-4 text-center">
              <a
                href="#technical-matrix"
                class="group border-border/80 bg-secondary/60 hover:bg-secondary text-foreground inline-flex items-center gap-2 rounded-full border px-3.5 py-1 text-xs font-medium shadow-2xs transition-colors"
              >
                <lucide-icon [img]="CheckCircle2" class="text-primary size-3.5" />
                <span>Technical Architecture Comparison</span>
                <lucide-icon
                  [img]="ArrowRight"
                  class="text-muted-foreground size-3 transition-transform group-hover:translate-x-0.5"
                />
              </a>
    
              <h2 class="text-foreground text-3xl font-bold tracking-tight sm:text-4xl">
                {{ title }}
              </h2>
    
              <p class="text-muted-foreground text-base sm:text-lg">
                {{ description }}
              </p>
            </div>
    
            <!-- Controls Strip: Search & Filter Toggles -->
            <div
              class="border-border mt-10 flex flex-col gap-3 border-b pb-4 sm:flex-row sm:items-center sm:justify-between"
            >
              <div class="relative w-full sm:w-72">
                <lucide-icon
                  [img]="Search"
                  class="text-muted-foreground absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2"
                />
                <input
                  type="text"
                  placeholder="Filter capabilities..."
                  [value]="searchQuery()"
                  (input)="searchQuery.set($any($event.target).value)"
                  class="border-border bg-muted/40 text-foreground placeholder:text-muted-foreground focus-visible:ring-primary h-8 w-full rounded-md border pr-3 pl-8 text-xs focus-visible:ring-1 focus-visible:outline-none"
                />
              </div>
    
              <div class="flex items-center gap-2">
                <button type="button" [class]="differencesButtonClass" (click)="onlyDifferences.set(!onlyDifferences())">
                  <lucide-icon [img]="Filter" class="size-3" />
                  <span>Show Differences Only</span>
                </button>
              </div>
            </div>
    
            <!-- Dense Comparison Matrix Table -->
            <div class="border-border bg-card mt-6 overflow-x-auto rounded-lg border shadow-xs">
              <table class="w-full text-left text-xs">
                <!-- Table Sticky Header -->
                <thead class="border-border bg-muted/50 text-foreground border-b font-medium">
                  <tr>
                    <th scope="col" class="w-1/2 py-3.5 pr-3 pl-4 text-sm font-semibold sm:w-5/12 sm:pl-6">
                      Capability & Specification
                    </th>
                    <th
                      scope="col"
                      class="text-primary bg-primary/5 border-border border-x px-3 py-3.5 text-center font-bold sm:w-3/12"
                    >
                      <div class="flex flex-col items-center">
                        <span>UIPKGE Registry</span>
                        <span class="text-muted-foreground text-xs font-normal">Unbundled Code Ownership</span>
                      </div>
                    </th>
                    <th scope="col" class="text-muted-foreground px-3 py-3.5 text-center sm:w-2/12">
                      <div class="flex flex-col items-center">
                        <span>NPM Monolith</span>
                        <span class="text-muted-foreground text-xs font-normal">e.g. MUI / AntD</span>
                      </div>
                    </th>
                    <th scope="col" class="text-muted-foreground px-3 py-3.5 text-center sm:w-2/12">
                      <div class="flex flex-col items-center">
                        <span>In-House Bespoke</span>
                        <span class="text-muted-foreground text-xs font-normal">Built from scratch</span>
                      </div>
                    </th>
                  </tr>
                </thead>
    
                <!-- Table Body with Collapsible Category Rows -->
                <tbody class="divide-border/60 divide-y">
                  @for (category of filteredCategories(); track category.id) {
                    <!-- Category Header Row -->
                    <tr
                      class="bg-muted/30 hover:bg-muted/50 cursor-pointer transition-colors select-none"
                      (click)="toggleCategory(category.id)"
                    >
                      <td colspan="4" class="py-2.5 pr-3 pl-4 sm:pl-6">
                        <div class="text-foreground flex items-center gap-1.5 text-xs font-bold tracking-wider uppercase">
                          @if (!collapsedCategories()[category.id]) {
                            <lucide-icon [img]="ChevronDown" class="text-muted-foreground size-3.5" />
                          } @else {
                            <lucide-icon [img]="ChevronRight" class="text-muted-foreground size-3.5" />
                          }
                          <span>{{ category.title }}</span>
                          <span class="text-muted-foreground ml-1 text-xs font-normal">
                            ({{ category.features.length }})
                          </span>
                        </div>
                      </td>
                    </tr>
    
                    <!-- Feature Items Rows -->
                    @if (!collapsedCategories()[category.id]) {
                      @for (feature of category.features; track feature.name) {
                        <tr class="hover:bg-muted/10 transition-colors">
                          <!-- Feature Info (Col 1) -->
                          <td class="space-y-0.5 py-3 pr-3 pl-4 sm:pl-6">
                            <div class="text-foreground font-semibold">{{ feature.name }}</div>
                            <div class="text-muted-foreground text-xs">{{ feature.description }}</div>
                          </td>
    
                          <!-- UIPKGE (Col 2 - Highlighted) -->
                          <td class="bg-primary/5 border-border border-x px-3 py-3 text-center">
                            <div class="flex items-center justify-center">
                              @if (feature.uipkge === true) {
                                <span
                                  class="bg-success/10 text-success inline-flex size-5 items-center justify-center rounded-full font-bold"
                                >
                                  <lucide-icon [img]="Check" class="size-3.5 stroke-[2.5]" />
                                </span>
                              } @else if (typeof feature.uipkge === 'string') {
                                <span class="bg-muted text-foreground rounded px-1.5 py-0.5 font-mono text-xs font-medium">
                                  {{ feature.uipkge }}
                                </span>
                              } @else {
                                <span class="text-muted-foreground">
                                  <lucide-icon [img]="Minus" class="size-3.5" />
                                </span>
                              }
                            </div>
                          </td>
    
                          <!-- NPM Monolith (Col 3) -->
                          <td class="px-3 py-3 text-center">
                            <div class="flex items-center justify-center">
                              @if (feature.npmMonolith === true) {
                                <span
                                  class="bg-success/10 text-success inline-flex size-5 items-center justify-center rounded-full"
                                >
                                  <lucide-icon [img]="Check" class="size-3.5" />
                                </span>
                              } @else if (typeof feature.npmMonolith === 'string') {
                                <span
                                  class="bg-muted/60 text-muted-foreground rounded px-1.5 py-0.5 font-mono text-xs font-medium"
                                >
                                  {{ feature.npmMonolith }}
                                </span>
                              } @else {
                                <span class="text-muted-foreground/60">
                                  <lucide-icon [img]="Minus" class="size-3.5" />
                                </span>
                              }
                            </div>
                          </td>
    
                          <!-- In-House Bespoke (Col 4) -->
                          <td class="px-3 py-3 text-center">
                            <div class="flex items-center justify-center">
                              @if (feature.inHouse === true) {
                                <span
                                  class="bg-success/10 text-success inline-flex size-5 items-center justify-center rounded-full"
                                >
                                  <lucide-icon [img]="Check" class="size-3.5" />
                                </span>
                              } @else if (typeof feature.inHouse === 'string') {
                                <span
                                  class="bg-muted/60 text-muted-foreground rounded px-1.5 py-0.5 font-mono text-xs font-medium"
                                >
                                  {{ feature.inHouse }}
                                </span>
                              } @else {
                                <span class="text-muted-foreground/60">
                                  <lucide-icon [img]="Minus" class="size-3.5" />
                                </span>
                              }
                            </div>
                          </td>
                        </tr>
                      }
                    }
                  }
                </tbody>
              </table>
            </div>
          </div>
        </section>
      `,
    })
    export class UiFeatureMatrixTableDenseComponent {
      protected readonly ArrowRight = ArrowRight
      protected readonly Check = Check
      protected readonly CheckCircle2 = CheckCircle2
      protected readonly ChevronDown = ChevronDown
      protected readonly ChevronRight = ChevronRight
      protected readonly Filter = Filter
      protected readonly Minus = Minus
      protected readonly Search = Search
    
      @Input() title = 'Engineered for teams who demand complete source code control.'
      @Input() description =
        'Compare how unbundled registry components stack up against traditional monolithic npm packages and bespoke in-house design systems.'
      @Input() set categories(v: MatrixCategory[] | undefined) {
        this.customCategories.set(v ? v.map((c) => ({ ...c, features: [...c.features] })) : undefined)
      }
      @Input('class') className?: string
    
      private readonly customCategories = signal<MatrixCategory[] | undefined>(undefined)
    
      readonly searchQuery = signal('')
      readonly onlyDifferences = signal(false)
      readonly collapsedCategories = signal<Record<string, boolean>>({})
    
      readonly activeCategories = computed(() => this.customCategories() ?? DEFAULT_CATEGORIES)
    
      readonly filteredCategories = computed(() => {
        const query = this.searchQuery().toLowerCase().trim()
        const onlyDiff = this.onlyDifferences()
        return this.activeCategories()
          .map((cat) => ({
            ...cat,
            features: cat.features.filter((f) => {
              const matchesQuery =
                !query || f.name.toLowerCase().includes(query) || f.description.toLowerCase().includes(query)
              if (!matchesQuery) return false
              if (onlyDiff) {
                const isSame = f.uipkge === f.npmMonolith && f.npmMonolith === f.inHouse
                if (isSame) return false
              }
              return true
            }),
          }))
          .filter((cat) => cat.features.length > 0)
      })
    
      get rootClass(): string {
        return cn('bg-background relative overflow-hidden py-16 sm:py-24', this.className)
      }
    
      get differencesButtonClass(): string {
        return cn(
          'flex items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-xs font-medium transition-colors',
          this.onlyDifferences()
            ? 'border-primary bg-primary/10 text-primary'
            : 'border-border bg-card text-muted-foreground hover:text-foreground',
        )
      }
    
      toggleCategory(catId: string): void {
        this.collapsedCategories.update((prev) => ({ ...prev, [catId]: !prev[catId] }))
      }
    }
    
  • src/app/components/blocks/feature-matrix-table-dense/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/feature-matrix-table-dense.json