@@ -23,11 +23,29 @@ const basePath = import.meta.env.BASE_URL.endsWith("/")
2323 <a href =" #frameworks" data-section =" frameworks" >Frameworks</a >
2424 <a href ={ ` ${basePath }docs/ ` } data-section =" docs" >Docs</a >
2525 </nav >
26- <a href =" https://github.com/devalade/shipnode" target =" _blank" rel =" noopener" class =" github-link" id =" github-link" aria-label =" View ShipNode on GitHub" >
27- <svg width =" 18" height =" 18" viewBox =" 0 0 24 24" fill =" currentColor" ><path d =" M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" /></svg >
28- <span >GitHub</span >
29- </a >
26+ <div class =" header-actions" >
27+ <button
28+ type =" button"
29+ class =" menu-toggle"
30+ id =" menu-toggle"
31+ aria-expanded =" false"
32+ aria-controls =" main-nav"
33+ aria-label =" Open menu"
34+ >
35+ <svg class =" menu-icon menu-icon-open" width =" 18" height =" 18" viewBox =" 0 0 24 24" fill =" none" stroke =" currentColor" stroke-width =" 2" stroke-linecap =" round" aria-hidden =" true" >
36+ <path d =" M4 7h16M4 12h16M4 17h16" />
37+ </svg >
38+ <svg class =" menu-icon menu-icon-close" width =" 18" height =" 18" viewBox =" 0 0 24 24" fill =" none" stroke =" currentColor" stroke-width =" 2" stroke-linecap =" round" aria-hidden =" true" >
39+ <path d =" M6 6l12 12M18 6L6 18" />
40+ </svg >
41+ </button >
42+ <a href =" https://github.com/devalade/shipnode" target =" _blank" rel =" noopener" class =" github-link" id =" github-link" aria-label =" View ShipNode on GitHub" >
43+ <svg width =" 18" height =" 18" viewBox =" 0 0 24 24" fill =" currentColor" aria-hidden =" true" ><path d =" M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" /></svg >
44+ <span >GitHub</span >
45+ </a >
46+ </div >
3047 </div >
48+ <div class =" nav-backdrop" id =" nav-backdrop" hidden ></div >
3149</header >
3250
3351<style >
@@ -52,6 +70,7 @@ const basePath = import.meta.env.BASE_URL.endsWith("/")
5270 display: flex;
5371 align-items: center;
5472 justify-content: space-between;
73+ gap: var(--space-md);
5574 }
5675
5776 .logo {
@@ -63,10 +82,54 @@ const basePath = import.meta.env.BASE_URL.endsWith("/")
6382 color: var(--text-primary);
6483 transition: opacity var(--transition-fast), transform var(--transition-fast);
6584 letter-spacing: -0.01em;
85+ flex-shrink: 0;
6686 }
6787 .logo:hover { opacity: 0.8; }
6888 .logo:active { transform: scale(0.97); }
6989
90+ .header-actions {
91+ display: flex;
92+ align-items: center;
93+ gap: var(--space-sm);
94+ flex-shrink: 0;
95+ }
96+
97+ .menu-toggle {
98+ display: none;
99+ align-items: center;
100+ justify-content: center;
101+ width: 2.5rem;
102+ height: 2.5rem;
103+ padding: 0;
104+ border: 1px solid var(--border-strong);
105+ border-radius: var(--radius-md);
106+ background: transparent;
107+ color: var(--text-primary);
108+ cursor: pointer;
109+ transition: border-color var(--transition-fast), background var(--transition-fast), transform var(--transition-fast);
110+ }
111+ .menu-toggle:hover {
112+ border-color: var(--accent-primary);
113+ background: var(--accent-muted);
114+ }
115+ .menu-toggle:active {
116+ transform: scale(0.97);
117+ }
118+ .menu-toggle:focus-visible {
119+ outline: 2px solid var(--accent-primary);
120+ outline-offset: 2px;
121+ }
122+
123+ .menu-icon-close {
124+ display: none;
125+ }
126+ .header.is-menu-open .menu-icon-open {
127+ display: none;
128+ }
129+ .header.is-menu-open .menu-icon-close {
130+ display: block;
131+ }
132+
70133 .nav {
71134 display: flex;
72135 gap: 0.35rem;
@@ -85,11 +148,19 @@ const basePath = import.meta.env.BASE_URL.endsWith("/")
85148 border-radius: var(--radius-full);
86149 }
87150 .nav a:hover { color: var(--text-primary); background: rgba(237, 237, 223, 0.05); }
151+ .nav a:focus-visible {
152+ outline: 2px solid var(--accent-primary);
153+ outline-offset: 2px;
154+ }
88155 .nav a.active {
89156 color: var(--text-primary);
90157 background: rgba(237, 237, 223, 0.08);
91158 }
92159
160+ .nav-backdrop {
161+ display: none;
162+ }
163+
93164 .github-link {
94165 display: flex;
95166 align-items: center;
@@ -110,19 +181,124 @@ const basePath = import.meta.env.BASE_URL.endsWith("/")
110181 .github-link:active {
111182 transform: scale(0.97) translateY(0);
112183 }
184+ .github-link:focus-visible {
185+ outline: 2px solid var(--accent-primary);
186+ outline-offset: 2px;
187+ }
188+
189+ :global(body.menu-open) {
190+ overflow: hidden;
191+ }
113192
114193 @media (max-width: 768px) {
115- .nav { display: none; }
194+ .menu-toggle {
195+ display: flex;
196+ }
197+
198+ .nav {
199+ display: none;
200+ position: fixed;
201+ top: 64px;
202+ left: 0;
203+ right: 0;
204+ z-index: var(--z-modal);
205+ flex-direction: column;
206+ gap: 0;
207+ padding: var(--space-sm) var(--space-md) var(--space-md);
208+ border: none;
209+ border-bottom: 1px solid var(--border-primary);
210+ border-radius: 0;
211+ background: rgba(16, 17, 15, 0.96);
212+ backdrop-filter: blur(18px) saturate(130%);
213+ -webkit-backdrop-filter: blur(18px) saturate(130%);
214+ box-shadow: var(--shadow-md);
215+ }
216+
217+ .header.is-menu-open .nav {
218+ display: flex;
219+ }
220+
221+ .nav a {
222+ font-size: 0.9375rem;
223+ padding: 0.75rem 1rem;
224+ border-radius: var(--radius-md);
225+ }
226+
227+ .nav-backdrop {
228+ display: none;
229+ position: fixed;
230+ inset: 64px 0 0;
231+ z-index: calc(var(--z-modal) - 1);
232+ background: rgba(5, 6, 4, 0.45);
233+ border: none;
234+ padding: 0;
235+ margin: 0;
236+ cursor: pointer;
237+ }
238+
239+ .header.is-menu-open .nav-backdrop {
240+ display: block;
241+ }
242+
116243 .github-link span { display: none; }
117244 .github-link { padding: 0.5rem; border-radius: var(--radius-md); }
118245 }
119246
120247 @media (max-width: 420px) {
121248 .header-inner { padding: 0 var(--space-md); }
122249 }
250+
251+ @media (prefers-reduced-motion: reduce) {
252+ .nav,
253+ .menu-toggle,
254+ .github-link {
255+ transition: none;
256+ }
257+ }
123258</style >
124259
125260<script >
261+ const header = document.getElementById("header");
262+ const menuToggle = document.getElementById("menu-toggle");
263+ const mainNav = document.getElementById("main-nav");
264+ const navBackdrop = document.getElementById("nav-backdrop");
265+
266+ function setMenuOpen(open: boolean) {
267+ if (!header || !menuToggle || !mainNav) return;
268+ header.classList.toggle("is-menu-open", open);
269+ menuToggle.setAttribute("aria-expanded", String(open));
270+ menuToggle.setAttribute("aria-label", open ? "Close menu" : "Open menu");
271+ document.body.classList.toggle("menu-open", open);
272+ if (navBackdrop) {
273+ navBackdrop.hidden = !open;
274+ }
275+ }
276+
277+ function closeMenu(returnFocusToToggle = false) {
278+ const wasOpen = header?.classList.contains("is-menu-open") ?? false;
279+ setMenuOpen(false);
280+ if (wasOpen && returnFocusToToggle) {
281+ menuToggle?.focus();
282+ }
283+ }
284+
285+ menuToggle?.addEventListener("click", () => {
286+ const isOpen = header?.classList.contains("is-menu-open") ?? false;
287+ setMenuOpen(!isOpen);
288+ });
289+
290+ navBackdrop?.addEventListener("click", () => closeMenu(true));
291+
292+ mainNav?.querySelectorAll("a").forEach((link) => {
293+ link.addEventListener("click", () => closeMenu(false));
294+ });
295+
296+ document.addEventListener("keydown", (e) => {
297+ if (e.key === "Escape" && header?.classList.contains("is-menu-open")) {
298+ closeMenu(true);
299+ }
300+ });
301+
126302 // Active section highlighting on scroll
127303 const sections = ["how-it-works", "features", "commands", "frameworks"];
128304 const navLinks = document.querySelectorAll<HTMLAnchorElement>("#main-nav a[data-section]");
0 commit comments