Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions apps/web/src/components/AppSidebarLayout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { describe, expect, it } from "vitest";

import { shouldAcceptThreadSidebarWidth } from "./AppSidebarLayout";

describe("shouldAcceptThreadSidebarWidth", () => {
it("allows shrinking even when the sidebar is wider than the available content area", () => {
expect(
shouldAcceptThreadSidebarWidth({
currentWidth: 900,
nextWidth: 800,
wrapperClientWidth: 700,
}),
).toBe(true);
});

it("rejects expansion that would leave less than the minimum main content width", () => {
expect(
shouldAcceptThreadSidebarWidth({
currentWidth: 500,
nextWidth: 600,
wrapperClientWidth: 1_000,
}),
).toBe(false);
});

it("allows expansion when the main content minimum remains available", () => {
expect(
shouldAcceptThreadSidebarWidth({
currentWidth: 300,
nextWidth: 340,
wrapperClientWidth: 1_000,
}),
).toBe(true);
});
});
23 changes: 21 additions & 2 deletions apps/web/src/components/AppSidebarLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,21 @@ import {
const THREAD_SIDEBAR_WIDTH_STORAGE_KEY = "chat_thread_sidebar_width";
const THREAD_SIDEBAR_MIN_WIDTH = 13 * 16;
const THREAD_MAIN_CONTENT_MIN_WIDTH = 40 * 16;

export function shouldAcceptThreadSidebarWidth({
currentWidth,
nextWidth,
wrapperClientWidth,
}: {
currentWidth: number;
nextWidth: number;
wrapperClientWidth: number;
}): boolean {
return (
nextWidth <= currentWidth || wrapperClientWidth - nextWidth >= THREAD_MAIN_CONTENT_MIN_WIDTH
);
}

export function AppSidebarLayout({ children }: { children: ReactNode }) {
const navigate = useNavigate();

Expand Down Expand Up @@ -61,8 +76,12 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) {
className="border-r border-border bg-card text-foreground"
resizable={{
minWidth: THREAD_SIDEBAR_MIN_WIDTH,
shouldAcceptWidth: ({ nextWidth, wrapper }) =>
wrapper.clientWidth - nextWidth >= THREAD_MAIN_CONTENT_MIN_WIDTH,
shouldAcceptWidth: ({ currentWidth, nextWidth, wrapper }) =>
shouldAcceptThreadSidebarWidth({
currentWidth,
nextWidth,
wrapperClientWidth: wrapper.clientWidth,
}),
storageKey: THREAD_SIDEBAR_WIDTH_STORAGE_KEY,
}}
>
Expand Down
45 changes: 45 additions & 0 deletions apps/web/src/components/NoActiveThreadState.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { describe, expect, it } from "vitest";

import { shouldShowNoActiveThreadSidebarTrigger } from "./NoActiveThreadState";

describe("shouldShowNoActiveThreadSidebarTrigger", () => {
it("shows the trigger when the mobile sidebar sheet is closed", () => {
expect(
shouldShowNoActiveThreadSidebarTrigger({
isMobile: true,
open: true,
openMobile: false,
}),
).toBe(true);
});

it("hides the trigger when the mobile sidebar sheet is already open", () => {
expect(
shouldShowNoActiveThreadSidebarTrigger({
isMobile: true,
open: true,
openMobile: true,
}),
).toBe(false);
});

it("shows the trigger when the desktop sidebar is collapsed", () => {
expect(
shouldShowNoActiveThreadSidebarTrigger({
isMobile: false,
open: false,
openMobile: false,
}),
).toBe(true);
});

it("hides the trigger when the desktop sidebar is expanded", () => {
expect(
shouldShowNoActiveThreadSidebarTrigger({
isMobile: false,
open: true,
openMobile: false,
}),
).toBe(false);
});
});
30 changes: 25 additions & 5 deletions apps/web/src/components/NoActiveThreadState.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,28 @@
import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from "./ui/empty";
import { SidebarInset, SidebarTrigger } from "./ui/sidebar";
import { SidebarInset, SidebarTrigger, useSidebar } from "./ui/sidebar";
import { isElectron } from "../env";
import { cn } from "~/lib/utils";

export function shouldShowNoActiveThreadSidebarTrigger({
isMobile,
open,
openMobile,
}: {
isMobile: boolean;
open: boolean;
openMobile: boolean;
}): boolean {
return isMobile ? !openMobile : !open;
}

export function NoActiveThreadState() {
const { isMobile, open, openMobile } = useSidebar();
const showSidebarTrigger = shouldShowNoActiveThreadSidebarTrigger({
isMobile,
open,
openMobile,
});

return (
<SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground">
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-x-hidden bg-background">
Expand All @@ -16,12 +35,13 @@ export function NoActiveThreadState() {
)}
>
{isElectron ? (
<span className="text-xs text-muted-foreground/50 wco:pr-[calc(100vw-env(titlebar-area-width)-env(titlebar-area-x)+1em)]">
No active thread
</span>
<div className="flex min-w-0 items-center gap-2 wco:pr-[calc(100vw-env(titlebar-area-width)-env(titlebar-area-x)+1em)]">
{showSidebarTrigger ? <SidebarTrigger className="size-7 shrink-0" /> : null}
<span className="truncate text-xs text-muted-foreground/50">No active thread</span>
</div>
) : (
<div className="flex items-center gap-2">
<SidebarTrigger className="size-7 shrink-0 md:hidden" />
{showSidebarTrigger ? <SidebarTrigger className="size-7 shrink-0" /> : null}
<span className="text-sm font-medium text-foreground md:text-muted-foreground/60">
No active thread
</span>
Expand Down
Loading