fix(titlebar): reserve Windows window controls for logo
This commit is contained in:
@@ -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.
|
||||
@@ -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}
|
||||
|
||||
@@ -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 ·');
|
||||
|
||||
20
tests/e2e/windows-titlebar.spec.ts
Normal file
20
tests/e2e/windows-titlebar.spec.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user