fix(navigation): implement persistent shell with client-side highlighting for rock-solid mobile menu
All checks were successful
Deploy Theme / deploy (push) Successful in 14s

This commit is contained in:
2026-02-20 11:25:13 +01:00
parent d7702d6697
commit 5db7ad8e6b
2 changed files with 25 additions and 16 deletions

View File

@@ -3,6 +3,7 @@
x-data="{ x-data="{
theme: localStorage.getItem('theme') || (window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'), theme: localStorage.getItem('theme') || (window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'),
mobileMenuOpen: false, mobileMenuOpen: false,
currentPath: window.location.pathname,
init() { init() {
$watch('theme', val => { $watch('theme', val => {
localStorage.setItem('theme', val); localStorage.setItem('theme', val);
@@ -10,10 +11,15 @@
}); });
document.documentElement.setAttribute('data-theme', this.theme); document.documentElement.setAttribute('data-theme', this.theme);
document.documentElement.classList.remove('hidden'); document.documentElement.classList.remove('hidden');
// Update currentPath on navigation to keep menu highlights in sync
document.addEventListener('htmx:afterSettle', () => {
this.currentPath = window.location.pathname;
this.mobileMenuOpen = false;
});
} }
}" }"
:data-theme="theme" :data-theme="theme"
@htmx:before-request.window="mobileMenuOpen = false"
class="hidden"> class="hidden">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
@@ -30,26 +36,25 @@
{{ghost_head}} {{ghost_head}}
</head> </head>
<body <body
class="{{body_class}} font-sans antialiased" class="{{body_class}} font-sans antialiased min-h-screen flex flex-col"
hx-boost="true" hx-boost="true"
hx-target="#viewport" hx-target="#main-content"
hx-select="#viewport" hx-select="#main-content"
hx-swap="innerHTML show:top"
> >
{{!-- {{!--
This container is the target for HTMX swaps. PERSISTENT SHELL:
Moving everything inside ensures navigation highlights and states update on every click. These elements are OUTSIDE #main-content, so they never swap.
This keeps Alpine state (mobileMenuOpen) perfectly stable.
--}} --}}
<div id="viewport" class="min-h-screen flex flex-col">
{{> mobile-menu}} {{> mobile-menu}}
{{> header}} {{> header}}
<main class="flex-grow"> <main id="main-content" class="flex-grow">
{{{body}}} {{{body}}}
</main> </main>
{{> footer}} {{> footer}}
</div>
{{ghost_foot}} {{ghost_foot}}
<script src="{{asset "js/ghost-config.js"}}"></script> <script src="{{asset "js/ghost-config.js"}}"></script>

View File

@@ -1,6 +1,10 @@
<ul class="nav"> <ul class="nav">
{{#foreach navigation}} {{#foreach navigation}}
<li class="{{link_class for=url class=(concat "nav-" slug)}}"> {{!--
We use Alpine to dynamically apply the nav-current class based on currentPath.
This works even when the navigation itself is not swapped by HTMX.
--}}
<li class="nav-{{slug}}" :class="currentPath === '{{url}}' ? 'nav-current' : ''">
<a href="{{url absolute="true"}}" class="nav-link"> <a href="{{url absolute="true"}}" class="nav-link">
{{label}} {{label}}
</a> </a>