Design Library

Security console - severity ledger

console-severity-ledger.tsx

componentskill: design-taste-frontenddashboardsecuritydenselistdark

Variants of security-console-2026-q3: console-counted-filters, console-counted-rail

Brief

The prompt that produced this. Re-run it to generate more in the same taste.

Aesthetic
Dark-first security console - cool slate, one teal accent, tinted-not-solid chips, tabular numerals, hairline separation, mono for machine strings, 8px controls and 14px containers
Reference
https://linear.app
Intent
an operator reads risk at a glance at 2am. colour means exactly one thing: teal is interaction, the severity ramp is how bad it is. never both, never a third vocabulary
Guardrails
always tabular-nums on every count, a non-zero count draws a visible sliver even at 0.03 percent, zero is good news and stays quiet, both themes
never a second accent, colour on a taxonomy, a hero-metric card restating a number already in the nav, solid badge fills in a list

Install

pnpm dlx shadcn@latest add http://localhost:3040/r/console-severity-ledger.json

Source

Meta block and library type import stripped - this is what pastes cleanly elsewhere.

import { ShieldAlert } from 'lucide-react'

type Severity = 'critical' | 'high' | 'medium' | 'low' | 'info'

/**
 * The whole colour vocabulary, in one place. Five copies of this map with
 * slightly different opacities is how a codebase ends up rendering the same
 * "high" finding in three different oranges.
 *
 * Light shades sit at -600 so they clear 4.5:1 on paper-white; dark shades at
 * -400 so they clear it on the elevated card. The fill is a 10 percent tint,
 * never solid: a column of solid badges reads as noise, not as ranking.
 */
const SEV: Record<Severity, { chip: string; bar: string; text: string }> = {
  critical: {
    chip: 'border-red-500/30 bg-red-500/10 text-red-700 dark:text-red-400',
    bar: 'bg-red-500/70',
    text: 'text-red-700 dark:text-red-400',
  },
  high: {
    chip: 'border-orange-500/30 bg-orange-500/10 text-orange-700 dark:text-orange-400',
    bar: 'bg-orange-500/70',
    text: 'text-orange-700 dark:text-orange-400',
  },
  medium: {
    chip: 'border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-400',
    bar: 'bg-amber-500/70',
    text: 'text-amber-700 dark:text-amber-400',
  },
  low: {
    chip: 'border-blue-500/30 bg-blue-500/10 text-blue-700 dark:text-blue-400',
    bar: 'bg-blue-500/70',
    text: 'text-blue-700 dark:text-blue-400',
  },
  info: {
    chip: 'border-zinc-500/30 bg-zinc-500/10 text-zinc-600 dark:text-zinc-400',
    bar: 'bg-zinc-500/70',
    text: 'text-zinc-600 dark:text-zinc-400',
  },
}

const ORDER: Severity[] = ['critical', 'high', 'medium', 'low', 'info']

const COUNTS: Record<Severity, number> = {
  critical: 30,
  high: 5295,
  medium: 589,
  low: 113,
  info: 80774,
}

type Finding = { name: string; template: string; host: string; sev: Severity; age: string }

const FINDINGS: Finding[] = [
  {
    name: 'form-data 4.0.0: unsafe random function in boundary generation',
    template: 'CVE-2025-7783',
    host: 'preview.komplyor.fr',
    sev: 'critical',
    age: '20d',
  },
  {
    name: 'phpspreadsheet 1.29.6: SSRF in IOFactory::load when filename is user controlled',
    template: 'CVE-2026-34084',
    host: 'media.changevivienne.com',
    sev: 'critical',
    age: '20d',
  },
  {
    name: 'twig 3.16.0: sandbox bypass in 2.16.x and 3.9.0 through 3.25.x',
    template: 'CVE-2026-24425',
    host: 'old.pma.changevivienne.com',
    sev: 'high',
    age: '22d',
  },
  {
    name: 'Apache HTTP Server 2.4.62: mod_proxy request smuggling',
    template: 'apache-httpd-smuggling',
    host: 'www.changevivienne.com',
    sev: 'high',
    age: '31d',
  },
  {
    name: 'TLS certificate expires in under 14 days',
    template: 'ssl-expiry-window',
    host: 'astraea-voyance.com',
    sev: 'medium',
    age: '2d',
  },
  {
    name: 'Directory listing enabled on static asset path',
    template: 'dir-listing',
    host: 'bijouxdantan.fr',
    sev: 'low',
    age: '49d',
  },
]

/** A count of 30 against 80k rounds to 0 percent, and an empty bar reads as
 *  "none" on the one row that matters most. Non-zero always draws. */
function width(value: number, max: number): string {
  if (value <= 0 || max <= 0) return '0%'
  return `${Math.max(2, Math.round((value / max) * 100))}%`
}

export default function ConsoleSeverityLedger() {
  const max = Math.max(...ORDER.map((s) => COUNTS[s]))
  const total = ORDER.reduce((sum, s) => sum + COUNTS[s], 0)

  return (
    <div className="min-h-[100dvh] bg-background p-4 text-foreground sm:p-8">
      <div className="mx-auto max-w-3xl">
        <header className="mb-7">
          <h1 className="text-xl font-semibold tracking-tight">Open vulnerabilities</h1>
          <p className="mt-1.5 max-w-[68ch] text-sm text-muted-foreground">
            Everything the engine matched on the last pass, ranked by what can actually hurt you.
          </p>
        </header>

        <section className="rounded-xl border border-border/70 bg-card p-5 shadow-[0_1px_2px_rgb(0_0_0/0.06),0_10px_28px_-16px_rgb(0_0_0/0.25)] dark:shadow-[0_1px_2px_rgb(0_0_0/0.35),0_10px_28px_-16px_rgb(0_0_0/0.65)]">
          <div className="flex flex-wrap items-end gap-x-8 gap-y-6">
            <div>
              <div className="text-4xl font-semibold leading-none tabular-nums">
                {total.toLocaleString('en-US')}
              </div>
              <div className="mt-2 text-xs text-muted-foreground">Total open</div>
            </div>
            {/* Five columns will not fit a 375px screen, so they fold to three
                and only line up in one row once there is room. `basis-full`
                rather than `flex-1`, or the flex basis wins and nothing wraps. */}
            <div className="grid min-w-0 basis-full grid-cols-3 gap-x-2 gap-y-5 sm:flex-1 sm:basis-0 sm:grid-cols-5">
              {ORDER.map((sev) => {
                const n = COUNTS[sev]
                return (
                  <div key={sev} className="min-w-0">
                    <div
                      className={`text-2xl font-semibold leading-none tabular-nums ${
                        n > 0 ? SEV[sev].text : 'text-muted-foreground'
                      }`}
                    >
                      {n.toLocaleString('en-US')}
                    </div>
                    <div className="mt-2 text-xs capitalize text-muted-foreground">{sev}</div>
                  </div>
                )
              })}
            </div>
          </div>

          <div className="mt-6 space-y-2.5 border-t border-border/70 pt-5">
            {ORDER.map((sev) => (
              <div key={sev} className="flex items-center gap-3">
                <div className="w-16 shrink-0 text-sm capitalize">{sev}</div>
                <div className="h-1.5 flex-1 overflow-hidden rounded-full bg-muted">
                  <div
                    className={`h-full rounded-full ${SEV[sev].bar}`}
                    style={{ width: width(COUNTS[sev], max) }}
                  />
                </div>
                <span className="w-16 shrink-0 text-right text-xs tabular-nums text-muted-foreground">
                  {COUNTS[sev].toLocaleString('en-US')}
                </span>
              </div>
            ))}
          </div>
        </section>

        <ul className="mt-5 divide-y divide-border/60 overflow-hidden rounded-xl border border-border/70 bg-card">
          {FINDINGS.map((f) => (
            <li key={f.template + f.host}>
              <button
                type="button"
                className="flex w-full items-start gap-3 px-4 py-3 text-left transition-[background-color,transform] duration-[160ms] ease-[cubic-bezier(0.23,1,0.32,1)] hover:bg-muted/40 active:scale-[0.995] motion-reduce:transition-none motion-reduce:active:scale-100"
              >
                <span
                  className={`mt-0.5 inline-flex shrink-0 items-center rounded-md border px-1.5 py-0.5 text-[11px] font-medium uppercase tracking-wide ${SEV[f.sev].chip}`}
                >
                  {f.sev}
                </span>
                <span className="min-w-0 flex-1">
                  <span className="block truncate text-sm">{f.name}</span>
                  <span className="mt-1 flex min-w-0 items-center gap-2 text-[11px] text-muted-foreground">
                    <span className="min-w-0 truncate rounded border border-border/70 bg-muted/40 px-1 font-mono">
                      {f.template}
                    </span>
                    <span className="min-w-0 truncate font-mono">{f.host}</span>
                    <span className="shrink-0 tabular-nums">{f.age}</span>
                  </span>
                </span>
              </button>
            </li>
          ))}
        </ul>

        <p className="mt-4 flex items-center gap-2 text-xs text-muted-foreground">
          <ShieldAlert className="size-3.5 shrink-0" />
          Teal is reserved for things you can click. Nothing on this page is teal, because nothing
          here is an action.
        </p>
      </div>
    </div>
  )
}