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

Commit 306a922

Browse files
authored
feat(ui): top New session button, My tasks section, space selector, tab icons
Sidebar feedback round four. Create moves to a New session button at the top of the nav (the Fab is gone), landing on the composer which now carries a Space dropdown to retarget where the task files — the typed draft survives the switch. Space rows trade their inline New session row for a hover plus beside the gear, and their task lists show five recents flat (no inner scroll) with View all still leading to Recents. A cross-space My tasks section sits above Spaces, each row wearing its space's name in muted text and opening the task inside that space. The header page tabs pick up their channelPages icons. Generated-By: PostHog Code Task-Id: 0331ac58-0a1c-4b4e-b884-2ff7d8e71986
1 parent ab7f302 commit 306a922

8 files changed

Lines changed: 296 additions & 71 deletions

File tree

packages/ui/src/features/canvas/components/AllSpacesSection.tsx

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -78,15 +78,7 @@ export function AllSpacesSection() {
7878

7979
return (
8080
<div className="shrink-0 border-border border-t">
81-
<div
82-
className={cn(
83-
"flex flex-col gap-px px-2 pt-1",
84-
// Open, the section ends in rows with hover controls at their right
85-
// edge — clear the floating create button. The bare header can share
86-
// its row with it.
87-
open ? "pb-12" : "pb-2",
88-
)}
89-
>
81+
<div className="flex flex-col gap-px px-2 pt-1 pb-2">
9082
{/* Same shape as a pinned space row, so the carets line up. */}
9183
<Button
9284
variant="default"

packages/ui/src/features/canvas/components/ChannelTabs.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { CHANNEL_SECTIONS } from "@posthog/ui/features/canvas/channelSections";
44
import { ChannelPinnedMenu } from "@posthog/ui/features/canvas/components/ChannelPinnedMenu";
55
import {
66
type ChannelPageKey,
7+
channelPageIcon,
78
channelPageLabel,
89
} from "@posthog/ui/features/canvas/components/channelPages";
910
import { useFeatureFlag } from "@posthog/ui/features/feature-flags/useFeatureFlag";
@@ -99,6 +100,7 @@ export function ChannelPageTabs({
99100
>
100101
{tabs.map((key) => (
101102
<TabsTrigger key={key} value={key} className="gap-1.5 px-2.5 py-2">
103+
{channelPageIcon(key, { size: 14 })}
102104
<span className="font-medium text-[13px]">
103105
{channelPageLabel(key)}
104106
</span>

packages/ui/src/features/canvas/components/ChannelsSidebar.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -134,12 +134,11 @@ export function ChannelsSidebar() {
134134
{/* The static spaces sidebar (prototype): one nav where every space
135135
expands inline with its tasks beneath it. Replaces the panes
136136
slider under this flag. The nav owns its own scroll regions
137-
(pinned spaces scroll; All spaces docks at the bottom); the Fab
138-
keeps create (task, canvas, space) in the same corner as the
139-
panes layout. */}
137+
(pinned spaces scroll; All spaces docks at the bottom) and its
138+
own create entry points (New session at the top, New space in
139+
the directory), so no Fab here. */}
140140
<Box className="relative min-h-0 flex-1">
141141
<SpacesSidebarNav />
142-
<ChannelsFab />
143142
</Box>
144143
</>
145144
) : bodyChannelsWorld ? (
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { CaretRightIcon } from "@phosphor-icons/react";
2+
import { Button, cn } from "@posthog/quill";
3+
import { useChannels } from "@posthog/ui/features/canvas/hooks/useChannels";
4+
import {
5+
normalizeChannelName,
6+
PERSONAL_CHANNEL_NAME,
7+
useTaskChannels,
8+
} from "@posthog/ui/features/canvas/hooks/useTaskChannels";
9+
import { useSpacesSidebarStore } from "@posthog/ui/features/canvas/stores/spacesSidebarStore";
10+
import { SidebarItem } from "@posthog/ui/features/sidebar/components/SidebarItem";
11+
import { useTasks } from "@posthog/ui/features/tasks/useTasks";
12+
import { useNavigate, useRouterState } from "@tanstack/react-router";
13+
import { useMemo } from "react";
14+
15+
const MY_TASKS_CAP = 10;
16+
17+
/**
18+
* The viewer's tasks across every space, newest first, each wearing its
19+
* space's name in muted text on the right. Rows open the task inside its
20+
* space; tasks that predate spaces (no backend channel) fall back to the
21+
* personal space. Same folding header shape as the space rows below it.
22+
*/
23+
export function MyTasksSection() {
24+
const open = useSpacesSidebarStore((s) => s.openMyTasks);
25+
const toggleMyTasks = useSpacesSidebarStore((s) => s.toggleMyTasks);
26+
const navigate = useNavigate();
27+
const pathname = useRouterState({ select: (s) => s.location.pathname });
28+
29+
// useTasks() without showAllUsers is already scoped to the viewer.
30+
const { data: myTasks = [] } = useTasks();
31+
const { channels: backendChannels } = useTaskChannels({ enabled: open });
32+
const { channels: folderChannels } = useChannels();
33+
34+
const rows = useMemo(() => {
35+
const backendById = new Map(backendChannels.map((c) => [c.id, c]));
36+
const folderByName = new Map(
37+
folderChannels.map((c) => [normalizeChannelName(c.name), c]),
38+
);
39+
const me = folderChannels.find((c) => c.name === PERSONAL_CHANNEL_NAME);
40+
return [...myTasks]
41+
.sort(
42+
(a, b) =>
43+
(Date.parse(b.updated_at) || 0) - (Date.parse(a.updated_at) || 0),
44+
)
45+
.slice(0, MY_TASKS_CAP)
46+
.map((task) => {
47+
const backend = task.channel
48+
? backendById.get(task.channel)
49+
: undefined;
50+
const spaceName = backend
51+
? backend.channel_type === "personal"
52+
? PERSONAL_CHANNEL_NAME
53+
: backend.name
54+
: null;
55+
const folder = spaceName
56+
? folderByName.get(normalizeChannelName(spaceName))
57+
: undefined;
58+
return {
59+
task,
60+
spaceName,
61+
// The route needs the folder channel; unmapped tasks open under #me.
62+
folderId: (folder ?? me)?.id,
63+
};
64+
});
65+
}, [myTasks, backendChannels, folderChannels]);
66+
67+
return (
68+
<div className="flex flex-col gap-px">
69+
{/* Same shape as a space row, so the carets line up. */}
70+
<Button
71+
variant="default"
72+
left
73+
aria-expanded={open}
74+
onClick={toggleMyTasks}
75+
className="w-full gap-1.5 text-left"
76+
>
77+
<CaretRightIcon
78+
size={12}
79+
className={cn(
80+
"shrink-0 text-muted-foreground transition-transform",
81+
open && "rotate-90",
82+
)}
83+
/>
84+
<span className="min-w-0 flex-1 truncate font-medium text-[13px] text-muted-foreground group-hover/button:text-foreground">
85+
My tasks
86+
</span>
87+
</Button>
88+
89+
{open && (
90+
<div className="flex flex-col gap-px pb-1 pl-5">
91+
{rows.length === 0 ? (
92+
<div className="px-2 py-1.5 text-[12px] text-muted-foreground">
93+
No tasks yet
94+
</div>
95+
) : (
96+
rows.map(({ task, spaceName, folderId }) => (
97+
<SidebarItem
98+
key={task.id}
99+
depth={0}
100+
label={<span>{task.title || "Untitled task"}</span>}
101+
isActive={pathname.endsWith(`/tasks/${task.id}`)}
102+
onClick={
103+
folderId
104+
? () =>
105+
void navigate({
106+
to: "/website/$channelId/tasks/$taskId",
107+
params: { channelId: folderId, taskId: task.id },
108+
})
109+
: undefined
110+
}
111+
endContent={
112+
spaceName ? (
113+
<span className="ml-1 max-w-20 shrink-0 truncate text-[11px] text-muted-foreground">
114+
{spaceName}
115+
</span>
116+
) : undefined
117+
}
118+
/>
119+
))
120+
)}
121+
</div>
122+
)}
123+
</div>
124+
);
125+
}

packages/ui/src/features/canvas/components/SpaceSection.tsx

Lines changed: 28 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import { useIsChannelUnread } from "@posthog/ui/features/canvas/hooks/useUnreadC
1010
import { useCurrentChannelStore } from "@posthog/ui/features/canvas/stores/currentChannelStore";
1111
import { useSpacesSidebarStore } from "@posthog/ui/features/canvas/stores/spacesSidebarStore";
1212
import { useCommandCenterStore } from "@posthog/ui/features/command-center/commandCenterStore";
13-
import { SidebarItem } from "@posthog/ui/features/sidebar/components/SidebarItem";
1413
import { useRenameTask } from "@posthog/ui/features/tasks/useTaskMutations";
1514
import { useTasks } from "@posthog/ui/features/tasks/useTasks";
1615
import { toast } from "@posthog/ui/primitives/toast";
@@ -19,15 +18,15 @@ import { track } from "@posthog/ui/shell/analytics";
1918
import { useNavigate, useRouterState } from "@tanstack/react-router";
2019
import { type Ref, useMemo, useState } from "react";
2120

22-
const RECENTS_CAP = 10;
21+
const RECENTS_CAP = 5;
2322

2423
/**
2524
* One pinned space in the static sidebar. The whole row is one click target:
2625
* it folds the space's task list open and closed (caret included — no separate
27-
* hover zones). The hover gear on the right is what opens the space itself in
28-
* the main view, where the header tabs (Feed/Context/Loops/Artifacts) live.
29-
* #me wears its lock in the same right-hand well, stepping aside for the gear
30-
* on hover.
26+
* hover zones). Hovering reveals two controls on the right: a plus that files
27+
* a new task into this space, and the gear that opens the space itself in the
28+
* main view, where the header tabs (Feed/Context/Loops/Artifacts) live. #me
29+
* wears its lock in the same right-hand well, stepping aside on hover.
3130
*
3231
* Expand state is local view state in `spacesSidebarStore`; the space's
3332
* presence in the sidebar is its star, managed from the All spaces directory.
@@ -152,11 +151,12 @@ export function SpaceSection({
152151
{channel.name}
153152
</span>
154153
{/* Trailing well, reserved so the name truncates clear of the lock
155-
and hover gear rather than shifting when they appear. */}
156-
<span aria-hidden className="size-6 shrink-0" />
154+
and the two hover controls rather than shifting when they appear. */}
155+
<span aria-hidden className="h-6 w-12 shrink-0" />
157156
</Button>
158157
{/* Overlays, not children — the row is a button already. The lock
159-
yields its spot to the gear on hover so the well never doubles up. */}
158+
yields its spot to the controls on hover so the well never doubles
159+
up. */}
160160
{glyph && (
161161
<span
162162
aria-hidden
@@ -165,6 +165,22 @@ export function SpaceSection({
165165
{glyph}
166166
</span>
167167
)}
168+
<Button
169+
variant="default"
170+
size="icon-sm"
171+
aria-label={`New task in ${channel.name}`}
172+
onClick={() => {
173+
track(ANALYTICS_EVENTS.CHANNEL_ACTION, {
174+
action_type: "new_task_open",
175+
surface: "sidebar",
176+
channel_id: channel.id,
177+
});
178+
openTaskInput({ channelId: channel.id });
179+
}}
180+
className="-translate-y-1/2 absolute top-1/2 right-[30px] text-muted-foreground opacity-0 transition-opacity focus-visible:opacity-100 group-hover/space:opacity-100"
181+
>
182+
<PlusIcon size={14} />
183+
</Button>
168184
<Button
169185
variant="default"
170186
size="icon-sm"
@@ -180,21 +196,6 @@ export function SpaceSection({
180196
groups' trees (pl-5). */}
181197
{open && (
182198
<div className="flex flex-col gap-px pb-1 pl-5">
183-
{/* Creating is the list's first move, so it leads the list — filed
184-
straight into this space, like the Fab's New task with a channel. */}
185-
<SidebarItem
186-
depth={0}
187-
icon={<PlusIcon size={14} className="text-muted-foreground" />}
188-
label={<span className="text-muted-foreground">New session</span>}
189-
onClick={() => {
190-
track(ANALYTICS_EVENTS.CHANNEL_ACTION, {
191-
action_type: "new_task_open",
192-
surface: "sidebar",
193-
channel_id: channel.id,
194-
});
195-
openTaskInput({ channelId: channel.id });
196-
}}
197-
/>
198199
{isLoading && sectionItems.length === 0 ? (
199200
<div className="flex flex-col gap-2 px-2 py-2">
200201
<Skeleton className="h-3 w-3/4" />
@@ -206,11 +207,9 @@ export function SpaceSection({
206207
</div>
207208
) : (
208209
<>
209-
{/* A five-row window (rows are ~26px + the px gaps); the rest of
210-
the capped list scrolls within it, View all stays put below.
211-
The right padding keeps the rows' trailing badges clear of
212-
the overlay scrollbar instead of drawn over by it. */}
213-
<div className="flex max-h-[134px] flex-col gap-px overflow-y-auto pr-1">
210+
{/* Five recents, no inner scroll — the full list is one click
211+
away on the space's Recents page via View all. */}
212+
<div className="flex flex-col gap-px">
214213
{sectionItems.map((item) => (
215214
<ChannelItemRow
216215
key={item.key}

packages/ui/src/features/canvas/components/SpacesSidebarNav.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,26 @@
11
import { PointerSensor } from "@dnd-kit/dom";
22
import { type DragDropEvents, DragDropProvider } from "@dnd-kit/react";
33
import { useSortable } from "@dnd-kit/react/sortable";
4+
import { PlusIcon } from "@phosphor-icons/react";
45
import {
6+
Button,
57
Switch,
68
Tooltip,
79
TooltipContent,
810
TooltipTrigger,
911
} from "@posthog/quill";
12+
import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events";
1013
import { AllSpacesSection } from "@posthog/ui/features/canvas/components/AllSpacesSection";
1114
import { ChannelNav } from "@posthog/ui/features/canvas/components/ChannelNav";
15+
import { MyTasksSection } from "@posthog/ui/features/canvas/components/MyTasksSection";
1216
import { SpaceSection } from "@posthog/ui/features/canvas/components/SpaceSection";
1317
import type { Channel } from "@posthog/ui/features/canvas/hooks/useChannels";
1418
import { useStarredChannelSlots } from "@posthog/ui/features/canvas/hooks/useStarredChannelSlots";
1519
import { PERSONAL_CHANNEL_NAME } from "@posthog/ui/features/canvas/hooks/useTaskChannels";
20+
import { useCurrentChannelStore } from "@posthog/ui/features/canvas/stores/currentChannelStore";
1621
import { useSpacesSidebarStore } from "@posthog/ui/features/canvas/stores/spacesSidebarStore";
22+
import { openTaskInput } from "@posthog/ui/router/useOpenTask";
23+
import { track } from "@posthog/ui/shell/analytics";
1724
import type { RefCallback } from "react";
1825
import { useMemo } from "react";
1926

@@ -55,6 +62,7 @@ export function SpacesSidebarNav() {
5562
const toggleOnlyMyTasks = useSpacesSidebarStore((s) => s.toggleOnlyMyTasks);
5663
const spaceOrder = useSpacesSidebarStore((s) => s.spaceOrder);
5764
const setSpaceOrder = useSpacesSidebarStore((s) => s.setSpaceOrder);
65+
const currentChannelId = useCurrentChannelStore((s) => s.currentChannelId);
5866

5967
const me = pinnedSpaces.find((c) => c.name === PERSONAL_CHANNEL_NAME);
6068
// The user's drag order over the starred set; spaces they've never dragged
@@ -88,6 +96,36 @@ export function SpacesSidebarNav() {
8896
<div className="flex h-full min-h-0 flex-col">
8997
<ChannelNav />
9098

99+
{/* The create entry point, now that the floating button is gone. Files
100+
into the space you're in; the composer's space selector can retarget
101+
it. */}
102+
<div className="shrink-0 px-2 pb-1.5">
103+
<Button
104+
variant="outline"
105+
size="sm"
106+
className="w-full justify-start gap-1.5"
107+
onClick={() => {
108+
track(ANALYTICS_EVENTS.CHANNEL_ACTION, {
109+
action_type: "new_task_open",
110+
surface: "sidebar",
111+
channel_id: currentChannelId ?? me?.id,
112+
});
113+
openTaskInput({
114+
channelId: currentChannelId ?? me?.id,
115+
space: "website",
116+
});
117+
}}
118+
>
119+
<PlusIcon size={14} />
120+
New session
121+
</Button>
122+
</div>
123+
124+
{/* The viewer's tasks across every space. */}
125+
<div className="shrink-0 px-2 pb-1">
126+
<MyTasksSection />
127+
</div>
128+
91129
{/* The section label, with one filter over every space's task list. */}
92130
<div className="flex shrink-0 items-center justify-between px-3 pb-1.5 text-[12px] text-muted-foreground">
93131
Spaces

0 commit comments

Comments
 (0)