Security console - counted filter bar
console-counted-filters.tsx
componentskill: emil-design-engdashboardsecurityfilterstabledense
Variants of security-console-2026-q3: console-counted-rail, console-severity-ledger
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
- a filter bar that tells you what a click will show before you click it. the three metric cards this replaced restated the page title, the nav count and one severity slice, and cost a full band of vertical space above the data
- Guardrails
- always a count on every filter, press feedback at 140ms on a strong ease-out, one selected treatment shared by chips, tabs and rowsnever animation on a keyboard-repeated action, a metric card that repeats a number already on screen, a second accent for the selected state
Install
pnpm dlx shadcn@latest add http://localhost:3040/r/console-counted-filters.json
Source
Meta block and library type import stripped - this is what pastes cleanly elsewhere.
import { Search } from 'lucide-react'
import { useMemo, useState } from 'react'
type Severity = 'critical' | 'high' | 'medium' | 'low' | 'info'
type Filter = Severity | 'all'
const CHIP: Record<Severity, string> = {
critical: 'border-red-500/30 bg-red-500/10 text-red-700 dark:text-red-400',
high: 'border-orange-500/30 bg-orange-500/10 text-orange-700 dark:text-orange-400',
medium: 'border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-400',
low: 'border-blue-500/30 bg-blue-500/10 text-blue-700 dark:text-blue-400',
info: 'border-zinc-500/30 bg-zinc-500/10 text-zinc-600 dark:text-zinc-400',
}
const FILTERS: Filter[] = ['all', 'critical', 'high', 'medium', 'low', 'info']
type Row = { name: string; template: string; host: string; sev: Severity; seen: string }
const ROWS: Row[] = [
{
name: 'unsafe random function in boundary generation',
template: 'CVE-2025-7783',
host: 'preview.komplyor.fr',
sev: 'critical',
seen: '07-10 11:27',
},
{
name: 'SSRF in IOFactory::load when filename is user controlled',
template: 'CVE-2026-34084',
host: 'media.changevivienne.com',
sev: 'critical',
seen: '07-10 11:04',
},
{
name: 'sandbox bypass in twig 2.16.x and 3.9.0 through 3.25.x',
template: 'CVE-2026-24425',
host: 'old.pma.changevivienne.com',
sev: 'high',
seen: '07-06 06:22',
},
{
name: 'mod_proxy request smuggling',
template: 'apache-httpd-smuggling',
host: 'www.changevivienne.com',
sev: 'high',
seen: '07-06 06:21',
},
{
name: 'TLS certificate expires in under 14 days',
template: 'ssl-expiry-window',
host: 'astraea-voyance.com',
sev: 'medium',
seen: '07-02 07:57',
},
{
name: 'directory listing enabled on static asset path',
template: 'dir-listing',
host: 'bijouxdantan.fr',
sev: 'low',
seen: '06-29 02:38',
},
{
name: 'server banner discloses exact patch version',
template: 'banner-disclosure',
host: 'godotcap.com',
sev: 'info',
seen: '06-29 02:31',
},
]
/** Strong ease-out. The built-in CSS curves are too weak to read as intended,
* and ease-in on a control feels broken because it withholds the first frame,
* which is the frame the user is watching. */
const EASE = 'ease-[cubic-bezier(0.23,1,0.32,1)]'
function Chip({
active,
count,
onClick,
children,
}: {
active: boolean
count: number
onClick: () => void
children: React.ReactNode
}) {
return (
<button
type="button"
onClick={onClick}
aria-pressed={active}
className={[
'inline-flex cursor-pointer items-center gap-1.5 rounded-md border px-2.5 py-1 text-sm capitalize',
'transition-[color,background-color,border-color,transform] duration-[140ms]',
EASE,
'active:scale-[0.97] motion-reduce:transition-none motion-reduce:active:scale-100',
active
? 'border-teal-500/40 bg-teal-500/10 text-teal-700 dark:text-teal-300'
: 'border-border text-muted-foreground hover:bg-muted/50 hover:text-foreground',
].join(' ')}
>
{children}
<span className="text-[11px] tabular-nums">{count.toLocaleString('en-US')}</span>
</button>
)
}
export default function ConsoleCountedFilters() {
const [filter, setFilter] = useState<Filter>('all')
const [query, setQuery] = useState('')
const counts = useMemo(() => {
const base: Record<Filter, number> = {
all: ROWS.length,
critical: 0,
high: 0,
medium: 0,
low: 0,
info: 0,
}
for (const row of ROWS) base[row.sev] += 1
return base
}, [])
const rows = useMemo(() => {
const q = query.trim().toLowerCase()
return ROWS.filter((r) => filter === 'all' || r.sev === filter).filter(
(r) => !q || r.name.toLowerCase().includes(q) || r.host.toLowerCase().includes(q),
)
}, [filter, query])
return (
<div className="min-h-[100dvh] bg-background p-4 text-foreground sm:p-8">
<div className="mx-auto max-w-5xl">
<header className="mb-7 flex flex-wrap items-start justify-between gap-x-6 gap-y-3">
<div className="min-w-0">
<h1 className="text-xl font-semibold tracking-tight">Vulnerabilities</h1>
<p className="mt-1.5 max-w-[68ch] text-sm text-muted-foreground">
Findings from the nuclei engine.
</p>
</div>
<span className="shrink-0 text-sm text-muted-foreground">
<span className="font-medium tabular-nums text-red-700 dark:text-red-400">47</span>{' '}
affected services
</span>
</header>
<div className="mb-4 flex flex-wrap items-center gap-2">
{FILTERS.map((f) => (
<Chip
key={f}
active={filter === f}
count={counts[f]}
onClick={() => setFilter(f)}
>
{f}
</Chip>
))}
<div className="relative ml-auto">
<Search className="pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search findings"
aria-label="Search findings"
className={`h-9 w-56 rounded-md border border-input bg-transparent pl-8 pr-3 text-sm transition-[border-color] duration-[140ms] ${EASE} placeholder:text-muted-foreground focus-visible:border-teal-500/50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-500 motion-reduce:transition-none`}
/>
</div>
</div>
<div className="overflow-hidden rounded-xl border border-border/70 bg-card">
<div className="overflow-x-auto">
<table className="w-full caption-bottom text-sm">
<thead>
<tr className="border-b border-border">
<th className="h-9 px-3 text-left text-xs font-medium text-muted-foreground">
Severity
</th>
<th className="h-9 px-3 text-left text-xs font-medium text-muted-foreground">
Finding
</th>
<th className="h-9 px-3 text-left text-xs font-medium text-muted-foreground">
Host
</th>
<th className="h-9 px-3 text-left text-xs font-medium text-muted-foreground">
Template
</th>
<th className="h-9 px-3 text-right text-xs font-medium text-muted-foreground">
Last seen
</th>
</tr>
</thead>
<tbody>
{rows.map((r) => (
<tr
key={r.template + r.host}
className={`border-b border-border/60 transition-colors duration-[120ms] ${EASE} last:border-0 hover:bg-muted/40 motion-reduce:transition-none`}
>
<td className="px-3 py-2.5 align-middle">
<span
className={`inline-flex items-center rounded-md border px-1.5 py-0.5 text-[11px] font-medium uppercase tracking-wide ${CHIP[r.sev]}`}
>
{r.sev}
</span>
</td>
<td className="max-w-[22rem] truncate px-3 py-2.5 align-middle">{r.name}</td>
<td className="px-3 py-2.5 align-middle font-mono text-xs text-muted-foreground">
{r.host}
</td>
<td className="px-3 py-2.5 align-middle">
<span className="rounded border border-border/70 bg-muted/40 px-1.5 py-0.5 font-mono text-[11px] text-muted-foreground">
{r.template}
</span>
</td>
<td className="whitespace-nowrap px-3 py-2.5 text-right align-middle text-xs tabular-nums text-muted-foreground">
{r.seen}
</td>
</tr>
))}
</tbody>
</table>
</div>
{rows.length === 0 && (
<div className="flex flex-col items-center gap-2 px-6 py-10 text-center">
<p className="text-sm font-medium">No findings match</p>
<p className="text-xs text-muted-foreground">
Clear the search, or widen the severity filter.
</p>
</div>
)}
</div>
</div>
</div>
)
}