Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { AppLayout } from "./AppLayout";
import { APP_OVERLAY_LAYER } from "@/components/ui/app-overlay-layers";
import {
setCompactSecondaryPanelShelfShowing,
setCompactSecondaryPanelPresentation,
} from "@/components/ui/secondary-panel-shelf-visibility";

const viewportState = vi.hoisted(() => ({ compact: false }));
Expand Down Expand Up @@ -167,12 +167,12 @@ function renderPluginPanelRoute(): void {
describe("AppLayout plugin panel header", () => {
beforeEach(() => {
viewportState.compact = false;
setCompactSecondaryPanelShelfShowing(false);
setCompactSecondaryPanelPresentation("closed");
});

afterEach(() => {
cleanup();
setCompactSecondaryPanelShelfShowing(false);
setCompactSecondaryPanelPresentation("closed");
vi.clearAllMocks();
});

Expand All @@ -197,11 +197,13 @@ describe("AppLayout plugin panel header", () => {
expect(trigger.style.zIndex).toBe(
String(APP_OVERLAY_LAYER.sidebarTrigger),
);
act(() => setCompactSecondaryPanelPresentation("shelf"));
expect(screen.queryByTestId("app-sidebar-trigger-overlay")).toBeNull();

act(() => setCompactSecondaryPanelShelfShowing(true));
act(() => setCompactSecondaryPanelPresentation("full"));
expect(screen.queryByTestId("app-sidebar-trigger-overlay")).toBeNull();

act(() => setCompactSecondaryPanelShelfShowing(false));
act(() => setCompactSecondaryPanelPresentation("closed"));
expect(screen.getByTestId("app-sidebar-trigger-overlay")).not.toBeNull();
});
});
13 changes: 8 additions & 5 deletions apps/app/src/components/layout/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ import { getThreadDisplayTitle } from "@/lib/thread-title";
import { cn } from "@bb/shared-ui/lib/utils";
import { APP_OVERLAY_LAYER } from "@/components/ui/app-overlay-layers";
import {
isCompactSecondaryPanelShelfShowing,
getCompactSecondaryPanelPresentation,
subscribeCompactSecondaryPanelShelfShowing,
} from "@/components/ui/secondary-panel-shelf-visibility";
import { ProjectPathDialog } from "@/components/dialogs/ProjectPathDialog";
Expand Down Expand Up @@ -212,13 +212,16 @@ function SidebarTriggerOverlay({
usesDesktopChrome,
}: SidebarTriggerOverlayProps) {
const isCompactViewport = useIsCompactViewport();
const compactSecondaryPanelShelfShowing = useSyncExternalStore(
const compactSecondaryPanelPresentation = useSyncExternalStore(
subscribeCompactSecondaryPanelShelfShowing,
isCompactSecondaryPanelShelfShowing,
() => false,
getCompactSecondaryPanelPresentation,
() => "closed",
);
const shortcut = useAppCommandShortcut("sidebar.toggle");
if (isCompactViewport && compactSecondaryPanelShelfShowing) {
if (
isCompactViewport &&
compactSecondaryPanelPresentation !== "closed"
) {
return null;
}
const triggerProps = {
Expand Down
10 changes: 9 additions & 1 deletion apps/app/src/components/plugin/PluginPanelRightPanelHost.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@bb/shared-ui/tooltip";
import { useAppCommandHandler } from "@/components/commands/AppCommandProvider";
import { PluginIcon } from "@/components/plugin/PluginIcon";
import { PluginSlotMount } from "@/components/plugin/PluginSlotMount";
import { RIGHT_PANEL_TOGGLE_ICON_NAME } from "@/components/secondary-panel/panelToggleControlState";
import {
getCompactPanelPresentation,
RIGHT_PANEL_TOGGLE_ICON_NAME,
} from "@/components/secondary-panel/panelToggleControlState";
import { SecondaryPanelLayout } from "@/components/secondary-panel/SecondaryPanelLayout";
import {
LazyBrowserTabDeck,
Expand Down Expand Up @@ -971,6 +974,11 @@ export function PluginPanelRightPanelHost({
mainPanelId={`plugin-panel-main-${panelHostId}`}
main={children}
composerHost={null}
compactPresentation={getCompactPanelPresentation(
activeTab?.kind,
fixedTabs[0]?.tab.kind ??
panelTabs.find((tab) => tab.isHidden !== true)?.tab.kind,
)}
renderPanel={renderPanel}
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
import { useState, type ReactNode } from "react";
import { Icon } from "@bb/shared-ui/icon";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import {
createGitDiffFixedPanelTab,
createThreadInfoFixedPanelTab,
type HostFilePreviewFixedPanelTab,
type SecondaryFixedPanelTab,
} from "@/lib/fixed-panel-tabs-state";
import {
CompactHomePage,
MOBILE_RECENTS_VISIBILITY_CLASS,
MobileRecentsVisibilityStyle,
} from "@/views/mobile-home-story-fixtures";
import { CompactSecondaryPanelShelf } from "./CompactSecondaryPanelShelf";
import {
ThreadSecondaryPanel,
type SecondaryPanelRenderableTab,
} from "./ThreadSecondaryPanel";
import { ThreadMetadataContent } from "./ThreadMetadataContent";
import { baseProps as baseMetadataProps } from "./ThreadMetadataContent.fixtures";
import { FilePreview } from "./FilePreview";
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
import { threadListQueryKey } from "@/hooks/queries/query-keys";

export default {
title: "right-panel/Compact shelf",
};

const noop = () => {};

const STORY_FILE_PATH = "apps/app/src/views/RootComposeCompactHome.tsx";
const STORY_FILE_SOURCE = `export function RootComposeCompactHome({
children,
composer,
}: RootComposeCompactHomeProps) {
const { regionRef, composerRef } = useCompactHomeMetrics();
return (
<div ref={regionRef} className="relative min-h-0 flex-1">
{children}
<div ref={composerRef}>{composer}</div>
</div>
);
}`;

function createStoryFileTab(path: string): HostFilePreviewFixedPanelTab {
return {
environmentId: "env_story",
hostId: "host_story",
id: `host-file-preview:${encodeURIComponent(path)}:thread%3Athr_story%3Aenvironment%3Aenv_story`,
kind: "host-file-preview",
lineRange: null,
path,
threadId: "thr_story",
};
}

const fileTab = createStoryFileTab(STORY_FILE_PATH);

const MANY_TAB_PATHS: string[] = [
STORY_FILE_PATH,
"apps/app/src/components/secondary-panel/SecondaryPanelTabStrip.tsx",
"apps/app/src/components/secondary-panel/CompactSecondaryPanelShelf.tsx",
"apps/app/src/hooks/queries/thread-queries.ts",
"apps/app/src/components/ui/sidebar.tsx",
"apps/app/src/components/ui/theme.css",
"apps/app/src/app.css",
"package.json",
"README.md",
"apps/server/src/services/providers/provider-registry.ts",
"an-unusually-long-component-filename-that-must-truncate.tsx",
]

function StoryFileContent({ path }: { path: string }) {
return (
<FilePreview
path={path}
state={{
kind: "ready",
lineRange: null,
textPreviewKind: null,
file: {
cacheKey: `compact-shelf-story:${path}`,
name: path.split("/").at(-1) ?? path,
contents: STORY_FILE_SOURCE,
},
}}
/>
);
}

interface ShelfPanelProps {
activeTab: SecondaryFixedPanelTab;
onSelectTab: (tab: SecondaryFixedPanelTab) => void;
onClose: () => void;
filePaths?: string[];
}

function ShelfPanel({
activeTab,
onSelectTab,
onClose,
filePaths = [STORY_FILE_PATH],
}: ShelfPanelProps) {
const tabs: SecondaryPanelRenderableTab[] = filePaths.map((path, index) => {
const tab = path === STORY_FILE_PATH ? fileTab : createStoryFileTab(path);
return {
label: path.split("/").at(-1) ?? path,
isPinned: index === 0 && filePaths.length > 1,
leadingVisual: <Icon name="File" className="size-3.5" aria-hidden />,
statusLabel: null,
onSelect: () => onSelectTab(tab),
onClose: noop,
renderContent: () => <StoryFileContent path={path} />,
tab,
};
});
const infoTab = createThreadInfoFixedPanelTab();
const diffTab = createGitDiffFixedPanelTab();

return (
<ThreadSecondaryPanel
activeTab={activeTab}
canUseGitUi
requestedMergeBaseBranch="main"
environmentId={undefined}
isOpen
metadataContent={<ThreadMetadataContent {...baseMetadataProps} />}
tabs={tabs}
fixedTabs={[
{
ariaLabel: "Show thread info panel",
label: "Info",
leadingVisual: <Icon name="Info" />,
onSelect: () => onSelectTab(infoTab),
tab: infoTab,
title: "Thread info",
},
{
ariaLabel: "Show diff panel",
label: "Diff",
leadingVisual: <Icon name="FileDiff" />,
onSelect: () => onSelectTab(diffTab),
tab: diffTab,
title: "Diff",
},
]}
onPanelFocus={noop}
onCollapse={noop}
onClose={onClose}
onTabReorder={noop}
onOpenNewTab={noop}
isConversationCollapsed={false}
onToggleConversationCollapse={noop}
renderAsDrawer
inlinePanelToggle="hidden"
showConversationCollapseControl={false}
/>
);
}

function Stage({ children }: { children: ReactNode }) {
const [queryClient] = useState(() => {
const client = new QueryClient({
defaultOptions: { queries: { retry: false, staleTime: Infinity } },
});
client.setQueryData(
threadListQueryKey({
projectId: baseMetadataProps.thread.projectId,
sourceThreadId: baseMetadataProps.thread.id,
originKind: "fork",
archived: false,
}),
[],
);
return client;
});
return (
<QueryClientProvider client={queryClient}>
<div
className={`${MOBILE_RECENTS_VISIBILITY_CLASS} fixed inset-0 flex flex-col bg-background`}
>
<MobileRecentsVisibilityStyle />
<SidebarProvider defaultOpen={false} className="min-h-0">
<SidebarInset className="min-h-0 overflow-hidden">
<CompactHomePage />
</SidebarInset>
{children}
</SidebarProvider>
</div>
</QueryClientProvider>
);
}

function ShelfStory({
initialPresentation,
}: {
initialPresentation: "shelf" | "full";
}) {
const [activeTab, setActiveTab] = useState<SecondaryFixedPanelTab>(
initialPresentation === "shelf" ? createThreadInfoFixedPanelTab() : fileTab,
);
const [open, setOpen] = useState(true);
const presentation = activeTab.kind === "thread-info" ? "shelf" : "full";

return (
<Stage>
<CompactSecondaryPanelShelf
open={open}
onClose={() => setOpen(false)}
presentation={presentation}
srLabel="Right panel"
>
<ShelfPanel
activeTab={activeTab}
onSelectTab={setActiveTab}
onClose={() => setOpen(false)}
/>
</CompactSecondaryPanelShelf>
</Stage>
);
}

export function Shelf() {
return <ShelfStory initialPresentation="shelf" />;
}

export function FullPage() {
return <ShelfStory initialPresentation="full" />;
}

export function ManyTabs() {
const [activeTab, setActiveTab] = useState<SecondaryFixedPanelTab>(fileTab);
const presentation = activeTab.kind === "thread-info" ? "shelf" : "full";
return (
<Stage>
<CompactSecondaryPanelShelf
open
onClose={noop}
presentation={presentation}
srLabel="Right panel"
>
<ShelfPanel
activeTab={activeTab}
filePaths={MANY_TAB_PATHS}
onSelectTab={setActiveTab}
onClose={noop}
/>
</CompactSecondaryPanelShelf>
</Stage>
);
}

export function Closed() {
return (
<Stage>
<CompactSecondaryPanelShelf
open={false}
onClose={noop}
presentation="shelf"
srLabel="Right panel"
>
<ShelfPanel
activeTab={createThreadInfoFixedPanelTab()}
onSelectTab={noop}
onClose={noop}
/>
</CompactSecondaryPanelShelf>
</Stage>
);
}
Loading
Loading