From d1f2e32b13a2fb593eecbf42222a43c16c6dea69 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Mon, 10 Aug 2026 04:24:42 -0500 Subject: [PATCH] =?UTF-8?q?fix(desktop):=20titlebar=20clusters=20=E2=80=94?= =?UTF-8?q?=20macOS=20Y=20nudge,=2024px=20targets,=2013.9px=20icons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Left cluster gets a macOS-only translate to sit on the traffic-light row. All titlebar tools use 24×24 hit areas with 13.9px Codicons (inline size beats unlayered codicon.css). Clusters share one flex shell with no gap — buttons abut and the hit target is the spacing. --- apps/desktop/src/app/artifacts/index.tsx | 5 +-- apps/desktop/src/app/contrib/controller.tsx | 6 ++-- apps/desktop/src/app/contrib/wiring.tsx | 14 +++----- apps/desktop/src/app/hud/hud-shell.tsx | 4 +-- .../desktop/src/app/overlays/overlay-view.tsx | 4 +-- .../src/app/shell/titlebar-controls.tsx | 34 +++++++++++-------- apps/desktop/src/app/shell/titlebar-icon.tsx | 19 +++++++++++ apps/desktop/src/app/shell/titlebar.test.ts | 34 ++++++++++++++++++- apps/desktop/src/app/shell/titlebar.ts | 33 ++++++++++++++++-- apps/desktop/src/components/ui/button.tsx | 2 +- apps/desktop/src/components/ui/codicon.tsx | 2 +- apps/desktop/src/styles.css | 12 +++++-- 12 files changed, 130 insertions(+), 39 deletions(-) create mode 100644 apps/desktop/src/app/shell/titlebar-icon.tsx diff --git a/apps/desktop/src/app/artifacts/index.tsx b/apps/desktop/src/app/artifacts/index.tsx index 1e3ee17787..01e90e1287 100644 --- a/apps/desktop/src/app/artifacts/index.tsx +++ b/apps/desktop/src/app/artifacts/index.tsx @@ -2,6 +2,7 @@ import type * as React from 'react' import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useNavigate } from 'react-router' +import { TitlebarIcon } from '@/app/shell/titlebar-icon' import { ZoomableImage } from '@/components/chat/zoomable-image' import { PageLoader } from '@/components/page-loader' import { Button } from '@/components/ui/button' @@ -28,7 +29,7 @@ import { urlSlugTitleLabel, useLinkTitle } from '@/lib/external-link' -import { FileImage, FileText, FolderOpen, Link2, Loader2, RefreshCw } from '@/lib/icons' +import { FileImage, FileText, FolderOpen, Link2 } from '@/lib/icons' import { downloadGatewayMediaFile, isRemoteGateway } from '@/lib/media' import { normalize } from '@/lib/text' import { fmtDayTime } from '@/lib/time' @@ -329,7 +330,7 @@ export function ArtifactsView({ setStatusbarItemGroup: _setStatusbarItemGroup, . size="icon-titlebar" variant="ghost" > - {refreshing ? : } + {refreshing ? : } } diff --git a/apps/desktop/src/app/contrib/controller.tsx b/apps/desktop/src/app/contrib/controller.tsx index 0f6e86cf06..0210188e23 100644 --- a/apps/desktop/src/app/contrib/controller.tsx +++ b/apps/desktop/src/app/contrib/controller.tsx @@ -746,13 +746,13 @@ export function ContribController() { /> diff --git a/apps/desktop/src/app/contrib/wiring.tsx b/apps/desktop/src/app/contrib/wiring.tsx index 13f54d59e2..88f55cc718 100644 --- a/apps/desktop/src/app/contrib/wiring.tsx +++ b/apps/desktop/src/app/contrib/wiring.tsx @@ -112,7 +112,7 @@ import { useSessionStateCache } from '../session/hooks/use-session-state-cache' import { startWorkspaceSession } from '../session/workspace-session-target' import { useOverlayRouting } from '../shell/hooks/use-overlay-routing' import { useWindowControlsOverlayWidth } from '../shell/hooks/use-window-controls-overlay-width' -import { titlebarControlsPosition } from '../shell/titlebar' +import { titlebarControlsPosition, titlebarControlsYNudge, titlebarToolsWidthCss } from '../shell/titlebar' import { TitlebarControls } from '../shell/titlebar-controls' import { UpdatesOverlay } from '../updates-overlay' @@ -967,11 +967,6 @@ export function ContribWiring({ children }: { children: ReactNode }) { const rightTitlebarTools = useTitlebarToolContributions('right') const connection = useStore($connection) const controlsPos = titlebarControlsPosition(connection?.windowButtonPosition, Boolean(connection?.isFullscreen)) - // Exact vertical centering: titlebarControlsPosition() returns - // (TITLEBAR_HEIGHT - TITLEBAR_CONTROL_HEIGHT) / 2, but TitlebarControls - // also applies a hard translate-y-0.5 (+2px) to its clusters. Cancel that - // constant so cluster center == bar center — measured, not eyeballed. - const controlsTranslateY = 2 // Windows/WSLg reserve native min/max/close on the right (AppShell parity: // prefer the live WCO measurement, fall back to the static reservation). const measuredOverlayWidth = useWindowControlsOverlayWidth() @@ -982,11 +977,11 @@ export function ContribWiring({ children }: { children: ReactNode }) { // sits ABOVE the grid, so AppShell's pane-width anchoring doesn't apply. const SYSTEM_TOOL_COUNT = 4 const paneToolCount = rightTitlebarTools.filter(tool => !tool.hidden).length - const systemToolsWidth = `calc(${SYSTEM_TOOL_COUNT} * (var(--titlebar-control-size) + 0.25rem))` + const systemToolsWidth = titlebarToolsWidthCss(SYSTEM_TOOL_COUNT) const titlebarToolsWidth = paneToolCount > 0 - ? `calc(${systemToolsWidth} + ${paneToolCount} * (var(--titlebar-control-size) + 0.25rem))` + ? `calc(${systemToolsWidth} + ${titlebarToolsWidthCss(paneToolCount)})` : systemToolsWidth return ( @@ -996,7 +991,8 @@ export function ContribWiring({ children }: { children: ReactNode }) { style={ { '--titlebar-controls-left': `${controlsPos.left}px`, - '--titlebar-controls-top': `${controlsPos.top - controlsTranslateY}px`, + '--titlebar-controls-top': `${controlsPos.top}px`, + '--titlebar-controls-y-nudge': titlebarControlsYNudge(connection?.windowButtonPosition), '--titlebar-tools-right': titlebarToolsRight, '--titlebar-tools-width': titlebarToolsWidth, '--shell-preview-toolbar-gap': systemToolsWidth diff --git a/apps/desktop/src/app/hud/hud-shell.tsx b/apps/desktop/src/app/hud/hud-shell.tsx index e91c637453..6585cb4b9c 100644 --- a/apps/desktop/src/app/hud/hud-shell.tsx +++ b/apps/desktop/src/app/hud/hud-shell.tsx @@ -2,8 +2,8 @@ import { useStore } from '@nanostores/react' import { type CSSProperties, useCallback, useEffect, useRef, useState } from 'react' import { useNavigate } from 'react-router' +import { TitlebarIcon } from '@/app/shell/titlebar-icon' import { Button } from '@/components/ui/button' -import { Codicon } from '@/components/ui/codicon' import { Tip } from '@/components/ui/tooltip' import { useI18n } from '@/i18n' import { chatMessageText } from '@/lib/chat-messages' @@ -390,7 +390,7 @@ export function HudShell() { type="button" variant="ghost" > - + diff --git a/apps/desktop/src/app/overlays/overlay-view.tsx b/apps/desktop/src/app/overlays/overlay-view.tsx index 812a07a122..5ff4810004 100644 --- a/apps/desktop/src/app/overlays/overlay-view.tsx +++ b/apps/desktop/src/app/overlays/overlay-view.tsx @@ -1,8 +1,8 @@ import { type CSSProperties, type ReactNode, useEffect } from 'react' import { TITLEBAR_HEIGHT } from '@/app/shell/titlebar' +import { TitlebarIcon } from '@/app/shell/titlebar-icon' import { Button } from '@/components/ui/button' -import { Codicon } from '@/components/ui/codicon' import { translateNow } from '@/i18n' import { ESCAPE_PRIORITY, isTopEscapeLayer, pushEscapeLayer } from '@/lib/escape-layers' import { triggerHaptic } from '@/lib/haptics' @@ -113,7 +113,7 @@ export function OverlayView({ size="icon-titlebar" variant="ghost" > - + diff --git a/apps/desktop/src/app/shell/titlebar-controls.tsx b/apps/desktop/src/app/shell/titlebar-controls.tsx index ee9b566bbd..dd2fc3fb98 100644 --- a/apps/desktop/src/app/shell/titlebar-controls.tsx +++ b/apps/desktop/src/app/shell/titlebar-controls.tsx @@ -6,7 +6,6 @@ import { hudTargetSessionId } from '@/app/hud/handoff' import { toggleLayoutEditMode } from '@/components/pane-shell/edit-mode' import { resetLayoutTree } from '@/components/pane-shell/tree/store' import { Button } from '@/components/ui/button' -import { Codicon } from '@/components/ui/codicon' import { Tip, TipKeybindLabel } from '@/components/ui/tooltip' import { useI18n } from '@/i18n' import { triggerHaptic } from '@/lib/haptics' @@ -23,7 +22,8 @@ import { import { appViewForPath, isOverlayView } from '../routes' -import { titlebarButtonClass } from './titlebar' +import { titlebarButtonClass, titlebarIconSizeCss, titlebarToolClusterClass, TITLEBAR_ICON_BADGE_SCALE } from './titlebar' +import { TitlebarIcon } from './titlebar-icon' export interface TitlebarTool { id: string @@ -61,12 +61,12 @@ function LayoutGlyph({ modHeld }: { modHeld: boolean }) { return ( <> - + - + - + @@ -128,7 +128,7 @@ export function TitlebarControls({ leftTools = [], tools = [], onOpenSettings }: const leftToolbarTools: TitlebarTool[] = [ { actionId: 'view.toggleSidebar', - icon: , + icon: , id: 'sidebar', label: leftEdge.open ? t.titlebar.hideSidebar : t.titlebar.showSidebar, onSelect: () => { @@ -138,7 +138,7 @@ export function TitlebarControls({ leftTools = [], tools = [], onOpenSettings }: }, { actionId: 'view.flipPanes', - icon: , + icon: , id: 'flip-panes', label: t.titlebar.swapSidebarSides, onSelect: () => { @@ -151,7 +151,7 @@ export function TitlebarControls({ leftTools = [], tools = [], onOpenSettings }: const rightSidebarTool: TitlebarTool = { actionId: 'view.toggleRightSidebar', - icon: , + icon: , id: 'right-sidebar', label: rightEdge.open ? t.titlebar.hideRightSidebar : t.titlebar.showRightSidebar, onSelect: () => { @@ -188,7 +188,7 @@ export function TitlebarControls({ leftTools = [], tools = [], onOpenSettings }: // crowds the ⌘⇧H hint off the tooltip. Label only — the hint is appended // from the action registry, same as every other tool here. actionId: 'view.toggleHud', - icon: , + icon: , id: 'hud', label: t.titlebar.enterHud, onSelect: () => { @@ -198,14 +198,14 @@ export function TitlebarControls({ leftTools = [], tools = [], onOpenSettings }: }, { active: hapticsMuted, - icon: , + icon: , id: 'haptics', label: hapticsMuted ? t.titlebar.unmuteHaptics : t.titlebar.muteHaptics, onSelect: toggleHaptics }, { actionId: 'nav.settings', - icon: , + icon: , id: 'settings', label: t.titlebar.openSettings, onSelect: () => { @@ -230,7 +230,10 @@ export function TitlebarControls({ leftTools = [], tools = [], onOpenSettings }: <>
{leftToolbarTools .filter(tool => !tool.hidden) @@ -250,7 +253,10 @@ export function TitlebarControls({ leftTools = [], tools = [], onOpenSettings }: {visiblePaneTools.length > 0 && (
{visiblePaneTools.map(tool => ( @@ -260,7 +266,7 @@ export function TitlebarControls({ leftTools = [], tools = [], onOpenSettings }:
{visibleSystemTools.map(tool => ( diff --git a/apps/desktop/src/app/shell/titlebar-icon.tsx b/apps/desktop/src/app/shell/titlebar-icon.tsx new file mode 100644 index 0000000000..9c2e98495b --- /dev/null +++ b/apps/desktop/src/app/shell/titlebar-icon.tsx @@ -0,0 +1,19 @@ +import { Codicon } from '@/components/ui/codicon' +import { cn } from '@/lib/utils' + +import { titlebarIconSizeCss } from './titlebar' + +/** Titlebar-cluster glyph — 13.9px inline (see titlebarIconSizeCss). */ +export function TitlebarIcon({ + className, + name, + size = titlebarIconSizeCss(), + spinning +}: { + className?: string + name: string + size?: number | string + spinning?: boolean +}) { + return +} diff --git a/apps/desktop/src/app/shell/titlebar.test.ts b/apps/desktop/src/app/shell/titlebar.test.ts index 8b6f2d8678..2b650ba9da 100644 --- a/apps/desktop/src/app/shell/titlebar.test.ts +++ b/apps/desktop/src/app/shell/titlebar.test.ts @@ -2,11 +2,29 @@ import { describe, expect, it } from 'vitest' import { TITLEBAR_CONTROL_OFFSET_X, + TITLEBAR_CONTROL_SIZE, TITLEBAR_EDGE_INSET, TITLEBAR_FALLBACK_WINDOW_BUTTON_X, - titlebarControlsPosition + TITLEBAR_ICON_SIZE, + TITLEBAR_MAC_TRAFFIC_LIGHTS_Y_NUDGE, + titlebarControlsPosition, + titlebarControlsYNudge, + titlebarIconSizeCss, + titlebarToolsWidthCss } from './titlebar' +describe('titlebar sizing', () => { + it('uses 24×24 hit targets and 13.9px glyphs', () => { + expect(TITLEBAR_CONTROL_SIZE).toBe(24) + expect(TITLEBAR_ICON_SIZE).toBe(13.9) + expect(titlebarIconSizeCss()).toBe('13.9px') + }) + + it('reserves width from abutting hit targets only', () => { + expect(titlebarToolsWidthCss(4)).toBe('calc(4 * var(--titlebar-control-size))') + }) +}) + describe('titlebarControlsPosition', () => { it('offsets controls from visible traffic lights', () => { expect(titlebarControlsPosition({ x: 24, y: 10 }).left).toBe(24 + TITLEBAR_CONTROL_OFFSET_X) @@ -24,3 +42,17 @@ describe('titlebarControlsPosition', () => { expect(titlebarControlsPosition(undefined).left).toBe(TITLEBAR_FALLBACK_WINDOW_BUTTON_X + TITLEBAR_CONTROL_OFFSET_X) }) }) + +describe('titlebarControlsYNudge', () => { + it('nudges the left cluster on macOS when traffic lights are visible', () => { + expect(titlebarControlsYNudge({ x: 24, y: 10 })).toBe(TITLEBAR_MAC_TRAFFIC_LIGHTS_Y_NUDGE) + }) + + it('stays flat on Windows/Linux and macOS fullscreen', () => { + expect(titlebarControlsYNudge(null)).toBe('0px') + }) + + it('nudges while macOS window-button position is still unknown', () => { + expect(titlebarControlsYNudge(undefined)).toBe(TITLEBAR_MAC_TRAFFIC_LIGHTS_Y_NUDGE) + }) +}) diff --git a/apps/desktop/src/app/shell/titlebar.ts b/apps/desktop/src/app/shell/titlebar.ts index 20fbba1736..63e902d3bf 100644 --- a/apps/desktop/src/app/shell/titlebar.ts +++ b/apps/desktop/src/app/shell/titlebar.ts @@ -2,22 +2,51 @@ import type { HermesConnection } from '@/global' export const TITLEBAR_HEIGHT = 34 export const MACOS_TRAFFIC_LIGHTS_HEIGHT = 14 -export const TITLEBAR_ICON_SIZE = 12 +/** Titlebar tool hit target (both axes). */ +export const TITLEBAR_CONTROL_SIZE = 24 +/** Codicon glyph box in titlebar clusters — optical match to traffic-light row. */ +export const TITLEBAR_ICON_SIZE = 13.9 +export const TITLEBAR_ICON_BADGE_SCALE = 0.65 export const TITLEBAR_CONTROL_OFFSET_X = 74 -export const TITLEBAR_CONTROL_HEIGHT = 22 +export const TITLEBAR_CONTROL_HEIGHT = TITLEBAR_CONTROL_SIZE export const TITLEBAR_CONTROLS_TOP = (TITLEBAR_HEIGHT - TITLEBAR_CONTROL_HEIGHT) / 2 + +/** Inline font-size for titlebar Codicons — beats unlayered codicon.css `font: 16px`. */ +export function titlebarIconSizeCss(scale = 1): string { + return `${TITLEBAR_ICON_SIZE * scale}px` +} export const TITLEBAR_FALLBACK_WINDOW_BUTTON_X = 24 // Edge inset used when no left-side native controls take up that space — // Windows/Linux (native overlay is on the right) and macOS fullscreen // (traffic lights are hidden). Matches the right-cluster's 0.75rem padding. export const TITLEBAR_EDGE_INSET = 14 +// macOS traffic-light row only: nudge the left toolbar cluster down to sit on +// the same optical center as the native buttons. null windowButtonPosition means +// Windows/Linux (controls on the right) or macOS fullscreen (lights hidden). +export const TITLEBAR_MAC_TRAFFIC_LIGHTS_Y_NUDGE = 'calc(var(--spacing) * 0.9)' + +export function titlebarControlsYNudge( + windowButtonPosition: HermesConnection['windowButtonPosition'] | undefined +): string { + return windowButtonPosition !== null ? TITLEBAR_MAC_TRAFFIC_LIGHTS_Y_NUDGE : '0px' +} + // Titlebar palette only. All sizing/radius/cursor/centering come from the // shared