Skip to content
Draft
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
48 changes: 23 additions & 25 deletions client/docs/accessibility-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,11 @@ acceptance criterion in this sandboxed environment:

### 1. Color contrast (status badges + primary text)

| Location | Before | After | Ratio before → after |
|---|---|---|---|
| `components/ui/Badge/Badge.css` — 9 status variants + 5 generic variants | text colors as light as `hsl(x, y%, 30-45%)` over a 10%-alpha fill | darkened lightness per-hue (see inline comment in the file) | as low as **2.75:1** → all ≥ **4.6:1** against both white and `soil-50` page backgrounds |
| `text-soil-400` / `text-soil-500` used as real copy (captions, labels, hints, dt/dd pairs) across `Header`, `StatTile`, `ChartCard`, `ComingSoonCard`, `LifecycleStepper`, `CreateCampaignPage`, `DesignFoundationsPage`, `AppLayout` | `soil-400` **2.98:1**, `soil-500` **4.10:1** on white | bumped to `soil-600` | **≥ 5.96:1** |
| `FundCampaignModal` / `CampaignDetailPage` / `InvestorDashboardPage` / `InvestmentCard` / `InvestorSummaryStats` — a second, independent Tailwind-default (slate/emerald/amber) design system used only in these files | `text-slate-400` **2.56:1**; primary CTA buttons `bg-emerald-600` white text **3.77:1**; `bg-amber-600` white text **3.19:1** | `slate-400`→`slate-600`; buttons →`emerald-700`/`amber-700` (hover `-800`) | **≥ 4.66:1** (text), **≥ 5.02:1** (buttons) |
| Location | Before | After | Ratio before → after |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `components/ui/Badge/Badge.css` — 9 status variants + 5 generic variants | text colors as light as `hsl(x, y%, 30-45%)` over a 10%-alpha fill | darkened lightness per-hue (see inline comment in the file) | as low as **2.75:1** → all ≥ **4.6:1** against both white and `soil-50` page backgrounds |
| `text-soil-400` / `text-soil-500` used as real copy (captions, labels, hints, dt/dd pairs) across `Header`, `StatTile`, `ChartCard`, `ComingSoonCard`, `LifecycleStepper`, `CreateCampaignPage`, `DesignFoundationsPage`, `AppLayout` | `soil-400` **2.98:1**, `soil-500` **4.10:1** on white | bumped to `soil-600` | **≥ 5.96:1** |
| `FundCampaignModal` / `CampaignDetailPage` / `InvestorDashboardPage` / `InvestmentCard` / `InvestorSummaryStats` — a second, independent Tailwind-default (slate/emerald/amber) design system used only in these files | `text-slate-400` **2.56:1**; primary CTA buttons `bg-emerald-600` white text **3.77:1**; `bg-amber-600` white text **3.19:1** | `slate-400`→`slate-600`; buttons →`emerald-700`/`amber-700` (hover `-800`) | **≥ 4.66:1** (text), **≥ 5.02:1** (buttons) |

`dark:` variants in the slate/emerald files were **not** changed — they
already passed (6.9–17.8:1) since Tailwind's default dark-mode palette
Expand All @@ -61,15 +61,12 @@ Two large (`text-2xl font-bold`, 24px) stat values using `amber-600`/
as WCAG "large text," where the threshold is 3:1, and they measure 3.19:1
and 3.77:1 respectively — already compliant.

**Note:** `FundCampaignModal.tsx` and `CampaignDetailPage.tsx` use a
**Note:** `FundCampaignModal.tsx` and `CampaignDetailPage.tsx` previously used a
Tailwind slate/emerald palette instead of the earth-tone `soil`/`leaf`/
`status-*` tokens used everywhere else in the app — this is exactly the
"independently-built pages regress accessibility" failure mode issue #65
describes. `CampaignDetailPage`'s status pill was also hardcoded to green
regardless of actual campaign status; it now uses the shared, contrast-
verified `<StatusBadge>` component instead. Fully unifying the two design
systems is a larger change than an a11y/contrast fix and is called out as a
follow-up below rather than attempted here.
`status-*` tokens used everywhere else in the app. This has now been resolved
(issue #148): all components and pages have been unified to use the shared
`soil`/`leaf`/`status-*` design tokens and shared UI components (`Button`, `Card`,
`<StatusBadge>`, `<Modal>`).

### 2. Keyboard navigation — campaign funding flow

Expand All @@ -85,6 +82,7 @@ instead of duplicating dialog chrome, which fixes all of the above for
free and is verified by the four keyboard tests described above.

Additional fixes in the same flow:

- `CampaignDetailPage`'s progress bar had no accessible role/value; added
`role="progressbar"` with `aria-valuenow`/`aria-valuemin`/`aria-valuemax`/
`aria-label`.
Expand All @@ -110,11 +108,11 @@ Additional fixes in the same flow:

### 3. Responsive layout at 360px

| Location | Problem | Fix |
|---|---|---|
| `DesignFoundationsPage` palette swatch rows | 11 fixed 32px swatches + gaps = ~392px in a non-wrapping flex row, inside a 312px-wide content area at 360px viewport → horizontal overflow | added `flex-wrap` |
| `Header`'s wallet-connection error panel | `absolute right-6 ... max-w-sm` (384px) could exceed a 360px viewport | now `inset-x-4` (fluid, clamped to viewport minus margin) below the `sm` breakpoint, restoring the original floating position at `sm:` and up |
| `Header` nav row | no wrap fallback if the logo + connected-wallet chip + disconnect button ever exceeded available width | added `flex-wrap` as a safety margin |
| Location | Problem | Fix |
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `DesignFoundationsPage` palette swatch rows | 11 fixed 32px swatches + gaps = ~392px in a non-wrapping flex row, inside a 312px-wide content area at 360px viewport → horizontal overflow | added `flex-wrap` |
| `Header`'s wallet-connection error panel | `absolute right-6 ... max-w-sm` (384px) could exceed a 360px viewport | now `inset-x-4` (fluid, clamped to viewport minus margin) below the `sm` breakpoint, restoring the original floating position at `sm:` and up |
| `Header` nav row | no wrap fallback if the logo + connected-wallet chip + disconnect button ever exceeded available width | added `flex-wrap` as a safety margin |

Other primary pages (`AnalyticsDashboardPage`, `CreateCampaignPage`,
`CampaignDetailPage`, `InvestorDashboardPage`, `InvestmentCard`) were
Expand All @@ -125,6 +123,7 @@ changes were needed there.
### 4. Automated regression test

`src/__tests__/accessibility.test.tsx` adds:

- 4 `jest-axe` scans (DOM/ARIA structural checks) across the audited routes
and the funding modal.
- 4 keyboard-navigation tests for the funding flow (dialog focus, Tab trap,
Expand All @@ -145,14 +144,13 @@ that flow is built (tracked separately; per issue #65's own guidance to
"document any [issues] that need a follow-up rather than blocking this
issue indefinitely").

Two smaller items are noted but intentionally not fixed here, to keep this
PR scoped to accessibility/responsive behavior rather than expanding into
unrelated feature or architecture work:
One smaller item is noted but intentionally not fixed here:

- `CreateCampaignPage`/`CampaignDetailPage`/`FundCampaignModal` aren't wired
into `App.tsx`'s router yet (a pre-existing gap, not introduced by this
PR) — their accessibility was verified by rendering them directly in
tests/code review.
- `FundCampaignModal`/`CampaignDetailPage`/`InvestorDashboardPage` use a
different Tailwind palette (slate/emerald) than the rest of the app
(soil/leaf/status tokens); contrast was fixed in place, but unifying the
two design systems is a larger refactor than this audit's scope.
- ~~`FundCampaignModal`/`CampaignDetailPage`/`InvestorDashboardPage` use a
different Tailwind palette (slate/emerald)~~ — **Resolved in #148**: All
components and pages have been unified to use the shared `soil`/`leaf`/`status-*`
design system and shared UI components (`Button`, `Card`, `StatusBadge`, `Modal`).
68 changes: 34 additions & 34 deletions client/src/components/campaign/FundCampaignModal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useState } from 'react';
import { Modal } from '../ui/Modal/Modal';
import { Button } from '../ui/Button/Button';
import { useWallet } from '../../context/WalletContext';
import { useFundCampaign } from '../../hooks/contract/useEscrowMutations';
import {
Expand Down Expand Up @@ -111,7 +112,7 @@ export const FundCampaignModal: React.FC<FundCampaignModalProps> = ({
title={
<>
Fund Campaign
<span className="mt-1 block text-sm font-normal text-slate-500 dark:text-slate-400">
<span className="mt-1 block text-sm font-normal text-soil-600 dark:text-soil-400">
{campaignTitle}
</span>
</>
Expand All @@ -122,41 +123,43 @@ export const FundCampaignModal: React.FC<FundCampaignModalProps> = ({
{successResult ? (
<div className="space-y-4 py-2 text-center">
<div
className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-emerald-100 text-2xl font-bold text-emerald-700 dark:bg-emerald-950 dark:text-emerald-400"
className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-leaf-100 text-2xl font-bold text-leaf-700 dark:bg-leaf-950 dark:text-leaf-400"
aria-hidden="true"
>
</div>
<h3 className="text-lg font-semibold text-slate-900 dark:text-white">
<h3 className="text-lg font-semibold text-soil-900 dark:text-soil-50">
Contribution Successful!
</h3>
<p className="text-sm text-slate-600 dark:text-slate-300">
<p className="text-sm text-soil-600 dark:text-soil-300">
You contributed{' '}
<span className="font-semibold text-emerald-700 dark:text-emerald-400">
<span className="font-semibold text-leaf-700 dark:text-leaf-400">
${numAmount.toLocaleString()}
</span>{' '}
to {campaignTitle}.
</p>

{successResult.txHash && (
<div className="rounded-xl bg-slate-50 p-3 text-left dark:bg-slate-800/60">
<span className="block font-mono text-xs text-slate-600 dark:text-slate-400">
<div className="rounded-xl border border-soil-200 bg-soil-50 p-3 text-left dark:border-soil-800 dark:bg-soil-900/60">
<span className="block font-mono text-xs text-soil-600 dark:text-soil-400">
Transaction Status
</span>
<span className="break-all font-mono text-xs text-slate-700 dark:text-slate-300">
<span className="break-all font-mono text-xs text-soil-700 dark:text-soil-300">
{successResult.txHash}
</span>
</div>
)}

<div className="pt-2">
<button
<Button
type="button"
variant="primary"
size="lg"
onClick={resetAndClose}
className="w-full rounded-xl bg-emerald-700 px-4 py-3 font-medium text-white transition hover:bg-emerald-800"
className="w-full"
>
Done
</button>
</Button>
</div>
</div>
) : (
Expand All @@ -166,27 +169,27 @@ export const FundCampaignModal: React.FC<FundCampaignModalProps> = ({
{!isConnected && (
<div
role="status"
className="rounded-xl border border-amber-200 bg-amber-50 p-3 text-sm font-medium text-amber-800 dark:border-amber-800 dark:bg-amber-950/50 dark:text-amber-300"
className="rounded-xl border border-amber-200 bg-amber-50 p-3 text-sm font-medium text-amber-900 dark:border-amber-800 dark:bg-amber-950/50 dark:text-amber-300"
>
Connect your wallet to fund this campaign.
</div>
)}

{/* Stats bar */}
<div className="grid grid-cols-2 gap-3 rounded-xl border border-slate-100 bg-slate-50 p-3 dark:border-slate-800 dark:bg-slate-800/40">
<div className="grid grid-cols-2 gap-3 rounded-xl border border-soil-200 bg-soil-50 p-3 dark:border-soil-800 dark:bg-soil-900/40">
<div>
<span className="block text-xs text-slate-600 dark:text-slate-400">
<span className="block text-xs text-soil-600 dark:text-soil-400">
Remaining Target
</span>
<span className="text-sm font-semibold text-slate-900 dark:text-white">
<span className="text-sm font-semibold text-soil-900 dark:text-soil-50">
${remainingTarget.toLocaleString()}
</span>
</div>
<div>
<span className="block text-xs text-slate-600 dark:text-slate-400">
<span className="block text-xs text-soil-600 dark:text-soil-400">
Est. Share
</span>
<span className="text-sm font-semibold text-emerald-700 dark:text-emerald-400">
<span className="text-sm font-semibold text-leaf-700 dark:text-leaf-400">
{estimatedShare}%
</span>
</div>
Expand All @@ -197,7 +200,7 @@ export const FundCampaignModal: React.FC<FundCampaignModalProps> = ({
<div
id="contribution-amount-error"
role="alert"
className="rounded-xl border border-red-200 bg-red-50 p-3 text-sm text-red-700 dark:border-red-800 dark:bg-red-950/50 dark:text-red-300"
className="rounded-xl border border-status-failed/20 bg-status-failed-light p-3 text-sm text-status-failed-dark dark:border-red-800 dark:bg-red-950/50 dark:text-red-300"
>
{error}
</div>
Expand All @@ -207,13 +210,13 @@ export const FundCampaignModal: React.FC<FundCampaignModalProps> = ({
<div>
<label
htmlFor="contribution-amount"
className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300"
className="mb-1 block text-sm font-medium text-soil-700 dark:text-soil-300"
>
Contribution Amount (USDC)
</label>
<div className="relative">
<span
className="absolute left-3 top-1/2 -translate-y-1/2 font-semibold text-slate-600 dark:text-slate-400"
className="absolute left-3 top-1/2 -translate-y-1/2 font-semibold text-soil-600 dark:text-soil-400"
aria-hidden="true"
>
$
Expand All @@ -234,44 +237,41 @@ export const FundCampaignModal: React.FC<FundCampaignModalProps> = ({
aria-describedby={
error ? 'contribution-amount-error' : undefined
}
className="w-full rounded-xl border border-slate-300 bg-white py-2.5 pl-8 pr-4 text-slate-900 outline-none transition focus:border-transparent focus:ring-2 focus:ring-emerald-500 dark:border-slate-700 dark:bg-slate-800 dark:text-white"
className="w-full rounded-xl border border-soil-300 bg-white py-2.5 pl-8 pr-4 text-soil-900 outline-none transition focus:border-leaf-500 focus:ring-2 focus:ring-leaf-500 dark:border-soil-700 dark:bg-soil-900 dark:text-white"
/>
</div>
</div>

{/* Quick selectors */}
<div className="flex items-center gap-2">
<span className="text-xs text-slate-600 dark:text-slate-400">
<span className="text-xs text-soil-600 dark:text-soil-400">
Quick fill:
</span>
{[25, 50, 100].map((pct) => (
<button
type="button"
key={pct}
onClick={() => handlePercentageSelect(pct)}
className="rounded-lg border border-slate-200 px-2.5 py-1 text-xs text-slate-600 transition hover:bg-slate-100 dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-800"
className="rounded-lg border border-soil-300 px-2.5 py-1 text-xs text-soil-700 transition hover:bg-soil-100 dark:border-soil-700 dark:text-soil-300 dark:hover:bg-soil-800"
>
{pct}%
</button>
))}
</div>

{/* Footer Buttons */}
<div className="flex items-center justify-end gap-3 border-t border-slate-100 pt-3 dark:border-slate-800">
<button
type="button"
onClick={resetAndClose}
className="rounded-xl border border-slate-300 px-4 py-2.5 font-medium text-slate-700 transition hover:bg-slate-50 dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-800"
>
<div className="flex items-center justify-end gap-3 border-t border-soil-200 pt-3 dark:border-soil-800">
<Button type="button" variant="outline" onClick={resetAndClose}>
Cancel
</button>
<button
</Button>
<Button
type="submit"
variant="primary"
isLoading={loading}
disabled={loading || remainingTarget <= 0 || !isConnected}
className="rounded-xl bg-emerald-700 px-5 py-2.5 font-semibold text-white shadow-sm transition hover:bg-emerald-800 disabled:opacity-50"
>
{loading ? 'Confirming...' : 'Confirm Contribution'}
</button>
Confirm Contribution
</Button>
</div>
</form>
)}
Expand Down
Loading