Skip to content
Merged
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
1 change: 1 addition & 0 deletions .claude/metrics/review-metrics.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,4 @@
{"pr":1141,"issues":[1135,1136,1137,1138,1139,1140],"epic":null,"type":"fix","createdAt":"2026-03-22T10:59:20Z","mergedAt":"2026-03-22T12:30:00Z","filesChanged":17,"linesChanged":865,"touchesClient":true,"touchesServer":false,"fixLoopCount":0,"internalFixLoopCount":1,"fixLoopTriggers":[{"round":1,"agents":["frontend-developer"]}],"reviews":[{"agent":"product-architect","verdict":"approve","findings":{"critical":0,"high":0,"medium":0,"low":0,"informational":2},"round":1},{"agent":"ux-designer","verdict":"approve","findings":{"critical":0,"high":0,"medium":0,"low":2,"informational":1},"round":1}],"totalFindings":{"critical":0,"high":0,"medium":0,"low":2,"informational":3}}
{"pr":1144,"issues":[1142],"epic":null,"type":"feat","createdAt":"2026-03-22T11:13:49Z","mergedAt":"2026-03-22T13:00:00Z","filesChanged":30,"linesChanged":1788,"touchesClient":true,"touchesServer":false,"fixLoopCount":0,"internalFixLoopCount":1,"fixLoopTriggers":[{"round":1,"agents":["frontend-developer","translator","e2e-test-engineer"]}],"reviews":[{"agent":"product-architect","verdict":"approve","findings":{"critical":0,"high":0,"medium":0,"low":0,"informational":0},"round":1},{"agent":"ux-designer","verdict":"approve","findings":{"critical":0,"high":0,"medium":0,"low":0,"informational":0},"round":1},{"agent":"product-owner","verdict":"approve","findings":{"critical":0,"high":0,"medium":0,"low":0,"informational":2},"round":1}],"totalFindings":{"critical":0,"high":0,"medium":0,"low":0,"informational":2}}
{"pr":1163,"issues":[1162],"epic":null,"type":"fix","createdAt":"2026-03-22T13:23:33Z","mergedAt":"2026-03-22T14:00:00Z","filesChanged":6,"linesChanged":44,"touchesClient":true,"touchesServer":false,"fixLoopCount":0,"internalFixLoopCount":0,"fixLoopTriggers":[],"reviews":[{"agent":"product-architect","verdict":"approve","findings":{"critical":0,"high":0,"medium":0,"low":0,"informational":0},"round":1}],"totalFindings":{"critical":0,"high":0,"medium":0,"low":0,"informational":0}}
{"pr":1165,"issues":[1161],"epic":null,"type":"fix","createdAt":"2026-03-22T13:39:25Z","mergedAt":"2026-03-22T14:30:00Z","filesChanged":7,"linesChanged":292,"touchesClient":true,"touchesServer":false,"fixLoopCount":0,"internalFixLoopCount":0,"fixLoopTriggers":[],"reviews":[{"agent":"product-architect","verdict":"approve","findings":{"critical":0,"high":0,"medium":0,"low":0,"informational":1},"round":1},{"agent":"ux-designer","verdict":"approve","findings":{"critical":0,"high":0,"medium":1,"low":1,"informational":0},"round":1}],"totalFindings":{"critical":0,"high":0,"medium":1,"low":1,"informational":1}}
8 changes: 4 additions & 4 deletions client/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ describe('App', () => {
});
});

it('renders the AppShell layout with sidebar and header', async () => {
it('renders the AppShell layout with sidebar and floating menu button', async () => {
render(<App />);

// Wait for auth loading to complete
Expand All @@ -365,9 +365,9 @@ describe('App', () => {
const sidebar = screen.getByRole('complementary');
expect(sidebar).toBeInTheDocument();

// Header should be present
const header = screen.getByRole('banner');
expect(header).toBeInTheDocument();
// Floating menu button (FAB) should be present
const fab = screen.getByTestId('menu-fab');
expect(fab).toBeInTheDocument();

// Main content area should be present
const main = screen.getByRole('main');
Expand Down
45 changes: 45 additions & 0 deletions client/src/components/AppShell/AppShell.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,48 @@
display: block;
}
}

.menuFab {
display: none;
position: fixed;
bottom: var(--spacing-6);
right: var(--spacing-6);
min-width: 44px;
min-height: 44px;
width: var(--spacing-12);
height: var(--spacing-12);
border-radius: var(--radius-full);
background-color: var(--color-primary);
color: var(--color-primary-text);
font-size: var(--font-size-2xl);
border: none;
cursor: pointer;
z-index: calc(var(--z-overlay) + 10);
box-shadow: var(--shadow-md);
transition: var(--transition-button);
}

.menuFab:hover {
background-color: var(--color-primary-hover);
box-shadow: var(--shadow-lg);
}

.menuFab:focus-visible {
outline: none;
box-shadow: var(--shadow-focus);
}

/* Mobile and tablet: show floating menu button */
@media (max-width: 1024px) {
.menuFab {
display: flex;
align-items: center;
justify-content: center;
}
}

@media (prefers-reduced-motion: reduce) {
.menuFab {
transition: none;
}
}
66 changes: 40 additions & 26 deletions client/src/components/AppShell/AppShell.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ describe('AppShell', () => {
}
});

it('renders sidebar, header, and outlet area', () => {
it('renders sidebar, floating menu button, and outlet area', () => {
renderWithRouter(
<Routes>
<Route element={<AppShellModule.AppShell />} path="*">
Expand All @@ -63,9 +63,9 @@ describe('AppShell', () => {
const sidebar = screen.getByRole('complementary');
expect(sidebar).toBeInTheDocument();

// Header should be present
const header = screen.getByRole('banner');
expect(header).toBeInTheDocument();
// Floating menu button (FAB) should be present
const fab = screen.getByTestId('menu-fab');
expect(fab).toBeInTheDocument();

// Main content area should be present
const main = screen.getByRole('main');
Expand Down Expand Up @@ -133,7 +133,7 @@ describe('AppShell', () => {
expect(screen.getByRole('button', { name: /^settings$/i })).toBeInTheDocument();
});

it('renders header with menu toggle button', () => {
it('renders floating menu button for mobile sidebar toggle', () => {
renderWithRouter(
<Routes>
<Route element={<AppShellModule.AppShell />} path="*">
Expand All @@ -142,8 +142,24 @@ describe('AppShell', () => {
</Routes>,
);

const menuButton = screen.getByRole('button', { name: /open menu/i });
expect(menuButton).toBeInTheDocument();
const fab = screen.getByTestId('menu-fab');
expect(fab).toBeInTheDocument();
expect(fab).toHaveAttribute('type', 'button');
});

it('floating menu button has data-testid and type="button"', () => {
renderWithRouter(
<Routes>
<Route element={<AppShellModule.AppShell />} path="*">
<Route index element={<div>Test Content</div>} />
</Route>
</Routes>,
);

const fab = screen.getByTestId('menu-fab');
expect(fab).toBeInTheDocument();
expect(fab).toHaveAttribute('type', 'button');
expect(fab).toHaveAttribute('aria-label', 'Open menu');
});

it('overlay is not visible initially (sidebar starts closed)', () => {
Expand Down Expand Up @@ -270,9 +286,9 @@ describe('AppShell', () => {
let overlay = document.querySelector('[data-testid="sidebar-overlay"]');
expect(overlay).toBeInTheDocument();

// Header button should now say "Close menu"
const header = screen.getByRole('banner');
menuButton = within(header).getByRole('button', { name: /close menu/i });
// FAB button should now say "Close menu"
menuButton = screen.getByTestId('menu-fab');
expect(menuButton).toHaveAttribute('aria-label', 'Close menu');

// Close
await user.click(menuButton);
Expand All @@ -298,17 +314,16 @@ describe('AppShell', () => {
</Routes>,
);

// Initially button shows hamburger icon
let menuButton = screen.getByRole('button', { name: /open menu/i });
expect(menuButton).toHaveTextContent('☰');
// Initially FAB shows hamburger icon
const fab = screen.getByTestId('menu-fab');
expect(fab).toHaveTextContent('☰');

// Click to open
await user.click(menuButton);
await user.click(fab);

// Header button should now show close icon
const header = screen.getByRole('banner');
menuButton = within(header).getByRole('button', { name: /close menu/i });
expect(menuButton).toHaveTextContent('✕');
// FAB button should now show close icon
const fabAfterOpen = screen.getByTestId('menu-fab');
expect(fabAfterOpen).toHaveTextContent('✕');
});

it('clicking close button inside sidebar closes the sidebar', async () => {
Expand Down Expand Up @@ -351,16 +366,15 @@ describe('AppShell', () => {
</Routes>,
);

// Initially button has "Open menu" label
let menuButton = screen.getByRole('button', { name: /open menu/i });
expect(menuButton).toHaveAttribute('aria-label', 'Open menu');
// Initially FAB has "Open menu" label
const fab = screen.getByTestId('menu-fab');
expect(fab).toHaveAttribute('aria-label', 'Open menu');

// Click to open
await user.click(menuButton);
await user.click(fab);

// Header button should now have "Close menu" label
const header = screen.getByRole('banner');
menuButton = within(header).getByRole('button', { name: /close menu/i });
expect(menuButton).toHaveAttribute('aria-label', 'Close menu');
// FAB button should now have "Close menu" label
const fabAfterOpen = screen.getByTestId('menu-fab');
expect(fabAfterOpen).toHaveAttribute('aria-label', 'Close menu');
});
});
13 changes: 11 additions & 2 deletions client/src/components/AppShell/AppShell.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Outlet } from 'react-router-dom';
import { Suspense, useState, useCallback, useEffect } from 'react';
import { Header } from '../Header/Header';
import { useTranslation } from 'react-i18next';
import { Sidebar } from '../Sidebar/Sidebar';
import styles from './AppShell.module.css';

export function AppShell() {
const { t } = useTranslation('common');
const [isSidebarOpen, setIsSidebarOpen] = useState(false);

const handleToggleSidebar = useCallback(() => {
Expand Down Expand Up @@ -39,8 +40,16 @@ export function AppShell() {
data-testid="sidebar-overlay"
/>
)}
<button
type="button"
className={styles.menuFab}
onClick={handleToggleSidebar}
aria-label={isSidebarOpen ? t('aria.closeMenu') : t('aria.openMenu')}
data-testid="menu-fab"
>
{isSidebarOpen ? '✕' : '☰'}
</button>
<div className={styles.mainContent}>
<Header onToggleSidebar={handleToggleSidebar} isSidebarOpen={isSidebarOpen} />
<main className={styles.pageContent}>
<Suspense fallback={<div className={styles.loading}>Loading...</div>}>
<Outlet />
Expand Down
47 changes: 0 additions & 47 deletions client/src/components/Header/Header.module.css

This file was deleted.

88 changes: 0 additions & 88 deletions client/src/components/Header/Header.test.tsx

This file was deleted.

25 changes: 0 additions & 25 deletions client/src/components/Header/Header.tsx

This file was deleted.

Loading