UIPackage
Menu

Framework

Change language

Boilerplate repo

Newsletter Signup

blockmarketing

Engineering dispatch and release radar workbench with channel tracks (Architecture, Releases, Security), live archive previews, and subscriber metrics.

Also available for React ->

Installation

$npx shadcn-vue@latest add https://uipkge.dev/r/vue/newsletter-signup.json
Named registry:npx shadcn-vue@latest add @uipkge/newsletter-signupInstalls to:app/components/blocks/

Variants

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
variant
'centered''split'
'split'optional

Schema

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

ChannelOption
interface ChannelOption {
  id: string
  title: string
  frequency: string
  description: string
  badge: string
}

Files installed (1)

  • app/components/blocks/NewsletterSignup.vue10 kB
    <script setup lang="ts">
    import { ref } from 'vue'
    import { ArrowRight, BellRing, BookOpen, Check, Mail, Send } 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 { Input } from '@/components/ui/input'
    import { Separator } from '@/components/ui/separator'
    
    interface Props {
      variant?: 'centered' | 'split'
    }
    
    const props = withDefaults(defineProps<Props>(), {
      variant: 'split',
    })
    
    interface ChannelOption {
      id: string
      title: string
      frequency: string
      description: string
      badge: string
    }
    
    const channels: 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',
      },
    ]
    
    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.',
      },
    ]
    
    const email = ref('')
    const selectedChannel = ref('architecture')
    const submitted = ref(false)
    const activePreviewIssue = ref(0)
    const isSubmitting = ref(false)
    
    function handleSubmit() {
      if (!email.value || !email.value.includes('@')) return
      isSubmitting.value = true
      setTimeout(() => {
        isSubmitting.value = false
        submitted.value = true
      }, 400)
    }
    </script>
    
    <template>
      <section data-slot="newsletter-signup" class="bg-background relative overflow-hidden px-4 py-16 sm:px-6 lg:px-8">
        <div class="mx-auto max-w-6xl space-y-10">
          <!-- Section Header -->
          <div class="max-w-3xl space-y-3">
            <Badge variant="secondary" class="gap-1.5 px-3 py-1 font-mono text-xs shadow-xs">
              <Mail class="text-primary size-3.5" />
              Engineering Dispatch & Architecture Radar
            </Badge>
            <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">
              <Card class="border-border bg-card overflow-hidden shadow-sm">
                <CardHeader class="bg-muted/20 border-border/60 border-b pb-4">
                  <div class="flex items-center justify-between">
                    <CardTitle class="flex items-center gap-2 text-base font-semibold">
                      <BellRing class="text-primary size-4" />
                      Select Dispatch Track
                    </CardTitle>
                    <span class="text-muted-foreground font-mono text-xs">Zero Spam Guarantee</span>
                  </div>
                  <CardDescription class="text-xs">
                    Choose the telemetry streams you want delivered to your inbox.
                  </CardDescription>
                </CardHeader>
    
                <CardContent class="space-y-5 p-6">
                  <!-- Channel Radio Cards -->
                  <div class="space-y-2.5">
                    <div
                      v-for="ch in channels"
                      :key="ch.id"
                      class="group cursor-pointer rounded-xl border p-3.5 transition-colors"
                      :class="
                        selectedChannel === ch.id
                          ? 'border-primary bg-primary/5 ring-primary/20 shadow-xs ring-1'
                          : 'border-border bg-background hover:bg-muted/40'
                      "
                      @click="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="selectedChannel === ch.id ? 'border-primary bg-primary' : 'border-muted-foreground/40'"
                          >
                            <div v-if="selectedChannel === ch.id" class="bg-primary-foreground size-1.5 rounded-full" />
                          </div>
                          <span class="text-foreground text-xs font-semibold">{{ ch.title }}</span>
                        </div>
                        <Badge variant="outline" class="font-mono text-xs">
                          {{ ch.frequency }}
                        </Badge>
                      </div>
                      <p class="text-muted-foreground mt-1.5 pl-5.5 text-xs leading-relaxed">
                        {{ ch.description }}
                      </p>
                    </div>
                  </div>
    
                  <Separator />
    
                  <!-- Dispatch Subscription Input -->
                  <form v-if="!submitted" class="space-y-3" @submit.prevent="handleSubmit">
                    <div class="flex flex-col gap-2 sm:flex-row">
                      <Input
                        v-model="email"
                        type="email"
                        placeholder="[email protected]"
                        required
                        class="h-10 flex-1 font-mono text-xs"
                        aria-label="Email address for dispatch"
                      />
                      <Button type="submit" :disabled="isSubmitting" class="h-10 gap-1.5 px-5 text-xs font-semibold">
                        <span>Subscribe to Stream</span>
                        <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 &bull; 1-click unsubscribe</span>
                      <span class="text-success font-semibold">18,412 Active Subscribers</span>
                    </div>
                  </form>
    
                  <!-- Subscription Success View -->
                  <div v-else 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">
                      <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>
                </CardContent>
              </Card>
            </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">
                  <BookOpen class="text-primary size-3.5" /> Archive Sampler
                </p>
                <span class="text-muted-foreground font-mono text-xs"
                  >Issue #{{ recentIssues[activePreviewIssue].id }}</span
                >
              </div>
    
              <!-- Interactive Issue Cards -->
              <div class="space-y-3">
                <div
                  v-for="(issue, idx) in recentIssues"
                  :key="issue.id"
                  class="bg-card cursor-pointer rounded-xl border p-4 transition-colors"
                  :class="
                    activePreviewIssue === idx ? 'border-primary/50 shadow-xs' : 'border-border hover:border-border/80'
                  "
                  @click="activePreviewIssue = idx"
                >
                  <div class="text-muted-foreground mb-1.5 flex items-center justify-between font-mono text-xs">
                    <span>{{ issue.date }} &bull; 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">
                    <Badge v-for="tag in issue.tags" :key="tag" variant="secondary" class="px-1.5 py-0 font-mono text-xs">
                      #{{ tag }}
                    </Badge>
                  </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 <ArrowRight class="size-3" />
                </a>
              </div>
            </div>
          </div>
        </div>
      </section>
    </template>
    

Raw manifest:https://uipkge.dev/r/vue/newsletter-signup.json