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 shadcn-vue@latest add https://uipkge.dev/r/vue/email-campaign-composer.json$npx shadcn-vue@latest add https://uipkge.dev/r/vue/email-campaign-composer.json$yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/email-campaign-composer.json$bunx shadcn-vue@latest add https://uipkge.dev/r/vue/email-campaign-composer.jsonNamed registry:
npx shadcn-vue@latest add @uipkge/email-campaign-composerInstalls to:app/components/blocks/Variants
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
className | string | — | optional |
Files installed (1)
app/components/blocks/EmailCampaignComposer.vue44.5 kB
<script setup lang="ts"> import { computed, ref } from 'vue' import { ArrowRight, Calendar, Check, CheckCircle2, Eye, Flame, Inbox, Lock, Mail, Monitor, Send, ShieldCheck, Smartphone, Sparkles, Split, Terminal, UserCheck, Users, Zap, } from 'lucide-vue-next' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from '@/components/ui/dialog' import { Input } from '@/components/ui/input' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Separator } from '@/components/ui/separator' import { Switch } from '@/components/ui/switch' import { cn } from '@/lib/utils' interface Props { className?: string } const props = defineProps<Props>() // Campaign state const campaignName = ref('Product Launch: UIPKGE 2.0 Global Release') const selectedAudience = ref('active-devs') // Subject line & Preheader state const subjectA = ref('🚀 Introducing UIPKGE 2.0: The unbundled UI registry') const subjectB = ref('Ship polished UIs 10x faster with UIPKGE 2.0 ⚡') const previewText = ref('370+ production components with zero npm dependencies. Native Vue 3 & React code.') const senderName = ref('UIPKGE Team') const senderEmail = ref('updates@uipkge.dev') const replyToEmail = ref('support@uipkge.dev') // A/B testing state const isAbTestEnabled = ref(false) const activePreviewVariant = ref<'A' | 'B'>('A') // Viewport state type ViewportMode = 'desktop' | 'mobile' const viewport = ref<ViewportMode>('desktop') // Test Proof Dialog state const isTestModalOpen = ref(false) const testTargetEmail = ref('alex.developer@company.com') const selectedClient = ref<'all' | 'gmail' | 'apple' | 'outlook'>('all') const isSendingTest = ref(false) const testSentSuccess = ref(false) // Schedule Broadcast Dialog state const isScheduleModalOpen = ref(false) const scheduleTiming = ref<'optimal' | 'immediate' | 'custom'>('optimal') const isScheduling = ref(false) const scheduleSuccess = ref(false) // Audience segment metadata const audienceOptions = [ { 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 currentAudience = computed(() => { return audienceOptions.find((item) => item.id === selectedAudience.value) ?? audienceOptions[0] }) // Subject analysis const currentActiveSubject = computed(() => { if (isAbTestEnabled.value && activePreviewVariant.value === 'B') { return subjectB.value } return subjectA.value }) const subjectCharCount = computed(() => currentActiveSubject.value.length) const preheaderCharCount = computed(() => previewText.value.length) const emojiCount = computed(() => { const matches = currentActiveSubject.value.match(/\p{Extended_Pictographic}/gu) return matches ? matches.length : 0 }) // AI suggestions list const aiSuggestions = [ { 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', }, ] function applyAiSubject(text: string) { if (isAbTestEnabled.value && activePreviewVariant.value === 'B') { subjectB.value = text } else { subjectA.value = text } } function handleSendTest() { if (!testTargetEmail.value) return isSendingTest.value = true testSentSuccess.value = false setTimeout(() => { isSendingTest.value = false testSentSuccess.value = true setTimeout(() => { testSentSuccess.value = false isTestModalOpen.value = false }, 1800) }, 800) } function handleScheduleBroadcast() { isScheduling.value = true scheduleSuccess.value = false setTimeout(() => { isScheduling.value = false scheduleSuccess.value = true setTimeout(() => { scheduleSuccess.value = false isScheduleModalOpen.value = false }, 1800) }, 900) } </script> <template> <div data-slot="email-campaign-composer" :class="cn('w-full space-y-6', props.className)"> <!-- Top Header Bar --> <Card class="border-border bg-card shadow-xs"> <CardHeader class="pb-4"> <div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between"> <!-- Campaign Title & Status Badge --> <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"> <Mail class="size-4" /> </div> <h2 class="text-foreground text-lg font-semibold tracking-tight sm:text-xl"> {{ campaignName }} </h2> <!-- Status Badge: Blue info styled --> <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>Draft Saved · {{ currentAudience.formattedCount }} Recipients</span> </Badge> </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> <!-- Top Actions: Send Test Proof & Schedule Broadcast --> <div class="flex flex-wrap items-center gap-2.5"> <!-- Send Test Proof Dialog --> <Dialog v-model:open="isTestModalOpen"> <DialogTrigger as-child> <Button variant="outline" size="sm" class="gap-1.5 text-xs font-medium"> <Mail class="text-muted-foreground size-3.5" /> <span>Send Test Proof</span> </Button> </DialogTrigger> <DialogContent class="sm:max-w-md"> <DialogHeader> <DialogTitle class="flex items-center gap-2 text-base"> <Mail class="text-primary size-4" /> Dispatch Test Proof </DialogTitle> <DialogDescription class="text-xs"> Send a full fidelity rendering test to inspect subject line, preheader, and layout across email clients. </DialogDescription> </DialogHeader> <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> <Input v-model="testTargetEmail" 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" size="xs" :variant="selectedClient === 'all' ? 'default' : 'outline'" class="justify-start text-xs font-medium" @click="selectedClient = 'all'" > 🌐 All Clients (Auto) </Button> <Button type="button" size="xs" :variant="selectedClient === 'gmail' ? 'default' : 'outline'" class="justify-start text-xs font-medium" @click="selectedClient = 'gmail'" > ✉️ Gmail (Web/App) </Button> <Button type="button" size="xs" :variant="selectedClient === 'apple' ? 'default' : 'outline'" class="justify-start text-xs font-medium" @click="selectedClient = 'apple'" > 🍏 Apple Mail (iOS) </Button> <Button type="button" size="xs" :variant="selectedClient === 'outlook' ? 'default' : 'outline'" class="justify-start text-xs font-medium" @click="selectedClient = 'outlook'" > 💼 Outlook 365 </Button> </div> </div> <div v-if="testSentSuccess" class="border-success/30 bg-success/10 text-success flex items-center gap-2 rounded-md border p-2.5 text-xs font-medium" > <CheckCircle2 class="size-4 shrink-0" /> <span>Test proof dispatched to {{ testTargetEmail }}!</span> </div> </div> <DialogFooter class="gap-2 sm:gap-0"> <Button variant="outline" size="sm" :disabled="isSendingTest" @click="isTestModalOpen = false"> Cancel </Button> <Button size="sm" class="gap-1.5" :disabled="isSendingTest" @click="handleSendTest"> <Send class="size-3.5" /> <span>{{ isSendingTest ? 'Dispatching...' : 'Send Test Proof' }}</span> </Button> </DialogFooter> </DialogContent> </Dialog> <!-- Schedule Broadcast Dialog --> <Dialog v-model:open="isScheduleModalOpen"> <DialogTrigger as-child> <Button size="sm" class="gap-1.5 text-xs font-medium shadow-xs"> <Send class="size-3.5 fill-current" /> <span>Schedule Broadcast</span> </Button> </DialogTrigger> <DialogContent class="sm:max-w-md"> <DialogHeader> <DialogTitle class="flex items-center gap-2 text-base"> <Calendar class="text-primary size-4" /> Schedule Email Broadcast </DialogTitle> <DialogDescription class="text-xs"> Queue this broadcast for {{ currentAudience.count.toLocaleString() }} verified subscribers with deliverability rate limit guardrails. </DialogDescription> </DialogHeader> <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> <Badge variant="success" class="h-4.5 px-1.5 text-xs font-normal">0.2 / 10.0 (Safe)</Badge> </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="border-border hover:border-primary/50 flex flex-wrap items-center justify-between rounded-lg border p-3 text-left text-xs transition-colors" :class="scheduleTiming === 'optimal' ? 'border-primary bg-primary/5' : 'bg-card'" @click="scheduleTiming = '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="flex size-4 items-center justify-center rounded-full border" :class=" scheduleTiming === 'optimal' ? 'border-primary bg-primary text-primary-foreground' : 'border-border' " > <div v-if="scheduleTiming === 'optimal'" class="size-1.5 rounded-full bg-white" /> </div> </button> <button type="button" class="border-border hover:border-primary/50 flex flex-wrap items-center justify-between rounded-lg border p-3 text-left text-xs transition-colors" :class="scheduleTiming === 'immediate' ? 'border-primary bg-primary/5' : 'bg-card'" @click="scheduleTiming = '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="flex size-4 items-center justify-center rounded-full border" :class=" scheduleTiming === 'immediate' ? 'border-primary bg-primary text-primary-foreground' : 'border-border' " > <div v-if="scheduleTiming === 'immediate'" class="size-1.5 rounded-full bg-white" /> </div> </button> </div> </div> <div v-if="scheduleSuccess" class="border-success/30 bg-success/10 text-success flex items-center gap-2 rounded-md border p-2.5 text-xs font-medium" > <CheckCircle2 class="size-4 shrink-0" /> <span>Broadcast successfully queued for dispatch!</span> </div> </div> <DialogFooter class="gap-2 sm:gap-0"> <Button variant="outline" size="sm" :disabled="isScheduling" @click="isScheduleModalOpen = false"> Cancel </Button> <Button size="sm" class="gap-1.5" :disabled="isScheduling" @click="handleScheduleBroadcast"> <Send class="size-3.5" /> <span>{{ isScheduling ? 'Scheduling...' : 'Confirm & Schedule' }}</span> </Button> </DialogFooter> </DialogContent> </Dialog> </div> </div> </CardHeader> </Card> <!-- 2-Column Campaign Studio Grid --> <div class="grid grid-cols-1 items-start gap-6 lg:grid-cols-12"> <!-- LEFT PANEL: Campaign Setup & Audience (45% -> lg:col-span-5) --> <div class="space-y-5 lg:col-span-5"> <!-- 1. Audience Segment Selector Card --> <Card class="border-border bg-card shadow-xs"> <CardHeader 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"> <Users class="size-3.5" /> </div> <CardTitle class="text-sm font-semibold tracking-tight">Audience Segment</CardTitle> </div> <Badge variant="secondary" class="h-5 gap-1 px-2 text-xs font-normal"> <UserCheck class="text-success size-3" /> Verified List </Badge> </div> <CardDescription class="text-xs"> Select subscriber segment to calculate deliverability pacing and personalization tags. </CardDescription> </CardHeader> <CardContent class="space-y-3.5 pt-0"> <div class="space-y-1.5"> <label class="text-foreground text-xs font-medium">Target Segment</label> <Select v-model="selectedAudience"> <SelectTrigger class="h-9 w-full text-xs font-medium [&>span]:truncate [&>svg]:shrink-0" aria-label="Select Audience Segment" > <SelectValue placeholder="Select audience segment" /> </SelectTrigger> <SelectContent> <SelectItem v-for="aud in audienceOptions" :key="aud.id" :value="aud.id"> <span>{{ aud.label }}</span> </SelectItem> </SelectContent> </Select> </div> <!-- Audience Telemetry Badges Grid --> <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-foreground 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-foreground text-info mt-0.5 text-sm font-bold tracking-tight"> {{ currentAudience.clickRate }} </div> </div> </div> </CardContent> </Card> <!-- 2. Subject Line & AI Optimizer Card --> <Card class="border-border bg-card shadow-xs"> <CardHeader 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"> <Sparkles class="size-3.5" /> </div> <CardTitle class="text-sm font-semibold tracking-tight">Subject Line & AI Scorer</CardTitle> </div> <!-- AI Subject Scorer Green Badge --> <Badge variant="outline" class="border-success/30 bg-success/10 text-success gap-1 px-2 text-xs font-medium" > <Sparkles class="text-success size-3" /> <span>94 / 100 · High Open Rate Impact</span> </Badge> </div> <CardDescription class="text-xs"> AI predictive heuristic analyzing urgency, sentiment, character count, and spam filter risk. </CardDescription> </CardHeader> <CardContent class="space-y-4 pt-0"> <!-- A/B Subject Variant Switcher --> <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"> <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> <Switch id="vue-ab-toggle" v-model="isAbTestEnabled" /> </div> <!-- Subject Input Variant A --> <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> <Badge variant="outline" class="text-muted-foreground h-4.5 gap-1 px-1.5 text-xs font-normal"> <Flame class="text-warning size-2.5" /> <span>{{ emojiCount }} emoji</span> </Badge> </div> </div> <Input v-model="subjectA" class="text-xs" placeholder="Enter campaign subject line..." /> </div> <!-- Subject Input Variant B (Visible if A/B Enabled) --> <div v-if="isAbTestEnabled" 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> <Input v-model="subjectB" class="text-xs" placeholder="Enter alternate subject line B..." /> <div class="flex items-center gap-1 pt-1"> <Button type="button" size="xs" :variant="activePreviewVariant === 'A' ? 'default' : 'outline'" class="text-xs font-medium" @click="activePreviewVariant = 'A'" > Preview Variant A </Button> <Button type="button" size="xs" :variant="activePreviewVariant === 'B' ? 'default' : 'outline'" class="text-xs font-medium" @click="activePreviewVariant = 'B'" > Preview Variant B </Button> </div> </div> <!-- AI Subject Score Breakdown Metrics --> <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"> <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"> <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"> <Check class="text-success size-3" /> Spam Keyword Risk </span> <span class="text-foreground text-success font-medium">Very Low (0.1%)</span> </div> </div> <!-- 1-Click AI Subject Line Suggestions --> <div class="space-y-2"> <span class="text-muted-foreground flex items-center gap-1 text-xs font-medium"> <Sparkles class="text-primary size-3" /> AI Optimization Suggestions (Click to apply): </span> <div class="space-y-1.5"> <button v-for="(suggestion, idx) in aiSuggestions" :key="idx" type="button" class="border-border hover:border-primary/40 hover:bg-muted/50 flex w-full items-center justify-between rounded-md border p-2 text-left text-xs transition-colors" @click="applyAiSubject(suggestion.text)" > <span class="text-foreground truncate font-medium">{{ suggestion.text }}</span> <Badge variant="secondary" class="text-success ml-2 shrink-0 font-mono text-xs"> {{ suggestion.score }}/100 </Badge> </button> </div> </div> <Separator /> <!-- 3. Preview Text / Preheader --> <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">{{ preheaderCharCount }}/90 chars</span> </div> <Input v-model="previewText" class="text-xs" placeholder="Secondary snippet displayed in subscriber inbox lists..." /> </div> <Separator /> <!-- 4. Sender Identity --> <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> <Input v-model="senderName" class="text-xs" placeholder="UIPKGE Team" /> </div> <div class="space-y-1"> <span class="text-muted-foreground text-xs">From Address</span> <Input v-model="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"> <ShieldCheck class="size-3.5 shrink-0" /> <span>Domain verified: <strong>mail.uipkge.dev</strong> (SPF & DKIM valid)</span> </div> <Badge variant="outline" class="border-success/30 text-success h-4.5 px-1.5 text-xs"> TLS 1.3 </Badge> </div> </div> </CardContent> </Card> </div> <!-- RIGHT PANEL: Live Multi-Device Email Preview (55% -> lg:col-span-7) --> <div class="space-y-5 lg:col-span-7"> <!-- Live Email Canvas Container Card --> <Card class="border-border bg-card overflow-hidden shadow-xs"> <CardHeader 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"> <Eye class="text-primary size-4" /> <CardTitle class="text-sm font-semibold tracking-tight">Live Newsletter Preview</CardTitle> <Badge v-if="isAbTestEnabled" variant="secondary" class="text-xs font-normal"> Showing Variant {{ activePreviewVariant }} </Badge> </div> <!-- Device Viewport Toggle (Desktop 600px vs Mobile 375px) --> <div class="bg-muted border-border inline-flex items-center rounded-lg border p-1 shadow-xs"> <Button type="button" size="xs" :variant="viewport === 'desktop' ? 'default' : 'ghost'" class="gap-1.5 text-xs font-medium" @click="viewport = 'desktop'" > <Monitor class="size-3.5" /> <span>Desktop 600px</span> </Button> <Button type="button" size="xs" :variant="viewport === 'mobile' ? 'default' : 'ghost'" class="gap-1.5 text-xs font-medium" @click="viewport = 'mobile'" > <Smartphone class="size-3.5" /> <span>Mobile 375px</span> </Button> </div> </div> </CardHeader> <!-- Rendered Newsletter Preview Canvas --> <CardContent class="bg-muted/30 flex min-h-[580px] flex-col items-center justify-start p-4 sm:p-6"> <div :class=" cn( 'w-full transition-[max-width] duration-300 ease-in-out', viewport === 'desktop' ? 'max-w-[580px]' : 'max-w-[360px]', ) " > <!-- Window Chrome Bar Frame --> <div class="border-border bg-card overflow-hidden rounded-xl border shadow-sm"> <!-- Top Chrome Bar --> <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 class="bg-warning/80 size-2.5 rounded-full" /> <div class="bg-success/80 size-2.5 rounded-full" /> </div> <div class="text-muted-foreground flex items-center gap-1.5 font-mono text-xs"> <Inbox class="size-3" /> <span>UIPKGE Preview · {{ viewport === 'desktop' ? '600px' : '375px' }}</span> </div> <Badge variant="outline" class="border-border text-muted-foreground h-4.5 gap-1 px-1.5 text-xs font-normal" > <Lock class="text-success size-2.5" /> TLS 1.3 </Badge> </div> <!-- Envelope Metadata Readout --> <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> <!-- High-Fidelity Rendered Email Newsletter Body --> <div class="bg-card space-y-5 p-5 sm:p-7"> <!-- Logo Banner --> <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" > <Zap class="size-3.5 fill-current" /> </div> <span class="text-foreground text-sm font-bold tracking-tight">UIPKGE</span> </div> <Badge variant="secondary" class="text-xs font-medium">Changelog #24 · Release 2.0</Badge> </div> <!-- Hero Headline & Description --> <div class="space-y-2.5"> <Badge variant="outline" class="text-primary border-primary/30 text-xs font-semibold tracking-wider uppercase" > Major Product Release </Badge> <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> <!-- Product Visual with Code Snippet & Callout Pills --> <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"> <Terminal class="text-primary size-3" /> <span>components/ui/button.vue</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-vue add @uipkge/button </div> <!-- Mini Interactive UI Demo inside Email --> <div class="bg-card border-border flex flex-wrap items-center justify-center gap-2 rounded-md border p-3" > <Button size="xs" variant="default">Primary</Button> <Button size="xs" variant="secondary">Secondary</Button> <Button size="xs" variant="outline">Outline</Button> <Button size="xs" variant="destructive">Destructive</Button> </div> <!-- 3 Callout Pills --> <div class="flex flex-wrap items-center justify-center gap-1.5 pt-1"> <Badge variant="secondary" class="text-xs font-normal">⚡ Zero npm dependencies</Badge> <Badge variant="secondary" class="text-xs font-normal">🎨 OKLCH Dark Mode</Badge> <Badge variant="secondary" class="text-xs font-normal">🧩 Vue 3 + React Parity</Badge> </div> </div> </div> <!-- Primary CTA Button --> <div class="flex flex-col items-center justify-center space-y-2 pt-1 text-center"> <Button size="lg" class="w-full px-8 font-semibold shadow-xs sm:w-auto"> <span>Explore Components</span> <ArrowRight class="ml-1 size-4" /> </Button> <p class="text-muted-foreground text-xs"> Free & Open Source · MIT License · 370+ Primitives & Blocks </p> </div> <Separator /> <!-- Unsubscribe & Footer Details --> <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> </CardContent> </Card> <!-- Deliverability & Inbox Placement Card --> <Card class="border-border bg-card shadow-xs"> <CardHeader 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"> <ShieldCheck class="size-4" /> </div> <CardTitle class="text-sm font-semibold tracking-tight" >Deliverability & Inbox Placement Scorecard</CardTitle > </div> <Badge variant="outline" class="border-success/30 text-success h-5 px-2 text-xs"> 99.8% Inbox Probability </Badge> </div> <CardDescription class="text-xs"> Heuristic spam risk evaluation, DNS authentication compliance, and subscriber engagement telemetry. </CardDescription> </CardHeader> <CardContent class="space-y-4 pt-0"> <!-- 3 Core Scorecard Metrics --> <div class="grid grid-cols-1 gap-3 sm:grid-cols-3"> <!-- Metric 1: Spam Score --> <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> <Badge variant="success" class="h-4.5 px-1 text-xs">Optimal</Badge> </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> <!-- Metric 2: Domain Auth --> <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> <Badge variant="success" class="h-4.5 px-1 text-xs">100% Pass</Badge> </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> <!-- Metric 3: Reading Time --> <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> <!-- Detailed Checklist Row --> <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"> <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"> <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"> <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"> <Check class="text-success size-3.5" /> HTTPS Asset Links </span> <span class="text-foreground font-medium">6 Valid Links</span> </div> </div> </CardContent> </Card> </div> </div> </div> </template>
Raw manifest:https://uipkge.dev/r/vue/email-campaign-composer.json