Menu

Change language

GitHub repo

Ab Test Variant Results

blockanalytics

Optimizely 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

$npx uipkge-ng@latest add ab-test-variant-results
Manifest:https://uipkge.dev/r/angular/ab-test-variant-results.jsonInstalls to:src/app/components/blocks/ab-test-variant-results/

Variants

Loading interactive previews…

Schema

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

AbTestPrimaryMetric
interface AbTestPrimaryMetric {
  id: string
  label: string
  value: string
  unit: string
  badge: string
  badgeVariant: BadgeVariant
  badgeIcon: LucideIconData
  description: string
  highlight: boolean
}
AbTestVariant
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
}
AbTestFunnelArm
interface AbTestFunnelArm {
  count: string
  rate: string
  progress: number
  retention: string
}
AbTestFunnelStep
interface AbTestFunnelStep {
  id: string
  stepNumber: string
  name: string
  description: string
  icon: LucideIconData
  control: AbTestFunnelArm
  challenger: AbTestFunnelArm
  delta: string
  isPositive: boolean | null
  insight: string
}
AbTestGuardrail
interface 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 &amp; 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 &amp; 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 &amp; 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

Raw manifest:https://uipkge.dev/r/angular/ab-test-variant-results.json