Merge pull request #1084 from 4gray/agent/sources-header-style

Align sources header with panel style
This commit is contained in:
4gray authored and GitHub committed 2026-06-20 13:50:22 +02:00
commit 2f27895e6b
3 files changed
+191 -22

No files matched your search

@@ -4,9 +4,9 @@
>
@if (visibleSourcesCount() > 0) {
<header class="sources-header">
<div class="sources-header__text">
<div class="sources-header__meta">
<h2 class="sources-title">{{ title() }}</h2>
<p class="sources-subtitle">{{ subtitle() }}</p>
<span class="sources-subtitle">{{ subtitle() }}</span>
</div>
<button
type="button"
@@ -36,7 +36,10 @@
</mat-menu>
</header>
}
<div class="sources-content">
<div
class="sources-content"
[class.app-scrollbar]="visibleSourcesCount() > 0"
>
<app-recent-playlists
[searchQueryInput]="searchQuery()"
(addPlaylistClicked)="onAddPlaylist($event)"
@@ -1,3 +1,5 @@
@use '../../../../../../ui/styles/panel-header' as panel;
:host {
display: block;
height: 100%;
@@ -14,32 +16,19 @@
}
.sources-header {
position: sticky;
top: 0;
z-index: 2;
padding: 20px 20px 8px;
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
@include panel.standard-panel-header($sticky: true);
}
.sources-header__text {
min-width: 0;
flex: 1;
.sources-header__meta {
@include panel.standard-panel-meta();
}
.sources-title {
margin: 0;
font-size: 1.2rem;
font-weight: 650;
letter-spacing: -0.02em;
@include panel.standard-panel-title();
}
.sources-subtitle {
margin: 4px 0 0;
font-size: 0.875rem;
color: var(--mat-sys-on-surface-variant, rgba(255, 255, 255, 0.6));
@include panel.standard-panel-subtitle();
}
.sort-trigger {
@@ -47,6 +36,7 @@
display: inline-flex;
align-items: center;
gap: 4px;
app-region: no-drag;
.sort-trigger__label {
font-size: 0.85rem;
@@ -65,7 +55,7 @@
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 0 20px 20px;
padding: 16px 20px 20px;
}
.sources-content app-recent-playlists {
@@ -87,3 +77,22 @@
display: flex;
}
}
@media (max-width: 768px) {
.sources-header {
flex-direction: column;
align-items: flex-start;
padding: 10px 16px;
min-height: unset;
}
.sources-header__meta {
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.sources-content {
padding: 12px 16px 16px;
}
}
@@ -0,0 +1,157 @@
import { Component, input, output } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { ActivatedRoute, convertToParamMap } from '@angular/router';
import { provideMockStore } from '@ngrx/store/testing';
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
import { of } from 'rxjs';
import {
selectActiveTypeFilters,
selectAllPlaylistsMeta,
} from '@iptvnator/m3u-state';
import { SortBy, SortOrder, SortService } from '@iptvnator/services';
import { WORKSPACE_SHELL_ACTIONS } from '@iptvnator/workspace/shell/util';
import { WorkspaceSourcesComponent } from './workspace-sources.component';
@Component({
selector: 'app-recent-playlists',
template: '',
standalone: true,
})
class MockRecentPlaylistsComponent {
readonly searchQueryInput = input('');
readonly addPlaylistClicked = output<void>();
}
describe('WorkspaceSourcesComponent', () => {
let fixture: ComponentFixture<WorkspaceSourcesComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [WorkspaceSourcesComponent, NoopAnimationsModule],
providers: [
provideMockStore({
selectors: [
{
selector: selectActiveTypeFilters,
value: ['m3u', 'xtream', 'stalker'],
},
{
selector: selectAllPlaylistsMeta,
value: [
{
_id: 'playlist-1',
title: 'Playlist 1',
},
],
},
],
}),
{
provide: ActivatedRoute,
useValue: {
queryParamMap: of(convertToParamMap({})),
},
},
{
provide: SortService,
useValue: {
getSortOptions: () =>
of({
by: SortBy.DATE_ADDED,
order: SortOrder.DESC,
}),
setSortOptions: jest.fn(),
},
},
{
provide: WORKSPACE_SHELL_ACTIONS,
useValue: {
openAddPlaylistDialog: jest.fn(),
},
},
{
provide: TranslateService,
useValue: {
instant: (
key: string,
params?: Record<string, string | number>
) => {
if (key === 'WORKSPACE.SOURCES.ALL_PLAYLISTS') {
return 'All Playlists';
}
if (
key === 'WORKSPACE.SOURCES.PLAYLIST_COUNT_ONE'
) {
return '1 playlist';
}
if (
key === 'WORKSPACE.SOURCES.PLAYLIST_COUNT_OTHER'
) {
return `${params?.['count']} playlists`;
}
if (key === 'HOME.SORT_OPTIONS.NEWEST') {
return 'Date added (Newest first)';
}
return key;
},
get: (key: string) => of(key),
stream: (key: string) => of(key),
onLangChange: of(null),
onTranslationChange: of(null),
onDefaultLangChange: of(null),
currentLang: 'en',
defaultLang: 'en',
},
},
],
})
.overrideComponent(WorkspaceSourcesComponent, {
set: {
imports: [
MatButtonModule,
MatIconModule,
MatMenuModule,
MockRecentPlaylistsComponent,
TranslatePipe,
],
},
})
.compileComponents();
fixture = TestBed.createComponent(WorkspaceSourcesComponent);
});
it('renders the shared panel header structure without paragraph subtitle margins', async () => {
fixture.detectChanges();
await fixture.whenStable();
const header: HTMLElement =
fixture.nativeElement.querySelector('.sources-header');
const meta: HTMLElement =
fixture.nativeElement.querySelector('.sources-header__meta');
const title: HTMLElement =
fixture.nativeElement.querySelector('.sources-title');
const subtitle: HTMLElement =
fixture.nativeElement.querySelector('.sources-subtitle');
expect(header).not.toBeNull();
expect(meta).not.toBeNull();
expect(title.textContent?.trim()).toBe('All Playlists');
expect(subtitle.textContent?.trim()).toBe('1 playlist');
expect(subtitle.tagName).toBe('SPAN');
});
it('uses the shared always-visible scrollbar utility for the playlist list', async () => {
fixture.detectChanges();
await fixture.whenStable();
const content: HTMLElement =
fixture.nativeElement.querySelector('.sources-content');
expect(content.classList.contains('app-scrollbar')).toBe(true);
});
});