From 35e5a0133144632f9a37a9d196dc50eb5e694ba2 Mon Sep 17 00:00:00 2001 From: Padreug Date: Wed, 27 May 2026 11:13:48 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20app=20shell=20=E2=80=94=20header,=20foo?= =?UTF-8?q?ter,=20layout,=20theme=20toggle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SiteHeader is sticky with backdrop blur, wordmark left and tracked uppercase nav right (Portfolio / Contact). Mobile breakpoint collapses to a Sheet drawer. ThemeToggle drops a class onto and persists to localStorage; initial value respects the system prefers-color-scheme. SiteFooter is intentionally bare: wordmark, single Inquire link, copyright. No social, no phone, no address — the brief is anonymity-first. AppLayout composes the three plus Sonner's Toaster (used by the contact form later). App.vue now renders AppLayout instead of a bare RouterView. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/App.vue | 4 +- src/components/layout/AppLayout.vue | 17 ++++++ src/components/layout/SiteFooter.vue | 29 ++++++++++ src/components/layout/SiteHeader.vue | 80 +++++++++++++++++++++++++++ src/components/layout/ThemeToggle.vue | 18 ++++++ src/composables/useTheme.ts | 27 +++++++++ 6 files changed, 173 insertions(+), 2 deletions(-) create mode 100644 src/components/layout/AppLayout.vue create mode 100644 src/components/layout/SiteFooter.vue create mode 100644 src/components/layout/SiteHeader.vue create mode 100644 src/components/layout/ThemeToggle.vue create mode 100644 src/composables/useTheme.ts diff --git a/src/App.vue b/src/App.vue index a78fb6f..b18ca84 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,7 +1,7 @@ diff --git a/src/components/layout/AppLayout.vue b/src/components/layout/AppLayout.vue new file mode 100644 index 0000000..a477f85 --- /dev/null +++ b/src/components/layout/AppLayout.vue @@ -0,0 +1,17 @@ + + + diff --git a/src/components/layout/SiteFooter.vue b/src/components/layout/SiteFooter.vue new file mode 100644 index 0000000..98a6cdb --- /dev/null +++ b/src/components/layout/SiteFooter.vue @@ -0,0 +1,29 @@ + + + diff --git a/src/components/layout/SiteHeader.vue b/src/components/layout/SiteHeader.vue new file mode 100644 index 0000000..3472228 --- /dev/null +++ b/src/components/layout/SiteHeader.vue @@ -0,0 +1,80 @@ + + + diff --git a/src/components/layout/ThemeToggle.vue b/src/components/layout/ThemeToggle.vue new file mode 100644 index 0000000..2e8c326 --- /dev/null +++ b/src/components/layout/ThemeToggle.vue @@ -0,0 +1,18 @@ + + + diff --git a/src/composables/useTheme.ts b/src/composables/useTheme.ts new file mode 100644 index 0000000..2b2e0a8 --- /dev/null +++ b/src/composables/useTheme.ts @@ -0,0 +1,27 @@ +import { ref, watchEffect } from 'vue' + +type Theme = 'light' | 'dark' + +const STORAGE_KEY = 'ewd:theme' + +function initialTheme(): Theme { + if (typeof window === 'undefined') return 'light' + const stored = window.localStorage.getItem(STORAGE_KEY) + if (stored === 'light' || stored === 'dark') return stored + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' +} + +const theme = ref(initialTheme()) + +watchEffect(() => { + if (typeof document === 'undefined') return + document.documentElement.classList.toggle('dark', theme.value === 'dark') + window.localStorage.setItem(STORAGE_KEY, theme.value) +}) + +export function useTheme() { + function toggle() { + theme.value = theme.value === 'dark' ? 'light' : 'dark' + } + return { theme, toggle } +}