364 lines
7.4 KiB
Plaintext
364 lines
7.4 KiB
Plaintext
---
|
|
import Icon from './Icon.astro';
|
|
import ThemeToggle from './ThemeToggle.astro';
|
|
import type { iconPaths } from './IconPaths';
|
|
|
|
/** Main menu items */
|
|
const textLinks: { label: string; href: string }[] = [
|
|
{ label: 'Home', href: '/' },
|
|
{ label: 'Work', href: '/work/' },
|
|
{ label: 'About', href: '/about/' },
|
|
];
|
|
|
|
/** Icon links to social media — edit these with links to your profiles! */
|
|
const iconLinks: { label: string; href: string; icon: keyof typeof iconPaths }[] = [
|
|
{ label: 'GitHub', href: 'https://github.com/alexlebens', icon: 'github-logo' },
|
|
{ label: 'LinkedIn', href: 'https://www.linkedin.com/in/alexanderlebens', icon: 'codepen-logo' },
|
|
];
|
|
---
|
|
|
|
<nav>
|
|
<div class="menu-header">
|
|
<a href="/" class="site-title">
|
|
<Icon icon="terminal-window" color="var(--accent-regular)" size="1.6em" gradient />
|
|
Alex Lebens
|
|
</a>
|
|
<menu-button>
|
|
<template>
|
|
<button class="menu-button" aria-expanded="false">
|
|
<span class="sr-only">Menu</span>
|
|
<Icon icon="list" />
|
|
</button>
|
|
</template>
|
|
</menu-button>
|
|
</div>
|
|
<noscript>
|
|
<ul class="nav-items">
|
|
{
|
|
textLinks.map(({ label, href }) => (
|
|
<li>
|
|
<a
|
|
aria-current={Astro.url.pathname === href}
|
|
class:list={[
|
|
'link',
|
|
{
|
|
active:
|
|
Astro.url.pathname === href ||
|
|
(href !== '/' && Astro.url.pathname.startsWith(href)),
|
|
},
|
|
]}
|
|
href={href}
|
|
>
|
|
{label}
|
|
</a>
|
|
</li>
|
|
))
|
|
}
|
|
</ul>
|
|
</noscript>
|
|
<noscript>
|
|
<div class="menu-footer">
|
|
<div class="socials">
|
|
{
|
|
iconLinks.map(({ href, icon, label }) => (
|
|
<a href={href} class="social">
|
|
<span class="sr-only">{label}</span>
|
|
<Icon icon={icon} />
|
|
</a>
|
|
))
|
|
}
|
|
</div>
|
|
</div>
|
|
</noscript>
|
|
<div id="menu-content" hidden>
|
|
<ul class="nav-items">
|
|
{
|
|
textLinks.map(({ label, href }) => (
|
|
<li>
|
|
<a
|
|
aria-current={Astro.url.pathname === href}
|
|
class:list={[
|
|
'link',
|
|
{
|
|
active:
|
|
Astro.url.pathname === href ||
|
|
(href !== '/' && Astro.url.pathname.startsWith(href)),
|
|
},
|
|
]}
|
|
href={href}
|
|
>
|
|
{label}
|
|
</a>
|
|
</li>
|
|
))
|
|
}
|
|
</ul>
|
|
<div class="menu-footer">
|
|
<div class="socials">
|
|
{
|
|
iconLinks.map(({ href, icon, label }) => (
|
|
<a href={href} class="social">
|
|
<span class="sr-only">{label}</span>
|
|
<Icon icon={icon} />
|
|
</a>
|
|
))
|
|
}
|
|
</div>
|
|
|
|
<div class="theme-toggle">
|
|
<ThemeToggle />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<script>
|
|
class MenuButton extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
|
|
// Inject menu toggle button when JS runs.
|
|
this.appendChild(this.querySelector('template')!.content.cloneNode(true));
|
|
const btn = this.querySelector('button')!;
|
|
|
|
// Hide menu (shown by default to support no-JS browsers).
|
|
const menu = document.getElementById('menu-content')!;
|
|
menu.hidden = true;
|
|
// Add "menu-content" class in JS to avoid covering content in non-JS browsers.
|
|
menu.classList.add('menu-content');
|
|
|
|
/** Set whether the menu is currently expanded or collapsed. */
|
|
const setExpanded = (expand: boolean) => {
|
|
btn.setAttribute('aria-expanded', expand ? 'true' : 'false');
|
|
menu.hidden = !expand;
|
|
};
|
|
|
|
// Toggle menu visibility when the menu button is clicked.
|
|
btn.addEventListener('click', () => setExpanded(menu.hidden));
|
|
|
|
// Hide menu button for large screens.
|
|
const handleViewports = (e: MediaQueryList | MediaQueryListEvent) => {
|
|
setExpanded(e.matches);
|
|
btn.hidden = e.matches;
|
|
};
|
|
const mediaQueries = window.matchMedia('(min-width: 50em)');
|
|
handleViewports(mediaQueries);
|
|
mediaQueries.addEventListener('change', handleViewports);
|
|
}
|
|
}
|
|
customElements.define('menu-button', MenuButton);
|
|
</script>
|
|
|
|
<style>
|
|
nav {
|
|
z-index: 9999;
|
|
position: relative;
|
|
font-family: var(--font-brand);
|
|
font-weight: 500;
|
|
margin-bottom: 3.5rem;
|
|
}
|
|
|
|
.menu-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: 0.5rem;
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.site-title {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
align-items: center;
|
|
line-height: 1.1;
|
|
color: var(--gray-0);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.menu-button {
|
|
position: relative;
|
|
display: flex;
|
|
border: 0;
|
|
border-radius: 999rem;
|
|
padding: 0.5rem;
|
|
font-size: 1.5rem;
|
|
color: var(--gray-300);
|
|
background: radial-gradient(var(--gray-900), var(--gray-800) 150%);
|
|
box-shadow: var(--shadow-md);
|
|
}
|
|
|
|
.menu-button[aria-expanded='true'] {
|
|
color: var(--gray-0);
|
|
background: linear-gradient(180deg, var(--gray-600), transparent),
|
|
radial-gradient(var(--gray-900), var(--gray-800) 150%);
|
|
}
|
|
|
|
.menu-button[hidden] {
|
|
display: none;
|
|
}
|
|
|
|
.menu-button::before {
|
|
position: absolute;
|
|
inset: -1px;
|
|
content: '';
|
|
background: var(--gradient-stroke);
|
|
border-radius: 999rem;
|
|
z-index: -1;
|
|
}
|
|
|
|
.menu-content {
|
|
position: absolute;
|
|
left: 0;
|
|
right: 0;
|
|
}
|
|
|
|
.nav-items {
|
|
margin: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
font-size: var(--text-md);
|
|
line-height: 1.2;
|
|
list-style: none;
|
|
padding: 2rem;
|
|
background-color: var(--gray-999);
|
|
border-bottom: 1px solid var(--gray-800);
|
|
}
|
|
|
|
.link {
|
|
display: inline-block;
|
|
color: var(--gray-300);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.link.active {
|
|
color: var(--gray-0);
|
|
}
|
|
|
|
.menu-footer {
|
|
--icon-size: var(--text-xl);
|
|
--icon-padding: 0.5rem;
|
|
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: 0.75rem;
|
|
padding: 1.5rem 2rem 1.5rem 1.5rem;
|
|
background-color: var(--gray-999);
|
|
border-radius: 0 0 0.75rem 0.75rem;
|
|
box-shadow: var(--shadow-lg);
|
|
}
|
|
|
|
.socials {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.625rem;
|
|
font-size: var(--icon-size);
|
|
}
|
|
|
|
.social {
|
|
display: flex;
|
|
padding: var(--icon-padding);
|
|
text-decoration: none;
|
|
color: var(--accent-dark);
|
|
transition: color var(--theme-transition);
|
|
}
|
|
|
|
.social:hover,
|
|
.social:focus {
|
|
color: var(--accent-text-over);
|
|
}
|
|
|
|
.theme-toggle {
|
|
display: flex;
|
|
align-items: center;
|
|
height: calc(var(--icon-size) + 2 * var(--icon-padding));
|
|
}
|
|
|
|
@media (min-width: 50em) {
|
|
nav {
|
|
display: grid;
|
|
grid-template-columns: 1fr auto 1fr;
|
|
align-items: center;
|
|
padding: 2.5rem 5rem;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.menu-header {
|
|
padding: 0;
|
|
}
|
|
|
|
.site-title {
|
|
font-size: var(--text-lg);
|
|
}
|
|
|
|
.menu-content {
|
|
display: contents;
|
|
}
|
|
|
|
.nav-items {
|
|
position: relative;
|
|
flex-direction: row;
|
|
font-size: var(--text-sm);
|
|
border-radius: 999rem;
|
|
border: 0;
|
|
padding: 0.5rem 0.5625rem;
|
|
background: radial-gradient(var(--gray-900), var(--gray-800) 150%);
|
|
box-shadow: var(--shadow-md);
|
|
}
|
|
|
|
.nav-items::before {
|
|
position: absolute;
|
|
inset: -1px;
|
|
content: '';
|
|
background: var(--gradient-stroke);
|
|
border-radius: 999rem;
|
|
z-index: -1;
|
|
}
|
|
|
|
.link {
|
|
padding: 0.5rem 1rem;
|
|
border-radius: 999rem;
|
|
transition:
|
|
color var(--theme-transition),
|
|
background-color var(--theme-transition);
|
|
}
|
|
|
|
.link:hover,
|
|
.link:focus {
|
|
color: var(--gray-100);
|
|
background-color: var(--accent-subtle-overlay);
|
|
}
|
|
|
|
.link.active {
|
|
color: var(--accent-text-over);
|
|
background-color: var(--accent-regular);
|
|
}
|
|
|
|
.menu-footer {
|
|
--icon-padding: 0.375rem;
|
|
|
|
justify-self: flex-end;
|
|
align-items: center;
|
|
padding: 0;
|
|
background-color: transparent;
|
|
box-shadow: none;
|
|
}
|
|
|
|
.socials {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
@media (min-width: 60em) {
|
|
.socials {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 0;
|
|
}
|
|
}
|
|
@media (forced-colors: active) {
|
|
.link.active {
|
|
color: SelectedItem;
|
|
}
|
|
}
|
|
</style>
|