Merge remote-tracking branch 'origin/master' into claude/iptvnator-vod-hero-redesign-2c8950

# Conflicts:
#	apps/electron-backend-e2e/src/detail-back-lane.e2e.ts
#	apps/web-e2e/src/m3u-movie-details.e2e.ts
#	docs/architecture/iptvnator-ui-guidelines.md
#	libs/ui/components/src/lib/content-hero/content-hero.component.scss
#	libs/ui/components/src/lib/portal-detail-shell/portal-detail-shell.component.scss
This commit is contained in:
4gray committed 2026-10-03 11:25:57 +02:00
commit 87d7e1d46c
122 files changed
+2080 -1040

No files matched your search

@@ -162,8 +162,9 @@ Contracts:
player subtree, so shell state changes cannot recreate the `<video>`.
- **External MPV/VLC sessions do not flip the layout to watch** — browse
layout stays, and the primary CTA keeps its "Stop <player>" behavior.
- The shell's sticky arrow emits the host-owned `backClicked` in browse and
watch alike (unless `backAvailable=false`, when it is not rendered at all):
- The shell's Back, rendered by the workspace header, emits the host-owned
`backClicked` in browse and watch alike (unless `backAvailable=false`, when
the shell registers none):
hosts wire it to their route-level `goBack()`, straight back to the list —
everything browse offers is also visible in watch, so a two-step unwind
would be ceremony. Escape alone unwinds one level: in watch it emits
@@ -174,7 +175,7 @@ Contracts:
[Portal Detail Navigation](./portal-detail-navigation.md).
- The now-playing bar has one exit of its own: the "Close player" button
emits `closed` and returns to browse without navigating. It carries no back
arrow — a second arrow beside the sticky one, with a different meaning,
arrow — a second arrow beside the header's, with a different meaning,
was the duplicate this contract removes.
- Entering watch scrolls the shell to the top; leaving keeps the scroll
position.
@@ -725,6 +725,27 @@ Settings use the same system but are flatter than content-heavy views.
`EpgProgrammeDialogService` opens the programme dialog at 540px from the
timeline, list, guide and channel rows, with a panel class that scopes its
surface overrides.
- **Destructive actions.** Material only emits `warn` button colors for M2
themes, so the `color` input is a no-op here. A button that removes or
discards user data uses the global `.app-destructive-button` class from
`m3-theme.scss` (error/on-error tokens per theme, for filled, text,
outlined and icon buttons), as the unsaved-changes dialog's Discard does.
Confirmations go through `DialogService.openConfirmDialog` with a
translated verb as the required `confirmLabel` ("Remove playlist",
"Clear") and `tone: 'destructive'` when the action loses data; the dismiss
defaults to "Cancel". Never confirm with "Yes"/"No". When the verb itself
is "Cancel …", pass `cancelLabel` "Close" so the two buttons do not read
alike. `theme-tokens.e2e.ts` checks the label and the error fill in both
themes.
## Source Type Icons
`SOURCE_TYPE_ICONS` in `@iptvnator/shared/interfaces` is the only source of
provider icons: Xtream `cloud`, Stalker `cast`, the M3U family
`playlist_play`, and per playlist `link` (URL), `description` (local file or
text) and `subject` (pasted text in the add flow). Use
`getPlaylistSourceIcon()` for a stored playlist. An icon never stands for two
providers, and the Dashboard rail icon is never a provider icon.
## Phone Layout
+28 -29
View File
@@ -23,30 +23,29 @@ do not reach global player shortcuts. Descendant controls retain their native
keys and Tab order. Entering watch still scrolls to the top; Back and saved
catalog scroll positions retain the existing navigation contract below.
The shell owns a single sticky Back control, outside the collapsing hero. Its
zero-height wrapper is a direct child of the scroll owner, so the control stays
16 px from the top throughout long episode lists without shifting the hero.
The button has an opaque app-themed surface, visible keyboard focus, an Escape
shortcut hint via native `title` and Electron `no-drag` hit testing. The hint
does not create an overlay that could consume the first Escape press.
The shell owns the page's Back action, but the workspace header renders it.
While `backAvailable()` is true, the shell registers a target with
`WorkspaceBackNavigationService` (`@iptvnator/portal/shared/data-access`). The
target, the `WorkspaceBackTarget` contract in `@iptvnator/portal/shared/util`,
carries a label (the host's `backLabel`, else the translated "Back"), whether
Escape currently runs it, and `run()`, which emits `backClicked`. The service
keeps a stack in which the newest registration wins, and each release removes
only its own target. A loading shell replaced by the loaded one therefore
cannot clear its successor, whichever is destroyed first. The header shows the target as an `arrow_back`
icon button in its leading slot (`data-test-id="workspace-header-back"`), to
the right of the macOS traffic lights. That is where desktop apps and Material's
top app bar keep navigation. The header never scrolls, so the control stays
visible over long episode lists, and nothing floats over the scroll owner:
detail columns keep symmetric insets and their full width. The button has
Electron `no-drag` hit testing. In browse its tooltip and `aria-keyshortcuts`
advertise Escape, and an Escape pressed on the focused button runs Back
itself, because the shell's browse Escape requires focus inside the page. At
≤640 px Back takes the context drawer toggle's slot (one navigation icon); the
list it returns to shows the toggle again. This replaced #1763's 72 px lane
reserved beside a sticky in-page arrow, along with its phone bar. Electron E2E
`detail-header-back.e2e.ts` covers 1280, 780 and 375 px in browse and watch.
The control floats, so the content makes room for it: while it is shown, every
content column (hero content, watch-state player, episodes, About and extras)
starts after a 72 px lane — 16 px inset, 40 px button, 16 px gap — and no scroll
position puts the arrow over a heading or the player. The shell derives the lane
from the button's geometry and publishes it as `--detail-back-lane`, which
`content-hero` and `content-about` read too; hosts with `backAvailable=false`
keep their own inset. The lane is horizontal because watch state is short of
height, not width. A pane narrower than 400 px is the exception — a phone, or a
desktop pane beside the context panel — because a 72 px column there would clip
the player's controls. The pane decides, not the viewport: a `ResizeObserver`
on the shell's border box (scrollbar-independent) sets `shell-host--compact`,
and the wrapper becomes an in-flow 56 px sticky bar on `--app-header-bg` that
content scrolls beneath and that takes the taps over it. Electron E2E
(`detail-back-lane.e2e.ts`) sweeps both states with the lane at 1280 and
780 px and with the bar at 700 and 375 px.
The sticky control is route-level Back in both states: it emits `backClicked`
The header Back is route-level in both states: it emits `backClicked`
whether or not inline playback is active, so the arrow keeps one meaning and
the list is one click away while watching. Only Escape unwinds one level: watch
emits `closePlayerRequested`, browse emits `backClicked`. Hosts retain their
@@ -61,13 +60,13 @@ Escape bubbles through the shell before Material's body-level tooltip dispatcher
so focused detail actions return with one press even while their tooltip is open.
The document listener remains the outside-shell watch fallback; `defaultPrevented`
prevents duplicate actions and preserves descendant handlers' priority.
After Escape closes a player, lost focus moves to the sticky control (or the
shell when there is no browse Back), without scrolling or stealing existing
focus.
After Escape closes a player, lost focus moves to the shell itself, without
scrolling or stealing existing focus, so the next Escape and the scroll keys
keep working on the page.
Hosts without browse navigation set `backAvailable=false`: M3U uses its channel
sidebar, and collection bootstrap placeholders have no return handler. They
render no sticky arrow in either state and have no browse Escape action; their
register no header Back in either state and have no browse Escape action; their
watch exits are the bar's Close player button and Escape. Loading/error shells
with a return handler keep Back available.
@@ -77,8 +76,8 @@ with a return handler keep Back available.
- Stalker uses an inline/store-state detail model.
- Detail pages themselves are two-state (browse ↔ watch) inside
`PortalDetailShellComponent`; entering/leaving watch is a layout state,
not a navigation. Route-level back semantics are unchanged; the one
sticky arrow returns to the list from either state, while Escape and the
not a navigation. Route-level back semantics are unchanged; the header's
one Back arrow returns to the list from either state, while Escape and the
now-playing bar's Close button close the inline player. See
[Embedded Inline Playback](./embedded-inline-playback.md).
- Favorites and recently viewed collections now use collection-owned inline detail
+10 -3
View File
@@ -172,11 +172,18 @@ and that pause holds the slide.
hover lift are not clipped; the fades are offset by the same variable so
they reach the track's clipping edge and no card strip shows beyond them.
4. Cards are keyboard-focusable router links; `scroll-snap-align: start`
means arrow-key nav lands on card boundaries.
means arrow-key nav lands on card boundaries. A card that receives
keyboard or script focus scrolls fully into the viewport: Chromium skips
its own focus scroll once 32px of an element shows, so the track's
`focusin` handler moves to the first card-start snap position revealing
the whole card (a card wider than the viewport aligns at its own start).
Focus caused by a press inside the track (within 100ms of `pointerdown`,
650ms for touch) leaves the rail still, so the card does not slide from
under the pointer before the click.
5. Image handling: `loading="lazy"`, `decoding="async"`, fallback icon tile
when `imageUrl` is missing or `error` fires.
6. Dashboard hero, rail containers, rail cards, and "Manage all" links expose
stable `data-test-id` hooks. Treat these as the supported Electron E2E
6. Dashboard hero, rail containers, rail viewports and tracks, rail cards and
their links, and "Manage all" links expose stable `data-test-id` hooks. Treat these as the supported Electron E2E
selector surface; do not target internal CSS class names.
## Data Flow
+11 -5
View File
@@ -90,12 +90,18 @@ The shell is intentionally split into four persistent regions:
because its data source is the SQLite worker bridge.
2. Provider-aware context links derived from the active or current playlist.
3. Settings remains a persistent footer shortcut in the rail.
4. No brand mark: it only repeated the first workspace link (Dashboard,
or Sources when the dashboard is off).
2. Top header:
1. Playlist switcher.
2. Route-aware search input and command palette trigger.
3. Add source action.
4. Optional playlist refresh and route-specific shortcut actions.
5. Downloads shortcut in Electron.
1. Leading Back slot, shown while the current page registers a target
with `WorkspaceBackNavigationService` (detail pages today). At
≤640 px it takes the context drawer toggle's place. See
[Portal Detail Navigation](./portal-detail-navigation.md).
2. Playlist switcher.
3. Route-aware search input and command palette trigger.
4. Add source action.
5. Optional playlist refresh and route-specific shortcut actions.
6. Downloads shortcut in Electron.
3. Main body:
1. Optional left context panel.
2. Main router outlet content.