update hamburger menu

This commit is contained in:
Nikita 2026-09-26 21:40:01 +05:00
parent 284c6b5c96
commit afe04e6f67
2 changed files with 464 additions and 194 deletions

View file

@ -3,25 +3,33 @@
========================= */
.header {
position: sticky;
position: fixed;
top: 0;
left: 0;
right: 0;
display: flex;
justify-content: center;
z-index: 1000;
z-index: 1001;
width: 100%;
height: 90px;
color: var(--color-text);
background: rgba(0, 0, 0, 0.85);
background: rgba(0, 0, 0, 0.82);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border-bottom: 1px solid var(--color-line);
}
.headerSpacer {
width: 100%;
height: 90px;
flex-shrink: 0;
}
/* =========================
LOGO
========================= */
@ -61,21 +69,21 @@
}
/* =========================
LANG TOGGLE
LANG TOGGLE (Header)
========================= */
.langToggle {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
height: 32px;
padding: 0 12px;
gap: 5px;
height: 34px;
padding: 0 14px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 9999px;
color: var(--color-text);
font-size: 11.5px;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.04em;
text-decoration: none;
@ -92,20 +100,12 @@
}
.langToggleDot {
width: 5px;
height: 5px;
width: 6px;
height: 6px;
border-radius: 50%;
background: #3b82f6;
}
.mobileLangRow {
display: flex;
align-items: center;
margin-top: var(--space-lg);
padding-top: var(--space-md);
border-top: 1px solid var(--color-line);
}
/* =========================
DESKTOP NAV
========================= */
@ -113,12 +113,9 @@
.desktopNav ul {
display: flex;
align-items: center;
gap: var(--space-lg);
margin: 0;
padding: 0;
list-style: none;
}
@ -129,34 +126,24 @@
.desktopNav a {
position: relative;
display: inline-block;
padding: 8px 0;
color: var(--color-muted);
font-size: var(--fs-nav);
font-weight: 500;
letter-spacing: -0.01em;
text-decoration: none;
transition: color 0.25s ease;
}
.desktopNav a::after {
content: "";
position: absolute;
left: 0;
bottom: 2px;
width: 0;
height: 1px;
background: var(--color-text);
transition: width 0.3s ease;
}
@ -169,129 +156,325 @@
}
/* =========================
HAMBURGER
HAMBURGER BUTTON
========================= */
.hamburger {
display: none;
position: relative;
width: 34px;
height: 24px;
align-items: center;
justify-content: center;
width: 42px;
height: 42px;
margin: 0;
padding: 0;
background: transparent;
border: 0;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 12px;
cursor: pointer;
transition: background 0.2s ease, border-color 0.2s ease;
-webkit-tap-highlight-color: transparent;
}
.hamburger span {
.hamburger:hover {
background: rgba(255, 255, 255, 0.08);
border-color: rgba(255, 255, 255, 0.2);
}
.hamburger.active {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.25);
}
.hamburgerBox {
position: relative;
width: 20px;
height: 14px;
}
.hamburgerLine {
position: absolute;
left: 0;
width: 100%;
height: 2px;
border-radius: 10px;
border-radius: 2px;
background: var(--color-text);
transition:
transform 0.3s ease,
opacity 0.3s ease;
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1),
opacity 0.2s ease;
}
.hamburger span:nth-child(1) {
.hamburgerLine:nth-child(1) {
top: 0;
}
.hamburger span:nth-child(2) {
top: 50%;
transform: translateY(-50%);
.hamburgerLine:nth-child(2) {
top: 6px;
}
.hamburger span:nth-child(3) {
bottom: 0;
.hamburgerLine:nth-child(3) {
top: 12px;
}
.hamburger.active span:nth-child(1) {
transform: translateY(11px) rotate(45deg);
.hamburger.active .hamburgerLine:nth-child(1) {
transform: translateY(6px) rotate(45deg);
}
.hamburger.active span:nth-child(2) {
.hamburger.active .hamburgerLine:nth-child(2) {
opacity: 0;
transform: translateX(10px);
transform: scaleX(0);
}
.hamburger.active span:nth-child(3) {
transform: translateY(-11px) rotate(-45deg);
.hamburger.active .hamburgerLine:nth-child(3) {
transform: translateY(-6px) rotate(-45deg);
}
/* =========================
MOBILE MENU
MOBILE BACKDROP
========================= */
.mobileBackdrop {
display: none;
}
@media (max-width: 768px) {
.mobileBackdrop {
display: block;
position: fixed;
inset: 0;
z-index: 998;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: opacity 0.35s ease, visibility 0.35s ease;
}
.mobileBackdrop.active {
opacity: 1;
visibility: visible;
pointer-events: auto;
}
}
/* =========================
MOBILE MENU DRAWER
========================= */
.mobileMenu {
display: none;
}
@media (max-width: 768px) {
.mobileMenu {
display: flex;
position: fixed;
top: 90px;
right: 0;
top: 0;
left: 0;
width: 100%;
height: calc(100vh - 90px);
height: 100dvh;
z-index: 999;
padding: var(--space-2xl) var(--container-padding);
background: rgba(8, 8, 8, 0.94);
backdrop-filter: blur(28px);
-webkit-backdrop-filter: blur(28px);
background: var(--color-bg);
opacity: 0;
visibility: hidden;
pointer-events: none;
transform: translateY(-10px);
transform: translateX(100%);
transition: transform 0.45s cubic-bezier(0.32, 0.72, 0, 1);
transition: opacity 0.35s cubic-bezier(0.16, 1, 0.3, 1),
transform 0.35s cubic-bezier(0.16, 1, 0.3, 1),
visibility 0.35s ease;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.mobileMenu.active {
transform: translateX(0);
opacity: 1;
visibility: visible;
pointer-events: auto;
transform: translateY(0);
}
.mobileMenu ul {
.mobileMenuInner {
display: flex;
flex-direction: column;
justify-content: space-between;
width: 100%;
min-height: 100%;
padding: calc(70px + var(--space-md)) var(--container-padding) max(var(--space-lg), env(safe-area-inset-bottom, 24px));
box-sizing: border-box;
}
gap: var(--space-md);
/* Nav items */
.mobileNavList {
display: flex;
flex-direction: column;
margin: 0;
padding: 0;
list-style: none;
}
.mobileMenu li {
.mobileNavItem {
margin: 0;
padding: 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
opacity: 0;
transform: translateY(14px);
transition: opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1),
transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}
.mobileMenu a {
display: inline-block;
color: var(--color-text);
font-size: var(--fs-display);
font-weight: 600;
line-height: 1;
letter-spacing: -0.04em;
.mobileMenu.active .mobileNavItem {
opacity: 1;
transform: translateY(0);
}
.mobileNavLink {
display: flex;
align-items: center;
gap: 16px;
padding: 20px 4px;
text-decoration: none;
transition: color 0.25s ease;
color: var(--color-text);
transition: color 0.2s ease, transform 0.2s ease;
-webkit-tap-highlight-color: transparent;
}
.mobileMenu a:hover {
.mobileNavLink:active {
opacity: 0.7;
transform: translateX(4px);
}
.mobileNavNum {
font-size: 13px;
font-weight: 600;
color: var(--color-faint);
letter-spacing: 0.05em;
}
.mobileNavText {
font-size: clamp(24px, 6vw, 32px);
font-weight: 700;
letter-spacing: -0.04em;
line-height: 1.1;
}
.mobileNavArrow {
margin-left: auto;
font-size: 22px;
color: var(--color-faint);
transition: transform 0.2s ease, color 0.2s ease;
}
.mobileNavLink:hover .mobileNavArrow,
.mobileNavLink:active .mobileNavArrow {
color: #93c5fd;
transform: translate(2px, -2px);
}
/* Extra / Action section */
.mobileExtra {
display: flex;
flex-direction: column;
gap: var(--space-md);
margin-top: var(--space-xl);
padding-top: var(--space-md);
opacity: 0;
transform: translateY(14px);
transition: opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1),
transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}
.mobileMenu.active .mobileExtra {
opacity: 1;
transform: translateY(0);
}
.mobileActionGroup {
display: flex;
flex-direction: column;
gap: 10px;
}
.mobileResumeBtn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
height: 48px;
padding: 0 20px;
background: #ffffff;
color: #000000;
border-radius: 12px;
font-size: 14px;
font-weight: 600;
text-decoration: none;
transition: transform 0.2s ease, background 0.2s ease;
-webkit-tap-highlight-color: transparent;
}
.mobileResumeBtn:active {
transform: scale(0.98);
background: #e2e8f0;
}
.mobileLangBtn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
height: 44px;
padding: 0 16px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 12px;
color: var(--color-text);
font-size: 13px;
font-weight: 600;
text-decoration: none;
transition: background 0.2s ease, border-color 0.2s ease;
-webkit-tap-highlight-color: transparent;
}
.mobileLangBtn:active {
background: rgba(37, 99, 235, 0.15);
border-color: rgba(59, 130, 246, 0.4);
color: #93c5fd;
}
.mobileSocials {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding-top: 12px;
}
.mobileSocialLink {
flex: 1;
display: inline-flex;
align-items: center;
justify-content: center;
height: 38px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
color: var(--color-muted);
font-size: 12px;
font-weight: 500;
text-decoration: none;
transition: all 0.2s ease;
-webkit-tap-highlight-color: transparent;
}
.mobileSocialLink:active {
color: #fff;
background: rgba(255, 255, 255, 0.08);
}
}
/* =========================
@ -299,7 +482,8 @@
========================= */
@media (min-width: 1600px) {
.header {
.header,
.headerSpacer {
height: 110px;
}
}
@ -309,7 +493,8 @@
========================= */
@media (max-width: 1024px) {
.header {
.header,
.headerSpacer {
height: 80px;
}
@ -323,7 +508,8 @@
========================= */
@media (max-width: 768px) {
.header {
.header,
.headerSpacer {
height: 70px;
}
@ -332,12 +518,7 @@
}
.hamburger {
display: block;
}
.mobileMenu {
top: 70px;
height: calc(100vh - 70px);
display: inline-flex;
}
}
@ -346,23 +527,16 @@
========================= */
@media (max-width: 480px) {
.header {
.header,
.headerSpacer {
height: 64px;
}
.logo {
font-size: 25px;
font-size: 24px;
}
.hamburger {
width: 29px;
height: 21px;
}
.mobileMenu {
top: 64px;
height: calc(100vh - 64px);
padding-top: var(--space-lg);
.mobileMenuInner {
padding-top: calc(64px + var(--space-sm));
}
}

View file

@ -30,10 +30,16 @@ export const Header = () => {
switchHref = `${switchHref}/`;
}
const resumeHref =
locale === "en"
? "/Nikita_Egorov_Resume_EN.pdf"
: "/Nikita_Egorov_Resume_RU.pdf";
const resumeLabel = locale === "en" ? "Resume (PDF)" : "Резюме (PDF)";
const navItems = [
{ label: dict.nav.about, href: `/${locale}/#about` },
{ label: dict.nav.projects, href: `/${locale}/projects/` },
{ label: dict.nav.contact, href: `/${locale}/#contact` },
{ num: "01", label: dict.nav.about, href: `/${locale}/#about` },
{ num: "02", label: dict.nav.projects, href: `/${locale}/projects/` },
{ num: "03", label: dict.nav.contact, href: `/${locale}/#contact` },
];
useEffect(() => {
@ -45,13 +51,20 @@ export const Header = () => {
if (window.innerWidth > 768) setOpen(false);
};
document.documentElement.classList.toggle("no-scroll", open);
if (open) {
document.documentElement.classList.add("no-scroll");
document.body.classList.add("no-scroll");
} else {
document.documentElement.classList.remove("no-scroll");
document.body.classList.remove("no-scroll");
}
window.addEventListener("keydown", onKeyDown);
window.addEventListener("resize", onResize);
return () => {
document.documentElement.classList.remove("no-scroll");
document.body.classList.remove("no-scroll");
window.removeEventListener("keydown", onKeyDown);
window.removeEventListener("resize", onResize);
};
@ -60,6 +73,7 @@ export const Header = () => {
const close = () => setOpen(false);
return (
<>
<header className={styles.header}>
<div className="cont-header">
<div className={styles.left}>
@ -79,7 +93,7 @@ export const Header = () => {
</ul>
</nav>
{/* Modern Single-Button Language Switcher */}
{/* Language Switcher */}
<Link
href={switchHref}
className={styles.langToggle}
@ -90,7 +104,7 @@ export const Header = () => {
<span>{targetLabel}</span>
</Link>
{/* Hamburger */}
{/* Hamburger Button */}
<button
className={`${styles.hamburger} ${open ? styles.active : ""}`}
type="button"
@ -98,40 +112,122 @@ export const Header = () => {
aria-expanded={open}
onClick={() => setOpen((value) => !value)}
>
<span />
<span />
<span />
<div className={styles.hamburgerBox}>
<span className={styles.hamburgerLine} />
<span className={styles.hamburgerLine} />
<span className={styles.hamburgerLine} />
</div>
</button>
</div>
</div>
</header>
{/* Spacer to preserve normal document flow with fixed header */}
<div className={styles.headerSpacer} aria-hidden="true" />
{/* Mobile Drawer Navigation */}
<div
className={`${styles.mobileBackdrop} ${open ? styles.active : ""}`}
onClick={close}
aria-hidden="true"
/>
<nav
className={`${styles.mobileMenu} ${open ? styles.active : ""}`}
aria-label="Мобильная навигация"
aria-hidden={!open}
>
<ul>
{navItems.map((item) => (
<li key={item.href}>
<Link href={item.href} onClick={close}>
{item.label}
<div className={styles.mobileMenuInner}>
<ul className={styles.mobileNavList}>
{navItems.map((item, idx) => (
<li
key={item.href}
className={styles.mobileNavItem}
style={{ transitionDelay: open ? `${0.06 + idx * 0.05}s` : "0s" }}
>
<Link
href={item.href}
onClick={close}
className={styles.mobileNavLink}
>
<span className={styles.mobileNavNum}>{item.num}</span>
<span className={styles.mobileNavText}>{item.label}</span>
<span className={styles.mobileNavArrow} aria-hidden="true">
↗
</span>
</Link>
</li>
))}
</ul>
<div className={styles.mobileLangRow}>
<div
className={styles.mobileExtra}
style={{ transitionDelay: open ? "0.22s" : "0s" }}
>
<div className={styles.mobileActionGroup}>
<a
href={resumeHref}
download
className={styles.mobileResumeBtn}
onClick={close}
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="7 10 12 15 17 10" />
<line x1="12" y1="15" x2="12" y2="3" />
</svg>
<span>{resumeLabel}</span>
</a>
<Link
href={switchHref}
className={styles.langToggle}
className={styles.mobileLangBtn}
onClick={close}
aria-label={`Switch to ${targetLocale === "en" ? "English" : "Russian"}`}
>
<span className={styles.langToggleDot} aria-hidden="true" />
<span>{targetLabel}</span>
<span>{locale === "ru" ? "English version" : "Русская версия"} ({targetLabel})</span>
</Link>
</div>
<div className={styles.mobileSocials}>
<a
href="https://t.me/autli"
target="_blank"
rel="noreferrer"
className={styles.mobileSocialLink}
>
Telegram ↗
</a>
<a
href="https://github.com/autli"
target="_blank"
rel="noreferrer"
className={styles.mobileSocialLink}
>
GitHub ↗
</a>
<a
href="mailto:ne60201@gmail.com"
className={styles.mobileSocialLink}
>
Email ↗
</a>
</div>
</div>
</div>
</nav>
</header>
</>
);
};