diff --git a/client/docs/accessibility-audit.md b/client/docs/accessibility-audit.md index d26251b..9793dbc 100644 --- a/client/docs/accessibility-audit.md +++ b/client/docs/accessibility-audit.md @@ -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 @@ -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 `` 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`, +``, ``). ### 2. Keyboard navigation — campaign funding flow @@ -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`. @@ -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 @@ -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, @@ -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`). diff --git a/client/src/components/campaign/FundCampaignModal.tsx b/client/src/components/campaign/FundCampaignModal.tsx index 4f51941..d6ed2d1 100644 --- a/client/src/components/campaign/FundCampaignModal.tsx +++ b/client/src/components/campaign/FundCampaignModal.tsx @@ -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 { @@ -111,7 +112,7 @@ export const FundCampaignModal: React.FC = ({ title={ <> Fund Campaign - + {campaignTitle} @@ -122,41 +123,43 @@ export const FundCampaignModal: React.FC = ({ {successResult ? (
-

+

Contribution Successful!

-

+

You contributed{' '} - + ${numAmount.toLocaleString()} {' '} to {campaignTitle}.

{successResult.txHash && ( -
- +
+ Transaction Status - + {successResult.txHash}
)}
- +
) : ( @@ -166,27 +169,27 @@ export const FundCampaignModal: React.FC = ({ {!isConnected && (
Connect your wallet to fund this campaign.
)} {/* Stats bar */} -
+
- + Remaining Target - + ${remainingTarget.toLocaleString()}
- + Est. Share - + {estimatedShare}%
@@ -197,7 +200,7 @@ export const FundCampaignModal: React.FC = ({ @@ -207,13 +210,13 @@ export const FundCampaignModal: React.FC = ({
{/* Quick selectors */}
- + Quick fill: {[25, 50, 100].map((pct) => ( @@ -249,7 +252,7 @@ export const FundCampaignModal: React.FC = ({ 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}% @@ -257,21 +260,18 @@ export const FundCampaignModal: React.FC = ({
{/* Footer Buttons */} -
- - + Confirm Contribution +
)} diff --git a/client/src/components/investor/InvestmentCard.tsx b/client/src/components/investor/InvestmentCard.tsx index 0410188..6fa4faf 100644 --- a/client/src/components/investor/InvestmentCard.tsx +++ b/client/src/components/investor/InvestmentCard.tsx @@ -1,5 +1,8 @@ import React, { useState } from 'react'; import type { FundedInvestment } from '../../lib/soroban/investorService'; +import { StatusBadge } from '../campaign/StatusBadge'; +import { Button } from '../ui/Button/Button'; +import type { CampaignStatusTag } from '../../lib/soroban/types'; export interface InvestmentCardProps { investment: FundedInvestment; @@ -7,16 +10,6 @@ export interface InvestmentCardProps { onClaimReturn: (campaignId: string) => Promise; } -const statusBadgeStyles: Record = { - Active: 'bg-blue-100 text-blue-800 dark:bg-blue-950 dark:text-blue-300', - Funding: - 'bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300', - Settled: - 'bg-purple-100 text-purple-800 dark:bg-purple-950 dark:text-purple-300', - Resolved: 'bg-amber-100 text-amber-800 dark:bg-amber-950 dark:text-amber-300', - Failed: 'bg-rose-100 text-rose-800 dark:bg-rose-950 dark:text-rose-300', -}; - export const InvestmentCard: React.FC = ({ investment, onClaimRefund, @@ -51,36 +44,29 @@ export const InvestmentCard: React.FC = ({ !investment.claimed; return ( -
+
- - {investment.status} - - + + ID: {investment.campaignId}
-

+

{investment.title}

-
+
Contributed:{' '} - + ${investment.amountContributed.toLocaleString()}
Claimable:{' '} - + ${investment.claimableAmount.toLocaleString()}
@@ -89,29 +75,31 @@ export const InvestmentCard: React.FC = ({
{investment.claimed ? ( - + Claimed ) : isRefundable ? ( - + Claim Refund + ) : isReturnable ? ( - + Claim Return + ) : ( - + No payout pending )} diff --git a/client/src/components/investor/InvestorSummaryStats.tsx b/client/src/components/investor/InvestorSummaryStats.tsx index 3806e61..cd8afbe 100644 --- a/client/src/components/investor/InvestorSummaryStats.tsx +++ b/client/src/components/investor/InvestorSummaryStats.tsx @@ -12,38 +12,38 @@ export const InvestorSummaryStats: React.FC = ({ }) => { return (
-
- +
+ Funded Projects - + {totalCampaigns}
-
- +
+ Total Contributed - + ${stats.totalInvested.toLocaleString()}
-
- +
+ Claimable / Pending - + ${stats.totalPending.toLocaleString()}
-
- +
+ Total Claimed - + ${stats.totalClaimed.toLocaleString()}
diff --git a/client/src/pages/ActivityFeedPage.tsx b/client/src/pages/ActivityFeedPage.tsx index 981f4fe..b827fb0 100644 --- a/client/src/pages/ActivityFeedPage.tsx +++ b/client/src/pages/ActivityFeedPage.tsx @@ -125,7 +125,7 @@ export function ActivityFeedPage() { }} > Enter one or more campaign IDs above to view activity, or configure{' '} - + VITE_KNOWN_CAMPAIGN_IDS {' '} to preload the global feed. diff --git a/client/src/pages/CampaignDetailPage.tsx b/client/src/pages/CampaignDetailPage.tsx index 987e47c..96e298d 100644 --- a/client/src/pages/CampaignDetailPage.tsx +++ b/client/src/pages/CampaignDetailPage.tsx @@ -6,6 +6,7 @@ import { ActivityFeed } from '../components/campaign/ActivityFeed'; import { useCampaignLiveUpdates } from '../hooks/useCampaignLiveUpdates'; import { useCampaign } from '../hooks/contract/useEscrowQueries'; import { DetailPageSkeleton } from '../components/ui/Skeleton/Skeleton'; +import { Button } from '../components/ui/Button/Button'; export const CampaignDetailPage: React.FC = () => { const { id } = useParams<{ id: string }>(); @@ -27,11 +28,11 @@ export const CampaignDetailPage: React.FC = () => { if (isError || !campaign || !id) { return (
-
-

+
+

Campaign Not Found

-

+

The requested campaign "{id || ''}" could not be loaded from the on-chain contract.

@@ -64,36 +65,36 @@ export const CampaignDetailPage: React.FC = () => { return (
-
+
- + ID: {id}
-

+

{title}

-

+

Farmer: {campaign.farmer}

- + ${currentRaised.toLocaleString()}{' '} - + raised - + Target: ${totalTarget.toLocaleString()} ({percentage}%)
{ aria-label={`Campaign funding progress: ${percentage}% of target raised`} >
-
- +
@@ -132,7 +134,7 @@ export const CampaignDetailPage: React.FC = () => { onSuccess={handleFundingSuccess} /> -
+
{ return (
{/* Header */} -
+
-

+

Investor Dashboard

-

+

Track funded campaigns, claimable returns, and pro-rata refunds.

{isConnected && publicKey ? ( -
+
) : ( -
+
Wallet Disconnected
)}
{!isConnected || !publicKey ? ( -
+
-

+

Connect Your Wallet

-

+

Connect your Soroban-compatible wallet to view your active investments, claimable returns, and refund balances.

@@ -91,22 +91,22 @@ export const InvestorDashboardPage: React.FC = () => { />
-

+

Your Contributions ({(investments ?? []).length})

{(investments ?? []).length === 0 ? ( -
+
-

+

No Funded Investments Found

-

+

You haven't contributed to any PIP campaigns yet. Browse active campaigns to start investing.