Product components · @jengacore/risk-score
RiskScore
Mulisa: score, three labelled bands with a pin, and the rules that fired.
Mulisa
92, high risk58, medium risk31, low risk
78High risk
Score 78 of 100, high risk band.Why it fired · 2
- VEL-014More than 12 transfers from one MSISDN in 10 min
- PAY-021First transfer to payee above 2,000,000 SLSH
Install
terminal
npx shadcn@latest add @jengacore/risk-scoreWithout the namespace configured: npx shadcn@latest add https://ui.jengacore.com/r/<new-york-v4|base-nova>/risk-score.json
Registry dependencies
Source
components/jc/risk-score.tsx
import * as React from "react"
import { RuleChip } from "@/components/jc/rule-chip"
import { cn } from "@/lib/utils"
export type FiredRule = { code: string; condition: string }
/**
* Mulisa score: the number, a three-band scale (low / medium / high) with a pin, and the rules that fired.
* Bands are fixed per tenant (default 0–40–70–100) and labelled with words. Always show why.
*/
export function RiskScore({
score,
bands = [40, 70],
rules,
className,
}: {
score: number
bands?: [number, number]
rules: FiredRule[]
className?: string
}) {
const [mid, high] = bands
const band = score >= high ? "High" : score >= mid ? "Medium" : "Low"
const tone = score >= high ? "text-destructive" : score >= mid ? "text-warning" : "text-success"
return (
<div data-slot="risk-score" className={cn("grid gap-3", className)}>
<div className="flex items-baseline gap-2">
<span className="font-display text-[32px] leading-9 font-semibold tracking-[-0.01em] tabular-nums">{score}</span>
<span className={cn("text-sm font-medium", tone)}>{band} risk</span>
</div>
<div className="relative pt-1" aria-hidden>
<div className="flex h-1.5 overflow-hidden rounded-full">
<span className="bg-success-subtle contrast:bg-success/30" style={{ width: `${mid}%` }} />
<span className="bg-warning-subtle contrast:bg-warning/30" style={{ width: `${high - mid}%` }} />
<span className="flex-1 bg-destructive-subtle contrast:bg-destructive/30" />
</div>
<span className="absolute top-0 h-3.5 w-0.5 -translate-x-1/2 rounded-full bg-foreground" style={{ left: `${Math.min(Math.max(score, 0), 100)}%` }} />
<div className="mt-1.5 flex justify-between font-mono text-[10.5px] text-muted-foreground">
<span>0 Low</span>
<span style={{ marginLeft: `${mid - 10}%` }}>{mid} Medium</span>
<span>{high} High · 100</span>
</div>
</div>
<span className="sr-only">
Score {score} of 100, {band.toLowerCase()} risk band.
</span>
{rules.length ? (
<div className="flex flex-col gap-2 border-t border-border pt-4">
<span className="eyebrow">Why it fired · {rules.length}</span>
<ul className="grid gap-2.5">
{rules.map((r) => (
<li key={r.code} className="flex items-start gap-2.5 text-[13px] leading-[18px]">
<RuleChip code={r.code} className="mt-[-2px]" />
<span className="min-w-0">{r.condition}</span>
</li>
))}
</ul>
</div>
) : null}
</div>
)
}