- {grouped.length > 0 && (
-
- {grouped.map(([cat, items], catIdx) => (
-
-
{CATEGORY_LABELS[cat] || cat}
-
- {items.map((item, idx) => (
-
-
- {item.label}
- {item.desc && }
-
-
- {item.statusType === 'clear' ? (
-
- ) : item.statusType === 'unknown' ? (
-
- ) : (
-
- )}
-
- {item.status}
-
-
-
- ))}
+ {!hasChecks && (
+
No checks are available for this layer.
+ )}
+
+ {issues.length > 0 && (
+
+
+ Issues found
+ {issues.length}
+
+
+ {issues.map((item, idx) => (
+
+ ))}
+
+
+ )}
+
+ {notAnalyzed.length > 0 && (
+
+
+ Not analyzed
+ {notAnalyzed.length}
+
+
+ {notAnalyzed.map((item, idx) => (
+
+ ))}
+
+ Coverage unavailable — treat as unknown, not safe.
+
+ )}
+
+ {cleared.length > 0 && (
+
+
+ Cleared
+ {cleared.length}
+
+
+ {cleared.map((item, idx) => (
+
+
+ {item.label}
+ {item.desc && }
-
- ))}
-
+ ))}
+
+
)}
diff --git a/frontend/src/components/report/LayerModal.scss b/frontend/src/components/report/LayerModal.scss
index 5fe58fd..e608727 100644
--- a/frontend/src/components/report/LayerModal.scss
+++ b/frontend/src/components/report/LayerModal.scss
@@ -1,5 +1,6 @@
// ===========================================================================
-// LayerModal – Compact, decision-focused layer detail dialog
+// LayerModal — severity-triaged layer detail dialog
+// Issues first (loud) → Not analyzed (neutral) → Cleared (quiet).
// ===========================================================================
:root {
@@ -24,60 +25,50 @@
}
// ---------------------------------------------------------------------------
-// Container
+// Container — accent rail reflects the authoritative verdict, not the layer.
// ---------------------------------------------------------------------------
.lm-content {
+ --lm-accent: var(--risk-neutral);
+
font-family: var(--report-font, var(--font-sans));
background: var(--theme-bg-card);
border: 1px solid var(--theme-border);
+ border-top: 3px solid var(--lm-accent);
color: var(--theme-text-primary);
- max-width: 420px;
+ max-width: 460px;
width: 92vw;
- max-height: 80vh;
+ max-height: 82vh;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
scrollbar-color: var(--theme-border-subtle) transparent;
box-shadow: var(--theme-shadow-dropdown);
- border-radius: var(--radius-lg, 12px);
+ border-radius: 14px;
padding: 0;
animation: lmSlideIn var(--lm-transition-duration) var(--lm-ease) both;
display: flex;
flex-direction: column;
gap: 0;
- &[data-layer="security"] {
- border-left: 3px solid var(--risk-warn, #f59e0b);
- }
- &[data-layer="privacy"] {
- border-left: 3px solid var(--risk-warn, #f59e0b);
- }
- &[data-layer="governance"] {
- border-left: 3px solid var(--risk-good, #10b981);
- }
+ &[data-band="GOOD"] { --lm-accent: var(--risk-good); }
+ &[data-band="WARN"] { --lm-accent: var(--risk-warn); }
+ &[data-band="BAD"] { --lm-accent: var(--risk-bad); }
}
@keyframes lmSlideIn {
- from {
- opacity: 0;
- transform: translate(-50%, -48%) scale(0.98);
- }
- to {
- opacity: 1;
- transform: translate(-50%, -50%) scale(1);
- }
+ from { opacity: 0; transform: translate(-50%, -48%) scale(0.985); }
+ to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}
// ---------------------------------------------------------------------------
-// Header: compact identity bar — icon + title left, verdict right
-// Override DialogHeader's default (flex-col, text-center) so content is row, left/right.
+// Header — identity left, verdict right
// ---------------------------------------------------------------------------
.lm-header-wrap {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
- text-align: center;
+ text-align: left;
gap: 0;
margin: 0;
padding: 0;
@@ -89,19 +80,16 @@
margin: 0;
padding: 0;
border: none;
- font-size: inherit;
- font-weight: inherit;
- line-height: inherit;
+ font: inherit;
letter-spacing: inherit;
- tracking-tight: none;
}
.lm-header-inner {
display: flex;
align-items: center;
- justify-content: center;
+ justify-content: space-between;
width: 100%;
- padding: 16px 20px 14px;
+ padding: 18px 20px 16px;
padding-right: 48px;
border-bottom: 1px solid var(--theme-border-subtle);
gap: 12px;
@@ -111,229 +99,236 @@
display: flex;
align-items: center;
gap: 10px;
- flex-shrink: 0;
+ min-width: 0;
}
.lm-header-inner .lm-icon {
- font-size: 20px;
+ font-size: 19px;
line-height: 1;
}
.lm-header-inner .lm-title {
- font-size: 1.05rem;
- font-weight: 600;
+ font-size: 1.0625rem;
+ font-weight: 650;
color: var(--theme-text-primary);
letter-spacing: -0.01em;
}
+// Verdict pill — the layer's authoritative status, restated in the modal.
.lm-verdict-pill {
+ flex-shrink: 0;
font-size: 0.6875rem;
font-weight: 700;
text-transform: uppercase;
- letter-spacing: 0.05em;
- padding: 5px 12px;
+ letter-spacing: 0.06em;
+ padding: 5px 11px;
border-radius: var(--radius-full, 9999px);
white-space: nowrap;
+ border: 1px solid transparent;
}
+.lm-verdict-good { color: var(--risk-good); background: var(--risk-good-bg); border-color: var(--risk-good-border); }
+.lm-verdict-warn { color: var(--risk-warn); background: var(--risk-warn-bg); border-color: var(--risk-warn-border); }
+.lm-verdict-bad { color: var(--risk-bad); background: var(--risk-bad-bg); border-color: var(--risk-bad-border); }
+.lm-verdict-na { color: var(--theme-text-muted); background: var(--theme-bg-secondary); border-color: var(--theme-border); }
-.lm-verdict-good {
- color: var(--risk-good);
- background: var(--risk-good-bg);
- border: 1px solid var(--risk-good-border);
-}
-.lm-verdict-warn {
- color: var(--risk-warn);
- background: var(--risk-warn-bg);
- border: 1px solid var(--risk-warn-border);
-}
-.lm-verdict-bad {
- color: var(--risk-bad);
- background: var(--risk-bad-bg);
- border: 1px solid var(--risk-bad-border);
-}
-.lm-verdict-na {
- color: var(--theme-text-muted);
- background: var(--theme-bg-secondary);
- border: 1px solid var(--theme-border);
-}
-
-// ---------------------------------------------------------------------------
-// Body: no summary, checks only
// ---------------------------------------------------------------------------
-// ---------------------------------------------------------------------------
-// Body: no summary, checks only
+// Body — tiers stacked by severity
// ---------------------------------------------------------------------------
.lm-body {
display: flex;
flex-direction: column;
- gap: 0;
- padding: 14px 20px 20px;
+ gap: 18px;
+ padding: 16px 20px 20px;
}
-.lm-stats-row {
+.lm-empty {
+ margin: 0;
+ padding: 8px 0;
+ font-size: 0.8125rem;
+ color: var(--theme-text-muted);
+}
+
+.lm-tier {
display: flex;
- align-items: center;
- gap: 16px;
- padding-bottom: 12px;
- margin-bottom: 12px;
- border-bottom: 1px solid var(--theme-border-subtle);
+ flex-direction: column;
+ gap: 8px;
}
-.lm-stat {
- font-size: 0.8125rem;
- color: var(--theme-text-muted);
- display: inline-flex;
+.lm-tier-head {
+ display: flex;
align-items: center;
- gap: 4px;
+ gap: 8px;
}
-.lm-stat-num {
+.lm-tier-title {
+ font-size: 0.6875rem;
font-weight: 700;
- color: var(--theme-text-primary);
- font-variant-numeric: tabular-nums;
+ text-transform: uppercase;
+ letter-spacing: 0.09em;
+ color: var(--theme-text-muted);
}
-.lm-stat-issues .lm-stat-num {
- color: var(--risk-warn);
+.lm-tier--issues .lm-tier-title {
+ color: var(--risk-bad);
}
-.lm-stat-clear {
- color: var(--risk-good);
- font-weight: 600;
+.lm-tier-count {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 18px;
+ height: 18px;
+ padding: 0 5px;
+ font-size: 0.6875rem;
+ font-weight: 700;
+ font-variant-numeric: tabular-nums;
+ color: var(--theme-text-secondary);
+ background: var(--theme-bg-tertiary);
+ border-radius: var(--radius-full, 9999px);
+
+ // Accent text on a tint (AA-safe), matching the verdict-pill treatment.
+ &--issues {
+ color: var(--risk-bad);
+ background: var(--risk-bad-bg);
+ border: 1px solid var(--risk-bad-border);
+ }
}
-// ---------------------------------------------------------------------------
-// Check groups: flat grid
-// ---------------------------------------------------------------------------
-.lm-checks {
- display: flex;
- flex-direction: column;
- gap: 14px;
+.lm-tier-note {
+ margin: 2px 0 0;
+ font-size: 0.75rem;
+ line-height: 1.4;
+ color: var(--theme-text-subtle);
}
-.lm-group {
+.lm-rows {
display: flex;
flex-direction: column;
- gap: 6px;
- animation: lmFadeUp 0.3s var(--lm-ease) both;
-}
-
-.lm-group-label {
- font-size: 0.625rem;
- font-weight: 700;
- text-transform: uppercase;
- letter-spacing: 0.08em;
- color: var(--theme-text-muted);
- padding-left: 2px;
+ gap: 8px;
}
-.lm-group-items {
- display: flex;
- flex-wrap: wrap;
- gap: 6px;
-}
+// ---------------------------------------------------------------------------
+// Primary row — issues + not analyzed
+// ---------------------------------------------------------------------------
+.lm-check {
+ --row-accent: var(--risk-neutral);
+ --row-tint: var(--theme-bg-secondary);
-// Check card: full width row — label + info left, status right
-.lm-check-card {
- display: flex;
+ position: relative;
+ display: grid;
+ grid-template-columns: auto 1fr auto;
align-items: center;
- justify-content: space-between;
- width: 100%;
gap: 10px;
- padding: 7px 10px;
- border-radius: var(--radius, 8px);
- background: var(--theme-bg-secondary);
+ width: 100%;
+ padding: 11px 13px 11px 15px;
+ border-radius: var(--radius, 10px);
+ background: var(--row-tint);
border: 1px solid var(--theme-border-subtle);
- transition: background 0.15s ease, border-color 0.15s ease;
- animation: lmFadeUp 0.25s var(--lm-ease) both;
- box-sizing: border-box;
+ overflow: hidden;
+ animation: lmFadeUp 0.28s var(--lm-ease) both;
- &:hover {
- background: var(--theme-bg-tertiary);
- border-color: var(--theme-border);
- }
+ &.lm-tone-bad { --row-accent: var(--risk-bad); --row-tint: var(--risk-bad-bg); border-color: var(--risk-bad-border); }
+ &.lm-tone-warn { --row-accent: var(--risk-warn); --row-tint: var(--risk-warn-bg); border-color: var(--risk-warn-border); }
+ &.lm-tone-neutral { --row-accent: var(--risk-neutral); --row-tint: var(--theme-bg-secondary); border-style: dashed; border-color: var(--theme-border); }
+}
- &.lm-check-issues {
- border-color: var(--risk-warn-border);
- background: var(--risk-warn-bg);
- &:hover {
- background: var(--risk-warn-bg);
- border-color: var(--risk-warn-border);
- }
- }
+.lm-check-rail {
+ position: absolute;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ width: 3px;
+ background: var(--row-accent);
}
-.lm-check-left {
- display: flex;
+.lm-check-glyph {
+ display: inline-flex;
align-items: center;
- gap: 6px;
- min-width: 0;
- flex: 1;
+ justify-content: center;
+ color: var(--row-accent);
}
-.lm-check-card .lm-check-name {
- font-size: 0.8125rem;
- font-weight: 500;
- color: var(--theme-text-primary);
- line-height: 1.3;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
+.lm-check-main {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 6px 8px;
+ min-width: 0;
}
-// Status: icon + short label (right-aligned in card)
-.lm-status-wrap {
+.lm-check-name {
display: inline-flex;
align-items: center;
- gap: 4px;
- flex-shrink: 0;
+ gap: 5px;
+ font-size: 0.875rem;
+ font-weight: 600;
+ color: var(--theme-text-primary);
+ line-height: 1.25;
}
-.lm-status-icon {
- width: 14px;
- height: 14px;
- flex-shrink: 0;
+.lm-check-tag {
+ font-size: 0.625rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ color: var(--theme-text-muted);
+ padding: 2px 6px;
+ border-radius: 4px;
+ background: var(--theme-bg-tertiary);
}
-.lm-status {
+.lm-check-status {
+ flex-shrink: 0;
font-size: 0.625rem;
font-weight: 700;
text-transform: uppercase;
- letter-spacing: 0.03em;
- padding: 3px 7px;
- border-radius: 5px;
- border: 1px solid;
+ letter-spacing: 0.05em;
+ padding: 4px 9px;
+ border-radius: var(--radius-full, 9999px);
white-space: nowrap;
- color: var(--theme-text-primary);
+ border: 1px solid transparent;
}
-
-.lm-status-clear {
- color: var(--theme-text-primary);
- border-color: var(--risk-good-border);
- background: var(--risk-good-bg);
-}
-
+// Dark ink on a solid accent: loud, and AA-contrast on both amber and red in
+// either theme (white-on-amber failed WCAG).
.lm-status-issues {
- color: var(--theme-text-primary);
- border-color: var(--risk-warn-border);
- background: var(--risk-warn-bg);
+ color: #1c1a17;
+ background: var(--row-accent);
+ font-weight: 800;
}
-
-// Neutral "Not analyzed" status — no coverage, so neither safe nor flagged.
.lm-status-unknown {
color: var(--theme-text-secondary);
- border-color: var(--theme-border);
background: var(--theme-bg-tertiary);
+ border-color: var(--theme-border);
}
-.lm-check-card.lm-check-clear .lm-status-icon {
- color: var(--risk-good);
+// ---------------------------------------------------------------------------
+// Cleared tier — quiet, dense, low-contrast (no green pill noise)
+// ---------------------------------------------------------------------------
+.lm-clear-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 4px 16px;
}
-.lm-check-card.lm-check-issues .lm-status-icon {
- color: var(--risk-warn);
+
+.lm-clear-row {
+ display: flex;
+ align-items: center;
+ gap: 7px;
+ padding: 5px 2px;
+ min-width: 0;
}
-.lm-check-card.lm-check-unknown .lm-status-icon {
- color: var(--theme-text-secondary);
+
+.lm-clear-tick {
+ color: var(--risk-good);
+ opacity: 0.85;
+ flex-shrink: 0;
+}
+
+.lm-clear-name {
+ font-size: 0.8125rem;
+ font-weight: 500;
+ line-height: 1.3;
+ color: var(--theme-text-muted);
}
// ---------------------------------------------------------------------------
@@ -347,7 +342,7 @@
color: var(--theme-text-muted);
cursor: help;
flex-shrink: 0;
- opacity: 0.5;
+ opacity: 0.55;
transition: opacity 0.15s ease, color 0.15s ease;
outline: none;
border-radius: 50%;
@@ -391,25 +386,13 @@
}
@keyframes lmTooltipIn {
- from {
- opacity: 0;
- transform: translateX(-50%) translateY(4px);
- }
- to {
- opacity: 1;
- transform: translateX(-50%) translateY(0);
- }
+ from { opacity: 0; transform: translateX(-50%) translateY(4px); }
+ to { opacity: 1; transform: translateX(-50%) translateY(0); }
}
@keyframes lmFadeUp {
- from {
- opacity: 0;
- transform: translateY(4px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
+ from { opacity: 0; transform: translateY(5px); }
+ to { opacity: 1; transform: translateY(0); }
}
// ---------------------------------------------------------------------------
@@ -422,14 +405,16 @@
}
.lm-header-inner {
flex-wrap: wrap;
+ justify-content: flex-start;
gap: 8px;
padding-right: 48px;
}
- .lm-group-items {
- flex-direction: column;
+ .lm-verdict-pill {
+ align-self: flex-start;
}
- .lm-check-card {
- width: 100%;
+ .lm-clear-grid {
+ grid-template-columns: 1fr;
+ gap: 2px;
}
}
@@ -457,66 +442,20 @@
box-shadow: var(--theme-shadow-dropdown);
}
-.light .lm-header {
- border-bottom-color: var(--theme-border-subtle);
- .lm-title { color: var(--theme-text-primary); }
-}
-
.light .lm-header-inner {
border-bottom-color: var(--theme-border-subtle);
}
-.light .lm-stats-row {
- border-bottom-color: var(--theme-border-subtle);
-}
-
-.light .lm-stat {
- color: var(--theme-text-muted);
+.light .lm-check {
+ background: var(--row-tint, var(--theme-bg-secondary));
}
-.light .lm-stat-num {
- color: var(--theme-text-primary);
-}
-
-.light .lm-group-label {
- color: var(--theme-text-muted);
-}
-
-.light .lm-check-card {
+.light .lm-check.lm-tone-neutral {
background: var(--theme-bg-secondary);
- border-color: var(--theme-border-subtle);
- &:hover {
- background: var(--theme-bg-tertiary);
- border-color: var(--theme-border);
- }
- &.lm-check-issues {
- background: var(--risk-warn-bg);
- border-color: var(--risk-warn-border);
- }
-}
-
-.light .lm-check-card .lm-check-name {
- color: var(--theme-text-primary);
-}
-
-.light .lm-status-clear {
- background: var(--risk-good-bg);
- border-color: var(--risk-good-border);
- color: var(--risk-good);
}
.light .lm-status-issues {
- background: var(--risk-warn-bg);
- border-color: var(--risk-warn-border);
- color: var(--risk-warn);
-}
-
-.light .lm-info-trigger {
- color: var(--extensionshield-text-muted);
- &:hover,
- &:focus-visible {
- color: var(--extensionshield-text-secondary);
- }
+ color: #fff;
}
.light .lm-info-tooltip {
diff --git a/frontend/src/components/report/LayerModal.test.jsx b/frontend/src/components/report/LayerModal.test.jsx
index 5c8191e..c9da00c 100644
--- a/frontend/src/components/report/LayerModal.test.jsx
+++ b/frontend/src/components/report/LayerModal.test.jsx
@@ -1,55 +1,63 @@
/**
- * LayerModal status-mapping tests (Phase 1 follow-up).
+ * LayerModal status + triage tests (presentation correctness).
*
- * A check whose underlying analysis did not run has no coverage and must read as
- * "Not analyzed", never "Clear" (which overstates certainty). The network/exfil
- * analyzer reports this via details.network_analysis_enabled === false.
+ * Truthful statuses:
+ * - "Issue"/"High risk" only when the check ran and found something (severity >= 0.4)
+ * - "Not analyzed" when coverage is absent (never "Clear")
+ * - "Clear" only when the check ran and found nothing material
+ * Triage ordering: issues (most severe first) -> not analyzed -> cleared.
*
- * These exercise the pure status mapping only — no rendering required.
+ * Pure logic only — no rendering required.
*/
import { describe, it, expect } from 'vitest';
-import { humanizeFactor, isNotAnalyzed } from './LayerModal';
+import { humanizeFactor, isNotAnalyzed, triageFactors } from './LayerModal';
describe('LayerModal status mapping', () => {
it('Data Sharing with no network coverage is "Not analyzed", not "Clear"', () => {
- const factor = {
+ const result = humanizeFactor({
name: 'NetworkExfil',
severity: 0,
confidence: 0.5,
details: { network_analysis_enabled: false },
- };
- const result = humanizeFactor(factor);
+ });
expect(result.status).toBe('Not analyzed');
expect(result.statusType).toBe('unknown');
+ expect(result.tone).toBe('neutral');
expect(result.label).toBe('Data Sharing');
});
it('Data Sharing WITH coverage and no findings reads "Clear"', () => {
- const factor = {
+ const result = humanizeFactor({
name: 'NetworkExfil',
severity: 0,
- confidence: 0.7,
details: { network_analysis_enabled: true, domains_analyzed: 3 },
- };
- const result = humanizeFactor(factor);
+ });
expect(result.status).toBe('Clear');
expect(result.statusType).toBe('clear');
+ expect(result.tone).toBe('good');
});
- it('an actual ISSUE-level finding wins over the not-analyzed flag', () => {
- const factor = {
- name: 'NetworkExfil',
- severity: 0.6,
- details: { network_analysis_enabled: false },
- };
- const result = humanizeFactor(factor);
+ it('a moderate finding (>=0.4) is an amber "Issue"', () => {
+ const result = humanizeFactor({ name: 'Maintenance', severity: 0.5 });
expect(result.status).toBe('Issue');
expect(result.statusType).toBe('issues');
+ expect(result.tone).toBe('warn');
});
- it('a normal sub-threshold factor without coverage flags is "Clear"', () => {
- const factor = { name: 'CaptureSignals', severity: 0.1, details: {} };
- expect(humanizeFactor(factor).status).toBe('Clear');
+ it('a severe finding (>=0.7) is a red "High risk"', () => {
+ const result = humanizeFactor({ name: 'ToSViolations', severity: 0.8 });
+ expect(result.status).toBe('High risk');
+ expect(result.statusType).toBe('issues');
+ expect(result.tone).toBe('bad');
+ });
+
+ it('an actual finding wins over the not-analyzed flag', () => {
+ const result = humanizeFactor({
+ name: 'NetworkExfil',
+ severity: 0.6,
+ details: { network_analysis_enabled: false },
+ });
+ expect(result.statusType).toBe('issues');
});
it('isNotAnalyzed only fires on the explicit coverage flag', () => {
@@ -59,3 +67,34 @@ describe('LayerModal status mapping', () => {
expect(isNotAnalyzed({})).toBe(false);
});
});
+
+describe('LayerModal triage ordering', () => {
+ const factors = [
+ { name: 'SAST', severity: 0.1 }, // clear
+ { name: 'CaptureSignals', severity: 0.5 }, // issue (warn)
+ { name: 'NetworkExfil', severity: 0, details: { network_analysis_enabled: false } }, // not analyzed
+ { name: 'ToSViolations', severity: 0.9 }, // issue (bad)
+ { name: 'Webstore', severity: 0.2 }, // clear
+ ];
+
+ it('separates the three tiers correctly', () => {
+ const { issues, notAnalyzed, cleared } = triageFactors(factors);
+ expect(issues.map((i) => i.label)).toEqual(['Policy Violations', 'Screen Capture']); // severe first
+ expect(notAnalyzed.map((i) => i.label)).toEqual(['Data Sharing']);
+ expect(cleared.map((i) => i.label)).toEqual(['Code Safety', 'Store Reputation']); // alphabetical
+ });
+
+ it('a not-analyzed check never lands in the cleared tier', () => {
+ const { cleared, notAnalyzed } = triageFactors(factors);
+ expect(cleared.some((i) => i.label === 'Data Sharing')).toBe(false);
+ expect(notAnalyzed.some((i) => i.label === 'Data Sharing')).toBe(true);
+ });
+
+ it('handles an empty layer without throwing', () => {
+ const { all, issues, notAnalyzed, cleared } = triageFactors([]);
+ expect(all).toEqual([]);
+ expect(issues).toEqual([]);
+ expect(notAnalyzed).toEqual([]);
+ expect(cleared).toEqual([]);
+ });
+});
diff --git a/frontend/src/components/report/ResultsSidebarTile.jsx b/frontend/src/components/report/ResultsSidebarTile.jsx
index 2bb4491..01067e8 100644
--- a/frontend/src/components/report/ResultsSidebarTile.jsx
+++ b/frontend/src/components/report/ResultsSidebarTile.jsx
@@ -62,8 +62,9 @@ const ResultsSidebarTile = ({
{getBandLabel()}
{findingsCount > 0 && (
-
- {findingsCount} {findingsCount === 1 ? 'finding' : 'findings'}
+
+
+ {findingsCount} {findingsCount === 1 ? 'issue' : 'issues'}
)}
diff --git a/frontend/src/components/report/ResultsSidebarTile.scss b/frontend/src/components/report/ResultsSidebarTile.scss
index d65449a..4400dc9 100644
--- a/frontend/src/components/report/ResultsSidebarTile.scss
+++ b/frontend/src/components/report/ResultsSidebarTile.scss
@@ -1,23 +1,3 @@
-@keyframes tile-clickable-hint {
- 0%,
- 100% {
- box-shadow: 0 0 0 0 rgba(255, 255, 255, 0);
- border-color: rgba(255, 255, 255, 0.08);
- }
- 25% {
- box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.12);
- border-color: rgba(255, 255, 255, 0.18);
- }
- 50% {
- box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.08);
- border-color: rgba(255, 255, 255, 0.14);
- }
- 75% {
- box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
- border-color: rgba(255, 255, 255, 0.12);
- }
-}
-
.results-sidebar-tile {
--tile-band-color: var(--risk-neutral);
background: rgba(255, 255, 255, 0.04);
@@ -36,7 +16,6 @@
&.is-clickable {
cursor: pointer;
- animation: tile-clickable-hint 2.2s ease-in-out 2;
&:hover {
background: rgba(255, 255, 255, 0.06);
@@ -48,15 +27,20 @@
&.band-good {
--tile-band-color: var(--risk-good);
- border-color: rgba(16, 185, 129, 0.25);
+ border-color: var(--risk-good-border);
}
&.band-warn {
--tile-band-color: var(--risk-warn);
- border-color: rgba(245, 158, 11, 0.25);
+ border-color: var(--risk-warn-border);
}
&.band-bad {
--tile-band-color: var(--risk-bad);
- border-color: rgba(239, 68, 68, 0.25);
+ border-color: var(--risk-bad-border);
+ }
+
+ &:focus-visible {
+ outline: 2px solid var(--color-ring, var(--theme-border-strong));
+ outline-offset: 2px;
}
}
@@ -109,26 +93,29 @@
font-size: 0.75rem;
font-weight: 600;
border-radius: 9999px;
- color: var(--theme-text-primary);
- background: rgba(16, 185, 129, 0.25);
border: 1px solid transparent;
+ // Saturated risk color on its own tint — AA-safe and theme-aware (matches
+ // the LayerModal verdict pill), instead of hardcoded rgba on white text.
&.tile-pill-good {
- background: rgba(16, 185, 129, 0.25);
- border-color: rgba(16, 185, 129, 0.4);
+ color: var(--risk-good);
+ background: var(--risk-good-bg);
+ border-color: var(--risk-good-border);
}
&.tile-pill-warn {
- background: rgba(245, 158, 11, 0.35);
- border-color: rgba(245, 158, 11, 0.4);
+ color: var(--risk-warn);
+ background: var(--risk-warn-bg);
+ border-color: var(--risk-warn-border);
}
&.tile-pill-bad {
- background: rgba(239, 68, 68, 0.25);
- border-color: rgba(239, 68, 68, 0.4);
+ color: var(--risk-bad);
+ background: var(--risk-bad-bg);
+ border-color: var(--risk-bad-border);
}
&.tile-pill-na {
- background: rgba(255, 255, 255, 0.08);
- border-color: rgba(255, 255, 255, 0.12);
- color: rgba(255, 255, 255, 0.6);
+ color: var(--theme-text-muted);
+ background: var(--theme-bg-tertiary);
+ border-color: var(--theme-border);
}
}
@@ -138,9 +125,24 @@
}
.tile-findings-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
font-size: 0.75rem;
- font-weight: 500;
- color: rgba(248, 250, 252, 0.55);
+ font-weight: 600;
+ color: var(--theme-text-secondary);
+
+ .tile-findings-dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: currentColor;
+ flex-shrink: 0;
+ }
+
+ // Issues are a real signal, not gray metadata — tint by severity band.
+ &--warn { color: var(--risk-warn); }
+ &--bad { color: var(--risk-bad); }
}
/* Row 3: Progress bar */
@@ -201,6 +203,8 @@
.light .tile-findings-badge {
color: var(--extensionshield-text-secondary);
}
+.light .tile-findings-badge--warn { color: var(--risk-warn); }
+.light .tile-findings-badge--bad { color: var(--risk-bad); }
.light .tile-pill {
&.tile-pill-good {
diff --git a/frontend/src/pages/scanner/ScanResultsPageV2.jsx b/frontend/src/pages/scanner/ScanResultsPageV2.jsx
index 1e9f5ba..c5696ef 100644
--- a/frontend/src/pages/scanner/ScanResultsPageV2.jsx
+++ b/frontend/src/pages/scanner/ScanResultsPageV2.jsx
@@ -786,7 +786,7 @@ const ScanResultsPageV2 = () => {