mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 13:46:16 -08:00
Merge pull request #1084 from 4gray/agent/sources-header-style
Align sources header with panel style
This commit is contained in:
3 files changed
+191
-22
No files matched your search
+6
-3
@@ -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)"
|
||||
|
||||
+28
-19
@@ -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;
|
||||
}
|
||||
}
|
||||
+157
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user