Sheets via neuer KeyboardAwareSheet-Composable (in Modal pattern, auto-grow mit Tastatur, paddingBottom-Lift): EditMail, AddDomain, CreateRoom, ConnectMail. GameOverScreen behält Spring-Slide-In, nutzt RN Keyboard.addListener für Lift. - KeyboardAwareSheet.tsx — universal modal with sheet-grow + keyboard-padding - react-native-keyboard-controller installiert + KeyboardProvider in Root - Snake: time + ScoreProgressBar + useSnakeSounds (haptic, audio TODO) - Tetris: title weg, Buttons zentriert, kein Pressable mit style-fn - DPad-Buttons 60→48, more bg, no scale - useMe: pub-sub listener pattern für app-weite avatar/nickname-Updates - dm.tsx: resolveAvatar wrap (iron.png-Warning) - Mail-error-humanizer + locales Recovery-Doc-Update in docs/internal/RECOVERY_LOG_2026-05-10.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
96 lines
3.9 KiB
Vue
96 lines
3.9 KiB
Vue
<template>
|
|
<div data-layout="default" class="flex flex-col overflow-hidden bg-default text-highlighted"
|
|
:style="{ height: vpHeight + 'px' }">
|
|
<!-- Header sticky, innerhalb des Flex-Flows -->
|
|
<header class="shrink-0 h-16 border-b border-default bg-default/95 backdrop-blur-md">
|
|
<div class="max-w-6xl mx-auto px-4 sm:px-6 h-full flex items-center justify-between">
|
|
<NuxtLink to="/" class="flex items-center gap-2">
|
|
<div class="w-8 h-8 rounded-lg bg-primary flex items-center justify-center">
|
|
<UIcon name="i-heroicons-shield-check" class="text-white" />
|
|
</div>
|
|
<span class="font-bold text-lg text-highlighted">ReBreak</span>
|
|
</NuxtLink>
|
|
|
|
<nav class="hidden sm:flex items-center gap-6 text-sm text-muted">
|
|
<NuxtLink to="/pricing" class="hover:text-highlighted transition-colors">{{ $t("nav.pricing") }}</NuxtLink>
|
|
<NuxtLink to="/resources" class="hover:text-highlighted transition-colors">{{ $t("nav.resources") }}</NuxtLink>
|
|
</nav>
|
|
|
|
<!-- App-Download CTA statt Login (Marketing hat keine Auth) -->
|
|
<div class="flex items-center gap-3">
|
|
<a href="https://apps.apple.com/app/rebreak" target="_blank" rel="noopener">
|
|
<UButton size="sm" color="primary">{{ $t("nav.download_app") }}</UButton>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Scrollbarer Inhalt -->
|
|
<div class="flex-1 overflow-y-auto">
|
|
<slot />
|
|
|
|
<!-- DSGVO-Pflicht: Datenschutz + Impressum public erreichbar -->
|
|
<footer class="border-t border-default mt-12 pb-24 md:pb-6 pt-6 px-4">
|
|
<div
|
|
class="max-w-6xl mx-auto flex flex-col sm:flex-row items-center justify-between gap-3 text-xs text-muted">
|
|
<p>© {{ new Date().getFullYear() }} Rebreak</p>
|
|
<nav class="flex flex-wrap items-center gap-x-5 gap-y-2">
|
|
<NuxtLink to="/datenschutz" class="hover:text-primary-400 transition-colors">Datenschutz</NuxtLink>
|
|
<NuxtLink to="/impressum" class="hover:text-primary-400 transition-colors">Impressum</NuxtLink>
|
|
<NuxtLink to="/nutzungsbedingungen" class="hover:text-primary-400 transition-colors">
|
|
Nutzungsbedingungen
|
|
</NuxtLink>
|
|
</nav>
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
|
|
<!-- Floating Pill Tab-Bar (Mobile) -->
|
|
<div class="fixed bottom-3 left-4 right-4 z-50 pointer-events-none">
|
|
<nav
|
|
class="pointer-events-auto max-w-lg mx-auto bg-white/80 dark:bg-elevated backdrop-blur-md rounded-4xl shadow-[0_4px_24px_rgba(0,0,0,0.12)] dark:shadow-[0_4px_24px_rgba(0,0,0,0.3)] ring-1 ring-black/10 dark:ring-white/10 px-2 py-1.5">
|
|
<div class="flex items-center">
|
|
<NuxtLink v-for="tab in tabs" :key="tab.to" :to="tab.to"
|
|
class="relative flex-1 flex flex-col items-center gap-0.5 px-1 py-1.5 rounded-3xl transition-colors"
|
|
:class="isActive(tab.to) ? 'text-primary-400' : 'text-muted'">
|
|
<UIcon :name="isActive(tab.to) ? tab.iconActive : tab.icon" class="size-6 shrink-0" />
|
|
<span class="text-[10px] leading-none whitespace-nowrap font-semibold">{{ tab.label }}</span>
|
|
</NuxtLink>
|
|
</div>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
const { height: vpHeight } = useViewportHeight();
|
|
const route = useRoute();
|
|
const { t } = useI18n();
|
|
|
|
const tabs = computed(() => [
|
|
{
|
|
to: "/",
|
|
icon: "i-heroicons-home",
|
|
iconActive: "i-heroicons-home-solid",
|
|
label: t('pricing.footer_home'),
|
|
},
|
|
{
|
|
to: "/pricing",
|
|
icon: "i-heroicons-credit-card",
|
|
iconActive: "i-heroicons-credit-card-solid",
|
|
label: t('pricing.footer_pricing'),
|
|
},
|
|
{
|
|
to: "/resources",
|
|
icon: "i-heroicons-book-open",
|
|
iconActive: "i-heroicons-book-open-solid",
|
|
label: t('pricing.footer_resources'),
|
|
},
|
|
]);
|
|
|
|
function isActive(to: string) {
|
|
if (to === "/") return route.path === "/";
|
|
return route.path.startsWith(to);
|
|
}
|
|
</script>
|