feat: add swipeable sidebar drawer for anime details on mobile

This commit is contained in:
2026-06-25 21:47:21 +02:00
parent e9917730b8
commit adcae02c18
2 changed files with 129 additions and 2 deletions

View File

@@ -90,5 +90,115 @@ const initThemesDialog = (): void => {
});
};
const initAnimeSidebarDrawer = (): void => {
onReady(() => {
const drawer = document.querySelector<HTMLElement>("[data-anime-sidebar-drawer]");
const openButton = document.querySelector<HTMLButtonElement>("[data-anime-sidebar-open]");
const closeButton = document.querySelector<HTMLButtonElement>("[data-anime-sidebar-close]");
const backdrop = document.querySelector<HTMLButtonElement>("[data-anime-sidebar-backdrop]");
if (!drawer || !openButton || !closeButton || !backdrop) {
return;
}
let startX = 0;
let startY = 0;
let activeTouchId: number | null = null;
let isOpen = false;
const syncState = (): void => {
drawer.classList.toggle("hidden", !isOpen);
drawer.classList.toggle("translate-x-0", isOpen);
drawer.classList.toggle("translate-x-full", !isOpen);
backdrop.classList.toggle("hidden", !isOpen);
document.body.classList.toggle("overflow-hidden", isOpen);
openButton.setAttribute("aria-expanded", String(isOpen));
};
const open = (): void => {
isOpen = true;
syncState();
};
const close = (): void => {
isOpen = false;
syncState();
};
openButton.addEventListener("click", open);
closeButton.addEventListener("click", close);
backdrop.addEventListener("click", close);
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && isOpen) {
close();
}
});
const getTouch = (event: TouchEvent): Touch | null => {
if (activeTouchId === null) {
return event.touches[0] ?? event.changedTouches[0] ?? null;
}
return (
Array.from(event.touches).find((touch) => touch.identifier === activeTouchId) ??
Array.from(event.changedTouches).find((touch) => touch.identifier === activeTouchId) ??
null
);
};
document.addEventListener(
"touchstart",
(event) => {
if (window.innerWidth >= 1024) {
return;
}
const touch = event.touches[0];
if (!touch) {
return;
}
if (!isOpen && touch.clientX < window.innerWidth - 28) {
return;
}
activeTouchId = touch.identifier;
startX = touch.clientX;
startY = touch.clientY;
},
{ passive: true },
);
document.addEventListener(
"touchend",
(event) => {
if (window.innerWidth >= 1024 || activeTouchId === null) {
return;
}
const touch = getTouch(event);
activeTouchId = null;
if (!touch) {
return;
}
const deltaX = touch.clientX - startX;
const deltaY = Math.abs(touch.clientY - startY);
if (deltaY > 60) {
return;
}
if (!isOpen && startX >= window.innerWidth - 28 && deltaX < -48) {
open();
return;
}
if (isOpen && deltaX > 48) {
close();
}
},
{ passive: true },
);
syncState();
});
};
initSynopsisToggle();
initThemesDialog();
initAnimeSidebarDrawer();

View File

@@ -46,7 +46,22 @@
<div class="grow lg:max-w-4xl">
{{template "anime_synopsis" dict "Anime" $anime}}
</div>
<aside class="fixed right-0 top-0 hidden h-screen w-80 shrink-0 flex-col overflow-y-auto border-l border-border-light bg-background-sidebar p-8 lg:flex">
<button type="button" data-anime-sidebar-open class="fixed right-0 top-1/2 z-90 flex h-14 -translate-y-1/2 items-center gap-2 border border-r-0 border-border-light bg-background-sidebar px-3 text-sm text-foreground-muted shadow-card transition-colors hover:text-foreground lg:hidden" aria-controls="anime-sidebar-drawer" aria-expanded="false">
<svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M9 18l6-6-6-6" />
</svg>
<span class="sr-only">Open anime details</span>
</button>
<div data-anime-sidebar-drawer class="fixed inset-x-0 top-16 bottom-0 z-100 hidden w-full translate-x-full flex-col bg-background-sidebar shadow-card transition-transform duration-300 lg:static lg:z-auto lg:flex lg:h-screen lg:w-80 lg:translate-x-0 lg:overflow-y-auto lg:border-l lg:border-border-light lg:p-8">
<aside class="flex h-full flex-col overflow-y-auto p-8 lg:h-auto lg:p-0">
<div class="mb-6 flex items-center justify-between lg:hidden">
<h3 class="text-base font-normal text-foreground">Details</h3>
<button type="button" data-anime-sidebar-close class="inline-flex size-9 items-center justify-center text-foreground-muted transition-colors hover:text-foreground" aria-label="Close anime details">
<svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M18 6 6 18M6 6l12 12" />
</svg>
</button>
</div>
<div class="flex flex-col gap-8">
<section>
<h3 class="mb-6 text-base font-normal text-foreground">Information</h3>
@@ -178,7 +193,9 @@
</div>
</section>
</div>
</aside>
</aside>
</div>
<button type="button" data-anime-sidebar-backdrop class="fixed inset-0 z-90 hidden bg-black/30 lg:hidden" aria-label="Close anime details"></button>
</div>
</div>
</div>