Frontend: App shell — responsive layout, navigation, routing #17
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Build the app shell that provides the responsive layout skeleton and navigation for all authenticated screens. Three distinct layouts by breakpoint — not a scaled-up mobile.
Responsive Breakpoints
Mobile Tab Bar
--green-tintbg +--green-darktext, 10px DM Sans weight 500Tablet Navigation
--green-tintbg +--green-darktextDesktop Sidebar (224px)
--greenbg, 4px radius--green-tintbg +--green-darktext + weight 500Desktop 3-Panel Layout
Many screens use the 3-panel model:
Pre-auth Layout
Onboarding screens (A1, A2, A4) use a separate full-viewport split layout — no sidebar/tabs.
Acceptance Criteria
--green-tintbg +--green-darktext (always)Spec references: Navigation and layout specs are embedded in each journey file. Key references:
specs/frontend/j2-plan-the-week.html(C1 planner has the most complete sidebar spec)specs/frontend/j6-household-setup.html(A1, A2, A4)👨💻 Kai — Frontend Engineer
Questions & Observations
(group)layouts. Are we doing(auth)for the app shell and(public)for pre-auth? Or a different split? This affects where+layout.svelteand+layout.server.tslive and how the auth guard inhooks.server.tsmaps to route groups.+layout.server.tsof the auth group? If so, every navigation triggers a fetch. Should we consider a separate$state-based store or invalidation strategy so sidebar data doesn't refetch on every page navigation?Suggestions
AppShell.svelteas the top-level layout component, withMobileTabBar.svelte,TabletNavBar.svelte, andDesktopSidebar.svelteas children. Use a$derived()breakpoint rune (from amatchMedialistener in$effect()) to switch between them. Keep each nav component focused and testable.window.matchMediain an$effect()inside a sharedbreakpoint.svelte.tsmodule that exports a reactive$state. Avoidresizeevent listeners —matchMediais more performant and matches the CSS breakpoint model exactly.(public)with its own+layout.sveltethat renders the full-viewport split. No nav components loaded at all — clean separation.$page.url.pathnamefrom$app/storesin a$derived()to determine active nav item. Keep the nav item config as a simple array of{ href, label, icon }objects — one source of truth for all three nav variants.hrefvalues. Breakpoint switching can be tested by mockingmatchMedia.🏗️ Backend Engineer — Spring Boot / PostgreSQL Specialist
Questions & Observations
GET /api/households/currentor similar on every layout load, we should ensure it's lightweight and cacheable.hooks.server.tsneeds to validate the session cookie against the backend on every request. Is there a session validation endpoint (GET /api/auth/meor similar) already in place? This is a critical dependency for the routing/layout split.Suggestions
GET /api/auth/methat returns{ user: { displayName, role }, household: { name } }— just enough for the shell. Avoid loading heavy data on every navigation. The variety score can be a separate, lazily-loaded call.Cache-Control: private, max-age=60so the browser doesn't re-fetch on every SvelteKit navigation. These values change rarely.🧪 QA Engineer — Test Specialist
Questions & Observations
/planner, the Planner tab has--green-tintbackground and--green-darktext, and all other tabs have default styling." Without that precision, we'll argue about what "correct" means in review./login) successfully authenticates? Does the shell swap layouts without a full page reload? Is there a loading state during the transition? This is a critical path that needs explicit test coverage./recipes/123and open it while logged in, does the correct nav item highlight? What if I'm logged out — do I get redirected to login and then back to/recipes/123after auth?Suggestions
setViewportSizeto test all three breakpoints explicitly. Don't rely on default viewport — be intentional about testing at 767px, 768px, 1024px, and 1025px (boundary values).🔒 Sable — Security Engineer
Questions & Observations
hooks.server.ts, not in individual+layout.server.tsor+page.server.tsfiles. If the auth check is per-layout, a misconfigured route group could accidentally expose an authenticated page without the guard.hooks.server.tsvalidates the session cookie against the Spring Boot backend on every request. What happens if the backend is slow or down? Does the frontend show a loading state, redirect to login, or render a broken shell? A timeout or error in session validation should fail closed (redirect to login), not fail open (render the authenticated shell without data).hooks.server.ts, but it's worth verifying when the shell is implemented.env(safe-area-inset-bottom)from CSS, not a hardcoded 20px — hardcoded values don't adapt to different devices and could overlap content on some phones or leave too much space on others.Suggestions
/login,/register,/invite/*) that requires a valid session. If session validation fails → 302 to/login?redirect={originalUrl}. This is the only place auth is checked for page loads.password_hash,session_token, or internal IDs that could enable enumeration are never included in the layout data response.hooks.server.ts. At minimum:default-src 'self',script-src 'self', nounsafe-inlinefor scripts. Get this right now — it's much harder to add later.🎨 Atlas — UI/UX Designer
Questions & Observations
position: sticky; height: 100vh. With logo section + 2 nav sections + variety score widget, we're fine for v1. But if nav grows (admin section, notifications), we'll need an overflow strategy. For now, "never scrolls" is correct — just noting the constraint.--green-tintbg +--green-darktext across all three breakpoints. No exceptions. Don't let any breakpoint use a different active style (e.g., underline on tablet). This consistency is a core design system rule.Suggestions
bg-[var(--green-tint)]andtext-[var(--green-dark)]— notbg-green-50ortext-green-800. This ensures the active state stays in sync if the palette evolves.display: none/display: flexper breakpoint media query is sufficient — no JS-driven animation needed.--radius-md(6px) for the active background, matching the default system radius. Desktop sidebar items already spec 6px radius. Keep it consistent across all three.👨💻 Kai — Frontend Engineer — Discussion Outcomes
Interactive discussion with Marcel to resolve open implementation questions before starting work.
Resolved Items
Route group structure:
(public)for pre-auth (login, register, invite) +(app)for all authenticated screens. Root+layout.svelteis minimal (html, fonts, global CSS). Auth enforcement lives inhooks.server.ts, not in layout groups. No third group needed — cook mode will be a fullscreen modal inside(app), not a separate route group.3-panel layout ownership: Each page owns its own right detail panel. The app shell provides the 224px sidebar + a
flex-1content area. Pages decide internally whether to render a 280px aside. No snippet-passing or shared panel state needed.Mobile/tablet nav item mismatch:
/membersis its own route, always accessible via URL. On mobile/tablet, it's reachable from the Settings screen (which acts as a household management hub). The tab bar stays at 4 items (Planner, Recipes, Shopping, Settings). Desktop sidebar shows all 5 items across two sections (Plan + Household).Backend API dependency:
(app)/+layout.server.tsowns loading user profile + household name from the real backend API. No mocks or stubs — wire against actual endpoints from day one so missing contracts surface immediately as blockers.Breakpoint switching: CSS-only via Tailwind media query utilities. All three nav components (mobile tab bar, tablet nav, desktop sidebar) render in the DOM; visibility controlled by breakpoint classes. No JS-driven
matchMedianeeded for the shell.Auth guard scope: A basic auth guard in
hooks.server.tsis in scope for this issue — validate session cookie against backend, redirect to/loginif invalid, populateevent.localswith user data. CSRF protection, CSP headers, and security hardening are follow-up issues.Overall Read
The issue is well-scoped. With these decisions locked in, the implementation path is clear: route groups → hooks auth guard → layout server load → shell component with three CSS-switched nav variants. No blockers besides confirming which backend endpoints exist for user/household data.
Implementation Complete
All acceptance criteria implemented on branch
feat/issue-16-design-system.What was built
src/lib/nav/nav.ts) — single source of truth for mobile (4 items) and desktop (5 items, 2 sections) navigationenv(safe-area-inset-bottom)padding, 44×44px touch targets,md:hiddenhidden md:flex lg:hiddenhooks.server.ts) — validates session cookie viaGET /v1/auth/me, populatesevent.locals.benutzer+event.locals.haushalt, redirects to/loginif unauthenticated(app)with AppShell layout + server load,(public)with full-viewport split layout/→/plannerfor authenticated users/planner,/recipes,/shopping,/settings,/members,/login--green-tintbg +--green-darktext +aria-current="page"across all breakpointsCommits
7ae1f3dfeat(nav): add shared navigation config with mobile and desktop itemsd3fa899feat(nav): add MobileTabBar with active state and safe-area padding8f33f46feat(nav): add TabletNavBar with horizontal pills and active state56cfd13feat(nav): add DesktopSidebar with logo, nav sections, and variety widget slotcfe38c3feat(nav): add AppShell layout with breakpoint-switched navigation7a17873feat(auth): add auth guard in hooks.server.ts with session validation9626bdefeat(shell): add route groups, layout server load, redirect, and placeholder pagesTest coverage
70 tests passing, 0 type errors. Tests cover: