Hang every folder off one edge, and give the drawer a way out

Two things the drill-down got wrong, both found on a phone-width window.

The folders did not line up. The rule that drops the twisty's 30px
gutter was hung on the rows offering a drill, so only folders with
children lost it -- they sat 18px left of every folder without any, and
the column of icons came apart. Whether a folder has children is not a
reason to hang it somewhere else. The class moves to the list, which is
what the indent is a property of; icons now share one column and labels
another, at every level and on the back row too.

There was no obvious way back out of the drawer. It covers the top bar
-- it is taller than it -- so the hamburger that opened it is
underneath, and pressing the same place again did nothing at all, since
that handler only ever set the drawer open. The dimmed strip beside the
drawer was the only exit, and nothing says so. There is now a close
where the hamburger was, moved by the same rule so it lands on exactly
the same pixels, and the hamburger itself toggles rather than only
opening. Escape closes it too, for a tablet with a keyboard.

Raising the top bar over the drawer instead would have been the smaller
change and is not available: the drawer is at 950 and a full-screen
composer at 800, so a top bar above the first is also above the second.
This commit is contained in:
2026-08-31 22:04:25 -07:00
parent 20929ddc2c
commit 94639e8420
13 changed files with 90 additions and 9 deletions
+30 -2
View File
@@ -1,6 +1,6 @@
import { useEffect, useState, type ReactNode } from "react";
import { Link, useLocation } from "wouter";
import { BookOpen, Calendar, ChevronsUpDown, FolderOpen, Globe, HelpCircle, LogOut, Mail, Menu as MenuIcon, Moon, PenSquare, Plus, RefreshCw, Settings, Sun, Upload, Users } from "lucide-react";
import { BookOpen, Calendar, ChevronsUpDown, FolderOpen, Globe, HelpCircle, LogOut, Mail, Menu as MenuIcon, Moon, PenSquare, Plus, RefreshCw, Settings, Sun, Upload, Users, X } from "lucide-react";
import { useSession } from "@/store/session";
import { toggleTarget, useEffectiveTheme, useSettings } from "@/store/settings";
import { useMail } from "@/store/mail";
@@ -40,6 +40,14 @@ export function AppShell({ children }: { children: ReactNode }) {
useGlobalShortcuts({ onHelp: () => setHelpOpen(true) });
useEffect(() => setDrawer(false), [location]);
// Escape closes it too, for the tablet with a keyboard attached.
useEffect(() => {
if (!drawer) return;
const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setDrawer(false); };
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [drawer]);
// Deep link: /mail?compose=new (PWA shortcut) / mailto handler
useEffect(() => {
const params = new URLSearchParams(window.location.search);
@@ -68,7 +76,7 @@ export function AppShell({ children }: { children: ReactNode }) {
return (
<div className="app">
<header className="topbar">
<button className="icon-btn" aria-label={t("Menu")} onClick={() => (isMobile ? setDrawer(true) : update({ sidebarCollapsed: !collapsed }))}>
<button className="icon-btn" aria-label={t("Menu")} onClick={() => (isMobile ? setDrawer((d) => !d) : update({ sidebarCollapsed: !collapsed }))}>
<MenuIcon size={22} />
</button>
<Link href="/mail" className="brand">
@@ -120,6 +128,26 @@ export function AppShell({ children }: { children: ReactNode }) {
<div className={`app-body ${collapsed && !isMobile ? "collapsed" : ""}`}>
<div className={`drawer-backdrop ${drawer ? "open" : ""}`} onClick={() => setDrawer(false)} />
<aside className={`sidebar ${drawer ? "open" : ""}`}>
{/*
The way back out.
The drawer covers the top bar -- it has to, being taller than it --
so the hamburger that opened it is underneath, and pressing the
same place again did nothing. That left the dimmed strip beside the
drawer as the only exit, which is not a thing anyone is told about.
Putting a close where the hamburger was means the second press
lands on the control that undoes the first, which is where the hand
is already going. It cannot be done by raising the top bar over the
drawer instead: it would then also sit over a full-screen composer,
which is stacked lower still.
*/}
{isMobile && (
<div className="drawer-head">
<button className="icon-btn" aria-label={t("Close menu")} onClick={() => setDrawer(false)}>
<X size={22} />
</button>
</div>
)}
{/* Whatever this pane is for. Files offered Compose, which wrote mail
from the file manager and was the one thing nobody wanted there. */}
<button