Menu

Change language

GitHub repo

Meeting Agenda Notes

blockproductivity

Linear and Notion style structured meeting document with attendee avatars, timeboxed agenda topics, rich discussion notes, interactive action items checklist, and key decisions log.

Installation

$npx uipkge-ng@latest add meeting-agenda-notes
Manifest:https://uipkge.dev/r/angular/meeting-agenda-notes.jsonInstalls to:src/app/components/blocks/meeting-agenda-notes/

Variants

Loading interactive previews…

Schema

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

Attendee
interface Attendee {
  id: string
  name: string
  role: string
  initials: string
  isHost?: boolean
  status: 'online' | 'busy' | 'away'
  colorClass: string
}
AgendaTopic
interface AgendaTopic {
  id: string
  number: number
  title: string
  duration: string
  timeRange: string
  lead: string
  leadInitials: string
  status: 'completed' | 'active' | 'upcoming'
  summary: string
  tags: string[]
}
NoteEntry
interface NoteEntry {
  id: string
  author: string
  authorInitials: string
  authorColor: string
  timestamp: string
  content: string
  tag: string
}
ActionItem
interface ActionItem {
  id: string
  title: string
  assignee: string
  assigneeInitials: string
  assigneeColor: string
  dueDate: string
  priority: 'Urgent' | 'High' | 'Medium' | 'Low'
  completed: boolean
}
KeyDecision
interface KeyDecision {
  id: string
  number: number
  title: string
  context: string
  status: 'Adopted' | 'Under Review'
  decidedBy: string
  time: string
}

Files installed (2)

  • src/app/components/blocks/meeting-agenda-notes/meeting-agenda-notes.component.ts47 kB
    // Linear and Notion style structured meeting document with attendee avatars, timeboxed
    // agenda topics, rich discussion notes, interactive action items checklist, and key
    // decisions log. Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core'
    import {
      Calendar,
      Check,
      CheckCircle2,
      Clock,
      Copy,
      ExternalLink,
      FileDown,
      FileSpreadsheet,
      FileText,
      GitPullRequest,
      ListOrdered,
      Lock,
      LucideAngularModule,
      Plus,
      Radio,
      UserCheck,
      Video,
    } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import { UiAvatarComponent, UiAvatarFallbackComponent } from '@/ui/avatar/avatar.component'
    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 { UiCheckboxComponent } from '@/ui/checkbox/checkbox.component'
    import { UiSeparatorComponent } from '@/ui/separator/separator.component'
    
    export interface Attendee {
      id: string
      name: string
      role: string
      initials: string
      isHost?: boolean
      status: 'online' | 'busy' | 'away'
      colorClass: string
    }
    
    export interface AgendaTopic {
      id: string
      number: number
      title: string
      duration: string
      timeRange: string
      lead: string
      leadInitials: string
      status: 'completed' | 'active' | 'upcoming'
      summary: string
      tags: string[]
    }
    
    export interface NoteEntry {
      id: string
      author: string
      authorInitials: string
      authorColor: string
      timestamp: string
      content: string
      tag: string
    }
    
    export interface ActionItem {
      id: string
      title: string
      assignee: string
      assigneeInitials: string
      assigneeColor: string
      dueDate: string
      priority: 'Urgent' | 'High' | 'Medium' | 'Low'
      completed: boolean
    }
    
    export interface KeyDecision {
      id: string
      number: number
      title: string
      context: string
      status: 'Adopted' | 'Under Review'
      decidedBy: string
      time: string
    }
    
    const noteTagOptions = ['#architecture', '#pipeline', '#decision', '#action']
    const priorityOptions: ActionItem['priority'][] = ['Urgent', 'High', 'Medium', 'Low']
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-meeting-agenda-notes, [ui-meeting-agenda-notes]',
      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,
        UiBadgeComponent,
        UiButtonComponent,
        UiCardComponent,
        UiCardContentComponent,
        UiCardDescriptionComponent,
        UiCardHeaderComponent,
        UiCardTitleComponent,
        UiCheckboxComponent,
        UiSeparatorComponent,
      ],
      template: `
        <div data-slot="meeting-agenda-notes" [class]="rootClass">
          <!-- Meeting Header Card -->
          <div ui-card class="border-border bg-card shadow-xs">
            <div ui-card-content class="space-y-5 p-4 sm:p-6">
              <div class="flex flex-wrap items-center justify-between gap-3">
                <div class="flex flex-wrap items-center gap-2">
                  <span ui-badge variant="secondary" class="gap-1.5 px-2.5 py-1 text-xs font-medium">
                    <span class="bg-destructive size-2 animate-pulse rounded-full"></span>
                    <span class="font-mono tracking-tight tabular-nums">REC · 45:12</span>
                  </span>
                  <span ui-badge variant="outline" class="text-muted-foreground text-xs"> Architecture Sync </span>
                  <span ui-badge variant="outline" class="text-muted-foreground text-xs"> Bi-Weekly Cadence </span>
                </div>
    
                <div class="flex flex-wrap items-center gap-2">
                  <button
                    ui-button
                    variant="outline"
                    size="sm"
                    class="h-8 gap-1.5 text-xs shadow-xs"
                    (click)="exportNotesMarkdown()"
                  >
                    @if (copiedNotes()) {
                      <lucide-icon [img]="Check" class="text-success size-3.5" />
                    } @else {
                      <lucide-icon [img]="FileDown" class="text-muted-foreground size-3.5" />
                    }
                    {{ copiedNotes() ? 'Notes Copied!' : 'Export Notes Markdown' }}
                  </button>
                  <button ui-button size="sm" class="h-8 gap-1.5 text-xs shadow-xs">
                    <lucide-icon [img]="Video" class="size-3.5" />
                    Join Call
                  </button>
                </div>
              </div>
    
              <div class="space-y-2">
                <h1 class="text-foreground text-xl font-bold tracking-tight sm:text-2xl">
                  Q3 Product Architecture & Monorepo Scaling Sync
                </h1>
    
                <div class="text-muted-foreground flex flex-wrap items-center gap-x-5 gap-y-2 text-xs">
                  <div class="flex items-center gap-1.5">
                    <lucide-icon [img]="Calendar" class="text-foreground/70 size-3.5" />
                    <span class="text-foreground/90 font-medium">Friday, Aug 21, 2026</span>
                    <span>·</span>
                    <span class="font-mono tabular-nums">14:00 - 15:00 PST</span>
                  </div>
                  <div class="flex items-center gap-1.5">
                    <lucide-icon [img]="Clock" class="text-foreground/70 size-3.5" />
                    <span ui-badge variant="secondary" class="px-2 py-0.5 font-mono text-xs tabular-nums">
                      60 mins · 45 mins elapsed
                    </span>
                  </div>
                  <div class="flex items-center gap-1.5">
                    <lucide-icon [img]="Video" class="text-foreground/70 size-3.5" />
                    <button
                      type="button"
                      class="text-foreground hover:text-primary inline-flex min-h-6 cursor-pointer items-center gap-1 font-medium transition-colors"
                      (click)="copyRoomLink()"
                    >
                      <span>Zoom Room #482-901</span>
                      @if (copiedRoomLink()) {
                        <lucide-icon [img]="Check" class="text-success size-3" />
                      } @else {
                        <lucide-icon [img]="Copy" class="text-muted-foreground size-3" />
                      }
                    </button>
                  </div>
                </div>
              </div>
    
              <div ui-separator class="bg-border/60"></div>
    
              <div class="flex flex-col justify-between gap-4 sm:flex-row sm:items-center">
                <div class="space-y-2">
                  <div class="flex items-center gap-2">
                    <span class="text-muted-foreground text-xs font-semibold tracking-wider uppercase"> Attendees </span>
                    <span ui-badge variant="secondary" class="h-4.5 px-1.5 py-0 font-mono text-xs tabular-nums">
                      6 Present
                    </span>
                  </div>
    
                  <div class="flex flex-wrap items-center gap-2.5">
                    @for (attendee of attendees(); track attendee.id) {
                      <div
                        class="group border-border/80 bg-background/80 hover:border-border hover:bg-muted/40 relative flex items-center gap-2 rounded-full border py-1 pr-2.5 pl-1 shadow-xs transition-colors"
                      >
                        <div class="relative flex items-center justify-center">
                          <ui-avatar size="sm" class="size-6 text-xs">
                            <ui-avatar-fallback [class]="attendee.colorClass">{{ attendee.initials }}</ui-avatar-fallback>
                          </ui-avatar>
                          <span
                            class="border-background bg-success absolute -right-0.5 -bottom-0.5 size-2 rounded-full border-2"
                            title="Present in meeting"
                          ></span>
                        </div>
                        <span class="text-foreground text-xs font-medium">{{ attendee.name }}</span>
                        @if (attendee.isHost) {
                          <span ui-badge variant="default" class="h-4 px-1 text-xs leading-none font-semibold uppercase">
                            Host
                          </span>
                        }
                      </div>
                    }
                  </div>
                </div>
    
                <div
                  class="border-border/60 bg-muted/30 text-muted-foreground hidden items-center gap-3 rounded-lg border p-2.5 text-xs xl:flex"
                >
                  <lucide-icon [img]="Radio" class="text-success size-4 animate-pulse" />
                  <div>
                    <div class="text-foreground font-medium">Live Transcription</div>
                    <div class="text-muted-foreground font-mono">Whisper-v3 Active</div>
                  </div>
                </div>
              </div>
            </div>
          </div>
    
          <!-- 2-Column Meeting Workspace -->
          <div class="grid grid-cols-1 gap-6 lg:grid-cols-12">
            <!-- Left Panel: Timeboxed Agenda & Rich Notes (8 cols) -->
            <div class="space-y-6 lg:col-span-7 xl:col-span-8">
              <div ui-card class="border-border bg-card shadow-xs">
                <div ui-card-header class="px-5 pt-5 pb-3 sm:px-6">
                  <div class="flex flex-wrap items-center justify-between gap-2">
                    <div class="flex items-center gap-2">
                      <div class="bg-primary/10 text-primary flex size-7 items-center justify-center rounded-md">
                        <lucide-icon [img]="ListOrdered" class="size-4" />
                      </div>
                      <div>
                        <h3 ui-card-title class="text-base font-semibold">Timeboxed Agenda</h3>
                        <p ui-card-description class="text-xs">4 topics structured with real-time progress indicators</p>
                      </div>
                    </div>
                    <span ui-badge variant="outline" class="text-muted-foreground font-mono text-xs tabular-nums">
                      60m Total · 45m Elapsed (75%)
                    </span>
                  </div>
                  <div class="bg-muted mt-3 h-1.5 w-full overflow-hidden rounded-full">
                    <div class="bg-primary h-full w-3/4 rounded-full"></div>
                  </div>
                </div>
    
                <div ui-card-content class="space-y-3 p-5 pt-2 sm:p-6">
                  @for (topic of agendaTopics(); track topic.id) {
                    <div [class]="topicCardClass(topic.status)">
                      <div class="flex flex-col justify-between gap-3 sm:flex-row sm:items-start">
                        <div class="flex-1 space-y-1.5">
                          <div class="flex flex-wrap items-center gap-2">
                            <span
                              class="bg-muted text-muted-foreground flex size-5 items-center justify-center rounded-full font-mono text-xs font-semibold"
                            >
                              {{ topic.number }}
                            </span>
                            <h3 class="text-foreground text-sm font-semibold">{{ topic.title }}</h3>
                            @if (topic.status === 'completed') {
                              <span ui-badge variant="success" class="gap-1 text-xs">
                                <lucide-icon [img]="CheckCircle2" class="text-success size-3" />
                                Completed
                              </span>
                            } @else if (topic.status === 'active') {
                              <span ui-badge variant="default" class="gap-1.5 text-xs font-medium">
                                <span class="bg-primary-foreground size-1.5 rounded-full"></span>
                                Current Topic
                              </span>
                            } @else {
                              <span ui-badge variant="outline" class="text-muted-foreground gap-1 text-xs">
                                <lucide-icon [img]="Clock" class="size-3" />
                                Upcoming
                              </span>
                            }
                          </div>
                          <p class="text-muted-foreground text-xs leading-relaxed">{{ topic.summary }}</p>
                          <div class="flex flex-wrap items-center gap-1.5 pt-1">
                            @for (tag of topic.tags; track tag) {
                              <span
                                ui-badge
                                variant="secondary"
                                class="text-muted-foreground px-1.5 py-0 text-xs font-normal"
                              >
                                {{ tag }}
                              </span>
                            }
                          </div>
                        </div>
                        <div
                          class="border-border/50 flex shrink-0 items-center justify-between gap-1.5 border-t pt-2 sm:flex-col sm:items-end sm:border-t-0 sm:pt-0"
                        >
                          <div class="flex items-center gap-1.5">
                            <span class="text-muted-foreground font-mono text-xs tabular-nums">{{ topic.duration }}</span>
                            <span class="text-muted-foreground font-mono text-xs tabular-nums">{{ topic.timeRange }}</span>
                          </div>
                          <div class="flex items-center gap-1.5">
                            <ui-avatar size="xs" class="size-4.5 text-xs">
                              <ui-avatar-fallback class="bg-muted text-muted-foreground text-xs font-semibold">
                                {{ topic.leadInitials }}
                              </ui-avatar-fallback>
                            </ui-avatar>
                            <span class="text-foreground/80 text-xs font-medium">{{ topic.lead }}</span>
                          </div>
                        </div>
                      </div>
                    </div>
                  }
                </div>
              </div>
    
              <!-- Discussion Notes & Technical Blueprint -->
              <div ui-card class="border-border bg-card shadow-xs">
                <div ui-card-header class="px-5 pt-5 pb-3 sm:px-6">
                  <div class="flex flex-wrap items-center justify-between gap-2">
                    <div class="flex items-center gap-2">
                      <div class="bg-primary/10 text-primary flex size-7 items-center justify-center rounded-md">
                        <lucide-icon [img]="FileText" class="size-4" />
                      </div>
                      <div>
                        <h3 ui-card-title class="text-base font-semibold">Discussion Notes & Technical Blueprint</h3>
                        <p ui-card-description class="text-xs">Structured architectural takeaways and runtime benchmarks</p>
                      </div>
                    </div>
                    <span class="text-muted-foreground font-mono text-xs tabular-nums">
                      Last edited 2m ago by Elena Rostova
                    </span>
                  </div>
                </div>
    
                <div ui-card-content class="space-y-5 p-5 pt-2 sm:p-6">
                  <div class="border-border bg-muted/40 space-y-1 rounded-lg border p-3.5">
                    <div class="text-foreground flex items-center gap-1.5 text-xs font-semibold">
                      <lucide-icon [img]="FileText" class="text-primary size-3.5" />
                      Core Monorepo Mandate
                    </div>
                    <p class="text-muted-foreground text-xs leading-relaxed">
                      Components are the product, not an npm library. Consumers run
                      <code class="bg-muted text-foreground rounded px-1.5 py-0.5 font-mono font-medium"
                        >npx shadcn-vue add &lt;url&gt;</code
                      >
                      and own the code directly. All block primitives must compose raw layouts without baking rigid data
                      arrays into primitives.
                    </p>
                  </div>
    
                  <div class="space-y-2.5">
                    <h4 class="text-muted-foreground text-xs font-bold tracking-wider uppercase">
                      Architectural Takeaways
                    </h4>
                    <div class="space-y-2">
                      <div class="border-border/60 bg-card flex items-start gap-2.5 rounded-md border p-3 shadow-xs">
                        <div
                          class="bg-success/10 text-success mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full"
                        >
                          <lucide-icon [img]="Check" class="size-3" />
                        </div>
                        <div class="space-y-0.5 text-xs">
                          <span class="text-foreground font-semibold">Zero Runtime Overhead Delivery Model</span>
                          <p class="text-muted-foreground leading-relaxed">
                            Zero build-time vendor lock-in. Primitives handle focus rings, keyboard navigation, and
                            accessibility semantics while leaving styling tokens fully customizable by end consumers.
                          </p>
                        </div>
                      </div>
                      <div class="border-border/60 bg-card flex items-start gap-2.5 rounded-md border p-3 shadow-xs">
                        <div
                          class="bg-info/10 text-info mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full"
                        >
                          <lucide-icon [img]="Check" class="size-3" />
                        </div>
                        <div class="space-y-0.5 text-xs">
                          <span class="text-foreground font-semibold">Perceptually Uniform OKLCH Palette</span>
                          <p class="text-muted-foreground leading-relaxed">
                            Color spaces mapped directly to standard OKLCH coordinates via Tailwind CSS v4
                            <code class="bg-muted text-foreground rounded px-1 py-0.5 font-mono">&#64;theme inline</code>
                            tokens, ensuring high-fidelity rendering across Apple P3 displays.
                          </p>
                        </div>
                      </div>
                      <div class="border-border/60 bg-card flex items-start gap-2.5 rounded-md border p-3 shadow-xs">
                        <div
                          class="bg-primary/10 text-primary mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full"
                        >
                          <lucide-icon [img]="Check" class="size-3" />
                        </div>
                        <div class="space-y-0.5 text-xs">
                          <span class="text-foreground font-semibold">Flat Registry Dependencies Graph</span>
                          <p class="text-muted-foreground leading-relaxed">
                            All
                            <code class="bg-muted text-foreground rounded px-1 py-0.5 font-mono">registryDependencies</code>
                            declare flat URLs (<code class="bg-muted text-foreground rounded px-1 py-0.5 font-mono"
                              >https://uipkge.dev/r/button.json</code
                            >), eliminating nested resolver locks in downstream CLIs.
                          </p>
                        </div>
                      </div>
                    </div>
                  </div>
    
                  <div class="space-y-2">
                    <div class="flex items-center justify-between">
                      <h4 class="text-muted-foreground text-xs font-bold tracking-wider uppercase">Manifest Blueprint</h4>
                      <button
                        type="button"
                        class="text-muted-foreground hover:text-foreground flex min-h-6 cursor-pointer items-center gap-1 text-xs transition-colors"
                        (click)="copyCodeSnippet()"
                      >
                        @if (copiedSnippet()) {
                          <lucide-icon [img]="Check" class="text-success size-3" />
                        } @else {
                          <lucide-icon [img]="Copy" class="size-3" />
                        }
                        <span class="font-mono text-xs">{{ copiedSnippet() ? 'Copied' : 'Copy Manifest' }}</span>
                      </button>
                    </div>
                    <div
                      class="border-border/80 bg-muted/60 relative overflow-hidden rounded-lg border p-3 font-mono text-xs"
                    >
                      <pre
                        class="text-foreground/90 overflow-x-auto leading-relaxed"
                      ><code><span class="text-primary font-semibold">export default</span> <span class="text-info">defineRegistryItem</span>({{'{'}}
      name: <span class="text-success">'meeting-agenda-notes'</span>,
      type: <span class="text-success">'registry:block'</span>,
      categories: [<span class="text-success">'productivity'</span>, <span class="text-success">'collaboration'</span>],
      dependencies: [<span class="text-success">'lucide-vue-next'</span>],
      registryDependencies: [
        <span class="text-warning">'https://uipkge.dev/r/avatar.json'</span>,
        <span class="text-warning">'https://uipkge.dev/r/badge.json'</span>,
        <span class="text-warning">'https://uipkge.dev/r/button.json'</span>,
        <span class="text-warning">'https://uipkge.dev/r/card.json'</span>,
        <span class="text-warning">'https://uipkge.dev/r/checkbox.json'</span>,
        <span class="text-warning">'https://uipkge.dev/r/separator.json'</span>,
      ],
      {{'}'}})</code></pre>
                    </div>
                  </div>
    
                  <div class="space-y-3 pt-2">
                    <h4 class="text-muted-foreground text-xs font-bold tracking-wider uppercase">Live Annotations</h4>
                    <div class="space-y-2.5">
                      @for (note of liveNotes(); track note.id) {
                        <div class="border-border/60 bg-muted/20 flex items-start gap-2.5 rounded-lg border p-3">
                          <ui-avatar size="sm" class="size-6 shrink-0 text-xs">
                            <ui-avatar-fallback [class]="note.authorColor">{{ note.authorInitials }}</ui-avatar-fallback>
                          </ui-avatar>
                          <div class="flex-1 space-y-1">
                            <div class="flex items-center justify-between gap-2">
                              <div class="flex items-center gap-1.5">
                                <span class="text-foreground text-xs font-semibold">{{ note.author }}</span>
                                <span ui-badge variant="outline" class="text-muted-foreground px-1 py-0 font-mono text-xs">
                                  {{ note.tag }}
                                </span>
                              </div>
                              <span class="text-muted-foreground font-mono text-xs tabular-nums">{{ note.timestamp }}</span>
                            </div>
                            <p class="text-foreground/90 text-xs leading-relaxed">{{ note.content }}</p>
                          </div>
                        </div>
                      }
                    </div>
    
                    <div class="flex flex-col items-stretch gap-2 pt-2 sm:flex-row sm:items-center">
                      <div class="relative flex-1">
                        <input
                          [value]="newNoteDraft()"
                          (input)="newNoteDraft.set($any($event.target).value)"
                          type="text"
                          placeholder="Append architecture note or observation..."
                          class="border-input bg-background text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/40 h-8 w-full rounded-md border px-3 py-1 text-xs shadow-xs outline-none focus-visible:ring-2"
                          (keydown.enter)="addNote()"
                        />
                      </div>
                      <div class="flex items-center gap-1.5">
                        <select
                          [value]="newNoteTag()"
                          (change)="newNoteTag.set($any($event.target).value)"
                          class="border-input bg-background text-foreground focus-visible:border-ring focus-visible:ring-ring/40 h-8 rounded-md border px-2 text-xs shadow-xs outline-none focus-visible:ring-2"
                        >
                          @for (opt of noteTags; track opt) {
                            <option [value]="opt">{{ opt }}</option>
                          }
                        </select>
                        <button
                          ui-button
                          size="sm"
                          variant="secondary"
                          class="h-8 gap-1 text-xs shadow-xs"
                          [disabled]="!newNoteDraft().trim()"
                          (click)="addNote()"
                        >
                          <lucide-icon [img]="Plus" class="size-3.5" />
                          Post
                        </button>
                      </div>
                    </div>
                  </div>
                </div>
              </div>
            </div>
    
            <!-- Right Sidebar: Action Items, Decisions, & Meta -->
            <div class="space-y-6 lg:col-span-5 xl:col-span-4">
              <div ui-card class="border-border bg-card shadow-xs">
                <div ui-card-header class="px-5 pt-5 pb-3">
                  <div class="flex items-center justify-between gap-2">
                    <div class="flex items-center gap-2">
                      <div class="bg-success/10 text-success flex size-7 items-center justify-center rounded-md">
                        <lucide-icon [img]="UserCheck" class="size-4" />
                      </div>
                      <div>
                        <h3 ui-card-title class="text-base font-semibold">Action Items</h3>
                        <p ui-card-description class="text-xs">Assigned deliverables & deadlines</p>
                      </div>
                    </div>
                    <span ui-badge variant="secondary" class="font-mono text-xs tabular-nums">
                      {{ completedTasksCount() }}/{{ totalTasksCount() }} Done ({{ completionPercentage() }}%)
                    </span>
                  </div>
                  <div class="bg-muted mt-3 h-1.5 w-full overflow-hidden rounded-full">
                    <div
                      class="bg-success h-full rounded-full transition-[width] duration-300"
                      [style.width]="completionPercentage() + '%'"
                    ></div>
                  </div>
                </div>
    
                <div ui-card-content class="space-y-4 p-5 pt-2">
                  <div class="space-y-2.5">
                    @for (task of actionItems(); track task.id) {
                      <div [class]="taskCardClass(task.completed)">
                        <div class="pt-0.5">
                          <ui-checkbox
                            [id]="task.id"
                            [checked]="task.completed"
                            (checkedChange)="toggleTask(task.id, !!$event)"
                          />
                        </div>
                        <div class="flex-1 space-y-1.5">
                          <label [for]="task.id" [class]="taskLabelClass(task.completed)">
                            {{ task.title }}
                          </label>
                          <div class="text-muted-foreground flex flex-wrap items-center justify-between gap-1.5 text-xs">
                            <div class="flex items-center gap-1.5">
                              <ui-avatar size="xs" class="size-4 text-xs">
                                <ui-avatar-fallback [class]="task.assigneeColor + ' text-xs'">
                                  {{ task.assigneeInitials }}
                                </ui-avatar-fallback>
                              </ui-avatar>
                              <span class="text-foreground/80 text-xs font-medium">{{ task.assignee }}</span>
                            </div>
                            <div class="flex items-center gap-1.5">
                              <span
                                ui-badge
                                [variant]="taskPriorityVariant(task.priority)"
                                class="h-4.5 px-1.5 text-xs font-semibold uppercase"
                              >
                                {{ task.priority }}
                              </span>
                              <span class="text-muted-foreground font-mono text-xs tabular-nums">{{ task.dueDate }}</span>
                            </div>
                          </div>
                        </div>
                      </div>
                    }
                  </div>
    
                  <div class="border-border/80 bg-muted/20 space-y-2 rounded-lg border border-dashed p-3">
                    <input
                      [value]="newTaskTitle()"
                      (input)="newTaskTitle.set($any($event.target).value)"
                      type="text"
                      placeholder="New action item description..."
                      class="border-input bg-background text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring h-7 w-full rounded border px-2.5 text-xs shadow-xs outline-none focus-visible:ring-1"
                      (keydown.enter)="addTask()"
                    />
                    <div class="flex items-center justify-between gap-2">
                      <select
                        [value]="newTaskAssignee()"
                        (change)="newTaskAssignee.set($any($event.target).value)"
                        class="border-input bg-background text-foreground h-7 rounded border px-2 text-xs shadow-xs outline-none"
                      >
                        @for (att of attendees(); track att.id) {
                          <option [value]="att.name">{{ att.name }}</option>
                        }
                      </select>
                      <select
                        [value]="newTaskPriority()"
                        (change)="newTaskPriority.set(priorityFromString($any($event.target).value))"
                        class="border-input bg-background text-foreground h-7 rounded border px-2 text-xs shadow-xs outline-none"
                      >
                        @for (p of priorities; track p) {
                          <option [value]="p">{{ p }}</option>
                        }
                      </select>
                      <button
                        ui-button
                        size="xs"
                        variant="default"
                        class="h-7 gap-1 text-xs"
                        [disabled]="!newTaskTitle().trim()"
                        (click)="addTask()"
                      >
                        <lucide-icon [img]="Plus" class="size-3" />
                        Add
                      </button>
                    </div>
                  </div>
                </div>
              </div>
    
              <!-- Key Decisions Log Card -->
              <div ui-card class="border-border bg-card shadow-xs">
                <div ui-card-header class="px-5 pt-5 pb-3">
                  <div class="flex items-center justify-between gap-2">
                    <div class="flex items-center gap-2">
                      <div class="bg-primary/10 text-primary flex size-7 items-center justify-center rounded-md">
                        <lucide-icon [img]="CheckCircle2" class="size-4" />
                      </div>
                      <div>
                        <h3 ui-card-title class="text-base font-semibold">Key Decisions Log</h3>
                        <p ui-card-description class="text-xs">Recorded architectural consensus</p>
                      </div>
                    </div>
                    <span ui-badge variant="outline" class="text-muted-foreground font-mono text-xs tabular-nums">
                      {{ keyDecisions().length }} Recorded
                    </span>
                  </div>
                </div>
    
                <div ui-card-content class="space-y-3 p-5 pt-2">
                  @for (decision of keyDecisions(); track decision.id) {
                    <div class="border-border/80 bg-card space-y-2 rounded-lg border p-3.5 shadow-xs">
                      <div class="flex items-start justify-between gap-2">
                        <div class="flex items-center gap-1.5">
                          <span class="text-muted-foreground font-mono text-xs font-bold"> D-0{{ decision.number }} </span>
                          <span
                            ui-badge
                            [variant]="decision.status === 'Adopted' ? 'success' : 'warning'"
                            class="h-4.5 px-1.5 text-xs font-semibold uppercase"
                          >
                            {{ decision.status }}
                          </span>
                        </div>
                        <span class="text-muted-foreground font-mono text-xs tabular-nums">{{ decision.time }}</span>
                      </div>
                      <h4 class="text-foreground text-xs leading-snug font-semibold">{{ decision.title }}</h4>
                      <p class="text-muted-foreground text-xs leading-relaxed">{{ decision.context }}</p>
                      <div class="text-muted-foreground border-border/40 flex items-center gap-1 border-t pt-1 text-xs">
                        <span>Decided by:</span>
                        <span class="text-foreground font-medium">{{ decision.decidedBy }}</span>
                      </div>
                    </div>
                  }
                </div>
              </div>
    
              <!-- Meeting Metadata & Shared Resources Card -->
              <div ui-card class="border-border bg-card shadow-xs">
                <div ui-card-header class="px-5 pt-4 pb-2">
                  <h3 ui-card-title class="text-muted-foreground text-xs font-bold tracking-wider uppercase">
                    Session Resources & Security
                  </h3>
                </div>
                <div ui-card-content class="space-y-3 p-5 pt-1">
                  <div class="space-y-2 text-xs">
                    <div class="text-muted-foreground flex items-center justify-between">
                      <span class="flex items-center gap-1.5">
                        <lucide-icon [img]="Lock" class="text-foreground/70 size-3.5" />
                        Access Level
                      </span>
                      <span class="text-foreground font-medium">Internal (SSO Auth)</span>
                    </div>
                    <div class="text-muted-foreground flex items-center justify-between">
                      <span class="flex items-center gap-1.5">
                        <lucide-icon [img]="Calendar" class="text-foreground/70 size-3.5" />
                        Next Sync
                      </span>
                      <span class="text-foreground font-mono font-medium tabular-nums">Aug 28 · 14:00 PST</span>
                    </div>
                  </div>
                  <div ui-separator class="bg-border/60"></div>
                  <div class="space-y-1.5">
                    <span class="text-muted-foreground text-xs font-semibold">Attached Artifacts</span>
                    <div class="space-y-1">
                      <a
                        href="#artifacts"
                        class="text-foreground hover:bg-muted group flex items-center justify-between rounded-md p-1.5 text-xs transition-colors"
                      >
                        <span class="flex items-center gap-2">
                          <lucide-icon [img]="FileSpreadsheet" class="text-success size-3.5" />
                          <span>OKLCH Token Audit Matrix.xlsx</span>
                        </span>
                        <lucide-icon
                          [img]="ExternalLink"
                          class="text-muted-foreground group-hover:text-foreground size-3 transition-colors"
                        />
                      </a>
                      <a
                        href="#artifacts"
                        class="text-foreground hover:bg-muted group flex items-center justify-between rounded-md p-1.5 text-xs transition-colors"
                      >
                        <span class="flex items-center gap-2">
                          <lucide-icon [img]="GitPullRequest" class="text-chart-2 size-3.5" />
                          <span>PR #412: Flat Dependency Resolution</span>
                        </span>
                        <lucide-icon
                          [img]="ExternalLink"
                          class="text-muted-foreground group-hover:text-foreground size-3 transition-colors"
                        />
                      </a>
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      `,
    })
    export class UiMeetingAgendaNotesComponent {
      protected readonly Calendar = Calendar
      protected readonly Check = Check
      protected readonly CheckCircle2 = CheckCircle2
      protected readonly Clock = Clock
      protected readonly Copy = Copy
      protected readonly ExternalLink = ExternalLink
      protected readonly FileDown = FileDown
      protected readonly FileSpreadsheet = FileSpreadsheet
      protected readonly FileText = FileText
      protected readonly GitPullRequest = GitPullRequest
      protected readonly ListOrdered = ListOrdered
      protected readonly Lock = Lock
      protected readonly Plus = Plus
      protected readonly Radio = Radio
      protected readonly UserCheck = UserCheck
      protected readonly Video = Video
    
      protected readonly noteTags = noteTagOptions
      protected readonly priorities = priorityOptions
    
      @Input('class') className?: string
    
      readonly copiedRoomLink = signal(false)
      readonly copiedNotes = signal(false)
      readonly copiedSnippet = signal(false)
    
      readonly attendees = signal<Attendee[]>([
        {
          id: 'att-1',
          name: 'Elena Rostova',
          role: 'Lead Architect',
          initials: 'ER',
          isHost: true,
          status: 'online',
          colorClass: 'bg-primary/15 text-primary font-semibold',
        },
        {
          id: 'att-2',
          name: 'Marcus Vance',
          role: 'Staff Engineer',
          initials: 'MV',
          status: 'online',
          colorClass: 'bg-info/15 text-info font-semibold',
        },
        {
          id: 'att-3',
          name: 'Sarah Jenkins',
          role: 'Product Lead',
          initials: 'SJ',
          status: 'online',
          colorClass: 'bg-chart-1/15 text-chart-1 font-semibold',
        },
        {
          id: 'att-4',
          name: 'David Chen',
          role: 'Core Infra',
          initials: 'DC',
          status: 'online',
          colorClass: 'bg-warning/15 text-warning font-semibold',
        },
        {
          id: 'att-5',
          name: 'Sofia Rossi',
          role: 'Design Systems',
          initials: 'SR',
          status: 'online',
          colorClass: 'bg-destructive/15 text-destructive font-semibold',
        },
        {
          id: 'att-6',
          name: 'Alex Rivera',
          role: 'DevRel',
          initials: 'AR',
          status: 'online',
          colorClass: 'bg-chart-2/15 text-chart-2 font-semibold',
        },
      ])
    
      readonly agendaTopics = signal<AgendaTopic[]>([
        {
          id: 'topic-1',
          number: 1,
          title: 'OKLCH Token Migration Status',
          duration: '15m',
          timeRange: '14:00 - 14:15',
          lead: 'Elena Rostova',
          leadInitials: 'ER',
          status: 'completed',
          summary:
            'Audit of 140+ CSS custom properties completed. Confirmed wide-gamut Display P3 color spaces with zero degradation in legacy sRGB browsers.',
          tags: ['Design System', 'Tailwind v4'],
        },
        {
          id: 'topic-2',
          number: 2,
          title: 'Dual-Framework AST Verification Pipeline',
          duration: '20m',
          timeRange: '14:15 - 14:35',
          lead: 'Marcus Vance',
          leadInitials: 'MV',
          status: 'active',
          summary:
            'Live AST comparisons between Vue SFC templates and React TSX JSX trees. Validating component prop parity, variant definitions, and event handlers automatically in CI.',
          tags: ['Compiler', 'CI/CD', 'Parity'],
        },
        {
          id: 'topic-3',
          number: 3,
          title: '100-Block Registry Expansion Strategy',
          duration: '15m',
          timeRange: '14:35 - 14:50',
          lead: 'Sarah Jenkins',
          leadInitials: 'SJ',
          status: 'upcoming',
          summary:
            'Prioritization matrix for upcoming vertical blocks: Healthcare appointment management, HRMS payroll ledger, and DevTools query workbench.',
          tags: ['Roadmap', 'Blocks'],
        },
        {
          id: 'topic-4',
          number: 4,
          title: 'Q&A & Retrospective',
          duration: '10m',
          timeRange: '14:50 - 15:00',
          lead: 'Open Floor',
          leadInitials: 'OF',
          status: 'upcoming',
          summary:
            'Consensus check on flat URL dependency format, open architectural blockers, and sprint task assignment review.',
          tags: ['Open Floor', 'Review'],
        },
      ])
    
      readonly liveNotes = signal<NoteEntry[]>([
        {
          id: 'note-1',
          author: 'Marcus Vance',
          authorInitials: 'MV',
          authorColor: 'bg-info/15 text-info',
          timestamp: '14:22 PST',
          content:
            'The AST parity checker runs under 340ms locally with caching. We will add a strict blocking threshold in GitHub Actions.',
          tag: '#pipeline',
        },
        {
          id: 'note-2',
          author: 'Sofia Rossi',
          authorInitials: 'SR',
          authorColor: 'bg-destructive/15 text-destructive',
          timestamp: '14:31 PST',
          content:
            'All color tokens now satisfy WCAG AAA for text-foreground and AA for muted-foreground across dark surfaces.',
          tag: '#design-tokens',
        },
      ])
    
      readonly newNoteDraft = signal('')
      readonly newNoteTag = signal('#architecture')
    
      readonly actionItems = signal<ActionItem[]>([
        {
          id: 'task-1',
          title: 'Publish OKLCH color token specification to documentation',
          assignee: 'Elena Rostova',
          assigneeInitials: 'ER',
          assigneeColor: 'bg-success/15 text-success',
          dueDate: 'Aug 24, 2026',
          priority: 'High',
          completed: true,
        },
        {
          id: 'task-2',
          title: 'Configure AST parity tests in GitHub Actions CI workflow',
          assignee: 'Marcus Vance',
          assigneeInitials: 'MV',
          assigneeColor: 'bg-info/15 text-info',
          dueDate: 'Aug 26, 2026',
          priority: 'Urgent',
          completed: false,
        },
        {
          id: 'task-3',
          title: 'Draft block wireframes for Healthcare & HRMS verticals',
          assignee: 'Sarah Jenkins',
          assigneeInitials: 'SJ',
          assigneeColor: 'bg-chart-1/15 text-chart-1',
          dueDate: 'Aug 28, 2026',
          priority: 'Medium',
          completed: false,
        },
        {
          id: 'task-4',
          title: 'Benchmark bundle size delta across 50+ imported primitives',
          assignee: 'David Chen',
          assigneeInitials: 'DC',
          assigneeColor: 'bg-warning/15 text-warning',
          dueDate: 'Sep 02, 2026',
          priority: 'Low',
          completed: false,
        },
      ])
    
      readonly newTaskTitle = signal('')
      readonly newTaskAssignee = signal('Alex Rivera')
      readonly newTaskPriority = signal<ActionItem['priority']>('Medium')
    
      readonly keyDecisions = signal<KeyDecision[]>([
        {
          id: 'dec-1',
          number: 1,
          title: 'Adopt flat registryDependencies URLs across all 300+ items',
          context:
            'Declaring flat JSON URLs eliminates circular dependency warnings and simplifies downstream registry resolver graphs.',
          status: 'Adopted',
          decidedBy: 'Elena Rostova',
          time: '14:18 PST',
        },
        {
          id: 'dec-2',
          number: 2,
          title: 'Enforce strict 120-char line width & no-semicolons styleguide',
          context:
            'Standardizes AST transpilation outputs so generated Vue SFC and React TSX items maintain identical formatting invariants.',
          status: 'Adopted',
          decidedBy: 'Squad Consensus',
          time: '14:32 PST',
        },
        {
          id: 'dec-3',
          number: 3,
          title: 'Deprecate legacy /r/nuxt/ aliases by end of Q3',
          context:
            'Consumers will be directed to unified /r/vue/ endpoints while maintaining backwards-compatible 301 redirects.',
          status: 'Under Review',
          decidedBy: 'Sarah Jenkins',
          time: '14:45 PST',
        },
      ])
    
      readonly completedTasksCount = computed(() => this.actionItems().filter((t) => t.completed).length)
      readonly totalTasksCount = computed(() => this.actionItems().length)
      readonly completionPercentage = computed(() =>
        this.totalTasksCount() === 0 ? 0 : Math.round((this.completedTasksCount() / this.totalTasksCount()) * 100),
      )
    
      get rootClass(): string {
        return cn('text-foreground w-full space-y-6', this.className)
      }
    
      topicCardClass(status: AgendaTopic['status']): string {
        return cn(
          'group relative rounded-lg border p-4 transition-colors duration-150',
          status === 'active' && 'border-primary/50 bg-primary/[0.03] shadow-xs',
          status === 'completed' && 'border-border/80 bg-muted/20',
          status === 'upcoming' && 'border-border/60 bg-card hover:border-border',
        )
      }
    
      taskCardClass(completed: boolean): string {
        return cn(
          'group flex items-start gap-3 rounded-lg border p-3 transition-colors',
          completed
            ? 'border-border/40 bg-muted/20 opacity-75'
            : 'border-border/80 bg-card hover:border-border hover:bg-muted/10',
        )
      }
    
      taskLabelClass(completed: boolean): string {
        return cn(
          'block cursor-pointer text-xs leading-relaxed font-medium transition-colors select-none',
          completed ? 'text-muted-foreground line-through' : 'text-foreground',
        )
      }
    
      taskPriorityVariant(p: ActionItem['priority']): 'destructive' | 'warning' | 'secondary' {
        if (p === 'Urgent' || p === 'High') return 'destructive'
        if (p === 'Medium') return 'warning'
        return 'secondary'
      }
    
      priorityFromString(v: string): ActionItem['priority'] {
        return (priorityOptions as string[]).includes(v) ? (v as ActionItem['priority']) : 'Medium'
      }
    
      flash(signalRef: { set: (v: boolean) => void }): void {
        signalRef.set(true)
        setTimeout(() => signalRef.set(false), 2000)
      }
    
      copyRoomLink(): void {
        try {
          navigator.clipboard?.writeText('https://zoom.us/j/4829017734')
        } catch {
          // Clipboard fallback
        }
        this.flash(this.copiedRoomLink)
      }
    
      copyNotesMarkdown(): void {
        try {
          navigator.clipboard?.writeText(this.notesMarkdown())
        } catch {
          // Clipboard fallback
        }
        this.flash(this.copiedNotes)
      }
    
      exportNotesMarkdown(): void {
        this.copyNotesMarkdown()
      }
    
      copyCodeSnippet(): void {
        try {
          navigator.clipboard?.writeText(
            `export default defineRegistryItem({\n  name: 'meeting-agenda-notes',\n  type: 'registry:block',\n  categories: ['productivity', 'collaboration'],\n  dependencies: ['lucide-vue-next'],\n  registryDependencies: [\n    'https://uipkge.dev/r/avatar.json',\n    'https://uipkge.dev/r/badge.json',\n    'https://uipkge.dev/r/button.json',\n    'https://uipkge.dev/r/card.json',\n    'https://uipkge.dev/r/checkbox.json',\n    'https://uipkge.dev/r/separator.json',\n  ],\n})`,
          )
        } catch {
          // Clipboard fallback
        }
        this.flash(this.copiedSnippet)
      }
    
      addNote(): void {
        if (!this.newNoteDraft().trim()) return
        this.liveNotes.update((prev) => [
          ...prev,
          {
            id: `note-${Date.now()}`,
            author: 'Elena Rostova',
            authorInitials: 'ER',
            authorColor: 'bg-success/15 text-success',
            timestamp: 'Just now',
            content: this.newNoteDraft().trim(),
            tag: this.newNoteTag(),
          },
        ])
        this.newNoteDraft.set('')
      }
    
      toggleTask(id: string, checked: boolean): void {
        this.actionItems.update((prev) => prev.map((t) => (t.id === id ? { ...t, completed: checked } : t)))
      }
    
      addTask(): void {
        if (!this.newTaskTitle().trim()) return
        const attendee = this.attendees().find((a) => a.name === this.newTaskAssignee()) ?? this.attendees()[0]!
        this.actionItems.update((prev) => [
          ...prev,
          {
            id: `task-${Date.now()}`,
            title: this.newTaskTitle().trim(),
            assignee: attendee.name,
            assigneeInitials: attendee.initials,
            assigneeColor: attendee.colorClass,
            dueDate: 'Aug 30, 2026',
            priority: this.newTaskPriority(),
            completed: false,
          },
        ])
        this.newTaskTitle.set('')
      }
    
      notesMarkdown(): string {
        return `# Q3 Product Architecture & Monorepo Scaling Sync
    **Date:** Friday, Aug 21, 2026 · 14:00 - 15:00 PST
    **Attendees:** Elena Rostova (Host), Marcus Vance, Sarah Jenkins, David Chen, Sofia Rossi, Alex Rivera
    
    ## Timeboxed Agenda
    ${this.agendaTopics()
      .map((t) => `- [${t.status === 'completed' ? 'x' : ' '}] **${t.title}** (${t.duration} · ${t.timeRange}) - ${t.lead}`)
      .join('\n')}
    
    ## Key Architectural Decisions
    ${this.keyDecisions()
      .map((d) => `${d.number}. **${d.title}** [${d.status}]`)
      .join('\n')}
    
    ## Action Items
    ${this.actionItems()
      .map(
        (task) =>
          `- [${task.completed ? 'x' : ' '}] **${task.title}** (Assignee: @${task.assignee}, Due: ${task.dueDate}, Priority: ${task.priority})`,
      )
      .join('\n')}
    `
      }
    }
    
  • src/app/components/blocks/meeting-agenda-notes/index.ts0.2 kB

Raw manifest:https://uipkge.dev/r/angular/meeting-agenda-notes.json