mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 19:46:16 -08:00
fix(workspace): keep the macOS rail clear of the lights when zoomed out
App zoom (webFrame.setZoomLevel) scales CSS pixels but not the native traffic lights: at the minimum zoom the fixed 56px inset put the first rail link at about 27 window pixels, over the buttons (y 20 to 34). The rail now publishes the page zoom factor (outerWidth / innerWidth, refreshed on resize) and keeps at least 48 window pixels: max(56px, 48px / zoom). The macOS E2E now requires a native button position instead of falling back to y=0, measures the gap in window pixels, and repeats it at the minimum zoom (fails at 27 without this change). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f14d608ce7
commit
51b8928df5
6 files changed
+119
-7
No files matched your search
@@ -217,10 +217,37 @@ test.describe('macOS traffic lights', () => {
|
||||
const lights = await app.electronApp.evaluate(({ BrowserWindow }) =>
|
||||
BrowserWindow.getAllWindows()[0]?.getWindowButtonPosition()
|
||||
);
|
||||
// The buttons are about 14pt tall; keep a visible gap below them.
|
||||
expect(linkBox?.y ?? 0).toBeGreaterThanOrEqual(
|
||||
(lights?.y ?? 0) + 14 + 16
|
||||
expect(lights, 'native window button position').toBeTruthy();
|
||||
const lightsY = lights?.y ?? Number.NaN;
|
||||
// The buttons are about 14pt tall; keep a visible gap below them,
|
||||
// measured in window pixels (CSS pixels times the zoom factor).
|
||||
const linkTopInWindowPixels = async (): Promise<number> => {
|
||||
const [box, zoom] = await Promise.all([
|
||||
firstLink.boundingBox(),
|
||||
page.evaluate(() => window.outerWidth / window.innerWidth),
|
||||
]);
|
||||
return (box?.y ?? Number.NaN) * zoom;
|
||||
};
|
||||
expect(await linkTopInWindowPixels()).toBeGreaterThanOrEqual(
|
||||
lightsY + 14 + 16
|
||||
);
|
||||
|
||||
// App zoom scales CSS pixels but not the native buttons: at the
|
||||
// smallest zoom the inset must still clear them.
|
||||
for (let step = 0; step < 8; step++) {
|
||||
await page.evaluate(() =>
|
||||
window.electron.adjustZoomLevel('out')
|
||||
);
|
||||
}
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(() => window.outerWidth / window.innerWidth)
|
||||
)
|
||||
.toBeLessThan(0.6);
|
||||
await expect
|
||||
.poll(linkTopInWindowPixels)
|
||||
.toBeGreaterThanOrEqual(lightsY + 14 + 8);
|
||||
await page.evaluate(() => window.electron.adjustZoomLevel('reset'));
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
|
||||
@@ -330,7 +330,11 @@ The Electron window hides the native title bar on all desktop platforms
|
||||
sit in the 56 px header band above the rail, so the macOS rail
|
||||
(`.app-rail.is-macos`) starts its first link at 56 px: level with the
|
||||
content area and the dashboard hero, with its hover surface clear of the
|
||||
lights. `window-controls.e2e.ts` checks both on macOS.
|
||||
lights. App zoom scales CSS pixels but not the lights, so the rail
|
||||
publishes the page zoom factor (`outerWidth / innerWidth`, refreshed on
|
||||
`resize`) as `--rail-zoom-factor` and keeps at least 48 window pixels when
|
||||
zoomed out. `window-controls.e2e.ts` checks the alignment and the gap at
|
||||
default and minimum zoom on macOS.
|
||||
2. Windows and Linux use renderer-drawn window controls
|
||||
(`app-window-controls`, `libs/ui/components/src/lib/window-controls/`).
|
||||
`frame` is intentionally left untouched so native resize borders and
|
||||
|
||||
+5
-1
@@ -1,6 +1,10 @@
|
||||
<!-- No brand mark: it linked to the page the first workspace link opens.
|
||||
The page's Back lives in the header's leading slot. -->
|
||||
<aside class="app-rail" [class.is-macos]="isMacOS()">
|
||||
<aside
|
||||
class="app-rail"
|
||||
[class.is-macos]="isMacOS()"
|
||||
[style.--rail-zoom-factor]="isMacOS() ? zoomFactor() : null"
|
||||
>
|
||||
<app-workspace-shell-rail-links [links]="workspaceLinks()" />
|
||||
|
||||
@if (primaryContextLinks().length > 0) {
|
||||
|
||||
+3
-1
@@ -32,8 +32,10 @@
|
||||
// The traffic lights sit in the header band (56px, see the shell grid).
|
||||
// The first link starts below that band, where the content area and the
|
||||
// dashboard hero start, so its hover surface never touches the lights.
|
||||
// App zoom scales CSS pixels but not the lights: zoomed out, the inset
|
||||
// keeps 48 window pixels (lights' bottom plus a gap) instead.
|
||||
&.is-macos {
|
||||
padding-top: 56px;
|
||||
padding-top: max(56px, calc(48px / var(--rail-zoom-factor, 1)));
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
|
||||
+42
@@ -84,6 +84,48 @@ describe('WorkspaceShellRailComponent', () => {
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
describe('macOS zoom inset', () => {
|
||||
const setWindowWidths = (outer: number, inner: number) => {
|
||||
Object.defineProperty(window, 'outerWidth', {
|
||||
configurable: true,
|
||||
value: outer,
|
||||
});
|
||||
Object.defineProperty(window, 'innerWidth', {
|
||||
configurable: true,
|
||||
value: inner,
|
||||
});
|
||||
};
|
||||
const zoomVar = () =>
|
||||
(
|
||||
fixture.nativeElement.querySelector('.app-rail') as HTMLElement
|
||||
).style.getPropertyValue('--rail-zoom-factor');
|
||||
|
||||
afterEach(() => setWindowWidths(1024, 1024));
|
||||
|
||||
it('publishes the page zoom factor on macOS and follows resize', () => {
|
||||
setWindowWidths(1200, 1200);
|
||||
fixture.componentRef.setInput('isMacOS', true);
|
||||
fixture.detectChanges();
|
||||
TestBed.tick();
|
||||
fixture.detectChanges();
|
||||
expect(zoomVar()).toBe('1');
|
||||
|
||||
// Zoomed out to 50%: the viewport holds twice the CSS pixels.
|
||||
setWindowWidths(1200, 2400);
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
fixture.detectChanges();
|
||||
expect(zoomVar()).toBe('0.5');
|
||||
});
|
||||
|
||||
it('leaves the inset alone off macOS', () => {
|
||||
setWindowWidths(1200, 2400);
|
||||
fixture.detectChanges();
|
||||
TestBed.tick();
|
||||
fixture.detectChanges();
|
||||
expect(zoomVar()).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
it('has no brand mark duplicating the first workspace link', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
|
||||
+34
-1
@@ -1,4 +1,12 @@
|
||||
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
effect,
|
||||
inject,
|
||||
input,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { RouterLink } from '@angular/router';
|
||||
@@ -32,4 +40,29 @@ export class WorkspaceShellRailComponent {
|
||||
>(null);
|
||||
readonly railProviderClass = input('rail-context-region');
|
||||
readonly isSettingsRoute = input(false);
|
||||
|
||||
/**
|
||||
* The page zoom factor. App zoom (`webFrame.setZoomLevel`) scales CSS
|
||||
* pixels but not the native traffic lights, so the macOS top inset is
|
||||
* kept in window pixels when zoomed out. Chromium reports the window in
|
||||
* window pixels and the viewport in CSS pixels, and fires `resize` when
|
||||
* the zoom changes.
|
||||
*/
|
||||
protected readonly zoomFactor = signal(1);
|
||||
|
||||
constructor() {
|
||||
const view = inject(DOCUMENT).defaultView;
|
||||
effect((onCleanup) => {
|
||||
if (!this.isMacOS() || !view) return;
|
||||
const update = () =>
|
||||
this.zoomFactor.set(
|
||||
view.innerWidth > 0 && view.outerWidth > 0
|
||||
? view.outerWidth / view.innerWidth
|
||||
: 1
|
||||
);
|
||||
update();
|
||||
view.addEventListener('resize', update);
|
||||
onCleanup(() => view.removeEventListener('resize', update));
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user