mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 12:06:16 -08:00
* feat(stalker): append portal pages on scroll and drop pagination everywhere Second and final PR of the pagination removal (plan: .plans/2026-08-09-infinite-scroll-catalog.md). Stalker VOD/series grids now feed the shared infinite-scroll contract from server-paged appends: portal pages (server-side size, typically 14) accumulate into one deduplicated paginatedContent list, page 1 replaces it for the skeleton, hasMoreContent derives from accumulated length vs total_items (portals that ignore requested page sizes still terminate), and a failed page > 1 keeps the accumulated pages on screen with a tail retry (retryContentPage reloads the same page; loadMore refuses to skip past an unresolved append error). The facade splits the resource's loading flag by page — skeleton for page one, tail spinner for appends — and keeps per-identity scroll offsets for Stalker's INLINE detail round trips; the shared view re-arms its one-shot restore when a detail opens in the same component instance. The transitional supportsInfiniteScroll flag and every paged member are deleted from PortalCatalogFacade; the shared catalog view loses the mat-paginator, the ?page= round-trip, and the paged query-param branch. The ITV all-channels grid becomes a client-side render window over the cached full list (the app's last paginator), and Stalker search pages past its first capped request via the layout's nearEnd, with a progress guard for portals that report no usable total. Validation: 1600 unit tests across 7 projects green (new: vod/series append + failed-append retry, facade loading split/loadMore guards/scroll snapshots, ITV window model, compat selector update); catalog-sorting e2e 5/5 (Stalker spec rewritten to scroll model with p>=2 network asserts and an inline-detail spot-restore round trip; one unrelated nav-timeout flake reproduced only under parallel machine load), search e2e 16/16, web stalker e2e green (all-channels grid asserts the windowed count instead of a paginator range label); lint clean; release note added and validated; stalker-portal.md, CLAUDE.md, and ui-guidelines updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(stalker): reset paging on content-type switch and never skip failed search pages Round-1 review findings on #1395: 1. Codex P1: switching /vod -> /series with the same category id ('*' on both section roots) left page > 1 in place — setSelectedContentType did not touch paging and setSelectedCategory('*') no-ops on an unchanged id — so the new type's FIRST response was treated as an append onto the old type's accumulated list. The type setter now resets the page (and no-ops entirely when the type repeats, keeping detail round-trip restores intact). 2. Greptile P1 + Codex P2: a failed search append left searchHasMore true, so the next near-end advanced to page N+1 and permanently omitted the failed page. The search now tracks searchAppendError: a failed append keeps the accumulated pages and the next near-end RETRIES the same page; a failed fresh search (page 1) clears the previous query's cards instead of rendering them under the new term (Codex P2). The page-merge/failure logic moved into applySearchPageSuccess/Failure methods: Angular resource() never re-fires on params changes in this repo's template-less jest harnesses (store-hosted resources do), so the extracted methods carry the unit coverage — accumulation + dedupe, no-total progress guard, retry-not-skip, fresh-failure clear — plus a selection spec for the type-switch page reset. portal-stalker-feature 260, portal-stalker-data-access 464, lint clean; catalog-sorting e2e 5/5 and web stalker e2e green. search.e2e shows machine-load nav-timeout flakes on unrelated M3U/live specs (a runaway third-party process pegs the host CPU); CI provides the clean independent run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(stalker): include the portal in the search paging identity Round-2 Codex P1 on #1395: Angular reuses the search route across /stalker/A/search -> /stalker/B/search, and the paging identity covered only term + filter — the page number and accumulator survived the portal change, so the next near-end fetched portal B at the OLD page number and appended it onto portal A's results while skipping B's first page. The active playlist id now joins the page-reset identity, the resource params, the stale-response guard, and the layout's near-end reset key. Regression spec: switching the active playlist on a reused route resets the page to 1 and rotates the scroll reset key. portal-stalker-feature 261, lint clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(stalker): end paging on no-progress appends even with a reported total Round-3 Codex P2s on #1395 (same defect in both accumulators): the no-progress guard only applied when the portal reported no usable total_items. After a mid-list portal mutation, deduplication can leave the unique list permanently shorter than the claimed total — hasMore then stayed true forever and every scroll crossing kept requesting pages past the end of the data. An append that adds no unique items now ends paging in both places: the catalog clamps totalCount to the accumulated length (hasMoreContent turns false and the count badge reflects what is actually reachable), and the search requires append progress in the total-backed branch exactly like the no-total branch. Regression specs cover a duplicate page under a larger claimed total for both. portal-stalker-data-access 465, portal-stalker-feature 262, lint clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(stalker): explicit search retry control and per-portal scroll identities Round-4 findings on #1395: 1. Greptile P1: with the results pane parked at the bottom, repeated append failures exhausted the scroll auto-fill budget while the near-end latch stayed armed — the retry path was reachable only through another nearEnd event that could never fire. The search page now renders an explicit retry control under the results whenever an append has failed (same wording as the catalog grid tail), wired to the existing retry-same-page path, so recovery never depends on producing another scroll event. 2. Codex P2: the facade's saved-scroll map survives a same-config portal switch (the vod/series route provider is reused across /stalker/A -> /stalker/B), and its identity lacked the playlist — portal A's offset could restore onto portal B's unrelated catalog. The playlist id now leads the scroll identity; regression spec covers the cross-portal non-restore and the return restore. portal-stalker-feature 263, lint clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(stalker): restore the search results scroll after an inline detail Round-5 Codex P2 on #1395: the search layout destroys the results container while an inline detail is shown (showDetails) and recreates it at offset zero — with the new multi-page accumulation a user could load several pages, open a result far down the list, and land back at the top on close even though the accumulated results survived. SearchLayoutComponent now exposes a scroll handoff for hosts whose details replace the results (getResultsScrollTop / restoreResultsScrollTop on the container it owns), and the Stalker search captures the offset when a detail opens and restores it one-shot after the container is recreated on close. Regression specs cover the layout handoff methods and the capture/restore round trip. portal-shared-ui 90, portal-stalker-feature 264, lint clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(stalker): clear accumulated search results for unsearchable portals Round-6 Codex P2 on #1395: the loader's early returns (deleted or malformed playlist on a reused route) predate the accumulator and returned [] without touching it — the previous portal's cards kept rendering under the new context once loading settled. Every no-portal early return now goes through resetSearchAccumulator(), which empties the accumulated list and both paging flags; the short-term path uses it too (and now also clears a stale append error). Regression spec covers the full reset. portal-stalker-feature 265, lint clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
489 lines
21 KiB
Markdown
489 lines
21 KiB
Markdown
# IPTVnator UI Guidelines
|
|
|
|
This document captures the current UI language used across IPTVnator, with emphasis on channel lists, EPG views, settings surfaces, and shared selection patterns.
|
|
|
|
Use it when changing existing views or introducing new list-based UI in the workspace, Xtream, or Stalker flows.
|
|
|
|
## Core Principles
|
|
|
|
1. Prefer shared components over duplicated markup.
|
|
The canonical channel row is `app-channel-list-item`.
|
|
|
|
2. Drive emphasis through selection state, not through constant decoration.
|
|
Neutral rows should stay quiet. Only active or current items should pick up strong color.
|
|
|
|
3. Use the same selection language everywhere.
|
|
Selected nav items, channels, and current EPG cards should feel like the same system.
|
|
|
|
4. Keep dark and light themes intentionally different.
|
|
Dark theme can carry more density and tinted surfaces.
|
|
Light theme should be flatter and cleaner, with white or near-white cards.
|
|
|
|
5. Scroll ownership must be explicit.
|
|
Headers stay visible. Lists scroll. Do not let nested panes compete for scroll.
|
|
|
|
## Canonical References
|
|
|
|
- Channel row:
|
|
`libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.html`
|
|
- Channel row styles:
|
|
`libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.scss`
|
|
- Shared EPG timeline:
|
|
`libs/ui/epg/src/lib/epg-timeline/epg-timeline.component.html`
|
|
- Shared EPG timeline styles:
|
|
`libs/ui/epg/src/lib/epg-timeline/epg-timeline.component.scss`
|
|
- Shared EPG list:
|
|
`libs/ui/epg/src/lib/epg-list-view/epg-list-view.component.ts`
|
|
- Shared EPG list styles:
|
|
`libs/ui/epg/src/lib/epg-list-view/epg-list-view.component.scss`
|
|
- Shared list selection style:
|
|
`libs/ui/styles/_nav-list.scss`
|
|
- Theme tokens:
|
|
`apps/web/src/m3-theme.scss`
|
|
- Settings surfaces:
|
|
`apps/web/src/app/settings/settings.component.scss`
|
|
- Detail view shell styles:
|
|
`libs/ui/styles/_detail-view.scss`
|
|
|
|
## Shared Tokens
|
|
|
|
These tokens are the base for interactive emphasis:
|
|
|
|
- `--app-selection-color`
|
|
- `--app-selection-on-color`
|
|
- `--app-selection-surface`
|
|
- `--app-selection-surface-strong`
|
|
- `--app-selection-border`
|
|
- `--app-selection-glow`
|
|
|
|
Use the app's own surface tokens for neutral surfaces (defined for both themes
|
|
in `apps/web/src/m3-theme.scss`):
|
|
|
|
- `--app-shell-bg` / `--app-rail-bg` / `--app-header-bg` / `--app-content-bg`
|
|
- `--app-widget-bg` / `--app-widget-header-bg` — panels and popovers
|
|
- `--app-card-hover-bg` — raised or hovered rows
|
|
- `--app-widget-border` / `--app-rail-border` — hairlines
|
|
- `--app-on-surface` — primary text
|
|
- `--app-eyebrow-color` — secondary/muted text
|
|
|
|
Angular Material mixins and Material-component overrides may use the tokens
|
|
owned by that component. Outside a Material-owned component, prefer the
|
|
app-owned tokens above. A `--mat-sys-*` reference is acceptable there only
|
|
after the built light and dark theme contexts both prove that it is emitted,
|
|
and it must still have a real app-token or literal fallback, for example:
|
|
`var(--mat-sys-surface-container, var(--app-widget-bg))`.
|
|
|
|
Several existing app surfaces still reference Material system tokens without
|
|
that proof or use hard-coded layout/selection colors. Treat those references
|
|
as migration debt, not patterns to copy.
|
|
|
|
Do not hardcode unrelated accent colors for selected state when these tokens already exist.
|
|
|
|
## Selection Pattern
|
|
|
|
Apply the same visual recipe to selected list items, active channels, and current EPG items:
|
|
|
|
- Background:
|
|
`linear-gradient(135deg, var(--app-selection-surface-strong), var(--app-selection-surface))`
|
|
- Border:
|
|
`var(--app-selection-border)`
|
|
- Glow:
|
|
outer shadow using `var(--app-selection-glow)`
|
|
- Lift:
|
|
`transform: translateY(-1px)` for selected list items only
|
|
- Text:
|
|
selected text should inherit `var(--app-selection-color)`
|
|
|
|
Use `var(--app-selection-on-color)` when text or an icon sits directly on a
|
|
solid `var(--app-selection-color)` fill.
|
|
|
|
Use this pattern for:
|
|
|
|
- `.nav-item.selected` / `.nav-item.active`
|
|
- `.channel-list-item.active`
|
|
- `.epg-item.current-program`
|
|
|
|
Do not add extra badges, left rails, or second selection systems unless there is a strong reason.
|
|
|
|
## Detail Views
|
|
|
|
VOD and series detail screens share the detail-view Sass mixin (`@mixin base`)
|
|
from `libs/ui/styles/_detail-view.scss`. Feature-local `styles/detail-view.scss`
|
|
files should only `@use` that module and `@include detail-view.base(...)` with
|
|
small typography overrides when a provider needs them.
|
|
|
|
Do not copy the full detail-view stylesheet into feature libraries. Add shared
|
|
layout changes to the mixin, and keep provider-specific differences explicit in
|
|
the wrapper file that includes it.
|
|
|
|
## Electron Drag Regions
|
|
|
|
Every interactive descendant of a drag region—including buttons, links,
|
|
inputs, overlays, and resize handles—requires `app-region: no-drag`. The shared
|
|
directive-generated `.resize-handle` does not set this centrally yet. Until
|
|
that debt is fixed, consumers in drag regions must cover the handle themselves
|
|
and must not assume it already opts out.
|
|
|
|
## Channel List Item
|
|
|
|
The shared row should be reused instead of rebuilding channel markup per view.
|
|
|
|
### Current Reference Values
|
|
|
|
- Current minimum height:
|
|
`68px`
|
|
- Horizontal gap:
|
|
`12px`
|
|
- Padding:
|
|
`8px 10px 8px 12px`
|
|
- Radius:
|
|
`12px`
|
|
- Current logo shell:
|
|
`44x44`, rounded, subtle inset treatment
|
|
- Compact variant:
|
|
`52px` min height with slightly tighter padding
|
|
|
|
These values describe the current shared row, not a fixed-width contract. Keep
|
|
the row responsive: the text column uses `min-width: 0` and ellipsis, while
|
|
logos, drag affordances, and trailing actions use `flex-shrink: 0`. Prefer
|
|
minimum dimensions and flexible columns over fixed row widths.
|
|
|
|
### Content Layout
|
|
|
|
- Title is one line, medium-bold, slightly condensed
|
|
- Program title is a secondary line with lower emphasis
|
|
- Timeline uses three columns:
|
|
start time, progress bar, end time
|
|
- Action buttons sit on the trailing edge and inherit row color
|
|
|
|
### Responsive Information Priority
|
|
|
|
- EPG-enabled, noncompact rows keep a fixed `68px` height that matches the
|
|
virtual-scroll stride. EPG-disabled, compact rows use a matching fixed `52px`
|
|
row and virtual-scroll size.
|
|
- At `310px` and below, hide the end time while keeping the start time and
|
|
progress bar.
|
|
- At `270px` and below, hide the decorative logo while retaining program
|
|
context and actions, and tighten horizontal padding to preserve the remaining
|
|
content.
|
|
- At `220px` and below, hide the start time while keeping the progress bar.
|
|
- In EPG-preview rows, narrow width alone must not remove the channel name,
|
|
program title or no-program placeholder, progress bar, drag affordance when
|
|
applicable, or enabled actions.
|
|
- Radio consumers without EPG render the row as compact instead of showing a
|
|
false no-program placeholder. Compact rows keep the logo at `270px`, then
|
|
hide the logo and actions at `220px`.
|
|
- `isRadio` alone must not change row height inside a fixed-size mixed virtual
|
|
list; the consumer's `showEpg` state and virtual-scroll item size own density.
|
|
- Loading skeletons mirror the same responsive hierarchy and row geometry.
|
|
|
|
### Logo Rules
|
|
|
|
- Show fallback icon only when no image is available or image loading fails
|
|
- Do not render placeholder and real logo at the same time
|
|
- Keep logos contained with `object-fit: contain`
|
|
|
|
## EPG Views
|
|
|
|
The shared timeline and list still contain local dark surfaces, blue selection
|
|
accents, and white foregrounds. These non-semantic hard-coded colors are
|
|
migration debt. New work should use app surface/selection/text tokens and must
|
|
not spread those local fallbacks. Semantic live, error, and status colors may
|
|
remain local when the meaning is explicit.
|
|
|
|
### Shared EPG Pane
|
|
|
|
- Header title stays sticky
|
|
- Program list is the only scrolling region
|
|
- Add bottom padding so the last program is not clipped
|
|
- Current program card uses the same selection treatment as selected channels
|
|
|
|
### Collapsible Live EPG
|
|
|
|
- Live TV layouts with an internal player render `app-epg-timeline` as the
|
|
EPG content, including playlist-specific live pages and the global
|
|
favorites/recent live tabs.
|
|
- The timeline's own panel bar owns the current-program summary and live date
|
|
navigation together; there is no separate wrapper component around it.
|
|
- Collapsed state is shared across M3U, Xtream, and Stalker with
|
|
`live-epg-panel-state`; missing or invalid values restore to expanded.
|
|
- The collapsed panel is a slim current-program strip with a trailing progress
|
|
line and an expand button. Date controls stay out of the collapsed strip.
|
|
- Do not render the collapsed strip for external MPV/VLC playback; those
|
|
layouts keep the full EPG-only panel.
|
|
- Keep the EPG content mounted while collapsed so current-program state can
|
|
continue updating.
|
|
|
|
### Collapsible Live Sidebar
|
|
|
|
- M3U, Xtream, and Stalker live layouts share a single sidebar collapse toggle
|
|
that hides the channels rail to give the player and EPG full width.
|
|
- 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
|
|
`app-grid-list`. Use the grid list's logo-oriented live variant so channel
|
|
logos stay contained in 16:9 thumbnails instead of being cropped like
|
|
VOD/series posters. Selecting a channel from that root grid starts playback,
|
|
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.
|
|
- Collapsed state is owned by `LiveLayoutSidebarStateService`
|
|
(`providedIn: 'root'`) in `@iptvnator/portal/shared/util`. Every surface that
|
|
participates injects the service and reads `isCollapsed`; any toggle calls
|
|
`service.toggle()`. Persistence delegates to the existing
|
|
`live-sidebar-state` helpers, so the localStorage key stays unchanged and
|
|
missing/invalid values restore to expanded.
|
|
- A `mat-icon-button` with `chevron_left` lives in the sidebar header and
|
|
toggles state. While collapsed, a floating `chevron_right` mini-fab appears
|
|
at the left edge of `.content-container` to restore the rail (and the
|
|
categories rail, in Xtream/Stalker live).
|
|
- Keyboard shortcut: `Cmd/Ctrl+B`. The handler ignores events that originate
|
|
inside `<input>`, `<textarea>`, `<select>`, or content-editable elements via
|
|
the shared `isTypingInInput` helper.
|
|
- The CSS class `.sidebar-collapsed` (channels rail) and
|
|
`.context-panel--collapsed` (workspace shell categories rail) both override
|
|
the inline width set by the `appResizable` directive with
|
|
`width: 0 !important; min-width: 0 !important`. The directive's persisted
|
|
width is preserved so uncollapsing restores the user's previous resized
|
|
width. Both rails share the same 180 ms width transition so motion stays in
|
|
lockstep.
|
|
- At the phone breakpoint the M3U layout's bottom-drawer rule overrides the
|
|
desktop collapse to `height: 0` instead of `width: 0`. The floating restore
|
|
handle stays visible there: the collapse toggle is reachable by touch, so
|
|
hiding the handle left a phone with no way to bring the list back short of
|
|
`Cmd/Ctrl+B`.
|
|
|
|
### EPG Card
|
|
|
|
- Radius:
|
|
`11px` (`.epg-timeline__block` in
|
|
`libs/ui/epg/src/lib/epg-timeline/epg-timeline-track.component.scss`)
|
|
- Neutral cards use low-contrast surface treatment
|
|
- Current card uses selection surface and selection border
|
|
- Description should clamp rather than overflow
|
|
|
|
### Sticky Header
|
|
|
|
- Keep the title readable above content
|
|
- Use a solid or near-solid backing surface
|
|
- Do not let it overlap or cover player controls
|
|
|
|
## Progress Bars
|
|
|
|
Channel preview progress and EPG current-program progress should stay visually aligned.
|
|
|
|
### Track
|
|
|
|
- Height:
|
|
`6px`
|
|
- Shape:
|
|
full pill radius
|
|
- Neutral background:
|
|
medium gray or neutral surface tint
|
|
- Include a slight inset edge so the remaining duration is visible
|
|
|
|
### Fill
|
|
|
|
- Use `--app-selection-color`
|
|
- Add a subtle sheen, not a heavy gradient
|
|
- Add a restrained glow, not a neon effect
|
|
|
|
The progress bar should clearly communicate:
|
|
|
|
- completed duration
|
|
- remaining duration
|
|
|
|
Avoid making the track too faint, especially in dark theme.
|
|
|
|
## Navigation Lists
|
|
|
|
Use the shared `nav-list.scss` treatment for sidebar and context-panel list items.
|
|
|
|
### Rules
|
|
|
|
- Keep labels one line with ellipsis
|
|
- Keep icon area clear from the selection border and any decorative rail
|
|
- Hover is neutral surface, not the selected color
|
|
- Selected state uses the shared selection recipe
|
|
|
|
If the label is too long for the rail, shorten the label key instead of shrinking the component until it becomes inconsistent.
|
|
|
|
## Settings Surfaces
|
|
|
|
Settings use the same system but are flatter than content-heavy views.
|
|
|
|
### Light Theme
|
|
|
|
- Prefer white or near-white cards
|
|
- Use app-owned neutral borders, or a proven Material token with a real
|
|
fallback such as
|
|
`var(--mat-sys-outline-variant, var(--app-widget-border))`
|
|
- Keep active sections mostly defined by outline and subtle tint
|
|
- Avoid dark translucent backgrounds
|
|
|
|
### Dark Theme
|
|
|
|
- Denser tinted surfaces are acceptable
|
|
- Neutral rows can use low-opacity dark overlays
|
|
- Keep strong blue tint reserved for active sections and selected items
|
|
|
|
## Phone Layout
|
|
|
|
`640px` is the phone breakpoint. Use `@media (max-width: 640px)` rather than
|
|
inventing a nearby value: several surfaces cooperate at this width, and a
|
|
component that picks `599px` leaves a band where the shell has already stacked
|
|
but the component has not.
|
|
|
|
### Rails become rows, stacks, or drawers
|
|
|
|
- The workspace shell rail turns into a horizontal top bar. Everything inside
|
|
it has to opt into the row direction — a nested list that keeps
|
|
`flex-direction: column` stacks its links out of the bar and over the header.
|
|
The bar scrolls sideways once a portal contributes its sections, and the
|
|
settings link is `position: sticky` so it never scrolls out of reach.
|
|
- The shell context panel (categories, filters, settings sections) is an
|
|
off-canvas drawer: hidden by default so the route content owns the full
|
|
pane, opened from a toggle in the workspace header, closed by selection,
|
|
backdrop tap, Escape, or any navigation. State lives in
|
|
`WorkspaceShellContextDrawerService` (root-provided from
|
|
`@iptvnator/workspace/shell/util` — see below for why); the
|
|
panels call `close()` after selections that do not navigate — a
|
|
NavigationEnd listener alone misses Stalker ITV/radio categories, settings
|
|
sections, sources filters, and collection filters. The drawer positioning
|
|
is `position: fixed` on the sidebar host, which also removes it from the
|
|
shell grid, so the phone `workspace-body` stays single-pane. The drawer is
|
|
modal for keyboard and screen-reader users: `CdkTrapFocus` captures and
|
|
contains Tab focus while open, the shell marks the rail, header, content,
|
|
and playback footer `inert` (a focus trap alone does not stop a screen
|
|
reader's virtual cursor from activating obscured controls), the panel
|
|
itself is the initial focus target (`tabindex="-1"` + `cdkFocusInitial`,
|
|
so capture still works when a category list is loading or empty and
|
|
renders no focusable rows), and the shell restores focus to the header
|
|
toggle on close — deferred one tick, because the toggle is inside the
|
|
inert header and `focus()` on a still-inert element is silently ignored.
|
|
The service closes the drawer when the viewport leaves the phone
|
|
breakpoint so the trap and inert state can never hold the in-flow desktop
|
|
layout. While open, the shell consumes Escape (downstream consumers —
|
|
the inline player's close handler, the shared controls shortcuts — check
|
|
`defaultPrevented`, so one keypress cannot close both the drawer and the
|
|
obscured player) and suppresses workspace-level shortcuts (Ctrl/Cmd+F
|
|
global search, Ctrl/Cmd+K command palette, Ctrl/Cmd+R global recent, the
|
|
`?` shortcuts dialog — dialogs must not stack a second focus trap on the
|
|
modal drawer, and navigation must not act behind it), and document-level
|
|
shortcuts owned by routed content (shared controls, Embedded MPV legacy
|
|
dock, radio audio player, the live layouts' Ctrl/Cmd+B sidebar toggle,
|
|
the M3U player's digit-key channel switching and sidebar toggle) opt out
|
|
on their own by checking for an `inert` ancestor, since `inert` does not
|
|
silence document-level listeners. Any NEW document-level key listener on
|
|
routed content must apply the same `closest('[inert]')` guard. The service is root-provided
|
|
from `@iptvnator/workspace/shell/util` so consumers outside the shell's
|
|
element injector (AppComponent's Ctrl/Cmd+R handler) can observe it
|
|
without pulling the lazy shell chunk into the eager bundle. The shell
|
|
also registers the open drawer with
|
|
`EmbeddedMpvOverlayVisibilityService.acquireExternalModalSurface()`:
|
|
the native-view video surface is composited outside DOM stacking and
|
|
would paint straight over the drawer regardless of z-index. The drawer carries its own phone-only close
|
|
button: touch screen-reader users have no hardware Escape and cannot
|
|
reach the inert header toggle or the aria-hidden backdrop, so the
|
|
trapped surface itself must offer dismissal even when its list is
|
|
loading or empty.
|
|
The toggle's label is variant-aware — categories, filters, or settings
|
|
sections — because a fixed label would misdescribe two of the three.
|
|
- Other side rails stack above the content instead of beside it: the
|
|
live-layout channel sidebar and the M3U channel drawer.
|
|
|
|
### Resizable rails need `!important`
|
|
|
|
`ResizableDirective` writes the persisted desktop width as an inline style, so
|
|
a phone rule must be `width: 100% !important` to win. Hide `.resize-handle` in
|
|
the same rule — dragging is meaningless at full width. Since there is no global
|
|
`border-box` reset, a full-width rail with its own padding also needs
|
|
`box-sizing: border-box` or it overflows the viewport.
|
|
|
|
### State the content's floor, not the list's ceiling
|
|
|
|
On routes that stack two lists above the player (live TV shows the categories
|
|
panel and the channel list), capping both lists still leaves the video a
|
|
sliver. Give the player container a `min-height` instead and let the lists
|
|
shrink into what is left.
|
|
|
|
### What to drop
|
|
|
|
Prefer removing a control over shrinking everything around it:
|
|
|
|
- Keyboard-only affordances — the `⌘K` badge, the shortcuts button.
|
|
- Counts and subtitles that a neighbouring control already states.
|
|
|
|
Never drop the only way back to a hidden surface. A collapse toggle that is
|
|
reachable by touch needs its restore affordance to be reachable too.
|
|
|
|
## Theme Guidance
|
|
|
|
### Light Theme
|
|
|
|
- Flat beats glossy
|
|
- White and app-owned widget/content surface layers should separate content
|
|
- Selection should read as a blue outline plus soft tint, not a solid slab
|
|
|
|
### Dark Theme
|
|
|
|
- Slight translucency is acceptable
|
|
- Background layers can be deeper and more cinematic
|
|
- Keep contrast readable without going pure white everywhere
|
|
|
|
## Reuse Strategy
|
|
|
|
Before creating new markup or CSS:
|
|
|
|
1. Check whether `app-channel-list-item` can be reused.
|
|
2. Check whether `app-epg-timeline` already provides the correct structure.
|
|
3. Check whether `nav-list.scss` already solves the list-selection problem.
|
|
4. Inspect the public APIs of `@iptvnator/ui/components`,
|
|
`@iptvnator/ui/epg`, `@iptvnator/ui/playback`,
|
|
`@iptvnator/ui/shared-portals`, `@iptvnator/portal/shared/ui`, and
|
|
`@iptvnator/playlist/shared/ui`.
|
|
5. Put provider-neutral collection loading, persistence, and cross-provider
|
|
orchestration in `@iptvnator/portal/shared/data-access`, not a UI library or
|
|
the shared util library.
|
|
6. Extend tokens first, duplicate styles last.
|
|
|
|
## Implementation Workflow
|
|
|
|
When updating IPTVnator UI:
|
|
|
|
1. Inspect the current shared component first.
|
|
2. Reuse the shared structure where possible.
|
|
3. Keep selection, progress, and spacing in sync across Xtream, Stalker, and shared portal views.
|
|
4. Verify in both light and dark themes.
|
|
5. Run the focused component/unit target and the closest Playwright workflow.
|
|
6. Use the running Electron app through CDP only for Electron-only gaps or
|
|
additional layout inspection; it does not replace available E2E coverage.
|
|
|
|
## Anti-Patterns
|
|
|
|
Avoid these:
|
|
|
|
- introducing a new selected-state color unrelated to the theme tokens
|
|
- copying the shared EPG's hard-coded dark/blue fallbacks into new surfaces
|
|
- duplicating channel row markup in portal-specific views
|
|
- showing placeholder logos behind real logos
|
|
- making entire panes scroll when only the list should scroll
|
|
- using dark translucent fills unchanged in light theme
|
|
- solving cramped sidebars with smaller fonts instead of shorter labels
|
|
|
|
## Definition Of Done For UI Changes
|
|
|
|
A visual change is not done until:
|
|
|
|
1. Shared component reuse was considered first.
|
|
2. Light theme and dark theme both look intentional.
|
|
3. Selection and progress states match existing IPTVnator patterns.
|
|
4. Scroll behavior is correct.
|
|
5. The result was checked in the running app for layout-sensitive work.
|