Transaction Feed
blockfinanceBrex/Ramp-style real-time corporate transaction feed with expense categorization, summary financial stat cards, search and filter toolbar, cardholder metadata, receipt attachment triggers, and row action menus.
Installation
$pnpm dlx uipkge-ng@latest add transaction-feed$npx uipkge-ng@latest add transaction-feed$yarn dlx uipkge-ng@latest add transaction-feed$bunx uipkge-ng@latest add transaction-feedManifest:
https://uipkge.dev/r/angular/transaction-feed.jsonInstalls to:src/app/components/blocks/transaction-feed/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.
Cardholderinterface Cardholder {
name: string
avatar: string
initials: string
cardLast4: string
cardType: 'Virtual' | 'Physical'
}Transactioninterface Transaction {
id: string
merchant: string
merchantNote: string
merchantIcon: LucideIconData
category: string
categoryKey: CategoryKey
cardholder: Cardholder
date: string
status: TransactionStatus
statusLabel: string
amount: string
isRefund: boolean
hasReceipt: boolean
receiptFileName?: string
}npm dependencies
Files installed (2)
src/app/components/blocks/transaction-feed/transaction-feed.component.ts27.6 kB
// Brex/Ramp-style real-time corporate transaction feed with expense categorization, // summary financial stat cards, search and filter toolbar, cardholder metadata, receipt // attachment triggers, and row action menus. Port of the React block 1:1. import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core' import { Building2, Car, Clock, Cloud, CreditCard, Download, Eye, FileCheck, GitBranch, LucideAngularModule, Mail, MessageSquare, MoreHorizontal, Palette, Paperclip, Plane, Plus, Receipt, RotateCcw, Search, ShieldAlert, TrendingUp, Upload, Utensils, type LucideIconData, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiAvatarComponent, UiAvatarFallbackComponent, UiAvatarImageComponent } from '@/ui/avatar/avatar.component' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent } from '@/ui/card/card.component' import { UiDropdownMenuComponent, UiDropdownMenuContentComponent, UiDropdownMenuItemComponent, UiDropdownMenuSeparatorComponent, UiDropdownMenuTriggerComponent, } from '@/ui/dropdown-menu/dropdown-menu.component' import { UiInputComponent } from '@/ui/input/input.component' import { UiSelectComponent, UiSelectContentComponent, UiSelectItemComponent, UiSelectTriggerComponent, UiSelectValueComponent, } from '@/ui/select/select.component' import { UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, } from '@/ui/table/table.component' export type TransactionStatus = 'cleared' | 'pending' | 'receipt_needed' export type CategoryKey = 'cloud' | 'travel' | 'saas' | 'transport' | 'office' | 'meals' export interface Cardholder { name: string avatar: string initials: string cardLast4: string cardType: 'Virtual' | 'Physical' } export interface Transaction { id: string merchant: string merchantNote: string merchantIcon: LucideIconData category: string categoryKey: CategoryKey cardholder: Cardholder date: string status: TransactionStatus statusLabel: string amount: string isRefund: boolean hasReceipt: boolean receiptFileName?: string } const categoryStyles: Record<CategoryKey, string> = { cloud: 'bg-info/10 text-info border-info/20', travel: 'bg-chart-1/10 text-chart-1 border-chart-1/20', saas: 'bg-chart-2/10 text-chart-2 border-chart-2/20', transport: 'bg-warning/10 text-warning border-warning/20', office: 'bg-chart-3/10 text-chart-3 border-chart-3/20', meals: 'bg-success/10 text-success border-success/20', } const statusStyles: Record<TransactionStatus, string> = { cleared: 'bg-success/10 text-success border-success/20', pending: 'bg-info/10 text-info border-info/20', receipt_needed: 'bg-warning/10 text-warning border-warning/20', } const statusDots: Record<TransactionStatus, string> = { cleared: 'bg-success', pending: 'bg-info', receipt_needed: 'bg-warning', } const transactions: Transaction[] = [ { id: 'tx-01', merchant: 'AWS Cloud Services', merchantNote: 'Production us-east-1 compute & S3', merchantIcon: Cloud, category: 'Cloud Hosting', categoryKey: 'cloud', cardholder: { name: 'Elena Rostova', avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100&auto=format&fit=crop&q=80', initials: 'ER', cardLast4: '4892', cardType: 'Virtual', }, date: 'Aug 21, 2026 · 14:28', status: 'cleared', statusLabel: 'Cleared', amount: '-$1,420.00', isRefund: false, hasReceipt: true, receiptFileName: 'aws-invoice-aug-2026.pdf', }, { id: 'tx-02', merchant: 'Delta Air Lines', merchantNote: 'SFO → JFK nonstop (Q3 Conf)', merchantIcon: Plane, category: 'Travel', categoryKey: 'travel', cardholder: { name: 'Marcus Chen', avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&auto=format&fit=crop&q=80', initials: 'MC', cardLast4: '3190', cardType: 'Physical', }, date: 'Aug 21, 2026 · 11:15', status: 'pending', statusLabel: 'Pending', amount: '-$849.00', isRefund: false, hasReceipt: true, receiptFileName: 'delta-eticket-dl8932.pdf', }, { id: 'tx-03', merchant: 'GitHub Enterprise', merchantNote: '42 Enterprise team seats', merchantIcon: GitBranch, category: 'SaaS', categoryKey: 'saas', cardholder: { name: 'Amara Okafor', avatar: 'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?w=100&auto=format&fit=crop&q=80', initials: 'AO', cardLast4: '9455', cardType: 'Virtual', }, date: 'Aug 20, 2026 · 09:00', status: 'cleared', statusLabel: 'Cleared', amount: '-$882.00', isRefund: false, hasReceipt: true, receiptFileName: 'github-inv-aug2026.pdf', }, { id: 'tx-04', merchant: 'Uber for Business', merchantNote: 'Client dinner return ride', merchantIcon: Car, category: 'Transport', categoryKey: 'transport', cardholder: { name: 'Sarah Jenkins', avatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=100&auto=format&fit=crop&q=80', initials: 'SJ', cardLast4: '6021', cardType: 'Physical', }, date: 'Aug 19, 2026 · 22:45', status: 'receipt_needed', statusLabel: 'Receipt Needed', amount: '-$48.50', isRefund: false, hasReceipt: false, }, { id: 'tx-05', merchant: 'Figma Professional', merchantNote: 'Design team organization plan', merchantIcon: Palette, category: 'SaaS', categoryKey: 'saas', cardholder: { name: 'Priya Nair', avatar: 'https://images.unsplash.com/photo-1580489944761-15a19d654956?w=100&auto=format&fit=crop&q=80', initials: 'PN', cardLast4: '7712', cardType: 'Virtual', }, date: 'Aug 19, 2026 · 16:30', status: 'cleared', statusLabel: 'Cleared', amount: '-$360.00', isRefund: false, hasReceipt: true, receiptFileName: 'figma-subscription-receipt.pdf', }, { id: 'tx-06', merchant: 'WeWork Coworking', merchantNote: 'NYC satellite team hot desks', merchantIcon: Building2, category: 'Office', categoryKey: 'office', cardholder: { name: 'Jonas Weber', avatar: 'https://images.unsplash.com/photo-1522075469751-3a6694fb2f61?w=100&auto=format&fit=crop&q=80', initials: 'JW', cardLast4: '1109', cardType: 'Physical', }, date: 'Aug 18, 2026 · 10:00', status: 'receipt_needed', statusLabel: 'Receipt Needed', amount: '-$1,250.00', isRefund: false, hasReceipt: false, }, { id: 'tx-07', merchant: 'Google Workspace', merchantNote: 'Security & suite - 55 users', merchantIcon: Mail, category: 'SaaS', categoryKey: 'saas', cardholder: { name: 'Elena Rostova', avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100&auto=format&fit=crop&q=80', initials: 'ER', cardLast4: '4892', cardType: 'Virtual', }, date: 'Aug 17, 2026 · 08:30', status: 'cleared', statusLabel: 'Cleared', amount: '-$660.00', isRefund: false, hasReceipt: true, receiptFileName: 'google-workspace-statement.pdf', }, { id: 'tx-08', merchant: 'Slack Pro', merchantNote: 'Vendor annual prepay refund adjustment', merchantIcon: MessageSquare, category: 'SaaS', categoryKey: 'saas', cardholder: { name: 'Amara Okafor', avatar: 'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?w=100&auto=format&fit=crop&q=80', initials: 'AO', cardLast4: '9455', cardType: 'Virtual', }, date: 'Aug 16, 2026 · 15:40', status: 'cleared', statusLabel: 'Cleared', amount: '+$5,000.00', isRefund: true, hasReceipt: true, receiptFileName: 'slack-credit-adjustment.pdf', }, { id: 'tx-09', merchant: 'Sweetgreen Catering', merchantNote: 'Quarterly all-hands team lunch', merchantIcon: Utensils, category: 'Meals', categoryKey: 'meals', cardholder: { name: 'Marcus Chen', avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&auto=format&fit=crop&q=80', initials: 'MC', cardLast4: '3190', cardType: 'Physical', }, date: 'Aug 15, 2026 · 12:15', status: 'receipt_needed', statusLabel: 'Receipt Needed', amount: '-$320.00', isRefund: false, hasReceipt: false, }, ] @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-transaction-feed, [ui-transaction-feed]', 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, UiAvatarComponent, UiAvatarFallbackComponent, UiAvatarImageComponent, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiDropdownMenuComponent, UiDropdownMenuContentComponent, UiDropdownMenuItemComponent, UiDropdownMenuSeparatorComponent, UiDropdownMenuTriggerComponent, UiInputComponent, UiSelectComponent, UiSelectContentComponent, UiSelectItemComponent, UiSelectTriggerComponent, UiSelectValueComponent, UiTableComponent, UiTableBodyComponent, UiTableCellComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, ], template: ` <div data-slot="transaction-feed" [class]="rootClass"> <!-- Header --> <div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <div class="space-y-1"> <h2 class="text-foreground text-2xl font-bold tracking-tight sm:text-3xl">Transactions</h2> <p class="text-muted-foreground text-sm"> Real-time ledger of card expenses, wire transfers, and subscription charges. </p> </div> <div class="flex items-center gap-2.5"> <button ui-button aria-label="Download attachment" variant="outline" size="sm"> <lucide-icon [img]="Download" class="size-4" /> Export CSV </button> <button ui-button size="sm"> <lucide-icon [img]="Plus" class="size-4" /> Add Transaction </button> </div> </div> <!-- 4 Financial Stat Cards --> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> <div ui-card class="border-border shadow-xs"> <div ui-card-content class="space-y-2 p-5"> <div class="flex items-center justify-between"> <p class="text-muted-foreground text-sm font-medium">Total Spent This Month</p> <div class="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg"> <lucide-icon [img]="CreditCard" class="size-4" /> </div> </div> <p class="text-foreground text-2xl font-bold tracking-tight tabular-nums">$42,850.00</p> <div class="text-muted-foreground flex items-center gap-1.5 text-xs"> <span class="text-success inline-flex items-center font-medium"> <lucide-icon [img]="TrendingUp" class="mr-0.5 size-3.5" /> +8.4% </span> <span>vs previous month</span> </div> </div> </div> <div ui-card class="border-border shadow-xs"> <div ui-card-content class="space-y-2 p-5"> <div class="flex items-center justify-between"> <p class="text-muted-foreground text-sm font-medium">Pending Approvals</p> <div class="bg-info/10 text-info flex size-8 items-center justify-center rounded-lg"> <lucide-icon [img]="Clock" class="size-4" /> </div> </div> <p class="text-foreground text-2xl font-bold tracking-tight tabular-nums">6 transactions</p> <p class="text-muted-foreground text-xs">$3,410.00 requiring review</p> </div> </div> <div ui-card class="border-border shadow-xs"> <div ui-card-content class="space-y-2 p-5"> <div class="flex items-center justify-between"> <p class="text-muted-foreground text-sm font-medium">Missing Receipts</p> <span ui-badge wrap variant="outline" class="border-warning/30 bg-warning/10 text-warning text-xs"> Action required </span> </div> <p class="text-foreground text-2xl font-bold tracking-tight tabular-nums">3</p> <p class="text-muted-foreground text-xs">Total $1,618.50 unverified</p> </div> </div> <div ui-card class="border-border shadow-xs"> <div ui-card-content class="space-y-2 p-5"> <div class="flex items-center justify-between"> <p class="text-muted-foreground text-sm font-medium">Average Transaction</p> <div class="bg-muted text-muted-foreground flex size-8 items-center justify-center rounded-lg"> <lucide-icon [img]="Receipt" class="size-4" /> </div> </div> <p class="text-foreground text-2xl font-bold tracking-tight tabular-nums">$185.20</p> <p class="text-muted-foreground text-xs">Across 231 card charges</p> </div> </div> </div> <!-- Filter Toolbar --> <div class="border-border bg-card flex flex-col gap-3 rounded-lg border p-3 shadow-xs md:flex-row md:items-center md:justify-between" > <div class="flex flex-1 flex-col gap-2.5 sm:flex-row sm:items-center"> <div class="relative min-w-0 flex-1"> <lucide-icon [img]="Search" class="text-muted-foreground pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2" /> <ui-input [value]="search()" (valueChange)="search.set($event)" type="search" placeholder="Search transactions..." class="h-9 w-full pl-9 text-xs sm:text-sm" /> </div> <div class="flex flex-wrap items-center gap-2"> <ui-select [value]="categoryFilter()" (valueChange)="categoryFilter.set($event)"> <button ui-select-trigger class="h-9 w-full text-xs sm:w-40"> <ui-select-value placeholder="All Categories" /> </button> <ui-select-content> <ui-select-item value="all">All Categories</ui-select-item> <ui-select-item value="cloud">Cloud Hosting</ui-select-item> <ui-select-item value="saas">Software & SaaS</ui-select-item> <ui-select-item value="travel">Travel</ui-select-item> <ui-select-item value="meals">Meals & Dining</ui-select-item> <ui-select-item value="office">Office</ui-select-item> <ui-select-item value="transport">Transport</ui-select-item> </ui-select-content> </ui-select> <ui-select [value]="dateRangeFilter()" (valueChange)="dateRangeFilter.set($event)"> <button ui-select-trigger class="h-9 w-full text-xs sm:w-36"> <ui-select-value placeholder="Date Range" /> </button> <ui-select-content> <ui-select-item value="this_month">This Month</ui-select-item> <ui-select-item value="last_30">Last 30 Days</ui-select-item> <ui-select-item value="last_90">Last 90 Days</ui-select-item> <ui-select-item value="ytd">Year to Date</ui-select-item> <ui-select-item value="all_time">All Time</ui-select-item> </ui-select-content> </ui-select> <ui-select [value]="statusFilter()" (valueChange)="statusFilter.set($event)"> <button ui-select-trigger class="h-9 w-full text-xs sm:w-36"> <ui-select-value placeholder="All Statuses" /> </button> <ui-select-content> <ui-select-item value="all">All Statuses</ui-select-item> <ui-select-item value="cleared">Cleared</ui-select-item> <ui-select-item value="pending">Pending</ui-select-item> <ui-select-item value="receipt_needed">Receipt Needed</ui-select-item> </ui-select-content> </ui-select> </div> </div> @if (isFiltered()) { <button ui-button variant="ghost" size="sm" class="text-muted-foreground hover:text-foreground h-9 self-start px-2 text-xs md:self-auto" (click)="resetFilters()" > <lucide-icon [img]="RotateCcw" class="mr-1.5 size-3.5" /> Reset filters </button> } </div> <!-- Transactions Table --> <div class="border-border bg-card overflow-hidden rounded-lg border shadow-xs"> <div class="overflow-x-auto"> <table ui-table> <thead ui-table-header class="bg-muted/40"> <tr ui-table-row> <th ui-table-head class="min-w-[220px]">Merchant / Note</th> <th ui-table-head class="min-w-[130px]">Category</th> <th ui-table-head class="min-w-[180px]">Cardholder</th> <th ui-table-head class="min-w-[150px]">Date & Time</th> <th ui-table-head class="min-w-[130px]">Status</th> <th ui-table-head class="min-w-[120px] text-right">Amount</th> <th ui-table-head class="min-w-[110px] text-center">Receipt</th> <th ui-table-head class="w-12 text-right"> <span class="sr-only">Actions</span> </th> </tr> </thead> <tbody ui-table-body> @for (tx of filteredTransactions(); track tx.id) { <tr ui-table-row class="hover:bg-muted/50 transition-colors"> <td ui-table-cell> <div class="flex items-center gap-3"> <div class="border-border/60 bg-muted/60 text-foreground flex size-9 shrink-0 items-center justify-center rounded-lg border shadow-xs" > <lucide-icon [img]="tx.merchantIcon" class="size-4" /> </div> <div class="min-w-0"> <p class="text-foreground truncate text-sm font-medium">{{ tx.merchant }}</p> <p class="text-muted-foreground truncate text-xs">{{ tx.merchantNote }}</p> </div> </div> </td> <td ui-table-cell> <span ui-badge variant="outline" [class]="cn('text-xs font-normal', categoryStyles[tx.categoryKey])" > {{ tx.category }} </span> </td> <td ui-table-cell> <div class="flex items-center gap-2.5"> <ui-avatar class="size-7"> <img ui-avatar-image [src]="tx.cardholder.avatar" [alt]="tx.cardholder.name" /> <span ui-avatar-fallback class="text-xs">{{ tx.cardholder.initials }}</span> </ui-avatar> <div class="min-w-0"> <p class="text-foreground truncate text-xs font-medium">{{ tx.cardholder.name }}</p> <p class="text-muted-foreground flex items-center gap-1 font-mono text-xs"> <span>••••</span> <span>{{ tx.cardholder.cardLast4 }}</span> </p> </div> </div> </td> <td ui-table-cell class="text-muted-foreground text-xs whitespace-nowrap tabular-nums"> {{ tx.date }} </td> <td ui-table-cell> <span ui-badge variant="outline" [class]="cn('text-xs font-medium capitalize', statusStyles[tx.status])" > <span [class]="cn('mr-1.5 size-1.5 rounded-full', statusDots[tx.status])"></span> {{ tx.statusLabel }} </span> </td> <td ui-table-cell class="text-right text-sm font-semibold whitespace-nowrap tabular-nums"> <span [class]="tx.isRefund ? 'text-success' : 'text-foreground'">{{ tx.amount }}</span> </td> <td ui-table-cell class="text-center"> <div class="flex items-center justify-center"> @if (tx.hasReceipt) { <span class="text-muted-foreground hover:text-foreground bg-muted/60 hover:bg-muted inline-flex cursor-pointer items-center gap-1 rounded-md px-2 py-1 text-xs font-medium transition-colors" [attr.title]="tx.receiptFileName || 'View receipt'" > <lucide-icon [img]="Paperclip" class="text-muted-foreground size-3.5" /> <span>Attached</span> </span> } @else { <button type="button" class="focus-visible:ring-ring bg-warning/10 text-warning hover:bg-warning/20 inline-flex cursor-pointer items-center gap-1 rounded-md px-2 py-1 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none" > <lucide-icon [img]="Upload" class="size-3" /> <span>Upload</span> </button> } </div> </td> <td ui-table-cell class="text-right"> <ui-dropdown-menu> <button ui-button ui-dropdown-menu-trigger variant="ghost" size="icon-sm" class="text-muted-foreground hover:text-foreground" > <lucide-icon [img]="MoreHorizontal" class="size-4" /> <span class="sr-only">Open actions for {{ tx.merchant }}</span> </button> <ui-dropdown-menu-content align="end" class="w-44"> <ui-dropdown-menu-item> <lucide-icon [img]="Eye" class="mr-2 size-4" /> View Details </ui-dropdown-menu-item> <ui-dropdown-menu-item> <lucide-icon [img]="Paperclip" class="mr-2 size-4" /> {{ tx.hasReceipt ? 'View Receipt' : 'Attach Receipt' }} </ui-dropdown-menu-item> <ui-dropdown-menu-item> <lucide-icon [img]="FileCheck" class="mr-2 size-4" /> Match Invoice </ui-dropdown-menu-item> <ui-dropdown-menu-separator /> <ui-dropdown-menu-item class="text-destructive focus:text-destructive focus:bg-destructive/10"> <lucide-icon [img]="ShieldAlert" class="mr-2 size-4" /> Dispute </ui-dropdown-menu-item> </ui-dropdown-menu-content> </ui-dropdown-menu> </td> </tr> } @if (filteredTransactions().length === 0) { <tr ui-table-row> <td ui-table-cell [attr.colspan]="8" class="h-36 text-center"> <div class="flex flex-col items-center justify-center gap-2"> <lucide-icon [img]="Search" class="text-muted-foreground/50 size-6" /> <p class="text-foreground text-sm font-medium">No transactions match your filters</p> <p class="text-muted-foreground text-xs"> Try clearing your search query or reset category/status </p> <button ui-button variant="outline" size="sm" class="mt-2 text-xs" (click)="resetFilters()"> Reset filters </button> </div> </td> </tr> } </tbody> </table> </div> <!-- Pagination footer strip --> <div class="border-border bg-card text-muted-foreground flex flex-col items-center justify-between gap-3 border-t px-4 py-3 text-xs sm:flex-row" > <p class="tabular-nums"> Showing <span class="text-foreground font-medium">{{ filteredTransactions().length }}</span> of <span class="text-foreground font-medium">{{ allTransactions.length }}</span> transactions </p> <div class="flex items-center gap-2"> <button ui-button variant="outline" size="sm" class="h-8 text-xs" disabled>Previous</button> <button ui-button variant="outline" size="sm" class="h-8 text-xs" disabled>Next</button> </div> </div> </div> </div> `, }) export class UiTransactionFeedComponent { protected readonly Clock = Clock protected readonly CreditCard = CreditCard protected readonly Download = Download protected readonly Eye = Eye protected readonly FileCheck = FileCheck protected readonly MoreHorizontal = MoreHorizontal protected readonly Paperclip = Paperclip protected readonly Plus = Plus protected readonly Receipt = Receipt protected readonly RotateCcw = RotateCcw protected readonly Search = Search protected readonly ShieldAlert = ShieldAlert protected readonly TrendingUp = TrendingUp protected readonly Upload = Upload protected readonly cn = cn protected readonly categoryStyles = categoryStyles protected readonly statusStyles = statusStyles protected readonly statusDots = statusDots protected readonly allTransactions = transactions @Input('class') className?: string readonly search = signal('') readonly categoryFilter = signal('all') readonly dateRangeFilter = signal('this_month') readonly statusFilter = signal('all') readonly isFiltered = computed( () => this.search().trim() !== '' || this.categoryFilter() !== 'all' || this.dateRangeFilter() !== 'this_month' || this.statusFilter() !== 'all', ) readonly filteredTransactions = computed(() => { const query = this.search().trim().toLowerCase() return transactions.filter((tx) => { const matchesCategory = this.categoryFilter() === 'all' || tx.categoryKey === this.categoryFilter() const matchesStatus = this.statusFilter() === 'all' || tx.status === this.statusFilter() const matchesQuery = !query || tx.merchant.toLowerCase().includes(query) || tx.merchantNote.toLowerCase().includes(query) || tx.category.toLowerCase().includes(query) || tx.cardholder.name.toLowerCase().includes(query) || tx.cardholder.cardLast4.includes(query) return matchesCategory && matchesStatus && matchesQuery }) }) get rootClass(): string { return cn('w-full space-y-6', this.className) } resetFilters(): void { this.search.set('') this.categoryFilter.set('all') this.dateRangeFilter.set('this_month') this.statusFilter.set('all') } }src/app/components/blocks/transaction-feed/index.ts0.2 kB
export { UiTransactionFeedComponent, type Cardholder, type CategoryKey, type Transaction, type TransactionStatus, } from './transaction-feed.component'
Raw manifest:https://uipkge.dev/r/angular/transaction-feed.json