diff --git a/bun.lock b/bun.lock
index edc8ce7..5029b3b 100644
--- a/bun.lock
+++ b/bun.lock
@@ -43,6 +43,7 @@
"svelte-adapter-bun": "^1.0.1",
"svelte-check": "^4.3.3",
"tailwindcss": "^4.1.17",
+ "tw-animate-css": "^1.4.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.46.3",
"vite": "^7.2.2",
@@ -1037,6 +1038,8 @@
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+ "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
+
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
diff --git a/web/package.json b/web/package.json
index a0bad4f..e8909ec 100644
--- a/web/package.json
+++ b/web/package.json
@@ -10,6 +10,7 @@
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
+ "check:all": "bun run check && bun run lint && vitest run",
"test": "vitest",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"format": "prettier --write .",
@@ -40,6 +41,7 @@
"svelte-adapter-bun": "^1.0.1",
"svelte-check": "^4.3.3",
"tailwindcss": "^4.1.17",
+ "tw-animate-css": "^1.4.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.46.3",
"vite": "^7.2.2",
diff --git a/web/src/app.css b/web/src/app.css
index c4176c9..ad33c77 100644
--- a/web/src/app.css
+++ b/web/src/app.css
@@ -1,5 +1,6 @@
@import "tailwindcss";
+@import "tw-animate-css";
@plugin "@iconify/tailwind4" {
prefixes: octicon;
}
diff --git a/web/src/lib/components/menu-bar/MenuBar.svelte b/web/src/lib/components/menu-bar/MenuBar.svelte
new file mode 100644
index 0000000..0e27ddc
--- /dev/null
+++ b/web/src/lib/components/menu-bar/MenuBar.svelte
@@ -0,0 +1,103 @@
+
+
+{#snippet keybind(key: string)}
+ {Keybinds.getModifierKey()}+{key}
+{/snippet}
+
+
+
+ diffs.dev
+
+
+
+
+
+ GitHub Repository
+
+
+
+
+
+ Chrome Extension
+
+
+
+
+
+ Firefox Add-on
+
+
+
+ {
+ viewer.openSettingsDialog();
+ }}
+ >
+ Open Settings
+ {@render keybind(",")}
+
+
+
+
+
+ File
+
+
+ {
+ viewer.openOpenDiffDialog();
+ }}
+ >
+ Open
+ {@render keybind("O")}
+
+
+
+
+
+ View
+
+
+ {
+ viewer.expandAll();
+ }}
+ >
+ Expand All
+
+ {
+ viewer.collapseAll();
+ }}
+ >
+ Collapse All
+
+
+
+
+
diff --git a/web/src/lib/components/settings-popover/SettingsPopover.svelte b/web/src/lib/components/settings-popover/SettingsPopover.svelte
deleted file mode 100644
index 4b733d2..0000000
--- a/web/src/lib/components/settings-popover/SettingsPopover.svelte
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-
-
-{#snippet sectionHeader(text: string)}
-
{text}
-{/snippet}
-
-{#snippet globalThemeSetting()}
-
-
-
-
-
-{/snippet}
-
-
-
-
-
-
-
- {@render children?.()}
-
-
-
-
diff --git a/web/src/lib/components/settings/SettingsDialog.svelte b/web/src/lib/components/settings/SettingsDialog.svelte
new file mode 100644
index 0000000..e00547a
--- /dev/null
+++ b/web/src/lib/components/settings/SettingsDialog.svelte
@@ -0,0 +1,74 @@
+
+
+
+
+{#snippet sectionHeader(text: string)}
+ {text}
+{/snippet}
+
+{#snippet globalThemeSetting()}
+
+
+
+
+
+{/snippet}
+
+
+
+
+
+
+
+
+ {@render globalThemeSetting()}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/lib/components/settings-popover/SettingsPopoverGroup.svelte b/web/src/lib/components/settings/SettingsGroup.svelte
similarity index 80%
rename from web/src/lib/components/settings-popover/SettingsPopoverGroup.svelte
rename to web/src/lib/components/settings/SettingsGroup.svelte
index a8a2517..d57087e 100644
--- a/web/src/lib/components/settings-popover/SettingsPopoverGroup.svelte
+++ b/web/src/lib/components/settings/SettingsGroup.svelte
@@ -1,6 +1,3 @@
-
-
@@ -107,260 +47,88 @@
/>
-{#snippet settingsPopover()}
-
- {@render globalThemeSetting()}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-{/snippet}
-
{#if viewer.loadingState.loading}
{/if}
-
-
-
-
-
-
- {#if viewer.fileTreeFilterDebounced.current}
-
- {/if}
-
-
-
- {#if viewer.filteredFileDetails.length !== viewer.fileDetails.length}
-
- Showing {viewer.filteredFileDetails.length} of {viewer.fileDetails.length} files
-
- {/if}
-
-
- {#snippet fileSnippet(value: FileDetails)}
-
scrollToFileClick(e, value.index)}
- use:focusFileDoubleClick={{ index: value.index }}
- onkeydown={(e) => e.key === "Enter" && viewer.scrollToFile(value.index)}
- role="button"
- tabindex="0"
- id={"file-tree-file-" + value.index}
- >
-
- {value.toFile.substring(value.toFile.lastIndexOf("/") + 1)}
- viewer.toggleChecked(value.index)}
- checked={viewer.fileStates[value.index].checked}
- />
-
- {/snippet}
-
- {#snippet nodeRenderer({ node, collapsed, toggleCollapse })}
- {@const folderIcon = collapsed ? "octicon--file-directory-fill-16" : "octicon--file-directory-open-fill-16"}
- {#if node.data.type === "file"}
- {@render fileSnippet(node.data.data as FileDetails)}
- {:else}
- e.key === "Enter" && toggleCollapse()}
- role="button"
- tabindex="0"
- >
-
- {node.data.data}
- {#if collapsed}
-
- {:else}
-
- {/if}
-
- {/if}
- {/snippet}
- {#snippet childWrapper({ node, collapsed, children })}
-
- {@render children({ node })}
-
- {/snippet}
-
-
-
-
-
-
+
+
+
+
+
+
+
+
{#if viewer.diffMetadata !== null}
-
+
+
+
{/if}
-
-
-
- {@render settingsPopover()}
-
-
-
Info
-
-
- GitHub Repository
-
-
-
- Chrome Extension
-
-
-
- Firefox Add-on
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
i} bind:this={viewer.vlist}>
- {#snippet children(value, index)}
-
-
- {#if !viewer.fileStates[index].collapsed && value.type === "image"}
- {@const image = value.image}
-
- {#if image.load}
- {#if image.fileA !== null && image.fileB !== null}
- {#await Promise.all([image.fileA.getValue(), image.fileB.getValue()])}
-
- {:then images}
-
- {/await}
+
+
i} bind:this={viewer.vlist}>
+ {#snippet children(value, index)}
+
+
+ {#if !viewer.fileStates[index].collapsed && value.type === "image"}
+ {@const image = value.image}
+
+ {#if image.load}
+ {#if image.fileA !== null && image.fileB !== null}
+ {#await Promise.all([image.fileA.getValue(), image.fileB.getValue()])}
+
+ {:then images}
+
+ {/await}
+ {:else}
+ {#await requireEitherImage(image).getValue()}
+
+ {:then file}
+
+ {/await}
+ {/if}
{:else}
- {#await requireEitherImage(image).getValue()}
-
- {:then file}
-
- {/await}
+
+
+
{/if}
- {:else}
-
-
-
- {/if}
-
- {/if}
- {#if !viewer.fileStates[index].collapsed && value.type === "text" && (!value.patchHeaderDiffOnly || !globalOptions.omitPatchHeaderOnlyHunks)}
-
- viewer.searchResults.then((r) => r.lines.get(value))}
- activeSearchResult={viewer.activeSearchResult && viewer.activeSearchResult.file === value
- ? viewer.activeSearchResult.idx
- : undefined}
- cache={viewer.diffViewCache}
- cacheKey={value}
- />
-
- {/if}
-
- {/snippet}
-
+
+ {/if}
+ {#if !viewer.fileStates[index].collapsed && value.type === "text" && (!value.patchHeaderDiffOnly || !globalOptions.omitPatchHeaderOnlyHunks)}
+
+ viewer.searchResults.then((r) => r.lines.get(value))}
+ activeSearchResult={viewer.activeSearchResult && viewer.activeSearchResult.file === value
+ ? viewer.activeSearchResult.idx
+ : undefined}
+ cache={viewer.diffViewCache}
+ cacheKey={value}
+ />
+
+ {/if}
+
+ {/snippet}
+
+
-
-
diff --git a/web/src/routes/ActionsPopover.svelte b/web/src/routes/ActionsPopover.svelte
deleted file mode 100644
index eb87134..0000000
--- a/web/src/routes/ActionsPopover.svelte
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-
-
-
-
-
-
- {
- viewer.expandAll();
- open = false;
- }}
- >
- Expand All
-
- {
- viewer.collapseAll();
- open = false;
- }}
- >
- Collapse All
-
-
-
-
-
diff --git a/web/src/routes/OpenDiffDialog.svelte b/web/src/routes/OpenDiffDialog.svelte
index d0dbb7c..3c2c45c 100644
--- a/web/src/routes/OpenDiffDialog.svelte
+++ b/web/src/routes/OpenDiffDialog.svelte
@@ -280,13 +280,14 @@
{/snippet}
- Open new diff
-
+
-
+
Open New Diff
diff --git a/web/src/routes/Sidebar.svelte b/web/src/routes/Sidebar.svelte
new file mode 100644
index 0000000..ad1be4f
--- /dev/null
+++ b/web/src/routes/Sidebar.svelte
@@ -0,0 +1,194 @@
+
+
+
+
+
+
+
+ {#if viewer.fileTreeFilterDebounced.current}
+
+ {/if}
+
+
+
+ {#if viewer.filteredFileDetails.length !== viewer.fileDetails.length}
+
+ Showing {viewer.filteredFileDetails.length} of {viewer.fileDetails.length} files
+
+ {/if}
+
+
+ {#snippet fileSnippet(value: FileDetails)}
+
scrollToFileClick(e, value.index)}
+ use:focusFileDoubleClick={{ index: value.index }}
+ onkeydown={(e) => e.key === "Enter" && viewer.scrollToFile(value.index)}
+ role="button"
+ tabindex="0"
+ id={"file-tree-file-" + value.index}
+ >
+
+ {value.toFile.substring(value.toFile.lastIndexOf("/") + 1)}
+ viewer.toggleChecked(value.index)}
+ checked={viewer.fileStates[value.index].checked}
+ />
+
+ {/snippet}
+
+ {#snippet nodeRenderer({ node, collapsed, toggleCollapse })}
+ {@const folderIcon = collapsed ? "octicon--file-directory-fill-16" : "octicon--file-directory-open-fill-16"}
+ {#if node.data.type === "file"}
+ {@render fileSnippet(node.data.data as FileDetails)}
+ {:else}
+ e.key === "Enter" && toggleCollapse()}
+ role="button"
+ tabindex="0"
+ >
+
+ {node.data.data}
+ {#if collapsed}
+
+ {:else}
+
+ {/if}
+
+ {/if}
+ {/snippet}
+ {#snippet childWrapper({ node, collapsed, children })}
+
+ {@render children({ node })}
+
+ {/snippet}
+
+
+
+
+
+