feat: add mobile menu and modal

This commit is contained in:
Stefan Imhoff
2026-01-29 20:12:24 +01:00
committed by Stefan Imhoff
parent 10b674cb2f
commit 76a7b16d7a
8 changed files with 196 additions and 22 deletions

View File

@@ -9,7 +9,7 @@ import navigation from '../data/navigation.json';
<ul class="flex flex-wrap"> <ul class="flex flex-wrap">
{ {
navigation.map(({ title, url }) => ( navigation.map(({ title, url }) => (
<li class="mie-[10px] xs:mie-[15px]"> <li class="mie-[10px] xs:mie-[5px]">
<Link <Link
class="rounded-2 pli-3 pbe-2 pbs-3 hover:bg-shibui-950/10 focus:bg-shibui-950/10 hover:dark:bg-shibui-50/10 focus:dark:bg-shibui-50/10" class="rounded-2 pli-3 pbe-2 pbs-3 hover:bg-shibui-950/10 focus:bg-shibui-950/10 hover:dark:bg-shibui-50/10 focus:dark:bg-shibui-50/10"
data-umami-event={title} data-umami-event={title}

View File

@@ -0,0 +1,22 @@
---
import { Menu } from './icons';
import Link from './Link.astro';
---
<Link
aria-label="Open the navigation menu"
class="h-clickarea w-clickarea scale-75 transition-transform duration-500 ease-in-out hover:scale-90 focus:scale-90 md:col-span-1 md:hidden print:hidden"
href="#menu"
id="menu-link"
title="Menu"
>
<button
aria-hidden="true"
class="flex h-clickarea w-clickarea cursor-pointer items-center justify-center border-none text-[0] outline-none"
data-umami-event="Menu"
tabindex={-1}
type="button"
>
<Menu aria-hidden="true" className="icon h-icon w-icon" />
</button>
</Link>

View File

@@ -0,0 +1,92 @@
---
import Link from './Link.astro';
import MenuNavigation from './MenuNavigation.astro';
import { Close } from './icons';
---
<dialog id="menu-dialog">
<header
class="flex h-[clamp(1.5rem,_5.55vw,_9rem)] w-full items-center justify-end overflow-x-hidden block-start-0 md:h-[clamp(4rem,_5.55vw,_9rem)] md:pie-gap md:pis-gap"
>
<Link
id="close-menu"
class="relative left-[10px] outline-none transition-transform duration-500 ease-in-out block-start-0 hover:scale-125"
>
<button
aria-label="Close Menu Modal"
class="flex h-[2em] w-[2em] cursor-pointer items-center justify-center text-shibui-950 dark:text-shibui-200/[0.87]"
>
<Close className="icon h-[1.5em] w-[1.5em]" />
</button>
</Link>
</header>
<section
class="flex h-[calc(100%_-_clamp(1.5rem,_5.55vw,_9rem))] w-full flex-col items-center justify-center overflow-x-hidden md:h-[calc(100%_-_clamp(4rem,_5.55vw,_9rem))] md:pie-gap md:pis-gap"
>
<MenuNavigation />
</section>
</dialog>
<style is:global>
dialog::backdrop {
@apply bg-shibui-100 backdrop-blur-md dark:bg-shibui-900 md:bg-shibui-100/60 md:dark:bg-shibui-900/60;
animation: show-dimmer 0.2s ease-in-out;
}
dialog.hide::backdrop {
animation: hide-dimmer 0.2s ease-in-out;
}
dialog {
@apply h-dvh max-w-[950px] rounded-2 md:max-h-[90vh];
}
dialog[open] {
@apply w-[calc(100%_-_5.55vw_*_2)] bg-shibui-100 p-0 font-sans font-normal leading-relaxed text-shibui-950 common-ligatures dark:bg-shibui-900 dark:text-shibui-200/[0.87] md:dark:bg-shibui-800;
animation: show-modal 0.2s ease-in-out;
}
dialog.hide {
animation: hide-modal 0.2s ease-in-out;
}
@keyframes show-modal {
from {
opacity: 0;
transform: translateY(50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes show-dimmer {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes hide-modal {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(50px);
}
}
@keyframes hide-dimmer {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
</style>

View File

@@ -0,0 +1,44 @@
---
import data from '../data/subnavigation.json';
import Link from './Link.astro';
---
<nav class="navigation glow flex gap-15" aria-label="Subnavigation" role="navigation">
<ul>
<li class="mbe-[20px]">
<Link
href="/"
class="text-5 font-light decoration-4 underline-offset-auto hover:underline hover:decoration-shibui-900/20 focus:underline focus:decoration-shibui-900/20 dark:hover:decoration-shibui-100/20 dark:focus:decoration-shibui-100/20"
>
Home
</Link>
</li>
{
data.main.map(({ title, url }) => (
<li class="mbe-[20px]">
<Link
href={url}
class="text-5 font-light decoration-4 underline-offset-auto hover:underline hover:decoration-shibui-900/20 focus:underline focus:decoration-shibui-900/20 dark:hover:decoration-shibui-100/20 dark:focus:decoration-shibui-100/20"
>
{title}
</Link>
</li>
))
}
</ul>
<ul class="mie-10">
{
data.misc.map(({ title, url }) => (
<li class="mbe-[5px]">
<Link
href={url}
class="font-light decoration-4 underline-offset-auto hover:underline hover:decoration-shibui-900/20 focus:underline focus:decoration-shibui-900/20 dark:hover:decoration-shibui-100/20 dark:focus:decoration-shibui-100/20"
>
{title}
</Link>
</li>
))
}
</ul>
</nav>

View File

@@ -4,6 +4,7 @@ import ThemeToggle from '../components/ThemeToggle.astro';
import Logo from '../components/Logo.astro'; import Logo from '../components/Logo.astro';
import SearchLink from './SearchLink.astro'; import SearchLink from './SearchLink.astro';
import MenuLink from './MenuLink.astro';
export interface Props { export interface Props {
class?: string; class?: string;
@@ -27,5 +28,6 @@ const { class: className, navigation = true } = Astro.props;
{navigation && <MainNavigation />} {navigation && <MainNavigation />}
<SearchLink /> <SearchLink />
<ThemeToggle /> <ThemeToggle />
<MenuLink />
</div> </div>
</header> </header>

View File

@@ -32,7 +32,7 @@
function setSearchLink() { function setSearchLink() {
const body = document.body; const body = document.body;
const dialog = document.querySelector('dialog'); const dialog = document.querySelector<HTMLDialogElement>('#search-dialog');
const openDialogLink = document.getElementById('search-link'); const openDialogLink = document.getElementById('search-link');
const closeDialogLink = document.getElementById('close-search'); const closeDialogLink = document.getElementById('close-search');
@@ -76,25 +76,37 @@
}); });
} }
function setSearchModalLink() { function setMenuLink() {
const dialog = document.querySelector('dialog'); const body = document.body;
const dialog = document.querySelector<HTMLDialogElement>('#menu-dialog');
const openDialogLink = document.getElementById('menu-link');
const closeDialogLink = document.getElementById('close-menu');
dialog?.addEventListener('click', (event) => { dialog?.addEventListener('close', () => {
if (event.target === dialog) { body.style.overflow = 'auto';
if (!dialog.classList.contains('hide')) { });
dialog.classList.add('hide');
dialog.addEventListener( openDialogLink?.addEventListener('click', (event) => {
event.preventDefault();
dialog?.showModal();
body.style.overflow = 'hidden';
});
closeDialogLink?.addEventListener('click', (event) => {
event.preventDefault();
if (!dialog?.classList.contains('hide')) {
dialog?.classList.add('hide');
dialog?.addEventListener(
'animationend', 'animationend',
(animationEvent) => { (animationEvent) => {
if (animationEvent.animationName === 'hide-modal') { if (animationEvent.animationName === 'hide-modal') {
dialog.close(); dialog?.close();
dialog.classList.remove('hide'); dialog?.classList.remove('hide');
} }
}, },
{ once: true } { once: true }
); );
} }
}
}); });
} }
@@ -122,7 +134,7 @@
setActiveLink(); setActiveLink();
setEmailLink(); setEmailLink();
setSearchLink(); setSearchLink();
setSearchModalLink(); setMenuLink();
setUpLink(); setUpLink();
} }

View File

@@ -5,7 +5,7 @@ import Link from './Link.astro';
import { Close } from './icons'; import { Close } from './icons';
--- ---
<dialog> <dialog id="search-dialog">
<header <header
class="flex h-[clamp(1.5rem,_5.55vw,_9rem)] w-full items-center justify-end overflow-x-hidden block-start-0 md:h-[clamp(4rem,_5.55vw,_9rem)] md:pie-gap md:pis-gap" class="flex h-[clamp(1.5rem,_5.55vw,_9rem)] w-full items-center justify-end overflow-x-hidden block-start-0 md:h-[clamp(4rem,_5.55vw,_9rem)] md:pie-gap md:pis-gap"
> >
@@ -115,7 +115,7 @@ import { Close } from './icons';
} }
.pagefind-ui__results { .pagefind-ui__results {
@apply !grid-cols-[repeat(auto-fill,_minmax(400px,_1fr))] !gap-x-gap !pbe-12 lg:!grid; @apply !gap-x-gap !pbe-12 lg:!grid;
} }
.pagefind-ui__result { .pagefind-ui__result {

View File

@@ -9,6 +9,7 @@ import ThemeProvider from '../components/ThemeProvider.astro';
import PageHeader from '../components/PageHeader.astro'; import PageHeader from '../components/PageHeader.astro';
import PageFooter from '../components/PageFooter.astro'; import PageFooter from '../components/PageFooter.astro';
import SearchModal from '../components/SearchModal.astro'; import SearchModal from '../components/SearchModal.astro';
import MenuModal from '../components/MenuModal.astro';
import Scripts from '../components/Scripts.astro'; import Scripts from '../components/Scripts.astro';
export interface Props { export interface Props {
@@ -172,6 +173,7 @@ const webManifest = isProduction && {
{footer && <PageFooter />} {footer && <PageFooter />}
</div> </div>
<SearchModal /> <SearchModal />
<MenuModal />
<script> <script>
console.info( console.info(
'👋 I see youre interested in the source code of this site? You can find it here 👉 https://github.com/kogakure/website-astro-stefanimhoff.de' '👋 I see youre interested in the source code of this site? You can find it here 👉 https://github.com/kogakure/website-astro-stefanimhoff.de'