Customer Journey Map
blockanalyticsVisual customer lifecycle funnel and journey map tracking user touchpoints across Acquisition, Activation, Monetization, and Retention. Features conversion rates, friction analysis, sentiment gauges, growth experiment notes, and cross-stage velocity summaries.
Installation
$pnpm dlx uipkge-ng@latest add customer-journey-map$npx uipkge-ng@latest add customer-journey-map$yarn dlx uipkge-ng@latest add customer-journey-map$bunx uipkge-ng@latest add customer-journey-mapManifest:
https://uipkge.dev/r/angular/customer-journey-map.jsonInstalls to:src/app/components/blocks/customer-journey-map/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.
JourneyStageinterface JourneyStage {
id: string
stageNumber: string
title: string
subtitle: string
icon: LucideIconData
volume: string
volumeLabel: string
volumeChange: string
volumeChangeTrend: 'up' | 'down'
conversionRate: string
conversionLabel: string
primaryAction: {
icon: LucideIconData
text: string
}
keyMetrics: {
label: string
value: string
detail?: string
}[]
touchpoints: {
name: string
channel: string
type: 'organic' | 'app' | 'email' | 'billing'
}[]
sentiment: {
score: number
maxScore: number
label: string
icon: LucideIconData
progressColor: string
gaugeLabel: string
}
frictionPoint: string
opportunity: {
experiment: string
expectedImpact: string
}
badgeVariant: Extract<BadgeVariant, 'outline' | 'secondary' | 'default' | 'success' | 'info' | 'warning'>
}Files installed (2)
src/app/components/blocks/customer-journey-map/customer-journey-map.component.ts33.9 kB
import { Component, Input, signal, ChangeDetectionStrategy } from '@angular/core' import { ArrowRight, ArrowUpRight, CheckCircle2, Clock, Compass, CreditCard, Download, HeartHandshake, Lightbulb, LucideAngularModule, MousePointerClick, Rocket, Share2, ShieldAlert, Smile, TrendingUp, Zap, type LucideIconData, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent, type BadgeVariant } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiProgressComponent } from '@/ui/progress/progress.component' import { UiSeparatorComponent } from '@/ui/separator/separator.component' export interface JourneyStage { id: string stageNumber: string title: string subtitle: string icon: LucideIconData volume: string volumeLabel: string volumeChange: string volumeChangeTrend: 'up' | 'down' conversionRate: string conversionLabel: string primaryAction: { icon: LucideIconData text: string } keyMetrics: { label: string value: string detail?: string }[] touchpoints: { name: string channel: string type: 'organic' | 'app' | 'email' | 'billing' }[] sentiment: { score: number maxScore: number label: string icon: LucideIconData progressColor: string gaugeLabel: string } frictionPoint: string opportunity: { experiment: string expectedImpact: string } badgeVariant: Extract<BadgeVariant, 'outline' | 'secondary' | 'default' | 'success' | 'info' | 'warning'> } const timeframes = [ { id: '30d', label: 'Last 30 Days' }, { id: '90d', label: 'Last 90 Days' }, { id: 'qtd', label: 'Quarter to Date' }, { id: 'ytd', label: 'Year to Date' }, ] const stages: JourneyStage[] = [ { id: 'acquisition', stageNumber: '01', title: 'Acquisition', subtitle: 'Traffic & Inbound Discovery', icon: Compass, volume: '120,000', volumeLabel: 'Unique Visitors', volumeChange: '+14.2%', volumeChangeTrend: 'up', conversionRate: '6.8%', conversionLabel: 'Visitor to Signup', primaryAction: { icon: MousePointerClick, text: 'Browses docs & visits pricing', }, keyMetrics: [ { label: 'Top Channel', value: 'Organic Search', detail: '48% of volume' }, { label: 'Signup Conv.', value: '6.8%', detail: '8,160 accounts' }, { label: 'Avg. Bounce', value: '41.2%', detail: '-2.4% vs benchmark' }, ], touchpoints: [ { name: 'SEO landing pages & technical blog', channel: 'Search', type: 'organic' }, { name: 'Interactive sandbox demo playground', channel: 'Web App', type: 'app' }, { name: 'Product Hunt launch & GitHub repo', channel: 'Community', type: 'organic' }, { name: 'Developer search & social ad campaigns', channel: 'Paid Ads', type: 'organic' }, ], sentiment: { score: 8.2, maxScore: 10, label: 'High Intent', icon: Smile, progressColor: 'bg-chart-1', gaugeLabel: '8.2 / 10', }, frictionPoint: 'Technical landing page bounce rate spikes on legacy mobile browsers (54%).', opportunity: { experiment: 'Optimize hero headline for developer intent to lift bounce rate by 5%', expectedImpact: '+420 signups/mo', }, badgeVariant: 'secondary', }, { id: 'activation', stageNumber: '02', title: 'Activation', subtitle: 'Onboarding & First Success', icon: Rocket, volume: '8,160', volumeLabel: 'Signups Onboarded', volumeChange: '+8.6%', volumeChangeTrend: 'up', conversionRate: '78.0%', conversionLabel: 'Reached Aha Milestone', primaryAction: { icon: Zap, text: 'Runs first CLI query & creates project', }, keyMetrics: [ { label: 'Key Milestone', value: 'Created 1st Project', detail: '78% completed' }, { label: 'Time to Value', value: '12 min', detail: 'Median onboarding' }, { label: 'Stage Drop-off', value: '22.0%', detail: 'Step 3 invite friction' }, ], touchpoints: [ { name: 'Automated 4-part welcome email sequence', channel: 'Email', type: 'email' }, { name: 'Interactive 3-step setup checklist', channel: 'In-App Modal', type: 'app' }, { name: 'Pre-populated template project schemas', channel: 'Workspace', type: 'app' }, { name: 'CLI quickstart terminal prompt', channel: 'Developer Tool', type: 'app' }, ], sentiment: { score: 8.8, maxScore: 10, label: 'Enthusiastic', icon: Smile, progressColor: 'bg-chart-2', gaugeLabel: '8.8 / 10', }, frictionPoint: 'Drop-off occurs during mandatory colleague invite screen before dashboard access.', opportunity: { experiment: 'Add single-sign on prompt to reduce drop-off by 8%', expectedImpact: '+650 activated teams', }, badgeVariant: 'info', }, { id: 'monetization', stageNumber: '03', title: 'Monetization', subtitle: 'Free to Paid Conversion', icon: CreditCard, volume: '1,420', volumeLabel: 'Paid Customers', volumeChange: '+22.4%', volumeChangeTrend: 'up', conversionRate: '17.4%', conversionLabel: 'Signup to Paid Rate', primaryAction: { icon: TrendingUp, text: 'Selects Team plan & enters credit card', }, keyMetrics: [ { label: 'Avg. Order Value', value: '$49/mo', detail: 'Blended Team/Pro' }, { label: 'Conv. Rate', value: '17.4%', detail: '1,420 total accounts' }, { label: 'Gross New MRR', value: '+$69,580', detail: 'Paced above target' }, ], touchpoints: [ { name: 'Feature gate limit trigger modal', channel: 'In-App', type: 'app' }, { name: 'Self-serve pricing comparison drawer', channel: 'Web App', type: 'app' }, { name: 'Stripe checkout with annual savings toggle', channel: 'Billing Gateway', type: 'billing' }, { name: 'Instant VAT receipt & team tax invoice', channel: 'Automated Email', type: 'email' }, ], sentiment: { score: 8.4, maxScore: 10, label: 'ROI Positive', icon: Smile, progressColor: 'bg-chart-3', gaugeLabel: '8.4 / 10', }, frictionPoint: '12% checkout abandonment when international VAT verification requires manual review.', opportunity: { experiment: 'Introduce annual billing 20% discount badge in checkout to lift ARPU', expectedImpact: '+$14.2k MRR expansion', }, badgeVariant: 'warning', }, { id: 'retention', stageNumber: '04', title: 'Retention & Advocacy', subtitle: 'Active Renewals & Expansion', icon: HeartHandshake, volume: '92.0%', volumeLabel: 'Net Retention Rate', volumeChange: '+3.1%', volumeChangeTrend: 'up', conversionRate: '92.0%', conversionLabel: 'Cohort Net Retention', primaryAction: { icon: Share2, text: 'Shares referral link & invites teammates', }, keyMetrics: [ { label: 'NPS Score', value: '+68', detail: 'Industry world-class' }, { label: 'Referral Velocity', value: '1.2 invites', detail: 'Per active user/mo' }, { label: 'Monthly Logo Churn', value: '0.8%', detail: 'Negative revenue churn' }, ], touchpoints: [ { name: 'Automated weekly performance digest', channel: 'Email Digest', type: 'email' }, { name: 'Quarterly proactive CSM review email', channel: 'Success Touch', type: 'email' }, { name: 'Referral program link inside settings', channel: 'Workspace Settings', type: 'app' }, { name: 'Changelog notification & release notes', channel: 'In-App Feed', type: 'app' }, ], sentiment: { score: 9.4, maxScore: 10, label: 'Brand Advocates', icon: Smile, progressColor: 'bg-chart-4', gaugeLabel: '9.4 / 10', }, frictionPoint: 'Feature discovery lag for advanced multi-region latency controls on growing teams.', opportunity: { experiment: 'Launch one-click workspace invite link to increase referral velocity to 1.8', expectedImpact: '+35% viral coefficient', }, badgeVariant: 'success', }, ] const overallSummary = { funnelConversion: '1.18%', funnelConversionDetail: '1,420 Paid from 120,000 Visitors', timeToValue: '12 min', timeToValueDetail: 'Median signup to Aha moment', conversionVelocity: '4.2 days', conversionVelocityDetail: 'First touch to paid subscription', netRetention: '92.0%', netRetentionDetail: 'Expansion outpacing churn by 3.8x', } @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-customer-journey-map, [ui-customer-journey-map]', 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, UiCardHeaderComponent, UiCardTitleComponent, UiCardDescriptionComponent, UiCardContentComponent, UiProgressComponent, UiSeparatorComponent, ], template: ` <div data-uipkge="" data-slot="customer-journey-map" [class]="rootClass"> <!-- Header Section --> <div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <div class="space-y-1"> <div class="flex min-w-0 items-center gap-2"> <h2 class="text-foreground text-2xl font-bold tracking-tight">Customer Journey & Growth Lifecycle Map</h2> <span ui-badge variant="outline" class="border-primary/30 text-primary bg-primary/5 text-xs font-medium"> Live Lifecycle </span> </div> <p class="text-muted-foreground text-sm"> Analyze conversion velocity, friction points, and retention cohorts across user lifecycle stages. </p> </div> <!-- Action Toolbar --> <div class="flex flex-wrap items-center gap-2"> <div class="bg-muted/50 border-border inline-flex items-center rounded-lg border p-1"> @for (tf of timeframes; track tf.id) { <button type="button" [class]="timeframeClass(tf.id)" (click)="selectedTimeframe.set(tf.id)"> {{ tf.label }} </button> } </div> <button ui-button aria-label="Download attachment" variant="outline" size="sm" class="h-8 gap-1.5 text-xs font-medium" [disabled]="isExporting()" (click)="handleExport()" > @if (!isExporting()) { <lucide-icon [img]="Download" class="size-3.5" aria-hidden="true" /> } @else { <lucide-icon [img]="CheckCircle2" class="text-success size-3.5" aria-hidden="true" /> } <span>{{ isExporting() ? 'Exported' : 'Export Journey Map' }}</span> </button> </div> </div> <!-- Connected Horizontal Funnel Flow Ribbon --> <div class="border-border bg-card/60 rounded-xl border p-3 shadow-xs backdrop-blur-xs"> <div class="grid grid-cols-1 gap-2 md:grid-cols-7 md:items-center"> <!-- Stage 1 Pill --> <div class="bg-muted/40 border-border/60 flex items-center justify-between rounded-lg border px-3 py-2"> <div class="flex min-w-0 items-center gap-2"> <div class="bg-chart-1/15 text-chart-1 flex size-6 items-center justify-center rounded-md"> <lucide-icon [img]="Compass" class="size-3.5" aria-hidden="true" /> </div> <div> <p class="text-foreground text-xs font-semibold">1. Acquisition</p> <p class="text-muted-foreground text-xs tabular-nums">120,000 Visitors</p> </div> </div> </div> <!-- Connector 1 --> <div class="flex flex-col items-center justify-center py-1 md:py-0"> <div class="flex items-center gap-1"> <span class="text-muted-foreground text-xs font-medium tabular-nums">6.8%</span> <lucide-icon [img]="ArrowRight" class="text-muted-foreground size-3.5" aria-hidden="true" /> </div> <span class="text-muted-foreground/70 hidden text-xs md:inline">Signup rate</span> </div> <!-- Stage 2 Pill --> <div class="bg-muted/40 border-border/60 flex items-center justify-between rounded-lg border px-3 py-2"> <div class="flex min-w-0 items-center gap-2"> <div class="bg-chart-2/15 text-chart-2 flex size-6 items-center justify-center rounded-md"> <lucide-icon [img]="Rocket" class="size-3.5" aria-hidden="true" /> </div> <div> <p class="text-foreground text-xs font-semibold">2. Activation</p> <p class="text-muted-foreground text-xs tabular-nums">8,160 Signups</p> </div> </div> </div> <!-- Connector 2 --> <div class="flex flex-col items-center justify-center py-1 md:py-0"> <div class="flex items-center gap-1"> <span class="text-muted-foreground text-xs font-medium tabular-nums">17.4%</span> <lucide-icon [img]="ArrowRight" class="text-muted-foreground size-3.5" aria-hidden="true" /> </div> <span class="text-muted-foreground/70 hidden text-xs md:inline">Upgrade rate</span> </div> <!-- Stage 3 Pill --> <div class="bg-muted/40 border-border/60 flex items-center justify-between rounded-lg border px-3 py-2"> <div class="flex min-w-0 items-center gap-2"> <div class="bg-chart-3/15 text-chart-3 flex size-6 items-center justify-center rounded-md"> <lucide-icon [img]="CreditCard" class="size-3.5" aria-hidden="true" /> </div> <div> <p class="text-foreground text-xs font-semibold">3. Monetization</p> <p class="text-muted-foreground text-xs tabular-nums">1,420 Paid</p> </div> </div> </div> <!-- Connector 3 --> <div class="flex flex-col items-center justify-center py-1 md:py-0"> <div class="flex items-center gap-1"> <span class="text-muted-foreground text-xs font-medium tabular-nums">92.0%</span> <lucide-icon [img]="ArrowRight" class="text-muted-foreground size-3.5" aria-hidden="true" /> </div> <span class="text-muted-foreground/70 hidden text-xs md:inline">Retention</span> </div> <!-- Stage 4 Pill --> <div class="bg-muted/40 border-border/60 flex items-center justify-between rounded-lg border px-3 py-2"> <div class="flex min-w-0 items-center gap-2"> <div class="bg-chart-4/15 text-chart-4 flex size-6 items-center justify-center rounded-md"> <lucide-icon [img]="HeartHandshake" class="size-3.5" aria-hidden="true" /> </div> <div> <p class="text-foreground text-xs font-semibold">4. Retention</p> <p class="text-muted-foreground text-xs tabular-nums">1,306 Advocates</p> </div> </div> </div> </div> </div> <!-- 4-Stage Horizontal Journey Grid --> <div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4"> @for (stage of stages; track stage.id; let idx = $index) { <div ui-card role="button" tabindex="0" [attr.aria-pressed]="selectedStageIndex() === idx" [class]="stageCardClass(idx)" (click)="toggleStage(idx)" (keydown.enter)="toggleStage(idx)" (keydown.space)="toggleStageKey(idx, $event)" > <div ui-card-header class="space-y-3 pb-3"> <!-- Stage Badge & Number --> <div class="flex items-center justify-between"> <span ui-badge [variant]="stage.badgeVariant" class="text-xs font-semibold"> Stage {{ stage.stageNumber }} </span> <span class="text-muted-foreground text-xs font-medium tracking-wider uppercase"> {{ stage.conversionLabel }} </span> </div> <!-- Stage Icon & Title --> <div class="flex items-start gap-3"> <div [class]="stageIconClass(idx)"> <lucide-icon [img]="stage.icon" class="size-5" aria-hidden="true" /> </div> <div class="min-w-0 flex-1"> <h3 ui-card-title class="text-foreground text-base font-bold">{{ stage.title }}</h3> <p ui-card-description class="text-muted-foreground text-xs">{{ stage.subtitle }}</p> </div> </div> <!-- Stage Volume Metric --> <div class="bg-muted/40 border-border/60 flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5 rounded-lg border p-3" > <div> <p class="text-muted-foreground text-xs font-medium">{{ stage.volumeLabel }}</p> <p class="text-foreground text-2xl font-bold tracking-tight tabular-nums">{{ stage.volume }}</p> </div> <div class="text-right"> <span ui-badge variant="outline" class="border-success/30 bg-success/10 text-success gap-1 text-xs font-medium" > <lucide-icon [img]="TrendingUp" class="size-3" aria-hidden="true" /> <span class="tabular-nums">{{ stage.volumeChange }}</span> </span> <p class="text-muted-foreground mt-1 text-xs"> Conv: <span class="text-foreground font-semibold tabular-nums">{{ stage.conversionRate }}</span> </p> </div> </div> </div> <div ui-card-content class="space-y-4 pt-0"> <!-- Primary Customer Action --> <div class="space-y-1.5"> <p class="text-muted-foreground text-xs font-medium">Primary Customer Action</p> <div class="bg-accent/40 border-border/80 text-foreground flex min-w-0 items-center gap-2 rounded-md border px-2.5 py-1.5 text-xs font-medium" > <lucide-icon [img]="stage.primaryAction.icon" class="text-primary size-3.5 shrink-0" aria-hidden="true" /> <span class="truncate">{{ stage.primaryAction.text }}</span> </div> </div> <!-- Key Metrics Grid --> <div class="space-y-1.5"> <p class="text-muted-foreground text-xs font-medium">Key Stage Signals</p> <div class="grid grid-cols-1 gap-1.5"> @for (metric of stage.keyMetrics; track $index) { <div class="bg-background/60 border-border/60 flex items-center justify-between rounded-md border px-2.5 py-1.5 text-xs" > <span class="text-muted-foreground">{{ metric.label }}</span> <div class="flex items-center gap-1.5"> <span class="text-foreground font-semibold tabular-nums">{{ metric.value }}</span> @if (metric.detail) { <span class="text-muted-foreground text-xs">({{ metric.detail }})</span> } </div> </div> } </div> </div> <!-- Key Touchpoints List --> <div class="space-y-1.5"> <p class="text-muted-foreground text-xs font-medium">Active Touchpoints</p> <ul class="space-y-1"> @for (tp of stage.touchpoints; track $index) { <li class="text-foreground/90 flex items-start gap-2 text-xs"> <div class="bg-primary/20 text-primary mt-0.5 flex size-3.5 shrink-0 items-center justify-center rounded-full" > <span class="text-xs">•</span> </div> <div class="min-w-0 flex-1"> <span>{{ tp.name }}</span> <span ui-badge variant="outline" class="text-muted-foreground border-border ml-1.5 px-1 py-0 text-xs" > {{ tp.channel }} </span> </div> </li> } </ul> </div> <ui-separator class="my-2" /> <!-- Sentiment Indicator --> <div class="space-y-1.5"> <div class="flex items-center justify-between text-xs"> <span class="text-muted-foreground flex items-center gap-1"> <!-- host display:contents + [class] on the svg: the svg itself is the flex item and shrinks when the label wraps, as React's bare <svg> does --> <lucide-icon [img]="Smile" [class]="'text-primary size-3.5'" aria-hidden="true" /> Sentiment Score </span> <span class="text-foreground font-semibold tabular-nums"> {{ stage.sentiment.gaugeLabel }} ({{ stage.sentiment.label }}) </span> </div> <ui-progress [value]="sentimentValue(stage)" class="h-1.5" /> </div> <!-- Friction / Drop-off Point Note --> <div class="bg-destructive/5 border-destructive/20 rounded-md border p-2.5"> <div class="flex items-start gap-2"> <!-- [class], not class=: lucide-angular copies a static class onto its host AND the svg, doubling mt-0.5 --> <lucide-icon [img]="ShieldAlert" [class]="'text-destructive mt-0.5 size-3.5 shrink-0'" aria-hidden="true" /> <div class="text-xs"> <span class="text-destructive font-semibold">Friction Point: </span> <span class="text-foreground/80">{{ stage.frictionPoint }}</span> </div> </div> </div> <!-- Opportunity / Next Experiment Box --> <div class="bg-primary/5 border-primary/20 rounded-md border p-2.5"> <div class="flex items-start gap-2"> <lucide-icon [img]="Lightbulb" [class]="'text-primary mt-0.5 size-3.5 shrink-0'" aria-hidden="true" /> <div class="min-w-0 flex-1 space-y-0.5 text-xs"> <div class="flex flex-wrap items-center justify-between gap-1"> <span class="text-primary font-semibold">Growth Experiment:</span> <span ui-badge variant="outline" class="border-primary/30 text-primary bg-primary/10 text-xs font-semibold tabular-nums" > {{ stage.opportunity.expectedImpact }} </span> </div> <p class="text-foreground/80 leading-relaxed">{{ stage.opportunity.experiment }}</p> </div> </div> </div> </div> </div> } </div> <!-- Cross-Stage Conversion Velocity & Funnel Health Summary --> <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-header class="pb-3"> <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"> <div> <h3 ui-card-title class="text-foreground text-lg font-bold"> Lifecycle Conversion Velocity & Health Diagnostics </h3> <p ui-card-description class="text-muted-foreground text-xs"> End-to-end efficiency metrics tracking user progression speed from initial acquisition to retained advocacy. </p> </div> <span ui-badge variant="outline" class="border-success/30 bg-success/10 text-success w-fit gap-1 text-xs font-semibold" > <lucide-icon [img]="CheckCircle2" class="size-3.5" aria-hidden="true" /> Healthy Velocity (+18% QoQ) </span> </div> </div> <div ui-card-content class="space-y-5"> <!-- Metric Cards Strip --> <div class="grid grid-cols-2 gap-3 sm:grid-cols-4"> <div class="bg-muted/30 border-border/70 rounded-lg border p-3"> <div class="flex items-center gap-1.5"> <!-- host display:contents + [class] on the svg: the svg shrinks when the label wraps, as in React --> <lucide-icon [img]="TrendingUp" [class]="'text-chart-1 size-3.5'" aria-hidden="true" /> <span class="text-muted-foreground text-xs font-medium">Overall Conversion</span> </div> <p class="text-foreground mt-1 text-2xl font-bold tracking-tight tabular-nums"> {{ overallSummary.funnelConversion }} </p> <p class="text-muted-foreground text-xs">{{ overallSummary.funnelConversionDetail }}</p> </div> <div class="bg-muted/30 border-border/70 rounded-lg border p-3"> <div class="flex items-center gap-1.5"> <lucide-icon [img]="Zap" [class]="'text-chart-2 size-3.5'" aria-hidden="true" /> <span class="text-muted-foreground text-xs font-medium">Time to Value (TTV)</span> </div> <p class="text-foreground mt-1 text-2xl font-bold tracking-tight tabular-nums"> {{ overallSummary.timeToValue }} </p> <p class="text-muted-foreground text-xs">{{ overallSummary.timeToValueDetail }}</p> </div> <div class="bg-muted/30 border-border/70 rounded-lg border p-3"> <div class="flex items-center gap-1.5"> <lucide-icon [img]="Clock" [class]="'text-chart-3 size-3.5'" aria-hidden="true" /> <span class="text-muted-foreground text-xs font-medium">Avg. Conversion Velocity</span> </div> <p class="text-foreground mt-1 text-2xl font-bold tracking-tight tabular-nums"> {{ overallSummary.conversionVelocity }} </p> <p class="text-muted-foreground text-xs">{{ overallSummary.conversionVelocityDetail }}</p> </div> <div class="bg-muted/30 border-border/70 rounded-lg border p-3"> <div class="flex items-center gap-1.5"> <lucide-icon [img]="HeartHandshake" [class]="'text-chart-4 size-3.5'" aria-hidden="true" /> <span class="text-muted-foreground text-xs font-medium">Net Revenue Retention</span> </div> <p class="text-foreground mt-1 text-2xl font-bold tracking-tight tabular-nums"> {{ overallSummary.netRetention }} </p> <p class="text-muted-foreground text-xs">{{ overallSummary.netRetentionDetail }}</p> </div> </div> <!-- Stage Transition Flow Breakdown Bar --> <div class="space-y-2"> <div class="flex items-center justify-between text-xs"> <span class="text-muted-foreground font-medium">Lifecycle Pass-Through Velocity</span> <span class="text-foreground font-semibold tabular-nums">4 Stages Active • 120,000 Cohort Cohere</span> </div> <!-- Multi-segment Funnel Bar --> <div class="bg-muted/60 flex h-3.5 w-full overflow-hidden rounded-full p-0.5"> <div class="bg-chart-1 h-full w-[45%] rounded-l-full" title="Acquisition (120,000)"></div> <div class="bg-chart-2 h-full w-[25%]" title="Activation (8,160)"></div> <div class="bg-chart-3 h-full w-[18%]" title="Monetization (1,420)"></div> <div class="bg-chart-4 h-full w-[12%] rounded-r-full" title="Retention (92% NRR)"></div> </div> <div class="grid grid-cols-2 gap-2 text-xs sm:grid-cols-4"> <div class="flex items-center gap-1.5"> <span class="bg-chart-1 size-2 rounded-full"></span> <span class="text-muted-foreground">Acquisition (100%)</span> </div> <div class="flex items-center gap-1.5"> <span class="bg-chart-2 size-2 rounded-full"></span> <span class="text-muted-foreground">Activation (6.8%)</span> </div> <div class="flex items-center gap-1.5"> <span class="bg-chart-3 size-2 rounded-full"></span> <span class="text-muted-foreground">Monetization (17.4%)</span> </div> <div class="flex items-center gap-1.5"> <span class="bg-chart-4 size-2 rounded-full"></span> <span class="text-muted-foreground">Retention (92.0%)</span> </div> </div> </div> <ui-separator /> <!-- Key Strategic Growth Takeaway Callout --> <div class="bg-muted/40 border-border/80 flex flex-col gap-3 rounded-lg border p-4 sm:flex-row sm:items-center sm:justify-between" > <div class="flex items-start gap-3"> <div class="bg-primary/10 text-primary border-primary/20 mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-lg border" > <lucide-icon [img]="TrendingUp" class="size-4" aria-hidden="true" /> </div> <div class="space-y-1"> <div class="flex min-w-0 items-center gap-2"> <p class="text-foreground text-sm font-semibold">Primary Strategic Growth Takeaway</p> <span ui-badge variant="outline" class="border-warning/30 bg-warning/10 text-warning text-xs font-medium" > High Priority </span> </div> <p class="text-muted-foreground text-xs leading-relaxed"> The largest leverage point in the current cohort is the <strong class="text-foreground">Activation-to-Monetization conversion gap (17.4%)</strong>. Implementing contextual in-app checkout prompts at high-usage milestones and launching annual billing discounts is modeled to generate an additional <span class="text-foreground font-semibold tabular-nums">+$14,200 / mo</span> in net new MRR. </p> </div> </div> <button ui-button size="sm" class="h-8 shrink-0 gap-1.5 text-xs font-semibold"> <span>Apply Experiments</span> <lucide-icon [img]="ArrowUpRight" class="size-3.5" aria-hidden="true" /> </button> </div> </div> </div> </div> `, }) export class UiCustomerJourneyMapComponent { @Input('class') className?: string readonly selectedTimeframe = signal('30d') readonly isExporting = signal(false) readonly selectedStageIndex = signal<number | null>(null) protected readonly timeframes = timeframes protected readonly stages = stages protected readonly overallSummary = overallSummary protected readonly ArrowRight = ArrowRight protected readonly ArrowUpRight = ArrowUpRight protected readonly CheckCircle2 = CheckCircle2 protected readonly Clock = Clock protected readonly Compass = Compass protected readonly CreditCard = CreditCard protected readonly Download = Download protected readonly HeartHandshake = HeartHandshake protected readonly Lightbulb = Lightbulb protected readonly Rocket = Rocket protected readonly ShieldAlert = ShieldAlert protected readonly Smile = Smile protected readonly TrendingUp = TrendingUp protected readonly Zap = Zap get rootClass(): string { return cn('space-y-6', this.className) } handleExport(): void { this.isExporting.set(true) setTimeout(() => { this.isExporting.set(false) }, 1200) } toggleStage(idx: number): void { this.selectedStageIndex.set(this.selectedStageIndex() === idx ? null : idx) } toggleStageKey(idx: number, event: KeyboardEvent): void { event.preventDefault() this.toggleStage(idx) } protected timeframeClass(id: string): string { return cn( 'focus-visible:ring-ring rounded-md px-2.5 py-1 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:outline-hidden', this.selectedTimeframe() === id ? 'bg-background text-foreground shadow-xs' : 'text-muted-foreground hover:text-foreground', ) } protected stageCardClass(idx: number): string { return cn( 'border-border hover:border-primary/40 focus-visible:ring-ring relative flex cursor-pointer flex-col justify-between transition-colors duration-200 focus-visible:ring-2 focus-visible:outline-none', this.selectedStageIndex() === idx && 'ring-primary/40 ring-2', ) } protected stageIconClass(idx: number): string { return cn( 'flex size-10 shrink-0 items-center justify-center rounded-lg border shadow-xs', idx === 0 && 'border-chart-1/30 bg-chart-1/10 text-chart-1', idx === 1 && 'border-chart-2/30 bg-chart-2/10 text-chart-2', idx === 2 && 'border-chart-3/30 bg-chart-3/10 text-chart-3', idx === 3 && 'border-chart-4/30 bg-chart-4/10 text-chart-4', ) } protected sentimentValue(stage: JourneyStage): number { return (stage.sentiment.score / stage.sentiment.maxScore) * 100 } }src/app/components/blocks/customer-journey-map/index.ts0.1 kB
export { UiCustomerJourneyMapComponent, type JourneyStage } from './customer-journey-map.component'
Raw manifest:https://uipkge.dev/r/angular/customer-journey-map.json