Menu

Change language

GitHub repo

Coming Soon

blockmarketing

Interactive launch and maintenance workbench with high-impact countdown, VIP early access waitlist queue allocator, live launch roadmap progress tracker, and feature sneak-peek previews.

Installation

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

Variants

Loading interactive previews…

Schema

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

ComingSoonMilestone
interface ComingSoonMilestone {
  id: string
  title: string
  category: string
  status: 'completed' | 'in_progress' | 'scheduled'
  completionPercent: number
  date: string
}
FeatureTeaser
interface FeatureTeaser {
  id: string
  badge: string
  title: string
  description: string
  icon: LucideIconData
  statLabel: string
  statValue: string
  codeSnippet: string
}

Files installed (2)

  • src/app/components/blocks/coming-soon/coming-soon.component.ts25.2 kB
    // Interactive launch and maintenance workbench with high-impact countdown, VIP early
    // access waitlist queue allocator, live launch roadmap progress tracker, and feature
    // sneak-peek previews. Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, computed, Input, OnDestroy, OnInit, signal } from '@angular/core'
    import {
      Activity,
      ArrowRight,
      ArrowUpRight,
      Bot,
      Check,
      CheckCircle2,
      Copy,
      LucideAngularModule,
      Palette,
      Rocket,
      ShieldCheck,
      Terminal,
      Zap,
      type LucideIconData,
    } 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 { UiProgressComponent } from '@/ui/progress/progress.component'
    import { UiSeparatorComponent } from '@/ui/separator/separator.component'
    
    function pad(value: number): string {
      return String(value).padStart(2, '0')
    }
    
    export interface ComingSoonMilestone {
      id: string
      title: string
      category: string
      status: 'completed' | 'in_progress' | 'scheduled'
      completionPercent: number
      date: string
    }
    
    const initialMilestones: ComingSoonMilestone[] = [
      {
        id: 'm1',
        title: 'Dual-Framework Compiler & Monorepo Architecture',
        category: 'Core Infra',
        status: 'completed',
        completionPercent: 100,
        date: 'Aug 15, 2026',
      },
      {
        id: 'm2',
        title: 'Tailwind CSS v4 OKLCH Dynamic Theming Engine',
        category: 'Design System',
        status: 'completed',
        completionPercent: 100,
        date: 'Aug 18, 2026',
      },
      {
        id: 'm3',
        title: 'Zero-Lockin Code Distribution Registry & CLI',
        category: 'DX & Tooling',
        status: 'completed',
        completionPercent: 100,
        date: 'Aug 21, 2026',
      },
      {
        id: 'm4',
        title: 'Multi-Region Edge Registry Cache & Global SLA Testing',
        category: 'Infrastructure',
        status: 'in_progress',
        completionPercent: 88,
        date: 'Target: 3 Days',
      },
      {
        id: 'm5',
        title: 'Public V2.0 Global General Availability Rollout',
        category: 'Release',
        status: 'scheduled',
        completionPercent: 0,
        date: 'Target: 7 Days',
      },
    ]
    
    export interface FeatureTeaser {
      id: string
      badge: string
      title: string
      description: string
      icon: LucideIconData
      statLabel: string
      statValue: string
      codeSnippet: string
    }
    
    const teasers: FeatureTeaser[] = [
      {
        id: 'agentic',
        badge: 'Flagship AI',
        title: 'Autonomous Multi-Agent Orchestrator',
        description: 'Decompose complex workflow DAGs into parallel subagent execution with real-time vector memory.',
        icon: Bot,
        statLabel: 'DAG Execution Speed',
        statValue: '120ms P99',
        codeSnippet: 'npx shadcn-vue@latest add @uipkge/ai-agent-orchestrator',
      },
      {
        id: 'design',
        badge: 'Design Engineering',
        title: 'Tailwind v4 OKLCH Fluid Theme Tokens',
        description: 'Perceptually uniform color spaces with automatic high-contrast dark mode switching & zero CSS bloat.',
        icon: Palette,
        statLabel: 'Bundle Size Overhead',
        statValue: '0.00 kB',
        codeSnippet: 'npx shadcn-vue@latest add @uipkge/theme-customize',
      },
      {
        id: 'edge',
        badge: 'Ultra Low Latency',
        title: 'Cloudflare Workers Multi-Region Cache',
        description: 'Global component manifest resolution with smart geo-routing and edge-cached JSON trees.',
        icon: Zap,
        statLabel: 'Global Median Latency',
        statValue: '8.4ms',
        codeSnippet: 'curl -s https://uipkge.dev/r/vue/init.json',
      },
    ]
    
    const roleOptions = [
      { id: 'frontend', label: 'Frontend / UI' },
      { id: 'architect', label: 'Tech Lead / Arch' },
      { id: 'founder', label: 'CTO / Founder' },
    ] as const
    
    export type WaitlistRole = (typeof roleOptions)[number]['id']
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-coming-soon, [ui-coming-soon]',
      standalone: true,
      // React renders the root <section> 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,
        UiInputComponent,
        UiProgressComponent,
        UiSeparatorComponent,
      ],
      template: `
        <section data-slot="coming-soon" [class]="rootClass">
          <!-- Top Telemetry & Status Bar -->
          <div
            class="border-border/70 mx-auto flex w-full max-w-6xl flex-col items-center justify-between gap-4 border-b pb-12 sm:flex-row"
          >
            <div class="flex items-center gap-3">
              <div
                class="bg-primary text-primary-foreground flex size-9 items-center justify-center rounded-xl font-bold shadow-xs"
              >
                <lucide-icon [img]="Rocket" class="size-5" />
              </div>
              <div>
                <div class="flex items-center gap-2">
                  <span class="text-foreground text-sm font-semibold tracking-tight">UIPKGE Engine</span>
                  <span ui-badge variant="outline" class="border-primary/30 text-primary px-1.5 py-0.5 font-mono text-xs">
                    {{ productVersion }}
                  </span>
                </div>
                <p class="text-muted-foreground text-xs">Dual-framework registry & visual workbench suite</p>
              </div>
            </div>
    
            <!-- Global SLA Readiness Pill -->
            <div class="bg-muted/40 border-border flex items-center gap-3 rounded-full border px-3.5 py-1.5">
              <div class="bg-success flex size-2 animate-pulse rounded-full"></div>
              <span class="text-muted-foreground font-mono text-xs">
                Staging Infrastructure: <strong class="text-foreground font-semibold">99.99% Operational</strong>
              </span>
              <div ui-separator orientation="vertical" class="h-3.5"></div>
              <a
                [href]="statusUrl"
                target="_blank"
                rel="noreferrer"
                class="text-primary inline-flex items-center gap-1 font-mono text-xs hover:underline"
              >
                Live Status <lucide-icon [img]="ArrowUpRight" class="size-3" />
              </a>
            </div>
          </div>
    
          <!-- Main Hero & Countdown Body -->
          <div class="mx-auto my-auto w-full max-w-6xl space-y-16 py-10">
            <!-- Headline & Subtitle -->
            <div class="mx-auto max-w-3xl space-y-4 text-center">
              <span ui-badge variant="secondary" class="gap-1.5 px-3 py-1 font-mono text-xs shadow-xs">
                <lucide-icon [img]="Rocket" class="text-primary size-3.5" />
                V2.0 General Availability Launch Matrix
              </span>
              <h1 class="text-foreground text-4xl leading-[1.1] font-bold tracking-tight sm:text-5xl lg:text-6xl">
                Production-grade design engineering workbenches.
              </h1>
              <p class="text-muted-foreground mx-auto max-w-2xl text-base leading-relaxed sm:text-lg">
                We are deploying 281+ production-grade primitives and composed workbenches with zero package dependencies.
                Own your source code with complete architectural freedom.
              </p>
            </div>
    
            <!-- High-Impact Digital Countdown Grid -->
            <div class="mx-auto grid max-w-2xl grid-cols-2 gap-4 sm:grid-cols-4">
              @for (unit of units(); track unit.label) {
                <div
                  ui-card
                  class="border-border bg-card/70 group hover:border-primary/40 relative overflow-hidden p-5 text-center shadow-xs backdrop-blur-xs transition-colors"
                >
                  <div class="text-foreground font-mono text-4xl font-bold tracking-tight tabular-nums sm:text-5xl">
                    {{ unit.value }}
                  </div>
                  <div class="text-muted-foreground mt-2 font-mono text-xs font-medium tracking-widest uppercase">
                    {{ unit.label }}
                  </div>
                  <div
                    class="bg-primary/20 group-hover:bg-primary absolute right-0 bottom-0 left-0 h-0.5 transition-colors"
                  ></div>
                </div>
              }
            </div>
    
            <!-- Early Access & Queue Tracker Section -->
            <div class="mx-auto w-full max-w-xl">
              <div ui-card class="border-border bg-card overflow-hidden shadow-sm">
                <div ui-card-header class="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]="ShieldCheck" class="text-primary size-4" />
                      Early Access Allocation
                    </h3>
                    <span ui-badge variant="outline" class="border-success/20 bg-success/10 text-success font-mono text-xs">
                      Batch #2 Opening Soon
                    </span>
                  </div>
                  <p ui-card-description class="text-xs">
                    Request priority invite access. Get instant sandbox access to all 281+ blocks before general public
                    launch.
                  </p>
                </div>
    
                <div ui-card-content>
                  @if (!submitted()) {
                    <form class="space-y-4" (submit)="onWaitlistSubmit($event)">
                      <!-- Role Selector Tabs -->
                      <div class="space-y-1.5">
                        <label class="text-foreground text-xs font-medium">Your Primary Engineering Focus</label>
                        <div class="grid grid-cols-3 gap-2">
                          @for (r of roles; track r.id) {
                            <button
                              type="button"
                              [class]="roleClass(r.id)"
                              (click)="role.set(r.id)"
                            >
                              {{ r.label }}
                            </button>
                          }
                        </div>
                      </div>
    
                      <!-- Email Input & Submit -->
                      <div class="flex flex-col gap-2 sm:flex-row">
                        <div class="relative flex-1">
                          <ui-input
                            [value]="email()"
                            (valueChange)="email.set($event)"
                            type="email"
                            placeholder="you@company.com"
                            required
                            class="h-10 font-mono text-xs"
                          />
                        </div>
                        <button ui-button type="submit" class="h-10 gap-1.5 px-5 text-xs font-semibold">
                          <span>Reserve Spot</span>
                          <lucide-icon [img]="ArrowRight" class="size-3.5" />
                        </button>
                      </div>
                      <p class="text-muted-foreground text-center text-xs">
                        No spam. You will only receive your single cryptographic invite token.
                      </p>
                    </form>
                  } @else {
                    <div class="bg-muted/40 border-border space-y-4 rounded-xl border p-4 text-center">
                      <div class="bg-success/10 text-success mx-auto flex size-10 items-center justify-center rounded-full">
                        <lucide-icon [img]="Check" class="size-5" />
                      </div>
                      <div class="space-y-1">
                        <p class="text-foreground text-sm font-semibold">You are #{{ queuePosition() }} in queue!</p>
                        <p class="text-muted-foreground text-xs">
                          Invitation tokens for Batch #2 will be delivered to
                          <strong class="text-foreground font-mono">{{ email() }}</strong
                          >.
                        </p>
                      </div>
    
                      <div class="border-border flex flex-col items-center justify-between gap-3 border-t pt-2 sm:flex-row">
                        <span class="text-muted-foreground font-mono text-xs">Move up 5 spots per teammate invite:</span>
                        <button
                          ui-button
                          variant="outline"
                          size="sm"
                          class="h-8 gap-1.5 font-mono text-xs"
                          (click)="copyReferral()"
                        >
                          @if (copiedReferral()) {
                            <lucide-icon [img]="Check" class="text-success size-3" />
                          } @else {
                            <lucide-icon [img]="Copy" class="size-3" />
                          }
                          <span>{{ copiedReferral() ? 'Copied Link' : 'Copy Invite Link' }}</span>
                        </button>
                      </div>
                    </div>
                  }
                </div>
              </div>
            </div>
    
            <!-- Interactive Feature Sneak-Peek Carousel & Terminal Preview -->
            <div class="space-y-6">
              <div class="flex flex-col justify-between gap-4 sm:flex-row sm:items-center">
                <div>
                  <h2 class="text-foreground flex items-center gap-2 text-lg font-semibold tracking-tight">
                    <lucide-icon [img]="Zap" class="text-primary size-4" />
                    Incoming Flagship Capabilities
                  </h2>
                  <p class="text-muted-foreground text-xs">Sneak peek preview of the upcoming architecture release</p>
                </div>
    
                <!-- Teaser Navigation Tabs -->
                <div class="bg-muted/40 border-border flex items-center gap-1.5 rounded-lg border p-1">
                  @for (t of teasersList; track t.id; let idx = $index) {
                    <button
                      type="button"
                      [class]="teaserTabClass(idx)"
                      (click)="activeTeaserIndex.set(idx)"
                    >
                      {{ t.badge }}
                    </button>
                  }
                </div>
              </div>
    
              <!-- Active Teaser Card -->
              <div ui-card class="border-border bg-card overflow-hidden">
                <div class="divide-border grid grid-cols-1 divide-y lg:grid-cols-12 lg:divide-x lg:divide-y-0">
                  <div class="space-y-4 p-6 sm:p-8 lg:col-span-7">
                    <div class="flex items-center gap-2.5">
                      <div class="bg-primary/10 text-primary rounded-lg p-2">
                        <lucide-icon [img]="activeTeaser().icon" class="size-5" />
                      </div>
                      <span ui-badge variant="secondary" class="font-mono text-xs">
                        {{ activeTeaser().badge }}
                      </span>
                    </div>
    
                    <h3 class="text-foreground text-xl font-bold tracking-tight">{{ activeTeaser().title }}</h3>
                    <p class="text-muted-foreground text-xs leading-relaxed sm:text-sm">
                      {{ activeTeaser().description }}
                    </p>
    
                    <div class="flex items-center gap-6 pt-2">
                      <div>
                        <p class="text-muted-foreground font-mono text-xs">{{ activeTeaser().statLabel }}</p>
                        <p class="text-foreground mt-0.5 font-mono text-xl font-bold">
                          {{ activeTeaser().statValue }}
                        </p>
                      </div>
                      <div ui-separator orientation="vertical" class="h-8"></div>
                      <div>
                        <p class="text-muted-foreground font-mono text-xs">Framework Support</p>
                        <p class="text-success mt-1 text-xs font-semibold">Vue 3.5 & React 19 Parity</p>
                      </div>
                    </div>
                  </div>
    
                  <!-- Terminal Snippet Box -->
                  <div class="bg-muted/20 flex flex-col justify-between space-y-4 p-6 lg:col-span-5">
                    <div class="space-y-2">
                      <div class="flex items-center justify-between">
                        <span class="text-muted-foreground flex items-center gap-1.5 font-mono text-xs font-medium">
                          <lucide-icon [img]="Terminal" class="size-3.5" /> Direct CLI Command
                        </span>
                        <button
                          ui-button
                          variant="ghost"
                          size="sm"
                          class="h-7 gap-1.5 px-2 font-mono text-xs"
                          (click)="copyTeaserSnippet(activeTeaser().codeSnippet)"
                        >
                          @if (copiedCli()) {
                            <lucide-icon [img]="Check" class="text-success size-3" />
                          } @else {
                            <lucide-icon [img]="Copy" class="size-3" />
                          }
                          <span>{{ copiedCli() ? 'Copied' : 'Copy' }}</span>
                        </button>
                      </div>
                      <div
                        class="bg-background border-border text-foreground selection:bg-primary/20 overflow-x-auto rounded-lg border p-3 font-mono text-xs"
                      >
                        <code>{{ activeTeaser().codeSnippet }}</code>
                      </div>
                    </div>
    
                    <div
                      class="border-border/80 bg-background/50 text-muted-foreground flex items-center gap-2 rounded-lg border p-3 text-xs"
                    >
                      <lucide-icon [img]="CheckCircle2" class="text-success size-3.5 shrink-0" />
                      <span>Transitive dependencies, OKLCH styles & TS types included.</span>
                    </div>
                  </div>
                </div>
              </div>
            </div>
    
            <!-- Launch Readiness Milestones Tracker -->
            <div class="border-border/70 space-y-6 border-t pt-4">
              <div class="flex flex-col justify-between gap-4 sm:flex-row sm:items-center">
                <div>
                  <h2 class="text-foreground flex items-center gap-2 text-lg font-semibold tracking-tight">
                    <lucide-icon [img]="Activity" class="text-primary size-4" />
                    V2.0 Launch Milestones & Engineering Progress
                  </h2>
                  <p class="text-muted-foreground text-xs">Transparent real-time build and release status</p>
                </div>
                <div class="flex items-center gap-3">
                  <span class="text-foreground font-mono text-xs font-semibold"
                    >Overall Readiness: {{ overallReadiness() }}%</span
                  >
                  <div ui-progress [value]="overallReadiness()" class="h-2 w-28"></div>
                </div>
              </div>
    
              <div class="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3">
                @for (m of milestones; track m.id) {
                  <div
                    class="border-border bg-card hover:border-primary/30 flex flex-col justify-between space-y-3 rounded-xl border p-4 shadow-xs transition-colors"
                  >
                    <div class="flex items-start justify-between gap-2">
                      <span
                        ui-badge
                        variant="outline"
                        [class]="milestoneBadgeClass(m.status)"
                      >
                        {{ milestoneLabel(m.status) }}
                      </span>
                      <span class="text-muted-foreground font-mono text-xs">{{ m.date }}</span>
                    </div>
    
                    <div>
                      <p class="text-foreground text-xs leading-snug font-semibold">{{ m.title }}</p>
                      <p class="text-muted-foreground mt-1 font-mono text-xs">{{ m.category }}</p>
                    </div>
    
                    <div class="space-y-1 pt-1">
                      <div class="text-muted-foreground flex justify-between font-mono text-xs">
                        <span>Progress</span>
                        <span>{{ m.completionPercent }}%</span>
                      </div>
                      <div ui-progress [value]="m.completionPercent" class="h-1.5"></div>
                    </div>
                  </div>
                }
              </div>
            </div>
          </div>
    
          <!-- Footer -->
          <footer
            class="border-border text-muted-foreground mx-auto flex w-full max-w-6xl flex-col items-center justify-between gap-4 border-t pt-8 text-xs sm:flex-row"
          >
            <div class="flex items-center gap-2">
              <span>&copy; {{ currentYear }} UIPKGE. Open source under MIT License.</span>
            </div>
            <div class="flex items-center gap-4">
              <a
                href="https://github.com/uday-a/uipkge-registry"
                target="_blank"
                rel="noreferrer"
                class="hover:text-foreground transition-colors"
              >
                GitHub
              </a>
              <div ui-separator orientation="vertical" class="h-3"></div>
              <a href="https://uipkge.dev" target="_blank" rel="noreferrer" class="hover:text-foreground transition-colors">
                Registry Docs
              </a>
              <div ui-separator orientation="vertical" class="h-3"></div>
              <a [href]="statusUrl" target="_blank" rel="noreferrer" class="hover:text-foreground transition-colors">
                Status Page
              </a>
            </div>
          </footer>
        </section>
      `,
    })
    export class UiComingSoonComponent implements OnInit, OnDestroy {
      protected readonly Activity = Activity
      protected readonly ArrowRight = ArrowRight
      protected readonly ArrowUpRight = ArrowUpRight
      protected readonly Check = Check
      protected readonly CheckCircle2 = CheckCircle2
      protected readonly Copy = Copy
      protected readonly Rocket = Rocket
      protected readonly ShieldCheck = ShieldCheck
      protected readonly Terminal = Terminal
      protected readonly Zap = Zap
    
      protected readonly milestones = initialMilestones
      protected readonly teasersList = teasers
      protected readonly roles = roleOptions
    
      @Input() targetIso = ''
      @Input() statusUrl = 'https://status.uipkge.dev'
      @Input() productVersion = 'v2.0 Beta'
      @Input('class') className?: string
    
      readonly days = signal('00')
      readonly hours = signal('00')
      readonly minutes = signal('00')
      readonly seconds = signal('00')
      readonly activeTeaserIndex = signal(0)
    
      // Waitlist form state
      readonly email = signal('')
      readonly role = signal<WaitlistRole>('frontend')
      readonly submitted = signal(false)
      readonly queuePosition = signal<number | null>(null)
      readonly copiedReferral = signal(false)
      readonly copiedCli = signal(false)
    
      private deadline = 0
      private timer: ReturnType<typeof setInterval> | undefined
      private copyTimers: ReturnType<typeof setTimeout>[] = []
    
      readonly units = computed(() => [
        { label: 'Days', value: this.days() },
        { label: 'Hours', value: this.hours() },
        { label: 'Minutes', value: this.minutes() },
        { label: 'Seconds', value: this.seconds() },
      ])
    
      readonly overallReadiness = computed(() =>
        Math.round(initialMilestones.reduce((acc, m) => acc + m.completionPercent, 0) / initialMilestones.length),
      )
    
      readonly activeTeaser = computed(() => teasers[this.activeTeaserIndex()] ?? teasers[0])
    
      get currentYear(): number {
        return new Date().getFullYear()
      }
    
      get rootClass(): string {
        return cn(
          'bg-background relative flex min-h-screen flex-col justify-between overflow-hidden px-4 py-16 sm:px-6 lg:px-8',
          this.className,
        )
      }
    
      ngOnInit(): void {
        this.deadline = this.targetIso ? new Date(this.targetIso).getTime() : Date.now() + 7 * 24 * 60 * 60 * 1000
        this.tick()
        this.timer = setInterval(() => this.tick(), 1000)
      }
    
      ngOnDestroy(): void {
        clearInterval(this.timer)
        this.copyTimers.forEach((t) => clearTimeout(t))
      }
    
      tick(): void {
        const diff = Math.max(0, this.deadline - Date.now())
        this.days.set(pad(Math.floor(diff / 86_400_000)))
        this.hours.set(pad(Math.floor((diff % 86_400_000) / 3_600_000)))
        this.minutes.set(pad(Math.floor((diff % 3_600_000) / 60_000)))
        this.seconds.set(pad(Math.floor((diff % 60_000) / 1000)))
      }
    
      onWaitlistSubmit(event: Event): void {
        event.preventDefault()
        this.handleWaitlistSubmit()
      }
    
      handleWaitlistSubmit(): void {
        if (!this.email()) return
        const randomPos = Math.floor(Math.random() * 80) + 120
        this.queuePosition.set(randomPos)
        this.submitted.set(true)
      }
    
      copyReferral(): void {
        void navigator.clipboard?.writeText(`https://uipkge.dev/invite?ref=queue_${this.queuePosition()}`)
        this.copiedReferral.set(true)
        this.copyTimers.push(setTimeout(() => this.copiedReferral.set(false), 2000))
      }
    
      copyTeaserSnippet(snippet: string): void {
        void navigator.clipboard?.writeText(snippet)
        this.copiedCli.set(true)
        this.copyTimers.push(setTimeout(() => this.copiedCli.set(false), 2000))
      }
    
      roleClass(id: WaitlistRole): string {
        return cn(
          'rounded-lg border px-2.5 py-1.5 text-center text-xs font-medium transition-colors',
          this.role() === id
            ? 'border-primary bg-primary/10 text-primary font-semibold shadow-xs'
            : 'border-border bg-background text-muted-foreground hover:text-foreground',
        )
      }
    
      teaserTabClass(idx: number): string {
        return cn(
          'rounded-md px-3 py-1 text-xs font-medium transition-colors',
          this.activeTeaserIndex() === idx
            ? 'bg-background text-foreground font-semibold shadow-xs'
            : 'text-muted-foreground hover:text-foreground',
        )
      }
    
      milestoneBadgeClass(status: ComingSoonMilestone['status']): string {
        return cn(
          'px-2 py-0.5 font-mono text-xs',
          status === 'completed' && 'border-success/20 bg-success/10 text-success',
          status === 'in_progress' && 'border-warning/20 bg-warning/10 text-warning',
          status === 'scheduled' && 'bg-muted text-muted-foreground border-border',
        )
      }
    
      milestoneLabel(status: ComingSoonMilestone['status']): string {
        if (status === 'completed') return 'Completed'
        if (status === 'in_progress') return 'In Progress'
        return 'Scheduled'
      }
    }
    
  • src/app/components/blocks/coming-soon/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/coming-soon.json