Frontend: App shell, navigation, routing, and design tokens #32
Reference in New Issue
Block a user
Delete Branch "feat/issue-16-design-system"
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
@themetokens (colors, spacing, radii, shadows, fonts) with completeness and WCAG AA contrast testshooks.server.tsvalidating session viaGET /v1/auth/me, redirecting to/login(app)with AppShell + server data load,(public)with full-viewport split layout/redirects to/planner; placeholder pages for all routesTest plan
npm run check— 0 errors, 0 warnings/loginrenders without nav chrome/loginCloses #16, closes #17
🤖 Generated with Claude Code
👨💻 Kai — Frontend Engineer
Verdict: ⚠️ Approved with concerns
Blockers
None.
Concerns
resolve: { conditions: ['browser'] }at the top level ofvite.config.ts— This applies thebrowsercondition globally, not just to tests. This could affect SSR builds by forcing browser-only exports for all dependencies duringvite build. Safer to scope it undertest.resolve.conditionsor usetest.aliasto remap onlysvelteto its client entry. Worth verifying withnpm run buildthat SSR still works.$pagemock usesrequire()syntax — All four component test files mock$app/storeswithconst { readable } = require('svelte/store'). This is CJS in an ESM project. It works because vitest hoistsvi.mock()calls, but it's fragile. Prefer dynamicimport():Active route matching with
startsWithhas a false-positive risk —$page.url.pathname.startsWith('/settings')would match/settings-advancedor/settings2if those routes ever existed. Currently safe because routes are well-defined, but a prefix match on/would activate everything. Worth noting for future routes — consider exact-match or segment-boundary matching (pathname === href || pathname.startsWith(href + '/')).No icons rendered — The
NavItemtype has anicon: stringfield and it's populated in the config, but none of the three nav components actually render any icon. The spec calls for 16px icons in the sidebar and icons in the mobile tab bar. This should be a fast follow — the slot is there, the data is there, just no visual output yet.Suggestions
$app/storesmock — Four test files have the identical mock block. Extract into a shared test helper likesrc/lib/__tests__/mocks.tsand import it. Less copy-paste, easier to change when you need a different route.(public)/+layout.sveltemissinglang="ts"— The script tag uses$props()which is Svelte 5, butlang="ts"is omitted. Won't break anything, but inconsistent with all other components.font-display: swap— The design system loads Fraunces and DM Sans, butapp.cssdoesn't have@font-facerules withfont-display: swap. If fonts are loaded via Google Fonts link inapp.html, this is fine, but worth checking theapp.htmlfile (not in this diff) to confirm.Overall: clean, well-structured, good component decomposition. The TDD flow is visible in the commit history. Ship it with the
resolve.conditionsconcern addressed.🏗️ Backend Engineer
Verdict: ✅ Approved
Observations
This PR is frontend-only. From a backend perspective, I checked:
API contract usage —
hooks.server.tscallsGET /v1/auth/mevia the typedapiClient. This endpoint exists in the OpenAPI spec and returnsApiResponseUserResponsewithUserResponsecontainingid,displayName,householdId,householdName,householdRole. The field access in hooks matches the spec correctly:user.id,user.displayName,user.householdId, etc.Session cookie name —
event.cookies.get('session')assumes the Spring Boot session cookie is namedsession. Spring Security defaults toJSESSIONID. This needs to match the backend'sserver.servlet.session.cookie.nameconfiguration. If the backend hasn't been configured to usesession, this will silently fail (no cookie found → redirect to login forever). Verify the cookie name matches the backend config.Non-null assertions —
locals.benutzer!andlocals.haushalt!in+layout.server.tsare safe here because the hooks guard ensures these are populated before any(app)route loads. The layering is correct.No backend changes needed — All endpoints the frontend depends on (
/v1/auth/me,/v1/households/mine) already exist. No new backend work required.Suggestion
householdRolefield is cast as'planer' | 'mitglied'directly from the API string. If the backend adds a third role, this cast will silently pass. Consider a runtime validation or at least a type guard. Low priority for now.🧪 QA Engineer
Verdict: ⚠️ Approved with concerns
What's Covered (Good)
That's 31 new tests for the app shell. A good start.
What's Missing
No test for active state on non-
/plannerroutes — Every component test mocks$page.url.pathnameas/planner. We never verify that navigating to/recipesactivates the Recipes tab and deactivates Planner. This is a one-dimensional test of the active state logic. At minimum, add a test with a different route to prove the logic isn't hardcoded.No test for the
isPublicRouteboundary — The auth guard test checks/login(exact match) but not/login/(trailing slash),/login?redirect=/planner(with query params), or/invite/abc123(sub-path). TheisPublicRoutefunction usesstartsWith(route + '/')which handles sub-paths, but/login?redirect=...would fail because?≠/. This could cause a redirect loop on the login page if query params are present. This is a real bug path.No test for the root
/redirect —+page.server.tsdoesredirect(302, '/planner')but there's no test asserting this behavior. It's simple, but it's also the first thing a user hits.No test for
(app)/+layout.server.ts— The load function readslocals.benutzer!andlocals.haushalt!. What iflocals.benutzeris undefined (e.g., a race condition or future refactor removes the hook guard)? The!assertion would throw a runtime error with no useful message. A test would document this assumption.Auth guard doesn't test static assets — Requests to
/favicon.ico,/_app/immutable/..., or other static files also hithooks.server.ts. If the backend is down and a user loads a cached page, every CSS/JS request would redirect to/login. Consider adding static paths toPUBLIC_ROUTESor checking for file extensions.Suggestions
it.eachto test multiple routes per component. One test per route is overkill, butit.each(['/planner', '/recipes', '/shopping'])proves the logic generalizes.Coverage Assessment
Estimated coverage for new code: ~75%. The happy paths are well covered, but the edge cases (different active routes, public route boundary, static assets) have gaps. Above the 80% floor for most files, but
hooks.server.tsis likely below it due to the untestedisPublicRouteedge cases.🔒 Sable — Security Engineer
Verdict: ⚠️ Approved with concerns
Concerns
Auth guard bypasses static assets and API routes —
hooks.server.tsonly exempts/login,/register,/invite. Every other request — including/_app/immutable/...(SvelteKit static assets),/favicon.ico, and any future/api/...routes — hits the auth guard. If the backend is unreachable, even loading CSS/JS for the login page would redirect to/login, creating an infinite loop. Add SvelteKit's internal paths to the bypass or checkevent.isSubRequest/ file extensions.isPublicRoutedoesn't account for query strings —event.url.pathnameis clean (no query string), so this is actually fine. Verified:new URL('http://x/login?foo=bar').pathname→/login. No issue here. ✅No
redirectparameter preservation — When redirecting to/login, the original URL is lost. The user navigates to/recipes/abc, gets sent to/login, logs in, and lands on/planner(the default redirect). Best practice:redirect(302, '/login?redirect=' + encodeURIComponent(event.url.pathname)). Not a security issue, but a UX gap that affects the auth flow.Non-null assertions with
!on API response fields —hooks.server.ts:30-37usesuser.id!,user.displayName!,user.householdId!, etc. If the API returns a user without a household (e.g., newly registered user who hasn't joined/created one yet),householdIdandhouseholdNamecould benull. This would setlocals.haushalt = { id: null, name: null }without any error, and the sidebar would rendernullas text. Add a check for users without households — redirect them to an onboarding flow or show a household setup page.No CSRF protection — The issue discussion (comment #1233) explicitly scoped CSRF and CSP as follow-up issues. Acknowledged. But I want to flag: the login form (when implemented) must have CSRF protection from day one. Don't ship the login form without it.
Session cookie not forwarded in
apiClientcall — Inhooks.server.ts:21,apiClient(event.fetch)is used. SvelteKit'sevent.fetchautomatically forwards cookies, so the session cookie reaches the backend. ✅ Correct usage.Verified (No Issues)
{@html}usage anywhere — no XSS vectors. ✅+layout.server.tspassesbenutzer(id, name, rolle) andhaushalt(id, name). No password hashes, no session tokens. ✅(public)has no+layout.server.ts, so no auth data leaks to public pages. ✅Recommendation
The static asset bypass (concern #1) is the most impactful issue. In production, if the backend has a brief outage, the login page itself would break because its CSS and JS would be redirected to
/login. Fix before merge or immediately after.🎨 Atlas — UI/UX Designer
Verdict: ⚠️ Approved with concerns
Design System Compliance
The
app.css@themetokens are correct and complete. Color scales, spacing, radii, shadows, fonts — all match the spec. WCAG contrast tests are a nice touch. ✅Concerns
No icons in any nav component — The spec explicitly calls for icons in all three nav variants:
The
NavItemtype has anicon: stringfield, the config has icon names (calendar,book,shopping-cart, etc.), but no component renders them. The nav will look like a list of plain text links. This is a significant visual gap. I understand this may be a deliberate placeholder (icons need an icon system like Lucide or SVG sprites), but it should be tracked as a fast follow.Mobile tab bar missing icon-above-label layout — The spec shows icons stacked above labels in the mobile tab bar. Current implementation has
flex-col items-center gap-1which is correct structurally, but without the icon, it's just a label. Once icons are added, verify the icon isabovethe label (flex-col handles this).Desktop sidebar household name placement — The spec says the household name is below the icon+name row as a sub-line. The current implementation renders it as a separate
<p>element below the<div class="flex items-center gap-2">that contains the icon and app name. This is correct — thehouseholdNamesits below. ✅Tablet nav at the top, not bottom — Confirmed from the discussion: tablet nav should be at the top of the viewport, inline (not fixed). The
TabletNavBaris rendered before<main>in the flex column, which places it at the top. ✅ Correct placement.Section label font size is 8px —
DesktopSidebar.svelte:22usestext-[8px]for section titles. This matches the spec, but 8px is extremely small. It's above the minimum for decorative/label text under WCAG (which doesn't set a minimum font size, but recommends ≥12px for readability). Monitor user feedback — we may need to bump this to 9-10px.Missing hover states — None of the nav components define hover styles. The spec doesn't explicitly call them out, but interactive elements without hover feedback feel broken on desktop. Recommend adding
hover:bg-[var(--color-subtle)]on inactive nav items for desktop and tablet.Verified (Correct)
--green-tintbg +--green-darktext across all three breakpoints ✅--radius-md(6px) ✅min-w-[224px]✅--font-displayfor app name,--font-sansfor nav items ✅md:hidden,hidden md:flex lg:hidden,hidden lg:flex✅Summary
The structural implementation is correct. The main gap is icons — without them, the navigation doesn't match the visual spec. Track as a follow-up issue and ship the shell.
Review Concerns Addressed
All 12 reviewer concerns have been resolved across 12 commits:
resolve.conditionsglobal scopedb4b01crequire()in test mocksawait import()05bf66d(public)/+layout.sveltemissinglang="ts"d7f3175/_app/and/faviconbypass2bdb101isPublicRouteboundary testscc74c00?redirect=to login URL92c7d8fsessionvsJSESSIONIDJSESSIONID3255037'Kein Haushalt', nullablehaushalt.idaeaca76startsWithfalse-positive riskisActiveRoute()with segment-boundary checkbd8e901/plannerroutes4bd020f5c066d3hover:bg-[var(--color-subtle)]on tablet + desktop nav682580eTest suite: 86 tests passing (was 70), 0 type errors, 0 warnings.