From 7ede0016f945dc396ead08be142d5b5d4f14d3ea Mon Sep 17 00:00:00 2001 From: 4gray Date: Thu, 30 Jul 2026 00:32:51 +0200 Subject: [PATCH] docs(ui): define live panel toggle consistency --- ...30-live-panel-toggle-consistency-design.md | 278 ++++++++++++++++++ 1 file changed, 278 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-30-live-panel-toggle-consistency-design.md diff --git a/docs/superpowers/specs/2026-07-30-live-panel-toggle-consistency-design.md b/docs/superpowers/specs/2026-07-30-live-panel-toggle-consistency-design.md new file mode 100644 index 000000000..02fd037a8 --- /dev/null +++ b/docs/superpowers/specs/2026-07-30-live-panel-toggle-consistency-design.md @@ -0,0 +1,278 @@ +# Consistent Live Panel Toggles + +## Context + +IPTVnator's Live TV surfaces can contain three independently useful regions: +Groups or Categories, Channels, and Guide. Their current collapse controls do +not consistently describe or control those regions: + +- Xtream and Stalker use one persisted `live-sidebar-state` value for both the + Groups and Channels regions. The Channels-header action therefore hides two + panels even though its location and label imply one. +- The workspace shell also consumes that state for the Groups region, so a + single action crosses component and layout ownership boundaries. +- M3U keeps a separate local signal but writes the same storage key. Views can + disagree in memory while overwriting one another's next-session preference. +- M3U Groups exposes the action from the selected Channels header, but the + action hides Groups as well. Some loading, empty, and mobile states have no + usable restore path. +- Favorites and Recent already use a stable collection-header action because + their Channels panel has no separate Groups sibling. +- Guide uses a leading disclosure in its own toolbar, which matches the panel's + structure. External-player layouts can still expose that action even when + collapsing Guide is ineffective. +- Width-zero panels can leave focusable descendants in the tab order, and + hidden-panel actions do not consistently transfer focus to their restore + controls. + +This is the second increment of issue #1118. It makes panel ownership explicit +without changing how the external-player region consumes space. Content-aware +right-region behavior remains a separate PR. + +## Product Decision + +Use a hybrid placement rule: + +1. A panel owns its toggle by default. +2. In an expanded panel, the toggle sits in that panel's header or toolbar. +3. When a panel is collapsed, exactly one restore control remains at its former + boundary. +4. Favorites and Recent deliberately keep their current collection-header + Channels toggle because that header is the stable owner for the single list + region. Do not add a duplicate control inside the list. +5. Guide keeps its current leading disclosure because it is a vertical + expand/collapse affordance rather than another left-sidebar action. + +This preserves spatial predictability without forcing unlike views into a +global toolbar. A user should be able to infer what will disappear from the +control's location, icon, accessible name, and tooltip. + +## Panel Model + +Treat Groups, Channels, and Guide as separate persisted user intents: + +| Intent | New storage key | Default | +| -------- | ------------------------------- | ---------- | +| Groups | `live-groups-panel-state` | `expanded` | +| Channels | `live-channels-panel-state` | `expanded` | +| Guide | Existing `live-epg-panel-state` | `expanded` | + +A root-provided `LiveLayoutPanelStateService` owns the two left-panel signals +and their persistence. M3U, the workspace shell, Xtream, Stalker, Favorites, +and Recent consume the same live service instead of maintaining local copies. +Guide may continue using its existing state helper in this increment; its +control-availability rules must still be consistent across consumers. + +The service distinguishes: + +- **Intent state:** the user's persisted choice for a panel. +- **Effective state:** whether the current route, content, viewport, or + playback mode can and should render that panel now. + +Route absence, responsive auto-collapse, radio mode, and player capability must +never overwrite intent. Returning to a compatible layout restores the user's +choice. + +### Legacy migration + +On the first read of the new left-panel keys: + +1. If a new key is valid, keep it. +2. Otherwise, if legacy `live-sidebar-state` is valid, seed the missing Groups + and Channels intents from it. +3. Otherwise, use `expanded`. +4. Persist both resolved new values so migration is idempotent. +5. Keep the legacy key read-only for compatibility during this PR. New actions + never write it. + +This preserves an existing user's compact or expanded layout while making +future changes independent. + +## Placement and Coverage + +Expanded left-panel toggles use the same compact icon-button treatment and are +the final trailing action in their header: + +- **Groups/Categories:** after search, sort, or manage actions. +- **Channels:** after sort, refresh, or other channel-list actions. +- **Guide:** keep the leading disclosure in the Guide toolbar. + +The following matrix defines which controls exist, including loading, empty, +and zero-search-result states: + +| Surface | Groups | Channels | Guide | +| ------------------------------------------------ | :-----------: | :-----------: | :---: | +| Portal root / All Items | Yes | No | No | +| Xtream or Stalker Live category | Yes | Yes | Yes¹ | +| M3U All Channels | No | Yes | Yes¹ | +| M3U Groups | Yes | Yes | Yes¹ | +| Unified Favorites or Recent, Live tab | No | Yes² | Yes¹ | +| Movie, series, search result, or detail surfaces | No | No | No | +| Radio playback | As applicable | As applicable | No | + +¹ Show Guide disclosure only when Guide is present and the current playback +host can actually collapse it. An external MPV/VLC host must not expose a +no-op disclosure. + +² Keep the existing collection-header action as the sole Channels toggle. +Playlist-scoped and global Favorites/Recent all use this unified collection +surface; legacy M3U `favorites`/`recent` view values do not define a second +placement. + +“Portal root / All Items” means the root catalog grid before a category is +selected. It has the workspace Groups/Categories rail but no independent +Channels rail, even though selecting an item can enter the categorized live +layout. + +Loading, empty, and zero-search-result states retain the controls for any +Groups or Channels panel structurally present in that surface. Guide is the +exception: no Guide control is rendered until the Guide region itself exists +for a selected playable item. + +When both Groups and Channels are collapsed, boundary restore controls appear +in stable left-to-right order: Groups, then Channels. A collapsed panel retains +no second hidden or duplicate action. + +## Control Semantics + +Each panel has a distinct visual and accessible identity: + +- Groups: category/group-list icon; “Hide groups” / “Show groups”. +- Channels: channel/list icon; “Hide channels” / “Show channels”. +- Guide: existing vertical disclosure; “Collapse guide” / “Expand guide”. + +Expanded and restore controls: + +- reference the owned panel with `aria-controls`; +- report the effective visibility with `aria-expanded`; +- have a translated, action-oriented accessible name and tooltip; +- expose at least a `40px` interactive target; +- retain a stable test identifier based on the panel, not the provider; +- do not use `aria-pressed`, because these are disclosure actions rather than + toggle buttons representing an on/off setting. + +When an action hides the panel containing keyboard focus, focus moves to that +panel's restore control after the layout settles. Restoring moves focus to the +expanded panel's header toggle. Hidden panels are removed from interaction +with `inert` and `aria-hidden="true"` while their collapsed DOM is retained. + +The resize handle is not a replacement for a disclosure action and is not +changed in this PR. + +## Keyboard and Responsive Behavior + +`Cmd/Ctrl+B` remains a master left-panel visibility command on Live surfaces. +It changes effective visibility without destroying the independent Groups and +Channels intents: + +- If any applicable left panel is effectively visible, the command + temporarily suppresses all applicable left panels. +- Invoking it again restores each panel to its previously persisted intent. +- A panel-local action while master suppression is active first exits + suppression, then applies the requested panel intent. +- Views with only Channels apply the command to Channels. Views with only + Groups apply it to Groups. + +Guide receives no new shortcut in this increment. + +On narrow layouts, automatic pressure relief hides Groups before Channels. +Responsive suppression is effective-only and does not persist. The former +boundary still offers a pointer and keyboard restore path whenever the layout +has enough room to honor it. In particular, M3U mobile must no longer hide the +only Channels restore control. + +## Visual Behavior + +The controls should read as structural chrome rather than primary actions: + +- use the existing panel-header neutral icon-button style; +- keep spacing aligned with neighboring header actions; +- avoid a new global toolbar, floating pill, or persistent overlay; +- avoid edge chevrons as the sole expanded-state action; +- reserve layout space only for restore controls that are currently needed. + +Collapsing one left panel must allow its adjacent content to reclaim the freed +width. Collapsing Groups must not implicitly collapse Channels, and collapsing +Channels must not change Groups. This PR does not redistribute the right-side +external-player region. + +## Implementation Boundaries + +The implementation should: + +1. Replace `LiveLayoutSidebarStateService` with the explicit left-panel state + service and migration. +2. Move M3U from its local same-key signal to that shared service. +3. Add the missing Groups-header control in the workspace shell and the + independent Channels controls in applicable live layouts. +4. Preserve the collection-header exception for Favorites and Recent. +5. Normalize collapsed restore controls, focus transfer, `inert`, and + accessible disclosure semantics. +6. Hide Guide disclosure when the current player host cannot collapse Guide. +7. Keep controls present through loading, empty, and search-zero states. + +The implementation must not: + +- change the channel-row responsive EPG behavior from PR1; +- make the external-player right region content-aware; +- add Guide/search discovery or a global Guide route; +- redesign movie, series, or detail layouts; +- make resize separators keyboard-operable as part of this scope. + +## Testing + +### Unit and component coverage + +- Test valid, invalid, partial, and idempotent legacy-state migration. +- Test independent Groups and Channels persistence and master suppression. +- Test each applicable surface's expanded control, collapsed restore control, + stable accessible name, `aria-controls`, and `aria-expanded`. +- Test focus transfer in both directions and ensure collapsed descendants + cannot remain keyboard-focusable. +- Test loading, empty, and search-zero states. +- Test that M3U shares the service and keeps a mobile restore path. +- Test that external MPV/VLC and radio layouts do not expose a no-op Guide + disclosure. +- Test that Favorites and Recent render exactly one Channels control in their + collection header. + +### Electron E2E and manual verification + +Add an atomized Electron flow for panel toggles using mock M3U, Xtream, and +Stalker data. Verify independent hide/restore behavior, persistence across +navigation, the legacy migration fixture, `Cmd/Ctrl+B`, keyboard focus, and the +view matrix above. + +Use `agent-browser` over Electron CDP to inspect representative desktop and +narrow/mobile layouts. Verify: + +- header alignment and target size; +- no duplicate or missing controls; +- reclaimed content width after each independent collapse; +- Groups-then-Channels restore ordering; +- no clipping, overlap, dead space introduced on the left, or invisible tab + stops; +- no Guide disclosure for external-player and radio modes. + +Run affected unit, lint, build, Electron E2E, and release-note validation +targets before completion. + +## Documentation and Release Notes + +Update the canonical UI guideline with the panel ownership, placement, and +responsive rules. Update any architecture documentation that still describes +one shared sidebar state. Add one user-facing release note covering independent +and consistent Live panel controls. + +## Follow-up + +The next PR should make the right region content-aware. With an external +player: + +- keep Guide when useful EPG content exists; +- remove the empty player/Guide region when no useful content exists; +- let Channels reclaim a bounded amount of the freed width rather than + stretching unbounded across the window. + +That decision requires its own layout prototypes and playback-mode matrix; it +is intentionally not hidden inside this state and accessibility refactor.