15 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 02-ssr-shell | 02 | execute | 2 |
|
|
true |
|
|
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.
<execution_context>
@/.claude/get-shit-done/workflows/execute-plan.md
@/.claude/get-shit-done/templates/summary.md
</execution_context>
Structure (per D-01, D-03):
<header>withclass="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:
classbinding comparingroute.pathwithlocalePath(path) - Active state:
border-b-2 border-primary-500accent 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>displayinglocale === '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>: showheroicons:sunwhen dark mode active (clicking switches to light),heroicons:moonwhen 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:
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'
}
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).
<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.
- Create
app/layouts/default.vue(per D-15):
<template>
<div class="min-h-screen flex flex-col">
<AppHeader />
<main class="flex-1">
<slot />
</main>
<AppFooter />
</div>
</template>
- Update
app/app.vueto use the default layout and add useLocaleHead() for global hreflang/canonical (per RESEARCH Pattern 3):
<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.
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"
<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>
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().
<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> |
<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>