diff --git a/tools/v2/team/legal-and-compliance-review-flag/components/LegalComplianceReviewFlag.tsx b/tools/v2/team/legal-and-compliance-review-flag/components/LegalComplianceReviewFlag.tsx new file mode 100644 index 00000000..bbcd9f54 --- /dev/null +++ b/tools/v2/team/legal-and-compliance-review-flag/components/LegalComplianceReviewFlag.tsx @@ -0,0 +1,295 @@ +import React from "react"; +import { AlertTriangle, CheckCircle2, FileWarning, Loader2, ShieldCheck } from "lucide-react"; + +export type LegalReviewSeverity = "low" | "medium" | "high" | "critical"; +export type LegalReviewStatus = "pending" | "needs-review" | "approved" | "blocked"; +export type LegalReviewSurfaceState = "loading" | "error" | "empty" | "success"; + +export interface LegalReviewItem { + id: string; + subject: string; + requester: string; + department: string; + severity: LegalReviewSeverity; + status: LegalReviewStatus; + reason: string; + receivedAt: string; +} + +export interface LegalComplianceReviewFlagProps { + state: LegalReviewSurfaceState; + items?: LegalReviewItem[]; + selectedId?: string; + errorMessage?: string; + onSelectItem?: (id: string) => void; + onApprove?: (id: string) => void; + onEscalate?: (id: string) => void; + onDismiss?: (id: string) => void; + onRetry?: () => void; +} + +const severityTone: Record = { + low: "bg-slate-100 text-slate-700 border-slate-200", + medium: "bg-cyan-50 text-cyan-800 border-cyan-200", + high: "bg-amber-50 text-amber-800 border-amber-200", + critical: "bg-red-50 text-red-800 border-red-200", +}; + +function getSelectedItem(items: LegalReviewItem[], selectedId?: string) { + return items.find((item) => item.id === selectedId) || items[0]; +} + +function StatePanel({ + state, + errorMessage, + onRetry, +}: Pick) { + if (state === "loading") { + return ( +
+
+ ); + } + + if (state === "error") { + return ( +
+
+
+
+ ); + } + + return ( +
+
+ ); +} + +function ReviewList({ + items, + selectedId, + onSelectItem, +}: { + items: LegalReviewItem[]; + selectedId?: string; + onSelectItem?: (id: string) => void; +}) { + return ( +
+
+ +
+
+ {items.map((item) => { + const selected = item.id === selectedId; + + return ( + + ); + })} +
+
+ ); +} + +function ReviewDetail({ + item, + onApprove, + onEscalate, + onDismiss, +}: { + item: LegalReviewItem; + onApprove?: (id: string) => void; + onEscalate?: (id: string) => void; + onDismiss?: (id: string) => void; +}) { + return ( +
+
+
+ +
+
+
Requester
+
{item.requester}
+
+
+
Received
+
{item.receivedAt}
+
+
+
Status
+
{item.status}
+
+
+
Department
+
{item.department}
+
+
+ +
+ + + +
+
+ ); +} + +export function LegalComplianceReviewFlag({ + state, + items = [], + selectedId, + errorMessage, + onSelectItem, + onApprove, + onEscalate, + onDismiss, + onRetry, +}: LegalComplianceReviewFlagProps) { + if (state !== "success" || items.length === 0) { + return ( + + ); + } + + const selected = getSelectedItem(items, selectedId); + + return ( +
+
+

+ Legal and compliance +

+

+ Review flagged messages +

+
+ + +
+ ); +} diff --git a/tools/v2/team/legal-and-compliance-review-flag/components/index.ts b/tools/v2/team/legal-and-compliance-review-flag/components/index.ts new file mode 100644 index 00000000..b71883ad --- /dev/null +++ b/tools/v2/team/legal-and-compliance-review-flag/components/index.ts @@ -0,0 +1,8 @@ +export type { + LegalReviewItem, + LegalReviewSeverity, + LegalReviewStatus, + LegalReviewSurfaceState, + LegalComplianceReviewFlagProps, +} from "./LegalComplianceReviewFlag"; +export { LegalComplianceReviewFlag } from "./LegalComplianceReviewFlag"; diff --git a/tools/v2/team/legal-and-compliance-review-flag/docs/ACCESSIBILITY.md b/tools/v2/team/legal-and-compliance-review-flag/docs/ACCESSIBILITY.md new file mode 100644 index 00000000..e04d2b22 --- /dev/null +++ b/tools/v2/team/legal-and-compliance-review-flag/docs/ACCESSIBILITY.md @@ -0,0 +1,36 @@ +# Legal and Compliance Review Flag Accessibility + +## Screen Reader Support + +- The root UI uses a named `main` landmark through `aria-labelledby`. +- Loading and empty states use `role="status"` with `aria-live="polite"`. +- Error state uses `role="alert"` with `aria-live="assertive"`. +- Review list entries expose descriptive `aria-label` values and selected state + with `aria-pressed`. +- The action cluster uses `role="group"` with a label tied to the selected + review flag. +- Decorative icons are marked with `aria-hidden="true"`. + +## Keyboard Support + +- Review rows are implemented as native buttons, so Enter and Space activation + work without custom key handlers. +- Approve, escalate, dismiss, and retry actions are native buttons. +- Focus rings use `focus-visible` styles with visible two-pixel rings and + offsets. +- Tab order follows the visible workflow: heading, queue items, detail actions. + +## States + +The component exposes four reviewable states: + +- `loading`: polite live region with progress icon. +- `error`: assertive live region and optional retry action. +- `empty`: polite live region for zero review flags. +- `success`: queue plus selected detail panel and action group. + +## Isolation + +This UI is not mounted into the main app. Future integration should pass +sanitized review items through props and keep routing, auth, database, +mail-rendering, wallet, and Stellar behavior outside this folder. diff --git a/tools/v2/team/legal-and-compliance-review-flag/docs/VISUAL_STYLE.md b/tools/v2/team/legal-and-compliance-review-flag/docs/VISUAL_STYLE.md new file mode 100644 index 00000000..87bbeef1 --- /dev/null +++ b/tools/v2/team/legal-and-compliance-review-flag/docs/VISUAL_STYLE.md @@ -0,0 +1,31 @@ +# Legal and Compliance Review Flag Visual Style + +## Direction + +The local UI uses a restrained compliance console style: quiet slate surfaces, +small uppercase section labels, crisp borders, and severity accents that stay +readable during repeated review work. + +## Palette + +- Slate surfaces for the neutral work area. +- Cyan accents for selection and navigation context. +- Amber and red accents for high-risk legal review states. +- Emerald accents for approved or healthy states. + +The shared design system is not changed. All styling stays in local component +class names so a future integration can adapt tokens separately. + +## Layout + +- A two-column desktop layout separates the queue from the selected flag. +- The queue remains a single interactive list, not nested cards. +- Detail metadata uses compact definition-list panels for quick scanning. +- Mobile and narrow layouts collapse naturally into a single column. + +## Interaction Treatment + +- Buttons use native elements for keyboard behavior. +- Selected queue items receive a visible cyan surface. +- Focus states use visible rings and do not rely on color alone. +- Severity badges combine text labels with color for accessibility. diff --git a/tools/v2/team/legal-and-compliance-review-flag/fixtures/ui-review-cases.json b/tools/v2/team/legal-and-compliance-review-flag/fixtures/ui-review-cases.json new file mode 100644 index 00000000..95689f6a --- /dev/null +++ b/tools/v2/team/legal-and-compliance-review-flag/fixtures/ui-review-cases.json @@ -0,0 +1,37 @@ +{ + "tool": "legal-and-compliance-review-flag", + "source": "synthetic-ui-fixture", + "states": ["loading", "error", "empty", "success"], + "items": [ + { + "id": "legal-flag-001", + "subject": "Contract clause needs data retention review", + "requester": "Jordan Lee", + "department": "Sales Ops", + "severity": "high", + "status": "needs-review", + "reason": "The proposed clause references customer data retention terms that need compliance review before reply.", + "receivedAt": "2026-07-01T09:00:00.000Z" + }, + { + "id": "legal-flag-002", + "subject": "Marketing claim requires approval", + "requester": "Mira Patel", + "department": "Marketing", + "severity": "medium", + "status": "pending", + "reason": "The draft includes a regulated performance claim and should be reviewed before campaign distribution.", + "receivedAt": "2026-07-01T11:30:00.000Z" + }, + { + "id": "legal-flag-003", + "subject": "Blocked export language", + "requester": "Noah Kim", + "department": "Support", + "severity": "critical", + "status": "blocked", + "reason": "The message references export controlled material and should stay blocked until legal clearance.", + "receivedAt": "2026-07-01T14:15:00.000Z" + } + ] +} diff --git a/tools/v2/team/legal-and-compliance-review-flag/tests/ui-accessibility-contract.test.mjs b/tools/v2/team/legal-and-compliance-review-flag/tests/ui-accessibility-contract.test.mjs new file mode 100644 index 00000000..f3772c2c --- /dev/null +++ b/tools/v2/team/legal-and-compliance-review-flag/tests/ui-accessibility-contract.test.mjs @@ -0,0 +1,75 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import test from "node:test"; + +const currentDir = dirname(fileURLToPath(import.meta.url)); +const toolDir = join(currentDir, ".."); +const componentPath = join(toolDir, "components", "LegalComplianceReviewFlag.tsx"); +const fixturePath = join(toolDir, "fixtures", "ui-review-cases.json"); +const accessibilityPath = join(toolDir, "docs", "ACCESSIBILITY.md"); +const visualStylePath = join(toolDir, "docs", "VISUAL_STYLE.md"); + +async function readJson(path) { + const raw = await readFile(path, "utf8"); + return JSON.parse(raw); +} + +test("UI fixture covers all review surface states", async () => { + const fixture = await readJson(fixturePath); + + assert.equal(fixture.tool, "legal-and-compliance-review-flag"); + assert.deepEqual(fixture.states, ["loading", "error", "empty", "success"]); + assert.ok(fixture.items.length >= 3); + + for (const item of fixture.items) { + assert.ok(item.id); + assert.ok(item.subject); + assert.ok(item.requester); + assert.ok(item.department); + assert.ok(["low", "medium", "high", "critical"].includes(item.severity)); + assert.ok(["pending", "needs-review", "approved", "blocked"].includes(item.status)); + assert.ok(item.reason); + assert.match(item.receivedAt, /^\d{4}-\d{2}-\d{2}T/); + } +}); + +test("component exposes accessible landmarks, states, and controls", async () => { + const source = await readFile(componentPath, "utf8"); + + for (const expected of [ + 'role="status"', + 'role="alert"', + 'aria-live="polite"', + 'aria-live="assertive"', + "aria-label", + "aria-pressed", + 'role="group"', + "focus-visible:ring-2", + 'type="button"', + ]) { + assert.ok(source.includes(expected), `${expected} is required`); + } + + for (const state of ["loading", "error", "empty", "success"]) { + assert.ok(source.includes(state), `${state} state is missing`); + } + + for (const action of ["Approve", "Escalate", "Dismiss", "Retry"]) { + assert.ok(source.includes(action), `${action} action is missing`); + } +}); + +test("accessibility and style docs describe isolated UI expectations", async () => { + const [accessibility, visualStyle] = await Promise.all([ + readFile(accessibilityPath, "utf8"), + readFile(visualStylePath, "utf8"), + ]); + + assert.ok(accessibility.includes("Screen Reader Support")); + assert.ok(accessibility.includes("Keyboard Support")); + assert.ok(accessibility.includes("This UI is not mounted into the main app")); + assert.ok(visualStyle.includes("compliance console")); + assert.ok(visualStyle.includes("The shared design system is not changed")); +});