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
12 changes: 8 additions & 4 deletions frontend/src/renderer/components/CenterPanelShell.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import type { ReactNode } from "react";
import { cn } from "../lib/utils";
import { useWindowFullScreen } from "../hooks/useWindowFullScreen";
import { isMacPlatform } from "../lib/platform";
import { isLinuxPlatform, isMacPlatform } from "../lib/platform";
import { useUiStore } from "../stores/ui-store";

const isMac = isMacPlatform();
const isLinux = isLinuxPlatform();

/**
* Shared inset center panel: sidebar-colored outer frame with a bordered inner
* surface. Used by the shell's app routes (kanban / session), the welcome board,
* and settings. Chrome lives in `styles.css` (`center-panel-shell` +
* `center-panel-surface`).
*
* `titlebarAlign` (default true) pulls Board/Terminal titles into the macOS
* traffic-light band.
* `titlebarAlign` (default true) pulls Board/Terminal titles up level with the
* fixed TitlebarNav cluster (macOS + Linux).
*/
export function CenterPanelShell({
className,
Expand All @@ -28,14 +29,17 @@ export function CenterPanelShell({
}) {
const isSidebarOpen = useUiStore((state) => state.isSidebarOpen);
const isFullScreen = useWindowFullScreen();
const align = titlebarAlign && isMac;
const align = titlebarAlign && (isMac || isLinux);
const titlebarClearance = align && !isSidebarOpen;

return (
<div
className={cn(
"center-panel-shell",
align && "center-panel-shell--mac",
// Linux has no traffic lights, so the sidebar-closed title clears the
// cluster at its own left edge rather than the macOS offset.
isLinux && "center-panel-shell--linux",
titlebarClearance && "center-panel-shell--titlebar-clearance",
titlebarClearance && isFullScreen && "center-panel-shell--titlebar-clearance-fullscreen",
align && isFullScreen && "center-panel-shell--fullscreen",
Expand Down
30 changes: 6 additions & 24 deletions frontend/src/renderer/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ import {
SidebarRail,
SidebarMenuSub,
SidebarMenuSubItem,
SidebarTrigger,
useSidebar,
} from "./ui/sidebar";
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
Expand All @@ -51,12 +50,13 @@ import { useUiStore } from "../stores/ui-store";
import { ConfirmDialog } from "./ConfirmDialog";
import { CreateProjectFlow, type CreateProjectInput } from "./CreateProjectFlow";
import { ResizeHandle } from "./ResizeHandle";
import { isMacPlatform, isWindowsPlatform } from "../lib/platform";
import { isLinuxPlatform, isMacPlatform } from "../lib/platform";

// On macOS the native traffic lights and fixed TitlebarNav occupy the titlebar
// above this surface. Win/Linux hang the sidebar under their shell chrome.
// macOS + Linux paint framed chrome: the fixed TitlebarNav cluster carries the
// sidebar toggle + history arrows above this surface. Windows hangs the sidebar
// under its custom titlebar.
const isMac = isMacPlatform();
const isWindows = isWindowsPlatform();
const isLinux = isLinuxPlatform();
const noDragStyle = isMac ? ({ WebkitAppRegion: "no-drag" } as React.CSSProperties) : undefined;

// Shared styling for the per-project hover action buttons (dashboard,
Expand Down Expand Up @@ -262,24 +262,6 @@ export function Sidebar({
nightly
</span>
)}
{!isMac && !isWindows && (
<Tooltip>
<TooltipTrigger asChild>
<SidebarTrigger
aria-label={isCollapsed ? "Expand sidebar" : "Collapse sidebar"}
className={cn(
"shrink-0 text-passive hover:bg-interactive-hover hover:text-foreground",
isCollapsed
? "grid size-9 rounded-lg [&_svg]:size-4"
: "sidebar-expanded-chrome size-icon-xl rounded-sm p-0 [&_svg]:size-icon-lg",
)}
/>
</TooltipTrigger>
<TooltipContent side={isCollapsed ? "right" : undefined}>
{isCollapsed ? "Expand sidebar · ⌘B" : "Collapse sidebar · ⌘B"}
</TooltipContent>
</Tooltip>
)}
</div>
</SidebarHeader>

Expand Down Expand Up @@ -341,7 +323,7 @@ export function Sidebar({
<SidebarFooter
className={cn(
"relative mt-auto gap-0 overflow-hidden px-2.5 pb-0 pt-1.5 transition-[padding] duration-200 ease-linear group-data-[collapsible=icon]:min-h-16 group-data-[collapsible=icon]:items-center group-data-[collapsible=icon]:px-1.5 group-data-[collapsible=icon]:pb-0 group-data-[collapsible=icon]:pt-1.5",
isMac ? "mb-3" : "mb-5",
isMac || isLinux ? "mb-3" : "mb-5",
)}
>
{/* Always-present daemon status mirror for the smoke suite: no visible
Expand Down
26 changes: 18 additions & 8 deletions frontend/src/renderer/components/TitlebarNav.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { useCanGoBack, useRouter } from "@tanstack/react-router";
import { ArrowLeft, ArrowRight, PanelLeft } from "lucide-react";
import { useEffect, useState } from "react";
import { isMacPlatform } from "../lib/platform";
import { isLinuxPlatform, isMacPlatform } from "../lib/platform";
import { useUiStore } from "../stores/ui-store";

const isMac = isMacPlatform();
const isLinux = isLinuxPlatform();
const noDragStyle = isMac ? ({ WebkitAppRegion: "no-drag" } as React.CSSProperties) : undefined;

// macOS-only sidebar chrome cluster (sidebar toggle + history arrows). It stays
// fixed while the sidebar expands, collapses, or appears as a hover preview.
// Sidebar chrome cluster (sidebar toggle + history arrows). It stays fixed while
// the sidebar expands, collapses, or appears as a hover preview. macOS pins it
// beside the traffic lights; Linux has no traffic lights, so it sits at the
// sidebar's top-left. (Windows keeps these controls in its own titlebar.)
// The installed router has no useCanGoForward, and deriving one as
// `__TSR_index < history.length - 1` (the upstream hook's approach) is wrong
// here: window.history.length also counts entries the router never created —
Expand Down Expand Up @@ -46,12 +49,19 @@ export function TitlebarNav({
const canGoBack = useCanGoBack();
const canGoForward = useCanGoForward();

if (!isMac) return null;
if (!isMac && !isLinux) return null;

// Stay pinned beside the traffic lights. Nudge down slightly so the
// toggle/arrows share a centerline with the native dots (y: 12).
const leftClass = isFullScreen ? "left-titlebar-cluster-left-fullscreen" : "left-titlebar-cluster-left";
const topClass = isFullScreen ? "top-0" : "top-0.5";
// macOS: pinned beside the traffic lights, nudged down so the toggle/arrows
// share a centerline with the native dots (y: 12). Linux: no traffic lights,
// so it sits at the sidebar's top-left within the reserved titlebar band.
const leftClass = !isMac
? "left-1.5"
: isFullScreen
? "left-titlebar-cluster-left-fullscreen"
: "left-titlebar-cluster-left";
// Linux: match the framed board titlebar's y (mac inset 2px + surface border
// 1px) so the cluster shares its centerline with the project title.
const topClass = !isMac ? "top-0.75" : isFullScreen ? "top-0" : "top-0.5";

return (
<div
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/renderer/lib/platform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,13 @@ describe("renderer platform behavior", () => {
expect(usesBoardActionsInPanel()).toBe(false);
});

it("uses the framed app topbar on Linux", () => {
it("hides the shell topbar on Linux and keeps board actions in the panel", () => {
spoofPlatform("Linux x86_64");

expect(isLinuxPlatform()).toBe(true);
expect(isWindowsPlatform()).toBe(false);
expect(usesFramedAppTopbar()).toBe(true);
expect(hidesShellTopbar()).toBe(false);
expect(usesBoardActionsInPanel()).toBe(false);
expect(hidesShellTopbar()).toBe(true);
expect(usesBoardActionsInPanel()).toBe(true);
});
});
9 changes: 6 additions & 3 deletions frontend/src/renderer/lib/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,14 @@ export function usesFramedAppTopbar(): boolean {
}

/**
* macOS: shell does not mount ShellTopbar (full-height inset panel + drag
* strip). SessionView mounts the same topbar in-panel for session actions.
* macOS + Linux: shell does not mount ShellTopbar (full-height inset panel).
* The sidebar toggle + history arrows live in the fixed TitlebarNav cluster and
* board/session actions mount in-panel, so both platforms share the framed
* chrome. (macOS additionally paints a traffic-light drag strip.) Windows keeps
* the ShellTopbar under its custom titlebar.
*/
export function hidesShellTopbar(): boolean {
return isMacPlatform();
return isMacPlatform() || isLinuxPlatform();
}

/**
Expand Down
12 changes: 4 additions & 8 deletions frontend/src/renderer/routes/_shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,6 @@ function ShellLayout() {
: routeParams.sessionId
? workspaces.find((workspace) => workspace.sessions.some((session) => session.id === routeParams.sessionId))?.id
: undefined;
const isSessionRoute =
Boolean(matchRoute({ to: "/projects/$projectId/sessions/$sessionId", fuzzy: true })) ||
Boolean(matchRoute({ to: "/sessions/$sessionId", fuzzy: true }));
// First-launch root board only (no projects in scope).
const isWelcomeBoard = Boolean(matchRoute({ to: "/" })) && workspaces.length === 0;
const isSettingsRoute =
Expand Down Expand Up @@ -547,15 +544,14 @@ function ShellLayout() {
} as CSSProperties
}
>
{/* Hang the fixed sidebar below shell chrome on Win/Linux. macOS
keeps a full-height sidebar beneath the fixed titlebar controls. */}
{/* macOS + Linux reserve a titlebar band for the fixed TitlebarNav
cluster above a full-height sidebar; Windows hangs the sidebar
below its custom titlebar. */}
<Sidebar
hideEdgeBorder={isWelcomeBoard}
isOverlay={isSidebarPeekOpen && !isSidebarOpen}
onPreviewLeave={scheduleSidebarPeekClose}
underTopbar={
isMac || isWindows || (!framedAppTopbar && !hideShellTopbar && (isLinux ? isSessionRoute : true))
}
underTopbar={isMac || isWindows || isLinux}
topbarOffset={isWindows ? "titlebar" : "toolbar"}
onCreateProject={createProject}
onInitializeProject={initializeProjectRepository}
Expand Down
11 changes: 8 additions & 3 deletions frontend/src/renderer/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -399,9 +399,9 @@
padding-left: var(--size-center-panel-inline-inset);
}

/* macOS: one uniform inset on every side (matches the near-flush top that keeps
* Board/Terminal titles level with the fixed TitlebarNav). Two classes so it
* also wins over the `.sidebar-hidden` left gutter above. */
/* macOS + Linux: one uniform inset on every side (matches the near-flush top
* that keeps Board/Terminal titles level with the fixed TitlebarNav). Two
* classes so it also wins over the `.sidebar-hidden` left gutter above. */
.center-panel-shell.center-panel-shell--mac {
padding: var(--size-center-panel-inset-mac);
}
Expand All @@ -426,6 +426,11 @@
padding-left: calc(var(--size-titlebar-content-offset) - var(--size-center-panel-inset-mac));
}

/* Linux clears the cluster from its own left edge (no traffic-light row). */
.center-panel-shell--linux.center-panel-shell--titlebar-clearance .center-panel-titlebar {
padding-left: calc(var(--size-titlebar-content-offset-linux) - var(--size-center-panel-inset-mac));
}

/* Fullscreen: the shell is flush, so measure from the cluster's own left edge. */
.center-panel-shell--titlebar-clearance-fullscreen .center-panel-titlebar {
padding-left: calc(
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/styles/tokens.css
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,9 @@
--size-sidebar-default: 240px;
--size-sidebar-mobile: 288px;
--size-titlebar-cluster-left: 79px;
/* Linux has no traffic lights: the cluster sits at the sidebar's own left
* inset (TitlebarNav `left-1.5`). */
--size-titlebar-cluster-left-linux: 6px;
/* TitlebarNav cluster: 3 × --size-control-md buttons + 2 × 4px flex gaps. */
--size-titlebar-cluster-width: calc(3 * var(--size-control-md) + 2 * 4px);
/* Breathing room between the last arrow and the adjacent in-panel title. */
Expand All @@ -228,6 +231,9 @@
--size-titlebar-content-offset: calc(
var(--size-titlebar-cluster-left) + var(--size-titlebar-cluster-width) + var(--size-titlebar-content-gap)
);
--size-titlebar-content-offset-linux: calc(
var(--size-titlebar-cluster-left-linux) + var(--size-titlebar-cluster-width) + var(--size-titlebar-content-gap)
);
/* Clears the macOS traffic-light row used by the fixed titlebar controls. */
--size-traffic-light-clearance: 36px;
--size-traffic-light-clearance-fullscreen: var(--space-2);
Expand Down
Loading