ROI — Calculator
blockmarketingInput-driven ROI estimate where team size, close days, and analyst rate drive a computed saving, with the assumptions stated beneath the figure.
Installation
$pnpm dlx uipkge-ng@latest add roi-calculator-inputs$npx uipkge-ng@latest add roi-calculator-inputs$yarn dlx uipkge-ng@latest add roi-calculator-inputs$bunx uipkge-ng@latest add roi-calculator-inputsManifest:
https://uipkge.dev/r/angular/roi-calculator-inputs.jsonInstalls to:src/app/components/blocks/roi-calculator-inputs/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.
RoiInputinterface RoiInput {
id: 'analysts' | 'days' | 'rate'
label: string
min: number
max: number
step: number
}Files installed (2)
src/app/components/blocks/roi-calculator-inputs/roi-calculator-inputs.component.ts5.8 kB
// Input-driven ROI estimate: team size, close days, and analyst rate drive a computed // saving, with the assumptions stated beneath the figure. Port of the React block 1:1. import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core' import { ArrowRight, LucideAngularModule } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent } from '@/ui/card/card.component' import { UiSeparatorComponent } from '@/ui/separator/separator.component' import { UiSliderComponent } from '@/ui/slider/slider.component' const DAYS_SAVED_SHARE = 0.55 const CLOSES_PER_YEAR = 12 interface RoiInput { id: 'analysts' | 'days' | 'rate' label: string min: number max: number step: number } const INPUTS: RoiInput[] = [ { id: 'analysts', label: 'People involved in close', min: 1, max: 40, step: 1 }, { id: 'days', label: 'Days the close takes today', min: 2, max: 20, step: 1 }, { id: 'rate', label: 'Fully-loaded day rate', min: 200, max: 1200, step: 20 }, ] @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-roi-calculator-inputs, [ui-roi-calculator-inputs]', standalone: true, // React renders the <section> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiSeparatorComponent, UiSliderComponent, ], template: ` <section data-slot="roi-calculator-inputs" [class]="rootClass"> <div class="mx-auto max-w-4xl px-6 py-20 lg:py-28"> <div class="max-w-2xl"> <span ui-badge variant="secondary">Estimate</span> <h2 class="mt-4 text-3xl font-semibold tracking-tight sm:text-4xl">What the close currently costs you</h2> <p class="text-muted-foreground mt-3 text-lg"> Three inputs, one arithmetic model, stated in full underneath. Adjust anything that looks wrong. </p> </div> <div ui-card class="mt-10"> <div ui-card-content class="grid gap-8 p-6 sm:grid-cols-[1.2fr_1fr] sm:p-8"> <div class="space-y-6"> @for (input of INPUTS; track input.id) { <div> <div class="flex items-baseline justify-between gap-4"> <label [attr.for]="input.id" class="text-sm font-medium">{{ input.label }}</label> <span class="font-mono text-sm tabular-nums">{{ displayValue(input.id) }}</span> </div> <ui-slider [value]="modelValue(input.id)" (valueChange)="setModel(input.id, $event)" [min]="input.min" [max]="input.max" [step]="input.step" class="mt-3" [attr.aria-label]="input.label" /> </div> } </div> <div class="border-border bg-muted/30 flex flex-col justify-center rounded-lg border p-6 text-center"> <p class="text-muted-foreground text-xs tracking-wide uppercase">Estimated annual saving</p> <p class="font-display mt-2 text-4xl font-bold tracking-tight tabular-nums">{{ formatted() }}</p> <p class="text-muted-foreground mt-2 text-xs"> {{ daysSaved() }} days saved per close, {{ closesPerYear }} closes a year </p> <button ui-button class="mt-5"> Check it against your numbers <lucide-icon [img]="ArrowRight" class="ml-2 size-4" /> </button> </div> </div> <ui-separator /> <div class="px-6 py-4 sm:px-8"> <p class="text-muted-foreground text-xs leading-relaxed"> <span class="font-medium">The model. </span> Saving = days saved × closes per year × people × day rate, where days saved assumes {{ sharePercent }}% of close time is reconciliation that certified definitions remove. That share is the median across 240 rollouts; yours will differ, and the figure is only as good as that assumption. </p> </div> </div> </div> </section> `, }) export class UiRoiCalculatorInputsComponent { protected readonly ArrowRight = ArrowRight protected readonly INPUTS = INPUTS protected readonly closesPerYear = CLOSES_PER_YEAR protected readonly sharePercent = Math.round(DAYS_SAVED_SHARE * 100) @Input('class') className?: string readonly analysts = signal<number[]>([6]) readonly closeDays = signal<number[]>([9]) readonly rate = signal<number[]>([520]) readonly daysSaved = computed(() => Math.round((this.closeDays()[0] ?? 0) * DAYS_SAVED_SHARE * 10) / 10) readonly annualSaving = computed(() => Math.round(this.daysSaved() * CLOSES_PER_YEAR * (this.analysts()[0] ?? 0) * (this.rate()[0] ?? 0)), ) readonly formatted = computed(() => `£${this.annualSaving().toLocaleString('en-GB')}`) get rootClass(): string { return cn('bg-background', this.className) } modelValue(id: RoiInput['id']): number[] { if (id === 'analysts') return this.analysts() if (id === 'days') return this.closeDays() return this.rate() } setModel(id: RoiInput['id'], value: number[]): void { if (id === 'analysts') this.analysts.set(value) else if (id === 'days') this.closeDays.set(value) else this.rate.set(value) } displayValue(id: RoiInput['id']): string { if (id === 'analysts') return `${this.analysts()[0]}` if (id === 'days') return `${this.closeDays()[0]} days` return `£${this.rate()[0]}` } }src/app/components/blocks/roi-calculator-inputs/index.ts0.1 kB
export { UiRoiCalculatorInputsComponent } from './roi-calculator-inputs.component'
Raw manifest:https://uipkge.dev/r/angular/roi-calculator-inputs.json