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:
371
docs/design-system-redesign/ui_kits/home/HomePage.jsx
Normal file
371
docs/design-system-redesign/ui_kits/home/HomePage.jsx
Normal 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 };
|
||||
662
docs/design-system-redesign/ui_kits/home/HomePrimitives.jsx
Normal file
662
docs/design-system-redesign/ui_kits/home/HomePrimitives.jsx
Normal 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,
|
||||
};
|
||||
443
docs/design-system-redesign/ui_kits/home/KitDocs.jsx
Normal file
443
docs/design-system-redesign/ui_kits/home/KitDocs.jsx
Normal 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 };
|
||||
37
docs/design-system-redesign/ui_kits/home/README.md
Normal file
37
docs/design-system-redesign/ui_kits/home/README.md
Normal 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.
|
||||
39
docs/design-system-redesign/ui_kits/home/index.html
Normal file
39
docs/design-system-redesign/ui_kits/home/index.html
Normal 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>
|
||||
Reference in New Issue
Block a user