Skip to content

Commit 58a4efd

Browse files
committed
refactor(sidebar): consolidate footer and account actions into bottom rail
1 parent 76d82ee commit 58a4efd

6 files changed

Lines changed: 383 additions & 250 deletions

File tree

src/features/app/components/Sidebar.test.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,26 @@ describe("Sidebar", () => {
153153
expect(creditsLabel.textContent ?? "").toContain("120");
154154
});
155155

156+
it("opens the account menu from the bottom rail", () => {
157+
render(
158+
<Sidebar
159+
{...baseProps}
160+
activeWorkspaceId="ws-1"
161+
accountInfo={{
162+
email: "dimillian@example.com",
163+
type: "chatgpt",
164+
planType: "pro",
165+
requiresOpenaiAuth: false,
166+
}}
167+
/>,
168+
);
169+
170+
fireEvent.click(screen.getByRole("button", { name: "Account" }));
171+
172+
expect(screen.getByText("dimillian@example.com")).toBeTruthy();
173+
expect(screen.getByRole("button", { name: "Switch account" })).toBeTruthy();
174+
});
175+
156176
it("renders threads-only mode as a global chronological list", () => {
157177
const older = Date.now() - 10_000;
158178
const newer = Date.now();

src/features/app/components/Sidebar.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ import type {
1010
import { memo, useCallback, useEffect, useMemo, useState } from "react";
1111
import type { MouseEvent, RefObject } from "react";
1212
import { FolderOpen } from "lucide-react";
13-
import { SidebarCornerActions } from "./SidebarCornerActions";
14-
import { SidebarFooter } from "./SidebarFooter";
13+
import { SidebarBottomRail } from "./SidebarBottomRail";
1514
import { SidebarHeader } from "./SidebarHeader";
1615
import { SidebarSearchBar } from "./SidebarSearchBar";
1716
import { SidebarThreadsOnlySection } from "./SidebarThreadsOnlySection";
@@ -1028,15 +1027,13 @@ export const Sidebar = memo(function Sidebar({
10281027
)}
10291028
</div>
10301029
</div>
1031-
<SidebarFooter
1030+
<SidebarBottomRail
10321031
sessionPercent={sessionPercent}
10331032
weeklyPercent={weeklyPercent}
10341033
sessionResetLabel={sessionResetLabel}
10351034
weeklyResetLabel={weeklyResetLabel}
10361035
creditsLabel={creditsLabel}
10371036
showWeekly={showWeekly}
1038-
/>
1039-
<SidebarCornerActions
10401037
onOpenSettings={onOpenSettings}
10411038
onOpenDebug={onOpenDebug}
10421039
showDebugButton={showDebugButton}
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import ScrollText from "lucide-react/dist/esm/icons/scroll-text";
2+
import Settings from "lucide-react/dist/esm/icons/settings";
3+
import User from "lucide-react/dist/esm/icons/user";
4+
import X from "lucide-react/dist/esm/icons/x";
5+
import { useEffect } from "react";
6+
import {
7+
MenuTrigger,
8+
PopoverSurface,
9+
} from "../../design-system/components/popover/PopoverPrimitives";
10+
import { useMenuController } from "../hooks/useMenuController";
11+
12+
type SidebarBottomRailProps = {
13+
sessionPercent: number | null;
14+
weeklyPercent: number | null;
15+
sessionResetLabel: string | null;
16+
weeklyResetLabel: string | null;
17+
creditsLabel: string | null;
18+
showWeekly: boolean;
19+
onOpenSettings: () => void;
20+
onOpenDebug: () => void;
21+
showDebugButton: boolean;
22+
showAccountSwitcher: boolean;
23+
accountLabel: string;
24+
accountActionLabel: string;
25+
accountDisabled: boolean;
26+
accountSwitching: boolean;
27+
accountCancelDisabled: boolean;
28+
onSwitchAccount: () => void;
29+
onCancelSwitchAccount: () => void;
30+
};
31+
32+
type UsageRowProps = {
33+
label: string;
34+
percent: number | null;
35+
resetLabel: string | null;
36+
};
37+
38+
function UsageRow({ label, percent, resetLabel }: UsageRowProps) {
39+
return (
40+
<div className="sidebar-usage-row">
41+
<div className="sidebar-usage-row-head">
42+
<span className="sidebar-usage-name">{label}</span>
43+
<span className="sidebar-usage-value">
44+
{percent === null ? "--" : `${percent}%`}
45+
</span>
46+
</div>
47+
<div className="sidebar-usage-bar" aria-hidden>
48+
<span className="sidebar-usage-bar-fill" style={{ width: `${percent ?? 0}%` }} />
49+
</div>
50+
{resetLabel && <div className="sidebar-usage-reset">{resetLabel}</div>}
51+
</div>
52+
);
53+
}
54+
55+
export function SidebarBottomRail({
56+
sessionPercent,
57+
weeklyPercent,
58+
sessionResetLabel,
59+
weeklyResetLabel,
60+
creditsLabel,
61+
showWeekly,
62+
onOpenSettings,
63+
onOpenDebug,
64+
showDebugButton,
65+
showAccountSwitcher,
66+
accountLabel,
67+
accountActionLabel,
68+
accountDisabled,
69+
accountSwitching,
70+
accountCancelDisabled,
71+
onSwitchAccount,
72+
onCancelSwitchAccount,
73+
}: SidebarBottomRailProps) {
74+
const accountMenu = useMenuController();
75+
const {
76+
isOpen: accountMenuOpen,
77+
containerRef: accountMenuRef,
78+
close: closeAccountMenu,
79+
toggle: toggleAccountMenu,
80+
} = accountMenu;
81+
82+
useEffect(() => {
83+
if (!showAccountSwitcher) {
84+
closeAccountMenu();
85+
}
86+
}, [closeAccountMenu, showAccountSwitcher]);
87+
88+
return (
89+
<div className="sidebar-bottom-rail">
90+
<div className="sidebar-usage-panel">
91+
<div className="sidebar-usage-header">
92+
<div className="sidebar-usage-kicker">Usage</div>
93+
{creditsLabel && <div className="sidebar-usage-credits">{creditsLabel}</div>}
94+
</div>
95+
<div className="sidebar-usage-list">
96+
<UsageRow
97+
label="Session"
98+
percent={sessionPercent}
99+
resetLabel={sessionResetLabel}
100+
/>
101+
{showWeekly && (
102+
<UsageRow
103+
label="Weekly"
104+
percent={weeklyPercent}
105+
resetLabel={weeklyResetLabel}
106+
/>
107+
)}
108+
</div>
109+
</div>
110+
<div
111+
className={`sidebar-bottom-actions${showAccountSwitcher ? "" : " is-compact"}`}
112+
>
113+
{showAccountSwitcher && (
114+
<div className="sidebar-account-menu" ref={accountMenuRef}>
115+
<MenuTrigger
116+
isOpen={accountMenuOpen}
117+
popupRole="dialog"
118+
className="ghost sidebar-labeled-button sidebar-account-trigger"
119+
activeClassName="is-open"
120+
onClick={toggleAccountMenu}
121+
aria-label="Account"
122+
>
123+
<span className="sidebar-account-trigger-content">
124+
<span className="sidebar-account-avatar" aria-hidden>
125+
<User size={12} aria-hidden />
126+
</span>
127+
<span className="sidebar-account-trigger-label">Account</span>
128+
</span>
129+
</MenuTrigger>
130+
{accountMenuOpen && (
131+
<PopoverSurface className="sidebar-account-popover" role="dialog">
132+
<div className="sidebar-account-title">Account</div>
133+
<div className="sidebar-account-value">{accountLabel}</div>
134+
<div className="sidebar-account-actions-row">
135+
<button
136+
type="button"
137+
className="primary sidebar-account-action"
138+
onClick={onSwitchAccount}
139+
disabled={accountDisabled}
140+
aria-busy={accountSwitching}
141+
>
142+
<span className="sidebar-account-action-content">
143+
{accountSwitching && (
144+
<span className="sidebar-account-spinner" aria-hidden />
145+
)}
146+
<span>{accountActionLabel}</span>
147+
</span>
148+
</button>
149+
{accountSwitching && (
150+
<button
151+
type="button"
152+
className="secondary sidebar-account-cancel"
153+
onClick={onCancelSwitchAccount}
154+
disabled={accountCancelDisabled}
155+
aria-label="Cancel account switch"
156+
title="Cancel"
157+
>
158+
<X size={12} aria-hidden />
159+
</button>
160+
)}
161+
</div>
162+
</PopoverSurface>
163+
)}
164+
</div>
165+
)}
166+
<div className="sidebar-utility-actions">
167+
<button
168+
className="ghost sidebar-labeled-button sidebar-utility-button"
169+
type="button"
170+
onClick={onOpenSettings}
171+
aria-label="Open settings"
172+
>
173+
<Settings size={14} aria-hidden />
174+
<span>Settings</span>
175+
</button>
176+
{showDebugButton && (
177+
<button
178+
className="ghost sidebar-utility-button"
179+
type="button"
180+
onClick={onOpenDebug}
181+
aria-label="Open debug log"
182+
>
183+
<ScrollText size={14} aria-hidden />
184+
</button>
185+
)}
186+
</div>
187+
</div>
188+
</div>
189+
);
190+
}

src/features/app/components/SidebarCornerActions.tsx

Lines changed: 0 additions & 131 deletions
This file was deleted.

0 commit comments

Comments
 (0)