Skip to content

Commit 551e2be

Browse files
committed
fix(web): restore sidebar access in zoomed empty state
1 parent b3e8c03 commit 551e2be

4 files changed

Lines changed: 126 additions & 7 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import { shouldAcceptThreadSidebarWidth } from "./AppSidebarLayout";
4+
5+
describe("shouldAcceptThreadSidebarWidth", () => {
6+
it("allows shrinking even when the sidebar is wider than the available content area", () => {
7+
expect(
8+
shouldAcceptThreadSidebarWidth({
9+
currentWidth: 900,
10+
nextWidth: 800,
11+
wrapperClientWidth: 700,
12+
}),
13+
).toBe(true);
14+
});
15+
16+
it("rejects expansion that would leave less than the minimum main content width", () => {
17+
expect(
18+
shouldAcceptThreadSidebarWidth({
19+
currentWidth: 500,
20+
nextWidth: 600,
21+
wrapperClientWidth: 1_000,
22+
}),
23+
).toBe(false);
24+
});
25+
26+
it("allows expansion when the main content minimum remains available", () => {
27+
expect(
28+
shouldAcceptThreadSidebarWidth({
29+
currentWidth: 300,
30+
nextWidth: 340,
31+
wrapperClientWidth: 1_000,
32+
}),
33+
).toBe(true);
34+
});
35+
});

apps/web/src/components/AppSidebarLayout.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,21 @@ import {
1111
const THREAD_SIDEBAR_WIDTH_STORAGE_KEY = "chat_thread_sidebar_width";
1212
const THREAD_SIDEBAR_MIN_WIDTH = 13 * 16;
1313
const THREAD_MAIN_CONTENT_MIN_WIDTH = 40 * 16;
14+
15+
export function shouldAcceptThreadSidebarWidth({
16+
currentWidth,
17+
nextWidth,
18+
wrapperClientWidth,
19+
}: {
20+
currentWidth: number;
21+
nextWidth: number;
22+
wrapperClientWidth: number;
23+
}): boolean {
24+
return (
25+
nextWidth <= currentWidth || wrapperClientWidth - nextWidth >= THREAD_MAIN_CONTENT_MIN_WIDTH
26+
);
27+
}
28+
1429
export function AppSidebarLayout({ children }: { children: ReactNode }) {
1530
const navigate = useNavigate();
1631

@@ -61,8 +76,12 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) {
6176
className="border-r border-border bg-card text-foreground"
6277
resizable={{
6378
minWidth: THREAD_SIDEBAR_MIN_WIDTH,
64-
shouldAcceptWidth: ({ nextWidth, wrapper }) =>
65-
wrapper.clientWidth - nextWidth >= THREAD_MAIN_CONTENT_MIN_WIDTH,
79+
shouldAcceptWidth: ({ currentWidth, nextWidth, wrapper }) =>
80+
shouldAcceptThreadSidebarWidth({
81+
currentWidth,
82+
nextWidth,
83+
wrapperClientWidth: wrapper.clientWidth,
84+
}),
6685
storageKey: THREAD_SIDEBAR_WIDTH_STORAGE_KEY,
6786
}}
6887
>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import { shouldShowNoActiveThreadSidebarTrigger } from "./NoActiveThreadState";
4+
5+
describe("shouldShowNoActiveThreadSidebarTrigger", () => {
6+
it("shows the trigger when the mobile sidebar sheet is closed", () => {
7+
expect(
8+
shouldShowNoActiveThreadSidebarTrigger({
9+
isMobile: true,
10+
open: true,
11+
openMobile: false,
12+
}),
13+
).toBe(true);
14+
});
15+
16+
it("hides the trigger when the mobile sidebar sheet is already open", () => {
17+
expect(
18+
shouldShowNoActiveThreadSidebarTrigger({
19+
isMobile: true,
20+
open: true,
21+
openMobile: true,
22+
}),
23+
).toBe(false);
24+
});
25+
26+
it("shows the trigger when the desktop sidebar is collapsed", () => {
27+
expect(
28+
shouldShowNoActiveThreadSidebarTrigger({
29+
isMobile: false,
30+
open: false,
31+
openMobile: false,
32+
}),
33+
).toBe(true);
34+
});
35+
36+
it("hides the trigger when the desktop sidebar is expanded", () => {
37+
expect(
38+
shouldShowNoActiveThreadSidebarTrigger({
39+
isMobile: false,
40+
open: true,
41+
openMobile: false,
42+
}),
43+
).toBe(false);
44+
});
45+
});

apps/web/src/components/NoActiveThreadState.tsx

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,28 @@
11
import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from "./ui/empty";
2-
import { SidebarInset, SidebarTrigger } from "./ui/sidebar";
2+
import { SidebarInset, SidebarTrigger, useSidebar } from "./ui/sidebar";
33
import { isElectron } from "../env";
44
import { cn } from "~/lib/utils";
55

6+
export function shouldShowNoActiveThreadSidebarTrigger({
7+
isMobile,
8+
open,
9+
openMobile,
10+
}: {
11+
isMobile: boolean;
12+
open: boolean;
13+
openMobile: boolean;
14+
}): boolean {
15+
return isMobile ? !openMobile : !open;
16+
}
17+
618
export function NoActiveThreadState() {
19+
const { isMobile, open, openMobile } = useSidebar();
20+
const showSidebarTrigger = shouldShowNoActiveThreadSidebarTrigger({
21+
isMobile,
22+
open,
23+
openMobile,
24+
});
25+
726
return (
827
<SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground">
928
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-x-hidden bg-background">
@@ -16,12 +35,13 @@ export function NoActiveThreadState() {
1635
)}
1736
>
1837
{isElectron ? (
19-
<span className="text-xs text-muted-foreground/50 wco:pr-[calc(100vw-env(titlebar-area-width)-env(titlebar-area-x)+1em)]">
20-
No active thread
21-
</span>
38+
<div className="flex min-w-0 items-center gap-2 wco:pr-[calc(100vw-env(titlebar-area-width)-env(titlebar-area-x)+1em)]">
39+
{showSidebarTrigger ? <SidebarTrigger className="size-7 shrink-0" /> : null}
40+
<span className="truncate text-xs text-muted-foreground/50">No active thread</span>
41+
</div>
2242
) : (
2343
<div className="flex items-center gap-2">
24-
<SidebarTrigger className="size-7 shrink-0 md:hidden" />
44+
{showSidebarTrigger ? <SidebarTrigger className="size-7 shrink-0" /> : null}
2545
<span className="text-sm font-medium text-foreground md:text-muted-foreground/60">
2646
No active thread
2747
</span>

0 commit comments

Comments
 (0)