Frontend: App shell — responsive layout, navigation, routing #17

Closed
opened 2026-04-02 11:26:14 +02:00 by marcel · 8 comments
Owner

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

Breakpoint Width Navigation
Mobile < 768px Fixed bottom tab bar (sticky, safe-area padding)
Tablet 768px–1024px Inline horizontal tab bar (static, not fixed)
Desktop > 1024px 224px left sidebar (never collapsible)

Mobile Tab Bar

  • Position: fixed, bottom: 0, 20px bottom padding (safe area)
  • 4 items: Planner, Recipes, Shopping, Settings
  • Active state: --green-tint bg + --green-dark text, 10px DM Sans weight 500

Tablet Navigation

  • Position: static (inline at bottom, not fixed)
  • Horizontal labelled pills: Planner, Recipes, Shopping, Settings
  • Active: --green-tint bg + --green-dark text

Desktop Sidebar (224px)

  • Position: sticky, height: 100vh, never scrolls
  • Logo section: 18px 14px 14px padding, 1px border bottom
    • Icon: 22×22px, --green bg, 4px radius
    • Name: Fraunces 15px weight 500
    • Subtext: household name, 10px muted
  • Nav sections: "Plan" (Planner, Recipes, Shopping) + "Household" (Members, Settings)
    • Section label: 8px DM Sans weight 500, uppercase, 0.1em tracking, muted
    • Items: 7px 6px padding, 13px DM Sans, 6px radius
    • Active: --green-tint bg + --green-dark text + weight 500
    • Icon: 16px, 20px column width
  • Variety score widget at bottom (see C1 spec)

Desktop 3-Panel Layout

Many screens use the 3-panel model:

  • Left sidebar: 224px fixed/sticky
  • Main content: flex 1 (only panel that scrolls)
  • Right detail panel: 280px fixed/sticky (screen-dependent)

Pre-auth Layout

Onboarding screens (A1, A2, A4) use a separate full-viewport split layout — no sidebar/tabs.

Acceptance Criteria

  • Mobile: bottom tab bar with correct active states
  • Tablet: inline horizontal pills
  • Desktop: 224px sidebar with logo, nav sections, variety widget slot
  • Active nav state: --green-tint bg + --green-dark text (always)
  • Route-based navigation between Planner, Recipes, Shopping, Settings
  • Pre-auth routes render without navigation chrome
## 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 | Breakpoint | Width | Navigation | |-----------|-------|-----------| | Mobile | < 768px | Fixed bottom tab bar (sticky, safe-area padding) | | Tablet | 768px–1024px | Inline horizontal tab bar (static, not fixed) | | Desktop | > 1024px | 224px left sidebar (never collapsible) | ## Mobile Tab Bar - Position: fixed, bottom: 0, 20px bottom padding (safe area) - 4 items: Planner, Recipes, Shopping, Settings - Active state: `--green-tint` bg + `--green-dark` text, 10px DM Sans weight 500 ## Tablet Navigation - Position: static (inline at bottom, not fixed) - Horizontal labelled pills: Planner, Recipes, Shopping, Settings - Active: `--green-tint` bg + `--green-dark` text ## Desktop Sidebar (224px) - Position: sticky, height: 100vh, never scrolls - **Logo section**: 18px 14px 14px padding, 1px border bottom - Icon: 22×22px, `--green` bg, 4px radius - Name: Fraunces 15px weight 500 - Subtext: household name, 10px muted - **Nav sections**: "Plan" (Planner, Recipes, Shopping) + "Household" (Members, Settings) - Section label: 8px DM Sans weight 500, uppercase, 0.1em tracking, muted - Items: 7px 6px padding, 13px DM Sans, 6px radius - Active: `--green-tint` bg + `--green-dark` text + weight 500 - Icon: 16px, 20px column width - **Variety score widget** at bottom (see C1 spec) ## Desktop 3-Panel Layout Many screens use the 3-panel model: - Left sidebar: 224px fixed/sticky - Main content: flex 1 (only panel that scrolls) - Right detail panel: 280px fixed/sticky (screen-dependent) ## Pre-auth Layout Onboarding screens (A1, A2, A4) use a separate full-viewport split layout — no sidebar/tabs. ## Acceptance Criteria - [ ] Mobile: bottom tab bar with correct active states - [ ] Tablet: inline horizontal pills - [ ] Desktop: 224px sidebar with logo, nav sections, variety widget slot - [ ] Active nav state: `--green-tint` bg + `--green-dark` text (always) - [ ] Route-based navigation between Planner, Recipes, Shopping, Settings - [ ] Pre-auth routes render without navigation chrome
marcel added the kind/uipriority/high labels 2026-04-02 11:29:47 +02:00
Author
Owner

Spec references: Navigation and layout specs are embedded in each journey file. Key references:

**Spec references:** Navigation and layout specs are embedded in each journey file. Key references: - Sidebar + desktop layout: [`specs/frontend/j2-plan-the-week.html`](../specs/frontend/j2-plan-the-week.html) (C1 planner has the most complete sidebar spec) - Mobile tab bar: all journey files include mobile previews with tab bar - Pre-auth layout: [`specs/frontend/j6-household-setup.html`](../specs/frontend/j6-household-setup.html) (A1, A2, A4)
Author
Owner

👨‍💻 Kai — Frontend Engineer

Questions & Observations

  • Layout group vs. route group: SvelteKit supports both (group) layouts. Are we doing (auth) for the app shell and (public) for pre-auth? Or a different split? This affects where +layout.svelte and +layout.server.ts live and how the auth guard in hooks.server.ts maps to route groups.
  • Variety score widget "slot": The acceptance criteria mention a "variety widget slot" in the sidebar, but the variety score comes from backend data tied to a specific week/household. Where does that data load — in the root +layout.server.ts of 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?
  • Tablet "inline at bottom, not fixed": This is unusual — an inline nav bar at the bottom of the content means it scrolls away. Is that intentional? On long pages (recipe list, shopping list), the user would need to scroll to the bottom to navigate. Worth confirming with Atlas whether this is the desired UX or if "inline" means "in the header area" instead.
  • 3-panel layout ownership: The spec mentions a 280px right detail panel that is "screen-dependent." Does the app shell own this panel (with a named snippet slot for pages to fill), or does each page independently render its own right panel? This has big implications for the layout component structure.
  • Mobile tab bar has 4 items, but desktop sidebar has 6: Desktop adds Members and splits nav into "Plan" + "Household" sections. On mobile/tablet, Members and the full Settings are presumably behind the Settings tab? The routing needs to handle this — same routes, different nav visibility.

Suggestions

  • Component split: I'd propose AppShell.svelte as the top-level layout component, with MobileTabBar.svelte, TabletNavBar.svelte, and DesktopSidebar.svelte as children. Use a $derived() breakpoint rune (from a matchMedia listener in $effect()) to switch between them. Keep each nav component focused and testable.
  • Breakpoint detection: Use window.matchMedia in an $effect() inside a shared breakpoint.svelte.ts module that exports a reactive $state. Avoid resize event listeners — matchMedia is more performant and matches the CSS breakpoint model exactly.
  • Pre-auth layout: Use a SvelteKit route group (public) with its own +layout.svelte that renders the full-viewport split. No nav components loaded at all — clean separation.
  • Active route detection: Use $page.url.pathname from $app/stores in 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.
  • Test strategy: Each nav component should have a Vitest + testing-library test covering: correct items rendered, active state applied to current route, and navigation links have correct href values. Breakpoint switching can be tested by mocking matchMedia.
## 👨‍💻 Kai — Frontend Engineer ### Questions & Observations - **Layout group vs. route group**: SvelteKit supports both `(group)` layouts. Are we doing `(auth)` for the app shell and `(public)` for pre-auth? Or a different split? This affects where `+layout.svelte` and `+layout.server.ts` live and how the auth guard in `hooks.server.ts` maps to route groups. - **Variety score widget "slot"**: The acceptance criteria mention a "variety widget slot" in the sidebar, but the variety score comes from backend data tied to a specific week/household. Where does that data load — in the root `+layout.server.ts` of 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? - **Tablet "inline at bottom, not fixed"**: This is unusual — an inline nav bar at the bottom of the content means it scrolls away. Is that intentional? On long pages (recipe list, shopping list), the user would need to scroll to the bottom to navigate. Worth confirming with Atlas whether this is the desired UX or if "inline" means "in the header area" instead. - **3-panel layout ownership**: The spec mentions a 280px right detail panel that is "screen-dependent." Does the app shell own this panel (with a named snippet slot for pages to fill), or does each page independently render its own right panel? This has big implications for the layout component structure. - **Mobile tab bar has 4 items, but desktop sidebar has 6**: Desktop adds Members and splits nav into "Plan" + "Household" sections. On mobile/tablet, Members and the full Settings are presumably behind the Settings tab? The routing needs to handle this — same routes, different nav visibility. ### Suggestions - **Component split**: I'd propose `AppShell.svelte` as the top-level layout component, with `MobileTabBar.svelte`, `TabletNavBar.svelte`, and `DesktopSidebar.svelte` as children. Use a `$derived()` breakpoint rune (from a `matchMedia` listener in `$effect()`) to switch between them. Keep each nav component focused and testable. - **Breakpoint detection**: Use `window.matchMedia` in an `$effect()` inside a shared `breakpoint.svelte.ts` module that exports a reactive `$state`. Avoid `resize` event listeners — `matchMedia` is more performant and matches the CSS breakpoint model exactly. - **Pre-auth layout**: Use a SvelteKit route group `(public)` with its own `+layout.svelte` that renders the full-viewport split. No nav components loaded at all — clean separation. - **Active route detection**: Use `$page.url.pathname` from `$app/stores` in 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. - **Test strategy**: Each nav component should have a Vitest + testing-library test covering: correct items rendered, active state applied to current route, and navigation links have correct `href` values. Breakpoint switching can be tested by mocking `matchMedia`.
Author
Owner

🏗️ Backend Engineer — Spring Boot / PostgreSQL Specialist

Questions & Observations

  • API contract for sidebar data: The sidebar shows the household name and a variety score widget. That means the frontend layout needs an API endpoint (or endpoints) that returns the current user's household info and variety score on every authenticated page load. Is there an existing endpoint for this, or does it need to be created as part of this issue? If the frontend calls GET /api/households/current or similar on every layout load, we should ensure it's lightweight and cacheable.
  • Session-based auth and SvelteKit SSR: The app uses Spring Security session cookies. For the app shell to decide "auth layout vs. pre-auth layout," the SvelteKit hooks.server.ts needs to validate the session cookie against the backend on every request. Is there a session validation endpoint (GET /api/auth/me or similar) already in place? This is a critical dependency for the routing/layout split.
  • OpenAPI spec coverage: The app shell will need to consume at least: user profile (name, role), household info (name), and variety score. These should be documented in the OpenAPI spec so the frontend can code against a contract. If they're not there yet, this issue has a backend dependency.

Suggestions

  • Lightweight "me" endpoint: If it doesn't exist, consider a single GET /api/auth/me that 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 headers: For the household name and user profile, consider Cache-Control: private, max-age=60 so the browser doesn't re-fetch on every SvelteKit navigation. These values change rarely.
  • No backend changes should be merged without this issue clarifying the API surface: If this issue is purely frontend, explicitly note which existing endpoints it depends on. If new endpoints are needed, spin them off as a separate backend issue so both tracks can proceed in parallel.
## 🏗️ Backend Engineer — Spring Boot / PostgreSQL Specialist ### Questions & Observations - **API contract for sidebar data**: The sidebar shows the household name and a variety score widget. That means the frontend layout needs an API endpoint (or endpoints) that returns the current user's household info and variety score on every authenticated page load. Is there an existing endpoint for this, or does it need to be created as part of this issue? If the frontend calls `GET /api/households/current` or similar on every layout load, we should ensure it's lightweight and cacheable. - **Session-based auth and SvelteKit SSR**: The app uses Spring Security session cookies. For the app shell to decide "auth layout vs. pre-auth layout," the SvelteKit `hooks.server.ts` needs to validate the session cookie against the backend on every request. Is there a session validation endpoint (`GET /api/auth/me` or similar) already in place? This is a critical dependency for the routing/layout split. - **OpenAPI spec coverage**: The app shell will need to consume at least: user profile (name, role), household info (name), and variety score. These should be documented in the OpenAPI spec so the frontend can code against a contract. If they're not there yet, this issue has a backend dependency. ### Suggestions - **Lightweight "me" endpoint**: If it doesn't exist, consider a single `GET /api/auth/me` that 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 headers**: For the household name and user profile, consider `Cache-Control: private, max-age=60` so the browser doesn't re-fetch on every SvelteKit navigation. These values change rarely. - **No backend changes should be merged without this issue clarifying the API surface**: If this issue is purely frontend, explicitly note which existing endpoints it depends on. If new endpoints are needed, spin them off as a separate backend issue so both tracks can proceed in parallel.
Author
Owner

🧪 QA Engineer — Test Specialist

Questions & Observations

  • Acceptance criteria are visual but not behavioral: The criteria say "Mobile: bottom tab bar with correct active states" — but what does "correct" mean in test terms? I'd like to see testable assertions: "When the user is on /planner, the Planner tab has --green-tint background and --green-dark text, and all other tabs have default styling." Without that precision, we'll argue about what "correct" means in review.
  • No mention of keyboard navigation or focus management: When the user navigates between routes, where does focus land? On mobile, if the tab bar is fixed at the bottom, does focus stay on the tab or move to the new page content? This is testable and often missed.
  • Pre-auth → auth transition: What happens when a user on a pre-auth route (e.g., /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.
  • Deep linking: If I bookmark /recipes/123 and 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/123 after auth?

Suggestions

  • Test matrix I'd want to see before this ships:
Scenario Mobile Tablet Desktop
Correct nav items rendered
Active state on current route
Navigation changes route + content
Pre-auth routes show no nav
Deep link highlights correct nav
Resize from mobile → desktop swaps layout
Keyboard navigation through nav items
  • E2E coverage: This is the app shell — it affects every single page. I'd recommend at least one Playwright test per breakpoint that verifies: nav renders, clicking a nav item changes the route, and the active state updates. These become the baseline smoke tests for every future feature.
  • Viewport testing: Use Playwright's setViewportSize to test all three breakpoints explicitly. Don't rely on default viewport — be intentional about testing at 767px, 768px, 1024px, and 1025px (boundary values).
## 🧪 QA Engineer — Test Specialist ### Questions & Observations - **Acceptance criteria are visual but not behavioral**: The criteria say "Mobile: bottom tab bar with correct active states" — but what does "correct" mean in test terms? I'd like to see testable assertions: "When the user is on `/planner`, the Planner tab has `--green-tint` background and `--green-dark` text, and all other tabs have default styling." Without that precision, we'll argue about what "correct" means in review. - **No mention of keyboard navigation or focus management**: When the user navigates between routes, where does focus land? On mobile, if the tab bar is fixed at the bottom, does focus stay on the tab or move to the new page content? This is testable and often missed. - **Pre-auth → auth transition**: What happens when a user on a pre-auth route (e.g., `/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. - **Deep linking**: If I bookmark `/recipes/123` and 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/123` after auth? ### Suggestions - **Test matrix I'd want to see before this ships**: | Scenario | Mobile | Tablet | Desktop | |---|---|---|---| | Correct nav items rendered | ✅ | ✅ | ✅ | | Active state on current route | ✅ | ✅ | ✅ | | Navigation changes route + content | ✅ | ✅ | ✅ | | Pre-auth routes show no nav | ✅ | ✅ | ✅ | | Deep link highlights correct nav | ✅ | ✅ | ✅ | | Resize from mobile → desktop swaps layout | — | — | ✅ | | Keyboard navigation through nav items | ✅ | ✅ | ✅ | - **E2E coverage**: This is the app shell — it affects every single page. I'd recommend at least one Playwright test per breakpoint that verifies: nav renders, clicking a nav item changes the route, and the active state updates. These become the baseline smoke tests for every future feature. - **Viewport testing**: Use Playwright's `setViewportSize` to test all three breakpoints explicitly. Don't rely on default viewport — be intentional about testing at 767px, 768px, 1024px, and 1025px (boundary values).
Author
Owner

🔒 Sable — Security Engineer

Questions & Observations

  • Auth guard placement: The issue mentions "pre-auth routes render without navigation chrome" — which implies there's a routing split between authenticated and unauthenticated paths. Where is this enforced? It must be in hooks.server.ts, not in individual +layout.server.ts or +page.server.ts files. If the auth check is per-layout, a misconfigured route group could accidentally expose an authenticated page without the guard.
  • Session validation on every SSR request: The app shell loads on every page. That means hooks.server.ts validates 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).
  • CSRF on navigation: SvelteKit form actions need CSRF protection. If the nav includes any form-based actions (e.g., logout button), ensure the CSRF token is present and validated. This should already be handled in hooks.server.ts, but it's worth verifying when the shell is implemented.
  • Household data in the sidebar: The sidebar displays the household name. This comes from the authenticated user's session/context. Ensure the API endpoint serving this data scopes it to the current user's household — no IDOR risk where changing a parameter could reveal another household's name.
  • Safe-area padding on mobile: The 20px bottom padding for the mobile tab bar is for safe area (notch/home bar). Make sure this uses 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

  • Centralized auth in hooks.server.ts: Define a route matcher (e.g., all routes except /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.
  • No sensitive data in client-side state: The household name is fine to render client-side, but ensure password_hash, session_token, or internal IDs that could enable enumeration are never included in the layout data response.
  • Content Security Policy: Since this is the app shell that wraps everything, this is the right time to set CSP headers in hooks.server.ts. At minimum: default-src 'self', script-src 'self', no unsafe-inline for scripts. Get this right now — it's much harder to add later.
## 🔒 Sable — Security Engineer ### Questions & Observations - **Auth guard placement**: The issue mentions "pre-auth routes render without navigation chrome" — which implies there's a routing split between authenticated and unauthenticated paths. Where is this enforced? It **must** be in `hooks.server.ts`, not in individual `+layout.server.ts` or `+page.server.ts` files. If the auth check is per-layout, a misconfigured route group could accidentally expose an authenticated page without the guard. - **Session validation on every SSR request**: The app shell loads on every page. That means `hooks.server.ts` validates 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). - **CSRF on navigation**: SvelteKit form actions need CSRF protection. If the nav includes any form-based actions (e.g., logout button), ensure the CSRF token is present and validated. This should already be handled in `hooks.server.ts`, but it's worth verifying when the shell is implemented. - **Household data in the sidebar**: The sidebar displays the household name. This comes from the authenticated user's session/context. Ensure the API endpoint serving this data scopes it to the current user's household — no IDOR risk where changing a parameter could reveal another household's name. - **Safe-area padding on mobile**: The 20px bottom padding for the mobile tab bar is for safe area (notch/home bar). Make sure this uses `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 - **Centralized auth in hooks.server.ts**: Define a route matcher (e.g., all routes except `/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. - **No sensitive data in client-side state**: The household name is fine to render client-side, but ensure `password_hash`, `session_token`, or internal IDs that could enable enumeration are never included in the layout data response. - **Content Security Policy**: Since this is the app shell that wraps everything, this is the right time to set CSP headers in `hooks.server.ts`. At minimum: `default-src 'self'`, `script-src 'self'`, no `unsafe-inline` for scripts. Get this right now — it's much harder to add later.
Author
Owner

🎨 Atlas — UI/UX Designer

Questions & Observations

  • Tablet nav "inline at bottom, not fixed": I want to flag this for discussion. An inline nav at the bottom of the page content means users must scroll past all content to reach navigation. On screens with long lists (recipes, shopping), this could be frustrating. My intent was "not sticky-fixed like mobile" — but the placement should still be at the top of the viewport as a horizontal bar, not literally at the bottom of the scrollable content. Let's clarify: tablet nav = top of page, inline (scrolls with content), horizontal pills. If the spec files are ambiguous on this, I'll update them.
  • Desktop sidebar "never scrolls" vs. many nav items: The sidebar is 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.
  • Variety score widget "slot": The sidebar spec references "see C1 spec" for the variety widget. For the app shell issue, I'd suggest implementing just the container/slot — a fixed-height area at the bottom of the sidebar — and filling it with actual data in the C1 (planner) issue. Don't block the shell on variety score implementation.
  • Mobile tab bar icon + label sizing: The spec says 10px DM Sans weight 500 for labels. That's very small. Ensure the touch target for each tab is at least 44×44px (WCAG 2.2 AA) even though the visible label is small. The tap area should extend beyond the visible text.
  • Active state consistency: The spec is intentionally rigid here — --green-tint bg + --green-dark text 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

  • Design token usage: The implementation should use CSS custom properties from the design system, not raw Tailwind colors. For example, bg-[var(--green-tint)] and text-[var(--green-dark)] — not bg-green-50 or text-green-800. This ensures the active state stays in sync if the palette evolves.
  • Transition between breakpoints: When resizing across breakpoints (dev tools, tablet rotation), the nav switch should feel clean. No layout jump or flash of the wrong nav. A simple CSS display: none / display: flex per breakpoint media query is sufficient — no JS-driven animation needed.
  • Fraunces font loading: The sidebar uses Fraunces for the app name. Ensure font-display: swap is set so the sidebar doesn't render invisible text while the font loads. DM Sans is the body font and should load first; Fraunces can load second without blocking.
  • Border-radius on active pills: Both mobile tabs and tablet pills should use --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.
## 🎨 Atlas — UI/UX Designer ### Questions & Observations - **Tablet nav "inline at bottom, not fixed"**: I want to flag this for discussion. An inline nav at the bottom of the page content means users must scroll past all content to reach navigation. On screens with long lists (recipes, shopping), this could be frustrating. My intent was "not sticky-fixed like mobile" — but the placement should still be **at the top of the viewport** as a horizontal bar, not literally at the bottom of the scrollable content. Let's clarify: **tablet nav = top of page, inline (scrolls with content), horizontal pills**. If the spec files are ambiguous on this, I'll update them. - **Desktop sidebar "never scrolls" vs. many nav items**: The sidebar is `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. - **Variety score widget "slot"**: The sidebar spec references "see C1 spec" for the variety widget. For the app shell issue, I'd suggest implementing just the **container/slot** — a fixed-height area at the bottom of the sidebar — and filling it with actual data in the C1 (planner) issue. Don't block the shell on variety score implementation. - **Mobile tab bar icon + label sizing**: The spec says 10px DM Sans weight 500 for labels. That's very small. Ensure the touch target for each tab is at least 44×44px (WCAG 2.2 AA) even though the visible label is small. The tap area should extend beyond the visible text. - **Active state consistency**: The spec is intentionally rigid here — `--green-tint` bg + `--green-dark` text 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 - **Design token usage**: The implementation should use CSS custom properties from the design system, not raw Tailwind colors. For example, `bg-[var(--green-tint)]` and `text-[var(--green-dark)]` — not `bg-green-50` or `text-green-800`. This ensures the active state stays in sync if the palette evolves. - **Transition between breakpoints**: When resizing across breakpoints (dev tools, tablet rotation), the nav switch should feel clean. No layout jump or flash of the wrong nav. A simple CSS `display: none` / `display: flex` per breakpoint media query is sufficient — no JS-driven animation needed. - **Fraunces font loading**: The sidebar uses Fraunces for the app name. Ensure font-display: swap is set so the sidebar doesn't render invisible text while the font loads. DM Sans is the body font and should load first; Fraunces can load second without blocking. - **Border-radius on active pills**: Both mobile tabs and tablet pills should use `--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.
Author
Owner

👨‍💻 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.svelte is minimal (html, fonts, global CSS). Auth enforcement lives in hooks.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-1 content area. Pages decide internally whether to render a 280px aside. No snippet-passing or shared panel state needed.

  • Mobile/tablet nav item mismatch: /members is 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.ts owns 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 matchMedia needed for the shell.

  • Auth guard scope: A basic auth guard in hooks.server.ts is in scope for this issue — validate session cookie against backend, redirect to /login if invalid, populate event.locals with 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.

## 👨‍💻 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.svelte` is minimal (html, fonts, global CSS). Auth enforcement lives in `hooks.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-1` content area. Pages decide internally whether to render a 280px aside. No snippet-passing or shared panel state needed. - **Mobile/tablet nav item mismatch**: `/members` is 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.ts` owns 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 `matchMedia` needed for the shell. - **Auth guard scope**: A basic auth guard in `hooks.server.ts` is in scope for this issue — validate session cookie against backend, redirect to `/login` if invalid, populate `event.locals` with 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.
Author
Owner

Implementation Complete

All acceptance criteria implemented on branch feat/issue-16-design-system.

What was built

  • Shared nav config (src/lib/nav/nav.ts) — single source of truth for mobile (4 items) and desktop (5 items, 2 sections) navigation
  • MobileTabBar — fixed bottom bar, 4 items, env(safe-area-inset-bottom) padding, 44×44px touch targets, md:hidden
  • TabletNavBar — horizontal pills at top, 4 items, hidden md:flex lg:hidden
  • DesktopSidebar — 224px sticky sidebar with logo section (Fraunces font, household name), Plan + Haushalt nav sections, variety widget slot
  • AppShell — layout component composing all three nav variants with CSS-only breakpoint switching
  • Auth guard (hooks.server.ts) — validates session cookie via GET /v1/auth/me, populates event.locals.benutzer + event.locals.haushalt, redirects to /login if unauthenticated
  • Route groups(app) with AppShell layout + server load, (public) with full-viewport split layout
  • Root redirect//planner for authenticated users
  • Placeholder pages/planner, /recipes, /shopping, /settings, /members, /login
  • Active state--green-tint bg + --green-dark text + aria-current="page" across all breakpoints

Commits

  • 7ae1f3d feat(nav): add shared navigation config with mobile and desktop items
  • d3fa899 feat(nav): add MobileTabBar with active state and safe-area padding
  • 8f33f46 feat(nav): add TabletNavBar with horizontal pills and active state
  • 56cfd13 feat(nav): add DesktopSidebar with logo, nav sections, and variety widget slot
  • cfe38c3 feat(nav): add AppShell layout with breakpoint-switched navigation
  • 7a17873 feat(auth): add auth guard in hooks.server.ts with session validation
  • 9626bde feat(shell): add route groups, layout server load, redirect, and placeholder pages

Test coverage

70 tests passing, 0 type errors. Tests cover:

  • Nav config structure (6 tests)
  • MobileTabBar rendering + active state (5 tests)
  • TabletNavBar rendering + active state (4 tests)
  • DesktopSidebar logo, sections, items, active state, widget slot (8 tests)
  • AppShell composition of all nav variants (4 tests)
  • Auth guard: public routes, redirects, session validation, locals population (4 tests)
## Implementation Complete All acceptance criteria implemented on branch `feat/issue-16-design-system`. ### What was built - **Shared nav config** (`src/lib/nav/nav.ts`) — single source of truth for mobile (4 items) and desktop (5 items, 2 sections) navigation - **MobileTabBar** — fixed bottom bar, 4 items, `env(safe-area-inset-bottom)` padding, 44×44px touch targets, `md:hidden` - **TabletNavBar** — horizontal pills at top, 4 items, `hidden md:flex lg:hidden` - **DesktopSidebar** — 224px sticky sidebar with logo section (Fraunces font, household name), Plan + Haushalt nav sections, variety widget slot - **AppShell** — layout component composing all three nav variants with CSS-only breakpoint switching - **Auth guard** (`hooks.server.ts`) — validates session cookie via `GET /v1/auth/me`, populates `event.locals.benutzer` + `event.locals.haushalt`, redirects to `/login` if unauthenticated - **Route groups** — `(app)` with AppShell layout + server load, `(public)` with full-viewport split layout - **Root redirect** — `/` → `/planner` for authenticated users - **Placeholder pages** — `/planner`, `/recipes`, `/shopping`, `/settings`, `/members`, `/login` - **Active state** — `--green-tint` bg + `--green-dark` text + `aria-current="page"` across all breakpoints ### Commits - `7ae1f3d` feat(nav): add shared navigation config with mobile and desktop items - `d3fa899` feat(nav): add MobileTabBar with active state and safe-area padding - `8f33f46` feat(nav): add TabletNavBar with horizontal pills and active state - `56cfd13` feat(nav): add DesktopSidebar with logo, nav sections, and variety widget slot - `cfe38c3` feat(nav): add AppShell layout with breakpoint-switched navigation - `7a17873` feat(auth): add auth guard in hooks.server.ts with session validation - `9626bde` feat(shell): add route groups, layout server load, redirect, and placeholder pages ### Test coverage 70 tests passing, 0 type errors. Tests cover: - Nav config structure (6 tests) - MobileTabBar rendering + active state (5 tests) - TabletNavBar rendering + active state (4 tests) - DesktopSidebar logo, sections, items, active state, widget slot (8 tests) - AppShell composition of all nav variants (4 tests) - Auth guard: public routes, redirects, session validation, locals population (4 tests)
Sign in to join this conversation.