Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ function UserMessageRow({
timestamp: string;
}) {
return (
<ThreadItem>
<ThreadItem className="rounded-none">
<ThreadItemGutter>
<UserAvatar user={author} size="lg" className="sticky top-2" />
</ThreadItemGutter>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
113 changes: 59 additions & 54 deletions packages/ui/src/features/canvas/components/ChannelBackRow.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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
Expand Down Expand Up @@ -53,60 +57,61 @@ export function ChannelBackRow({ channelId }: { channelId: string }) {

return (
<div className="relative mx-2 mt-1">
<Tooltip content="Back to spaces" side="bottom">
<button
type="button"
aria-label="Back to spaces"
onClick={() => {
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"
>
<CaretLeftIcon
size={12}
className="shrink-0 text-muted-foreground"
weight="bold"
/>
<span className="flex w-4 shrink-0 items-center justify-center">
{channelGlyph(current?.name, {
size: 14,
space: spacesLayout,
className: "text-muted-foreground",
})}
</span>
<span className="min-w-0 flex-1 truncate font-semibold text-[13px] text-foreground">
{current ? (
current.name
) : isLoading ? (
// A placeholder word here would read as a real channel named
// "channel"; a skeleton says "still loading" honestly.
<Skeleton className="h-3.5 w-24" />
) : (
"Unavailable"
)}
</span>
<span aria-hidden className="size-6 shrink-0" />
</button>
<Tooltip>
<TooltipTrigger
render={
<button
type="button"
aria-label="Back to spaces"
onClick={() => {
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"
>
<CaretLeftIcon
size={12}
className="shrink-0 text-muted-foreground"
weight="bold"
/>
<span className="flex w-4 shrink-0 items-center justify-center">
{channelGlyph(current?.name, {
size: 14,
space: spacesLayout,
className: "text-muted-foreground",
})}
</span>
<span className="min-w-0 flex-1 truncate font-semibold text-[13px] text-foreground">
{current ? (
current.name
) : isLoading ? (
// A placeholder word here would read as a real channel named
// "channel"; a skeleton says "still loading" honestly.
<Skeleton className="h-3.5 w-24" />
) : (
"Unavailable"
)}
</span>
<span aria-hidden className="size-6 shrink-0" />
</button>
}
/>
<TooltipContent side="bottom">Back to spaces</TooltipContent>
</Tooltip>
{/* #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 && <RowStar channel={current} />}
{/* Inert star for #me, so the row reads the same on every channel. */}
{current && !showStar && (
<span
aria-hidden
className="-translate-y-1/2 pointer-events-none absolute top-1/2 right-[6px] flex size-6 items-center justify-center text-muted-foreground opacity-40"
>
<StarIcon size={14} weight="fill" />
</span>
)}
</div>
);
}
158 changes: 94 additions & 64 deletions packages/ui/src/features/canvas/components/ChannelNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@ import {
RepeatIcon,
SlidersHorizontal,
} from "@phosphor-icons/react";
import { cn } from "@posthog/quill";
import {
Button,
Kbd,
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@posthog/quill";
import { LOOPS_FLAG } from "@posthog/shared";
import {
ANALYTICS_EVENTS,
Expand All @@ -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,
Expand Down Expand Up @@ -53,21 +59,30 @@ function NavIcon({
badge?: ReactNode;
}) {
return (
<Tooltip content={label} shortcut={shortcut} side="bottom">
<button
type="button"
aria-label={label}
onClick={onClick}
className={cn(
"relative flex size-8 shrink-0 items-center justify-center rounded-lg transition-colors duration-100",
isActive
? "bg-fill-selected text-foreground"
: "text-muted-foreground hover:bg-fill-hover hover:text-foreground",
)}
>
{icon}
{badge}
</button>
<Tooltip>
{/* 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. */}
<TooltipTrigger
render={
<Button
variant="default"
size="icon"
aria-label={label}
data-selected={isActive || undefined}
onClick={onClick}
className="relative shrink-0 text-muted-foreground data-selected:bg-fill-selected data-selected:text-foreground"
>
{icon}
{badge}
</Button>
}
/>
<TooltipContent side="bottom">
{label}
{shortcut && <Kbd className="ml-1.5">{shortcut}</Kbd>}
</TooltipContent>
</Tooltip>
);
}
Expand All @@ -92,62 +107,77 @@ export function ChannelNav() {
action();
};

const isInbox = view.type === "inbox";
const isActivity = view.type === "activity";
const isCommandCenter = view.type === "command-center";

return (
<div className="flex shrink-0 gap-2 px-2 pt-2 pb-1">
<NavIcon
icon={<EnvelopeSimple size={16} />}
label="Inbox"
shortcut={formatHotkey(SHORTCUTS.INBOX)}
isActive={view.type === "inbox"}
onClick={withTrack("inbox", navigateToInbox)}
badge={<CountBadge count={counts.pulls} className={ICON_BADGE_CLASS} />}
/>
<NavIcon
icon={<BellIcon size={16} weight={isActivity ? "fill" : "regular"} />}
label="Activity"
isActive={isActivity}
onClick={withTrack("activity", navigateToActivity)}
badge={
<CountBadge count={unseenActivity} className={ICON_BADGE_CLASS} />
}
/>
<NavIcon
icon={
<Lightning size={16} weight={isCommandCenter ? "fill" : "regular"} />
}
label="Command Center"
isActive={isCommandCenter}
onClick={withTrack("command_center", navigateToWebsiteCommandCenter)}
badge={
<CountBadge
count={commandCenterCount}
tone="neutral"
className={ICON_BADGE_CLASS}
/>
}
/>
{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.
<TooltipProvider delay={400}>
<div className="flex shrink-0 gap-2 px-2 pt-2 pb-1">
<NavIcon
icon={
<EnvelopeSimple size={16} weight={isInbox ? "fill" : "regular"} />
}
label="Inbox"
shortcut={formatHotkey(SHORTCUTS.INBOX)}
isActive={isInbox}
onClick={withTrack("inbox", navigateToInbox)}
badge={
<CountBadge count={counts.pulls} className={ICON_BADGE_CLASS} />
}
/>
<NavIcon
icon={<BellIcon size={16} weight={isActivity ? "fill" : "regular"} />}
label="Activity"
isActive={isActivity}
onClick={withTrack("activity", navigateToActivity)}
badge={
<CountBadge count={unseenActivity} className={ICON_BADGE_CLASS} />
}
/>
<NavIcon
icon={
<RepeatIcon
<Lightning
size={16}
weight={view.type === "loops" ? "fill" : "regular"}
weight={isCommandCenter ? "fill" : "regular"}
/>
}
label="Command Center"
isActive={isCommandCenter}
onClick={withTrack("command_center", navigateToWebsiteCommandCenter)}
badge={
<CountBadge
count={commandCenterCount}
tone="neutral"
className={ICON_BADGE_CLASS}
/>
}
label="Loops"
isActive={view.type === "loops"}
onClick={withTrack("loops", navigateToLoops)}
/>
) : null}
<NavIcon
icon={<SlidersHorizontal size={16} />}
label="Configure"
isActive={false}
onClick={withTrack("configure", () => openSettings("agents"))}
/>
</div>
{loopsEnabled ? (
<NavIcon
icon={
<RepeatIcon
size={16}
weight={view.type === "loops" ? "fill" : "regular"}
/>
}
label="Loops"
isActive={view.type === "loops"}
onClick={withTrack("loops", navigateToLoops)}
/>
) : null}
<NavIcon
icon={<SlidersHorizontal size={16} />}
label="Configure"
isActive={false}
onClick={withTrack("configure", () => openSettings("agents"))}
/>
</div>
</TooltipProvider>
);
}
Loading
Loading