feat(sidebar): drag it wider, fold it away, and have it stay that way

The sidebar was 268px and always there. On a wide monitor that wastes the
room; on a laptop it takes room the list needs.

The width drags from a handle on the border — 7px of target over a 1px
line, because a 1px target is a target you miss — and the whole column
folds away from a button in the topbar. Both are remembered.

In localStorage rather than on the account: this is a property of the
screen you are sitting at, not of you. The same person on a laptop and a
wide monitor wants different answers, and syncing it would make one of
them wrong every time they switch.

The handle is also a slider for the keyboard. Arrow keys move it, Home
puts it back, and so does a double-click — a drag handle with no keyboard
equivalent is a setting some people simply cannot reach.

Folding is desktop-only, and the media query says so rather than leaving
it to chance: below the breakpoint the sidebar is a drawer that opens over
the page, and a collapsed drawer would open to nothing. Clamped to
200–480px in the store, so a stored value from an older build cannot make
it unusable either.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kristian Wind
2026-08-17 16:09:16 +02:00
committed by Kristian Wind
co-authored by Claude Opus 5
parent c4e9e1519b
commit 8c748c2a88
3 changed files with 205 additions and 3 deletions
+64 -2
View File
@@ -1,6 +1,6 @@
<script>
import '../app.css';
import { app, theme } from '$lib/stores.svelte.js';
import { app, theme, sidebar } from '$lib/stores.svelte.js';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import Sidebar from '$lib/components/Sidebar.svelte';
@@ -66,11 +66,20 @@
{:else if !app.user}
<SignIn />
{:else}
<div class="shell" class:sidebar-open={sidebarOpen}>
<div
class="shell"
class:sidebar-open={sidebarOpen}
class:sidebar-collapsed={sidebar.collapsed}
style="--sidebar-width: {sidebar.width}px"
>
<Sidebar onnavigate={() => (sidebarOpen = false)} />
<main>
<header class="topbar">
<!-- Two controls, not one: on a phone the sidebar is a drawer that
opens over the page, on a desktop it is a column that folds away.
Same words, different actions, so each is shown only where it
applies. -->
<button
class="menu"
onclick={() => (sidebarOpen = !sidebarOpen)}
@@ -82,6 +91,19 @@
</svg>
</button>
<button
class="fold"
onclick={() => sidebar.toggle()}
aria-label={sidebar.collapsed ? 'Vis sidebjælken' : 'Skjul sidebjælken'}
aria-pressed={sidebar.collapsed}
title={sidebar.collapsed ? 'Vis sidebjælken' : 'Skjul sidebjælken'}
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<rect x="3" y="4" width="18" height="16" rx="2" />
<path d="M9 4v16" />
</svg>
</button>
<button class="search" onclick={() => (paletteOpen = true)}>
<span>Søg</span>
<kbd>⌘K</kbd>
@@ -173,7 +195,32 @@
color: var(--ink-muted);
}
.fold {
display: grid;
width: 32px;
height: 32px;
place-items: center;
border-radius: var(--radius);
color: var(--ink-faint);
transition: color var(--fast) var(--ease);
}
.fold:hover {
color: var(--ink);
}
/* Folded away rather than merely narrow: width 0 with the border gone, so
nothing of it is left to catch the eye. */
.shell.sidebar-collapsed :global(.sidebar) {
width: 0;
padding-left: 0;
padding-right: 0;
border-right: 0;
overflow: hidden;
}
.menu svg,
.fold svg,
.theme svg {
width: 18px;
height: 18px;
@@ -277,6 +324,11 @@
display: grid;
}
/* The drawer is the whole answer here; folding a drawer means nothing. */
.fold {
display: none;
}
.scrim {
display: block;
}
@@ -292,5 +344,15 @@
.shell.sidebar-open :global(.sidebar) {
transform: translateX(0);
}
/* A collapsed desktop sidebar must not follow you to a phone, where the
drawer would then open to nothing. */
.shell.sidebar-collapsed :global(.sidebar) {
width: var(--sidebar-width);
padding-left: max(var(--s3), env(safe-area-inset-left));
padding-right: var(--s3);
border-right: 1px solid var(--line);
overflow-y: auto;
}
}
</style>