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

Commit cbbe30e

Browse files
authored
feat(canvas): lock glyph for private channels, and theme-token colours in the sidebar
A private channel now shows a lock where a public one shows a hash, the Slack convention, so privacy reads at a glance. One helper (channelGlyph) decides it for every surface that renders channel identity — sidebar list, switcher trigger and rows, channel header, breadcrumb, command palette — which also retires the awkward trailing lock the #me row carried, since the leading glyph now says it. The personal #me channel is the only channel that is private today: neither the folder Channel nor the backend TaskChannel carries a privacy flag, so isPrivateChannel is the single place that has to learn about one when it lands. Also moves the channel sidebar off the absolute gray scale and onto the theme's fill/foreground tokens. The fill tokens are color-mix(foreground N%, transparent) — relative to the surface and tuned per mode (4% light, 7% dark) — whereas gray-3 is absolute, and its relationship to the chrome it is painted on inverts between modes: 3 L* below a near-white chrome in light, where darker reads heavier, so the nav's filled chips came out as muddy beige slabs. They now follow the same transparent-rest, fill-on-hover idiom as every other sidebar control.</body> Generated-By: PostHog Code Task-Id: c401c7cb-e436-4da3-80bf-1d3008ee5d38
1 parent 0568bc2 commit cbbe30e

10 files changed

Lines changed: 129 additions & 63 deletions

File tree

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { HashIcon } from "@phosphor-icons/react";
21
import {
32
Button,
43
Tooltip,
54
TooltipContent,
65
TooltipTrigger,
76
} from "@posthog/quill";
7+
import { channelGlyph } from "@posthog/ui/features/canvas/components/channelGlyph";
88
import { HeaderTitleEditor } from "@posthog/ui/features/task-detail/HeaderTitleEditor";
99
import { Flex, Text } from "@radix-ui/themes";
1010
import { useNavigate } from "@tanstack/react-router";
@@ -52,7 +52,10 @@ export function ChannelBreadcrumb({
5252

5353
const channelSegment = (
5454
<>
55-
<HashIcon size={12} className="mt-px shrink-0 text-muted-foreground/80" />
55+
{channelGlyph(channelName, {
56+
size: 12,
57+
className: "mt-px shrink-0 text-muted-foreground/80",
58+
})}
5659
<Text
5760
className="min-w-0 truncate whitespace-nowrap font-medium text-[13px]"
5861
title={channelName}

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { HashIcon, StarIcon } from "@phosphor-icons/react";
1+
import { StarIcon } from "@phosphor-icons/react";
22
import { Button, cn } from "@posthog/quill";
33
import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events";
44
import { ChannelTabs } from "@posthog/ui/features/canvas/components/ChannelTabs";
5+
import { channelGlyph } from "@posthog/ui/features/canvas/components/channelGlyph";
56
import { useChannelStarToggle } from "@posthog/ui/features/canvas/hooks/useChannelStars";
67
import {
78
type Channel,
@@ -64,7 +65,10 @@ export function ChannelHeader({ channelId }: { channelId: string }) {
6465
size="sm"
6566
className={cn("min-w-0", isHome ? "bg-fill-selected" : "")}
6667
>
67-
<HashIcon size={20} className="shrink-0 text-muted-foreground/80" />
68+
{channelGlyph(channelName, {
69+
size: 20,
70+
className: "shrink-0 text-muted-foreground/80",
71+
})}
6872
<Text className="min-w-0 truncate font-medium" title={channelName}>
6973
{channelName ?? "Channel"}
7074
</Text>

‎packages/ui/src/features/canvas/components/ChannelItemRow.tsx‎

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export function statusVariantFor(
6060
}
6161

6262
const HOVER_ACTION_CLASS =
63-
"flex h-5 w-5 cursor-pointer items-center justify-center rounded text-gray-10 transition-colors hover:bg-gray-4 hover:text-gray-12";
63+
"flex h-5 w-5 cursor-pointer items-center justify-center rounded text-muted-foreground transition-colors hover:bg-fill-hover hover:text-foreground";
6464

6565
export function ChannelItemRow({ item }: { item: ChannelItem }) {
6666
return (
@@ -79,7 +79,7 @@ export function ChannelItemRow({ item }: { item: ChannelItem }) {
7979
onClick={item.onClick}
8080
endContent={
8181
<>
82-
<span className="shrink-0 text-[11px] text-gray-11 group-hover:hidden">
82+
<span className="shrink-0 text-[11px] text-muted-foreground group-hover:hidden">
8383
{formatRelativeTimeShort(item.ts)}
8484
</span>
8585
<span className="hidden shrink-0 items-center gap-0.5 group-hover:flex">
@@ -122,14 +122,14 @@ export function ChannelItemRow({ item }: { item: ChannelItem }) {
122122
>
123123
<PreviewCard.Popup className="w-64 rounded-lg border border-border bg-background p-3 shadow-lg outline-none">
124124
<div className="flex items-start gap-2.5">
125-
<span className="flex size-8 shrink-0 items-center justify-center rounded-md bg-gray-3">
125+
<span className="flex size-8 shrink-0 items-center justify-center rounded-md bg-muted">
126126
{item.icon}
127127
</span>
128128
<div className="min-w-0 flex-1">
129-
<p className="break-words font-medium text-[13px] text-gray-12 leading-snug">
129+
<p className="break-words font-medium text-[13px] text-foreground leading-snug">
130130
{item.title}
131131
</p>
132-
<p className="mt-0.5 text-[11px] text-gray-10">
132+
<p className="mt-0.5 text-[11px] text-muted-foreground">
133133
{item.kind === "canvas" ? "Canvas" : "Task"} · updated{" "}
134134
{formatRelativeTimeShort(item.ts)}
135135
</p>
@@ -152,10 +152,12 @@ export function ChannelItemRow({ item }: { item: ChannelItem }) {
152152
</Avatar>
153153
)}
154154
<div className="min-w-0">
155-
<p className="truncate text-[12px] text-gray-12">
155+
<p className="truncate text-[12px] text-foreground">
156156
{item.authorName ?? "Unknown"}
157157
</p>
158-
<p className="text-[10px] text-gray-10">Created by</p>
158+
<p className="text-[10px] text-muted-foreground">
159+
Created by
160+
</p>
159161
</div>
160162
</div>
161163
)}

‎packages/ui/src/features/canvas/components/ChannelNav.tsx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ function IconBadge({
4242
"-top-1 -right-1 absolute inline-flex h-3.5 min-w-3.5 items-center justify-center rounded-full px-1 font-semibold text-[9px] tabular-nums leading-none ring-2 ring-chrome",
4343
tone === "notification"
4444
? "bg-(--red-9) text-white"
45-
: "bg-gray-5 text-gray-11",
45+
: "bg-muted text-muted-foreground",
4646
)}
4747
>
4848
{count > 99 ? "99+" : count}
@@ -74,8 +74,8 @@ function NavIcon({
7474
className={cn(
7575
"relative flex size-8 shrink-0 items-center justify-center rounded-lg transition-colors duration-100",
7676
isActive
77-
? "bg-fill-selected text-gray-12"
78-
: "bg-gray-3 text-gray-11 hover:bg-gray-4 hover:text-gray-12",
77+
? "bg-fill-selected text-foreground"
78+
: "text-muted-foreground hover:bg-fill-hover hover:text-foreground",
7979
)}
8080
>
8181
{icon}

‎packages/ui/src/features/canvas/components/ChannelSidebar.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,10 @@ const CREATED_BY_OPTIONS: readonly { value: CreatedByFilter; label: string }[] =
5252
] as const;
5353

5454
const HEADER_ICON_BUTTON_CLASS =
55-
"flex size-5 shrink-0 items-center justify-center rounded text-gray-10 transition-colors hover:bg-gray-3 hover:text-gray-12";
55+
"flex size-5 shrink-0 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-fill-hover hover:text-foreground";
5656

5757
const cnHeaderButton = (active: boolean) =>
58-
cn(HEADER_ICON_BUTTON_CLASS, active && "bg-gray-3 text-gray-12");
58+
cn(HEADER_ICON_BUTTON_CLASS, active && "bg-fill-selected text-foreground");
5959

6060
const RECENTS_CAP = 30;
6161

‎packages/ui/src/features/canvas/components/ChannelSwitcher.tsx‎

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
import { Popover as BasePopover } from "@base-ui/react/popover";
2-
import {
3-
CaretUpDownIcon,
4-
HashIcon,
5-
PlusIcon,
6-
StarIcon,
7-
} from "@phosphor-icons/react";
2+
import { CaretUpDownIcon, PlusIcon, StarIcon } from "@phosphor-icons/react";
83
import {
94
ContextMenu,
105
ContextMenuContent,
@@ -20,6 +15,7 @@ import {
2015
} from "@posthog/quill";
2116
import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events";
2217
import { CreateChannelModal } from "@posthog/ui/features/canvas/components/CreateChannelModal";
18+
import { channelGlyph } from "@posthog/ui/features/canvas/components/channelGlyph";
2319
import {
2420
useChannelStars,
2521
useChannelStarToggle,
@@ -72,7 +68,7 @@ function SwitcherRow({
7268
const row = (
7369
<div
7470
className={cn(
75-
"group flex items-center rounded-md transition-colors hover:bg-gray-3",
71+
"group flex items-center rounded-md transition-colors hover:bg-fill-hover",
7672
active && "bg-fill-selected",
7773
)}
7874
>
@@ -82,9 +78,12 @@ function SwitcherRow({
8278
className="flex min-w-0 flex-1 items-center gap-2 px-2 py-1 text-left text-[13px]"
8379
>
8480
<span className="flex w-4 shrink-0 items-center justify-center">
85-
<HashIcon size={14} className="text-gray-10" />
81+
{channelGlyph(channel.name, {
82+
size: 14,
83+
className: "text-muted-foreground",
84+
})}
8685
</span>
87-
<span className="min-w-0 flex-1 truncate text-gray-12">
86+
<span className="min-w-0 flex-1 truncate text-foreground">
8887
{channel.name}
8988
</span>
9089
{hotkeySlot != null && (
@@ -96,7 +95,7 @@ function SwitcherRow({
9695
// still has to be filled to keep the rows aligned.
9796
<span
9897
aria-hidden
99-
className="mr-1 flex size-5 shrink-0 items-center justify-center text-gray-10 opacity-40"
98+
className="mr-1 flex size-5 shrink-0 items-center justify-center text-muted-foreground opacity-40"
10099
>
101100
<StarIcon size={13} weight="fill" />
102101
</span>
@@ -106,7 +105,7 @@ function SwitcherRow({
106105
aria-label={isStarred ? "Unstar channel" : "Star channel"}
107106
onClick={trackedToggleStar}
108107
className={cn(
109-
"mr-1 flex size-5 shrink-0 items-center justify-center rounded text-gray-10 transition-colors hover:text-gray-12",
108+
"mr-1 flex size-5 shrink-0 items-center justify-center rounded text-muted-foreground transition-colors hover:text-foreground",
110109
!isStarred && "opacity-0 group-hover:opacity-100",
111110
)}
112111
>
@@ -148,7 +147,7 @@ function TriggerStar({ channel }: { channel: Channel }) {
148147
}}
149148
// Parks in the trigger's reserved well: 8px padding + 12px caret + 6px
150149
// gap = 26px from the right edge.
151-
className="-translate-y-1/2 absolute top-1/2 right-[26px] flex size-6 items-center justify-center rounded text-gray-10 transition-colors hover:bg-gray-4 hover:text-gray-12"
150+
className="-translate-y-1/2 absolute top-1/2 right-[26px] flex size-6 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-fill-hover hover:text-foreground"
152151
>
153152
<StarIcon size={14} weight={isStarred ? "fill" : "regular"} />
154153
</button>
@@ -247,16 +246,22 @@ export function ChannelSwitcher({ channelId }: { channelId: string }) {
247246
// Fixed height with an unconditional star well: sized off its
248247
// contents, a starrable channel ran 4px taller than #me and
249248
// everything below shifted on switch.
250-
className="flex h-8 w-full items-center gap-1.5 rounded-md border border-border px-2 text-left transition-colors hover:bg-gray-3 aria-expanded:bg-gray-3"
249+
className="flex h-8 w-full items-center gap-1.5 rounded-md border border-border px-2 text-left transition-colors hover:bg-fill-hover aria-expanded:bg-fill-selected"
251250
>
252251
<span className="flex w-4 shrink-0 items-center justify-center">
253-
<HashIcon size={14} className="text-gray-10" />
252+
{channelGlyph(current?.name, {
253+
size: 14,
254+
className: "text-muted-foreground",
255+
})}
254256
</span>
255-
<span className="min-w-0 flex-1 truncate font-semibold text-[13px] text-gray-12">
257+
<span className="min-w-0 flex-1 truncate font-semibold text-[13px] text-foreground">
256258
{current?.name ?? "channel"}
257259
</span>
258260
<span aria-hidden className="size-6 shrink-0" />
259-
<CaretUpDownIcon size={12} className="shrink-0 text-gray-10" />
261+
<CaretUpDownIcon
262+
size={12}
263+
className="shrink-0 text-muted-foreground"
264+
/>
260265
</button>
261266
}
262267
/>
@@ -300,7 +305,7 @@ export function ChannelSwitcher({ channelId }: { channelId: string }) {
300305
/>
301306
))}
302307
{topFiltered.length === 0 && restFiltered.length === 0 && (
303-
<p className="px-2 py-2 text-[12px] text-gray-10">
308+
<p className="px-2 py-2 text-[12px] text-muted-foreground">
304309
No channels match "{query.trim()}".
305310
</p>
306311
)}
@@ -312,7 +317,7 @@ export function ChannelSwitcher({ channelId }: { channelId: string }) {
312317
setOpen(false);
313318
setCreateOpen(true);
314319
}}
315-
className="flex w-full items-center gap-2 rounded-md px-2 py-1 text-left text-[13px] text-gray-11 transition-colors hover:bg-gray-3"
320+
className="flex w-full items-center gap-2 rounded-md px-2 py-1 text-left text-[13px] text-muted-foreground transition-colors hover:bg-fill-hover"
316321
>
317322
<span className="flex w-4 shrink-0 items-center justify-center">
318323
<PlusIcon size={14} />
@@ -326,7 +331,7 @@ export function ChannelSwitcher({ channelId }: { channelId: string }) {
326331
{current && !showStar && (
327332
<span
328333
aria-hidden
329-
className="-translate-y-1/2 pointer-events-none absolute top-1/2 right-[26px] flex size-6 items-center justify-center text-gray-10 opacity-40"
334+
className="-translate-y-1/2 pointer-events-none absolute top-1/2 right-[26px] flex size-6 items-center justify-center text-muted-foreground opacity-40"
330335
>
331336
<StarIcon size={14} weight="fill" />
332337
</span>

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

Lines changed: 15 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
FileTextIcon,
88
HashIcon,
99
LinkIcon,
10-
LockSimpleIcon,
1110
PencilSimpleIcon,
1211
PlusIcon,
1312
StarIcon,
@@ -43,6 +42,7 @@ import {
4342
TooltipTrigger,
4443
} from "@posthog/quill";
4544
import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events";
45+
import { channelGlyph } from "@posthog/ui/features/canvas/components/channelGlyph";
4646
import { RenameChannelModal } from "@posthog/ui/features/canvas/components/RenameChannelModal";
4747
import { trackAndCreateCanvas } from "@posthog/ui/features/canvas/createCanvasAnalytics";
4848
import { ensurePersonalChannel } from "@posthog/ui/features/canvas/ensurePersonalChannel";
@@ -346,18 +346,18 @@ function ChannelSection({
346346
params: { channelId: channel.id },
347347
});
348348
}}
349-
className="w-full min-w-0 justify-start gap-2 data-selected:bg-fill-selected data-selected:text-gray-12"
349+
className="w-full min-w-0 justify-start gap-2 data-selected:bg-fill-selected data-selected:text-foreground"
350350
>
351-
<HashIcon
352-
size={14}
353-
weight={isUnread ? "bold" : undefined}
354-
className={cn(
351+
{channelGlyph(channel.name, {
352+
size: 14,
353+
weight: isUnread ? "bold" : undefined,
354+
className: cn(
355355
"shrink-0",
356356
isUnread || isActive
357357
? "text-foreground"
358358
: "text-muted-foreground group-hover/button:text-foreground",
359-
)}
360-
/>
359+
),
360+
})}
361361
<span
362362
className={cn(
363363
"truncate text-[13px] group-hover/chan:pr-8",
@@ -583,18 +583,18 @@ function PersonalChannelRow() {
583583
data-selected={isActive || undefined}
584584
disabled={isCreating}
585585
onClick={() => void open()}
586-
className="w-full min-w-0 justify-start gap-2 data-selected:bg-fill-selected data-selected:text-gray-12"
586+
className="w-full min-w-0 justify-start gap-2 data-selected:bg-fill-selected data-selected:text-foreground"
587587
>
588-
<HashIcon
589-
size={14}
590-
weight={isUnread ? "bold" : undefined}
591-
className={cn(
588+
{channelGlyph(PERSONAL_CHANNEL_NAME, {
589+
size: 14,
590+
weight: isUnread ? "bold" : undefined,
591+
className: cn(
592592
"shrink-0",
593593
isUnread || isActive
594594
? "text-foreground"
595595
: "text-muted-foreground group-hover/button:text-foreground",
596-
)}
597-
/>
596+
),
597+
})}
598598
<span
599599
className={cn(
600600
"truncate text-[13px]",
@@ -606,17 +606,6 @@ function PersonalChannelRow() {
606606
>
607607
{PERSONAL_CHANNEL_NAME}
608608
</span>
609-
{/* The lock and the hover "+" share the right edge, so fade the lock
610-
out as the "+" comes in. */}
611-
<LockSimpleIcon
612-
size={12}
613-
className={cn(
614-
"ml-auto shrink-0 text-chrome-foreground transition-opacity",
615-
newMenuOpen
616-
? "opacity-0"
617-
: "opacity-100 group-hover/chan:opacity-0",
618-
)}
619-
/>
620609
</Button>
621610
<div className="absolute top-0 right-1">
622611
<DropdownMenu open={newMenuOpen} onOpenChange={setNewMenuOpen}>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { describe, expect, it } from "vitest";
2+
import { isPrivateChannel } from "./channelGlyph";
3+
4+
describe("isPrivateChannel", () => {
5+
it.each([
6+
["me", true],
7+
[" Me ", true],
8+
["ME", true],
9+
["code", false],
10+
["posthog-feedback", false],
11+
// Not a prefix match: only the personal channel itself is private.
12+
["meeting-notes", false],
13+
["team-me", false],
14+
[undefined, false],
15+
["", false],
16+
])("%s -> %s", (name, expected) => {
17+
expect(isPrivateChannel(name)).toBe(expected);
18+
});
19+
});

0 commit comments

Comments
 (0)