jengacore.shadcn registry · Tailwind v4 · Radix and Base UI
Components
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-score

Without 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>
  )
}