Ab Test Variant Results
blockanalyticsOptimizely and Statsig style A/B test statistical significance scorecard and conversion delta comparison. Features experiment status header, 4 primary experiment metrics cards, variants comparison table with statistical verdicts, and conversion funnel step drop-off comparison.
Installation
$pnpm dlx uipkge-ng@latest add ab-test-variant-results$npx uipkge-ng@latest add ab-test-variant-results$yarn dlx uipkge-ng@latest add ab-test-variant-results$bunx uipkge-ng@latest add ab-test-variant-resultsManifest:
https://uipkge.dev/r/angular/ab-test-variant-results.jsonInstalls to:src/app/components/blocks/ab-test-variant-results/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.
AbTestPrimaryMetricinterface AbTestPrimaryMetric {
id: string
label: string
value: string
unit: string
badge: string
badgeVariant: BadgeVariant
badgeIcon: LucideIconData
description: string
highlight: boolean
}AbTestVariantinterface AbTestVariant {
id: string
code: string
type: string
title: string
description: string
split: string
visitors: string
conversions: string
conversionLabel: string
conversionRate: string
rateBadge: string
rateBadgeVariant: BadgeVariant
revenuePerVisitor: string
revenueDelta?: string
totalRevenue: string
verdict: string
verdictVariant: BadgeVariant
isWinner: boolean
}AbTestFunnelArminterface AbTestFunnelArm {
count: string
rate: string
progress: number
retention: string
}AbTestFunnelStepinterface AbTestFunnelStep {
id: string
stepNumber: string
name: string
description: string
icon: LucideIconData
control: AbTestFunnelArm
challenger: AbTestFunnelArm
delta: string
isPositive: boolean | null
insight: string
}AbTestGuardrailinterface AbTestGuardrail {
id: string
title: string
status: string
badgeVariant: BadgeVariant
value: string
description: string
icon: LucideIconData
}npm dependencies
Files installed (2)
src/app/components/blocks/ab-test-variant-results/ab-test-variant-results.component.ts30.9 kB
import { Component, Input, signal, ChangeDetectionStrategy } from '@angular/core' import { Activity, CheckCheck, CheckCircle2, Clock, CreditCard, DollarSign, LucideAngularModule, Pause, Play, ShieldCheck, ShoppingCart, TrendingUp, Trophy, Truck, Users, 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 { UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, } from '@/ui/table/table.component' export interface AbTestPrimaryMetric { id: string label: string value: string unit: string badge: string badgeVariant: BadgeVariant badgeIcon: LucideIconData description: string highlight: boolean } export interface AbTestVariant { id: string code: string type: string title: string description: string split: string visitors: string conversions: string conversionLabel: string conversionRate: string rateBadge: string rateBadgeVariant: BadgeVariant revenuePerVisitor: string revenueDelta?: string totalRevenue: string verdict: string verdictVariant: BadgeVariant isWinner: boolean } export interface AbTestFunnelArm { count: string rate: string progress: number retention: string } export interface AbTestFunnelStep { id: string stepNumber: string name: string description: string icon: LucideIconData control: AbTestFunnelArm challenger: AbTestFunnelArm delta: string isPositive: boolean | null insight: string } export interface AbTestGuardrail { id: string title: string status: string badgeVariant: BadgeVariant value: string description: string icon: LucideIconData } const primaryMetrics: AbTestPrimaryMetric[] = [ { id: 'conversion-lift', label: 'Overall Conversion Lift', value: '+24.8%', unit: 'relative', badge: '+1.27% Absolute', badgeVariant: 'success', badgeIcon: TrendingUp, description: 'Challenger (6.41%) vs Baseline Control (5.14%)', highlight: true, }, { id: 'sample-size', label: 'Total Sample Size', value: '48,290', unit: 'unique visitors', badge: '50 / 50 Split', badgeVariant: 'secondary', badgeIcon: Users, description: '24,145 unique visitors per variant group', highlight: false, }, { id: 'statistical-confidence', label: 'Statistical Confidence', value: '99.2%', unit: 'confidence', badge: 'p = 0.008', badgeVariant: 'outline', badgeIcon: Activity, description: 'Exceeds 95.0% significance threshold (α = 0.05)', highlight: false, }, { id: 'revenue-impact', label: 'Est. Annual Revenue Impact', value: '+$142,000', unit: 'ARR', badge: '+$1.08 / visitor', badgeVariant: 'success', badgeIcon: DollarSign, description: 'Based on 131,500 annual checkouts ($5.28 vs $4.20 RPV)', highlight: true, }, ] const variants: AbTestVariant[] = [ { id: 'variant-a', code: 'Variant A', type: 'Control', title: 'Multi-Step Wizard', description: '3-step segmented checkout flow with separate shipping, billing, and review steps.', split: '50%', visitors: '24,145', conversions: '1,240', conversionLabel: '1,240 checkouts', conversionRate: '5.14%', rateBadge: 'Baseline', rateBadgeVariant: 'secondary', revenuePerVisitor: '$4.20', totalRevenue: '$101,409.00', verdict: 'Baseline Control Group', verdictVariant: 'outline', isWinner: false, }, { id: 'variant-b', code: 'Variant B', type: 'Challenger', title: 'Single-Page with Accordion', description: 'Unified single-page layout with collapsible accordion sections and sticky order summary.', split: '50%', visitors: '24,145', conversions: '1,548', conversionLabel: '1,548 checkouts (+308)', conversionRate: '6.41%', rateBadge: '+24.8% relative lift', rateBadgeVariant: 'success', revenuePerVisitor: '$5.28', revenueDelta: '+$1.08 / visitor', totalRevenue: '$127,485.60', verdict: 'Winner · 99.2% probability to beat control', verdictVariant: 'success', isWinner: true, }, ] const funnelSteps: AbTestFunnelStep[] = [ { id: 'step-1', stepNumber: '01', name: 'Cart Review', description: 'Initial checkout landing and cart items validation', icon: ShoppingCart, control: { count: '24,145', rate: '100.0%', progress: 100, retention: '100% of traffic', }, challenger: { count: '24,145', rate: '100.0%', progress: 100, retention: '100% of traffic', }, delta: '0.0%', isPositive: null, insight: 'Uniform 50/50 randomized traffic partition without sample ratio bias.', }, { id: 'step-2', stepNumber: '02', name: 'Shipping Address', description: 'Delivery destination selection & shipping method', icon: Truck, control: { count: '18,350', rate: '76.0%', progress: 76, retention: '76.0% retained', }, challenger: { count: '20,523', rate: '85.0%', progress: 85, retention: '85.0% retained', }, delta: '+11.8% stage lift', isPositive: true, insight: 'Auto-fill and inline address validation lowered initial drop-off by 9.0 percentage points.', }, { id: 'step-3', stepNumber: '03', name: 'Payment Details', description: 'Credit card input, digital wallet, or invoice terms', icon: CreditCard, control: { count: '8,450', rate: '35.0%', progress: 35, retention: '46.0% retained', }, challenger: { count: '11,831', rate: '49.0%', progress: 49, retention: '57.6% retained', }, delta: '+40.0% stage lift', isPositive: true, insight: 'Accordion structure preserved visible cart totals, reducing payment abandonment substantially.', }, { id: 'step-4', stepNumber: '04', name: 'Checkout Completed', description: 'Payment authorized & order confirmation rendered', icon: CheckCheck, control: { count: '1,240', rate: '5.14%', progress: 5.14, retention: '14.7% of payment step', }, challenger: { count: '1,548', rate: '6.41%', progress: 6.41, retention: '13.1% of payment step', }, delta: '+24.8% final lift', isPositive: true, insight: 'Yielded 308 net incremental checkouts with statistical significance (p = 0.008).', }, ] const guardrails: AbTestGuardrail[] = [ { id: 'srm', title: 'Sample Ratio Mismatch (SRM)', status: 'Passed', badgeVariant: 'success', value: 'p = 0.982', description: 'Chi-square goodness of fit verifies balanced 50.0% / 50.0% traffic delivery.', icon: ShieldCheck, }, { id: 'mde', title: 'Minimum Detectable Effect', status: 'Achieved', badgeVariant: 'success', value: 'Observed +24.8% vs MDE 5.0%', description: 'Statistical power exceeds 99% for primary conversion rate metric.', icon: Zap, }, { id: 'perf', title: 'Performance Guardrail', status: 'Healthy', badgeVariant: 'secondary', value: '1.1s vs 1.3s LCP', description: 'Single-page architecture improved largest contentful paint by 180ms.', icon: Clock, }, ] /** * AbTestVariantResults -- the React block, 1:1: an A/B experiment scorecard with a status * header (pause / declare winner), four primary metric cards, a variants comparison table, * a per-step funnel drop-off comparison and a guardrails footer. Pass `title`, * `experimentId` and `duration` to relabel it; edit the data arrays above to change the rest. */ @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-ab-test-variant-results, [ui-ab-test-variant-results]', standalone: true, // React renders the root <div> itself: the host stays out of layout, `class` goes to the root // and the `title` input never becomes a native tooltip on the host. host: { '[attr.class]': '"contents"', '[attr.title]': 'null' }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardHeaderComponent, UiCardTitleComponent, UiCardDescriptionComponent, UiCardContentComponent, UiTableComponent, UiTableHeaderComponent, UiTableBodyComponent, UiTableRowComponent, UiTableHeadComponent, UiTableCellComponent, ], template: ` <div data-uipkge="" data-slot="ab-test-variant-results" [class]="rootClass"> <!-- Winner Rollout Banner (if declared) --> @if (isWinnerDeclared()) { <div class="border-success/30 bg-success/10 text-foreground dark:text-foreground flex flex-col items-start justify-between gap-3 rounded-lg border p-4 shadow-xs sm:flex-row sm:items-center" > <div class="flex items-center gap-3"> <div class="bg-success flex size-9 shrink-0 items-center justify-center rounded-md text-white shadow-xs"> <lucide-icon [img]="Trophy" class="size-4" aria-hidden="true" /> </div> <div class="space-y-0.5"> <p class="text-foreground text-sm font-semibold">Variant B (Single-Page Accordion) Declared Winner</p> <p class="text-muted-foreground text-xs"> 100% traffic rollout initiated. Estimated annual revenue increase of +$142,000 ARR in effect. </p> </div> </div> <span ui-badge variant="success" class="font-semibold">100% Traffic Live</span> </div> } <!-- Header Section --> <div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between"> <div class="space-y-1.5"> <div class="flex flex-wrap items-center gap-2.5"> <h2 class="text-foreground text-xl font-bold tracking-tight sm:text-2xl">{{ title }}</h2> <span ui-badge variant="outline" class="font-mono text-xs">{{ experimentId }}</span> <span ui-badge variant="success" class="border-success/20 bg-success/10 text-success gap-1.5 font-medium"> <lucide-icon [img]="CheckCircle2" class="size-3" aria-hidden="true" /> <span>Statistically Significant · 99.2% Confidence</span> </span> @if (isPaused()) { <span ui-badge variant="secondary" class="gap-1 font-medium"> <lucide-icon [img]="Pause" class="size-3" aria-hidden="true" /> <span>Paused</span> </span> } </div> <p class="text-muted-foreground text-xs leading-relaxed sm:text-sm"> {{ duration }} · Primary Goal: <span class="text-foreground font-medium">Completed Checkout Rate</span> </p> </div> <!-- Action Buttons --> <div class="flex flex-wrap items-center gap-2.5"> <button ui-button size="sm" variant="outline" class="gap-1.5 shadow-xs" (click)="togglePause()"> <lucide-icon [img]="isPaused() ? Play : Pause" class="size-3.5" aria-hidden="true" /> <span>{{ isPaused() ? 'Resume Test' : 'Pause Test' }}</span> </button> <button ui-button size="sm" variant="default" [disabled]="isWinnerDeclared()" class="gap-1.5 shadow-xs" (click)="handleDeclareWinner()" > <lucide-icon [img]="Trophy" class="size-3.5" aria-hidden="true" /> <span>{{ isWinnerDeclared() ? 'Winner Rolled Out 100%' : 'Declare Winner & Roll Out 100%' }}</span> </button> </div> </div> <!-- 4 Primary Experiment Metrics Cards --> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> @for (metric of primaryMetrics; track metric.id) { <div ui-card [class]="metricCardClass(metric)"> <div ui-card-header class="pb-2"> <div class="flex items-center justify-between gap-2"> <h3 ui-card-title class="text-muted-foreground text-xs font-medium tracking-wider uppercase"> {{ metric.label }} </h3> <span ui-badge [variant]="metric.badgeVariant" class="gap-1 font-medium tabular-nums"> <lucide-icon [img]="metric.badgeIcon" class="size-3" aria-hidden="true" /> <span>{{ metric.badge }}</span> </span> </div> </div> <div ui-card-content class="space-y-1.5 pb-4"> <div class="flex items-baseline gap-1.5"> <span [class]="metricValueClass(metric)">{{ metric.value }}</span> @if (metric.unit) { <span class="text-muted-foreground text-xs font-normal">{{ metric.unit }}</span> } </div> <p class="text-muted-foreground text-xs leading-relaxed">{{ metric.description }}</p> </div> </div> } </div> <!-- Variants Comparison Table Card --> <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> <div class="flex items-center gap-2"> <h3 ui-card-title class="text-foreground text-base font-semibold"> Variants Comparison & Statistical Significance </h3> <span ui-badge variant="outline" class="font-mono text-xs">Statsig / Optimizely Model</span> </div> <p ui-card-description class="mt-1 text-xs"> Frequentist two-tailed Z-test with continuous monitoring and sample ratio correction. </p> </div> <div class="border-border/80 bg-muted/30 flex items-center gap-2 self-start rounded-lg border px-3 py-1.5 text-xs sm:self-auto" > <span class="text-muted-foreground">Significance Alpha:</span> <span class="text-foreground font-mono font-semibold tabular-nums">α = 0.05</span> </div> </div> </div> <div ui-card-content class="p-0"> <div class="overflow-x-auto"> <div ui-table> <thead ui-table-header class="bg-muted/40"> <tr ui-table-row class="border-border hover:bg-transparent"> <th ui-table-head class="text-foreground text-xs font-semibold">Variant & Layout</th> <th ui-table-head class="text-foreground text-xs font-semibold">Traffic Split</th> <th ui-table-head class="text-foreground text-right text-xs font-semibold">Conversions</th> <th ui-table-head class="text-foreground text-right text-xs font-semibold">Conversion Rate</th> <th ui-table-head class="text-foreground text-right text-xs font-semibold">Revenue / Visitor</th> <th ui-table-head class="text-foreground text-right text-xs font-semibold">Statistical Verdict</th> </tr> </thead> <tbody ui-table-body> @for (variant of variants; track variant.id) { <tr ui-table-row [class]="variantRowClass(variant)"> <!-- Variant Name & Description --> <td ui-table-cell class="py-4"> <div class="flex items-start gap-2.5"> <div [class]="variantMarkClass(variant)">{{ variant.code === 'Variant A' ? 'A' : 'B' }}</div> <div class="space-y-0.5"> <div class="flex items-center gap-1.5"> <span class="text-foreground text-sm font-semibold">{{ variant.code }}</span> <span ui-badge [variant]="variant.isWinner ? 'success' : 'secondary'" class="text-xs font-medium" > {{ variant.type }} </span> </div> <p class="text-foreground text-xs font-medium">{{ variant.title }}</p> <p class="text-muted-foreground text-xs">{{ variant.description }}</p> </div> </div> </td> <!-- Traffic Split --> <td ui-table-cell class="py-4 align-middle"> <div class="space-y-1"> <div class="flex items-center gap-1.5"> <span class="text-foreground text-sm font-semibold tabular-nums">{{ variant.split }}</span> <span class="text-muted-foreground text-xs">allocation</span> </div> <p class="text-muted-foreground text-xs tabular-nums">{{ variant.visitors }} visitors</p> </div> </td> <!-- Conversions --> <td ui-table-cell class="py-4 text-right align-middle"> <div class="space-y-0.5"> <span class="text-foreground text-sm font-bold tabular-nums">{{ variant.conversions }}</span> <p class="text-muted-foreground text-xs">completed checkouts</p> </div> </td> <!-- Conversion Rate --> <td ui-table-cell class="py-4 text-right align-middle"> <div class="flex flex-col items-end gap-1"> <span [class]="conversionRateClass(variant)">{{ variant.conversionRate }}</span> <span ui-badge [variant]="variant.rateBadgeVariant" class="gap-1 text-xs font-medium tabular-nums" > @if (variant.isWinner) { <lucide-icon [img]="TrendingUp" class="size-3" aria-hidden="true" /> } <span>{{ variant.rateBadge }}</span> </span> </div> </td> <!-- Revenue per Visitor --> <td ui-table-cell class="py-4 text-right align-middle"> <div class="space-y-0.5"> <span [class]="revenueClass(variant)">{{ variant.revenuePerVisitor }}</span> @if (variant.revenueDelta) { <p class="text-success text-success text-xs font-semibold tabular-nums"> {{ variant.revenueDelta }} </p> } @else { <p class="text-muted-foreground text-xs tabular-nums">{{ variant.totalRevenue }}</p> } </div> </td> <!-- Statistical Verdict --> <td ui-table-cell class="py-4 text-right align-middle"> <div class="flex flex-col items-end gap-1"> <span ui-badge [variant]="variant.verdictVariant" [class]="verdictBadgeClass(variant)"> @if (variant.isWinner) { <lucide-icon [img]="Trophy" class="text-success size-3.5" aria-hidden="true" /> } @else { <lucide-icon [img]="CheckCircle2" class="text-muted-foreground size-3.5" aria-hidden="true" /> } <span>{{ variant.verdict }}</span> </span> </div> </td> </tr> } </tbody> </div> </div> </div> </div> <!-- Conversion Funnel Step Drop-off Comparison Card --> <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-header class="pb-4"> <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"> <div> <div class="flex items-center gap-2"> <h3 ui-card-title class="text-foreground text-base font-semibold"> Conversion Funnel Step Drop-off Comparison </h3> <span ui-badge variant="secondary" class="text-xs">4-Stage Funnel</span> </div> <p ui-card-description class="mt-1 text-xs"> Direct comparison of drop-off, progression, and conversion retention between Control and Challenger at each step. </p> </div> <!-- Legend --> <div class="flex flex-wrap items-center gap-4 text-xs"> <div class="flex items-center gap-1.5"> <span class="border-border bg-muted-foreground/30 size-3 rounded-xs border"></span> <span class="text-muted-foreground">Variant A (Control)</span> </div> <div class="flex items-center gap-1.5"> <span class="bg-success size-3 rounded-xs"></span> <span class="text-foreground font-medium">Variant B (Challenger)</span> </div> </div> </div> </div> <div ui-card-content class="space-y-6"> <div class="grid grid-cols-1 gap-4"> @for (step of funnelSteps; track step.id) { <div class="border-border bg-card/60 hover:bg-muted/10 rounded-lg border p-4 shadow-xs transition-colors"> <div class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between"> <!-- Step Meta --> <div class="flex items-start gap-3 lg:w-1/3"> <div class="border-border bg-muted/50 text-foreground flex size-9 shrink-0 items-center justify-center rounded-md border shadow-xs" > <lucide-icon [img]="step.icon" class="size-4" aria-hidden="true" /> </div> <div class="space-y-0.5"> <div class="flex items-center gap-2"> <span class="text-muted-foreground font-mono text-xs font-bold" >STEP {{ step.stepNumber }}</span > <span class="text-foreground text-sm font-semibold">{{ step.name }}</span> </div> <p class="text-muted-foreground text-xs">{{ step.description }}</p> </div> </div> <!-- Comparative Bars --> <div class="space-y-2 lg:w-1/2"> <!-- Variant A (Control) Bar --> <div class="space-y-1"> <div class="text-muted-foreground flex items-center justify-between text-xs"> <span class="flex items-center gap-1.5"> <span class="text-foreground font-medium">Control:</span> <span class="tabular-nums">{{ step.control.count }} visitors</span> </span> <span class="font-mono font-medium tabular-nums">{{ step.control.rate }}</span> </div> <div class="bg-muted h-2 w-full overflow-hidden rounded-full"> <div class="bg-muted-foreground/40 h-full rounded-full transition-[width] duration-300" [style.width.%]="step.control.progress" ></div> </div> </div> <!-- Variant B (Challenger) Bar --> <div class="space-y-1"> <div class="flex items-center justify-between text-xs"> <span class="flex items-center gap-1.5"> <span class="text-foreground font-medium">Challenger:</span> <span class="text-foreground font-semibold tabular-nums" >{{ step.challenger.count }} visitors</span > </span> <span class="text-success text-success font-mono font-bold tabular-nums"> {{ step.challenger.rate }} </span> </div> <div class="bg-muted h-2 w-full overflow-hidden rounded-full"> <div class="bg-success h-full rounded-full transition-[width] duration-300" [style.width.%]="step.challenger.progress" ></div> </div> </div> </div> <!-- Step Lift Badge & Insight --> <div class="flex flex-col items-start gap-1 lg:w-1/6 lg:items-end"> <span ui-badge [variant]="step.isPositive ? 'success' : 'secondary'" [class]="stepDeltaClass(step)"> @if (step.isPositive) { <lucide-icon [img]="TrendingUp" class="size-3" aria-hidden="true" /> } <span>{{ step.delta }}</span> </span> <span class="text-muted-foreground text-left text-xs tabular-nums lg:text-right"> {{ step.challenger.retention }} </span> </div> </div> <!-- Bottom micro insight --> <div class="border-border/60 bg-muted/20 text-muted-foreground mt-3 flex items-center gap-2 rounded-md border px-3 py-1.5 text-xs" > <span class="text-foreground shrink-0 font-medium">Stage Note:</span> <span class="leading-relaxed">{{ step.insight }}</span> </div> </div> } </div> </div> </div> <!-- Guardrails & Quality Verification Footer --> <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-header class="pb-3"> <div class="flex items-center gap-2"> <h3 ui-card-title class="text-foreground text-base font-semibold"> Statistical Guardrails & Health Checks </h3> <span ui-badge variant="outline" class="font-mono text-xs">Integrity Verified</span> </div> <p ui-card-description class="text-xs"> Automatic telemetry assertions verifying sample validity, statistical power, and site performance. </p> </div> <div ui-card-content> <div class="grid grid-cols-1 gap-4 md:grid-cols-3"> @for (guard of guardrails; track guard.id) { <div class="border-border bg-card/60 hover:bg-muted/10 flex flex-col justify-between rounded-lg border p-4 shadow-xs transition-colors" > <div class="space-y-2.5"> <div class="flex items-center justify-between"> <div class="border-border bg-muted/50 text-foreground flex size-8 items-center justify-center rounded-md border shadow-xs" > <lucide-icon [img]="guard.icon" class="size-4" aria-hidden="true" /> </div> <span ui-badge [variant]="guard.badgeVariant" class="text-xs font-medium">{{ guard.status }}</span> </div> <div class="space-y-1"> <h4 class="text-foreground text-sm font-semibold">{{ guard.title }}</h4> <p class="text-success text-success font-mono text-xs font-semibold tabular-nums"> {{ guard.value }} </p> <p class="text-muted-foreground text-xs leading-relaxed">{{ guard.description }}</p> </div> </div> </div> } </div> </div> </div> </div> `, }) export class UiAbTestVariantResultsComponent { @Input() title = 'Checkout Flow: Multi-Step vs Single-Page' @Input() experimentId = 'EXP-1082' @Input() duration = 'Running for 14 days · 48,290 visitors' @Input('class') className?: string readonly isPaused = signal(false) readonly isWinnerDeclared = signal(false) protected readonly primaryMetrics = primaryMetrics protected readonly variants = variants protected readonly funnelSteps = funnelSteps protected readonly guardrails = guardrails protected readonly CheckCircle2 = CheckCircle2 protected readonly Pause = Pause protected readonly Play = Play protected readonly TrendingUp = TrendingUp protected readonly Trophy = Trophy get rootClass(): string { return cn('w-full space-y-6', this.className) } handleDeclareWinner(): void { this.isWinnerDeclared.set(true) this.isPaused.set(true) } togglePause(): void { this.isPaused.update((prev) => !prev) } protected metricCardClass(metric: AbTestPrimaryMetric): string { return cn( 'border-border bg-card hover:border-border/80 shadow-xs transition-colors', metric.highlight && 'border-success/30 bg-success/[0.02] dark:bg-success/10]', ) } protected metricValueClass(metric: AbTestPrimaryMetric): string { return cn( 'text-2xl font-bold tracking-tight tabular-nums sm:text-3xl', metric.highlight ? 'text-success' : 'text-foreground', ) } protected variantRowClass(variant: AbTestVariant): string { return cn( 'border-border transition-colors', variant.isWinner ? 'bg-success/[0.04] hover:bg-success/[0.08] bg-success/[0.07]' : 'hover:bg-muted/30', ) } protected variantMarkClass(variant: AbTestVariant): string { return cn( 'flex size-7 shrink-0 items-center justify-center rounded-md text-xs font-bold shadow-xs', variant.isWinner ? 'bg-success text-white' : 'border-border bg-muted text-muted-foreground border', ) } protected conversionRateClass(variant: AbTestVariant): string { return cn('text-sm font-bold tabular-nums', variant.isWinner ? 'text-success text-base' : 'text-foreground') } protected revenueClass(variant: AbTestVariant): string { return cn('text-sm font-bold tabular-nums', variant.isWinner ? 'text-success' : 'text-foreground') } protected verdictBadgeClass(variant: AbTestVariant): string { return cn( 'gap-1.5 py-1 text-xs font-semibold tabular-nums', variant.isWinner && 'border-success/30 bg-success/15 text-success', ) } protected stepDeltaClass(step: AbTestFunnelStep): string { return cn( 'gap-1 text-xs font-semibold tabular-nums', step.isPositive && 'border-success/30 bg-success/15 text-success', ) } }src/app/components/blocks/ab-test-variant-results/index.ts0.2 kB
export { UiAbTestVariantResultsComponent, type AbTestPrimaryMetric, type AbTestVariant, type AbTestFunnelArm, type AbTestFunnelStep, type AbTestGuardrail, } from './ab-test-variant-results.component'
Raw manifest:https://uipkge.dev/r/angular/ab-test-variant-results.json