mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-05 13:29:44 +02:00
refactor(desktop): pin tab — drop accent left border, swap leading icon to Pin (MUL-2449)
Jiayuan reported that the accent left border on pinned tabs reads as a heavy black edge in light mode and looks unrefined. Replace it with a quieter identifier: pinned tabs swap their route icon for a Pin glyph in the leading slot (same size, no extra horizontal space). The hidden X close button stays as the secondary cue. RFC §3 D1v moves from iii FINAL to iv FINAL; iii is demoted to v2 FINAL → v3 REMOVED. Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -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(<TabBar />);
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 && <Icon className="size-3.5 shrink-0" />}
|
||||
{LeadingIcon && <LeadingIcon className="size-3.5 shrink-0" />}
|
||||
<span
|
||||
className="min-w-0 flex-1 overflow-hidden whitespace-nowrap text-left"
|
||||
style={{
|
||||
|
||||
Reference in New Issue
Block a user