feat: slide sidebar when expanding and folding
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" }));
|
||||
|
||||
Reference in New Issue
Block a user