footer : fix show/hide logic, transition and scroll throttle. related to #51
All checks were successful
Deploy / Deploy to Production (push) Successful in 23s

- Fix scroll listener (cleanup, local querySelector, scrollHeight calc)
- Fix media query syntax in variables.css (missing space in `and (`)
- Use transform: translateY instead of bottom for GPU-accelerated transition
- Throttle scroll handler with requestAnimationFrame
- Move Footer to App.svelte (global), remove per-view imports

refs #51

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
isUnknown 2026-04-01 19:16:58 +02:00
parent 77a1c58573
commit 0afbcf4088
9 changed files with 48 additions and 53 deletions

View file

@ -5,6 +5,7 @@
import Header from '@components/layout/Header.svelte'
import Cursor from '@components/layout/Cursor.svelte'
import Footer from '@components/layout/Footer.svelte'
import LanguageSwitcher from '@components/ui/LanguageSwitcher.svelte'
import Home from '@views/Home.svelte'
@ -173,6 +174,8 @@
</main>
<LanguageSwitcher />
<Footer />
<style>
:global(#app) {
height: 100vh;

View file

@ -2,38 +2,45 @@
import { site } from '@state/site.svelte'
import { locale } from '@state/locale.svelte'
import { t } from '@i18n'
import { slides } from '@state/slides.svelte'
const logo = $derived(site.logo)
const title = $derived(site.title || 'World Game')
const contact = $derived(site.contact || {})
const socials = $derived(contact.socials ?? [])
const year = new Date().getFullYear()
let isHidden = $state(true)
let email = $state('')
let status = $state(null)
$effect(() => {
const activeSlide = document.querySelector('.slide[data-slide="' + slides.active?.id + '"]')
const scrollableContainer = activeSlide?.querySelector('.page-scrollable')
async function handleSubscribe(e) {
e.preventDefault()
if (!email) return
try {
const res = await fetch('/newsletter.json', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email })
isHidden = true
if (!scrollableContainer) return
let rafId = null
function onScroll() {
if (rafId) return
rafId = requestAnimationFrame(() => {
const threshold = window.innerWidth > 800 ? 100 : 200
const atBottom = scrollableContainer.scrollTop >= scrollableContainer.scrollHeight - scrollableContainer.clientHeight - threshold
isHidden = !atBottom
rafId = null
})
if (res.ok) {
status = { type: 'success', message: t('newsletter_success') }
email = ''
} else {
status = { type: 'error', message: t('newsletter_error') }
}
} catch {
status = { type: 'error', message: t('newsletter_error') }
}
scrollableContainer.addEventListener('scroll', onScroll)
onScroll()
return () => {
scrollableContainer.removeEventListener('scroll', onScroll)
if (rafId) cancelAnimationFrame(rafId)
}
})
</script>
<footer class="page-scrollable-footer">
<footer class={["page-scrollable-footer", {hidden: isHidden}]}>
<div class="footer-main">
<!-- Logo -->
@ -98,18 +105,17 @@
<style>
footer {
position: absolute;
width: 100vw;
bottom: 0;
background: #0d0e22;
z-index: 2;
transition: transform .3s var(--ease-standard);
}
:global(.collection .page-scrollable-footer) {
margin-left: -10.3rem;
margin-top: 5rem;
}
:global(.article-wrapper .page-scrollable-footer) {
margin-left: -3.3rem;
footer.hidden {
transform: translateY(var(--footer-height));
}
/* --- Main row --- */
@ -222,17 +228,6 @@
width: 7%;
height: 2px;
}
:global(.collection .page-scrollable-footer) {
margin-left: -1.5rem;
}
:global(.article-wrapper .page-scrollable-footer) {
margin-left: -1.3rem;
}
:global(.about .page-scrollable-footer) {
margin-left: 0;
}
}
/* --- Tablet (701px912px) --- */

View file

@ -20,6 +20,10 @@
overflow-x: hidden;
}
.page-scrollable:not(.white-paper) .page-container > *:last-child {
padding-bottom: calc(var(--footer-height) + 5rem);
}
/* Vertical Lines — utilisées dans Menu.svelte (golden-grid) */
.vertical-line {
grid-area: auto;

View file

@ -62,4 +62,12 @@
--font-size-expertise: 22px;
--font-size-expertise-mobile: 18px;
--font-size-expertise-tablet: 20px;
--footer-height: 13rem;
}
@media screen and (max-width: 800px) {
:root {
--footer-height: 29rem;
}
}

View file

@ -4,7 +4,6 @@
import { navigation } from '@state/navigation.svelte'
import { t } from '@i18n'
import Footer from '@components/layout/Footer.svelte'
let { data } = $props()
@ -168,8 +167,6 @@
</div>
</section>
{/if}
<Footer />
</div>
</div>

View file

@ -6,7 +6,6 @@
import { t } from '@i18n'
import { onMount } from 'svelte'
import Footer from '@components/layout/Footer.svelte'
import WhitePaperDialog from '@components/WhitePaperDialog.svelte'
import ShareButtons from '@components/blocks/ShareButtons.svelte'
import ArticleRelated from '@components/blocks/ArticleRelated.svelte'
@ -144,8 +143,6 @@
<ArticleRelated related={data.related} />
{/if}
</article>
<Footer />
</div>
<WhitePaperDialog uri={activeWhitePaperUri} onClose={() => activeWhitePaperUri = null} />

View file

@ -5,7 +5,6 @@
import Article from '@views/Article.svelte'
import { t } from '@i18n'
import Footer from '@components/layout/Footer.svelte'
let { data } = $props()
@ -183,8 +182,6 @@
{#if articleLoading}
<p class="collection-loading">{t('loading')}</p>
{/if}
<Footer />
</div>
{/if}

View file

@ -1,6 +1,5 @@
<script>
import { navigation } from '@state/navigation.svelte'
import Footer from '@components/layout/Footer.svelte'
let { data } = $props()
@ -28,8 +27,6 @@
{/each}
</section>
{/if}
<Footer />
</div>
</div>

View file

@ -4,7 +4,6 @@
import { locale } from '@state/locale.svelte'
import WhitePaper from '@views/WhitePaper.svelte'
import { t } from '@i18n'
import Footer from '@components/layout/Footer.svelte'
let { data } = $props()
@ -154,8 +153,6 @@
{#if itemLoading}
<p class="collection-loading">{t('loading')}</p>
{/if}
<Footer class="page-scrollable-footer" />
</div>
{/if}
</section>