Files
iptvnator/docs/architecture/iptvnator-ui-guidelines.md
T
4grayandClaude Opus 5 004cb65fe6 feat(portals): find the same movie in your other playlists
A movie that exists in several imported Xtream playlists now shows a
"Sources N" chip on its detail page and in the player. Switching playlist
mid-film keeps the timecode, a preferred source can be pinned per movie, and
a failed stream offers the alternatives instead of a dead end.

The governing rule is that a guess is never presented as a fact. Every
metadata value carries where it came from — `api` (the provider said so),
`parsed` (inferred from the title) or `probe` (we contacted the stream).
Facts render as plain tags, guesses are prefixed `~` in a warning colour, and
an unknown value renders no tag at all plus a "check" affordance. Ranking and
failover read through `factualOnly()`, so a filename claiming 4K is
structurally unable to outrank a source that was actually reached. A probe
that could not complete reports "unknown", never "unavailable".

Scope is deliberately narrow: Xtream to Xtream, movies only, Electron only.
Stalker never reaches the `content` table and M3U is a JSON blob whose search
forces live content; both are additive later, since the candidate type
already carries all three portal kinds. In the PWA every entry point is gated
off and the chip renders nothing.

Auto-failover is opt-in and off by default. Each source is tried at most once
per session, so it terminates structurally, and the switch is never silent —
the toast names the new playlist, offers an undo, and warns that the dub may
differ only when both sides state an audio track as fact.

Notable details:
- Playlist names are routinely the pasted URL, credentials included. They are
  never rendered raw; a short host-only label is derived instead.
- Quality is derived from pixel width, not height: a 2.39:1 1080p master is
  1920x800, and bucketing that by height would publish "720p" as a fact.
- Switching is a single `inlinePlayback.set()` so the player and engine
  survive and re-seek; the carried position is read before the 15s
  persistence throttle so it does not rewind.
- Sources from one playlist collapse into a group, since the same film often
  appears there several times under different stream ids.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 08:34:56 +02:00

337 lines
12 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 list selection style:
`apps/web/src/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-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
**Do not reach for `--mat-sys-*` surface tokens.** Angular Material's system
tokens are *not* emitted globally in this app — only `--mat-sys-on-surface`
exists. `var(--mat-sys-surface-container-high)` and friends resolve to nothing,
which usually goes unnoticed because the surrounding Material component
supplies its own default and masks it. Outside a Material component — a CDK
overlay, a projected panel — the same reference silently produces a fully
transparent, unreadable surface.
If a `--mat-sys-*` token is genuinely needed, always give it a real fallback:
`var(--mat-sys-on-primary, #fff)`.
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 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.
## Channel List Item
The shared row should be reused instead of rebuilding channel markup per view.
### Structure
- Min height:
`68px`
- Horizontal gap:
`12px`
- Padding:
`8px 10px 8px 12px`
- Radius:
`12px`
- Logo shell:
`44x44`, rounded, subtle inset treatment
- Compact variant:
`52px` min height with slightly tighter padding
### 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
### 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
### 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.
- Below 600 px viewport, the M3U layout's mobile bottom-drawer rule overrides
the desktop collapse to `height: 0` instead of `width: 0`, and the floating
restore handle is hidden.
### 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 neutral borders from `--mat-sys-outline-variant`
- 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
## Theme Guidance
### Light Theme
- Flat beats glossy
- White and surface-container 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. 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. Verify in the running Electron app when the change is visual or layout-sensitive.
## Anti-Patterns
Avoid these:
- introducing a new selected-state color unrelated to the theme tokens
- 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.