feat(switcher): rebuild the playlist switcher as an overlay popover

The switcher's mat-menu becomes a CDK connected overlay
(app-playlist-switcher-panel), 440px wide, with a dimmed, blurred
backdrop; only its list scrolls and row menus open above it.

- Header with the source count, Add playlist and All sources.
- Search by name or host, autofocused; the type filter is one segmented
  control (All / M3U / Xtream / Stalker with counts) instead of three
  checkboxes, remembered between openings (the old checkbox setting is
  migrated). The search is no longer kept between openings.
- "Recent" (the three most recently opened, kept across restarts) above
  "All" in the Sources page order, as compact source rows: the current
  one tinted with a check. Each row's ⋯ menu holds Refresh, Playlist
  info, Account info, Check again and Remove (with Undo); the footer's
  info, account and add actions moved there and to the header.
- Keyboard: a listbox driven by aria-activedescendant from the search
  field. Arrow keys move and wrap, Enter opens, Shift+F10 opens the
  row's menu, Escape closes and returns focus to the trigger. Hints in
  the footer.
- Deleting from a row menu moves focus to the toast's Undo, since the
  row (and its focus) left the page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 committed 2026-10-12 22:34:22 +02:00
1 parent 106414c118
commit 9feab4f83b
49 files changed
+1987 -1813

No files matched your search

@@ -80,7 +80,7 @@ export async function importPlaylist(page: Page): Promise<string> {
.getByRole('button', { name: 'Add playlist', exact: true })
.click(),
]);
await expect(page.getByText('1 channels')).toBeVisible();
await expect(page.getByText('1 channel', { exact: false })).toBeVisible();
return page.url();
}
+7 -4
View File
@@ -182,12 +182,15 @@ test('@stalker account info dialog shows subscription facts from the portal', as
}) => {
await addStalkerPortal(page);
// Open the header playlist switcher and its "Account info" entry for
// the active stalker portal.
await page.locator('.playlist-switcher-trigger').click();
// Open the header playlist switcher and the "Account info" entry in the
// active stalker portal's row menu.
await page.locator('[data-test-id="playlist-switcher-trigger"]').click();
await page
.locator('.context-action-item', { hasText: 'Account info' })
.locator(
'app-playlist-switcher-panel [role="option"][aria-current="true"] .source-row__more'
)
.click();
await page.getByRole('menuitem', { name: 'Account info' }).click();
const dialog = page.locator('mat-dialog-container');
await expect(dialog).toBeVisible();
+4 -7
View File
@@ -61,18 +61,15 @@ async function openPlaylistDetailsDialog(page: Page, title: string) {
await playlistSettingsButton.click();
} else {
await page
.locator('app-playlist-switcher .playlist-switcher-trigger')
.locator('[data-test-id="playlist-switcher-trigger"]')
.click();
const switcherMenu = page.getByRole('menu').filter({ hasText: title });
const sourceRow = switcherMenu
.locator('.playlist-item')
const sourceRow = page
.locator('app-playlist-switcher-panel [role="option"]')
.filter({ hasText: title })
.first();
await expect(sourceRow).toBeVisible();
await sourceRow
.getByRole('button', { name: 'Playlist actions' })
.click();
await sourceRow.locator('.source-row__more').click();
await page.getByRole('menuitem', { name: 'Playlist info' }).click();
}