diff --git a/README.md b/README.md index fba0596..c2e8164 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ The app runs at http://localhost:5173 by default. ``` src/ - components/ reusable UI (Navbar, Footer, QuoteCard, TransferRow, ...) + components/ reusable UI (Navbar, Sidebar, Footer, QuoteCard, TransferRow, ...) pages/ route screens (Home, SendMoney, Transfers, NotFound) services/ mock api, wallet, fx and quote logic hooks/ useWallet, useTransfers @@ -63,6 +63,20 @@ cp .env.example .env - `npm run test:watch` — watch mode for local test development - `npm run lighthouse` — run Lighthouse CI against the local preview server +## Responsive Layout + +The app adapts to three viewport ranges: + +| Range | Target | Layout | +|---|---|---| +| >1024px | Desktop | Full sidebar (240px), 3-column features grid | +| 721–1024px | Tablet landscape | Full sidebar with toggle, 2-column features grid, compact content padding | +| ≤720px | Tablet portrait / mobile | Collapsed sidebar (60px), single-column layouts, wrapped navbar | +| ≤420px | Small mobile | Stacked hero actions, single-column currency grid | + +The Sidebar is rendered alongside the main content area on all viewports wider than +720px and collapses automatically at narrower widths. + ## Testing The test suite includes comprehensive coverage of wallet connection handling: @@ -86,9 +100,9 @@ The test suite includes comprehensive coverage of wallet connection handling: - User interaction flows Integration tests cover send-money validation, successful transfer submission, -pending button behavior, duplicate-submission prevention, Transfers page filter -sync (search, status, and date-range presets), and keyboard tab order across -the main pages. +pending button behavior, duplicate-submission prevention, Transfers page +filter sync (search, status, and date-range presets such as last 7/30/90 days), +and landscape tablet layout integration. ## Accessibility diff --git a/src/App.jsx b/src/App.jsx index 8cc9ab4..8b66af6 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -3,6 +3,7 @@ import { AppProvider } from './context/AppContext.jsx' import { LocaleProvider } from './context/I18nContext.jsx' import ErrorBoundary from './components/ErrorBoundary.jsx' import Navbar from './components/Navbar.jsx' +import Sidebar from './components/Sidebar.jsx' import Footer from './components/Footer.jsx' import Home from './pages/Home.jsx' import SendMoney from './pages/SendMoney.jsx' @@ -22,6 +23,8 @@ export default function App() { Skip to content + +
@@ -34,6 +37,7 @@ export default function App() {
+
diff --git a/src/responsive.css b/src/responsive.css index f83cd4c..e95e41a 100644 --- a/src/responsive.css +++ b/src/responsive.css @@ -1,5 +1,15 @@ /* Responsive tweaks for narrow viewports. */ +@media (min-width: 721px) and (max-width: 1024px) { + .features { + grid-template-columns: repeat(2, 1fr); + } + + .app-main { + padding: 1.5rem 1.25rem; + } +} + @media (max-width: 720px) { .navbar { flex-wrap: wrap; diff --git a/test/integration/landscape-layout.test.jsx b/test/integration/landscape-layout.test.jsx new file mode 100644 index 0000000..05771e8 --- /dev/null +++ b/test/integration/landscape-layout.test.jsx @@ -0,0 +1,88 @@ +import fs from 'node:fs' +import path from 'node:path' +import { render, screen } from '@testing-library/react' +import { beforeEach, describe, expect, it } from 'vitest' +import App from '../../src/App.jsx' + +function readCSS(filename) { + return fs.readFileSync( + path.resolve(process.cwd(), 'src', filename), + 'utf8', + ) +} + +describe('landscape layout for tablets', () => { + describe('responsive.css', () => { + const css = readCSS('responsive.css') + + it('contains a tablet landscape breakpoint (721px–1024px)', () => { + expect(css).toMatch( + /@media\s*\(\s*min-width\s*:\s*721px\s*\)\s*and\s*\(\s*max-width\s*:\s*1024px\s*\)/, + ) + }) + + it('adjusts .features to 2 columns within the landscape breakpoint', () => { + const start = css.indexOf('@media (min-width: 721px)') + const block = css.slice(start, css.indexOf('@media (max-width: 720px)')) + expect(block).toContain('grid-template-columns') + expect(block).toContain('repeat(2, 1fr)') + }) + + it('adjusts .app-main padding within the landscape breakpoint', () => { + const start = css.indexOf('@media (min-width: 721px)') + const block = css.slice(start, css.indexOf('@media (max-width: 720px)')) + expect(block).toContain('.app-main') + expect(block).toContain('padding') + }) + }) + + describe('sidebar integration in App', () => { + beforeEach(() => { + localStorage.clear() + }) + + it('renders the Sidebar as part of the app layout', () => { + render() + const sidebar = document.querySelector('.sidebar') + expect(sidebar).toBeInTheDocument() + }) + + it('renders sidebar navigation links within the app', () => { + render() + const homeLinks = screen.getAllByText('Home') + expect(homeLinks.length).toBeGreaterThanOrEqual(1) + const sidebarLinks = document.querySelectorAll('.sidebar-link') + expect(sidebarLinks.length).toBe(3) + }) + + it('renders the sidebar toggle button', () => { + render() + const toggle = screen.getByLabelText('Collapse sidebar') + expect(toggle).toBeInTheDocument() + }) + + it('renders the content area alongside the sidebar', () => { + render() + const app = document.querySelector('.app') + const sidebar = document.querySelector('.sidebar') + const content = document.querySelector('.app-content') + + expect(app).toBeInTheDocument() + expect(sidebar).toBeInTheDocument() + expect(content).toBeInTheDocument() + + expect(app.contains(sidebar)).toBe(true) + expect(app.contains(content)).toBe(true) + }) + + it('renders Navbar and Footer inside .app-content', () => { + render() + const content = document.querySelector('.app-content') + const navbar = document.querySelector('.navbar') + const footer = document.querySelector('.footer') + + expect(content.contains(navbar)).toBe(true) + expect(content.contains(footer)).toBe(true) + }) + }) +})