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:
235
docs/design-system-redesign/README.md
Normal file
235
docs/design-system-redesign/README.md
Normal file
@@ -0,0 +1,235 @@
|
||||
# STEAM Security Design System
|
||||
|
||||
A design system for the **STEAM Security Dashboard** — a self-hosted vulnerability management workbench used by the NTS-AEO-STEAM and NTS-AEO-ACCESS-ENG business units. This repo captures the visual language, content patterns, tokens, and UI kit needed to extend or rebuild the product without drifting from its established look.
|
||||
|
||||
## What the product is
|
||||
|
||||
The STEAM Security Dashboard centralises:
|
||||
|
||||
- **CVE tracking** — searchable, filterable, vendor-aware CVE list with NVD auto-fill, document attachment, and group-based ownership
|
||||
- **Ivanti / RiskSense host findings** — live remediation queue with FP / Archer / CARD workflows, inline editing, per-finding notes, and a personal "Ivanti Queue" staging list
|
||||
- **AEO compliance posture** — weekly xlsx upload with drift detection, diff preview, per-team metric health cards, device-level violation tracking, and timestamped notes
|
||||
- **Archer EXC tickets** — risk-acceptance ticket tracking linked to CVE / vendor pairs
|
||||
- **Knowledge base** — internal document library (PDF, Markdown, Office, etc.) for runbooks, advisories, and policies
|
||||
- **Admin panel** — user / group management, audit log, system info — all gated behind an Admin group
|
||||
|
||||
Four user groups (`Admin`, `Standard_User`, `Leadership`, `Read_Only`) define every permission boundary, and every state-changing action is audit-logged.
|
||||
|
||||
## The 6 pages
|
||||
|
||||
1. **Home / Dashboard** — CVE list, filters, calendar widget for due dates
|
||||
2. **Reporting** — Ivanti host findings, charts, queue, export
|
||||
3. **Compliance** — AEO posture, metric health cards, device drill-in
|
||||
4. **Knowledge Base** — document library
|
||||
5. **Exports** — bulk export tools (group-gated)
|
||||
6. **Admin Panel** — user management, audit log, system info (Admin only)
|
||||
|
||||
## Sources
|
||||
|
||||
- **Codebase:** `https://vulcan.apophisnetworking.net/jramos/cve-dashboard` (Gitea, master branch). Auth required; raw file fetch is gated. The repo's own `README.md` (fetched via the source viewer) is the most accurate functional spec we had access to and is the basis for this system.
|
||||
- **Existing design ref:** `DESIGN_SYSTEM.md` (290 lines, in-repo) — referenced in the audit but not directly accessible from the host.
|
||||
- **Component audit** provided in the project brief: 29 components, 5 primitives, 14 composites, 5 pages, 1 context provider.
|
||||
- **Stack:** React 19, lucide-react, recharts, react-markdown + rehype-sanitize, mermaid, xlsx. Backend Express 5 / SQLite3.
|
||||
|
||||
## Index — what's in this folder
|
||||
|
||||
| Path | What it is |
|
||||
|---|---|
|
||||
| `README.md` | This file — context, content + visual foundations, iconography |
|
||||
| `SKILL.md` | Agent Skill manifest for Claude Code compatibility |
|
||||
| `colors_and_type.css` | Source-of-truth tokens — color, type, spacing, radii, elevation |
|
||||
| `fonts/` | Font references (Outfit + JetBrains Mono via Google Fonts CDN) |
|
||||
| `assets/` | Logo mark, brand SVGs, severity icons |
|
||||
| `preview/` | Design System tab cards — registered as assets |
|
||||
| `ui_kits/cve-dashboard/` | High-fidelity recreation of the dashboard, focused on Knowledge Base |
|
||||
|
||||
---
|
||||
|
||||
## CONTENT FUNDAMENTALS
|
||||
|
||||
The product is a tactical operations console for security engineers. Copy is dense, terse, and assumes a reader who already knows what a CVE, EXC ticket, FP workflow, and BU filter are. There is no marketing voice, no onboarding nudges, and no exclamation marks.
|
||||
|
||||
### Voice & tone
|
||||
- **Operational, not editorial.** Buttons say "Sync", "Confirm Upload", "Reconcile Config", "Add to Queue". Never "Let's get started" or "You're all set".
|
||||
- **Imperative for actions, declarative for state.** "Save", "Delete", "Hide Selected" — never "Saving your changes…" with three dots and a heart.
|
||||
- **No emoji.** Status is communicated through colour-coded badges and short text labels.
|
||||
- **Title Case for navigation and headers**, Sentence case for body and inline labels. Tabs and buttons: `User Management`, `Audit Log`, `System Info`. Helper text: `Filter tickets by CVE ID, vendor, or status.`
|
||||
|
||||
### Person & address
|
||||
- **Second-person sparingly** — only when the system is talking *about* the user's data: "your login", "your filtered view", "your queue". Never "Welcome back, {name}".
|
||||
- **First-person plural never.** No "We've updated" or "Let us know".
|
||||
- **Errors are direct, no apology.** "SESSION_SECRET environment variable must be set." "Login rate limited — wait 15 minutes." Never "Oops! Something went wrong."
|
||||
|
||||
### Casing
|
||||
- **CVE IDs:** uppercase with hyphen — `CVE-2024-12345`. Validated against `/^CVE-\d{4}-\d{4,}$/`.
|
||||
- **EXC numbers:** uppercase — `EXC-12345`. Validated `/^EXC-\d+$/`.
|
||||
- **Severity labels:** Title Case — `Critical`, `High`, `Medium`, `Low`. Status labels: `Open`, `Addressed`, `In Progress`, `Resolved`.
|
||||
- **Workflow state badges:** SHOUT CASE for SLA states only — `OVERDUE`, `AT_RISK`, `WITHIN_SLA`. Everything else is Title Case.
|
||||
- **Group names:** snake_case in code (`Standard_User`), Title Case in UI (`Standard User`).
|
||||
|
||||
### Density and units
|
||||
- Numerical metrics are bare integers ("12 findings", "47 devices"). Percentages always carry the % sign with no space.
|
||||
- Dates are explicit, no relative time except "Last sync: 4h ago" patterns.
|
||||
- Column headers are short — `Host`, `IP Address`, `DNS`, `BU`, `SLA` — never `Host Name (Editable)`.
|
||||
|
||||
### Specific copy conventions seen in product
|
||||
- "— empty —" as a filter option for empty cells
|
||||
- "Hidden (N)" pattern for counted UI states
|
||||
- "+N" badge for overflow (e.g., 2 CVEs shown, "+5" badge)
|
||||
- "↻" revert glyph next to overridden cells, with a small amber dot ● for the overridden state
|
||||
- Tooltips appear after a 300ms delay and are session-cached
|
||||
- "View in Reporting →" inline link pattern with a literal arrow
|
||||
|
||||
### What NOT to write
|
||||
- No motivational copy ("Great work!", "You're crushing it")
|
||||
- No question-mark headlines ("Need help?")
|
||||
- No marketing CTAs ("Upgrade now", "Try premium")
|
||||
- No mascot or persona — the system is the system
|
||||
|
||||
---
|
||||
|
||||
## VISUAL FOUNDATIONS
|
||||
|
||||
The dashboard reads as a **dark tactical intelligence console** — slate / graphite backgrounds, sky-blue as the primary accent and ambient glow, severity colours used like signal flags, animated pulse-glow status dots, and information density prioritised over breathing room. The aesthetic is closer to a SOC / NOC mission display than to a flat enterprise SaaS.
|
||||
|
||||
### Colour vibe
|
||||
- **Dark slate base.** `#0F172A` (deep slate) for the page, `#1E293B` for surfaces, `#334155` for elevated surfaces and borders. Almost black, never pure black. The cool tone is consistent — no warm shadows.
|
||||
- **Sky blue is the brand accent** — `#38BDF8` is the primary action / link / focused state colour. It appears in buttons, active nav items, link text, and the "create" badge in the audit log.
|
||||
- **Severity is a fixed semantic system** — the colours below MUST mean what they mean and nothing else.
|
||||
- Critical → Red `#EF4444`
|
||||
- High → Amber `#F59E0B`
|
||||
- Medium → Sky `#38BDF8`
|
||||
- Low → Emerald `#10B981`
|
||||
- **Neutral text scale** — `#F1F5F9` (primary fg), `#CBD5E1` (secondary), `#94A3B8` (muted), `#64748B` (placeholder / disabled). Never pure white.
|
||||
- **Group badges** — Admin red, Standard_User accent blue, Leadership amber, Read_Only muted grey. The same severity language reappears here for status urgency.
|
||||
|
||||
### Typography
|
||||
- **Outfit** for all UI (headers, body, buttons, navigation). Geometric sans, friendly but precise; weights 400 / 500 / 600 / 700.
|
||||
- **JetBrains Mono** for *data* — CVE IDs, IP addresses, hostnames, EXC numbers, finding IDs, code blocks. Anything you'd grep for.
|
||||
- **Scale** is compact. Page titles 24–28px / 600 weight; section headers 16–18px / 600; body 14px / 400; data table cells 13px / 400 mono. Line-height stays tight (1.4) to preserve density.
|
||||
|
||||
### Spacing
|
||||
- **4 / 8 / 12 / 16 / 24 / 32 / 48** — a roughly 4px grid. Cards have 16–20px internal padding; rows in dense tables have 8–10px vertical padding; modals have 24px internal padding.
|
||||
- **Section gaps** are 24–32px. Between siblings, 12–16px is the dominant rhythm.
|
||||
|
||||
### Backgrounds
|
||||
- **No imagery.** No hero photographs, illustrations, or marketing visuals. The page is solid `#0F172A`.
|
||||
- **Subtle sky-blue grid is allowed.** A 20×20px grid at `rgba(14,165,233,0.025)` (`.grid-bg` utility) sits behind hero / empty regions. It is barely visible and never dominates.
|
||||
- **Surfaces use diagonal gradients**, not flat fills — `linear-gradient(135deg, rgba(30,41,59,0.95), rgba(51,65,85,0.9))` is the canonical card surface.
|
||||
|
||||
### Cards and surfaces (`intel-card`)
|
||||
- **Background:** diagonal gradient `135deg, rgba(30,41,59,0.95) 0%, rgba(51,65,85,0.9) 50%, rgba(30,41,59,0.95) 100%`
|
||||
- **Border:** 1.5px solid `rgba(14,165,233,0.30)` — sky-blue at low alpha, not slate grey
|
||||
- **Radius:** 8px (default) / 12px (modals) / 4px (chips)
|
||||
- **Internal padding:** 16–20px
|
||||
- **Resting shadow:** `0 4px 12px rgba(0,0,0,0.4)` + `0 2px 6px rgba(0,0,0,0.3)` + inset `0 1px 0 rgba(14,165,233,0.10)` (sky highlight on top edge)
|
||||
- **Hover:** border opacity climbs to `0.50`, the card lifts `translateY(-2px)`, and gains a `0 0 30px rgba(14,165,233,0.10)` ambient glow. A `::after` shimmer sweeps left→right on entry.
|
||||
- **Stat cards** add a 2px `linear-gradient(90deg, transparent, #0EA5E9, transparent)` rail on the top edge.
|
||||
|
||||
### Borders
|
||||
- **Sky-blue at low alpha** is the dominant border treatment — `rgba(14,165,233,0.15)` for subtle dividers, `0.25` for default, `0.40` for strong / hover. Pure slate `#334155` borders appear only on tables and inputs at rest.
|
||||
- Focus state: 2px sky-blue ring `0 0 0 2px rgba(14,165,233,0.15)` plus the border swaps to solid `#0EA5E9`.
|
||||
- Severity-tinted left borders are NOT a pattern — colour is carried by badges, dots, and glow.
|
||||
|
||||
### Animation
|
||||
- **Pulse-glow on status dots is canonical.** Every severity / SLA badge has an 8px circle that pulses `box-shadow: 0 0 5px → 15px currentColor` on a 2s ease-in-out loop (`@keyframes pulse-glow`).
|
||||
- **Card hover lift** is 300ms cubic-bezier(0.4,0,0.2,1) with a `::after` shimmer sweep — `linear-gradient(90deg, transparent, rgba(14,165,233,0.08), transparent)` translating from `left:-100%` to `100%` over 500ms.
|
||||
- **Buttons** have a circular ripple `::before` that scales from 0×0 to 300×300 on hover (500ms).
|
||||
- Modal entry: 200ms fade + slight translate. Slide-out panels: 240ms ease-out from the right.
|
||||
- Tooltips have a deliberate **300ms hover delay** before appearing.
|
||||
- A `.scan-line` utility (3s loop) is available for hero / loading affordances — used sparingly.
|
||||
|
||||
### Hover states
|
||||
- **Cards** lift `-2px`, border opacity climbs from `0.30` → `0.50`, and a sky-blue ambient glow `0 0 30px rgba(14,165,233,0.10)` appears.
|
||||
- **Buttons** brighten their gradient fill from `0.15/0.10` to `0.25/0.20` alpha, gain a `0 0 20px` brand-color glow, and lift `-1px`.
|
||||
- **Text links** lighten from `#38BDF8` to `#7DD3FC` and the bottom border brightens to match.
|
||||
- **Table rows** get a `rgba(0,217,255,0.06)` wash plus `0 2px 8px rgba(0,217,255,0.10)` sub-shadow.
|
||||
- The audit notes a current anti-pattern: hover states implemented via `onMouseEnter` / `onMouseLeave` JS handlers. The design system standard is **CSS `:hover` pseudo-classes** — JS hover is a defect to migrate away from.
|
||||
|
||||
### Press / active states
|
||||
- Button: shifts to `#0EA5E9` (slightly darker than hover), no shrink, no shadow change. Press is a colour signal, not a physics signal.
|
||||
- Rows / interactive cards: `#475569` background on `:active`.
|
||||
|
||||
### Transparency & blur
|
||||
- Modal backdrops: `rgba(10, 14, 39, 0.97)` with `backdrop-filter: blur(12px)`. The blur is heavy and the backdrop is near-opaque — modals fully obscure the background.
|
||||
- Tooltips: gradient `linear-gradient(135deg, #334155, #475569)` with a sky-blue border and `0 4px 12px` + `0 0 16px rgba(14,165,233,0.15)` glow.
|
||||
- Inputs: translucent `rgba(30,41,59,0.6)` background with `inset 0 2px 4px rgba(0,0,0,0.2)` for a subtle recessed feel.
|
||||
|
||||
### Inner / outer shadows
|
||||
- **Both are used.** Cards combine outer drop + inner sky-blue highlight: `0 4px 12px rgba(0,0,0,0.4), inset 0 1px 0 rgba(14,165,233,0.10)`.
|
||||
- **Inputs are recessed** — `inset 0 2px 4px rgba(0,0,0,0.2)` plus a `0 1px 0 rgba(255,255,255,0.03)` top sheen.
|
||||
- **Document items** (within KB / vendor lists) use a stronger inset `inset 0 2px 4px rgba(0,0,0,0.3)` to read as nested / pressed-in.
|
||||
- Modals lift on `0 20px 60px rgba(0,0,0,0.6) + 0 10px 30px rgba(14,165,233,0.10)` — heavier than most enterprise products, but the brand glow is the signature.
|
||||
|
||||
### Layout rules
|
||||
- **Full-width fluid** above 1024px — the dashboard fills the viewport, with content max-width capping at ~1600px on very wide displays.
|
||||
- **Top app bar is fixed** — height 56px, contains brand mark, page nav, and `UserMenu`. Sits above all content with `z-index: 50`.
|
||||
- **Side nav drawer (NavDrawer)** slides from the left on icon click; it does *not* push content (overlay model).
|
||||
- **Slide-out panels** (Atlas, Compliance Detail) come from the right, ~480px wide on desktop, full-width on narrow viewports.
|
||||
- **Modals** are centered, max-width 640px (small) or 960px (wizard / upload), with the standard backdrop.
|
||||
|
||||
### Severity language
|
||||
This is the most important visual rule in the product. Severity badges use the **`status-badge` pattern**:
|
||||
- 2px solid border at `0.6` alpha
|
||||
- Diagonal gradient fill at `0.20 / 0.15` alpha
|
||||
- **Lighter text** for legibility — `#FCA5A5` (critical), `#FCD34D` (high), `#7DD3FC` (medium), `#6EE7B7` (low) — not the raw severity colour
|
||||
- Text-shadow `0 0 8px` brand-color at `0.4` alpha
|
||||
- 8px filled circle dot with a pulsing `box-shadow: 0 0 12px / 0 0 6px` glow on a 2s loop
|
||||
- `0 4px 8px rgba(0,0,0,0.4)` outer shadow
|
||||
- **Always JetBrains Mono, uppercase, 0.5px letter-spacing**
|
||||
|
||||
Secondary references can use simpler tinted pills (`rgba(brand,0.12)` background + brand text, no border, no glow). Single coloured dots `●` next to numeric scores are also valid. The colour-to-severity mapping is fixed across every component.
|
||||
|
||||
### Headings — the brand glow
|
||||
Page titles and section headers are **JetBrains Mono, uppercase, sky-blue `#38BDF8`**, with `text-shadow: 0 0 16px rgba(14,165,233,0.30), 0 0 32px rgba(14,165,233,0.15)`. This is the most identifiable single signal in the product — every page header reads as a glowing terminal title. Outfit is reserved for body, helper, and table cell text. The Knowledge Base markdown viewer continues this language: `h1` sky-blue, `h2` emerald, `h3` amber — a deliberate severity-coloured hierarchy.
|
||||
|
||||
---
|
||||
|
||||
## ICONOGRAPHY
|
||||
|
||||
The product uses **lucide-react** as its sole icon system. Lucide is a 1.5px-stroke, geometric, open-source icon set — clean, restrained, and perfectly aligned with the dark tactical aesthetic.
|
||||
|
||||
### Rules
|
||||
- **All icons are line / stroke style** — never filled glyphs (with one exception: the calendar's red due-date dot is a filled circle, but it's a status indicator, not an icon).
|
||||
- **Stroke width:** 1.5–2px (lucide default). 1.5px on small icons (≤16px), 2px on larger icons.
|
||||
- **Sizes:** 14px (inline with text), 16px (default UI), 20px (nav items, prominent buttons), 24px (page-header icons).
|
||||
- **Colour:** inherits `currentColor` — text-foreground for default, `#38BDF8` for active / accent, severity colours when used as a status indicator.
|
||||
- **No emoji anywhere.** Status, severity, and category use icons + colour; never `🔴` or `⚠️`.
|
||||
- **No unicode-as-icon shortcuts** beyond `●` (status dot), `↻` (revert / cycle), `↱` (redirect), `⊙` (filter handle), `→` (inline link), `+N` (count badge). These are part of the typography, not stand-ins for missing icons.
|
||||
|
||||
### Brand mark
|
||||
The product has no published logo file in the repo (the audit references `AtlasIcon` as a custom SVG brand icon — Atlas appears to be the action-plan integration, not the dashboard's own brand). For this design system the brand mark is a **typographic stack**: `STEAM` in Outfit 700 with a sky-blue underline accent and a small shield glyph (lucide `Shield`) to the left. See `assets/logo.svg` and `assets/atlas-shield.svg`.
|
||||
|
||||
### Substitutions flagged
|
||||
- **Atlas action-plan brand icon** is recreated as a generic shield (lucide `Shield`) tinted sky-blue. **If you have the real `AtlasIcon` SVG, please attach it** — the in-product version is custom and not available from the repo URL.
|
||||
- Fonts (Outfit, JetBrains Mono) load from Google Fonts CDN. **If you need offline font files, attach the woff2s** and we'll bundle them into `fonts/`.
|
||||
|
||||
### Icons used per page (from README)
|
||||
- **Home:** Calendar (CalendarWidget), Search, Filter, Plus, Upload, Edit, Trash, X (close)
|
||||
- **Reporting:** RefreshCw (Sync), Eye / EyeOff (row visibility), Check, Filter (⊙ in column header), Columns, Download (Export), MoreHorizontal
|
||||
- **Compliance:** Upload, AlertTriangle (drift breaking), AlertCircle (drift silent-miss), Info, ChevronRight, FileText
|
||||
- **Knowledge Base:** FileText, FilePlus, Folder, Download, Eye
|
||||
- **Admin:** Users, ScrollText (audit log), Activity (system info), Shield (admin badge)
|
||||
- **Universal:** ChevronDown, ChevronUp, Check, X, Loader, ExternalLink
|
||||
|
||||
When picking an icon, prefer the lucide-react name from this list before introducing a new one.
|
||||
|
||||
---
|
||||
|
||||
## UI Kits
|
||||
|
||||
| Kit | Path | What it covers |
|
||||
|---|---|---|
|
||||
| `cve-dashboard` | `ui_kits/cve-dashboard/` | App shell (top bar, nav drawer, user menu), Knowledge Base page + viewer, primitives (Button, Badge, Pill, Input, Select, Modal shell, SlideOutPanel, DataTable, GroupBadge, SeverityBadge, EmptyState, LoadingState) |
|
||||
|
||||
The Knowledge Base page is the focused recreation. Other surfaces (Reporting, Compliance, Admin) are intentionally not built out — the primitives + shell are sufficient to compose them.
|
||||
|
||||
---
|
||||
|
||||
## How to use this system
|
||||
|
||||
1. **Tokens first.** Import `colors_and_type.css` into the root of any HTML file. All colour, type, radius, shadow, and spacing decisions should pull from these CSS custom properties.
|
||||
2. **Pick a primitive before inventing.** Severity badges, group badges, status pills, table row, modal shell, slide-out panel — they all live in `ui_kits/cve-dashboard/`.
|
||||
3. **Match the density.** When in doubt, tighter is more on-brand than airier.
|
||||
4. **Lucide for icons.** Use the lucide-react CDN or copy individual SVGs from the lucide site. Do not draw your own.
|
||||
5. **No emoji, no gradients, no illustration, no marketing copy.** The product is a console.
|
||||
23
docs/design-system-redesign/SKILL.md
Normal file
23
docs/design-system-redesign/SKILL.md
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: steam-security-design
|
||||
description: Use this skill to generate well-branded interfaces and assets for the STEAM Security Dashboard (NTS-AEO vulnerability management workbench), either for production or throwaway prototypes/mocks. Contains essential design guidelines, colors, type, fonts, assets, and UI kit components for prototyping.
|
||||
user-invocable: true
|
||||
---
|
||||
|
||||
Read the README.md file within this skill, and explore the other available files.
|
||||
|
||||
If creating visual artifacts (slides, mocks, throwaway prototypes, etc), copy assets out and create static HTML files for the user to view. Always pull tokens from `colors_and_type.css` and reuse the primitives in `ui_kits/cve-dashboard/Primitives.jsx` (Button, SeverityBadge, SlaPill, GroupBadge, Field/Input/Select, Card, EmptyState, Icon) before inventing.
|
||||
|
||||
If working on production code, copy assets and read the rules here to become an expert in designing with this brand.
|
||||
|
||||
If the user invokes this skill without any other guidance, ask them what they want to build or design, ask some questions, and act as an expert designer who outputs HTML artifacts _or_ production code, depending on the need.
|
||||
|
||||
## Quick reference
|
||||
|
||||
- **Visual vibe:** dark tactical intelligence console. Slate base, sky-blue accent, severity colours used like signal flags. Information density over breathing room.
|
||||
- **Type:** Outfit (UI), JetBrains Mono (data, IDs, code).
|
||||
- **No emoji, no gradients, no illustration, no marketing copy.** This is an operations tool, not a brand site.
|
||||
- **Severity is fixed:** Critical→Red · High→Amber · Medium→Sky · Low→Emerald. Do not remap.
|
||||
- **Icons:** lucide-react line style, 1.5–2px stroke, currentColor.
|
||||
- **Six pages exist:** Home, Reporting, Compliance, Knowledge Base, Exports, Admin Panel.
|
||||
- **Four user groups:** Admin, Standard_User, Leadership, Read_Only.
|
||||
4
docs/design-system-redesign/assets/atlas-shield.svg
Normal file
4
docs/design-system-redesign/assets/atlas-shield.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
|
||||
<path d="M16 4 L26 7 L26 16 C26 22 21.5 28 16 30 C10.5 28 6 22 6 16 L6 7 Z" stroke="#38BDF8" stroke-width="2" stroke-linejoin="round" fill="rgba(56,189,248,0.12)"></path>
|
||||
<path d="M11 16 L15 20 L22 12" stroke="#38BDF8" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" fill="none"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 411 B |
9
docs/design-system-redesign/assets/logo.svg
Normal file
9
docs/design-system-redesign/assets/logo.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="160" height="40" viewBox="0 0 160 40" fill="none">
|
||||
|
||||
<path d="M14 6 L24 9 L24 18 C24 24 19.5 30 14 32 C8.5 30 4 24 4 18 L4 9 Z" stroke="#38BDF8" stroke-width="2" stroke-linejoin="round" fill="rgba(56,189,248,0.1)"></path>
|
||||
<path d="M9 18 L13 22 L20 14" stroke="#38BDF8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"></path>
|
||||
|
||||
<text x="34" y="22" font-family="Outfit, sans-serif" font-weight="700" font-size="18" fill="#F1F5F9" letter-spacing="0.02em">STEAM</text>
|
||||
<text x="34" y="34" font-family="Outfit, sans-serif" font-weight="500" font-size="10" fill="#94A3B8" letter-spacing="0.18em">SECURITY</text>
|
||||
<line x1="34" y1="26" x2="92" y2="26" stroke="#38BDF8" stroke-width="1.5"></line>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 781 B |
1
docs/design-system-redesign/assets/severity-critical.svg
Normal file
1
docs/design-system-redesign/assets/severity-critical.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" fill="#EF4444"></circle></svg>
|
||||
|
After Width: | Height: | Size: 153 B |
1
docs/design-system-redesign/assets/severity-high.svg
Normal file
1
docs/design-system-redesign/assets/severity-high.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" fill="#F59E0B"></circle></svg>
|
||||
|
After Width: | Height: | Size: 153 B |
1
docs/design-system-redesign/assets/severity-low.svg
Normal file
1
docs/design-system-redesign/assets/severity-low.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" fill="#10B981"></circle></svg>
|
||||
|
After Width: | Height: | Size: 153 B |
1
docs/design-system-redesign/assets/severity-medium.svg
Normal file
1
docs/design-system-redesign/assets/severity-medium.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" fill="#38BDF8"></circle></svg>
|
||||
|
After Width: | Height: | Size: 153 B |
323
docs/design-system-redesign/colors_and_type.css
Normal file
323
docs/design-system-redesign/colors_and_type.css
Normal file
@@ -0,0 +1,323 @@
|
||||
/* ===================================================================
|
||||
STEAM Security Dashboard — Design Tokens
|
||||
Source of truth for color, type, spacing, radii, elevation, motion.
|
||||
Mirrors the production frontend/src/App.css "tactical intelligence"
|
||||
palette. Import in <head> of any HTML in this design system.
|
||||
=================================================================== */
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap');
|
||||
|
||||
:root {
|
||||
/* ── Color · Surfaces (modern slate foundation) ─────────────── */
|
||||
--intel-darkest: #0F172A; /* page background */
|
||||
--intel-dark: #1E293B; /* card / panel surface */
|
||||
--intel-medium: #334155; /* elevated surface, hover row */
|
||||
--intel-light: #475569; /* muted border, disabled chip */
|
||||
--intel-grid: rgba(14, 165, 233, 0.08); /* grid backdrop */
|
||||
|
||||
/* Aliases — friendlier names */
|
||||
--bg-page: var(--intel-darkest);
|
||||
--bg-surface: var(--intel-dark);
|
||||
--bg-elevated: var(--intel-medium);
|
||||
--bg-hover: var(--intel-light);
|
||||
--bg-input: rgba(30, 41, 59, 0.6);
|
||||
--bg-overlay: rgba(10, 14, 39, 0.97);
|
||||
|
||||
/* ── Color · Foreground ─────────────────────────────────────── */
|
||||
--text-primary: #F8FAFC;
|
||||
--text-secondary: #E2E8F0;
|
||||
--text-tertiary: #CBD5E1;
|
||||
--text-muted: #94A3B8;
|
||||
--text-disabled: #64748B;
|
||||
--text-faint: #475569;
|
||||
--text-on-accent: #0F172A;
|
||||
|
||||
/* Aliases */
|
||||
--fg-1: var(--text-primary);
|
||||
--fg-2: var(--text-secondary);
|
||||
--fg-muted: var(--text-muted);
|
||||
--fg-disabled: var(--text-disabled);
|
||||
|
||||
/* ── Color · Borders ────────────────────────────────────────── */
|
||||
--border-subtle: rgba(14, 165, 233, 0.15);
|
||||
--border-default: rgba(14, 165, 233, 0.25);
|
||||
--border-strong: rgba(14, 165, 233, 0.40);
|
||||
--border-focus: #0EA5E9;
|
||||
|
||||
--border-1: var(--border-subtle);
|
||||
--border-2: var(--border-default);
|
||||
|
||||
/* ── Color · Brand accent (sky blue — primary signal) ───────── */
|
||||
--intel-accent: #0EA5E9; /* raw sky-500 */
|
||||
--intel-accent-bright: #38BDF8; /* sky-400 — text on dark */
|
||||
--intel-accent-soft: #7DD3FC; /* sky-300 */
|
||||
--intel-accent-15: rgba(14, 165, 233, 0.15);
|
||||
--intel-accent-08: rgba(14, 165, 233, 0.08);
|
||||
|
||||
--accent: var(--intel-accent);
|
||||
--accent-bright: var(--intel-accent-bright);
|
||||
--accent-soft: var(--intel-accent-soft);
|
||||
--accent-wash: var(--intel-accent-08);
|
||||
--accent-hover: #0284C7; /* sky-600 — pressed/hover for filled buttons */
|
||||
--fg-on-accent: var(--text-on-accent);
|
||||
--fg-3: var(--text-tertiary);
|
||||
--fg-muted: var(--text-muted);
|
||||
--fg-disabled: var(--text-disabled);
|
||||
--border-3: var(--border-strong);
|
||||
|
||||
/* ── Color · Semantic / severity (FIXED — never remap) ──────── */
|
||||
--intel-danger: #EF4444; /* Critical · Overdue · Delete */
|
||||
--intel-warning: #F59E0B; /* High · At-Risk · Caution */
|
||||
--intel-success: #10B981; /* Low · Within-SLA · OK */
|
||||
--intel-info: #0EA5E9; /* Medium · Info · Standard */
|
||||
|
||||
--sev-critical: var(--intel-danger);
|
||||
--sev-high: var(--intel-warning);
|
||||
--sev-medium: var(--intel-info);
|
||||
--sev-low: var(--intel-success);
|
||||
|
||||
/* Severity text-on-dark (lighter; better contrast) */
|
||||
--sev-critical-text: #FCA5A5;
|
||||
--sev-high-text: #FCD34D;
|
||||
--sev-medium-text: #7DD3FC;
|
||||
--sev-low-text: #6EE7B7;
|
||||
|
||||
/* Severity fills */
|
||||
--sev-critical-bg: rgba(239, 68, 68, 0.20);
|
||||
--sev-high-bg: rgba(245, 158, 11, 0.20);
|
||||
--sev-medium-bg: rgba(14, 165, 233, 0.20);
|
||||
--sev-low-bg: rgba(16, 185, 129, 0.20);
|
||||
|
||||
/* ── Color · Group badges ───────────────────────────────────── */
|
||||
--group-admin: #EF4444;
|
||||
--group-standard: #38BDF8;
|
||||
--group-leadership: #F59E0B;
|
||||
--group-readonly: #94A3B8;
|
||||
|
||||
/* ── Type · Families ────────────────────────────────────────── */
|
||||
--font-ui: 'Outfit', system-ui, -apple-system, sans-serif;
|
||||
--font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;
|
||||
|
||||
/* In production EVERYTHING uses Outfit by default; mono is
|
||||
reserved for badges, buttons, code, table data, and section
|
||||
headers (which are also UPPERCASE, letter-spaced). */
|
||||
|
||||
/* ── Type · Scale ───────────────────────────────────────────── */
|
||||
--fs-display: 28px;
|
||||
--fs-h1: 24px;
|
||||
--fs-h2: 18px;
|
||||
--fs-h3: 16px;
|
||||
--fs-body: 14px;
|
||||
--fs-sm: 13px;
|
||||
--fs-xs: 12px;
|
||||
--fs-tiny: 11px;
|
||||
|
||||
--lh-tight: 1.2;
|
||||
--lh-normal: 1.4;
|
||||
--lh-loose: 1.6;
|
||||
|
||||
--fw-regular: 400;
|
||||
--fw-medium: 500;
|
||||
--fw-semibold: 600;
|
||||
--fw-bold: 700;
|
||||
|
||||
--tracking-wide: 0.05em; /* mono buttons, badges */
|
||||
--tracking-wider: 0.10em; /* uppercase headings */
|
||||
|
||||
/* ── Spacing (4-px grid) ────────────────────────────────────── */
|
||||
--sp-1: 4px;
|
||||
--sp-2: 8px;
|
||||
--sp-3: 12px;
|
||||
--sp-4: 16px;
|
||||
--sp-5: 20px;
|
||||
--sp-6: 24px;
|
||||
--sp-8: 32px;
|
||||
--sp-10: 40px;
|
||||
--sp-12: 48px;
|
||||
|
||||
/* ── Radii ──────────────────────────────────────────────────── */
|
||||
--r-xs: 3px;
|
||||
--r-sm: 4px;
|
||||
--r-md: 6px;
|
||||
--r-lg: 8px;
|
||||
--r-xl: 12px;
|
||||
--r-pill: 999px;
|
||||
|
||||
/* ── Elevation (with sky-blue inner highlight) ──────────────── */
|
||||
--shadow-rest: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
--shadow-card: 0 4px 12px rgba(0, 0, 0, 0.4),
|
||||
0 2px 6px rgba(0, 0, 0, 0.3),
|
||||
inset 0 1px 0 rgba(14, 165, 233, 0.10);
|
||||
--shadow-card-hover: 0 8px 24px rgba(14, 165, 233, 0.15),
|
||||
0 4px 12px rgba(0, 0, 0, 0.4),
|
||||
inset 0 1px 0 rgba(14, 165, 233, 0.20),
|
||||
0 0 30px rgba(14, 165, 233, 0.10);
|
||||
--shadow-popover: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
--shadow-modal: 0 20px 60px rgba(0, 0, 0, 0.6),
|
||||
0 10px 30px rgba(14, 165, 233, 0.10);
|
||||
--shadow-focus: 0 0 0 2px rgba(14, 165, 233, 0.15);
|
||||
|
||||
/* Severity glow (used by status badge dots) */
|
||||
--glow-danger: 0 0 12px rgba(239, 68, 68, 0.6),
|
||||
0 0 6px rgba(239, 68, 68, 0.4);
|
||||
--glow-warning: 0 0 12px rgba(245, 158, 11, 0.6),
|
||||
0 0 6px rgba(245, 158, 11, 0.4);
|
||||
--glow-info: 0 0 12px rgba(14, 165, 233, 0.6),
|
||||
0 0 6px rgba(14, 165, 233, 0.4);
|
||||
--glow-success: 0 0 12px rgba(16, 185, 129, 0.6),
|
||||
0 0 6px rgba(16, 185, 129, 0.4);
|
||||
|
||||
/* Heading text-shadow glow */
|
||||
--glow-heading: 0 0 16px rgba(14, 165, 233, 0.30),
|
||||
0 0 32px rgba(14, 165, 233, 0.15);
|
||||
|
||||
/* ── Motion ─────────────────────────────────────────────────── */
|
||||
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--dur-fast: 150ms;
|
||||
--dur-med: 200ms;
|
||||
--dur-slow: 300ms;
|
||||
|
||||
/* ── Layout ─────────────────────────────────────────────────── */
|
||||
--topbar-h: 64px;
|
||||
--drawer-w: 240px;
|
||||
--panel-w: 480px;
|
||||
--content-max: 1600px;
|
||||
--z-topbar: 50;
|
||||
--z-drawer: 60;
|
||||
--z-modal: 100;
|
||||
--z-tooltip: 120;
|
||||
}
|
||||
|
||||
/* ── Base ─────────────────────────────────────────────────────── */
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
* {
|
||||
font-family: var(--font-ui);
|
||||
}
|
||||
|
||||
html, body {
|
||||
background-color: var(--bg-page);
|
||||
color: var(--text-secondary);
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--fs-body);
|
||||
line-height: var(--lh-normal);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
margin: 0;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Faint grid backdrop — apply to <body> or hero containers */
|
||||
.grid-bg {
|
||||
background-image:
|
||||
linear-gradient(var(--intel-grid) 1px, transparent 1px),
|
||||
linear-gradient(90deg, var(--intel-grid) 1px, transparent 1px);
|
||||
background-size: 20px 20px;
|
||||
}
|
||||
|
||||
/* ── Semantic type ───────────────────────────────────────────── */
|
||||
.t-display {
|
||||
font: var(--fw-bold) var(--fs-display)/var(--lh-tight) var(--font-mono);
|
||||
color: var(--intel-accent-bright);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: var(--tracking-wider);
|
||||
text-shadow: var(--glow-heading);
|
||||
}
|
||||
.t-h1 {
|
||||
font: var(--fw-bold) var(--fs-h1)/var(--lh-tight) var(--font-mono);
|
||||
color: var(--intel-accent-bright);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: var(--tracking-wider);
|
||||
}
|
||||
.t-h2 {
|
||||
font: var(--fw-semibold) var(--fs-h2)/var(--lh-tight) var(--font-mono);
|
||||
color: var(--text-primary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: var(--tracking-wide);
|
||||
}
|
||||
.t-h3 {
|
||||
font: var(--fw-semibold) var(--fs-h3)/var(--lh-normal) var(--font-ui);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.t-body {
|
||||
font: var(--fw-regular) var(--fs-body)/var(--lh-normal) var(--font-ui);
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
.t-sm {
|
||||
font: var(--fw-regular) var(--fs-sm)/var(--lh-normal) var(--font-ui);
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
.t-meta {
|
||||
font: var(--fw-regular) var(--fs-xs)/var(--lh-normal) var(--font-ui);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.t-label {
|
||||
font: var(--fw-medium) var(--fs-xs)/var(--lh-normal) var(--font-mono);
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: var(--tracking-wide);
|
||||
}
|
||||
.t-mono {
|
||||
font: var(--fw-regular) var(--fs-sm)/var(--lh-normal) var(--font-mono);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.t-mono-sm {
|
||||
font: var(--fw-regular) var(--fs-xs)/var(--lh-normal) var(--font-mono);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.t-code {
|
||||
font: var(--fw-medium) var(--fs-sm)/var(--lh-normal) var(--font-mono);
|
||||
color: var(--intel-success);
|
||||
background: var(--intel-darkest);
|
||||
border: 1px solid var(--border-default);
|
||||
padding: 1px 6px;
|
||||
border-radius: var(--r-sm);
|
||||
}
|
||||
|
||||
/* ── Animations (used by status badges, scan lines) ──────────── */
|
||||
@keyframes pulse-glow {
|
||||
0%, 100% { box-shadow: 0 0 5px currentColor; }
|
||||
50% { box-shadow: 0 0 15px currentColor; }
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes scan {
|
||||
0%, 100% { transform: translateY(-100%); opacity: 0; }
|
||||
50% { transform: translateY(2000%); opacity: 0.5; }
|
||||
}
|
||||
|
||||
/* ── Focus ───────────────────────────────────────────────────── */
|
||||
*:focus-visible {
|
||||
outline: none;
|
||||
border-color: var(--border-focus);
|
||||
box-shadow: var(--shadow-focus);
|
||||
}
|
||||
|
||||
/* ── Scrollbar ───────────────────────────────────────────────── */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--intel-dark);
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(14, 165, 233, 0.3);
|
||||
border-radius: var(--r-sm);
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(14, 165, 233, 0.5);
|
||||
}
|
||||
10
docs/design-system-redesign/fonts/README.md
Normal file
10
docs/design-system-redesign/fonts/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Fonts
|
||||
|
||||
This system uses two Google Fonts loaded via CDN inside `colors_and_type.css`:
|
||||
|
||||
- **Outfit** (300/400/500/600/700/800) — UI font
|
||||
- **JetBrains Mono** (400/500/600/700) — data, code, IDs
|
||||
|
||||
Both are imported at the top of `colors_and_type.css`. No local font files are bundled.
|
||||
|
||||
If you need offline assets, please attach the original `.woff2` files and we'll move them into this folder and switch the import to `@font-face` declarations.
|
||||
26
docs/design-system-redesign/preview/_card.css
Normal file
26
docs/design-system-redesign/preview/_card.css
Normal file
@@ -0,0 +1,26 @@
|
||||
/* Shared preview card scaffold — used by every card in /preview */
|
||||
@import url('../colors_and_type.css');
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--bg-page);
|
||||
color: var(--fg-1);
|
||||
font-family: var(--font-ui);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 20px 24px;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.card-row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
||||
.card-grid { display: grid; gap: 10px; }
|
||||
.col { display: flex; flex-direction: column; gap: 6px; }
|
||||
.spacer { flex: 1; }
|
||||
47
docs/design-system-redesign/preview/brand-logo.html
Normal file
47
docs/design-system-redesign/preview/brand-logo.html
Normal file
@@ -0,0 +1,47 @@
|
||||
<!doctype html><html><head><meta charset="utf-8"><title>Brand</title><link rel="stylesheet" href="_card.css"><style>
|
||||
.brand {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
padding: 14px 16px;
|
||||
background: linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(51,65,85,0.9) 100%);
|
||||
border: 1px solid rgba(14,165,233,0.30);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.brand-mark {
|
||||
width: 36px; height: 36px; border-radius: 8px;
|
||||
background: rgba(14,165,233,0.10); border: 1px solid rgba(14,165,233,0.4);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
color: var(--intel-accent-bright);
|
||||
box-shadow: inset 0 1px 0 rgba(14,165,233,0.2), 0 0 12px rgba(14,165,233,0.15);
|
||||
}
|
||||
.brand-name {
|
||||
font: 700 16px/1 var(--font-mono);
|
||||
color: var(--intel-accent-bright);
|
||||
text-transform: uppercase; letter-spacing: 0.10em;
|
||||
text-shadow: var(--glow-heading);
|
||||
}
|
||||
.brand-sub {
|
||||
font: 400 11px/1 var(--font-mono);
|
||||
color: var(--text-muted); margin-top: 4px;
|
||||
}
|
||||
</style></head><body>
|
||||
<div class="card">
|
||||
<div class="brand">
|
||||
<div class="brand-mark">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="2" width="18" height="20" rx="3"/>
|
||||
<circle cx="12" cy="11" r="5.5"/>
|
||||
<line x1="6.5" y1="11" x2="17.5" y2="11"/>
|
||||
<line x1="12" y1="5.5" x2="12" y2="16.5"/>
|
||||
<path d="M9.5 5.8C8.6 7.3 8 9 8 11s0.6 3.7 1.5 5.2"/>
|
||||
<path d="M14.5 5.8C15.4 7.3 16 9 16 11s-0.6 3.7-1.5 5.2"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="brand-name">STEAM Security</div>
|
||||
<div class="brand-sub">vulnerability management dashboard</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="t-meta">Atlas globe-badge mark + uppercase mono wordmark with sky-blue glow.</div>
|
||||
</div>
|
||||
</body></html>
|
||||
15
docs/design-system-redesign/preview/colors-accent.html
Normal file
15
docs/design-system-redesign/preview/colors-accent.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!doctype html><html><head><meta charset="utf-8"><title>Accent</title><link rel="stylesheet" href="_card.css"></head><body>
|
||||
<div class="card">
|
||||
<div class="card-row" style="gap:0;border-radius:8px;overflow:hidden;height:80px">
|
||||
<div style="flex:1;background:var(--accent);display:flex;align-items:flex-end;padding:8px"><div><div style="font:600 11px/1 var(--font-mono);color:var(--fg-on-accent)">--accent</div><div style="font:400 10px/1.4 var(--font-mono);color:var(--fg-on-accent);opacity:.75;margin-top:3px">#38BDF8</div></div></div>
|
||||
<div style="flex:1;background:var(--accent-hover);display:flex;align-items:flex-end;padding:8px"><div><div style="font:600 11px/1 var(--font-mono);color:var(--fg-on-accent)">accent-hover</div><div style="font:400 10px/1.4 var(--font-mono);color:var(--fg-on-accent);opacity:.75;margin-top:3px">#7DD3FC</div></div></div>
|
||||
<div style="flex:1;background:var(--accent-press);display:flex;align-items:flex-end;padding:8px"><div><div style="font:600 11px/1 var(--font-mono);color:var(--fg-on-accent)">accent-press</div><div style="font:400 10px/1.4 var(--font-mono);color:var(--fg-on-accent);opacity:.75;margin-top:3px">#0EA5E9</div></div></div>
|
||||
</div>
|
||||
<div class="card-row" style="gap:8px">
|
||||
<button style="background:var(--accent);color:var(--fg-on-accent);border:none;padding:8px 16px;border-radius:6px;font:600 13px var(--font-ui);cursor:pointer">Sync</button>
|
||||
<button style="background:var(--accent-hover);color:var(--fg-on-accent);border:none;padding:8px 16px;border-radius:6px;font:600 13px var(--font-ui)">:hover</button>
|
||||
<button style="background:var(--accent-press);color:var(--fg-on-accent);border:none;padding:8px 16px;border-radius:6px;font:600 13px var(--font-ui)">:active</button>
|
||||
<span class="t-meta">Sky-blue accent — primary action, link, focused state</span>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
||||
10
docs/design-system-redesign/preview/colors-foreground.html
Normal file
10
docs/design-system-redesign/preview/colors-foreground.html
Normal file
@@ -0,0 +1,10 @@
|
||||
<!doctype html><html><head><meta charset="utf-8"><title>Foreground</title><link rel="stylesheet" href="_card.css"></head><body>
|
||||
<div class="card">
|
||||
<div class="col" style="gap:8px">
|
||||
<div class="card-row" style="gap:14px"><span style="display:inline-block;width:24px;height:24px;background:var(--fg-1);border-radius:4px"></span><span class="t-h3" style="color:var(--fg-1);width:120px">--fg-1</span><span class="t-mono-sm" style="color:var(--fg-muted)">#F1F5F9</span><span class="t-meta">Headings, primary text</span></div>
|
||||
<div class="card-row" style="gap:14px"><span style="display:inline-block;width:24px;height:24px;background:var(--fg-2);border-radius:4px"></span><span class="t-h3" style="color:var(--fg-2);width:120px">--fg-2</span><span class="t-mono-sm" style="color:var(--fg-muted)">#CBD5E1</span><span class="t-meta">Body, secondary</span></div>
|
||||
<div class="card-row" style="gap:14px"><span style="display:inline-block;width:24px;height:24px;background:var(--fg-muted);border-radius:4px"></span><span class="t-h3" style="color:var(--fg-muted);width:120px">--fg-muted</span><span class="t-mono-sm" style="color:var(--fg-muted)">#94A3B8</span><span class="t-meta">Meta, captions, helper</span></div>
|
||||
<div class="card-row" style="gap:14px"><span style="display:inline-block;width:24px;height:24px;background:var(--fg-disabled);border-radius:4px"></span><span class="t-h3" style="color:var(--fg-disabled);width:120px">--fg-disabled</span><span class="t-mono-sm" style="color:var(--fg-muted)">#64748B</span><span class="t-meta">Placeholder, disabled</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
||||
54
docs/design-system-redesign/preview/colors-severity.html
Normal file
54
docs/design-system-redesign/preview/colors-severity.html
Normal file
@@ -0,0 +1,54 @@
|
||||
<!doctype html><html><head><meta charset="utf-8"><title>Severity</title><link rel="stylesheet" href="_card.css"><style>
|
||||
.sev-badge {
|
||||
display: inline-flex; align-items: center; gap: 8px;
|
||||
padding: 6px 14px; border-radius: 6px;
|
||||
font: 700 12px/1 var(--font-mono);
|
||||
letter-spacing: 0.5px; text-transform: uppercase;
|
||||
border: 2px solid;
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.4);
|
||||
}
|
||||
.sev-badge::before {
|
||||
content: ''; width: 8px; height: 8px; border-radius: 50%;
|
||||
animation: pulse-glow 2s ease-in-out infinite;
|
||||
}
|
||||
.sev-critical {
|
||||
background: linear-gradient(135deg, rgba(239,68,68,0.20) 0%, rgba(239,68,68,0.15) 100%);
|
||||
border-color: rgba(239,68,68,0.6); color: var(--sev-critical-text);
|
||||
text-shadow: 0 0 8px rgba(239,68,68,0.4);
|
||||
}
|
||||
.sev-critical::before { background: var(--sev-critical); box-shadow: var(--glow-danger); }
|
||||
.sev-high {
|
||||
background: linear-gradient(135deg, rgba(245,158,11,0.20) 0%, rgba(245,158,11,0.15) 100%);
|
||||
border-color: rgba(245,158,11,0.6); color: var(--sev-high-text);
|
||||
text-shadow: 0 0 8px rgba(245,158,11,0.4);
|
||||
}
|
||||
.sev-high::before { background: var(--sev-high); box-shadow: var(--glow-warning); }
|
||||
.sev-med {
|
||||
background: linear-gradient(135deg, rgba(14,165,233,0.20) 0%, rgba(14,165,233,0.15) 100%);
|
||||
border-color: rgba(14,165,233,0.6); color: var(--sev-medium-text);
|
||||
text-shadow: 0 0 8px rgba(14,165,233,0.4);
|
||||
}
|
||||
.sev-med::before { background: var(--sev-medium); box-shadow: var(--glow-info); }
|
||||
.sev-low {
|
||||
background: linear-gradient(135deg, rgba(16,185,129,0.20) 0%, rgba(16,185,129,0.15) 100%);
|
||||
border-color: rgba(16,185,129,0.6); color: var(--sev-low-text);
|
||||
text-shadow: 0 0 8px rgba(16,185,129,0.4);
|
||||
}
|
||||
.sev-low::before { background: var(--sev-low); box-shadow: var(--glow-success); }
|
||||
</style></head><body>
|
||||
<div class="card">
|
||||
<div class="card-row" style="gap:10px">
|
||||
<span class="sev-badge sev-critical">CRITICAL</span>
|
||||
<span class="sev-badge sev-high">HIGH</span>
|
||||
<span class="sev-badge sev-med">MEDIUM</span>
|
||||
<span class="sev-badge sev-low">LOW</span>
|
||||
</div>
|
||||
<div class="card-row" style="gap:14px;font:400 12px var(--font-mono);color:var(--fg-muted)">
|
||||
<span><span style="color:var(--sev-critical)">●</span> #EF4444</span>
|
||||
<span><span style="color:var(--sev-high)">●</span> #F59E0B</span>
|
||||
<span><span style="color:var(--sev-medium)">●</span> #0EA5E9</span>
|
||||
<span><span style="color:var(--sev-low)">●</span> #10B981</span>
|
||||
</div>
|
||||
<div class="t-meta">Pulsing dots + gradient fills + glow text-shadow. Mono uppercase, never remap.</div>
|
||||
</div>
|
||||
</body></html>
|
||||
17
docs/design-system-redesign/preview/colors-status.html
Normal file
17
docs/design-system-redesign/preview/colors-status.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!doctype html><html><head><meta charset="utf-8"><title>SLA & Status</title><link rel="stylesheet" href="_card.css"></head><body>
|
||||
<div class="card">
|
||||
<div class="t-label">SLA states</div>
|
||||
<div class="card-row" style="gap:8px">
|
||||
<span style="padding:4px 10px;border-radius:999px;background:var(--sev-critical-bg);color:var(--sev-critical);font:700 11px var(--font-mono);letter-spacing:.05em">OVERDUE</span>
|
||||
<span style="padding:4px 10px;border-radius:999px;background:var(--sev-high-bg);color:var(--sev-high);font:700 11px var(--font-mono);letter-spacing:.05em">AT_RISK</span>
|
||||
<span style="padding:4px 10px;border-radius:999px;background:var(--sev-low-bg);color:var(--sev-low);font:700 11px var(--font-mono);letter-spacing:.05em">WITHIN_SLA</span>
|
||||
</div>
|
||||
<div class="t-label">Status</div>
|
||||
<div class="card-row" style="gap:8px">
|
||||
<span style="padding:4px 10px;border-radius:4px;background:rgba(56,189,248,0.12);color:var(--accent);font:500 12px var(--font-ui)">Open</span>
|
||||
<span style="padding:4px 10px;border-radius:4px;background:rgba(245,158,11,0.12);color:var(--sev-high);font:500 12px var(--font-ui)">In Progress</span>
|
||||
<span style="padding:4px 10px;border-radius:4px;background:rgba(148,163,184,0.12);color:var(--fg-muted);font:500 12px var(--font-ui)">Addressed</span>
|
||||
<span style="padding:4px 10px;border-radius:4px;background:rgba(16,185,129,0.12);color:var(--sev-low);font:500 12px var(--font-ui)">Resolved</span>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
||||
11
docs/design-system-redesign/preview/colors-surfaces.html
Normal file
11
docs/design-system-redesign/preview/colors-surfaces.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<!doctype html><html><head><meta charset="utf-8"><title>Surface palette</title><link rel="stylesheet" href="_card.css"></head><body>
|
||||
<div class="card">
|
||||
<div class="card-row" style="gap:0;border:1px solid var(--border-1);border-radius:8px;overflow:hidden;height:100px">
|
||||
<div style="flex:1;background:var(--bg-page);display:flex;align-items:flex-end;padding:8px"><div><div style="font:600 11px/1 var(--font-mono);color:var(--fg-1)">--bg-page</div><div style="font:400 10px/1.4 var(--font-mono);color:var(--fg-muted);margin-top:3px">#0F172A</div></div></div>
|
||||
<div style="flex:1;background:var(--bg-surface);display:flex;align-items:flex-end;padding:8px"><div><div style="font:600 11px/1 var(--font-mono);color:var(--fg-1)">--bg-surface</div><div style="font:400 10px/1.4 var(--font-mono);color:var(--fg-muted);margin-top:3px">#1E293B</div></div></div>
|
||||
<div style="flex:1;background:var(--bg-elevated);display:flex;align-items:flex-end;padding:8px"><div><div style="font:600 11px/1 var(--font-mono);color:var(--fg-1)">--bg-elevated</div><div style="font:400 10px/1.4 var(--font-mono);color:var(--fg-muted);margin-top:3px">#334155</div></div></div>
|
||||
<div style="flex:1;background:var(--bg-hover);display:flex;align-items:flex-end;padding:8px"><div><div style="font:600 11px/1 var(--font-mono);color:var(--fg-1)">--bg-hover</div><div style="font:400 10px/1.4 var(--font-mono);color:var(--fg-muted);margin-top:3px">#475569</div></div></div>
|
||||
</div>
|
||||
<div class="t-meta">Page → surface → elevated → hover. Each step lifts ≈ one slate stop.</div>
|
||||
</div>
|
||||
</body></html>
|
||||
40
docs/design-system-redesign/preview/components-buttons.html
Normal file
40
docs/design-system-redesign/preview/components-buttons.html
Normal file
@@ -0,0 +1,40 @@
|
||||
<!doctype html><html><head><meta charset="utf-8"><title>Buttons</title><link rel="stylesheet" href="_card.css"><style>
|
||||
.intel-btn {
|
||||
position: relative; overflow: hidden;
|
||||
font: 600 13px/1 var(--font-mono);
|
||||
letter-spacing: 0.5px; text-transform: uppercase;
|
||||
padding: 10px 20px; border-radius: 6px;
|
||||
border: 1px solid; cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.1);
|
||||
}
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, rgba(14,165,233,0.15) 0%, rgba(14,165,233,0.10) 100%);
|
||||
border-color: var(--intel-accent); color: var(--intel-accent-bright);
|
||||
text-shadow: 0 0 6px rgba(14,165,233,0.2);
|
||||
}
|
||||
.btn-danger {
|
||||
background: linear-gradient(135deg, rgba(239,68,68,0.15) 0%, rgba(239,68,68,0.10) 100%);
|
||||
border-color: var(--intel-danger); color: #F87171;
|
||||
text-shadow: 0 0 6px rgba(239,68,68,0.2);
|
||||
}
|
||||
.btn-success {
|
||||
background: linear-gradient(135deg, rgba(16,185,129,0.15) 0%, rgba(16,185,129,0.10) 100%);
|
||||
border-color: var(--intel-success); color: #34D399;
|
||||
text-shadow: 0 0 6px rgba(16,185,129,0.2);
|
||||
}
|
||||
.btn-ghost {
|
||||
background: transparent; border-color: var(--border-default);
|
||||
color: var(--text-muted); text-shadow: none;
|
||||
}
|
||||
</style></head><body>
|
||||
<div class="card">
|
||||
<div class="card-row" style="gap:10px">
|
||||
<button class="intel-btn btn-primary">Sync</button>
|
||||
<button class="intel-btn btn-success">Approve FP</button>
|
||||
<button class="intel-btn btn-danger">Delete</button>
|
||||
<button class="intel-btn btn-ghost">Cancel</button>
|
||||
</div>
|
||||
<div class="t-meta">Mono · uppercase · gradient fills · 1px brand-color border · soft text-glow.</div>
|
||||
</div>
|
||||
</body></html>
|
||||
43
docs/design-system-redesign/preview/components-groups.html
Normal file
43
docs/design-system-redesign/preview/components-groups.html
Normal file
@@ -0,0 +1,43 @@
|
||||
<!doctype html><html><head><meta charset="utf-8"><title>Stat cards</title><link rel="stylesheet" href="_card.css"><style>
|
||||
.stat-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
|
||||
.stat-card {
|
||||
background: linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(51,65,85,0.9) 100%);
|
||||
border: 1.5px solid rgba(14,165,233,0.35);
|
||||
border-radius: 8px; padding: 14px 16px;
|
||||
position: relative; overflow: hidden;
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
.stat-card::before {
|
||||
content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px;
|
||||
background: linear-gradient(90deg, transparent, var(--intel-accent), transparent);
|
||||
opacity: 0.8; box-shadow: 0 0 8px rgba(14,165,233,0.5);
|
||||
}
|
||||
.stat-label {
|
||||
font: 500 11px/1 var(--font-mono); letter-spacing: 0.05em;
|
||||
text-transform: uppercase; color: var(--text-muted); margin-bottom: 8px;
|
||||
}
|
||||
.stat-val { font: 700 24px/1 var(--font-mono); color: var(--text-primary); }
|
||||
.stat-delta-up { color: var(--intel-success); font: 500 11px var(--font-mono); }
|
||||
.stat-delta-down { color: var(--intel-danger); font: 500 11px var(--font-mono); }
|
||||
</style></head><body>
|
||||
<div class="card">
|
||||
<div class="stat-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Open Findings</div>
|
||||
<div class="stat-val">1,247</div>
|
||||
<div class="stat-delta-up">↑ 12 vs last sync</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">FP Pending</div>
|
||||
<div class="stat-val">38</div>
|
||||
<div class="stat-delta-down">↓ 4 today</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Compliance %</div>
|
||||
<div class="stat-val">94.2</div>
|
||||
<div class="stat-delta-up">↑ 0.8% wk</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="t-meta">Top accent rail · gradient surface · sky inner highlight · 4-px lift on hover.</div>
|
||||
</div>
|
||||
</body></html>
|
||||
19
docs/design-system-redesign/preview/components-inputs.html
Normal file
19
docs/design-system-redesign/preview/components-inputs.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<!doctype html><html><head><meta charset="utf-8"><title>Inputs</title><link rel="stylesheet" href="_card.css"><style>
|
||||
.field{display:flex;flex-direction:column;gap:4px;flex:1;min-width:160px}
|
||||
.field label{font:500 11px var(--font-ui);color:var(--fg-muted);text-transform:uppercase;letter-spacing:.06em}
|
||||
.field input,.field select{background:var(--bg-input);color:var(--fg-1);border:1px solid var(--border-1);border-radius:6px;padding:8px 10px;font:400 13px var(--font-ui);outline:none}
|
||||
.field input:focus,.field select:focus{border-color:var(--border-focus);box-shadow:var(--shadow-focus)}
|
||||
.field input::placeholder{color:var(--fg-disabled)}
|
||||
</style></head><body>
|
||||
<div class="card">
|
||||
<div class="card-row" style="gap:14px;align-items:flex-start">
|
||||
<div class="field"><label>Search</label><input placeholder="CVE-2024-…" /></div>
|
||||
<div class="field"><label>Vendor</label><select><option>All vendors</option><option>Cisco</option><option>Juniper</option></select></div>
|
||||
<div class="field"><label>Severity</label><select><option>All</option><option>Critical</option><option>High</option></select></div>
|
||||
</div>
|
||||
<div class="card-row" style="gap:14px">
|
||||
<div class="field" style="max-width:240px"><label>Focused</label><input value="EXC-30482" style="border-color:var(--border-focus);box-shadow:var(--shadow-focus)" /></div>
|
||||
<div class="field" style="max-width:240px"><label>Disabled</label><input value="read only" disabled style="opacity:.5" /></div>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
||||
22
docs/design-system-redesign/preview/components-table.html
Normal file
22
docs/design-system-redesign/preview/components-table.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<!doctype html><html><head><meta charset="utf-8"><title>Table</title><link rel="stylesheet" href="_card.css"><style>
|
||||
table{width:100%;border-collapse:separate;border-spacing:0;font:400 12px var(--font-ui)}
|
||||
th{font:500 10px var(--font-ui);text-transform:uppercase;letter-spacing:.06em;color:var(--fg-muted);text-align:left;padding:8px 10px;background:var(--bg-surface);border-bottom:1px solid var(--border-1)}
|
||||
td{padding:9px 10px;border-bottom:1px solid var(--border-1);color:var(--fg-2)}
|
||||
tr:last-child td{border-bottom:none}
|
||||
tr.hover td{background:var(--accent-wash)}
|
||||
.mono{font-family:var(--font-mono)}
|
||||
.sev{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:6px;vertical-align:middle}
|
||||
</style></head><body>
|
||||
<div class="card" style="padding:0">
|
||||
<div style="border:1px solid var(--border-1);border-radius:8px;overflow:hidden;height:100%">
|
||||
<table>
|
||||
<thead><tr><th>Severity</th><th>CVE</th><th>Host</th><th>Due</th><th>SLA</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><span class="sev" style="background:var(--sev-critical)"></span><span class="mono" style="color:var(--fg-1)">9.8</span></td><td class="mono" style="color:var(--fg-1)">CVE-2024-21412</td><td class="mono">bdc-edge-fw01</td><td class="mono" style="color:var(--sev-critical)">Apr 21</td><td><span style="font:700 10px var(--font-mono);color:var(--sev-critical);background:var(--sev-critical-bg);padding:2px 8px;border-radius:999px">OVERDUE</span></td></tr>
|
||||
<tr class="hover"><td><span class="sev" style="background:var(--sev-high)"></span><span class="mono" style="color:var(--fg-1)">8.9</span></td><td class="mono" style="color:var(--fg-1)">CVE-2024-3661</td><td class="mono">bdc-core-rtr03</td><td class="mono" style="color:var(--sev-high)">May 06</td><td><span style="font:700 10px var(--font-mono);color:var(--sev-high);background:var(--sev-high-bg);padding:2px 8px;border-radius:999px">AT_RISK</span></td></tr>
|
||||
<tr><td><span class="sev" style="background:var(--sev-medium)"></span><span class="mono" style="color:var(--fg-1)">8.6</span></td><td class="mono" style="color:var(--fg-1)">CVE-2023-46604</td><td class="mono">bdc-mq-broker</td><td class="mono">Jun 14</td><td><span style="font:700 10px var(--font-mono);color:var(--sev-low);background:var(--sev-low-bg);padding:2px 8px;border-radius:999px">WITHIN_SLA</span></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
||||
21
docs/design-system-redesign/preview/components-workflow.html
Normal file
21
docs/design-system-redesign/preview/components-workflow.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!doctype html><html><head><meta charset="utf-8"><title>Workflow badges</title><link rel="stylesheet" href="_card.css"><style>
|
||||
.wf{display:inline-flex;align-items:center;gap:6px;padding:3px 9px;border-radius:4px;font:600 11px var(--font-mono);letter-spacing:.04em}
|
||||
</style></head><body>
|
||||
<div class="card">
|
||||
<div class="t-label">FP workflow states</div>
|
||||
<div class="card-row" style="gap:8px">
|
||||
<span class="wf" style="background:var(--sev-medium-bg);color:var(--sev-medium)">Actionable</span>
|
||||
<span class="wf" style="background:var(--sev-high-bg);color:var(--sev-high)">Requested</span>
|
||||
<span class="wf" style="background:rgba(148,163,184,0.16);color:var(--fg-muted)">Reworked</span>
|
||||
<span class="wf" style="background:var(--sev-low-bg);color:var(--sev-low)">Approved</span>
|
||||
<span class="wf" style="background:var(--sev-critical-bg);color:var(--sev-critical)">Rejected</span>
|
||||
<span class="wf" style="background:var(--sev-critical-bg);color:var(--sev-critical)">Expired</span>
|
||||
</div>
|
||||
<div class="t-label">Queue type tags</div>
|
||||
<div class="card-row" style="gap:8px">
|
||||
<span class="wf" style="background:var(--sev-high-bg);color:var(--sev-high)">FP</span>
|
||||
<span class="wf" style="background:var(--sev-medium-bg);color:var(--sev-medium)">ARCHER</span>
|
||||
<span class="wf" style="background:var(--sev-low-bg);color:var(--sev-low)">CARD</span>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
||||
11
docs/design-system-redesign/preview/elevation.html
Normal file
11
docs/design-system-redesign/preview/elevation.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<!doctype html><html><head><meta charset="utf-8"><title>Elevation</title><link rel="stylesheet" href="_card.css"></head><body>
|
||||
<div class="card">
|
||||
<div class="card-row" style="gap:18px">
|
||||
<div style="text-align:center"><div style="width:90px;height:60px;background:var(--bg-surface);border:1px solid var(--border-1);border-radius:8px;box-shadow:var(--shadow-rest)"></div><div class="t-mono-sm" style="margin-top:8px">rest</div></div>
|
||||
<div style="text-align:center"><div style="width:90px;height:60px;background:var(--bg-surface);border:1px solid var(--border-1);border-radius:8px;box-shadow:var(--shadow-popover)"></div><div class="t-mono-sm" style="margin-top:8px">popover</div></div>
|
||||
<div style="text-align:center"><div style="width:90px;height:60px;background:var(--bg-surface);border:1px solid var(--border-1);border-radius:12px;box-shadow:var(--shadow-modal)"></div><div class="t-mono-sm" style="margin-top:8px">modal</div></div>
|
||||
<div style="text-align:center"><div style="width:90px;height:60px;background:var(--bg-surface);border:1px solid var(--border-focus);border-radius:8px;box-shadow:var(--shadow-focus)"></div><div class="t-mono-sm" style="margin-top:8px">focus</div></div>
|
||||
</div>
|
||||
<div class="t-meta">Outer shadows only. No insets. Shadows visible but never dramatic on dark.</div>
|
||||
</div>
|
||||
</body></html>
|
||||
21
docs/design-system-redesign/preview/iconography.html
Normal file
21
docs/design-system-redesign/preview/iconography.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!doctype html><html><head><meta charset="utf-8"><title>Iconography</title><link rel="stylesheet" href="_card.css"><style>
|
||||
.ic{display:flex;flex-direction:column;align-items:center;gap:5px;color:var(--fg-2);width:64px}
|
||||
.ic svg{stroke:currentColor;fill:none;stroke-width:1.75;stroke-linecap:round;stroke-linejoin:round}
|
||||
.ic .lbl{font:400 10px var(--font-mono);color:var(--fg-muted)}
|
||||
</style></head><body>
|
||||
<div class="card">
|
||||
<div class="t-label">Lucide line icons · 1.5–2px stroke · currentColor</div>
|
||||
<div class="card-row" style="gap:8px;justify-content:flex-start">
|
||||
<div class="ic"><svg width="22" height="22" viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg><div class="lbl">shield</div></div>
|
||||
<div class="ic"><svg width="22" height="22" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg><div class="lbl">search</div></div>
|
||||
<div class="ic"><svg width="22" height="22" viewBox="0 0 24 24"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg><div class="lbl">filter</div></div>
|
||||
<div class="ic"><svg width="22" height="22" viewBox="0 0 24 24"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"/><path d="M16 16h5v5"/></svg><div class="lbl">sync</div></div>
|
||||
<div class="ic"><svg width="22" height="22" viewBox="0 0 24 24"><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><div class="lbl">download</div></div>
|
||||
<div class="ic"><svg width="22" height="22" viewBox="0 0 24 24"><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><div class="lbl">upload</div></div>
|
||||
<div class="ic"><svg width="22" height="22" viewBox="0 0 24 24"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg><div class="lbl">eye</div></div>
|
||||
<div class="ic"><svg width="22" height="22" viewBox="0 0 24 24"><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><div class="lbl">calendar</div></div>
|
||||
<div class="ic"><svg width="22" height="22" viewBox="0 0 24 24"><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><div class="lbl">file</div></div>
|
||||
<div class="ic"><svg width="22" height="22" viewBox="0 0 24 24"><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><div class="lbl">alert</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
||||
13
docs/design-system-redesign/preview/radii.html
Normal file
13
docs/design-system-redesign/preview/radii.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html><html><head><meta charset="utf-8"><title>Radii</title><link rel="stylesheet" href="_card.css"></head><body>
|
||||
<div class="card">
|
||||
<div class="card-row" style="gap:14px">
|
||||
<div style="text-align:center"><div style="width:56px;height:56px;background:var(--bg-elevated);border:1px solid var(--border-2);border-radius:3px"></div><div class="t-mono-sm" style="margin-top:6px">3 · xs</div></div>
|
||||
<div style="text-align:center"><div style="width:56px;height:56px;background:var(--bg-elevated);border:1px solid var(--border-2);border-radius:4px"></div><div class="t-mono-sm" style="margin-top:6px">4 · sm</div></div>
|
||||
<div style="text-align:center"><div style="width:56px;height:56px;background:var(--bg-elevated);border:1px solid var(--border-2);border-radius:6px"></div><div class="t-mono-sm" style="margin-top:6px">6 · md</div></div>
|
||||
<div style="text-align:center"><div style="width:56px;height:56px;background:var(--bg-elevated);border:1px solid var(--border-2);border-radius:8px"></div><div class="t-mono-sm" style="margin-top:6px">8 · lg</div></div>
|
||||
<div style="text-align:center"><div style="width:56px;height:56px;background:var(--bg-elevated);border:1px solid var(--border-2);border-radius:12px"></div><div class="t-mono-sm" style="margin-top:6px">12 · xl</div></div>
|
||||
<div style="text-align:center"><div style="width:84px;height:32px;background:var(--bg-elevated);border:1px solid var(--border-2);border-radius:999px;margin-top:12px"></div><div class="t-mono-sm" style="margin-top:6px">pill</div></div>
|
||||
</div>
|
||||
<div class="t-meta">Chips 4 · button/input 6 · cards 8 · modals 12 · pills for badges and toggles.</div>
|
||||
</div>
|
||||
</body></html>
|
||||
17
docs/design-system-redesign/preview/spacing.html
Normal file
17
docs/design-system-redesign/preview/spacing.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!doctype html><html><head><meta charset="utf-8"><title>Spacing</title><link rel="stylesheet" href="_card.css"></head><body>
|
||||
<div class="card">
|
||||
<div class="t-label">4px grid · sp-1 → sp-12</div>
|
||||
<div class="card-row" style="align-items:flex-end;gap:10px">
|
||||
<div style="text-align:center"><div style="width:4px;height:24px;background:var(--accent)"></div><div class="t-mono-sm" style="margin-top:6px">4</div></div>
|
||||
<div style="text-align:center"><div style="width:8px;height:24px;background:var(--accent)"></div><div class="t-mono-sm" style="margin-top:6px">8</div></div>
|
||||
<div style="text-align:center"><div style="width:12px;height:24px;background:var(--accent)"></div><div class="t-mono-sm" style="margin-top:6px">12</div></div>
|
||||
<div style="text-align:center"><div style="width:16px;height:24px;background:var(--accent)"></div><div class="t-mono-sm" style="margin-top:6px">16</div></div>
|
||||
<div style="text-align:center"><div style="width:20px;height:24px;background:var(--accent)"></div><div class="t-mono-sm" style="margin-top:6px">20</div></div>
|
||||
<div style="text-align:center"><div style="width:24px;height:24px;background:var(--accent)"></div><div class="t-mono-sm" style="margin-top:6px">24</div></div>
|
||||
<div style="text-align:center"><div style="width:32px;height:24px;background:var(--accent)"></div><div class="t-mono-sm" style="margin-top:6px">32</div></div>
|
||||
<div style="text-align:center"><div style="width:40px;height:24px;background:var(--accent)"></div><div class="t-mono-sm" style="margin-top:6px">40</div></div>
|
||||
<div style="text-align:center"><div style="width:48px;height:24px;background:var(--accent)"></div><div class="t-mono-sm" style="margin-top:6px">48</div></div>
|
||||
</div>
|
||||
<div class="t-meta">Card padding 16–20 · row vertical 8–10 · section gap 24–32 · modal padding 24.</div>
|
||||
</div>
|
||||
</body></html>
|
||||
12
docs/design-system-redesign/preview/type-mono.html
Normal file
12
docs/design-system-redesign/preview/type-mono.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!doctype html><html><head><meta charset="utf-8"><title>Type · mono</title><link rel="stylesheet" href="_card.css"></head><body>
|
||||
<div class="card">
|
||||
<div class="col" style="gap:8px">
|
||||
<div class="t-label">JetBrains Mono · used for data</div>
|
||||
<div style="font:500 16px var(--font-mono);color:var(--fg-1)">CVE-2024-21412</div>
|
||||
<div style="font:400 14px var(--font-mono);color:var(--fg-2)">10.42.18.137 · bdc-edge-fw01.steam.local</div>
|
||||
<div style="font:400 13px var(--font-mono);color:var(--fg-2)">EXC-30482 FP#9821 finding-id 5048124</div>
|
||||
<div style="font:400 12px var(--font-mono);color:var(--fg-muted)">VRR 9.4 · 2026-04-29 · WITHIN_SLA</div>
|
||||
<div style="font:600 13px var(--font-mono);color:var(--fg-1);background:var(--bg-elevated);padding:6px 10px;border-radius:4px;display:inline-block;align-self:flex-start">openssl rand -base64 32</div>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
||||
12
docs/design-system-redesign/preview/type-ui.html
Normal file
12
docs/design-system-redesign/preview/type-ui.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!doctype html><html><head><meta charset="utf-8"><title>Type · UI font</title><link rel="stylesheet" href="_card.css"></head><body>
|
||||
<div class="card">
|
||||
<div class="col" style="gap:6px">
|
||||
<div style="display:flex;align-items:baseline;gap:14px"><span class="t-display">Outfit Display 28</span><span class="t-mono-sm">700 / 1.2 / -0.01em</span></div>
|
||||
<div style="display:flex;align-items:baseline;gap:14px"><span class="t-h1">Page title 24</span><span class="t-mono-sm">600 / 1.2</span></div>
|
||||
<div style="display:flex;align-items:baseline;gap:14px"><span class="t-h2">Section header 18</span><span class="t-mono-sm">600 / 1.2</span></div>
|
||||
<div style="display:flex;align-items:baseline;gap:14px"><span class="t-h3">Card title 16</span><span class="t-mono-sm">600 / 1.4</span></div>
|
||||
<div style="display:flex;align-items:baseline;gap:14px"><span class="t-body">Body 14 — searchable filterable list</span><span class="t-mono-sm">400 / 1.4</span></div>
|
||||
<div style="display:flex;align-items:baseline;gap:14px"><span class="t-meta">Meta 12 — last sync 4h ago</span><span class="t-mono-sm">400 / 1.4</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
||||
@@ -0,0 +1,618 @@
|
||||
// CompPrimitives.jsx — primitives for the Compliance page kit.
|
||||
// Lifted directly from frontend/src/components/pages/CompliancePage.js.
|
||||
// Identity color is teal (#14B8A6); status colors map green/amber/red onto
|
||||
// "Meets/Exceeds Target", "Within 15% of Target", and "Below 15% of Target".
|
||||
|
||||
const { useState: useCompState, useRef: useCompRef } = React;
|
||||
|
||||
/* ── Tokens ──────────────────────────────────────────────────────
|
||||
Two layers:
|
||||
• Status — drives every percentage display + the worst-status
|
||||
ribbon on metric cards. Always one of three.
|
||||
• Category — owns the colored MetricBadge that flags which
|
||||
program a failing metric belongs to. */
|
||||
const C_COLORS = {
|
||||
teal: '#14B8A6',
|
||||
tealMid: '#5EEAD4',
|
||||
green: '#10B981',
|
||||
amber: '#F59E0B',
|
||||
red: '#EF4444',
|
||||
sky: '#0EA5E9',
|
||||
purple: '#8B5CF6',
|
||||
orange: '#F97316',
|
||||
slate: '#64748B',
|
||||
};
|
||||
|
||||
const STATUS_COLOR = {
|
||||
'Meets/Exceeds Target': C_COLORS.green,
|
||||
'Within 15% of Target': C_COLORS.amber,
|
||||
'Below 15% of Target': C_COLORS.red,
|
||||
};
|
||||
|
||||
const CATEGORY_COLORS = {
|
||||
'Vulnerability Management': C_COLORS.red,
|
||||
'Access & MFA': C_COLORS.amber,
|
||||
'Logging & Monitoring': C_COLORS.purple,
|
||||
'End-of-Life OS': C_COLORS.orange,
|
||||
'Decommissioned Assets': C_COLORS.slate,
|
||||
'Asset Data Quality': C_COLORS.slate,
|
||||
'Application Security': C_COLORS.sky,
|
||||
'Disaster Recovery': C_COLORS.teal,
|
||||
'Endpoint Protection': C_COLORS.orange,
|
||||
};
|
||||
|
||||
const statusColor = s => STATUS_COLOR[s] || C_COLORS.red;
|
||||
const pctDisplay = p => `${Math.round(p * 100)}%`;
|
||||
const cAlpha = (hex, a) => {
|
||||
const h = hex.replace('#', '');
|
||||
return `rgba(${parseInt(h.slice(0,2),16)},${parseInt(h.slice(2,4),16)},${parseInt(h.slice(4,6),16)},${a})`;
|
||||
};
|
||||
|
||||
/* ── PageHeader ──────────────────────────────────────────────────
|
||||
AEO Compliance — title in teal w/ glow, last-report meta beneath,
|
||||
refresh + upload-report on the right. Mirrors the KB / Reporting
|
||||
header pattern but with teal instead of green. */
|
||||
function CompPageHeader({ title = 'AEO Compliance', lastReport, networkScore, verticalScore, onRefresh, onUpload, onRollback, isAdmin }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 24, gap: 16 }}>
|
||||
<div>
|
||||
<h2 style={{
|
||||
margin: '0 0 6px 0',
|
||||
fontFamily: 'var(--font-mono)', fontSize: 24, fontWeight: 700,
|
||||
color: C_COLORS.teal, textTransform: 'uppercase', letterSpacing: '0.1em',
|
||||
textShadow: `0 0 16px ${cAlpha(C_COLORS.teal, 0.4)}`,
|
||||
}}>{title}</h2>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap', fontFamily: 'var(--font-mono)', fontSize: 11 }}>
|
||||
{lastReport ? (
|
||||
<>
|
||||
<span style={{ color: 'var(--fg-disabled)' }}>
|
||||
Last report: <span style={{ color: 'var(--fg-2)' }}>{lastReport}</span>
|
||||
</span>
|
||||
{isAdmin && (
|
||||
<button onClick={onRollback} style={{
|
||||
background: 'transparent', border: '1px solid rgba(239,68,68,0.25)',
|
||||
borderRadius: 4, padding: '2px 6px', cursor: 'pointer',
|
||||
color: 'var(--fg-2)', display: 'inline-flex', alignItems: 'center', gap: 4,
|
||||
fontSize: 10, fontFamily: 'var(--font-mono)',
|
||||
}}>
|
||||
<CompIcon name="rotate" size={10} color="currentColor" /> Rollback
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<span style={{ color: 'var(--fg-disabled)' }}>No reports uploaded</span>
|
||||
)}
|
||||
{networkScore != null && (
|
||||
<span style={{ color: 'var(--fg-2)' }}>Network: <span style={{ color: C_COLORS.teal }}>{networkScore}</span></span>
|
||||
)}
|
||||
{verticalScore != null && (
|
||||
<span style={{ color: 'var(--fg-2)' }}>Vertical: <span style={{ color: C_COLORS.teal }}>{verticalScore}</span></span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, flexShrink: 0 }}>
|
||||
<CompIconButton icon="refresh" onClick={onRefresh} />
|
||||
<CompButton variant="primary" icon="upload" onClick={onUpload}>Upload Report</CompButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Buttons ───────────────────────────────────────────────────── */
|
||||
function CompButton({ variant = 'neutral', icon, size = 'md', children, ...rest }) {
|
||||
const [hover, setHover] = useCompState(false);
|
||||
const v = {
|
||||
primary: { bg: hover ? cAlpha(C_COLORS.teal, 0.28) : cAlpha(C_COLORS.teal, 0.18), bd: C_COLORS.teal, fg: C_COLORS.teal },
|
||||
neutral: { bg: hover ? cAlpha(C_COLORS.teal, 0.10) : 'transparent', bd: cAlpha(C_COLORS.teal, 0.30), fg: C_COLORS.teal },
|
||||
danger: { bg: hover ? 'rgba(239,68,68,0.18)' : 'rgba(239,68,68,0.10)', bd: C_COLORS.red, fg: C_COLORS.red },
|
||||
ghost: { bg: hover ? 'rgba(255,255,255,0.04)' : 'transparent', bd: 'rgba(100,116,139,0.40)', fg: 'var(--fg-2)' },
|
||||
}[variant];
|
||||
const padX = size === 'sm' ? 10 : 16;
|
||||
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.05em',
|
||||
cursor: 'pointer', transition: 'all 160ms ease', whiteSpace: 'nowrap',
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
{icon && <CompIcon name={icon} size={fs + 2} color={v.fg} />}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function CompIconButton({ icon, onClick, color = C_COLORS.teal }) {
|
||||
const [hover, setHover] = useCompState(false);
|
||||
return (
|
||||
<button onClick={onClick}
|
||||
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
||||
style={{
|
||||
background: hover ? cAlpha(color, 0.10) : 'transparent',
|
||||
border: `1px solid ${hover ? color : cAlpha(color, 0.25)}`,
|
||||
borderRadius: 6, padding: 8, cursor: 'pointer',
|
||||
color: hover ? color : 'var(--fg-2)',
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||||
transition: 'all 160ms ease',
|
||||
}}>
|
||||
<CompIcon name={icon} size={16} color="currentColor" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── TeamTabs ──────────────────────────────────────────────────── */
|
||||
function TeamTabs({ teams, active, onChange }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: 6, marginBottom: 24 }}>
|
||||
{teams.map(team => {
|
||||
const on = active === team;
|
||||
return (
|
||||
<button key={team} onClick={() => onChange(team)} style={{
|
||||
padding: '8px 18px', cursor: 'pointer',
|
||||
fontFamily: 'var(--font-mono)', fontSize: 12, fontWeight: 600,
|
||||
textTransform: 'uppercase', letterSpacing: '0.06em',
|
||||
borderRadius: 6,
|
||||
border: `1px solid ${on ? C_COLORS.teal : cAlpha(C_COLORS.teal, 0.20)}`,
|
||||
background: on ? cAlpha(C_COLORS.teal, 0.18) : 'transparent',
|
||||
color: on ? C_COLORS.teal : 'var(--fg-disabled)',
|
||||
transition: 'all 160ms ease',
|
||||
}}>{team}</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── VariantPill ─────────────────────────────────────────────────
|
||||
The compliance % pill that lives inside MetricHealthCard. One per
|
||||
priority/variant within a metric family. Dot only shown when the
|
||||
variant isn't already meeting target — green pills stay quiet. */
|
||||
function VariantPill({ status, pct, label }) {
|
||||
const color = statusColor(status);
|
||||
const isOk = status === 'Meets/Exceeds Target';
|
||||
return (
|
||||
<span style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 4,
|
||||
padding: '2px 7px',
|
||||
background: cAlpha(color, 0.12),
|
||||
border: `1px solid ${cAlpha(color, 0.25)}`,
|
||||
borderRadius: 3,
|
||||
fontFamily: 'var(--font-mono)', fontSize: 10,
|
||||
color: 'var(--fg-2)', whiteSpace: 'nowrap',
|
||||
}}>
|
||||
{!isOk && (
|
||||
<span style={{
|
||||
display: 'inline-block', width: 4, height: 4, borderRadius: '50%',
|
||||
background: color, boxShadow: `0 0 5px ${color}`,
|
||||
}} />
|
||||
)}
|
||||
{label && <span style={{ color: 'var(--fg-disabled)' }}>{label}</span>}
|
||||
<span style={{ color, fontWeight: 600 }}>{pctDisplay(pct)}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── StatusRibbon ────────────────────────────────────────────────
|
||||
The lozenge at the bottom of MetricHealthCard. "OK" when meeting,
|
||||
abbreviated status text otherwise. */
|
||||
function StatusRibbon({ status }) {
|
||||
const color = statusColor(status);
|
||||
const isOk = status === 'Meets/Exceeds Target';
|
||||
return (
|
||||
<div style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 5,
|
||||
fontFamily: 'var(--font-mono)', fontSize: 10,
|
||||
textTransform: 'uppercase', letterSpacing: '0.04em',
|
||||
color, padding: '3px 9px',
|
||||
background: cAlpha(color, 0.10),
|
||||
borderRadius: 999,
|
||||
border: `1px solid ${cAlpha(color, 0.30)}`,
|
||||
}}>
|
||||
<span style={{
|
||||
display: 'inline-block', width: 5, height: 5, borderRadius: '50%',
|
||||
background: color, boxShadow: isOk ? 'none' : `0 0 6px ${color}`,
|
||||
}} />
|
||||
{isOk ? 'OK' : status.replace(' of Target', '')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── MetricHealthCard ────────────────────────────────────────────
|
||||
The big clickable cards in the metric strip. Click to filter the
|
||||
device table; click the info "i" to open the metric definition
|
||||
panel. Border + ID color shift when active. */
|
||||
function MetricHealthCard({ family, active, onClick, onInfoClick, onHover, onLeave }) {
|
||||
const [h, setH] = useCompState(false);
|
||||
const color = statusColor(family.worstStatus);
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
onMouseEnter={(e) => { setH(true); onHover && onHover(e.currentTarget); }}
|
||||
onMouseLeave={() => { setH(false); onLeave && onLeave(); }}
|
||||
style={{
|
||||
position: 'relative', textAlign: 'left', cursor: 'pointer',
|
||||
background: active
|
||||
? cAlpha(color, 0.15)
|
||||
: 'linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(15,23,42,0.98) 100%)',
|
||||
border: `1.5px solid ${active ? color : (h ? cAlpha(color, 0.50) : cAlpha(color, 0.25))}`,
|
||||
borderRadius: 8,
|
||||
padding: '14px 16px',
|
||||
minWidth: 160, flex: '1 1 0',
|
||||
transition: 'all 160ms ease',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
onClick={(e) => { e.stopPropagation(); onInfoClick && onInfoClick(family.metricId); }}
|
||||
style={{
|
||||
position: 'absolute', top: 8, right: 8,
|
||||
display: 'inline-flex', cursor: 'pointer', padding: 2,
|
||||
color: 'var(--fg-disabled)', borderRadius: 3,
|
||||
}}
|
||||
>
|
||||
<CompIcon name="info" size={13} color="currentColor" />
|
||||
</span>
|
||||
<div style={{
|
||||
fontFamily: 'var(--font-mono)', fontSize: 15, fontWeight: 700,
|
||||
color: active ? color : 'var(--fg-1)', marginBottom: 4, paddingRight: 20,
|
||||
}}>{family.metricId}</div>
|
||||
<div style={{
|
||||
fontFamily: 'var(--font-mono)', fontSize: 10,
|
||||
color: 'var(--fg-disabled)', textTransform: 'uppercase', letterSpacing: '0.05em',
|
||||
marginBottom: 8,
|
||||
}}>{family.category}</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 5, marginBottom: 8 }}>
|
||||
{family.entries.map((e, i) => (
|
||||
<VariantPill
|
||||
key={e.metric_id + '-' + i}
|
||||
status={e.status} pct={e.compliance_pct}
|
||||
label={family.entries.length > 1 ? (e.priority || `#${i + 1}`) : null}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div style={{
|
||||
fontFamily: 'var(--font-mono)', fontSize: 10,
|
||||
color: 'var(--fg-disabled)', marginBottom: 8,
|
||||
}}>target {pctDisplay(family.target)}</div>
|
||||
<StatusRibbon status={family.worstStatus} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── MetricBadge ─────────────────────────────────────────────────
|
||||
Compact category-tinted ID chip used in device-row "Failing Metrics"
|
||||
columns and inside detail panels. */
|
||||
function MetricBadge({ metricId, category }) {
|
||||
const color = CATEGORY_COLORS[category] || C_COLORS.slate;
|
||||
return (
|
||||
<span style={{
|
||||
display: 'inline-flex', alignItems: 'center',
|
||||
padding: '2px 7px',
|
||||
background: cAlpha(color, 0.12),
|
||||
border: `1px solid ${cAlpha(color, 0.30)}`,
|
||||
borderRadius: 3, color,
|
||||
fontFamily: 'var(--font-mono)', fontSize: 10, fontWeight: 600,
|
||||
whiteSpace: 'nowrap',
|
||||
}}>{metricId}</span>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── SeenBadge ───────────────────────────────────────────────────
|
||||
"1×" / "3×" / "5×" — how many cycles a host has been failing the
|
||||
same set of metrics. Color escalates: slate → amber → red. */
|
||||
function SeenBadge({ count }) {
|
||||
const color = count > 3 ? C_COLORS.red : count > 1 ? C_COLORS.amber : C_COLORS.slate;
|
||||
return (
|
||||
<span style={{
|
||||
fontFamily: 'var(--font-mono)', fontSize: 10, fontWeight: 700,
|
||||
color, padding: '2px 7px',
|
||||
background: cAlpha(color, 0.10),
|
||||
border: `1px solid ${cAlpha(color, 0.30)}`,
|
||||
borderRadius: 3, whiteSpace: 'nowrap',
|
||||
}}>{count}×</span>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── DeviceTable + DeviceRow ─────────────────────────────────────
|
||||
The non-compliant host list. Toolbar has Active/Resolved tabs +
|
||||
hostname search. Rows show hostname, IP, type, failing metric
|
||||
badges, seen count, and a notes indicator. */
|
||||
function DeviceTable({ children }) {
|
||||
return (
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(15,23,42,0.98) 100%)',
|
||||
border: '1px solid rgba(20,184,166,0.15)',
|
||||
borderRadius: 8, overflow: 'hidden',
|
||||
}}>{children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DeviceTableToolbar({ tab, onTabChange, count, search, onSearchChange }) {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||||
padding: '14px 16px', borderBottom: '1px solid rgba(255,255,255,0.05)',
|
||||
}}>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
{['active', 'resolved'].map(t => {
|
||||
const on = tab === t;
|
||||
return (
|
||||
<button key={t} onClick={() => onTabChange(t)} style={{
|
||||
padding: '6px 14px', cursor: 'pointer',
|
||||
fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600,
|
||||
textTransform: 'uppercase', letterSpacing: '0.04em',
|
||||
borderRadius: 4,
|
||||
border: `1px solid ${on ? cAlpha(C_COLORS.teal, 0.40) : 'transparent'}`,
|
||||
background: on ? cAlpha(C_COLORS.teal, 0.10) : 'transparent',
|
||||
color: on ? C_COLORS.teal : 'var(--fg-disabled)',
|
||||
}}>
|
||||
{t}
|
||||
{on && <span style={{ marginLeft: 6, color: 'var(--fg-2)' }}>({count})</span>}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<CompSearchInput value={search} onChange={onSearchChange} placeholder="Search hostname…" width={220} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CompSearchInput({ value, onChange, placeholder, width = 240 }) {
|
||||
const [focus, setFocus] = useCompState(false);
|
||||
return (
|
||||
<input
|
||||
value={value} onChange={onChange} placeholder={placeholder}
|
||||
onFocus={() => setFocus(true)} onBlur={() => setFocus(false)}
|
||||
style={{
|
||||
background: 'rgba(15,23,42,0.85)',
|
||||
border: `1px solid ${focus ? cAlpha(C_COLORS.teal, 0.60) : cAlpha(C_COLORS.teal, 0.20)}`,
|
||||
borderRadius: 4, color: 'var(--fg-1)', outline: 'none',
|
||||
padding: '6px 12px', fontSize: 12, fontFamily: 'var(--font-mono)',
|
||||
width, transition: 'border-color 160ms ease',
|
||||
boxShadow: focus ? `0 0 0 3px ${cAlpha(C_COLORS.teal, 0.10)}` : 'none',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const DEVICE_GRID = '2.5fr 1.1fr 1fr 2fr 0.5fr 0.4fr';
|
||||
|
||||
function DeviceTableHeader() {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'grid', gridTemplateColumns: DEVICE_GRID,
|
||||
padding: '8px 16px',
|
||||
borderBottom: '1px solid rgba(255,255,255,0.05)',
|
||||
fontFamily: 'var(--font-mono)', fontSize: 10, fontWeight: 600,
|
||||
color: 'var(--fg-disabled)', textTransform: 'uppercase', letterSpacing: '0.08em',
|
||||
}}>
|
||||
<span>Hostname</span><span>IP Address</span><span>Type</span>
|
||||
<span>Failing Metrics</span><span>Seen</span><span></span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DeviceRow({ hostname, ip, type, failingMetrics, seenCount, hasNotes, selected, onClick }) {
|
||||
const [hover, setHover] = useCompState(false);
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
||||
style={{
|
||||
display: 'grid', gridTemplateColumns: DEVICE_GRID,
|
||||
padding: '10px 16px',
|
||||
borderBottom: '1px solid rgba(255,255,255,0.04)',
|
||||
cursor: 'pointer',
|
||||
background: selected ? cAlpha(C_COLORS.teal, 0.08) : (hover ? 'rgba(255,255,255,0.025)' : 'transparent'),
|
||||
borderLeft: selected ? `2px solid ${C_COLORS.teal}` : '2px solid transparent',
|
||||
transition: 'all 160ms ease', alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
fontFamily: 'var(--font-mono)', fontSize: 12,
|
||||
color: selected ? C_COLORS.teal : 'var(--fg-1)',
|
||||
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
||||
}}>{hostname}</div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-2)' }}>{ip || '—'}</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--fg-disabled)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{type || '—'}</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
|
||||
{failingMetrics.map(m => <MetricBadge key={m.metric_id} metricId={m.metric_id} category={m.category} />)}
|
||||
</div>
|
||||
<div><SeenBadge count={seenCount} /></div>
|
||||
<div style={{ display: 'flex', justifyContent: 'center' }}>
|
||||
{hasNotes && <CompIcon name="message" size={13} color={cAlpha(C_COLORS.teal, 0.7)} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── EmptyState — for table body when there's nothing to show. ── */
|
||||
function CompEmpty({ children }) {
|
||||
return (
|
||||
<div style={{
|
||||
padding: 48, textAlign: 'center',
|
||||
color: 'var(--fg-disabled)', fontFamily: 'var(--font-mono)', fontSize: 12,
|
||||
}}>{children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── ChartCard — wrapper around any of the 6 charts on the page. ── */
|
||||
function ChartCard({ title, subtitle, children, height = 240 }) {
|
||||
return (
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(15,23,42,0.98) 100%)',
|
||||
border: '1px solid rgba(20,184,166,0.15)',
|
||||
borderRadius: 8, padding: 16,
|
||||
}}>
|
||||
<div style={{
|
||||
fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600,
|
||||
color: 'var(--fg-1)', textTransform: 'uppercase', letterSpacing: '0.08em',
|
||||
marginBottom: subtitle ? 2 : 12,
|
||||
}}>{title}</div>
|
||||
{subtitle && (
|
||||
<div style={{
|
||||
fontFamily: 'var(--font-mono)', fontSize: 10,
|
||||
color: 'var(--fg-disabled)', marginBottom: 12,
|
||||
}}>{subtitle}</div>
|
||||
)}
|
||||
<div style={{ height }}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── ChartLegend — shared legend row used at the top of stacked charts. ── */
|
||||
function ChartLegend({ items }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', marginBottom: 8 }}>
|
||||
{items.map(it => (
|
||||
<span key={it.label} style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||||
fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--fg-2)',
|
||||
}}>
|
||||
<span style={{
|
||||
display: 'inline-block', width: 10, height: 10, borderRadius: 2,
|
||||
background: it.color,
|
||||
}} />
|
||||
{it.label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── DefinitionTooltip ───────────────────────────────────────────
|
||||
The hover popover that surfaces a metric's title + business
|
||||
justification + data sources. */
|
||||
function DefinitionTooltip({ title, justification, sources }) {
|
||||
return (
|
||||
<div style={{
|
||||
width: 300,
|
||||
background: 'linear-gradient(135deg, rgba(30,41,59,0.98) 0%, rgba(15,23,42,0.99) 100%)',
|
||||
border: `1px solid ${cAlpha(C_COLORS.teal, 0.30)}`,
|
||||
borderRadius: 8,
|
||||
boxShadow: '0 8px 32px rgba(0,0,0,0.6)',
|
||||
padding: '12px 14px',
|
||||
}}>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 13, fontWeight: 700, color: 'var(--fg-1)', marginBottom: 6, lineHeight: 1.3 }}>{title}</div>
|
||||
{justification && (
|
||||
<div style={{ fontFamily: 'var(--font-display)', fontSize: 12, color: 'var(--fg-2)', lineHeight: 1.4, marginBottom: 6 }}>{justification}</div>
|
||||
)}
|
||||
{sources && (
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--fg-disabled)' }}>Sources: {sources}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── RollbackDialog ──────────────────────────────────────────────
|
||||
Centered modal w/ red identity. "Reverses the most recent upload"
|
||||
message + danger confirm. */
|
||||
function RollbackDialog({ reportLabel, onCancel, onConfirm, loading }) {
|
||||
return (
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0, zIndex: 60,
|
||||
background: 'rgba(10,14,39,0.92)', backdropFilter: 'blur(8px)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16,
|
||||
}}>
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, rgba(30,41,59,0.98) 0%, rgba(15,23,42,0.99) 100%)',
|
||||
border: '1px solid rgba(239,68,68,0.30)', borderRadius: 12,
|
||||
boxShadow: '0 20px 60px rgba(0,0,0,0.7)',
|
||||
width: '100%', maxWidth: 420, padding: 28,
|
||||
}}>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 15, fontWeight: 700, color: C_COLORS.red, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: 12 }}>
|
||||
Rollback Upload
|
||||
</div>
|
||||
<div style={{ fontSize: 13, color: 'var(--fg-1)', lineHeight: 1.5, marginBottom: 8, fontFamily: 'var(--font-display)' }}>
|
||||
This will reverse the most recent upload:
|
||||
</div>
|
||||
<div style={{
|
||||
fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--fg-2)',
|
||||
background: 'rgba(15,23,42,0.6)', borderRadius: 6,
|
||||
padding: '10px 12px', marginBottom: 18,
|
||||
border: '1px solid rgba(239,68,68,0.15)',
|
||||
}}>
|
||||
<div><span style={{ color: 'var(--fg-disabled)' }}>File:</span> {reportLabel}</div>
|
||||
<div style={{ marginTop: 4, fontSize: 10, color: 'var(--fg-disabled)' }}>
|
||||
New items will be deleted, resolved items will be reactivated, and the upload record will be removed.
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 10 }}>
|
||||
<CompButton variant="ghost" onClick={onCancel} style={{ flex: 1, justifyContent: 'center' }}>Cancel</CompButton>
|
||||
<button onClick={onConfirm} disabled={loading} style={{
|
||||
flex: 2, padding: 10,
|
||||
background: loading ? 'rgba(239,68,68,0.05)' : 'rgba(239,68,68,0.10)',
|
||||
border: `1px solid ${C_COLORS.red}`, borderRadius: 6,
|
||||
color: C_COLORS.red, cursor: loading ? 'wait' : 'pointer',
|
||||
fontFamily: 'var(--font-mono)', fontSize: 12, fontWeight: 600,
|
||||
textTransform: 'uppercase', letterSpacing: '0.05em',
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 6,
|
||||
opacity: loading ? 0.6 : 1,
|
||||
}}>
|
||||
<CompIcon name="rotate" size={13} color="currentColor" />
|
||||
{loading ? 'Rolling back…' : 'Confirm Rollback'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── RollbackToast — bottom-right confirmation/error toast. ── */
|
||||
function RollbackToast({ tone = 'success', message, detail, onDismiss }) {
|
||||
const c = tone === 'error' ? C_COLORS.red : C_COLORS.green;
|
||||
return (
|
||||
<div onClick={onDismiss} style={{
|
||||
position: 'absolute', bottom: 24, right: 24, zIndex: 70,
|
||||
background: 'linear-gradient(135deg, rgba(30,41,59,0.98) 0%, rgba(15,23,42,0.99) 100%)',
|
||||
border: `1px solid ${cAlpha(c, 0.40)}`, borderRadius: 8,
|
||||
boxShadow: '0 8px 32px rgba(0,0,0,0.5)',
|
||||
padding: '14px 20px', maxWidth: 360,
|
||||
fontFamily: 'var(--font-mono)', fontSize: 12, color: c, cursor: 'pointer',
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: detail ? 4 : 0 }}>
|
||||
<CompIcon name={tone === 'error' ? 'alert' : 'rotate'} size={14} color="currentColor" />
|
||||
{message}
|
||||
</div>
|
||||
{detail && <div style={{ fontSize: 10, color: 'var(--fg-2)' }}>{detail}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── CompIcon — every icon used by the compliance page. ── */
|
||||
function CompIcon({ 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 '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 '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 'rotate': return <svg {...p}><path d="M3 12a9 9 0 1 0 9-9"/><polyline points="3 4 3 9 8 9"/></svg>;
|
||||
case 'message': return <svg {...p}><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>;
|
||||
case 'info': return <svg {...p}><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></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': return <svg {...p}><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>;
|
||||
case 'loader': return <svg {...p}><line x1="12" y1="2" x2="12" y2="6"/><line x1="12" y1="18" x2="12" y2="22"/><line x1="4.93" y1="4.93" x2="7.76" y2="7.76"/><line x1="16.24" y1="16.24" x2="19.07" y2="19.07"/><line x1="2" y1="12" x2="6" y2="12"/><line x1="18" y1="12" x2="22" y2="12"/><line x1="4.93" y1="19.07" x2="7.76" y2="16.24"/><line x1="16.24" y1="7.76" x2="19.07" y2="4.93"/></svg>;
|
||||
case 'x': return <svg {...p}><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>;
|
||||
default: return <svg {...p}><circle cx="12" cy="12" r="10"/></svg>;
|
||||
}
|
||||
}
|
||||
|
||||
window.COMP = {
|
||||
COLORS: C_COLORS, STATUS_COLOR, CATEGORY_COLORS,
|
||||
statusColor, pctDisplay, cAlpha,
|
||||
CompPageHeader, CompButton, CompIconButton, TeamTabs,
|
||||
VariantPill, StatusRibbon, MetricHealthCard, MetricBadge, SeenBadge,
|
||||
DeviceTable, DeviceTableToolbar, DeviceTableHeader, DeviceRow, CompSearchInput, CompEmpty,
|
||||
ChartCard, ChartLegend,
|
||||
DefinitionTooltip, RollbackDialog, RollbackToast,
|
||||
CompIcon,
|
||||
};
|
||||
@@ -0,0 +1,319 @@
|
||||
// CompliancePage.jsx — full-page assembly of the AEO Compliance view.
|
||||
// Rebuilt from frontend/src/components/pages/CompliancePage.js with
|
||||
// inline-rendered chart placeholders that match Recharts visually.
|
||||
|
||||
const {
|
||||
COLORS: PC, statusColor: pStatusColor, pctDisplay: pPct, cAlpha: pAlpha,
|
||||
CompPageHeader, CompButton, TeamTabs,
|
||||
MetricHealthCard, DeviceTable, DeviceTableToolbar, DeviceTableHeader, DeviceRow, CompEmpty,
|
||||
ChartCard, ChartLegend, RollbackDialog, RollbackToast, CompIcon: PIcon,
|
||||
} = window.COMP;
|
||||
|
||||
const { useState: useCompPageState } = React;
|
||||
|
||||
/* ── Sample data — what summary + items endpoints look like ── */
|
||||
const SAMPLE_FAMILIES = [
|
||||
{
|
||||
metricId: 'VM-CRITICAL', category: 'Vulnerability Management', target: 0.95, worstStatus: 'Below 15% of Target',
|
||||
entries: [
|
||||
{ metric_id: 'VM-CRITICAL', priority: 'P1', compliance_pct: 0.74, status: 'Below 15% of Target' },
|
||||
{ metric_id: 'VM-CRITICAL', priority: 'P2', compliance_pct: 0.91, status: 'Within 15% of Target' },
|
||||
],
|
||||
},
|
||||
{
|
||||
metricId: 'AUTH-MFA', category: 'Access & MFA', target: 0.98, worstStatus: 'Within 15% of Target',
|
||||
entries: [{ metric_id: 'AUTH-MFA', compliance_pct: 0.94, status: 'Within 15% of Target' }],
|
||||
},
|
||||
{
|
||||
metricId: 'LOG-COVERAGE', category: 'Logging & Monitoring', target: 0.90, worstStatus: 'Meets/Exceeds Target',
|
||||
entries: [{ metric_id: 'LOG-COVERAGE', compliance_pct: 0.97, status: 'Meets/Exceeds Target' }],
|
||||
},
|
||||
{
|
||||
metricId: 'EOL-OS', category: 'End-of-Life OS', target: 1.00, worstStatus: 'Below 15% of Target',
|
||||
entries: [{ metric_id: 'EOL-OS', compliance_pct: 0.62, status: 'Below 15% of Target' }],
|
||||
},
|
||||
{
|
||||
metricId: 'EDR-DEPLOY', category: 'Endpoint Protection', target: 0.95, worstStatus: 'Meets/Exceeds Target',
|
||||
entries: [{ metric_id: 'EDR-DEPLOY', compliance_pct: 0.96, status: 'Meets/Exceeds Target' }],
|
||||
},
|
||||
];
|
||||
|
||||
const SAMPLE_DEVICES = [
|
||||
{ hostname: 'app-prod-04.steam.internal', ip: '10.42.18.4', type: 'Linux server', failingMetrics: [{ metric_id: 'VM-CRITICAL', category: 'Vulnerability Management' }, { metric_id: 'EOL-OS', category: 'End-of-Life OS' }], seenCount: 5, hasNotes: true },
|
||||
{ hostname: 'db-staging-01.steam.internal', ip: '10.42.20.11', type: 'Linux server', failingMetrics: [{ metric_id: 'VM-CRITICAL', category: 'Vulnerability Management' }], seenCount: 2, hasNotes: false },
|
||||
{ hostname: 'fileshare-02.steam.internal', ip: '10.42.16.32', type: 'Windows server', failingMetrics: [{ metric_id: 'AUTH-MFA', category: 'Access & MFA' }], seenCount: 1, hasNotes: false },
|
||||
{ hostname: 'jumpbox-east.steam.internal', ip: '10.42.4.7', type: 'Linux server', failingMetrics: [{ metric_id: 'AUTH-MFA', category: 'Access & MFA' }, { metric_id: 'VM-CRITICAL', category: 'Vulnerability Management' }], seenCount: 4, hasNotes: true },
|
||||
{ hostname: 'legacy-billing.steam.internal', ip: '10.42.8.18', type: 'Windows server', failingMetrics: [{ metric_id: 'EOL-OS', category: 'End-of-Life OS' }], seenCount: 7, hasNotes: false },
|
||||
];
|
||||
|
||||
/* ── Inline chart visuals — semantic stand-ins for Recharts. ── */
|
||||
function NetworkScoreChart() {
|
||||
const points = [82, 84, 81, 86, 85, 87, 88];
|
||||
return (
|
||||
<ChartSvg>
|
||||
<Line points={points} color={PC.teal} fill={pAlpha(PC.teal, 0.15)} />
|
||||
<YAxisLabels labels={['100%', '80%', '60%']} />
|
||||
</ChartSvg>
|
||||
);
|
||||
}
|
||||
function StatusDistributionChart() {
|
||||
const data = [
|
||||
{ meets: 62, within: 22, below: 16 },
|
||||
{ meets: 65, within: 20, below: 15 },
|
||||
{ meets: 67, within: 21, below: 12 },
|
||||
{ meets: 72, within: 18, below: 10 },
|
||||
];
|
||||
return <StackedBars data={data} keys={['meets', 'within', 'below']} colors={[PC.green, PC.amber, PC.red]} />;
|
||||
}
|
||||
function TeamHealthChart() {
|
||||
return (
|
||||
<ChartSvg>
|
||||
<Line points={[78, 80, 79, 83, 85, 88]} color={PC.teal} />
|
||||
<Line points={[68, 70, 73, 71, 74, 76]} color={PC.amber} />
|
||||
</ChartSvg>
|
||||
);
|
||||
}
|
||||
function NewRecurringResolvedChart() {
|
||||
const data = [
|
||||
{ new_count: 12, recurring_count: 7, resolved_count: -10 },
|
||||
{ new_count: 8, recurring_count: 9, resolved_count: -14 },
|
||||
{ new_count: 14, recurring_count: 5, resolved_count: -8 },
|
||||
{ new_count: 9, recurring_count: 6, resolved_count: -12 },
|
||||
];
|
||||
return (
|
||||
<ChartSvg>
|
||||
<ChartLegend items={[
|
||||
{ label: 'New', color: PC.red },
|
||||
{ label: 'Recurring', color: PC.amber },
|
||||
{ label: 'Resolved', color: PC.green },
|
||||
]} />
|
||||
<StackedBars data={data} keys={['new_count', 'recurring_count', 'resolved_count']} colors={[PC.red, PC.amber, PC.green]} centered />
|
||||
</ChartSvg>
|
||||
);
|
||||
}
|
||||
function AvgDaysToResolveChart() {
|
||||
const rows = [
|
||||
{ label: 'AUTH-MFA', v: 4 },
|
||||
{ label: 'VM-CRITICAL', v: 12 },
|
||||
{ label: 'EOL-OS', v: 28 },
|
||||
{ label: 'EDR-DEPLOY', v: 6 },
|
||||
];
|
||||
return <HorizontalBars rows={rows} max={32} color={PC.teal} unit="days" />;
|
||||
}
|
||||
function PersistentFindingsChart() {
|
||||
const rows = [
|
||||
{ label: 'legacy-billing', v: 7 },
|
||||
{ label: 'app-prod-04', v: 5 },
|
||||
{ label: 'jumpbox-east', v: 4 },
|
||||
{ label: 'db-staging-01', v: 2 },
|
||||
];
|
||||
return <HorizontalBars rows={rows} max={8} color={PC.amber} unit="cycles" />;
|
||||
}
|
||||
|
||||
/* Tiny SVG primitives — flat, deterministic, no library. */
|
||||
function ChartSvg({ children, height = 180 }) {
|
||||
return (
|
||||
<div style={{ position: 'relative', width: '100%', height }}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function Line({ points, color, fill }) {
|
||||
const max = Math.max(...points);
|
||||
const min = Math.min(...points) * 0.85;
|
||||
const range = max - min || 1;
|
||||
const w = 100, h = 100;
|
||||
const step = w / (points.length - 1);
|
||||
const path = points.map((v, i) => `${i === 0 ? 'M' : 'L'} ${i * step} ${h - ((v - min) / range) * h}`).join(' ');
|
||||
const fillPath = path + ` L ${w} ${h} L 0 ${h} Z`;
|
||||
return (
|
||||
<svg width="100%" height="100%" viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none" style={{ overflow: 'visible' }}>
|
||||
{fill && <path d={fillPath} fill={fill} />}
|
||||
<path d={path} fill="none" stroke={color} strokeWidth="1.5" />
|
||||
{points.map((v, i) => (
|
||||
<circle key={i} cx={i * step} cy={h - ((v - min) / range) * h} r="1.5" fill={color} />
|
||||
))}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
function YAxisLabels({ labels }) {
|
||||
return (
|
||||
<div style={{
|
||||
position: 'absolute', top: 0, bottom: 0, left: -2,
|
||||
display: 'flex', flexDirection: 'column', justifyContent: 'space-between',
|
||||
fontFamily: 'var(--font-mono)', fontSize: 9, color: 'var(--fg-disabled)',
|
||||
pointerEvents: 'none',
|
||||
}}>
|
||||
{labels.map(l => <span key={l}>{l}</span>)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function StackedBars({ data, keys, colors, centered = false }) {
|
||||
const total = (d) => keys.reduce((s, k) => s + Math.abs(d[k]), 0);
|
||||
const maxTotal = Math.max(...data.map(total));
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: centered ? 'center' : 'flex-end', gap: 12, height: '100%', paddingTop: 8 }}>
|
||||
{data.map((d, i) => {
|
||||
const segs = keys.map((k, ki) => ({ v: d[k], color: colors[ki], k }));
|
||||
return (
|
||||
<div key={i} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', height: '100%' }}>
|
||||
<div style={{ flex: 1, width: '100%', display: 'flex', flexDirection: 'column', justifyContent: 'flex-end' }}>
|
||||
{segs.map((s, si) => (
|
||||
<div key={si} style={{
|
||||
width: '100%', height: `${(Math.abs(s.v) / maxTotal) * 100}%`,
|
||||
background: s.color, opacity: 0.85,
|
||||
borderTopLeftRadius: si === 0 ? 2 : 0,
|
||||
borderTopRightRadius: si === 0 ? 2 : 0,
|
||||
}} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function HorizontalBars({ rows, max, color, unit }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, paddingTop: 8 }}>
|
||||
{rows.map(r => (
|
||||
<div key={r.label} style={{ display: 'grid', gridTemplateColumns: '120px 1fr 50px', gap: 8, alignItems: 'center' }}>
|
||||
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-2)', textAlign: 'right' }}>{r.label}</span>
|
||||
<div style={{ height: 14, background: 'rgba(255,255,255,0.04)', borderRadius: 3, overflow: 'hidden' }}>
|
||||
<div style={{ width: `${(r.v / max) * 100}%`, height: '100%', background: color, opacity: 0.85, borderRadius: 3 }} />
|
||||
</div>
|
||||
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color, fontWeight: 600 }}>{r.v} {unit}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Page assembly ── */
|
||||
function CompliancePage() {
|
||||
const [team, setTeam] = useCompPageState('STEAM');
|
||||
const [tab, setTab] = useCompPageState('active');
|
||||
const [filter, setFilter] = useCompPageState(null);
|
||||
const [search, setSearch] = useCompPageState('');
|
||||
const [selected, setSelected] = useCompPageState(null);
|
||||
const [rollback, setRollback] = useCompPageState(null);
|
||||
|
||||
const filteredDevices = SAMPLE_DEVICES
|
||||
.filter(d => !filter || d.failingMetrics.some(m => filter.includes(m.metric_id)))
|
||||
.filter(d => !search || d.hostname.toLowerCase().includes(search.toLowerCase()));
|
||||
|
||||
return (
|
||||
<div data-screen-label="01 Compliance" style={{
|
||||
position: 'relative',
|
||||
padding: 32, minHeight: '100vh', background: 'var(--bg-page)',
|
||||
fontFamily: 'var(--font-display)',
|
||||
}}>
|
||||
<CompPageHeader
|
||||
lastReport="2026-04-21"
|
||||
networkScore="88%"
|
||||
verticalScore="84%"
|
||||
isAdmin
|
||||
onRollback={() => setRollback('confirm')}
|
||||
/>
|
||||
|
||||
<TeamTabs teams={['STEAM', 'ACCESS-ENG']} active={team} onChange={setTeam} />
|
||||
|
||||
{/* Metric Health */}
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<div style={{
|
||||
fontFamily: 'var(--font-mono)', fontSize: 10, fontWeight: 600,
|
||||
color: 'var(--fg-disabled)', textTransform: 'uppercase', letterSpacing: '0.1em',
|
||||
marginBottom: 10,
|
||||
}}>
|
||||
Metric Health — click to filter
|
||||
{filter && (
|
||||
<button onClick={() => setFilter(null)} style={{
|
||||
marginLeft: 12, color: PC.teal, background: 'none', border: 'none',
|
||||
cursor: 'pointer', fontSize: 10, fontFamily: 'var(--font-mono)',
|
||||
}}>× clear filter</button>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
|
||||
{SAMPLE_FAMILIES.map(family => {
|
||||
const ids = family.entries.map(e => e.metric_id);
|
||||
const isActive = filter !== null && filter.length === ids.length && ids.every(id => filter.includes(id));
|
||||
return (
|
||||
<div key={family.metricId} style={{ display: 'flex', flex: '1 1 0', minWidth: 160 }}>
|
||||
<MetricHealthCard
|
||||
family={family}
|
||||
active={isActive}
|
||||
onClick={() => setFilter(isActive ? null : ids)}
|
||||
onInfoClick={() => {}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Charts */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16, marginBottom: 24 }}>
|
||||
<ChartCard title="Network Compliance" subtitle="Trailing 7 days">
|
||||
<NetworkScoreChart />
|
||||
</ChartCard>
|
||||
<ChartCard title="Status Distribution" subtitle="Last 4 cycles">
|
||||
<StatusDistributionChart />
|
||||
</ChartCard>
|
||||
<ChartCard title="Team Health" subtitle="STEAM vs ACCESS-ENG">
|
||||
<TeamHealthChart />
|
||||
</ChartCard>
|
||||
<ChartCard title="New / Recurring / Resolved" subtitle="Per cycle" height={200}>
|
||||
<NewRecurringResolvedChart />
|
||||
</ChartCard>
|
||||
<ChartCard title="Avg Days to Resolve" subtitle="By metric">
|
||||
<AvgDaysToResolveChart />
|
||||
</ChartCard>
|
||||
<ChartCard title="Most Persistent Findings" subtitle="By cycles seen">
|
||||
<PersistentFindingsChart />
|
||||
</ChartCard>
|
||||
</div>
|
||||
|
||||
{/* Device table */}
|
||||
<DeviceTable>
|
||||
<DeviceTableToolbar
|
||||
tab={tab} onTabChange={setTab}
|
||||
count={filteredDevices.length}
|
||||
search={search} onSearchChange={e => setSearch(e.target.value)}
|
||||
/>
|
||||
<DeviceTableHeader />
|
||||
{filteredDevices.length === 0 ? (
|
||||
<CompEmpty>No non-compliant devices match the current filter</CompEmpty>
|
||||
) : (
|
||||
filteredDevices.map(d => (
|
||||
<DeviceRow
|
||||
key={d.hostname}
|
||||
hostname={d.hostname} ip={d.ip} type={d.type}
|
||||
failingMetrics={d.failingMetrics}
|
||||
seenCount={d.seenCount} hasNotes={d.hasNotes}
|
||||
selected={selected === d.hostname}
|
||||
onClick={() => setSelected(selected === d.hostname ? null : d.hostname)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</DeviceTable>
|
||||
|
||||
{rollback === 'confirm' && (
|
||||
<RollbackDialog
|
||||
reportLabel="2026-04-21"
|
||||
onCancel={() => setRollback(null)}
|
||||
onConfirm={() => setRollback('toast')}
|
||||
/>
|
||||
)}
|
||||
{rollback === 'toast' && (
|
||||
<RollbackToast
|
||||
tone="success"
|
||||
message="Upload rolled back"
|
||||
detail="42 items deleted, 18 reactivated"
|
||||
onDismiss={() => setRollback(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
window.COMP_PAGE = { CompliancePage };
|
||||
363
docs/design-system-redesign/ui_kits/compliance/KitDocs.jsx
Normal file
363
docs/design-system-redesign/ui_kits/compliance/KitDocs.jsx
Normal file
@@ -0,0 +1,363 @@
|
||||
// KitDocs.jsx — browseable docs page for the Compliance kit.
|
||||
|
||||
const { useState: useDocsCompState } = React;
|
||||
const {
|
||||
COLORS: DCC, statusColor: dStatus, pctDisplay: dPct, cAlpha: dA,
|
||||
CompPageHeader: DHeader, CompButton: DBtn, TeamTabs: DTabs,
|
||||
VariantPill: DVPill, StatusRibbon: DRibbon, MetricHealthCard: DMHC,
|
||||
MetricBadge: DMB, SeenBadge: DSB,
|
||||
DeviceTable: DDT, DeviceTableToolbar: DDTT, DeviceTableHeader: DDTH, DeviceRow: DDR,
|
||||
CompEmpty: DEmpty, ChartCard: DChart, ChartLegend: DLegend,
|
||||
DefinitionTooltip: DTip, RollbackDialog: DRoll, RollbackToast: DToast,
|
||||
CompIcon: DIcon,
|
||||
} = window.COMP;
|
||||
const { CompliancePage: DPage } = window.COMP_PAGE;
|
||||
|
||||
function CSection({ 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: DCC.teal, 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 CCode({ children }) {
|
||||
return (
|
||||
<code style={{
|
||||
display: 'inline-block', padding: '2px 6px', borderRadius: 3,
|
||||
background: dA(DCC.teal, 0.10), border: `1px solid ${dA(DCC.teal, 0.18)}`,
|
||||
fontFamily: 'var(--font-mono)', fontSize: 11, color: DCC.tealMid,
|
||||
}}>{children}</code>
|
||||
);
|
||||
}
|
||||
|
||||
function CSwatch({ 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>
|
||||
<CCode>{value}</CCode>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CSpec({ 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 CSpecimen({ children, padding = 24 }) {
|
||||
return (
|
||||
<div style={{
|
||||
padding,
|
||||
background: 'rgba(15,23,42,0.5)',
|
||||
border: '1px solid rgba(255,255,255,0.05)', borderRadius: 8,
|
||||
}}>{children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
const TABS = [
|
||||
{ id: 'overview', label: 'Overview' },
|
||||
{ id: 'tokens', label: 'Tokens' },
|
||||
{ id: 'components', label: 'Components' },
|
||||
{ id: 'assemblies', label: 'Assemblies' },
|
||||
{ id: 'reference', label: 'Reference Page' },
|
||||
];
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
const SAMPLE_FAMILY_BAD = {
|
||||
metricId: 'VM-CRITICAL', category: 'Vulnerability Management', target: 0.95, worstStatus: 'Below 15% of Target',
|
||||
entries: [
|
||||
{ metric_id: 'VM-CRITICAL', priority: 'P1', compliance_pct: 0.74, status: 'Below 15% of Target' },
|
||||
{ metric_id: 'VM-CRITICAL', priority: 'P2', compliance_pct: 0.91, status: 'Within 15% of Target' },
|
||||
],
|
||||
};
|
||||
const SAMPLE_FAMILY_OK = {
|
||||
metricId: 'EDR-DEPLOY', category: 'Endpoint Protection', target: 0.95, worstStatus: 'Meets/Exceeds Target',
|
||||
entries: [{ metric_id: 'EDR-DEPLOY', compliance_pct: 0.96, status: 'Meets/Exceeds Target' }],
|
||||
};
|
||||
|
||||
function CKitDocs() {
|
||||
const [active, setActive] = useDocsCompState('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 style={{ padding: '40px 48px 0 48px', maxWidth: 1280, margin: '0 auto' }}>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 700, color: DCC.teal, 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: DCC.teal, textTransform: 'uppercase', letterSpacing: '0.08em',
|
||||
textShadow: `0 0 24px ${dA(DCC.teal, 0.30)}`,
|
||||
}}>Compliance</h1>
|
||||
<p style={{
|
||||
margin: '12px 0 0 0', maxWidth: 720, fontSize: 15, lineHeight: 1.6,
|
||||
color: 'var(--fg-muted)', fontFamily: 'var(--font-display)',
|
||||
}}>
|
||||
The AEO Compliance view: per-team metric health, six trend charts, and a non-compliant device
|
||||
drilldown. Identity color is teal — distinct from the green-titled CVE pages — with status colors
|
||||
that map green/amber/red onto target adherence.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<nav style={{
|
||||
position: 'sticky', top: 0, zIndex: 10, marginTop: 28,
|
||||
background: 'rgba(2,6,23,0.85)', backdropFilter: 'blur(8px)',
|
||||
borderBottom: `1px solid ${dA(DCC.teal, 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 ? DCC.teal : 'transparent'}`,
|
||||
color: on ? DCC.teal : '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>
|
||||
|
||||
<main style={{ maxWidth: 1280, margin: '0 auto', padding: '0 48px 96px 48px' }}>
|
||||
|
||||
<CSection id="overview" eyebrow="01 — Overview" title="Why this kit exists" blurb="Compliance has its own visual identity inside the suite — teal page title, status colors driven by target adherence, and a metric-card pattern that does double duty as a filter. This kit captures the vocabulary so other audit-style views can reuse it.">
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
|
||||
<CSpecimen>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: DCC.teal, 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)' }}>
|
||||
Teal owns the page header, the active team tab, the upload CTA, the active device row,
|
||||
and any "neutral compliance signal" surface. Status colors (green/amber/red) own
|
||||
everything that represents target adherence — never decorative.
|
||||
</p>
|
||||
</CSpecimen>
|
||||
<CSpecimen>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: DCC.teal, 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)' }}>
|
||||
Page header → team tabs → metric health row (one card per metric family) →
|
||||
3×2 chart grid → device table with active/resolved tabs and hostname search.
|
||||
Selecting a metric card filters the table; selecting a row opens a detail panel.
|
||||
</p>
|
||||
</CSpecimen>
|
||||
</div>
|
||||
</CSection>
|
||||
|
||||
<CSection id="tokens" eyebrow="02 — Tokens" title="Status, category, and identity color" blurb="Status colors are reserved for target adherence. Category colors tag failing-metric badges by program area so a host's failure mix is scannable.">
|
||||
<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 }}>Status (target adherence)</div>
|
||||
<CSwatch name="green" value={DCC.green} role="Meets/Exceeds Target · success" />
|
||||
<CSwatch name="amber" value={DCC.amber} role="Within 15% of Target · attention" />
|
||||
<CSwatch name="red" value={DCC.red} role="Below 15% of Target · critical" />
|
||||
<div style={{ marginTop: 24, fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-disabled)', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: 8, fontWeight: 600 }}>Identity</div>
|
||||
<CSwatch name="teal" value={DCC.teal} role="Page title · CTA · selected row" />
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-disabled)', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: 8, fontWeight: 600 }}>Category</div>
|
||||
<CSwatch name="red" value={DCC.red} role="Vulnerability Management" />
|
||||
<CSwatch name="amber" value={DCC.amber} role="Access & MFA" />
|
||||
<CSwatch name="purple" value={DCC.purple} role="Logging & Monitoring" />
|
||||
<CSwatch name="orange" value={DCC.orange} role="End-of-Life OS · Endpoint Protection" />
|
||||
<CSwatch name="sky" value={DCC.sky} role="Application Security" />
|
||||
<CSwatch name="slate" value={DCC.slate} role="Asset Data Quality · Decommissioned" />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginTop: 32 }}>
|
||||
<CSpec label="Card chrome">background <CCode>linear-gradient(135deg, rgba(30,41,59,.95), rgba(15,23,42,.98))</CCode></CSpec>
|
||||
<CSpec label="Metric card border">resting <CCode>1.5px solid {`{statusColor}`} @ 0.25</CCode> · hover <CCode>0.50</CCode> · active <CCode>1.0</CCode> + 15% bg fill</CSpec>
|
||||
<CSpec label="Title type"><CCode>var(--font-mono)</CCode> · 24 / 700 · uppercase · 0.1em tracking · 16px text-shadow glow</CSpec>
|
||||
<CSpec label="Worst-status logic">A family's <CCode>worstStatus</CCode> is the lowest-severity entry across all variants — drives card border + ribbon</CSpec>
|
||||
</div>
|
||||
</CSection>
|
||||
|
||||
<CSection id="components" eyebrow="03 — Components" title="The pieces" blurb="Every primitive used by the page assembly. All are exported on window.COMP.">
|
||||
<h3 style={subhead}>CompPageHeader</h3>
|
||||
<p style={subblurb}>Teal title with glow, last-report meta + optional rollback button, network/vertical scores, and a refresh + upload CTA on the right.</p>
|
||||
<CSpecimen>
|
||||
<DHeader lastReport="2026-04-21" networkScore="88%" verticalScore="84%" isAdmin onRollback={() => {}} />
|
||||
</CSpecimen>
|
||||
|
||||
<h3 style={subhead}>TeamTabs</h3>
|
||||
<p style={subblurb}>Two-team toggle pinned above the metric strip. The active tab fills with teal at 18% alpha.</p>
|
||||
<CSpecimen>
|
||||
<DTabs teams={['STEAM', 'ACCESS-ENG']} active="STEAM" onChange={() => {}} />
|
||||
</CSpecimen>
|
||||
|
||||
<h3 style={subhead}>CompButton</h3>
|
||||
<p style={subblurb}>Four variants. Primary is the lone teal CTA (Upload Report). Danger fronts the rollback flow.</p>
|
||||
<CSpecimen>
|
||||
<div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
|
||||
<DBtn variant="primary" icon="upload">Upload Report</DBtn>
|
||||
<DBtn variant="neutral" icon="refresh">Refresh</DBtn>
|
||||
<DBtn variant="danger" icon="rotate">Rollback</DBtn>
|
||||
<DBtn variant="ghost">Cancel</DBtn>
|
||||
</div>
|
||||
</CSpecimen>
|
||||
|
||||
<h3 style={subhead}>MetricHealthCard</h3>
|
||||
<p style={subblurb}>The big clickable card in the metric strip. Border + ID color follow the family's <em>worst</em> status, so a single bad variant turns the whole family red. Click filters the device table; the info "i" opens a definition panel.</p>
|
||||
<CSpecimen>
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
<div style={{ flex: 1 }}><DMHC family={SAMPLE_FAMILY_BAD} active={false} onClick={() => {}} onInfoClick={() => {}} /></div>
|
||||
<div style={{ flex: 1 }}><DMHC family={SAMPLE_FAMILY_BAD} active={true} onClick={() => {}} onInfoClick={() => {}} /></div>
|
||||
<div style={{ flex: 1 }}><DMHC family={SAMPLE_FAMILY_OK} active={false} onClick={() => {}} onInfoClick={() => {}} /></div>
|
||||
</div>
|
||||
</CSpecimen>
|
||||
|
||||
<h3 style={subhead}>VariantPill · StatusRibbon</h3>
|
||||
<p style={subblurb}>Atoms inside MetricHealthCard. VariantPill = one priority's % readout. StatusRibbon = the bottom lozenge.</p>
|
||||
<CSpecimen>
|
||||
<div style={{ display: 'flex', gap: 8, marginBottom: 12, flexWrap: 'wrap' }}>
|
||||
<DVPill status="Meets/Exceeds Target" pct={0.97} />
|
||||
<DVPill status="Within 15% of Target" pct={0.91} label="P2" />
|
||||
<DVPill status="Below 15% of Target" pct={0.74} label="P1" />
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||
<DRibbon status="Meets/Exceeds Target" />
|
||||
<DRibbon status="Within 15% of Target" />
|
||||
<DRibbon status="Below 15% of Target" />
|
||||
</div>
|
||||
</CSpecimen>
|
||||
|
||||
<h3 style={subhead}>MetricBadge · SeenBadge</h3>
|
||||
<p style={subblurb}>Row-level chips in the device table. MetricBadge tints by category; SeenBadge escalates slate→amber→red as repeat-failure count grows.</p>
|
||||
<CSpecimen>
|
||||
<div style={{ display: 'flex', gap: 8, marginBottom: 12, flexWrap: 'wrap' }}>
|
||||
<DMB metricId="VM-CRITICAL" category="Vulnerability Management" />
|
||||
<DMB metricId="AUTH-MFA" category="Access & MFA" />
|
||||
<DMB metricId="LOG-COVERAGE" category="Logging & Monitoring" />
|
||||
<DMB metricId="EOL-OS" category="End-of-Life OS" />
|
||||
<DMB metricId="EDR-DEPLOY" category="Endpoint Protection" />
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<DSB count={1} /><DSB count={3} /><DSB count={5} /><DSB count={7} />
|
||||
</div>
|
||||
</CSpecimen>
|
||||
|
||||
<h3 style={subhead}>DeviceRow</h3>
|
||||
<p style={subblurb}>One non-compliant host per row. Selected state shifts the left border + hostname color to teal.</p>
|
||||
<CSpecimen padding={0}>
|
||||
<DDT>
|
||||
<DDTH />
|
||||
<DDR hostname="app-prod-04.steam.internal" ip="10.42.18.4" type="Linux server" failingMetrics={[{ metric_id: 'VM-CRITICAL', category: 'Vulnerability Management' }, { metric_id: 'EOL-OS', category: 'End-of-Life OS' }]} seenCount={5} hasNotes={true} selected={true} onClick={() => {}} />
|
||||
<DDR hostname="db-staging-01.steam.internal" ip="10.42.20.11" type="Linux server" failingMetrics={[{ metric_id: 'VM-CRITICAL', category: 'Vulnerability Management' }]} seenCount={2} hasNotes={false} onClick={() => {}} />
|
||||
</DDT>
|
||||
</CSpecimen>
|
||||
|
||||
<h3 style={subhead}>ChartCard</h3>
|
||||
<p style={subblurb}>Wrapper for any of the six trend charts. Title in mono uppercase, optional subtitle in disabled grey, 240px chart well by default.</p>
|
||||
<CSpecimen>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
|
||||
<DChart title="Network Compliance" subtitle="Trailing 7 days" height={120}>
|
||||
<div style={{ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--fg-disabled)', fontFamily: 'var(--font-mono)', fontSize: 11 }}>chart well</div>
|
||||
</DChart>
|
||||
<DChart title="Status Distribution" subtitle="Last 4 cycles" height={120}>
|
||||
<DLegend items={[{ label: 'Meets', color: DCC.green }, { label: 'Within 15%', color: DCC.amber }, { label: 'Below 15%', color: DCC.red }]} />
|
||||
</DChart>
|
||||
</div>
|
||||
</CSpecimen>
|
||||
|
||||
<h3 style={subhead}>DefinitionTooltip</h3>
|
||||
<p style={subblurb}>Hover popover used to surface a metric's title, business justification, and data sources.</p>
|
||||
<CSpecimen>
|
||||
<DTip title="VM-CRITICAL — Critical Vulnerabilities Patched" justification="Track the percentage of critical CVEs patched within the SLA window. Below-target performance creates exploitable risk on production assets." sources="Tenable, Atlas, JIRA" />
|
||||
</CSpecimen>
|
||||
</CSection>
|
||||
|
||||
<CSection id="assemblies" eyebrow="04 — Assemblies" title="How the parts compose">
|
||||
<h3 style={subhead}>Metric health row</h3>
|
||||
<p style={subblurb}>One MetricHealthCard per family, flexed evenly. Click a card to filter the device table to only its IDs; an "× clear filter" button appears in the section label when active.</p>
|
||||
<CSpecimen>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, fontWeight: 600, color: 'var(--fg-disabled)', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: 10 }}>
|
||||
Metric Health — click to filter
|
||||
<span style={{ marginLeft: 12, color: DCC.teal }}>× clear filter</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 10 }}>
|
||||
<div style={{ flex: 1 }}><DMHC family={SAMPLE_FAMILY_BAD} active={true} onClick={() => {}} onInfoClick={() => {}} /></div>
|
||||
<div style={{ flex: 1 }}><DMHC family={SAMPLE_FAMILY_OK} active={false} onClick={() => {}} onInfoClick={() => {}} /></div>
|
||||
</div>
|
||||
</CSpecimen>
|
||||
|
||||
<h3 style={subhead}>Device table</h3>
|
||||
<p style={subblurb}>Toolbar (active/resolved tabs + hostname search) → header row → DeviceRows. Empty/loading/error states are centered messages inside the same chrome.</p>
|
||||
<CSpecimen padding={0}>
|
||||
<DDT>
|
||||
<DDTT tab="active" onTabChange={() => {}} count={3} search="" onSearchChange={() => {}} />
|
||||
<DDTH />
|
||||
<DDR hostname="app-prod-04.steam.internal" ip="10.42.18.4" type="Linux server" failingMetrics={[{ metric_id: 'VM-CRITICAL', category: 'Vulnerability Management' }, { metric_id: 'EOL-OS', category: 'End-of-Life OS' }]} seenCount={5} hasNotes={true} onClick={() => {}} />
|
||||
<DDR hostname="jumpbox-east.steam.internal" ip="10.42.4.7" type="Linux server" failingMetrics={[{ metric_id: 'AUTH-MFA', category: 'Access & MFA' }]} seenCount={4} hasNotes={true} onClick={() => {}} />
|
||||
<DDR hostname="legacy-billing.steam.internal" ip="10.42.8.18" type="Windows server" failingMetrics={[{ metric_id: 'EOL-OS', category: 'End-of-Life OS' }]} seenCount={7} hasNotes={false} onClick={() => {}} />
|
||||
</DDT>
|
||||
</CSpecimen>
|
||||
</CSection>
|
||||
|
||||
<CSection id="reference" eyebrow="05 — Reference" title="Full Compliance page" blurb="Every primitive composed exactly as CompliancePage.js renders. The frame below is scrollable.">
|
||||
<div className="sample-frame" style={{
|
||||
border: `1px solid ${dA(DCC.teal, 0.20)}`, borderRadius: 12,
|
||||
overflow: 'hidden', maxHeight: 900, overflowY: 'auto',
|
||||
background: 'var(--bg-page)',
|
||||
}}>
|
||||
<DPage />
|
||||
</div>
|
||||
</CSection>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
window.COMP_DOCS = { CKitDocs };
|
||||
36
docs/design-system-redesign/ui_kits/compliance/README.md
Normal file
36
docs/design-system-redesign/ui_kits/compliance/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Compliance UI Kit
|
||||
|
||||
Visual vocabulary for the AEO Compliance view (`CompliancePage.js`).
|
||||
|
||||
## Files
|
||||
- `index.html` — entry point.
|
||||
- `CompPrimitives.jsx` — `CompPageHeader`, `CompButton`, `TeamTabs`, `MetricHealthCard`, `VariantPill`, `StatusRibbon`, `MetricBadge`, `SeenBadge`, `DeviceTable`/`DeviceRow`, `ChartCard`, `DefinitionTooltip`, `RollbackDialog`, `RollbackToast`, `CompIcon`.
|
||||
- `CompliancePage.jsx` — full-page assembly.
|
||||
- `KitDocs.jsx` — Overview · Tokens · Components · Assemblies · Reference.
|
||||
|
||||
## Identity
|
||||
| Surface | Color | Hex |
|
||||
|----------------------|--------|-----------|
|
||||
| Page title + glow | teal | `#14B8A6` |
|
||||
| Active team tab | teal | `#14B8A6` |
|
||||
| Upload Report CTA | teal | `#14B8A6` |
|
||||
| Selected device row | teal | `#14B8A6` |
|
||||
|
||||
## Status colors (target adherence)
|
||||
| Status | Color | Hex |
|
||||
|-------------------------|--------|-----------|
|
||||
| Meets/Exceeds Target | green | `#10B981` |
|
||||
| Within 15% of Target | amber | `#F59E0B` |
|
||||
| Below 15% of Target | red | `#EF4444` |
|
||||
|
||||
## Category colors (badge tinting)
|
||||
red · Vulnerability Management — amber · Access & MFA — purple · Logging & Monitoring — orange · End-of-Life OS / Endpoint Protection — sky · Application Security — slate · Asset Data Quality / Decommissioned
|
||||
|
||||
## Layout
|
||||
Page header → team tabs → metric health row → 3×2 chart grid → device table.
|
||||
|
||||
## Page-level rules
|
||||
1. Status colors are reserved for target adherence; never decorative.
|
||||
2. A family's `worstStatus` (lowest-severity variant) drives card border + ribbon — one bad variant turns the whole family red.
|
||||
3. Clicking a metric card filters the device table to its IDs; an "× clear filter" button is the only escape hatch shown inline in the section label.
|
||||
4. SeenBadge escalates slate (1×) → amber (2–3×) → red (4×+).
|
||||
30
docs/design-system-redesign/ui_kits/compliance/index.html
Normal file
30
docs/design-system-redesign/ui_kits/compliance/index.html
Normal file
@@ -0,0 +1,30 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>STEAM Security · Compliance 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(20,184,166,0.25); 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="CompPrimitives.jsx"></script>
|
||||
<script type="text/babel" src="CompliancePage.jsx"></script>
|
||||
<script type="text/babel" src="KitDocs.jsx"></script>
|
||||
<script type="text/babel">
|
||||
const { CKitDocs } = window.COMP_DOCS;
|
||||
function App() { return <main data-screen-label="Compliance Kit"><CKitDocs /></main>; }
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
151
docs/design-system-redesign/ui_kits/cve-dashboard/AppShell.jsx
Normal file
151
docs/design-system-redesign/ui_kits/cve-dashboard/AppShell.jsx
Normal file
@@ -0,0 +1,151 @@
|
||||
// AppShell.jsx — top bar, nav drawer, user menu for the STEAM Security Dashboard.
|
||||
const { useState: useState_AS } = React;
|
||||
const { Icon: I_AS, GroupBadge: GB_AS } = window.SDS;
|
||||
|
||||
function TopBar({ user, currentPage, onNav, onMenuClick }) {
|
||||
return (
|
||||
<header style={{
|
||||
height: 56, position: 'sticky', top: 0, zIndex: 50,
|
||||
background: 'var(--bg-surface)', borderBottom: '1px solid var(--border-1)',
|
||||
display: 'flex', alignItems: 'center', padding: '0 20px', gap: 16,
|
||||
}}>
|
||||
<button onClick={onMenuClick} style={{
|
||||
background: 'transparent', border: 'none', color: 'var(--fg-2)',
|
||||
cursor: 'pointer', padding: 6, display: 'flex', alignItems: 'center',
|
||||
}}><I_AS.Menu size={20} /></button>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<I_AS.Shield size={22} style={{ color: 'var(--accent)' }} />
|
||||
<div>
|
||||
<div style={{ font: '700 15px var(--font-ui)', color: 'var(--fg-1)', letterSpacing: '0.02em', lineHeight: 1 }}>STEAM</div>
|
||||
<div style={{ font: '500 9px var(--font-ui)', color: 'var(--fg-muted)', letterSpacing: '0.18em', marginTop: 2 }}>SECURITY</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav style={{ display: 'flex', gap: 2, marginLeft: 24 }}>
|
||||
{['Home', 'Reporting', 'Compliance', 'Knowledge Base', 'Exports'].map(p => (
|
||||
<NavTab key={p} label={p} active={currentPage === p} onClick={() => onNav(p)} />
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div style={{ flex: 1 }} />
|
||||
|
||||
<UserMenu user={user} />
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
function NavTab({ label, active, onClick }) {
|
||||
const [hover, setHover] = useState_AS(false);
|
||||
return (
|
||||
<button onClick={onClick}
|
||||
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
||||
style={{
|
||||
background: active ? 'var(--accent-soft)' : (hover ? 'var(--bg-elevated)' : 'transparent'),
|
||||
color: active ? 'var(--accent)' : 'var(--fg-2)',
|
||||
border: 'none', borderRadius: 6,
|
||||
padding: '8px 12px', fontFamily: 'var(--font-ui)', fontWeight: active ? 600 : 500, fontSize: 13,
|
||||
cursor: 'pointer', transition: 'background 150ms, color 150ms',
|
||||
}}>{label}</button>
|
||||
);
|
||||
}
|
||||
|
||||
function UserMenu({ user }) {
|
||||
const [open, setOpen] = useState_AS(false);
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<button onClick={() => setOpen(!open)} style={{
|
||||
background: 'transparent', border: '1px solid var(--border-1)', borderRadius: 6,
|
||||
padding: '6px 10px', display: 'flex', alignItems: 'center', gap: 10,
|
||||
color: 'var(--fg-1)', cursor: 'pointer', fontFamily: 'var(--font-ui)', fontSize: 13,
|
||||
}}>
|
||||
<div style={{
|
||||
width: 26, height: 26, borderRadius: '50%', background: 'var(--accent-soft)',
|
||||
color: 'var(--accent)', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontFamily: 'var(--font-ui)', fontWeight: 700, fontSize: 11,
|
||||
}}>{user.name.split(' ').map(p => p[0]).join('').slice(0, 2)}</div>
|
||||
<span>{user.name}</span>
|
||||
<I_AS.ChevronD size={14} />
|
||||
</button>
|
||||
{open && (
|
||||
<div style={{
|
||||
position: 'absolute', right: 0, top: '110%', minWidth: 240,
|
||||
background: 'var(--bg-surface)', border: '1px solid var(--border-1)',
|
||||
borderRadius: 8, boxShadow: 'var(--shadow-popover)', padding: 8, zIndex: 60,
|
||||
}}>
|
||||
<div style={{ padding: '8px 10px', borderBottom: '1px solid var(--border-1)', marginBottom: 6 }}>
|
||||
<div style={{ font: '600 13px var(--font-ui)', color: 'var(--fg-1)' }}>{user.name}</div>
|
||||
<div style={{ font: '400 11px var(--font-mono)', color: 'var(--fg-muted)', marginTop: 2 }}>{user.email}</div>
|
||||
<div style={{ marginTop: 8 }}><GB_AS group={user.group} /></div>
|
||||
</div>
|
||||
{['Manage Users', 'Audit Log', 'Settings', 'Sign Out'].map((it, i) => (
|
||||
<MenuItem key={it} label={it} danger={i === 3} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MenuItem({ label, danger }) {
|
||||
const [hover, setHover] = useState_AS(false);
|
||||
return <button
|
||||
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
||||
style={{
|
||||
width: '100%', textAlign: 'left',
|
||||
background: hover ? 'var(--bg-elevated)' : 'transparent',
|
||||
color: danger ? 'var(--sev-critical)' : 'var(--fg-2)',
|
||||
border: 'none', borderRadius: 4, padding: '7px 10px',
|
||||
fontFamily: 'var(--font-ui)', fontSize: 13, cursor: 'pointer',
|
||||
}}>{label}</button>;
|
||||
}
|
||||
|
||||
function NavDrawer({ open, onClose, currentPage, onNav, isAdmin }) {
|
||||
if (!open) return null;
|
||||
const items = [
|
||||
{ label: 'Home', icon: I_AS.Activity },
|
||||
{ label: 'Reporting', icon: I_AS.FileText },
|
||||
{ label: 'Compliance', icon: I_AS.Shield },
|
||||
{ label: 'Knowledge Base', icon: I_AS.Folder },
|
||||
{ label: 'Exports', icon: I_AS.Download },
|
||||
...(isAdmin ? [{ label: 'Admin Panel', icon: I_AS.Users }] : []),
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<div onClick={onClose} style={{
|
||||
position: 'fixed', inset: 0, background: 'var(--bg-overlay)',
|
||||
backdropFilter: 'blur(4px)', zIndex: 60,
|
||||
}} />
|
||||
<aside style={{
|
||||
position: 'fixed', left: 0, top: 0, bottom: 0, width: 240, zIndex: 61,
|
||||
background: 'var(--bg-surface)', borderRight: '1px solid var(--border-1)',
|
||||
padding: 16, display: 'flex', flexDirection: 'column', gap: 4,
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8, padding: '4px 6px' }}>
|
||||
<span style={{ font: '600 11px var(--font-ui)', color: 'var(--fg-muted)', textTransform: 'uppercase', letterSpacing: '0.06em' }}>Navigation</span>
|
||||
<button onClick={onClose} style={{ background: 'transparent', border: 'none', color: 'var(--fg-muted)', cursor: 'pointer', display: 'flex' }}><I_AS.X size={16} /></button>
|
||||
</div>
|
||||
{items.map(it => (
|
||||
<DrawerItem key={it.label} {...it} active={currentPage === it.label}
|
||||
onClick={() => { onNav(it.label); onClose(); }} />
|
||||
))}
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerItem({ label, icon: IcCmp, active, onClick }) {
|
||||
const [hover, setHover] = useState_AS(false);
|
||||
return <button onClick={onClick}
|
||||
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 10,
|
||||
background: active ? 'var(--accent-soft)' : (hover ? 'var(--bg-elevated)' : 'transparent'),
|
||||
color: active ? 'var(--accent)' : 'var(--fg-2)',
|
||||
border: 'none', borderRadius: 6, padding: '9px 10px',
|
||||
fontFamily: 'var(--font-ui)', fontWeight: active ? 600 : 500, fontSize: 13,
|
||||
cursor: 'pointer', textAlign: 'left',
|
||||
}}><IcCmp size={16} />{label}</button>;
|
||||
}
|
||||
|
||||
window.SDS_Shell = { TopBar, NavDrawer };
|
||||
@@ -0,0 +1,351 @@
|
||||
// KnowledgeBase.jsx — recreation of the Knowledge Base page.
|
||||
const { useState: useState_KB, useMemo: useMemo_KB } = React;
|
||||
const { Button: Btn_KB, Card: Card_KB, Field: F_KB, Input: In_KB, Select: Sel_KB,
|
||||
EmptyState: ES_KB, Icon: I_KB } = window.SDS;
|
||||
|
||||
const KB_ARTICLES = [
|
||||
{ id: 1, title: 'NVD CVE Triage Runbook', category: 'Runbooks',
|
||||
description: 'Standard procedure for triaging incoming NVD-sourced CVEs across vendor pairs.',
|
||||
type: 'pdf', size: '412 KB', date: '2026-04-22', author: 'jramos', exts: ['pdf'] },
|
||||
{ id: 2, title: 'FP Workflow Submission Guide', category: 'Runbooks',
|
||||
description: 'How to compile evidence and submit False Positive workflows through the Ivanti Queue.',
|
||||
type: 'md', size: '24 KB', date: '2026-04-18', author: 'mhall' },
|
||||
{ id: 3, title: 'Cisco IOS-XE Advisory · cisco-sa-2024-0341', category: 'Vendor Advisories',
|
||||
description: 'Vendor advisory for Cisco IOS-XE Web UI privilege escalation. Linked to 12 host findings.',
|
||||
type: 'pdf', size: '1.3 MB', date: '2026-04-15', author: 'jramos' },
|
||||
{ id: 4, title: 'AEO Compliance Schema Reference', category: 'Policies',
|
||||
description: 'Authoritative metric ID list for the NTS_AEO weekly report. Used by the drift checker.',
|
||||
type: 'md', size: '38 KB', date: '2026-04-09', author: 'kpatel' },
|
||||
{ id: 5, title: 'Archer Risk Acceptance Process', category: 'Policies',
|
||||
description: 'EXC ticket lifecycle, required documentation, and standard SLAs for risk acceptance.',
|
||||
type: 'docx', size: '186 KB', date: '2026-04-02', author: 'mhall' },
|
||||
{ id: 6, title: 'Q2 Vulnerability Posture Briefing', category: 'Reports',
|
||||
description: 'Leadership briefing on Critical/High remediation throughput for FY26-Q2.',
|
||||
type: 'pptx', size: '4.7 MB', date: '2026-03-30', author: 'jramos' },
|
||||
{ id: 7, title: 'Ivanti / RiskSense API Integration Notes', category: 'Internal Docs',
|
||||
description: 'Authentication, BU filters, severity range tuning, and rate-limit notes.',
|
||||
type: 'md', size: '11 KB', date: '2026-03-21', author: 'kpatel' },
|
||||
{ id: 8, title: 'CVSS Severity Cascade Rules', category: 'Internal Docs',
|
||||
description: 'How v3.1 → v3.0 → v2.0 fallback is applied when scoring CVEs from NVD.',
|
||||
type: 'md', size: '6 KB', date: '2026-03-14', author: 'mhall' },
|
||||
];
|
||||
|
||||
const CATEGORIES = ['All', 'Runbooks', 'Vendor Advisories', 'Policies', 'Reports', 'Internal Docs'];
|
||||
|
||||
const TYPE_COLORS = {
|
||||
pdf: { c: '#EF4444', label: 'PDF' },
|
||||
md: { c: '#38BDF8', label: 'MD' },
|
||||
docx: { c: '#7DD3FC', label: 'DOCX' },
|
||||
pptx: { c: '#F59E0B', label: 'PPTX' },
|
||||
xlsx: { c: '#10B981', label: 'XLSX' },
|
||||
};
|
||||
|
||||
function FileTypeChip({ type }) {
|
||||
const v = TYPE_COLORS[type] || { c: 'var(--fg-muted)', label: type.toUpperCase() };
|
||||
return <span style={{
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||||
width: 36, height: 36, borderRadius: 6,
|
||||
background: 'var(--bg-elevated)', border: `1px solid ${v.c}`,
|
||||
color: v.c, fontFamily: 'var(--font-mono)', fontWeight: 700, fontSize: 10,
|
||||
flexShrink: 0,
|
||||
}}>{v.label}</span>;
|
||||
}
|
||||
|
||||
function ArticleRow({ article, onOpen, onDownload }) {
|
||||
const [hover, setHover] = useState_KB(false);
|
||||
return (
|
||||
<div onClick={onOpen}
|
||||
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 14,
|
||||
padding: '14px 18px',
|
||||
background: hover
|
||||
? 'linear-gradient(90deg, rgba(14,165,233,0.10) 0%, rgba(14,165,233,0.04) 100%)'
|
||||
: 'transparent',
|
||||
borderBottom: '1px solid var(--border-subtle)',
|
||||
boxShadow: hover ? 'inset 3px 0 0 var(--intel-accent)' : 'none',
|
||||
cursor: 'pointer', transition: 'all 200ms cubic-bezier(0.4,0,0.2,1)',
|
||||
}}>
|
||||
<FileTypeChip type={article.type} />
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ font: '600 14px var(--font-ui)', color: 'var(--fg-1)', marginBottom: 3,
|
||||
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{article.title}</div>
|
||||
<div style={{ font: '400 12px var(--font-ui)', color: 'var(--fg-muted)',
|
||||
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{article.description}</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 4, minWidth: 110 }}>
|
||||
<span style={{
|
||||
padding: '2px 8px', borderRadius: 4, background: 'var(--bg-elevated)',
|
||||
color: 'var(--fg-2)', font: '500 11px var(--font-ui)',
|
||||
}}>{article.category}</span>
|
||||
<span style={{ font: '400 11px var(--font-mono)', color: 'var(--fg-muted)' }}>{article.date}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 4, minWidth: 90 }}>
|
||||
<span style={{ font: '400 11px var(--font-mono)', color: 'var(--fg-muted)' }}>{article.size}</span>
|
||||
<span style={{ font: '400 11px var(--font-mono)', color: 'var(--fg-disabled)' }}>{article.author}</span>
|
||||
</div>
|
||||
<button onClick={(e) => { e.stopPropagation(); onDownload(article); }} style={{
|
||||
background: 'transparent', border: '1px solid var(--border-1)', borderRadius: 6,
|
||||
padding: 7, cursor: 'pointer', color: 'var(--fg-2)', display: 'flex',
|
||||
}} title="Download"><I_KB.Download size={14} /></button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CategoryPill({ label, active, count, onClick }) {
|
||||
const [hover, setHover] = useState_KB(false);
|
||||
return <button onClick={onClick}
|
||||
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
||||
style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 8,
|
||||
padding: '6px 14px', borderRadius: 6,
|
||||
background: active
|
||||
? 'linear-gradient(135deg, rgba(14,165,233,0.20) 0%, rgba(14,165,233,0.12) 100%)'
|
||||
: (hover ? 'rgba(14,165,233,0.08)' : 'transparent'),
|
||||
color: active ? 'var(--intel-accent-bright)' : 'var(--fg-2)',
|
||||
border: `1px solid ${active ? 'var(--intel-accent)' : 'var(--border-default)'}`,
|
||||
font: `700 11px var(--font-mono)`, textTransform: 'uppercase', letterSpacing: '0.5px',
|
||||
textShadow: active ? '0 0 8px rgba(14,165,233,0.4)' : 'none',
|
||||
boxShadow: active ? '0 0 16px rgba(14,165,233,0.20)' : 'none',
|
||||
cursor: 'pointer', transition: 'all 200ms cubic-bezier(0.4,0,0.2,1)',
|
||||
}}>{label}<span style={{
|
||||
font: '700 10px var(--font-mono)', color: active ? 'var(--intel-accent-bright)' : 'var(--fg-muted)',
|
||||
padding: '1px 6px', borderRadius: 999,
|
||||
background: active ? 'rgba(14,165,233,0.15)' : 'rgba(148,163,184,0.10)',
|
||||
}}>{count}</span></button>;
|
||||
}
|
||||
|
||||
function KnowledgeBaseViewer({ article, onClose }) {
|
||||
return (
|
||||
<>
|
||||
<div onClick={onClose} style={{
|
||||
position: 'fixed', inset: 0, background: 'var(--bg-overlay)',
|
||||
backdropFilter: 'blur(4px)', zIndex: 100,
|
||||
}} />
|
||||
<div style={{
|
||||
position: 'fixed', right: 0, top: 0, bottom: 0, width: 'min(640px, 100vw)',
|
||||
background: 'var(--bg-surface)', borderLeft: '1px solid var(--border-1)',
|
||||
boxShadow: 'var(--shadow-modal)', zIndex: 101,
|
||||
display: 'flex', flexDirection: 'column', animation: 'slideIn 240ms cubic-bezier(0.16,1,0.3,1)',
|
||||
}}>
|
||||
<header style={{
|
||||
padding: '20px 24px', borderBottom: '1px solid var(--border-1)',
|
||||
display: 'flex', alignItems: 'flex-start', gap: 14,
|
||||
}}>
|
||||
<FileTypeChip type={article.type} />
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ font: '600 16px var(--font-ui)', color: 'var(--fg-1)' }}>{article.title}</div>
|
||||
<div style={{ font: '400 12px var(--font-mono)', color: 'var(--fg-muted)', marginTop: 4 }}>
|
||||
{article.category} · {article.size} · {article.date} · {article.author}
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={onClose} style={{
|
||||
background: 'transparent', border: 'none', color: 'var(--fg-muted)',
|
||||
cursor: 'pointer', padding: 6, display: 'flex',
|
||||
}}><I_KB.X size={18} /></button>
|
||||
</header>
|
||||
|
||||
<div style={{ flex: 1, overflowY: 'auto', padding: '24px' }}>
|
||||
<div style={{ font: '500 11px var(--font-ui)', color: 'var(--fg-muted)',
|
||||
textTransform: 'uppercase', letterSpacing: '0.06em', marginBottom: 8 }}>Description</div>
|
||||
<div style={{ font: '400 14px/1.6 var(--font-ui)', color: 'var(--fg-2)', marginBottom: 24 }}>
|
||||
{article.description}
|
||||
</div>
|
||||
|
||||
<div style={{ font: '500 11px var(--font-ui)', color: 'var(--fg-muted)',
|
||||
textTransform: 'uppercase', letterSpacing: '0.06em', marginBottom: 8 }}>Preview</div>
|
||||
<div style={{
|
||||
background: 'var(--bg-page)', border: '1px solid var(--border-1)',
|
||||
borderRadius: 8, padding: 24, minHeight: 320,
|
||||
font: '400 13px/1.7 var(--font-ui)', color: 'var(--fg-2)',
|
||||
}}>
|
||||
<h3 style={{ font: '600 18px var(--font-ui)', color: 'var(--fg-1)', margin: '0 0 12px' }}>
|
||||
{article.title}
|
||||
</h3>
|
||||
<p style={{ margin: '0 0 12px' }}>
|
||||
This document is rendered inline in a sandboxed iframe (PDF) or as sanitised HTML
|
||||
from the <code style={{ font: '500 12px var(--font-mono)', color: 'var(--accent)',
|
||||
background: 'var(--bg-elevated)', padding: '1px 6px', borderRadius: 3 }}>
|
||||
react-markdown</code> + <code style={{ font: '500 12px var(--font-mono)', color: 'var(--accent)',
|
||||
background: 'var(--bg-elevated)', padding: '1px 6px', borderRadius: 3 }}>rehype-sanitize</code> pipeline.
|
||||
</p>
|
||||
<p style={{ margin: '0 0 12px' }}>
|
||||
Authenticated users in any group may view; only Admin and Standard_User may upload or delete.
|
||||
</p>
|
||||
<ul style={{ margin: '0 0 12px 18px', padding: 0 }}>
|
||||
<li>Allowed types: PDF, MD, TXT, Office, HTML, JSON, YAML, images</li>
|
||||
<li>10 MB per-file limit · file extension allowlist</li>
|
||||
<li>Standard_User can delete only articles they created</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer style={{
|
||||
padding: '16px 24px', borderTop: '1px solid var(--border-1)',
|
||||
display: 'flex', gap: 8, justifyContent: 'flex-end',
|
||||
}}>
|
||||
<Btn_KB variant="ghost" icon={<I_KB.External size={14} />}>Open in tab</Btn_KB>
|
||||
<Btn_KB variant="primary" icon={<I_KB.Download size={14} />}>Download</Btn_KB>
|
||||
</footer>
|
||||
</div>
|
||||
<style>{`@keyframes slideIn{from{transform:translateX(100%)}to{transform:translateX(0)}}`}</style>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function UploadModal({ onClose }) {
|
||||
const [drag, setDrag] = useState_KB(false);
|
||||
return (
|
||||
<>
|
||||
<div onClick={onClose} style={{
|
||||
position: 'fixed', inset: 0, background: 'var(--bg-overlay)',
|
||||
backdropFilter: 'blur(4px)', zIndex: 100,
|
||||
}} />
|
||||
<div style={{
|
||||
position: 'fixed', left: '50%', top: '50%', transform: 'translate(-50%, -50%)',
|
||||
width: 'min(560px, 92vw)', background: 'var(--bg-surface)',
|
||||
border: '1px solid var(--border-1)', borderRadius: 12,
|
||||
boxShadow: 'var(--shadow-modal)', zIndex: 101,
|
||||
}}>
|
||||
<header style={{
|
||||
padding: '20px 24px', borderBottom: '1px solid var(--border-1)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
}}>
|
||||
<div style={{ font: '600 16px var(--font-ui)', color: 'var(--fg-1)' }}>Upload Article</div>
|
||||
<button onClick={onClose} style={{
|
||||
background: 'transparent', border: 'none', color: 'var(--fg-muted)',
|
||||
cursor: 'pointer', padding: 6, display: 'flex',
|
||||
}}><I_KB.X size={18} /></button>
|
||||
</header>
|
||||
|
||||
<div style={{ padding: 24, display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
<F_KB label="Title"><In_KB placeholder="e.g. Cisco IOS-XE Advisory · cisco-sa-2024-0341" /></F_KB>
|
||||
<F_KB label="Category">
|
||||
<Sel_KB defaultValue="Runbooks">
|
||||
{CATEGORIES.filter(c => c !== 'All').map(c => <option key={c}>{c}</option>)}
|
||||
</Sel_KB>
|
||||
</F_KB>
|
||||
<F_KB label="Description">
|
||||
<textarea placeholder="Short description for the library list…" rows={3} style={{
|
||||
background: 'var(--bg-input)', color: 'var(--fg-1)',
|
||||
border: '1px solid var(--border-1)', borderRadius: 6, padding: '8px 10px',
|
||||
fontFamily: 'var(--font-ui)', fontSize: 13, outline: 'none', resize: 'vertical',
|
||||
}} />
|
||||
</F_KB>
|
||||
|
||||
<div
|
||||
onDragOver={(e) => { e.preventDefault(); setDrag(true); }}
|
||||
onDragLeave={() => setDrag(false)}
|
||||
onDrop={(e) => { e.preventDefault(); setDrag(false); }}
|
||||
style={{
|
||||
border: `2px dashed ${drag ? 'var(--accent)' : 'var(--border-2)'}`,
|
||||
borderRadius: 8, padding: 24, textAlign: 'center',
|
||||
background: drag ? 'var(--accent-soft)' : 'var(--bg-page)',
|
||||
transition: 'all 150ms',
|
||||
}}>
|
||||
<div style={{ color: drag ? 'var(--accent)' : 'var(--fg-muted)', display: 'flex', justifyContent: 'center', marginBottom: 8 }}>
|
||||
<I_KB.Upload size={28} />
|
||||
</div>
|
||||
<div style={{ font: '600 13px var(--font-ui)', color: 'var(--fg-1)' }}>Drop file or click to browse</div>
|
||||
<div style={{ font: '400 11px var(--font-mono)', color: 'var(--fg-muted)', marginTop: 4 }}>
|
||||
PDF · MD · DOCX · XLSX · PPTX · TXT — max 10 MB
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer style={{
|
||||
padding: '16px 24px', borderTop: '1px solid var(--border-1)',
|
||||
display: 'flex', gap: 8, justifyContent: 'flex-end',
|
||||
}}>
|
||||
<Btn_KB variant="ghost" onClick={onClose}>Cancel</Btn_KB>
|
||||
<Btn_KB variant="primary" icon={<I_KB.Upload size={14} />}>Upload</Btn_KB>
|
||||
</footer>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function KnowledgeBasePage() {
|
||||
const [search, setSearch] = useState_KB('');
|
||||
const [category, setCategory] = useState_KB('All');
|
||||
const [active, setActive] = useState_KB(null);
|
||||
const [uploading, setUploading] = useState_KB(false);
|
||||
|
||||
const counts = useMemo_KB(() => {
|
||||
const c = { All: KB_ARTICLES.length };
|
||||
KB_ARTICLES.forEach(a => { c[a.category] = (c[a.category] || 0) + 1; });
|
||||
return c;
|
||||
}, []);
|
||||
|
||||
const filtered = useMemo_KB(() => KB_ARTICLES.filter(a => {
|
||||
if (category !== 'All' && a.category !== category) return false;
|
||||
if (search) {
|
||||
const q = search.toLowerCase();
|
||||
return a.title.toLowerCase().includes(q) || a.description.toLowerCase().includes(q);
|
||||
}
|
||||
return true;
|
||||
}), [search, category]);
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px 24px 48px', maxWidth: 1280, margin: '0 auto' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 20 }}>
|
||||
<div>
|
||||
<h1 style={{
|
||||
font: '700 24px var(--font-mono)', color: 'var(--intel-accent-bright)',
|
||||
margin: 0, textTransform: 'uppercase', letterSpacing: '0.10em',
|
||||
textShadow: 'var(--glow-heading)',
|
||||
}}>Knowledge Base</h1>
|
||||
<div style={{ font: '400 13px var(--font-ui)', color: 'var(--fg-muted)', marginTop: 6 }}>
|
||||
Internal reference material — runbooks, advisories, policies
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<Btn_KB variant="ghost" icon={<I_KB.Download size={14} />}>Export List</Btn_KB>
|
||||
<Btn_KB variant="primary" icon={<I_KB.FilePlus size={14} />} onClick={() => setUploading(true)}>Upload Article</Btn_KB>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 12, marginBottom: 16 }}>
|
||||
<div style={{ flex: 1, maxWidth: 420 }}>
|
||||
<In_KB icon={<I_KB.Search size={14} />}
|
||||
placeholder="Search title or description…"
|
||||
value={search} onChange={(e) => setSearch(e.target.value)} />
|
||||
</div>
|
||||
<Sel_KB defaultValue="newest" style={{ minWidth: 160 }}>
|
||||
<option value="newest">Newest first</option>
|
||||
<option value="oldest">Oldest first</option>
|
||||
<option value="title">Title A–Z</option>
|
||||
</Sel_KB>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 16 }}>
|
||||
{CATEGORIES.map(c => (
|
||||
<CategoryPill key={c} label={c} count={counts[c] || 0}
|
||||
active={category === c} onClick={() => setCategory(c)} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Card_KB padding={0}>
|
||||
{filtered.length === 0 ? (
|
||||
<ES_KB icon={<I_KB.FileText size={32} />}
|
||||
title="No articles match"
|
||||
message="Try clearing the search box or selecting a different category." />
|
||||
) : filtered.map((a, i) => (
|
||||
<ArticleRow key={a.id} article={a}
|
||||
onOpen={() => setActive(a)}
|
||||
onDownload={(art) => console.log('download', art.title)} />
|
||||
))}
|
||||
</Card_KB>
|
||||
|
||||
<div style={{ marginTop: 12, font: '400 12px var(--font-mono)', color: 'var(--fg-muted)' }}>
|
||||
{filtered.length} article{filtered.length === 1 ? '' : 's'}
|
||||
{category !== 'All' && <> · filtered to <span style={{ color: 'var(--accent)' }}>{category}</span></>}
|
||||
</div>
|
||||
|
||||
{active && <KnowledgeBaseViewer article={active} onClose={() => setActive(null)} />}
|
||||
{uploading && <UploadModal onClose={() => setUploading(false)} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
window.SDS_KB = { KnowledgeBasePage };
|
||||
250
docs/design-system-redesign/ui_kits/cve-dashboard/Primitives.jsx
Normal file
250
docs/design-system-redesign/ui_kits/cve-dashboard/Primitives.jsx
Normal file
@@ -0,0 +1,250 @@
|
||||
// Primitives.jsx — shared low-level UI for the STEAM Security Dashboard kit.
|
||||
// Plain inline styles + token CSS variables. No external libs.
|
||||
|
||||
const { useState } = React;
|
||||
|
||||
/* ── Buttons ─────────────────────────────────────────────────── */
|
||||
function Button({ variant = 'secondary', size = 'md', icon, children, onClick, disabled, style, ...rest }) {
|
||||
const [hover, setHover] = useState(false);
|
||||
const sizing = size === 'sm'
|
||||
? { padding: '6px 12px', fontSize: 11 }
|
||||
: { padding: '10px 18px', fontSize: 13 };
|
||||
const variants = {
|
||||
primary: {
|
||||
bgRest: 'linear-gradient(135deg, rgba(14,165,233,0.15) 0%, rgba(14,165,233,0.10) 100%)',
|
||||
bgHover: 'linear-gradient(135deg, rgba(14,165,233,0.25) 0%, rgba(14,165,233,0.20) 100%)',
|
||||
bd: '#0EA5E9', fg: '#38BDF8',
|
||||
glow: '0 0 20px rgba(14,165,233,0.25)', tshadow: '0 0 6px rgba(14,165,233,0.2)',
|
||||
},
|
||||
success: {
|
||||
bgRest: 'linear-gradient(135deg, rgba(16,185,129,0.15) 0%, rgba(16,185,129,0.10) 100%)',
|
||||
bgHover: 'linear-gradient(135deg, rgba(16,185,129,0.25) 0%, rgba(16,185,129,0.20) 100%)',
|
||||
bd: '#10B981', fg: '#34D399',
|
||||
glow: '0 0 20px rgba(16,185,129,0.25)', tshadow: '0 0 6px rgba(16,185,129,0.2)',
|
||||
},
|
||||
danger: {
|
||||
bgRest: 'linear-gradient(135deg, rgba(239,68,68,0.15) 0%, rgba(239,68,68,0.10) 100%)',
|
||||
bgHover: 'linear-gradient(135deg, rgba(239,68,68,0.25) 0%, rgba(239,68,68,0.20) 100%)',
|
||||
bd: '#EF4444', fg: '#F87171',
|
||||
glow: '0 0 20px rgba(239,68,68,0.25)', tshadow: '0 0 6px rgba(239,68,68,0.2)',
|
||||
},
|
||||
secondary: {
|
||||
bgRest: 'transparent', bgHover: 'rgba(14,165,233,0.08)',
|
||||
bd: 'rgba(14,165,233,0.30)', fg: 'var(--fg-2)',
|
||||
glow: 'none', tshadow: 'none',
|
||||
},
|
||||
ghost: {
|
||||
bgRest: 'transparent', bgHover: 'rgba(14,165,233,0.06)',
|
||||
bd: 'transparent', fg: 'var(--fg-3)',
|
||||
glow: 'none', tshadow: 'none',
|
||||
},
|
||||
};
|
||||
const v = variants[variant] || variants.secondary;
|
||||
return (
|
||||
<button onClick={onClick} disabled={disabled}
|
||||
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
||||
style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||||
background: disabled ? 'var(--bg-elevated)' : (hover ? v.bgHover : v.bgRest),
|
||||
color: disabled ? 'var(--fg-disabled)' : v.fg,
|
||||
border: `1px solid ${disabled ? 'var(--border-1)' : v.bd}`,
|
||||
borderRadius: 6,
|
||||
fontFamily: 'var(--font-mono)', fontWeight: 600,
|
||||
textTransform: 'uppercase', letterSpacing: '0.5px',
|
||||
textShadow: v.tshadow,
|
||||
boxShadow: hover && !disabled
|
||||
? `${v.glow}, 0 4px 12px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.10)`
|
||||
: '0 2px 6px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.10)',
|
||||
transform: hover && !disabled ? 'translateY(-1px)' : 'translateY(0)',
|
||||
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||
transition: 'all 300ms cubic-bezier(0.4,0,0.2,1)',
|
||||
...sizing, ...style,
|
||||
}} {...rest}>
|
||||
{icon}{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Severity badge — gradient + pulse-glow dot ──────────────── */
|
||||
if (typeof document !== 'undefined' && !document.getElementById('sds-pulse-glow')) {
|
||||
const s = document.createElement('style');
|
||||
s.id = 'sds-pulse-glow';
|
||||
s.textContent = '@keyframes sds-pulse-glow{0%,100%{opacity:1}50%{opacity:0.7}}';
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
function SeverityBadge({ level, score }) {
|
||||
const map = {
|
||||
Critical: { c: '#EF4444', text: '#FCA5A5', glow: '0 0 12px rgba(239,68,68,0.6), 0 0 6px rgba(239,68,68,0.4)' },
|
||||
High: { c: '#F59E0B', text: '#FCD34D', glow: '0 0 12px rgba(245,158,11,0.6), 0 0 6px rgba(245,158,11,0.4)' },
|
||||
Medium: { c: '#0EA5E9', text: '#7DD3FC', glow: '0 0 12px rgba(14,165,233,0.6), 0 0 6px rgba(14,165,233,0.4)' },
|
||||
Low: { c: '#10B981', text: '#6EE7B7', glow: '0 0 12px rgba(16,185,129,0.6), 0 0 6px rgba(16,185,129,0.4)' },
|
||||
};
|
||||
const v = map[level] || map.Medium;
|
||||
return (
|
||||
<span style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 8,
|
||||
padding: '6px 14px', borderRadius: 6,
|
||||
background: `linear-gradient(135deg, ${v.c}33 0%, ${v.c}26 100%)`,
|
||||
color: v.text, border: `2px solid ${v.c}99`,
|
||||
fontFamily: 'var(--font-mono)', fontWeight: 700, fontSize: 11,
|
||||
letterSpacing: '0.5px', textTransform: 'uppercase',
|
||||
textShadow: `0 0 8px ${v.c}66`,
|
||||
boxShadow: '0 4px 8px rgba(0,0,0,0.4)',
|
||||
}}>
|
||||
<span style={{
|
||||
width: 8, height: 8, borderRadius: '50%', background: v.c,
|
||||
boxShadow: v.glow, animation: 'sds-pulse-glow 2s ease-in-out infinite',
|
||||
}} />
|
||||
{level.toUpperCase()}{score && <span style={{ marginLeft: 4 }}>{score}</span>}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── SLA pill ────────────────────────────────────────────────── */
|
||||
function SlaPill({ status }) {
|
||||
const map = {
|
||||
OVERDUE: { c: 'var(--sev-critical)', bg: 'var(--sev-critical-bg)' },
|
||||
AT_RISK: { c: 'var(--sev-high)', bg: 'var(--sev-high-bg)' },
|
||||
WITHIN_SLA: { c: 'var(--sev-low)', bg: 'var(--sev-low-bg)' },
|
||||
};
|
||||
const v = map[status] || map.WITHIN_SLA;
|
||||
return <span style={{
|
||||
padding: '2px 9px', borderRadius: 999, background: v.bg, color: v.c,
|
||||
fontFamily: 'var(--font-mono)', fontWeight: 700, fontSize: 10, letterSpacing: '.05em',
|
||||
}}>{status}</span>;
|
||||
}
|
||||
|
||||
/* ── Group badge ─────────────────────────────────────────────── */
|
||||
function GroupBadge({ group }) {
|
||||
const map = {
|
||||
Admin: { c: 'var(--group-admin)', bg: 'rgba(239,68,68,0.10)' },
|
||||
Standard_User: { c: 'var(--group-standard)', bg: 'rgba(56,189,248,0.10)' },
|
||||
Leadership: { c: 'var(--group-leadership)', bg: 'rgba(245,158,11,0.10)' },
|
||||
Read_Only: { c: 'var(--group-readonly)', bg: 'rgba(148,163,184,0.10)' },
|
||||
};
|
||||
const v = map[group] || map.Read_Only;
|
||||
const label = group.replace('_', ' ');
|
||||
return <span style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||||
padding: '3px 9px', borderRadius: 999,
|
||||
color: v.c, background: v.bg, border: `1px solid ${v.c}`,
|
||||
fontFamily: 'var(--font-ui)', fontWeight: 600, fontSize: 11,
|
||||
}}>
|
||||
<span style={{ width: 6, height: 6, borderRadius: '50%', background: v.c }} />
|
||||
{label}
|
||||
</span>;
|
||||
}
|
||||
|
||||
/* ── Field / Input ───────────────────────────────────────────── */
|
||||
function Field({ label, children, style }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, ...style }}>
|
||||
{label && <label style={{
|
||||
fontFamily: 'var(--font-ui)', fontWeight: 500, fontSize: 11,
|
||||
color: 'var(--fg-muted)', textTransform: 'uppercase', letterSpacing: '0.06em',
|
||||
}}>{label}</label>}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Input({ icon, ...rest }) {
|
||||
const [focus, setFocus] = useState(false);
|
||||
return (
|
||||
<div style={{ position: 'relative', display: 'flex', alignItems: 'center' }}>
|
||||
{icon && <span style={{ position: 'absolute', left: 10, color: 'var(--fg-muted)', display: 'flex' }}>{icon}</span>}
|
||||
<input
|
||||
onFocus={() => setFocus(true)} onBlur={() => setFocus(false)}
|
||||
style={{
|
||||
width: '100%', boxSizing: 'border-box',
|
||||
background: 'var(--bg-input)', color: 'var(--fg-1)',
|
||||
border: `1px solid ${focus ? 'var(--border-focus)' : 'var(--border-1)'}`,
|
||||
boxShadow: focus ? 'var(--shadow-focus)' : 'none',
|
||||
borderRadius: 6, padding: icon ? '8px 10px 8px 32px' : '8px 10px',
|
||||
fontFamily: 'var(--font-ui)', fontSize: 13, outline: 'none',
|
||||
transition: 'border 150ms',
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Select({ children, ...rest }) {
|
||||
const [focus, setFocus] = useState(false);
|
||||
return (
|
||||
<select
|
||||
onFocus={() => setFocus(true)} onBlur={() => setFocus(false)}
|
||||
style={{
|
||||
background: 'var(--bg-input)', color: 'var(--fg-1)',
|
||||
border: `1px solid ${focus ? 'var(--border-focus)' : 'var(--border-1)'}`,
|
||||
boxShadow: focus ? 'var(--shadow-focus)' : 'none',
|
||||
borderRadius: 6, padding: '8px 10px',
|
||||
fontFamily: 'var(--font-ui)', fontSize: 13, outline: 'none',
|
||||
appearance: 'none',
|
||||
}}
|
||||
{...rest}>
|
||||
{children}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Card ────────────────────────────────────────────────────── */
|
||||
function Card({ children, style, padding = 20 }) {
|
||||
return <div style={{
|
||||
background: 'linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(51,65,85,0.9) 50%, rgba(30,41,59,0.95) 100%)',
|
||||
border: '1.5px solid rgba(14,165,233,0.30)',
|
||||
borderRadius: 8, padding,
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.4), 0 2px 6px rgba(0,0,0,0.3), inset 0 1px 0 rgba(14,165,233,0.10)',
|
||||
...style,
|
||||
}}>{children}</div>;
|
||||
}
|
||||
|
||||
/* ── Empty state ─────────────────────────────────────────────── */
|
||||
function EmptyState({ icon, title, message, action }) {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center',
|
||||
gap: 12, padding: '48px 24px', color: 'var(--fg-muted)', textAlign: 'center',
|
||||
}}>
|
||||
{icon && <div style={{ color: 'var(--fg-disabled)' }}>{icon}</div>}
|
||||
<div style={{ fontFamily: 'var(--font-ui)', fontWeight: 600, fontSize: 16, color: 'var(--fg-2)' }}>{title}</div>
|
||||
{message && <div style={{ fontFamily: 'var(--font-ui)', fontSize: 13, maxWidth: 360 }}>{message}</div>}
|
||||
{action}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Lucide icons (inline SVG, currentColor) ─────────────────── */
|
||||
const ic = (path) => ({ size = 16, strokeWidth = 1.75, ...rest }) => (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" strokeWidth={strokeWidth} strokeLinecap="round" strokeLinejoin="round"
|
||||
style={{ flexShrink: 0 }} {...rest}>{path}</svg>
|
||||
);
|
||||
const Icon = {
|
||||
Shield: ic(<><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></>),
|
||||
Search: ic(<><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></>),
|
||||
Filter: ic(<><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></>),
|
||||
Sync: ic(<><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"/><path d="M16 16h5v5"/></>),
|
||||
Download: ic(<><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"/></>),
|
||||
Upload: ic(<><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"/></>),
|
||||
File: ic(<><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"/></>),
|
||||
FileText: ic(<><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"/><line x1="9" y1="13" x2="15" y2="13"/><line x1="9" y1="17" x2="13" y2="17"/></>),
|
||||
FilePlus: ic(<><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"/><line x1="12" y1="12" x2="12" y2="18"/><line x1="9" y1="15" x2="15" y2="15"/></>),
|
||||
Folder: ic(<><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></>),
|
||||
Eye: ic(<><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></>),
|
||||
X: ic(<><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></>),
|
||||
ChevronD: ic(<><polyline points="6 9 12 15 18 9"/></>),
|
||||
ChevronR: ic(<><polyline points="9 18 15 12 9 6"/></>),
|
||||
Plus: ic(<><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></>),
|
||||
Menu: ic(<><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="18" x2="21" y2="18"/></>),
|
||||
Trash: ic(<><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"/></>),
|
||||
External: ic(<><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></>),
|
||||
Calendar: ic(<><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"/></>),
|
||||
Activity: ic(<><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></>),
|
||||
Users: ic(<><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></>),
|
||||
Scroll: ic(<><path d="M8 21h12a2 2 0 0 0 2-2v-2H10v2a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v3h4"/><path d="M19 17V5a2 2 0 0 0-2-2H4"/></>),
|
||||
Bell: ic(<><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></>),
|
||||
};
|
||||
|
||||
window.SDS = { Button, SeverityBadge, SlaPill, GroupBadge, Field, Input, Select, Card, EmptyState, Icon };
|
||||
30
docs/design-system-redesign/ui_kits/cve-dashboard/README.md
Normal file
30
docs/design-system-redesign/ui_kits/cve-dashboard/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# CVE Dashboard UI Kit
|
||||
|
||||
High-fidelity recreation of the **STEAM Security Dashboard** chrome plus a focused build of the **Knowledge Base** page.
|
||||
|
||||
## Files
|
||||
|
||||
| File | What |
|
||||
|---|---|
|
||||
| `index.html` | Mounts the kit. Opens to the Knowledge Base page; top-bar nav switches between pages. |
|
||||
| `Primitives.jsx` | `Button`, `Field`, `Input`, `Select`, `Card`, `SeverityBadge`, `SlaPill`, `GroupBadge`, `EmptyState`, `Icon` (lucide line icons inlined as SVG). |
|
||||
| `AppShell.jsx` | Top bar (brand mark + nav + UserMenu), NavDrawer overlay. |
|
||||
| `KnowledgeBase.jsx` | Knowledge Base page · article rows · category filter · upload modal · slide-out viewer. |
|
||||
|
||||
## How to use
|
||||
|
||||
1. Open `index.html` in a browser.
|
||||
2. The header nav lets you switch pages — `Knowledge Base` is fully built; the other tabs render a placeholder.
|
||||
3. Click any article row to open the **viewer panel** (slide-out from the right).
|
||||
4. Click **Upload Article** to open the upload modal.
|
||||
|
||||
## What is NOT built
|
||||
|
||||
This kit intentionally cuts the scope to one page (the Knowledge Base) plus the chrome. Reporting, Compliance, Home, Admin, and Exports are placeholders — the primitives in `Primitives.jsx` and the shell in `AppShell.jsx` are sufficient to compose those surfaces in a few hours.
|
||||
|
||||
## Conventions used
|
||||
|
||||
- All colour, type, spacing, radius, elevation pulls from `../../colors_and_type.css`.
|
||||
- No external icon library — lucide icons are inlined as SVG inside `Icon.*`.
|
||||
- Hover states are JS-driven here (mirrors the legacy dashboard pattern); production code should migrate these to CSS `:hover`.
|
||||
- All data is fake. Network calls are stubbed.
|
||||
66
docs/design-system-redesign/ui_kits/cve-dashboard/index.html
Normal file
66
docs/design-system-redesign/ui_kits/cve-dashboard/index.html
Normal file
@@ -0,0 +1,66 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>STEAM Security · CVE Dashboard 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); }
|
||||
</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="Primitives.jsx"></script>
|
||||
<script type="text/babel" src="AppShell.jsx"></script>
|
||||
<script type="text/babel" src="KnowledgeBase.jsx"></script>
|
||||
|
||||
<script type="text/babel">
|
||||
const { useState } = React;
|
||||
const { TopBar, NavDrawer } = window.SDS_Shell;
|
||||
const { KnowledgeBasePage } = window.SDS_KB;
|
||||
const { Card, EmptyState, Icon } = window.SDS;
|
||||
|
||||
const USER = { name: 'J. Ramos', email: 'jramos@steam.local', group: 'Admin' };
|
||||
|
||||
function Placeholder({ name }) {
|
||||
return (
|
||||
<div style={{ padding: '48px 24px', maxWidth: 1280, margin: '0 auto' }}>
|
||||
<h1 style={{ font: '600 24px var(--font-ui)', color: 'var(--fg-1)', margin: '0 0 4px' }}>{name}</h1>
|
||||
<div style={{ font: '400 13px var(--font-ui)', color: 'var(--fg-muted)', marginBottom: 24 }}>
|
||||
This surface is intentionally not built out in the UI kit — primitives + shell are sufficient to compose it.
|
||||
</div>
|
||||
<Card padding={0}>
|
||||
<EmptyState icon={<Icon.FileText size={32} />}
|
||||
title={`${name} placeholder`}
|
||||
message="Open the Knowledge Base tab to see the focused page recreation." />
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [page, setPage] = useState('Knowledge Base');
|
||||
const [drawer, setDrawer] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<TopBar user={USER} currentPage={page} onNav={setPage} onMenuClick={() => setDrawer(true)} />
|
||||
<NavDrawer open={drawer} onClose={() => setDrawer(false)}
|
||||
currentPage={page} onNav={setPage} isAdmin={USER.group === 'Admin'} />
|
||||
<main data-screen-label={page}>
|
||||
{page === 'Knowledge Base' ? <KnowledgeBasePage /> : <Placeholder name={page} />}
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
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>
|
||||
481
docs/design-system-redesign/ui_kits/reporting/KitDocs.jsx
Normal file
481
docs/design-system-redesign/ui_kits/reporting/KitDocs.jsx
Normal file
@@ -0,0 +1,481 @@
|
||||
// KitDocs.jsx — browseable docs page for the Reporting kit.
|
||||
// Sections: Overview · Tokens · Components · Assemblies · Reference page.
|
||||
|
||||
const { useState: useDocsState } = React;
|
||||
const {
|
||||
COLORS: DC, PageHeader, RptButton, KbCard, PillTab, FilterChip, StatusBanner,
|
||||
ToolbarLabel, SeverityDot, SlaPill, WorkflowBadge, DonutSample, RptIcon: DI,
|
||||
} = window.RPT;
|
||||
const { ReportingPage } = window.RPT_PAGE;
|
||||
|
||||
/* ── Layout primitives ─────────────────────────────────────────── */
|
||||
function Section({ 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: DC.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: 640, margin: '8px 0 0 0',
|
||||
}}>{blurb}</p>
|
||||
)}
|
||||
</div>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Spec({ 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 CodeChip({ 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: DC.skySoft,
|
||||
}}>{children}</code>
|
||||
);
|
||||
}
|
||||
|
||||
function SwatchRow({ 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>
|
||||
<CodeChip>{value}</CodeChip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Sticky tab nav ─────────────────────────────────────────────── */
|
||||
function TabNav({ active, onChange }) {
|
||||
const items = [
|
||||
{ id: 'overview', label: 'Overview' },
|
||||
{ id: 'tokens', label: 'Tokens' },
|
||||
{ id: 'components', label: 'Components' },
|
||||
{ id: 'assemblies', label: 'Assemblies' },
|
||||
{ id: 'reference', label: 'Reference page' },
|
||||
];
|
||||
return (
|
||||
<div style={{
|
||||
position: 'sticky', top: 0, zIndex: 20,
|
||||
background: 'rgba(15,23,42,0.92)',
|
||||
backdropFilter: 'blur(8px)',
|
||||
borderBottom: '1px solid rgba(14,165,233,0.12)',
|
||||
padding: '14px 24px',
|
||||
}}>
|
||||
<div style={{ maxWidth: 1280, margin: '0 auto', display: 'flex', alignItems: 'center', gap: 16 }}>
|
||||
<div style={{
|
||||
fontFamily: 'var(--font-mono)', fontSize: 13, fontWeight: 700,
|
||||
color: DC.green, textTransform: 'uppercase', letterSpacing: '0.12em',
|
||||
textShadow: '0 0 12px rgba(16,185,129,0.25)',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
Reporting Kit
|
||||
</div>
|
||||
<div style={{ width: 1, height: 18, background: 'rgba(255,255,255,0.08)' }} />
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
{items.map((it) => (
|
||||
<PillTab key={it.id} active={active === it.id} onClick={() => onChange(it.id)}>
|
||||
{it.label}
|
||||
</PillTab>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Overview ───────────────────────────────────────────────────── */
|
||||
function OverviewSection() {
|
||||
return (
|
||||
<Section
|
||||
id="overview"
|
||||
eyebrow="01 · Overview"
|
||||
title="Reporting page UI kit"
|
||||
blurb="The visual vocabulary used by /reporting. Aligned to the Knowledge Base pattern: green-glow page identity, sky-blue surface accents, mono uppercase labels, Knowledge-Base card chrome on every panel."
|
||||
>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))', gap: 14 }}>
|
||||
<KbCard label="Page identity" hover={false}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<div style={{
|
||||
fontFamily: 'var(--font-mono)', fontSize: 18, fontWeight: 700,
|
||||
color: DC.green, textTransform: 'uppercase', letterSpacing: '0.1em',
|
||||
textShadow: '0 0 12px rgba(16,185,129,0.25)',
|
||||
}}>Reporting</div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-muted)' }}>
|
||||
Green is reserved for the page title + the lone primary action (Sync). Everything else is sky.
|
||||
</div>
|
||||
</div>
|
||||
</KbCard>
|
||||
<KbCard label="Surface accent" hover={false}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<div style={{
|
||||
padding: 10, borderRadius: 6,
|
||||
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.35)',
|
||||
fontFamily: 'var(--font-mono)', fontSize: 11, color: DC.skySoft,
|
||||
}}>
|
||||
KB card · sky border · 0.12 → 0.35 on hover
|
||||
</div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-muted)' }}>
|
||||
Same chrome for donuts, trend, and findings panel. No more colored left-rails.
|
||||
</div>
|
||||
</div>
|
||||
</KbCard>
|
||||
<KbCard label="Type" hover={false}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-disabled)', textTransform: 'uppercase', letterSpacing: '0.1em' }}>
|
||||
Card label · 11 / 600 / 0.1em
|
||||
</div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 14, color: 'var(--fg-1)' }}>JetBrains Mono · everywhere</div>
|
||||
<div style={{ fontFamily: 'var(--font-display)', fontSize: 13, color: 'var(--fg-muted)' }}>Outfit · prose only (blurbs)</div>
|
||||
</div>
|
||||
</KbCard>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Tokens ─────────────────────────────────────────────────────── */
|
||||
function TokensSection() {
|
||||
return (
|
||||
<Section
|
||||
id="tokens"
|
||||
eyebrow="02 · Tokens"
|
||||
title="Color roles, type, spacing"
|
||||
blurb="Reporting uses the dashboard token set. These are the specific roles the page leans on."
|
||||
>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(320px, 1fr))', gap: 14 }}>
|
||||
<KbCard label="Color roles" hover={false}>
|
||||
<SwatchRow name="--accent (sky-500)" value="#0EA5E9" role="Surfaces · pills · table headers · neutral btn" />
|
||||
<SwatchRow name="--intel-success" value="#10B981" role="Page title glow · primary Sync button" />
|
||||
<SwatchRow name="--intel-warning" value="#F59E0B" role="Filter active · anomaly · At-Risk SLA" />
|
||||
<SwatchRow name="--intel-danger" value="#EF4444" role="Errors · Critical sev · Overdue SLA" />
|
||||
<SwatchRow name="--text-disabled" value="#64748B" role="Card labels · meta text" />
|
||||
<SwatchRow name="--text-faint" value="#475569" role="Subtitle · separator counts" />
|
||||
</KbCard>
|
||||
<KbCard label="Card chrome" hover={false}>
|
||||
<Spec label="Background"><CodeChip>linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(15,23,42,0.98) 100%)</CodeChip></Spec>
|
||||
<Spec label="Border (rest)"><CodeChip>1.5px solid rgba(14,165,233,0.12)</CodeChip></Spec>
|
||||
<Spec label="Border (hover)"><CodeChip>1.5px solid rgba(14,165,233,0.35)</CodeChip></Spec>
|
||||
<Spec label="Radius"><CodeChip>8px</CodeChip></Spec>
|
||||
<Spec label="Padding"><CodeChip>16px (donuts) / 20px (panels)</CodeChip></Spec>
|
||||
<Spec label="Label divider"><CodeChip>1px solid rgba(255,255,255,0.04)</CodeChip></Spec>
|
||||
</KbCard>
|
||||
<KbCard label="Type scale" hover={false}>
|
||||
<Spec label="Page title">JetBrains Mono · 24 / 700 · 0.1em · uppercase · green glow</Spec>
|
||||
<Spec label="Subtitle / meta">Mono · 12 / 400 · slate-muted</Spec>
|
||||
<Spec label="Card label">Mono · 11 / 600 · 0.1em · uppercase · slate-disabled</Spec>
|
||||
<Spec label="Toolbar label">Mono · 11 / 700 · 0.1em · uppercase · sky</Spec>
|
||||
<Spec label="Button">Mono · 12 / 600 · 0.05em · uppercase</Spec>
|
||||
<Spec label="Pill tab">Mono · 11 / 600 · 0.05em · uppercase</Spec>
|
||||
<Spec label="Table cell">Mono · 11 / 400</Spec>
|
||||
</KbCard>
|
||||
<KbCard label="Spacing & motion" hover={false}>
|
||||
<Spec label="Page gap"><CodeChip>20px</CodeChip> between major sections</Spec>
|
||||
<Spec label="Donut grid"><CodeChip>repeat(auto-fill, minmax(220px, 1fr))</CodeChip> · gap 14</Spec>
|
||||
<Spec label="Toolbar gap">8px between buttons · 6px subtle group</Spec>
|
||||
<Spec label="Hover transition"><CodeChip>border-color 150ms cubic-bezier(0.4,0,0.2,1)</CodeChip></Spec>
|
||||
<Spec label="Spinner"><CodeChip>1s linear infinite</CodeChip></Spec>
|
||||
</KbCard>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Components ─────────────────────────────────────────────────── */
|
||||
function ComponentsSection() {
|
||||
const [tab, setTab] = useDocsState('ivanti');
|
||||
return (
|
||||
<Section
|
||||
id="components"
|
||||
eyebrow="03 · Components"
|
||||
title="Primitives"
|
||||
blurb="Each component is a thin wrapper around the inline-style pattern used in ReportingPage.js. Drop into other pages that need to inherit the same vocabulary."
|
||||
>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(360px, 1fr))', gap: 14 }}>
|
||||
{/* Buttons */}
|
||||
<KbCard label="Buttons" hover={false}>
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', padding: '4px 0 12px' }}>
|
||||
<RptButton variant="primary" icon={<DI.Refresh size={13} />}>Sync</RptButton>
|
||||
<RptButton variant="neutral" icon={<DI.Atlas size={13} />}>Atlas</RptButton>
|
||||
<RptButton variant="subtle" icon={<DI.Download size={12} />}>Export</RptButton>
|
||||
<RptButton variant="danger" icon={<DI.AlertCircle size={12} />}>Reset</RptButton>
|
||||
<RptButton variant="neutral" disabled icon={<DI.Loader size={13} />}>Disabled</RptButton>
|
||||
</div>
|
||||
<Spec label="primary">Green tinted-fill · the only primary on the page (Sync)</Spec>
|
||||
<Spec label="neutral">Sky outlined · transparent · for Atlas, Prev/Next, etc.</Spec>
|
||||
<Spec label="subtle">Sky tinted-fill · for in-toolbar actions (Export, Queue, Columns)</Spec>
|
||||
<Spec label="danger">Red tinted-fill · destructive only</Spec>
|
||||
</KbCard>
|
||||
|
||||
{/* Pill tabs */}
|
||||
<KbCard label="Pill tabs (metric switcher)" hover={false}>
|
||||
<div style={{ display: 'flex', gap: 5, alignItems: 'center', padding: '4px 0 12px' }}>
|
||||
<DI.PieChart size={14} style={{ color: '#334155', marginRight: 4 }} />
|
||||
<PillTab active={tab === 'ivanti'} onClick={() => setTab('ivanti')}>Ivanti Findings</PillTab>
|
||||
<PillTab active={tab === 'atlas'} onClick={() => setTab('atlas')}>Atlas Coverage</PillTab>
|
||||
<PillTab active={tab === 'sla'} onClick={() => setTab('sla')}>SLA</PillTab>
|
||||
</div>
|
||||
<Spec label="Active">sky border + sky-15% fill + sky text</Spec>
|
||||
<Spec label="Hover (inactive)">subtle white-10% border, slate-300 text</Spec>
|
||||
</KbCard>
|
||||
|
||||
{/* Filter chips */}
|
||||
<KbCard label="Filter chips" hover={false}>
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', padding: '4px 0 12px' }}>
|
||||
<FilterChip color={DC.amber}>Severity: Critical</FilterChip>
|
||||
<FilterChip color={DC.sky}>Action: Patch</FilterChip>
|
||||
<FilterChip color={DC.red}>SLA: Overdue</FilterChip>
|
||||
</div>
|
||||
<Spec label="Color">Tinted to the dimension being filtered</Spec>
|
||||
<Spec label="Click">Clears the filter</Spec>
|
||||
</KbCard>
|
||||
|
||||
{/* Status banners */}
|
||||
<KbCard label="Status banners" hover={false}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, padding: '4px 0 12px' }}>
|
||||
<StatusBanner tone="error">Atlas: connection refused — retry in 30s</StatusBanner>
|
||||
<StatusBanner tone="warn">Sync stale (last success 4 hours ago)</StatusBanner>
|
||||
<StatusBanner tone="info">12 findings reassigned to platform-team</StatusBanner>
|
||||
</div>
|
||||
<Spec label="Placement">Header-level for system errors; inline above target for action results</Spec>
|
||||
</KbCard>
|
||||
|
||||
{/* Severity / SLA / Workflow badges */}
|
||||
<KbCard label="Cell badges" hover={false}>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 14, padding: '4px 0 12px' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<SeverityDot level="Critical" />
|
||||
<SeverityDot level="High" />
|
||||
<SeverityDot level="Medium" />
|
||||
<SeverityDot level="Low" />
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<SlaPill status="OVERDUE" />
|
||||
<SlaPill status="AT_RISK" />
|
||||
<SlaPill status="WITHIN_SLA" />
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<WorkflowBadge state="OPEN" />
|
||||
<WorkflowBadge state="FP" />
|
||||
<WorkflowBadge state="EXC" />
|
||||
<WorkflowBadge state="REMEDIATED" />
|
||||
</div>
|
||||
</div>
|
||||
<Spec label="Severity">Dot + glow + soft-text label · fixed semantic colors</Spec>
|
||||
<Spec label="SLA">Pill · OVERDUE/AT_RISK/WITHIN_SLA</Spec>
|
||||
<Spec label="Workflow">Tagged badge · OPEN/FP/EXC/REMEDIATED/ARCHIVED</Spec>
|
||||
</KbCard>
|
||||
|
||||
{/* KB card itself */}
|
||||
<KbCard label="KB Card" hover={false}>
|
||||
<KbCard label="Open vs Closed" style={{ marginBottom: 10 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'center', padding: '10px 0' }}>
|
||||
<DonutSample
|
||||
segments={[
|
||||
{ label: 'Open', value: 184, color: DC.sky },
|
||||
{ label: 'Closed', value: 712, color: DC.green },
|
||||
]}
|
||||
size={110}
|
||||
centerLabel="TOTAL" centerValue="896" />
|
||||
</div>
|
||||
</KbCard>
|
||||
<Spec label="Container">KB card chrome + label divider</Spec>
|
||||
<Spec label="Body">Centered donut · 170 min-height · responsive auto-fill grid</Spec>
|
||||
</KbCard>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Assemblies ─────────────────────────────────────────────────── */
|
||||
function AssembliesSection() {
|
||||
return (
|
||||
<Section
|
||||
id="assemblies"
|
||||
eyebrow="04 · Assemblies"
|
||||
title="Page-level patterns"
|
||||
blurb="Three combinations the Reporting page is built from. Reuse them as-is on related pages (e.g. dashboards, audit logs)."
|
||||
>
|
||||
{/* Header assembly */}
|
||||
<KbCard label="① Page header + meta + actions" hover={false} style={{ marginBottom: 14 }}>
|
||||
<div style={{ padding: '8px 0' }}>
|
||||
<PageHeader
|
||||
title="Reporting"
|
||||
meta={
|
||||
<>
|
||||
Last sync: 2 minutes ago
|
||||
<span style={{ marginLeft: 10, color: '#334155' }}>· 184 of 896 findings</span>
|
||||
<span style={{ marginLeft: 8, color: DC.amber }}>(3 filters active)</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<RptButton variant="neutral" icon={<DI.Atlas size={13} />}>Atlas</RptButton>
|
||||
<RptButton variant="primary" icon={<DI.Refresh size={13} />}>Sync</RptButton>
|
||||
</PageHeader>
|
||||
</div>
|
||||
<Spec label="Title">Mono uppercase · green glow · 24px</Spec>
|
||||
<Spec label="Meta line">Sync timestamp → record count → active filter count (amber)</Spec>
|
||||
<Spec label="Actions">Right-aligned · neutral secondaries → primary on far right</Spec>
|
||||
</KbCard>
|
||||
|
||||
{/* Donut grid assembly */}
|
||||
<KbCard label="② Metric tabs + donut grid" hover={false} style={{ marginBottom: 14 }}>
|
||||
<div style={{ padding: '8px 0' }}>
|
||||
<div style={{ display: 'flex', gap: 5, alignItems: 'center', marginBottom: 12 }}>
|
||||
<DI.PieChart size={14} style={{ color: '#334155', marginRight: 4 }} />
|
||||
<PillTab active onClick={() => {}}>Ivanti Findings</PillTab>
|
||||
<PillTab active={false} onClick={() => {}}>Atlas Coverage</PillTab>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', gap: 12 }}>
|
||||
{[
|
||||
{ label: 'Open vs Closed', segs: [{ label: 'Open', value: 184, color: DC.sky }, { label: 'Closed', value: 712, color: DC.green }], cl: 'TOTAL', cv: '896' },
|
||||
{ label: 'Action Coverage', segs: [{ label: 'Patch', value: 96, color: DC.sky }, { label: 'Mitigate', value: 42, color: DC.green }, { label: 'Accept', value: 28, color: '#A78BFA' }], cl: 'ASSIGNED', cv: '184' },
|
||||
{ label: 'FP Status', segs: [{ label: 'Pending', value: 14, color: DC.amber }, { label: 'Approved', value: 31, color: DC.green }, { label: 'Rejected', value: 6, color: DC.red }], cl: 'FINDINGS', cv: '51' },
|
||||
].map((d) => (
|
||||
<KbCard key={d.label} label={d.label}>
|
||||
<div style={{ display: 'flex', justifyContent: 'center', minHeight: 150 }}>
|
||||
<DonutSample size={100} segments={d.segs} centerLabel={d.cl} centerValue={d.cv} />
|
||||
</div>
|
||||
</KbCard>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Spec label="Tabs">Pill row sits above grid · scopes which donuts render</Spec>
|
||||
<Spec label="Grid">Auto-fill, 220px min · each donut is its own KB card</Spec>
|
||||
</KbCard>
|
||||
|
||||
{/* Findings panel chrome */}
|
||||
<KbCard label="③ Findings panel chrome (toolbar + filters + table)" hover={false}>
|
||||
<div style={{
|
||||
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)', borderRadius: 8, padding: 16,
|
||||
marginTop: 8,
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||||
paddingBottom: 10, marginBottom: 10,
|
||||
borderBottom: '1px solid rgba(255,255,255,0.04)',
|
||||
}}>
|
||||
<ToolbarLabel count="184 of 896">Host Findings</ToolbarLabel>
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
<RptButton variant="subtle" icon={<DI.Download size={12} />}>Export</RptButton>
|
||||
<RptButton variant="subtle" icon={<DI.ListTodo size={12} />}>Queue</RptButton>
|
||||
<RptButton variant="subtle" icon={<DI.Settings size={12} />}>Columns</RptButton>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||
<FilterChip color={DC.amber}>Severity: Critical, High</FilterChip>
|
||||
<FilterChip color={DC.sky}>Action: Patch</FilterChip>
|
||||
<FilterChip color={DC.red}>SLA: Overdue</FilterChip>
|
||||
</div>
|
||||
</div>
|
||||
<Spec label="Toolbar">Mono uppercase label + count · subtle action buttons right</Spec>
|
||||
<Spec label="Filter row">Tinted chips, click-to-clear</Spec>
|
||||
<Spec label="Header migration">Sync/Atlas no longer live here — they're in the page header</Spec>
|
||||
</KbCard>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Reference page ─────────────────────────────────────────────── */
|
||||
function ReferenceSection() {
|
||||
return (
|
||||
<Section
|
||||
id="reference"
|
||||
eyebrow="05 · Reference page"
|
||||
title="Full Reporting page"
|
||||
blurb="Static mock of /reporting using only kit primitives. Use this to verify any change you make to a primitive flows through the page intact."
|
||||
>
|
||||
<div style={{
|
||||
background: 'var(--bg-page)',
|
||||
border: '1px solid rgba(14,165,233,0.12)',
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
<ReportingPage />
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Top-level docs page ─────────────────────────────────────────── */
|
||||
function KitDocs() {
|
||||
const [active, setActive] = useDocsState('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' });
|
||||
}
|
||||
};
|
||||
|
||||
// observe scroll position to update active tab
|
||||
React.useEffect(() => {
|
||||
const sections = ['overview', 'tokens', 'components', 'assemblies', 'reference']
|
||||
.map((id) => document.getElementById(id))
|
||||
.filter(Boolean);
|
||||
const onScroll = () => {
|
||||
const y = window.scrollY + 160;
|
||||
let cur = sections[0]?.id;
|
||||
for (const s of sections) {
|
||||
if (s.offsetTop <= y) cur = s.id;
|
||||
}
|
||||
setActive(cur);
|
||||
};
|
||||
window.addEventListener('scroll', onScroll, { passive: true });
|
||||
onScroll();
|
||||
return () => window.removeEventListener('scroll', onScroll);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TabNav active={active} onChange={handle} />
|
||||
<div style={{ maxWidth: 1280, margin: '0 auto', padding: '0 24px 80px' }}>
|
||||
<OverviewSection />
|
||||
<TokensSection />
|
||||
<ComponentsSection />
|
||||
<AssembliesSection />
|
||||
<ReferenceSection />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
window.RPT_DOCS = { KitDocs };
|
||||
36
docs/design-system-redesign/ui_kits/reporting/README.md
Normal file
36
docs/design-system-redesign/ui_kits/reporting/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Reporting UI Kit
|
||||
|
||||
The visual vocabulary used by `/reporting` after the Knowledge Base alignment pass.
|
||||
|
||||
## Files
|
||||
- `index.html` — entry point. Loads the kit docs page.
|
||||
- `ReportPrimitives.jsx` — `PageHeader`, `RptButton`, `KbCard`, `PillTab`, `FilterChip`, `StatusBanner`, `ToolbarLabel`, `SeverityDot`, `SlaPill`, `WorkflowBadge`, `DonutSample`, `RptIcon`.
|
||||
- `ReportingPage.jsx` — full-page reference assembly (`ReportingPage`).
|
||||
- `KitDocs.jsx` — browseable docs (Overview · Tokens · Components · Assemblies · Reference).
|
||||
|
||||
## Color roles
|
||||
- **Sky `#0EA5E9`** — surface accent (panel borders, tab pill active, donut highlight, table header text, neutral secondary buttons).
|
||||
- **Green `#10B981`** — page identity only: title glow + the lone primary action (Sync).
|
||||
- **Amber `#F59E0B`** — filter active, anomaly callout, At-Risk SLA.
|
||||
- **Red `#EF4444`** — error / Critical / Overdue.
|
||||
|
||||
## Card chrome (one chrome, every panel)
|
||||
```
|
||||
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 */
|
||||
radius: 8px
|
||||
label: mono · 11 / 600 · 0.1em · uppercase · slate-disabled
|
||||
divider: 1px solid rgba(255,255,255,0.04) under the label
|
||||
```
|
||||
|
||||
## Button hierarchy
|
||||
- `primary` (green tinted-fill) — **only** Sync uses this.
|
||||
- `neutral` (sky outlined transparent) — Atlas, Prev/Next, refresh.
|
||||
- `subtle` (sky tinted-fill) — Export, Queue, Columns, Rows.
|
||||
- `danger` (red tinted-fill) — destructive only.
|
||||
|
||||
## Page-level rules
|
||||
1. `Sync` and `Atlas` live in the **page header**, not the findings panel toolbar.
|
||||
2. The page title is the only place green appears as identity. Anywhere else, green = success state.
|
||||
3. Every metric panel is a KB card. No more colored left-rails.
|
||||
4. Filter chips tint to the dimension being filtered (severity → amber, SLA → red, action → sky).
|
||||
@@ -0,0 +1,393 @@
|
||||
// ReportPrimitives.jsx — Reporting-specific UI vocabulary.
|
||||
// All inline styles + tokens from ../../colors_and_type.css.
|
||||
// Mirrors the live Reporting page (frontend/src/components/pages/ReportingPage.js)
|
||||
// after the Knowledge-Base alignment pass.
|
||||
|
||||
const { useState: useRPTState } = React;
|
||||
|
||||
/* ─────────────────────────────────────────────────────────────────
|
||||
COLOR ROLE MAP (Reporting)
|
||||
──────────────────────────────────────────────────────────────────
|
||||
Sky-blue (#0EA5E9) → primary surface accent (panel borders,
|
||||
tab pill active, donut highlight, table
|
||||
header text, neutral secondary buttons)
|
||||
Green (#10B981) → page identity (header glow + primary
|
||||
Sync button)
|
||||
Amber (#F59E0B) → filter-active indicator, anomaly callout
|
||||
Red (#EF4444) → error / overdue
|
||||
Slate stack → muted text + dividers (#475569 → #334155)
|
||||
──────────────────────────────────────────────────────────────── */
|
||||
|
||||
const COLORS = {
|
||||
sky: '#0EA5E9',
|
||||
skySoft: '#7DD3FC',
|
||||
green: '#10B981',
|
||||
amber: '#F59E0B',
|
||||
red: '#EF4444',
|
||||
redSoft: '#FCA5A5',
|
||||
};
|
||||
|
||||
/* ── Page header ─────────────────────────────────────────────────
|
||||
Big mono uppercase title in green w/ glow + count subtitle.
|
||||
Right side: neutral icon-tinted secondaries + tinted-fill primary.
|
||||
Lifted from the existing Knowledge Base page header pattern. */
|
||||
function PageHeader({ title = 'Reporting', meta, children }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 16 }}>
|
||||
<div>
|
||||
<h2 style={{
|
||||
fontFamily: 'var(--font-mono)', fontSize: 24, fontWeight: 700,
|
||||
color: COLORS.green, textTransform: 'uppercase', letterSpacing: '0.1em',
|
||||
textShadow: '0 0 16px rgba(16,185,129,0.25)',
|
||||
margin: '0 0 4px 0',
|
||||
}}>
|
||||
{title}
|
||||
</h2>
|
||||
{meta && (
|
||||
<div style={{ fontSize: 12, color: 'var(--fg-muted)', fontFamily: 'var(--font-mono)' }}>
|
||||
{meta}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, flexShrink: 0, alignItems: 'center' }}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Buttons ─────────────────────────────────────────────────────
|
||||
THREE variants documented for Reporting:
|
||||
• primary — green tinted-fill (lone primary action: Sync)
|
||||
• neutral — sky outlined transparent (Atlas, refresh, etc.)
|
||||
• subtle — sky tinted-fill (Export, Queue, Column manager)
|
||||
*/
|
||||
function RptButton({ variant = 'neutral', icon, children, disabled, ...rest }) {
|
||||
const [hover, setHover] = useRPTState(false);
|
||||
const v = {
|
||||
primary: {
|
||||
bgRest: 'rgba(16,185,129,0.18)',
|
||||
bgHover: 'rgba(16,185,129,0.26)',
|
||||
bd: COLORS.green, fg: COLORS.green,
|
||||
},
|
||||
neutral: {
|
||||
bgRest: 'transparent',
|
||||
bgHover: 'rgba(14,165,233,0.06)',
|
||||
bd: 'rgba(14,165,233,0.25)', fg: COLORS.sky,
|
||||
},
|
||||
subtle: {
|
||||
bgRest: 'rgba(14,165,233,0.08)',
|
||||
bgHover: 'rgba(14,165,233,0.16)',
|
||||
bd: 'rgba(14,165,233,0.35)', fg: COLORS.sky,
|
||||
},
|
||||
danger: {
|
||||
bgRest: 'rgba(239,68,68,0.08)',
|
||||
bgHover: 'rgba(239,68,68,0.16)',
|
||||
bd: 'rgba(239,68,68,0.30)', fg: COLORS.red,
|
||||
},
|
||||
}[variant];
|
||||
return (
|
||||
<button
|
||||
disabled={disabled}
|
||||
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
||||
style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||||
background: hover && !disabled ? v.bgHover : v.bgRest,
|
||||
border: `1px solid ${hover && !disabled && variant === 'neutral' ? 'rgba(14,165,233,0.55)' : v.bd}`,
|
||||
color: disabled ? 'var(--fg-disabled)' : v.fg,
|
||||
padding: '8px 14px', borderRadius: 6,
|
||||
fontFamily: 'var(--font-mono)', fontSize: 12, fontWeight: 600,
|
||||
textTransform: 'uppercase', letterSpacing: '0.05em',
|
||||
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||
opacity: disabled ? 0.5 : 1,
|
||||
transition: 'all 150ms cubic-bezier(0.4,0,0.2,1)',
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
{icon}{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── KB-style card (sky) — used for donuts + findings panel ──── */
|
||||
function KbCard({ children, padding = 16, label, labelExtra, hover = true, style }) {
|
||||
const [h, setH] = useRPTState(false);
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={() => hover && setH(true)} onMouseLeave={() => setH(false)}
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(15,23,42,0.98) 100%)',
|
||||
border: `1.5px solid ${h ? 'rgba(14,165,233,0.35)' : 'rgba(14,165,233,0.12)'}`,
|
||||
borderRadius: 8, padding,
|
||||
display: 'flex', flexDirection: 'column', gap: 10,
|
||||
transition: 'border-color 150ms cubic-bezier(0.4,0,0.2,1)',
|
||||
...style,
|
||||
}}>
|
||||
{label && (
|
||||
<div style={{
|
||||
fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600,
|
||||
color: 'var(--fg-disabled)', textTransform: 'uppercase', letterSpacing: '0.1em',
|
||||
paddingBottom: 8,
|
||||
borderBottom: '1px solid rgba(255,255,255,0.04)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
}}>
|
||||
<span>{label}</span>
|
||||
{labelExtra}
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Pill tab (Ivanti / Atlas) ───────────────────────────────── */
|
||||
function PillTab({ active, color = COLORS.sky, onClick, children }) {
|
||||
const [hover, setHover] = useRPTState(false);
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600,
|
||||
textTransform: 'uppercase', letterSpacing: '0.05em',
|
||||
cursor: 'pointer', borderRadius: 4,
|
||||
border: `1px solid ${active ? color : (hover ? 'rgba(255,255,255,0.10)' : 'transparent')}`,
|
||||
background: active ? `${color}26` : 'transparent',
|
||||
color: active ? color : (hover ? '#94A3B8' : 'var(--fg-muted)'),
|
||||
transition: 'all 120ms',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Filter chip (active filter pin in the toolbar) ──────────── */
|
||||
function FilterChip({ color = COLORS.amber, onClear, children }) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClear}
|
||||
style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||||
padding: '6px 12px',
|
||||
background: `${color}14`,
|
||||
border: `1px solid ${color}4D`,
|
||||
borderRadius: 6,
|
||||
color, cursor: 'pointer',
|
||||
fontFamily: 'var(--font-mono)', fontSize: 12, fontWeight: 600,
|
||||
letterSpacing: '0.05em',
|
||||
}}
|
||||
>
|
||||
<RptIcon.Filter size={11} />
|
||||
{children}
|
||||
<span style={{ marginLeft: 2, opacity: 0.7 }}>×</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Status banner (error / Atlas error / sync error) ────────── */
|
||||
function StatusBanner({ tone = 'error', children }) {
|
||||
const tones = {
|
||||
error: { bg: 'rgba(239,68,68,0.08)', bd: 'rgba(239,68,68,0.25)', fg: COLORS.redSoft, icon: COLORS.red },
|
||||
warn: { bg: 'rgba(245,158,11,0.10)', bd: 'rgba(245,158,11,0.28)', fg: '#FCD34D', icon: COLORS.amber },
|
||||
info: { bg: 'rgba(14,165,233,0.08)', bd: 'rgba(14,165,233,0.25)', fg: COLORS.skySoft, icon: COLORS.sky },
|
||||
};
|
||||
const t = tones[tone];
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'flex-start', gap: 8,
|
||||
padding: '10px 14px', background: t.bg, border: `1px solid ${t.bd}`,
|
||||
borderRadius: 8,
|
||||
}}>
|
||||
<RptIcon.AlertCircle size={15} style={{ color: t.icon, flexShrink: 0, marginTop: 1 }} />
|
||||
<span style={{ fontSize: 12, color: t.fg, fontFamily: 'var(--font-mono)' }}>{children}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Toolbar label (small mono uppercase, used inside findings panel) ── */
|
||||
function ToolbarLabel({ children, accent = COLORS.sky, count }) {
|
||||
return (
|
||||
<div style={{
|
||||
fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 700,
|
||||
color: accent, textTransform: 'uppercase', letterSpacing: '0.1em',
|
||||
}}>
|
||||
{children}
|
||||
{count != null && (
|
||||
<span style={{ marginLeft: 10, color: '#334155', fontWeight: 400 }}>
|
||||
{count}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Severity dot (used in table rows) ───────────────────────── */
|
||||
function SeverityDot({ level }) {
|
||||
const map = {
|
||||
Critical: { c: COLORS.red, text: '#FCA5A5' },
|
||||
High: { c: COLORS.amber, text: '#FCD34D' },
|
||||
Medium: { c: COLORS.sky, text: '#7DD3FC' },
|
||||
Low: { c: COLORS.green, text: '#6EE7B7' },
|
||||
Info: { c: '#94A3B8', text: '#CBD5E1' },
|
||||
};
|
||||
const v = map[level] || map.Info;
|
||||
return (
|
||||
<span style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||||
fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600,
|
||||
color: v.text, letterSpacing: '0.04em',
|
||||
}}>
|
||||
<span style={{
|
||||
width: 7, height: 7, borderRadius: '50%', background: v.c,
|
||||
boxShadow: `0 0 6px ${v.c}99`,
|
||||
}} />
|
||||
{level}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── SLA pill (table cell) ───────────────────────────────────── */
|
||||
function SlaPill({ status }) {
|
||||
const map = {
|
||||
OVERDUE: { c: COLORS.red, bg: 'rgba(239,68,68,0.16)' },
|
||||
AT_RISK: { c: COLORS.amber, bg: 'rgba(245,158,11,0.16)' },
|
||||
WITHIN_SLA: { c: COLORS.green, bg: 'rgba(16,185,129,0.16)' },
|
||||
};
|
||||
const v = map[status] || map.WITHIN_SLA;
|
||||
return (
|
||||
<span style={{
|
||||
padding: '2px 8px', borderRadius: 999,
|
||||
background: v.bg, color: v.c,
|
||||
fontFamily: 'var(--font-mono)', fontWeight: 700, fontSize: 10,
|
||||
letterSpacing: '0.05em',
|
||||
}}>
|
||||
{status.replace('_', ' ')}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Workflow badge (table cell) ─────────────────────────────── */
|
||||
function WorkflowBadge({ state }) {
|
||||
const map = {
|
||||
OPEN: { c: COLORS.sky, bg: 'rgba(14,165,233,0.14)' },
|
||||
FP: { c: COLORS.amber, bg: 'rgba(245,158,11,0.14)' },
|
||||
EXC: { c: '#A78BFA', bg: 'rgba(167,139,250,0.14)' },
|
||||
REMEDIATED:{ c: COLORS.green, bg: 'rgba(16,185,129,0.14)' },
|
||||
ARCHIVED: { c: '#94A3B8', bg: 'rgba(148,163,184,0.14)' },
|
||||
};
|
||||
const v = map[state] || { c: 'var(--fg-muted)', bg: 'rgba(148,163,184,0.10)' };
|
||||
return (
|
||||
<span style={{
|
||||
padding: '2px 8px', borderRadius: 4,
|
||||
background: v.bg, color: v.c, border: `1px solid ${v.c}55`,
|
||||
fontFamily: 'var(--font-mono)', fontWeight: 700, fontSize: 10,
|
||||
letterSpacing: '0.05em',
|
||||
}}>
|
||||
{state}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Donut placeholder — semantic stand-in for the real recharts donut ── */
|
||||
function DonutSample({ size = 130, segments, centerLabel, centerValue }) {
|
||||
// segments: [{ label, value, color }]
|
||||
const total = segments.reduce((s, x) => s + x.value, 0);
|
||||
const cx = size / 2, cy = size / 2;
|
||||
const outerR = size / 2 - 4, innerR = outerR - 16;
|
||||
let angle = -90;
|
||||
const arcs = segments.map((seg) => {
|
||||
const sweep = (seg.value / total) * 360;
|
||||
const a0 = (angle * Math.PI) / 180;
|
||||
const a1 = ((angle + sweep) * Math.PI) / 180;
|
||||
const large = sweep > 180 ? 1 : 0;
|
||||
const x0 = cx + outerR * Math.cos(a0), y0 = cy + outerR * Math.sin(a0);
|
||||
const x1 = cx + outerR * Math.cos(a1), y1 = cy + outerR * Math.sin(a1);
|
||||
const xi1 = cx + innerR * Math.cos(a1), yi1 = cy + innerR * Math.sin(a1);
|
||||
const xi0 = cx + innerR * Math.cos(a0), yi0 = cy + innerR * Math.sin(a0);
|
||||
const d = `M ${x0} ${y0} A ${outerR} ${outerR} 0 ${large} 1 ${x1} ${y1}
|
||||
L ${xi1} ${yi1} A ${innerR} ${innerR} 0 ${large} 0 ${xi0} ${yi0} Z`;
|
||||
angle += sweep;
|
||||
return { d, color: seg.color };
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 10 }}>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<svg width={size} height={size}>
|
||||
{arcs.map((a, i) => (
|
||||
<path key={i} d={a.d} fill={a.color} stroke="rgba(15,23,42,0.95)" strokeWidth="1" />
|
||||
))}
|
||||
</svg>
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0,
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
|
||||
pointerEvents: 'none',
|
||||
}}>
|
||||
<div style={{
|
||||
fontFamily: 'var(--font-mono)', fontSize: 22, fontWeight: 700,
|
||||
color: 'var(--fg-1)', lineHeight: 1,
|
||||
}}>{centerValue}</div>
|
||||
{centerLabel && (
|
||||
<div style={{
|
||||
fontFamily: 'var(--font-mono)', fontSize: 9, fontWeight: 600,
|
||||
color: 'var(--fg-disabled)', textTransform: 'uppercase', letterSpacing: '0.12em',
|
||||
marginTop: 4,
|
||||
}}>
|
||||
{centerLabel}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* Legend */}
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '4px 10px', justifyContent: 'center', maxWidth: size + 32 }}>
|
||||
{segments.map((s) => (
|
||||
<div key={s.label} style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 4,
|
||||
fontFamily: 'var(--font-mono)', fontSize: 9.5, color: 'var(--fg-muted)',
|
||||
letterSpacing: '0.04em',
|
||||
}}>
|
||||
<span style={{ width: 8, height: 8, borderRadius: 2, background: s.color, flexShrink: 0 }} />
|
||||
<span>{s.label} <span style={{ color: 'var(--fg-disabled)' }}>{s.value}</span></span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Inline lucide icons (Reporting subset) ──────────────────── */
|
||||
const _ic = (path) => ({ size = 14, strokeWidth = 1.75, ...rest }) => (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" strokeWidth={strokeWidth} strokeLinecap="round" strokeLinejoin="round"
|
||||
style={{ flexShrink: 0 }} {...rest}>{path}</svg>
|
||||
);
|
||||
const RptIcon = {
|
||||
Refresh: _ic(<><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"/><path d="M16 16h5v5"/></>),
|
||||
PieChart: _ic(<><path d="M21.21 15.89A10 10 0 1 1 8 2.83"/><path d="M22 12A10 10 0 0 0 12 2v10z"/></>),
|
||||
Filter: _ic(<><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></>),
|
||||
Download: _ic(<><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"/></>),
|
||||
ChevronD: _ic(<><polyline points="6 9 12 15 18 9"/></>),
|
||||
ChevronUp: _ic(<><polyline points="18 15 12 9 6 15"/></>),
|
||||
ChevronUpDn:_ic(<><path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/></>),
|
||||
ListTodo: _ic(<><rect x="3" y="5" width="6" height="6" rx="1"/><path d="m3 17 2 2 4-4"/><path d="M13 6h8"/><path d="M13 12h8"/><path d="M13 18h8"/></>),
|
||||
Settings: _ic(<><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33h0a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51h0a1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82v0a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></>),
|
||||
Eye: _ic(<><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></>),
|
||||
EyeOff: _ic(<><path d="M9.88 9.88a3 3 0 1 0 4.24 4.24"/><path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68"/><path d="M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61"/><line x1="2" y1="2" x2="22" y2="22"/></>),
|
||||
AlertCircle:_ic(<><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"/></>),
|
||||
AlertTri: _ic(<><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></>),
|
||||
Atlas: _ic(<><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10"/><path d="M12 2a15.3 15.3 0 0 0-4 10 15.3 15.3 0 0 0 4 10"/></>),
|
||||
Search: _ic(<><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></>),
|
||||
Square: _ic(<><rect x="3" y="3" width="18" height="18" rx="2"/></>),
|
||||
CheckSq: _ic(<><polyline points="9 11 12 14 22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></>),
|
||||
Loader: _ic(<><line x1="12" y1="2" x2="12" y2="6"/><line x1="12" y1="18" x2="12" y2="22"/><line x1="4.93" y1="4.93" x2="7.76" y2="7.76"/><line x1="16.24" y1="16.24" x2="19.07" y2="19.07"/><line x1="2" y1="12" x2="6" y2="12"/><line x1="18" y1="12" x2="22" y2="12"/><line x1="4.93" y1="19.07" x2="7.76" y2="16.24"/><line x1="16.24" y1="7.76" x2="19.07" y2="4.93"/></>),
|
||||
TrendUp: _ic(<><polyline points="22 7 13.5 15.5 8.5 10.5 2 17"/><polyline points="16 7 22 7 22 13"/></>),
|
||||
};
|
||||
|
||||
window.RPT = {
|
||||
COLORS,
|
||||
PageHeader, RptButton, KbCard, PillTab, FilterChip, StatusBanner,
|
||||
ToolbarLabel, SeverityDot, SlaPill, WorkflowBadge, DonutSample,
|
||||
RptIcon,
|
||||
};
|
||||
299
docs/design-system-redesign/ui_kits/reporting/ReportingPage.jsx
Normal file
299
docs/design-system-redesign/ui_kits/reporting/ReportingPage.jsx
Normal file
@@ -0,0 +1,299 @@
|
||||
// ReportingPage.jsx — full-page assembly using only RPT primitives.
|
||||
// Mirrors frontend/src/components/pages/ReportingPage.js after the KB pass.
|
||||
|
||||
const { useState: useRPSt } = React;
|
||||
const {
|
||||
COLORS: RC,
|
||||
PageHeader, RptButton, KbCard, PillTab, FilterChip, StatusBanner,
|
||||
ToolbarLabel, SeverityDot, SlaPill, WorkflowBadge, DonutSample,
|
||||
RptIcon: RI,
|
||||
} = window.RPT;
|
||||
|
||||
/* Sample findings rows. Static — purely for layout. */
|
||||
const SAMPLE_ROWS = [
|
||||
{ id: 'F-10241', host: 'web-prod-04.steam.local', os: 'Ubuntu 22.04', sev: 'Critical', cve: 'CVE-2024-3094', age: 4, sla: 'OVERDUE', state: 'OPEN', action: 'Patch', owner: 'platform' },
|
||||
{ id: 'F-10238', host: 'kafka-broker-2.steam.local',os: 'RHEL 9.3', sev: 'Critical', cve: 'CVE-2024-21626', age: 11, sla: 'OVERDUE', state: 'FP', action: 'Investigate',owner: 'data-eng' },
|
||||
{ id: 'F-10202', host: 'auth-prod-01.steam.local', os: 'Ubuntu 22.04', sev: 'High', cve: 'CVE-2024-1086', age: 3, sla: 'AT_RISK', state: 'OPEN', action: 'Patch', owner: 'platform' },
|
||||
{ id: 'F-10197', host: 'edge-cdn-09.steam.local', os: 'Alpine 3.19', sev: 'High', cve: 'CVE-2024-23222', age: 2, sla: 'AT_RISK', state: 'EXC', action: 'Accept', owner: 'edge' },
|
||||
{ id: 'F-10185', host: 'analytics-w-3.steam.local',os: 'Ubuntu 20.04', sev: 'Medium', cve: 'CVE-2023-50387', age: 14, sla: 'WITHIN_SLA', state: 'OPEN', action: 'Mitigate', owner: 'analytics' },
|
||||
{ id: 'F-10180', host: 'mail-relay-1.steam.local', os: 'Debian 12', sev: 'Medium', cve: 'CVE-2024-22195', age: 9, sla: 'WITHIN_SLA', state: 'REMEDIATED', action: 'Patch', owner: 'platform' },
|
||||
{ id: 'F-10164', host: 'jumphost-2.steam.local', os: 'Ubuntu 22.04', sev: 'Low', cve: 'CVE-2023-45288', age: 22, sla: 'WITHIN_SLA', state: 'OPEN', action: 'Defer', owner: 'sre' },
|
||||
];
|
||||
|
||||
/* Tiny anomaly bar chart placeholder for the trend section. */
|
||||
function TrendChartPlaceholder() {
|
||||
const data = [22, 28, 21, 24, 30, 27, 26, 25, 31, 38, 42, 45, 41, 36, 33];
|
||||
const closed = [10, 14, 12, 13, 18, 20, 22, 21, 24, 26, 28, 30, 31, 30, 29];
|
||||
const max = Math.max(...data);
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 4, height: 120, padding: '4px 0' }}>
|
||||
{data.map((d, i) => (
|
||||
<div key={i} style={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'flex-end', gap: 2 }}>
|
||||
<div style={{
|
||||
height: `${(d / max) * 100}%`,
|
||||
background: 'linear-gradient(180deg, rgba(14,165,233,0.85), rgba(14,165,233,0.45))',
|
||||
borderRadius: '2px 2px 0 0',
|
||||
}} />
|
||||
<div style={{
|
||||
height: `${(closed[i] / max) * 60}%`,
|
||||
background: 'rgba(16,185,129,0.55)',
|
||||
borderRadius: '0 0 2px 2px',
|
||||
}} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ReportingPage() {
|
||||
const [tab, setTab] = useRPSt('ivanti');
|
||||
const [actionFilter, setActionFilter] = useRPSt(null);
|
||||
|
||||
/* Donut data (illustrative) */
|
||||
const ivantiDonuts = [
|
||||
{
|
||||
label: 'Open vs Closed',
|
||||
donut: <DonutSample
|
||||
segments={[
|
||||
{ label: 'Open', value: 184, color: RC.sky },
|
||||
{ label: 'Closed', value: 712, color: RC.green },
|
||||
]}
|
||||
centerLabel="TOTAL" centerValue="896" />,
|
||||
},
|
||||
{
|
||||
label: 'Action Coverage',
|
||||
labelExtra: actionFilter && (
|
||||
<span style={{ color: RC.amber, fontSize: 9 }}>● filtered</span>
|
||||
),
|
||||
donut: <DonutSample
|
||||
segments={[
|
||||
{ label: 'Patch', value: 96, color: RC.sky },
|
||||
{ label: 'Mitigate', value: 42, color: RC.green },
|
||||
{ label: 'Accept', value: 28, color: '#A78BFA' },
|
||||
{ label: 'Investigate', value: 18, color: RC.amber },
|
||||
]}
|
||||
centerLabel="ASSIGNED" centerValue="184" />,
|
||||
},
|
||||
{
|
||||
label: 'FP Finding Status',
|
||||
donut: <DonutSample
|
||||
segments={[
|
||||
{ label: 'Pending', value: 14, color: RC.amber },
|
||||
{ label: 'Approved', value: 31, color: RC.green },
|
||||
{ label: 'Rejected', value: 6, color: RC.red },
|
||||
]}
|
||||
centerLabel="FINDINGS" centerValue="51" />,
|
||||
},
|
||||
{
|
||||
label: 'FP Workflow Status',
|
||||
donut: <DonutSample
|
||||
segments={[
|
||||
{ label: 'In Review', value: 8, color: RC.sky },
|
||||
{ label: 'Closed', value: 22, color: RC.green },
|
||||
{ label: 'Escalated', value: 4, color: RC.red },
|
||||
]}
|
||||
centerLabel="FP TICKETS" centerValue="34" />,
|
||||
},
|
||||
];
|
||||
|
||||
const atlasDonuts = [
|
||||
{
|
||||
label: 'Host Coverage',
|
||||
donut: <DonutSample
|
||||
segments={[
|
||||
{ label: 'With Plans', value: 312, color: RC.green },
|
||||
{ label: 'Without Plans', value: 88, color: RC.amber },
|
||||
]}
|
||||
centerLabel="HOSTS" centerValue="400" />,
|
||||
},
|
||||
{
|
||||
label: 'Plan Types',
|
||||
donut: <DonutSample
|
||||
segments={[
|
||||
{ label: 'Patch', value: 142, color: RC.sky },
|
||||
{ label: 'Mitigate', value: 68, color: RC.green },
|
||||
{ label: 'Accept', value: 31, color: '#A78BFA' },
|
||||
]}
|
||||
centerLabel="PLANS" centerValue="241" />,
|
||||
},
|
||||
{
|
||||
label: 'Plan Status',
|
||||
donut: <DonutSample
|
||||
segments={[
|
||||
{ label: 'Active', value: 184, color: RC.green },
|
||||
{ label: 'Pending', value: 42, color: RC.amber },
|
||||
{ label: 'Stalled', value: 15, color: RC.red },
|
||||
]}
|
||||
centerLabel="STATUS" centerValue="241" />,
|
||||
},
|
||||
];
|
||||
|
||||
const donuts = tab === 'ivanti' ? ivantiDonuts : atlasDonuts;
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex', flexDirection: 'column', gap: 20,
|
||||
padding: 24, maxWidth: 1280, margin: '0 auto',
|
||||
}}>
|
||||
{/* Page header */}
|
||||
<PageHeader
|
||||
title="Reporting"
|
||||
meta={
|
||||
<>
|
||||
Last sync: 2 minutes ago
|
||||
<span style={{ marginLeft: 10, color: '#334155' }}>· 184 of 896 findings</span>
|
||||
<span style={{ marginLeft: 8, color: RC.amber }}>(3 filters active)</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<RptButton variant="neutral" icon={<RI.Atlas size={13} />}>Atlas</RptButton>
|
||||
<RptButton variant="primary" icon={<RI.Refresh size={13} />}>Sync</RptButton>
|
||||
</PageHeader>
|
||||
|
||||
{/* Header-level error */}
|
||||
<StatusBanner tone="error">Atlas: connection refused — retry in 30s</StatusBanner>
|
||||
|
||||
{/* Metrics tabs */}
|
||||
<div style={{ display: 'flex', gap: 5, alignItems: 'center' }}>
|
||||
<RI.PieChart size={14} style={{ color: '#334155', marginRight: 4 }} />
|
||||
<PillTab active={tab === 'ivanti'} onClick={() => setTab('ivanti')}>Ivanti Findings</PillTab>
|
||||
<PillTab active={tab === 'atlas'} onClick={() => setTab('atlas')}>Atlas Coverage</PillTab>
|
||||
</div>
|
||||
|
||||
{/* Donut grid */}
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(220px, 1fr))',
|
||||
gap: 14,
|
||||
}}>
|
||||
{donuts.map((d) => (
|
||||
<KbCard key={d.label} label={d.label} labelExtra={d.labelExtra}>
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: 170 }}>
|
||||
{d.donut}
|
||||
</div>
|
||||
</KbCard>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Trend section */}
|
||||
<KbCard label="Open vs Closed · last 30 days" labelExtra={
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 5, color: RC.amber, fontSize: 10 }}>
|
||||
<RI.AlertTri size={11} /> spike detected day 12
|
||||
</span>
|
||||
}>
|
||||
<TrendChartPlaceholder />
|
||||
<div style={{ display: 'flex', gap: 14, fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--fg-muted)', justifyContent: 'center' }}>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 5 }}>
|
||||
<span style={{ width: 9, height: 9, background: RC.sky, borderRadius: 2 }} /> Open
|
||||
</span>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 5 }}>
|
||||
<span style={{ width: 9, height: 9, background: 'rgba(16,185,129,0.55)', borderRadius: 2 }} /> Closed
|
||||
</span>
|
||||
</div>
|
||||
</KbCard>
|
||||
|
||||
{/* Findings table panel */}
|
||||
<div style={{
|
||||
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)',
|
||||
borderRadius: 8, padding: 20,
|
||||
}}>
|
||||
{/* Toolbar */}
|
||||
<div style={{
|
||||
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||||
marginBottom: 12, paddingBottom: 10,
|
||||
borderBottom: '1px solid rgba(255,255,255,0.04)',
|
||||
}}>
|
||||
<ToolbarLabel count="184 of 896">Host Findings</ToolbarLabel>
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
<RptButton variant="subtle" icon={<RI.Download size={12} />}>Export</RptButton>
|
||||
<RptButton variant="subtle" icon={<RI.ListTodo size={12} />}>Queue</RptButton>
|
||||
<RptButton variant="subtle" icon={<RI.Settings size={12} />}>Columns</RptButton>
|
||||
<RptButton variant="subtle" icon={<RI.EyeOff size={12} />}>Rows</RptButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search + filter chip row */}
|
||||
<div style={{ display: 'flex', gap: 10, alignItems: 'center', marginBottom: 12, flexWrap: 'wrap' }}>
|
||||
<div style={{
|
||||
position: 'relative', flex: '1 1 280px', maxWidth: 360,
|
||||
}}>
|
||||
<RI.Search size={13} style={{ position: 'absolute', left: 10, top: '50%', transform: 'translateY(-50%)', color: 'var(--fg-disabled)' }} />
|
||||
<input
|
||||
defaultValue="kafka"
|
||||
placeholder="Search host, CVE, owner…"
|
||||
style={{
|
||||
width: '100%', padding: '8px 10px 8px 30px',
|
||||
background: 'rgba(15,23,42,0.6)',
|
||||
border: '1px solid rgba(14,165,233,0.18)',
|
||||
borderRadius: 6,
|
||||
color: 'var(--fg-1)', fontFamily: 'var(--font-mono)', fontSize: 12,
|
||||
outline: 'none',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<FilterChip color={RC.amber}>Severity: Critical, High</FilterChip>
|
||||
<FilterChip color={RC.sky}>Action: Patch</FilterChip>
|
||||
<FilterChip color={RC.red}>SLA: Overdue</FilterChip>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div style={{ overflow: 'auto', borderRadius: 6, border: '1px solid rgba(255,255,255,0.04)' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontFamily: 'var(--font-mono)', fontSize: 11 }}>
|
||||
<thead>
|
||||
<tr style={{ background: 'rgba(14,165,233,0.06)' }}>
|
||||
{['ID', 'Host', 'OS', 'Severity', 'CVE', 'Age', 'SLA', 'State', 'Action', 'Owner'].map((h) => (
|
||||
<th key={h} style={{
|
||||
textAlign: 'left', padding: '8px 12px',
|
||||
color: RC.sky, textTransform: 'uppercase', letterSpacing: '0.08em',
|
||||
fontWeight: 700, fontSize: 10,
|
||||
borderBottom: '1px solid rgba(14,165,233,0.18)',
|
||||
}}>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
|
||||
{h}
|
||||
<RI.ChevronUpDn size={10} style={{ opacity: 0.5 }} />
|
||||
</span>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{SAMPLE_ROWS.map((r, i) => (
|
||||
<tr key={r.id} style={{
|
||||
background: i % 2 === 0 ? 'transparent' : 'rgba(255,255,255,0.015)',
|
||||
borderBottom: '1px solid rgba(255,255,255,0.03)',
|
||||
}}>
|
||||
<td style={{ padding: '10px 12px', color: RC.sky, fontWeight: 600 }}>{r.id}</td>
|
||||
<td style={{ padding: '10px 12px', color: 'var(--fg-1)' }}>{r.host}</td>
|
||||
<td style={{ padding: '10px 12px', color: 'var(--fg-muted)' }}>{r.os}</td>
|
||||
<td style={{ padding: '10px 12px' }}><SeverityDot level={r.sev} /></td>
|
||||
<td style={{ padding: '10px 12px', color: 'var(--fg-2)' }}>{r.cve}</td>
|
||||
<td style={{ padding: '10px 12px', color: 'var(--fg-muted)' }}>{r.age}d</td>
|
||||
<td style={{ padding: '10px 12px' }}><SlaPill status={r.sla} /></td>
|
||||
<td style={{ padding: '10px 12px' }}><WorkflowBadge state={r.state} /></td>
|
||||
<td style={{ padding: '10px 12px', color: 'var(--fg-2)' }}>{r.action}</td>
|
||||
<td style={{ padding: '10px 12px', color: 'var(--fg-muted)' }}>{r.owner}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination footer */}
|
||||
<div style={{
|
||||
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||||
paddingTop: 12, marginTop: 4,
|
||||
fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-muted)',
|
||||
}}>
|
||||
<span>Showing 1–{SAMPLE_ROWS.length} of 184</span>
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
<RptButton variant="neutral">‹ Prev</RptButton>
|
||||
<RptButton variant="neutral">Next ›</RptButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
window.RPT_PAGE = { ReportingPage };
|
||||
46
docs/design-system-redesign/ui_kits/reporting/index.html
Normal file
46
docs/design-system-redesign/ui_kits/reporting/index.html
Normal file
@@ -0,0 +1,46 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>STEAM Security · Reporting 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); }
|
||||
|
||||
/* Anchor scroll offset under the sticky tab strip */
|
||||
:target { scroll-margin-top: 120px; }
|
||||
|
||||
/* Hide scrollbars on the in-page sample regions */
|
||||
.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="ReportPrimitives.jsx"></script>
|
||||
<script type="text/babel" src="ReportingPage.jsx"></script>
|
||||
<script type="text/babel" src="KitDocs.jsx"></script>
|
||||
|
||||
<script type="text/babel">
|
||||
const { useState } = React;
|
||||
const { KitDocs } = window.RPT_DOCS;
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<main data-screen-label="Reporting Kit">
|
||||
<KitDocs />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user