fix(ui): keep dialog action rows on one line (#1762)

* fix(ui): keep dialog action rows on one line

Settings "Unsaved changes" dialog:
- Cancel / Discard / Save replace the phrase labels in all 19 locales, and
  the dismiss now comes first; the shared CANCEL key replaces the unused
  UNSAVED_DIALOG_STAY.
- At the 640px phone breakpoint the actions stack one per row, full width,
  in DOM order.

EPG programme dialog:
- mat-dialog-title gives the dialog an accessible name.
- The footer Close is the only dismiss; it comes first and the primary
  action last.
- The archive copy/download tools move under their notice, so the footer
  stays on one row.
- Channel rows now open it through EpgProgrammeDialogService, which owns
  the 540px config and a panel class scoping the surface overrides.

Adds a web-e2e layout spec (en, de, ru, fr, hu, ar on one row; de and ru
stacked on a phone), extends the Electron EPG spec to all three openers,
and documents the dialog contract in the UI guidelines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(epg): stack programme dialog actions on phones

Below the 640px phone breakpoint the viewport caps the programme dialog,
and a long translated primary label ("Regarder depuis le début") no
longer fit beside Close. The footer had no wrap, and the dialog hides
overflow, so the label was clipped.

- At the phone breakpoint, the archive tools and the footer now stack one
  full-width button per row, in DOM order.
- Buttons grow to fit their label, so a long label wraps inside its
  button instead of being clipped.
- On desktop the footer can wrap again as a last resort.

The new Electron test opens a past programme in French at a 360px
viewport and measures both rows. It fails against the previous
stylesheet (the footer buttons are 44px narrower than the row).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 authored and GitHub committed 2026-09-30 21:54:09 +02:00
1 parent d4118ad442
commit cb317bad4c
31 files changed
+853 -187

No files matched your search

@@ -2,18 +2,18 @@ import { DatePipe } from '@angular/common';
import { signal } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { MatDialog } from '@angular/material/dialog';
import { TranslateModule } from '@ngx-translate/core';
import { SettingsStore } from '@iptvnator/services';
import { EpgProgram } from '@iptvnator/shared/interfaces';
import { EpgProgrammeDialogService } from '@iptvnator/ui/epg';
import { ChannelListItemComponent } from './channel-list-item.component';
describe('ChannelListItemComponent', () => {
let fixture: ComponentFixture<ChannelListItemComponent>;
let dialog: { open: jest.Mock };
let programmeDialog: { open: jest.Mock };
beforeEach(async () => {
dialog = {
programmeDialog = {
open: jest.fn(),
};
@@ -25,8 +25,8 @@ describe('ChannelListItemComponent', () => {
],
providers: [
{
provide: MatDialog,
useValue: dialog,
provide: EpgProgrammeDialogService,
useValue: programmeDialog,
},
],
}).compileComponents();
@@ -140,6 +140,27 @@ describe('ChannelListItemComponent', () => {
expect(buttons[1]).not.toContain('slot-empty');
});
it('opens the programme through the shared dialog service', () => {
const program: EpgProgram = {
start: '2026-04-05 05:30:00',
stop: '2026-04-05 06:00:00',
channel: 'channel-1',
title: 'Current Show',
desc: 'Current description',
category: null,
};
fixture.componentRef.setInput('name', 'Info Channel');
fixture.componentRef.setInput('epgProgram', program);
fixture.detectChanges();
// The service owns the open config, so the row opens the dialog at
// the same width as the timeline, list and guide.
fixture.nativeElement.querySelector('.program-info-button').click();
expect(programmeDialog.open).toHaveBeenCalledTimes(1);
expect(programmeDialog.open).toHaveBeenCalledWith(program);
});
it('keeps an inert info slot while the row has no programme', () => {
fixture.componentRef.setInput('name', 'No EPG Yet');
fixture.componentRef.setInput('showFavoriteButton', true);
@@ -155,7 +176,7 @@ describe('ChannelListItemComponent', () => {
expect(slot.disabled).toBe(true);
slot.click();
expect(dialog.open).not.toHaveBeenCalled();
expect(programmeDialog.open).not.toHaveBeenCalled();
});
it('renders the catch-up badge only when catch-up is available', () => {
@@ -271,7 +292,10 @@ describe('ChannelListItemComponent', () => {
TranslateModule.forRoot(),
],
providers: [
{ provide: MatDialog, useValue: dialog },
{
provide: EpgProgrammeDialogService,
useValue: programmeDialog,
},
{
provide: SettingsStore,
useValue: {
@@ -307,7 +331,10 @@ describe('ChannelListItemComponent with an EPG display offset', () => {
TranslateModule.forRoot(),
],
providers: [
{ provide: MatDialog, useValue: { open: jest.fn() } },
{
provide: EpgProgrammeDialogService,
useValue: { open: jest.fn() },
},
{
provide: SettingsStore,
useValue: {
@@ -11,14 +11,10 @@ import {
signal,
} from '@angular/core';
import { MatIconButton } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog';
import { MatIcon } from '@angular/material/icon';
import { MatTooltip } from '@angular/material/tooltip';
import { TranslatePipe } from '@ngx-translate/core';
import {
EpgItemDescriptionComponent,
getProgramTimeMs,
} from '@iptvnator/ui/epg';
import { EpgProgrammeDialogService, getProgramTimeMs } from '@iptvnator/ui/epg';
import { EpgProgram } from '@iptvnator/shared/interfaces';
import { SettingsStore } from '@iptvnator/services';
import { applyChannelNameStrip } from '@iptvnator/shared/m3u-utils';
@@ -39,7 +35,7 @@ import { applyChannelNameStrip } from '@iptvnator/shared/m3u-utils';
],
})
export class ChannelListItemComponent {
private readonly dialog = inject(MatDialog);
private readonly programmeDialog = inject(EpgProgrammeDialogService);
private readonly logoFailed = signal(false);
private readonly settingsStore = inject(SettingsStore);
readonly epgOffsetMinutes = this.settingsStore.resolvedEpgOffsetMinutes;
@@ -99,9 +95,7 @@ export class ChannelListItemComponent {
*/
showProgramDescription(program: EpgProgram, event: MouseEvent): void {
event.stopPropagation();
this.dialog.open(EpgItemDescriptionComponent, {
data: program,
});
this.programmeDialog.open(program);
}
/** Guarded click for the reserved (possibly still empty) info slot. */