Newsletter Signup
blockmarketingEngineering dispatch and release radar workbench with channel tracks (Architecture, Releases, Security), live archive previews, and subscriber metrics.
Installation
$pnpm dlx uipkge-ng@latest add newsletter-signup$npx uipkge-ng@latest add newsletter-signup$yarn dlx uipkge-ng@latest add newsletter-signup$bunx uipkge-ng@latest add newsletter-signupManifest:
https://uipkge.dev/r/angular/newsletter-signup.jsonInstalls to:src/app/components/blocks/newsletter-signup/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
ChannelOptioninterface ChannelOption {
id: string
title: string
frequency: string
description: string
badge: string
}Files installed (2)
src/app/components/blocks/newsletter-signup/newsletter-signup.component.ts12.9 kB
// Engineering dispatch signup: channel track selector, live dispatch form with // success state, archive sampler, and RSS note. Port of the Vue/React block 1:1. import { ChangeDetectionStrategy, Component, Input, signal } from '@angular/core' import { ArrowRight, BellRing, BookOpen, Check, LucideAngularModule, Mail, Send } 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 { UiInputComponent } from '@/ui/input/input.component' import { UiSeparatorComponent } from '@/ui/separator/separator.component' export type NewsletterSignupVariant = 'centered' | 'split' interface ChannelOption { id: string title: string frequency: string description: string badge: string } export const newsletterChannels: ChannelOption[] = [ { id: 'architecture', title: 'Architecture & Token Deep Dives', frequency: 'Bi-Weekly', description: 'Technical breakdowns on OKLCH color science, headless AST transformers, and zero-lockin paradigms.', badge: 'Flagship', }, { id: 'releases', title: 'Registry Ship Notes & RFCs', frequency: 'Monthly', description: 'Direct changelogs, new workbench releases, breaking primitive refactors, and roadmap discussions.', badge: 'Changelog', }, { id: 'security', title: 'Security & Dependency Bulletins', frequency: 'As-Needed', description: 'Zero-day vulnerability alerts, upstream headless primitive fixes, and patch notifications.', badge: 'Critical', }, ] export const recentIssues = [ { id: 48, date: 'Aug 2026', title: 'The Death of Monolithic npm UI Packages', reads: '4 min read', tags: ['Architecture', 'Registry', 'AST'], snippet: 'Why the next decade of frontend engineering belongs to unbundled registries where the consumer owns the source code.', }, { id: 47, date: 'Jul 2026', title: 'OKLCH Theming in Tailwind CSS v4', reads: '6 min read', tags: ['Tailwind v4', 'Color Science', 'Tokens'], snippet: 'Mastering perceptual lightness, chroma gamut mapping, and dynamic theme switching without runtime CSS bloat.', }, ] @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-newsletter-signup, [ui-newsletter-signup]', standalone: true, // The block renders its own root <section>: keep the host out of layout. host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, UiInputComponent, UiSeparatorComponent, ], template: ` <section data-slot="newsletter-signup" [class]="rootClass"> <div class="mx-auto max-w-6xl space-y-10"> <!-- Section Header --> <div class="max-w-3xl space-y-3"> <span ui-badge variant="secondary" class="gap-1.5 px-3 py-1 font-mono text-xs shadow-xs"> <lucide-icon [img]="Mail" class="text-primary size-3.5" /> Engineering Dispatch & Architecture Radar </span> <h2 class="text-foreground text-3xl font-bold tracking-tight sm:text-4xl"> Ship notes, not marketing fluff. </h2> <p class="text-muted-foreground text-sm leading-relaxed sm:text-base"> Join 18,400+ frontend architects, design engineers, and systems builders. Plain-text insights into component architecture, token systems, and dual-framework engineering. </p> </div> <!-- Main Layout --> <div class="grid grid-cols-1 items-start gap-8 lg:grid-cols-12"> <!-- Left Column: Channel Selector & Live Dispatch Form (7 Cols) --> <div class="space-y-6 lg:col-span-7"> <div ui-card class="border-border bg-card overflow-hidden shadow-sm"> <div ui-card-header class="bg-muted/20 border-border/60 border-b pb-4"> <div class="flex items-center justify-between"> <h3 ui-card-title class="flex items-center gap-2 text-base font-semibold"> <lucide-icon [img]="BellRing" class="text-primary size-4" /> Select Dispatch Track </h3> <span class="text-muted-foreground font-mono text-xs">Zero Spam Guarantee</span> </div> <p ui-card-description class="text-xs"> Choose the telemetry streams you want delivered to your inbox. </p> </div> <div ui-card-content class="space-y-5 p-6"> <!-- Channel Radio Cards --> <div class="space-y-2.5"> @for (ch of channels; track ch.id) { <div class="group cursor-pointer rounded-xl border p-3.5 transition-colors" [class]="channelCardClass(ch.id)" (click)="selectedChannel.set(ch.id)" (keydown.enter)="selectedChannel.set(ch.id)" tabindex="0" role="radio" [attr.aria-checked]="selectedChannel() === ch.id" > <div class="flex items-center justify-between gap-2"> <div class="flex items-center gap-2"> <div class="flex size-3.5 items-center justify-center rounded-full border transition-colors" [class]="channelDotClass(ch.id)" > @if (selectedChannel() === ch.id) { <div class="bg-primary-foreground size-1.5 rounded-full"></div> } </div> <span class="text-foreground text-xs font-semibold">{{ ch.title }}</span> </div> <span ui-badge variant="outline" class="font-mono text-xs">{{ ch.frequency }}</span> </div> <p class="text-muted-foreground mt-1.5 pl-5.5 text-xs leading-relaxed">{{ ch.description }}</p> </div> } </div> <ui-separator /> <!-- Dispatch Subscription Input --> @if (!submitted()) { <form class="space-y-3" (submit)="handleSubmit($event)"> <div class="flex flex-col gap-2 sm:flex-row"> <ui-input [value]="email()" (valueChange)="email.set($event)" type="email" placeholder="architect@company.com" required class="h-10 flex-1 font-mono text-xs" aria-label="Email address for dispatch" /> <button ui-button type="submit" [disabled]="isSubmitting()" class="h-10 gap-1.5 px-5 text-xs font-semibold" > <span>Subscribe to Stream</span> <lucide-icon [img]="Send" class="size-3.5" /> </button> </div> <div class="text-muted-foreground flex items-center justify-between pt-1 font-mono text-xs"> <span>Plain text • 1-click unsubscribe</span> <span class="text-success font-semibold">18,412 Active Subscribers</span> </div> </form> } @else { <div class="border-success/20 bg-success/10 space-y-2 rounded-xl border p-5 text-center"> <div class="bg-success/20 text-success mx-auto flex size-9 items-center justify-center rounded-full" > <lucide-icon [img]="Check" class="size-5" /> </div> <p class="text-foreground text-sm font-semibold">Dispatches Activated!</p> <p class="text-muted-foreground text-xs"> Verification sent to <strong class="text-foreground font-mono">{{ email() }}</strong >. Check your inbox to confirm delivery. </p> </div> } </div> </div> </div> <!-- Right Column: Recent Issue Sampler & Archives (5 Cols) --> <div class="space-y-4 lg:col-span-5"> <div class="flex items-center justify-between"> <p class="text-muted-foreground flex items-center gap-1.5 font-mono text-xs tracking-wider uppercase"> <lucide-icon [img]="BookOpen" class="text-primary size-3.5" /> Archive Sampler </p> <span class="text-muted-foreground font-mono text-xs">Issue #{{ issues[activePreviewIssue()].id }}</span> </div> <!-- Interactive Issue Cards --> <div class="space-y-3"> @for (issue of issues; track issue.id; let idx = $index) { <div class="bg-card cursor-pointer rounded-xl border p-4 transition-colors" [class]="previewCardClass(idx)" (click)="activePreviewIssue.set(idx)" (keydown.enter)="activePreviewIssue.set(idx)" tabindex="0" role="button" [attr.aria-pressed]="activePreviewIssue() === idx" > <div class="text-muted-foreground mb-1.5 flex items-center justify-between font-mono text-xs"> <span>{{ issue.date }} • Issue #{{ issue.id }}</span> <span class="text-foreground font-medium">{{ issue.reads }}</span> </div> <h3 class="text-foreground mb-1.5 text-sm leading-snug font-bold">{{ issue.title }}</h3> <p class="text-muted-foreground text-xs leading-relaxed">{{ issue.snippet }}</p> <div class="mt-3 flex flex-wrap gap-1.5"> @for (tag of issue.tags; track tag) { <span ui-badge variant="secondary" class="px-1.5 py-0 font-mono text-xs">#{{ tag }}</span> } </div> </div> } </div> <!-- RSS / Mastodon / CLI note --> <div class="border-border bg-muted/20 text-muted-foreground flex items-center justify-between rounded-lg border p-3.5 text-xs" > <span class="font-mono">Prefer RSS feeds?</span> <a href="https://uipkge.dev/rss.xml" target="_blank" rel="noreferrer" class="text-primary inline-flex items-center gap-1 font-mono hover:underline" > uipkge.dev/rss.xml <lucide-icon [img]="ArrowRight" class="size-3" /> </a> </div> </div> </div> </div> </section> `, }) export class UiNewsletterSignupComponent { protected readonly ArrowRight = ArrowRight protected readonly BellRing = BellRing protected readonly BookOpen = BookOpen protected readonly Check = Check protected readonly Mail = Mail protected readonly Send = Send protected readonly channels = newsletterChannels protected readonly issues = recentIssues @Input() variant: NewsletterSignupVariant = 'split' @Input('class') className?: string readonly email = signal('') readonly selectedChannel = signal('architecture') readonly submitted = signal(false) readonly activePreviewIssue = signal(0) readonly isSubmitting = signal(false) get rootClass(): string { return cn('bg-background relative overflow-hidden px-4 py-16 sm:px-6 lg:px-8', this.className) } channelCardClass(id: string): string { return cn( this.selectedChannel() === id ? 'border-primary bg-primary/5 ring-primary/20 shadow-xs ring-1' : 'border-border bg-background hover:bg-muted/40', ) } channelDotClass(id: string): string { return cn(this.selectedChannel() === id ? 'border-primary bg-primary' : 'border-muted-foreground/40') } previewCardClass(idx: number): string { return cn( this.activePreviewIssue() === idx ? 'border-primary/50 shadow-xs' : 'border-border hover:border-border/80', ) } handleSubmit(event?: Event): void { event?.preventDefault() if (!this.email() || !this.email().includes('@')) return this.isSubmitting.set(true) setTimeout(() => { this.isSubmitting.set(false) this.submitted.set(true) }, 400) } }src/app/components/blocks/newsletter-signup/index.ts0.1 kB
export { UiNewsletterSignupComponent, type NewsletterSignupVariant, newsletterChannels, recentIssues, } from './newsletter-signup.component'
Raw manifest:https://uipkge.dev/r/angular/newsletter-signup.json