diff --git a/apps/desktop/src/renderer/src/components/tab-bar.test.tsx b/apps/desktop/src/renderer/src/components/tab-bar.test.tsx index 9938af3f7..f1b59d026 100644 --- a/apps/desktop/src/renderer/src/components/tab-bar.test.tsx +++ b/apps/desktop/src/renderer/src/components/tab-bar.test.tsx @@ -131,4 +131,21 @@ describe("TabBar hover action buttons", () => { const pinnedTab = getByLabelText("Issues (pinned)"); expect(within(pinnedTab).getByText("Issues")).toBeTruthy(); }); + + it("renders the Pin glyph as the leading icon on a pinned tab and the route icon on an unpinned tab", () => { + state.byWorkspace.acme.tabs = [ + { id: "tA", path: "/acme/issues", title: "Issues", icon: "ListTodo", pinned: true }, + { id: "tB", path: "/acme/projects", title: "Projects", icon: "ListTodo", pinned: false }, + ]; + const { getByLabelText } = render(); + const pinnedTab = getByLabelText("Issues (pinned)"); + const unpinnedTab = getByLabelText("Projects"); + // lucide-react renders the icon name into the class list. The leading + // slot icon is size-3.5; the hover Pin/Unpin action button is size-2.5, + // so we qualify on size to avoid matching the action glyph. + expect(pinnedTab.querySelector(".lucide-pin.size-3\\.5")).toBeTruthy(); + expect(pinnedTab.querySelector(".lucide-list-todo")).toBeNull(); + expect(unpinnedTab.querySelector(".lucide-list-todo.size-3\\.5")).toBeTruthy(); + expect(unpinnedTab.querySelector(".lucide-pin.size-3\\.5")).toBeNull(); + }); }); diff --git a/apps/desktop/src/renderer/src/components/tab-bar.tsx b/apps/desktop/src/renderer/src/components/tab-bar.tsx index ff033dffc..35f4e5c78 100644 --- a/apps/desktop/src/renderer/src/components/tab-bar.tsx +++ b/apps/desktop/src/renderer/src/components/tab-bar.tsx @@ -84,7 +84,11 @@ function SortableTabItem({ isDragging, } = useSortable({ id: tab.id }); - const Icon = TAB_ICONS[tab.icon]; + // Pinned tabs swap the route icon for a Pin glyph as the static "I am + // pinned" indicator (RFC §3 D1v-iv FINAL). The route information is still + // present in the title, and this avoids a hard left accent border that read + // as visually heavy in light mode. + const LeadingIcon = tab.pinned ? Pin : TAB_ICONS[tab.icon]; const style = { transform: CSS.Transform.toString(transform), @@ -112,10 +116,10 @@ function SortableTabItem({ e.stopPropagation(); }; - // Pinned tabs keep their full title (RFC §3 D1v-ii FINAL). The only weak - // visual differences vs. unpinned tabs are the accent left border and the - // suppressed X (closing requires explicit Unpin). Pin/Unpin is reachable - // via the hover action button below and the right-click menu fallback. + // Pinned tabs keep their full title (RFC §3 D1v-ii FINAL). The only visual + // differences vs. unpinned tabs are the leading Pin icon (swapped in above) + // and the suppressed X (closing requires explicit Unpin). Pin/Unpin is + // reachable via the hover action button below and the right-click menu. const showCloseButton = !tab.pinned && !isOnly; const tabButton = ( @@ -133,11 +137,10 @@ function SortableTabItem({ isActive ? "bg-sidebar-accent font-medium text-sidebar-accent-foreground" : "bg-sidebar-accent/50 text-muted-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", - tab.pinned && "border-l-2 border-l-primary/60", isDragging && "opacity-60", )} > - {Icon && } + {LeadingIcon && }