Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

Commit 0762f2d

Browse files
committed
add sidebar more tab, customize and tracking
1 parent 3cf6615 commit 0762f2d

11 files changed

Lines changed: 406 additions & 25 deletions

File tree

packages/shared/src/analytics-events.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,32 @@ export interface CommandMenuActionProperties {
237237
channel_id?: string;
238238
}
239239

240+
export type SidebarNavItem =
241+
| "new_task"
242+
| "home"
243+
| "search"
244+
| "inbox"
245+
| "agents"
246+
| "skills"
247+
| "mcp_servers"
248+
| "command_center"
249+
| "contexts"
250+
| "activity"
251+
| "more"
252+
| "customize_sidebar";
253+
254+
export interface SidebarNavItemClickedProperties {
255+
item: SidebarNavItem;
256+
/** True when the row was clicked inside the expanded More section. */
257+
in_more: boolean;
258+
}
259+
260+
export interface SidebarCustomizedProperties {
261+
item: SidebarNavItem;
262+
/** True when the item was promoted to the top level, false when moved under More. */
263+
visible: boolean;
264+
}
265+
240266
export interface BrainrotActivatedProperties {
241267
/** Grid layout preset, e.g. "2x2". */
242268
layout: string;
@@ -1113,6 +1139,8 @@ export const ANALYTICS_EVENTS = {
11131139
BRAINROT_ACTIVATED: "Brainrot activated",
11141140
SKILL_BUTTON_TRIGGERED: "Skill button triggered",
11151141
POSTHOG_WEB_OPENED: "PostHog web opened",
1142+
SIDEBAR_NAV_ITEM_CLICKED: "Sidebar nav item clicked",
1143+
SIDEBAR_CUSTOMIZED: "Sidebar customized",
11161144

11171145
// Permission events
11181146
PERMISSION_RESPONDED: "Permission responded",
@@ -1271,6 +1299,8 @@ export type EventPropertyMap = {
12711299
[ANALYTICS_EVENTS.BRAINROT_ACTIVATED]: BrainrotActivatedProperties;
12721300
[ANALYTICS_EVENTS.SKILL_BUTTON_TRIGGERED]: SkillButtonTriggeredProperties;
12731301
[ANALYTICS_EVENTS.POSTHOG_WEB_OPENED]: never;
1302+
[ANALYTICS_EVENTS.SIDEBAR_NAV_ITEM_CLICKED]: SidebarNavItemClickedProperties;
1303+
[ANALYTICS_EVENTS.SIDEBAR_CUSTOMIZED]: SidebarCustomizedProperties;
12741304

12751305
// Permission events
12761306
[ANALYTICS_EVENTS.PERMISSION_RESPONDED]: PermissionRespondedProperties;
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import {
2+
type Icon,
3+
Lightbulb,
4+
MagnifyingGlass,
5+
Plugs,
6+
} from "@phosphor-icons/react";
7+
import {
8+
ANALYTICS_EVENTS,
9+
type SidebarNavItem,
10+
} from "@posthog/shared/analytics-events";
11+
import {
12+
MORE_NAV_ITEMS,
13+
type MoreNavItemId,
14+
} from "@posthog/ui/features/sidebar/constants";
15+
import { useSidebarStore } from "@posthog/ui/features/sidebar/sidebarStore";
16+
import { track } from "@posthog/ui/shell/analytics";
17+
import { Button, Checkbox, Dialog, Flex, Text } from "@radix-ui/themes";
18+
19+
const ITEM_ICONS: Record<MoreNavItemId, Icon> = {
20+
search: MagnifyingGlass,
21+
skills: Lightbulb,
22+
"mcp-servers": Plugs,
23+
};
24+
25+
const ITEM_ANALYTICS_IDS: Record<MoreNavItemId, SidebarNavItem> = {
26+
search: "search",
27+
skills: "skills",
28+
"mcp-servers": "mcp_servers",
29+
};
30+
31+
interface CustomizeSidebarDialogProps {
32+
open: boolean;
33+
onOpenChange: (open: boolean) => void;
34+
}
35+
36+
export function CustomizeSidebarDialog({
37+
open,
38+
onOpenChange,
39+
}: CustomizeSidebarDialogProps) {
40+
const hiddenNavItems = useSidebarStore((s) => s.hiddenNavItems);
41+
const setNavItemHidden = useSidebarStore((s) => s.setNavItemHidden);
42+
43+
return (
44+
<Dialog.Root open={open} onOpenChange={onOpenChange}>
45+
<Dialog.Content maxWidth="360px">
46+
<Dialog.Title>Customize sidebar</Dialog.Title>
47+
<Dialog.Description className="text-gray-10 text-sm">
48+
Choose which items appear in your sidebar. Unchecked items live under
49+
More.
50+
</Dialog.Description>
51+
52+
<Flex direction="column" gap="3" mt="4">
53+
{MORE_NAV_ITEMS.map(({ id, label }) => {
54+
const ItemIcon = ITEM_ICONS[id];
55+
const visible = !hiddenNavItems.includes(id);
56+
return (
57+
<Text key={id} as="label" size="2">
58+
<Flex gap="2" align="center">
59+
<Checkbox
60+
checked={visible}
61+
onCheckedChange={(checked) => {
62+
const nextVisible = checked === true;
63+
setNavItemHidden(id, !nextVisible);
64+
track(ANALYTICS_EVENTS.SIDEBAR_CUSTOMIZED, {
65+
item: ITEM_ANALYTICS_IDS[id],
66+
visible: nextVisible,
67+
});
68+
}}
69+
/>
70+
<ItemIcon size={16} />
71+
{label}
72+
</Flex>
73+
</Text>
74+
);
75+
})}
76+
</Flex>
77+
78+
<Flex mt="4" justify="end">
79+
<Dialog.Close>
80+
<Button variant="solid">Done</Button>
81+
</Dialog.Close>
82+
</Flex>
83+
</Dialog.Content>
84+
</Dialog.Root>
85+
);
86+
}

packages/ui/src/features/sidebar/components/SidebarNavSection.tsx

Lines changed: 139 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
import { Badge, Switch } from "@posthog/quill";
22
import { PROJECT_BLUEBIRD_FLAG } from "@posthog/shared";
3-
import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events";
3+
import {
4+
ANALYTICS_EVENTS,
5+
type SidebarNavItem,
6+
} from "@posthog/shared/analytics-events";
47
import { HOME_TAB_FLAG } from "@posthog/shared/constants";
58
import { useCommandCenterStore } from "@posthog/ui/features/command-center/commandCenterStore";
69
import { useFeatureFlag } from "@posthog/ui/features/feature-flags/useFeatureFlag";
710
import { useInboxAllReports } from "@posthog/ui/features/inbox/hooks/useInboxAllReports";
11+
import {
12+
MORE_NAV_ITEMS,
13+
type MoreNavItemId,
14+
} from "@posthog/ui/features/sidebar/constants";
815
import { useSidebarStore } from "@posthog/ui/features/sidebar/sidebarStore";
916
import { useTasks } from "@posthog/ui/features/tasks/useTasks";
1017
import {
@@ -27,12 +34,16 @@ import { useCommandMenuStore } from "@posthog/ui/shell/commandMenuStore";
2734
import { Box, Flex } from "@radix-ui/themes";
2835
import { useRouterState } from "@tanstack/react-router";
2936
import { SquircleDashed } from "lucide-react";
37+
import { useState } from "react";
38+
import { CustomizeSidebarDialog } from "./CustomizeSidebarDialog";
3039
import { ActivityItem } from "./items/ActivityItem";
3140
import { AgentsItem } from "./items/AgentsItem";
3241
import { CommandCenterItem } from "./items/CommandCenterItem";
42+
import { CustomizeSidebarItem } from "./items/CustomizeSidebarItem";
3343
import { HomeItem } from "./items/HomeItem";
3444
import { InboxItem } from "./items/InboxItem";
3545
import { McpServersItem } from "./items/McpServersItem";
46+
import { MoreItem } from "./items/MoreItem";
3647
import { NewTaskItem } from "./items/NewTaskItem";
3748
import { SearchItem } from "./items/SearchItem";
3849
import { SkillsItem } from "./items/SkillsItem";
@@ -54,7 +65,9 @@ interface SidebarNavSectionProps {
5465
// into either layout. In the Channels space, destinations with a /website
5566
// mirror (Home, Skills, MCP servers, Command Center) stay in that space;
5667
// Inbox, Agents and New task have no mirror yet and jump back to Code.
57-
// Search opens the command menu in place.
68+
// Search opens the command menu in place. Search, Skills and MCP servers are
69+
// tucked under the collapsible More row by default; the Customize sidebar
70+
// dialog promotes them back to the top level.
5871
export function SidebarNavSection({
5972
commandCenterActiveCount: providedActiveCount,
6073
}: SidebarNavSectionProps = {}) {
@@ -133,50 +146,149 @@ export function SidebarNavSection({
133146

134147
const openCommandMenu = useCommandMenuStore((s) => s.open);
135148

149+
// Every nav row reports which item was clicked so per-item usage is
150+
// measurable; in_more distinguishes clicks inside the expanded More section.
151+
const withNavTrack =
152+
(item: SidebarNavItem, action: () => void, inMore = false) =>
153+
() => {
154+
track(ANALYTICS_EVENTS.SIDEBAR_NAV_ITEM_CLICKED, {
155+
item,
156+
in_more: inMore,
157+
});
158+
action();
159+
};
160+
161+
const hiddenNavItems = useSidebarStore((s) => s.hiddenNavItems);
162+
const hidden = new Set<MoreNavItemId>(hiddenNavItems);
163+
const [moreExpanded, setMoreExpanded] = useState(false);
164+
const [customizeOpen, setCustomizeOpen] = useState(false);
165+
166+
// While More is collapsed, an active item hidden under it takes over the
167+
// More row so the current page stays visible in the nav.
168+
const moreItemActive: Record<MoreNavItemId, boolean> = {
169+
search: false,
170+
skills: isSkillsActive,
171+
"mcp-servers": isMcpServersActive,
172+
};
173+
const activeHiddenItem = MORE_NAV_ITEMS.find(
174+
({ id }) => hidden.has(id) && moreItemActive[id],
175+
);
176+
const takeoverLabel =
177+
!moreExpanded && activeHiddenItem ? activeHiddenItem.label : null;
178+
136179
return (
137180
<Flex direction="column" className="shrink-0 gap-px px-2 py-2">
138181
<Box mb="2">
139-
<NewTaskItem isActive={isHomeActive} onClick={goNewTask} />
182+
<NewTaskItem
183+
isActive={isHomeActive}
184+
onClick={withNavTrack("new_task", goNewTask)}
185+
/>
140186
</Box>
141187

142188
{homeTabEnabled && (
143189
<Box>
144-
<HomeItem isActive={isHomeViewActive} onClick={goHome} />
190+
<HomeItem
191+
isActive={isHomeViewActive}
192+
onClick={withNavTrack("home", goHome)}
193+
/>
145194
</Box>
146195
)}
147196

148-
<Box>
149-
<SearchItem onClick={openCommandMenu} />
150-
</Box>
197+
{!hidden.has("search") && (
198+
<Box>
199+
<SearchItem onClick={withNavTrack("search", openCommandMenu)} />
200+
</Box>
201+
)}
151202

152203
<Box>
153204
<InboxItem
154205
isActive={isInboxActive}
155-
onClick={navigateToInbox}
206+
onClick={withNavTrack("inbox", navigateToInbox)}
156207
pullRequestCount={inboxPullRequestCount}
157208
/>
158209
</Box>
159210

160211
<Box>
161-
<AgentsItem isActive={isAgentsActive} onClick={navigateToAgents} />
212+
<AgentsItem
213+
isActive={isAgentsActive}
214+
onClick={withNavTrack("agents", navigateToAgents)}
215+
/>
162216
</Box>
163217

164-
<Box>
165-
<SkillsItem isActive={isSkillsActive} onClick={goSkills} />
166-
</Box>
218+
{!hidden.has("skills") && (
219+
<Box>
220+
<SkillsItem
221+
isActive={isSkillsActive}
222+
onClick={withNavTrack("skills", goSkills)}
223+
/>
224+
</Box>
225+
)}
167226

168-
<Box>
169-
<McpServersItem isActive={isMcpServersActive} onClick={goMcpServers} />
170-
</Box>
227+
{!hidden.has("mcp-servers") && (
228+
<Box>
229+
<McpServersItem
230+
isActive={isMcpServersActive}
231+
onClick={withNavTrack("mcp_servers", goMcpServers)}
232+
/>
233+
</Box>
234+
)}
171235

172-
<Box mb={bluebirdEnabled ? undefined : "2"}>
236+
<Box>
173237
<CommandCenterItem
174238
isActive={isCommandCenterActive}
175-
onClick={goCommandCenter}
239+
onClick={withNavTrack("command_center", goCommandCenter)}
176240
activeCount={commandCenterActiveCount}
177241
/>
178242
</Box>
179243

244+
{/* Everything the user shoved off the top level lives here, plus the
245+
Customize entry point. Collapsed by default; when a hidden item is
246+
the active page it takes over the More row (see takeoverLabel). */}
247+
<Flex
248+
direction="column"
249+
className="gap-px"
250+
mb={bluebirdEnabled ? undefined : "2"}
251+
>
252+
<MoreItem
253+
expanded={moreExpanded}
254+
activeItemLabel={takeoverLabel}
255+
onClick={withNavTrack("more", () => setMoreExpanded((e) => !e))}
256+
/>
257+
258+
{moreExpanded && (
259+
<>
260+
{hidden.has("search") && (
261+
<SearchItem
262+
depth={1}
263+
onClick={withNavTrack("search", openCommandMenu, true)}
264+
/>
265+
)}
266+
{hidden.has("skills") && (
267+
<SkillsItem
268+
depth={1}
269+
isActive={isSkillsActive}
270+
onClick={withNavTrack("skills", goSkills, true)}
271+
/>
272+
)}
273+
{hidden.has("mcp-servers") && (
274+
<McpServersItem
275+
depth={1}
276+
isActive={isMcpServersActive}
277+
onClick={withNavTrack("mcp_servers", goMcpServers, true)}
278+
/>
279+
)}
280+
<CustomizeSidebarItem
281+
depth={1}
282+
onClick={withNavTrack(
283+
"customize_sidebar",
284+
() => setCustomizeOpen(true),
285+
true,
286+
)}
287+
/>
288+
</>
289+
)}
290+
</Flex>
291+
180292
{/* "Channels" is a toggle laid out as a nav row: the # label and Alpha
181293
badge on the left, a Switch on the right. It flips the channels
182294
feature rather than routing — enabling it reveals the Canvas row
@@ -199,6 +311,10 @@ export function SidebarNavSection({
199311
checked={channelsEnabled}
200312
onCheckedChange={(checked) => {
201313
setChannelsEnabled(checked);
314+
track(ANALYTICS_EVENTS.SIDEBAR_NAV_ITEM_CLICKED, {
315+
item: "contexts",
316+
in_more: false,
317+
});
202318
track(ANALYTICS_EVENTS.CHANNEL_ACTION, {
203319
action_type: "toggle_channels",
204320
surface: "nav",
@@ -222,10 +338,15 @@ export function SidebarNavSection({
222338
<Box>
223339
<ActivityItem
224340
isActive={isActivityActive}
225-
onClick={navigateToActivity}
341+
onClick={withNavTrack("activity", navigateToActivity)}
226342
/>
227343
</Box>
228344
)}
345+
346+
<CustomizeSidebarDialog
347+
open={customizeOpen}
348+
onOpenChange={setCustomizeOpen}
349+
/>
229350
</Flex>
230351
);
231352
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { SlidersHorizontal } from "@phosphor-icons/react";
2+
import { SidebarItem } from "../SidebarItem";
3+
4+
interface CustomizeSidebarItemProps {
5+
onClick: () => void;
6+
depth?: number;
7+
}
8+
9+
export function CustomizeSidebarItem({
10+
onClick,
11+
depth = 0,
12+
}: CustomizeSidebarItemProps) {
13+
return (
14+
<SidebarItem
15+
depth={depth}
16+
icon={<SlidersHorizontal size={16} />}
17+
label="Customize sidebar"
18+
onClick={onClick}
19+
/>
20+
);
21+
}

0 commit comments

Comments
 (0)