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