feat: slide sidebar when expanding and folding

This commit is contained in:
2026-10-01 11:48:50 +09:00
parent a07d70b107
commit 3ee1698957
5 changed files with 120 additions and 22 deletions
@@ -18,11 +18,9 @@
<div class="sidebar-frame" class:folded>
<Bevel as="div" class="sidebar-frame__bevel" top={false} bottom={false} left={false} fill>
<aside class="sidebar-frame__surface" aria-label="Sidebar">
{#if !folded}
<div class="sidebar-frame-content">
{@render children()}
</div>
{/if}
<div class="sidebar-frame-content" inert={folded} aria-hidden={folded}>
{@render children()}
</div>
<div class="sidebar-control-row">
<button
@@ -4,11 +4,15 @@
.sidebar-frame {
--sidebar-item-hover: oklch(96% 0 0);
--sidebar-item-active: oklch(92.5% 0 0);
--sidebar-width: clamp(220px, 20vw, 280px);
--sidebar-motion-duration: 220ms;
--sidebar-motion-easing: cubic-bezier(0.2, 0, 0, 1);
display: grid;
grid-column: 1;
grid-row: 1 / 3;
width: clamp(220px, 20vw, 280px);
width: var(--sidebar-width);
transition: width var(--sidebar-motion-duration) var(--sidebar-motion-easing);
min-width: 0;
min-height: 0;
overflow: hidden;
@@ -34,16 +38,26 @@
}
}
.sidebar-frame.folded {
width: max-content;
width: calc(32px + var(--space-2) + var(--space-2) + 1px);
overflow: hidden;
}
.sidebar-frame-content {
flex: 1 1 auto;
width: calc(var(--sidebar-width) - 1px);
transform: translateX(0);
visibility: visible;
transition: transform var(--sidebar-motion-duration) var(--sidebar-motion-easing),
visibility 0s;
min-width: 0;
min-height: 0;
overflow-y: auto;
padding-inline: var(--space-3);
}
.sidebar-frame.folded .sidebar-frame-content {
transform: translateX(-100%);
visibility: hidden;
transition-delay: 0s, var(--sidebar-motion-duration);
}
.sidebar-frame-content,
.global-sidebar,
.workspace-sidebar,
@@ -104,12 +118,7 @@
}
.sidebar-control-row {
margin-top: auto;
padding: var(--space-4) var(--space-3) 0;
}
.sidebar-frame.folded .sidebar-control-row {
justify-content: center;
padding-inline: var(--space-2);
padding-top: 0;
padding: var(--space-4) var(--space-2) 0;
}
.workspace-label {
flex: 1 1 auto;
@@ -639,17 +648,39 @@
@media (max-width: 760px) {
.sidebar-frame {
--sidebar-width: 100%;
z-index: 20;
grid-column: 1;
grid-row: 2;
width: 100%;
height: 100%;
background: var(--bg);
transform: translateX(0);
visibility: visible;
transition: transform var(--sidebar-motion-duration) var(--sidebar-motion-easing),
visibility 0s;
}
.sidebar-frame.folded {
display: none;
width: 100%;
transform: translateX(-100%);
visibility: hidden;
pointer-events: none;
transition-delay: 0s, var(--sidebar-motion-duration);
}
.sidebar-frame .sidebar-frame-content,
.sidebar-frame.folded .sidebar-frame-content {
transform: none;
visibility: inherit;
transition: none;
}
.sidebar-control-row {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.sidebar-frame {
--sidebar-motion-duration: 0ms;
}
}
}
+3 -5
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import { page } from '$app/state';
import { setContext } from 'svelte';
import { MediaQuery } from 'svelte/reactivity';
import WorkspaceAlerts from '$lib/workspace/alerts/WorkspaceAlerts.svelte';
import Bevel from '$lib/workspace/ui/Bevel.svelte';
import {
@@ -36,6 +37,7 @@
}
let sidebarFolded = $state(loadSidebarFolded());
const mobileLayout = new MediaQuery('(max-width: 760px)');
$effect(() => {
const folded = sidebarFolded;
@@ -102,7 +104,7 @@
</nav>
</header>
</Bevel>
<main class="app-shell__main">
<main class="app-shell__main" inert={mobileLayout.current && !sidebarFolded}>
{@render children()}
</main>
</div>
@@ -230,9 +232,5 @@
overflow-y: auto;
padding: var(--space-4);
}
.app-shell.sidebar-open .app-shell__main {
display: none;
}
}
</style>
@@ -26,10 +26,17 @@ afterEach(() => {
test("sidebar starts unfolded and persists both desktop and mobile toggles", async () => {
const view = render(Layout, { props });
expect(screen.getByRole("button", { name: "Fold sidebar" }).getAttribute("aria-expanded")).toBe("true");
const content = view.container.querySelector<HTMLElement>(".sidebar-frame-content")!;
expect(content.inert).toBe(false);
await fireEvent.click(screen.getByRole("button", { name: "Fold sidebar" }));
expect(view.container.querySelector(".sidebar-frame-content")).toBe(content);
expect(content.inert).toBe(true);
expect(content.getAttribute("aria-hidden")).toBe("true");
expect(localStorage.getItem(storageKey)).toBe("true");
expect(view.container.querySelector(".app-shell")?.classList.contains("sidebar-open")).toBe(false);
await fireEvent.click(screen.getByRole("button", { name: "Show sidebar" }));
expect(content.inert).toBe(false);
expect(content.getAttribute("aria-hidden")).toBe("false");
expect(localStorage.getItem(storageKey)).toBe("false");
expect(screen.getByRole("button", { name: "Fold sidebar" })).not.toBeNull();
await fireEvent.click(screen.getByRole("button", { name: "Hide sidebar" }));