Email Campaign Composer
blockmarketingHigh-craft marketing email broadcast designer, audience segment selector, AI subject line optimizer, A/B testing split, live multi-device newsletter preview, and deliverability scorecard.
Installation
$pnpm dlx uipkge-ng@latest add email-campaign-composer$npx uipkge-ng@latest add email-campaign-composer$yarn dlx uipkge-ng@latest add email-campaign-composer$bunx uipkge-ng@latest add email-campaign-composerManifest:
https://uipkge.dev/r/angular/email-campaign-composer.jsonInstalls to:src/app/components/blocks/email-campaign-composer/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.
AudienceSegmentinterface AudienceSegment {
id: string
label: string
count: number
formattedCount: string
openRate: string
clickRate: string
deliverability: string
}Files installed (2)
src/app/components/blocks/email-campaign-composer/email-campaign-composer.component.ts47.7 kB
// High-craft marketing email broadcast designer, audience segment selector, AI subject line // optimizer, A/B testing split, live multi-device newsletter preview, and deliverability // scorecard. Port of the React block 1:1. import { ChangeDetectionStrategy, Component, DestroyRef, Input, computed, inject, signal } from '@angular/core' import { ArrowRight, Calendar, Check, CheckCircle2, Eye, Flame, Inbox, Lock, LucideAngularModule, Mail, Monitor, Send, ShieldCheck, Smartphone, Sparkles, Split, Terminal, UserCheck, Users, Zap, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiDialogComponent, UiDialogContentComponent, UiDialogDescriptionComponent, UiDialogFooterComponent, UiDialogHeaderComponent, UiDialogTitleComponent, } from '@/ui/dialog/dialog.component' import { UiInputComponent } from '@/ui/input/input.component' import { UiSelectComponent, UiSelectContentComponent, UiSelectItemComponent, UiSelectTriggerComponent, UiSelectValueComponent, } from '@/ui/select/select.component' import { UiSeparatorComponent } from '@/ui/separator/separator.component' import { UiSwitchComponent } from '@/ui/switch/switch.component' export type CampaignViewport = 'desktop' | 'mobile' export type CampaignClient = 'all' | 'gmail' | 'apple' | 'outlook' export type ScheduleTiming = 'optimal' | 'immediate' interface AudienceSegment { id: string label: string count: number formattedCount: string openRate: string clickRate: string deliverability: string } const AUDIENCE_OPTIONS: AudienceSegment[] = [ { id: 'active-devs', label: 'All Active Developers (42,500 subscribers)', count: 42500, formattedCount: '42.5k', openRate: '38.4%', clickRate: '12.2%', deliverability: '99.9%', }, { id: 'tech-leads', label: 'Enterprise Tech Leads (8,200 subscribers)', count: 8200, formattedCount: '8.2k', openRate: '46.1%', clickRate: '18.5%', deliverability: '99.8%', }, { id: 'free-users', label: 'Free Plan Users (28,000 subscribers)', count: 28000, formattedCount: '28.0k', openRate: '31.2%', clickRate: '8.9%', deliverability: '99.7%', }, ] const AI_SUGGESTIONS = [ { text: '🚀 Introducing UIPKGE 2.0: The unbundled UI registry', score: 94, tag: 'High Open Rate Impact' }, { text: 'Ship polished UIs 10x faster with UIPKGE 2.0 ⚡', score: 92, tag: 'Strong Action Verb' }, { text: 'Zero npm dependencies: How UIPKGE 2.0 changes frontend dev', score: 89, tag: 'Curiosity Spike' }, ] @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-email-campaign-composer, [ui-email-campaign-composer]', standalone: true, // React renders the root <div> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, UiDialogComponent, UiDialogContentComponent, UiDialogDescriptionComponent, UiDialogFooterComponent, UiDialogHeaderComponent, UiDialogTitleComponent, UiInputComponent, UiSelectComponent, UiSelectContentComponent, UiSelectItemComponent, UiSelectTriggerComponent, UiSelectValueComponent, UiSeparatorComponent, UiSwitchComponent, ], template: ` <div data-slot="email-campaign-composer" [class]="rootClass"> <!-- Top Header Bar --> <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-header class="pb-4"> <div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between"> <div class="space-y-1.5"> <div class="flex flex-wrap items-center gap-2.5"> <div class="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg shadow-xs"> <lucide-icon [img]="Mail" class="size-4" /> </div> <h2 class="text-foreground text-lg font-semibold tracking-tight sm:text-xl">{{ campaignName }}</h2> <span ui-badge variant="outline" class="border-info/30 bg-info/10 text-info gap-1.5 px-2.5 py-0.5 text-xs font-medium" > <span class="bg-info size-1.5 rounded-full"></span> <span>Draft Saved · {{ currentAudience().formattedCount }} Recipients</span> </span> </div> <p class="text-muted-foreground text-xs"> Configure campaign targeting, optimize open rates with AI scoring, and preview live multi-device inbox rendering. </p> </div> <div class="flex flex-wrap items-center gap-2.5"> <button ui-button variant="outline" size="sm" class="gap-1.5 text-xs font-medium" (click)="isTestModalOpen.set(true)" > <lucide-icon [img]="Mail" class="text-muted-foreground size-3.5" /> <span>Send Test Proof</span> </button> <button ui-button size="sm" class="gap-1.5 text-xs font-medium shadow-xs" (click)="isScheduleModalOpen.set(true)" > <lucide-icon [img]="Send" class="size-3.5 fill-current" /> <span>Schedule Broadcast</span> </button> </div> </div> </div> </div> <!-- 2-Column Campaign Studio Grid --> <div class="grid grid-cols-1 items-start gap-6 lg:grid-cols-12"> <!-- LEFT PANEL: Campaign Setup & Audience --> <div class="space-y-5 lg:col-span-5"> <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-header class="pb-3"> <div class="flex flex-wrap items-center justify-between"> <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]="Users" class="size-3.5" /> </div> <h3 ui-card-title class="text-sm font-semibold tracking-tight">Audience Segment</h3> </div> <span ui-badge variant="secondary" class="h-5 gap-1 px-2 text-xs font-normal"> <lucide-icon [img]="UserCheck" class="text-success size-3" /> Verified List </span> </div> <p ui-card-description class="text-xs"> Select subscriber segment to calculate deliverability pacing and personalization tags. </p> </div> <div ui-card-content class="space-y-3.5 pt-0"> <div class="space-y-1.5"> <label class="text-foreground text-xs font-medium">Target Segment</label> <ui-select [value]="selectedAudience()" (valueChange)="selectedAudience.set($event)"> <button ui-select-trigger class="h-9 w-full text-xs font-medium [&>span]:truncate [&>svg]:shrink-0" aria-label="Select Audience Segment" > <ui-select-value placeholder="Select audience segment" /> </button> <ui-select-content> @for (aud of audienceOptions; track aud.id) { <ui-select-item [value]="aud.id"> <span>{{ aud.label }}</span> </ui-select-item> } </ui-select-content> </ui-select> </div> <div class="grid grid-cols-3 gap-2 pt-1"> <div class="border-border bg-muted/40 rounded-lg border p-2.5 text-center"> <div class="text-muted-foreground text-xs font-medium">Recipients</div> <div class="text-foreground mt-0.5 text-sm font-bold tracking-tight"> {{ currentAudience().formattedCount }} </div> </div> <div class="border-border bg-muted/40 rounded-lg border p-2.5 text-center"> <div class="text-muted-foreground text-xs font-medium">Avg Open Rate</div> <div class="text-success mt-0.5 text-sm font-bold tracking-tight"> {{ currentAudience().openRate }} </div> </div> <div class="border-border bg-muted/40 rounded-lg border p-2.5 text-center"> <div class="text-muted-foreground text-xs font-medium">Avg CTR</div> <div class="text-info mt-0.5 text-sm font-bold tracking-tight"> {{ currentAudience().clickRate }} </div> </div> </div> </div> </div> <!-- Subject Line & AI Optimizer Card --> <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-header class="pb-3"> <div class="flex flex-wrap items-center justify-between"> <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]="Sparkles" class="size-3.5" /> </div> <h3 ui-card-title class="text-sm font-semibold tracking-tight">Subject Line & AI Scorer</h3> </div> <span ui-badge variant="outline" class="border-success/30 bg-success/10 text-success gap-1 px-2 text-xs font-medium" > <lucide-icon [img]="Sparkles" class="text-success size-3" /> <span>94 / 100 · High Open Rate Impact</span> </span> </div> <p ui-card-description class="text-xs"> AI predictive heuristic analyzing urgency, sentiment, character count, and spam filter risk. </p> </div> <div ui-card-content class="space-y-4 pt-0"> <div class="border-border bg-muted/30 flex flex-wrap items-center justify-between rounded-lg border p-3"> <div class="space-y-0.5"> <div class="text-foreground flex items-center gap-1.5 text-xs font-medium"> <lucide-icon [img]="Split" class="text-primary size-3.5" /> <span>A/B Subject Variant Testing</span> </div> <p class="text-muted-foreground text-xs">Test 2 subject lines with 50/50 subscriber split</p> </div> <button ui-switch id="ng-ab-toggle" [checked]="isAbTestEnabled()" (checkedChange)="isAbTestEnabled.set($event)" ></button> </div> <div class="space-y-1.5"> <div class="flex flex-wrap items-center justify-between text-xs"> <label class="text-foreground font-medium"> Subject Line {{ isAbTestEnabled() ? '(Variant A · 50%)' : '' }} </label> <div class="flex items-center gap-1.5"> <span class="text-muted-foreground font-mono text-xs">{{ subjectA().length }}/60 chars</span> <span ui-badge variant="outline" class="text-muted-foreground h-4.5 gap-1 px-1.5 text-xs font-normal" > <lucide-icon [img]="Flame" class="text-warning size-2.5" /> <span>{{ emojiCount() }} emoji</span> </span> </div> </div> <ui-input [value]="subjectA()" (valueChange)="subjectA.set($event)" class="text-xs" placeholder="Enter campaign subject line..." /> </div> @if (isAbTestEnabled()) { <div class="space-y-1.5"> <div class="flex flex-wrap items-center justify-between text-xs"> <label class="text-foreground font-medium">Subject Line (Variant B · 50%)</label> <span class="text-muted-foreground font-mono text-xs">{{ subjectB().length }}/60 chars</span> </div> <ui-input [value]="subjectB()" (valueChange)="subjectB.set($event)" class="text-xs" placeholder="Enter alternate subject line B..." /> <div class="flex items-center gap-1 pt-1"> <button type="button" ui-button size="xs" [variant]="activePreviewVariant() === 'A' ? 'default' : 'outline'" class="text-xs font-medium" (click)="activePreviewVariant.set('A')" > Preview Variant A </button> <button type="button" ui-button size="xs" [variant]="activePreviewVariant() === 'B' ? 'default' : 'outline'" class="text-xs font-medium" (click)="activePreviewVariant.set('B')" > Preview Variant B </button> </div> </div> } <div class="border-border bg-muted/20 space-y-2 rounded-lg border p-3 text-xs"> <div class="flex flex-wrap items-center justify-between"> <span class="text-muted-foreground flex items-center gap-1.5"> <lucide-icon [img]="Check" class="text-success size-3" /> Sentiment Tone </span> <span class="text-foreground font-medium">Positive (98%)</span> </div> <div class="flex flex-wrap items-center justify-between"> <span class="text-muted-foreground flex items-center gap-1.5"> <lucide-icon [img]="Check" class="text-success size-3" /> Urgency & Relevance </span> <span class="text-foreground font-medium">High (91%)</span> </div> <div class="flex flex-wrap items-center justify-between"> <span class="text-muted-foreground flex items-center gap-1.5"> <lucide-icon [img]="Check" class="text-success size-3" /> Spam Keyword Risk </span> <span class="text-success font-medium">Very Low (0.1%)</span> </div> </div> <div class="space-y-2"> <span class="text-muted-foreground flex items-center gap-1 text-xs font-medium"> <lucide-icon [img]="Sparkles" class="text-primary size-3" /> AI Optimization Suggestions (Click to apply): </span> <div class="space-y-1.5"> @for (suggestion of aiSuggestions; track suggestion.text) { <button type="button" class="border-border hover:border-primary/40 hover:bg-muted/50 focus-visible:ring-ring flex w-full items-center justify-between rounded-md border p-2 text-left text-xs transition-colors outline-none focus-visible:ring-2" (click)="applyAiSubject(suggestion.text)" > <span class="text-foreground truncate font-medium">{{ suggestion.text }}</span> <span ui-badge variant="secondary" class="text-success ml-2 shrink-0 font-mono text-xs"> {{ suggestion.score }}/100 </span> </button> } </div> </div> <div ui-separator></div> <div class="space-y-1.5"> <div class="flex flex-wrap items-center justify-between text-xs"> <label class="text-foreground font-medium">Preview Text / Preheader</label> <span class="text-muted-foreground font-mono text-xs">{{ previewText().length }}/90 chars</span> </div> <ui-input [value]="previewText()" (valueChange)="previewText.set($event)" class="text-xs" placeholder="Secondary snippet displayed in subscriber inbox lists..." /> </div> <div ui-separator></div> <div class="space-y-3"> <label class="text-foreground text-xs font-medium">Sender Identity</label> <div class="grid grid-cols-1 gap-2.5 sm:grid-cols-2"> <div class="space-y-1"> <span class="text-muted-foreground text-xs">Sender Display Name</span> <ui-input [value]="senderName" class="text-xs" placeholder="UIPKGE Team" /> </div> <div class="space-y-1"> <span class="text-muted-foreground text-xs">From Address</span> <ui-input [value]="senderEmail" class="text-xs" placeholder="updates@uipkge.dev" /> </div> </div> <div class="border-success/20 bg-success/5 text-success flex flex-wrap items-center justify-between rounded-md border px-2.5 py-1.5 text-xs" > <div class="flex items-center gap-1.5"> <lucide-icon [img]="ShieldCheck" class="size-3.5 shrink-0" /> <span>Domain verified: <strong>mail.uipkge.dev</strong> (SPF & DKIM valid)</span> </div> <span ui-badge variant="outline" class="border-success/30 text-success h-4.5 px-1.5 text-xs"> TLS 1.3 </span> </div> </div> </div> </div> </div> <!-- RIGHT PANEL: Live Multi-Device Email Preview --> <div class="space-y-5 lg:col-span-7"> <div ui-card class="border-border bg-card overflow-hidden shadow-xs"> <div ui-card-header class="border-border bg-muted/20 border-b pb-3"> <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div class="flex items-center gap-2"> <lucide-icon [img]="Eye" class="text-primary size-4" /> <h3 ui-card-title class="text-sm font-semibold tracking-tight">Live Newsletter Preview</h3> @if (isAbTestEnabled()) { <span ui-badge variant="secondary" class="text-xs font-normal"> Showing Variant {{ activePreviewVariant() }} </span> } </div> <div class="bg-muted border-border inline-flex items-center rounded-lg border p-1 shadow-xs"> <button type="button" ui-button size="xs" [variant]="viewport() === 'desktop' ? 'default' : 'ghost'" class="gap-1.5 text-xs font-medium" (click)="viewport.set('desktop')" > <lucide-icon [img]="Monitor" class="size-3.5" /> <span>Desktop 600px</span> </button> <button type="button" ui-button size="xs" [variant]="viewport() === 'mobile' ? 'default' : 'ghost'" class="gap-1.5 text-xs font-medium" (click)="viewport.set('mobile')" > <lucide-icon [img]="Smartphone" class="size-3.5" /> <span>Mobile 375px</span> </button> </div> </div> </div> <div ui-card-content class="bg-muted/30 flex min-h-[580px] flex-col items-center justify-start p-4 sm:p-6"> <div [class]="previewCanvasClass"> <div class="border-border bg-card overflow-hidden rounded-xl border shadow-sm"> <div class="border-border bg-muted/60 flex flex-wrap items-center justify-between border-b px-4 py-2.5" > <div class="flex items-center gap-1.5"> <div class="bg-destructive/80 size-2.5 rounded-full"></div> <div class="bg-warning/80 size-2.5 rounded-full"></div> <div class="bg-success/80 size-2.5 rounded-full"></div> </div> <div class="text-muted-foreground flex items-center gap-1.5 font-mono text-xs"> <lucide-icon [img]="Inbox" class="size-3" /> <span>UIPKGE Preview · {{ viewport() === 'desktop' ? '600px' : '375px' }}</span> </div> <span ui-badge variant="outline" class="border-border text-muted-foreground h-4.5 gap-1 px-1.5 text-xs font-normal" > <lucide-icon [img]="Lock" class="text-success size-2.5" /> TLS 1.3 </span> </div> <div class="border-border bg-card/70 space-y-2 border-b p-4 text-xs"> <div class="flex flex-wrap items-center justify-between"> <div class="flex items-center gap-2"> <div class="bg-primary text-primary-foreground flex size-7 items-center justify-center rounded-full font-bold" > U </div> <div> <div class="flex items-center gap-1.5"> <span class="text-foreground font-semibold">{{ senderName }}</span> <span class="text-muted-foreground"><{{ senderEmail }}></span> </div> <div class="text-muted-foreground text-xs">To: alex.developer@enterprise.com</div> </div> </div> <span class="text-muted-foreground text-xs">Aug 21, 2026</span> </div> <div class="space-y-0.5 pt-1"> <div class="text-foreground text-sm font-semibold tracking-tight"> {{ currentActiveSubject() }} </div> <div class="text-muted-foreground line-clamp-1 text-xs">{{ previewText() }}</div> </div> </div> <div class="bg-card space-y-5 p-5 sm:p-7"> <div class="border-border flex flex-wrap items-center justify-between border-b pb-3.5"> <div class="flex items-center gap-2"> <div class="bg-primary text-primary-foreground flex size-6 items-center justify-center rounded-md" > <lucide-icon [img]="Zap" class="size-3.5 fill-current" /> </div> <span class="text-foreground text-sm font-bold tracking-tight">UIPKGE</span> </div> <span ui-badge variant="secondary" class="text-xs font-medium">Changelog #24 · Release 2.0</span> </div> <div class="space-y-2.5"> <span ui-badge variant="outline" class="text-primary border-primary/30 text-xs font-semibold tracking-wider uppercase" > Major Product Release </span> <h3 class="text-foreground text-xl leading-tight font-bold tracking-tight sm:text-2xl"> Ship polished UIs in minutes </h3> <p class="text-muted-foreground text-xs leading-relaxed sm:text-sm"> We completely rebuilt the registry engine. 370+ production components with zero npm dependencies, 100% code ownership, and full native support for Vue 3 and React with modern OKLCH tokens. </p> </div> <div class="border-border bg-muted/40 overflow-hidden rounded-lg border"> <div class="border-border bg-muted/60 flex flex-wrap items-center justify-between border-b px-3.5 py-2" > <div class="text-muted-foreground flex items-center gap-1.5 font-mono text-xs"> <lucide-icon [img]="Terminal" class="text-primary size-3" /> <span>components/ui/button.tsx</span> </div> <span class="text-success font-mono text-xs font-medium">TypeScript</span> </div> <div class="space-y-3 p-3.5"> <div class="border-border bg-background text-muted-foreground rounded-md border p-2.5 font-mono text-xs" > <span class="text-primary font-semibold">$</span> npx shadcn add @uipkge/button </div> <div class="bg-card border-border flex flex-wrap items-center justify-center gap-2 rounded-md border p-3" > <button ui-button size="xs" variant="default">Primary</button> <button ui-button size="xs" variant="secondary">Secondary</button> <button ui-button size="xs" variant="outline">Outline</button> <button ui-button size="xs" variant="destructive">Destructive</button> </div> <div class="flex flex-wrap items-center justify-center gap-1.5 pt-1"> <span ui-badge variant="secondary" class="text-xs font-normal">⚡ Zero npm dependencies</span> <span ui-badge variant="secondary" class="text-xs font-normal">🎨 OKLCH Dark Mode</span> <span ui-badge variant="secondary" class="text-xs font-normal">🧩 Vue 3 + React Parity</span> </div> </div> </div> <div class="flex flex-col items-center justify-center space-y-2 pt-1 text-center"> <button ui-button size="lg" class="w-full px-8 font-semibold shadow-xs sm:w-auto"> <span>Explore Components</span> <lucide-icon [img]="ArrowRight" class="ml-1 size-4" /> </button> <p class="text-muted-foreground text-xs"> Free & Open Source · MIT License · 370+ Primitives & Blocks </p> </div> <div ui-separator></div> <div class="text-muted-foreground space-y-2.5 pt-1 text-center text-xs"> <div class="flex flex-wrap items-center justify-center gap-3 font-medium"> <span class="hover:text-foreground cursor-pointer transition-colors">Documentation</span> <span>·</span> <span class="hover:text-foreground cursor-pointer transition-colors">GitHub</span> <span>·</span> <span class="hover:text-foreground cursor-pointer transition-colors">Discord</span> <span>·</span> <span class="hover:text-foreground cursor-pointer transition-colors">Preferences</span> </div> <p class="text-xs">UIPKGE Inc. · 548 Market St, Suite 29314, San Francisco, CA 94104</p> <p class="text-xs"> You received this email because you subscribed to UIPKGE updates. <span class="hover:text-foreground cursor-pointer underline"> Unsubscribe anytime (RFC 8058)</span >. </p> </div> </div> </div> </div> </div> </div> <!-- Deliverability & Inbox Placement Card --> <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-header class="pb-3"> <div class="flex flex-wrap items-center justify-between"> <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]="ShieldCheck" class="size-4" /> </div> <h3 ui-card-title class="text-sm font-semibold tracking-tight"> Deliverability & Inbox Placement Scorecard </h3> </div> <span ui-badge variant="outline" class="border-success/30 text-success h-5 px-2 text-xs"> 99.8% Inbox Probability </span> </div> <p ui-card-description class="text-xs"> Heuristic spam risk evaluation, DNS authentication compliance, and subscriber engagement telemetry. </p> </div> <div ui-card-content class="space-y-4 pt-0"> <div class="grid grid-cols-1 gap-3 sm:grid-cols-3"> <div class="border-border bg-muted/30 rounded-lg border p-3"> <div class="flex flex-wrap items-center justify-between text-xs"> <span class="text-muted-foreground">Spam Score</span> <span ui-badge variant="success" class="h-4.5 px-1 text-xs">Optimal</span> </div> <div class="mt-1 flex items-baseline gap-1"> <span class="text-success text-xl font-bold tracking-tight">0.2</span> <span class="text-muted-foreground text-xs">/ 10.0</span> </div> <div class="bg-muted mt-2 h-1.5 w-full overflow-hidden rounded-full"> <div class="bg-success h-full w-[98%] rounded-full"></div> </div> </div> <div class="border-border bg-muted/30 rounded-lg border p-3"> <div class="flex flex-wrap items-center justify-between text-xs"> <span class="text-muted-foreground">Auth Headers</span> <span ui-badge variant="success" class="h-4.5 px-1 text-xs">100% Pass</span> </div> <div class="mt-1 space-y-0.5 text-xs"> <div class="flex justify-between"> <span class="text-muted-foreground">SPF / DKIM</span> <span class="text-foreground font-medium">Valid (2048-bit)</span> </div> <div class="flex justify-between"> <span class="text-muted-foreground">DMARC</span> <span class="text-foreground font-medium">p=reject</span> </div> </div> </div> <div class="border-border bg-muted/30 rounded-lg border p-3"> <div class="flex flex-wrap items-center justify-between text-xs"> <span class="text-muted-foreground">Reading Time</span> <span class="text-foreground font-mono font-medium">185 words</span> </div> <div class="mt-1 flex items-baseline gap-1"> <span class="text-foreground text-xl font-bold tracking-tight">45</span> <span class="text-muted-foreground text-xs">seconds</span> </div> <div class="text-muted-foreground mt-1 text-xs">Payload size: 14.2 KB (Safe)</div> </div> </div> <div class="grid grid-cols-1 gap-2 text-xs sm:grid-cols-2"> <div class="border-border/80 bg-muted/20 flex flex-wrap items-center justify-between rounded-md border px-3 py-2" > <span class="text-muted-foreground flex items-center gap-1.5"> <lucide-icon [img]="Check" class="text-success size-3.5" /> Primary Tab Placement </span> <span class="text-foreground font-medium">99.8% (Gmail / Apple)</span> </div> <div class="border-border/80 bg-muted/20 flex flex-wrap items-center justify-between rounded-md border px-3 py-2" > <span class="text-muted-foreground flex items-center gap-1.5"> <lucide-icon [img]="Check" class="text-success size-3.5" /> RFC 8058 One-Click </span> <span class="text-foreground font-medium">Compliant</span> </div> <div class="border-border/80 bg-muted/20 flex flex-wrap items-center justify-between rounded-md border px-3 py-2" > <span class="text-muted-foreground flex items-center gap-1.5"> <lucide-icon [img]="Check" class="text-success size-3.5" /> Dark Mode Contrast </span> <span class="text-foreground font-medium">100% WCAG AAA</span> </div> <div class="border-border/80 bg-muted/20 flex flex-wrap items-center justify-between rounded-md border px-3 py-2" > <span class="text-muted-foreground flex items-center gap-1.5"> <lucide-icon [img]="Check" class="text-success size-3.5" /> HTTPS Asset Links </span> <span class="text-foreground font-medium">6 Valid Links</span> </div> </div> </div> </div> </div> </div> <!-- Send Test Proof Dialog --> <ui-dialog [open]="isTestModalOpen()" (openChange)="isTestModalOpen.set($event)"> <ui-dialog-content class="sm:max-w-md"> <div ui-dialog-header> <h2 ui-dialog-title class="flex items-center gap-2 text-base"> <lucide-icon [img]="Mail" class="text-primary size-4" /> Dispatch Test Proof </h2> <p ui-dialog-description class="text-xs"> Send a full fidelity rendering test to inspect subject line, preheader, and layout across email clients. </p> </div> <div class="space-y-4 py-2"> <div class="space-y-1.5"> <label class="text-muted-foreground text-xs font-medium">Proof Recipient Address</label> <ui-input [value]="testTargetEmail()" (valueChange)="testTargetEmail.set($event)" type="email" placeholder="alex.developer@company.com" class="text-xs" [disabled]="isSendingTest()" /> </div> <div class="space-y-1.5"> <label class="text-muted-foreground text-xs font-medium">Email Client Engine Simulation</label> <div class="grid grid-cols-2 gap-2"> <button type="button" ui-button size="xs" [variant]="selectedClient() === 'all' ? 'default' : 'outline'" class="justify-start text-xs font-medium" (click)="selectedClient.set('all')" > 🌐 All Clients (Auto) </button> <button type="button" ui-button size="xs" [variant]="selectedClient() === 'gmail' ? 'default' : 'outline'" class="justify-start text-xs font-medium" (click)="selectedClient.set('gmail')" > ✉️ Gmail (Web/App) </button> <button type="button" ui-button size="xs" [variant]="selectedClient() === 'apple' ? 'default' : 'outline'" class="justify-start text-xs font-medium" (click)="selectedClient.set('apple')" > 🍏 Apple Mail (iOS) </button> <button type="button" ui-button size="xs" [variant]="selectedClient() === 'outlook' ? 'default' : 'outline'" class="justify-start text-xs font-medium" (click)="selectedClient.set('outlook')" > 💼 Outlook 365 </button> </div> </div> @if (testSentSuccess()) { <div class="border-success/30 bg-success/10 text-success flex items-center gap-2 rounded-md border p-2.5 text-xs font-medium" > <lucide-icon [img]="CheckCircle2" class="size-4 shrink-0" /> <span>Test proof dispatched to {{ testTargetEmail() }}!</span> </div> } </div> <div ui-dialog-footer class="gap-2 sm:gap-0"> <button ui-button variant="outline" size="sm" [disabled]="isSendingTest()" (click)="isTestModalOpen.set(false)" > Cancel </button> <button ui-button size="sm" class="gap-1.5" [disabled]="isSendingTest()" (click)="handleSendTest()"> <lucide-icon [img]="Send" class="size-3.5" /> <span>{{ isSendingTest() ? 'Dispatching...' : 'Send Test Proof' }}</span> </button> </div> </ui-dialog-content> </ui-dialog> <!-- Schedule Broadcast Dialog --> <ui-dialog [open]="isScheduleModalOpen()" (openChange)="isScheduleModalOpen.set($event)"> <ui-dialog-content class="sm:max-w-md"> <div ui-dialog-header> <h2 ui-dialog-title class="flex items-center gap-2 text-base"> <lucide-icon [img]="Calendar" class="text-primary size-4" /> Schedule Email Broadcast </h2> <p ui-dialog-description class="text-xs"> Queue this broadcast for {{ currentAudience().count.toLocaleString() }} verified subscribers with deliverability rate limit guardrails. </p> </div> <div class="space-y-4 py-2"> <div class="border-border bg-muted/40 space-y-2 rounded-lg border p-3 text-xs"> <div class="flex flex-wrap items-center justify-between"> <span class="text-muted-foreground">Audience Target</span> <span class="text-foreground font-medium">{{ currentAudience().label }}</span> </div> <div class="flex flex-wrap items-center justify-between"> <span class="text-muted-foreground">Estimated Reach</span> <span class="text-foreground font-semibold" >{{ currentAudience().count.toLocaleString() }} recipients</span > </div> <div class="flex flex-wrap items-center justify-between"> <span class="text-muted-foreground">Spam Safety Score</span> <span ui-badge variant="success" class="h-4.5 px-1.5 text-xs font-normal">0.2 / 10.0 (Safe)</span> </div> </div> <div class="space-y-2"> <label class="text-muted-foreground text-xs font-medium">Broadcast Timing</label> <div class="grid grid-cols-1 gap-2"> <button type="button" [class]="timingCardClass('optimal')" (click)="scheduleTiming.set('optimal')"> <div class="space-y-0.5"> <div class="text-foreground font-medium">✨ AI Optimal Send Time (Recommended)</div> <div class="text-muted-foreground text-xs"> Tuesday, Aug 25 @ 9:00 AM EDT (+18% projected opens) </div> </div> <div [class]="timingRadioClass('optimal')"> @if (scheduleTiming() === 'optimal') { <div class="size-1.5 rounded-full bg-white"></div> } </div> </button> <button type="button" [class]="timingCardClass('immediate')" (click)="scheduleTiming.set('immediate')"> <div class="space-y-0.5"> <div class="text-foreground font-medium">⚡ Send Immediately</div> <div class="text-muted-foreground text-xs"> Dispatch immediately with warm-up concurrency pacing </div> </div> <div [class]="timingRadioClass('immediate')"> @if (scheduleTiming() === 'immediate') { <div class="size-1.5 rounded-full bg-white"></div> } </div> </button> </div> </div> @if (scheduleSuccess()) { <div class="border-success/30 bg-success/10 text-success flex items-center gap-2 rounded-md border p-2.5 text-xs font-medium" > <lucide-icon [img]="CheckCircle2" class="size-4 shrink-0" /> <span>Broadcast successfully queued for dispatch!</span> </div> } </div> <div ui-dialog-footer class="gap-2 sm:gap-0"> <button ui-button variant="outline" size="sm" [disabled]="isScheduling()" (click)="isScheduleModalOpen.set(false)" > Cancel </button> <button ui-button size="sm" class="gap-1.5" [disabled]="isScheduling()" (click)="handleScheduleBroadcast()"> <lucide-icon [img]="Send" class="size-3.5" /> <span>{{ isScheduling() ? 'Scheduling...' : 'Confirm & Schedule' }}</span> </button> </div> </ui-dialog-content> </ui-dialog> </div> `, }) export class UiEmailCampaignComposerComponent { protected readonly ArrowRight = ArrowRight protected readonly Calendar = Calendar protected readonly Check = Check protected readonly CheckCircle2 = CheckCircle2 protected readonly Eye = Eye protected readonly Flame = Flame protected readonly Inbox = Inbox protected readonly Lock = Lock protected readonly Mail = Mail protected readonly Monitor = Monitor protected readonly Send = Send protected readonly ShieldCheck = ShieldCheck protected readonly Smartphone = Smartphone protected readonly Sparkles = Sparkles protected readonly Split = Split protected readonly Terminal = Terminal protected readonly UserCheck = UserCheck protected readonly Users = Users protected readonly Zap = Zap protected readonly audienceOptions = AUDIENCE_OPTIONS protected readonly aiSuggestions = AI_SUGGESTIONS protected readonly campaignName = 'Product Launch: UIPKGE 2.0 Global Release' protected readonly senderName = 'UIPKGE Team' protected readonly senderEmail = 'updates@uipkge.dev' @Input('class') className?: string readonly selectedAudience = signal('active-devs') readonly subjectA = signal('🚀 Introducing UIPKGE 2.0: The unbundled UI registry') readonly subjectB = signal('Ship polished UIs 10x faster with UIPKGE 2.0 ⚡') readonly previewText = signal('370+ production components with zero npm dependencies. Native Vue 3 & React code.') readonly isAbTestEnabled = signal(false) readonly activePreviewVariant = signal<'A' | 'B'>('A') readonly viewport = signal<CampaignViewport>('desktop') readonly isTestModalOpen = signal(false) readonly testTargetEmail = signal('alex.developer@company.com') readonly selectedClient = signal<CampaignClient>('all') readonly isSendingTest = signal(false) readonly testSentSuccess = signal(false) readonly isScheduleModalOpen = signal(false) readonly scheduleTiming = signal<ScheduleTiming>('optimal') readonly isScheduling = signal(false) readonly scheduleSuccess = signal(false) private timers: ReturnType<typeof setTimeout>[] = [] constructor() { inject(DestroyRef).onDestroy(() => this.timers.forEach((t) => clearTimeout(t))) } readonly currentAudience = computed( () => AUDIENCE_OPTIONS.find((item) => item.id === this.selectedAudience()) ?? AUDIENCE_OPTIONS[0]!, ) readonly currentActiveSubject = computed(() => { if (this.isAbTestEnabled() && this.activePreviewVariant() === 'B') return this.subjectB() return this.subjectA() }) readonly emojiCount = computed(() => { const matches = this.currentActiveSubject().match(/\p{Extended_Pictographic}/gu) return matches ? matches.length : 0 }) get rootClass(): string { return cn('w-full space-y-6', this.className) } get previewCanvasClass(): string { return cn( 'w-full transition-[max-width] duration-300 ease-in-out', this.viewport() === 'desktop' ? 'max-w-[580px]' : 'max-w-[360px]', ) } timingCardClass(kind: ScheduleTiming): string { return cn( 'border-border hover:border-primary/50 focus-visible:ring-ring flex flex-wrap items-center justify-between rounded-lg border p-3 text-left text-xs transition-colors outline-none focus-visible:ring-2', this.scheduleTiming() === kind ? 'border-primary bg-primary/5' : 'bg-card', ) } timingRadioClass(kind: ScheduleTiming): string { return cn( 'flex size-4 items-center justify-center rounded-full border', this.scheduleTiming() === kind ? 'border-primary bg-primary text-primary-foreground' : 'border-border', ) } applyAiSubject(text: string): void { if (this.isAbTestEnabled() && this.activePreviewVariant() === 'B') this.subjectB.set(text) else this.subjectA.set(text) } handleSendTest(): void { if (!this.testTargetEmail()) return this.isSendingTest.set(true) this.testSentSuccess.set(false) this.timers.push( setTimeout(() => { this.isSendingTest.set(false) this.testSentSuccess.set(true) this.timers.push( setTimeout(() => { this.testSentSuccess.set(false) this.isTestModalOpen.set(false) }, 1800), ) }, 800), ) } handleScheduleBroadcast(): void { this.isScheduling.set(true) this.scheduleSuccess.set(false) this.timers.push( setTimeout(() => { this.isScheduling.set(false) this.scheduleSuccess.set(true) this.timers.push( setTimeout(() => { this.scheduleSuccess.set(false) this.isScheduleModalOpen.set(false) }, 1800), ) }, 900), ) } }src/app/components/blocks/email-campaign-composer/index.ts0.2 kB
export { UiEmailCampaignComposerComponent, type CampaignClient, type CampaignViewport, type ScheduleTiming, } from './email-campaign-composer.component'
Raw manifest:https://uipkge.dev/r/angular/email-campaign-composer.json