Skip to content

Commit c6252b2

Browse files
adamleithpclaude
andcommitted
feat(channels): collapsible sidebar groups, # icons, two-step create
Group the channel sidebar under collapsible "Starred"/"Channels" headers. The whole header row is the trigger; it rests as a "#" and swaps to a chevron on hover/focus. Collapse state persists per section in the shared sidebar store, keyed `channels:*` to stay clear of the Code sidebar's folder keys. Uses Base UI's Collapsible directly rather than quill's: quill styles its trigger as a button (which fought the MenuLabel styling the headers had) and animates panel height, which janked against a list this long. The panel is `keepMounted` — every row builds a context menu, dropdown, tooltip and two dialogs up front, so unmounting on close made each expand rebuild the lot (~940ms for 46 channels, vs ~80ms to collapse). Replace every remaining SquircleDashed with a "#", so channel rows, group headers, tabs, header, breadcrumb, rename field and command palette all speak one icon. Split channel creation into two steps: name (CTA "Next"), then "what's this channel about?" with Skip/Create. Step two is a real nested dialog, so quill scales and dims step one behind it and the stack itself says there's another step; escaping step two returns to the name, intact. Nothing is created until Create (channel + CONTEXT.md plan session) or Skip (channel alone). The nesting offset is inverted from quill's default so step one peeks above rather than below. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019G63f4afY9vsbKsvK654Wj
1 parent 6e1b273 commit c6252b2

9 files changed

Lines changed: 290 additions & 158 deletions

File tree

packages/ui/src/features/browser-tabs/BrowserTabStrip.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
BrainIcon,
3+
HashIcon,
34
HouseIcon,
45
PlugsConnectedIcon,
56
RobotIcon,
@@ -50,7 +51,6 @@ import {
5051
useRouter,
5152
useRouterState,
5253
} from "@tanstack/react-router";
53-
import { SquircleDashed } from "lucide-react";
5454
import { type ReactNode, useEffect, useMemo } from "react";
5555
import { useHotkeys } from "react-hotkeys-hook";
5656
import {
@@ -526,7 +526,7 @@ export function BrowserTabStrip() {
526526
return {
527527
id: t.id,
528528
label: meta?.label ?? channel ?? "Channel",
529-
icon: <SquircleDashed size={14} />,
529+
icon: <HashIcon size={14} />,
530530
channelName: channel,
531531
// No section meta → the channel's index page.
532532
isChannelHome: !meta,

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

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { HashIcon } from "@phosphor-icons/react";
12
import {
23
Button,
34
Tooltip,
@@ -7,7 +8,6 @@ import {
78
import { HeaderTitleEditor } from "@posthog/ui/features/task-detail/HeaderTitleEditor";
89
import { Flex, Text } from "@radix-ui/themes";
910
import { useNavigate } from "@tanstack/react-router";
10-
import { SquircleDashed } from "lucide-react";
1111
import { type ReactNode, useState } from "react";
1212

1313
interface ChannelBreadcrumbProps {
@@ -48,10 +48,7 @@ export function ChannelBreadcrumb({
4848

4949
const channelSegment = (
5050
<>
51-
<SquircleDashed
52-
size={12}
53-
className="mt-px shrink-0 text-muted-foreground/80"
54-
/>
51+
<HashIcon size={12} className="mt-px shrink-0 text-muted-foreground/80" />
5552
<Text
5653
className="min-w-0 truncate whitespace-nowrap font-medium text-[13px]"
5754
title={channelName}

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

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1+
import { HashIcon } from "@phosphor-icons/react";
12
import { Button, cn } from "@posthog/quill";
23
import { ChannelTabs } from "@posthog/ui/features/canvas/components/ChannelTabs";
34
import { useChannels } from "@posthog/ui/features/canvas/hooks/useChannels";
45
import { Text } from "@radix-ui/themes";
56
import { useNavigate, useRouterState } from "@tanstack/react-router";
6-
import { SquircleDashed } from "lucide-react";
77

88
// The shared channel header: a clickable "# channel" that doubles as the Home
99
// item — it routes to the channel home (`/website/$channelId`, like the sidebar
@@ -29,10 +29,7 @@ export function ChannelHeader({ channelId }: { channelId: string }) {
2929
size="sm"
3030
className={cn("min-w-0", isHome ? "bg-fill-selected" : "")}
3131
>
32-
<SquircleDashed
33-
size={20}
34-
className="shrink-0 text-muted-foreground/80"
35-
/>
32+
<HashIcon size={20} className="shrink-0 text-muted-foreground/80" />
3633
<Text className="min-w-0 truncate font-medium" title={channelName}>
3734
{channelName ?? "Channel"}
3835
</Text>

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FileTextIcon, PlusIcon } from "@phosphor-icons/react";
1+
import { FileTextIcon, HashIcon, PlusIcon } from "@phosphor-icons/react";
22
import {
33
Button,
44
DropdownMenu,
@@ -12,7 +12,6 @@ import {
1212
import { CreateChannelModal } from "@posthog/ui/features/canvas/components/CreateChannelModal";
1313
import { openTaskInput } from "@posthog/ui/router/useOpenTask";
1414
import { useRouterState } from "@tanstack/react-router";
15-
import { SquircleDashed } from "lucide-react";
1615
import { useState } from "react";
1716

1817
// The create affordance for the Channels space, floated over the bottom-right
@@ -52,7 +51,7 @@ export function ChannelsFab() {
5251
</Tooltip>
5352
<DropdownMenuContent align="center" side="top" sideOffset={6}>
5453
<DropdownMenuItem onClick={() => setModalOpen(true)}>
55-
<SquircleDashed size={14} className="text-gray-9" />
54+
<HashIcon size={14} className="text-gray-9" />
5655
New channel
5756
</DropdownMenuItem>
5857
<DropdownMenuItem

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

Lines changed: 95 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
import { Collapsible } from "@base-ui/react/collapsible";
12
import {
3+
CaretDownIcon,
4+
CaretRightIcon,
25
ChartBarIcon,
36
DotsThreeIcon,
47
FileTextIcon,
8+
HashIcon,
59
LinkIcon,
610
LockSimpleIcon,
711
PencilSimpleIcon,
@@ -30,6 +34,8 @@ import {
3034
DropdownMenuItem,
3135
DropdownMenuSeparator,
3236
DropdownMenuTrigger,
37+
Empty,
38+
EmptyHeader,
3339
MenuLabel,
3440
Tooltip,
3541
TooltipContent,
@@ -54,11 +60,11 @@ import {
5460
useTaskChannels,
5561
} from "@posthog/ui/features/canvas/hooks/useTaskChannels";
5662
import { copyChannelLink } from "@posthog/ui/features/canvas/utils/copyChannelLink";
63+
import { useSidebarStore } from "@posthog/ui/features/sidebar/sidebarStore";
5764
import { toast } from "@posthog/ui/primitives/toast";
5865
import { track } from "@posthog/ui/shell/analytics";
59-
import { Box, Flex, Text } from "@radix-ui/themes";
66+
import { Box, Flex } from "@radix-ui/themes";
6067
import { useNavigate, useRouterState } from "@tanstack/react-router";
61-
import { SquircleDashed } from "lucide-react";
6268
import { Fragment, type ReactNode, useEffect, useRef, useState } from "react";
6369
import { hostClient } from "../hostClient";
6470

@@ -333,7 +339,7 @@ function ChannelSection({ channel }: { channel: Channel }) {
333339
}}
334340
className="w-full min-w-0 justify-start gap-2 data-selected:bg-fill-selected data-selected:text-gray-12"
335341
>
336-
<SquircleDashed size={14} className="shrink-0 text-gray-9" />
342+
<HashIcon size={14} className="shrink-0 text-gray-9" />
337343
<span
338344
className={cn(
339345
"truncate font-medium text-[13px] text-gray-12 group-hover/chan:pr-8",
@@ -551,7 +557,7 @@ function PersonalChannelRow() {
551557
onClick={() => void open()}
552558
className="w-full min-w-0 justify-start gap-2 data-selected:bg-fill-selected data-selected:text-gray-12"
553559
>
554-
<SquircleDashed size={14} className="shrink-0 text-gray-9" />
560+
<HashIcon size={14} className="shrink-0 text-gray-9" />
555561
<span className="truncate font-medium text-[13px] text-gray-12">
556562
{PERSONAL_CHANNEL_NAME}
557563
</span>
@@ -614,6 +620,77 @@ function PersonalChannelRow() {
614620
);
615621
}
616622

623+
// Collapse state is keyed per section in the shared sidebar store, so it
624+
// persists across navigation and restarts. Prefixed to stay clear of the Code
625+
// sidebar's folder sections, which key the same set by folder path.
626+
const STARRED_SECTION_ID = "channels:starred";
627+
const CHANNELS_SECTION_ID = "channels:all";
628+
629+
// A collapsible sidebar group ("Starred" / "Channels"). Base UI directly rather
630+
// than quill's Collapsible: quill styles its trigger as a button (which fought
631+
// the label styling) and animates the panel height (which janked on a list this
632+
// long). Unstyled parts give a plain label row that snaps.
633+
//
634+
// The whole header row is the trigger. It rests as a "#" and swaps to a chevron
635+
// on hover or keyboard focus, so the row only advertises the disclosure when
636+
// you're actually reaching for it.
637+
function ChannelGroup({
638+
sectionId,
639+
label,
640+
className,
641+
children,
642+
}: {
643+
sectionId: string;
644+
label: string;
645+
className?: string;
646+
children: ReactNode;
647+
}) {
648+
const collapsedSections = useSidebarStore((s) => s.collapsedSections);
649+
const toggleSection = useSidebarStore((s) => s.toggleSection);
650+
const isOpen = !collapsedSections.has(sectionId);
651+
652+
return (
653+
<Collapsible.Root
654+
open={isOpen}
655+
onOpenChange={() => toggleSection(sectionId)}
656+
className={className}
657+
>
658+
{/* MenuLabel carries the sidebar's label styling; `render` keeps it a
659+
real button so the whole row is clickable. */}
660+
<Collapsible.Trigger
661+
className="group/group-trigger flex w-full items-center gap-2"
662+
render={<MenuLabel render={<button type="button" />} />}
663+
>
664+
<span className="relative flex size-3.5 shrink-0 items-center justify-center">
665+
<HashIcon
666+
size={14}
667+
className="group-hover/group-trigger:hidden group-focus-visible/group-trigger:hidden"
668+
/>
669+
{isOpen ? (
670+
<CaretDownIcon
671+
size={14}
672+
className="hidden group-hover/group-trigger:block group-focus-visible/group-trigger:block"
673+
/>
674+
) : (
675+
<CaretRightIcon
676+
size={14}
677+
className="hidden group-hover/group-trigger:block group-focus-visible/group-trigger:block"
678+
/>
679+
)}
680+
</span>
681+
{label}
682+
</Collapsible.Trigger>
683+
{/* Stay mounted while collapsed. Every row builds a context menu, a
684+
dropdown, a tooltip and two dialogs up front, so unmounting on close
685+
makes each expand rebuild the lot (~940ms for 46 channels, vs ~80ms
686+
to collapse). */}
687+
<Collapsible.Panel keepMounted>
688+
<div className="pl-5">{children}</div>
689+
</Collapsible.Panel>
690+
</Collapsible.Root>
691+
);
692+
}
693+
617694
// The channel list — the Channels space sidebar body. The private "#me"
618695
// channel is pinned at the top; starred channels surface in their own section
619696
// so the ones you use most stay in reach; the rest sit under a "Channels"
@@ -651,39 +728,25 @@ export function ChannelsList() {
651728
<PersonalChannelRow />
652729

653730
{starred.length > 0 && (
654-
<>
655-
<Box>
656-
<MenuLabel className="flex items-center gap-2 uppercase">
657-
<StarIcon size={14} className="text-gray-9" />
658-
Starred
659-
</MenuLabel>
660-
</Box>
661-
<div className="pl-2">
662-
{starred.map((channel) => (
663-
<ChannelSection key={channel.id} channel={channel} />
664-
))}
665-
</div>
666-
</>
731+
<ChannelGroup sectionId={STARRED_SECTION_ID} label="Starred">
732+
{starred.map((channel) => (
733+
<ChannelSection key={channel.id} channel={channel} />
734+
))}
735+
</ChannelGroup>
667736
)}
668737

669-
<Box className={cn(starred.length > 0 && "mt-3")}>
670-
<MenuLabel className="flex items-center gap-2 uppercase">
671-
<SquircleDashed size={14} className="text-gray-9" />
672-
Channels
673-
</MenuLabel>
674-
</Box>
675-
676-
{!isLoading && channels.length === 0 && (
677-
<Text size="1" className="px-2 text-gray-9">
678-
No channels yet. Create one to get started.
679-
</Text>
680-
)}
681-
682-
<div className="pl-2">
738+
<ChannelGroup sectionId={CHANNELS_SECTION_ID} label="Channels">
739+
{!isLoading && channels.length === 0 && (
740+
<Empty className="px-2 py-1 text-subtle-foreground text-xs">
741+
<EmptyHeader className="text-left">
742+
No channels other channels yet.
743+
</EmptyHeader>
744+
</Empty>
745+
)}
683746
{others.map((channel) => (
684747
<ChannelSection key={channel.id} channel={channel} />
685748
))}
686-
</div>
749+
</ChannelGroup>
687750
</Flex>
688751
</TooltipProvider>
689752
);

0 commit comments

Comments
 (0)