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
31 changes: 26 additions & 5 deletions src/browser/components/TitleBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,13 @@ export function TitleBar() {
)}
style={leftInset > 0 ? { paddingLeft: leftInset } : undefined}
>
<div className={cn("mr-4 flex min-w-0 items-center gap-2", isDesktop && "titlebar-no-drag")}>
<div
className={cn(
"mr-4 flex min-w-0",
leftInset > 0 ? "flex-col" : "items-center gap-2",
isDesktop && "titlebar-no-drag"
)}
>
<Tooltip>
<TooltipTrigger asChild>
<div
Expand All @@ -273,9 +279,14 @@ export function TitleBar() {
onClick={handleUpdateClick}
onMouseEnter={handleIndicatorHover}
>
<div className="relative h-4 w-[35px] overflow-hidden">
<div
className={cn(
"relative overflow-hidden",
leftInset > 0 ? "h-3 w-[26px]" : "h-4 w-[35px]"
)}
>
<MuxLogo
className={cn("block h-full w-full", leftInset > 0 && "-translate-y-px")}
className={cn("block h-full w-full", leftInset > 0 || "-translate-y-px")}
/>
{showUpdateShimmer && (
<div
Expand All @@ -290,7 +301,12 @@ export function TitleBar() {
/>
)}
</div>
<div className="text-accent flex h-3.5 w-3.5 items-center justify-center">
<div
className={cn(
"text-accent flex items-center justify-center",
leftInset > 0 ? "h-3 w-3" : "h-3.5 w-3.5"
)}
>
{updateBadgeIcon}
</div>
</div>
Expand All @@ -301,7 +317,12 @@ export function TitleBar() {
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<div className="min-w-0 cursor-text truncate text-xs font-normal tracking-wider select-text">
<div
className={cn(
"min-w-0 cursor-text truncate font-normal tracking-wider select-text",
leftInset > 0 ? "text-[10px]" : "text-xs"
)}
>
{gitDescribe ?? "(dev)"}
</div>
</TooltipTrigger>
Expand Down
130 changes: 38 additions & 92 deletions src/browser/stories/App.titlebar.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,109 +1,55 @@
/**
* Workspace titlebar / header stories
* Title bar stories - demonstrates title bar layout variants
*/

import React from "react";
import { appMeta, AppWithMocks, type AppStory } from "./meta.js";
import {
NOW,
createWorkspace,
groupWorkspacesByProject,
type GitStatusFixture,
} from "./mockFactory";
import { createGitStatusExecutor, expandProjects, selectWorkspace } from "./storyHelpers";
import { GIT_STATUS_INDICATOR_MODE_KEY } from "@/common/constants/storage";
import { within, userEvent, waitFor } from "@storybook/test";

import { createMockORPCClient } from "@/browser/stories/mocks/orpc";

export default {
...appMeta,
title: "App/Titlebar",
title: "App/TitleBar",
};

/**
* Git status tooltip in workspace header - verifies alignment is near the indicator.
* The header uses tooltipPosition="bottom" which requires align="start" to stay anchored.
* macOS desktop mode with traffic lights inset.
* Logo is stacked above version to fit in constrained space.
*/
export const GitStatusTooltip: AppStory = {
export const MacOSDesktop: AppStory = {
decorators: [
(Story) => {
// Save and restore window.api to prevent leaking to other stories
const originalApiRef = React.useRef(window.api);
window.api = {
platform: "darwin",
versions: {
node: "20.0.0",
chrome: "120.0.0",
electron: "28.0.0",
},
// This function's presence triggers isDesktopMode() → true
getIsRosetta: () => Promise.resolve(false),
};

// Cleanup on unmount
React.useEffect(() => {
const savedApi = originalApiRef.current;
return () => {
window.api = savedApi;
};
}, []);

return <Story />;
},
],
render: () => (
<AppWithMocks
setup={() => {
window.localStorage.setItem(GIT_STATUS_INDICATOR_MODE_KEY, JSON.stringify("line-delta"));

const workspaces = [
createWorkspace({
id: "ws-active",
name: "feature/tooltip-test",
projectName: "my-app",
createdAt: new Date(NOW - 3600000).toISOString(),
}),
];

const gitStatus = new Map<string, GitStatusFixture>([
[
"ws-active",
{
ahead: 3,
behind: 2,
dirty: 5,
outgoingAdditions: 150,
outgoingDeletions: 30,
headCommit: "WIP: Testing tooltip alignment",
},
],
]);

// Select workspace so header is visible
selectWorkspace(workspaces[0]);
expandProjects(["/home/user/projects/my-app"]);

return createMockORPCClient({
projects: groupWorkspacesByProject(workspaces),
workspaces,
executeBash: createGitStatusExecutor(gitStatus),
});
}}
setup={() =>
createMockORPCClient({
projects: new Map(),
workspaces: [],
})
}
/>
),
play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
const canvas = within(canvasElement);

// Wait for the workspace header to render with git status
await waitFor(
() => {
canvas.getByTestId("workspace-header");
},
{ timeout: 5000 }
);

// Wait for git status to appear in the header specifically
const header = canvas.getByTestId("workspace-header");
await waitFor(
() => {
within(header).getByText("+150");
},
{ timeout: 5000 }
);

// Hover over the git status indicator in the header (not the sidebar)
const plusIndicator = within(header).getByText("+150");
await userEvent.hover(plusIndicator);

// Wait for tooltip to appear with correct alignment (portaled with data-state="open")
// The key fix: data-align="start" anchors tooltip near the indicator (not "center")
await waitFor(
() => {
const tooltip = document.body.querySelector<HTMLElement>(
'.bg-modal-bg[data-state="open"][data-align="start"]'
);
if (!tooltip) throw new Error("git status tooltip not visible with align=start");
// Verify tooltip has expected structure
within(tooltip).getByText("Divergence:");
},
{ timeout: 5000 }
);

// Double-RAF to ensure layout is stable after async rendering
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
},
};
Loading