aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-05-19 00:54:11 +0000
committerFuwn <[email protected]>2026-05-19 00:54:11 +0000
commit64535ee9dce5f1a7b03a30a6f58282ce24b0cda7 (patch)
tree0252fda01866a50f7ad87e86bcb9744689a7b2b1
parentfix(a11y): bump touch targets to 44px under pointer:coarse (diff)
downloaddue.moe-64535ee9dce5f1a7b03a30a6f58282ce24b0cda7.tar.xz
due.moe-64535ee9dce5f1a7b03a30a6f58282ce24b0cda7.zip
feat(nav): float header as a corner hamburger under 800px
Below 800px the inline header overflows the viewport. Strips card chrome from .header itself and floats it position:fixed at top:1.25rem/right :1.25rem so it does not consume a horizontal band. The 44x44 toggle button carries the desktop banner's exact card recipe (--base0011 glass, shadow-card-emphasized + 5px --base02 ring, blur, 8px radius), and the open panel mirrors it as a separately-positioned card below. Menu closes on route navigation and Escape. Header stays visible while the menu is open so a scroll-driven hide does not chop the open sheet mid-interaction. Profile-avatar anchor and the bullet separator are hidden in the mobile menu (avatar is redundant alongside the Profile dropdown; separator reads as line noise vertically).
-rw-r--r--src/routes/+layout.svelte132
1 files changed, 130 insertions, 2 deletions
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index 0cfde36d..a7a03066 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -54,6 +54,7 @@ let previousScrollPosition = 0;
let notificationInterval: ReturnType<typeof setInterval> | undefined =
undefined;
let lenis: Lenis | undefined = undefined;
+let isMenuOpen = false;
addMessages("en", english as unknown as LocaleDictionary);
addMessages("ja", japanese as unknown as LocaleDictionary);
@@ -87,6 +88,8 @@ $: way = data.url.includes("/user")
? 200
: -200;
+$: if ($navigating) isMenuOpen = false;
+
const handleScroll = () => {
const currentScrollPosition = window.scrollY;
@@ -214,6 +217,8 @@ $: {
<HeadTitle />
+<svelte:window on:keydown={(e) => { if (e.key === 'Escape' && isMenuOpen) isMenuOpen = false; }} />
+
<CommandPalette
items={[
...defaultActions,
@@ -239,8 +244,25 @@ $: {
<Announcement />
<div class="container">
- <div class="card card-centered card-glass header" class:header-hidden={!isHeaderVisible}>
- <div>
+ <div
+ class="card card-centered card-glass header"
+ class:header-hidden={!isHeaderVisible && !isMenuOpen}
+ class:menu-open={isMenuOpen}
+ >
+ <button
+ type="button"
+ class="menu-toggle"
+ aria-label="Menu"
+ aria-expanded={isMenuOpen}
+ aria-controls="primary-nav"
+ onclick={() => (isMenuOpen = !isMenuOpen)}
+ >
+ <span class="menu-bar"></span>
+ <span class="menu-bar"></span>
+ <span class="menu-bar"></span>
+ </button>
+
+ <div class="nav-items" id="primary-nav">
<a href={root('/')} class="header-item">{$locale().navigation.home}</a><a
href={root('/completed')}
class="header-item"
@@ -425,4 +447,110 @@ $: {
.separator {
color: var(--base04);
}
+
+ .menu-toggle {
+ display: none;
+ }
+
+ @media (max-width: 800px) {
+ .header {
+ position: fixed;
+ top: 1.25rem;
+ right: 1.25rem;
+ left: auto;
+ width: auto;
+ margin: 0;
+ padding: 0;
+ background: transparent;
+ box-shadow: none;
+ backdrop-filter: none;
+ -webkit-backdrop-filter: none;
+ z-index: 10;
+ }
+
+ .header.header-hidden {
+ transform: none;
+ }
+
+ .menu-toggle {
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ gap: 5px;
+ width: 44px;
+ height: 44px;
+ padding: 0;
+ border: none;
+ border-radius: 8px;
+ background: var(--base0011);
+ box-shadow:
+ var(--shadow-card-emphasized),
+ 0 0 0 5px var(--base02);
+ backdrop-filter: blur(4px);
+ -webkit-backdrop-filter: blur(4px);
+ cursor: pointer;
+ }
+
+ .menu-toggle:hover {
+ background: var(--base0011);
+ }
+
+ .menu-bar {
+ display: block;
+ width: 20px;
+ height: 2px;
+ background: var(--base06);
+ border-radius: 2px;
+ transition:
+ transform var(--duration-fast) var(--ease-in-out-quart),
+ opacity var(--duration-fast) var(--ease-in-out-quart);
+ }
+
+ .menu-toggle[aria-expanded='true'] .menu-bar:nth-child(1) {
+ transform: translateY(7px) rotate(45deg);
+ }
+
+ .menu-toggle[aria-expanded='true'] .menu-bar:nth-child(2) {
+ opacity: 0;
+ }
+
+ .menu-toggle[aria-expanded='true'] .menu-bar:nth-child(3) {
+ transform: translateY(-7px) rotate(-45deg);
+ }
+
+ .nav-items {
+ display: none;
+ }
+
+ .header.menu-open .nav-items {
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ position: absolute;
+ top: calc(100% + 0.75rem);
+ right: 0;
+ min-width: 200px;
+ padding: 0.5rem;
+ border-radius: 8px;
+ background: var(--base0011);
+ box-shadow:
+ var(--shadow-card-emphasized),
+ 0 0 0 5px var(--base02);
+ backdrop-filter: blur(4px);
+ -webkit-backdrop-filter: blur(4px);
+ }
+
+ .header.menu-open .nav-items :global(.header-item) {
+ display: block;
+ margin: 0;
+ padding: 0.6rem 0.75rem;
+ }
+
+ .header.menu-open .nav-items :global(.separator),
+ .header.menu-open .nav-items :global(.header-item:has(.avatar)) {
+ display: none;
+ }
+ }
</style>