{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "audit-log",
  "title": "Audit Log",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/audit-log/audit-log.component.ts",
      "content": "// Admin activity trail in a SectionCard: live indicator, search across actor/target,\n// action-type filter, and rows pairing an avatar actor with a color-coded action badge,\n// monospace target, IP and relative timestamp. Port of the Vue/React block 1:1 —\n// filtering is functional against the `entries` input; swap the stub data for your source.\nimport { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core'\nimport { Download, Fingerprint, LucideAngularModule, RadioTower, Search } from 'lucide-angular'\nimport { cn } from '@/lib/utils'\nimport { UiAvatarComponent, UiAvatarFallbackComponent } from '@/ui/avatar/avatar.component'\nimport { UiBadgeComponent, type BadgeVariant } from '@/ui/badge/badge.component'\nimport { UiButtonComponent } from '@/ui/button/button.component'\nimport { UiInputComponent } from '@/ui/input/input.component'\nimport { UiRelativeTimeComponent } from '@/ui/relative-time/relative-time.component'\nimport { UiSectionCardComponent } from '@/ui/section-card/section-card.component'\nimport {\n  UiSelectComponent,\n  UiSelectContentComponent,\n  UiSelectItemComponent,\n  UiSelectTriggerComponent,\n  UiSelectValueComponent,\n} from '@/ui/select/select.component'\n\nexport type AuditAction = 'sign-in' | 'user.updated' | 'role.changed' | 'export' | 'delete'\n\nexport interface AuditEntry {\n  id: string\n  actor: string\n  action: AuditAction\n  target: string\n  ip: string\n  date: Date\n}\n\nconst actionTone: Record<AuditAction, BadgeVariant> = {\n  'sign-in': 'secondary',\n  'user.updated': 'default',\n  'role.changed': 'warning',\n  export: 'info',\n  delete: 'destructive',\n}\n\nconst now = Date.now()\nconst minutesAgo = (m: number): Date => new Date(now - m * 60_000)\nconst hoursAgo = (h: number): Date => new Date(now - h * 3_600_000)\nconst daysAgo = (d: number): Date => new Date(now - d * 86_400_000)\n\nconst stubEntries: AuditEntry[] = [\n  {\n    id: 'a1',\n    actor: 'Amara Osei',\n    action: 'role.changed',\n    target: 'priya@acme.com → Editor',\n    ip: '77.12.44.9',\n    date: minutesAgo(4),\n  },\n  { id: 'a2', actor: 'System', action: 'export', target: 'payroll-2026-07.csv', ip: '—', date: minutesAgo(38) },\n  {\n    id: 'a3',\n    actor: 'Jonas Weber',\n    action: 'sign-in',\n    target: 'admin.acme.com',\n    ip: '84.190.201.3',\n    date: hoursAgo(1),\n  },\n  {\n    id: 'a4',\n    actor: 'Priya Nair',\n    action: 'user.updated',\n    target: 'profile.phone',\n    ip: '103.25.8.77',\n    date: hoursAgo(3),\n  },\n  {\n    id: 'a5',\n    actor: 'Amara Osei',\n    action: 'delete',\n    target: 'draft-policy-v3.pdf',\n    ip: '77.12.44.9',\n    date: hoursAgo(5),\n  },\n  { id: 'a6', actor: 'Marcus Lee', action: 'sign-in', target: 'admin.acme.com', ip: '45.62.110.204', date: daysAgo(1) },\n  {\n    id: 'a7',\n    actor: 'Priya Nair',\n    action: 'user.updated',\n    target: 'team/onboarding-flow',\n    ip: '103.25.8.77',\n    date: daysAgo(1),\n  },\n  { id: 'a8', actor: 'System', action: 'role.changed', target: 'contractors → Viewer', ip: '—', date: daysAgo(2) },\n  {\n    id: 'a9',\n    actor: 'Jonas Weber',\n    action: 'export',\n    target: 'audit-trail-q2.json',\n    ip: '84.190.201.3',\n    date: daysAgo(2),\n  },\n  {\n    id: 'a10',\n    actor: 'Marcus Lee',\n    action: 'delete',\n    target: 'staging-deploy-key',\n    ip: '45.62.110.204',\n    date: daysAgo(3),\n  },\n]\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-audit-log, [ui-audit-log]',\n  standalone: true,\n  // SectionCard renders the card chrome itself: the host stays out of layout.\n  host: { '[attr.class]': '\"contents\"' },\n  imports: [\n    LucideAngularModule,\n    UiAvatarComponent,\n    UiAvatarFallbackComponent,\n    UiBadgeComponent,\n    UiButtonComponent,\n    UiInputComponent,\n    UiRelativeTimeComponent,\n    UiSectionCardComponent,\n    UiSelectComponent,\n    UiSelectContentComponent,\n    UiSelectItemComponent,\n    UiSelectTriggerComponent,\n    UiSelectValueComponent,\n  ],\n  template: `\n    <ui-section-card\n      data-slot=\"audit-log\"\n      title=\"Audit log\"\n      description=\"Who changed what, when — across your workspace.\"\n      [class]=\"rootClass\"\n    >\n      <span slot=\"header-action\" class=\"text-success flex items-center gap-1.5 text-xs font-medium\">\n        <lucide-icon [img]=\"RadioTower\" class=\"size-3.5\" aria-hidden=\"true\" />\n        live\n      </span>\n\n      <div class=\"flex flex-col gap-2 sm:flex-row sm:items-center\">\n        <div class=\"relative flex-1\">\n          <lucide-icon\n            [img]=\"Search\"\n            class=\"text-muted-foreground pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2\"\n            aria-hidden=\"true\"\n          />\n          <ui-input\n            [value]=\"search()\"\n            (valueChange)=\"search.set($event)\"\n            placeholder=\"Search actor or target…\"\n            class=\"pl-8\"\n          />\n        </div>\n        <ui-select [value]=\"actionFilter()\" (valueChange)=\"setActionFilter($event)\">\n          <button ui-select-trigger class=\"sm:w-44\">\n            <ui-select-value placeholder=\"All actions\" />\n          </button>\n          <ui-select-content>\n            <ui-select-item value=\"all\">All actions</ui-select-item>\n            @for (action of actions; track action) {\n              <ui-select-item [value]=\"action\">{{ action }}</ui-select-item>\n            }\n          </ui-select-content>\n        </ui-select>\n        <button ui-button aria-label=\"Download attachment\" variant=\"outline\" size=\"sm\">\n          <lucide-icon [img]=\"Download\" aria-hidden=\"true\" />\n          Export\n        </button>\n      </div>\n\n      <ul class=\"-mb-4 divide-y\">\n        @if (filtered().length === 0) {\n          <li class=\"text-muted-foreground flex flex-col items-center gap-2 py-10 text-sm\">\n            <lucide-icon [img]=\"Fingerprint\" class=\"size-6 opacity-50\" aria-hidden=\"true\" />\n            No activity matches your filters.\n          </li>\n        }\n        @for (entry of filtered(); track entry.id) {\n          <li class=\"flex items-center gap-3 py-3\">\n            <ui-avatar class=\"size-7\">\n              <ui-avatar-fallback class=\"text-xs\">{{ initials(entry.actor) }}</ui-avatar-fallback>\n            </ui-avatar>\n            <div class=\"min-w-0 flex-1\">\n              <div class=\"flex items-center gap-2\">\n                <p class=\"truncate text-sm font-medium\">{{ entry.actor }}</p>\n                <span ui-badge [variant]=\"tone(entry.action)\" class=\"shrink-0\">{{ entry.action }}</span>\n              </div>\n              <p class=\"text-muted-foreground truncate font-mono text-xs\">{{ entry.target }}</p>\n            </div>\n            <div class=\"hidden text-right sm:block\">\n              <ui-relative-time [date]=\"entry.date\" class=\"text-muted-foreground block text-xs\" />\n              <p class=\"text-muted-foreground/70 font-mono text-xs\">{{ entry.ip }}</p>\n            </div>\n          </li>\n        }\n      </ul>\n    </ui-section-card>\n  `,\n})\nexport class UiAuditLogComponent {\n  protected readonly Download = Download\n  protected readonly Fingerprint = Fingerprint\n  protected readonly RadioTower = RadioTower\n  protected readonly Search = Search\n\n  protected readonly actions: AuditAction[] = ['sign-in', 'user.updated', 'role.changed', 'export', 'delete']\n\n  @Input() set entries(v: AuditEntry[] | undefined) {\n    this.entriesInput.set(v)\n  }\n  @Input() set initialSearch(v: string) {\n    this.search.set(v ?? '')\n  }\n  @Input() set initialAction(v: 'all' | AuditAction) {\n    this.actionFilter.set(v ?? 'all')\n  }\n  @Input('class') className?: string\n\n  readonly entriesInput = signal<AuditEntry[] | undefined>(undefined)\n  readonly search = signal('')\n  readonly actionFilter = signal<'all' | AuditAction>('all')\n\n  readonly source = computed(() => this.entriesInput() ?? stubEntries)\n\n  readonly filtered = computed(() => {\n    const query = this.search()\n    const action = this.actionFilter()\n    return this.source().filter((entry) => {\n      const matchesQuery = query === '' || `${entry.actor} ${entry.target}`.toLowerCase().includes(query.toLowerCase())\n      const matchesAction = action === 'all' || entry.action === action\n      return matchesQuery && matchesAction\n    })\n  })\n\n  get rootClass(): string {\n    return cn(this.className)\n  }\n\n  tone(action: AuditAction): BadgeVariant {\n    return actionTone[action]\n  }\n\n  setActionFilter(value: string): void {\n    this.actionFilter.set(value as 'all' | AuditAction)\n  }\n\n  initials(name: string): string {\n    return name\n      .split(' ')\n      .map((part) => part[0])\n      .slice(0, 2)\n      .join('')\n      .toUpperCase()\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/audit-log/audit-log.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/audit-log/index.ts",
      "content": "export { UiAuditLogComponent, type AuditAction, type AuditEntry } from './audit-log.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/audit-log/index.ts"
    }
  ],
  "dependencies": [
    "lucide-angular"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/avatar.json",
    "https://uipkge.dev/r/angular/badge.json",
    "https://uipkge.dev/r/angular/button.json",
    "https://uipkge.dev/r/angular/input.json",
    "https://uipkge.dev/r/angular/relative-time.json",
    "https://uipkge.dev/r/angular/section-card.json",
    "https://uipkge.dev/r/angular/select.json"
  ],
  "description": "Admin activity trail in a SectionCard: live indicator, search across actor/target, action-type filter, and rows pairing an avatar actor with a color-coded action badge, monospace target, IP and relative timestamp. Filtering is functional against the `entries` prop; swap the stub data for your source.",
  "categories": [
    "legal",
    "dashboard",
    "data"
  ]
}