From 0a2373f192bd68d2765000b71570828ec6ba9072 Mon Sep 17 00:00:00 2001 From: 4gray <4gray@users.noreply.github.com> Date: Sun, 6 Sep 2026 14:42:59 +0200 Subject: [PATCH] feat(portals): fold live TV panels in nested levels with a category dropdown (#1556) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Live TV panels now fold from the outside in, in three nested levels, instead of one toggle that hid the categories rail and the channel list together: 1. **Categories + channels + player** (browse, unchanged). 2. **Channels + player** — a new `chevron_left` in the categories rail header hides only that rail. The channels header then turns its title into a **category dropdown** that opens the same shell panel as a popover (search, sort, counts, selection are one implementation), plus a `chevron_right` that brings the rail back. 3. **Player only** — the channels header chevron, as before. The floating restore handle and `Cmd/Ctrl+B` return to the level the user collapsed from, not always to level 1. Every level is restored as stored, per surface (`live-sidebar-state:`, from #1555): a hidden rail is discoverable through the workspace header toggle and the hidden-list empty state that #1555 added, so this PR no longer needs its original "player-only never restores" rule. The level `Cmd/Ctrl+B` comes back to is seeded from the restored level and kept for the session. ## Design notes - Nested levels rather than two independent booleans: "channels hidden, categories visible" makes no sense since a category click has to bring the channels back anyway. The model follows the outside-in collapse of three-pane apps (Mail, Slack, Plex). - The categories rail folds at level 2 **only while a category is selected**: the live root ("All Items" grid) has no channels header to host the way back, so folding there would strand the user. Level 3 folds it regardless, because the floating restore handle lives in the content area. - `LIVE_CATEGORIES_POPOVER` (`@iptvnator/portal/shared/util`) is the DI bridge: the workspace shell provides `WorkspaceLiveCategoriesPopoverService` (CDK overlay hosting `WorkspaceContextPanelComponent` in `presentation="popover"`), the Xtream and Stalker live layouts inject it optionally and keep their plain heading without a provider. - M3U and the unified live tab have no categories rail and treat level 2 like level 1; their code is untouched. ## Merged with #1555 (per-surface rail state) #1555 landed while this PR was open and reworked the same service: state per surface (`m3u` / `portal` / `collection`), a workspace header toggle, the hidden-list empty state, and the legacy shared key forgotten on startup. This PR keeps that model and layers the three levels onto the `portal` surface (`areCategoriesHiddenFor`, `hideCategories` / `showCategories` / `collapse` / `expand` per surface; `toggle(surface)` returns to the level the surface collapsed from). "Show playing channel" uses `expand('portal')` so it keeps a deliberately hidden categories rail folded, and the category sort preference moved to `PortalCategorySortStateService` so the popover copy of the context panel and the retained rail agree. ## Also fixed along the way - The channels header showed "Channels" instead of the category name: provider category ids are strings, the selection is numeric. Compared via `String()` now. - A collapsed context panel left a 22px padding strip beside the channels rail. - The panel toggle labels said "Hide channels list" while also hiding categories; labels and tooltips are honest now (8 new i18n keys, all 18 locales). Docs: `docs/architecture/iptvnator-ui-guidelines.md` ("Collapsible Live Sidebar" rewritten), `docs/architecture/workspace-shell.md`. Release note: `.changes/portals-live-panel-collapse-levels.md`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Fable 5.1 --- .../portals-live-panel-collapse-levels.md | 10 + AGENTS.md | 30 ++ CLAUDE.md | 30 ++ .../src/remote-control.e2e.ts | 39 +- apps/web-e2e/src/stalker.e2e.ts | 50 ++- apps/web/src/assets/i18n/ar.json | 10 +- apps/web/src/assets/i18n/ary.json | 10 +- apps/web/src/assets/i18n/by.json | 10 +- apps/web/src/assets/i18n/de.json | 10 +- apps/web/src/assets/i18n/el.json | 10 +- apps/web/src/assets/i18n/en.json | 10 +- apps/web/src/assets/i18n/es.json | 10 +- apps/web/src/assets/i18n/fr.json | 10 +- apps/web/src/assets/i18n/hu.json | 10 +- apps/web/src/assets/i18n/it.json | 10 +- apps/web/src/assets/i18n/ja.json | 10 +- apps/web/src/assets/i18n/ko.json | 10 +- apps/web/src/assets/i18n/nl.json | 10 +- apps/web/src/assets/i18n/pl.json | 10 +- apps/web/src/assets/i18n/pt.json | 10 +- apps/web/src/assets/i18n/ru.json | 10 +- apps/web/src/assets/i18n/tr.json | 10 +- apps/web/src/assets/i18n/zh.json | 10 +- apps/web/src/assets/i18n/zhtw.json | 10 +- docs/architecture/iptvnator-ui-guidelines.md | 124 +++++- docs/architecture/workspace-shell.md | 6 + libs/portal/shared/util/src/index.ts | 4 + .../src/lib/category-sort-state.service.ts | 26 ++ .../shared/util/src/lib/focus-handoff.ts | 50 +++ .../src/lib/live-categories-popover.token.ts | 33 ++ .../live-layout-sidebar-state.service.spec.ts | 62 +++ .../lib/live-layout-sidebar-state.service.ts | 68 ++- .../src/lib/live-panels-controller.spec.ts | 129 ++++++ .../util/src/lib/live-panels-controller.ts | 116 +++++ .../util/src/lib/live-sidebar-state.spec.ts | 1 + .../shared/util/src/lib/live-sidebar-state.ts | 21 +- .../stalker-live-navigation.spec.ts | 4 +- .../stalker-live-navigation.ts | 2 +- .../stalker-live-stream-layout.component.html | 88 ++-- .../stalker-live-stream-layout.component.ts | 29 +- ...alker-live-stream-layout.epg-races.spec.ts | 2 + ...er-live-stream-layout.panel-search.spec.ts | 2 + ...stream-layout.playback-session-key.spec.ts | 2 + ...e-stream-auto-open-queue.component.spec.ts | 5 +- .../live-stream-layout.component.html | 78 +++- .../live-stream-layout.component.spec.ts | 35 -- .../live-stream-layout.component.ts | 36 +- .../live-stream-layout.sidebar-levels.spec.ts | 405 ++++++++++++++++++ ...am-live-channel-navigation.service.spec.ts | 5 +- .../xtream-live-channel-navigation.service.ts | 2 +- libs/ui/styles/_portal-sidebar.scss | 53 +++ ...space-context-category-view.component.html | 7 +- ...rkspace-context-category-view.component.ts | 19 +- .../workspace-context-panel.component.html | 20 + .../workspace-context-panel.component.spec.ts | 200 ++++++++- .../workspace-context-panel.component.ts | 76 +++- ...ace-live-categories-popover.component.html | 22 + ...ace-live-categories-popover.component.scss | 61 +++ ...space-live-categories-popover.component.ts | 72 ++++ ...ce-live-categories-popover.service.spec.ts | 286 +++++++++++++ ...rkspace-live-categories-popover.service.ts | 168 ++++++++ ...space-shell-context-sidebar.component.html | 3 + ...space-shell-context-sidebar.component.scss | 7 +- ...ce-shell-context-sidebar.component.spec.ts | 120 +++++- ...rkspace-shell-context-sidebar.component.ts | 81 +++- .../workspace-shell.component.ts | 8 + ...space-shell-context-drawer.service.spec.ts | 12 + .../workspace-shell-context-drawer.service.ts | 8 + 68 files changed, 2710 insertions(+), 197 deletions(-) create mode 100644 .changes/portals-live-panel-collapse-levels.md create mode 100644 libs/portal/shared/util/src/lib/category-sort-state.service.ts create mode 100644 libs/portal/shared/util/src/lib/focus-handoff.ts create mode 100644 libs/portal/shared/util/src/lib/live-categories-popover.token.ts create mode 100644 libs/portal/shared/util/src/lib/live-panels-controller.spec.ts create mode 100644 libs/portal/shared/util/src/lib/live-panels-controller.ts create mode 100644 libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.sidebar-levels.spec.ts create mode 100644 libs/workspace/shell/feature/src/lib/workspace-live-categories-popover/workspace-live-categories-popover.component.html create mode 100644 libs/workspace/shell/feature/src/lib/workspace-live-categories-popover/workspace-live-categories-popover.component.scss create mode 100644 libs/workspace/shell/feature/src/lib/workspace-live-categories-popover/workspace-live-categories-popover.component.ts create mode 100644 libs/workspace/shell/feature/src/lib/workspace-live-categories-popover/workspace-live-categories-popover.service.spec.ts create mode 100644 libs/workspace/shell/feature/src/lib/workspace-live-categories-popover/workspace-live-categories-popover.service.ts diff --git a/.changes/portals-live-panel-collapse-levels.md b/.changes/portals-live-panel-collapse-levels.md new file mode 100644 index 000000000..bec26b05d --- /dev/null +++ b/.changes/portals-live-panel-collapse-levels.md @@ -0,0 +1,10 @@ +--- +type: feature +area: portals +--- + +Live TV panels now fold in steps: hide only the categories rail and keep the +channel list, or hide both for a player-only view. With categories hidden, the +list header becomes a category dropdown, so switching categories stays one +click away, and hiding both panels then bringing them back returns to the +level you had before. diff --git a/AGENTS.md b/AGENTS.md index 87948701d..22937d53f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -260,6 +260,36 @@ never narrows shared provider pages or resets their index. Category changes reset list windows and retain playback/active EPG. Contract: `docs/architecture/stalker-portal.md` (Full ITV Channel List Cache). +## Live TV Panel Levels + +Portal live layouts (Xtream `live`, Stalker `itv`/`radio`) fold their panels +from the outside in, in three nested levels owned by `LiveSidebarState` +(`@iptvnator/portal/shared/util`): `expanded` (categories rail + channels rail ++ player), `categories-hidden` (channels rail + player) and `collapsed` +(player only). `LiveLayoutSidebarStateService` is the single source of truth, per +surface (`m3u` / `portal` / `collection`; the levels apply to `portal`); the +shell context sidebar folds the categories rail on +`areCategoriesHiddenFor('portal')` (at level 2 only while the portal store has +a selected category — the live root has no channels header to host the way +back — and always at level 3), the channels rail folds on +`isCollapsedFor('portal')`. While the rail is folded the +channels header turns its title into a category dropdown that opens the same +`WorkspaceContextPanelComponent` as a CDK popover through the +`LIVE_CATEGORIES_POPOVER` token: the workspace shell provides +`WorkspaceLiveCategoriesPopoverService` (focus-trapped `role="dialog"`, +closed by backdrop, Escape, selection, its footer and any `NavigationStart`), +the live layouts reach it through `createLivePanelsController()` (level +flags, dropdown bridge and focus handoff in one shared object; the token is +optional). `Cmd/Ctrl+B`, the header toggle and the +floating restore handle return to the level the user collapsed from (the +target is session-only; every level is restored as stored per surface). +Folded rails carry `inert`, and +`handoffFocusOnLiveSidebarChange()` / `focusIfFocusLost()` move focus to the +replacement affordance only when the activated button was removed or inerted. +M3U and the unified live tab have no categories rail and treat level 2 like +level 1. Contract: `docs/architecture/iptvnator-ui-guidelines.md` +("Collapsible Live Sidebar"). + ## Channel and Detail Keyboard Scrolling Channel scroll owners use `ChannelScrollFocusDirective`; pointer selection diff --git a/CLAUDE.md b/CLAUDE.md index 01af2cd7a..71435ee62 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1739,6 +1739,36 @@ preventing destination failures from penalizing the initial endpoint. Contracts: copy and Retry now; Stalker preserves cached account data on a failed refresh. Contract: `docs/architecture/host-connectivity-guard.md`. +## Live TV Panel Levels + +Portal live layouts (Xtream `live`, Stalker `itv`/`radio`) fold their panels +from the outside in, in three nested levels owned by `LiveSidebarState` +(`@iptvnator/portal/shared/util`): `expanded` (categories rail + channels rail ++ player), `categories-hidden` (channels rail + player) and `collapsed` +(player only). `LiveLayoutSidebarStateService` is the single source of truth, per +surface (`m3u` / `portal` / `collection`; the levels apply to `portal`); the +shell context sidebar folds the categories rail on +`areCategoriesHiddenFor('portal')` (at level 2 only while the portal store has +a selected category — the live root has no channels header to host the way +back — and always at level 3), the channels rail folds on +`isCollapsedFor('portal')`. While the rail is folded the +channels header turns its title into a category dropdown that opens the same +`WorkspaceContextPanelComponent` as a CDK popover through the +`LIVE_CATEGORIES_POPOVER` token: the workspace shell provides +`WorkspaceLiveCategoriesPopoverService` (focus-trapped `role="dialog"`, +closed by backdrop, Escape, selection, its footer and any `NavigationStart`), +the live layouts reach it through `createLivePanelsController()` (level +flags, dropdown bridge and focus handoff in one shared object; the token is +optional). `Cmd/Ctrl+B`, the header toggle and the +floating restore handle return to the level the user collapsed from (the +target is session-only; every level is restored as stored per surface). +Folded rails carry `inert`, and +`handoffFocusOnLiveSidebarChange()` / `focusIfFocusLost()` move focus to the +replacement affordance only when the activated button was removed or inerted. +M3U and the unified live tab have no categories rail and treat level 2 like +level 1. Contract: `docs/architecture/iptvnator-ui-guidelines.md` +("Collapsible Live Sidebar"). + ## Live Channel Return Xtream and Stalker (including radio) capture displayed playback order on explicit diff --git a/apps/electron-backend-e2e/src/remote-control.e2e.ts b/apps/electron-backend-e2e/src/remote-control.e2e.ts index 82d079cfe..e26056872 100644 --- a/apps/electron-backend-e2e/src/remote-control.e2e.ts +++ b/apps/electron-backend-e2e/src/remote-control.e2e.ts @@ -82,6 +82,13 @@ test.describe('Electron Remote Control', () => { .getByTestId('channel-item'); await expect(rows.nth(2)).toBeVisible(); if (provider === 'xtream') { + // Read the order before sorting: the menu closes before the + // virtual list re-renders, and reading the titles too early + // captured the server order while the click below landed + // on the re-sorted first row (flaky on slow runners). + const serverOrder = await rows + .locator('.channel-name') + .allTextContents(); await page .getByRole('button', { name: 'Sort channels', @@ -91,6 +98,11 @@ test.describe('Electron Remote Control', () => { await page .getByRole('menuitem', { name: 'Name Z-A' }) .click(); + await expect + .poll(() => + rows.locator('.channel-name').allTextContents() + ) + .not.toEqual(serverOrder); } const titles = await rows .locator('.channel-name') @@ -448,15 +460,24 @@ async function waitForRemoteStatus( ): Promise { let latestStatus: RemoteControlStatus | null = null; - await expect - .poll( - async () => { - latestStatus = await getRemoteStatus(request, port); - return latestStatus ? predicate(latestStatus) : false; - }, - { timeout: 20000 } - ) - .toBe(true); + try { + await expect + .poll( + async () => { + latestStatus = await getRemoteStatus(request, port); + return latestStatus ? predicate(latestStatus) : false; + }, + { timeout: 20000 } + ) + .toBe(true); + } catch (error) { + // The predicate alone says nothing about how far the status got; + // surface the last payload so a CI failure is diagnosable. + throw new Error( + `Remote status never matched. Last status: ${JSON.stringify(latestStatus)}`, + { cause: error } + ); + } return latestStatus as RemoteControlStatus; } diff --git a/apps/web-e2e/src/stalker.e2e.ts b/apps/web-e2e/src/stalker.e2e.ts index 8f0f48bd3..e3888c843 100644 --- a/apps/web-e2e/src/stalker.e2e.ts +++ b/apps/web-e2e/src/stalker.e2e.ts @@ -513,22 +513,50 @@ test('@stalker radio — stations use the inline audio player without EPG', asyn timeout: 20_000, }); - // The rail's own chevron hides the list; the workspace header carries a - // second toggle with the same accessible name, so scope to the rail. + // Level 2: only the categories rail folds; the channels header turns + // its title into a category dropdown that opens the same rail as a + // popover, whose footer brings the rail back. await page - .locator('.sidebar') - .getByRole('button', { name: 'Hide channels list' }) + .getByRole('button', { name: 'Hide categories', exact: true }) .click(); - const restoreButton = page.locator('.sidebar-restore'); + const categoryDropdown = page.locator( + '[data-test-id="live-category-dropdown"]' + ); + // The folded rail is 0px wide and inert; its rows keep their own boxes, + // so the rail's attribute is the reliable signal. + const categoriesRail = page.locator('aside.context-panel--route'); + await expect(categoryDropdown).toBeVisible(); + await expect(categoriesRail).toHaveAttribute('inert', ''); + await categoryDropdown.click(); + await page + .getByRole('dialog') + .getByRole('button', { name: 'Show categories panel' }) + .click(); + await expect(categoryDropdown).toBeHidden(); + await expect(categoriesRail).not.toHaveAttribute('inert'); + await expect(categories.nth(1)).toBeVisible(); + + // Level 3: both rails fold; the floating handle restores them. + await page + .getByRole('button', { name: 'Hide categories and channels' }) + .click(); + const restoreButton = page.getByRole('button', { + name: 'Show categories and channels', + }); await expect(restoreButton).toBeVisible(); - await expect( - page.locator('app-workspace-shell-header .header-sidebar-toggle') - ).toHaveAttribute('aria-pressed', 'false'); + await expect(categoriesRail).toHaveAttribute('inert', ''); + await expect(page.locator('.sidebar')).toHaveAttribute('inert', ''); + // The workspace header carries a second toggle that mirrors the state. + const headerToggle = page.locator( + 'app-workspace-shell-header .header-sidebar-toggle' + ); + await expect(headerToggle).toHaveAttribute('aria-pressed', 'false'); await restoreButton.click(); + await expect(restoreButton).toBeHidden(); + await expect(headerToggle).toHaveAttribute('aria-pressed', 'true'); + await expect(categoriesRail).not.toHaveAttribute('inert'); + await expect(page.locator('.sidebar')).not.toHaveAttribute('inert'); await expect(stations.first()).toBeVisible(); - await expect( - page.locator('app-workspace-shell-header .header-sidebar-toggle') - ).toHaveAttribute('aria-pressed', 'true'); await expect(page.locator('app-epg-timeline')).toHaveCount(0); expect(epgRequests).toHaveLength(0); diff --git a/apps/web/src/assets/i18n/ar.json b/apps/web/src/assets/i18n/ar.json index 729b84b1c..df9c49545 100644 --- a/apps/web/src/assets/i18n/ar.json +++ b/apps/web/src/assets/i18n/ar.json @@ -676,7 +676,15 @@ "SHOW_CHANNELS_LIST": "إظهار قائمة القنوات", "TOGGLE_SIDEBAR_TOOLTIP": "تبديل قائمة القنوات (⌘/Ctrl+B)", "CHANNELS_LIST_HIDDEN": "قائمة القنوات مخفية", - "CHANNELS_LIST_HIDDEN_HINT": "أعدها باستخدام الزر أدناه أو اضغط ⌘/Ctrl+B." + "CHANNELS_LIST_HIDDEN_HINT": "أعدها باستخدام الزر أدناه أو اضغط ⌘/Ctrl+B.", + "HIDE_CATEGORIES": "إخفاء الفئات", + "SHOW_CATEGORIES": "إظهار الفئات", + "SHOW_CATEGORIES_PANEL": "إظهار لوحة الفئات", + "CHOOSE_CATEGORY": "اختيار الفئة", + "HIDE_PANELS": "إخفاء الفئات والقنوات", + "HIDE_PANELS_TOOLTIP": "إخفاء اللوحات (⌘/Ctrl+B)", + "SHOW_PANELS": "إظهار الفئات والقنوات", + "SHOW_PANELS_TOOLTIP": "إظهار اللوحات (⌘/Ctrl+B)" }, "CHANNELS": { "SHOW_PLAYING_CHANNEL": "إظهار القناة قيد التشغيل", diff --git a/apps/web/src/assets/i18n/ary.json b/apps/web/src/assets/i18n/ary.json index cc6a21c4d..7239c91d4 100644 --- a/apps/web/src/assets/i18n/ary.json +++ b/apps/web/src/assets/i18n/ary.json @@ -676,7 +676,15 @@ "SHOW_CHANNELS_LIST": "إظهار قائمة القنوات", "TOGGLE_SIDEBAR_TOOLTIP": "تبديل قائمة القنوات (⌘/Ctrl+B)", "CHANNELS_LIST_HIDDEN": "قائمة القنوات مخبية", - "CHANNELS_LIST_HIDDEN_HINT": "رجّعها بالزر اللي تحت ولا ضغط على ⌘/Ctrl+B." + "CHANNELS_LIST_HIDDEN_HINT": "رجّعها بالزر اللي تحت ولا ضغط على ⌘/Ctrl+B.", + "HIDE_CATEGORIES": "إخفاء الفئات", + "SHOW_CATEGORIES": "إظهار الفئات", + "SHOW_CATEGORIES_PANEL": "إظهار لوحة الفئات", + "CHOOSE_CATEGORY": "اختار فئة", + "HIDE_PANELS": "إخفاء الفئات والقنوات", + "HIDE_PANELS_TOOLTIP": "إخفاء اللوحات (⌘/Ctrl+B)", + "SHOW_PANELS": "إظهار الفئات والقنوات", + "SHOW_PANELS_TOOLTIP": "إظهار اللوحات (⌘/Ctrl+B)" }, "CHANNELS": { "SHOW_PLAYING_CHANNEL": "بيّن القناة اللي خدامة", diff --git a/apps/web/src/assets/i18n/by.json b/apps/web/src/assets/i18n/by.json index e0c736366..ff82bccf8 100644 --- a/apps/web/src/assets/i18n/by.json +++ b/apps/web/src/assets/i18n/by.json @@ -676,7 +676,15 @@ "SHOW_CHANNELS_LIST": "Паказаць спіс каналаў", "TOGGLE_SIDEBAR_TOOLTIP": "Пераключыць спіс каналаў (⌘/Ctrl+B)", "CHANNELS_LIST_HIDDEN": "Спіс каналаў схаваны", - "CHANNELS_LIST_HIDDEN_HINT": "Вярніце яго кнопкай ніжэй або націсніце ⌘/Ctrl+B." + "CHANNELS_LIST_HIDDEN_HINT": "Вярніце яго кнопкай ніжэй або націсніце ⌘/Ctrl+B.", + "HIDE_CATEGORIES": "Схаваць катэгорыі", + "SHOW_CATEGORIES": "Паказаць катэгорыі", + "SHOW_CATEGORIES_PANEL": "Паказаць панэль катэгорый", + "CHOOSE_CATEGORY": "Выбраць катэгорыю", + "HIDE_PANELS": "Схаваць катэгорыі і каналы", + "HIDE_PANELS_TOOLTIP": "Схаваць панэлі (⌘/Ctrl+B)", + "SHOW_PANELS": "Паказаць катэгорыі і каналы", + "SHOW_PANELS_TOOLTIP": "Паказаць панэлі (⌘/Ctrl+B)" }, "CHANNELS": { "SHOW_PLAYING_CHANNEL": "Паказаць бягучы канал", diff --git a/apps/web/src/assets/i18n/de.json b/apps/web/src/assets/i18n/de.json index 141aa1314..b3ba34b20 100644 --- a/apps/web/src/assets/i18n/de.json +++ b/apps/web/src/assets/i18n/de.json @@ -676,7 +676,15 @@ "SHOW_CHANNELS_LIST": "Senderliste einblenden", "TOGGLE_SIDEBAR_TOOLTIP": "Senderliste umschalten (⌘/Strg+B)", "CHANNELS_LIST_HIDDEN": "Die Senderliste ist ausgeblendet", - "CHANNELS_LIST_HIDDEN_HINT": "Blenden Sie sie über die Schaltfläche unten oder mit ⌘/Strg+B wieder ein." + "CHANNELS_LIST_HIDDEN_HINT": "Blenden Sie sie über die Schaltfläche unten oder mit ⌘/Strg+B wieder ein.", + "HIDE_CATEGORIES": "Kategorien ausblenden", + "SHOW_CATEGORIES": "Kategorien einblenden", + "SHOW_CATEGORIES_PANEL": "Kategorien-Panel einblenden", + "CHOOSE_CATEGORY": "Kategorie auswählen", + "HIDE_PANELS": "Kategorien und Sender ausblenden", + "HIDE_PANELS_TOOLTIP": "Panels ausblenden (⌘/Ctrl+B)", + "SHOW_PANELS": "Kategorien und Sender einblenden", + "SHOW_PANELS_TOOLTIP": "Panels einblenden (⌘/Ctrl+B)" }, "CHANNELS": { "SHOW_PLAYING_CHANNEL": "Laufenden Sender anzeigen", diff --git a/apps/web/src/assets/i18n/el.json b/apps/web/src/assets/i18n/el.json index 2492622d6..dba844c76 100644 --- a/apps/web/src/assets/i18n/el.json +++ b/apps/web/src/assets/i18n/el.json @@ -676,7 +676,15 @@ "SHOW_CHANNELS_LIST": "Εμφάνιση λίστας καναλιών", "TOGGLE_SIDEBAR_TOOLTIP": "Εναλλαγή λίστας καναλιών (⌘/Ctrl+B)", "CHANNELS_LIST_HIDDEN": "Η λίστα καναλιών είναι κρυφή", - "CHANNELS_LIST_HIDDEN_HINT": "Επαναφέρετέ την με το παρακάτω κουμπί ή πατήστε ⌘/Ctrl+B." + "CHANNELS_LIST_HIDDEN_HINT": "Επαναφέρετέ την με το παρακάτω κουμπί ή πατήστε ⌘/Ctrl+B.", + "HIDE_CATEGORIES": "Απόκρυψη κατηγοριών", + "SHOW_CATEGORIES": "Εμφάνιση κατηγοριών", + "SHOW_CATEGORIES_PANEL": "Εμφάνιση πίνακα κατηγοριών", + "CHOOSE_CATEGORY": "Επιλογή κατηγορίας", + "HIDE_PANELS": "Απόκρυψη κατηγοριών και καναλιών", + "HIDE_PANELS_TOOLTIP": "Απόκρυψη πινάκων (⌘/Ctrl+B)", + "SHOW_PANELS": "Εμφάνιση κατηγοριών και καναλιών", + "SHOW_PANELS_TOOLTIP": "Εμφάνιση πινάκων (⌘/Ctrl+B)" }, "CHANNELS": { "SHOW_PLAYING_CHANNEL": "Εμφάνιση του καναλιού που παίζει", diff --git a/apps/web/src/assets/i18n/en.json b/apps/web/src/assets/i18n/en.json index 24c173208..b88314af5 100644 --- a/apps/web/src/assets/i18n/en.json +++ b/apps/web/src/assets/i18n/en.json @@ -676,7 +676,15 @@ "SHOW_CHANNELS_LIST": "Show channels list", "TOGGLE_SIDEBAR_TOOLTIP": "Toggle channels list (⌘/Ctrl+B)", "CHANNELS_LIST_HIDDEN": "The channels list is hidden", - "CHANNELS_LIST_HIDDEN_HINT": "Bring it back with the button below or press ⌘/Ctrl+B." + "CHANNELS_LIST_HIDDEN_HINT": "Bring it back with the button below or press ⌘/Ctrl+B.", + "HIDE_CATEGORIES": "Hide categories", + "SHOW_CATEGORIES": "Show categories", + "SHOW_CATEGORIES_PANEL": "Show categories panel", + "CHOOSE_CATEGORY": "Choose category", + "HIDE_PANELS": "Hide categories and channels", + "HIDE_PANELS_TOOLTIP": "Hide panels (⌘/Ctrl+B)", + "SHOW_PANELS": "Show categories and channels", + "SHOW_PANELS_TOOLTIP": "Show panels (⌘/Ctrl+B)" }, "CHANNELS": { "SHOW_PLAYING_CHANNEL": "Show playing channel", diff --git a/apps/web/src/assets/i18n/es.json b/apps/web/src/assets/i18n/es.json index c9c4444ce..f9eed4b71 100644 --- a/apps/web/src/assets/i18n/es.json +++ b/apps/web/src/assets/i18n/es.json @@ -676,7 +676,15 @@ "SHOW_CHANNELS_LIST": "Mostrar lista de canales", "TOGGLE_SIDEBAR_TOOLTIP": "Alternar lista de canales (⌘/Ctrl+B)", "CHANNELS_LIST_HIDDEN": "La lista de canales está oculta", - "CHANNELS_LIST_HIDDEN_HINT": "Vuelve a mostrarla con el botón de abajo o pulsa ⌘/Ctrl+B." + "CHANNELS_LIST_HIDDEN_HINT": "Vuelve a mostrarla con el botón de abajo o pulsa ⌘/Ctrl+B.", + "HIDE_CATEGORIES": "Ocultar categorías", + "SHOW_CATEGORIES": "Mostrar categorías", + "SHOW_CATEGORIES_PANEL": "Mostrar panel de categorías", + "CHOOSE_CATEGORY": "Elegir categoría", + "HIDE_PANELS": "Ocultar categorías y canales", + "HIDE_PANELS_TOOLTIP": "Ocultar paneles (⌘/Ctrl+B)", + "SHOW_PANELS": "Mostrar categorías y canales", + "SHOW_PANELS_TOOLTIP": "Mostrar paneles (⌘/Ctrl+B)" }, "CHANNELS": { "SHOW_PLAYING_CHANNEL": "Mostrar el canal en reproducción", diff --git a/apps/web/src/assets/i18n/fr.json b/apps/web/src/assets/i18n/fr.json index 6ac584f32..664c34688 100644 --- a/apps/web/src/assets/i18n/fr.json +++ b/apps/web/src/assets/i18n/fr.json @@ -676,7 +676,15 @@ "SHOW_CHANNELS_LIST": "Afficher la liste des chaînes", "TOGGLE_SIDEBAR_TOOLTIP": "Basculer la liste des chaînes (⌘/Ctrl+B)", "CHANNELS_LIST_HIDDEN": "La liste des chaînes est masquée", - "CHANNELS_LIST_HIDDEN_HINT": "Réaffichez-la avec le bouton ci-dessous ou appuyez sur ⌘/Ctrl+B." + "CHANNELS_LIST_HIDDEN_HINT": "Réaffichez-la avec le bouton ci-dessous ou appuyez sur ⌘/Ctrl+B.", + "HIDE_CATEGORIES": "Masquer les catégories", + "SHOW_CATEGORIES": "Afficher les catégories", + "SHOW_CATEGORIES_PANEL": "Afficher le panneau des catégories", + "CHOOSE_CATEGORY": "Choisir une catégorie", + "HIDE_PANELS": "Masquer les catégories et les chaînes", + "HIDE_PANELS_TOOLTIP": "Masquer les panneaux (⌘/Ctrl+B)", + "SHOW_PANELS": "Afficher les catégories et les chaînes", + "SHOW_PANELS_TOOLTIP": "Afficher les panneaux (⌘/Ctrl+B)" }, "CHANNELS": { "SHOW_PLAYING_CHANNEL": "Afficher la chaîne en cours", diff --git a/apps/web/src/assets/i18n/hu.json b/apps/web/src/assets/i18n/hu.json index da78c5060..f124d7e43 100644 --- a/apps/web/src/assets/i18n/hu.json +++ b/apps/web/src/assets/i18n/hu.json @@ -676,7 +676,15 @@ "SHOW_CHANNELS_LIST": "Csatornalista megjelenítése", "TOGGLE_SIDEBAR_TOOLTIP": "Csatornalista megjelenítése vagy elrejtése (⌘/Ctrl+B)", "CHANNELS_LIST_HIDDEN": "A csatornalista el van rejtve", - "CHANNELS_LIST_HIDDEN_HINT": "Az alábbi gombbal vagy a ⌘/Ctrl+B billentyűkkel jelenítheti meg újra." + "CHANNELS_LIST_HIDDEN_HINT": "Az alábbi gombbal vagy a ⌘/Ctrl+B billentyűkkel jelenítheti meg újra.", + "HIDE_CATEGORIES": "Kategóriák elrejtése", + "SHOW_CATEGORIES": "Kategóriák megjelenítése", + "SHOW_CATEGORIES_PANEL": "Kategóriapanel megjelenítése", + "CHOOSE_CATEGORY": "Kategória kiválasztása", + "HIDE_PANELS": "Kategóriák és csatornák elrejtése", + "HIDE_PANELS_TOOLTIP": "Panelek elrejtése (⌘/Ctrl+B)", + "SHOW_PANELS": "Kategóriák és csatornák megjelenítése", + "SHOW_PANELS_TOOLTIP": "Panelek megjelenítése (⌘/Ctrl+B)" }, "CHANNELS": { "SHOW_PLAYING_CHANNEL": "Lejátszott csatorna megjelenítése", diff --git a/apps/web/src/assets/i18n/it.json b/apps/web/src/assets/i18n/it.json index 0ae8eb489..500fc5288 100644 --- a/apps/web/src/assets/i18n/it.json +++ b/apps/web/src/assets/i18n/it.json @@ -676,7 +676,15 @@ "SHOW_CHANNELS_LIST": "Mostra elenco canali", "TOGGLE_SIDEBAR_TOOLTIP": "Attiva/disattiva elenco canali (⌘/Ctrl+B)", "CHANNELS_LIST_HIDDEN": "L'elenco canali è nascosto", - "CHANNELS_LIST_HIDDEN_HINT": "Mostralo di nuovo con il pulsante qui sotto o premi ⌘/Ctrl+B." + "CHANNELS_LIST_HIDDEN_HINT": "Mostralo di nuovo con il pulsante qui sotto o premi ⌘/Ctrl+B.", + "HIDE_CATEGORIES": "Nascondi categorie", + "SHOW_CATEGORIES": "Mostra categorie", + "SHOW_CATEGORIES_PANEL": "Mostra il pannello delle categorie", + "CHOOSE_CATEGORY": "Scegli categoria", + "HIDE_PANELS": "Nascondi categorie e canali", + "HIDE_PANELS_TOOLTIP": "Nascondi i pannelli (⌘/Ctrl+B)", + "SHOW_PANELS": "Mostra categorie e canali", + "SHOW_PANELS_TOOLTIP": "Mostra i pannelli (⌘/Ctrl+B)" }, "CHANNELS": { "SHOW_PLAYING_CHANNEL": "Mostra il canale in riproduzione", diff --git a/apps/web/src/assets/i18n/ja.json b/apps/web/src/assets/i18n/ja.json index 5da77c661..ee6d83bbd 100644 --- a/apps/web/src/assets/i18n/ja.json +++ b/apps/web/src/assets/i18n/ja.json @@ -676,7 +676,15 @@ "SHOW_CHANNELS_LIST": "チャンネル一覧を表示", "TOGGLE_SIDEBAR_TOOLTIP": "チャンネル一覧の切り替え (⌘/Ctrl+B)", "CHANNELS_LIST_HIDDEN": "チャンネル一覧は非表示です", - "CHANNELS_LIST_HIDDEN_HINT": "下のボタンまたは ⌘/Ctrl+B で再表示できます。" + "CHANNELS_LIST_HIDDEN_HINT": "下のボタンまたは ⌘/Ctrl+B で再表示できます。", + "HIDE_CATEGORIES": "カテゴリーを非表示", + "SHOW_CATEGORIES": "カテゴリーを表示", + "SHOW_CATEGORIES_PANEL": "カテゴリーパネルを表示", + "CHOOSE_CATEGORY": "カテゴリーを選択", + "HIDE_PANELS": "カテゴリーとチャンネルを非表示", + "HIDE_PANELS_TOOLTIP": "パネルを非表示 (⌘/Ctrl+B)", + "SHOW_PANELS": "カテゴリーとチャンネルを表示", + "SHOW_PANELS_TOOLTIP": "パネルを表示 (⌘/Ctrl+B)" }, "CHANNELS": { "SHOW_PLAYING_CHANNEL": "再生中のチャンネルを表示", diff --git a/apps/web/src/assets/i18n/ko.json b/apps/web/src/assets/i18n/ko.json index 64cd75eb1..513200dae 100644 --- a/apps/web/src/assets/i18n/ko.json +++ b/apps/web/src/assets/i18n/ko.json @@ -676,7 +676,15 @@ "SHOW_CHANNELS_LIST": "채널 목록 표시", "TOGGLE_SIDEBAR_TOOLTIP": "채널 목록 전환 (⌘/Ctrl+B)", "CHANNELS_LIST_HIDDEN": "채널 목록이 숨겨져 있습니다", - "CHANNELS_LIST_HIDDEN_HINT": "아래 버튼을 누르거나 ⌘/Ctrl+B를 눌러 다시 표시하세요." + "CHANNELS_LIST_HIDDEN_HINT": "아래 버튼을 누르거나 ⌘/Ctrl+B를 눌러 다시 표시하세요.", + "HIDE_CATEGORIES": "카테고리 숨기기", + "SHOW_CATEGORIES": "카테고리 표시", + "SHOW_CATEGORIES_PANEL": "카테고리 패널 표시", + "CHOOSE_CATEGORY": "카테고리 선택", + "HIDE_PANELS": "카테고리 및 채널 숨기기", + "HIDE_PANELS_TOOLTIP": "패널 숨기기 (⌘/Ctrl+B)", + "SHOW_PANELS": "카테고리 및 채널 표시", + "SHOW_PANELS_TOOLTIP": "패널 표시 (⌘/Ctrl+B)" }, "CHANNELS": { "SHOW_PLAYING_CHANNEL": "재생 중인 채널 표시", diff --git a/apps/web/src/assets/i18n/nl.json b/apps/web/src/assets/i18n/nl.json index e50c3dda2..5dbcaa5c2 100644 --- a/apps/web/src/assets/i18n/nl.json +++ b/apps/web/src/assets/i18n/nl.json @@ -676,7 +676,15 @@ "SHOW_CHANNELS_LIST": "Zenderlijst tonen", "TOGGLE_SIDEBAR_TOOLTIP": "Zenderlijst aan-/uitzetten (⌘/Ctrl+B)", "CHANNELS_LIST_HIDDEN": "De zenderlijst is verborgen", - "CHANNELS_LIST_HIDDEN_HINT": "Toon de lijst opnieuw met de knop hieronder of druk op ⌘/Ctrl+B." + "CHANNELS_LIST_HIDDEN_HINT": "Toon de lijst opnieuw met de knop hieronder of druk op ⌘/Ctrl+B.", + "HIDE_CATEGORIES": "Categorieën verbergen", + "SHOW_CATEGORIES": "Categorieën tonen", + "SHOW_CATEGORIES_PANEL": "Categorieënpaneel tonen", + "CHOOSE_CATEGORY": "Categorie kiezen", + "HIDE_PANELS": "Categorieën en kanalen verbergen", + "HIDE_PANELS_TOOLTIP": "Panelen verbergen (⌘/Ctrl+B)", + "SHOW_PANELS": "Categorieën en kanalen tonen", + "SHOW_PANELS_TOOLTIP": "Panelen tonen (⌘/Ctrl+B)" }, "CHANNELS": { "SHOW_PLAYING_CHANNEL": "Huidige zender tonen", diff --git a/apps/web/src/assets/i18n/pl.json b/apps/web/src/assets/i18n/pl.json index ea7f39e10..23f1c5005 100644 --- a/apps/web/src/assets/i18n/pl.json +++ b/apps/web/src/assets/i18n/pl.json @@ -676,7 +676,15 @@ "SHOW_CHANNELS_LIST": "Pokaż listę kanałów", "TOGGLE_SIDEBAR_TOOLTIP": "Przełącz listę kanałów (⌘/Ctrl+B)", "CHANNELS_LIST_HIDDEN": "Lista kanałów jest ukryta", - "CHANNELS_LIST_HIDDEN_HINT": "Przywróć ją przyciskiem poniżej lub naciśnij ⌘/Ctrl+B." + "CHANNELS_LIST_HIDDEN_HINT": "Przywróć ją przyciskiem poniżej lub naciśnij ⌘/Ctrl+B.", + "HIDE_CATEGORIES": "Ukryj kategorie", + "SHOW_CATEGORIES": "Pokaż kategorie", + "SHOW_CATEGORIES_PANEL": "Pokaż panel kategorii", + "CHOOSE_CATEGORY": "Wybierz kategorię", + "HIDE_PANELS": "Ukryj kategorie i kanały", + "HIDE_PANELS_TOOLTIP": "Ukryj panele (⌘/Ctrl+B)", + "SHOW_PANELS": "Pokaż kategorie i kanały", + "SHOW_PANELS_TOOLTIP": "Pokaż panele (⌘/Ctrl+B)" }, "CHANNELS": { "SHOW_PLAYING_CHANNEL": "Pokaż odtwarzany kanał", diff --git a/apps/web/src/assets/i18n/pt.json b/apps/web/src/assets/i18n/pt.json index 59eebef7d..ec5a90c4a 100644 --- a/apps/web/src/assets/i18n/pt.json +++ b/apps/web/src/assets/i18n/pt.json @@ -676,7 +676,15 @@ "SHOW_CHANNELS_LIST": "Mostrar lista de canais", "TOGGLE_SIDEBAR_TOOLTIP": "Alternar lista de canais (⌘/Ctrl+B)", "CHANNELS_LIST_HIDDEN": "A lista de canais está oculta", - "CHANNELS_LIST_HIDDEN_HINT": "Volte a mostrá-la com o botão abaixo ou pressione ⌘/Ctrl+B." + "CHANNELS_LIST_HIDDEN_HINT": "Volte a mostrá-la com o botão abaixo ou pressione ⌘/Ctrl+B.", + "HIDE_CATEGORIES": "Ocultar categorias", + "SHOW_CATEGORIES": "Mostrar categorias", + "SHOW_CATEGORIES_PANEL": "Mostrar painel de categorias", + "CHOOSE_CATEGORY": "Escolher categoria", + "HIDE_PANELS": "Ocultar categorias e canais", + "HIDE_PANELS_TOOLTIP": "Ocultar painéis (⌘/Ctrl+B)", + "SHOW_PANELS": "Mostrar categorias e canais", + "SHOW_PANELS_TOOLTIP": "Mostrar painéis (⌘/Ctrl+B)" }, "CHANNELS": { "SHOW_PLAYING_CHANNEL": "Mostrar canal em reprodução", diff --git a/apps/web/src/assets/i18n/ru.json b/apps/web/src/assets/i18n/ru.json index 152862748..8bd781ad2 100644 --- a/apps/web/src/assets/i18n/ru.json +++ b/apps/web/src/assets/i18n/ru.json @@ -676,7 +676,15 @@ "SHOW_CHANNELS_LIST": "Показать список каналов", "TOGGLE_SIDEBAR_TOOLTIP": "Переключить список каналов (⌘/Ctrl+B)", "CHANNELS_LIST_HIDDEN": "Список каналов скрыт", - "CHANNELS_LIST_HIDDEN_HINT": "Верните его кнопкой ниже или нажмите ⌘/Ctrl+B." + "CHANNELS_LIST_HIDDEN_HINT": "Верните его кнопкой ниже или нажмите ⌘/Ctrl+B.", + "HIDE_CATEGORIES": "Скрыть категории", + "SHOW_CATEGORIES": "Показать категории", + "SHOW_CATEGORIES_PANEL": "Показать панель категорий", + "CHOOSE_CATEGORY": "Выбрать категорию", + "HIDE_PANELS": "Скрыть категории и каналы", + "HIDE_PANELS_TOOLTIP": "Скрыть панели (⌘/Ctrl+B)", + "SHOW_PANELS": "Показать категории и каналы", + "SHOW_PANELS_TOOLTIP": "Показать панели (⌘/Ctrl+B)" }, "CHANNELS": { "SHOW_PLAYING_CHANNEL": "Показать текущий канал", diff --git a/apps/web/src/assets/i18n/tr.json b/apps/web/src/assets/i18n/tr.json index a9df159cb..c7f6a8aee 100644 --- a/apps/web/src/assets/i18n/tr.json +++ b/apps/web/src/assets/i18n/tr.json @@ -676,7 +676,15 @@ "SHOW_CHANNELS_LIST": "Kanal listesini göster", "TOGGLE_SIDEBAR_TOOLTIP": "Kanal listesini aç/kapat (⌘/Ctrl+B)", "CHANNELS_LIST_HIDDEN": "Kanal listesi gizli", - "CHANNELS_LIST_HIDDEN_HINT": "Aşağıdaki düğmeyle geri getirin veya ⌘/Ctrl+B tuşlarına basın." + "CHANNELS_LIST_HIDDEN_HINT": "Aşağıdaki düğmeyle geri getirin veya ⌘/Ctrl+B tuşlarına basın.", + "HIDE_CATEGORIES": "Kategorileri gizle", + "SHOW_CATEGORIES": "Kategorileri göster", + "SHOW_CATEGORIES_PANEL": "Kategoriler panelini göster", + "CHOOSE_CATEGORY": "Kategori seç", + "HIDE_PANELS": "Kategorileri ve kanalları gizle", + "HIDE_PANELS_TOOLTIP": "Panelleri gizle (⌘/Ctrl+B)", + "SHOW_PANELS": "Kategorileri ve kanalları göster", + "SHOW_PANELS_TOOLTIP": "Panelleri göster (⌘/Ctrl+B)" }, "CHANNELS": { "SHOW_PLAYING_CHANNEL": "Oynatılan kanalı göster", diff --git a/apps/web/src/assets/i18n/zh.json b/apps/web/src/assets/i18n/zh.json index 9fc8fb6cd..26c86aefd 100644 --- a/apps/web/src/assets/i18n/zh.json +++ b/apps/web/src/assets/i18n/zh.json @@ -676,7 +676,15 @@ "SHOW_CHANNELS_LIST": "显示频道列表", "TOGGLE_SIDEBAR_TOOLTIP": "切换频道列表 (⌘/Ctrl+B)", "CHANNELS_LIST_HIDDEN": "频道列表已隐藏", - "CHANNELS_LIST_HIDDEN_HINT": "点击下方按钮或按 ⌘/Ctrl+B 重新显示。" + "CHANNELS_LIST_HIDDEN_HINT": "点击下方按钮或按 ⌘/Ctrl+B 重新显示。", + "HIDE_CATEGORIES": "隐藏分类", + "SHOW_CATEGORIES": "显示分类", + "SHOW_CATEGORIES_PANEL": "显示分类面板", + "CHOOSE_CATEGORY": "选择分类", + "HIDE_PANELS": "隐藏分类和频道", + "HIDE_PANELS_TOOLTIP": "隐藏面板 (⌘/Ctrl+B)", + "SHOW_PANELS": "显示分类和频道", + "SHOW_PANELS_TOOLTIP": "显示面板 (⌘/Ctrl+B)" }, "CHANNELS": { "SHOW_PLAYING_CHANNEL": "显示正在播放的频道", diff --git a/apps/web/src/assets/i18n/zhtw.json b/apps/web/src/assets/i18n/zhtw.json index 5d9f5931d..d2ebfcd78 100644 --- a/apps/web/src/assets/i18n/zhtw.json +++ b/apps/web/src/assets/i18n/zhtw.json @@ -676,7 +676,15 @@ "SHOW_CHANNELS_LIST": "顯示頻道列表", "TOGGLE_SIDEBAR_TOOLTIP": "切換頻道列表 (⌘/Ctrl+B)", "CHANNELS_LIST_HIDDEN": "頻道列表已隱藏", - "CHANNELS_LIST_HIDDEN_HINT": "點選下方按鈕或按 ⌘/Ctrl+B 重新顯示。" + "CHANNELS_LIST_HIDDEN_HINT": "點選下方按鈕或按 ⌘/Ctrl+B 重新顯示。", + "HIDE_CATEGORIES": "隱藏類別", + "SHOW_CATEGORIES": "顯示類別", + "SHOW_CATEGORIES_PANEL": "顯示類別面板", + "CHOOSE_CATEGORY": "選擇類別", + "HIDE_PANELS": "隱藏類別和頻道", + "HIDE_PANELS_TOOLTIP": "隱藏面板 (⌘/Ctrl+B)", + "SHOW_PANELS": "顯示類別和頻道", + "SHOW_PANELS_TOOLTIP": "顯示面板 (⌘/Ctrl+B)" }, "CHANNELS": { "SHOW_PLAYING_CHANNEL": "顯示正在播放的頻道", diff --git a/docs/architecture/iptvnator-ui-guidelines.md b/docs/architecture/iptvnator-ui-guidelines.md index 546ac4c91..5cea2d8d3 100644 --- a/docs/architecture/iptvnator-ui-guidelines.md +++ b/docs/architecture/iptvnator-ui-guidelines.md @@ -267,8 +267,24 @@ remain local when the meaning is explicit. ### Collapsible Live Sidebar -- M3U, Xtream, and Stalker live layouts share a sidebar collapse toggle that - hides the channels rail to give the player and EPG full width. +- Live-TV panels fold from the outside in, in three nested levels owned by + `LiveSidebarState` (`@iptvnator/portal/shared/util`): + 1. `expanded` — categories rail + channels rail + player. + 2. `categories-hidden` — channels rail + player. The shell's categories + rail (`WorkspaceShellContextSidebarComponent`, live sections only: + Xtream `live`, Stalker `itv`/`radio`) folds; the channels header turns + its category title into a dropdown that opens the same rail as a + popover, so switching categories stays one click away. + 3. `collapsed` — player + EPG only ("theater"). +- There is deliberately no "channels hidden, categories visible" state: a + category click has to bring the channels back anyway. Surfaces without a + categories rail (M3U, the unified-collection live tab) treat level 2 like + level 1 — and so does the live ROOT (no selected category, Xtream `/live` + "All Items", Stalker's all-items grid): there is no channels rail to host + the way back, so the shell folds the categories rail at level 2 only while + the portal store has a selected category (`hasLiveCategorySelection`), and + the rail's hide chevron is withheld there too. Level 3 folds it regardless, + since the floating restore handle lives in the content area. - Xtream Live TV's root view (`/live` with no selected category) follows the same paginated `All Items` shell as VOD and Series: a widget header with the total channel count, page-size controls, and page navigation above the shared @@ -278,26 +294,69 @@ remain local when the meaning is explicit. selects the channel's category, highlights the active category and channel, and scrolls the category rail plus virtual channels list to the selected rows when those rails are visible. -- In Xtream and Stalker live TV, the same toggle also collapses the workspace - shell context sidebar (the "Live Categories" rail rendered by - `WorkspaceShellContextSidebarComponent`), matching M3U's "everything quiets" - behaviour. The shell categories rail only collapses when the active section - is `live` (Xtream) or `itv`/`radio` (Stalker); movies, series, favorites, - and recent routes leave it untouched. +- Affordances, each in the panel it acts on: + - A `chevron_left` in the categories rail header + (`WorkspaceContextPanelComponent`, `presentation="sidebar"`, live + sections only) → level 2 (`hideCategories('portal')`). + - A `chevron_right` at the start of the channels header + (`data-test-id="live-show-categories"`) and the popover footer's + "Show categories panel" → level 1, through the shell's + `LiveCategoriesPopover.showCategoriesPanel()`: it sets + `showCategories('portal')` and, at phone widths where the rail is the + off-canvas context drawer whose open state the level does not drive, + also opens that drawer (`WorkspaceShellContextDrawerService.open()`). + - The category dropdown (`data-test-id="live-category-dropdown"`) opens + `LIVE_CATEGORIES_POPOVER` anchored below itself. The token lives in + `@iptvnator/portal/shared/util`; the workspace shell provides it + (`WorkspaceLiveCategoriesPopoverService`, CDK overlay hosting + `WorkspaceLiveCategoriesPopoverComponent`, which stamps the context + panel with `presentation="popover"`) and the live layouts reach it + through their `LivePanelsController` (`createLivePanelsController()` + in a field initializer: level flags, the popover bridge and the focus + handoff in one shared object, so the layout components carry none of + it; without a provider the header keeps its plain title). The stamped + panel opts out of the live-TV column keyboard contract + (`columnHandoff=false`: no `#portal-categories` id, no ArrowRight + handoff to `#live-channels`), since the dialog's focus trap would + bounce that handoff back inside and a second id would shadow the + folded rail's; the category sort preference is shared through + `PortalCategorySortStateService`, so a sort picked in the popover + survives into the restored rail. Backdrop, Escape, the footer, any + category selection (`categorySelected` output), any router + `NavigationStart` and any live-panel level change (`Cmd/Ctrl+B` + reaches the layout through the dialog) close it; focus returns to the + trigger. The popover host is a `role="dialog"` with `aria-modal` and a + `CdkTrapFocus` host directive that captures focus on open, matching + the trigger's `aria-haspopup="dialog"`. + - The `chevron_left` in the channels header → level 3 + (`collapse('portal')`). + - While collapsed, a floating `chevron_right` mini-fab at the left edge of + `.content-container`, the workspace header toggle and `Cmd/Ctrl+B` + (`toggle(surface)`) return to the level the user collapsed from, not + always to level 1. The shortcut handler ignores events that originate + inside ``, `