diff --git a/docs/superpowers/plans/2026-07-31-live-panel-toggle-consistency.md b/docs/superpowers/plans/2026-07-31-live-panel-toggle-consistency.md index 0e76c4839..13b44c5e7 100644 --- a/docs/superpowers/plans/2026-07-31-live-panel-toggle-consistency.md +++ b/docs/superpowers/plans/2026-07-31-live-panel-toggle-consistency.md @@ -59,6 +59,7 @@ SCSS, ngx-translate. ### Task 1: Introduce independent panel intent and legacy migration **Files:** + - Create: `libs/portal/shared/data-access/src/lib/live-layout-panel-state.service.ts` - Create: @@ -179,12 +180,13 @@ git commit -m "feat(ui): add independent live panel state" ### Task 2: Make Guide disclosure capability explicit **Files:** + - Modify: `libs/ui/epg/src/lib/epg-timeline/epg-timeline.component.{ts,html,scss,spec.ts}` - Modify: `libs/ui/epg/src/lib/epg-list-view/epg-list-view.component.{ts,html,scss,spec.ts}` -- [ ] **Step 1: Write failing component tests** +- [x] **Step 1: Write failing component tests** For both timeline and list, assert that `collapsible=false` renders a static heading with no disclosure button, while `collapsible=true` exposes a 40px @@ -195,8 +197,9 @@ label: fixture.componentRef.setInput('collapsible', false); fixture.componentRef.setInput('panelId', 'live-guide-panel'); await fixture.whenStable(); -expect(fixture.nativeElement.querySelector('[data-testid="live-guide-toggle"]')) - .toBeNull(); +expect( + fixture.nativeElement.querySelector('[data-testid="live-guide-toggle"]') +).toBeNull(); fixture.componentRef.setInput('collapsible', true); await fixture.whenStable(); @@ -207,7 +210,7 @@ expect(toggle.getAttribute('aria-controls')).toBe('live-guide-panel'); expect(toggle.getAttribute('aria-expanded')).toBe('true'); ``` -- [ ] **Step 2: Run both targets and verify RED** +- [x] **Step 2: Run both targets and verify RED** ```bash pnpm nx test ui-epg --runInBand @@ -215,7 +218,7 @@ pnpm nx test ui-epg --runInBand Expected: FAIL because the new inputs and static-heading branch do not exist. -- [ ] **Step 3: Implement capability, ARIA, and target sizing** +- [x] **Step 3: Implement capability, ARIA, and target sizing** Add: @@ -229,7 +232,7 @@ content in a non-interactive container. Bind `aria-controls` to `panelId`, bind the schedule region `id`, add `data-testid="live-guide-toggle"`, and set `min-height: 40px`. -- [ ] **Step 4: Run and verify GREEN** +- [x] **Step 4: Run and verify GREEN** ```bash pnpm nx test ui-epg --runInBand @@ -238,7 +241,7 @@ pnpm nx lint ui-epg Expected: both commands exit 0. -- [ ] **Step 5: Commit** +- [x] **Step 5: Commit** ```bash git add libs/ui/epg @@ -248,6 +251,7 @@ git commit -m "fix(ui): expose guide collapse capability" ### Task 3: Define shared M3U panel-control contracts **Files:** + - Modify: `libs/ui/components/src/lib/channel-list-container/all-channels-view/all-channels-view.component.{ts,html,scss,spec.ts}` - Modify: @@ -266,9 +270,7 @@ and bidirectional focus: ```ts expect(groupsHide.getAttribute('aria-controls')).toBe('live-groups-panel'); -expect(channelsHide.getAttribute('aria-controls')).toBe( - 'live-channels-panel' -); +expect(channelsHide.getAttribute('aria-controls')).toBe('live-channels-panel'); groupsHide.click(); expect(groupsPanelExpandedChange).toHaveBeenCalledWith(false); fixture.componentRef.setInput('groupsPanelExpanded', false); @@ -343,6 +345,7 @@ git commit -m "feat(ui): add independent M3U panel controls" ### Task 4: Give the workspace Groups panel explicit ownership **Files:** + - Modify: `libs/workspace/shell/feature/src/lib/workspace-context-panel/workspace-context-panel.component.{ts,html,scss,spec.ts}` - Modify: @@ -407,6 +410,7 @@ git commit -m "feat(ui): add workspace groups disclosure" ### Task 5: Integrate independent Channels state in Xtream and Stalker **Files:** + - Modify: `libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.{ts,html,scss,spec.ts}` - Modify: @@ -443,9 +447,7 @@ Inject `LiveLayoutPanelStateService`, compute Channels effective visibility for ```ts this.livePanelState.toggleMasterSuppression( - this.showLiveChannelSidebar() - ? ['groups', 'channels'] - : ['groups'] + this.showLiveChannelSidebar() ? ['groups', 'channels'] : ['groups'] ); ``` @@ -485,6 +487,7 @@ git commit -m "feat(ui): separate portal live panel toggles" ### Task 6: Integrate shared state and mobile restoration in M3U **Files:** + - Modify: `libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.{ts,html,scss,spec.ts}` @@ -552,6 +555,7 @@ git commit -m "feat(ui): separate M3U live panel toggles" ### Task 7: Preserve the single collection-header Channels control **Files:** + - Modify: `libs/portal/shared/ui/src/lib/components/unified-collection/unified-collection-page.component.{ts,html,scss,spec.ts}` - Modify: @@ -607,6 +611,7 @@ git commit -m "feat(ui): align collection live panel control" ### Task 8: Translate panel actions **Files:** + - Modify: `apps/web/src/assets/i18n/*.json` - [ ] **Step 1: Add the English source keys** @@ -648,6 +653,7 @@ git commit -m "feat(i18n): translate live panel controls" ### Task 9: Add atomized Electron regression coverage **Files:** + - Create: `apps/electron-backend-e2e/src/live-panel-toggles.e2e.ts` - Reuse: `apps/electron-backend-e2e/src/electron-test-fixtures.ts` - Reuse: `apps/electron-backend-e2e/src/portal-mock-fixtures.ts` @@ -695,6 +701,7 @@ git commit -m "test(e2e): cover live panel toggles" ### Task 10: Update canonical docs and release note **Files:** + - Modify: `docs/architecture/iptvnator-ui-guidelines.md` - Modify: `CLAUDE.md` - Create: `.changes/ui-live-panel-toggles.md` @@ -745,6 +752,7 @@ git commit -m "docs(ui): document live panel ownership" ### Task 11: Full verification and strict review **Files:** + - Review every file changed relative to `origin/master`. - [ ] **Step 1: Run affected unit targets** diff --git a/libs/ui/epg/src/lib/epg-list-view/epg-list-view.component.html b/libs/ui/epg/src/lib/epg-list-view/epg-list-view.component.html index b8559e77a..28ed1a15e 100644 --- a/libs/ui/epg/src/lib/epg-list-view/epg-list-view.component.html +++ b/libs/ui/epg/src/lib/epg-list-view/epg-list-view.component.html @@ -7,22 +7,31 @@ >
- + } @else { + {{ channelName() }} {{ sourceLabel() }} - + } @@ -112,7 +121,8 @@ } {{ - viewDate() | date: 'EEE, d MMM' : '' : currentLocale() + viewDate() + | date: 'EEE, d MMM' : '' : currentLocale() }}
- } -
- @for (row of rows(); track row.key) { - + + @switch (state) { + @case ('loading') { +
+ @for (row of skeletonRows; track $index) { +
+ + +
}
-
- } - @default { - + } + @case ('list') { +
+ + @if (nowStripVisible() && nowRow(); as now) { + + } +
+ @for (row of rows(); track row.key) { + + } +
+
+ } + @default { + + } } } - } + diff --git a/libs/ui/epg/src/lib/epg-list-view/epg-list-view.component.scss b/libs/ui/epg/src/lib/epg-list-view/epg-list-view.component.scss index ecc2cf272..69b67b4a2 100644 --- a/libs/ui/epg/src/lib/epg-list-view/epg-list-view.component.scss +++ b/libs/ui/epg/src/lib/epg-list-view/epg-list-view.component.scss @@ -53,6 +53,7 @@ $font-mono: var(--font-mono, ui-monospace, 'SF Mono', Menlo, monospace); padding: 4px 6px; border-radius: 7px; min-width: 0; + min-height: 40px; } .g-collapse:hover { background: $surface-2; @@ -81,6 +82,17 @@ $font-mono: var(--font-mono, ui-monospace, 'SF Mono', Menlo, monospace); text-align: left; min-width: 0; } +.g-heading-static { + justify-content: center; + min-height: 40px; +} +.g-panel-content { + display: flex; + flex: 1; + min-height: 0; + flex-direction: column; + overflow: hidden; +} .gt b { font-size: 13.5px; font-weight: 600; diff --git a/libs/ui/epg/src/lib/epg-list-view/epg-list-view.component.spec.ts b/libs/ui/epg/src/lib/epg-list-view/epg-list-view.component.spec.ts index e5f77bba8..182069939 100644 --- a/libs/ui/epg/src/lib/epg-list-view/epg-list-view.component.spec.ts +++ b/libs/ui/epg/src/lib/epg-list-view/epg-list-view.component.spec.ts @@ -1,7 +1,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatDialog } from '@angular/material/dialog'; import { EpgProgram } from '@iptvnator/shared/interfaces'; -import { TranslateService } from '@ngx-translate/core'; +import { TranslateModule } from '@ngx-translate/core'; import { BehaviorSubject } from 'rxjs'; import { EpgListViewComponent } from './epg-list-view.component'; import { EpgListRow } from './epg-list-view.utils'; @@ -39,7 +39,7 @@ describe('EpgListViewComponent', () => { undefined ); TestBed.configureTestingModule({ - imports: [EpgListViewComponent], + imports: [EpgListViewComponent, TranslateModule.forRoot()], providers: [ { provide: MatDialog, @@ -47,14 +47,6 @@ describe('EpgListViewComponent', () => { open: () => ({ afterClosed: () => dialogResult }), }, }, - { - provide: TranslateService, - useValue: { - currentLang: 'en', - defaultLang: 'en', - onLangChange: new BehaviorSubject(null), - }, - }, ], }); @@ -234,6 +226,38 @@ describe('EpgListViewComponent', () => { expect(emitted).toEqual([true]); }); + it('renders an accessible Guide disclosure only when the host can collapse it', async () => { + setInputs({ + channelName: 'Sports', + collapsible: true, + panelId: 'live-guide-panel', + }); + await fixture.whenStable(); + + const toggle = fixture.nativeElement.querySelector( + '[data-testid="live-guide-toggle"]' + ) as HTMLButtonElement | null; + expect(toggle).not.toBeNull(); + expect(toggle?.getAttribute('aria-controls')).toBe('live-guide-panel'); + expect(toggle?.getAttribute('aria-expanded')).toBe('true'); + expect( + fixture.nativeElement.querySelector('#live-guide-panel') + ).not.toBeNull(); + + setInputs({ collapsible: false }); + await fixture.whenStable(); + + expect( + fixture.nativeElement.querySelector( + '[data-testid="live-guide-toggle"]' + ) + ).toBeNull(); + expect( + fixture.nativeElement.querySelector('.g-heading-static') + ?.textContent + ).toContain('Sports'); + }); + it('derives a live progress percentage from the summary when collapsed', () => { const start = new Date(Date.now() - 30 * 60_000).toISOString(); const stop = new Date(Date.now() + 30 * 60_000).toISOString(); diff --git a/libs/ui/epg/src/lib/epg-list-view/epg-list-view.component.ts b/libs/ui/epg/src/lib/epg-list-view/epg-list-view.component.ts index ace1429e4..d30a3320a 100644 --- a/libs/ui/epg/src/lib/epg-list-view/epg-list-view.component.ts +++ b/libs/ui/epg/src/lib/epg-list-view/epg-list-view.component.ts @@ -87,6 +87,8 @@ export class EpgListViewComponent { readonly emptyReason = input('none'); readonly selectedDate = input(null); readonly collapsed = input(false); + readonly collapsible = input(true); + readonly panelId = input(null); readonly summary = input(null); readonly summaryLabelKey = input('EPG.CURRENT_PROGRAM'); @@ -135,7 +137,9 @@ export class EpgListViewComponent { ); readonly nowRowMinutesLeft = computed(() => { const row = this.nowRow(); - return row ? Math.max(0, Math.round((row.stopMs - this.nowMs()) / 60_000)) : null; + return row + ? Math.max(0, Math.round((row.stopMs - this.nowMs()) / 60_000)) + : null; }); readonly viewDate = computed(() => parseEpgDateKey(this.viewDayKey())); diff --git a/libs/ui/epg/src/lib/epg-timeline/epg-timeline.component.html b/libs/ui/epg/src/lib/epg-timeline/epg-timeline.component.html index cabb39a71..55c0ad053 100644 --- a/libs/ui/epg/src/lib/epg-timeline/epg-timeline.component.html +++ b/libs/ui/epg/src/lib/epg-timeline/epg-timeline.component.html @@ -7,37 +7,46 @@ >
- + } @else { + {{ channelName() }} - @if (!collapsed() && hasSummary()) { - - @if (!isLivePlayback()) { - history - } - {{ summary()?.title }} - - } @else { - {{ sourceLabel() }} - } + {{ sourceLabel() }} - + } @@ -55,7 +64,10 @@ summary()?.title }} @if (progress() !== null) { - {{ - viewDate() | date: 'EEE, d MMM' : '' : currentLocale() + viewDate() + | date: 'EEE, d MMM' : '' : currentLocale() }}