---
phase: 02-ssr-shell
plan: 02
type: execute
wave: 2
depends_on: [02-01]
files_modified:
  - app/components/layout/AppHeader.vue
  - app/components/layout/AppFooter.vue
  - app/layouts/default.vue
  - app/app.vue
autonomous: true
requirements: [COMP-05, COMP-06, I18N-03, THEME-01]

must_haves:
  truths:
    - "Header is sticky with logo left, nav center-right, toggles far right"
    - "Language toggle switches FR/EN and persists via cookie"
    - "Theme toggle switches dark/light and persists via cookie"
    - "Mobile hamburger opens UDrawer with nav links and toggles"
    - "Footer shows copyright and social icon links"
    - "Default layout wraps all pages with header + slot + footer"
  artifacts:
    - path: "app/components/layout/AppHeader.vue"
      provides: "Sticky header with nav, lang toggle, theme toggle, mobile drawer"
      min_lines: 80
    - path: "app/components/layout/AppFooter.vue"
      provides: "Minimal footer with copyright and social icons"
      min_lines: 20
    - path: "app/layouts/default.vue"
      provides: "Default Nuxt layout: header + slot + footer"
      contains: "AppHeader"
  key_links:
    - from: "app/components/layout/AppHeader.vue"
      to: "@nuxtjs/i18n"
      via: "setLocale() for language switching"
      pattern: "setLocale"
    - from: "app/components/layout/AppHeader.vue"
      to: "@nuxtjs/color-mode"
      via: "useColorMode() for theme toggle"
      pattern: "useColorMode"
    - from: "app/layouts/default.vue"
      to: "app/components/layout/AppHeader.vue"
      via: "component import"
      pattern: "AppHeader"
---

<objective>
Build the header (with desktop nav, mobile drawer, language/theme toggles), footer, and default layout.

Purpose: Provide the visible SSR shell that wraps all pages — navigation, toggles, and footer are functional.
Output: AppHeader.vue, AppFooter.vue, default.vue layout, updated app.vue.
</objective>

<execution_context>
@~/.claude/get-shit-done/workflows/execute-plan.md
@~/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/phases/02-ssr-shell/02-CONTEXT.md
@.planning/phases/02-ssr-shell/02-RESEARCH.md
@.planning/phases/02-ssr-shell/02-UI-SPEC.md
@.planning/phases/02-ssr-shell/02-01-SUMMARY.md

<interfaces>
<!-- From app.config.ts (created by Plan 01): primary color = 'brand' (#85cb85) -->
<!-- From app/locales/fr.json (created by Plan 01): keys nav.*, footer.*, a11y.* -->
<!-- From nuxt.config.ts: colorMode configured with cookie, i18n with prefix_except_default -->
<!-- From src/config/site.ts: social links array with Gitea, LinkedIn, Discord, Email -->

<!-- Nuxt UI v3 components available (auto-imported): UDrawer, UButton, UIcon, UNavigationMenu -->
<!-- @nuxtjs/i18n composables: useI18n(), useSetLocale(), useSwitchLocalePath(), useLocalePath() -->
<!-- @nuxtjs/color-mode composable: useColorMode() -->
<!-- Nuxt Icon sets: heroicons:*, simple-icons:* -->

<!-- User post-research decision: Footer social icon uses Gitea icon (simple-icons:gitea), NOT GitHub -->
<!-- Social links from src/config/site.ts: Gitea (gitea.kamisama.ovh), LinkedIn, Discord, Email -->
<!-- D-05 says: GitHub, LinkedIn, Fiverr — BUT user corrected to Gitea. Use: Gitea, LinkedIn, Fiverr -->
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: AppHeader with nav, language toggle, theme toggle, mobile drawer</name>
  <files>app/components/layout/AppHeader.vue</files>
  <read_first>
    - src/components/layout/AppHeader.vue (old header — migration reference for structure and nav links)
    - .planning/phases/02-ssr-shell/02-UI-SPEC.md (Component Inventory: AppHeader, LanguageToggle, ThemeToggle, MobileDrawer specs; Interaction States table; Copywriting Contract for aria-labels)
    - .planning/phases/02-ssr-shell/02-RESEARCH.md (Pattern 2: Language Switcher with useSetLocale; Pattern 1: ThemeToggle with useColorMode)
    - app/locales/fr.json (verify nav.* and a11y.* keys exist from Plan 01)
    - src/config/site.ts (check logo image path — public/images/logo.webp)
  </read_first>
  <action>
Create `app/components/layout/AppHeader.vue` as a single-file component containing:

**Structure (per D-01, D-03):**
- `<header>` with `class="sticky top-0 z-[1020] bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800"`
- Inner wrapper: `<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">`

**Left — Logo:**
- `<NuxtLink to="localePath('/')" :aria-label="t('a11y.logoLabel')">`
- Contains `<NuxtImg src="/images/logo.webp" alt="Killian Dalcin" width="40" height="40" loading="eager" />` + `<span class="text-lg font-semibold">Killian</span>`

**Center-Right — Desktop nav (hidden md:flex):**
- Use `<nav>` with `<NuxtLink>` for each route: home(/), projects(/projects), about(/about), contact(/contact), fiverr(/fiverr), formation(/formation)
- Use `useLocalePath()` to generate locale-aware paths
- Active link detection: `class` binding comparing `route.path` with `localePath(path)`
- Active state: `border-b-2 border-primary-500` accent underline
- Default state: `text-gray-700 dark:text-gray-300`
- Hover state: `hover:text-primary-500`
- Focus: `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2`
- Nav link labels from `t('nav.home')`, `t('nav.projects')`, etc.
- `aria-current="page"` on active link

**Far Right — Toggles:**

Language toggle (per D-04 — simple text button FR/EN):
- `<button>` displaying `locale === 'fr' ? 'EN' : 'FR'` (shows the OTHER language to switch to)
- `class="min-w-11 min-h-11 inline-flex items-center justify-center text-gray-700 dark:text-gray-300 font-medium hover:text-primary-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 transition-colors"`
- Click handler: `const setLocale = useSetLocale(); setLocale(locale.value === 'fr' ? 'en' : 'fr')`
- `:aria-label="t('a11y.langToggle')"`

Theme toggle (per D-09):
- `<button>` with `<UIcon>`: show `heroicons:sun` when dark mode active (clicking switches to light), `heroicons:moon` when light mode active
- Icon size: `class="w-5 h-5"`
- Button: `class="min-w-11 min-h-11 inline-flex items-center justify-center text-gray-600 dark:text-gray-400 hover:text-primary-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 transition-colors duration-300"`
- Click: `const colorMode = useColorMode(); colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'`
- `:aria-label="colorMode.value === 'dark' ? t('a11y.themeDark') : t('a11y.themeLight')"`

Hamburger button (md:hidden):
- `<button @click="drawerOpen = true" class="md:hidden min-w-11 min-h-11 ..." :aria-label="t('a11y.openMenu')">`
- `<UIcon name="heroicons:bars-3" class="w-6 h-6" />`

**Mobile Drawer (per D-02):**
- `<UDrawer v-model:open="drawerOpen" side="left">`
- Inside: close button with `<UIcon name="heroicons:x-mark" />` and `:aria-label="t('a11y.closeDrawer')"`
- Nav links stacked full-width, same routes as desktop
- Language toggle and theme toggle at bottom
- Click any nav link sets `drawerOpen = false`

**Script setup:**
```typescript
const { t, locale } = useI18n()
const localePath = useLocalePath()
const setLocale = useSetLocale()
const colorMode = useColorMode()
const route = useRoute()
const drawerOpen = ref(false)

const navLinks = computed(() => [
  { key: 'home', path: '/' },
  { key: 'projects', path: '/projects' },
  { key: 'about', path: '/about' },
  { key: 'contact', path: '/contact' },
  { key: 'fiverr', path: '/fiverr' },
  { key: 'formation', path: '/formation' },
])

function toggleLocale() {
  setLocale(locale.value === 'fr' ? 'en' : 'fr')
}

function toggleTheme() {
  colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
}
```
  </action>
  <verify>
    <automated>grep -q "useColorMode" app/components/layout/AppHeader.vue && grep -q "useSetLocale\|setLocale" app/components/layout/AppHeader.vue && grep -q "UDrawer" app/components/layout/AppHeader.vue && grep -q "sticky" app/components/layout/AppHeader.vue && grep -q "a11y.logoLabel" app/components/layout/AppHeader.vue && echo "PASS" || echo "FAIL"</automated>
  </verify>
  <acceptance_criteria>
    - app/components/layout/AppHeader.vue exists and contains `sticky top-0`
    - Contains `useColorMode()` for theme toggle
    - Contains `useSetLocale()` or `setLocale` for language switching
    - Contains `UDrawer` for mobile navigation
    - Contains `z-[1020]` for z-index
    - Contains `heroicons:sun` and `heroicons:moon` for theme icons
    - Contains `heroicons:bars-3` for hamburger
    - Contains `t('a11y.logoLabel')` for logo aria-label
    - Contains `localePath` for locale-aware routing
    - Contains `min-w-11 min-h-11` on interactive buttons (44px touch targets)
    - Contains `aria-current` for active nav link
    - Contains `focus-visible:ring-2` on interactive elements
  </acceptance_criteria>
  <done>AppHeader renders sticky header with desktop nav links, FR/EN text toggle using setLocale, dark/light icon toggle using useColorMode, and mobile UDrawer. All interactive elements have WCAG touch targets, focus rings, and ARIA labels from i18n.</done>
</task>

<task type="auto">
  <name>Task 2: AppFooter + default layout + app.vue update</name>
  <files>app/components/layout/AppFooter.vue, app/layouts/default.vue, app/app.vue</files>
  <read_first>
    - src/components/layout/AppFooter.vue (old footer — migration reference)
    - src/config/site.ts (social links: Gitea, LinkedIn, Discord, Email — note user wants Gitea icon not GitHub, and D-05 specifies Fiverr link too)
    - .planning/phases/02-ssr-shell/02-UI-SPEC.md (AppFooter spec, Interaction States for social icons)
    - .planning/phases/02-ssr-shell/02-RESEARCH.md (Pattern 3: useLocaleHead in app.vue)
    - app/app.vue (current state — has useHead with htmlAttrs lang)
    - app/locales/fr.json (verify footer.* and a11y.* keys)
  </read_first>
  <action>
1. Create `app/components/layout/AppFooter.vue`:

Per D-05: single band footer — copyright + social icons.
User post-research decision: use Gitea icon (not GitHub). Social links: Gitea (gitea.kamisama.ovh/kayjaydee), LinkedIn (linkedin.com/in/killian-dal-cin), Fiverr (fiverr.com/users/mr_kayjaydee).

```vue
<script setup lang="ts">
const { t } = useI18n()

const socialLinks = [
  { name: 'gitea', url: 'https://gitea.kamisama.ovh/kayjaydee', icon: 'simple-icons:gitea', ariaKey: 'a11y.github' },
  { name: 'linkedin', url: 'https://linkedin.com/in/killian-dal-cin', icon: 'simple-icons:linkedin', ariaKey: 'a11y.linkedin' },
  { name: 'fiverr', url: 'https://www.fiverr.com/users/mr_kayjaydee', icon: 'simple-icons:fiverr', ariaKey: 'a11y.fiverr' },
]
</script>
```

Template:
- `<footer class="py-6 bg-gray-100 dark:bg-gray-800">`
- Inner: `<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col md:flex-row items-center justify-between gap-4">`
- Left: `<p class="text-sm text-gray-600 dark:text-gray-400">{{ t('footer.copyright') }}</p>`
- Right: social icons in flex row, each `<a :href="link.url" target="_blank" rel="noopener noreferrer" :aria-label="t(link.ariaKey)">`
- Icon: `<UIcon :name="link.icon" class="w-5 h-5 text-gray-500 dark:text-gray-400 hover:text-primary-500 transition-colors duration-150" />`
- Focus ring on each `<a>`: `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2`

Note: a11y.github key text says "GitHub de Killian Dalcin" but links to Gitea — the executor should update the a11y key in fr.json/en.json to say "Gitea" instead of "GitHub" if not already correct. Check and fix if needed.

2. Create `app/layouts/default.vue` (per D-15):
```vue
<template>
  <div class="min-h-screen flex flex-col">
    <AppHeader />
    <main class="flex-1">
      <slot />
    </main>
    <AppFooter />
  </div>
</template>
```

3. Update `app/app.vue` to use the default layout and add useLocaleHead() for global hreflang/canonical (per RESEARCH Pattern 3):
```vue
<script setup lang="ts">
const { locale } = useI18n()
const head = useLocaleHead({ addSeoAttributes: true })

useHead({
  htmlAttrs: { lang: locale },
  link: computed(() => head.value.link || []),
  meta: computed(() => head.value.meta || []),
})
</script>

<template>
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>
```

Remove the existing `<NuxtRouteAnnouncer />` and `<div>` wrapper — the layout handles structure now.
  </action>
  <verify>
    <automated>grep -q "AppHeader" app/layouts/default.vue && grep -q "AppFooter" app/layouts/default.vue && grep -q "simple-icons:gitea" app/components/layout/AppFooter.vue && grep -q "useLocaleHead" app/app.vue && grep -q "NuxtLayout" app/app.vue && echo "PASS" || echo "FAIL"</automated>
  </verify>
  <acceptance_criteria>
    - app/components/layout/AppFooter.vue contains `simple-icons:gitea` (not github)
    - app/components/layout/AppFooter.vue contains `simple-icons:linkedin` and `simple-icons:fiverr`
    - app/components/layout/AppFooter.vue contains `target="_blank"` and `rel="noopener noreferrer"`
    - app/components/layout/AppFooter.vue contains `t('footer.copyright')`
    - app/layouts/default.vue contains `<AppHeader />` and `<AppFooter />`
    - app/layouts/default.vue contains `<slot />`
    - app/app.vue contains `useLocaleHead({ addSeoAttributes: true })`
    - app/app.vue contains `<NuxtLayout>` wrapping `<NuxtPage />`
    - app/app.vue does NOT contain `<NuxtRouteAnnouncer />`
  </acceptance_criteria>
  <done>AppFooter renders copyright + Gitea/LinkedIn/Fiverr social icons. Default layout wraps header + slot + footer. app.vue uses NuxtLayout and injects global hreflang/canonical via useLocaleHead().</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| External links (footer) | Social icon links open external URLs in new tabs |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-02-04 | Tampering | External social links | mitigate | All external links use `rel="noopener noreferrer"` to prevent reverse tabnabbing |
| T-02-05 | Spoofing | Locale switching | accept | setLocale only accepts 'fr' or 'en' — constrained by i18n config, no injection risk |
</threat_model>

<verification>
- `pnpm dev` starts and renders header + footer on localhost:3000
- Language toggle switches between FR/EN URLs
- Theme toggle switches dark/light classes
- Mobile hamburger opens UDrawer
- `curl http://localhost:3000` returns HTML with `<header>` and `<footer>` elements
</verification>

<success_criteria>
- Header sticky with nav links, FR/EN toggle, dark/light toggle, mobile drawer
- Footer shows copyright and 3 social icon links
- Default layout renders header + page content + footer
- app.vue injects global hreflang/canonical metadata
- All interactive elements have focus rings and ARIA labels
</success_criteria>

<output>
After completion, create `.planning/phases/02-ssr-shell/02-02-SUMMARY.md`
</output>
