Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions finverify-terminal/frontend/ACCESSIBILITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Workspace trust-score accessibility

Audit date: 2026-08-14

## Scope

This audit covers the trust and confidence indicators in `components/workspace`, including the default verification-coverage view and the selected-company integrity view.

## Implemented behavior

- Numeric trust and integrity scores expose a named ARIA `meter` with a 0–100 range and descriptive value text.
- Confidence levels are communicated in text and through accessible names, not by color alone.
- Trust scores and confidence bars are reachable with `Tab` and display a 2px visible focus outline.
- The verification-coverage grid uses native table, header, and caption semantics.
- The transaction-monitor filters have programmatic labels.

The score indicators are read-only. Focusing them exposes their value and context; Enter and Space intentionally perform no action.

## Verification

Automated checks used axe-core 4.10.3 with WCAG 2 A/AA and WCAG 2.1 A/AA tags.

| View | Passed checks | Critical violations |
| --- | ---: | ---: |
| Default workspace | 26 | 0 |
| NVIDIA integrity view | 21 | 0 |

Keyboard verification confirmed that all seven company trust scores and the selected-company integrity score plus its three confidence bars are reachable in DOM order and receive a visible focus indicator.

Repository checks:

- `npm run lint` — passes with no errors; existing warnings remain outside this change.
- `npm run build` — passes.

## Known follow-up work

The full workspace audit still reports pre-existing serious findings for low-contrast secondary text and some scrollable regions that are not keyboard focusable. They are outside the trust-score component scope and should be handled separately.
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"use client";

import React from "react";

interface AccessibleScoreProps {
label: string;
value: number;
valueText: string;
className?: string;
children: React.ReactNode;
}

/**
* Exposes a visual score as a named ARIA meter and gives keyboard users a
* visible focus target without adding button-like behavior.
*/
export default function AccessibleScore({
label,
value,
valueText,
className = "",
children,
}: AccessibleScoreProps) {
const boundedValue = Math.max(0, Math.min(value, 100));

return (
<span
role="meter"
tabIndex={0}
aria-label={label}
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={boundedValue}
aria-valuetext={valueText}
className={`rounded-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-t-blue ${className}`}
>
{children}
</span>
);
}
51 changes: 37 additions & 14 deletions finverify-terminal/frontend/components/workspace/FocusView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import NeedsAttention from "@/components/workspace/NeedsAttention";
import VerificationCoverage from "@/components/workspace/VerificationCoverage";
import LiveVerificationTrace from "@/components/workspace/LiveVerificationTrace";
import CommandBar from "@/components/workspace/CommandBar";
import AccessibleScore from "@/components/workspace/AccessibleScore";
import {
getFundamentals,
type QueryResponse,
Expand Down Expand Up @@ -56,7 +57,12 @@ function TrustBadge({ trust }: { trust: string }) {
? "bg-t-amber/10 text-t-amber border-t-amber/20"
: "bg-t-red/10 text-t-red border-t-red/20";
return (
<span className={`text-[8px] font-mono font-bold px-1.5 py-0.5 rounded border ${cls}`}>
<span
role="status"
tabIndex={0}
aria-label={`Trust confidence: ${trust.toLowerCase()}`}
className={`text-[8px] font-mono font-bold px-1.5 py-0.5 rounded border focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-t-blue ${cls}`}
>
{trust}
</span>
);
Expand Down Expand Up @@ -246,18 +252,25 @@ export default function FocusView({ selectedSymbol, onDeselect, onSelectSymbol }
</div>
<div className="flex items-center gap-2">
<span className="text-[9px] font-mono text-t-muted uppercase">Integrity:</span>
<span className={`text-[22px] font-mono font-bold tabular-nums ${band.color}`}>
{integrityScore}
</span>
<span className={`text-[8px] font-mono font-bold px-1.5 py-0.5 rounded border ${
band.label === "HIGH"
? "bg-t-green/10 text-t-green border-t-green/20"
: band.label === "MEDIUM"
? "bg-t-amber/10 text-t-amber border-t-amber/20"
: "bg-t-red/10 text-t-red border-t-red/20"
}`}>
{band.label}
</span>
<AccessibleScore
label={`${companyName} integrity score`}
value={integrityScore}
valueText={`${integrityScore} out of 100, ${band.label.toLowerCase()} integrity`}
className="inline-flex items-center gap-2"
>
<span className={`text-[22px] font-mono font-bold tabular-nums ${band.color}`}>
{integrityScore}
</span>
<span className={`text-[8px] font-mono font-bold px-1.5 py-0.5 rounded border ${
band.label === "HIGH"
? "bg-t-green/10 text-t-green border-t-green/20"
: band.label === "MEDIUM"
? "bg-t-amber/10 text-t-amber border-t-amber/20"
: "bg-t-red/10 text-t-red border-t-red/20"
}`}>
{band.label}
</span>
</AccessibleScore>
<span className="text-[8px] font-mono text-t-amber border border-t-amber/20 bg-t-amber/[0.04] px-1 py-0.5 rounded">
DEMO
</span>
Expand Down Expand Up @@ -328,7 +341,17 @@ function IntegrityTab({ symbol, score }: { symbol: string; score: number }) {
{ label: "SEC Agreement", weight: "30%", value: Math.min(secAgreement, 100), color: "bg-t-cyan" },
{ label: "DVL Confidence", weight: "30%", value: Math.min(dvlConfidence, 100), color: "bg-t-blue" },
].map(({ label, weight, value, color }) => (
<div key={label} className="space-y-1">
<div
key={label}
role="meter"
tabIndex={0}
aria-label={`${label} score`}
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={value}
aria-valuetext={`${value} out of 100; ${weight} of the composite score`}
className="space-y-1 rounded-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-t-blue"
>
<div className="flex items-center justify-between text-[9px] font-mono">
<span className="text-t-secondary">{label} <span className="text-t-muted">({weight})</span></span>
<span className={`font-bold ${value >= 80 ? "text-t-green" : value >= 50 ? "text-t-amber" : "text-t-red"}`}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,20 +104,20 @@ export default function GlobalTransactionMonitor({ onSelectSymbol: _onSelectSymb
</span>
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-1 text-[8px] font-mono text-t-muted">
<label className="flex items-center gap-1 text-[8px] font-mono text-t-muted">
<span>FLOW TYPE</span>
<select className="bg-transparent border border-t-border/50 rounded px-1 py-0.5 text-[8px] text-t-secondary font-mono outline-none">
<option>All Transactions</option>
</select>
</div>
<div className="flex items-center gap-1 text-[8px] font-mono text-t-muted">
</label>
<label className="flex items-center gap-1 text-[8px] font-mono text-t-muted">
<span>TIME RANGE</span>
<select className="bg-transparent border border-t-border/50 rounded px-1 py-0.5 text-[8px] text-t-secondary font-mono outline-none">
<option>24H</option>
<option>7D</option>
<option>30D</option>
</select>
</div>
</label>
<button className="text-[10px] text-t-muted hover:text-t-secondary transition-colors" title="Refresh">⟳</button>
<button className="text-[10px] text-t-muted hover:text-t-secondary transition-colors" title="Fullscreen">⛶</button>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import React from "react";
import AccessibleScore from "./AccessibleScore";

/**
* VerificationCoverage — Table showing per-company verification coverage.
Expand Down Expand Up @@ -31,6 +32,12 @@ function getTrustColor(score: number): string {
return "text-t-red";
}

function getTrustLevel(score: number): string {
if (score >= 97) return "high";
if (score >= 93) return "medium";
return "low";
}

export default function VerificationCoverage() {
return (
<div className="panel flex flex-col h-full min-h-0">
Expand All @@ -41,35 +48,59 @@ export default function VerificationCoverage() {
<span className="text-[7px] font-mono text-t-muted">TOP COMPANIES</span>
</div>

{/* Table header */}
<div className="grid grid-cols-[1fr_50px_55px_55px_60px] gap-1 px-2 py-1 text-[7px] font-mono text-t-muted uppercase tracking-wider border-b border-t-border/30">
<span>COMPANY</span>
<span className="text-right">VERIFIED</span>
<span className="text-right">CORRECTED</span>
<span className="text-right">CONFLICTS</span>
<span className="text-right">TRUST SCORE</span>
</div>

{/* Table body */}
<div className="flex-1 overflow-y-auto">
{DEMO_COVERAGE.map((row) => (
<div
key={row.company}
className="grid grid-cols-[1fr_50px_55px_55px_60px] gap-1 px-2 py-1 text-[9px] font-mono border-b border-t-border/10 last:border-b-0 hover:bg-white/[0.02] transition-colors"
>
<span className="text-t-secondary truncate">{row.company}</span>
<span className="text-right text-t-primary tabular-nums">{row.verified}</span>
<span className={`text-right tabular-nums ${row.corrected > 4 ? "text-t-amber" : "text-t-muted"}`}>
{row.corrected}
</span>
<span className={`text-right tabular-nums ${row.conflicts > 0 ? "text-t-red" : "text-t-muted"}`}>
{row.conflicts}
</span>
<span className={`text-right font-bold tabular-nums ${getTrustColor(row.trustScore)}`}>
{row.trustScore.toFixed(1)}%
</span>
</div>
))}
<table className="w-full table-fixed text-[9px] font-mono">
<caption className="sr-only">Verification coverage and trust scores for top companies</caption>
<colgroup>
<col />
<col className="w-[50px]" />
<col className="w-[55px]" />
<col className="w-[55px]" />
<col className="w-[60px]" />
</colgroup>
<thead className="text-[7px] text-t-muted uppercase tracking-wider border-b border-t-border/30">
<tr>
<th scope="col" className="px-2 py-1 text-left font-normal">COMPANY</th>
<th scope="col" className="py-1 text-right font-normal">VERIFIED</th>
<th scope="col" className="py-1 text-right font-normal">CORRECTED</th>
<th scope="col" className="py-1 text-right font-normal">CONFLICTS</th>
<th scope="col" className="pr-2 py-1 text-right font-normal">TRUST SCORE</th>
</tr>
</thead>
<tbody>
{DEMO_COVERAGE.map((row) => {
const trustLevel = getTrustLevel(row.trustScore);

return (
<tr
key={row.company}
className="border-b border-t-border/10 last:border-b-0 hover:bg-white/[0.02] transition-colors"
>
<th scope="row" className="px-2 py-1 text-left font-normal text-t-secondary truncate">
{row.company}
</th>
<td className="py-1 text-right text-t-primary tabular-nums">{row.verified}</td>
<td className={`py-1 text-right tabular-nums ${row.corrected > 4 ? "text-t-amber" : "text-t-muted"}`}>
{row.corrected}
</td>
<td className={`py-1 text-right tabular-nums ${row.conflicts > 0 ? "text-t-red" : "text-t-muted"}`}>
{row.conflicts}
</td>
<td className="pr-2 py-1 text-right">
<AccessibleScore
label={`${row.company} trust score`}
value={row.trustScore}
valueText={`${row.trustScore.toFixed(1)} percent, ${trustLevel} confidence`}
className={`inline-block font-bold tabular-nums ${getTrustColor(row.trustScore)}`}
>
{row.trustScore.toFixed(1)}%
</AccessibleScore>
</td>
</tr>
);
})}
</tbody>
</table>
</div>

<div className="px-2 py-1 border-t border-t-border/50 text-center">
Expand Down