From 03bb2547503612fa37afcede6bc08cee8a56618f Mon Sep 17 00:00:00 2001 From: Adam Leith Date: Tue, 28 Jul 2026 14:30:25 +0100 Subject: [PATCH 1/6] search center top --- .../canvas/components/ActivityTimeline.tsx | 2 +- .../src/features/command/CommandSearchBar.tsx | 38 +++++++++ packages/ui/src/router/routes/__root.tsx | 83 +++++++------------ 3 files changed, 68 insertions(+), 55 deletions(-) create mode 100644 packages/ui/src/features/command/CommandSearchBar.tsx diff --git a/packages/ui/src/features/canvas/components/ActivityTimeline.tsx b/packages/ui/src/features/canvas/components/ActivityTimeline.tsx index 482aa35c50..c8846bd6ed 100644 --- a/packages/ui/src/features/canvas/components/ActivityTimeline.tsx +++ b/packages/ui/src/features/canvas/components/ActivityTimeline.tsx @@ -72,7 +72,7 @@ function UserMessageRow({ timestamp: string; }) { return ( - + diff --git a/packages/ui/src/features/command/CommandSearchBar.tsx b/packages/ui/src/features/command/CommandSearchBar.tsx new file mode 100644 index 0000000000..16260c4541 --- /dev/null +++ b/packages/ui/src/features/command/CommandSearchBar.tsx @@ -0,0 +1,38 @@ +import { MagnifyingGlass } from "@phosphor-icons/react"; +import { Kbd } from "@posthog/quill"; +import { + formatHotkey, + SHORTCUTS, +} from "@posthog/ui/features/command/keyboard-shortcuts"; + +/** + * The search field over the content pane — a target you aim at, not an icon you + * hunt for. It opens the command menu rather than typing into anything, so it is + * a button wearing a field's clothes: the placeholder and the shortcut hint are + * what tell you the menu is behind it. + * + * It fills the title bar's middle, which is the content column's width, and caps + * itself so a wide window leaves it centred rather than stretched. + */ +export function CommandSearchBar({ onClick }: { onClick: () => void }) { + return ( + // The row is a window-drag region; the field has to opt out of it or the + // press that should open the menu drags the window instead. +
+ +
+ ); +} diff --git a/packages/ui/src/router/routes/__root.tsx b/packages/ui/src/router/routes/__root.tsx index 7cc2b195db..ff418538cc 100644 --- a/packages/ui/src/router/routes/__root.tsx +++ b/packages/ui/src/router/routes/__root.tsx @@ -2,7 +2,6 @@ import { ArrowSquareOut, CaretLeftIcon, CaretRightIcon, - MagnifyingGlass, } from "@phosphor-icons/react"; import { useHostTRPC, useHostTRPCClient } from "@posthog/host-router/react"; import { Button, ButtonGroup } from "@posthog/quill"; @@ -35,12 +34,9 @@ import { useCanvasDeepLink } from "@posthog/ui/features/canvas/hooks/useCanvasDe import { useChannelDeepLink } from "@posthog/ui/features/canvas/hooks/useChannelDeepLink"; import { useChannelsLayout } from "@posthog/ui/features/canvas/hooks/useChannelsLayout"; import { CommandMenu } from "@posthog/ui/features/command/CommandMenu"; +import { CommandSearchBar } from "@posthog/ui/features/command/CommandSearchBar"; import { GlobalFilePicker } from "@posthog/ui/features/command/GlobalFilePicker"; import { KeyboardShortcutsSheet } from "@posthog/ui/features/command/KeyboardShortcutsSheet"; -import { - formatHotkey, - SHORTCUTS, -} from "@posthog/ui/features/command/keyboard-shortcuts"; import { ConnectivityBanner } from "@posthog/ui/features/connectivity/ConnectivityBanner"; import { useNewTaskDeepLink } from "@posthog/ui/features/deep-links/useNewTaskDeepLink"; import { useOpenTargetDeepLink } from "@posthog/ui/features/deep-links/useOpenTargetDeepLink"; @@ -66,7 +62,6 @@ import { UpdateAvailableModal } from "@posthog/ui/features/updates/UpdateAvailab import { WhatsNewModal } from "@posthog/ui/features/updates/WhatsNewModal"; import { useWorkspaces } from "@posthog/ui/features/workspace/useWorkspace"; import LogosLandscape from "@posthog/ui/primitives/Logo"; -import { Tooltip } from "@posthog/ui/primitives/Tooltip"; import { useAppView } from "@posthog/ui/router/useAppView"; import { openTask, openTaskInput } from "@posthog/ui/router/useOpenTask"; import { track } from "@posthog/ui/shell/analytics"; @@ -399,58 +394,38 @@ function RootLayout() { )} - {(localWorkspaces || channelsLayout) && ( - - {/* Search rides the title bar rather than the sidebar nav — - it's chrome, not a destination — and leads the history - controls so those stay against the sidebar edge. */} - {channelsLayout && ( - - - - )} - {localWorkspaces && ( - - - - - )} - + {localWorkspaces && ( + + + + )} {/* The new layout has no global tab strip (tabs live inside the - task view); search/inbox/activity live in the sidebar nav. The - strip is also the only global owner of Cmd+W, so something has to - keep holding that key when it isn't mounted. */} + task view); inbox/activity live in the sidebar nav. The strip is + also the only global owner of Cmd+W, so something has to keep + holding that key when it isn't mounted. */} {channelsLayout ? ( - + <> + + + ) : ( )} From 127724a8598644496b2428bd4337d40cc78b39b2 Mon Sep 17 00:00:00 2001 From: Adam Leith Date: Tue, 28 Jul 2026 14:49:25 +0100 Subject: [PATCH 2/6] quill buttons, better selected state for space list --- .../features/canvas/components/ChannelNav.tsx | 28 +++++++++++-------- .../canvas/components/ChannelsList.tsx | 11 ++++++-- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/packages/ui/src/features/canvas/components/ChannelNav.tsx b/packages/ui/src/features/canvas/components/ChannelNav.tsx index 5089bd5fbd..0d5de47f0e 100644 --- a/packages/ui/src/features/canvas/components/ChannelNav.tsx +++ b/packages/ui/src/features/canvas/components/ChannelNav.tsx @@ -5,7 +5,7 @@ import { RepeatIcon, SlidersHorizontal, } from "@phosphor-icons/react"; -import { cn } from "@posthog/quill"; +import { Button } from "@posthog/quill"; import { LOOPS_FLAG } from "@posthog/shared"; import { ANALYTICS_EVENTS, @@ -54,20 +54,21 @@ function NavIcon({ }) { return ( - + ); } @@ -92,16 +93,19 @@ export function ChannelNav() { action(); }; + const isInbox = view.type === "inbox"; const isActivity = view.type === "activity"; const isCommandCenter = view.type === "command-center"; return (
} + icon={ + + } label="Inbox" shortcut={formatHotkey(SHORTCUTS.INBOX)} - isActive={view.type === "inbox"} + isActive={isInbox} onClick={withTrack("inbox", navigateToInbox)} badge={} /> diff --git a/packages/ui/src/features/canvas/components/ChannelsList.tsx b/packages/ui/src/features/canvas/components/ChannelsList.tsx index b04c2be60f..57a7191976 100644 --- a/packages/ui/src/features/canvas/components/ChannelsList.tsx +++ b/packages/ui/src/features/canvas/components/ChannelsList.tsx @@ -142,12 +142,17 @@ function SpaceRowSurface({ return ( span]:w-full [&>span]:gap-2", + // quill highlights an option with an offset focus ring, which suits a + // popup listbox but reads as a stray outline on a sidebar row — and at + // dark-theme contrast it outshouts the selected row it sits next to. + // Same fill the rows already hover to, matching ProjectSwitcher's list. + "ring-offset-0 data-highlighted:border-transparent data-highlighted:bg-fill-hover data-highlighted:ring-0", className, )} // The two branches take the same handlers typed against different From 4aa2c9551ef5f79c947d1fbd1203af8f237cdac7 Mon Sep 17 00:00:00 2001 From: Adam Leith Date: Tue, 28 Jul 2026 16:01:48 +0100 Subject: [PATCH 3/6] fix(spaces): share one tooltip provider across the nav icons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The nav's three icons each mounted their own tooltip provider, because that is what primitives/Tooltip does — and the skip-delay window is provider state, so three isolated providers never share it. Every hop between neighbouring icons paid the full warm-up again. They move to quill's Tooltip under a single provider, so once one tooltip is up its neighbour appears immediately. The shortcut hint that the old primitive took as a prop is a quill Kbd in the content now. The Inbox envelope was also the only icon left at a fixed weight, so it alone stayed outlined while it was the active view. Co-Authored-By: Claude Opus 5 (1M context) --- .../features/canvas/components/ChannelNav.tsx | 152 ++++++++++-------- .../src/features/command/CommandSearchBar.tsx | 2 +- 2 files changed, 90 insertions(+), 64 deletions(-) diff --git a/packages/ui/src/features/canvas/components/ChannelNav.tsx b/packages/ui/src/features/canvas/components/ChannelNav.tsx index 0d5de47f0e..019acc7b7c 100644 --- a/packages/ui/src/features/canvas/components/ChannelNav.tsx +++ b/packages/ui/src/features/canvas/components/ChannelNav.tsx @@ -5,7 +5,14 @@ import { RepeatIcon, SlidersHorizontal, } from "@phosphor-icons/react"; -import { Button } from "@posthog/quill"; +import { + Button, + Kbd, + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@posthog/quill"; import { LOOPS_FLAG } from "@posthog/shared"; import { ANALYTICS_EVENTS, @@ -21,7 +28,6 @@ import { useFeatureFlag } from "@posthog/ui/features/feature-flags/useFeatureFla import { useInboxAllReports } from "@posthog/ui/features/inbox/hooks/useInboxAllReports"; import { openSettings } from "@posthog/ui/features/settings/hooks/useOpenSettings"; import { CountBadge } from "@posthog/ui/primitives/CountBadge"; -import { Tooltip } from "@posthog/ui/primitives/Tooltip"; import { navigateToActivity, navigateToInbox, @@ -53,22 +59,30 @@ function NavIcon({ badge?: ReactNode; }) { return ( - + {/* quill's Button, with the sidebar's selected-row treatment — the same `data-selected` pairing the channel rows use, so the nav and the list below it read as one control set in either theme. `relative` is for the count badge, which pins to the button's corner. */} - + + {icon} + {badge} + + } + /> + + {label} + {shortcut && {shortcut}} + ); } @@ -98,60 +112,72 @@ export function ChannelNav() { const isCommandCenter = view.type === "command-center"; return ( -
- - } - label="Inbox" - shortcut={formatHotkey(SHORTCUTS.INBOX)} - isActive={isInbox} - onClick={withTrack("inbox", navigateToInbox)} - badge={} - /> - } - label="Activity" - isActive={isActivity} - onClick={withTrack("activity", navigateToActivity)} - badge={ - - } - /> - - } - label="Command Center" - isActive={isCommandCenter} - onClick={withTrack("command_center", navigateToWebsiteCommandCenter)} - badge={ - - } - /> - {loopsEnabled ? ( + // One provider for the row: once any tooltip is up, moving to its + // neighbour reveals that one immediately instead of serving the warm-up + // delay again. Per-tooltip providers (the old primitive mounted its own) + // cannot do that — the skip window is provider state, and isolated + // providers never share it. + +
+ } + label="Inbox" + shortcut={formatHotkey(SHORTCUTS.INBOX)} + isActive={isInbox} + onClick={withTrack("inbox", navigateToInbox)} + badge={ + + } + /> + } + label="Activity" + isActive={isActivity} + onClick={withTrack("activity", navigateToActivity)} + badge={ + + } + /> + + } + label="Command Center" + isActive={isCommandCenter} + onClick={withTrack("command_center", navigateToWebsiteCommandCenter)} + badge={ + } - label="Loops" - isActive={view.type === "loops"} - onClick={withTrack("loops", navigateToLoops)} /> - ) : null} - } - label="Configure" - isActive={false} - onClick={withTrack("configure", () => openSettings("agents"))} - /> -
+ {loopsEnabled ? ( + + } + label="Loops" + isActive={view.type === "loops"} + onClick={withTrack("loops", navigateToLoops)} + /> + ) : null} + } + label="Configure" + isActive={false} + onClick={withTrack("configure", () => openSettings("agents"))} + /> +
+ ); } diff --git a/packages/ui/src/features/command/CommandSearchBar.tsx b/packages/ui/src/features/command/CommandSearchBar.tsx index 16260c4541..74bec92a01 100644 --- a/packages/ui/src/features/command/CommandSearchBar.tsx +++ b/packages/ui/src/features/command/CommandSearchBar.tsx @@ -23,7 +23,7 @@ export function CommandSearchBar({ onClick }: { onClick: () => void }) { type="button" aria-label="Search" onClick={onClick} - className="mx-auto flex h-7 w-full max-w-140 items-center gap-2 rounded-sm border border-border bg-card pr-1 pl-2 hover:bg-card/50 dark:bg-fill-hover dark:hover:bg-input/80" + className="mx-auto flex h-7 w-full max-w-140 items-center gap-2 rounded-sm border border-border bg-fill-hover pr-1 pl-2" > From 016e7a8a8280abfedd18aefcf3562fc345242d47 Mon Sep 17 00:00:00 2001 From: Adam Leith Date: Tue, 28 Jul 2026 16:10:11 +0100 Subject: [PATCH 4/6] fix(spaces): move the last two sidebar tooltips onto quill The create button and the back row still used primitives/Tooltip, so they rendered the old dark card next to the nav's quill ones. The FAB carried both: quill off the layout, the legacy one on it. Its two branches collapse into a single quill tooltip that only chooses the text, with the create shortcut as a Kbd rather than the old primitive's prop. Co-Authored-By: Claude Opus 5 (1M context) --- .../canvas/components/ChannelBackRow.tsx | 100 ++++++++++-------- .../canvas/components/ChannelsFab.tsx | 35 +++--- 2 files changed, 71 insertions(+), 64 deletions(-) diff --git a/packages/ui/src/features/canvas/components/ChannelBackRow.tsx b/packages/ui/src/features/canvas/components/ChannelBackRow.tsx index e9a0712b07..995938a01b 100644 --- a/packages/ui/src/features/canvas/components/ChannelBackRow.tsx +++ b/packages/ui/src/features/canvas/components/ChannelBackRow.tsx @@ -1,5 +1,10 @@ import { CaretLeftIcon, StarIcon } from "@phosphor-icons/react"; -import { Skeleton } from "@posthog/quill"; +import { + Skeleton, + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@posthog/quill"; import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events"; import { channelGlyph } from "@posthog/ui/features/canvas/components/channelGlyph"; import { useChannelStarToggle } from "@posthog/ui/features/canvas/hooks/useChannelStars"; @@ -10,7 +15,6 @@ import { 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"; import { track } from "@posthog/ui/shell/analytics"; // An overlay rather than a sibling: the back button fills the row, and nesting @@ -53,49 +57,55 @@ export function ChannelBackRow({ channelId }: { channelId: string }) { return (
- - + + { + track(ANALYTICS_EVENTS.CHANNEL_ACTION, { + action_type: "browse_channels", + surface: "sidebar", + channel_id: channelId, + }); + showChannelList(); + }} + // Fixed height with an unconditional star well: sized off its + // contents, a starrable channel ran 4px taller than #me and + // everything below shifted on switch. No border — it's a row in + // the sidebar like the ones under it, not a control sitting on + // top. + className="flex h-8 w-full items-center gap-1.5 rounded-md px-2 text-left transition-colors hover:bg-fill-hover" + > + + + {channelGlyph(current?.name, { + size: 14, + space: spacesLayout, + className: "text-muted-foreground", + })} + + + {current ? ( + current.name + ) : isLoading ? ( + // A placeholder word here would read as a real channel named + // "channel"; a skeleton says "still loading" honestly. + + ) : ( + "Unavailable" + )} + + + + } + /> + Back to spaces {showStar && current && } {/* Inert star for #me, so the row reads the same on every channel. */} diff --git a/packages/ui/src/features/canvas/components/ChannelsFab.tsx b/packages/ui/src/features/canvas/components/ChannelsFab.tsx index 0596061a2d..cb654896f4 100644 --- a/packages/ui/src/features/canvas/components/ChannelsFab.tsx +++ b/packages/ui/src/features/canvas/components/ChannelsFab.tsx @@ -12,6 +12,7 @@ import { DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, + Kbd, Tooltip, TooltipContent, TooltipTrigger, @@ -27,7 +28,6 @@ import { } from "@posthog/ui/features/command/keyboard-shortcuts"; import { isContentEmpty } from "@posthog/ui/features/message-editor/content"; import { useDraftStore } from "@posthog/ui/features/message-editor/draftStore"; -import { Tooltip as ShortcutTooltip } from "@posthog/ui/primitives/Tooltip"; import { openTaskInput } from "@posthog/ui/router/useOpenTask"; import { track } from "@posthog/ui/shell/analytics"; import { useRouterState } from "@tanstack/react-router"; @@ -96,24 +96,21 @@ export function ChannelsFab({ channelId }: { channelId?: string }) { return ( <> - {channelsLayout ? ( - // The draft dot needs saying out loud, and the button is where the - // create shortcut is worth advertising. - - - - ) : ( - - } /> - - Create something new - - - )} + + } /> + + {channelsLayout ? ( + <> + {/* The draft dot needs saying out loud, and the button is where + the create shortcut is worth advertising. */} + {hasDraft ? "Create — you have a draft" : "Create"} + {formatHotkey(SHORTCUTS.NEW_TASK)} + + ) : ( + "Create something new" + )} + + Date: Tue, 28 Jul 2026 16:57:35 +0100 Subject: [PATCH 5/6] fix(spaces): leave #me's star well empty in the back row MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #me can't be starred, and the greyed-out star standing in for the control read as one you were being denied rather than one that doesn't apply. The well itself stays — it is a reserved span inside the button, not the star — so the row is still the same height on every space and nothing below it shifts when you switch. Co-Authored-By: Claude Opus 5 (1M context) --- .../canvas/components/ChannelBackRow.test.tsx | 5 +++-- .../features/canvas/components/ChannelBackRow.tsx | 13 ++++--------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/ui/src/features/canvas/components/ChannelBackRow.test.tsx b/packages/ui/src/features/canvas/components/ChannelBackRow.test.tsx index ad1298628d..705d724870 100644 --- a/packages/ui/src/features/canvas/components/ChannelBackRow.test.tsx +++ b/packages/ui/src/features/canvas/components/ChannelBackRow.test.tsx @@ -59,8 +59,9 @@ describe("ChannelBackRow", () => { expect(useChannelPaneStore.getState().pane).toBe("list"); }); - // #me can't be unstarred, but the well stays filled so the row doesn't - // change height (and everything below it shift) when you switch channels. + // #me can't be starred, so its well is empty — but the well is still there, + // so the row doesn't change height (and everything below it shift) when you + // switch spaces. it("offers a star on shared channels only", () => { renderRow(ENG.id); expect(screen.getByRole("button", { name: "Star space" })).toBeTruthy(); diff --git a/packages/ui/src/features/canvas/components/ChannelBackRow.tsx b/packages/ui/src/features/canvas/components/ChannelBackRow.tsx index 995938a01b..575441d56b 100644 --- a/packages/ui/src/features/canvas/components/ChannelBackRow.tsx +++ b/packages/ui/src/features/canvas/components/ChannelBackRow.tsx @@ -107,16 +107,11 @@ export function ChannelBackRow({ channelId }: { channelId: string }) { /> Back to spaces + {/* #me can't be starred, so its well stays empty — a greyed-out star read + as a control you were being denied. The well itself is unconditional + (see the button's reserved span), which is what keeps the row the same + height on every space. */} {showStar && current && } - {/* Inert star for #me, so the row reads the same on every channel. */} - {current && !showStar && ( - - - - )}
); } From e04e09c4fa7c7f8401bdcf8c65cea21b57eb9bff Mon Sep 17 00:00:00 2001 From: Adam Leith Date: Tue, 28 Jul 2026 18:53:04 +0100 Subject: [PATCH 6/6] spacing around search and width reduction --- packages/ui/src/features/command/CommandSearchBar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/features/command/CommandSearchBar.tsx b/packages/ui/src/features/command/CommandSearchBar.tsx index 74bec92a01..9b905e4b62 100644 --- a/packages/ui/src/features/command/CommandSearchBar.tsx +++ b/packages/ui/src/features/command/CommandSearchBar.tsx @@ -18,12 +18,12 @@ export function CommandSearchBar({ onClick }: { onClick: () => void }) { return ( // The row is a window-drag region; the field has to opt out of it or the // press that should open the menu drags the window instead. -
+