mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
fix(workspace): keep the macOS header clear of the lights when zoomed out (#1815)
* fix(workspace): keep the macOS header clear of the lights when zoomed out App zoom scales CSS pixels but not the native traffic lights. At zoom -3/-4 the header column starts near 29 window pixels, so Back and the playlist switcher slid under the lights, and the 27px header band let the lights overlap the context panel below. A shell-level TrafficLightsClearanceDirective now publishes the lights' clearance in CSS pixels (84 x 48 window pixels, from the page zoom factor) on macOS. The header band grows to the vertical clearance (the rail starts its first link at the same band, replacing the rail's own zoom listener), and the header's leading padding grows to the horizontal clearance less the rail column. Both equal the default layout at 100 %; Windows/Linux and the phone layout are unchanged. The native position is shared with the main process as MACOS_TRAFFIC_LIGHTS_POSITION. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(workspace): pass the header clearance poll labels as options Equivalent to the string form, which Playwright 1.62 also accepts, but explicit in every version (Greptile review). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(workspace): check the header switcher before history exists Since the header's history fallback, a list reached by navigation leads with Back. The macOS check now takes the switcher on the first page, which has nothing to go back to, and Back on a detail page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
17 files changed
+519
-131
No files matched your search
+9
-1
@@ -7,8 +7,12 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
height: 56px;
|
||||
// The shell's header band: 56px, taller on macOS when zoomed out.
|
||||
height: 100%;
|
||||
padding: 0 24px;
|
||||
// macOS: app zoom does not scale the traffic lights, so zoomed out the
|
||||
// leading controls keep their 100 % distance from them (see the shell).
|
||||
padding-inline-start: max(24px, var(--workspace-header-lights-inset, 0px));
|
||||
border-bottom: 1px solid var(--mat-sys-outline-variant);
|
||||
background: var(--app-header-bg, var(--mat-sys-surface-container-low));
|
||||
app-region: drag;
|
||||
@@ -318,6 +322,10 @@
|
||||
@media (max-width: 780px) {
|
||||
.workspace-header {
|
||||
padding-inline: 10px;
|
||||
padding-inline-start: max(
|
||||
10px,
|
||||
var(--workspace-header-lights-inset, 0px)
|
||||
);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
|
||||
+1
-5
@@ -1,10 +1,6 @@
|
||||
<!-- 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()"
|
||||
[style.--rail-zoom-factor]="isMacOS() ? zoomFactor() : null"
|
||||
>
|
||||
<aside class="app-rail" [class.is-macos]="isMacOS()">
|
||||
<app-workspace-shell-rail-links [links]="workspaceLinks()" />
|
||||
|
||||
@if (primaryContextLinks().length > 0) {
|
||||
|
||||
+4
-5
@@ -29,13 +29,12 @@
|
||||
--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
|
||||
// The traffic lights sit in the header band (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.
|
||||
// Zoomed out, the band grows to keep the lights inside it.
|
||||
&.is-macos {
|
||||
padding-top: max(56px, calc(48px / var(--rail-zoom-factor, 1)));
|
||||
padding-top: var(--workspace-header-band, 56px);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
|
||||
-42
@@ -84,48 +84,6 @@ 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();
|
||||
|
||||
|
||||
+1
-34
@@ -1,12 +1,4 @@
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
effect,
|
||||
inject,
|
||||
input,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { RouterLink } from '@angular/router';
|
||||
@@ -40,29 +32,4 @@ 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));
|
||||
});
|
||||
}
|
||||
}
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import {
|
||||
ZOOM_LEVEL_MAX,
|
||||
ZOOM_LEVEL_MIN,
|
||||
ZOOM_LEVEL_STEP,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import {
|
||||
pageZoomFactor,
|
||||
TrafficLightsClearanceDirective,
|
||||
trafficLightsClearance,
|
||||
} from './traffic-lights-clearance.directive';
|
||||
|
||||
@Component({
|
||||
template: `<div [appTrafficLightsClearance]="enabled()"></div>`,
|
||||
imports: [TrafficLightsClearanceDirective],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
class HostComponent {
|
||||
readonly enabled = signal(true);
|
||||
}
|
||||
|
||||
function setWindowWidths(outer: number, inner: number): void {
|
||||
Object.defineProperty(window, 'outerWidth', {
|
||||
configurable: true,
|
||||
value: outer,
|
||||
});
|
||||
Object.defineProperty(window, 'innerWidth', {
|
||||
configurable: true,
|
||||
value: inner,
|
||||
});
|
||||
}
|
||||
|
||||
describe('trafficLightsClearance', () => {
|
||||
it('matches the default layout at 100 %', () => {
|
||||
// x: the rail (60px) plus the header's padding (24px), where the
|
||||
// header content already starts; y: inside the 56px header band.
|
||||
expect(trafficLightsClearance(1)).toEqual({ x: 84, y: 48 });
|
||||
});
|
||||
|
||||
it('keeps the same window pixels at every supported zoom', () => {
|
||||
for (
|
||||
let level = ZOOM_LEVEL_MIN;
|
||||
level <= ZOOM_LEVEL_MAX;
|
||||
level += ZOOM_LEVEL_STEP
|
||||
) {
|
||||
const factor = 1.2 ** level;
|
||||
const clearance = trafficLightsClearance(factor);
|
||||
expect(clearance.x * factor).toBeCloseTo(84);
|
||||
expect(clearance.y * factor).toBeCloseTo(48);
|
||||
}
|
||||
});
|
||||
|
||||
it('outgrows the default inset and band when zoomed out', () => {
|
||||
const clearance = trafficLightsClearance(1.2 ** ZOOM_LEVEL_MIN);
|
||||
// Past the 60px rail: wider than the header's 24px padding.
|
||||
expect(clearance.x - 60).toBeGreaterThan(24);
|
||||
expect(clearance.y).toBeGreaterThan(56);
|
||||
});
|
||||
|
||||
it('treats an unusable factor as 100 %', () => {
|
||||
for (const factor of [0, -1, Number.NaN, Number.POSITIVE_INFINITY]) {
|
||||
expect(trafficLightsClearance(factor)).toEqual({ x: 84, y: 48 });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('pageZoomFactor', () => {
|
||||
it('divides window pixels by CSS pixels', () => {
|
||||
expect(pageZoomFactor({ outerWidth: 1200, innerWidth: 2400 })).toBe(
|
||||
0.5
|
||||
);
|
||||
expect(pageZoomFactor({ outerWidth: 1200, innerWidth: 1000 })).toBe(
|
||||
1.2
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to 100 % without a measured window', () => {
|
||||
expect(pageZoomFactor({ outerWidth: 0, innerWidth: 1200 })).toBe(1);
|
||||
expect(pageZoomFactor({ outerWidth: 1200, innerWidth: 0 })).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('TrafficLightsClearanceDirective', () => {
|
||||
let fixture: ComponentFixture<HostComponent>;
|
||||
|
||||
const clearance = () => {
|
||||
const style = (
|
||||
fixture.nativeElement.querySelector('div') as HTMLElement
|
||||
).style;
|
||||
return {
|
||||
x: style.getPropertyValue('--traffic-lights-clear-x'),
|
||||
y: style.getPropertyValue('--traffic-lights-clear-y'),
|
||||
};
|
||||
};
|
||||
const render = () => {
|
||||
fixture.detectChanges();
|
||||
TestBed.tick();
|
||||
fixture.detectChanges();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
setWindowWidths(1200, 1200);
|
||||
fixture = TestBed.createComponent(HostComponent);
|
||||
});
|
||||
|
||||
afterEach(() => setWindowWidths(1024, 1024));
|
||||
|
||||
it('publishes the clearance and follows zoom changes', () => {
|
||||
render();
|
||||
expect(clearance()).toEqual({ x: '84px', y: '48px' });
|
||||
|
||||
// Zoomed out to 50%: the viewport holds twice the CSS pixels.
|
||||
setWindowWidths(1200, 2400);
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
fixture.detectChanges();
|
||||
expect(clearance()).toEqual({ x: '168px', y: '96px' });
|
||||
});
|
||||
|
||||
it('measures the zoom before the first render', () => {
|
||||
setWindowWidths(1200, 2400);
|
||||
fixture.detectChanges();
|
||||
expect(clearance()).toEqual({ x: '168px', y: '96px' });
|
||||
});
|
||||
|
||||
it('publishes nothing off macOS and stops listening when disabled', () => {
|
||||
render();
|
||||
const removeListener = jest.spyOn(window, 'removeEventListener');
|
||||
|
||||
fixture.componentInstance.enabled.set(false);
|
||||
render();
|
||||
expect(clearance()).toEqual({ x: '', y: '' });
|
||||
expect(removeListener).toHaveBeenCalledWith(
|
||||
'resize',
|
||||
expect.any(Function)
|
||||
);
|
||||
|
||||
setWindowWidths(1200, 2400);
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
fixture.componentInstance.enabled.set(true);
|
||||
render();
|
||||
// Re-enabled, it measures the current zoom.
|
||||
expect(clearance()).toEqual({ x: '168px', y: '96px' });
|
||||
removeListener.mockRestore();
|
||||
});
|
||||
|
||||
it('stops listening when destroyed', () => {
|
||||
render();
|
||||
const removeListener = jest.spyOn(window, 'removeEventListener');
|
||||
fixture.destroy();
|
||||
expect(removeListener).toHaveBeenCalledWith(
|
||||
'resize',
|
||||
expect.any(Function)
|
||||
);
|
||||
removeListener.mockRestore();
|
||||
});
|
||||
});
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import {
|
||||
computed,
|
||||
Directive,
|
||||
effect,
|
||||
inject,
|
||||
input,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { MACOS_TRAFFIC_LIGHTS_POSITION } from '@iptvnator/shared/interfaces';
|
||||
|
||||
/**
|
||||
* The three buttons are 14 pt circles. macOS 26 spaces them 23 pt apart
|
||||
* (60 pt in all); earlier releases 20 pt (52 pt).
|
||||
*/
|
||||
const LIGHTS_WIDTH = 60;
|
||||
const LIGHTS_HEIGHT = 14;
|
||||
/**
|
||||
* Window points from the window's left edge to where header content may
|
||||
* start: the lights plus a gap. 84 is also where the header's content starts
|
||||
* at 100 % (the 60 px rail plus 24 px padding), so default zoom is unchanged.
|
||||
*/
|
||||
const CLEAR_X = MACOS_TRAFFIC_LIGHTS_POSITION.x + LIGHTS_WIDTH + 8;
|
||||
/** Window points from the top to where the content area may start. */
|
||||
const CLEAR_Y = MACOS_TRAFFIC_LIGHTS_POSITION.y + LIGHTS_HEIGHT + 14;
|
||||
|
||||
/** CSS pixels from the window's top-left corner that clear the lights. */
|
||||
export interface TrafficLightsClearance {
|
||||
readonly x: number;
|
||||
readonly y: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The page zoom factor. Chromium reports the window in window pixels and the
|
||||
* viewport in CSS pixels, so their ratio is the app zoom.
|
||||
*/
|
||||
export function pageZoomFactor(
|
||||
view: Pick<Window, 'innerWidth' | 'outerWidth'>
|
||||
): number {
|
||||
return view.innerWidth > 0 && view.outerWidth > 0
|
||||
? view.outerWidth / view.innerWidth
|
||||
: 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* The clearance at `zoomFactor`. App zoom (`webFrame.setZoomLevel`) scales
|
||||
* CSS pixels but not the native buttons: zoomed out, the same window points
|
||||
* hold more CSS pixels.
|
||||
*/
|
||||
export function trafficLightsClearance(
|
||||
zoomFactor: number
|
||||
): TrafficLightsClearance {
|
||||
const factor =
|
||||
Number.isFinite(zoomFactor) && zoomFactor > 0 ? zoomFactor : 1;
|
||||
return { x: CLEAR_X / factor, y: CLEAR_Y / factor };
|
||||
}
|
||||
|
||||
/**
|
||||
* Publishes the macOS traffic-light clearance on its host as
|
||||
* `--traffic-lights-clear-x` and `--traffic-lights-clear-y`, refreshed on
|
||||
* `resize` (Chromium fires it when the zoom changes). The shell grid, the
|
||||
* rail and the header read them; without them (off macOS) they fall back to
|
||||
* their default layout.
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[appTrafficLightsClearance]',
|
||||
host: {
|
||||
'[style.--traffic-lights-clear-x]': 'clearX()',
|
||||
'[style.--traffic-lights-clear-y]': 'clearY()',
|
||||
},
|
||||
})
|
||||
export class TrafficLightsClearanceDirective {
|
||||
/** True where the native traffic lights overlay the window (macOS). */
|
||||
readonly enabled = input(false, { alias: 'appTrafficLightsClearance' });
|
||||
|
||||
private readonly view = inject(DOCUMENT).defaultView;
|
||||
private readonly zoomFactor = signal(
|
||||
this.view ? pageZoomFactor(this.view) : 1
|
||||
);
|
||||
private readonly clearance = computed(() =>
|
||||
this.enabled() ? trafficLightsClearance(this.zoomFactor()) : null
|
||||
);
|
||||
protected readonly clearX = computed(() => toPx(this.clearance()?.x));
|
||||
protected readonly clearY = computed(() => toPx(this.clearance()?.y));
|
||||
|
||||
constructor() {
|
||||
effect((onCleanup) => {
|
||||
const view = this.view;
|
||||
if (!this.enabled() || !view) return;
|
||||
const update = () => this.zoomFactor.set(pageZoomFactor(view));
|
||||
update();
|
||||
view.addEventListener('resize', update);
|
||||
onCleanup(() => view.removeEventListener('resize', update));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function toPx(value: number | undefined): string | null {
|
||||
return value === undefined ? null : `${value}px`;
|
||||
}
|
||||
@@ -1,4 +1,9 @@
|
||||
<div class="workspace-shell" appPlaylistDropZone #dropZone="playlistDropZone">
|
||||
<div
|
||||
class="workspace-shell"
|
||||
appPlaylistDropZone
|
||||
#dropZone="playlistDropZone"
|
||||
[appTrafficLightsClearance]="facade.isMacOS"
|
||||
>
|
||||
<app-playlist-drop-overlay [state]="dropZone.overlayState()" />
|
||||
@if (facade.showXtreamImportOverlay()) {
|
||||
<app-workspace-shell-import-overlay />
|
||||
|
||||
+21
-3
@@ -6,9 +6,14 @@
|
||||
}
|
||||
|
||||
.workspace-shell {
|
||||
// The header band. On macOS it also holds the traffic lights, which app
|
||||
// zoom does not scale: zoomed out, it stays tall enough to contain them
|
||||
// (`TrafficLightsClearanceDirective` publishes the clearance), so no
|
||||
// content starts beside them. The macOS rail starts its first link here.
|
||||
--workspace-header-band: max(56px, var(--traffic-lights-clear-y, 0px));
|
||||
display: grid;
|
||||
grid-template-columns: 60px 1fr;
|
||||
grid-template-rows: 56px minmax(0, 1fr) auto;
|
||||
grid-template-rows: var(--workspace-header-band) minmax(0, 1fr) auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
@@ -27,6 +32,12 @@ app-workspace-shell-header {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
min-width: 0;
|
||||
// The leading inset that keeps the header's controls clear of the macOS
|
||||
// traffic lights: their clearance minus the rail column. It equals the
|
||||
// header's 24px padding at 100 % zoom and is negative off macOS.
|
||||
--workspace-header-lights-inset: calc(
|
||||
var(--traffic-lights-clear-x, 0px) - 60px
|
||||
);
|
||||
}
|
||||
|
||||
.workspace-body {
|
||||
@@ -72,11 +83,16 @@ app-workspace-shell-context-sidebar {
|
||||
box-shadow: -4px 8px 20px var(--app-content-shadow, rgba(0, 0, 0, 0.18));
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 780px) {
|
||||
.workspace-shell {
|
||||
grid-template-columns: 56px 1fr;
|
||||
grid-template-rows: 56px 1fr auto;
|
||||
grid-template-rows: var(--workspace-header-band) 1fr auto;
|
||||
}
|
||||
|
||||
app-workspace-shell-header {
|
||||
--workspace-header-lights-inset: calc(
|
||||
var(--traffic-lights-clear-x, 0px) - 56px
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,9 +107,11 @@ app-workspace-shell-context-sidebar {
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
// Below the rail's row, the header never meets the traffic lights.
|
||||
app-workspace-shell-header {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
--workspace-header-lights-inset: 0px;
|
||||
}
|
||||
|
||||
.workspace-body {
|
||||
|
||||
+14
@@ -18,6 +18,7 @@ import {
|
||||
WorkspacePortalContext,
|
||||
WorkspaceShellContextPanel,
|
||||
} from '@iptvnator/workspace/shell/util';
|
||||
import { TrafficLightsClearanceDirective } from './traffic-lights-clearance.directive';
|
||||
import { WorkspaceShellComponent } from './workspace-shell.component';
|
||||
import {
|
||||
WorkspaceHeaderBulkAction,
|
||||
@@ -270,6 +271,7 @@ describe('WorkspaceShellComponent', () => {
|
||||
MockWorkspaceShellHeaderComponent,
|
||||
MockWorkspaceShellImportOverlayComponent,
|
||||
MockWorkspaceShellRailComponent,
|
||||
TrafficLightsClearanceDirective,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
@@ -307,6 +309,14 @@ describe('WorkspaceShellComponent', () => {
|
||||
By.directive(MockWorkspaceShellHeaderComponent)
|
||||
).componentInstance as MockWorkspaceShellHeaderComponent;
|
||||
expect(header.activeDownloadsCount()).toBe(3);
|
||||
// macOS: the rail and header read the traffic-light clearance.
|
||||
expect(
|
||||
(
|
||||
fixture.nativeElement.querySelector(
|
||||
'.workspace-shell'
|
||||
) as HTMLElement
|
||||
).style.getPropertyValue('--traffic-lights-clear-x')
|
||||
).toMatch(/px$/);
|
||||
expect(
|
||||
fixture.nativeElement.querySelector(
|
||||
'app-workspace-shell-context-sidebar'
|
||||
@@ -357,6 +367,7 @@ describe('WorkspaceShellComponent', () => {
|
||||
MockWorkspaceShellHeaderComponent,
|
||||
MockWorkspaceShellImportOverlayComponent,
|
||||
MockWorkspaceShellRailComponent,
|
||||
TrafficLightsClearanceDirective,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
@@ -434,6 +445,7 @@ describe('WorkspaceShellComponent', () => {
|
||||
MockWorkspaceShellHeaderComponent,
|
||||
MockWorkspaceShellImportOverlayComponent,
|
||||
MockWorkspaceShellRailComponent,
|
||||
TrafficLightsClearanceDirective,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
@@ -507,6 +519,7 @@ describe('WorkspaceShellComponent', () => {
|
||||
MockWorkspaceShellHeaderComponent,
|
||||
MockWorkspaceShellImportOverlayComponent,
|
||||
MockWorkspaceShellRailComponent,
|
||||
TrafficLightsClearanceDirective,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
@@ -606,6 +619,7 @@ describe('WorkspaceShellComponent', () => {
|
||||
MockWorkspaceShellHeaderComponent,
|
||||
MockWorkspaceShellImportOverlayComponent,
|
||||
MockWorkspaceShellRailComponent,
|
||||
TrafficLightsClearanceDirective,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
|
||||
@@ -21,6 +21,7 @@ import { WorkspaceShellContextSidebarComponent } from './components/workspace-sh
|
||||
import { WorkspaceShellHeaderComponent } from './components/workspace-shell-header/workspace-shell-header.component';
|
||||
import { WorkspaceShellImportOverlayComponent } from './components/workspace-shell-import-overlay/workspace-shell-import-overlay.component';
|
||||
import { WorkspaceShellRailComponent } from './components/workspace-shell-rail/workspace-shell-rail.component';
|
||||
import { TrafficLightsClearanceDirective } from './traffic-lights-clearance.directive';
|
||||
import { WorkspaceShellFacade } from './services/workspace-shell.facade';
|
||||
import { WorkspaceShellXtreamImportService } from './services/workspace-shell-xtream-import.service';
|
||||
import { WorkspaceShellCommandPaletteService } from './services/workspace-shell-command-palette.service';
|
||||
@@ -42,6 +43,7 @@ import { LIVE_CATEGORIES_POPOVER } from '@iptvnator/portal/shared/util';
|
||||
PlaylistDropOverlayComponent,
|
||||
PlaylistDropZoneDirective,
|
||||
RouterOutlet,
|
||||
TrafficLightsClearanceDirective,
|
||||
WorkspaceShellContextSidebarComponent,
|
||||
WorkspaceShellHeaderComponent,
|
||||
WorkspaceShellImportOverlayComponent,
|
||||
|
||||
Reference in new issue
Block a user