docs(ui): plan live panel toggle consistency

This commit is contained in:
4gray committed 2026-07-31 08:41:16 +02:00
1 parent f58c2ba0d0
commit 85a56115c8
1 file changed
+831
@@ -0,0 +1,831 @@
# Consistent Live Panel Toggles Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use
> `superpowers:executing-plans` to implement this plan task-by-task. Steps use
> checkbox (`- [ ]`) syntax for tracking.
**Goal:** Give Groups and Channels independent persisted intent and consistent,
accessible disclosure controls across every Live TV surface while preventing
Guide no-ops.
**Architecture:** A root-provided `LiveLayoutPanelStateService` in
`@iptvnator/portal/shared/data-access` owns persisted Groups/Channels intent and
non-persisted master suppression. Route components supply local applicability
and responsive suppression when resolving effective visibility; UI components
own DOM focus transfer and boundary restore controls. The existing Guide state
remains separate, while shared EPG views gain an explicit `collapsible`
capability.
**Tech Stack:** Angular 21 signals and standalone components, Angular CDK
breakpoints, Angular Material icon buttons, Jest, Nx, Playwright Electron E2E,
SCSS, ngx-translate.
---
## File Map
- `libs/portal/shared/data-access/src/lib/live-layout-panel-state.service.ts`
owns migration, persisted left-panel intent, master suppression, and
effective-state resolution.
- `libs/portal/shared/data-access/src/lib/live-layout-panel-state.service.spec.ts`
proves migration and state semantics.
- `libs/portal/shared/util/src/lib/live-layout-sidebar-state.service.ts` is
removed; `live-sidebar-state.ts` stays as a read-only legacy parser.
- `libs/ui/epg/src/lib/epg-{timeline,list-view}/` owns the Guide disclosure
capability, ARIA relationship, and 40px target.
- `libs/ui/components/src/lib/channel-list-container/` owns M3U Groups/Channels
header controls, retained hidden DOM, loading-state controls, restore rails,
and intra-component focus.
- `libs/workspace/shell/feature/src/lib/workspace-context-panel/` owns the
expanded Groups header control.
- `libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-context-sidebar/`
owns Groups effective visibility, responsive suppression, restore rail,
retained DOM, and focus transfer.
- `libs/portal/{xtream,stalker}/feature/src/lib/*live-stream-layout/` owns each
provider's Channels effective visibility, boundary restore rail, shortcut,
focus, and Guide capability wiring.
- `libs/playlist/m3u/feature-player/src/lib/video-player/` owns M3U panel
applicability, mobile suppression, shortcut, focus, and shared state wiring.
- `libs/portal/shared/ui/src/lib/components/unified-collection/` owns the sole
Favorites/Recent Channels header control and shortcut.
- `apps/web/src/assets/i18n/*.json` owns translated Groups/Channels disclosure
names and tooltips.
- `apps/electron-backend-e2e/src/live-panel-toggles.e2e.ts` owns cross-provider
regression coverage.
- `docs/architecture/iptvnator-ui-guidelines.md` becomes the canonical panel
ownership/responsive contract.
- `.changes/ui-live-panel-toggles.md` records the user-visible change.
### 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:
`libs/portal/shared/data-access/src/lib/live-layout-panel-state.service.spec.ts`
- Modify: `libs/portal/shared/data-access/src/index.ts`
- Modify: `libs/portal/shared/util/src/index.ts`
- Delete:
`libs/portal/shared/util/src/lib/live-layout-sidebar-state.service.ts`
- [ ] **Step 1: Write failing migration and service tests**
Cover missing/invalid storage, valid legacy seeding, partial new-key migration,
new-key precedence, idempotence, independent persistence, local action clearing
master suppression, and effective-state resolution:
```ts
expect(service.groupsIntent()).toBe('expanded');
expect(service.channelsIntent()).toBe('collapsed');
expect(localStorage.getItem(LIVE_GROUPS_PANEL_STATE_STORAGE_KEY)).toBe(
'expanded'
);
expect(service.isPanelExpanded('groups', { applicable: true })).toBe(true);
expect(
service.isPanelExpanded('groups', {
applicable: true,
responsiveSuppressed: true,
})
).toBe(false);
service.toggleMasterSuppression(['groups', 'channels']);
expect(service.masterSuppressed()).toBe(true);
service.showPanel('channels');
expect(service.masterSuppressed()).toBe(false);
expect(service.channelsIntent()).toBe('expanded');
```
- [ ] **Step 2: Run the target and verify RED**
Run:
```bash
pnpm nx test portal-shared-data-access --runInBand
```
Expected: FAIL because `LiveLayoutPanelStateService` and its storage constants
do not exist.
- [ ] **Step 3: Implement the minimal signal service**
Use a const-backed panel kind, readonly public signals, and explicit commands:
```ts
export const LIVE_LAYOUT_PANEL = {
GROUPS: 'groups',
CHANNELS: 'channels',
} as const;
export type LiveLayoutPanel =
(typeof LIVE_LAYOUT_PANEL)[keyof typeof LIVE_LAYOUT_PANEL];
export interface LivePanelEffectiveContext {
readonly applicable: boolean;
readonly responsiveSuppressed?: boolean;
}
@Injectable({ providedIn: 'root' })
export class LiveLayoutPanelStateService {
readonly groupsIntent = this._groupsIntent.asReadonly();
readonly channelsIntent = this._channelsIntent.asReadonly();
readonly masterSuppressed = this._masterSuppressed.asReadonly();
isPanelExpanded(
panel: LiveLayoutPanel,
context: LivePanelEffectiveContext
): boolean {
return (
context.applicable &&
!context.responsiveSuppressed &&
!this._masterSuppressed() &&
this.intentFor(panel)() === 'expanded'
);
}
hidePanel(panel: LiveLayoutPanel): void;
showPanel(panel: LiveLayoutPanel): void;
toggleMasterSuppression(applicablePanels: readonly LiveLayoutPanel[]): void;
}
```
Migration reads valid new keys first, falls back per missing key to valid
`live-sidebar-state`, defaults to expanded, writes both new keys, and never
writes the legacy key.
- [ ] **Step 4: Replace exports and remove the legacy service**
Export the new service from `@iptvnator/portal/shared/data-access`. Remove only
the injectable legacy service export/file; retain
`restoreLiveSidebarState()` for migration compatibility.
- [ ] **Step 5: Run the target and verify GREEN**
Run:
```bash
pnpm nx test portal-shared-data-access --runInBand
pnpm nx lint portal-shared-data-access
pnpm nx lint portal-shared-util
```
Expected: all commands exit 0.
- [ ] **Step 6: Commit**
```bash
git add libs/portal/shared/data-access libs/portal/shared/util
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**
For both timeline and list, assert that `collapsible=false` renders a static
heading with no disclosure button, while `collapsible=true` exposes a 40px
button with `aria-controls`, `aria-expanded`, and action-oriented translated
label:
```ts
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();
fixture.componentRef.setInput('collapsible', true);
await fixture.whenStable();
const toggle = fixture.nativeElement.querySelector(
'[data-testid="live-guide-toggle"]'
);
expect(toggle.getAttribute('aria-controls')).toBe('live-guide-panel');
expect(toggle.getAttribute('aria-expanded')).toBe('true');
```
- [ ] **Step 2: Run both targets and verify RED**
```bash
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**
Add:
```ts
readonly collapsible = input(true);
readonly panelId = input<string | null>(null);
```
Render the button only when collapsible, otherwise render the same heading
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**
```bash
pnpm nx test ui-epg --runInBand
pnpm nx lint ui-epg
```
Expected: both commands exit 0.
- [ ] **Step 5: Commit**
```bash
git add libs/ui/epg
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:
`libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.{ts,html,scss,spec.ts}`
- Modify:
`libs/ui/components/src/lib/channel-list-container/channel-list-container.component.{ts,html,spec.ts}`
- Modify:
`libs/ui/components/src/lib/channel-list-loading-state/channel-list-loading-state.component.{ts,html,scss,spec.ts}`
- [ ] **Step 1: Write failing tests for panel-specific outputs**
Assert that All Channels emits `channelsPanelExpandedChange(false)` from its
header. Assert that Groups has separate Groups and Channels buttons/restore
buttons, stable panel IDs/test IDs, no duplicates, retained `inert` hidden DOM,
and bidirectional focus:
```ts
expect(groupsHide.getAttribute('aria-controls')).toBe('live-groups-panel');
expect(channelsHide.getAttribute('aria-controls')).toBe(
'live-channels-panel'
);
groupsHide.click();
expect(groupsPanelExpandedChange).toHaveBeenCalledWith(false);
fixture.componentRef.setInput('groupsPanelExpanded', false);
await fixture.whenStable();
expect(groupsPanel.hasAttribute('inert')).toBe(true);
expect(document.activeElement).toBe(groupsRestore);
```
The loading-state spec must prove both controls remain in the Groups skeleton
and Channels remains in the All Channels skeleton.
- [ ] **Step 2: Run Components tests and verify RED**
```bash
pnpm nx test components --runInBand
```
Expected: FAIL because panel-specific inputs/outputs and controls do not exist.
- [ ] **Step 3: Implement the All Channels and loading contracts**
Replace ambiguous `sidebarToggleRequested` with:
```ts
readonly channelsPanelExpandedChange = output<boolean>();
readonly channelsPanelExpanded = input(true);
```
Use `live-channels-panel-hide` and `live-channels-panel-restore` test IDs,
`aria-controls="live-channels-panel"`, `aria-expanded`, translated names, and
40px targets. Loading-state controls emit the same explicit requested value.
- [ ] **Step 4: Implement independent Groups layout**
Give `GroupsViewComponent`:
```ts
readonly groupsPanelExpanded = input(true);
readonly channelsPanelExpanded = input(true);
readonly groupsPanelExpandedChange = output<boolean>();
readonly channelsPanelExpandedChange = output<boolean>();
```
Keep both panels mounted. Apply `inert` and `aria-hidden` to collapsed panels,
render compact boundary restore rails in Groups-then-Channels order, reclaim
the hidden width, and focus the counterpart control after a zero-delay render
turn. The Groups toggle is the final action after search/sort/manage; the
Channels toggle remains the final action after channel sort.
- [ ] **Step 5: Thread the contract through `ChannelListContainerComponent`**
Add matching inputs and outputs and pass them through the All/Groups/loading
branches. Do not render panel actions for legacy M3U `favorites`/`recent`
branches.
- [ ] **Step 6: Run and verify GREEN**
```bash
pnpm nx test components --runInBand
pnpm nx lint components
```
Expected: both commands exit 0.
- [ ] **Step 7: Commit**
```bash
git add libs/ui/components
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:
`libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-context-sidebar/workspace-shell-context-sidebar.component.{ts,html,scss,spec.ts}`
- [ ] **Step 1: Write failing header and layout tests**
Assert the Groups header control is last, stays present in loading/empty/zero
search results, and emits an explicit collapse request only on Live/ITV/Radio.
Assert the context sidebar suppresses Groups at `max-width: 1023px` without
changing intent, retains collapsed DOM as inert, renders one boundary restore
button at wide widths, and transfers focus.
- [ ] **Step 2: Run and verify RED**
```bash
pnpm nx test workspace-shell-feature --runInBand
```
Expected: FAIL because the Groups disclosure contract does not exist.
- [ ] **Step 3: Add the Groups header output**
Add:
```ts
readonly groupsPanelExpandedChange = output<boolean>();
readonly showLiveGroupsToggle = computed(
() => this.isXtreamCategories() && this.section() === 'live'
|| this.isStalkerCategories()
&& (this.section() === 'itv' || this.section() === 'radio')
);
```
Render the translated 40px button last in `.context-header__actions`, with
panel-based test ID and ARIA attributes.
- [ ] **Step 4: Add state, responsive suppression, restore rail, and focus**
Inject `LiveLayoutPanelStateService` from data-access and `BreakpointObserver`.
Resolve effective Groups visibility from intent, master suppression,
applicability, and `(max-width: 1023px)`. Keep the `aside` mounted with
`inert`/`aria-hidden`, reserve a 40px restore column only when restoration is
currently fulfillable, and focus `live-groups-panel-hide` after restoration.
- [ ] **Step 5: Run and verify GREEN**
```bash
pnpm nx test workspace-shell-feature --runInBand
pnpm nx lint workspace-shell-feature
```
Expected: both commands exit 0.
- [ ] **Step 6: Commit**
```bash
git add libs/workspace/shell/feature
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:
`libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.{ts,html,scss,spec.ts}`
- Modify: `libs/ui/styles/_portal-layout.scss`
- [ ] **Step 1: Write failing provider component tests**
For each provider prove:
- Channels collapse does not change Groups intent.
- Groups collapse does not hide Channels.
- the expanded and restore controls have correct names, test IDs, ARIA, and
40px target;
- the collapsed Channels DOM is inert/aria-hidden and focus moves both ways;
- `Cmd/Ctrl+B` suppresses applicable Groups+Channels without persisting intent;
- root All Items applies the shortcut only to Groups;
- external MPV/VLC passes `collapsible=false` to Guide.
- [ ] **Step 2: Run both targets and verify RED**
```bash
pnpm nx test portal-xtream-feature --runInBand
pnpm nx test portal-stalker-feature --runInBand
```
Expected: FAIL because the components still consume the shared sidebar state.
- [ ] **Step 3: Replace Xtream state and markup**
Inject `LiveLayoutPanelStateService`, compute Channels effective visibility for
`showLiveChannelSidebar()`, use `hidePanel('channels')` /
`showPanel('channels')`, and call:
```ts
this.livePanelState.toggleMasterSuppression(
this.showLiveChannelSidebar()
? ['groups', 'channels']
: ['groups']
);
```
Keep the resizable Channels DOM mounted and inert when collapsed. Move restore
UI into a non-overlay boundary rail before `.content-container`. Pass
`[collapsible]="isEmbeddedPlayer"` and `panelId="live-guide-panel"`.
- [ ] **Step 4: Apply the same contract to Stalker**
Use the same panel IDs, test IDs, focus behavior, and master shortcut. Radio
must render no Guide disclosure because it renders no Guide region.
- [ ] **Step 5: Normalize shared boundary rail SCSS**
Replace the floating restore overlay in `_portal-layout.scss` with a 40px
flex-shrink-zero boundary rail. Keep width transitions at 180ms, never modify
the external-player content sizing, and do not touch channel-row styles.
- [ ] **Step 6: Run and verify GREEN**
```bash
pnpm nx test portal-xtream-feature --runInBand
pnpm nx test portal-stalker-feature --runInBand
pnpm nx lint portal-xtream-feature
pnpm nx lint portal-stalker-feature
```
Expected: all commands exit 0.
- [ ] **Step 7: Commit**
```bash
git add libs/portal/xtream/feature libs/portal/stalker/feature libs/ui/styles
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}`
- [ ] **Step 1: Write failing M3U feature tests**
Prove All Channels uses the shared Channels intent, Groups passes two
independent effective inputs, mobile responsive suppression does not persist
Groups intent, both collapsed controls order Groups before Channels, loading
retains structural controls, `Cmd/Ctrl+B` preserves intents, and mobile keeps a
Channels restore button.
- [ ] **Step 2: Run and verify RED**
```bash
pnpm nx test playlist-m3u-feature-player --runInBand
```
Expected: FAIL because M3U still owns a local legacy-key signal.
- [ ] **Step 3: Replace local state with the shared service**
Remove `restoreLiveSidebarState`/`persistLiveSidebarState` usage. Inject
`LiveLayoutPanelStateService`, observe `(max-width: 599px)`, and compute
applicability:
```ts
readonly groupsApplicable = computed(() => this.activeView() === 'groups');
readonly channelsApplicable = computed(
() => this.activeView() === 'groups' || this.activeView() === 'all'
);
```
Use responsive suppression only for Groups. Pass explicit panel inputs/outputs
through `app-sidebar` and the loading state.
- [ ] **Step 4: Preserve focus and mobile restore geometry**
For All Channels, hide the resizable panel with inert/aria-hidden and place the
restore rail between sidebar and content. For Groups, let the shared
GroupsView own its two internal rails. On mobile, keep the Channels restore
rail visible and let content reclaim height when Channels is collapsed.
- [ ] **Step 5: Wire Guide capability**
Pass `collapsible=shouldShowInlinePlayer(activeChannel)` and
`panelId="live-guide-panel"` to both EPG modes. Radio continues to omit the
Guide region.
- [ ] **Step 6: Run and verify GREEN**
```bash
pnpm nx test playlist-m3u-feature-player --runInBand
pnpm nx lint playlist-m3u-feature-player
```
Expected: both commands exit 0.
- [ ] **Step 7: Commit**
```bash
git add libs/playlist/m3u/feature-player
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:
`libs/portal/shared/ui/src/lib/components/unified-collection/unified-live-tab.component.{ts,html,scss,spec.ts}`
- [ ] **Step 1: Write failing Favorites/Recent tests**
Assert exactly one panel-based Channels control in the collection header for
both modes, including zero search results; no control inside the list; loading
Live state retains the header control; collapsed list DOM is inert/aria-hidden;
`Cmd/Ctrl+B` only suppresses Channels; external/radio Guide disclosure is
absent.
- [ ] **Step 2: Run and verify RED**
```bash
pnpm nx test portal-shared-ui --runInBand
```
Expected: FAIL because collection UI still uses the legacy service and
`aria-pressed`.
- [ ] **Step 3: Implement the collection exception**
Inject `LiveLayoutPanelStateService`, keep the one header button, bind
`aria-controls="live-channels-panel"` and effective `aria-expanded`, remove
`aria-pressed`, and use explicit show/hide commands. Render the same real
control in the loading header when the selected type is Live. Add the
non-typing `Cmd/Ctrl+B` host listener.
- [ ] **Step 4: Retain and gate the Live list DOM**
Bind `id="live-channels-panel"`, `inert`, and `aria-hidden` on the live-tab
sidebar. Pass Guide `collapsible=shouldUseInlinePlayer()` and omit it for radio
as today.
- [ ] **Step 5: Run and verify GREEN**
```bash
pnpm nx test portal-shared-ui --runInBand
pnpm nx lint portal-shared-ui
```
Expected: both commands exit 0.
- [ ] **Step 6: Commit**
```bash
git add libs/portal/shared/ui
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**
Replace the ambiguous tooltip contract with:
```json
"LAYOUT": {
"HIDE_GROUPS": "Hide groups",
"SHOW_GROUPS": "Show groups",
"HIDE_CHANNELS": "Hide channels",
"SHOW_CHANNELS": "Show channels",
"GROUPS_TOGGLE_TOOLTIP": "Hide or show groups",
"CHANNELS_TOGGLE_TOOLTIP": "Hide or show channels (⌘/Ctrl+B)"
}
```
- [ ] **Step 2: Add equivalent short translations to every locale**
Preserve all existing locale terminology and key order. Remove old layout keys
only after no source file references them.
- [ ] **Step 3: Verify key parity and usage**
```bash
pnpm run i18n:check
rg -n "HIDE_CHANNELS_LIST|SHOW_CHANNELS_LIST|TOGGLE_SIDEBAR_TOOLTIP" apps libs
```
Expected: i18n check exits 0 and `rg` finds no runtime references.
- [ ] **Step 4: Commit**
```bash
git add apps/web/src/assets/i18n
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`
- [ ] **Step 1: Write the M3U Electron flow**
Import the mock M3U fixture, enter Groups, independently hide/restore Groups and
Channels, verify focus/ARIA/inert/width reclamation, exercise `Control+B`, seed
legacy storage before reload, and check the mobile Channels restore path at a
narrow viewport.
- [ ] **Step 2: Run the M3U test and verify RED if a gap remains**
```bash
pnpm nx run electron-backend-e2e:e2e-ci--src/live-panel-toggles.e2e.ts \
--grep "@live-panels @m3u"
```
Expected after implementation: PASS. Any failure must identify a functional or
layout gap before adding provider flows.
- [ ] **Step 3: Add Xtream and Stalker flows**
Use the existing mock-server add helpers. Verify root Groups-only placement,
category Groups+Channels independence, persistence across navigation,
Groups-then-Channels restore ordering, zero-search-result controls, and no
Guide toggle for external player/radio.
- [ ] **Step 4: Run the full atomized target**
```bash
pnpm nx run electron-backend-e2e:e2e-ci--src/live-panel-toggles.e2e.ts
```
Expected: all `@live-panels` tests pass.
- [ ] **Step 5: Lint and commit**
```bash
pnpm nx lint electron-backend-e2e
git add apps/electron-backend-e2e/src/live-panel-toggles.e2e.ts
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`
- [ ] **Step 1: Replace the obsolete shared-sidebar documentation**
Document independent Groups/Channels storage keys, data-access ownership,
intent versus effective state, boundary controls, master suppression,
responsive Groups-first suppression, collection-header exception, and Guide
capability. Preserve the channel-row responsive contract from #1312 verbatim.
- [ ] **Step 2: Update `CLAUDE.md` where it describes live sidebar state**
Mirror the canonical names, service location, and shortcut semantics without
adding the future content-aware right-region behavior.
- [ ] **Step 3: Add the user-facing release note**
```md
---
type: fix
area: ui
issues: [1118]
---
Live TV now remembers Groups and Channels visibility independently, keeps
reliable restore controls across providers and screen sizes, and hides Guide
toggles when they cannot work.
```
- [ ] **Step 4: Validate documentation and release note**
```bash
git diff --check
pnpm run release:notes:validate
```
Expected: both commands exit 0.
- [ ] **Step 5: Commit**
```bash
git add docs/architecture/iptvnator-ui-guidelines.md CLAUDE.md \
.changes/ui-live-panel-toggles.md
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**
```bash
pnpm nx run-many \
-t test \
-p portal-shared-data-access,ui-epg,components,workspace-shell-feature,portal-xtream-feature,portal-stalker-feature,playlist-m3u-feature-player,portal-shared-ui \
--parallel=3
```
Expected: all affected unit targets pass.
- [ ] **Step 2: Run affected lint**
```bash
pnpm nx run-many \
-t lint \
-p portal-shared-data-access,portal-shared-util,ui-epg,components,workspace-shell-feature,portal-xtream-feature,portal-stalker-feature,playlist-m3u-feature-player,portal-shared-ui,electron-backend-e2e \
--parallel=3
```
Expected: all affected lint targets pass.
- [ ] **Step 3: Run build and atomized E2E**
```bash
pnpm nx build web
pnpm nx run electron-backend-e2e:e2e-ci--src/live-panel-toggles.e2e.ts
```
Expected: build and all panel E2E tests pass.
- [ ] **Step 4: Run repository policy checks**
```bash
pnpm run i18n:check
pnpm run release:notes:validate
pnpm run skills:validate
git diff --check origin/master...HEAD
```
Expected: all checks exit 0.
- [ ] **Step 5: Perform running Electron visual/accessibility verification**
Start `pnpm run serve:backend`, connect through the required Computer Use or
Electron CDP workflow, and inspect M3U, Xtream, Stalker, Favorites, Recent,
desktop, narrow, dark, and light states. Record exact target sizes, focus
movement, absence of duplicate/no-op controls, reclaimed widths, and restore
ordering.
- [ ] **Step 6: Review the complete diff against the design**
```bash
git status --short
git diff --stat origin/master...HEAD
git diff --check origin/master...HEAD
git log --oneline origin/master..HEAD
```
Re-read every requirement in the design and verify a code path plus test
covers it. Confirm no channel-row responsive selector and no external-player
right-region sizing rule changed.
- [ ] **Step 7: Fix review findings with test-first cycles**
For every finding, add or tighten the closest failing test, run it RED, make the
smallest correction, rerun GREEN, and commit with an appropriate conventional
message.
- [ ] **Step 8: Push and create the PR without merging**
```bash
git push -u origin agent/live-panel-toggle-consistency-2
gh pr create \
--title "fix(ui): make live panel toggles consistent" \
--body-file /tmp/live-panel-toggle-pr.md
```
The PR body must summarize state ownership, UX/accessibility behavior, testing,
visual verification, and explicitly state that content-aware external-player
layout and #1312 channel-row behavior are unchanged. Use `Refs #1118`, not
`Closes #1118`, because a third PR remains.