Menu

Change language

GitHub repo

Email Campaign Composer

blockmarketing

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.

Installation

$npx uipkge-ng@latest add email-campaign-composer
Manifest:https://uipkge.dev/r/angular/email-campaign-composer.jsonInstalls to:src/app/components/blocks/email-campaign-composer/

Variants

Loading interactive previews…

Schema

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

AudienceSegment
interface 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="[email protected]" />
                      </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">&lt;{{ senderEmail }}&gt;</span>
                              </div>
                              <div class="text-muted-foreground text-xs">To: [email protected]</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="[email protected]"
                    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 = '[email protected]'
    
      @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('[email protected]')
      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

Raw manifest:https://uipkge.dev/r/angular/email-campaign-composer.json