Email Template Previewer
blockmarketingResend and Klaviyo style marketing email designer preview with viewport switcher, subject line tester, deliverability & spam score analyzer, and live test dispatch modal.
Installation
$pnpm dlx uipkge-ng@latest add email-template-previewer$npx uipkge-ng@latest add email-template-previewer$yarn dlx uipkge-ng@latest add email-template-previewer$bunx uipkge-ng@latest add email-template-previewerManifest:
https://uipkge.dev/r/angular/email-template-previewer.jsonInstalls to:src/app/components/blocks/email-template-previewer/Variants
Loading interactive previews…
Loading interactive previews…
Files installed (2)
src/app/components/blocks/email-template-previewer/email-template-previewer.component.ts39.8 kB
// Resend and Klaviyo style marketing email designer preview with viewport switcher, subject // line tester, deliverability & spam score analyzer, and live test dispatch modal. // Port of the React block 1:1. import { ChangeDetectionStrategy, Component, DestroyRef, Input, computed, inject, signal } from '@angular/core' import { ArrowRight, Check, CheckCircle2, Code2, Copy, FileCode, Flame, Inbox, Layers, Lock, LucideAngularModule, Mail, Monitor, Palette, Send, ShieldCheck, Smartphone, Terminal, 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 { UiSeparatorComponent } from '@/ui/separator/separator.component' export type PreviewerViewport = 'desktop' | 'mobile' | 'raw' export type PreviewerClient = 'all' | 'gmail' | 'apple' | 'outlook' @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-email-template-previewer, [ui-email-template-previewer]', 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, UiSeparatorComponent, ], template: ` <div data-slot="email-template-previewer" [class]="rootClass"> <!-- Top Toolbar Card --> <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-header class="pb-4"> <div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <div class="space-y-1"> <div class="flex items-center gap-2"> <h3 ui-card-title class="text-lg font-semibold tracking-tight">Marketing Email Designer</h3> <span ui-badge variant="outline" class="text-muted-foreground gap-1 border-dashed"> <lucide-icon [img]="Send" class="text-primary size-3" /> Resend Engine </span> </div> <p ui-card-description class="text-xs"> Live viewport rendering, deliverability telemetry, and RFC-compliant HTML generator. </p> </div> <div class="flex flex-wrap items-center gap-2"> <div class="bg-muted/80 border-border inline-flex max-w-full items-center overflow-x-auto 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> <button type="button" ui-button size="xs" [variant]="viewport() === 'raw' ? 'default' : 'ghost'" class="gap-1.5 text-xs font-medium" (click)="viewport.set('raw')" > <lucide-icon [img]="Code2" class="size-3.5" /> <span>Raw HTML</span> </button> </div> <button ui-button variant="outline" size="sm" class="gap-1.5 text-xs" (click)="isTestModalOpen.set(true)"> <lucide-icon [img]="Send" class="text-primary size-3.5" /> <span>Send Test Email</span> </button> <button ui-button variant="default" size="sm" class="gap-1.5 text-xs" (click)="copyHtmlToClipboard()"> @if (isCopied()) { <lucide-icon [img]="Check" class="text-success size-3.5" /> } @else { <lucide-icon [img]="Copy" class="size-3.5" /> } <span>{{ isCopied() ? 'Copied HTML!' : 'Export HTML' }}</span> </button> </div> </div> </div> <div ui-separator></div> <div ui-card-content class="pt-4"> <div class="grid grid-cols-1 gap-4 lg:grid-cols-2"> <div class="space-y-1.5"> <div class="flex items-center justify-between text-xs"> <label class="text-foreground font-medium">Subject Line</label> <div class="flex items-center gap-1.5"> <span ui-badge [variant]="subjectStatus().variant" class="h-5 px-1.5 text-xs font-normal"> {{ subjectCharCount() }}/60 chars </span> <span ui-badge variant="outline" class="text-muted-foreground h-5 gap-1 px-1.5 text-xs font-normal"> <lucide-icon [img]="Flame" class="text-warning size-3" /> <span>{{ emojiCount() }} emoji</span> </span> </div> </div> <ui-input [value]="subject()" (valueChange)="subject.set($event)" class="text-xs" placeholder="Enter campaign subject line..." /> </div> <div class="space-y-1.5"> <div class="flex items-center justify-between text-xs"> <label class="text-foreground font-medium">Preview Text / Preheader</label> <span ui-badge variant="secondary" class="text-muted-foreground h-5 px-1.5 text-xs font-normal"> {{ preheaderCharCount() }}/90 chars </span> </div> <ui-input [value]="previewText()" (valueChange)="previewText.set($event)" class="text-xs" placeholder="Secondary snippet shown in mail list..." /> </div> </div> </div> </div> <!-- 2-Column Preview Workspace --> <div class="grid grid-cols-1 items-start gap-6 lg:grid-cols-12"> <div class="flex flex-col items-center lg:col-span-8"> <div class="border-border bg-muted/30 relative flex min-h-[720px] w-full flex-col items-center justify-start overflow-hidden rounded-xl border p-4 sm:p-6" > @if (viewport() === 'raw') { <div class="w-full space-y-3"> <div class="flex items-center justify-between"> <div class="flex items-center gap-2"> <lucide-icon [img]="FileCode" class="text-muted-foreground size-4" /> <span class="text-foreground font-mono text-xs font-semibold">template.compiled.html</span> <span ui-badge variant="secondary" class="h-4.5 text-xs">14.2 KB</span> </div> <button ui-button variant="outline" size="xs" class="gap-1 text-xs" (click)="copyHtmlToClipboard()"> @if (isCopied()) { <lucide-icon [img]="Check" class="text-success size-3" /> } @else { <lucide-icon [img]="Copy" class="size-3" /> } <span>{{ isCopied() ? 'Copied' : 'Copy Source' }}</span> </button> </div> <pre class="border-border bg-muted/70 text-foreground max-h-[640px] overflow-auto rounded-lg border p-4 font-mono text-xs leading-relaxed" ><code>{{ rawHtmlOutput() }}</code></pre> </div> } @else { <div [class]="previewCanvasClass"> <div class="border-border bg-card overflow-hidden rounded-xl border shadow-sm"> <div class="border-border bg-muted/50 flex 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 Inbox 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/60 space-y-2 border-b p-4 text-xs"> <div class="flex 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: {{ recipientEmail }}</div> </div> </div> <span class="text-muted-foreground text-xs">Aug 21, 2026, 10:00 AM</span> </div> <div class="pt-1"> <div class="text-foreground text-sm font-semibold tracking-tight">{{ subject() }}</div> <div class="text-muted-foreground line-clamp-1 text-xs">{{ previewText() }}</div> </div> </div> <div class="bg-card space-y-6 p-6 sm:p-8"> <div class="border-border flex items-center justify-between border-b pb-4"> <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</span> </div> <div class="space-y-3"> <span ui-badge variant="outline" class="text-primary border-primary/30 text-xs font-semibold tracking-wider uppercase" > Major Release </span> <h1 class="text-foreground text-2xl leading-tight font-bold tracking-tight sm:text-3xl"> Ship polished UIs in minutes </h1> <p class="text-muted-foreground text-sm leading-relaxed"> We completely rebuilt the registry engine. 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 items-center justify-between gap-2 border-b px-3.5 py-2" > <div class="text-muted-foreground flex min-w-0 items-center gap-1.5 font-mono text-xs"> <lucide-icon [img]="Terminal" class="text-primary size-3 shrink-0" /> <span class="truncate">components/ui/button.tsx</span> </div> <span class="text-success shrink-0 font-mono text-xs font-medium">TypeScript</span> </div> <div class="space-y-3 p-4"> <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">Delete</button> </div> </div> </div> <div class="flex flex-col items-center justify-center space-y-2 pt-2 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="size-4" /> </button> <p class="text-muted-foreground text-xs"> Free & open source · MIT License · 300+ Primitives & Blocks </p> </div> <div ui-separator></div> <div class="space-y-3.5"> <div class="flex items-start gap-3"> <div class="bg-primary/10 text-primary mt-0.5 flex size-6 shrink-0 items-center justify-center rounded-full" > <lucide-icon [img]="Zap" class="size-3.5" /> </div> <div class="space-y-0.5"> <h4 class="text-foreground text-xs font-semibold">Zero Dependency Footprint</h4> <p class="text-muted-foreground text-xs leading-relaxed"> Every component lives directly in your codebase. No semver conflicts or registry lock-in. </p> </div> </div> <div class="flex items-start gap-3"> <div class="bg-primary/10 text-primary mt-0.5 flex size-6 shrink-0 items-center justify-center rounded-full" > <lucide-icon [img]="Palette" class="size-3.5" /> </div> <div class="space-y-0.5"> <h4 class="text-foreground text-xs font-semibold">OKLCH Dark Mode Tokens</h4> <p class="text-muted-foreground text-xs leading-relaxed"> Calibrated light & dark surfaces with verified WCAG AA contrast and fluid spring physics. </p> </div> </div> <div class="flex items-start gap-3"> <div class="bg-primary/10 text-primary mt-0.5 flex size-6 shrink-0 items-center justify-center rounded-full" > <lucide-icon [img]="Layers" class="size-3.5" /> </div> <div class="space-y-0.5"> <h4 class="text-foreground text-xs font-semibold">Dual-Framework Registry</h4> <p class="text-muted-foreground text-xs leading-relaxed"> First-class TypeScript support for both Vue 3 (Reka UI) and React (Radix UI). </p> </div> </div> </div> <div ui-separator></div> <div class="text-muted-foreground space-y-3 pt-2 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</span>. </p> </div> </div> </div> </div> } </div> </div> <!-- Right Column: Quality & Deliverability Inspector --> <div class="space-y-4 lg:col-span-4"> <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-header class="pb-3"> <div class="flex items-center justify-between"> <h3 ui-card-title class="text-sm font-semibold tracking-tight">Spam Score Analyzer</h3> <lucide-icon [img]="ShieldCheck" class="text-success size-4" /> </div> <p ui-card-description class="text-xs">Predictive inbox delivery rating based on content heuristics.</p> </div> <div ui-card-content class="space-y-3.5"> <div class="border-success/20 bg-success/10 rounded-lg border p-3"> <div class="flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5"> <span class="text-success text-2xl font-bold tracking-tight">0.4</span> <span class="text-muted-foreground text-xs font-medium">/ 10.0 scale</span> </div> <div class="text-success mt-1.5 flex items-center gap-1.5 text-xs font-medium"> <lucide-icon [img]="CheckCircle2" class="size-3.5" /> <span>Excellent inbox probability</span> </div> <div class="bg-muted mt-2.5 h-1.5 w-full overflow-hidden rounded-full"> <div class="bg-success h-full w-[96%] rounded-full"></div> </div> </div> <div class="space-y-2 text-xs"> <div class="flex items-center justify-between"> <span class="text-muted-foreground flex items-center gap-1.5"> <lucide-icon [img]="Check" class="text-success size-3.5" /> Spam trigger words </span> <span class="text-foreground font-medium">0 detected</span> </div> <div class="flex items-center justify-between"> <span class="text-muted-foreground flex items-center gap-1.5"> <lucide-icon [img]="Check" class="text-success size-3.5" /> Text-to-image ratio </span> <span class="text-foreground font-medium">92% text (Safe)</span> </div> <div class="flex items-center justify-between"> <span class="text-muted-foreground flex items-center gap-1.5"> <lucide-icon [img]="Check" class="text-success size-3.5" /> RFC 8058 Unsubscribe </span> <span class="text-foreground font-medium">Included</span> </div> <div class="flex items-center justify-between"> <span class="text-muted-foreground flex items-center gap-1.5"> <lucide-icon [img]="Check" class="text-success size-3.5" /> Valid HTTPS links </span> <span class="text-foreground font-medium">6 links checked</span> </div> </div> </div> </div> <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-header class="pb-3"> <h3 ui-card-title class="text-sm font-semibold tracking-tight">Domain Authentication</h3> <p ui-card-description class="text-xs">Security headers verified against DNS records.</p> </div> <div ui-card-content class="space-y-2.5"> <div class="flex items-center justify-between text-xs"> <span class="text-muted-foreground">SPF Record</span> <span ui-badge variant="success" class="h-5 px-2 text-xs">Pass</span> </div> <div class="flex items-center justify-between text-xs"> <span class="text-muted-foreground">DKIM Signature</span> <span ui-badge variant="success" class="h-5 px-2 text-xs">Pass (2048-bit)</span> </div> <div class="flex items-center justify-between text-xs"> <span class="text-muted-foreground">DMARC Policy</span> <span ui-badge variant="success" class="h-5 px-2 text-xs">Pass (p=reject)</span> </div> <div class="flex items-center justify-between text-xs"> <span class="text-muted-foreground">TLS Handshake</span> <span ui-badge variant="outline" class="text-foreground h-5 px-2 text-xs">TLS 1.3 Active</span> </div> </div> </div> <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-header class="pb-3"> <h3 ui-card-title class="text-sm font-semibold tracking-tight">Contrast & Dark Mode</h3> <p ui-card-description class="text-xs">Client dark mode and WCAG contrast conformance.</p> </div> <div ui-card-content class="space-y-2.5 text-xs"> <div class="flex items-center justify-between"> <span class="text-muted-foreground">Dark Mode Contrast</span> <span ui-badge variant="success" class="h-5 px-2 text-xs">100% WCAG AA</span> </div> <div class="flex items-center justify-between"> <span class="text-muted-foreground">Text Contrast Ratio</span> <span class="text-foreground font-semibold">14.8:1 (AAA)</span> </div> <div class="flex items-center justify-between"> <span class="text-muted-foreground">Plaintext Fallback</span> <span class="text-foreground font-medium">Auto-generated</span> </div> </div> </div> <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-header class="pb-3"> <h3 ui-card-title class="text-sm font-semibold tracking-tight">Email Telemetry</h3> <p ui-card-description class="text-xs">Reading metrics and payload optimization.</p> </div> <div ui-card-content class="space-y-2.5 text-xs"> <div class="flex items-center justify-between"> <span class="text-muted-foreground">Estimated Reading Time</span> <span class="text-foreground font-medium">45 sec · 185 words</span> </div> <div class="flex items-center justify-between"> <span class="text-muted-foreground">Payload Size</span> <span class="text-foreground font-medium">14.2 KB (102 KB safe)</span> </div> <div class="flex items-center justify-between"> <span class="text-muted-foreground">Inline Assets</span> <span class="text-foreground font-medium">1 Vector SVG</span> </div> </div> </div> </div> </div> <!-- Send Test Dialog Modal --> <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 Preview </h2> <p ui-dialog-description class="text-xs"> Send a live test message to your inbox to inspect rendering 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">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">Target Preview Profile</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" (click)="selectedClient.set('all')" > 🌐 All Clients </button> <button type="button" ui-button size="xs" [variant]="selectedClient() === 'gmail' ? 'default' : 'outline'" class="justify-start text-xs" (click)="selectedClient.set('gmail')" > ✉️ Gmail (Web) </button> <button type="button" ui-button size="xs" [variant]="selectedClient() === 'apple' ? 'default' : 'outline'" class="justify-start text-xs" (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" (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 email 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() ? 'Sending...' : 'Send Test' }}</span> </button> </div> </ui-dialog-content> </ui-dialog> </div> `, }) export class UiEmailTemplatePreviewerComponent { protected readonly ArrowRight = ArrowRight protected readonly Check = Check protected readonly CheckCircle2 = CheckCircle2 protected readonly Code2 = Code2 protected readonly Copy = Copy protected readonly FileCode = FileCode protected readonly Flame = Flame protected readonly Inbox = Inbox protected readonly Layers = Layers protected readonly Lock = Lock protected readonly Mail = Mail protected readonly Monitor = Monitor protected readonly Palette = Palette protected readonly Send = Send protected readonly ShieldCheck = ShieldCheck protected readonly Smartphone = Smartphone protected readonly Terminal = Terminal protected readonly Zap = Zap protected readonly senderName = 'UIPKGE Team' protected readonly senderEmail = '[email protected]' protected readonly recipientEmail = '[email protected]' @Input('class') className?: string readonly viewport = signal<PreviewerViewport>('desktop') readonly subject = signal('🚀 Introducing UIPKGE 2.0: The unbundled UI registry') readonly previewText = signal( 'Explore 300+ production components with zero npm dependencies. Native Vue 3 & React code.', ) readonly isTestModalOpen = signal(false) readonly testTargetEmail = signal('[email protected]') readonly selectedClient = signal<PreviewerClient>('all') readonly isSendingTest = signal(false) readonly testSentSuccess = signal(false) readonly isCopied = signal(false) private timers: ReturnType<typeof setTimeout>[] = [] constructor() { inject(DestroyRef).onDestroy(() => this.timers.forEach((t) => clearTimeout(t))) } readonly subjectCharCount = computed(() => this.subject().length) readonly preheaderCharCount = computed(() => this.previewText().length) readonly emojiCount = computed(() => { const matches = this.subject().match(/\p{Extended_Pictographic}/gu) return matches ? matches.length : 0 }) readonly subjectStatus = computed(() => { const count = this.subjectCharCount() if (count >= 30 && count <= 60) return { label: 'Optimal length', variant: 'success' as const } if (count < 30) return { label: 'Short subject', variant: 'info' as const } return { label: 'Long subject (may truncate)', variant: 'warning' as const } }) readonly rawHtmlOutput = computed(() => { const subject = this.subject().replace(/"/g, '"') const preview = this.previewText().replace(/"/g, '"') return `<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>${subject}</title> <!--[if mso]> <noscript> <xml> <o:OfficeDocumentSettings> <o:PixelsPerInch>96</o:PixelsPerInch> </o:OfficeDocumentSettings> </xml> </noscript> <![endif]--> <style> body { margin:0; padding:0; background-color:#09090b; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif; color:#fafafa; } table { border-collapse:collapse; } .email-container { max-width:600px; margin:0 auto; background-color:#18181b; border:1px solid #27272a; border-radius:12px; overflow:hidden; } .cta-btn { display:inline-block; background-color:#ffffff; color:#09090b; font-weight:600; padding:12px 28px; border-radius:8px; text-decoration:none; } </style> </head> <body style="margin:0; padding:0; background-color:#09090b; color:#fafafa;"> <!-- Preheader text snippet --> <div style="display:none; max-height:0px; overflow:hidden; font-size:1px; line-height:1px; color:#fff; opacity:0;"> ${preview} </div> <table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color:#09090b;"> <tr> <td align="center" style="padding:40px 16px;"> <table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width:600px; background-color:#18181b; border:1px solid #27272a; border-radius:12px; overflow:hidden;"> <!-- Header Logo Banner --> <tr> <td style="padding:24px 32px; border-bottom:1px solid #27272a;"> <table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%"> <tr> <td align="left" style="font-weight:700; font-size:18px; color:#ffffff; letter-spacing:-0.02em;"> ⚡ UIPKGE </td> <td align="right" style="font-size:12px; color:#a1a1aa;"> Changelog #24 </td> </tr> </table> </td> </tr> <!-- Hero Section --> <tr> <td style="padding:36px 32px 20px 32px;"> <span style="display:inline-block; padding:4px 10px; font-size:11px; font-weight:600; text-transform:uppercase; letter-spacing:0.05em; background-color:rgba(255,255,255,0.08); color:#a1a1aa; border-radius:9999px; margin-bottom:16px;"> Major Release </span> <h1 style="margin:0 0 16px 0; font-size:26px; font-weight:700; line-height:1.25; color:#ffffff; letter-spacing:-0.03em;"> Ship polished UIs in minutes </h1> <p style="margin:0 0 24px 0; font-size:15px; line-height:1.6; color:#a1a1aa;"> We completely rebuilt the registry engine. Zero npm dependencies, 100% code ownership, and full native support for Vue 3 and React with modern OKLCH tokens. </p> </td> </tr> <!-- Code Snippet Card --> <tr> <td style="padding:0 32px 24px 32px;"> <table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color:#09090b; border:1px solid #27272a; border-radius:8px;"> <tr> <td style="padding:10px 16px; border-bottom:1px solid #27272a; font-family:monospace; font-size:11px; color:#71717a;"> Terminal • Quick Install </td> </tr> <tr> <td style="padding:14px 16px; font-family:monospace; font-size:13px; color:#38bdf8;"> npx shadcn-vue add @uipkge/button </td> </tr> </table> </td> </tr> <!-- Primary CTA Button --> <tr> <td align="center" style="padding:12px 32px 32px 32px;"> <table role="presentation" border="0" cellpadding="0" cellspacing="0"> <tr> <td align="center" style="border-radius:8px; background-color:#ffffff;"> <a href="https://uipkge.dev" target="_blank" style="font-size:14px; font-weight:600; color:#09090b; text-decoration:none; padding:12px 32px; display:inline-block; border-radius:8px;"> Explore Components → </a> </td> </tr> </table> </td> </tr> <!-- Highlights List --> <tr> <td style="padding:24px 32px; border-top:1px solid #27272a; background-color:rgba(255,255,255,0.02);"> <table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%"> <tr> <td style="padding:8px 0;"> <strong style="color:#ffffff; font-size:14px;">⚡ Zero Dependency Footprint:</strong> <span style="color:#a1a1aa; font-size:14px;"> Components live directly in your code.</span> </td> </tr> <tr> <td style="padding:8px 0;"> <strong style="color:#ffffff; font-size:14px;">🎨 OKLCH Dark Mode Tokens:</strong> <span style="color:#a1a1aa; font-size:14px;"> Fluid WCAG AA contrast calibrated for all themes.</span> </td> </tr> <tr> <td style="padding:8px 0;"> <strong style="color:#ffffff; font-size:14px;">🧩 Dual-Framework Registry:</strong> <span style="color:#a1a1aa; font-size:14px;"> Full TypeScript parity for Vue 3 and React.</span> </td> </tr> </table> </td> </tr> <!-- Footer --> <tr> <td style="padding:28px 32px; border-top:1px solid #27272a; text-align:center;"> <p style="margin:0 0 10px 0; font-size:12px; color:#71717a;"> UIPKGE Inc. • 548 Market St, Suite 29314, San Francisco, CA 94104 </p> <p style="margin:0; font-size:12px; color:#71717a;"> <a href="https://uipkge.dev" style="color:#a1a1aa; text-decoration:underline;">Docs</a> • <a href="https://github.com/uipkge" style="color:#a1a1aa; text-decoration:underline;">GitHub</a> • <a href="#unsubscribe" style="color:#a1a1aa; text-decoration:underline;">Unsubscribe</a> </p> </td> </tr> </table> </td> </tr> </table> </body> </html>` }) 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-[600px]' : 'max-w-[375px]', ) } async copyHtmlToClipboard(): Promise<void> { try { await navigator.clipboard.writeText(this.rawHtmlOutput()) this.isCopied.set(true) this.timers.push( setTimeout(() => { this.isCopied.set(false) }, 2000), ) } catch { // Clipboard unavailable (jsdom): leave the button state untouched. } } 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) }, 2000), ) }, 700), ) } }src/app/components/blocks/email-template-previewer/index.ts0.1 kB
export { UiEmailTemplatePreviewerComponent, type PreviewerClient, type PreviewerViewport, } from './email-template-previewer.component'
Raw manifest:https://uipkge.dev/r/angular/email-template-previewer.json