feat: add bulk triage view component and related stories
- Exported BulkTriageViewComponent and its related types from findings module. - Created a new accessibility test suite for score components using axe-core. - Introduced design tokens for score components to standardize styling. - Enhanced score breakdown popover for mobile responsiveness with drag handle. - Added date range selector functionality to score history chart component. - Implemented unit tests for date range selector in score history chart. - Created Storybook stories for bulk triage view and score history chart with date range selector.
This commit is contained in:
113
docs/ui/components/README.md
Normal file
113
docs/ui/components/README.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# UI Components
|
||||
|
||||
This directory contains documentation for the StellaOps Angular UI components.
|
||||
|
||||
## Evidence-Weighted Score (EWS) Components
|
||||
|
||||
The EWS component suite provides visual representations of vulnerability scores based on evidence-weighted analysis.
|
||||
|
||||
### Core Components
|
||||
|
||||
| Component | Purpose | Location |
|
||||
|-----------|---------|----------|
|
||||
| [ScorePill](./score-pill.md) | Compact score display with bucket coloring | `shared/components/score/` |
|
||||
| [ScoreBreakdownPopover](./score-breakdown-popover.md) | Detailed score breakdown with dimensions | `shared/components/score/` |
|
||||
| [ScoreBadge](./score-badge.md) | Evidence flag badges (live-signal, proven-path, etc.) | `shared/components/score/` |
|
||||
| [ScoreHistoryChart](./score-history-chart.md) | Timeline visualization of score changes | `shared/components/score/` |
|
||||
|
||||
### Feature Components
|
||||
|
||||
| Component | Purpose | Location |
|
||||
|-----------|---------|----------|
|
||||
| [FindingsList](./findings-list.md) | Findings table with EWS integration | `features/findings/` |
|
||||
| [BulkTriageView](./bulk-triage-view.md) | Bulk triage interface with bucket summaries | `features/findings/` |
|
||||
|
||||
## Score Buckets
|
||||
|
||||
All EWS components use a consistent bucket system:
|
||||
|
||||
| Bucket | Score Range | Color | Priority |
|
||||
|--------|-------------|-------|----------|
|
||||
| Act Now | 90-100 | Red (`#DC2626`) | Critical - Immediate action required |
|
||||
| Schedule Next | 70-89 | Amber (`#D97706`) | High - Schedule for next sprint |
|
||||
| Investigate | 40-69 | Blue (`#2563EB`) | Medium - Investigate when possible |
|
||||
| Watchlist | 0-39 | Gray (`#6B7280`) | Low - Monitor for changes |
|
||||
|
||||
## Evidence Flags
|
||||
|
||||
Findings can have special flags indicating evidence quality:
|
||||
|
||||
| Flag | Icon | Color | Description |
|
||||
|------|------|-------|-------------|
|
||||
| `live-signal` | Signal wave | Green | Active runtime signals detected |
|
||||
| `proven-path` | Checkmark | Blue | Verified reachability path confirmed |
|
||||
| `vendor-na` | Strikethrough | Gray | Vendor marked as not affected |
|
||||
| `speculative` | Question mark | Orange | Evidence is speculative/unconfirmed |
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Import Components
|
||||
|
||||
```typescript
|
||||
// Score components
|
||||
import {
|
||||
ScorePillComponent,
|
||||
ScoreBreakdownPopoverComponent,
|
||||
ScoreBadgeComponent,
|
||||
ScoreHistoryChartComponent,
|
||||
} from '@app/shared/components/score';
|
||||
|
||||
// Findings components
|
||||
import {
|
||||
FindingsListComponent,
|
||||
BulkTriageViewComponent,
|
||||
} from '@app/features/findings';
|
||||
```
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```html
|
||||
<!-- Display a score pill -->
|
||||
<stella-score-pill [score]="78" size="md" />
|
||||
|
||||
<!-- Display score badges -->
|
||||
<stella-score-badge type="live-signal" />
|
||||
<stella-score-badge type="proven-path" />
|
||||
|
||||
<!-- Full findings list with scoring -->
|
||||
<app-findings-list
|
||||
[findings]="findings"
|
||||
[autoLoadScores]="true"
|
||||
(findingSelect)="onFindingSelect($event)"
|
||||
/>
|
||||
```
|
||||
|
||||
## Storybook
|
||||
|
||||
Interactive examples and documentation are available in Storybook:
|
||||
|
||||
```bash
|
||||
cd src/Web/StellaOps.Web
|
||||
npm run storybook
|
||||
```
|
||||
|
||||
Navigate to:
|
||||
- `Score/ScorePill` - Score pill variants
|
||||
- `Score/ScoreBreakdownPopover` - Breakdown popover examples
|
||||
- `Score/ScoreBadge` - Evidence flag badges
|
||||
- `Score/ScoreHistoryChart` - History chart variants
|
||||
- `Findings/FindingsList` - Findings list with scoring
|
||||
- `Findings/BulkTriageView` - Bulk triage interface
|
||||
|
||||
## Design Tokens
|
||||
|
||||
Score colors are defined as CSS custom properties. See [design-tokens.md](./design-tokens.md) for the full token reference.
|
||||
|
||||
## Accessibility
|
||||
|
||||
All components follow WCAG 2.1 AA guidelines:
|
||||
- Proper ARIA labels and roles
|
||||
- Keyboard navigation support
|
||||
- Focus management
|
||||
- Color contrast ratios meet AA standards
|
||||
- Screen reader announcements for dynamic content
|
||||
246
docs/ui/components/bulk-triage-view.md
Normal file
246
docs/ui/components/bulk-triage-view.md
Normal file
@@ -0,0 +1,246 @@
|
||||
# BulkTriageViewComponent
|
||||
|
||||
Streamlined interface for triaging multiple findings at once with bucket-based organization.
|
||||
|
||||
## Overview
|
||||
|
||||
The `BulkTriageViewComponent` provides a triage-focused view with bucket summary cards, one-click bucket selection, and bulk actions.
|
||||
|
||||
## Selector
|
||||
|
||||
```html
|
||||
<app-bulk-triage-view />
|
||||
```
|
||||
|
||||
## Inputs
|
||||
|
||||
| Input | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `findings` | `ScoredFinding[]` | `[]` | Array of scored findings |
|
||||
| `selectedIds` | `Set<string>` | `new Set()` | Currently selected finding IDs |
|
||||
| `processing` | `boolean` | `false` | Whether an action is in progress |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Output | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `selectionChange` | `EventEmitter<string[]>` | Emits when selection changes |
|
||||
| `actionRequest` | `EventEmitter<BulkActionRequest>` | Emits when an action is triggered |
|
||||
| `actionComplete` | `EventEmitter<BulkActionResult>` | Emits when an action completes |
|
||||
|
||||
## Action Types
|
||||
|
||||
```typescript
|
||||
type BulkActionType = 'acknowledge' | 'suppress' | 'assign' | 'escalate';
|
||||
|
||||
interface BulkActionRequest {
|
||||
action: BulkActionType;
|
||||
findingIds: string[];
|
||||
assignee?: string; // For 'assign' action
|
||||
reason?: string; // For 'suppress' action
|
||||
}
|
||||
|
||||
interface BulkActionResult {
|
||||
action: BulkActionType;
|
||||
findingIds: string[];
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}
|
||||
```
|
||||
|
||||
## UI Sections
|
||||
|
||||
### Bucket Summary Cards
|
||||
Four cards showing findings grouped by priority bucket:
|
||||
- **Act Now** (red): 90-100 score
|
||||
- **Schedule Next** (amber): 70-89 score
|
||||
- **Investigate** (blue): 40-69 score
|
||||
- **Watchlist** (gray): 0-39 score
|
||||
|
||||
Each card displays:
|
||||
- Bucket name and color
|
||||
- Finding count
|
||||
- "Select All" button
|
||||
- Selection indicator
|
||||
|
||||
### Action Bar
|
||||
Appears when findings are selected:
|
||||
- Selection count
|
||||
- Clear selection button
|
||||
- Action buttons: Acknowledge, Suppress, Assign, Escalate
|
||||
- Undo button (when history exists)
|
||||
|
||||
### Progress Overlay
|
||||
Shown during bulk operations:
|
||||
- Action name
|
||||
- Progress bar
|
||||
- Percentage complete
|
||||
- Items processed count
|
||||
|
||||
### Modals
|
||||
- **Assign Modal**: Email input for assignee
|
||||
- **Suppress Modal**: Text area for suppression reason
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```html
|
||||
<app-bulk-triage-view
|
||||
[findings]="scoredFindings"
|
||||
[selectedIds]="selectedIds"
|
||||
(selectionChange)="onSelectionChange($event)"
|
||||
(actionRequest)="onActionRequest($event)"
|
||||
/>
|
||||
```
|
||||
|
||||
### Full Implementation
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'app-triage-page',
|
||||
template: `
|
||||
<app-bulk-triage-view
|
||||
[findings]="findings()"
|
||||
[selectedIds]="selectedIds()"
|
||||
[processing]="processing()"
|
||||
(selectionChange)="updateSelection($event)"
|
||||
(actionRequest)="handleAction($event)"
|
||||
(actionComplete)="onActionComplete($event)"
|
||||
/>
|
||||
`
|
||||
})
|
||||
export class TriagePageComponent {
|
||||
findings = signal<ScoredFinding[]>([]);
|
||||
selectedIds = signal<Set<string>>(new Set());
|
||||
processing = signal(false);
|
||||
|
||||
private triageService = inject(TriageService);
|
||||
|
||||
updateSelection(ids: string[]): void {
|
||||
this.selectedIds.set(new Set(ids));
|
||||
}
|
||||
|
||||
async handleAction(request: BulkActionRequest): Promise<void> {
|
||||
this.processing.set(true);
|
||||
|
||||
try {
|
||||
switch (request.action) {
|
||||
case 'acknowledge':
|
||||
await this.triageService.acknowledge(request.findingIds);
|
||||
break;
|
||||
case 'suppress':
|
||||
await this.triageService.suppress(request.findingIds, request.reason!);
|
||||
break;
|
||||
case 'assign':
|
||||
await this.triageService.assign(request.findingIds, request.assignee!);
|
||||
break;
|
||||
case 'escalate':
|
||||
await this.triageService.escalate(request.findingIds);
|
||||
break;
|
||||
}
|
||||
|
||||
this.selectedIds.set(new Set());
|
||||
await this.refreshFindings();
|
||||
} finally {
|
||||
this.processing.set(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### With Toast Notifications
|
||||
|
||||
```html
|
||||
<app-bulk-triage-view
|
||||
[findings]="findings"
|
||||
[selectedIds]="selectedIds"
|
||||
(actionComplete)="showToast($event)"
|
||||
/>
|
||||
```
|
||||
|
||||
```typescript
|
||||
showToast(result: BulkActionResult): void {
|
||||
if (result.success) {
|
||||
this.toast.success(
|
||||
`${result.action} completed for ${result.findingIds.length} findings`
|
||||
);
|
||||
} else {
|
||||
this.toast.error(`Action failed: ${result.error}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Bucket Selection
|
||||
|
||||
### Select All in Bucket
|
||||
Click "Select All" on a bucket card to select all findings in that bucket.
|
||||
|
||||
### Toggle Bucket
|
||||
Clicking "Select All" when all bucket items are selected will deselect them.
|
||||
|
||||
### Partial Selection
|
||||
When some items in a bucket are selected, the button shows a partial indicator.
|
||||
|
||||
## Action Descriptions
|
||||
|
||||
| Action | Icon | Description |
|
||||
|--------|------|-------------|
|
||||
| Acknowledge | Checkmark | Mark findings as reviewed |
|
||||
| Suppress | Eye-off | Suppress with reason (opens modal) |
|
||||
| Assign | User | Assign to team member (opens modal) |
|
||||
| Escalate | Alert | Mark for urgent attention |
|
||||
|
||||
## Undo Capability
|
||||
|
||||
The component maintains an undo stack for recent actions:
|
||||
- Up to 5 operations stored
|
||||
- Undo restores previous selection
|
||||
- Toast shows "Undo" button after action
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Bucket summary has `aria-label="Findings by priority"`
|
||||
- Select All buttons have `aria-pressed` state
|
||||
- Action bar has `role="toolbar"`
|
||||
- Progress overlay announces to screen readers
|
||||
- Modals trap focus and support Escape to close
|
||||
- Action buttons have descriptive labels
|
||||
|
||||
## Keyboard Navigation
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| Tab | Navigate between elements |
|
||||
| Enter/Space | Activate buttons |
|
||||
| Escape | Close modals |
|
||||
|
||||
## Styling
|
||||
|
||||
```css
|
||||
app-bulk-triage-view {
|
||||
--bucket-card-padding: 16px;
|
||||
--bucket-card-radius: 8px;
|
||||
--action-bar-bg: #f9fafb;
|
||||
--modal-max-width: 400px;
|
||||
}
|
||||
|
||||
/* Bucket colors */
|
||||
.bucket-card.act-now { --bucket-color: #DC2626; }
|
||||
.bucket-card.schedule-next { --bucket-color: #D97706; }
|
||||
.bucket-card.investigate { --bucket-color: #2563EB; }
|
||||
.bucket-card.watchlist { --bucket-color: #6B7280; }
|
||||
```
|
||||
|
||||
## Responsive Behavior
|
||||
|
||||
| Breakpoint | Layout |
|
||||
|------------|--------|
|
||||
| > 640px | 4 bucket cards in row |
|
||||
| <= 640px | 2x2 grid, action labels hidden |
|
||||
|
||||
## Related Components
|
||||
|
||||
- [FindingsList](./findings-list.md) - Findings table view
|
||||
- [ScorePill](./score-pill.md) - Score display
|
||||
- [ScoreBadge](./score-badge.md) - Evidence flags
|
||||
334
docs/ui/components/design-tokens.md
Normal file
334
docs/ui/components/design-tokens.md
Normal file
@@ -0,0 +1,334 @@
|
||||
# Design Tokens
|
||||
|
||||
CSS custom properties (design tokens) for the Evidence-Weighted Score component suite.
|
||||
|
||||
## Score Colors
|
||||
|
||||
### Bucket Colors
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Act Now (90-100) - Critical priority */
|
||||
--score-bucket-act-now: #DC2626;
|
||||
--score-bucket-act-now-light: #FEE2E2;
|
||||
--score-bucket-act-now-dark: #991B1B;
|
||||
|
||||
/* Schedule Next (70-89) - High priority */
|
||||
--score-bucket-schedule-next: #D97706;
|
||||
--score-bucket-schedule-next-light: #FEF3C7;
|
||||
--score-bucket-schedule-next-dark: #92400E;
|
||||
|
||||
/* Investigate (40-69) - Medium priority */
|
||||
--score-bucket-investigate: #2563EB;
|
||||
--score-bucket-investigate-light: #DBEAFE;
|
||||
--score-bucket-investigate-dark: #1E40AF;
|
||||
|
||||
/* Watchlist (0-39) - Low priority */
|
||||
--score-bucket-watchlist: #6B7280;
|
||||
--score-bucket-watchlist-light: #F3F4F6;
|
||||
--score-bucket-watchlist-dark: #374151;
|
||||
}
|
||||
```
|
||||
|
||||
### Flag Colors
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Live Signal - Active runtime signals */
|
||||
--score-flag-live-signal: #16A34A;
|
||||
--score-flag-live-signal-light: #DCFCE7;
|
||||
--score-flag-live-signal-dark: #166534;
|
||||
|
||||
/* Proven Path - Verified reachability */
|
||||
--score-flag-proven-path: #2563EB;
|
||||
--score-flag-proven-path-light: #DBEAFE;
|
||||
--score-flag-proven-path-dark: #1E40AF;
|
||||
|
||||
/* Vendor N/A - Vendor not affected */
|
||||
--score-flag-vendor-na: #6B7280;
|
||||
--score-flag-vendor-na-light: #F3F4F6;
|
||||
--score-flag-vendor-na-dark: #374151;
|
||||
|
||||
/* Speculative - Unconfirmed evidence */
|
||||
--score-flag-speculative: #D97706;
|
||||
--score-flag-speculative-light: #FEF3C7;
|
||||
--score-flag-speculative-dark: #92400E;
|
||||
}
|
||||
```
|
||||
|
||||
## Component Tokens
|
||||
|
||||
### ScorePill
|
||||
|
||||
```css
|
||||
:root {
|
||||
--score-pill-font-family: system-ui, -apple-system, sans-serif;
|
||||
--score-pill-font-weight: 600;
|
||||
--score-pill-border-radius: 4px;
|
||||
|
||||
/* Size: Small */
|
||||
--score-pill-sm-height: 20px;
|
||||
--score-pill-sm-min-width: 24px;
|
||||
--score-pill-sm-padding: 0 4px;
|
||||
--score-pill-sm-font-size: 12px;
|
||||
|
||||
/* Size: Medium */
|
||||
--score-pill-md-height: 24px;
|
||||
--score-pill-md-min-width: 32px;
|
||||
--score-pill-md-padding: 0 6px;
|
||||
--score-pill-md-font-size: 14px;
|
||||
|
||||
/* Size: Large */
|
||||
--score-pill-lg-height: 28px;
|
||||
--score-pill-lg-min-width: 40px;
|
||||
--score-pill-lg-padding: 0 8px;
|
||||
--score-pill-lg-font-size: 16px;
|
||||
|
||||
/* Interactive states */
|
||||
--score-pill-hover-scale: 1.05;
|
||||
--score-pill-focus-ring: 2px solid var(--color-focus);
|
||||
--score-pill-focus-offset: 2px;
|
||||
}
|
||||
```
|
||||
|
||||
### ScoreBadge
|
||||
|
||||
```css
|
||||
:root {
|
||||
--score-badge-font-family: system-ui, -apple-system, sans-serif;
|
||||
--score-badge-font-weight: 500;
|
||||
--score-badge-border-radius: 4px;
|
||||
|
||||
/* Size: Small */
|
||||
--score-badge-sm-height: 20px;
|
||||
--score-badge-sm-padding: 2px 6px;
|
||||
--score-badge-sm-font-size: 11px;
|
||||
--score-badge-sm-icon-size: 12px;
|
||||
|
||||
/* Size: Medium */
|
||||
--score-badge-md-height: 24px;
|
||||
--score-badge-md-padding: 4px 8px;
|
||||
--score-badge-md-font-size: 12px;
|
||||
--score-badge-md-icon-size: 14px;
|
||||
|
||||
/* Icon-only mode */
|
||||
--score-badge-icon-only-size: 20px;
|
||||
--score-badge-icon-only-padding: 4px;
|
||||
}
|
||||
```
|
||||
|
||||
### ScoreBreakdownPopover
|
||||
|
||||
```css
|
||||
:root {
|
||||
--score-popover-max-width: 360px;
|
||||
--score-popover-padding: 16px;
|
||||
--score-popover-border-radius: 8px;
|
||||
--score-popover-background: #FFFFFF;
|
||||
--score-popover-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
|
||||
/* Dimension bars */
|
||||
--score-dimension-bar-height: 8px;
|
||||
--score-dimension-bar-radius: 4px;
|
||||
--score-dimension-bar-bg: #E5E7EB;
|
||||
|
||||
/* Header */
|
||||
--score-popover-header-font-size: 24px;
|
||||
--score-popover-header-font-weight: 700;
|
||||
|
||||
/* Labels */
|
||||
--score-popover-label-font-size: 12px;
|
||||
--score-popover-label-color: #6B7280;
|
||||
|
||||
/* Explanations */
|
||||
--score-popover-explanation-font-size: 13px;
|
||||
--score-popover-explanation-color: #374151;
|
||||
}
|
||||
```
|
||||
|
||||
### ScoreHistoryChart
|
||||
|
||||
```css
|
||||
:root {
|
||||
--score-chart-line-color: #3B82F6;
|
||||
--score-chart-line-width: 2px;
|
||||
--score-chart-area-fill: rgba(59, 130, 246, 0.1);
|
||||
--score-chart-area-gradient-start: rgba(59, 130, 246, 0.2);
|
||||
--score-chart-area-gradient-end: rgba(59, 130, 246, 0);
|
||||
|
||||
/* Data points */
|
||||
--score-chart-point-size: 6px;
|
||||
--score-chart-point-hover-size: 8px;
|
||||
--score-chart-point-border-width: 2px;
|
||||
--score-chart-point-border-color: #FFFFFF;
|
||||
|
||||
/* Grid */
|
||||
--score-chart-grid-color: #E5E7EB;
|
||||
--score-chart-grid-width: 1px;
|
||||
|
||||
/* Bands */
|
||||
--score-chart-band-opacity: 0.1;
|
||||
|
||||
/* Axis */
|
||||
--score-chart-axis-color: #9CA3AF;
|
||||
--score-chart-axis-font-size: 11px;
|
||||
|
||||
/* Tooltip */
|
||||
--score-chart-tooltip-bg: #1F2937;
|
||||
--score-chart-tooltip-color: #FFFFFF;
|
||||
--score-chart-tooltip-padding: 8px 12px;
|
||||
--score-chart-tooltip-radius: 6px;
|
||||
}
|
||||
```
|
||||
|
||||
## Dark Mode
|
||||
|
||||
```css
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
/* Backgrounds */
|
||||
--score-popover-background: #1F2937;
|
||||
--score-chart-tooltip-bg: #374151;
|
||||
|
||||
/* Text */
|
||||
--score-popover-label-color: #D1D5DB;
|
||||
--score-popover-explanation-color: #F9FAFB;
|
||||
|
||||
/* Borders */
|
||||
--score-dimension-bar-bg: #374151;
|
||||
--score-chart-grid-color: #374151;
|
||||
|
||||
/* Adjust bucket light colors for dark mode */
|
||||
--score-bucket-act-now-light: rgba(220, 38, 38, 0.2);
|
||||
--score-bucket-schedule-next-light: rgba(217, 119, 6, 0.2);
|
||||
--score-bucket-investigate-light: rgba(37, 99, 235, 0.2);
|
||||
--score-bucket-watchlist-light: rgba(107, 114, 128, 0.2);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Semantic Tokens
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Focus states */
|
||||
--color-focus: #3B82F6;
|
||||
--color-focus-ring: rgba(59, 130, 246, 0.3);
|
||||
|
||||
/* Interactive */
|
||||
--color-interactive: #3B82F6;
|
||||
--color-interactive-hover: #2563EB;
|
||||
--color-interactive-active: #1D4ED8;
|
||||
|
||||
/* Status */
|
||||
--color-success: #16A34A;
|
||||
--color-warning: #D97706;
|
||||
--color-error: #DC2626;
|
||||
--color-info: #2563EB;
|
||||
|
||||
/* Neutral */
|
||||
--color-text-primary: #111827;
|
||||
--color-text-secondary: #6B7280;
|
||||
--color-text-disabled: #9CA3AF;
|
||||
--color-border: #E5E7EB;
|
||||
--color-background: #FFFFFF;
|
||||
--color-surface: #F9FAFB;
|
||||
}
|
||||
```
|
||||
|
||||
## Usage in Components
|
||||
|
||||
### SCSS Import
|
||||
|
||||
```scss
|
||||
// styles/_tokens.scss
|
||||
@use 'sass:map';
|
||||
|
||||
$score-buckets: (
|
||||
'act-now': (
|
||||
color: var(--score-bucket-act-now),
|
||||
light: var(--score-bucket-act-now-light),
|
||||
dark: var(--score-bucket-act-now-dark),
|
||||
),
|
||||
'schedule-next': (
|
||||
color: var(--score-bucket-schedule-next),
|
||||
light: var(--score-bucket-schedule-next-light),
|
||||
dark: var(--score-bucket-schedule-next-dark),
|
||||
),
|
||||
'investigate': (
|
||||
color: var(--score-bucket-investigate),
|
||||
light: var(--score-bucket-investigate-light),
|
||||
dark: var(--score-bucket-investigate-dark),
|
||||
),
|
||||
'watchlist': (
|
||||
color: var(--score-bucket-watchlist),
|
||||
light: var(--score-bucket-watchlist-light),
|
||||
dark: var(--score-bucket-watchlist-dark),
|
||||
),
|
||||
);
|
||||
|
||||
@mixin bucket-colors($bucket) {
|
||||
$colors: map.get($score-buckets, $bucket);
|
||||
background-color: map.get($colors, color);
|
||||
color: white;
|
||||
|
||||
&.light {
|
||||
background-color: map.get($colors, light);
|
||||
color: map.get($colors, dark);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### TypeScript Constants
|
||||
|
||||
```typescript
|
||||
// score-colors.ts
|
||||
export const SCORE_BUCKET_COLORS = {
|
||||
ActNow: {
|
||||
bg: '#DC2626',
|
||||
light: '#FEE2E2',
|
||||
dark: '#991B1B',
|
||||
},
|
||||
ScheduleNext: {
|
||||
bg: '#D97706',
|
||||
light: '#FEF3C7',
|
||||
dark: '#92400E',
|
||||
},
|
||||
Investigate: {
|
||||
bg: '#2563EB',
|
||||
light: '#DBEAFE',
|
||||
dark: '#1E40AF',
|
||||
},
|
||||
Watchlist: {
|
||||
bg: '#6B7280',
|
||||
light: '#F3F4F6',
|
||||
dark: '#374151',
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const SCORE_FLAG_COLORS = {
|
||||
'live-signal': { bg: '#16A34A', light: '#DCFCE7' },
|
||||
'proven-path': { bg: '#2563EB', light: '#DBEAFE' },
|
||||
'vendor-na': { bg: '#6B7280', light: '#F3F4F6' },
|
||||
'speculative': { bg: '#D97706', light: '#FEF3C7' },
|
||||
} as const;
|
||||
|
||||
export function getBucketColor(score: number): string {
|
||||
if (score >= 90) return SCORE_BUCKET_COLORS.ActNow.bg;
|
||||
if (score >= 70) return SCORE_BUCKET_COLORS.ScheduleNext.bg;
|
||||
if (score >= 40) return SCORE_BUCKET_COLORS.Investigate.bg;
|
||||
return SCORE_BUCKET_COLORS.Watchlist.bg;
|
||||
}
|
||||
```
|
||||
|
||||
## Accessibility Notes
|
||||
|
||||
- All color combinations meet WCAG 2.1 AA contrast requirements (4.5:1 for text)
|
||||
- Bucket colors use both color and position/labels for identification
|
||||
- Flag badges use icons in addition to colors
|
||||
- Focus states use high-contrast ring colors
|
||||
|
||||
## Related Files
|
||||
|
||||
- `src/Web/StellaOps.Web/src/styles/_tokens.scss` - SCSS token definitions
|
||||
- `src/Web/StellaOps.Web/src/app/core/constants/score-colors.ts` - TypeScript constants
|
||||
260
docs/ui/components/findings-list.md
Normal file
260
docs/ui/components/findings-list.md
Normal file
@@ -0,0 +1,260 @@
|
||||
# FindingsListComponent
|
||||
|
||||
Comprehensive findings list with Evidence-Weighted Score (EWS) integration, filtering, and bulk selection.
|
||||
|
||||
## Overview
|
||||
|
||||
The `FindingsListComponent` displays a sortable, filterable table of vulnerability findings with integrated score loading and display.
|
||||
|
||||
## Selector
|
||||
|
||||
```html
|
||||
<app-findings-list />
|
||||
```
|
||||
|
||||
## Inputs
|
||||
|
||||
| Input | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `findings` | `Finding[]` | `[]` | Array of findings to display |
|
||||
| `autoLoadScores` | `boolean` | `true` | Auto-fetch scores when findings change |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Output | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `findingSelect` | `EventEmitter<ScoredFinding>` | Emits when a finding row is clicked |
|
||||
| `selectionChange` | `EventEmitter<string[]>` | Emits finding IDs when selection changes |
|
||||
|
||||
## Data Structures
|
||||
|
||||
### Finding
|
||||
|
||||
```typescript
|
||||
interface Finding {
|
||||
id: string; // Unique finding ID
|
||||
advisoryId: string; // CVE/GHSA ID
|
||||
packageName: string;
|
||||
packageVersion: string;
|
||||
severity: 'critical' | 'high' | 'medium' | 'low';
|
||||
status: 'open' | 'in_progress' | 'fixed' | 'excepted';
|
||||
publishedAt?: string; // ISO 8601
|
||||
}
|
||||
```
|
||||
|
||||
### ScoredFinding
|
||||
|
||||
```typescript
|
||||
interface ScoredFinding extends Finding {
|
||||
score?: EvidenceWeightedScoreResult;
|
||||
scoreLoading: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
## Table Columns
|
||||
|
||||
| Column | Description | Sortable |
|
||||
|--------|-------------|----------|
|
||||
| Checkbox | Bulk selection | No |
|
||||
| Score | EWS score pill with flags | Yes |
|
||||
| Advisory | CVE/GHSA identifier | Yes |
|
||||
| Package | Package name and version | Yes |
|
||||
| Severity | CVSS severity level | Yes |
|
||||
| Status | Finding status | Yes |
|
||||
|
||||
## Features
|
||||
|
||||
### Score Loading
|
||||
When `autoLoadScores` is true, scores are fetched automatically via the `SCORING_API` injection token.
|
||||
|
||||
### Bucket Filtering
|
||||
Filter findings by priority bucket using the chip filters:
|
||||
- All (default)
|
||||
- Act Now (90-100)
|
||||
- Schedule Next (70-89)
|
||||
- Investigate (40-69)
|
||||
- Watchlist (0-39)
|
||||
|
||||
### Flag Filtering
|
||||
Filter by active score flags:
|
||||
- Live Signal
|
||||
- Proven Path
|
||||
- Vendor N/A
|
||||
- Speculative
|
||||
|
||||
### Search
|
||||
Text search across advisory ID and package name.
|
||||
|
||||
### Sorting
|
||||
Click column headers to sort. Click again to reverse order.
|
||||
|
||||
### Bulk Selection
|
||||
- Click checkboxes to select individual findings
|
||||
- Use "Select All" to select visible findings
|
||||
- Selection persists across filter changes
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```html
|
||||
<app-findings-list
|
||||
[findings]="findings"
|
||||
(findingSelect)="openFinding($event)"
|
||||
/>
|
||||
```
|
||||
|
||||
### Without Auto-Loading Scores
|
||||
|
||||
```html
|
||||
<app-findings-list
|
||||
[findings]="findings"
|
||||
[autoLoadScores]="false"
|
||||
/>
|
||||
```
|
||||
|
||||
### With Selection Handling
|
||||
|
||||
```html
|
||||
<app-findings-list
|
||||
[findings]="findings"
|
||||
[autoLoadScores]="true"
|
||||
(selectionChange)="onSelectionChange($event)"
|
||||
/>
|
||||
|
||||
<div class="bulk-actions" *ngIf="selectedIds.length > 0">
|
||||
<button (click)="acknowledgeSelected()">
|
||||
Acknowledge ({{ selectedIds.length }})
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
```typescript
|
||||
selectedIds: string[] = [];
|
||||
|
||||
onSelectionChange(ids: string[]): void {
|
||||
this.selectedIds = ids;
|
||||
}
|
||||
```
|
||||
|
||||
### Full Feature Example
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'app-vulnerability-dashboard',
|
||||
template: `
|
||||
<div class="dashboard">
|
||||
<h1>Vulnerabilities</h1>
|
||||
|
||||
<app-findings-list
|
||||
[findings]="findings()"
|
||||
[autoLoadScores]="true"
|
||||
(findingSelect)="openFindingDetail($event)"
|
||||
(selectionChange)="updateSelection($event)"
|
||||
/>
|
||||
|
||||
@if (selectedFinding()) {
|
||||
<app-finding-detail-panel
|
||||
[finding]="selectedFinding()"
|
||||
(close)="closeFindingDetail()"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export class VulnerabilityDashboardComponent {
|
||||
findings = signal<Finding[]>([]);
|
||||
selectedFinding = signal<ScoredFinding | null>(null);
|
||||
selectedIds = signal<string[]>([]);
|
||||
|
||||
constructor(private findingsService: FindingsService) {
|
||||
this.loadFindings();
|
||||
}
|
||||
|
||||
async loadFindings(): Promise<void> {
|
||||
this.findings.set(await this.findingsService.getFindings());
|
||||
}
|
||||
|
||||
openFindingDetail(finding: ScoredFinding): void {
|
||||
this.selectedFinding.set(finding);
|
||||
}
|
||||
|
||||
closeFindingDetail(): void {
|
||||
this.selectedFinding.set(null);
|
||||
}
|
||||
|
||||
updateSelection(ids: string[]): void {
|
||||
this.selectedIds.set(ids);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Dependency Injection
|
||||
|
||||
The component requires a `SCORING_API` provider:
|
||||
|
||||
```typescript
|
||||
import { SCORING_API, ScoringApiService } from '@app/core/services/scoring.service';
|
||||
|
||||
@NgModule({
|
||||
providers: [
|
||||
{ provide: SCORING_API, useClass: ScoringApiService }
|
||||
]
|
||||
})
|
||||
export class AppModule {}
|
||||
```
|
||||
|
||||
### Mock API for Testing
|
||||
|
||||
```typescript
|
||||
import { MockScoringApi } from '@app/core/services/scoring.service';
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: SCORING_API, useClass: MockScoringApi }
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
## Empty States
|
||||
|
||||
### No Findings
|
||||
|
||||
```html
|
||||
<!-- Displays: "No findings to display" -->
|
||||
<app-findings-list [findings]="[]" />
|
||||
```
|
||||
|
||||
### No Matches
|
||||
|
||||
When filters result in no matches:
|
||||
```html
|
||||
<!-- Displays: "No findings match the current filters" -->
|
||||
```
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Proper table semantics with `<table>`, `<thead>`, `<tbody>`
|
||||
- Sortable columns use `aria-sort`
|
||||
- Checkboxes have accessible labels
|
||||
- Filter chips are keyboard navigable
|
||||
- Focus management on filter/sort changes
|
||||
- Screen reader announces result counts
|
||||
|
||||
## Styling
|
||||
|
||||
```css
|
||||
app-findings-list {
|
||||
--table-header-bg: #f9fafb;
|
||||
--table-border-color: #e5e7eb;
|
||||
--table-row-hover: #f3f4f6;
|
||||
--table-row-selected: #eff6ff;
|
||||
}
|
||||
```
|
||||
|
||||
## Related Components
|
||||
|
||||
- [ScorePill](./score-pill.md) - Score display in table
|
||||
- [ScoreBadge](./score-badge.md) - Flag badges in table
|
||||
- [ScoreBreakdownPopover](./score-breakdown-popover.md) - Score details on click
|
||||
- [BulkTriageView](./bulk-triage-view.md) - Bulk operations
|
||||
166
docs/ui/components/score-badge.md
Normal file
166
docs/ui/components/score-badge.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# ScoreBadgeComponent
|
||||
|
||||
Score badge component displaying evidence flags with icons and labels.
|
||||
|
||||
## Overview
|
||||
|
||||
The `ScoreBadgeComponent` displays evidence quality flags that provide context about score reliability and data sources.
|
||||
|
||||
## Selector
|
||||
|
||||
```html
|
||||
<stella-score-badge />
|
||||
```
|
||||
|
||||
## Inputs
|
||||
|
||||
| Input | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `type` | `ScoreFlag` | required | The flag type to display |
|
||||
| `size` | `'sm' \| 'md'` | `'md'` | Size variant |
|
||||
| `showTooltip` | `boolean` | `true` | Show description on hover |
|
||||
| `showLabel` | `boolean` | `true` | Show label text (false = icon only) |
|
||||
|
||||
## Flag Types
|
||||
|
||||
| Type | Icon | Color | Description |
|
||||
|------|------|-------|-------------|
|
||||
| `live-signal` | Signal wave | Green (`#16A34A`) | Active runtime signals detected from deployed environments |
|
||||
| `proven-path` | Checkmark | Blue (`#2563EB`) | Verified reachability path to vulnerable code |
|
||||
| `vendor-na` | Strikethrough | Gray (`#6B7280`) | Vendor has marked as not affected |
|
||||
| `speculative` | Question mark | Orange (`#D97706`) | Evidence is speculative or unconfirmed |
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```html
|
||||
<stella-score-badge type="live-signal" />
|
||||
```
|
||||
|
||||
### All Flag Types
|
||||
|
||||
```html
|
||||
<stella-score-badge type="live-signal" />
|
||||
<stella-score-badge type="proven-path" />
|
||||
<stella-score-badge type="vendor-na" />
|
||||
<stella-score-badge type="speculative" />
|
||||
```
|
||||
|
||||
### Size Variants
|
||||
|
||||
```html
|
||||
<stella-score-badge type="proven-path" size="sm" />
|
||||
<stella-score-badge type="proven-path" size="md" />
|
||||
```
|
||||
|
||||
### Icon-Only Mode
|
||||
|
||||
```html
|
||||
<stella-score-badge type="live-signal" [showLabel]="false" />
|
||||
```
|
||||
|
||||
### With Score Pill
|
||||
|
||||
```html
|
||||
<div class="score-display">
|
||||
<stella-score-pill [score]="92" />
|
||||
<div class="flags">
|
||||
<stella-score-badge type="live-signal" size="sm" />
|
||||
<stella-score-badge type="proven-path" size="sm" />
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Rendering from Flags Array
|
||||
|
||||
```html
|
||||
@for (flag of scoreResult.flags; track flag) {
|
||||
<stella-score-badge [type]="flag" size="sm" />
|
||||
}
|
||||
```
|
||||
|
||||
### In a Table
|
||||
|
||||
```html
|
||||
<td class="flags-column">
|
||||
@for (flag of finding.score?.flags; track flag) {
|
||||
<stella-score-badge
|
||||
[type]="flag"
|
||||
size="sm"
|
||||
[showLabel]="false"
|
||||
/>
|
||||
}
|
||||
</td>
|
||||
```
|
||||
|
||||
## Flag Significance
|
||||
|
||||
### live-signal (Green - High Confidence)
|
||||
Indicates the vulnerability affects code that is actively being executed in production. This is the highest confidence indicator and typically elevates priority.
|
||||
|
||||
**Sources:**
|
||||
- Runtime telemetry from deployed containers
|
||||
- Function call traces
|
||||
- Code coverage data from production
|
||||
|
||||
### proven-path (Blue - Confirmed)
|
||||
A verified call path from application entry points to the vulnerable function has been confirmed through static or dynamic analysis.
|
||||
|
||||
**Sources:**
|
||||
- Static reachability analysis
|
||||
- Dynamic call graph analysis
|
||||
- Fuzzing results
|
||||
|
||||
### vendor-na (Gray - Vendor Override)
|
||||
The software vendor has issued a VEX statement indicating this vulnerability does not affect their product configuration or version.
|
||||
|
||||
**Sources:**
|
||||
- Vendor VEX documents
|
||||
- CSAF advisories
|
||||
- Distro security teams
|
||||
|
||||
### speculative (Orange - Unconfirmed)
|
||||
The evidence for this vulnerability is speculative or based on incomplete analysis. Score caps are typically applied.
|
||||
|
||||
**Sources:**
|
||||
- Incomplete static analysis
|
||||
- Heuristic-based detection
|
||||
- Unverified reports
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Uses `role="img"` with descriptive `aria-label`
|
||||
- Tooltip shown on focus for keyboard users
|
||||
- Icon colors meet WCAG AA contrast requirements
|
||||
- Screen reader announces full flag description
|
||||
|
||||
## Styling
|
||||
|
||||
```css
|
||||
stella-score-badge {
|
||||
--badge-font-size: 12px;
|
||||
--badge-padding: 4px 8px;
|
||||
--badge-border-radius: 4px;
|
||||
}
|
||||
```
|
||||
|
||||
### Live Signal Animation
|
||||
|
||||
The live-signal badge features a subtle pulse animation to draw attention:
|
||||
|
||||
```css
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
.live-signal-badge {
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
```
|
||||
|
||||
## Related Components
|
||||
|
||||
- [ScorePill](./score-pill.md) - Compact score display
|
||||
- [ScoreBreakdownPopover](./score-breakdown-popover.md) - Full breakdown with flags section
|
||||
172
docs/ui/components/score-breakdown-popover.md
Normal file
172
docs/ui/components/score-breakdown-popover.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# ScoreBreakdownPopoverComponent
|
||||
|
||||
Detailed score breakdown popover showing all evidence dimensions, flags, and explanations.
|
||||
|
||||
## Overview
|
||||
|
||||
The `ScoreBreakdownPopoverComponent` displays a comprehensive breakdown of an evidence-weighted score, including dimension bars, active flags, guardrails, and human-readable explanations.
|
||||
|
||||
## Selector
|
||||
|
||||
```html
|
||||
<stella-score-breakdown-popover />
|
||||
```
|
||||
|
||||
## Inputs
|
||||
|
||||
| Input | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `scoreResult` | `EvidenceWeightedScoreResult` | required | Full score result object |
|
||||
| `position` | `PopoverPosition` | `'bottom'` | Popover placement |
|
||||
|
||||
## Position Options
|
||||
|
||||
```typescript
|
||||
type PopoverPosition = 'top' | 'bottom' | 'left' | 'right';
|
||||
```
|
||||
|
||||
## Score Result Structure
|
||||
|
||||
```typescript
|
||||
interface EvidenceWeightedScoreResult {
|
||||
findingId: string;
|
||||
score: number; // 0-100
|
||||
bucket: ScoreBucket; // 'ActNow' | 'ScheduleNext' | 'Investigate' | 'Watchlist'
|
||||
inputs: {
|
||||
rch: number; // Reachability (0-1)
|
||||
rts: number; // Runtime signals (0-1)
|
||||
bkp: number; // Backport availability (0-1)
|
||||
xpl: number; // Exploitability (0-1)
|
||||
src: number; // Source trust (0-1)
|
||||
mit: number; // Mitigations (0-1)
|
||||
};
|
||||
weights: {
|
||||
rch: number;
|
||||
rts: number;
|
||||
bkp: number;
|
||||
xpl: number;
|
||||
src: number;
|
||||
mit: number;
|
||||
};
|
||||
flags: ScoreFlag[];
|
||||
explanations: string[];
|
||||
caps: {
|
||||
speculativeCap: boolean;
|
||||
notAffectedCap: boolean;
|
||||
runtimeFloor: boolean;
|
||||
};
|
||||
policyDigest: string;
|
||||
calculatedAt: string; // ISO 8601
|
||||
}
|
||||
```
|
||||
|
||||
## Dimension Labels
|
||||
|
||||
| Key | Label | Description |
|
||||
|-----|-------|-------------|
|
||||
| `rch` | Reachability | Path to vulnerable code exists |
|
||||
| `rts` | Runtime Signals | Active usage detected in production |
|
||||
| `bkp` | Backport | Fix backported to current version |
|
||||
| `xpl` | Exploitability | EPSS probability, known exploits |
|
||||
| `src` | Source Trust | Advisory source reliability |
|
||||
| `mit` | Mitigations | Active mitigations reduce risk |
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```html
|
||||
<stella-score-breakdown-popover [scoreResult]="scoreResult" />
|
||||
```
|
||||
|
||||
### With Position
|
||||
|
||||
```html
|
||||
<stella-score-breakdown-popover
|
||||
[scoreResult]="scoreResult"
|
||||
position="right"
|
||||
/>
|
||||
```
|
||||
|
||||
### Triggered from Score Pill
|
||||
|
||||
```html
|
||||
<div class="score-container">
|
||||
<stella-score-pill
|
||||
[score]="score"
|
||||
(scoreClick)="showPopover = true"
|
||||
/>
|
||||
|
||||
@if (showPopover) {
|
||||
<stella-score-breakdown-popover
|
||||
[scoreResult]="scoreResult"
|
||||
(close)="showPopover = false"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
```
|
||||
|
||||
### In a Dialog
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
template: `
|
||||
<div class="dialog-content">
|
||||
<h2>Score Details</h2>
|
||||
<stella-score-breakdown-popover
|
||||
[scoreResult]="scoreResult"
|
||||
/>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export class ScoreDialogComponent {
|
||||
scoreResult = input.required<EvidenceWeightedScoreResult>();
|
||||
}
|
||||
```
|
||||
|
||||
## Popover Sections
|
||||
|
||||
### 1. Header
|
||||
Displays the overall score with bucket label and color.
|
||||
|
||||
### 2. Dimensions Chart
|
||||
Horizontal bar chart showing all six dimensions with their normalized values (0-100%).
|
||||
|
||||
### 3. Flags Section
|
||||
Active flags displayed as badges. See [ScoreBadge](./score-badge.md) for flag types.
|
||||
|
||||
### 4. Guardrails Section
|
||||
Applied caps and floors:
|
||||
- **Speculative Cap**: Score limited due to unconfirmed evidence
|
||||
- **Not Affected Cap**: Score reduced due to vendor VEX
|
||||
- **Runtime Floor**: Score elevated due to active runtime signals
|
||||
|
||||
### 5. Explanations
|
||||
Human-readable explanations of factors affecting the score.
|
||||
|
||||
### 6. Footer
|
||||
- Policy digest (truncated SHA-256)
|
||||
- Calculation timestamp
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Uses `role="dialog"` with `aria-labelledby`
|
||||
- Focus trapped within popover when open
|
||||
- Escape key closes popover
|
||||
- Click outside closes popover
|
||||
- Screen reader announces dimension values
|
||||
|
||||
## Styling
|
||||
|
||||
```css
|
||||
stella-score-breakdown-popover {
|
||||
--popover-max-width: 360px;
|
||||
--popover-background: white;
|
||||
--popover-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
```
|
||||
|
||||
## Related Components
|
||||
|
||||
- [ScorePill](./score-pill.md) - Compact score display
|
||||
- [ScoreBadge](./score-badge.md) - Evidence flag badges
|
||||
217
docs/ui/components/score-history-chart.md
Normal file
217
docs/ui/components/score-history-chart.md
Normal file
@@ -0,0 +1,217 @@
|
||||
# ScoreHistoryChartComponent
|
||||
|
||||
Timeline visualization showing how a finding's evidence-weighted score has changed over time.
|
||||
|
||||
## Overview
|
||||
|
||||
The `ScoreHistoryChartComponent` renders an SVG line chart displaying score history with interactive data points and bucket-colored bands.
|
||||
|
||||
## Selector
|
||||
|
||||
```html
|
||||
<stella-score-history-chart />
|
||||
```
|
||||
|
||||
## Inputs
|
||||
|
||||
| Input | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `history` | `ScoreHistoryEntry[]` | `[]` | Array of historical score entries |
|
||||
| `width` | `number \| 'auto'` | `'auto'` | Chart width in pixels |
|
||||
| `height` | `number` | `200` | Chart height in pixels |
|
||||
| `showBands` | `boolean` | `true` | Show bucket background bands |
|
||||
| `showGrid` | `boolean` | `true` | Show horizontal grid lines |
|
||||
| `showRangeSelector` | `boolean` | `false` | Show date range filter controls |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Output | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `pointHover` | `EventEmitter<ScoreHistoryEntry>` | Emits when hovering over a data point |
|
||||
| `pointClick` | `EventEmitter<ScoreHistoryEntry>` | Emits when clicking a data point |
|
||||
|
||||
## History Entry Structure
|
||||
|
||||
```typescript
|
||||
interface ScoreHistoryEntry {
|
||||
score: number; // 0-100
|
||||
bucket: ScoreBucket;
|
||||
policyDigest: string; // SHA-256 of active policy
|
||||
calculatedAt: string; // ISO 8601 timestamp
|
||||
trigger: ScoreChangeTrigger;
|
||||
changedFactors: string[]; // ['rch', 'rts', ...]
|
||||
}
|
||||
|
||||
type ScoreChangeTrigger =
|
||||
| 'evidence_update' // New evidence received
|
||||
| 'policy_change' // Scoring policy modified
|
||||
| 'scheduled'; // Periodic recalculation
|
||||
```
|
||||
|
||||
## Chart Features
|
||||
|
||||
### Bucket Bands
|
||||
Colored background regions showing score thresholds:
|
||||
- Act Now (90-100): Light red
|
||||
- Schedule Next (70-89): Light amber
|
||||
- Investigate (40-69): Light blue
|
||||
- Watchlist (0-39): Light gray
|
||||
|
||||
### Data Points
|
||||
Interactive markers with trigger-type indicators:
|
||||
- Circle: `evidence_update`
|
||||
- Diamond: `policy_change`
|
||||
- Square: `scheduled`
|
||||
|
||||
### Tooltips
|
||||
Hover over any point to see:
|
||||
- Score and bucket
|
||||
- Timestamp
|
||||
- Trigger type
|
||||
- Changed factors (if any)
|
||||
|
||||
### Date Range Selector
|
||||
When enabled, provides filtering options:
|
||||
- Preset ranges: 7d, 30d, 90d, 1y, All
|
||||
- Custom date range picker
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```html
|
||||
<stella-score-history-chart [history]="scoreHistory" />
|
||||
```
|
||||
|
||||
### Custom Dimensions
|
||||
|
||||
```html
|
||||
<stella-score-history-chart
|
||||
[history]="scoreHistory"
|
||||
[width]="600"
|
||||
[height]="250"
|
||||
/>
|
||||
```
|
||||
|
||||
### Minimal Chart
|
||||
|
||||
```html
|
||||
<stella-score-history-chart
|
||||
[history]="scoreHistory"
|
||||
[showBands]="false"
|
||||
[showGrid]="false"
|
||||
[height]="150"
|
||||
/>
|
||||
```
|
||||
|
||||
### With Date Range Selector
|
||||
|
||||
```html
|
||||
<stella-score-history-chart
|
||||
[history]="extendedHistory"
|
||||
[showRangeSelector]="true"
|
||||
/>
|
||||
```
|
||||
|
||||
### Responsive Width
|
||||
|
||||
```html
|
||||
<div class="chart-container">
|
||||
<stella-score-history-chart
|
||||
[history]="scoreHistory"
|
||||
width="auto"
|
||||
/>
|
||||
</div>
|
||||
```
|
||||
|
||||
```css
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
min-width: 300px;
|
||||
}
|
||||
```
|
||||
|
||||
### With Event Handlers
|
||||
|
||||
```html
|
||||
<stella-score-history-chart
|
||||
[history]="scoreHistory"
|
||||
(pointClick)="showEntryDetails($event)"
|
||||
(pointHover)="updateTooltip($event)"
|
||||
/>
|
||||
```
|
||||
|
||||
```typescript
|
||||
showEntryDetails(entry: ScoreHistoryEntry): void {
|
||||
console.log('Clicked entry:', entry);
|
||||
this.selectedEntry = entry;
|
||||
}
|
||||
|
||||
updateTooltip(entry: ScoreHistoryEntry | null): void {
|
||||
this.hoveredEntry = entry;
|
||||
}
|
||||
```
|
||||
|
||||
## Date Range Presets
|
||||
|
||||
| Preset | Label | Filter |
|
||||
|--------|-------|--------|
|
||||
| `7d` | Last 7 days | Entries from past week |
|
||||
| `30d` | Last 30 days | Entries from past month |
|
||||
| `90d` | Last 90 days | Entries from past quarter |
|
||||
| `1y` | Last year | Entries from past 12 months |
|
||||
| `all` | All time | No filtering |
|
||||
| `custom` | Custom range | User-selected dates |
|
||||
|
||||
## Visualization Details
|
||||
|
||||
### Line Chart
|
||||
- Smooth curve interpolation
|
||||
- Area fill under the line with gradient opacity
|
||||
- Score range always 0-100 on Y-axis
|
||||
|
||||
### Grid Lines
|
||||
- Horizontal lines at bucket boundaries (40, 70, 90)
|
||||
- Vertical lines at regular time intervals
|
||||
|
||||
### Time Axis
|
||||
- Auto-formats based on date range
|
||||
- Labels: Jan 15, Feb 1, etc.
|
||||
|
||||
## Loading State
|
||||
|
||||
When `history` is empty or loading:
|
||||
|
||||
```html
|
||||
<stella-score-history-chart [history]="[]" />
|
||||
<!-- Displays: "No history available" -->
|
||||
```
|
||||
|
||||
## Accessibility
|
||||
|
||||
- SVG includes `role="img"` with descriptive `aria-label`
|
||||
- Data points are keyboard focusable
|
||||
- Tooltip content read by screen readers
|
||||
- Color not the only indicator (shape markers)
|
||||
|
||||
## Styling
|
||||
|
||||
```css
|
||||
stella-score-history-chart {
|
||||
--chart-line-color: #3b82f6;
|
||||
--chart-area-fill: rgba(59, 130, 246, 0.1);
|
||||
--chart-point-size: 6px;
|
||||
--chart-font-family: system-ui, sans-serif;
|
||||
}
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
- Uses requestAnimationFrame for smooth animations
|
||||
- Virtualizes data points when > 100 entries
|
||||
- Debounces resize observer
|
||||
|
||||
## Related Components
|
||||
|
||||
- [ScorePill](./score-pill.md) - Current score display
|
||||
- [ScoreBreakdownPopover](./score-breakdown-popover.md) - Current score breakdown
|
||||
116
docs/ui/components/score-pill.md
Normal file
116
docs/ui/components/score-pill.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# ScorePillComponent
|
||||
|
||||
Compact score display component with bucket-based color coding.
|
||||
|
||||
## Overview
|
||||
|
||||
The `ScorePillComponent` displays a 0-100 evidence-weighted score with automatic color coding based on priority bucket thresholds.
|
||||
|
||||
## Selector
|
||||
|
||||
```html
|
||||
<stella-score-pill />
|
||||
```
|
||||
|
||||
## Inputs
|
||||
|
||||
| Input | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `score` | `number` | `0` | Evidence-weighted score (0-100) |
|
||||
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Size variant |
|
||||
| `showTooltip` | `boolean` | `true` | Show bucket tooltip on hover |
|
||||
| `interactive` | `boolean` | `true` | Enable hover/click interactions |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Output | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `scoreClick` | `EventEmitter<number>` | Emits when the pill is clicked |
|
||||
|
||||
## Size Variants
|
||||
|
||||
| Size | Dimensions | Font Size | Use Case |
|
||||
|------|------------|-----------|----------|
|
||||
| `sm` | 24x20px | 12px | Tables, compact lists |
|
||||
| `md` | 32x24px | 14px | Standard UI elements |
|
||||
| `lg` | 40x28px | 16px | Dashboard emphasis |
|
||||
|
||||
## Color Mapping
|
||||
|
||||
The pill automatically applies colors based on score:
|
||||
|
||||
```typescript
|
||||
// Score -> Bucket -> Color
|
||||
score >= 90 // ActNow -> #DC2626 (red)
|
||||
score >= 70 // ScheduleNext -> #D97706 (amber)
|
||||
score >= 40 // Investigate -> #2563EB (blue)
|
||||
score < 40 // Watchlist -> #6B7280 (gray)
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```html
|
||||
<stella-score-pill [score]="78" />
|
||||
```
|
||||
|
||||
### In a Table
|
||||
|
||||
```html
|
||||
<td>
|
||||
<stella-score-pill [score]="finding.score" size="sm" />
|
||||
</td>
|
||||
```
|
||||
|
||||
### All Sizes
|
||||
|
||||
```html
|
||||
<stella-score-pill [score]="78" size="sm" />
|
||||
<stella-score-pill [score]="78" size="md" />
|
||||
<stella-score-pill [score]="78" size="lg" />
|
||||
```
|
||||
|
||||
### Non-Interactive
|
||||
|
||||
```html
|
||||
<stella-score-pill [score]="78" [interactive]="false" />
|
||||
```
|
||||
|
||||
### With Click Handler
|
||||
|
||||
```html
|
||||
<stella-score-pill
|
||||
[score]="78"
|
||||
(scoreClick)="openScoreDetails($event)"
|
||||
/>
|
||||
```
|
||||
|
||||
```typescript
|
||||
openScoreDetails(score: number): void {
|
||||
// Handle click - typically opens breakdown popover
|
||||
}
|
||||
```
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Uses `role="status"` for screen reader announcements
|
||||
- `aria-label` includes bucket name: "Score 78: Schedule Next"
|
||||
- Focusable when interactive
|
||||
- Supports keyboard activation (Enter/Space)
|
||||
|
||||
## Styling
|
||||
|
||||
The component uses Shadow DOM encapsulation. Override styles using CSS custom properties:
|
||||
|
||||
```css
|
||||
stella-score-pill {
|
||||
--score-pill-border-radius: 4px;
|
||||
--score-pill-font-weight: 600;
|
||||
}
|
||||
```
|
||||
|
||||
## Related Components
|
||||
|
||||
- [ScoreBreakdownPopover](./score-breakdown-popover.md) - Detailed breakdown on click
|
||||
- [ScoreBadge](./score-badge.md) - Evidence flag badges
|
||||
Reference in New Issue
Block a user