```
**Existing `sectionLabelStyle` (lines 44–51) — reuse unchanged inside tab panels:**
```ts
const sectionLabelStyle: React.CSSProperties = {
fontSize: 'var(--text-label-size, 13px)',
fontWeight: 600,
color: 'var(--color-text-muted)',
textTransform: 'uppercase',
letterSpacing: '0.06em',
marginBottom: 'var(--space-2, 8px)',
};
```
---
### `apps/pwa/e2e/layout.spec.ts` — add FAB/BottomTabBar overlap assertion
**Analog:** self, lines 30–60 (existing test structure pattern)
**Existing test structure to follow (lines 31-60):**
```ts
test.describe('Rule 1/3/4 — BottomTabBar tap targets and in-viewport position', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/calendar');
});
test('Calendar tab meets 44×44px touch-target minimum (Rule 1)', async ({ page }) => {
const nav = page.getByRole('navigation', { name: 'Main navigation' });
const calTab = nav.getByRole('link', { name: 'Calendar' });
const box = await calTab.boundingBox();
expect(box, 'Calendar tab bounding box must not be null').not.toBeNull();
expect(box!.width, 'Calendar tab width ≥ 44px').toBeGreaterThanOrEqual(44);
expect(box!.height, 'Calendar tab height ≥ 44px').toBeGreaterThanOrEqual(44);
});
```
**New overlap assertion to add:**
```ts
test('New Event FAB does not overlap BottomTabBar (A — phone only)', async ({ page }, testInfo) => {
test.skip(testInfo.project.name === 'desktop', 'Phone-only assertion');
await page.goto('/calendar');
const fab = page.getByRole('button', { name: 'New Event' });
const nav = page.getByRole('navigation', { name: 'Main navigation' });
const fabBox = await fab.boundingBox();
const navBox = await nav.boundingBox();
expect(fabBox, 'FAB bounding box must not be null').not.toBeNull();
expect(navBox, 'BottomTabBar bounding box must not be null').not.toBeNull();
// FAB bottom edge must be at or above the BottomTabBar top edge
expect(fabBox!.y + fabBox!.height).toBeLessThanOrEqual(navBox!.y);
});
```
---
### `apps/pwa/e2e/admin.spec.ts` (new file)
**Analog:** `apps/pwa/e2e/layout.spec.ts` — copy file header pattern, test.describe structure, page.goto pattern
**File header + structure pattern (layout.spec.ts lines 1–27):**
```ts
/**
* admin.spec.ts — TEST-XX
*
* Admin page UI assertions: two-tab ARIA pattern, success toasts, sheet centering.
* ...
*
* Runs on all three device profiles unless skipped via testInfo.project.name.
*/
import { test, expect } from '@playwright/test';
test.describe('Admin tab strip — ARIA tabs pattern (D-10)', () => {
test.beforeEach(async ({ page }) => {
// Note: requires an admin session — playwright.config.ts storageState for admin
await page.goto('/admin');
});
test('Tab strip has correct ARIA roles', async ({ page }) => {
await expect(page.getByRole('tablist')).toBeVisible();
await expect(page.getByRole('tab', { name: 'Members & Accounts' })).toBeVisible();
await expect(page.getByRole('tab', { name: 'Settings' })).toBeVisible();
});
test('ArrowRight switches to Settings tab', async ({ page }) => {
const membersTab = page.getByRole('tab', { name: 'Members & Accounts' });
await membersTab.focus();
await page.keyboard.press('ArrowRight');
await expect(page.getByRole('tab', { name: 'Settings' })).toHaveAttribute('aria-selected', 'true');
});
});
```
---
## Shared Patterns
### Phone/Desktop Breakpoint
**Source:** `apps/pwa/src/components/BottomTabBar.tsx` lines 23–25; `apps/pwa/src/App.tsx` line 64
**Apply to:** All sheet centering fixes (SettingsSheet, ChangePasswordSheet, LinkOidcSheet, ResetPasswordSheet, CredentialSheet), admin toast positioning, admin tab strip phone layout check
```ts
// Identical function defined in BottomTabBar.tsx, App.tsx, CalendarShell.tsx
function isPhone(): boolean {
return typeof window !== 'undefined' && window.matchMedia('(max-width: 767px)').matches;
}
// Or inline: window.matchMedia('(max-width: 767px)').matches
```
### Sheet/Dialog Pattern
**Source:** `apps/pwa/src/components/SettingsSheet.tsx` lines 170–202 (backdrop + dialog wrapper)
**Apply to:** All sheets that need desktop-centering fix
```tsx
{/* Backdrop — unchanged across all sheets */}
{/* Sheet outer wrapper — gains phone/desktop branch */}
```
### Section Divider Pattern
**Source:** `apps/pwa/src/components/SettingsSheet.tsx` lines 372–376
**Apply to:** Logout row separator in SettingsSheet
```tsx
```
### Mutation + Toast Pattern
**Source:** `apps/pwa/src/routes/AdminPage.tsx` lines 197–235 (`createMemberMutation`), `apps/pwa/src/components/SyncStateToast.tsx` lines 72–79 (auto-dismiss), lines 159–189 (toast render)
**Apply to:** AdminPage create-member and reset-password success feedback
Auto-dismiss pattern from SyncStateToast (lines 72–79):
```ts
useEffect(() => {
if (status !== 'done') return; // adapt: if (!toast) return;
const timer = setTimeout(() => {
setLastSyncedUid(null); // adapt: setToast(null)
}, 2000); // use 3000ms for admin toasts per UI-SPEC
return () => clearTimeout(timer);
}, [status, setLastSyncedUid]);
```
### Accessible Button Row Pattern
**Source:** `apps/pwa/src/components/SettingsSheet.tsx` lines 390–410 (Change password button row)
**Apply to:** Logout button in SettingsSheet
```tsx
```
---
## No Analog Found
| File | Role | Data Flow | Reason |
|------|------|-----------|--------|
| `apps/pwa/public/logo.svg` | asset | transform | New AI-generated SVG logo — no existing brand mark in codebase |
| `apps/pwa/public/favicon.svg`, `favicon.ico`, `icon-maskable-512.png` | asset | transform | New generated assets — none exist in `public/` yet |
| `apps/pwa/pwa-assets.config.ts` | config | transform | New `@vite-pwa/assets-generator` config — no prior asset-generation config in repo |
---
## Critical Pitfalls (for Planner)
1. **Maskable icon must be a separate file.** `icon-maskable-512.png` is a distinct generated asset with safe-zone padding — the defect is reusing `icon-512.png` for the maskable purpose. Fix by generating `icon-maskable-512.png` via `@vite-pwa/assets-generator`.
2. **`paddingBottom` in `contentStyle` is phone-only.** The `...(phone ? {...} : {})` spread pattern (from outerStyle at App.tsx lines 144–152) ensures desktop gets no extra bottom padding.
3. **`--sx-color-*` overrides must stay inside the combined `tokens.css` rule block.** Do not split them to a separate selector — they must override the `@schedule-x/theme-default` values by staying in the same specificity context.
4. **`fetchLocalLogout` error must not prevent navigation.** Wrap in try/catch and call `navigate('/login')` in both branches — fire-and-best-effort semantics per UI-SPEC §D-07.
5. **`--brand-logo-border-radius` update required after logo approval.** The current `50%` value (circle) clips an SVG logo that draws its own shape. Update to `12px` (warm/rounded) or `0` (if SVG has own border-radius) at the checkpoint — before wiring.
6. **Admin tab state is not URL-persisted.** Tab state is `useState` only — navigating away and back resets to Tab 1 ("Members & Accounts"). This is intentional per UI-SPEC §D-10.
---
## Metadata
**Analog search scope:** `apps/pwa/src/components/`, `apps/pwa/src/routes/`, `apps/pwa/src/api/`, `apps/pwa/e2e/`, `apps/pwa/`
**Files read:** 10 source files
**Pattern extraction date:** 2026-06-18