Skip to content

Commit efecbf2

Browse files
ymansurozerclaude
andcommitted
feat: collapse the file tree into a drawer on narrow screens
Below the 1100px breakpoint the file tree stacked on top of the diff and stole vertical space from the core review surface. It now becomes an off-canvas left drawer, hidden by default so the diff fills the viewport, sliding in over a dimmed backdrop when opened via the new header hamburger or the new ⇧B shortcut. Selecting a file (or Esc / backdrop-click) closes it. Desktop (>1100px) is untouched: the resizable side-by-side tree stays, and ⇧B is an inert no-op there since the drawer styles are media-gated. The hamburger is also suppressed in single-file / file-mode, which have no tree. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 480c85d commit efecbf2

5 files changed

Lines changed: 101 additions & 17 deletions

File tree

src/ui/index.html

Lines changed: 80 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,34 @@
271271
border-radius: var(--radius-sm)
272272
}
273273

274+
/* Hamburger that opens the off-canvas file drawer — the topbar's only tree affordance once
275+
the tree goes off-canvas. Hidden by default; revealed only under the ≤1100px media query
276+
(and re-hidden in single-file / file-mode, which have no tree). */
277+
.nav-toggle {
278+
display: none;
279+
align-items: center;
280+
justify-content: center;
281+
width: 26px;
282+
height: 26px;
283+
padding: 0;
284+
margin-left: -4px;
285+
border: 0;
286+
border-radius: var(--radius-sm);
287+
background: transparent;
288+
color: var(--muted);
289+
cursor: pointer
290+
}
291+
292+
.nav-toggle:hover {
293+
background: var(--surface-raised);
294+
color: var(--ink)
295+
}
296+
297+
/* Scrim behind the open file drawer; CSS-gated to narrow widths (see media query). */
298+
.tree-backdrop {
299+
display: none
300+
}
301+
274302
.actions {
275303
justify-self: end;
276304
display: flex;
@@ -427,6 +455,12 @@
427455
display: inline-flex
428456
}
429457

458+
/* No tree exists in these modes (tree is display:none below), so suppress the drawer toggle. */
459+
body.single .nav-toggle,
460+
body.file-mode .nav-toggle {
461+
display: none !important
462+
}
463+
430464
.right {
431465
border-left: 1px solid var(--line)
432466
}
@@ -2482,35 +2516,61 @@
24822516
margin-left: 4px
24832517
}
24842518

2519+
/* Narrow widths: the file tree leaves the flow and becomes an off-canvas left drawer, so the
2520+
app stays a fixed-height single column with the diff filling it (the core surface owns the
2521+
screen) — no more tree-on-top page-scroll stacking. The tree is hidden by default and slides
2522+
in over a dimmed backdrop when opened (hamburger / ⇧B); selecting a file auto-closes it. */
24852523
@media(max-width:1100px) {
2486-
body {
2487-
overflow: auto
2524+
.main {
2525+
grid-template-columns: minmax(0, 1fr)
24882526
}
24892527

2490-
.app {
2491-
height: auto;
2492-
min-height: 100%
2528+
.resizer {
2529+
display: none
24932530
}
24942531

2495-
.main {
2496-
grid-template-columns: 1fr
2532+
.nav-toggle {
2533+
display: inline-flex
24972534
}
24982535

2499-
.resizer {
2500-
display: none
2536+
.tree {
2537+
position: fixed;
2538+
top: 48px;
2539+
left: 0;
2540+
bottom: 0;
2541+
width: min(86vw, 300px);
2542+
z-index: 30;
2543+
border-right: 1px solid var(--line);
2544+
box-shadow: 8px 0 24px rgba(0, 0, 0, .35);
2545+
transform: translateX(-100%);
2546+
transition: transform .18s ease
2547+
}
2548+
2549+
.tree.drawer-open {
2550+
transform: translateX(0)
25012551
}
25022552

2503-
.tree,
2504-
.right {
2505-
border: 0;
2506-
border-bottom: 1px solid var(--line)
2553+
.tree-backdrop.open {
2554+
display: block;
2555+
position: fixed;
2556+
inset: 48px 0 0 0;
2557+
z-index: 29;
2558+
background: rgba(0, 0, 0, .5)
25072559
}
25082560

2561+
/* Float the comment surfaces over the diff rather than in-flow — an in-flow element would
2562+
add a row to the now fixed-height single column and clip the diff. */
25092563
.action-pop,
25102564
.composer {
2511-
position: static;
2565+
position: fixed;
2566+
left: 12px;
2567+
right: 12px;
2568+
bottom: 12px;
2569+
top: auto;
25122570
width: auto;
2513-
margin: 12px
2571+
max-width: none;
2572+
margin: 0;
2573+
z-index: 40
25142574
}
25152575
}
25162576

@@ -2538,7 +2598,8 @@
25382598
<body>
25392599
<div class="app" x-data>
25402600
<header class="top">
2541-
<div class="brand"><svg class="brand-logo" aria-hidden="true" viewBox="4 -5 120 120" fill="none" xmlns="http://www.w3.org/2000/svg"><g filter="url(#hb_f0)"><circle cx="54.0259" cy="55.9741" r="44.0259" fill="#EB5103"/></g><g filter="url(#hb_f1)"><circle cx="72.6284" cy="55.9741" r="44.0259" fill="#EFCA44"/></g><g filter="url(#hb_f2)"><circle cx="64.5923" cy="54.0259" r="44.0259" fill="#FAF6EA"/></g><defs><filter id="hb_f0" x="0.699" y="2.647" width="106.654" height="106.654" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"><feFlood flood-opacity="0" result="bg"/><feBlend mode="normal" in="SourceGraphic" in2="bg" result="shape"/><feGaussianBlur stdDeviation="4.6505"/></filter><filter id="hb_f1" x="19.301" y="2.647" width="106.654" height="106.654" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"><feFlood flood-opacity="0" result="bg"/><feBlend mode="normal" in="SourceGraphic" in2="bg" result="shape"/><feGaussianBlur stdDeviation="4.6505"/></filter><filter id="hb_f2" x="11.265" y="0.699" width="106.654" height="106.654" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"><feFlood flood-opacity="0" result="bg"/><feBlend mode="normal" in="SourceGraphic" in2="bg" result="shape"/><feGaussianBlur stdDeviation="4.6505"/></filter></defs></svg><span>Galley</span></div>
2601+
<div class="brand"><button class="nav-toggle" data-tip="Files (⇧B)" aria-label="Toggle file tree"
2602+
@click="$store.g.treeDrawerOpen = !$store.g.treeDrawerOpen"><svg class="ic" viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M2 4h12M2 8h12M2 12h12"/></svg></button><svg class="brand-logo" aria-hidden="true" viewBox="4 -5 120 120" fill="none" xmlns="http://www.w3.org/2000/svg"><g filter="url(#hb_f0)"><circle cx="54.0259" cy="55.9741" r="44.0259" fill="#EB5103"/></g><g filter="url(#hb_f1)"><circle cx="72.6284" cy="55.9741" r="44.0259" fill="#EFCA44"/></g><g filter="url(#hb_f2)"><circle cx="64.5923" cy="54.0259" r="44.0259" fill="#FAF6EA"/></g><defs><filter id="hb_f0" x="0.699" y="2.647" width="106.654" height="106.654" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"><feFlood flood-opacity="0" result="bg"/><feBlend mode="normal" in="SourceGraphic" in2="bg" result="shape"/><feGaussianBlur stdDeviation="4.6505"/></filter><filter id="hb_f1" x="19.301" y="2.647" width="106.654" height="106.654" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"><feFlood flood-opacity="0" result="bg"/><feBlend mode="normal" in="SourceGraphic" in2="bg" result="shape"/><feGaussianBlur stdDeviation="4.6505"/></filter><filter id="hb_f2" x="11.265" y="0.699" width="106.654" height="106.654" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"><feFlood flood-opacity="0" result="bg"/><feBlend mode="normal" in="SourceGraphic" in2="bg" result="shape"/><feGaussianBlur stdDeviation="4.6505"/></filter></defs></svg><span>Galley</span></div>
25422603
<div class="toggles">
25432604
<div class="toggle" x-show="$store.g.isMarkdownFile()" data-tip="Rendered / source (m)"><button :class="{active: $store.g.fileView==='rendered'}"
25442605
@click="$store.g.setFileView('rendered')">Rendered</button><button
@@ -2555,7 +2616,7 @@
25552616
<div class="top-progress" id="progressStrip" style="display:none"><i id="progressFill"></i></div>
25562617
</header>
25572618
<main class="main">
2558-
<aside class="tree">
2619+
<aside class="tree" :class="{'drawer-open': $store.g.treeDrawerOpen}">
25592620
<div class="tree-tabs" x-show="$store.g.hasGuide && $store.g.hasGuide()" x-cloak>
25602621
<!-- The w hint sits on the INACTIVE tab — the one pressing w switches to. -->
25612622
<button :class="{active: $store.g.sidebarTab==='tree'}" @click="$store.g.sidebarTab='tree'">Tree <kbd x-show="$store.g.sidebarTab!=='tree'">w</kbd></button>
@@ -2605,6 +2666,8 @@
26052666
</div>
26062667
<button class="tree-settings" @click="$store.g.openSettings()"><svg class="ic"><use href="#gly-settings"></use></svg><span>Settings</span><kbd>⇧,</kbd></button>
26072668
</aside>
2669+
<!-- Narrow-width only (CSS-gated): dims the diff behind the open file drawer; tap to close. -->
2670+
<div class="tree-backdrop" :class="{open: $store.g.treeDrawerOpen}" @click="$store.g.treeDrawerOpen=false"></div>
26082671
<div class="resizer" data-resize="left"></div>
26092672
<section class="center">
26102673
<div class="guidebar" x-show="$store.g.showGuideBar && $store.g.showGuideBar()" x-cloak>

src/ui/keys.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,11 @@ function escape() {
7474
S.composerOpen = false;
7575
S.popoverOpen = false;
7676
S.editingCommentId = null;
77+
return;
7778
}
79+
// Lowest priority: the narrow-width file drawer. Closes only once every transient surface
80+
// above it is gone, so Esc dismisses a composer/modal opened over the drawer first.
81+
if (S.treeDrawerOpen) S.treeDrawerOpen = false;
7882
}
7983

8084
// Stage a destructive action behind a confirm dialog (Enter confirms, Esc cancels).
@@ -323,6 +327,15 @@ const HOTKEYS: Hotkey[] = [
323327
test: k("w"),
324328
run: () => (S.sidebarTab = S.sidebarTab === "tree" ? "walkthrough" : "tree"),
325329
},
330+
{
331+
combo: "⇧B",
332+
desc: "Files drawer (narrow screens)",
333+
group: "View",
334+
when: navigable,
335+
test: shift("B"),
336+
// Toggles the off-canvas file tree at narrow widths; inert on desktop (drawer is media-gated).
337+
run: () => (S.treeDrawerOpen = !S.treeDrawerOpen),
338+
},
326339
{
327340
combo: "⇧E",
328341
desc: "Open in editor",

src/ui/main.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ S.treeRows = treeRows;
7979
// which shows the "Rendering…" indicator only for a cold open of a big file.
8080
S.selectFile = (i) => {
8181
if (i < 0 || !S.state.files[i]) return; // ignore out-of-range selections
82+
// Narrow-width drawer: opening a file is the drawer's whole purpose, so get it out of the
83+
// way. The single funnel for tree/walkthrough clicks + next/prev + guide nav; no-op when shut.
84+
S.treeDrawerOpen = false;
8285
S.overviewOpen = false;
8386
S.preview = null;
8487
S.fileIndex = i;

src/ui/store.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export const S: Store = Alpine.reactive<Store>({
4141
sendNote: "",
4242
overviewOpen: false,
4343
sidebarTab: "tree",
44+
treeDrawerOpen: false,
4445
fileView: "rendered",
4546
});
4647

src/ui/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,10 @@ export interface Store {
197197
// Which sidebar pane is showing when a guide is attached (no guide → tabs hidden, tree
198198
// only). Per-session like overviewOpen; settings.sidebarDefault seeds it at init.
199199
sidebarTab: "tree" | "walkthrough";
200+
// Narrow widths (≤1100px): the file tree is an off-canvas left drawer, hidden by default so
201+
// the diff owns the screen. Toggled by the header hamburger / ⇧B; auto-closed on file select.
202+
// Inert on desktop — the drawer styles are media-gated, so the tree stays side-by-side there.
203+
treeDrawerOpen: boolean;
200204
// file mode: how a markdown file is shown — "rendered" (comark/markdown-it preview,
201205
// comment on blocks) or "source" (@pierre/diffs raw/diff).
202206
fileView: "rendered" | "source";

0 commit comments

Comments
 (0)