From 3a82f3b516b9e2d7a86f1c8be69b343f4ff434fe Mon Sep 17 00:00:00 2001 From: akandeisaac021-design Date: Mon, 19 Oct 2026 10:54:21 +0100 Subject: [PATCH 01/10] refactor(billing-history): extract inline styles into BillingHistory.css --- src/pages/BillingHistory.css | 167 +++++++++++++++ src/pages/BillingHistory.tsx | 391 +++++------------------------------ 2 files changed, 222 insertions(+), 336 deletions(-) create mode 100644 src/pages/BillingHistory.css diff --git a/src/pages/BillingHistory.css b/src/pages/BillingHistory.css new file mode 100644 index 00000000..8b7fa7c4 --- /dev/null +++ b/src/pages/BillingHistory.css @@ -0,0 +1,167 @@ +/* BillingHistory.css — class-based styles extracted 1:1 from the former + inline styles. Every selector is scoped under .billing-history (0,2,0) + so it keeps beating global rules in index.css, as inline styles did. */ + +/* Two classes so this beats the legacy `.billing-history { max-width: 960px }` + in index.css regardless of stylesheet order (inline 1000px used to win). */ +.billing-history.bh-page { padding: 24px; max-width: 1000px; margin: 0 auto; } + +.billing-history .bh-header { margin-bottom: 20px; } +.billing-history .bh-header .eyebrow { margin-bottom: 4px; } + +.billing-history .bh-title { + font-size: clamp(1.35rem, 3vw, 1.75rem); + font-weight: 700; + margin: 0 0 6px; + color: var(--text-primary, var(--text, #f9fafb)); +} + +.billing-history .bh-subtitle { + color: var(--text-secondary, #9ca3af); + font-size: 0.875rem; + margin: 0; +} + +.billing-history .bh-kbd { + font-family: monospace; + font-size: 0.75rem; + padding: 1px 4px; + border-radius: 3px; + border: 1px solid var(--line, rgba(255, 255, 255, 0.15)); +} + +/* Visually hidden, still announced by screen readers (live region). */ +.billing-history .bh-visually-hidden { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip: rect(0 0 0 0); + white-space: nowrap; +} + +/* ── Filters ── */ +.billing-history .bh-filters { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-bottom: 16px; + align-items: center; +} +.billing-history .bh-field { display: flex; flex-direction: column; gap: 2px; } +.billing-history .bh-field--grow { flex: 1 1 180px; } +.billing-history .bh-field__label { + font-size: 0.7rem; + color: var(--text-secondary, #9ca3af); + font-weight: 600; +} +.billing-history .bh-field__control { + padding: 5px 8px; + border-radius: 6px; + border: 1px solid var(--line, rgba(255, 255, 255, 0.15)); + background: var(--bg-chip, rgba(255, 255, 255, 0.04)); + color: var(--text-primary, #f9fafb); + font-size: 0.8125rem; +} + +/* ── Summary strip ── */ +.billing-history .bh-summary { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + font-size: 0.8125rem; + color: var(--text-secondary, #9ca3af); +} +.billing-history .bh-net--positive { color: var(--success, #10b981); } +.billing-history .bh-net--negative { color: var(--danger, #ef4444); } +.billing-history .bh-net--zero { color: var(--text-primary, #f9fafb); } + +/* ── Empty state ── */ +.billing-history .bh-empty { + padding: 40px 24px; + text-align: center; + color: var(--text-secondary, #9ca3af); + border-radius: 10px; + border: 1px dashed var(--line, rgba(255, 255, 255, 0.1)); +} + +/* ── Table ── */ +.billing-history .bh-table-wrap { + overflow-x: auto; + border-radius: 10px; + border: 1px solid var(--line, rgba(255, 255, 255, 0.08)); +} +.billing-history .bh-table { + width: 100%; + border-collapse: collapse; + font-size: 0.8125rem; +} +.billing-history .bh-table__head-row { + background: var(--bg-chip, rgba(255, 255, 255, 0.03)); + border-bottom: 1px solid var(--line, rgba(255, 255, 255, 0.08)); +} +.billing-history .bh-table tbody tr:not(:last-child) { + border-bottom: 1px solid var(--line, rgba(255, 255, 255, 0.05)); +} + +.billing-history .bh-th { + padding: 10px 12px; + text-align: left; + font-weight: 600; + color: var(--text-secondary, #9ca3af); + white-space: nowrap; +} +.billing-history .bh-th--date { width: 160px; } +.billing-history .bh-th--description { width: auto; } +.billing-history .bh-th--narrow { width: 110px; } +.billing-history .bh-th--amount { text-align: right; width: 130px; } + +.billing-history .bh-sort-btn { + background: none; + border: none; + padding: 0; + cursor: pointer; + font: inherit; + font-weight: 600; + color: var(--text-secondary, #9ca3af); + display: inline-flex; + align-items: center; + gap: 4px; +} +.billing-history .bh-sort-btn--active { color: var(--accent, #6366f1); } +.billing-history .bh-sort-icon { font-size: 0.65rem; line-height: 1; } + +.billing-history .bh-td { padding: 10px 12px; } +.billing-history .bh-td--date { + white-space: nowrap; + color: var(--text-secondary, #9ca3af); + font-size: 0.75rem; +} +.billing-history .bh-td--type { color: var(--text-secondary, #9ca3af); } +.billing-history .bh-td--amount { text-align: right; font-weight: 600; white-space: nowrap; } +.billing-history .bh-td--hash { + font-family: monospace; + font-size: 0.7rem; + color: var(--accent, #6366f1); +} + +.billing-history .bh-description { + color: var(--text-primary, #f9fafb); + font-weight: 500; + display: inline-flex; + align-items: center; + gap: 6px; +} +.billing-history .bh-direction-icon { font-size: 0.75rem; } + +/* Credit / debit colouring shared by the direction arrow and the amount. */ +.billing-history .bh-tone--credit { color: var(--success, #10b981); } +.billing-history .bh-tone--debit { color: var(--danger, #ef4444); } + +/* ── Keyboard tip ── */ +.billing-history .bh-tip { + margin-top: 14px; + font-size: 0.75rem; + color: var(--text-secondary, #6b7280); +} diff --git a/src/pages/BillingHistory.tsx b/src/pages/BillingHistory.tsx index 44398ffb..0e280c95 100644 --- a/src/pages/BillingHistory.tsx +++ b/src/pages/BillingHistory.tsx @@ -28,6 +28,7 @@ import PreviewCard, { type PreviewCardData } from '../components/PreviewCard'; import StatusBadge, { type StatusVariant } from '../components/StatusBadge'; import { formatUsdcAmount, formatDateShort, resolveTimeZone } from '../utils/format'; import { useAccountContext } from '../hooks/useAccountContext'; +import './BillingHistory.css'; // ── Types ───────────────────────────────────────────────────────────────────── @@ -308,44 +309,16 @@ export function BillingHistory() { // ── Render ──────────────────────────────────────────────────────────────── return ( -
+
{/* Page heading */} -
-

- Billing -

-

+
+

Billing

+

Billing History

-

+

Hover or focus any transaction row to preview on-chain details. Press{' '} - - Esc - {' '} + Esc{' '} to dismiss the preview.

@@ -356,74 +329,34 @@ export function BillingHistory() { role="status" aria-live="polite" aria-atomic="true" - style={{ - position: 'absolute', - width: '1px', - height: '1px', - overflow: 'hidden', - clip: 'rect(0 0 0 0)', - whiteSpace: 'nowrap', - }} + className="bh-visually-hidden" > {filterSummary} {/* ── Filters bar ─────────────────────────────────────────────────── */} -
+
{/* Search */} -
{/* ── Summary strip ────────────────────────────────────────────────── */} From 019cab2a20ee3ae18197166d7d6bed6ce5cd34b7 Mon Sep 17 00:00:00 2001 From: akandeisaac021-design Date: Mon, 19 Oct 2026 22:46:27 +0100 Subject: [PATCH 06/10] feat(billing-history): mobile card, sheet and stacked-control styles --- src/pages/BillingHistory.css | 131 +++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/src/pages/BillingHistory.css b/src/pages/BillingHistory.css index 8b7fa7c4..9772c24a 100644 --- a/src/pages/BillingHistory.css +++ b/src/pages/BillingHistory.css @@ -165,3 +165,134 @@ font-size: 0.75rem; color: var(--text-secondary, #6b7280); } + +/* ── Transaction cards (rendered only below 640px) ──────────────────────── */ + +.billing-history .bh-card-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 12px; +} + +.billing-history .bh-card { + min-width: 0; + padding: 14px; + border-radius: 10px; + border: 1px solid var(--line, rgba(255, 255, 255, 0.08)); + background: var(--bg-chip, rgba(255, 255, 255, 0.03)); +} + +.billing-history .bh-card__top { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 10px; +} + +.billing-history .bh-card__top > :last-child { + flex-shrink: 0; +} + +.billing-history .bh-card__title { + min-width: 0; + margin: 0; + font-size: 0.9375rem; + font-weight: 600; + color: var(--text-primary, #f9fafb); + overflow-wrap: anywhere; +} + +.billing-history .bh-card__amount { + margin: 8px 0 0; + font-size: 1.125rem; + font-weight: 700; +} + +.billing-history .bh-card__footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + margin-top: 10px; + font-size: 0.75rem; + color: var(--text-secondary, #9ca3af); +} + +/* Shared look for the tappable buttons (44 x 44px minimum touch target). */ +.billing-history .bh-card__details, +.billing-history .bh-details__copy { + min-width: 44px; + min-height: 44px; + padding: 0 16px; + border-radius: 8px; + border: 1px solid var(--line, rgba(255, 255, 255, 0.15)); + background: none; + color: var(--accent, #6366f1); + font: inherit; + font-weight: 600; + cursor: pointer; +} + +/* ── Details sheet content ──────────────────────────────────────────────── */ + +.billing-history .bh-details { + margin: 0; +} + +.billing-history .bh-details dt { + margin-top: 14px; + font-size: 0.75rem; + font-weight: 600; + color: var(--text-secondary, #9ca3af); +} + +.billing-history .bh-details dd { + margin: 4px 0 0; + color: var(--text-primary, #f9fafb); + overflow-wrap: anywhere; +} + +.billing-history .bh-details__hash { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; +} + +.billing-history .bh-details__hash code { + font-family: monospace; + font-size: 0.8125rem; + word-break: break-all; +} + +/* ── Small screens: stacked controls, no sideways scrolling ─────────────── */ + +@media (max-width: 639px) { + .billing-history.bh-page { + padding: 16px; + } + + .billing-history .bh-filters { + flex-direction: column; + align-items: stretch; + } + + /* The 180px flex-basis would become a height in a column layout. */ + .billing-history .bh-field--grow { + flex: 0 0 auto; + } + + /* 16px text stops iOS Safari from zooming the page when a control is focused. */ + .billing-history .bh-field__control { + min-height: 44px; + font-size: 1rem; + } + + .billing-history .bh-summary { + flex-wrap: wrap; + gap: 4px 12px; + } +} From baf44612571a64c581262fd4d6f82d35dff5700e Mon Sep 17 00:00:00 2001 From: akandeisaac021-design Date: Tue, 20 Oct 2026 06:31:22 +0100 Subject: [PATCH 07/10] fix(billing-history): use tap wording instead of hover and keyboard hints on small screens --- src/pages/BillingHistory.test.tsx | 24 ++++++++++++++++++++++++ src/pages/BillingHistory.tsx | 26 +++++++++++++++++--------- 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/src/pages/BillingHistory.test.tsx b/src/pages/BillingHistory.test.tsx index c9e88258..ce93b44e 100644 --- a/src/pages/BillingHistory.test.tsx +++ b/src/pages/BillingHistory.test.tsx @@ -818,3 +818,27 @@ describe('BillingHistory — filters on mobile', () => { expect(mobile).toBe(MOCK_TRANSACTIONS.filter((t) => t.type === 'Deposit').length); }); }); + +describe('BillingHistory — touch-friendly wording', () => { + afterEach(() => { cleanup(); _reset(); window.matchMedia = originalMatchMedia; }); + + it('mobile: tells users to tap Details instead of hovering', () => { + setViewport('mobile'); + renderBH(); + expect(screen.getByText(/tap details/i)).toBeTruthy(); + expect(screen.queryByText(/hover/i)).toBeNull(); + }); + + it('mobile: hides the keyboard tip', () => { + setViewport('mobile'); + renderBH(); + expect(screen.queryByText(/keyboard tip/i)).toBeNull(); + }); + + it('desktop: keeps the hover wording and the keyboard tip', () => { + setViewport('desktop'); + renderBH(); + expect(screen.getByText(/hover or focus any transaction row/i)).toBeTruthy(); + expect(screen.getByText(/keyboard tip/i)).toBeTruthy(); + }); +}); diff --git a/src/pages/BillingHistory.tsx b/src/pages/BillingHistory.tsx index 2c953160..48a6471b 100644 --- a/src/pages/BillingHistory.tsx +++ b/src/pages/BillingHistory.tsx @@ -333,9 +333,15 @@ export function BillingHistory() { Billing History

- Hover or focus any transaction row to preview on-chain details. Press{' '} - Esc{' '} - to dismiss the preview. + {isMobile ? ( + 'Tap Details on any transaction to see its on-chain details.' + ) : ( + <> + Hover or focus any transaction row to preview on-chain details. Press{' '} + Esc{' '} + to dismiss the preview. + + )}

@@ -584,12 +590,14 @@ export function BillingHistory() { )} {/* ── Accessible keyboard tip ──────────────────────────────────────── */} -

- Keyboard tip: Tab to a transaction description to open its preview. - Press{' '} - Esc{' '} - to close it and return focus to that row. -

+ {!isMobile && ( +

+ Keyboard tip: Tab to a transaction description to open its preview. + Press{' '} + Esc{' '} + to close it and return focus to that row. +

+ )} setSelectedTx(null)} From 4b80c328f5fef50e157ce8a7c24f3f70dfeafa90 Mon Sep 17 00:00:00 2001 From: akandeisaac021-design Date: Tue, 20 Oct 2026 06:32:45 +0100 Subject: [PATCH 08/10] docs(billing-history): document the mobile card layout and details sheet --- docs/BillingHistoryPreview.md | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/docs/BillingHistoryPreview.md b/docs/BillingHistoryPreview.md index 5eb9df6e..96f47f77 100644 --- a/docs/BillingHistoryPreview.md +++ b/docs/BillingHistoryPreview.md @@ -39,6 +39,18 @@ The Billing History page displays a paginated table of USDC transactions. Each r 4. Press **Escape** to dismiss the card and return focus to the trigger. 5. Tab again to move to the next row (card closes automatically on blur). +### Touch user (below 640px) + +Below 640px the table is replaced by a list of transaction cards. Only one of the two layouts is in the DOM at a time, so screen readers never read both. + +1. Each card is a list item with a heading (the description), a status badge, the signed amount (`+` for credits, a minus sign for debits), the date, and a **Details** button. +2. Tap **Details** to open a bottom sheet with the same on-chain details as the hover preview: transaction hash (with a **Copy** button), network, confirmations and timestamp. +3. Close the sheet with the close button, a tap on the backdrop, or **Escape**. Focus returns to that card's Details button. +4. Sorting uses a single **Sort by** select (date or amount, either direction). It writes the same `sort` and `dir` URL params as the desktop column headers, so a shared link sorts the same way on both layouts. +5. Filters stack in one column and behave exactly as on desktop. The net-balance summary stays at the top. + +The breakpoint is `(max-width: 639px)`. It is read in JavaScript by the `useMediaQuery` hook (to choose which layout to render) and in CSS (to stack the filters). All touch targets are at least 44x44px. + --- ## Filtering @@ -187,11 +199,12 @@ All colours in `PreviewCard` and `BillingHistory` use CSS custom properties so b ## Responsive behaviour -- The table container uses `overflow-x: auto` so horizontal scrolling is available on small viewports. -- The preview card has `maxWidth: 90vw` — it never overflows the viewport on narrow screens. -- Filter controls use `flexWrap: wrap` so they stack on small screens without clipping. +- **Below 640px:** transactions render as cards (see "Touch user" above). The table is not rendered, so there is no horizontal scrolling at 320px. +- **640px and above:** the table renders as before. Its container keeps `overflow-x: auto` as a fallback for narrow desktop windows. +- The preview card has `maxWidth: 90vw`, so it never overflows the viewport on narrow screens. +- Filter controls wrap on wider screens and stack into one column below 640px. - The page heading uses `clamp(1.35rem, 3vw, 1.75rem)` for fluid sizing. - +- Styles live in `src/pages/BillingHistory.css` (`.billing-history .bh-*` classes), not inline. --- ## Running the tests From e17b906b3d602645aadd0cd18dcdde0de975e03a Mon Sep 17 00:00:00 2001 From: akandeisaac021-design Date: Tue, 20 Oct 2026 06:43:07 +0100 Subject: [PATCH 09/10] test(billing-history): add component tests and distinguish Details buttons by date --- src/components/BillingSortSelect.test.tsx | 30 ++++++++++ src/components/TransactionCard.test.tsx | 66 ++++++++++++++++++++++ src/components/TransactionCard.tsx | 2 +- src/components/TransactionDetails.test.tsx | 53 +++++++++++++++++ 4 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 src/components/BillingSortSelect.test.tsx create mode 100644 src/components/TransactionCard.test.tsx create mode 100644 src/components/TransactionDetails.test.tsx diff --git a/src/components/BillingSortSelect.test.tsx b/src/components/BillingSortSelect.test.tsx new file mode 100644 index 00000000..fa04c137 --- /dev/null +++ b/src/components/BillingSortSelect.test.tsx @@ -0,0 +1,30 @@ +// @vitest-environment jsdom +import { cleanup, fireEvent, render, screen } from '@testing-library/react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { BillingSortSelect } from './BillingSortSelect'; + +const getSelect = () => screen.getByRole('combobox', { name: /sort transactions/i }) as HTMLSelectElement; + +describe('BillingSortSelect', () => { + afterEach(cleanup); + + it('shows a disabled "Default order" placeholder when nothing is sorted', () => { + render(); + expect(getSelect().value).toBe(''); + expect(screen.getByRole('option', { name: 'Default order' })).toBeTruthy(); + }); + + it('offers the four date/amount options and no placeholder once sorted', () => { + render(); + expect(getSelect().value).toBe('amount:desc'); + expect(screen.getAllByRole('option')).toHaveLength(4); + expect(screen.queryByRole('option', { name: 'Default order' })).toBeNull(); + }); + + it('reports the chosen column and direction', () => { + const onChange = vi.fn(); + render(); + fireEvent.change(getSelect(), { target: { value: 'date:asc' } }); + expect(onChange).toHaveBeenCalledWith('date', 'asc'); + }); +}); diff --git a/src/components/TransactionCard.test.tsx b/src/components/TransactionCard.test.tsx new file mode 100644 index 00000000..b8ab71ac --- /dev/null +++ b/src/components/TransactionCard.test.tsx @@ -0,0 +1,66 @@ +// @vitest-environment jsdom +import { cleanup, fireEvent, render, screen } from '@testing-library/react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { TransactionCard } from './TransactionCard'; +import type { BillingTransaction } from '../pages/BillingHistory'; + +const tx: BillingTransaction = { + id: 'tx-1', + description: 'USDC vault deposit', + type: 'Deposit', + direction: 'credit', + amount: 100, + status: 'success', + timestamp: '2026-07-25T14:32:00Z', + txHash: 'A'.repeat(64), + network: 'Stellar Mainnet', + confirmations: 120, +}; + +function renderCard(overrides: Partial = {}, onDetails = vi.fn()) { + const item = { ...tx, ...overrides }; + render( +
    + +
, + ); + return { item, onDetails }; +} + +describe('TransactionCard', () => { + afterEach(cleanup); + + it('is a list item with the description as its heading', () => { + renderCard(); + expect(screen.getByRole('listitem')).toBeTruthy(); + expect(screen.getByRole('heading', { name: 'USDC vault deposit' })).toBeTruthy(); + }); + + it('shows a plus sign for credits and a minus sign for debits', () => { + renderCard(); + expect(screen.getByText(/^\+.*USDC$/)).toBeTruthy(); + cleanup(); + renderCard({ direction: 'debit' }); + expect(screen.getByText(/^\u2212.*USDC$/)).toBeTruthy(); + }); + + it('shows the date and a text status label (not colour alone)', () => { + renderCard(); + expect(screen.getByText('Jul 25, 02:32 PM')).toBeTruthy(); + expect(screen.getByText('Confirmed')).toBeTruthy(); + }); + + it('calls onDetails with the transaction and the tapped button', () => { + const { item, onDetails } = renderCard(); + const button = screen.getByRole('button', { name: /^details/i }); + fireEvent.click(button); + expect(onDetails).toHaveBeenCalledWith(item, button); + }); + + it('labels the Details button with the date so identical descriptions stay distinguishable', () => { + renderCard(); + expect( + screen.getByRole('button', { name: 'Details for USDC vault deposit, Jul 25, 02:32 PM' }), + ).toBeTruthy(); + }); +}); diff --git a/src/components/TransactionCard.tsx b/src/components/TransactionCard.tsx index e877ca85..626015b6 100644 --- a/src/components/TransactionCard.tsx +++ b/src/components/TransactionCard.tsx @@ -27,7 +27,7 @@ export function TransactionCard({ tx, dateText, statusLabel, onDetails }: Transa