Menu

Change language

GitHub repo

Trademark Patent Portfolio

blocklegal

USPTO and WIPO intellectual property asset portfolio tracker, patent grant status, jurisdiction coverage, and maintenance fee timeline with docket action workflow.

Installation

$npx uipkge-ng@latest add trademark-patent-portfolio
Manifest:https://uipkge.dev/r/angular/trademark-patent-portfolio.jsonInstalls to:src/app/components/blocks/trademark-patent-portfolio/

Variants

Loading interactive previews…

Schema

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

IpAsset
interface IpAsset {
  id: string
  identifier: string
  officialNumber: string
  title: string
  type: AssetType
  jurisdictionCode: string
  jurisdictionName: string
  jurisdictionFlag: string
  patentOffice: string
  applicationDate: string
  grantDate: string
  status: FilingStatus
  nextDeadline: string
  deadlineNumber: string
  deadlineNote: string
  ipcClass: string
  attorney: string
  gazetteUrl: string
  gazetteLabel: string
}

Files installed (2)

  • src/app/components/blocks/trademark-patent-portfolio/trademark-patent-portfolio.component.ts29.4 kB
    // USPTO and WIPO intellectual property asset portfolio tracker: patent grant status,
    // jurisdiction coverage, and maintenance fee timeline with docket action workflow.
    // Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core'
    import {
      Award,
      BadgeCheck,
      Building2,
      Calendar,
      Check,
      CheckCircle2,
      ChevronDown,
      Clock,
      Copy,
      Download,
      ExternalLink,
      FileText,
      Filter,
      LucideAngularModule,
      MoreHorizontal,
      Plus,
      Scale,
      Search,
      UserCheck,
    } 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,
      UiCardHeaderComponent,
      UiCardTitleComponent,
    } from '@/ui/card/card.component'
    import {
      UiDropdownMenuComponent,
      UiDropdownMenuContentComponent,
      UiDropdownMenuItemComponent,
      UiDropdownMenuLabelComponent,
      UiDropdownMenuSeparatorComponent,
      UiDropdownMenuTriggerComponent,
    } from '@/ui/dropdown-menu/dropdown-menu.component'
    import { UiInputComponent } from '@/ui/input/input.component'
    import {
      UiTableBodyComponent,
      UiTableCellComponent,
      UiTableComponent,
      UiTableHeadComponent,
      UiTableHeaderComponent,
      UiTableRowComponent,
    } from '@/ui/table/table.component'
    
    export type AssetType = 'Utility Patent' | 'Trademark' | 'PCT International'
    export type FilingStatus = 'Granted / Registered' | 'Published / Under Exam'
    
    export interface IpAsset {
      id: string
      identifier: string
      officialNumber: string
      title: string
      type: AssetType
      jurisdictionCode: string
      jurisdictionName: string
      jurisdictionFlag: string
      patentOffice: string
      applicationDate: string
      grantDate: string
      status: FilingStatus
      nextDeadline: string
      deadlineNumber: string
      deadlineNote: string
      ipcClass: string
      attorney: string
      gazetteUrl: string
      gazetteLabel: string
    }
    
    const ipAssets: IpAsset[] = [
      {
        id: 'ip-1',
        identifier: 'US Patent #11,849,201',
        officialNumber: 'US 11,849,201 B2',
        title: 'Zero-Dependency Dual-Framework AST Generation',
        type: 'Utility Patent',
        jurisdictionCode: 'US',
        jurisdictionName: 'United States',
        jurisdictionFlag: '🇺🇸',
        patentOffice: 'USPTO',
        applicationDate: '2023-04-12',
        grantDate: '2024-06-18',
        status: 'Granted / Registered',
        nextDeadline: 'Dec 2026',
        deadlineNumber: '2026-12-15',
        deadlineNote: 'USPTO 3.5-Year Maintenance Window',
        ipcClass: 'CPC G06F 8/41 · Abstract Syntax Trees',
        attorney: 'Vance & Morrison LLP',
        gazetteUrl: 'https://patents.google.com/patent/US11849201B2/en',
        gazetteLabel: 'View USPTO Gazette',
      },
      {
        id: 'ip-2',
        identifier: 'US Patent #11,420,184',
        officialNumber: 'US 11,420,184 B1',
        title: 'Perceptually Uniform OKLCH Dark Mode Algorithm',
        type: 'Utility Patent',
        jurisdictionCode: 'US',
        jurisdictionName: 'United States',
        jurisdictionFlag: '🇺🇸',
        patentOffice: 'USPTO',
        applicationDate: '2022-09-14',
        grantDate: '2023-08-22',
        status: 'Granted / Registered',
        nextDeadline: 'Feb 2027',
        deadlineNumber: '2027-02-22',
        deadlineNote: 'USPTO 3.5-Year Maintenance Window',
        ipcClass: 'CPC G06T 11/00 · Perceptual Colorimetry',
        attorney: 'Vance & Morrison LLP',
        gazetteUrl: 'https://patents.google.com/patent/US11420184B1/en',
        gazetteLabel: 'View USPTO Gazette',
      },
      {
        id: 'ip-3',
        identifier: 'US Trademark #98421042',
        officialNumber: 'Reg. #7,294,810 (SN 98421042)',
        title: 'UIPKGE™ (Class 09, 42)',
        type: 'Trademark',
        jurisdictionCode: 'US',
        jurisdictionName: 'United States',
        jurisdictionFlag: '🇺🇸',
        patentOffice: 'USPTO',
        applicationDate: '2023-02-10',
        grantDate: '2023-11-14',
        status: 'Granted / Registered',
        nextDeadline: 'Nov 2029',
        deadlineNumber: '2029-11-14',
        deadlineNote: 'Section 8 & 15 Affidavit of Use',
        ipcClass: 'IC 009, 042 · Computer Software & SaaS',
        attorney: 'Kilpatrick Townsend & Stockton',
        gazetteUrl: 'https://tsdr.uspto.gov/',
        gazetteLabel: 'View USPTO TSDR',
      },
      {
        id: 'ip-4',
        identifier: 'WIPO PCT/US2026/018249',
        officialNumber: 'WO 2026/148201 A1',
        title: 'Headless Component State Serialization',
        type: 'PCT International',
        jurisdictionCode: 'WIPO',
        jurisdictionName: 'WIPO International',
        jurisdictionFlag: '🌍',
        patentOffice: 'WIPO IB',
        applicationDate: '2026-01-15',
        grantDate: 'Pending',
        status: 'Published / Under Exam',
        nextDeadline: 'Jul 2027',
        deadlineNumber: '2027-07-15',
        deadlineNote: '30-Mo National Phase Entry Deadline',
        ipcClass: 'IPC G06F 9/44 · Component State Transfer',
        attorney: 'Vance & Morrison LLP',
        gazetteUrl: 'https://patentscope.wipo.int/',
        gazetteLabel: 'View WIPO Patentscope',
      },
      {
        id: 'ip-5',
        identifier: 'EP Patent #3,948,102',
        officialNumber: 'EP 3,948,102 A1',
        title: 'Reactive Virtual DOM Diffing Engine for Web Components',
        type: 'Utility Patent',
        jurisdictionCode: 'EU',
        jurisdictionName: 'European Union',
        jurisdictionFlag: '🇪🇺',
        patentOffice: 'EPO',
        applicationDate: '2024-03-02',
        grantDate: 'Pending',
        status: 'Published / Under Exam',
        nextDeadline: 'Mar 2027',
        deadlineNumber: '2027-03-31',
        deadlineNote: 'EPO 3rd Year Annuity Maintenance Fee',
        ipcClass: 'CPC G06F 9/455 · Virtualized DOM Execution',
        attorney: 'Bardehle Pagenberg Partnerschaft',
        gazetteUrl: 'https://worldwide.espacenet.com/',
        gazetteLabel: 'View EPO Espacenet',
      },
      {
        id: 'ip-6',
        identifier: 'EU Trademark #018942001',
        officialNumber: 'Reg. #018942001',
        title: 'UIPKGE DESIGN SYSTEM™ (Class 09, 35, 42)',
        type: 'Trademark',
        jurisdictionCode: 'EU',
        jurisdictionName: 'European Union',
        jurisdictionFlag: '🇪🇺',
        patentOffice: 'EUIPO',
        applicationDate: '2023-08-05',
        grantDate: '2024-01-19',
        status: 'Granted / Registered',
        nextDeadline: 'May 2030',
        deadlineNumber: '2030-05-08',
        deadlineNote: 'EUIPO 10-Year Trademark Renewal',
        ipcClass: 'IC 009, 035, 042 · Design Systems & Tooling',
        attorney: 'Bardehle Pagenberg Partnerschaft',
        gazetteUrl: 'https://euipo.europa.eu/eSearch/',
        gazetteLabel: 'View EUIPO eSearch',
      },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-trademark-patent-portfolio, [ui-trademark-patent-portfolio]',
      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,
        UiCardHeaderComponent,
        UiCardTitleComponent,
        UiDropdownMenuComponent,
        UiDropdownMenuContentComponent,
        UiDropdownMenuItemComponent,
        UiDropdownMenuLabelComponent,
        UiDropdownMenuSeparatorComponent,
        UiDropdownMenuTriggerComponent,
        UiInputComponent,
        UiTableComponent,
        UiTableBodyComponent,
        UiTableCellComponent,
        UiTableHeadComponent,
        UiTableHeaderComponent,
        UiTableRowComponent,
      ],
      template: `
        <div data-slot="trademark-patent-portfolio" [class]="rootClass">
          <!-- Header Section -->
          <div ui-card class="border-border shadow-xs">
            <div ui-card-header class="flex flex-col gap-4 pb-6 lg:flex-row lg:items-center lg:justify-between">
              <div class="space-y-1.5">
                <div class="flex flex-wrap items-center gap-2">
                  <span ui-badge variant="outline" class="gap-1.5 font-medium">
                    <lucide-icon [img]="Building2" class="text-muted-foreground size-3.5" />
                    <span>UIPKGE Technologies Inc.</span>
                  </span>
                  <span ui-badge wrap variant="secondary" class="gap-1 font-mono text-xs">
                    <lucide-icon [img]="Scale" class="text-primary size-3" />
                    <span>12 Patents · 6 Registered Trademarks</span>
                  </span>
                </div>
                <h2 ui-card-title class="text-xl font-bold tracking-tight sm:text-2xl">
                  Intellectual Property &amp; Patent Portfolio
                </h2>
                <p ui-card-description class="text-xs sm:text-sm">
                  USPTO, EPO, and WIPO asset docket, grant prosecution tracker, and statutory maintenance timeline.
                </p>
              </div>
    
              <div class="flex flex-wrap items-center gap-2.5">
                <button
                  ui-button
                  aria-label="Download attachment"
                  variant="outline"
                  class="gap-2 shadow-xs"
                  (click)="handleExport()"
                >
                  <lucide-icon [img]="Download" class="size-4" />
                  <span>Export Portfolio Report</span>
                </button>
                <button ui-button variant="default" class="gap-2 shadow-xs" (click)="handleFileNewAsset()">
                  <lucide-icon [img]="Plus" class="size-4" />
                  <span>File New IP Asset</span>
                </button>
              </div>
            </div>
          </div>
    
          <!-- Feedback Notification Toast/Banner if active -->
          @if (feedbackMessage()) {
            <div
              class="border-primary/30 bg-primary/10 text-primary flex items-center justify-between rounded-lg border px-4 py-2.5 text-xs font-medium shadow-xs transition-colors"
            >
              <div class="flex items-center gap-2">
                <lucide-icon [img]="CheckCircle2" class="size-4 shrink-0" />
                <span>{{ feedbackMessage() }}</span>
              </div>
              <button
                type="button"
                (click)="feedbackMessage.set(null)"
                class="text-primary/70 hover:text-primary rounded text-xs underline underline-offset-2"
              >
                Dismiss
              </button>
            </div>
          }
    
          <!-- 4 IP Portfolio KPI Cards Grid -->
          <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="p-5">
                <div class="flex items-center justify-between">
                  <span class="text-muted-foreground text-xs font-medium">Granted Patents</span>
                  <div class="bg-success/10 text-success flex size-8 items-center justify-center rounded-md">
                    <lucide-icon [img]="Award" class="size-4" />
                  </div>
                </div>
                <div class="mt-3">
                  <div class="text-success text-2xl font-bold tracking-tight tabular-nums">8 Patents Granted</div>
                  <p class="text-muted-foreground mt-1 text-xs">6 USPTO · 2 EPO · 100% Enforceable</p>
                </div>
              </div>
            </div>
    
            <div ui-card class="border-border shadow-xs">
              <div ui-card-content class="p-5">
                <div class="flex items-center justify-between">
                  <span class="text-muted-foreground text-xs font-medium">Pending Examination</span>
                  <div class="bg-warning/10 text-warning flex size-8 items-center justify-center rounded-md">
                    <lucide-icon [img]="Clock" class="size-4" />
                  </div>
                </div>
                <div class="mt-3">
                  <div class="text-warning text-2xl font-bold tracking-tight tabular-nums">4 Under Examination · USPTO</div>
                  <p class="text-muted-foreground mt-1 text-xs">2 PCT International · 1 USPTO · 1 EPO</p>
                </div>
              </div>
            </div>
    
            <div ui-card class="border-border shadow-xs">
              <div ui-card-content class="p-5">
                <div class="flex items-center justify-between">
                  <span class="text-muted-foreground text-xs font-medium">Registered Trademarks</span>
                  <div class="bg-info/10 text-info flex size-8 items-center justify-center rounded-md">
                    <lucide-icon [img]="BadgeCheck" class="size-4" />
                  </div>
                </div>
                <div class="mt-3">
                  <div class="text-info text-2xl font-bold tracking-tight tabular-nums">6 Trademarks Active</div>
                  <p class="text-muted-foreground mt-1 text-xs">Classes 09, 35, 42 · Global Protection</p>
                </div>
              </div>
            </div>
    
            <div ui-card class="border-border shadow-xs">
              <div ui-card-content class="p-5">
                <div class="flex items-center justify-between">
                  <span class="text-muted-foreground text-xs font-medium">Next Maintenance Fee</span>
                  <div class="bg-destructive/10 text-destructive flex size-8 items-center justify-center rounded-md">
                    <lucide-icon [img]="Calendar" class="size-4" />
                  </div>
                </div>
                <div class="mt-3">
                  <div class="text-destructive text-2xl font-bold tracking-tight tabular-nums">
                    Due Dec 15, 2026 · USPTO 3.5-Year Window
                  </div>
                  <p class="text-muted-foreground mt-1 text-xs">Est. fee $2,000 · Surcharge window opens</p>
                </div>
              </div>
            </div>
          </div>
    
          <!-- Filters & Table Container Card -->
          <div ui-card class="border-border shadow-xs">
            <div ui-card-header class="border-border border-b pb-4">
              <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
                <div>
                  <h3 ui-card-title class="text-base font-semibold">Registered Intellectual Property Assets</h3>
                  <p ui-card-description class="text-xs">
                    Showing {{ filteredAssets().length }} of {{ allAssets.length }} assets across USPTO, EUIPO, and WIPO
                  </p>
                </div>
    
                <div class="flex flex-wrap items-center gap-2">
                  <div class="relative min-w-[200px] flex-1 sm:w-64">
                    <lucide-icon
                      [img]="Search"
                      class="text-muted-foreground pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2"
                    />
                    <ui-input
                      [value]="searchQuery()"
                      (valueChange)="searchQuery.set($event)"
                      type="text"
                      placeholder="Search patent #, title, class..."
                      class="h-8 pl-8 text-xs"
                    />
                  </div>
    
                  <ui-dropdown-menu>
                    <button ui-button ui-dropdown-menu-trigger variant="outline" size="sm" class="h-8 gap-1.5 text-xs">
                      <lucide-icon [img]="Filter" class="size-3.5 opacity-70" />
                      <span>{{ selectedType() === 'all' ? 'Type: All' : selectedType() }}</span>
                      <lucide-icon [img]="ChevronDown" class="size-3 opacity-50" />
                    </button>
                    <ui-dropdown-menu-content align="end" class="w-48">
                      <ui-dropdown-menu-label class="text-xs">Filter by Type</ui-dropdown-menu-label>
                      <ui-dropdown-menu-separator />
                      <ui-dropdown-menu-item (select)="selectedType.set('all')">All Types</ui-dropdown-menu-item>
                      <ui-dropdown-menu-item (select)="selectedType.set('Utility Patent')"
                        >Utility Patent</ui-dropdown-menu-item
                      >
                      <ui-dropdown-menu-item (select)="selectedType.set('Trademark')">Trademark</ui-dropdown-menu-item>
                      <ui-dropdown-menu-item (select)="selectedType.set('PCT International')"
                        >PCT International</ui-dropdown-menu-item
                      >
                    </ui-dropdown-menu-content>
                  </ui-dropdown-menu>
    
                  <ui-dropdown-menu>
                    <button ui-button ui-dropdown-menu-trigger variant="outline" size="sm" class="h-8 gap-1.5 text-xs">
                      <span>{{ selectedStatus() === 'all' ? 'Status: All' : selectedStatus() }}</span>
                      <lucide-icon [img]="ChevronDown" class="size-3 opacity-50" />
                    </button>
                    <ui-dropdown-menu-content align="end" class="w-52">
                      <ui-dropdown-menu-label class="text-xs">Filter by Status</ui-dropdown-menu-label>
                      <ui-dropdown-menu-separator />
                      <ui-dropdown-menu-item (select)="selectedStatus.set('all')">All Statuses</ui-dropdown-menu-item>
                      <ui-dropdown-menu-item (select)="selectedStatus.set('Granted / Registered')">
                        Granted / Registered
                      </ui-dropdown-menu-item>
                      <ui-dropdown-menu-item (select)="selectedStatus.set('Published / Under Exam')">
                        Published / Under Exam
                      </ui-dropdown-menu-item>
                    </ui-dropdown-menu-content>
                  </ui-dropdown-menu>
                </div>
              </div>
            </div>
    
            <div ui-card-content class="p-0">
              <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-[280px]">Asset Identifier &amp; Title</th>
                      <th ui-table-head class="w-[140px]">Type</th>
                      <th ui-table-head class="w-[160px]">Jurisdiction</th>
                      <th ui-table-head class="w-[160px]">App / Grant Date</th>
                      <th ui-table-head class="w-[170px]">Filing Status</th>
                      <th ui-table-head class="w-[180px]">Next Renewal / Deadline</th>
                      <th ui-table-head class="w-[70px] text-right"><span class="sr-only">Actions</span></th>
                    </tr>
                  </thead>
                  <tbody ui-table-body>
                    @for (asset of filteredAssets(); track asset.id) {
                      <tr ui-table-row class="hover:bg-muted/40 transition-colors">
                        <td ui-table-cell class="py-3.5 align-top">
                          <div class="space-y-1">
                            <div class="flex flex-wrap items-center gap-1.5">
                              <span class="text-foreground font-mono text-xs font-bold tracking-tight">
                                {{ asset.identifier }}
                              </span>
                              <button
                                type="button"
                                (click)="copyText(asset.id, asset.officialNumber)"
                                class="text-muted-foreground hover:text-foreground focus-visible:ring-ring inline-flex items-center gap-1 rounded p-0.5 transition-colors focus-visible:ring-2 focus-visible:outline-hidden"
                                [attr.title]="'Copy ' + asset.officialNumber"
                              >
                                @if (copiedId() === asset.id) {
                                  <lucide-icon [img]="Check" class="text-success size-3" />
                                } @else {
                                  <lucide-icon [img]="Copy" class="size-3" />
                                }
                              </button>
                            </div>
                            <div class="text-foreground text-xs font-medium">{{ asset.title }}</div>
                            <div class="text-muted-foreground flex flex-wrap items-center gap-2 text-xs">
                              <span>{{ asset.ipcClass }}</span>
                              <span>•</span>
                              <span>Counsel: {{ asset.attorney }}</span>
                            </div>
                          </div>
                        </td>
    
                        <td ui-table-cell class="align-top">
                          <span ui-badge variant="outline" [class]="typeBadgeClass(asset.type)">
                            {{ asset.type }}
                          </span>
                        </td>
    
                        <td ui-table-cell class="align-top">
                          <div class="flex items-center gap-2">
                            <span class="text-base leading-none" [attr.aria-label]="asset.jurisdictionName">
                              {{ asset.jurisdictionFlag }}
                            </span>
                            <div>
                              <div class="text-foreground text-xs font-medium">{{ asset.jurisdictionName }}</div>
                              <div class="text-muted-foreground font-mono text-xs">{{ asset.patentOffice }}</div>
                            </div>
                          </div>
                        </td>
    
                        <td ui-table-cell class="align-top">
                          <div class="space-y-0.5 font-mono text-xs tabular-nums">
                            <div class="text-muted-foreground flex items-center gap-1.5">
                              <span class="text-muted-foreground/70">Filing:</span>
                              <span class="text-foreground font-medium">{{ asset.applicationDate }}</span>
                            </div>
                            <div class="text-muted-foreground flex items-center gap-1.5">
                              <span class="text-muted-foreground/70">Grant:</span>
                              <span
                                [class]="
                                  asset.grantDate === 'Pending' ? 'text-warning font-medium' : 'text-foreground font-medium'
                                "
                              >
                                {{ asset.grantDate }}
                              </span>
                            </div>
                          </div>
                        </td>
    
                        <td ui-table-cell class="align-top">
                          @if (asset.status === 'Granted / Registered') {
                            <div
                              class="border-success/30 bg-success/10 text-success inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-medium"
                            >
                              <span class="relative flex size-1.5">
                                <span class="bg-success absolute inline-flex h-full w-full rounded-full opacity-75"></span>
                                <span class="bg-success relative inline-flex size-1.5 rounded-full"></span>
                              </span>
                              <span>Granted / Registered</span>
                            </div>
                          } @else {
                            <div
                              class="border-warning/30 bg-warning/10 text-warning inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-medium"
                            >
                              <lucide-icon [img]="Clock" class="text-warning size-3" />
                              <span>Published / Under Exam</span>
                            </div>
                          }
                        </td>
    
                        <td ui-table-cell class="align-top">
                          <div class="space-y-0.5">
                            <div
                              class="text-foreground flex items-center gap-1.5 font-mono text-xs font-semibold tabular-nums"
                            >
                              <lucide-icon [img]="Calendar" class="text-muted-foreground size-3.5" />
                              <span>{{ asset.nextDeadline }}</span>
                            </div>
                            <p class="text-muted-foreground text-xs leading-tight">{{ asset.deadlineNote }}</p>
                          </div>
                        </td>
    
                        <td ui-table-cell class="py-3.5 text-right align-top">
                          <ui-dropdown-menu>
                            <button
                              ui-button
                              ui-dropdown-menu-trigger
                              variant="ghost"
                              size="icon-sm"
                              class="text-muted-foreground hover:text-foreground focus-visible:ring-ring"
                            >
                              <lucide-icon [img]="MoreHorizontal" class="size-4" />
                              <span class="sr-only">Open actions for {{ asset.identifier }}</span>
                            </button>
                            <ui-dropdown-menu-content align="end" class="w-56">
                              <ui-dropdown-menu-label class="text-xs">Asset Operations</ui-dropdown-menu-label>
                              <ui-dropdown-menu-separator />
                              <ui-dropdown-menu-item (select)="handleAction(asset.gazetteLabel, asset)">
                                <lucide-icon [img]="ExternalLink" class="mr-2 size-4" />
                                <span>{{ asset.gazetteLabel }}</span>
                              </ui-dropdown-menu-item>
                              <ui-dropdown-menu-item (select)="handleAction('Download Grant Certificate', asset)">
                                <lucide-icon [img]="Download" class="mr-2 size-4" />
                                <span>Download Grant Certificate</span>
                              </ui-dropdown-menu-item>
                              <ui-dropdown-menu-item (select)="handleAction('Assign Counsel', asset)">
                                <lucide-icon [img]="UserCheck" class="mr-2 size-4" />
                                <span>Assign Counsel</span>
                              </ui-dropdown-menu-item>
                              <ui-dropdown-menu-separator />
                              <ui-dropdown-menu-item (select)="copyText(asset.id, asset.officialNumber)">
                                <lucide-icon [img]="Copy" class="mr-2 size-4" />
                                <span>Copy Docket Number</span>
                              </ui-dropdown-menu-item>
                            </ui-dropdown-menu-content>
                          </ui-dropdown-menu>
                        </td>
                      </tr>
                    }
    
                    @if (filteredAssets().length === 0) {
                      <tr ui-table-row>
                        <td ui-table-cell [attr.colspan]="7" class="text-muted-foreground py-10 text-center text-xs">
                          <div class="flex flex-col items-center justify-center gap-2">
                            <lucide-icon [img]="FileText" class="text-muted-foreground/60 size-8" />
                            <p class="font-medium">No intellectual property assets match your search filters.</p>
                            <button ui-button variant="outline" size="sm" class="mt-1 text-xs" (click)="resetFilters()">
                              Reset Filters
                            </button>
                          </div>
                        </td>
                      </tr>
                    }
                  </tbody>
                </table>
              </div>
            </div>
          </div>
        </div>
      `,
    })
    export class UiTrademarkPatentPortfolioComponent {
      protected readonly Award = Award
      protected readonly BadgeCheck = BadgeCheck
      protected readonly Building2 = Building2
      protected readonly Calendar = Calendar
      protected readonly Check = Check
      protected readonly CheckCircle2 = CheckCircle2
      protected readonly ChevronDown = ChevronDown
      protected readonly Clock = Clock
      protected readonly Copy = Copy
      protected readonly Download = Download
      protected readonly ExternalLink = ExternalLink
      protected readonly FileText = FileText
      protected readonly Filter = Filter
      protected readonly MoreHorizontal = MoreHorizontal
      protected readonly Plus = Plus
      protected readonly Scale = Scale
      protected readonly Search = Search
      protected readonly UserCheck = UserCheck
    
      protected readonly allAssets = ipAssets
    
      @Input('class') className?: string
    
      readonly searchQuery = signal('')
      readonly selectedType = signal<'all' | AssetType>('all')
      readonly selectedStatus = signal<'all' | FilingStatus>('all')
      readonly copiedId = signal<string | null>(null)
      readonly feedbackMessage = signal<string | null>(null)
    
      private copyTimer: ReturnType<typeof setTimeout> | undefined
      private feedbackTimer: ReturnType<typeof setTimeout> | undefined
    
      readonly filteredAssets = computed(() => {
        const query = this.searchQuery().trim().toLowerCase()
        return ipAssets.filter((asset) => {
          const matchesType = this.selectedType() === 'all' || asset.type === this.selectedType()
          const matchesStatus = this.selectedStatus() === 'all' || asset.status === this.selectedStatus()
          const matchesSearch =
            !query ||
            asset.identifier.toLowerCase().includes(query) ||
            asset.officialNumber.toLowerCase().includes(query) ||
            asset.title.toLowerCase().includes(query) ||
            asset.jurisdictionName.toLowerCase().includes(query) ||
            asset.patentOffice.toLowerCase().includes(query) ||
            asset.ipcClass.toLowerCase().includes(query)
          return matchesType && matchesStatus && matchesSearch
        })
      })
    
      get rootClass(): string {
        return cn('w-full space-y-6', this.className)
      }
    
      typeBadgeClass(type: AssetType): string {
        return cn('font-mono text-xs font-medium', {
          'border-chart-1/30 bg-chart-1/10 text-chart-1': type === 'Utility Patent',
          'border-info/30 bg-info/10 text-info': type === 'Trademark',
          'border-warning/30 bg-warning/10 text-warning': type === 'PCT International',
        })
      }
    
      resetFilters(): void {
        this.searchQuery.set('')
        this.selectedType.set('all')
        this.selectedStatus.set('all')
      }
    
      copyText(id: string, text: string): void {
        try {
          void navigator.clipboard?.writeText(text)
        } catch {
          // Clipboard unavailable (e.g. jsdom without permissions) — still show feedback.
        }
        this.copiedId.set(id)
        clearTimeout(this.copyTimer)
        this.copyTimer = setTimeout(() => this.copiedId.set(null), 2000)
      }
    
      private flashFeedback(message: string): void {
        this.feedbackMessage.set(message)
        clearTimeout(this.feedbackTimer)
        this.feedbackTimer = setTimeout(() => this.feedbackMessage.set(null), 3000)
      }
    
      handleExport(): void {
        this.flashFeedback('Exporting IP Portfolio summary (CSV / PDF)...')
      }
    
      handleFileNewAsset(): void {
        this.flashFeedback('Opening USPTO / WIPO EFS-Web filing wizard...')
      }
    
      handleAction(action: string, asset: IpAsset): void {
        this.flashFeedback(`${action}: ${asset.identifier}`)
      }
    }
    
  • src/app/components/blocks/trademark-patent-portfolio/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/trademark-patent-portfolio.json