WIP: Dashboard redesign — design system overhaul and component updates

Frontend redesign in progress: updated styles, layout, and components
across all pages to align with new design system. Includes Jira API
compliance specs, property tests, and load test script.
This commit is contained in:
root
2026-04-29 14:20:23 +00:00
parent 37119b9c8a
commit 27192dd69f
78 changed files with 9902 additions and 1368 deletions

View File

@@ -0,0 +1,371 @@
// HomePage.jsx — full-page assembly of the CVE Dashboard Home view.
// Rebuilt 1:1 from frontend/src/App.js (currentPage === 'home').
//
// Layout: top stat row (4 metric cards) → 12-col grid below
// • col-span-9 (left): Quick CVE Lookup → Search/Filter → CVE list
// • col-span-3 (right): Calendar → Open Tickets → Archer → Ivanti
const {
COLORS: HC, StatCard, HomeCard, CardTitle, HomeButton, SeverityBadge, StatusBadge,
HomeInput, HomeSelect, FieldLabel, ResultBanner,
BigStat, MiniTicket, CVERow, VendorEntry,
HomeIcon: HI, CalendarMini, ArchiveSummary, ScrollList, EmptyState,
withAlpha: hAlpha,
} = window.HOME;
const { useState: useHomePageState } = React;
/* ── Sample data — close to what App.js renders against ──────── */
const SAMPLE_CVES = [
{
id: 'CVE-2025-1014',
severity: 'Critical',
description: 'Heap-based buffer overflow in the libnetfilter_queue user-space packet handler permits a remote attacker to execute arbitrary code via crafted ICMP traffic.',
statuses: ['Open', 'In Progress'],
vendors: [
{ vendor: 'Red Hat', severity: 'Critical', status: 'Open', docCount: 4 },
{ vendor: 'Ubuntu', severity: 'Critical', status: 'In Progress', docCount: 2 },
{ vendor: 'SUSE', severity: 'High', status: 'Resolved', docCount: 3 },
],
tickets: [
{ key: 'SEC-4821', summary: 'Patch netfilter on prod ingress fleet', status: 'In Progress' },
],
},
{
id: 'CVE-2025-0944',
severity: 'High',
description: 'Authentication bypass in admin console allows unauthenticated access to telemetry exports.',
statuses: ['Addressed'],
vendors: [
{ vendor: 'Cisco', severity: 'High', status: 'Addressed', docCount: 2 },
],
},
{
id: 'CVE-2024-9912',
severity: 'Medium',
description: 'Improper cert validation in the JIRA Server REST client could lead to MITM under attacker-controlled DNS.',
statuses: ['Resolved'],
vendors: [
{ vendor: 'Atlassian', severity: 'Medium', status: 'Resolved', docCount: 1 },
],
},
];
const SAMPLE_OPEN_TICKETS = [
{ key: 'SEC-4821', cveId: 'CVE-2025-1014', vendor: 'Red Hat', status: 'In Progress', summary: 'Patch netfilter ingress' },
{ key: 'SEC-4794', cveId: 'CVE-2025-0944', vendor: 'Cisco', status: 'Open', summary: 'Roll admin-console hotfix' },
{ key: 'SEC-4760', cveId: 'CVE-2024-9912', vendor: 'Atlassian', status: 'Open', summary: 'Validate cert chain' },
];
const SAMPLE_ARCHER = [
{ key: 'EXC-08291', cveId: 'CVE-2025-1014', vendor: 'SUSE', status: 'Pending Review' },
{ key: 'EXC-08214', cveId: 'CVE-2024-9912', vendor: 'Adobe', status: 'Draft' },
];
const SAMPLE_IVANTI = [
{ id: 'WF-1042', name: 'Quarterly compliance scan', state: 'In Review', type: 'compliance audit', when: 'Apr 24' },
{ id: 'WF-1038', name: 'Endpoint patch rollout — Linux fleet', state: 'In Progress', type: 'patch deploy', when: 'Apr 22' },
{ id: 'WF-1034', name: 'Identity provider rotation', state: 'Approved', type: 'access change', when: 'Apr 21' },
];
const ARCHIVE_SUMMARY = [
{ label: 'In Review', count: 12, tone: 'amber' },
{ label: 'In Progress', count: 8, tone: 'sky' },
{ label: 'Approved', count: 17, tone: 'green' },
{ label: 'Closed', count: 41, tone: 'neutral' },
];
/* ── Page ────────────────────────────────────────────────────── */
function HomePage() {
const [expanded, setExpanded] = useHomePageState(SAMPLE_CVES[0].id);
const [scanResult, setScanResult] = useHomePageState({ tone: 'success', text: 'CVE-2025-1014 addressed (3 vendors)' });
const [search, setSearch] = useHomePageState('');
const [vendor, setVendor] = useHomePageState('All Vendors');
const [severity, setSeverity] = useHomePageState('All Severities');
return (
<div data-screen-label="01 Home" style={{
padding: 32, minHeight: '100vh', background: 'var(--bg-page)',
fontFamily: 'var(--font-display)',
}}>
{/* ── Top: 4-up stats ── */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 16, marginBottom: 24 }}>
<StatCard label="Total CVEs" value="247" tone="sky" />
<StatCard label="Vendor Entries" value="412" tone="neutral" />
<StatCard label="Open Tickets" value="18" tone="amber" />
<StatCard label="Critical" value="6" tone="red" />
</div>
{/* ── 12-col body ── */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(12, 1fr)', gap: 24 }}>
{/* LEFT (col-span-9) */}
<div style={{ gridColumn: 'span 9', display: 'flex', flexDirection: 'column', gap: 16 }}>
{/* Quick CVE Lookup */}
<HomeCard>
<CardTitle color={HC.sky} icon="search">Quick CVE Lookup</CardTitle>
<div style={{ display: 'flex', gap: 12 }}>
<HomeInput placeholder="Enter CVE ID (e.g., CVE-2025-1014)" />
<HomeButton variant="primary" icon="search" onClick={() => setScanResult({ tone: 'success', text: 'CVE-2025-1014 addressed (3 vendors)' })}>
Scan
</HomeButton>
</div>
{scanResult && (
<div style={{ marginTop: 16 }}>
<ResultBanner tone={scanResult.tone} title={scanResult.text}>
<div style={{ display: 'grid', gap: 10, marginTop: 8 }}>
{SAMPLE_CVES[0].vendors.map(v => (
<div key={v.vendor} style={{
padding: 12, background: 'rgba(15,23,42,0.7)',
border: '1px solid rgba(14,165,233,0.30)', borderRadius: 6,
boxShadow: '0 4px 8px rgba(0,0,0,0.3)',
}}>
<div style={{ fontFamily: 'var(--font-display)', fontSize: 13, fontWeight: 600, color: 'var(--fg-1)', marginBottom: 6 }}>{v.vendor}</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 6, fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-2)' }}>
<span><strong style={{ color: 'var(--fg-1)' }}>Sev:</strong> {v.severity}</span>
<span><strong style={{ color: 'var(--fg-1)' }}>Status:</strong> {v.status}</span>
<span><strong style={{ color: 'var(--fg-1)' }}>Docs:</strong> {v.docCount}</span>
</div>
</div>
))}
</div>
</ResultBanner>
</div>
)}
</HomeCard>
{/* Search + Filter */}
<HomeCard>
<div style={{ display: 'grid', gap: 16 }}>
<div>
<FieldLabel icon="search">Search CVEs</FieldLabel>
<HomeInput value={search} onChange={e => setSearch(e.target.value)} placeholder="CVE ID or description…" />
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
<div>
<FieldLabel icon="filter">Vendor</FieldLabel>
<HomeSelect value={vendor} onChange={e => setVendor(e.target.value)} options={['All Vendors', 'Red Hat', 'Cisco', 'Ubuntu', 'SUSE', 'Atlassian', 'Adobe']} />
</div>
<div>
<FieldLabel icon="alert">Severity</FieldLabel>
<HomeSelect value={severity} onChange={e => setSeverity(e.target.value)} options={['All Severities', 'Critical', 'High', 'Medium', 'Low']} />
</div>
</div>
</div>
</HomeCard>
{/* Results summary */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<p style={{ margin: 0, fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--fg-2)' }}>
<strong style={{ color: HC.sky, fontWeight: 700 }}>{SAMPLE_CVES.length}</strong> CVEs
<span style={{ color: 'var(--fg-disabled)', margin: '0 8px' }}></span>
<span style={{ color: 'var(--fg-1)' }}>{SAMPLE_CVES.reduce((n, c) => n + c.vendors.length, 0)}</span> vendor entries
</p>
<HomeButton variant="primary" icon="download">Export 2 Docs</HomeButton>
</div>
{/* CVE list */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
{SAMPLE_CVES.map(cve => (
<CVERow
key={cve.id}
cveId={cve.id}
severity={cve.severity}
description={cve.description}
vendorCount={cve.vendors.length}
docCount={cve.vendors.reduce((s, v) => s + v.docCount, 0)}
statuses={cve.statuses}
expanded={expanded === cve.id}
onToggle={() => setExpanded(expanded === cve.id ? null : cve.id)}
>
{/* meta row */}
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16, fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--fg-2)' }}>
<span>Published: 2025-03-12</span>
<span style={{ color: HC.sky }}></span>
<span>{cve.vendors.length} affected vendor{cve.vendors.length !== 1 ? 's' : ''}</span>
{cve.vendors.length >= 2 && (
<HomeButton variant="danger" icon="trash" size="sm" style={{ marginLeft: 8 }}>Delete All</HomeButton>
)}
</div>
{/* vendor sub-cards */}
{cve.vendors.map((v, i) => (
<VendorEntry
key={`${cve.id}-${v.vendor}`}
vendor={v.vendor}
severity={v.severity}
status={v.status}
docCount={v.docCount}
onView={() => {}}
onEdit={() => {}}
onDelete={() => {}}
>
{/* For the first vendor of the first CVE, demonstrate the doc + ticket inset */}
{i === 0 && cve.id === SAMPLE_CVES[0].id && (
<>
<DocInset />
{cve.tickets && <TicketInset tickets={cve.tickets} />}
</>
)}
</VendorEntry>
))}
</CVERow>
))}
</div>
</div>
{/* RIGHT (col-span-3) */}
<div style={{ gridColumn: 'span 3', display: 'flex', flexDirection: 'column', gap: 16 }}>
{/* Calendar */}
<HomeCard padding={20} leftRail={HC.sky}>
<CardTitle color={HC.sky} icon="calendar">Calendar</CardTitle>
<CalendarMini today={26} markedDays={{ 14: 'red', 18: 'amber', 22: 'sky', 24: 'amber' }} />
</HomeCard>
{/* Open Tickets */}
<HomeCard padding={20} leftRail={HC.amber}>
<CardTitle
color={HC.amber}
icon="alert"
action={<HomeButton variant="warning" icon="plus" size="sm" />}
>Open Tickets</CardTitle>
<BigStat value={SAMPLE_OPEN_TICKETS.length} label="Active" color={HC.amber} />
<ScrollList maxHeight={280}>
{SAMPLE_OPEN_TICKETS.map(t => (
<MiniTicket key={t.key} keyText={t.key} cveId={t.cveId} vendor={t.vendor} summary={t.summary} status={t.status} tone="amber" onEdit={() => {}} onDelete={() => {}} />
))}
</ScrollList>
</HomeCard>
{/* Archer Risk */}
<HomeCard padding={20} leftRail={HC.purple}>
<CardTitle
color={HC.purple}
icon="shield"
action={<button style={{ background: hAlpha(HC.purple, 0.18), border: `1px solid ${HC.purple}`, color: HC.purple, padding: '4px 8px', borderRadius: 4, fontFamily: 'var(--font-mono)', fontSize: 11, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 4 }}><HI name="plus" size={12} color={HC.purple} /></button>}
>Archer Risk Tickets</CardTitle>
<BigStat value={SAMPLE_ARCHER.length} label="Active" color={HC.purple} />
<ScrollList maxHeight={220}>
{SAMPLE_ARCHER.map(t => (
<MiniTicket key={t.key} keyText={t.key} cveId={t.cveId} vendor={t.vendor} status={t.status} tone="purple" onEdit={() => {}} onDelete={() => {}} />
))}
</ScrollList>
</HomeCard>
{/* Ivanti Workflows */}
<HomeCard padding={20} leftRail={HC.teal}>
<CardTitle
color={HC.teal}
icon="activity"
action={<button style={{ background: hAlpha(HC.teal, 0.18), border: `1px solid ${HC.teal}`, color: HC.teal, padding: '4px 8px', borderRadius: 4, fontFamily: 'var(--font-mono)', fontSize: 11, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 4 }}><HI name="refresh" size={12} color={HC.teal} /> Sync</button>}
>Ivanti Workflows</CardTitle>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--fg-disabled)', marginBottom: 12 }}>
Synced Apr 26 · 9:42 AM
</div>
<ArchiveSummary items={ARCHIVE_SUMMARY} />
<BigStat value="78" label="Total Workflows" color={HC.teal} />
<ScrollList maxHeight={240}>
{SAMPLE_IVANTI.map(wf => (
<div key={wf.id} style={{
background: 'linear-gradient(135deg, rgba(30,41,59,0.85), rgba(51,65,85,0.75))',
border: `1px solid ${hAlpha(HC.teal, 0.25)}`, borderRadius: 6,
padding: 10,
boxShadow: '0 2px 6px rgba(0,0,0,0.25), inset 0 1px 0 rgba(255,255,255,0.03)',
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 6, marginBottom: 4 }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600, color: '#5EEAD4' }}>{wf.id}</span>
<StatusBadge tone="teal" size="sm">{wf.state}</StatusBadge>
</div>
<div style={{ fontFamily: 'var(--font-display)', fontSize: 12, color: 'var(--fg-1)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', marginBottom: 4 }}>{wf.name}</div>
<div style={{ display: 'flex', justifyContent: 'space-between', fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--fg-2)' }}>
<span>{wf.type}</span>
<span style={{ color: 'var(--fg-disabled)' }}>{wf.when}</span>
</div>
</div>
))}
</ScrollList>
</HomeCard>
</div>
</div>
</div>
);
}
/* ── Insets used inside the first VendorEntry ────────────────── */
function DocInset() {
return (
<div>
<h5 style={{
margin: '0 0 12px 0', display: 'flex', alignItems: 'center', gap: 6,
fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600,
color: 'var(--fg-1)', textTransform: 'uppercase', letterSpacing: '0.08em',
}}>
<HI name="doc" size={13} color={HC.sky} />
Documents (4)
</h5>
<div style={{ display: 'grid', gap: 8 }}>
{[
{ name: 'rh-advisory-2025-1014.pdf', meta: 'advisory · 220 KB' },
{ name: 'patch-notes-rhel9.pdf', meta: 'patch · 85 KB · approved by sec-eng' },
].map(d => (
<div key={d.name} style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
padding: '10px 12px', borderRadius: 4,
background: 'rgba(15,23,42,0.6)', border: '1px solid rgba(14,165,233,0.15)',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, flex: 1, minWidth: 0 }}>
<input type="checkbox" style={{ accentColor: HC.sky }} />
<HI name="doc" size={16} color={HC.sky} />
<div style={{ minWidth: 0 }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--fg-1)', fontWeight: 500 }}>{d.name}</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--fg-2)' }}>{d.meta}</div>
</div>
</div>
<div style={{ display: 'flex', gap: 6 }}>
<HomeButton variant="neutral" size="sm">View</HomeButton>
<HomeButton variant="danger" size="sm">Del</HomeButton>
</div>
</div>
))}
</div>
<HomeButton variant="neutral" icon="upload" size="sm" style={{ marginTop: 12 }}>Upload Doc</HomeButton>
</div>
);
}
function TicketInset({ tickets }) {
return (
<div style={{ marginTop: 16, paddingTop: 16, borderTop: '1px solid rgba(245,158,11,0.30)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
<h5 style={{
margin: 0, display: 'flex', alignItems: 'center', gap: 6,
fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600,
color: 'var(--fg-1)', textTransform: 'uppercase', letterSpacing: '0.08em',
}}>
<HI name="alert" size={13} color={HC.amber} />
JIRA Tickets ({tickets.length})
</h5>
<HomeButton variant="primary" icon="plus" size="sm">Add Ticket</HomeButton>
</div>
<div style={{ display: 'grid', gap: 8 }}>
{tickets.map(t => (
<div key={t.key} style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
padding: '10px 12px', borderRadius: 6,
background: 'linear-gradient(135deg, rgba(19,25,55,0.85), rgba(30,39,73,0.75))',
border: '1px solid rgba(255,184,0,0.30)',
boxShadow: '0 2px 6px rgba(0,0,0,0.25), inset 0 1px 0 rgba(255,255,255,0.04)',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flex: 1, minWidth: 0 }}>
<a href="#" onClick={e => e.preventDefault()} style={{ fontFamily: 'var(--font-mono)', fontSize: 12, fontWeight: 600, color: HC.sky, textDecoration: 'none' }}>{t.key}</a>
<span style={{ fontFamily: 'var(--font-display)', fontSize: 12, color: 'var(--fg-1)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{t.summary}</span>
<StatusBadge tone="amber" size="sm">{t.status}</StatusBadge>
</div>
</div>
))}
</div>
</div>
);
}
window.HOME_PAGE = { HomePage };

View File

@@ -0,0 +1,662 @@
// HomePrimitives.jsx — primitives for the CVE Dashboard Home page kit.
// Lifted directly from frontend/src/App.js (the home view), normalized to
// match the same vocabulary the Reporting + Knowledge Base kits use.
//
// Exported on window.HOME for the assembly + docs files to consume.
const { useState: useHomeState } = React;
/* ── Tokens ──────────────────────────────────────────────────────
Identical palette to Reporting + KB. Home adds purple (Archer)
and teal (Ivanti) — both used as left-rail / title-glow colors
on the right-side panel stack. */
const H_COLORS = {
sky: '#0EA5E9',
skySoft: '#7DD3FC',
green: '#10B981',
amber: '#F59E0B',
amberSoft: '#FCD34D',
red: '#EF4444',
redSoft: '#FCA5A5',
purple: '#8B5CF6',
teal: '#0D9488',
};
/* Card chrome shared with the rest of the system. One chrome, every panel. */
const CARD_BG = 'linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(15,23,42,0.98) 100%)';
const CARD_BORDER = '1.5px solid rgba(14,165,233,0.12)';
const CARD_BORDER_HOVER = '1.5px solid rgba(14,165,233,0.35)';
/* ── StatCard ────────────────────────────────────────────────────
Top-of-page metric tile. Color-coded by tone — sky for neutral
counts, amber for "needs attention", red for critical. Top edge
has a soft horizontal glow line in the same color. */
function StatCard({ label, value, tone = 'sky', mono = true }) {
const c = H_COLORS[tone] || H_COLORS.sky;
const isAccent = tone !== 'neutral';
return (
<div style={{
position: 'relative', overflow: 'hidden',
background: CARD_BG,
border: isAccent ? `2px solid ${c}` : CARD_BORDER,
borderRadius: 8, padding: 16,
boxShadow: isAccent
? `0 4px 16px rgba(0,0,0,0.5), 0 0 20px ${withAlpha(c, 0.15)}, inset 0 1px 0 ${withAlpha(c, 0.15)}`
: '0 4px 16px rgba(0,0,0,0.5)',
}}>
<div style={{
position: 'absolute', top: 0, left: 0, right: 0, height: 2,
background: `linear-gradient(90deg, transparent, ${c}, transparent)`,
boxShadow: `0 0 8px ${withAlpha(c, 0.5)}`,
}} />
<div style={{
fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600,
color: 'var(--fg-2)', textTransform: 'uppercase', letterSpacing: '0.1em',
marginBottom: 4,
}}>
{label}
</div>
<div style={{
fontFamily: mono ? 'var(--font-mono)' : 'var(--font-display)',
fontSize: 24, fontWeight: 700, color: c,
textShadow: isAccent ? `0 0 16px ${withAlpha(c, 0.4)}` : 'none',
lineHeight: 1,
}}>
{value}
</div>
</div>
);
}
/* ── HomeCard ────────────────────────────────────────────────────
Same chrome as Reporting's KbCard but without a label slot —
the home cards put their title inline above the body. Used as
the wrapper for Quick Lookup, the filter row, and CVE rows. */
function HomeCard({ children, padding = 24, hover = true, leftRail, style }) {
const [h, setH] = useHomeState(false);
return (
<div
onMouseEnter={() => hover && setH(true)}
onMouseLeave={() => setH(false)}
style={{
background: CARD_BG,
border: h ? CARD_BORDER_HOVER : CARD_BORDER,
borderLeft: leftRail ? `3px solid ${leftRail}` : (h ? CARD_BORDER_HOVER : CARD_BORDER).split(' ').slice(0).join(' '),
borderRadius: 8,
padding,
transition: 'border-color 200ms ease, box-shadow 200ms ease',
position: 'relative',
...style,
}}
>
{children}
</div>
);
}
/* ── CardTitle ───────────────────────────────────────────────────
Mono uppercase, glow color matches the card's identity (sky for
neutral, amber for tickets, purple for Archer, teal for Ivanti). */
function CardTitle({ color = H_COLORS.sky, icon, children, action }) {
return (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12, gap: 12 }}>
<h3 style={{
margin: 0,
fontFamily: 'var(--font-mono)', fontSize: 14, fontWeight: 600,
color, textTransform: 'uppercase', letterSpacing: '0.1em',
textShadow: `0 0 12px ${withAlpha(color, 0.4)}`,
display: 'flex', alignItems: 'center', gap: 8,
}}>
{icon && <HomeIcon name={icon} size={16} color={color} />}
{children}
</h3>
{action}
</div>
);
}
/* ── HomeButton ──────────────────────────────────────────────────
Wraps the four button variants the Home page uses, keeping the
exact same tinted-fill / outlined treatment as the Reporting kit
so all pages feel consistent. */
function HomeButton({ variant = 'neutral', icon, children, size = 'md', ...rest }) {
const [hover, setHover] = useHomeState(false);
const v = {
primary: { bg: hover ? 'rgba(16,185,129,0.18)' : 'rgba(16,185,129,0.10)', bd: H_COLORS.green, fg: H_COLORS.green },
neutral: { bg: hover ? 'rgba(14,165,233,0.10)' : 'transparent', bd: 'rgba(14,165,233,0.5)', fg: H_COLORS.sky },
subtle: { bg: hover ? 'rgba(14,165,233,0.16)' : 'rgba(14,165,233,0.08)', bd: 'rgba(14,165,233,0.30)', fg: H_COLORS.sky },
danger: { bg: hover ? 'rgba(239,68,68,0.18)' : 'rgba(239,68,68,0.10)', bd: 'rgba(239,68,68,0.5)', fg: H_COLORS.red },
warning: { bg: hover ? 'rgba(245,158,11,0.18)' : 'rgba(245,158,11,0.10)', bd: 'rgba(245,158,11,0.5)', fg: H_COLORS.amber },
}[variant];
const padX = size === 'sm' ? 10 : 14;
const padY = size === 'sm' ? 4 : 8;
const fs = size === 'sm' ? 11 : 12;
return (
<button
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
style={{
display: 'inline-flex', alignItems: 'center', gap: 6,
padding: `${padY}px ${padX}px`, borderRadius: 6,
background: v.bg, border: `1px solid ${v.bd}`, color: v.fg,
fontFamily: 'var(--font-mono)', fontSize: fs, fontWeight: 600,
textTransform: 'uppercase', letterSpacing: '0.06em',
cursor: 'pointer', transition: 'all 160ms ease', whiteSpace: 'nowrap',
}}
{...rest}
>
{icon && <HomeIcon name={icon} size={fs + 2} color={v.fg} />}
{children}
</button>
);
}
/* ── SeverityBadge ───────────────────────────────────────────────
Strong tinted-fill badge used in CVE rows. Critical/High/Medium/Low. */
function SeverityBadge({ level }) {
const map = {
Critical: { c: H_COLORS.red, text: H_COLORS.redSoft },
High: { c: H_COLORS.amber, text: H_COLORS.amberSoft },
Medium: { c: H_COLORS.sky, text: H_COLORS.skySoft },
Low: { c: H_COLORS.green, text: '#6EE7B7' },
}[level] || { c: H_COLORS.sky, text: H_COLORS.skySoft };
return (
<span style={{
display: 'inline-flex', alignItems: 'center', gap: 6,
background: `linear-gradient(135deg, ${withAlpha(map.c, 0.25)}, ${withAlpha(map.c, 0.20)})`,
border: `2px solid ${map.c}`, borderRadius: 6,
padding: '4px 10px',
color: map.text, fontWeight: 700, fontSize: 11,
textTransform: 'uppercase', letterSpacing: '0.05em',
fontFamily: 'var(--font-mono)',
textShadow: `0 0 8px ${withAlpha(map.c, 0.5)}`,
boxShadow: `0 0 16px ${withAlpha(map.c, 0.25)}, 0 4px 8px rgba(0,0,0,0.4)`,
}}>
<span style={{
display: 'inline-block', width: 6, height: 6, borderRadius: '50%',
background: map.c, boxShadow: `0 0 8px ${map.c}`,
}} />
{level}
</span>
);
}
/* ── StatusBadge ─────────────────────────────────────────────────
Tone-coded text badge used for ticket statuses (Open / In Progress /
Closed / Draft / Accepted). Smaller and lighter than SeverityBadge. */
function StatusBadge({ tone = 'sky', children, size = 'md' }) {
const c = H_COLORS[tone] || H_COLORS.sky;
const fs = size === 'sm' ? 10 : 11;
const pad = size === 'sm' ? '3px 7px' : '4px 9px';
return (
<span style={{
display: 'inline-flex', alignItems: 'center', gap: 6,
background: withAlpha(c, 0.18),
border: `1px solid ${c}`, borderRadius: 4,
padding: pad, color: c,
fontFamily: 'var(--font-mono)', fontSize: fs, fontWeight: 600,
textTransform: 'uppercase', letterSpacing: '0.05em',
whiteSpace: 'nowrap',
}}>
<span style={{
display: 'inline-block', width: 6, height: 6, borderRadius: '50%',
background: c, boxShadow: `0 0 6px ${c}`,
}} />
{children}
</span>
);
}
/* ── HomeInput / HomeSelect ──────────────────────────────────────
The intel-input look: dark fill + sky border on focus. */
function HomeInput({ icon, ...rest }) {
const [focus, setFocus] = useHomeState(false);
return (
<div style={{ position: 'relative', flex: 1 }}>
{icon && (
<div style={{ position: 'absolute', left: 12, top: '50%', transform: 'translateY(-50%)', color: H_COLORS.sky }}>
<HomeIcon name={icon} size={14} color={H_COLORS.sky} />
</div>
)}
<input
onFocus={() => setFocus(true)} onBlur={() => setFocus(false)}
style={{
width: '100%', boxSizing: 'border-box',
background: 'rgba(15,23,42,0.85)',
border: `1px solid ${focus ? H_COLORS.sky : 'rgba(14,165,233,0.25)'}`,
borderRadius: 6,
padding: icon ? '9px 12px 9px 34px' : '9px 12px',
color: 'var(--fg-1)',
fontFamily: 'var(--font-mono)', fontSize: 13,
outline: 'none', transition: 'border-color 160ms ease',
boxShadow: focus ? `0 0 0 3px ${withAlpha(H_COLORS.sky, 0.15)}` : 'none',
}}
{...rest}
/>
</div>
);
}
function HomeSelect({ value, onChange, options }) {
return (
<select value={value} onChange={onChange} style={{
width: '100%', boxSizing: 'border-box',
background: 'rgba(15,23,42,0.85)',
border: '1px solid rgba(14,165,233,0.25)', borderRadius: 6,
padding: '9px 12px', color: 'var(--fg-1)',
fontFamily: 'var(--font-mono)', fontSize: 13,
outline: 'none', appearance: 'none',
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%230EA5E9' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E")`,
backgroundRepeat: 'no-repeat', backgroundPosition: 'right 12px center', paddingRight: 32,
}}>
{options.map(o => <option key={o} value={o}>{o}</option>)}
</select>
);
}
function FieldLabel({ icon, children }) {
return (
<label style={{
display: 'flex', alignItems: 'center', gap: 6,
fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600,
color: 'var(--fg-2)', textTransform: 'uppercase', letterSpacing: '0.08em',
marginBottom: 8,
}}>
{icon && <HomeIcon name={icon} size={13} color="currentColor" />}
{children}
</label>
);
}
/* ── ResultBanner ────────────────────────────────────────────────
Sub-card used in Quick Lookup to surface scan results.
Tones: success (CVE addressed), warning (not found), error. */
function ResultBanner({ tone = 'success', icon, title, children }) {
const map = {
success: { c: H_COLORS.green, bg: 'rgba(16,185,129,0.10)', bd: 'rgba(16,185,129,0.30)' },
warning: { c: H_COLORS.amber, bg: 'rgba(245,158,11,0.10)', bd: 'rgba(245,158,11,0.30)' },
error: { c: H_COLORS.red, bg: 'rgba(239,68,68,0.10)', bd: 'rgba(239,68,68,0.30)' },
}[tone];
return (
<div style={{
display: 'flex', alignItems: 'flex-start', gap: 12,
padding: 16, borderRadius: 6,
background: map.bg, border: `1px solid ${map.bd}`,
}}>
<div style={{ color: map.c, marginTop: 1 }}>
<HomeIcon name={icon || tone} size={18} color={map.c} />
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{
fontFamily: 'var(--font-mono)', fontSize: 13, fontWeight: 600,
color: map.c, marginBottom: children ? 8 : 0,
}}>
{title}
</div>
{children}
</div>
</div>
);
}
/* ── BigStat ─────────────────────────────────────────────────────
The centered "active count + label" shown at the top of each
right-rail panel (Open Tickets · Archer · Ivanti). */
function BigStat({ value, label, color = H_COLORS.sky }) {
return (
<div style={{ textAlign: 'center', marginBottom: 12 }}>
<div style={{
fontFamily: 'var(--font-mono)', fontSize: 32, fontWeight: 700,
color, textShadow: `0 0 16px ${withAlpha(color, 0.4)}`, lineHeight: 1,
}}>
{value}
</div>
<div style={{
fontFamily: 'var(--font-mono)', fontSize: 10, fontWeight: 600,
color: 'var(--fg-2)', textTransform: 'uppercase', letterSpacing: '0.12em',
marginTop: 6,
}}>
{label}
</div>
</div>
);
}
/* ── MiniTicket ──────────────────────────────────────────────────
Compact card shown inside the right-rail scrollable lists.
Color-coded by category via the `tone` prop (amber/purple/teal). */
function MiniTicket({ keyText, cveId, vendor, status, tone = 'amber', summary, onEdit, onDelete }) {
const c = H_COLORS[tone] || H_COLORS.amber;
return (
<div style={{
background: 'linear-gradient(135deg, rgba(30,41,59,0.85), rgba(51,65,85,0.75))',
border: `1px solid ${withAlpha(c, 0.25)}`, borderRadius: 6,
padding: 10,
boxShadow: '0 2px 6px rgba(0,0,0,0.25), inset 0 1px 0 rgba(255,255,255,0.03)',
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 6, marginBottom: 4 }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600, color: H_COLORS.sky }}>
{keyText}
</span>
{(onEdit || onDelete) && (
<div style={{ display: 'flex', gap: 4 }}>
{onEdit && <button onClick={onEdit} style={iconBtn(H_COLORS.amber)}><HomeIcon name="edit" size={11} color="currentColor" /></button>}
{onDelete && <button onClick={onDelete} style={iconBtn(H_COLORS.red)}><HomeIcon name="trash" size={11} color="currentColor" /></button>}
</div>
)}
</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-1)', marginBottom: 2 }}>{cveId}</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--fg-2)' }}>{vendor}</div>
{summary && (
<div style={{
fontSize: 11, color: 'var(--fg-2)', marginTop: 4,
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
fontFamily: 'var(--font-display)',
}}>{summary}</div>
)}
{status && (
<div style={{ marginTop: 8 }}>
<StatusBadge tone={tone} size="sm">{status}</StatusBadge>
</div>
)}
</div>
);
}
const iconBtn = (color) => ({
background: 'transparent', border: 'none', color: 'var(--fg-2)',
cursor: 'pointer', padding: 2, display: 'inline-flex', alignItems: 'center',
transition: 'color 120ms ease',
});
/* ── CVERow ──────────────────────────────────────────────────────
The main "row" in the home feed. Collapsed = chevron · CVE-ID ·
description · meta row (severity badge, vendor count, doc count,
statuses). Expanded = full description + admin actions slot. */
function CVERow({ cveId, severity, description, vendorCount, docCount, statuses, expanded, onToggle, children }) {
return (
<div style={{
background: CARD_BG, border: CARD_BORDER, borderRadius: 8,
transition: 'border-color 200ms ease',
}}>
<button
onClick={onToggle}
style={{
width: '100%', textAlign: 'left',
background: 'transparent', border: 'none',
padding: 24, cursor: 'pointer',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 6 }}>
<span style={{
display: 'inline-block', transform: expanded ? 'rotate(0)' : 'rotate(-90deg)',
transition: 'transform 200ms ease', color: H_COLORS.sky,
}}>
<HomeIcon name="chevron" size={18} color={H_COLORS.sky} />
</span>
<h3 style={{
margin: 0, fontFamily: 'var(--font-mono)', fontSize: 22, fontWeight: 700,
color: H_COLORS.sky, letterSpacing: '-0.01em',
}}>{cveId}</h3>
</div>
<div style={{ marginLeft: 30 }}>
<p style={{
margin: '0 0 8px 0',
color: 'var(--fg-1)', fontSize: 13, lineHeight: 1.5,
fontFamily: 'var(--font-display)',
display: '-webkit-box', WebkitLineClamp: expanded ? 'unset' : 1,
WebkitBoxOrient: 'vertical', overflow: 'hidden',
textOverflow: 'ellipsis',
}}>
{description}
</p>
<div style={{ display: 'flex', alignItems: 'center', gap: 14, flexWrap: 'wrap' }}>
<SeverityBadge level={severity} />
<span style={metaText}>{vendorCount} vendor{vendorCount !== 1 ? 's' : ''}</span>
<span style={{ ...metaText, display: 'inline-flex', alignItems: 'center', gap: 4 }}>
<HomeIcon name="doc" size={11} color="currentColor" />
{docCount} doc{docCount !== 1 ? 's' : ''}
</span>
<span style={metaText}>{statuses.join(', ')}</span>
</div>
</div>
</button>
{expanded && children && (
<div style={{ padding: '0 24px 24px', marginLeft: 30 }}>
{children}
</div>
)}
</div>
);
}
const metaText = {
fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-2)',
};
/* ── VendorEntry ─────────────────────────────────────────────────
Sub-card inside an expanded CVE row, one per vendor that filed
the CVE. Holds vendor name, severity, status, doc count, and
inline action buttons. */
function VendorEntry({ vendor, severity, status, docCount, children, onView, onEdit, onDelete }) {
return (
<div style={{
background: 'linear-gradient(135deg, rgba(15,23,42,0.95) 0%, rgba(30,41,59,0.9) 100%)',
border: '1.5px solid rgba(14,165,233,0.30)', borderRadius: 6,
padding: 16, marginBottom: 12,
boxShadow: '0 4px 12px rgba(0,0,0,0.4), inset 0 1px 0 rgba(14,165,233,0.08)',
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12 }}>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 8 }}>
<h4 style={{ margin: 0, fontFamily: 'var(--font-display)', fontSize: 15, fontWeight: 600, color: 'var(--fg-1)' }}>{vendor}</h4>
<SeverityBadge level={severity} />
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 16, fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--fg-2)' }}>
<span>Status: <strong style={{ color: 'var(--fg-1)', fontWeight: 500 }}>{status}</strong></span>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
<HomeIcon name="doc" size={13} color="currentColor" />
{docCount} doc{docCount !== 1 ? 's' : ''}
</span>
</div>
</div>
<div style={{ display: 'flex', gap: 6, flexShrink: 0 }}>
{onView && <HomeButton variant="neutral" icon="eye" size="sm" onClick={onView}>View</HomeButton>}
{onEdit && <HomeButton variant="warning" icon="edit" size="sm" onClick={onEdit} />}
{onDelete && <HomeButton variant="danger" icon="trash" size="sm" onClick={onDelete} />}
</div>
</div>
{children && (
<div style={{ marginTop: 16, paddingTop: 16, borderTop: '1px solid rgba(14,165,233,0.20)' }}>
{children}
</div>
)}
</div>
);
}
/* ── HomeIcon ────────────────────────────────────────────────────
Inline SVGs covering every icon used on the home page so the kit
has no external icon-font dependency. Keys mirror lucide-react names. */
function HomeIcon({ name, size = 16, color = 'currentColor' }) {
const p = {
width: size, height: size, viewBox: '0 0 24 24', fill: 'none',
stroke: color, strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round',
style: { display: 'inline-block', verticalAlign: 'middle' },
};
switch (name) {
case 'search': return <svg {...p}><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>;
case 'filter': return <svg {...p}><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>;
case 'alert': return <svg {...p}><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>;
case 'check':
case 'success': return <svg {...p}><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>;
case 'warning': return <svg {...p}><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>;
case 'error':
case 'x': return <svg {...p}><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>;
case 'shield': return <svg {...p}><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>;
case 'activity': return <svg {...p}><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>;
case 'doc': return <svg {...p}><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>;
case 'eye': return <svg {...p}><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/></svg>;
case 'edit': return <svg {...p}><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>;
case 'trash': return <svg {...p}><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>;
case 'plus': return <svg {...p}><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>;
case 'refresh': return <svg {...p}><path d="M3 12a9 9 0 0 1 15-6.7L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-15 6.7L3 16"/><path d="M3 21v-5h5"/></svg>;
case 'chevron': return <svg {...p}><polyline points="6 9 12 15 18 9"/></svg>;
case 'upload': return <svg {...p}><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>;
case 'download': return <svg {...p}><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>;
case 'calendar': return <svg {...p}><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>;
default: return <svg {...p}><circle cx="12" cy="12" r="10"/></svg>;
}
}
/* ── CalendarMini ────────────────────────────────────────────────
Minimal calendar surface for the right rail. Static — accepts a
`today` index and an optional `markedDays` map for severity dots. */
function CalendarMini({ month = 'April 2026', today = 26, markedDays = {} }) {
const dows = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
// April 2026 starts on Wednesday — empty cells for S/M/T
const startOffset = 3;
const daysInMonth = 30;
const cells = [...Array(startOffset).fill(null), ...Array.from({ length: daysInMonth }, (_, i) => i + 1)];
return (
<div>
<div style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
marginBottom: 12,
}}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--fg-1)', fontWeight: 600 }}>{month}</span>
<div style={{ display: 'flex', gap: 4 }}>
<button style={navBtn}></button>
<button style={navBtn}></button>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: 2 }}>
{dows.map((d, i) => (
<div key={`dow-${i}`} style={{
fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--fg-disabled)',
textAlign: 'center', padding: '4px 0', fontWeight: 600,
}}>{d}</div>
))}
{cells.map((day, i) => {
if (day === null) return <div key={`empty-${i}`} />;
const mark = markedDays[day];
const isToday = day === today;
return (
<button key={`day-${day}`} style={{
position: 'relative',
padding: '6px 0', borderRadius: 4,
background: isToday ? withAlpha(H_COLORS.sky, 0.20) : 'transparent',
border: isToday ? `1px solid ${H_COLORS.sky}` : '1px solid transparent',
color: isToday ? H_COLORS.sky : 'var(--fg-1)',
fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: isToday ? 700 : 500,
cursor: 'pointer', transition: 'background 120ms ease',
}}>
{day}
{mark && (
<span style={{
position: 'absolute', bottom: 2, left: '50%', transform: 'translateX(-50%)',
width: 4, height: 4, borderRadius: '50%',
background: H_COLORS[mark] || H_COLORS.amber,
}} />
)}
</button>
);
})}
</div>
</div>
);
}
const navBtn = {
background: 'transparent', border: '1px solid rgba(14,165,233,0.25)',
color: H_COLORS.sky, borderRadius: 4, width: 22, height: 22,
fontFamily: 'var(--font-mono)', fontSize: 12, cursor: 'pointer',
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
};
/* ── ArchiveSummary ──────────────────────────────────────────────
The bar of state pills that lives at the top of the Ivanti card.
Each pill shows an Ivanti workflow state + count, color-coded. */
function ArchiveSummary({ items, activeFilter, onSelect }) {
return (
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 12 }}>
{items.map(it => {
const c = H_COLORS[it.tone] || H_COLORS.teal;
const active = activeFilter === it.label;
return (
<button
key={it.label}
onClick={() => onSelect && onSelect(active ? null : it.label)}
style={{
flex: '1 1 60px',
padding: '8px 10px',
background: active ? withAlpha(c, 0.20) : withAlpha(c, 0.08),
border: `1px solid ${active ? c : withAlpha(c, 0.30)}`,
borderRadius: 4,
cursor: 'pointer',
fontFamily: 'var(--font-mono)',
}}
>
<div style={{
fontSize: 16, fontWeight: 700, color: c,
textShadow: active ? `0 0 8px ${withAlpha(c, 0.5)}` : 'none', lineHeight: 1,
}}>{it.count}</div>
<div style={{
fontSize: 9, color: 'var(--fg-2)', textTransform: 'uppercase',
letterSpacing: '0.06em', marginTop: 4, fontWeight: 600,
}}>{it.label}</div>
</button>
);
})}
</div>
);
}
/* ── ScrollList ──────────────────────────────────────────────────
Generic max-height scroll wrapper for the right-rail panels. */
function ScrollList({ maxHeight = 300, children }) {
return (
<div style={{
maxHeight, overflowY: 'auto',
display: 'flex', flexDirection: 'column', gap: 8,
paddingRight: 4,
}}>
{children}
</div>
);
}
/* ── EmptyState ──────────────────────────────────────────────────
Center-aligned check-circle + caption, used inside ScrollList
when a panel has no items. */
function EmptyState({ icon = 'check', tone = 'green', children }) {
const c = H_COLORS[tone] || H_COLORS.green;
return (
<div style={{ textAlign: 'center', padding: '24px 0' }}>
<div style={{ display: 'inline-flex', marginBottom: 8 }}>
<HomeIcon name={icon} size={32} color={c} />
</div>
<p style={{
margin: 0, fontFamily: 'var(--font-mono)', fontSize: 12,
color: 'var(--fg-2)', fontStyle: 'italic',
}}>{children}</p>
</div>
);
}
/* ── helpers ─────────────────────────────────────────────────── */
function withAlpha(hex, a) {
const h = hex.replace('#', '');
const r = parseInt(h.slice(0, 2), 16);
const g = parseInt(h.slice(2, 4), 16);
const b = parseInt(h.slice(4, 6), 16);
return `rgba(${r}, ${g}, ${b}, ${a})`;
}
window.HOME = {
COLORS: H_COLORS,
StatCard, HomeCard, CardTitle, HomeButton, SeverityBadge, StatusBadge,
HomeInput, HomeSelect, FieldLabel, ResultBanner,
BigStat, MiniTicket, CVERow, VendorEntry,
HomeIcon, CalendarMini, ArchiveSummary, ScrollList, EmptyState,
withAlpha,
};

View File

@@ -0,0 +1,443 @@
// KitDocs.jsx — browseable docs page for the Home kit.
// Sections: Overview · Tokens · Components · Assemblies · Reference page.
const { useState: useDocsHomeState } = React;
const {
COLORS: DHC, StatCard: DStatCard, HomeCard: DHomeCard, CardTitle: DCardTitle,
HomeButton: DBtn, SeverityBadge: DSev, StatusBadge: DStatus,
HomeInput: DInput, HomeSelect: DSelect, FieldLabel: DLabel, ResultBanner: DBanner,
BigStat: DBigStat, MiniTicket: DMini, CVERow: DCVERow, VendorEntry: DVendor,
HomeIcon: DIcon, CalendarMini: DCal, ArchiveSummary: DArchive, ScrollList: DScroll,
EmptyState: DEmpty, withAlpha: dAlpha,
} = window.HOME;
const { HomePage: DHomePage } = window.HOME_PAGE;
/* ── Layout primitives (same vocabulary as the Reporting kit docs) ── */
function HSection({ id, eyebrow, title, blurb, children }) {
return (
<section id={id} style={{ paddingTop: 32, scrollMarginTop: 120 }}>
<div style={{ marginBottom: 16 }}>
{eyebrow && (
<div style={{
fontFamily: 'var(--font-mono)', fontSize: 10, fontWeight: 700,
color: DHC.sky, textTransform: 'uppercase', letterSpacing: '0.18em',
marginBottom: 6,
}}>{eyebrow}</div>
)}
<h2 style={{
fontFamily: 'var(--font-mono)', fontSize: 22, fontWeight: 700,
color: 'var(--fg-1)', margin: 0, letterSpacing: '0.02em',
}}>{title}</h2>
{blurb && (
<p style={{
fontFamily: 'var(--font-display)', fontSize: 14, lineHeight: 1.6,
color: 'var(--fg-muted)', maxWidth: 660, margin: '8px 0 0 0',
}}>{blurb}</p>
)}
</div>
{children}
</section>
);
}
function HSpec({ label, children }) {
return (
<div style={{
display: 'grid', gridTemplateColumns: '160px 1fr', gap: 16,
padding: '10px 0', borderBottom: '1px solid rgba(255,255,255,0.04)',
fontFamily: 'var(--font-mono)', fontSize: 12,
}}>
<div style={{ color: 'var(--fg-disabled)', textTransform: 'uppercase', letterSpacing: '0.06em', fontSize: 10, fontWeight: 600 }}>{label}</div>
<div style={{ color: 'var(--fg-2)' }}>{children}</div>
</div>
);
}
function HCode({ children }) {
return (
<code style={{
display: 'inline-block', padding: '2px 6px', borderRadius: 3,
background: 'rgba(14,165,233,0.10)', border: '1px solid rgba(14,165,233,0.18)',
fontFamily: 'var(--font-mono)', fontSize: 11, color: DHC.skySoft,
}}>{children}</code>
);
}
function HSwatch({ name, value, role }) {
return (
<div style={{
display: 'grid', gridTemplateColumns: '52px 1fr auto', alignItems: 'center', gap: 14,
padding: '8px 0', borderBottom: '1px solid rgba(255,255,255,0.04)',
}}>
<div style={{ height: 36, borderRadius: 6, background: value, border: '1px solid rgba(255,255,255,0.08)' }} />
<div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--fg-1)', fontWeight: 600 }}>{name}</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--fg-disabled)' }}>{role}</div>
</div>
<HCode>{value}</HCode>
</div>
);
}
function HSpecimen({ children, padding = 24, dark = true, style }) {
return (
<div style={{
padding,
background: dark ? 'rgba(15,23,42,0.5)' : 'transparent',
border: '1px solid rgba(255,255,255,0.05)', borderRadius: 8,
...style,
}}>{children}</div>
);
}
/* ── Sticky tab strip ─────────────────────────────────────────── */
const TABS = [
{ id: 'overview', label: 'Overview' },
{ id: 'tokens', label: 'Tokens' },
{ id: 'components', label: 'Components' },
{ id: 'assemblies', label: 'Assemblies' },
{ id: 'reference', label: 'Reference Page' },
];
function HKitDocs() {
const [active, setActive] = useDocsHomeState('overview');
const handle = (id) => {
setActive(id);
const el = document.getElementById(id);
if (el) {
const top = el.getBoundingClientRect().top + window.scrollY - 80;
window.scrollTo({ top, behavior: 'smooth' });
}
};
return (
<div style={{ minHeight: '100vh', background: 'var(--bg-page)' }}>
{/* Header */}
<header style={{
padding: '40px 48px 0 48px', maxWidth: 1280, margin: '0 auto',
}}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 700, color: DHC.sky, textTransform: 'uppercase', letterSpacing: '0.18em', marginBottom: 8 }}>
STEAM Security · UI Kit
</div>
<h1 style={{
margin: 0, fontFamily: 'var(--font-mono)', fontSize: 36, fontWeight: 700,
color: DHC.green, textTransform: 'uppercase', letterSpacing: '0.08em',
textShadow: '0 0 24px rgba(16,185,129,0.30)',
}}>
Home
</h1>
<p style={{
margin: '12px 0 0 0', maxWidth: 720, fontSize: 15, lineHeight: 1.6,
color: 'var(--fg-muted)', fontFamily: 'var(--font-display)',
}}>
The "command center" landing view of the CVE Dashboard. Pulls four signals into one screen:
a top metric strip, a CVE feed with vendor sub-rows, and a right-rail stack of
Calendar · JIRA · Archer · Ivanti. Built from the same chrome and tokens as the Reporting kit.
</p>
</header>
{/* Tab strip */}
<nav style={{
position: 'sticky', top: 0, zIndex: 10,
marginTop: 28,
background: 'rgba(2,6,23,0.85)', backdropFilter: 'blur(8px)',
borderBottom: '1px solid rgba(14,165,233,0.15)',
}}>
<div style={{ maxWidth: 1280, margin: '0 auto', padding: '0 48px', display: 'flex', gap: 4 }}>
{TABS.map(t => {
const on = active === t.id;
return (
<button key={t.id} onClick={() => handle(t.id)} style={{
padding: '14px 16px',
background: 'transparent', border: 'none',
borderBottom: `2px solid ${on ? DHC.sky : 'transparent'}`,
color: on ? DHC.sky : 'var(--fg-2)',
fontFamily: 'var(--font-mono)', fontSize: 12, fontWeight: 600,
textTransform: 'uppercase', letterSpacing: '0.08em',
cursor: 'pointer', transition: 'all 160ms ease',
}}>{t.label}</button>
);
})}
</div>
</nav>
{/* Body */}
<main style={{ maxWidth: 1280, margin: '0 auto', padding: '0 48px 96px 48px' }}>
{/* OVERVIEW */}
<HSection id="overview" eyebrow="01 — Overview" title="Why this kit exists" blurb="Documents the visual + behavioral vocabulary of the home view so other dashboards in the suite can re-use the right-rail stack, the CVE row pattern, and the four-up stat strip without re-deriving them.">
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
<HSpecimen>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: DHC.sky, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: 12, fontWeight: 600 }}>Identity</div>
<p style={{ margin: 0, fontSize: 13, color: 'var(--fg-1)', lineHeight: 1.6, fontFamily: 'var(--font-display)' }}>
Green appears in exactly one place: the page title in the chrome. Sky is the workhorse borders,
section titles, neutral buttons. Amber, red, purple, teal are reserved for specific data domains
(tickets, critical, Archer, Ivanti) and never used decoratively.
</p>
</HSpecimen>
<HSpecimen>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: DHC.sky, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: 12, fontWeight: 600 }}>Layout</div>
<p style={{ margin: 0, fontSize: 13, color: 'var(--fg-1)', lineHeight: 1.6, fontFamily: 'var(--font-display)' }}>
Top: 4-up stat strip. Body: 12-column grid, left 9 / right 3. Left holds the lookup filter CVE
feed flow. Right is a vertical stack of color-rail panels, each with a left-border identity color
and a centered big-number metric.
</p>
</HSpecimen>
</div>
</HSection>
{/* TOKENS */}
<HSection id="tokens" eyebrow="02 — Tokens" title="Color, type, and the right-rail palette" blurb="The four data domains on the home view each have an owned color used as: card left-rail border, card title color + glow, big-number value color, and badge tint.">
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 32 }}>
<div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-disabled)', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: 8, fontWeight: 600 }}>Right-rail identity</div>
<HSwatch name="sky" value={DHC.sky} role="Calendar · neutral surfaces · default" />
<HSwatch name="amber" value={DHC.amber} role="Open Tickets · 'needs attention'" />
<HSwatch name="purple" value={DHC.purple} role="Archer Risk Tickets" />
<HSwatch name="teal" value={DHC.teal} role="Ivanti Workflows" />
</div>
<div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-disabled)', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: 8, fontWeight: 600 }}>Severity / status</div>
<HSwatch name="green" value={DHC.green} role="Page identity glow · Low · success" />
<HSwatch name="red" value={DHC.red} role="Critical · destructive" />
<HSwatch name="amber" value={DHC.amber} role="High · in-progress" />
<HSwatch name="sky" value={DHC.sky} role="Medium · neutral status" />
</div>
</div>
<div style={{ marginTop: 32 }}>
<HSpec label="Card chrome">background <HCode>linear-gradient(135deg, rgba(30,41,59,.95) 0%, rgba(15,23,42,.98) 100%)</HCode></HSpec>
<HSpec label="Card border">resting <HCode>1.5px solid rgba(14,165,233,0.12)</HCode> · hover <HCode>0.35</HCode></HSpec>
<HSpec label="Card radius"><HCode>8px</HCode></HSpec>
<HSpec label="Title type"><HCode>var(--font-mono)</HCode> · 14 / 600 · uppercase · 0.1em tracking · 12px text-shadow glow in title color</HSpec>
<HSpec label="Big stat type"><HCode>var(--font-mono)</HCode> · 32 / 700 · 16px text-shadow glow at 0.4 alpha</HSpec>
<HSpec label="Stat label type"><HCode>var(--font-mono)</HCode> · 10 / 600 · uppercase · 0.12em tracking · fg-2</HSpec>
</div>
</HSection>
{/* COMPONENTS */}
<HSection id="components" eyebrow="03 — Components" title="The pieces" blurb="Every primitive used by the page assembly. All are exported on window.HOME so other pages in the dashboard can pull from the same vocabulary.">
{/* StatCard */}
<h3 style={subhead}>StatCard</h3>
<p style={subblurb}>Top-of-page metric tile. Color tone drives the 2px border, top-edge glow line, value color, and the inset highlight. Use <HCode>tone="neutral"</HCode> to suppress the colored treatment.</p>
<HSpecimen>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 12 }}>
<DStatCard label="Total CVEs" value="247" tone="sky" />
<DStatCard label="Vendor Entries" value="412" tone="neutral" />
<DStatCard label="Open Tickets" value="18" tone="amber" />
<DStatCard label="Critical" value="6" tone="red" />
</div>
</HSpecimen>
{/* Buttons */}
<h3 style={subhead}>HomeButton</h3>
<p style={subblurb}>Five variants. <strong style={{ color: DHC.green }}>Primary</strong> is reserved for the lone green CTA on each card. <strong style={{ color: DHC.sky }}>Neutral</strong> is the default for table-row + view actions. <strong style={{ color: DHC.amber }}>Warning</strong> = edit, <strong style={{ color: DHC.red }}>Danger</strong> = delete.</p>
<HSpecimen>
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
<DBtn variant="primary" icon="search">Scan</DBtn>
<DBtn variant="neutral" icon="eye">View</DBtn>
<DBtn variant="subtle" icon="download">Export</DBtn>
<DBtn variant="warning" icon="edit">Edit</DBtn>
<DBtn variant="danger" icon="trash">Delete</DBtn>
</div>
</HSpecimen>
{/* Badges */}
<h3 style={subhead}>SeverityBadge · StatusBadge</h3>
<p style={subblurb}>Severity is heavy: 2px solid border + glow + dot. Status is light: 1px border, smaller, used inside dense list cards.</p>
<HSpecimen>
<div style={{ display: 'flex', gap: 10, flexWrap: 'wrap', marginBottom: 16 }}>
<DSev level="Critical" /><DSev level="High" /><DSev level="Medium" /><DSev level="Low" />
</div>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
<DStatus tone="amber">In Progress</DStatus>
<DStatus tone="red">Open</DStatus>
<DStatus tone="green">Closed</DStatus>
<DStatus tone="purple">Pending Review</DStatus>
<DStatus tone="teal">Approved</DStatus>
</div>
</HSpecimen>
{/* Inputs */}
<h3 style={subhead}>HomeInput · HomeSelect · FieldLabel</h3>
<HSpecimen>
<div style={{ display: 'grid', gap: 16 }}>
<div>
<DLabel icon="search">Search CVEs</DLabel>
<DInput placeholder="CVE ID or description…" />
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
<div>
<DLabel icon="filter">Vendor</DLabel>
<DSelect value="All Vendors" onChange={() => {}} options={['All Vendors', 'Red Hat', 'Cisco', 'Ubuntu']} />
</div>
<div>
<DLabel icon="alert">Severity</DLabel>
<DSelect value="All Severities" onChange={() => {}} options={['All Severities', 'Critical', 'High', 'Medium', 'Low']} />
</div>
</div>
</div>
</HSpecimen>
{/* ResultBanner */}
<h3 style={subhead}>ResultBanner</h3>
<p style={subblurb}>Sub-card surfaced inside the Quick CVE Lookup card after a scan. Three tones map to the three terminal states.</p>
<HSpecimen>
<div style={{ display: 'grid', gap: 12 }}>
<DBanner tone="success" title="✓ CVE Addressed (3 vendors)">
<div style={{ fontFamily: 'var(--font-display)', fontSize: 12, color: 'var(--fg-2)' }}>
Red Hat (Open · 4 docs) · Ubuntu (In Progress · 2 docs) · SUSE (Resolved · 3 docs)
</div>
</DBanner>
<DBanner tone="warning" title="Not Found">
<div style={{ fontFamily: 'var(--font-display)', fontSize: 12, color: 'var(--fg-2)' }}>This CVE has not been addressed yet. No entry exists in the database.</div>
</DBanner>
<DBanner tone="error" title="Error">
<div style={{ fontFamily: 'var(--font-display)', fontSize: 12, color: 'var(--fg-2)' }}>NVD lookup failed: rate-limited (429). Retry in 30s.</div>
</DBanner>
</div>
</HSpecimen>
{/* BigStat */}
<h3 style={subhead}>BigStat</h3>
<p style={subblurb}>The centered "active count + label" shown at the top of every right-rail panel. Color follows panel identity.</p>
<HSpecimen>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 16 }}>
<DBigStat value="3" label="Active" color={DHC.amber} />
<DBigStat value="2" label="Active" color={DHC.purple} />
<DBigStat value="78" label="Total Workflows" color={DHC.teal} />
<DBigStat value="—" label="Never Synced" color={DHC.sky} />
</div>
</HSpecimen>
{/* MiniTicket */}
<h3 style={subhead}>MiniTicket</h3>
<p style={subblurb}>Compact card used inside right-rail scroll lists. Tone tints the border + status pill to match its parent panel's identity color.</p>
<HSpecimen>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3,1fr)', gap: 12 }}>
<DMini keyText="SEC-4821" cveId="CVE-2025-1014" vendor="Red Hat" status="In Progress" tone="amber" onEdit={() => {}} />
<DMini keyText="EXC-08291" cveId="CVE-2025-1014" vendor="SUSE" status="Pending Review" tone="purple" onEdit={() => {}} />
<DMini keyText="WF-1042" cveId="—" vendor="Compliance scan" status="In Review" tone="teal" />
</div>
</HSpecimen>
{/* Calendar */}
<h3 style={subhead}>CalendarMini</h3>
<p style={subblurb}>Right-rail calendar surface. Day cells accept a marker color so SLA / due-date dots can be projected onto the month.</p>
<HSpecimen>
<div style={{ maxWidth: 280 }}>
<DCal today={26} markedDays={{ 14: 'red', 18: 'amber', 22: 'sky', 24: 'amber' }} />
</div>
</HSpecimen>
{/* ArchiveSummary */}
<h3 style={subhead}>ArchiveSummary</h3>
<p style={subblurb}>State-pill bar that lives at the top of the Ivanti card. Each pill is a click target that filters the workflows below.</p>
<HSpecimen>
<div style={{ maxWidth: 320 }}>
<DArchive items={[
{ label: 'In Review', count: 12, tone: 'amber' },
{ label: 'In Progress', count: 8, tone: 'sky' },
{ label: 'Approved', count: 17, tone: 'green' },
{ label: 'Closed', count: 41, tone: 'neutral' },
]} activeFilter="In Review" />
</div>
</HSpecimen>
{/* CVERow + VendorEntry */}
<h3 style={subhead}>CVERow · VendorEntry</h3>
<p style={subblurb}>The collapsible CVE feed cards. Collapsed = chevron + ID + truncated description + meta row. Expanded = vendor sub-cards, optionally with a doc inset and a JIRA inset under each vendor.</p>
<HSpecimen padding={16}>
<DCVERow
cveId="CVE-2025-1014" severity="Critical"
description="Heap-based buffer overflow in libnetfilter_queue permits remote code execution via crafted ICMP traffic."
vendorCount={3} docCount={9} statuses={['Open', 'In Progress']}
expanded={true} onToggle={() => {}}
>
<DVendor vendor="Red Hat" severity="Critical" status="Open" docCount={4} onView={() => {}} />
<DVendor vendor="Ubuntu" severity="Critical" status="In Progress" docCount={2} onEdit={() => {}} />
</DCVERow>
</HSpecimen>
{/* EmptyState */}
<h3 style={subhead}>EmptyState</h3>
<HSpecimen>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2,1fr)', gap: 16 }}>
<DEmpty>No open tickets</DEmpty>
<DEmpty icon="alert" tone="amber">Click Sync to load workflow data</DEmpty>
</div>
</HSpecimen>
</HSection>
{/* ASSEMBLIES */}
<HSection id="assemblies" eyebrow="04 — Assemblies" title="How the parts compose" blurb="Three patterns that other dashboards in the suite should reuse verbatim.">
<h3 style={subhead}>Right-rail panel</h3>
<p style={subblurb}>HomeCard with a colored left-rail + matching CardTitle + BigStat + ScrollList of MiniTickets. The identity color owns all four.</p>
<HSpecimen>
<div style={{ maxWidth: 320 }}>
<DHomeCard padding={20} leftRail={DHC.amber}>
<DCardTitle color={DHC.amber} icon="alert" action={<DBtn variant="warning" icon="plus" size="sm" />}>Open Tickets</DCardTitle>
<DBigStat value="3" label="Active" color={DHC.amber} />
<DScroll maxHeight={220}>
<DMini keyText="SEC-4821" cveId="CVE-2025-1014" vendor="Red Hat" status="In Progress" tone="amber" onEdit={() => {}} onDelete={() => {}} summary="Patch netfilter ingress" />
<DMini keyText="SEC-4794" cveId="CVE-2025-0944" vendor="Cisco" status="Open" tone="amber" onEdit={() => {}} summary="Roll admin-console hotfix" />
</DScroll>
</DHomeCard>
</div>
</HSpecimen>
<h3 style={subhead}>Quick lookup → result banner</h3>
<HSpecimen>
<DHomeCard>
<DCardTitle color={DHC.sky} icon="search">Quick CVE Lookup</DCardTitle>
<div style={{ display: 'flex', gap: 12 }}>
<DInput placeholder="Enter CVE ID (e.g., CVE-2025-1014)" />
<DBtn variant="primary" icon="search">Scan</DBtn>
</div>
<div style={{ marginTop: 16 }}>
<DBanner tone="success" title="✓ CVE Addressed (3 vendors)">
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-2)' }}>Red Hat · Ubuntu · SUSE</div>
</DBanner>
</div>
</DHomeCard>
</HSpecimen>
<h3 style={subhead}>4-up stat strip</h3>
<HSpecimen>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 16 }}>
<DStatCard label="Total CVEs" value="247" tone="sky" />
<DStatCard label="Vendor Entries" value="412" tone="neutral" />
<DStatCard label="Open Tickets" value="18" tone="amber" />
<DStatCard label="Critical" value="6" tone="red" />
</div>
</HSpecimen>
</HSection>
{/* REFERENCE */}
<HSection id="reference" eyebrow="05 — Reference" title="Full Home page" blurb="Every primitive on this kit, composed exactly as App.js renders the home view. The frame below is a faithful reproduction — you can scroll inside it.">
<div className="sample-frame" style={{
border: '1px solid rgba(14,165,233,0.20)', borderRadius: 12,
overflow: 'hidden', maxHeight: 900, overflowY: 'auto',
background: 'var(--bg-page)',
}}>
<DHomePage />
</div>
</HSection>
</main>
</div>
);
}
const subhead = {
margin: '32px 0 6px 0',
fontFamily: 'var(--font-mono)', fontSize: 13, fontWeight: 700,
color: 'var(--fg-1)', textTransform: 'uppercase', letterSpacing: '0.08em',
};
const subblurb = {
margin: '0 0 12px 0',
fontFamily: 'var(--font-display)', fontSize: 13, lineHeight: 1.55,
color: 'var(--fg-muted)', maxWidth: 720,
};
window.HOME_DOCS = { HKitDocs };

View File

@@ -0,0 +1,37 @@
# Home UI Kit
Visual vocabulary for the CVE Dashboard home view (`currentPage === 'home'` in `frontend/src/App.js`).
## Files
- `index.html` — entry point.
- `HomePrimitives.jsx``StatCard`, `HomeCard`, `CardTitle`, `HomeButton`, `SeverityBadge`, `StatusBadge`, `HomeInput`, `HomeSelect`, `FieldLabel`, `ResultBanner`, `BigStat`, `MiniTicket`, `CVERow`, `VendorEntry`, `CalendarMini`, `ArchiveSummary`, `ScrollList`, `EmptyState`, `HomeIcon`.
- `HomePage.jsx` — full-page assembly (`HomePage`).
- `KitDocs.jsx` — browseable docs (Overview · Tokens · Components · Assemblies · Reference).
## Right-rail identity colors
Each right-side panel owns one color, applied consistently to four surfaces:
| Panel | Color | Hex | Used for |
|-------------------|----------|-----------|----------------------------------------------|
| Calendar | sky | `#0EA5E9` | left-rail, title glow, today cell, day dots |
| Open Tickets | amber | `#F59E0B` | left-rail, title glow, big stat, mini badges |
| Archer Risk | purple | `#8B5CF6` | left-rail, title glow, big stat, mini badges |
| Ivanti Workflows | teal | `#0D9488` | left-rail, title glow, big stat, mini badges |
## Layout
- **Top:** 4-up stat strip (sky · neutral · amber · red).
- **Body:** 12-col grid. Left 9 = Quick Lookup → Search/Filter → Results summary → CVE feed. Right 3 = vertical stack of right-rail panels.
## Card chrome (matches Reporting + KB)
```
background: linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(15,23,42,0.98) 100%)
border: 1.5px solid rgba(14,165,233,0.12) /* 0.35 on hover */
left-rail: 3px solid <identity-color> /* right-rail panels only */
radius: 8px
```
## Page-level rules
1. Green appears in **one** place: the page title in the chrome (and as the lone primary CTA when present, e.g. "Scan").
2. The four StatCard tones (sky/neutral/amber/red) map to (volume / inventory / attention / urgent). Don't reassign.
3. Severity uses the heavy 2px-border SeverityBadge; ticket statuses use the 1px-border StatusBadge.
4. Right-rail panels always lead with a BigStat. The number IS the headline.

View File

@@ -0,0 +1,39 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>STEAM Security · Home UI Kit</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="../../colors_and_type.css" />
<style>
body { margin: 0; min-height: 100vh; }
.page-bg { min-height: 100vh; background: var(--bg-page); }
:target { scroll-margin-top: 120px; }
.sample-frame::-webkit-scrollbar { width: 6px; height: 6px; }
.sample-frame::-webkit-scrollbar-thumb { background: rgba(14,165,233,0.2); border-radius: 4px; }
</style>
</head>
<body>
<div id="root" class="page-bg"></div>
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
<script type="text/babel" src="HomePrimitives.jsx"></script>
<script type="text/babel" src="HomePage.jsx"></script>
<script type="text/babel" src="KitDocs.jsx"></script>
<script type="text/babel">
const { HKitDocs } = window.HOME_DOCS;
function App() {
return (
<main data-screen-label="Home Kit">
<HKitDocs />
</main>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>