From 4350c9e07b8b9c2d859f91401636ab6d2244a0c2 Mon Sep 17 00:00:00 2001 From: Shy Alter Date: Tue, 28 Jul 2026 14:03:12 +0200 Subject: [PATCH 1/7] fix(spaces): restore section and space icons Generated-By: PostHog Code Task-Id: 69df779b-b72b-45bc-a11b-3deeff24ad49 --- .../features/browser-tabs/BrowserTabStrip.tsx | 9 ++++- packages/ui/src/features/canvas/AGENTS.md | 6 +-- .../canvas/components/ChannelBackRow.tsx | 3 ++ .../canvas/components/ChannelBreadcrumb.tsx | 3 ++ .../canvas/components/ChannelHeader.tsx | 1 + .../canvas/components/ChannelsFab.tsx | 3 +- .../canvas/components/ChannelsList.test.tsx | 8 ++++ .../canvas/components/ChannelsList.tsx | 38 ++++++++++++------- .../canvas/components/RenameChannelModal.tsx | 4 +- .../canvas/components/channelGlyph.test.tsx | 17 ++++++++- .../canvas/components/channelGlyph.tsx | 18 +++++++-- .../ui/src/features/command/CommandMenu.tsx | 1 + 12 files changed, 84 insertions(+), 27 deletions(-) diff --git a/packages/ui/src/features/browser-tabs/BrowserTabStrip.tsx b/packages/ui/src/features/browser-tabs/BrowserTabStrip.tsx index 5c36b5284d..8e82b3a126 100644 --- a/packages/ui/src/features/browser-tabs/BrowserTabStrip.tsx +++ b/packages/ui/src/features/browser-tabs/BrowserTabStrip.tsx @@ -1,5 +1,6 @@ import { BrainIcon, + CubeIcon, HashIcon, PlugsConnectedIcon, RobotIcon, @@ -505,7 +506,7 @@ export function BrowserTabStrip() { }; } // A channel tab: a sub-section (Artifacts/Recents/…) or the channel home. - // The section drives the label; the channel name carries the `#` hover + // The section drives the label; the channel name carries the space // context. Home has no section, so it labels by the channel name. if (channelId) { const meta = channelSectionFor(section); @@ -513,7 +514,11 @@ export function BrowserTabStrip() { id: t.id, label: meta?.label ?? channel ?? (spacesLayout ? "Space" : "Channel"), - icon: , + icon: spacesLayout ? ( + + ) : ( + + ), channelName: channel, // No section meta → the channel's index page. isChannelHome: !meta, diff --git a/packages/ui/src/features/canvas/AGENTS.md b/packages/ui/src/features/canvas/AGENTS.md index 5c576960b7..15889bebaa 100644 --- a/packages/ui/src/features/canvas/AGENTS.md +++ b/packages/ui/src/features/canvas/AGENTS.md @@ -47,9 +47,9 @@ The root `AGENTS.md` architecture rules still apply. slide and returning to the list doesn't rebuild every row. A two-finger horizontal swipe moves between them (`useChannelPaneSwipe`, wheel `deltaX` accumulated per gesture and locked until the wheel goes quiet). -- In the list, "Starred"/"Channels" are headings, not parents: under the layout - the rows sit at the heading's level (no indent) and the "#"/lock glyph belongs - to the rows. The alpha's indented tree is unchanged. +- In the list, "Starred"/"Spaces" are headings, not parents: under the layout + the rows sit at the heading's level (no indent) and the cube/lock glyph belongs + to the rows. The alpha's indented Channels tree and hash glyphs are unchanged. - One `ChannelsFab` serves both panes: given a `channelId` it creates inside that channel (task, canvas), and either way it can create a channel. Off the layout it keeps its original two-item menu. Archived moves out of the sidebar diff --git a/packages/ui/src/features/canvas/components/ChannelBackRow.tsx b/packages/ui/src/features/canvas/components/ChannelBackRow.tsx index 99d24c87fd..e9a0712b07 100644 --- a/packages/ui/src/features/canvas/components/ChannelBackRow.tsx +++ b/packages/ui/src/features/canvas/components/ChannelBackRow.tsx @@ -7,6 +7,7 @@ import { type Channel, useChannels, } from "@posthog/ui/features/canvas/hooks/useChannels"; +import { useChannelsLayout } from "@posthog/ui/features/canvas/hooks/useChannelsLayout"; import { PERSONAL_CHANNEL_NAME } from "@posthog/ui/features/canvas/hooks/useTaskChannels"; import { showChannelList } from "@posthog/ui/features/canvas/stores/channelPaneStore"; import { Tooltip } from "@posthog/ui/primitives/Tooltip"; @@ -45,6 +46,7 @@ function RowStar({ channel }: { channel: Channel }) { * it. Leaving the channel scoped means the route (and the main pane) stay put. */ export function ChannelBackRow({ channelId }: { channelId: string }) { + const spacesLayout = useChannelsLayout(); const { channels, isLoading } = useChannels(); const current = channels.find((c) => c.id === channelId); const showStar = current != null && current.name !== PERSONAL_CHANNEL_NAME; @@ -77,6 +79,7 @@ export function ChannelBackRow({ channelId }: { channelId: string }) { {channelGlyph(current?.name, { size: 14, + space: spacesLayout, className: "text-muted-foreground", })} diff --git a/packages/ui/src/features/canvas/components/ChannelBreadcrumb.tsx b/packages/ui/src/features/canvas/components/ChannelBreadcrumb.tsx index 763d156e32..e47614aa05 100644 --- a/packages/ui/src/features/canvas/components/ChannelBreadcrumb.tsx +++ b/packages/ui/src/features/canvas/components/ChannelBreadcrumb.tsx @@ -5,6 +5,7 @@ import { TooltipTrigger, } from "@posthog/quill"; import { channelGlyph } from "@posthog/ui/features/canvas/components/channelGlyph"; +import { useChannelsLayout } from "@posthog/ui/features/canvas/hooks/useChannelsLayout"; import { HeaderTitleEditor } from "@posthog/ui/features/task-detail/HeaderTitleEditor"; import { Flex, Text } from "@radix-ui/themes"; import { useNavigate } from "@tanstack/react-router"; @@ -45,6 +46,7 @@ export function ChannelBreadcrumb({ onRename, trailing, }: ChannelBreadcrumbProps) { + const spacesLayout = useChannelsLayout(); const currentEditScope = editScopeKey ?? leafLabel; const [editingScope, setEditingScope] = useState(null); const editing = editingScope === currentEditScope; @@ -54,6 +56,7 @@ export function ChannelBreadcrumb({ <> {channelGlyph(channelName, { size: 12, + space: spacesLayout, className: "mt-px shrink-0 text-muted-foreground/80", })} {channelGlyph(channelName, { size: 20, + space: channelsLayout, className: "shrink-0 text-muted-foreground/80", })} diff --git a/packages/ui/src/features/canvas/components/ChannelsFab.tsx b/packages/ui/src/features/canvas/components/ChannelsFab.tsx index a7e8be4fc0..0596061a2d 100644 --- a/packages/ui/src/features/canvas/components/ChannelsFab.tsx +++ b/packages/ui/src/features/canvas/components/ChannelsFab.tsx @@ -1,5 +1,6 @@ import { ChartBarIcon, + CubeIcon, FileTextIcon, HashIcon, PlusIcon, @@ -70,7 +71,7 @@ export function ChannelsFab({ channelId }: { channelId?: string }) { const newChannelItem = ( setModalOpen(true)}> - + {channelsLayout ? : } {channelsLayout ? "New space" : "New channel"} ); diff --git a/packages/ui/src/features/canvas/components/ChannelsList.test.tsx b/packages/ui/src/features/canvas/components/ChannelsList.test.tsx index 56e00378ee..ad25afb91d 100644 --- a/packages/ui/src/features/canvas/components/ChannelsList.test.tsx +++ b/packages/ui/src/features/canvas/components/ChannelsList.test.tsx @@ -120,6 +120,14 @@ describe("ChannelsList", () => { renderList(); expect(screen.getByText("Channels")).toBeTruthy(); }); + + it("shows section icons until the heading is hovered", () => { + renderList(); + + expect(screen.getByTestId("starred-section-icon")).toBeTruthy(); + expect(screen.getByTestId("spaces-section-icon")).toBeTruthy(); + expect(screen.getByTestId("channels:all-caret-up")).toBeTruthy(); + }); }); describe("search", () => { diff --git a/packages/ui/src/features/canvas/components/ChannelsList.tsx b/packages/ui/src/features/canvas/components/ChannelsList.tsx index abd850d870..9a7a8f5b30 100644 --- a/packages/ui/src/features/canvas/components/ChannelsList.tsx +++ b/packages/ui/src/features/canvas/components/ChannelsList.tsx @@ -1,7 +1,7 @@ import { Collapsible } from "@base-ui/react/collapsible"; import { CaretDownIcon, - CaretRightIcon, + CaretUpIcon, ChartBarIcon, DotsThreeIcon, FileTextIcon, @@ -9,6 +9,7 @@ import { LinkIcon, PencilSimpleIcon, PlusIcon, + SquaresFourIcon, StarIcon, TrashIcon, } from "@phosphor-icons/react"; @@ -442,6 +443,7 @@ function ChannelSection({ > {channelGlyph(channel.name, { size: 14, + space: spacesLayout, weight: isUnread ? "bold" : undefined, className: cn( "shrink-0", @@ -729,6 +731,7 @@ function PersonalChannelRow({ hotkeySlot }: { hotkeySlot?: number }) { > {channelGlyph(PERSONAL_CHANNEL_NAME, { size: 14, + space: spacesLayout, weight: isUnread ? "bold" : undefined, className: cn( "shrink-0", @@ -812,17 +815,16 @@ const CHANNELS_SECTION_ID = "channels:all"; // the label styling) and animates the panel height (which janked on a list this // long). Unstyled parts give a plain label row that snaps. // -// The whole header row is the trigger. Under the layout the icon well rests -// empty and fills with a chevron on hover or keyboard focus, so the row only -// advertises the disclosure when you're reaching for it — a "#" there read as a -// channel named "Starred", and the glyph belongs to the rows, not the label -// above them. +// The whole header row is the trigger. Its section glyph swaps to the current +// disclosure caret on hover or keyboard focus, matching the old sidebar while +// keeping Starred and Spaces distinct at rest. function ChannelGroup({ sectionId, label, className, flat, keepMounted = true, + icon, children, }: { sectionId: string; @@ -836,6 +838,7 @@ function ChannelGroup({ * rebuild on expand is better than highlighting a row nobody can see. */ keepMounted?: boolean; + icon: ReactNode; children: ReactNode; }) { const collapsedSections = useSidebarStore((s) => s.collapsedSections); @@ -860,20 +863,19 @@ function ChannelGroup({ render={} />} > - {!flat && ( - - )} + + {icon} + {isOpen ? ( - ) : ( - )} @@ -1006,6 +1008,7 @@ export function ChannelsList() { label="Starred" flat={channelsLayout} keepMounted={!channelsLayout} + icon={} > {starred.map((channel) => ( + ) : ( + + ) + } > {!isLoading && channels.length === 0 && ( diff --git a/packages/ui/src/features/canvas/components/RenameChannelModal.tsx b/packages/ui/src/features/canvas/components/RenameChannelModal.tsx index 79b7632730..64a0f58719 100644 --- a/packages/ui/src/features/canvas/components/RenameChannelModal.tsx +++ b/packages/ui/src/features/canvas/components/RenameChannelModal.tsx @@ -1,4 +1,4 @@ -import { HashIcon, XIcon } from "@phosphor-icons/react"; +import { CubeIcon, HashIcon, XIcon } from "@phosphor-icons/react"; import { validateChannelName } from "@posthog/core/canvas/channelName"; import { Button } from "@posthog/quill"; import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events"; @@ -114,7 +114,7 @@ export function RenameChannelModal({ }} > - + {spacesLayout ? : } diff --git a/packages/ui/src/features/canvas/components/channelGlyph.test.tsx b/packages/ui/src/features/canvas/components/channelGlyph.test.tsx index c5c9721736..27401260b1 100644 --- a/packages/ui/src/features/canvas/components/channelGlyph.test.tsx +++ b/packages/ui/src/features/canvas/components/channelGlyph.test.tsx @@ -1,5 +1,7 @@ +import { CubeIcon, HashIcon, LockSimpleIcon } from "@phosphor-icons/react"; +import type { ReactElement } from "react"; import { describe, expect, it } from "vitest"; -import { isPrivateChannel } from "./channelGlyph"; +import { channelGlyph, isPrivateChannel } from "./channelGlyph"; describe("isPrivateChannel", () => { it.each([ @@ -17,3 +19,16 @@ describe("isPrivateChannel", () => { expect(isPrivateChannel(name)).toBe(expected); }); }); + +describe("channelGlyph", () => { + it.each([ + ["channel", false, HashIcon], + ["space", true, CubeIcon], + ["private space", true, LockSimpleIcon], + ])("renders the %s glyph", (_, space, expectedIcon) => { + const name = expectedIcon === LockSimpleIcon ? "me" : "engineering"; + const glyph = channelGlyph(name, { space }) as ReactElement; + + expect(glyph.type).toBe(expectedIcon); + }); +}); diff --git a/packages/ui/src/features/canvas/components/channelGlyph.tsx b/packages/ui/src/features/canvas/components/channelGlyph.tsx index f312c0d000..8e790e3548 100644 --- a/packages/ui/src/features/canvas/components/channelGlyph.tsx +++ b/packages/ui/src/features/canvas/components/channelGlyph.tsx @@ -1,4 +1,5 @@ import { + CubeIcon, HashIcon, type IconWeight, LockSimpleIcon, @@ -23,14 +24,23 @@ export function isPrivateChannel(channelName: string | undefined): boolean { } /** - * A channel's leading glyph: a lock when it's private, otherwise a hash — the - * Slack convention, so privacy reads at a glance instead of having to be known. + * A channel's leading glyph: a lock when it's private, otherwise a cube for the + * Spaces layout or a hash for legacy Channels. */ export function channelGlyph( channelName: string | undefined, - opts?: { size?: number; className?: string; weight?: IconWeight }, + opts?: { + size?: number; + className?: string; + weight?: IconWeight; + space?: boolean; + }, ): ReactNode { - const Icon = isPrivateChannel(channelName) ? LockSimpleIcon : HashIcon; + const Icon = isPrivateChannel(channelName) + ? LockSimpleIcon + : opts?.space + ? CubeIcon + : HashIcon; return ( Date: Tue, 28 Jul 2026 14:03:14 +0200 Subject: [PATCH 2/7] fix(spaces): adjust section carets and row indentation Generated-By: PostHog Code Task-Id: 69df779b-b72b-45bc-a11b-3deeff24ad49 --- packages/ui/src/features/canvas/AGENTS.md | 6 ++--- .../canvas/components/ChannelsList.test.tsx | 24 +++++++++++++------ .../canvas/components/ChannelsList.tsx | 18 +++++++------- 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/packages/ui/src/features/canvas/AGENTS.md b/packages/ui/src/features/canvas/AGENTS.md index 15889bebaa..4d0b0ca17f 100644 --- a/packages/ui/src/features/canvas/AGENTS.md +++ b/packages/ui/src/features/canvas/AGENTS.md @@ -47,9 +47,9 @@ The root `AGENTS.md` architecture rules still apply. slide and returning to the list doesn't rebuild every row. A two-finger horizontal swipe moves between them (`useChannelPaneSwipe`, wheel `deltaX` accumulated per gesture and locked until the wheel goes quiet). -- In the list, "Starred"/"Spaces" are headings, not parents: under the layout - the rows sit at the heading's level (no indent) and the cube/lock glyph belongs - to the rows. The alpha's indented Channels tree and hash glyphs are unchanged. +- In the list, "Starred"/"Spaces" are headings above lightly indented rows; the + cube/lock glyph belongs to each row. The alpha's more deeply indented Channels + tree and hash glyphs are unchanged. - One `ChannelsFab` serves both panes: given a `channelId` it creates inside that channel (task, canvas), and either way it can create a channel. Off the layout it keeps its original two-item menu. Archived moves out of the sidebar diff --git a/packages/ui/src/features/canvas/components/ChannelsList.test.tsx b/packages/ui/src/features/canvas/components/ChannelsList.test.tsx index ad25afb91d..a607c4cfdd 100644 --- a/packages/ui/src/features/canvas/components/ChannelsList.test.tsx +++ b/packages/ui/src/features/canvas/components/ChannelsList.test.tsx @@ -93,23 +93,28 @@ describe("ChannelsList", () => { expect(me.parentElement?.textContent).toMatch(/me(⌘|Ctrl)/); }); - // "Starred" and "Channels" are headings over the rows, not parents of them — - // under the layout the rows sit at the heading's level and keep the "#" for - // themselves. The alpha's tree is unchanged. + // "Starred" and "Spaces" are headings over the rows. Spaces receive a small + // Slack-style inset; the alpha keeps its deeper tree indentation. describe("group headings", () => { beforeEach(() => { mocks.starredPaths = [ENG.path]; }); - it("does not indent rows under the layout", () => { + it("slightly indents rows under the layout", () => { renderList(); - expect(screen.getByText("engineering").closest(".pl-5")).toBeNull(); + expect(screen.getByText("engineering").closest("button")).toHaveClass( + "pl-4", + ); + expect(screen.getByText("me").closest("button")).toHaveClass("pl-4"); }); it("keeps the indented tree off the layout", () => { mocks.channelsLayout = false; renderList(); expect(screen.getByText("engineering").closest(".pl-5")).toBeTruthy(); + expect(screen.getByText("engineering").closest("button")).not.toHaveClass( + "pl-4", + ); }); it("rebrands only the spaces layout", () => { @@ -121,12 +126,17 @@ describe("ChannelsList", () => { expect(screen.getByText("Channels")).toBeTruthy(); }); - it("shows section icons until the heading is hovered", () => { + it("shows section icons and the caret for the current state", async () => { + const user = userEvent.setup(); renderList(); expect(screen.getByTestId("starred-section-icon")).toBeTruthy(); expect(screen.getByTestId("spaces-section-icon")).toBeTruthy(); - expect(screen.getByTestId("channels:all-caret-up")).toBeTruthy(); + expect(screen.getByTestId("channels:all-caret-down")).toBeTruthy(); + + await user.click(screen.getByText("Spaces")); + + expect(screen.getByTestId("channels:all-caret-right")).toBeTruthy(); }); }); diff --git a/packages/ui/src/features/canvas/components/ChannelsList.tsx b/packages/ui/src/features/canvas/components/ChannelsList.tsx index 9a7a8f5b30..cc9544e6f4 100644 --- a/packages/ui/src/features/canvas/components/ChannelsList.tsx +++ b/packages/ui/src/features/canvas/components/ChannelsList.tsx @@ -1,7 +1,7 @@ import { Collapsible } from "@base-ui/react/collapsible"; import { CaretDownIcon, - CaretUpIcon, + CaretRightIcon, ChartBarIcon, DotsThreeIcon, FileTextIcon, @@ -440,6 +440,7 @@ function ChannelSection({ data-selected={isActive || undefined} onClick={() => openChannel(channel)} {...focusProps} + className={spacesLayout ? "pl-4" : undefined} > {channelGlyph(channel.name, { size: 14, @@ -728,6 +729,7 @@ function PersonalChannelRow({ hotkeySlot }: { hotkeySlot?: number }) { data-selected={isActive || undefined} disabled={isCreating} onClick={() => void open()} + className={spacesLayout ? "pl-4" : undefined} > {channelGlyph(PERSONAL_CHANNEL_NAME, { size: 14, @@ -815,9 +817,9 @@ const CHANNELS_SECTION_ID = "channels:all"; // the label styling) and animates the panel height (which janked on a list this // long). Unstyled parts give a plain label row that snaps. // -// The whole header row is the trigger. Its section glyph swaps to the current -// disclosure caret on hover or keyboard focus, matching the old sidebar while -// keeping Starred and Spaces distinct at rest. +// The whole header row is the trigger. Its section glyph swaps to a down/right +// disclosure caret on hover or keyboard focus while keeping Starred and Spaces +// distinct at rest. function ChannelGroup({ sectionId, label, @@ -867,15 +869,15 @@ function ChannelGroup({ {icon} {isOpen ? ( - ) : ( - )} From 662e8f2c67d05d60be7b5209f7fc8f1c308df398 Mon Sep 17 00:00:00 2001 From: Shy Alter Date: Tue, 28 Jul 2026 14:03:16 +0200 Subject: [PATCH 3/7] fix(spaces): use cube focus section icon Generated-By: PostHog Code Task-Id: 69df779b-b72b-45bc-a11b-3deeff24ad49 --- packages/ui/src/features/canvas/components/ChannelsList.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/features/canvas/components/ChannelsList.tsx b/packages/ui/src/features/canvas/components/ChannelsList.tsx index cc9544e6f4..6701c60e99 100644 --- a/packages/ui/src/features/canvas/components/ChannelsList.tsx +++ b/packages/ui/src/features/canvas/components/ChannelsList.tsx @@ -3,13 +3,13 @@ import { CaretDownIcon, CaretRightIcon, ChartBarIcon, + CubeFocusIcon, DotsThreeIcon, FileTextIcon, HashIcon, LinkIcon, PencilSimpleIcon, PlusIcon, - SquaresFourIcon, StarIcon, TrashIcon, } from "@phosphor-icons/react"; @@ -1030,7 +1030,7 @@ export function ChannelsList() { keepMounted={!channelsLayout} icon={ channelsLayout ? ( - + ) : ( ) From 54104bbded9dbb83d7aaae56fb8dbd10c38ff2d9 Mon Sep 17 00:00:00 2001 From: Shy Alter Date: Tue, 28 Jul 2026 14:03:18 +0200 Subject: [PATCH 4/7] fix(spaces): align personal space row Generated-By: PostHog Code Task-Id: 69df779b-b72b-45bc-a11b-3deeff24ad49 --- packages/ui/src/features/canvas/AGENTS.md | 6 +++--- .../ui/src/features/canvas/components/ChannelsList.test.tsx | 2 +- packages/ui/src/features/canvas/components/ChannelsList.tsx | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/ui/src/features/canvas/AGENTS.md b/packages/ui/src/features/canvas/AGENTS.md index 4d0b0ca17f..897be5e307 100644 --- a/packages/ui/src/features/canvas/AGENTS.md +++ b/packages/ui/src/features/canvas/AGENTS.md @@ -47,9 +47,9 @@ The root `AGENTS.md` architecture rules still apply. slide and returning to the list doesn't rebuild every row. A two-finger horizontal swipe moves between them (`useChannelPaneSwipe`, wheel `deltaX` accumulated per gesture and locked until the wheel goes quiet). -- In the list, "Starred"/"Spaces" are headings above lightly indented rows; the - cube/lock glyph belongs to each row. The alpha's more deeply indented Channels - tree and hash glyphs are unchanged. +- In the list, "Starred"/"Spaces" are headings above lightly indented shared + rows; the pinned private "me" row aligns with the headings. The alpha's more + deeply indented Channels tree and hash glyphs are unchanged. - One `ChannelsFab` serves both panes: given a `channelId` it creates inside that channel (task, canvas), and either way it can create a channel. Off the layout it keeps its original two-item menu. Archived moves out of the sidebar diff --git a/packages/ui/src/features/canvas/components/ChannelsList.test.tsx b/packages/ui/src/features/canvas/components/ChannelsList.test.tsx index a607c4cfdd..787f37ef7d 100644 --- a/packages/ui/src/features/canvas/components/ChannelsList.test.tsx +++ b/packages/ui/src/features/canvas/components/ChannelsList.test.tsx @@ -105,7 +105,7 @@ describe("ChannelsList", () => { expect(screen.getByText("engineering").closest("button")).toHaveClass( "pl-4", ); - expect(screen.getByText("me").closest("button")).toHaveClass("pl-4"); + expect(screen.getByText("me").closest("button")).not.toHaveClass("pl-4"); }); it("keeps the indented tree off the layout", () => { diff --git a/packages/ui/src/features/canvas/components/ChannelsList.tsx b/packages/ui/src/features/canvas/components/ChannelsList.tsx index 6701c60e99..b29405fa0d 100644 --- a/packages/ui/src/features/canvas/components/ChannelsList.tsx +++ b/packages/ui/src/features/canvas/components/ChannelsList.tsx @@ -729,7 +729,6 @@ function PersonalChannelRow({ hotkeySlot }: { hotkeySlot?: number }) { data-selected={isActive || undefined} disabled={isCreating} onClick={() => void open()} - className={spacesLayout ? "pl-4" : undefined} > {channelGlyph(PERSONAL_CHANNEL_NAME, { size: 14, From c5de22766a9e2cea425b313fa1f2fa1e2a9dcf3b Mon Sep 17 00:00:00 2001 From: Shy Alter Date: Tue, 28 Jul 2026 14:03:20 +0200 Subject: [PATCH 5/7] refactor(spaces): reuse shared channel glyph Generated-By: PostHog Code Task-Id: 69df779b-b72b-45bc-a11b-3deeff24ad49 --- .../src/features/browser-tabs/BrowserTabStrip.tsx | 12 +++++------- .../canvas/components/ChannelsList.test.tsx | 13 ------------- .../src/features/canvas/components/ChannelsList.tsx | 9 +++------ .../canvas/components/RenameChannelModal.tsx | 5 +++-- 4 files changed, 11 insertions(+), 28 deletions(-) diff --git a/packages/ui/src/features/browser-tabs/BrowserTabStrip.tsx b/packages/ui/src/features/browser-tabs/BrowserTabStrip.tsx index 8e82b3a126..c8a72cc020 100644 --- a/packages/ui/src/features/browser-tabs/BrowserTabStrip.tsx +++ b/packages/ui/src/features/browser-tabs/BrowserTabStrip.tsx @@ -1,7 +1,5 @@ import { BrainIcon, - CubeIcon, - HashIcon, PlugsConnectedIcon, RobotIcon, SquaresFourIcon, @@ -25,6 +23,7 @@ import { } from "@posthog/shared"; import { channelSectionFor } from "@posthog/ui/features/canvas/channelSections"; import { iconForTemplate } from "@posthog/ui/features/canvas/components/canvasTemplateIcon"; +import { channelGlyph } from "@posthog/ui/features/canvas/components/channelGlyph"; import { ensurePersonalChannel } from "@posthog/ui/features/canvas/ensurePersonalChannel"; import { useChannelMutations, @@ -514,11 +513,10 @@ export function BrowserTabStrip() { id: t.id, label: meta?.label ?? channel ?? (spacesLayout ? "Space" : "Channel"), - icon: spacesLayout ? ( - - ) : ( - - ), + icon: channelGlyph(channel ?? undefined, { + size: 14, + space: spacesLayout, + }), channelName: channel, // No section meta → the channel's index page. isChannelHome: !meta, diff --git a/packages/ui/src/features/canvas/components/ChannelsList.test.tsx b/packages/ui/src/features/canvas/components/ChannelsList.test.tsx index 787f37ef7d..dfb85c1f23 100644 --- a/packages/ui/src/features/canvas/components/ChannelsList.test.tsx +++ b/packages/ui/src/features/canvas/components/ChannelsList.test.tsx @@ -125,19 +125,6 @@ describe("ChannelsList", () => { renderList(); expect(screen.getByText("Channels")).toBeTruthy(); }); - - it("shows section icons and the caret for the current state", async () => { - const user = userEvent.setup(); - renderList(); - - expect(screen.getByTestId("starred-section-icon")).toBeTruthy(); - expect(screen.getByTestId("spaces-section-icon")).toBeTruthy(); - expect(screen.getByTestId("channels:all-caret-down")).toBeTruthy(); - - await user.click(screen.getByText("Spaces")); - - expect(screen.getByTestId("channels:all-caret-right")).toBeTruthy(); - }); }); describe("search", () => { diff --git a/packages/ui/src/features/canvas/components/ChannelsList.tsx b/packages/ui/src/features/canvas/components/ChannelsList.tsx index b29405fa0d..b20fb36d9f 100644 --- a/packages/ui/src/features/canvas/components/ChannelsList.tsx +++ b/packages/ui/src/features/canvas/components/ChannelsList.tsx @@ -732,7 +732,6 @@ function PersonalChannelRow({ hotkeySlot }: { hotkeySlot?: number }) { > {channelGlyph(PERSONAL_CHANNEL_NAME, { size: 14, - space: spacesLayout, weight: isUnread ? "bold" : undefined, className: cn( "shrink-0", @@ -831,7 +830,7 @@ function ChannelGroup({ sectionId: string; label: string; className?: string; - /** Layout-only: rows sit at the label's level instead of indented under it. */ + /** Layout-only: removes the legacy tree indent; rows apply their own inset. */ flat?: boolean; /** * Off under the layout: a kept-mounted collapsed row is still an Autocomplete @@ -870,13 +869,11 @@ function ChannelGroup({ {isOpen ? ( ) : ( )} @@ -1009,7 +1006,7 @@ export function ChannelsList() { label="Starred" flat={channelsLayout} keepMounted={!channelsLayout} - icon={} + icon={} > {starred.map((channel) => ( + ) : ( ) diff --git a/packages/ui/src/features/canvas/components/RenameChannelModal.tsx b/packages/ui/src/features/canvas/components/RenameChannelModal.tsx index 64a0f58719..40cec25a1c 100644 --- a/packages/ui/src/features/canvas/components/RenameChannelModal.tsx +++ b/packages/ui/src/features/canvas/components/RenameChannelModal.tsx @@ -1,7 +1,8 @@ -import { CubeIcon, HashIcon, XIcon } from "@phosphor-icons/react"; +import { XIcon } from "@phosphor-icons/react"; import { validateChannelName } from "@posthog/core/canvas/channelName"; import { Button } from "@posthog/quill"; import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events"; +import { channelGlyph } from "@posthog/ui/features/canvas/components/channelGlyph"; import type { Channel } from "@posthog/ui/features/canvas/hooks/useChannels"; import { useChannelMutations } from "@posthog/ui/features/canvas/hooks/useChannels"; import { useChannelsLayout } from "@posthog/ui/features/canvas/hooks/useChannelsLayout"; @@ -114,7 +115,7 @@ export function RenameChannelModal({ }} > - {spacesLayout ? : } + {channelGlyph(channel.name, { size: 16, space: spacesLayout })} From 83f6778d1d6dfd911d3b27df326953179f609b72 Mon Sep 17 00:00:00 2001 From: Shy Alter Date: Tue, 28 Jul 2026 14:04:00 +0200 Subject: [PATCH 6/7] style(spaces): format section icon conditional Generated-By: PostHog Code Task-Id: 69df779b-b72b-45bc-a11b-3deeff24ad49 --- packages/ui/src/features/canvas/components/ChannelsList.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/ui/src/features/canvas/components/ChannelsList.tsx b/packages/ui/src/features/canvas/components/ChannelsList.tsx index b20fb36d9f..b04c2be60f 100644 --- a/packages/ui/src/features/canvas/components/ChannelsList.tsx +++ b/packages/ui/src/features/canvas/components/ChannelsList.tsx @@ -1025,11 +1025,7 @@ export function ChannelsList() { flat={channelsLayout} keepMounted={!channelsLayout} icon={ - channelsLayout ? ( - - ) : ( - - ) + channelsLayout ? : } > {!isLoading && channels.length === 0 && ( From 15a4d56d756b933e0f9b27563fe8ef61d17bd71a Mon Sep 17 00:00:00 2001 From: Shy Alter Date: Tue, 28 Jul 2026 14:13:02 +0200 Subject: [PATCH 7/7] test(spaces): mock layout flag in glyph consumers Generated-By: PostHog Code Task-Id: 69df779b-b72b-45bc-a11b-3deeff24ad49 --- .../ui/src/features/canvas/components/ChannelBackRow.test.tsx | 3 +++ .../src/features/canvas/components/ChannelBreadcrumb.test.tsx | 3 +++ 2 files changed, 6 insertions(+) diff --git a/packages/ui/src/features/canvas/components/ChannelBackRow.test.tsx b/packages/ui/src/features/canvas/components/ChannelBackRow.test.tsx index 933aa37cf0..ad1298628d 100644 --- a/packages/ui/src/features/canvas/components/ChannelBackRow.test.tsx +++ b/packages/ui/src/features/canvas/components/ChannelBackRow.test.tsx @@ -10,6 +10,9 @@ const mocks = vi.hoisted(() => ({ })); vi.mock("@posthog/ui/shell/analytics", () => ({ track: vi.fn() })); +vi.mock("@posthog/ui/features/canvas/hooks/useChannelsLayout", () => ({ + useChannelsLayout: () => true, +})); vi.mock("@posthog/ui/features/canvas/hooks/useChannels", () => ({ useChannels: () => ({ channels: mocks.channels, isLoading: mocks.isLoading }), })); diff --git a/packages/ui/src/features/canvas/components/ChannelBreadcrumb.test.tsx b/packages/ui/src/features/canvas/components/ChannelBreadcrumb.test.tsx index 69067a0ba2..f7124726ee 100644 --- a/packages/ui/src/features/canvas/components/ChannelBreadcrumb.test.tsx +++ b/packages/ui/src/features/canvas/components/ChannelBreadcrumb.test.tsx @@ -5,6 +5,9 @@ import { describe, expect, it, vi } from "vitest"; vi.mock("@tanstack/react-router", () => ({ useNavigate: () => vi.fn(), })); +vi.mock("@posthog/ui/features/canvas/hooks/useChannelsLayout", () => ({ + useChannelsLayout: () => true, +})); import { ChannelBreadcrumb } from "./ChannelBreadcrumb";