From c1d087f87ce360101ca7949717aaf3feec77cb67 Mon Sep 17 00:00:00 2001 From: Pavel Date: Wed, 1 Jul 2026 21:40:03 +0300 Subject: [PATCH 1/2] refactor(web): consolidate shape tokens and improve keyboard accessibility - Migrated hardcoded border-radius values to --app-radius-* tokens across ~80 stylesheets in apps/web and libs/*, adding a new --app-radius-2xl tier where the existing scale had a gap - Fixed the two genuinely theme-locked hardcoded colors in season-container (thumbnail/placeholder backgrounds) to use --app-content-bg / --app-widget-header-bg - Reverted an incorrect attempt to theme remote-control via --app-* tokens: it ships inside apps/remote-control-web, a standalone bundle that never loads m3-theme.scss, so those tokens don't exist there. It keeps its original self-contained dark palette and literal px radii instead - Added keyboard accessibility: channel-list-item, episode-card and episode-list-item were clickable elements with no tabindex/role, making them unreachable via keyboard. Added role="button", tabindex, Enter/Space activation, :focus-visible rings, and aria-label on icon-only buttons that previously relied solely on matTooltip - Added :focus-visible states to remote-control's touchpad/keypad/ volume buttons - Added regression tests for the new keyboard activation and aria-label behavior on channel-list-item --- apps/web/src/_settings-theme.scss | 8 +- ...delete-all-playlists-dialog.component.scss | 4 +- .../src/app/settings/settings.component.scss | 22 +- apps/web/src/m3-theme.scss | 27 +- apps/web/src/nav-list.scss | 4 +- apps/web/src/styles.scss | 10 +- .../add-playlist-dialog.component.scss | 6 +- .../file-upload/file-upload.component.scss | 8 +- .../video-player/video-player.component.scss | 2 +- .../playlist-switcher.component.scss | 14 +- .../empty-state/empty-state.component.scss | 2 +- .../empty-state.welcome-dashboard.scss | 4 +- .../empty-state.welcome-sources.scss | 6 +- .../playlist-item.component.scss | 6 +- .../category-content-view.component.scss | 4 +- .../feature/src/lib/downloads.component.scss | 26 +- .../content-rail-shell.component.scss | 4 +- .../favorites-layout.component.scss | 2 +- .../global-favorites-list.component.scss | 2 +- .../grid-list/grid-list.component.scss | 8 +- .../unified-collection-page.component.scss | 2 +- .../unified-grid-tab.component.scss | 2 +- .../lib/navigation/navigation.component.scss | 4 +- .../portal-rail-links.component.scss | 6 +- .../shared/ui/src/lib/styles/_skeleton.scss | 8 +- ...er-collection-channels-list.component.scss | 2 +- .../stalker-favorites-button.component.scss | 2 +- .../account-info/account-info.component.scss | 28 +- .../category-management-dialog.component.scss | 4 +- .../live-stream-layout.component.scss | 2 +- .../portal-channels-list.component.scss | 2 +- .../recently-added.component.scss | 4 +- .../vod-details-route.component.scss | 2 +- .../all-channels-view.component.scss | 2 +- .../channel-details-dialog.component.scss | 16 +- .../channel-list-container.component.scss | 2 +- .../channel-list-item-skeleton.component.scss | 12 +- .../channel-list-item.component.html | 16 + .../channel-list-item.component.scss | 25 +- .../channel-list-item.component.spec.ts | 25 + .../channel-list-item.component.ts | 5 + .../group-management-dialog.component.scss | 4 +- .../groups-view/groups-view.component.scss | 2 +- .../channel-list-loading-state.component.scss | 4 +- .../content-hero/content-hero.component.scss | 6 +- .../external-playback-dock.component.scss | 10 +- .../season-container.component.html | 14 + .../season-container.component.scss | 82 +++- .../epg-item-description.component.scss | 440 ++++++++++-------- .../epg-list-item.component.scss | 4 +- .../src/lib/epg-list/epg-list.component.scss | 8 +- .../epg-progress-panel.component.scss | 10 +- .../epg-source-status.component.scss | 2 +- .../multi-epg-container.component.scss | 8 +- .../audio-player/audio-player.component.scss | 8 +- .../embedded-mpv-player.component.scss | 14 +- .../portal-inline-player.component.scss | 4 +- ...layback-navigation-controls.component.scss | 2 +- .../web-player-view.component.scss | 8 +- .../remote-control.component.scss | 85 +++- .../src/lib/epg-view/epg-view.component.scss | 10 +- .../live-epg-panel.component.scss | 10 +- libs/ui/styles/_content-grid.scss | 4 +- libs/ui/styles/_detail-view-actions.scss | 2 +- libs/ui/styles/_detail-view.scss | 6 +- libs/ui/styles/_panel-header.scss | 2 +- libs/ui/styles/_portal-sidebar.scss | 2 +- .../lib/rails/dashboard-rail.component.scss | 6 +- .../workspace-dashboard-rails.component.scss | 24 +- .../global-favorites-list.component.scss | 2 +- .../playlist-drop-overlay.component.scss | 4 +- .../workspace-command-palette.component.scss | 2 +- ...space-context-category-view.component.scss | 4 +- .../workspace-context-panel.component.scss | 16 +- ...e-keyboard-shortcuts-dialog.component.scss | 10 +- ...space-shell-context-sidebar.component.scss | 34 ++ .../workspace-shell-header.component.scss | 6 +- ...kspace-shell-import-overlay.component.scss | 8 +- .../workspace-shell-rail-links.component.scss | 4 +- .../workspace-shell-rail.component.scss | 14 +- .../workspace-shell.component.scss | 2 +- ...space-sources-filters-panel.component.scss | 2 +- 82 files changed, 742 insertions(+), 487 deletions(-) diff --git a/apps/web/src/_settings-theme.scss b/apps/web/src/_settings-theme.scss index 8a843b60ea..e2cdf20034 100644 --- a/apps/web/src/_settings-theme.scss +++ b/apps/web/src/_settings-theme.scss @@ -158,7 +158,7 @@ flex-direction: column; gap: 14px; padding: 18px; - border-radius: 18px; + border-radius: var(--app-radius-2xl); border: 1px solid color-mix(in srgb, #ff4d6d 26%, transparent); background: radial-gradient( @@ -207,7 +207,7 @@ justify-content: center; width: 44px; height: 44px; - border-radius: 14px; + border-radius: var(--app-radius-xl); background: linear-gradient(135deg, #ff4d6d, #ff8b5e); color: #fff; box-shadow: @@ -255,7 +255,7 @@ align-items: center; gap: 12px; padding: 10px 14px; - border-radius: 14px; + border-radius: var(--app-radius-xl); border: 1px solid transparent; text-decoration: none; color: #fff; @@ -288,7 +288,7 @@ justify-content: center; width: 34px; height: 34px; - border-radius: 10px; + border-radius: var(--app-radius-md); background: rgba(255, 255, 255, 0.18); color: #fff; flex: 0 0 auto; diff --git a/apps/web/src/app/settings/settings-delete-all-playlists-dialog.component.scss b/apps/web/src/app/settings/settings-delete-all-playlists-dialog.component.scss index 9178d18065..36292d8537 100644 --- a/apps/web/src/app/settings/settings-delete-all-playlists-dialog.component.scss +++ b/apps/web/src/app/settings/settings-delete-all-playlists-dialog.component.scss @@ -44,7 +44,7 @@ padding: 10px 12px; border: 1px solid color-mix(in srgb, var(--mat-sys-outline) 46%, transparent); - border-radius: 14px; + border-radius: var(--app-radius-xl); background: color-mix( in srgb, var(--mat-sys-surface-container) 84%, @@ -80,7 +80,7 @@ .settings-delete-dialog__hint { padding: 12px 14px; - border-radius: 14px; + border-radius: var(--app-radius-xl); background: color-mix( in srgb, var(--app-selection-surface) 32%, diff --git a/apps/web/src/app/settings/settings.component.scss b/apps/web/src/app/settings/settings.component.scss index f3b7d22df8..186251f4cb 100644 --- a/apps/web/src/app/settings/settings.component.scss +++ b/apps/web/src/app/settings/settings.component.scss @@ -285,7 +285,7 @@ ); border: 1px solid color-mix(in srgb, var(--mat-sys-outline-variant) 60%, transparent); - border-radius: 10px; + border-radius: var(--app-radius-md); } .theme-switcher__option { @@ -501,7 +501,7 @@ padding: 12px 14px; border: 1px solid color-mix(in srgb, var(--mat-sys-outline-variant) 70%, transparent); - border-radius: 16px; + border-radius: var(--app-radius-xl); background: color-mix( in srgb, var(--mat-sys-surface-container-low) 88%, @@ -550,7 +550,7 @@ justify-content: center; width: 40px; height: 40px; - border-radius: 12px; + border-radius: var(--app-radius-lg); background: color-mix( in srgb, var(--mat-sys-surface-container-high) 82%, @@ -623,7 +623,7 @@ .settings-note { margin: 12px 18px 18px; padding: 10px 12px; - border-radius: 14px; + border-radius: var(--app-radius-xl); font-size: 0.84rem; background: color-mix(in srgb, var(--mat-sys-primary) 8%, transparent); border: 1px solid @@ -642,7 +642,7 @@ padding: 14px 16px; background: var(--settings-item-danger-bg); border: 1px solid var(--settings-item-danger-border); - border-radius: 16px; + border-radius: var(--app-radius-xl); grid-template-columns: 1fr; gap: 14px; } @@ -672,7 +672,7 @@ gap: 8px; min-height: 34px; padding: 4px 6px 4px 12px; - border-radius: 12px; + border-radius: var(--app-radius-lg); background: color-mix( in srgb, var(--mat-sys-surface-container-high) 78%, @@ -704,7 +704,7 @@ min-width: 26px; height: 22px; padding: 0 8px; - border-radius: 999px; + border-radius: var(--app-radius-pill); background: color-mix( in srgb, var(--mat-sys-on-surface) 10%, @@ -773,7 +773,7 @@ margin-top: 12px; margin-bottom: 2px; box-shadow: none; - border-radius: 16px; + border-radius: var(--app-radius-xl); padding: 12px; border-top: 1px solid var(--settings-action-bar-border); justify-content: space-between; @@ -787,7 +787,7 @@ color: var(--app-selection-on-color, #fff); font-weight: 500; padding: 0 20px; - border-radius: 10px; + border-radius: var(--app-radius-md); box-shadow: 0 4px 14px color-mix(in srgb, var(--app-selection-color) 28%, transparent); transition: @@ -835,7 +835,7 @@ .settings-page-header { padding: 12px 14px; - border-radius: 16px; + border-radius: var(--app-radius-xl); } .setting-item { @@ -883,7 +883,7 @@ position: static; flex-direction: column-reverse; align-items: stretch; - border-radius: 16px; + border-radius: var(--app-radius-xl); box-shadow: none; width: 100%; margin-left: 0; diff --git a/apps/web/src/m3-theme.scss b/apps/web/src/m3-theme.scss index 4f8ac3325c..efca9e87f5 100644 --- a/apps/web/src/m3-theme.scss +++ b/apps/web/src/m3-theme.scss @@ -53,6 +53,17 @@ $dark-theme: mat.define-theme( html { @include mat.all-component-themes($light-theme); + // ── Shape scale (theme-independent) ─────────────────────────────────── + // Central radius tokens so surfaces, list rows, inputs and the future + // advanced player control panel share one consistent rounding language. + --app-radius-xs: 6px; + --app-radius-sm: 8px; + --app-radius-md: 10px; + --app-radius-lg: 12px; + --app-radius-xl: 16px; + --app-radius-2xl: 20px; + --app-radius-pill: 999px; + // ── Light theme design tokens ───────────────────────────────────────── --app-shell-bg: #eef0f3; --app-rail-bg: #eef0f3; @@ -92,6 +103,12 @@ html { // Content area --app-content-shadow: -2px 4px 12px rgba(0, 0, 0, 0.06); + // Soft elevation scale — modern, low-contrast shadows for cards & panels + --app-shadow-soft: 0 1px 2px rgba(0, 0, 0, 0.04), + 0 4px 12px rgba(0, 0, 0, 0.05); + --app-shadow-raised: 0 2px 6px rgba(0, 0, 0, 0.06), + 0 6px 16px -8px rgba(0, 0, 0, 0.16); + // Widget surfaces — no borders, layered backgrounds --app-widget-bg: #ffffff; --app-widget-header-bg: #fafbfc; @@ -167,6 +184,12 @@ html { // Content area --app-content-shadow: -4px 8px 20px rgba(0, 0, 0, 0.18); + // Soft elevation scale — deeper on dark graphite surfaces + --app-shadow-soft: 0 1px 2px rgba(0, 0, 0, 0.25), + 0 6px 16px rgba(0, 0, 0, 0.28); + --app-shadow-raised: 0 3px 8px rgba(0, 0, 0, 0.32), + 0 10px 24px -10px rgba(0, 0, 0, 0.5); + // Widget surfaces — NO border, surface contrast does the work // Widgets sit on content-bg (#161a22), so widget-bg is notably lighter --app-widget-bg: #1e2330; @@ -213,7 +236,7 @@ html { --mdc-outlined-text-field-outline-color: var(--app-widget-header-border); --mdc-outlined-text-field-hover-outline-color: var(--app-muted-color); --mdc-outlined-text-field-focus-outline-color: var(--app-selection-color); - --mdc-outlined-text-field-container-shape: 10px; + --mdc-outlined-text-field-container-shape: var(--app-radius-md); --mdc-outlined-text-field-label-text-color: var(--app-muted-color); --mdc-outlined-text-field-input-text-color: var(--app-heading-color); --mdc-outlined-text-field-caret-color: var(--app-selection-color); @@ -227,7 +250,7 @@ html { // locked to the light-mode value at html-level inheritance. .mat-form-field-appearance-outline .mat-mdc-text-field-wrapper { background: color-mix(in srgb, var(--app-widget-bg) 85%, white 7%); - border-radius: 10px; + border-radius: var(--app-radius-md); } .mat-mdc-form-field .mat-mdc-input-element, diff --git a/apps/web/src/nav-list.scss b/apps/web/src/nav-list.scss index dfb6df1755..1188211d69 100644 --- a/apps/web/src/nav-list.scss +++ b/apps/web/src/nav-list.scss @@ -16,7 +16,7 @@ gap: 10px; padding: 6px 4px; border: 1px solid transparent; - border-radius: 10px; + border-radius: var(--app-radius-md); background: transparent; color: var(--mat-sys-on-surface-variant); text-align: left; @@ -95,7 +95,7 @@ top: 7px; bottom: 7px; width: 2px; - border-radius: 999px; + border-radius: var(--app-radius-pill); background: var(--app-selection-color); box-shadow: 0 0 10px var(--app-selection-glow); } */ diff --git a/apps/web/src/styles.scss b/apps/web/src/styles.scss index c4d00706e6..cf6379d252 100644 --- a/apps/web/src/styles.scss +++ b/apps/web/src/styles.scss @@ -58,7 +58,7 @@ var(--app-muted-color) 70%, transparent ); - border-radius: 999px; + border-radius: var(--app-radius-pill); border: 2px solid transparent; background-clip: content-box; min-height: 32px; @@ -83,7 +83,7 @@ color: var(--app-muted-color, #8891a4); font-variant-numeric: tabular-nums; padding: 2px 8px; - border-radius: 999px; + border-radius: var(--app-radius-pill); background: color-mix( in srgb, var(--app-muted-color, #8891a4) 18%, @@ -172,7 +172,7 @@ textarea, ::-webkit-scrollbar-thumb { background-color: var(--mat-sys-outline-variant); - border-radius: 999px; + border-radius: var(--app-radius-pill); &:hover { background-color: var(--mat-sys-outline); @@ -199,7 +199,7 @@ textarea, .workspace-command-palette-overlay .mat-mdc-dialog-surface { background: transparent; box-shadow: none; - border-radius: 14px; + border-radius: var(--app-radius-xl); padding: 0; overflow: visible; } @@ -213,7 +213,7 @@ textarea, .workspace-shortcuts-overlay .mat-mdc-dialog-surface { background: transparent; box-shadow: none; - border-radius: 14px; + border-radius: var(--app-radius-xl); padding: 0; overflow: visible; } diff --git a/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.scss b/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.scss index 799fe67c0b..ca97d6f24b 100644 --- a/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.scss +++ b/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.scss @@ -65,7 +65,7 @@ appearance: none; background: var(--apd-segment-track); border: 1px solid var(--apd-border); - border-radius: 10px; + border-radius: var(--app-radius-md); padding: 10px 11px 12px; display: flex; flex-direction: column; @@ -130,7 +130,7 @@ height: 32px; display: grid; place-items: center; - border-radius: 8px; + border-radius: var(--app-radius-sm); background: color-mix( in srgb, var(--apd-surface-raised) 75%, @@ -210,7 +210,7 @@ .cors-note { margin: 6px 0 0; padding: 10px 12px; - border-radius: 8px; + border-radius: var(--app-radius-sm); background: color-mix( in srgb, var(--apd-surface-raised) 50%, diff --git a/libs/playlist/import/feature/src/lib/file-upload/file-upload.component.scss b/libs/playlist/import/feature/src/lib/file-upload/file-upload.component.scss index a1f9c6b236..d6d534bc80 100644 --- a/libs/playlist/import/feature/src/lib/file-upload/file-upload.component.scss +++ b/libs/playlist/import/feature/src/lib/file-upload/file-upload.component.scss @@ -15,7 +15,7 @@ justify-content: center; gap: 8px; padding: 36px 24px 28px; - border-radius: 16px; + border-radius: var(--app-radius-xl); border: 1.5px dashed color-mix(in srgb, var(--app-body-color) 30%, transparent); background: color-mix(in srgb, var(--app-shell-bg) 55%, transparent); @@ -111,7 +111,7 @@ align-items: center; gap: 12px; padding: 14px 14px 14px 16px; - border-radius: 12px; + border-radius: var(--app-radius-lg); background: color-mix(in srgb, var(--app-shell-bg) 85%, transparent); box-shadow: inset 0 0 0 1px var(--app-widget-header-border); @@ -122,7 +122,7 @@ display: inline-flex; align-items: center; justify-content: center; - border-radius: 10px; + border-radius: var(--app-radius-md); background: color-mix( in srgb, var(--app-selection-color) 16%, @@ -166,7 +166,7 @@ background: transparent; width: 32px; height: 32px; - border-radius: 8px; + border-radius: var(--app-radius-sm); display: inline-flex; align-items: center; justify-content: center; diff --git a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.scss b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.scss index 2462266350..726eeb5e3d 100644 --- a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.scss +++ b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.scss @@ -85,7 +85,7 @@ min-width: 120px; padding: 2rem 3rem; border: 2px solid rgba(255, 255, 255, 0.1); - border-radius: 16px; + border-radius: var(--app-radius-xl); background: rgba(0, 0, 0, 0.85); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); color: #fff; diff --git a/libs/playlist/shared/ui/src/lib/playlist-switcher/playlist-switcher.component.scss b/libs/playlist/shared/ui/src/lib/playlist-switcher/playlist-switcher.component.scss index fdd6f4df91..0f07400e46 100644 --- a/libs/playlist/shared/ui/src/lib/playlist-switcher/playlist-switcher.component.scss +++ b/libs/playlist/shared/ui/src/lib/playlist-switcher/playlist-switcher.component.scss @@ -10,7 +10,7 @@ width: 100%; box-sizing: border-box; padding: 4px 8px 4px 4px; - border-radius: 10px; + border-radius: var(--app-radius-md); cursor: pointer; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); background: color-mix(in srgb, var(--app-on-surface) 5%, transparent); @@ -52,7 +52,7 @@ .trigger-icon-container { width: 30px; height: 30px; - border-radius: 8px; + border-radius: var(--app-radius-sm); display: inline-flex; align-items: center; justify-content: center; @@ -179,7 +179,7 @@ calc(100vw - 24px) ) !important; margin-top: -1px !important; - border-radius: 0 0 12px 12px !important; + border-radius: 0 0 var(--app-radius-lg) var(--app-radius-lg) !important; border: 1px solid var(--mat-sys-outline) !important; border-top: 0 !important; overflow: hidden !important; @@ -254,7 +254,7 @@ var(--app-on-surface) 6%, transparent ); - border-radius: 8px; + border-radius: var(--app-radius-sm); border: 1px solid transparent; transition: all 0.2s ease; } @@ -311,7 +311,7 @@ .type-filter-chip { height: 24px; padding: 0 10px; - border-radius: 999px; + border-radius: var(--app-radius-pill); border: 1px solid color-mix(in srgb, var(--mat-sys-outline-variant) 72%, transparent); background: color-mix( @@ -448,7 +448,7 @@ justify-content: center; width: 36px; height: 36px; - border-radius: 8px; + border-radius: var(--app-radius-sm); background: color-mix( in srgb, var(--app-on-surface) 6%, @@ -597,7 +597,7 @@ width: 100%; padding: 8px 10px; border: none; - border-radius: 8px; + border-radius: var(--app-radius-sm); background: transparent; color: var(--mat-sys-on-surface-variant); font: inherit; diff --git a/libs/playlist/shared/ui/src/lib/recent-playlists/empty-state/empty-state.component.scss b/libs/playlist/shared/ui/src/lib/recent-playlists/empty-state/empty-state.component.scss index a6ad6c6393..d2cf8d6b18 100644 --- a/libs/playlist/shared/ui/src/lib/recent-playlists/empty-state/empty-state.component.scss +++ b/libs/playlist/shared/ui/src/lib/recent-playlists/empty-state/empty-state.component.scss @@ -114,7 +114,7 @@ color: var(--app-selection-on-color, white); font-weight: 500; padding: 10px 24px; - border-radius: 10px; + border-radius: var(--app-radius-md); text-transform: none; letter-spacing: 0.01em; box-shadow: 0 4px 14px var(--app-selection-glow); diff --git a/libs/playlist/shared/ui/src/lib/recent-playlists/empty-state/empty-state.welcome-dashboard.scss b/libs/playlist/shared/ui/src/lib/recent-playlists/empty-state/empty-state.welcome-dashboard.scss index 154d51bba1..89ebe47fb3 100644 --- a/libs/playlist/shared/ui/src/lib/recent-playlists/empty-state/empty-state.welcome-dashboard.scss +++ b/libs/playlist/shared/ui/src/lib/recent-playlists/empty-state/empty-state.welcome-dashboard.scss @@ -154,7 +154,7 @@ font-weight: 500; font-size: clamp(0.875rem, 1.8vh, 1rem); padding: 10px 28px; - border-radius: 12px; + border-radius: var(--app-radius-lg); box-shadow: 0 4px 16px rgba(59, 130, 246, 0.3); transition: transform 0.2s ease, @@ -204,7 +204,7 @@ rgba(37, 99, 235, 0.04) ); border: 1px solid rgba(96, 165, 250, 0.18); - border-radius: 14px; + border-radius: var(--app-radius-xl); backdrop-filter: blur(8px); transform: rotate(-2deg); transform-origin: center; diff --git a/libs/playlist/shared/ui/src/lib/recent-playlists/empty-state/empty-state.welcome-sources.scss b/libs/playlist/shared/ui/src/lib/recent-playlists/empty-state/empty-state.welcome-sources.scss index 41e01a628a..6071b78852 100644 --- a/libs/playlist/shared/ui/src/lib/recent-playlists/empty-state/empty-state.welcome-sources.scss +++ b/libs/playlist/shared/ui/src/lib/recent-playlists/empty-state/empty-state.welcome-sources.scss @@ -81,7 +81,7 @@ rgba(37, 99, 235, 0.04) ); border: 1px solid rgba(96, 165, 250, 0.2); - border-radius: 16px; + border-radius: var(--app-radius-xl); transition: transform 0.2s ease, box-shadow 0.2s ease, @@ -143,7 +143,7 @@ font-size: 0.7rem; font-weight: 500; padding: 3px 9px; - border-radius: 999px; + border-radius: var(--app-radius-pill); background: rgba(96, 165, 250, 0.14); color: #93c5fd; border: 1px solid rgba(96, 165, 250, 0.22); @@ -159,7 +159,7 @@ font-weight: 500; font-size: 0.875rem; padding: 8px 18px; - border-radius: 10px; + border-radius: var(--app-radius-md); box-shadow: 0 4px 12px rgba(59, 130, 246, 0.25); transition: transform 0.2s ease, diff --git a/libs/playlist/shared/ui/src/lib/recent-playlists/playlist-item/playlist-item.component.scss b/libs/playlist/shared/ui/src/lib/recent-playlists/playlist-item/playlist-item.component.scss index b963935d62..fc20c3a47a 100644 --- a/libs/playlist/shared/ui/src/lib/recent-playlists/playlist-item/playlist-item.component.scss +++ b/libs/playlist/shared/ui/src/lib/recent-playlists/playlist-item/playlist-item.component.scss @@ -11,7 +11,7 @@ gap: 12px; padding: 8px 10px 8px 12px; border: 1px solid transparent; - border-radius: 8px; + border-radius: var(--app-radius-sm); background: transparent; color: inherit; cursor: pointer; @@ -125,7 +125,7 @@ .cdk-drag-preview { box-sizing: border-box; - border-radius: 8px; + border-radius: var(--app-radius-sm); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25), 0 4px 12px rgba(0, 0, 0, 0.15), @@ -162,7 +162,7 @@ button { width: 34px; height: 34px; - border-radius: 10px; + border-radius: var(--app-radius-md); color: inherit; opacity: 0.76; transition: diff --git a/libs/portal/catalog/feature/src/lib/category-content-view/category-content-view.component.scss b/libs/portal/catalog/feature/src/lib/category-content-view/category-content-view.component.scss index 36340c7158..5736c5b02a 100644 --- a/libs/portal/catalog/feature/src/lib/category-content-view/category-content-view.component.scss +++ b/libs/portal/catalog/feature/src/lib/category-content-view/category-content-view.component.scss @@ -72,7 +72,7 @@ gap: 6px; flex-shrink: 0; min-height: 32px; - border-radius: 999px; + border-radius: var(--app-radius-pill); color: var(--app-body-color, var(--mat-sys-on-surface-variant)); --mdc-text-button-label-text-color: currentColor; --mat-text-button-state-layer-color: currentColor; @@ -141,7 +141,7 @@ min-width: 18px; height: 18px; padding: 0 5px; - border-radius: 999px; + border-radius: var(--app-radius-pill); font-size: 0.68rem; font-weight: 700; line-height: 1; diff --git a/libs/portal/downloads/feature/src/lib/downloads.component.scss b/libs/portal/downloads/feature/src/lib/downloads.component.scss index 5ffd989e3c..1cd71d6800 100644 --- a/libs/portal/downloads/feature/src/lib/downloads.component.scss +++ b/libs/portal/downloads/feature/src/lib/downloads.component.scss @@ -95,7 +95,7 @@ background: var(--mat-sys-primary); color: var(--mat-sys-on-primary); padding: 0.125rem 0.5rem; - border-radius: 12px; + border-radius: var(--app-radius-lg); font-size: 0.75rem; font-weight: 600; } @@ -112,7 +112,7 @@ transparent ); border: 1px solid var(--mat-sys-outline-variant); - border-radius: 8px; + border-radius: var(--app-radius-sm); box-shadow: inset 0 1px 0 color-mix(in srgb, var(--mat-sys-on-surface) 8%, transparent); @@ -209,7 +209,7 @@ min-width: 0; min-height: 124px; padding: 1rem; - border-radius: 14px; + border-radius: var(--app-radius-xl); background: color-mix( in srgb, var(--app-widget-bg, var(--mat-sys-surface-container-high)) 88%, @@ -223,7 +223,7 @@ @extend %downloads-skeleton-shimmer; width: 64px; height: 90px; - border-radius: 10px; + border-radius: var(--app-radius-md); border: 1px solid var(--app-separator, var(--mat-sys-outline-variant)); } @@ -239,7 +239,7 @@ @extend %downloads-skeleton-shimmer; width: min(70%, 280px); height: 13px; - border-radius: 999px; + border-radius: var(--app-radius-pill); &--short { width: min(48%, 190px); @@ -255,7 +255,7 @@ @extend %downloads-skeleton-shimmer; width: 88px; height: 20px; - border-radius: 999px; + border-radius: var(--app-radius-pill); &:last-child { width: 132px; @@ -267,7 +267,7 @@ @extend %downloads-skeleton-shimmer; width: 100%; height: 6px; - border-radius: 999px; + border-radius: var(--app-radius-pill); margin-top: 0.05rem; } @@ -304,7 +304,7 @@ --app-widget-bg, var(--mat-sys-surface-container-high) ); - border-radius: 14px; + border-radius: var(--app-radius-xl); border: 1px solid var(--app-separator, var(--mat-sys-outline-variant)); box-shadow: var(--app-widget-shadow, none); transition: @@ -338,7 +338,7 @@ &-poster { width: 64px; height: 90px; - border-radius: 10px; + border-radius: var(--app-radius-md); overflow: hidden; flex-shrink: 0; background: var( @@ -408,7 +408,7 @@ gap: 0.35rem; font-weight: 500; padding: 2px 8px; - border-radius: 12px; + border-radius: var(--app-radius-lg); background: var( --app-widget-header-bg, var(--mat-sys-surface-container-high) @@ -450,7 +450,7 @@ monospace; background: transparent; padding: 2px 8px; - border-radius: 12px; + border-radius: var(--app-radius-lg); } &-error { @@ -462,7 +462,7 @@ transparent ); padding: 2px 8px; - border-radius: 12px; + border-radius: var(--app-radius-lg); } &-actions { @@ -517,7 +517,7 @@ background: var(--mat-sys-primary); color: var(--mat-sys-on-primary); padding: 0.125rem 0.5rem; - border-radius: 6px; + border-radius: var(--app-radius-xs); font-size: 0.7rem; font-weight: 700; text-transform: uppercase; diff --git a/libs/portal/shared/ui/src/lib/components/content-rail-shell/content-rail-shell.component.scss b/libs/portal/shared/ui/src/lib/components/content-rail-shell/content-rail-shell.component.scss index 66bb5cffb5..294cd2a06c 100644 --- a/libs/portal/shared/ui/src/lib/components/content-rail-shell/content-rail-shell.component.scss +++ b/libs/portal/shared/ui/src/lib/components/content-rail-shell/content-rail-shell.component.scss @@ -9,7 +9,7 @@ gap: 12px; padding: 12px 0 0; border: 1px solid transparent; - border-radius: 16px; + border-radius: var(--app-radius-xl); transition: background 0.2s ease, border-color 0.2s ease, @@ -62,7 +62,7 @@ align-items: center; gap: 2px; padding: 4px 8px 4px 10px; - border-radius: 999px; + border-radius: var(--app-radius-pill); font-size: 0.78rem; font-weight: 500; line-height: 1; diff --git a/libs/portal/shared/ui/src/lib/components/favorites-layout/favorites-layout.component.scss b/libs/portal/shared/ui/src/lib/components/favorites-layout/favorites-layout.component.scss index 232c9e9014..823fc56e87 100644 --- a/libs/portal/shared/ui/src/lib/components/favorites-layout/favorites-layout.component.scss +++ b/libs/portal/shared/ui/src/lib/components/favorites-layout/favorites-layout.component.scss @@ -58,7 +58,7 @@ color: var(--mat-sys-primary); background: color-mix(in srgb, var(--mat-sys-primary) 10%, var(--mat-sys-surface-container-high)); padding: 4px 10px; - border-radius: 999px; + border-radius: var(--app-radius-pill); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--mat-sys-primary) 30%, transparent), 0 1px 3px rgba(0, 0, 0, 0.18); diff --git a/libs/portal/shared/ui/src/lib/components/global-favorites-list/global-favorites-list.component.scss b/libs/portal/shared/ui/src/lib/components/global-favorites-list/global-favorites-list.component.scss index e22cb4a933..f71fde293a 100644 --- a/libs/portal/shared/ui/src/lib/components/global-favorites-list/global-favorites-list.component.scss +++ b/libs/portal/shared/ui/src/lib/components/global-favorites-list/global-favorites-list.component.scss @@ -22,7 +22,7 @@ } :host ::ng-deep .search-bar div { - border-radius: 20px; + border-radius: var(--app-radius-2xl); } :host ::ng-deep .search-bar div[matformfieldlineripple], diff --git a/libs/portal/shared/ui/src/lib/components/grid-list/grid-list.component.scss b/libs/portal/shared/ui/src/lib/components/grid-list/grid-list.component.scss index 4c5c043047..bb30c336c9 100644 --- a/libs/portal/shared/ui/src/lib/components/grid-list/grid-list.component.scss +++ b/libs/portal/shared/ui/src/lib/components/grid-list/grid-list.component.scss @@ -37,7 +37,7 @@ mat-card:hover { position: relative; width: 100%; box-sizing: border-box; - border-radius: 10px; + border-radius: var(--app-radius-md); overflow: hidden; aspect-ratio: 2 / 3; // maintain poster ratio background: var(--mat-sys-surface-container-high); @@ -47,7 +47,7 @@ mat-card:hover { width: 100%; height: 100%; object-fit: cover; - border-radius: 10px; + border-radius: var(--app-radius-md); display: block; // No hardcoded light bg — uses parent container bg background: transparent; @@ -60,7 +60,7 @@ mat-card:hover { display: grid; place-items: center; color: var(--app-muted-color, var(--mat-sys-on-surface-variant)); - border-radius: 10px; + border-radius: var(--app-radius-md); mat-icon { width: clamp(28px, 20%, 42px); @@ -145,7 +145,7 @@ mat-card:hover { color: #4ade80; // green-400, readable across themes font-size: 0.72rem; font-weight: 600; - border-radius: 6px; + border-radius: var(--app-radius-xs); display: flex; justify-content: center; align-items: center; diff --git a/libs/portal/shared/ui/src/lib/components/unified-collection/unified-collection-page.component.scss b/libs/portal/shared/ui/src/lib/components/unified-collection/unified-collection-page.component.scss index 9898557b49..87609b7036 100644 --- a/libs/portal/shared/ui/src/lib/components/unified-collection/unified-collection-page.component.scss +++ b/libs/portal/shared/ui/src/lib/components/unified-collection/unified-collection-page.component.scss @@ -55,7 +55,7 @@ .toggle-placeholder { height: 43px; width: 120px; - border-radius: 18px; + border-radius: var(--app-radius-2xl); background: color-mix(in srgb, var(--mat-sys-on-surface) 6%, transparent); &--scope { diff --git a/libs/portal/shared/ui/src/lib/components/unified-collection/unified-grid-tab.component.scss b/libs/portal/shared/ui/src/lib/components/unified-collection/unified-grid-tab.component.scss index f4c45795d8..7be807fad0 100644 --- a/libs/portal/shared/ui/src/lib/components/unified-collection/unified-grid-tab.component.scss +++ b/libs/portal/shared/ui/src/lib/components/unified-collection/unified-grid-tab.component.scss @@ -24,7 +24,7 @@ } :host ::ng-deep .search-bar div { - border-radius: 20px; + border-radius: var(--app-radius-2xl); } :host ::ng-deep .search-bar div[matformfieldlineripple], diff --git a/libs/portal/shared/ui/src/lib/navigation/navigation.component.scss b/libs/portal/shared/ui/src/lib/navigation/navigation.component.scss index d12c17c3e9..1b84ad957d 100644 --- a/libs/portal/shared/ui/src/lib/navigation/navigation.component.scss +++ b/libs/portal/shared/ui/src/lib/navigation/navigation.component.scss @@ -19,7 +19,7 @@ min-height: 28px; height: 28px; padding: 0 !important; - border-radius: 10px; + border-radius: var(--app-radius-md); margin: 2px 0; display: flex; justify-content: center; @@ -78,7 +78,7 @@ a.mat-mdc-list-item.mdc-list-item { display: flex; align-items: center; justify-content: center; - border-radius: 8px; + border-radius: var(--app-radius-sm); background: var(--mat-sys-surface-container); color: var(--mat-sys-on-surface-variant); border: 1px solid var(--mat-sys-outline-variant); diff --git a/libs/portal/shared/ui/src/lib/navigation/portal-rail-links.component.scss b/libs/portal/shared/ui/src/lib/navigation/portal-rail-links.component.scss index dda2ea5199..b9fbb54aea 100644 --- a/libs/portal/shared/ui/src/lib/navigation/portal-rail-links.component.scss +++ b/libs/portal/shared/ui/src/lib/navigation/portal-rail-links.component.scss @@ -11,7 +11,7 @@ min-height: 28px; height: 28px; padding: 0 !important; - border-radius: 10px; + border-radius: var(--app-radius-md); margin: 2px 0; display: flex; justify-content: center; @@ -45,7 +45,7 @@ nav.flex .portal-rail-link { height: 44px; padding: 0; gap: 0; - border-radius: 14px; + border-radius: var(--app-radius-xl); position: relative; display: inline-flex; align-items: center; @@ -88,7 +88,7 @@ nav.flex .portal-rail-link.active::after { transform: translateY(-50%); width: 4px; height: 20px; - border-radius: 999px; + border-radius: var(--app-radius-pill); background: var(--app-selection-color); box-shadow: 0 0 10px var(--app-selection-glow); } diff --git a/libs/portal/shared/ui/src/lib/styles/_skeleton.scss b/libs/portal/shared/ui/src/lib/styles/_skeleton.scss index 2ac9351d51..1f4a92e4a1 100644 --- a/libs/portal/shared/ui/src/lib/styles/_skeleton.scss +++ b/libs/portal/shared/ui/src/lib/styles/_skeleton.scss @@ -11,7 +11,7 @@ position: relative; width: 100%; aspect-ratio: 2 / 3; - border-radius: 10px; + border-radius: var(--app-radius-md); overflow: hidden; background: color-mix( in srgb, @@ -32,7 +32,7 @@ right: 8px; width: 34px; height: 14px; - border-radius: 999px; + border-radius: var(--app-radius-pill); background: color-mix( in srgb, var(--app-widget-header-bg, var(--mat-sys-surface-container-high)) 56%, @@ -49,7 +49,7 @@ .grid-skeleton-line { height: 9px; - border-radius: 999px; + border-radius: var(--app-radius-pill); background: color-mix( in srgb, var(--app-widget-header-bg, var(--mat-sys-surface-container-high)) 52%, @@ -95,7 +95,7 @@ .list-skeleton-avatar { width: 40px; height: 40px; - border-radius: 6px; + border-radius: var(--app-radius-xs); flex-shrink: 0; background: color-mix( in srgb, diff --git a/libs/portal/stalker/feature/src/lib/stalker-collection-channels-list/stalker-collection-channels-list.component.scss b/libs/portal/stalker/feature/src/lib/stalker-collection-channels-list/stalker-collection-channels-list.component.scss index 1509296611..11017a0ad1 100644 --- a/libs/portal/stalker/feature/src/lib/stalker-collection-channels-list/stalker-collection-channels-list.component.scss +++ b/libs/portal/stalker/feature/src/lib/stalker-collection-channels-list/stalker-collection-channels-list.component.scss @@ -31,7 +31,7 @@ flex: 1; width: 100%; height: 36px; - border-radius: 10px; + border-radius: var(--app-radius-md); border: 1px solid rgba(127, 127, 127, 0.22); background: linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0)), diff --git a/libs/portal/stalker/feature/src/lib/stalker-favorites-button/stalker-favorites-button.component.scss b/libs/portal/stalker/feature/src/lib/stalker-favorites-button/stalker-favorites-button.component.scss index 8a45e41415..ddcbceaa5e 100644 --- a/libs/portal/stalker/feature/src/lib/stalker-favorites-button/stalker-favorites-button.component.scss +++ b/libs/portal/stalker/feature/src/lib/stalker-favorites-button/stalker-favorites-button.component.scss @@ -9,7 +9,7 @@ background: rgba(109, 109, 110, 0.7); color: #fff; border: none; - border-radius: 16px; + border-radius: var(--app-radius-xl); cursor: pointer; transition: all 0.2s ease; diff --git a/libs/portal/xtream/feature/src/lib/account-info/account-info.component.scss b/libs/portal/xtream/feature/src/lib/account-info/account-info.component.scss index f553b6fb35..2d582be3df 100644 --- a/libs/portal/xtream/feature/src/lib/account-info/account-info.component.scss +++ b/libs/portal/xtream/feature/src/lib/account-info/account-info.component.scss @@ -35,7 +35,7 @@ .account-dialog__eyebrow-mark { width: 8px; height: 8px; - border-radius: 999px; + border-radius: var(--app-radius-pill); background: var(--app-provider-xtream, var(--mat-sys-primary)); box-shadow: 0 0 0 4px color-mix(in srgb, var(--app-provider-xtream, var(--mat-sys-primary)) 14%, transparent); } @@ -43,7 +43,7 @@ .account-dialog__eyebrow-divider { width: 3px; height: 3px; - border-radius: 999px; + border-radius: var(--app-radius-pill); background: currentColor; opacity: 0.5; } @@ -84,7 +84,7 @@ justify-content: center; min-height: 28px; padding: 0 10px; - border-radius: 999px; + border-radius: var(--app-radius-pill); border: 1px solid var(--app-separator); background: var(--mat-sys-surface-container); color: var(--app-body-color, var(--mat-sys-on-surface)); @@ -116,7 +116,7 @@ .account-dialog__message { padding: 10px 14px; - border-radius: 10px; + border-radius: var(--app-radius-md); border: 1px solid var(--app-separator); background: var(--mat-sys-surface-container); line-height: 1.5; @@ -134,7 +134,7 @@ display: grid; gap: 6px; padding: 12px 14px; - border-radius: 12px; + border-radius: var(--app-radius-lg); border: 1px solid var(--app-widget-border, transparent); background: var(--app-widget-bg, var(--mat-sys-surface-container)); box-shadow: var(--app-widget-shadow, none); @@ -149,7 +149,7 @@ display: inline-flex; align-items: center; justify-content: center; - border-radius: 10px; + border-radius: var(--app-radius-md); background: color-mix(in srgb, var(--app-provider-xtream, var(--mat-sys-primary)) 12%, transparent); color: var(--app-provider-xtream, var(--mat-sys-primary)); @@ -179,7 +179,7 @@ position: relative; overflow: hidden; height: 4px; - border-radius: 999px; + border-radius: var(--app-radius-pill); background: var(--app-separator); } @@ -208,7 +208,7 @@ display: grid; gap: 14px; padding: 16px; - border-radius: 16px; + border-radius: var(--app-radius-xl); border: 1px solid var(--app-widget-border, transparent); background: var(--app-widget-bg, var(--mat-sys-surface-container-low)); box-shadow: var(--app-widget-shadow, none); @@ -319,7 +319,7 @@ align-items: center; min-height: 28px; padding: 0 10px; - border-radius: 999px; + border-radius: var(--app-radius-pill); border: 1px solid color-mix(in srgb, var(--app-provider-xtream, var(--mat-sys-primary)) 16%, transparent); background: color-mix(in srgb, var(--app-provider-xtream, var(--mat-sys-primary)) 8%, transparent); color: var(--app-body-color, var(--mat-sys-on-surface)); @@ -345,7 +345,7 @@ justify-items: center; gap: 12px; padding: 36px 20px; - border-radius: 16px; + border-radius: var(--app-radius-xl); border: 1px dashed var(--app-separator); background: var(--app-widget-bg, var(--mat-sys-surface-container-low)); text-align: center; @@ -369,7 +369,7 @@ display: inline-flex; align-items: center; justify-content: center; - border-radius: 14px; + border-radius: var(--app-radius-xl); background: color-mix(in srgb, var(--mat-sys-error) 12%, transparent); color: var(--mat-sys-error); } @@ -382,7 +382,7 @@ .account-skeleton { position: relative; overflow: hidden; - border-radius: 999px; + border-radius: var(--app-radius-pill); background: var(--app-separator); } @@ -402,7 +402,7 @@ .account-skeleton--stat { min-height: 110px; - border-radius: 12px; + border-radius: var(--app-radius-lg); } .account-skeleton--heading { @@ -431,7 +431,7 @@ .account-dialog__close { min-width: 100px; - border-radius: 999px; + border-radius: var(--app-radius-pill); } @keyframes account-dialog-shimmer { diff --git a/libs/portal/xtream/feature/src/lib/category-management-dialog/category-management-dialog.component.scss b/libs/portal/xtream/feature/src/lib/category-management-dialog/category-management-dialog.component.scss index 2f1944edf0..36f879ca02 100644 --- a/libs/portal/xtream/feature/src/lib/category-management-dialog/category-management-dialog.component.scss +++ b/libs/portal/xtream/feature/src/lib/category-management-dialog/category-management-dialog.component.scss @@ -49,7 +49,7 @@ gap: 8px; padding: 0 10px; min-height: 36px; - border-radius: 10px; + border-radius: var(--app-radius-md); border: 1px solid var(--app-search-border, var(--mat-sys-outline-variant)); background: var(--app-search-bg, var(--mat-sys-surface-container)); cursor: text; @@ -113,7 +113,7 @@ min-height: 200px; overflow-y: auto; border: 1px solid var(--mat-sys-outline-variant); - border-radius: 8px; + border-radius: var(--app-radius-sm); } .category-item { diff --git a/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.scss b/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.scss index ee772f77fd..56b4550f9a 100644 --- a/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.scss +++ b/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.scss @@ -89,7 +89,7 @@ app-grid-list.live-all-items-grid { .archive-unavailable-banner { margin: 12px 12px 0; padding: 12px 14px; - border-radius: 14px; + border-radius: var(--app-radius-xl); border: 1px solid rgba(255, 255, 255, 0.08); background: linear-gradient( diff --git a/libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.scss b/libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.scss index 0df50214bb..23e8a793ca 100644 --- a/libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.scss +++ b/libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.scss @@ -44,7 +44,7 @@ flex: 1; width: 100%; height: 36px; - border-radius: 10px; + border-radius: var(--app-radius-md); border: 1px solid rgba(127, 127, 127, 0.22); background: linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0)), diff --git a/libs/portal/xtream/feature/src/lib/recently-added/recently-added.component.scss b/libs/portal/xtream/feature/src/lib/recently-added/recently-added.component.scss index 1ea7b22f63..0c2044a636 100644 --- a/libs/portal/xtream/feature/src/lib/recently-added/recently-added.component.scss +++ b/libs/portal/xtream/feature/src/lib/recently-added/recently-added.component.scss @@ -46,7 +46,7 @@ .recently-added-page__skeleton-header { width: 180px; height: 22px; - border-radius: 6px; + border-radius: var(--app-radius-xs); } .recently-added-page__skeleton-track { @@ -60,7 +60,7 @@ flex: 0 0 var(--cover-rail-width, 160px); width: var(--cover-rail-width, 160px); aspect-ratio: 2 / 3; - border-radius: 12px; + border-radius: var(--app-radius-lg); } .recently-added-page__skeleton-header, diff --git a/libs/portal/xtream/feature/src/lib/vod-details/vod-details-route.component.scss b/libs/portal/xtream/feature/src/lib/vod-details/vod-details-route.component.scss index c35099222b..71492caed5 100644 --- a/libs/portal/xtream/feature/src/lib/vod-details/vod-details-route.component.scss +++ b/libs/portal/xtream/feature/src/lib/vod-details/vod-details-route.component.scss @@ -20,7 +20,7 @@ margin-bottom: 4px; padding: 14px 16px; border: 1px solid color-mix(in srgb, var(--tag-border) 85%, transparent); - border-radius: 14px; + border-radius: var(--app-radius-xl); background: linear-gradient( 180deg, color-mix(in srgb, var(--surface-bg) 86%, transparent) 0%, diff --git a/libs/ui/components/src/lib/channel-list-container/all-channels-view/all-channels-view.component.scss b/libs/ui/components/src/lib/channel-list-container/all-channels-view/all-channels-view.component.scss index e74c10efbe..4d5840d818 100644 --- a/libs/ui/components/src/lib/channel-list-container/all-channels-view/all-channels-view.component.scss +++ b/libs/ui/components/src/lib/channel-list-container/all-channels-view/all-channels-view.component.scss @@ -92,7 +92,7 @@ } :host ::ng-deep .search-bar div { - border-radius: 20px; + border-radius: var(--app-radius-2xl); } :host ::ng-deep .search-bar div[matformfieldlineripple], diff --git a/libs/ui/components/src/lib/channel-list-container/channel-details-dialog/channel-details-dialog.component.scss b/libs/ui/components/src/lib/channel-list-container/channel-details-dialog/channel-details-dialog.component.scss index 7b53595d4f..e8e59c6b91 100644 --- a/libs/ui/components/src/lib/channel-list-container/channel-details-dialog/channel-details-dialog.component.scss +++ b/libs/ui/components/src/lib/channel-list-container/channel-details-dialog/channel-details-dialog.component.scss @@ -35,7 +35,7 @@ .channel-dialog__eyebrow-mark { width: 8px; height: 8px; - border-radius: 999px; + border-radius: var(--app-radius-pill); background: var(--mat-sys-primary); box-shadow: 0 0 0 4px color-mix(in srgb, var(--mat-sys-primary) 14%, transparent); } @@ -57,7 +57,7 @@ .channel-dialog__logo { width: 48px; height: 48px; - border-radius: 12px; + border-radius: var(--app-radius-lg); object-fit: contain; background: var(--mat-sys-surface-container); border: 1px solid var(--app-separator); @@ -94,7 +94,7 @@ justify-content: center; min-height: 26px; padding: 0 10px; - border-radius: 999px; + border-radius: var(--app-radius-pill); border: 1px solid var(--app-separator); background: var(--mat-sys-surface-container); color: var(--app-body-color, var(--mat-sys-on-surface)); @@ -133,7 +133,7 @@ display: grid; gap: 4px; padding: 12px 14px; - border-radius: 12px; + border-radius: var(--app-radius-lg); border: 1px solid var(--app-widget-border, transparent); background: var(--app-widget-bg, var(--mat-sys-surface-container)); box-shadow: var(--app-widget-shadow, none); @@ -147,7 +147,7 @@ display: inline-flex; align-items: center; justify-content: center; - border-radius: 10px; + border-radius: var(--app-radius-md); background: color-mix(in srgb, var(--mat-sys-primary) 12%, transparent); color: var(--mat-sys-primary); @@ -207,7 +207,7 @@ align-items: flex-start; gap: 8px; padding: 10px 10px 10px 14px; - border-radius: 10px; + border-radius: var(--app-radius-md); border: 1px solid var(--app-separator); background: var(--mat-sys-surface-container); @@ -237,7 +237,7 @@ display: grid; gap: 14px; padding: 16px; - border-radius: 16px; + border-radius: var(--app-radius-xl); border: 1px solid var(--app-widget-border, transparent); background: var(--app-widget-bg, var(--mat-sys-surface-container-low)); box-shadow: var(--app-widget-shadow, none); @@ -347,7 +347,7 @@ .channel-dialog__close { min-width: 100px; - border-radius: 999px; + border-radius: var(--app-radius-pill); } // ─── Responsive breakpoints ───────────────────────────────────────────────── diff --git a/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.scss b/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.scss index bf3560dc0f..89bd033fb0 100644 --- a/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.scss +++ b/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.scss @@ -31,7 +31,7 @@ .empty-source-state__icon-wrapper { width: 64px; height: 64px; - border-radius: 20px; + border-radius: var(--app-radius-2xl); background: linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0)), rgba(127, 127, 127, 0.1); diff --git a/libs/ui/components/src/lib/channel-list-container/channel-list-item-skeleton/channel-list-item-skeleton.component.scss b/libs/ui/components/src/lib/channel-list-container/channel-list-item-skeleton/channel-list-item-skeleton.component.scss index 8deb39f911..1cc0a2a120 100644 --- a/libs/ui/components/src/lib/channel-list-container/channel-list-item-skeleton/channel-list-item-skeleton.component.scss +++ b/libs/ui/components/src/lib/channel-list-container/channel-list-item-skeleton/channel-list-item-skeleton.component.scss @@ -14,7 +14,7 @@ gap: 12px; padding: 8px 10px 8px 12px; border: 1px solid transparent; - border-radius: 12px; + border-radius: var(--app-radius-lg); background: transparent; box-sizing: border-box; overflow: hidden; @@ -37,7 +37,7 @@ width: 44px; height: 44px; flex-shrink: 0; - border-radius: 10px; + border-radius: var(--app-radius-md); .compact & { width: 38px; @@ -56,20 +56,20 @@ .channel-name-skeleton { height: 11px; - border-radius: 999px; + border-radius: var(--app-radius-pill); max-width: 100%; } .epg-title-skeleton { height: 9px; - border-radius: 999px; + border-radius: var(--app-radius-pill); max-width: 100%; } .epg-progress-skeleton { height: 6px; width: 100%; - border-radius: 999px; + border-radius: var(--app-radius-pill); } .action-buttons-skeleton { @@ -83,7 +83,7 @@ .action-button-skeleton { width: 34px; height: 34px; - border-radius: 10px; + border-radius: var(--app-radius-md); } @container (max-width: 360px) { diff --git a/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.html b/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.html index c215f2cb86..8d4c1a71e8 100644 --- a/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.html +++ b/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.html @@ -5,9 +5,15 @@ [cdkDragDisabled]="!isDraggable()" cdkDragPreviewContainer="parent" [class.active]="selected()" + role="button" + tabindex="0" + [attr.aria-label]="name()" + [attr.aria-pressed]="selected()" (click)="onClick($event)" (dblclick)="onDoubleClick()" (contextmenu)="onContextMenu($event)" + (keydown.enter)="onKeydownActivate($event)" + (keydown.space)="onKeydownActivate($event)" data-test-id="channel-item" > @if (isDraggable()) { @@ -77,6 +83,9 @@ [matTooltip]=" 'EPG.PROGRAM_DIALOG.SHOW_PROGRAM_DETAILS' | translate " + [attr.aria-label]=" + 'EPG.PROGRAM_DIALOG.SHOW_PROGRAM_DETAILS' | translate + " (click)="showProgramDescription(program, $event)" > info @@ -92,6 +101,12 @@ : 'CHANNELS.ADD_FAVORITE' ) | translate " + [attr.aria-label]=" + (isFavorite() + ? 'CHANNELS.REMOVE_FAVORITE' + : 'CHANNELS.ADD_FAVORITE' + ) | translate + " (click)="onFavoriteClick($event)" > {{ @@ -104,6 +119,7 @@ mat-icon-button class="favorite-button" [matTooltip]="auxActionTooltip()" + [attr.aria-label]="auxActionTooltip()" (click)="onAuxActionClick($event)" > {{ auxActionIcon() }} diff --git a/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.scss b/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.scss index 4e7430ad3c..339b127c95 100644 --- a/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.scss +++ b/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.scss @@ -12,7 +12,7 @@ gap: 12px; padding: 8px 10px 8px 12px; border: 1px solid transparent; - border-radius: 12px; + border-radius: var(--app-radius-lg); background: transparent; color: inherit; cursor: pointer; @@ -27,6 +27,14 @@ &:hover { background: color-mix(in srgb, currentColor 8%, transparent); + border-color: color-mix(in srgb, currentColor 12%, transparent); + } + + &:focus-visible { + outline: none; + border-color: var(--app-selection-border); + box-shadow: 0 0 0 2px + color-mix(in srgb, var(--app-selection-color) 45%, transparent); } &.active { @@ -68,7 +76,7 @@ width: 44px; height: 44px; flex-shrink: 0; - border-radius: 10px; + border-radius: var(--app-radius-md); display: grid; place-items: center; overflow: hidden; @@ -156,7 +164,7 @@ width: 100%; height: 6px; overflow: hidden; - border-radius: 999px; + border-radius: var(--app-radius-pill); background-color: rgba(127, 127, 127, 0.28); background-image: linear-gradient( 180deg, @@ -205,18 +213,25 @@ button { width: 32px; height: 32px; - /* border-radius: 10px; */ + border-radius: var(--app-radius-sm); color: inherit; opacity: 0.76; transition: opacity 0.15s ease, background-color 0.15s ease, + box-shadow 0.15s ease, transform 0.15s ease; &:hover { opacity: 1; background: color-mix(in srgb, currentColor 10%, transparent); - /* transform: translateY(-1px); */ + } + + &:focus-visible { + outline: none; + opacity: 1; + box-shadow: 0 0 0 2px + color-mix(in srgb, var(--app-selection-color) 45%, transparent); } } diff --git a/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.spec.ts b/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.spec.ts index e101df1003..c489ddce76 100644 --- a/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.spec.ts +++ b/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.spec.ts @@ -112,6 +112,31 @@ describe('ChannelListItemComponent', () => { expect(activated).toHaveBeenCalledTimes(1); }); + it('exposes a keyboard-focusable row with an accessible name', () => { + fixture.componentRef.setInput('name', 'Cartoon Network'); + fixture.detectChanges(); + + const row: HTMLElement = + fixture.nativeElement.querySelector('.channel-list-item'); + + expect(row.getAttribute('role')).toBe('button'); + expect(row.getAttribute('tabindex')).toBe('0'); + expect(row.getAttribute('aria-label')).toBe('Cartoon Network'); + }); + + it('activates on Enter/Space keydown like a click', () => { + const clicked = jest.fn(); + fixture.componentInstance.clicked.subscribe(clicked); + + const preventDefault = jest.fn(); + fixture.componentInstance.onKeydownActivate({ + preventDefault, + } as unknown as KeyboardEvent); + + expect(preventDefault).toHaveBeenCalled(); + expect(clicked).toHaveBeenCalledTimes(1); + }); + it('emits a context menu request on right click when details are enabled', () => { fixture.componentRef.setInput('name', 'News One'); fixture.componentRef.setInput('showDetailsContextMenu', true); diff --git a/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.ts b/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.ts index 06400bc0ac..207c6d382f 100644 --- a/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.ts +++ b/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.ts @@ -96,6 +96,11 @@ export class ChannelListItemComponent { this.clicked.emit(); } + onKeydownActivate(event: Event): void { + event.preventDefault(); + this.clicked.emit(); + } + onDoubleClick(): void { this.activated.emit(); } diff --git a/libs/ui/components/src/lib/channel-list-container/groups-view/group-management-dialog/group-management-dialog.component.scss b/libs/ui/components/src/lib/channel-list-container/groups-view/group-management-dialog/group-management-dialog.component.scss index e4bfd5b2fc..26b7a557c6 100644 --- a/libs/ui/components/src/lib/channel-list-container/groups-view/group-management-dialog/group-management-dialog.component.scss +++ b/libs/ui/components/src/lib/channel-list-container/groups-view/group-management-dialog/group-management-dialog.component.scss @@ -40,7 +40,7 @@ gap: 8px; padding: 0 10px; min-height: 36px; - border-radius: 10px; + border-radius: var(--app-radius-md); border: 1px solid var(--app-search-border, var(--mat-sys-outline-variant)); background: var(--app-search-bg, var(--mat-sys-surface-container)); cursor: text; @@ -104,7 +104,7 @@ min-height: 200px; overflow-y: auto; border: 1px solid var(--mat-sys-outline-variant); - border-radius: 8px; + border-radius: var(--app-radius-sm); } .group-item { diff --git a/libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.scss b/libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.scss index 61c8f61b4f..30478e2f01 100644 --- a/libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.scss +++ b/libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.scss @@ -95,7 +95,7 @@ gap: 8px; padding: 0 10px; min-height: 36px; - border-radius: 10px; + border-radius: var(--app-radius-md); border: 1px solid var(--app-search-border, var(--mat-sys-outline-variant)); background: var(--app-search-bg, var(--mat-sys-surface-container)); cursor: text; diff --git a/libs/ui/components/src/lib/channel-list-loading-state/channel-list-loading-state.component.scss b/libs/ui/components/src/lib/channel-list-loading-state/channel-list-loading-state.component.scss index 3292c762c3..222d35281f 100644 --- a/libs/ui/components/src/lib/channel-list-loading-state/channel-list-loading-state.component.scss +++ b/libs/ui/components/src/lib/channel-list-loading-state/channel-list-loading-state.component.scss @@ -97,7 +97,7 @@ gap: 10px; min-height: 36px; padding: 0 10px; - border-radius: 12px; + border-radius: var(--app-radius-lg); background: color-mix( in srgb, var(--app-widget-header-bg, var(--mat-sys-surface-container-high)) 88%, @@ -115,7 +115,7 @@ .groups-loading-nav__count, .groups-loading-content__title, .groups-loading-content__subtitle { - border-radius: 999px; + border-radius: var(--app-radius-pill); } .groups-loading-nav__label { diff --git a/libs/ui/components/src/lib/content-hero/content-hero.component.scss b/libs/ui/components/src/lib/content-hero/content-hero.component.scss index 4b01a2d767..7aff4153f7 100644 --- a/libs/ui/components/src/lib/content-hero/content-hero.component.scss +++ b/libs/ui/components/src/lib/content-hero/content-hero.component.scss @@ -225,7 +225,7 @@ width: 100%; aspect-ratio: 2 / 3; object-fit: cover; - border-radius: 8px; + border-radius: var(--app-radius-sm); box-shadow: var(--poster-shadow); background: linear-gradient(135deg, #2a2a2a 0%, #1a1a1a 100%); transition: transform 0.3s ease; @@ -234,7 +234,7 @@ &__placeholder { width: 100%; aspect-ratio: 2 / 3; - border-radius: 8px; + border-radius: var(--app-radius-sm); background: linear-gradient(135deg, #3a3a3a 0%, #2a2a2a 100%); display: flex; flex-direction: column; @@ -436,7 +436,7 @@ background: #fff; color: #141414; border: none; - border-radius: 16px; + border-radius: var(--app-radius-xl); cursor: pointer; transition: all 0.2s ease; diff --git a/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.scss b/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.scss index 29158fd717..b1b4dacb9d 100644 --- a/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.scss +++ b/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.scss @@ -36,7 +36,7 @@ min-height: 48px; padding: 8px 10px; gap: 10px; - border-radius: 16px; + border-radius: var(--app-radius-xl); box-shadow: 0 12px 28px rgba(15, 23, 42, 0.16), inset 0 1px 0 rgba(255, 255, 255, 0.18); @@ -44,7 +44,7 @@ .external-playback-dock__artwork { width: 38px; height: 38px; - border-radius: 12px; + border-radius: var(--app-radius-lg); } .external-playback-dock__title { @@ -61,7 +61,7 @@ height: 48px; padding: 0; border: 0; - border-radius: 14px; + border-radius: var(--app-radius-xl); overflow: hidden; background: color-mix(in srgb, var(--mat-sys-surface-container-highest) 92%, transparent); box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.18); @@ -113,7 +113,7 @@ display: inline-flex; align-items: center; height: 18px; - border-radius: 999px; + border-radius: var(--app-radius-pill); background: color-mix( in srgb, var(--mat-sys-surface-container-highest) 80%, @@ -175,7 +175,7 @@ } .external-playback-dock__button { - border-radius: 999px; + border-radius: var(--app-radius-pill); white-space: nowrap; } diff --git a/libs/ui/components/src/lib/season-container/season-container.component.html b/libs/ui/components/src/lib/season-container/season-container.component.html index 7e7e97ce6a..ecf3a23f5a 100644 --- a/libs/ui/components/src/lib/season-container/season-container.component.html +++ b/libs/ui/components/src/lib/season-container/season-container.component.html @@ -95,7 +95,14 @@

This season is empty

@let episodeInfo = getEpisodeInfo(episode);
@let episodeInfo = getEpisodeInfo(episode);
Date: Wed, 1 Jul 2026 22:43:10 +0300 Subject: [PATCH 2/2] fix(web): stop keydown bubbling from triggering row activation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enter/Space on channel-list-item and season-container's episode cards was bound at the row/article level, so the event also fired when a descendant action button (favorite, info, aux, download, toggle-watched) had keyboard focus — pressing Space to toggle a favorite also selected/activated the row underneath it. Guard both handlers on event.target === event.currentTarget so only the row itself (not a bubbled child event) triggers activation. Also switch channel-list-item's aria-pressed to aria-current: the row isn't a toggle button, it's the current item in a list, and aria-pressed misleadingly implies it can be un-pressed. Found by Greptile review on the PR. --- .../channel-list-item.component.html | 2 +- .../channel-list-item.component.spec.ts | 17 ++++++++++++++++ .../channel-list-item.component.ts | 6 ++++++ .../season-container.component.html | 14 +++++++------ .../season-container.component.spec.ts | 20 +++++++++++++++++++ .../season-container.component.ts | 12 +++++++++++ 6 files changed, 64 insertions(+), 7 deletions(-) diff --git a/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.html b/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.html index 8d4c1a71e8..fcfdbdec3d 100644 --- a/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.html +++ b/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.html @@ -8,7 +8,7 @@ role="button" tabindex="0" [attr.aria-label]="name()" - [attr.aria-pressed]="selected()" + [attr.aria-current]="selected() || null" (click)="onClick($event)" (dblclick)="onDoubleClick()" (contextmenu)="onContextMenu($event)" diff --git a/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.spec.ts b/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.spec.ts index c489ddce76..7b613409e8 100644 --- a/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.spec.ts +++ b/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.spec.ts @@ -137,6 +137,23 @@ describe('ChannelListItemComponent', () => { expect(clicked).toHaveBeenCalledTimes(1); }); + it('does not activate the row when a keydown bubbles up from a descendant action button', () => { + fixture.componentRef.setInput('name', 'Cartoon Network'); + fixture.componentRef.setInput('showFavoriteButton', true); + fixture.detectChanges(); + + const clicked = jest.fn(); + fixture.componentInstance.clicked.subscribe(clicked); + + const favoriteButton: HTMLElement = + fixture.nativeElement.querySelector('.favorite-button'); + favoriteButton.dispatchEvent( + new KeyboardEvent('keydown', { key: ' ', bubbles: true }) + ); + + expect(clicked).not.toHaveBeenCalled(); + }); + it('emits a context menu request on right click when details are enabled', () => { fixture.componentRef.setInput('name', 'News One'); fixture.componentRef.setInput('showDetailsContextMenu', true); diff --git a/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.ts b/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.ts index 207c6d382f..0e356ea76f 100644 --- a/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.ts +++ b/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.ts @@ -97,6 +97,12 @@ export class ChannelListItemComponent { } onKeydownActivate(event: Event): void { + if (event.target !== event.currentTarget) { + // Ignore Enter/Space bubbling up from a descendant action button + // (favorite, info, aux) — it has its own click/keyboard handling. + return; + } + event.preventDefault(); this.clicked.emit(); } diff --git a/libs/ui/components/src/lib/season-container/season-container.component.html b/libs/ui/components/src/lib/season-container/season-container.component.html index ecf3a23f5a..837dd2674c 100644 --- a/libs/ui/components/src/lib/season-container/season-container.component.html +++ b/libs/ui/components/src/lib/season-container/season-container.component.html @@ -98,10 +98,11 @@

This season is empty

role="button" tabindex="0" (click)="selectEpisode(episode)" - (keydown.enter)="selectEpisode(episode)" + (keydown.enter)=" + onEpisodeKeydownActivate($event, episode) + " (keydown.space)=" - $event.preventDefault(); - selectEpisode(episode) + onEpisodeKeydownActivate($event, episode) " [class.episode-card--watched]=" isEpisodeWatched(episode) @@ -268,10 +269,11 @@

role="button" tabindex="0" (click)="selectEpisode(episode)" - (keydown.enter)="selectEpisode(episode)" + (keydown.enter)=" + onEpisodeKeydownActivate($event, episode) + " (keydown.space)=" - $event.preventDefault(); - selectEpisode(episode) + onEpisodeKeydownActivate($event, episode) " [class.episode-list-item--watched]=" isEpisodeWatched(episode) diff --git a/libs/ui/components/src/lib/season-container/season-container.component.spec.ts b/libs/ui/components/src/lib/season-container/season-container.component.spec.ts index 3788fbb4df..3465895d76 100644 --- a/libs/ui/components/src/lib/season-container/season-container.component.spec.ts +++ b/libs/ui/components/src/lib/season-container/season-container.component.spec.ts @@ -141,6 +141,26 @@ describe('SeasonContainerComponent', () => { ).toBeNull(); }); + it('does not select the episode when a keydown bubbles up from a descendant action button', () => { + const episode = createEpisode(); + setRequiredInputs({ '1': [episode] }); + fixture.detectChanges(); + + fixture.nativeElement.querySelector('.season-card').click(); + fixture.detectChanges(); + + const episodeClicked = jest.fn(); + component.episodeClicked.subscribe(episodeClicked); + + const toggleWatchedButton: HTMLElement = + fixture.nativeElement.querySelector('.toggle-watched-btn'); + toggleWatchedButton.dispatchEvent( + new KeyboardEvent('keydown', { key: ' ', bubbles: true }) + ); + + expect(episodeClicked).not.toHaveBeenCalled(); + }); + it('clears a stale selected season when the input seasons change', () => { const episode = createEpisode(); diff --git a/libs/ui/components/src/lib/season-container/season-container.component.ts b/libs/ui/components/src/lib/season-container/season-container.component.ts index 4c4a25fd01..222acfb327 100644 --- a/libs/ui/components/src/lib/season-container/season-container.component.ts +++ b/libs/ui/components/src/lib/season-container/season-container.component.ts @@ -153,6 +153,18 @@ export class SeasonContainerComponent implements OnInit, DoCheck { this.episodeClicked.emit(episode); } + onEpisodeKeydownActivate(event: Event, episode: XtreamSerieEpisode): void { + if (event.target !== event.currentTarget) { + // Ignore Enter/Space bubbling up from a descendant action button + // (download, play-local, toggle-watched) — it has its own click + // and keyboard handling. + return; + } + + event.preventDefault(); + this.selectEpisode(episode); + } + toggleWatched(event: Event, episode: XtreamSerieEpisode) { event.stopPropagation(); if (!this.playlistId()) {