fix(titlebar): reserve Windows window controls for logo

This commit is contained in:
2026-09-02 10:42:16 +08:00
parent d83757a115
commit e800d42595
5 changed files with 98 additions and 6 deletions

View File

@@ -0,0 +1,51 @@
# Task: Fix Windows titlebar logo overlap
## Identity
- Task ID: 20260901-windows-titlebar-logo-overlap-5100e298
- Mode: Feature
- Branch: codex/20260901-windows-titlebar-logo-overlap-5100e298-windows-titlebar-logo-overlap
- Worktree: D:\Datas\OthersProjects\makelore-windows-titlebar-logo-overlap-5100e298
- Base commit: 7f0e9310a7f394249fab3cadc5c82d70f91cbfeb
- Owner: codex
- Status: Ready for integration
## Scope
- Reproduce and fix the Windows application-shell overlap between the Makelore logo and native minimize/maximize/close controls.
- Add focused regression coverage at the existing window/layout seam.
- Preserve existing macOS/Linux behavior and the single Makelore light visual system.
## Intent And Constraints
- Use the smallest platform-aware layout or window-configuration change that guarantees a non-overlapping Windows caption-control reserve.
- Do not change app identity, protocols, global user-data paths, environment variables, backend contracts, or unrelated product layout.
- Verify the exact reported symptom first; run focused tests, typecheck, relevant lint, and `pnpm run build:vite` for the final change.
- Work only in the isolated task worktree; no subagent is authorized for this repository task.
## Outcome
- Confirmed the exact Windows Canvas failure with a deterministic Electron E2E: the 96px Logo ended at x=1280 while the minimize button began at x=1140.
- Added a single 148px Windows control-region width (three 44px buttons plus 8px padding on each side) and use it both to size the control container and inset the Logo. macOS keeps its existing zero inset and native traffic-light behavior.
- Added a dedicated Windows Canvas titlebar E2E and updated the existing Coding titlebar geometry assertion for the platform-specific layout.
- Updated the focused TitleBar unit test to lock the Windows inset.
## Verification
- Red-capable reproduction before the fix, twice: `node .\node_modules\@playwright\test\cli.js test tests/e2e/windows-titlebar.spec.ts` failed consistently with `Expected <= 1140, Received 1280`.
- `pnpm exec vitest run tests/unit/title-bar.test.tsx --maxWorkers=1`: 1 file / 11 tests passed.
- `pnpm run typecheck`: passed.
- Scoped ESLint for the four touched production/test files: passed with no output.
- `pnpm run lint:check`: passed with 0 errors and 5 pre-existing warnings in untouched `src/pages/Home/index.tsx` and `src/pages/Makelore/index.tsx`.
- `pnpm test`: 214 files / 1759 tests passed, 2 skipped; pressure suite 1/1 passed.
- `pnpm run build:vite`: passed after the production change.
- `node .\node_modules\@playwright\test\cli.js test tests/e2e/windows-titlebar.spec.ts`: 1/1 passed.
- `node .\node_modules\@playwright\test\cli.js test tests/e2e/pi-coding-first-chat.spec.ts --grep 'PI feature UI isolates Conversations'`: 1/1 passed.
## Follow-ups
- None.
## Promotion Candidates
- None. This is a local Renderer layout correction and does not change canonical architecture, domain rules, or product direction.

View File

@@ -17,6 +17,9 @@ import type { SidebarPeekSource } from './sidebar-peek';
type SidebarPeekChange = (open: boolean, source: SidebarPeekSource) => void;
// Three 44px buttons plus the control container's 8px padding on each side.
const WINDOWS_TITLEBAR_CONTROLS_WIDTH = 148;
type TitleBarProps = {
integrated?: boolean;
workspaceLayout?: boolean;
@@ -47,6 +50,7 @@ function ProductTitleBar({
overlay = false,
pageTitle,
children,
windowControlsWidth = 0,
nativeTrafficLights = false,
sidebarPeekOpen = false,
onSidebarPeekChange,
@@ -56,6 +60,7 @@ function ProductTitleBar({
overlay?: boolean;
pageTitle?: string;
children?: React.ReactNode;
windowControlsWidth?: number;
nativeTrafficLights?: boolean;
sidebarPeekOpen?: boolean;
onSidebarPeekChange?: SidebarPeekChange;
@@ -119,7 +124,7 @@ function ProductTitleBar({
overlay ? 'bg-transparent' : 'bg-background',
)}
style={{
right: '0px',
right: `${windowControlsWidth}px`,
}}
>
<img
@@ -218,6 +223,7 @@ function ProductTitleBar({
'no-drag absolute inset-y-0 right-0 z-10 flex items-center justify-end gap-1 px-2',
overlay && 'pointer-events-auto',
)}
style={windowControlsWidth ? { width: `${windowControlsWidth}px` } : undefined}
>
{children}
</div>
@@ -257,7 +263,15 @@ function WindowsTitleBar({ integrated, workspaceLayout, overlay, pageTitle, side
};
return (
<ProductTitleBar integrated={integrated} workspaceLayout={workspaceLayout} overlay={overlay} pageTitle={pageTitle} sidebarPeekOpen={sidebarPeekOpen} onSidebarPeekChange={onSidebarPeekChange}>
<ProductTitleBar
integrated={integrated}
workspaceLayout={workspaceLayout}
overlay={overlay}
pageTitle={pageTitle}
windowControlsWidth={WINDOWS_TITLEBAR_CONTROLS_WIDTH}
sidebarPeekOpen={sidebarPeekOpen}
onSidebarPeekChange={onSidebarPeekChange}
>
<div className="no-drag pointer-events-auto flex h-full">
<button
onClick={handleMinimize}

View File

@@ -711,9 +711,15 @@ test('PI feature UI isolates Conversations and exposes queue, interaction, model
]);
expect(titlebarBounds.every((bounds) => bounds?.y === 0 && bounds.height === 40)).toBe(true);
const logoBounds = await page.getByTestId('titlebar-logo').boundingBox();
const windowWidth = await page.evaluate(() => window.innerWidth);
expect(logoBounds).not.toBeNull();
expect(Math.abs((logoBounds!.x + logoBounds!.width) - windowWidth)).toBeLessThanOrEqual(1);
if (await page.evaluate(() => window.electron.platform === 'win32')) {
const minimizeBounds = await page.getByTitle('Minimize').boundingBox();
expect(minimizeBounds).not.toBeNull();
expect(logoBounds!.x + logoBounds!.width).toBeLessThanOrEqual(minimizeBounds!.x);
} else {
const windowWidth = await page.evaluate(() => window.innerWidth);
expect(Math.abs((logoBounds!.x + logoBounds!.width) - windowWidth)).toBeLessThanOrEqual(1);
}
const conversationHeader = page.getByTestId('coding-conversation-header');
await expect(conversationHeader).toContainText('新对话');
await expect(conversationHeader).not.toContainText('Pi ·');

View File

@@ -0,0 +1,20 @@
import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron';
test('keeps the Canvas logo clear of Windows window controls', async ({ launchElectronApp }) => {
test.skip(process.platform !== 'win32', 'Windows custom titlebar only');
const app = await launchElectronApp({ skipSetup: true });
try {
const page = await getStableWindow(app);
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
await page.getByTestId('ai-module-option-painting').click();
const logoBounds = await page.getByTestId('titlebar-logo').boundingBox();
const minimizeBounds = await page.getByTitle('Minimize').boundingBox();
expect(logoBounds).not.toBeNull();
expect(minimizeBounds).not.toBeNull();
expect(logoBounds!.x + logoBounds!.width).toBeLessThanOrEqual(minimizeBounds!.x);
} finally {
await closeElectronApp(app);
}
});

View File

@@ -188,14 +188,15 @@ describe('TitleBar platform behavior', () => {
expect(onSidebarPeekChange).toHaveBeenNthCalledWith(2, false, 'titlebar');
});
it('renders custom controls on Windows', async () => {
it('renders custom controls and reserves their space on Windows', async () => {
window.electron.platform = 'win32';
render(<TitleBar />);
render(<TitleBar integrated />);
expect(screen.getByTitle('Minimize')).toBeInTheDocument();
expect(screen.getByTitle('Maximize')).toBeInTheDocument();
expect(screen.getByTitle('Close')).toBeInTheDocument();
expect(screen.getByTestId('titlebar-logo')).toHaveStyle({ right: '148px' });
await waitFor(() => {
expect(invokeIpcMock).toHaveBeenCalledWith('window:isMaximized');