Skip to content

Commit e774c0a

Browse files
committed
Make sidebar resizable
1 parent 123d2ca commit e774c0a

File tree

14 files changed

+219
-90
lines changed

14 files changed

+219
-90
lines changed

bun.lock

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"chroma-js": "^3.1.2",
1414
"diff": "^8.0.2",
1515
"luxon": "^3.7.2",
16+
"paneforge": "^1.0.2",
1617
"runed": "^0.36.0",
1718
"shiki": "^3.15.0",
1819
"svelte-toolbelt": "^0.10.6",
@@ -882,6 +883,8 @@
882883

883884
"package-manager-detector": ["[email protected]", "", {}, "sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ=="],
884885

886+
"paneforge": ["[email protected]", "", { "dependencies": { "runed": "^0.23.4", "svelte-toolbelt": "^0.9.2" }, "peerDependencies": { "svelte": "^5.29.0" } }, "sha512-KzmIXQH1wCfwZ4RsMohD/IUtEjVhteR+c+ulb/CHYJHX8SuDXoJmChtsc/Xs5Wl8NHS4L5Q7cxL8MG40gSU1bA=="],
887+
885888
"parent-module": ["[email protected]", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
886889

887890
"parse5": ["[email protected]", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
@@ -1196,6 +1199,10 @@
11961199

11971200
"minizlib/minipass": ["[email protected]", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
11981201

1202+
"paneforge/runed": ["[email protected]", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA=="],
1203+
1204+
"paneforge/svelte-toolbelt": ["[email protected]", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.29.0", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.30.2" } }, "sha512-HCSWxCtVmv+c6g1ACb8LTwHVbDqLKJvHpo6J8TaqwUme2hj9ATJCpjCPNISR1OCq2Q4U1KT41if9ON0isINQZw=="],
1205+
11991206
"parse5/entities": ["[email protected]", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
12001207

12011208
"svelte-check/fdir": ["[email protected]", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="],
@@ -1224,6 +1231,8 @@
12241231

12251232
"csso/css-tree/mdn-data": ["[email protected]", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
12261233

1234+
"paneforge/svelte-toolbelt/runed": ["[email protected]", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-0cq6cA6sYGZwl/FvVqjx9YN+1xEBu9sDDyuWdDW1yWX7JF2wmvmVKfH+hVCZs+csW+P3ARH92MjI3H9QTagOQA=="],
1235+
12271236
"wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/[email protected]", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="],
12281237

12291238
"wrangler/esbuild/@esbuild/android-arm": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="],

web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"chroma-js": "^3.1.2",
5353
"diff": "^8.0.2",
5454
"luxon": "^3.7.2",
55+
"paneforge": "^1.0.2",
5556
"runed": "^0.36.0",
5657
"shiki": "^3.15.0",
5758
"svelte-toolbelt": "^0.10.6",

web/src/routes/SidebarToggle.svelte renamed to web/src/lib/components/SidebarToggle.svelte

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,19 @@
1212
let mergedProps = $derived(
1313
mergeProps(
1414
{
15+
title: viewer.layoutState.sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar",
1516
class: "flex size-6 items-center justify-center rounded-md btn-ghost text-primary",
1617
},
1718
restProps,
1819
),
1920
);
2021
</script>
2122

22-
<Button.Root
23-
title={viewer.sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
24-
type="button"
25-
data-side={globalOptions.sidebarLocation}
26-
onclick={() => (viewer.sidebarCollapsed = !viewer.sidebarCollapsed)}
27-
data-sidebar-toggle
28-
{...mergedProps}
29-
>
23+
<Button.Root type="button" data-side={globalOptions.sidebarLocation} onclick={() => viewer.layoutState.toggleSidebar()} data-sidebar-toggle {...mergedProps}>
3024
<span
3125
class="iconify size-4 shrink-0 octicon--sidebar-collapse-16 data-[collapsed=false]:octicon--sidebar-expand-16 data-[side=right]:scale-x-[-1]"
3226
aria-hidden="true"
33-
data-collapsed={viewer.sidebarCollapsed}
27+
data-collapsed={viewer.layoutState.sidebarCollapsed}
3428
data-side={globalOptions.sidebarLocation}
3529
></span>
3630
</Button.Root>

web/src/lib/components/menu-bar/MenuBar.svelte

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
import { MultiFileDiffViewerState } from "$lib/diff-viewer.svelte";
33
import { Keybinds } from "$lib/keybinds.svelte";
44
import { Menubar, Button } from "bits-ui";
5+
import SidebarToggle from "$lib/components/SidebarToggle.svelte";
56
67
const viewer = MultiFileDiffViewerState.get();
78
</script>
89

910
{#snippet keybind(key: string)}
10-
<span class="text-em-med">{Keybinds.getModifierKey()}+{key}</span>
11+
<span class="text-em-med">{Keybinds.formatModifierBind(key)}</span>
1112
{/snippet}
1213

1314
<Menubar.Root class="flex border-b leading-none">
@@ -97,7 +98,25 @@
9798
>
9899
Collapse All
99100
</Menubar.Item>
101+
<Menubar.Item
102+
class="flex justify-between gap-2 btn-ghost px-2 py-1 select-none"
103+
onSelect={() => {
104+
viewer.layoutState.toggleSidebar();
105+
}}
106+
>
107+
Toggle Sidebar
108+
{@render keybind("B")}
109+
</Menubar.Item>
110+
<Menubar.Item
111+
class="btn-ghost px-2 py-1 select-none"
112+
onSelect={() => {
113+
viewer.layoutState.resetLayout();
114+
}}
115+
>
116+
Reset Layout
117+
</Menubar.Item>
100118
</Menubar.Content>
101119
</Menubar.Portal>
102120
</Menubar.Menu>
121+
<SidebarToggle class="my-auto mr-2 ml-auto" />
103122
</Menubar.Root>

web/src/lib/diff-viewer.svelte.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { Context, Debounced, watch } from "runed";
1818
import { MediaQuery } from "svelte/reactivity";
1919
import { ProgressBarState } from "$lib/components/progress-bar/index.svelte";
2020
import { Keybinds } from "./keybinds.svelte";
21+
import { LayoutState, type PersistentLayoutState } from "./layout.svelte";
2122

2223
export const GITHUB_URL_PARAM = "github_url";
2324
export const PATCH_URL_PARAM = "patch_url";
@@ -200,8 +201,8 @@ export type DiffMetadata = GithubDiffMetadata | FileDiffMetadata;
200201
export class MultiFileDiffViewerState {
201202
private static readonly context = new Context<MultiFileDiffViewerState>("MultiFileDiffViewerState");
202203

203-
static init() {
204-
return MultiFileDiffViewerState.context.set(new MultiFileDiffViewerState());
204+
static init(layoutState: PersistentLayoutState | null) {
205+
return MultiFileDiffViewerState.context.set(new MultiFileDiffViewerState(layoutState));
205206
}
206207

207208
static get() {
@@ -232,20 +233,23 @@ export class MultiFileDiffViewerState {
232233
diffViewCache: Map<FileDetails, ConciseDiffViewCachedState> = new Map();
233234
vlist: VList<FileDetails> | undefined = $state();
234235
readonly loadingState: LoadingState = $state(new LoadingState());
236+
readonly layoutState;
235237

236238
// Transient state
237-
sidebarCollapsed = $state(false);
238239
openDiffDialogOpen = $state(false);
239240
settingsDialogOpen = $state(false);
240241
activeSearchResult: ActiveSearchResult | null = $state(null);
241242

242-
private constructor() {
243+
private constructor(layoutState: PersistentLayoutState | null) {
244+
this.layoutState = new LayoutState(layoutState);
245+
243246
// Make sure to revoke object URLs when the component is destroyed
244247
onDestroy(() => this.clearImages());
245248

246249
const keybinds = new Keybinds();
247250
keybinds.registerModifierBind("o", () => this.openOpenDiffDialog());
248251
keybinds.registerModifierBind(",", () => this.openSettingsDialog());
252+
keybinds.registerModifierBind("b", () => this.layoutState.toggleSidebar());
249253
}
250254

251255
openOpenDiffDialog() {

web/src/lib/global-options.svelte.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { BundledTheme } from "shiki";
22
import { browser } from "$app/environment";
33
import { getEffectiveGlobalTheme } from "$lib/theme.svelte";
4-
import { watchLocalStorage } from "$lib/util";
4+
import { setCookie, watchLocalStorage } from "$lib/util";
55
import { Context } from "runed";
66

77
export const DEFAULT_THEME_LIGHT: BundledTheme = "github-light-default";
@@ -68,7 +68,7 @@ export class GlobalOptions {
6868
return;
6969
}
7070
localStorage.setItem(GlobalOptions.key, this.serialize());
71-
document.cookie = `${GlobalOptions.key}=${encodeURIComponent(this.serializeCookie())}; path=/; max-age=31536000; SameSite=Lax`;
71+
setCookie(GlobalOptions.key, this.serializeCookie());
7272
}
7373

7474
private serialize() {

web/src/lib/keybinds.svelte.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@ import { on } from "svelte/events";
44
export class Keybinds {
55
private static readonly IS_MAC = typeof navigator !== "undefined" && navigator.userAgent.includes("Mac");
66

7-
static getModifierKey() {
7+
private static formatModifierKey() {
88
return Keybinds.IS_MAC ? "⌘" : "Ctrl";
99
}
1010

11+
static formatModifierBind(key: string) {
12+
return `${Keybinds.formatModifierKey()}+${key}`;
13+
}
14+
1115
private readonly binds = new Map<string, () => void>();
1216

1317
constructor() {

web/src/lib/layout.svelte.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { GlobalOptions } from "$lib/global-options.svelte";
2+
import type { Pane } from "paneforge";
3+
import { clearCookie, setCookie } from "./util";
4+
5+
export const ROOT_LAYOUT_KEY = "diff-viewer-root-layout";
6+
export interface PersistentLayoutState {
7+
sidebarWidth: number;
8+
}
9+
10+
export class LayoutState {
11+
private readonly globalOptions: GlobalOptions;
12+
13+
sidebarCollapsed = $state(false);
14+
15+
windowInnerWidth: number | undefined = $state();
16+
sidebarPane: Pane | undefined = $state();
17+
lastSidebarWidth: number | undefined = $state();
18+
19+
minSidebarWidth = $derived.by(() => {
20+
return this.getProportion(200, 0);
21+
});
22+
defaultSidebarWidth = $derived.by(() => {
23+
if (this.lastSidebarWidth !== undefined) {
24+
return this.lastSidebarWidth;
25+
}
26+
return this.getProportion(350, 0.25);
27+
});
28+
29+
defaultMainWidth = $derived.by(() => {
30+
if (this.lastSidebarWidth !== undefined) {
31+
return 100 - this.lastSidebarWidth;
32+
}
33+
return undefined;
34+
});
35+
36+
constructor(state: PersistentLayoutState | null) {
37+
this.lastSidebarWidth = state?.sidebarWidth;
38+
this.globalOptions = GlobalOptions.get();
39+
}
40+
41+
toggleSidebar() {
42+
this.sidebarCollapsed = !this.sidebarCollapsed;
43+
}
44+
45+
private getProportion(px: number, defaultValue: number) {
46+
if (this.windowInnerWidth === undefined) {
47+
return defaultValue;
48+
}
49+
return Math.max(0, Math.min(100, Math.ceil((px / this.windowInnerWidth) * 100)));
50+
}
51+
52+
resetLayout() {
53+
clearCookie(ROOT_LAYOUT_KEY);
54+
this.lastSidebarWidth = undefined;
55+
if (this.sidebarPane) {
56+
this.sidebarPane.resize(this.defaultSidebarWidth);
57+
}
58+
}
59+
60+
onSidebarResize(size: number, prevSize: number | undefined) {
61+
if (prevSize === undefined) {
62+
// Prevent initial resize from triggering update loop
63+
return;
64+
}
65+
66+
this.lastSidebarWidth = size;
67+
const rootLayout: PersistentLayoutState = {
68+
sidebarWidth: this.lastSidebarWidth,
69+
};
70+
setCookie(ROOT_LAYOUT_KEY, JSON.stringify(rootLayout));
71+
}
72+
}

web/src/lib/util.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ export type MutableValue<T> = {
1313
value: T;
1414
};
1515

16+
export function clearCookie(name: string) {
17+
document.cookie = name + "=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT";
18+
}
19+
20+
export function setCookie(name: string, value: string) {
21+
document.cookie = `${name}=${encodeURIComponent(value)}; path=/; max-age=31536000; SameSite=Lax`;
22+
}
23+
1624
function isFullCommitHash(s: string): boolean {
1725
return /^[0-9a-fA-F]{40}$/.test(s);
1826
}

web/src/routes/+layout.svelte

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22
import "../app.css";
33
import { initThemeHooks } from "$lib/theme.svelte";
44
import { Tooltip } from "bits-ui";
5+
import { type LayoutProps } from "./$types";
6+
import { GlobalOptions } from "$lib/global-options.svelte";
57
6-
let { children } = $props();
8+
let { children, data }: LayoutProps = $props();
9+
GlobalOptions.init(data.globalOptions);
710
811
initThemeHooks();
912
</script>

0 commit comments

Comments
 (0)