mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 09:01:03 -08:00
fix(workspace): start the macOS rail below the traffic lights (#1806)
This commit is contained in:
1 parent
7fd3d1dab0
commit
6f247fb538
7 files changed
+168
-4
No files matched your search
@@ -0,0 +1,6 @@
|
||||
---
|
||||
type: fix
|
||||
area: workspace
|
||||
---
|
||||
|
||||
On macOS the side rail now starts level with the page content and the dashboard banner, so its first button no longer crowds the window's close, minimize and zoom buttons.
|
||||
@@ -187,3 +187,69 @@ test.describe('Custom window controls', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('macOS traffic lights', () => {
|
||||
test.skip(
|
||||
process.platform !== 'darwin',
|
||||
'Only macOS draws the native traffic lights over the rail'
|
||||
);
|
||||
|
||||
test('@electron the first rail link starts with the content area, clear of the lights', async ({
|
||||
dataDir,
|
||||
}) => {
|
||||
const app = await launchElectronApp(dataDir);
|
||||
|
||||
try {
|
||||
const page = app.mainWindow;
|
||||
await expect(page.locator('.app-rail')).toHaveClass(/is-macos/);
|
||||
const firstLink = page.locator('.app-rail a').first();
|
||||
await expect(firstLink).toBeVisible();
|
||||
|
||||
const [linkBox, contentBox] = await Promise.all([
|
||||
firstLink.boundingBox(),
|
||||
page.locator('.workspace-content').boundingBox(),
|
||||
]);
|
||||
// Aligned with the content area, where the dashboard hero starts.
|
||||
expect(
|
||||
Math.abs((linkBox?.y ?? 0) - (contentBox?.y ?? -100))
|
||||
).toBeLessThanOrEqual(1);
|
||||
|
||||
const lights = await app.electronApp.evaluate(({ BrowserWindow }) =>
|
||||
BrowserWindow.getAllWindows()[0]?.getWindowButtonPosition()
|
||||
);
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -326,7 +326,15 @@ The Electron window hides the native title bar on all desktop platforms
|
||||
(`titleBarStyle: 'hidden'` in `apps/electron-backend/src/app/app.ts`):
|
||||
|
||||
1. macOS keeps the native traffic lights (`titleBarOverlay: true`,
|
||||
`trafficLightPosition`); the renderer draws no window buttons.
|
||||
`trafficLightPosition`); the renderer draws no window buttons. The lights
|
||||
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. 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) {
|
||||
|
||||
+6
-1
@@ -29,8 +29,13 @@
|
||||
--rail-link-active-border: var(--app-selection-border);
|
||||
--rail-link-active-glow: var(--app-selection-glow);
|
||||
|
||||
// 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: 36px;
|
||||
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