Meeting Agenda Notes
blockproductivityLinear and Notion style structured meeting document with attendee avatars, timeboxed agenda topics, rich discussion notes, interactive action items checklist, and key decisions log.
Installation
$pnpm dlx uipkge-ng@latest add meeting-agenda-notes$npx uipkge-ng@latest add meeting-agenda-notes$yarn dlx uipkge-ng@latest add meeting-agenda-notes$bunx uipkge-ng@latest add meeting-agenda-notesManifest:
https://uipkge.dev/r/angular/meeting-agenda-notes.jsonInstalls to:src/app/components/blocks/meeting-agenda-notes/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
Attendeeinterface Attendee {
id: string
name: string
role: string
initials: string
isHost?: boolean
status: 'online' | 'busy' | 'away'
colorClass: string
}AgendaTopicinterface AgendaTopic {
id: string
number: number
title: string
duration: string
timeRange: string
lead: string
leadInitials: string
status: 'completed' | 'active' | 'upcoming'
summary: string
tags: string[]
}NoteEntryinterface NoteEntry {
id: string
author: string
authorInitials: string
authorColor: string
timestamp: string
content: string
tag: string
}ActionIteminterface ActionItem {
id: string
title: string
assignee: string
assigneeInitials: string
assigneeColor: string
dueDate: string
priority: 'Urgent' | 'High' | 'Medium' | 'Low'
completed: boolean
}KeyDecisioninterface 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 <url></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">@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
export { UiMeetingAgendaNotesComponent, type Attendee, type AgendaTopic, type NoteEntry, type ActionItem, type KeyDecision, } from './meeting-agenda-notes.component'
Raw manifest:https://uipkge.dev/r/angular/meeting-agenda-notes.json