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)
+ })
+ })
+})