diff --git a/docs/architecture/iptvnator-ui-guidelines.md b/docs/architecture/iptvnator-ui-guidelines.md index 6464751e6..3810858f0 100644 --- a/docs/architecture/iptvnator-ui-guidelines.md +++ b/docs/architecture/iptvnator-ui-guidelines.md @@ -38,6 +38,8 @@ Use it when changing existing views or introducing new list-based UI in the work `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 @@ -84,6 +86,17 @@ Use this pattern for: 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 from +`libs/ui/styles/_detail-view.scss`. Feature-local `styles/detail-view.scss` +files should only import that mixin and pass 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. diff --git a/libs/portal/shared/ui/src/lib/components/portal-collection-live-shell/portal-collection-live-shell.component.ts b/libs/portal/shared/ui/src/lib/components/portal-collection-live-shell/portal-collection-live-shell.component.ts index 55cd22831..0b0d7cb8c 100644 --- a/libs/portal/shared/ui/src/lib/components/portal-collection-live-shell/portal-collection-live-shell.component.ts +++ b/libs/portal/shared/ui/src/lib/components/portal-collection-live-shell/portal-collection-live-shell.component.ts @@ -9,7 +9,8 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { TranslatePipe } from '@ngx-translate/core'; import { ResizableDirective } from '@iptvnator/ui/components'; import { EpgItem } from '@iptvnator/shared/interfaces'; -import { EpgViewComponent, WebPlayerViewComponent } from '@iptvnator/ui/shared-portals'; +import { WebPlayerViewComponent } from '@iptvnator/ui/playback'; +import { EpgViewComponent } from '@iptvnator/ui/shared-portals'; import { PortalEmptyStateComponent } from '../portal-empty-state/portal-empty-state.component'; @Component({ diff --git a/libs/portal/shared/ui/src/lib/components/unified-collection/unified-live-tab.component.ts b/libs/portal/shared/ui/src/lib/components/unified-collection/unified-live-tab.component.ts index 201ff1815..1969c56a1 100644 --- a/libs/portal/shared/ui/src/lib/components/unified-collection/unified-live-tab.component.ts +++ b/libs/portal/shared/ui/src/lib/components/unified-collection/unified-live-tab.component.ts @@ -42,6 +42,7 @@ import { ArtPlayerComponent, HtmlVideoPlayerComponent, VjsPlayerComponent, + WebPlayerViewComponent, } from '@iptvnator/ui/playback'; import { ResizableDirective } from '@iptvnator/ui/components'; import { SettingsStore } from '@iptvnator/services'; @@ -50,7 +51,6 @@ import { EpgViewComponent, LiveEpgPanelComponent, LiveEpgPanelSummary, - WebPlayerViewComponent, } from '@iptvnator/ui/shared-portals'; @Component({ diff --git a/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.spec.ts b/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.spec.ts index 57b387b41..9c4f773f0 100644 --- a/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.spec.ts +++ b/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.spec.ts @@ -18,10 +18,10 @@ import { MockPipe } from 'ng-mocks'; import { of } from 'rxjs'; import { PlaylistsService, SettingsStore } from '@iptvnator/services'; import { EpgProgram } from '@iptvnator/shared/interfaces'; +import { WebPlayerViewComponent } from '@iptvnator/ui/playback'; import { LiveEpgPanelComponent, LiveEpgPanelSummary, - WebPlayerViewComponent, } from '@iptvnator/ui/shared-portals'; import { StalkerLiveStreamLayoutComponent } from './stalker-live-stream-layout.component'; diff --git a/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts b/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts index d250a277c..42536422f 100644 --- a/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts +++ b/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts @@ -38,12 +38,14 @@ import { getTodayEpgDateKey, shiftEpgDateKey, } from '@iptvnator/ui/epg'; -import { AudioPlayerComponent } from '@iptvnator/ui/playback'; +import { + AudioPlayerComponent, + type PlaybackFallbackRequest, + WebPlayerViewComponent, +} from '@iptvnator/ui/playback'; import { LiveEpgPanelComponent, LiveEpgPanelSummary, - type PlaybackFallbackRequest, - WebPlayerViewComponent, } from '@iptvnator/ui/shared-portals'; import { LiveLayoutSidebarStateService, @@ -236,12 +238,10 @@ export class StalkerLiveStreamLayoutComponent implements OnDestroy { private unsubscribeRemoteCommand?: () => void; private epgLoadRequestId = 0; private playbackRequestId = 0; - private playbackResolution: - | { - channelId: string; - promise: Promise; - } - | null = null; + private playbackResolution: { + channelId: string; + promise: Promise; + } | null = null; private lastPlaylistId: string | null | undefined = undefined; constructor() { @@ -497,9 +497,7 @@ export class StalkerLiveStreamLayoutComponent implements OnDestroy { } handleRadioChannelSwitch(direction: 'next' | 'previous'): void { - this.handleAdjacentChannelChange( - direction === 'next' ? 'down' : 'up' - ); + this.handleAdjacentChannelChange(direction === 'next' ? 'down' : 'up'); } @HostListener('document:keydown', ['$event']) diff --git a/libs/portal/stalker/feature/src/lib/styles/detail-view.scss b/libs/portal/stalker/feature/src/lib/styles/detail-view.scss index ef24f0ddc..d1d98703e 100644 --- a/libs/portal/stalker/feature/src/lib/styles/detail-view.scss +++ b/libs/portal/stalker/feature/src/lib/styles/detail-view.scss @@ -1,564 +1,9 @@ -// ============================================================================ -// Netflix-style Detail View - Cinematic Streaming Design -// ============================================================================ - -@use '../../../../../../ui/styles/detail-view-actions' as detail-actions; - -:host { - --poster-width: 220px; - --poster-shadow: 0 8px 40px rgba(0, 0, 0, 0.15); - --content-max-width: 1400px; - - // Light theme colors (default - no .dark-theme class on body) - --hero-gradient-start: rgba(255, 250, 245, 0); - --hero-gradient-mid: rgba(255, 250, 245, 0.75); - --hero-gradient-end: rgba(255, 250, 245, 1); - --surface-bg: #fffaf5; - --text-primary: #1a1a1a; - --text-secondary: rgba(26, 26, 26, 0.75); - --text-muted: rgba(26, 26, 26, 0.5); - --accent-color: #b81d24; - --tag-bg: rgba(0, 0, 0, 0.06); - --tag-border: rgba(0, 0, 0, 0.12); - --play-idle-bg: linear-gradient( - 135deg, - #fffaf2 0%, - #ffffff 52%, - #f7ecdf 100% - ); - --play-idle-fg: #161310; - --play-idle-border: rgba(88, 48, 18, 0.12); - --play-idle-shadow: 0 18px 38px rgba(91, 56, 24, 0.14); - --play-launching-bg: linear-gradient( - 135deg, - #ffd36f 0%, - #ffb14c 46%, - #f57d35 100% - ); - --play-launching-fg: #241103; - --play-launching-border: rgba(142, 66, 0, 0.2); - --play-launching-shadow: 0 22px 44px rgba(244, 136, 55, 0.32); - --play-stop-bg: linear-gradient( - 135deg, - #ffd1c8 0%, - #ff9f8d 42%, - #ef5e64 100% - ); - --play-stop-fg: #321012; - --play-stop-border: rgba(162, 38, 45, 0.18); - --play-stop-shadow: 0 22px 46px rgba(214, 74, 86, 0.26); - --play-icon-shell-bg: rgba(255, 255, 255, 0.32); - --play-icon-shell-border: rgba(255, 255, 255, 0.3); - - width: 100%; - height: 100%; - display: block; - overflow: hidden; - background: var(--surface-bg); - color: var(--text-primary); -} - -// Dark theme overrides - cinematic dark mode -:host-context(.dark-theme) { - --hero-gradient-start: rgba(20, 20, 20, 0); - --hero-gradient-mid: rgba(20, 20, 20, 0.7); - --hero-gradient-end: rgba(20, 20, 20, 1); - --surface-bg: #141414; - --text-primary: #fff; - --text-secondary: rgba(255, 255, 255, 0.7); - --text-muted: rgba(255, 255, 255, 0.5); - --accent-color: #e50914; - --tag-bg: rgba(255, 255, 255, 0.1); - --tag-border: rgba(255, 255, 255, 0.2); - --poster-shadow: 0 8px 40px rgba(0, 0, 0, 0.5); - --play-idle-bg: linear-gradient( - 135deg, - #fff5e6 0%, - #f6efe5 58%, - #e7dac9 100% - ); - --play-idle-fg: #15110d; - --play-idle-border: rgba(255, 255, 255, 0.18); - --play-idle-shadow: 0 20px 42px rgba(0, 0, 0, 0.36); - --play-launching-bg: linear-gradient( - 135deg, - #ffdd79 0%, - #ffb347 42%, - #ef6a3a 100% - ); - --play-launching-fg: #1f1105; - --play-launching-border: rgba(255, 223, 136, 0.26); - --play-launching-shadow: 0 24px 50px rgba(255, 153, 71, 0.3); - --play-stop-bg: linear-gradient( - 135deg, - #ffd7cf 0%, - #ff9b8b 40%, - #eb5961 100% - ); - --play-stop-fg: #2d1012; - --play-stop-border: rgba(255, 205, 205, 0.24); - --play-stop-shadow: 0 24px 50px rgba(235, 89, 97, 0.24); - --play-icon-shell-bg: rgba(255, 255, 255, 0.28); - --play-icon-shell-border: rgba(255, 255, 255, 0.24); -} - -// ============================================================================ -// Details Section - Content Elements -// ============================================================================ - -.details { - // Styles for projected content elements - - &__tag { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 4px 12px; - font-size: 0.8125rem; - font-weight: 500; - background: var(--tag-bg); - border: 1px solid var(--tag-border); - border-radius: 4px; - backdrop-filter: blur(8px); - - &--rating { - color: #ffd700; - border-color: rgba(255, 215, 0, 0.3); - } - } - - &__meta-item { - font-size: 0.875rem; - - .label { - color: var(--text-muted); - font-weight: 400; - margin-bottom: 2px; - text-decoration: none; - display: block; - } - - .value { - color: var(--text-primary); - font-weight: 500; - } - } -} - -// ============================================================================ -// Action Buttons - Play, Favorite -// ============================================================================ - -.play-btn { - --play-btn-bg: var(--play-idle-bg); - --play-btn-fg: var(--play-idle-fg); - --play-btn-border: var(--play-idle-border); - --play-btn-shadow: var(--play-idle-shadow); - - position: relative; - isolation: isolate; - overflow: hidden; - display: inline-flex; - align-items: center; - gap: 12px; - padding: 10px 24px 10px 18px; - min-height: 64px; - font-size: 1rem; - font-weight: 600; - letter-spacing: 0; - background: var(--play-btn-bg); - background-size: 140% 140%; - background-position: 0% 50%; - color: var(--play-btn-fg); - border: 1px solid var(--play-btn-border); - border-radius: 16px; - box-shadow: var(--play-btn-shadow); - cursor: pointer; - transition: - transform 220ms cubic-bezier(0.22, 1, 0.36, 1), - background-position 280ms ease, - box-shadow 260ms ease, - border-color 220ms ease, - color 220ms ease, - filter 220ms ease; - - &::before { - content: ''; - position: absolute; - inset: 1px; - border-radius: inherit; - background: - linear-gradient(115deg, rgba(255, 255, 255, 0.34), transparent 36%), - linear-gradient(180deg, rgba(255, 255, 255, 0.18), transparent 62%); - opacity: 0.7; - transition: opacity 220ms ease; - z-index: -1; - } - - &:hover:not(:disabled) { - transform: translateY(-2px); - background-position: 100% 50%; - box-shadow: - var(--play-btn-shadow), - 0 10px 20px rgba(255, 255, 255, 0.16) inset; - } - - &:hover:not(:disabled)::before { - opacity: 0.95; - } - - &:active:not(:disabled) { - transform: translateY(0); - box-shadow: var(--play-btn-shadow); - } - - &:disabled { - cursor: progress; - opacity: 1; - filter: saturate(1.05); - } - - mat-icon { - font-size: 24px; - width: 24px; - height: 24px; - transition: - transform 220ms cubic-bezier(0.22, 1, 0.36, 1), - color 220ms ease, - filter 220ms ease; - } - - &:hover:not(:disabled) .play-btn__icon { - transform: scale(1.08); - } - - &__icon-shell { - flex: 0 0 auto; - display: inline-flex; - align-items: center; - justify-content: center; - width: 34px; - height: 34px; - border-radius: 999px; - background: var(--play-icon-shell-bg); - border: 1px solid var(--play-icon-shell-border); - box-shadow: - inset 0 1px 0 rgba(255, 255, 255, 0.38), - 0 6px 14px rgba(0, 0, 0, 0.08); - backdrop-filter: blur(10px); - transition: - transform 220ms ease, - background-color 220ms ease, - border-color 220ms ease, - box-shadow 220ms ease; - } - - @include detail-actions.play-button-copy(var(--play-btn-fg)); - - &--state-launching, - &--state-stop { - color: var(--play-btn-fg); - } - - &--state-launching { - --play-btn-bg: var(--play-launching-bg); - --play-btn-fg: var(--play-launching-fg); - --play-btn-border: var(--play-launching-border); - --play-btn-shadow: var(--play-launching-shadow); - animation: playButtonPulse 1.8s ease-in-out infinite; - - .play-btn__icon-shell { - box-shadow: - inset 0 1px 0 rgba(255, 255, 255, 0.42), - 0 10px 22px rgba(184, 89, 26, 0.2); - } - - .play-btn__icon { - animation: playIconLaunch 1.15s cubic-bezier(0.55, 0, 0.45, 1) - infinite; - } - } - - &--state-stop { - --play-btn-bg: var(--play-stop-bg); - --play-btn-fg: var(--play-stop-fg); - --play-btn-border: var(--play-stop-border); - --play-btn-shadow: var(--play-stop-shadow); - - .play-btn__icon-shell { - box-shadow: - inset 0 1px 0 rgba(255, 255, 255, 0.4), - 0 12px 24px rgba(145, 38, 53, 0.18); - } - - .play-btn__icon { - animation: playIconStop 1.2s ease-in-out infinite; - } - } -} - -@keyframes playButtonPulse { - 0%, - 100% { - box-shadow: var(--play-btn-shadow); - } - - 50% { - box-shadow: - var(--play-btn-shadow), - 0 0 0 6px rgba(255, 190, 92, 0.14); - } -} - -@keyframes playIconLaunch { - 0%, - 100% { - transform: rotate(0deg) scale(1); - } - - 50% { - transform: rotate(18deg) scale(1.1); - } -} - -@keyframes playIconStop { - 0%, - 100% { - transform: scale(1); - } - - 50% { - transform: scale(1.08); - } -} - -@media (prefers-reduced-motion: reduce) { - .play-btn, - .play-btn::before, - .play-btn mat-icon, - .play-btn__icon-shell { - animation: none !important; - transition-duration: 1ms !important; - } -} - -.favorite-btn, -.download-btn { - @include detail-actions.secondary-action-button; -} - -.favorite-btn { - @include detail-actions.favorite-button-state; -} - -.download-btn { - @include detail-actions.download-button-state; -} - -// ============================================================================ -// Content Sections - Below the hero fold -// ============================================================================ - -.content-section { - padding: 20px 0; - width: 100%; - box-sizing: border-box; - - &__title { - font-size: 1.2rem; - font-weight: 650; - letter-spacing: -0.02em; - margin: 0 0 20px 0; - color: var(--text-primary); - } -} - -// ============================================================================ -// Seasons & Episodes - Inside details section -// ============================================================================ - -.seasons { - margin-top: 24px; - width: 100%; - - .season-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 12px 0; - cursor: pointer; - transition: opacity 0.2s ease; - - &:hover { - opacity: 0.8; - } - - h4 { - margin: 0; - font-size: 1rem; - font-weight: 600; - } - - .expand-icon { - font-size: 0.75rem; - color: var(--text-muted); - } - } - - .episodes { - display: flex; - flex-wrap: wrap; - gap: 10px; - padding: 8px 0 16px; - - button { - margin: 0; - font-weight: 500; - } - } - - .episodes-loading, - .seasons-loading { - display: flex; - align-items: center; - gap: 12px; - padding: 20px 0; - color: var(--text-secondary); - } - - .no-episodes { - color: var(--text-muted); - font-style: italic; - padding: 8px 0; - } -} - -// Seasons loading when inside details -.details .seasons-loading { - display: flex; - align-items: center; - gap: 12px; - padding: 20px 0; - color: var(--text-secondary); - margin-top: 24px; -} - -// Season container when inside details section -.details app-season-container { - display: block; - width: 100%; - margin-top: 24px; - box-sizing: border-box; -} - -// ============================================================================ -// YouTube Trailer Section - Inside details -// ============================================================================ - -.youtube-trailer { - margin-top: 24px; - width: 100%; - - h3 { - font-size: 1rem; - font-weight: 600; - letter-spacing: -0.01em; - margin: 0 0 12px 0; - color: var(--text-primary); - } - - .trailer-wrapper { - position: relative; - width: 100%; - max-width: 560px; - aspect-ratio: 16 / 9; - border-radius: 8px; - overflow: hidden; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); - - iframe { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - border: none; - } - } -} - -// ============================================================================ -// Legacy container support (for gradual migration) -// ============================================================================ - -.container { - display: contents; -} - -.image { - display: none; // Hidden - now using .poster in hero -} - -// ============================================================================ -// Mobile Responsive - Option A: Keep poster grid layout -// ============================================================================ - -@media (max-width: 800px) { - :host { - --poster-width: 120px; - } - - .action-buttons { - width: 100%; - // This class is in ContentHeroComponent now, but these rules apply to children? - // No, .play-btn is a child of the PROJECTED content. - // So we should target the buttons directly for mobile - } - - @include detail-actions.mobile-action-buttons; - - .youtube-trailer { - margin-top: 16px; - - .trailer-wrapper { - max-width: 100%; - border-radius: 4px; - } - } - - .seasons { - padding: 16px 0 0; - } -} - -// ============================================================================ -// Animations -// ============================================================================ - -.youtube-trailer { - animation: fadeInUp 0.5s ease-out 0.4s both; -} - -@keyframes fadeInUp { - from { - opacity: 0; - transform: translateY(20px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -// ============================================================================ -// Utility - keep .label for backwards compatibility -// ============================================================================ - -.label { - font-weight: 400; - font-size: 0.8125rem; - color: var(--text-muted); - text-decoration: none; - padding: 0; - opacity: 1; -} +@use '../../../../../../ui/styles/detail-view' as detail-view; + +@include detail-view.base( + $season-title-font-size: 1.2rem, + $season-title-font-weight: 650, + $season-title-letter-spacing: -0.02em, + $episode-title-font-size: 1rem, + $episode-title-letter-spacing: -0.01em +); diff --git a/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.spec.ts b/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.spec.ts index c8d93eefd..93ba11174 100644 --- a/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.spec.ts +++ b/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.spec.ts @@ -19,11 +19,11 @@ import { XtreamUrlService, } from '@iptvnator/portal/xtream/data-access'; import { EpgListComponent, EpgProgramActivationEvent } from '@iptvnator/ui/epg'; +import { WebPlayerViewComponent } from '@iptvnator/ui/playback'; import { EpgViewComponent, LiveEpgPanelComponent, LiveEpgPanelSummary, - WebPlayerViewComponent, } from '@iptvnator/ui/shared-portals'; import { EpgItem, EpgProgram } from '@iptvnator/shared/interfaces'; import { PortalChannelsListComponent } from '../portal-channels-list/portal-channels-list.component'; diff --git a/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.ts b/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.ts index 0cff81747..d7b6cf7a9 100644 --- a/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.ts +++ b/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.ts @@ -47,14 +47,20 @@ import { shiftEpgDateKey, } from '@iptvnator/ui/epg'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { + type PlaybackFallbackRequest, + WebPlayerViewComponent, +} from '@iptvnator/ui/playback'; import { EpgViewComponent, LiveEpgPanelComponent, LiveEpgPanelSummary, - type PlaybackFallbackRequest, - WebPlayerViewComponent, } from '@iptvnator/ui/shared-portals'; -import { EpgItem, EpgProgram, ResolvedPortalPlayback } from '@iptvnator/shared/interfaces'; +import { + EpgItem, + EpgProgram, + ResolvedPortalPlayback, +} from '@iptvnator/shared/interfaces'; import { PortalChannelsListComponent } from '../portal-channels-list/portal-channels-list.component'; import { ActivatedRoute } from '@angular/router'; import { SettingsStore } from '@iptvnator/services'; diff --git a/libs/ui/components/src/lib/styles/detail-view.scss b/libs/ui/components/src/lib/styles/detail-view.scss index e59fb9bb5..0096a387f 100644 --- a/libs/ui/components/src/lib/styles/detail-view.scss +++ b/libs/ui/components/src/lib/styles/detail-view.scss @@ -1,562 +1,3 @@ -// ============================================================================ -// Netflix-style Detail View - Cinematic Streaming Design -// ============================================================================ +@use '../../../../styles/detail-view' as detail-view; -@use '../../../../styles/detail-view-actions' as detail-actions; - -:host { - --poster-width: 220px; - --poster-shadow: 0 8px 40px rgba(0, 0, 0, 0.15); - --content-max-width: 1400px; - - // Light theme colors (default - no .dark-theme class on body) - --hero-gradient-start: rgba(255, 250, 245, 0); - --hero-gradient-mid: rgba(255, 250, 245, 0.75); - --hero-gradient-end: rgba(255, 250, 245, 1); - --surface-bg: #fffaf5; - --text-primary: #1a1a1a; - --text-secondary: rgba(26, 26, 26, 0.75); - --text-muted: rgba(26, 26, 26, 0.5); - --accent-color: #b81d24; - --tag-bg: rgba(0, 0, 0, 0.06); - --tag-border: rgba(0, 0, 0, 0.12); - --play-idle-bg: linear-gradient( - 135deg, - #fffaf2 0%, - #ffffff 52%, - #f7ecdf 100% - ); - --play-idle-fg: #161310; - --play-idle-border: rgba(88, 48, 18, 0.12); - --play-idle-shadow: 0 18px 38px rgba(91, 56, 24, 0.14); - --play-launching-bg: linear-gradient( - 135deg, - #ffd36f 0%, - #ffb14c 46%, - #f57d35 100% - ); - --play-launching-fg: #241103; - --play-launching-border: rgba(142, 66, 0, 0.2); - --play-launching-shadow: 0 22px 44px rgba(244, 136, 55, 0.32); - --play-stop-bg: linear-gradient( - 135deg, - #ffd1c8 0%, - #ff9f8d 42%, - #ef5e64 100% - ); - --play-stop-fg: #321012; - --play-stop-border: rgba(162, 38, 45, 0.18); - --play-stop-shadow: 0 22px 46px rgba(214, 74, 86, 0.26); - --play-icon-shell-bg: rgba(255, 255, 255, 0.32); - --play-icon-shell-border: rgba(255, 255, 255, 0.3); - - width: 100%; - height: 100%; - display: block; - overflow: hidden; - background: var(--surface-bg); - color: var(--text-primary); -} - -// Dark theme overrides - cinematic dark mode -:host-context(.dark-theme) { - --hero-gradient-start: rgba(20, 20, 20, 0); - --hero-gradient-mid: rgba(20, 20, 20, 0.7); - --hero-gradient-end: rgba(20, 20, 20, 1); - --surface-bg: #141414; - --text-primary: #fff; - --text-secondary: rgba(255, 255, 255, 0.7); - --text-muted: rgba(255, 255, 255, 0.5); - --accent-color: #e50914; - --tag-bg: rgba(255, 255, 255, 0.1); - --tag-border: rgba(255, 255, 255, 0.2); - --poster-shadow: 0 8px 40px rgba(0, 0, 0, 0.5); - --play-idle-bg: linear-gradient( - 135deg, - #fff5e6 0%, - #f6efe5 58%, - #e7dac9 100% - ); - --play-idle-fg: #15110d; - --play-idle-border: rgba(255, 255, 255, 0.18); - --play-idle-shadow: 0 20px 42px rgba(0, 0, 0, 0.36); - --play-launching-bg: linear-gradient( - 135deg, - #ffdd79 0%, - #ffb347 42%, - #ef6a3a 100% - ); - --play-launching-fg: #1f1105; - --play-launching-border: rgba(255, 223, 136, 0.26); - --play-launching-shadow: 0 24px 50px rgba(255, 153, 71, 0.3); - --play-stop-bg: linear-gradient( - 135deg, - #ffd7cf 0%, - #ff9b8b 40%, - #eb5961 100% - ); - --play-stop-fg: #2d1012; - --play-stop-border: rgba(255, 205, 205, 0.24); - --play-stop-shadow: 0 24px 50px rgba(235, 89, 97, 0.24); - --play-icon-shell-bg: rgba(255, 255, 255, 0.28); - --play-icon-shell-border: rgba(255, 255, 255, 0.24); -} - -// ============================================================================ -// Details Section - Content Elements -// ============================================================================ - -.details { - // Styles for projected content elements - - &__tag { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 4px 12px; - font-size: 0.8125rem; - font-weight: 500; - background: var(--tag-bg); - border: 1px solid var(--tag-border); - border-radius: 4px; - backdrop-filter: blur(8px); - - &--rating { - color: #ffd700; - border-color: rgba(255, 215, 0, 0.3); - } - } - - &__meta-item { - font-size: 0.875rem; - - .label { - color: var(--text-muted); - font-weight: 400; - margin-bottom: 2px; - text-decoration: none; - display: block; - } - - .value { - color: var(--text-primary); - font-weight: 500; - } - } -} - -// ============================================================================ -// Action Buttons - Play, Favorite -// ============================================================================ - -.play-btn { - --play-btn-bg: var(--play-idle-bg); - --play-btn-fg: var(--play-idle-fg); - --play-btn-border: var(--play-idle-border); - --play-btn-shadow: var(--play-idle-shadow); - - position: relative; - isolation: isolate; - overflow: hidden; - display: inline-flex; - align-items: center; - gap: 12px; - padding: 10px 24px 10px 18px; - min-height: 64px; - font-size: 1rem; - font-weight: 600; - letter-spacing: 0; - background: var(--play-btn-bg); - background-size: 140% 140%; - background-position: 0% 50%; - color: var(--play-btn-fg); - border: 1px solid var(--play-btn-border); - border-radius: 16px; - box-shadow: var(--play-btn-shadow); - cursor: pointer; - transition: - transform 220ms cubic-bezier(0.22, 1, 0.36, 1), - background-position 280ms ease, - box-shadow 260ms ease, - border-color 220ms ease, - color 220ms ease, - filter 220ms ease; - - &::before { - content: ''; - position: absolute; - inset: 1px; - border-radius: inherit; - background: - linear-gradient(115deg, rgba(255, 255, 255, 0.34), transparent 36%), - linear-gradient(180deg, rgba(255, 255, 255, 0.18), transparent 62%); - opacity: 0.7; - transition: opacity 220ms ease; - z-index: -1; - } - - &:hover:not(:disabled) { - transform: translateY(-2px); - background-position: 100% 50%; - box-shadow: - var(--play-btn-shadow), - 0 10px 20px rgba(255, 255, 255, 0.16) inset; - } - - &:hover:not(:disabled)::before { - opacity: 0.95; - } - - &:active:not(:disabled) { - transform: translateY(0); - box-shadow: var(--play-btn-shadow); - } - - &:disabled { - cursor: progress; - opacity: 1; - filter: saturate(1.05); - } - - mat-icon { - font-size: 24px; - width: 24px; - height: 24px; - transition: - transform 220ms cubic-bezier(0.22, 1, 0.36, 1), - color 220ms ease, - filter 220ms ease; - } - - &:hover:not(:disabled) .play-btn__icon { - transform: scale(1.08); - } - - &__icon-shell { - flex: 0 0 auto; - display: inline-flex; - align-items: center; - justify-content: center; - width: 34px; - height: 34px; - border-radius: 999px; - background: var(--play-icon-shell-bg); - border: 1px solid var(--play-icon-shell-border); - box-shadow: - inset 0 1px 0 rgba(255, 255, 255, 0.38), - 0 6px 14px rgba(0, 0, 0, 0.08); - backdrop-filter: blur(10px); - transition: - transform 220ms ease, - background-color 220ms ease, - border-color 220ms ease, - box-shadow 220ms ease; - } - - @include detail-actions.play-button-copy(var(--play-btn-fg)); - - &--state-launching, - &--state-stop { - color: var(--play-btn-fg); - } - - &--state-launching { - --play-btn-bg: var(--play-launching-bg); - --play-btn-fg: var(--play-launching-fg); - --play-btn-border: var(--play-launching-border); - --play-btn-shadow: var(--play-launching-shadow); - animation: playButtonPulse 1.8s ease-in-out infinite; - - .play-btn__icon-shell { - box-shadow: - inset 0 1px 0 rgba(255, 255, 255, 0.42), - 0 10px 22px rgba(184, 89, 26, 0.2); - } - - .play-btn__icon { - animation: playIconLaunch 1.15s cubic-bezier(0.55, 0, 0.45, 1) - infinite; - } - } - - &--state-stop { - --play-btn-bg: var(--play-stop-bg); - --play-btn-fg: var(--play-stop-fg); - --play-btn-border: var(--play-stop-border); - --play-btn-shadow: var(--play-stop-shadow); - - .play-btn__icon-shell { - box-shadow: - inset 0 1px 0 rgba(255, 255, 255, 0.4), - 0 12px 24px rgba(145, 38, 53, 0.18); - } - - .play-btn__icon { - animation: playIconStop 1.2s ease-in-out infinite; - } - } -} - -@keyframes playButtonPulse { - 0%, - 100% { - box-shadow: var(--play-btn-shadow); - } - - 50% { - box-shadow: - var(--play-btn-shadow), - 0 0 0 6px rgba(255, 190, 92, 0.14); - } -} - -@keyframes playIconLaunch { - 0%, - 100% { - transform: rotate(0deg) scale(1); - } - - 50% { - transform: rotate(18deg) scale(1.1); - } -} - -@keyframes playIconStop { - 0%, - 100% { - transform: scale(1); - } - - 50% { - transform: scale(1.08); - } -} - -@media (prefers-reduced-motion: reduce) { - .play-btn, - .play-btn::before, - .play-btn mat-icon, - .play-btn__icon-shell { - animation: none !important; - transition-duration: 1ms !important; - } -} - -.favorite-btn, -.download-btn { - @include detail-actions.secondary-action-button; -} - -.favorite-btn { - @include detail-actions.favorite-button-state; -} - -.download-btn { - @include detail-actions.download-button-state; -} - -// ============================================================================ -// Content Sections - Below the hero fold -// ============================================================================ - -.content-section { - padding: 20px 0; - width: 100%; - box-sizing: border-box; - - &__title { - font-size: 1.375rem; - font-weight: 600; - margin: 0 0 20px 0; - color: var(--text-primary); - } -} - -// ============================================================================ -// Seasons & Episodes - Inside details section -// ============================================================================ - -.seasons { - margin-top: 24px; - width: 100%; - - .season-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 12px 0; - cursor: pointer; - transition: opacity 0.2s ease; - - &:hover { - opacity: 0.8; - } - - h4 { - margin: 0; - font-size: 1rem; - font-weight: 600; - } - - .expand-icon { - font-size: 0.75rem; - color: var(--text-muted); - } - } - - .episodes { - display: flex; - flex-wrap: wrap; - gap: 10px; - padding: 8px 0 16px; - - button { - margin: 0; - font-weight: 500; - } - } - - .episodes-loading, - .seasons-loading { - display: flex; - align-items: center; - gap: 12px; - padding: 20px 0; - color: var(--text-secondary); - } - - .no-episodes { - color: var(--text-muted); - font-style: italic; - padding: 8px 0; - } -} - -// Seasons loading when inside details -.details .seasons-loading { - display: flex; - align-items: center; - gap: 12px; - padding: 20px 0; - color: var(--text-secondary); - margin-top: 24px; -} - -// Season container when inside details section -.details app-season-container { - display: block; - width: 100%; - margin-top: 24px; - box-sizing: border-box; -} - -// ============================================================================ -// YouTube Trailer Section - Inside details -// ============================================================================ - -.youtube-trailer { - margin-top: 24px; - width: 100%; - - h3 { - font-size: 1.125rem; - font-weight: 600; - margin: 0 0 12px 0; - color: var(--text-primary); - } - - .trailer-wrapper { - position: relative; - width: 100%; - max-width: 560px; - aspect-ratio: 16 / 9; - border-radius: 8px; - overflow: hidden; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); - - iframe { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - border: none; - } - } -} - -// ============================================================================ -// Legacy container support (for gradual migration) -// ============================================================================ - -.container { - display: contents; -} - -.image { - display: none; // Hidden - now using .poster in hero -} - -// ============================================================================ -// Mobile Responsive - Option A: Keep poster grid layout -// ============================================================================ - -@media (max-width: 800px) { - :host { - --poster-width: 120px; - } - - .action-buttons { - width: 100%; - // This class is in ContentHeroComponent now, but these rules apply to children? - // No, .play-btn is a child of the PROJECTED content. - // So we should target the buttons directly for mobile - } - - @include detail-actions.mobile-action-buttons; - - .youtube-trailer { - margin-top: 16px; - - .trailer-wrapper { - max-width: 100%; - border-radius: 4px; - } - } - - .seasons { - padding: 16px 0 0; - } -} - -// ============================================================================ -// Animations -// ============================================================================ - -.youtube-trailer { - animation: fadeInUp 0.5s ease-out 0.4s both; -} - -@keyframes fadeInUp { - from { - opacity: 0; - transform: translateY(20px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -// ============================================================================ -// Utility - keep .label for backwards compatibility -// ============================================================================ - -.label { - font-weight: 400; - font-size: 0.8125rem; - color: var(--text-muted); - text-decoration: none; - padding: 0; - opacity: 1; -} +@include detail-view.base(); diff --git a/libs/ui/pipes/src/index.ts b/libs/ui/pipes/src/index.ts index 627599fa1..020926d58 100644 --- a/libs/ui/pipes/src/index.ts +++ b/libs/ui/pipes/src/index.ts @@ -1,4 +1,2 @@ -export * from './lib/filter.pipe'; export * from './lib/date-format.util'; export * from './lib/safe.pipe'; -export * from './lib/sort.pipe'; diff --git a/libs/ui/pipes/src/lib/filter.pipe.ts b/libs/ui/pipes/src/lib/filter.pipe.ts deleted file mode 100644 index 0f6fc1f6f..000000000 --- a/libs/ui/pipes/src/lib/filter.pipe.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Injectable, Pipe, PipeTransform } from '@angular/core'; - -@Pipe({ - name: 'filterBy', - standalone: true, -}) -@Injectable() -export class FilterPipe implements PipeTransform { - transform(array: any[], filter: string, property: string): any { - if (!array || !filter) { - return array; - } - return array.filter((item) => - item[property].toLowerCase().includes(filter.toLowerCase()) - ); - } -} diff --git a/libs/ui/pipes/src/lib/safe.pipe.ts b/libs/ui/pipes/src/lib/safe.pipe.ts index d91d2fcec..50e2e6e3c 100644 --- a/libs/ui/pipes/src/lib/safe.pipe.ts +++ b/libs/ui/pipes/src/lib/safe.pipe.ts @@ -1,9 +1,9 @@ -import { Pipe, PipeTransform } from '@angular/core'; +import { inject, Pipe, PipeTransform } from '@angular/core'; import { DomSanitizer } from '@angular/platform-browser'; @Pipe({ name: 'safe', standalone: true }) export class SafePipe implements PipeTransform { - constructor(private sanitizer: DomSanitizer) {} + private readonly sanitizer = inject(DomSanitizer); transform(url: string) { return this.sanitizer.bypassSecurityTrustResourceUrl(url); diff --git a/libs/ui/pipes/src/lib/sort.pipe.ts b/libs/ui/pipes/src/lib/sort.pipe.ts deleted file mode 100644 index 7cd5a73c0..000000000 --- a/libs/ui/pipes/src/lib/sort.pipe.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Injectable, Pipe, PipeTransform } from '@angular/core'; - -@Pipe({ - name: 'sortBy', - standalone: true, -}) -@Injectable() -export class SortPipe implements PipeTransform { - transform(array: any[], sortType: string): any { - if (!array || !sortType) { - return array; - } - return array.sort((a, b) => { - if (sortType === 'date') { - return parseInt(b.added) - parseInt(a.added); - } else if (sortType === 'rating') { - const ratingA = isNaN(parseFloat(a.rating)) ? 0 : parseFloat(a.rating); - const ratingB = isNaN(parseFloat(b.rating)) ? 0 : parseFloat(b.rating); - return ratingB - ratingA; - } else if (sortType === 'alpha') { - return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); - } else { - return 0; - } - }); - } -} diff --git a/libs/ui/playback/src/lib/styles/detail-view.scss b/libs/ui/playback/src/lib/styles/detail-view.scss index e59fb9bb5..0096a387f 100644 --- a/libs/ui/playback/src/lib/styles/detail-view.scss +++ b/libs/ui/playback/src/lib/styles/detail-view.scss @@ -1,562 +1,3 @@ -// ============================================================================ -// Netflix-style Detail View - Cinematic Streaming Design -// ============================================================================ +@use '../../../../styles/detail-view' as detail-view; -@use '../../../../styles/detail-view-actions' as detail-actions; - -:host { - --poster-width: 220px; - --poster-shadow: 0 8px 40px rgba(0, 0, 0, 0.15); - --content-max-width: 1400px; - - // Light theme colors (default - no .dark-theme class on body) - --hero-gradient-start: rgba(255, 250, 245, 0); - --hero-gradient-mid: rgba(255, 250, 245, 0.75); - --hero-gradient-end: rgba(255, 250, 245, 1); - --surface-bg: #fffaf5; - --text-primary: #1a1a1a; - --text-secondary: rgba(26, 26, 26, 0.75); - --text-muted: rgba(26, 26, 26, 0.5); - --accent-color: #b81d24; - --tag-bg: rgba(0, 0, 0, 0.06); - --tag-border: rgba(0, 0, 0, 0.12); - --play-idle-bg: linear-gradient( - 135deg, - #fffaf2 0%, - #ffffff 52%, - #f7ecdf 100% - ); - --play-idle-fg: #161310; - --play-idle-border: rgba(88, 48, 18, 0.12); - --play-idle-shadow: 0 18px 38px rgba(91, 56, 24, 0.14); - --play-launching-bg: linear-gradient( - 135deg, - #ffd36f 0%, - #ffb14c 46%, - #f57d35 100% - ); - --play-launching-fg: #241103; - --play-launching-border: rgba(142, 66, 0, 0.2); - --play-launching-shadow: 0 22px 44px rgba(244, 136, 55, 0.32); - --play-stop-bg: linear-gradient( - 135deg, - #ffd1c8 0%, - #ff9f8d 42%, - #ef5e64 100% - ); - --play-stop-fg: #321012; - --play-stop-border: rgba(162, 38, 45, 0.18); - --play-stop-shadow: 0 22px 46px rgba(214, 74, 86, 0.26); - --play-icon-shell-bg: rgba(255, 255, 255, 0.32); - --play-icon-shell-border: rgba(255, 255, 255, 0.3); - - width: 100%; - height: 100%; - display: block; - overflow: hidden; - background: var(--surface-bg); - color: var(--text-primary); -} - -// Dark theme overrides - cinematic dark mode -:host-context(.dark-theme) { - --hero-gradient-start: rgba(20, 20, 20, 0); - --hero-gradient-mid: rgba(20, 20, 20, 0.7); - --hero-gradient-end: rgba(20, 20, 20, 1); - --surface-bg: #141414; - --text-primary: #fff; - --text-secondary: rgba(255, 255, 255, 0.7); - --text-muted: rgba(255, 255, 255, 0.5); - --accent-color: #e50914; - --tag-bg: rgba(255, 255, 255, 0.1); - --tag-border: rgba(255, 255, 255, 0.2); - --poster-shadow: 0 8px 40px rgba(0, 0, 0, 0.5); - --play-idle-bg: linear-gradient( - 135deg, - #fff5e6 0%, - #f6efe5 58%, - #e7dac9 100% - ); - --play-idle-fg: #15110d; - --play-idle-border: rgba(255, 255, 255, 0.18); - --play-idle-shadow: 0 20px 42px rgba(0, 0, 0, 0.36); - --play-launching-bg: linear-gradient( - 135deg, - #ffdd79 0%, - #ffb347 42%, - #ef6a3a 100% - ); - --play-launching-fg: #1f1105; - --play-launching-border: rgba(255, 223, 136, 0.26); - --play-launching-shadow: 0 24px 50px rgba(255, 153, 71, 0.3); - --play-stop-bg: linear-gradient( - 135deg, - #ffd7cf 0%, - #ff9b8b 40%, - #eb5961 100% - ); - --play-stop-fg: #2d1012; - --play-stop-border: rgba(255, 205, 205, 0.24); - --play-stop-shadow: 0 24px 50px rgba(235, 89, 97, 0.24); - --play-icon-shell-bg: rgba(255, 255, 255, 0.28); - --play-icon-shell-border: rgba(255, 255, 255, 0.24); -} - -// ============================================================================ -// Details Section - Content Elements -// ============================================================================ - -.details { - // Styles for projected content elements - - &__tag { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 4px 12px; - font-size: 0.8125rem; - font-weight: 500; - background: var(--tag-bg); - border: 1px solid var(--tag-border); - border-radius: 4px; - backdrop-filter: blur(8px); - - &--rating { - color: #ffd700; - border-color: rgba(255, 215, 0, 0.3); - } - } - - &__meta-item { - font-size: 0.875rem; - - .label { - color: var(--text-muted); - font-weight: 400; - margin-bottom: 2px; - text-decoration: none; - display: block; - } - - .value { - color: var(--text-primary); - font-weight: 500; - } - } -} - -// ============================================================================ -// Action Buttons - Play, Favorite -// ============================================================================ - -.play-btn { - --play-btn-bg: var(--play-idle-bg); - --play-btn-fg: var(--play-idle-fg); - --play-btn-border: var(--play-idle-border); - --play-btn-shadow: var(--play-idle-shadow); - - position: relative; - isolation: isolate; - overflow: hidden; - display: inline-flex; - align-items: center; - gap: 12px; - padding: 10px 24px 10px 18px; - min-height: 64px; - font-size: 1rem; - font-weight: 600; - letter-spacing: 0; - background: var(--play-btn-bg); - background-size: 140% 140%; - background-position: 0% 50%; - color: var(--play-btn-fg); - border: 1px solid var(--play-btn-border); - border-radius: 16px; - box-shadow: var(--play-btn-shadow); - cursor: pointer; - transition: - transform 220ms cubic-bezier(0.22, 1, 0.36, 1), - background-position 280ms ease, - box-shadow 260ms ease, - border-color 220ms ease, - color 220ms ease, - filter 220ms ease; - - &::before { - content: ''; - position: absolute; - inset: 1px; - border-radius: inherit; - background: - linear-gradient(115deg, rgba(255, 255, 255, 0.34), transparent 36%), - linear-gradient(180deg, rgba(255, 255, 255, 0.18), transparent 62%); - opacity: 0.7; - transition: opacity 220ms ease; - z-index: -1; - } - - &:hover:not(:disabled) { - transform: translateY(-2px); - background-position: 100% 50%; - box-shadow: - var(--play-btn-shadow), - 0 10px 20px rgba(255, 255, 255, 0.16) inset; - } - - &:hover:not(:disabled)::before { - opacity: 0.95; - } - - &:active:not(:disabled) { - transform: translateY(0); - box-shadow: var(--play-btn-shadow); - } - - &:disabled { - cursor: progress; - opacity: 1; - filter: saturate(1.05); - } - - mat-icon { - font-size: 24px; - width: 24px; - height: 24px; - transition: - transform 220ms cubic-bezier(0.22, 1, 0.36, 1), - color 220ms ease, - filter 220ms ease; - } - - &:hover:not(:disabled) .play-btn__icon { - transform: scale(1.08); - } - - &__icon-shell { - flex: 0 0 auto; - display: inline-flex; - align-items: center; - justify-content: center; - width: 34px; - height: 34px; - border-radius: 999px; - background: var(--play-icon-shell-bg); - border: 1px solid var(--play-icon-shell-border); - box-shadow: - inset 0 1px 0 rgba(255, 255, 255, 0.38), - 0 6px 14px rgba(0, 0, 0, 0.08); - backdrop-filter: blur(10px); - transition: - transform 220ms ease, - background-color 220ms ease, - border-color 220ms ease, - box-shadow 220ms ease; - } - - @include detail-actions.play-button-copy(var(--play-btn-fg)); - - &--state-launching, - &--state-stop { - color: var(--play-btn-fg); - } - - &--state-launching { - --play-btn-bg: var(--play-launching-bg); - --play-btn-fg: var(--play-launching-fg); - --play-btn-border: var(--play-launching-border); - --play-btn-shadow: var(--play-launching-shadow); - animation: playButtonPulse 1.8s ease-in-out infinite; - - .play-btn__icon-shell { - box-shadow: - inset 0 1px 0 rgba(255, 255, 255, 0.42), - 0 10px 22px rgba(184, 89, 26, 0.2); - } - - .play-btn__icon { - animation: playIconLaunch 1.15s cubic-bezier(0.55, 0, 0.45, 1) - infinite; - } - } - - &--state-stop { - --play-btn-bg: var(--play-stop-bg); - --play-btn-fg: var(--play-stop-fg); - --play-btn-border: var(--play-stop-border); - --play-btn-shadow: var(--play-stop-shadow); - - .play-btn__icon-shell { - box-shadow: - inset 0 1px 0 rgba(255, 255, 255, 0.4), - 0 12px 24px rgba(145, 38, 53, 0.18); - } - - .play-btn__icon { - animation: playIconStop 1.2s ease-in-out infinite; - } - } -} - -@keyframes playButtonPulse { - 0%, - 100% { - box-shadow: var(--play-btn-shadow); - } - - 50% { - box-shadow: - var(--play-btn-shadow), - 0 0 0 6px rgba(255, 190, 92, 0.14); - } -} - -@keyframes playIconLaunch { - 0%, - 100% { - transform: rotate(0deg) scale(1); - } - - 50% { - transform: rotate(18deg) scale(1.1); - } -} - -@keyframes playIconStop { - 0%, - 100% { - transform: scale(1); - } - - 50% { - transform: scale(1.08); - } -} - -@media (prefers-reduced-motion: reduce) { - .play-btn, - .play-btn::before, - .play-btn mat-icon, - .play-btn__icon-shell { - animation: none !important; - transition-duration: 1ms !important; - } -} - -.favorite-btn, -.download-btn { - @include detail-actions.secondary-action-button; -} - -.favorite-btn { - @include detail-actions.favorite-button-state; -} - -.download-btn { - @include detail-actions.download-button-state; -} - -// ============================================================================ -// Content Sections - Below the hero fold -// ============================================================================ - -.content-section { - padding: 20px 0; - width: 100%; - box-sizing: border-box; - - &__title { - font-size: 1.375rem; - font-weight: 600; - margin: 0 0 20px 0; - color: var(--text-primary); - } -} - -// ============================================================================ -// Seasons & Episodes - Inside details section -// ============================================================================ - -.seasons { - margin-top: 24px; - width: 100%; - - .season-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 12px 0; - cursor: pointer; - transition: opacity 0.2s ease; - - &:hover { - opacity: 0.8; - } - - h4 { - margin: 0; - font-size: 1rem; - font-weight: 600; - } - - .expand-icon { - font-size: 0.75rem; - color: var(--text-muted); - } - } - - .episodes { - display: flex; - flex-wrap: wrap; - gap: 10px; - padding: 8px 0 16px; - - button { - margin: 0; - font-weight: 500; - } - } - - .episodes-loading, - .seasons-loading { - display: flex; - align-items: center; - gap: 12px; - padding: 20px 0; - color: var(--text-secondary); - } - - .no-episodes { - color: var(--text-muted); - font-style: italic; - padding: 8px 0; - } -} - -// Seasons loading when inside details -.details .seasons-loading { - display: flex; - align-items: center; - gap: 12px; - padding: 20px 0; - color: var(--text-secondary); - margin-top: 24px; -} - -// Season container when inside details section -.details app-season-container { - display: block; - width: 100%; - margin-top: 24px; - box-sizing: border-box; -} - -// ============================================================================ -// YouTube Trailer Section - Inside details -// ============================================================================ - -.youtube-trailer { - margin-top: 24px; - width: 100%; - - h3 { - font-size: 1.125rem; - font-weight: 600; - margin: 0 0 12px 0; - color: var(--text-primary); - } - - .trailer-wrapper { - position: relative; - width: 100%; - max-width: 560px; - aspect-ratio: 16 / 9; - border-radius: 8px; - overflow: hidden; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); - - iframe { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - border: none; - } - } -} - -// ============================================================================ -// Legacy container support (for gradual migration) -// ============================================================================ - -.container { - display: contents; -} - -.image { - display: none; // Hidden - now using .poster in hero -} - -// ============================================================================ -// Mobile Responsive - Option A: Keep poster grid layout -// ============================================================================ - -@media (max-width: 800px) { - :host { - --poster-width: 120px; - } - - .action-buttons { - width: 100%; - // This class is in ContentHeroComponent now, but these rules apply to children? - // No, .play-btn is a child of the PROJECTED content. - // So we should target the buttons directly for mobile - } - - @include detail-actions.mobile-action-buttons; - - .youtube-trailer { - margin-top: 16px; - - .trailer-wrapper { - max-width: 100%; - border-radius: 4px; - } - } - - .seasons { - padding: 16px 0 0; - } -} - -// ============================================================================ -// Animations -// ============================================================================ - -.youtube-trailer { - animation: fadeInUp 0.5s ease-out 0.4s both; -} - -@keyframes fadeInUp { - from { - opacity: 0; - transform: translateY(20px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -// ============================================================================ -// Utility - keep .label for backwards compatibility -// ============================================================================ - -.label { - font-weight: 400; - font-size: 0.8125rem; - color: var(--text-muted); - text-decoration: none; - padding: 0; - opacity: 1; -} +@include detail-view.base(); diff --git a/libs/ui/shared-portals/src/index.ts b/libs/ui/shared-portals/src/index.ts index 3b2ad317a..a59872e5a 100644 --- a/libs/ui/shared-portals/src/index.ts +++ b/libs/ui/shared-portals/src/index.ts @@ -1,7 +1,2 @@ export * from './lib/epg-view/epg-view.component'; export * from './lib/live-epg-panel/live-epg-panel.component'; -export * from './lib/live-stream-layout/live-stream-layout.component'; -export { - WebPlayerViewComponent, - type PlaybackFallbackRequest, -} from '@iptvnator/ui/playback'; diff --git a/libs/ui/shared-portals/src/lib/live-stream-layout/live-stream-layout.component.html b/libs/ui/shared-portals/src/lib/live-stream-layout/live-stream-layout.component.html deleted file mode 100644 index 7408ff411..000000000 --- a/libs/ui/shared-portals/src/lib/live-stream-layout/live-stream-layout.component.html +++ /dev/null @@ -1,98 +0,0 @@ -
- - @if (usesInternalPlayer()) { - @if (streamUrl) { -
- -
- } - } - @if (streamUrl) { -
- @if (usesInternalPlayer()) { - -
- -
-
- } @else { -
- -
- } -
- } @else { -
- {{ 'PORTALS.SELECT_CATEGORY' | translate }} -
- } -
diff --git a/libs/ui/shared-portals/src/lib/live-stream-layout/live-stream-layout.component.scss b/libs/ui/shared-portals/src/lib/live-stream-layout/live-stream-layout.component.scss deleted file mode 100644 index 519e8e7d5..000000000 --- a/libs/ui/shared-portals/src/lib/live-stream-layout/live-stream-layout.component.scss +++ /dev/null @@ -1,220 +0,0 @@ -@use '@angular/material' as mat; - -:host { - width: 100%; - height: 100%; - min-height: 0; - display: block; -} - -.itv-container { - display: grid; - // Use auto for sidebar to respect the directive's width - grid-template-columns: auto 1fr 1fr; - grid-template-rows: minmax(0, 1fr) minmax(0, 1fr); - gap: 0 0; - grid-auto-flow: row; - grid-template-areas: - 'sidebar video-player video-player' - 'sidebar epg epg'; - height: 100%; -} - -.itv-container--epg-collapsed { - grid-template-rows: minmax(0, 1fr) 56px; -} - -.itv-container-epg { - display: grid; - // Use auto for sidebar to respect the directive's width - grid-template-columns: auto 1fr 1fr; - grid-template-rows: repeat(3, minmax(0, 1fr)); - gap: 0 0; - grid-auto-flow: row; - grid-template-areas: - 'sidebar epg epg' - 'sidebar epg epg' - 'sidebar epg epg'; - height: 100%; -} - -.sidebar { - grid-area: sidebar; - // Default width (controlled by resizable directive) - width: 400px; - min-width: 250px; - max-width: 600px; - flex-shrink: 0; - position: relative; - border-right: 1px solid #333; - display: flex; - flex-direction: column; -} - -.video-player { - grid-area: video-player; - min-height: 0; - overflow: hidden; - display: flex; -} - -.video-player > app-web-player-view { - flex: 1; - min-height: 0; -} - -.epg { - grid-area: epg; - min-height: 0; - overflow: hidden; - display: flex; - flex-direction: column; - - &.epg-collapsed { - min-height: 56px; - } -} - -.search-panel { - margin-top: 10px; - display: flex; - justify-content: center; - margin-bottom: -15px; -} - -.search-bar { - width: 90%; - @include mat.form-field-density(-5); -} - -.epg-content { - height: 100%; - min-height: 0; - overflow: hidden; - display: flex; - flex-direction: column; - border-right: #111 1px solid; -} - -.epg-content > app-epg-view { - flex: 1; - min-height: 0; -} - -.epg > app-live-epg-panel { - flex: 1; - min-height: 0; -} - -.sidebar-content { - height: 100%; - overflow-y: auto; - border-right: #111 1px solid; - overflow-x: hidden; -} - -.no-channel-selected { - margin: 10px; -} - -::ng-deep { - .search-bar { - div { - border-radius: 20px; - } - } -} - -.nav-list { - flex: 1; - overflow-y: hidden; - padding: 0 8px 12px; - display: flex; - flex-direction: column; - gap: 2px; -} - -.nav-item { - position: relative; - width: 100%; - display: flex; - align-items: center; - gap: 10px; - padding: 6px 10px; - border: 1px solid transparent; - border-radius: 10px; - background: transparent; - color: var(--mat-sys-on-surface-variant); - text-align: left; - text-decoration: none; - cursor: pointer; - font-size: 0.875rem; - font-weight: 500; - box-sizing: border-box; - transition: - background 0.15s ease, - color 0.15s ease, - border-color 0.15s ease, - box-shadow 0.18s ease, - transform 0.18s ease; - - .stream-icon { - width: 36px; - height: 36px; - border-radius: 4px; - object-fit: cover; - flex-shrink: 0; - background: color-mix( - in srgb, - var(--mat-sys-on-surface) 5%, - transparent - ); - } - - span, - .title { - line-height: 1.2; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - flex: 1; - min-width: 0; - } - - &:hover { - background: var(--mat-sys-surface-container); - color: var(--mat-sys-on-surface); - } - - &.selected, - &.active { - background: linear-gradient( - 135deg, - var(--app-selection-surface-strong), - var(--app-selection-surface) - ) !important; - color: var(--app-selection-color) !important; - border-color: var(--app-selection-border) !important; - font-weight: 600; - box-shadow: - inset 0 0 0 1px var(--app-selection-border), - 0 12px 20px -18px var(--app-selection-glow); - transform: translateY(-1px); - - * { - color: var(--app-selection-color) !important; - } - - &::before { - content: ''; - position: absolute; - inset-inline-start: 6px; - top: 7px; - bottom: 7px; - width: 3px; - border-radius: 999px; - background: var(--app-selection-color); - box-shadow: 0 0 10px var(--app-selection-glow); - } - } -} diff --git a/libs/ui/shared-portals/src/lib/live-stream-layout/live-stream-layout.component.ts b/libs/ui/shared-portals/src/lib/live-stream-layout/live-stream-layout.component.ts deleted file mode 100644 index a4d08d749..000000000 --- a/libs/ui/shared-portals/src/lib/live-stream-layout/live-stream-layout.component.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { ScrollingModule } from '@angular/cdk/scrolling'; - -import { - ChangeDetectionStrategy, - Component, - computed, - EventEmitter, - Input, - Output, - signal, -} from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatIconModule } from '@angular/material/icon'; -import { MatInputModule } from '@angular/material/input'; -import { MatListModule } from '@angular/material/list'; -import { FilterPipe } from '@iptvnator/pipes'; -import { TranslateModule } from '@ngx-translate/core'; -import { - LiveEpgPanelState, - persistLiveEpgPanelState, - restoreLiveEpgPanelState, -} from '@iptvnator/portal/shared/util'; -import { WebPlayerViewComponent } from '@iptvnator/ui/playback'; -import { ResizableDirective } from '@iptvnator/ui/components'; -import { EpgItem, VideoPlayer, XtreamItem } from '@iptvnator/shared/interfaces'; -import { EpgViewComponent } from '../epg-view/epg-view.component'; -import { - LiveEpgPanelComponent, - LiveEpgPanelSummary, -} from '../live-epg-panel/live-epg-panel.component'; - -@Component({ - selector: 'app-live-stream-layout', - templateUrl: './live-stream-layout.component.html', - styleUrls: ['./live-stream-layout.component.scss'], - imports: [ - EpgViewComponent, - FilterPipe, - FormsModule, - LiveEpgPanelComponent, - MatListModule, - MatIconModule, - MatInputModule, - MatFormFieldModule, - ResizableDirective, - ScrollingModule, - WebPlayerViewComponent, - TranslateModule, - ], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class LiveStreamLayoutComponent { - @Input({ required: true }) channels!: XtreamItem[]; - @Input({ required: true }) player: VideoPlayer = VideoPlayer.VideoJs; - @Input() epgItems!: EpgItem[]; - @Input() streamUrl!: string; - @Input() activeLiveStream!: XtreamItem; - - @Output() itemClicked = new EventEmitter(); - - searchString = signal(''); - readonly liveEpgPanelState = signal( - restoreLiveEpgPanelState() - ); - readonly isLiveEpgPanelCollapsed = computed( - () => this.liveEpgPanelState() === 'collapsed' - ); - - trackBy(_index: number, item: XtreamItem) { - return item.stream_id; - } - - usesInternalPlayer(): boolean { - return ( - this.player === VideoPlayer.VideoJs || - this.player === VideoPlayer.Html5Player || - this.player === VideoPlayer.ArtPlayer - ); - } - - getLiveEpgPanelSummary(): LiveEpgPanelSummary | null { - const currentProgram = - this.epgItems?.find((item) => this.isCurrentProgram(item)) ?? null; - - if (!currentProgram) { - return null; - } - - return { - title: currentProgram.title, - start: currentProgram.start, - stop: currentProgram.stop ?? currentProgram.end, - }; - } - - onLiveEpgPanelCollapsedChange(collapsed: boolean): void { - const state: LiveEpgPanelState = collapsed ? 'collapsed' : 'expanded'; - this.liveEpgPanelState.set(state); - persistLiveEpgPanelState(state); - } - - private isCurrentProgram(item: EpgItem): boolean { - const end = item.stop ?? item.end; - const now = Date.now(); - const start = new Date(item.start).getTime(); - const stop = new Date(end).getTime(); - return now >= start && now <= stop; - } -} diff --git a/libs/ui/styles/_detail-view.scss b/libs/ui/styles/_detail-view.scss new file mode 100644 index 000000000..98b012e2b --- /dev/null +++ b/libs/ui/styles/_detail-view.scss @@ -0,0 +1,580 @@ +// ============================================================================ +// Netflix-style Detail View - Cinematic Streaming Design +// ============================================================================ + +@use 'detail-view-actions' as detail-actions; + +@mixin base( + $season-title-font-size: 1.375rem, + $season-title-font-weight: 600, + $season-title-letter-spacing: 0, + $episode-title-font-size: 1.125rem, + $episode-title-letter-spacing: 0 +) { + :host { + --poster-width: 220px; + --poster-shadow: 0 8px 40px rgba(0, 0, 0, 0.15); + --content-max-width: 1400px; + + // Light theme colors (default - no .dark-theme class on body) + --hero-gradient-start: rgba(255, 250, 245, 0); + --hero-gradient-mid: rgba(255, 250, 245, 0.75); + --hero-gradient-end: rgba(255, 250, 245, 1); + --surface-bg: #fffaf5; + --text-primary: #1a1a1a; + --text-secondary: rgba(26, 26, 26, 0.75); + --text-muted: rgba(26, 26, 26, 0.5); + --accent-color: #b81d24; + --tag-bg: rgba(0, 0, 0, 0.06); + --tag-border: rgba(0, 0, 0, 0.12); + --play-idle-bg: linear-gradient( + 135deg, + #fffaf2 0%, + #ffffff 52%, + #f7ecdf 100% + ); + --play-idle-fg: #161310; + --play-idle-border: rgba(88, 48, 18, 0.12); + --play-idle-shadow: 0 18px 38px rgba(91, 56, 24, 0.14); + --play-launching-bg: linear-gradient( + 135deg, + #ffd36f 0%, + #ffb14c 46%, + #f57d35 100% + ); + --play-launching-fg: #241103; + --play-launching-border: rgba(142, 66, 0, 0.2); + --play-launching-shadow: 0 22px 44px rgba(244, 136, 55, 0.32); + --play-stop-bg: linear-gradient( + 135deg, + #ffd1c8 0%, + #ff9f8d 42%, + #ef5e64 100% + ); + --play-stop-fg: #321012; + --play-stop-border: rgba(162, 38, 45, 0.18); + --play-stop-shadow: 0 22px 46px rgba(214, 74, 86, 0.26); + --play-icon-shell-bg: rgba(255, 255, 255, 0.32); + --play-icon-shell-border: rgba(255, 255, 255, 0.3); + + width: 100%; + height: 100%; + display: block; + overflow: hidden; + background: var(--surface-bg); + color: var(--text-primary); + } + + // Dark theme overrides - cinematic dark mode + :host-context(.dark-theme) { + --hero-gradient-start: rgba(20, 20, 20, 0); + --hero-gradient-mid: rgba(20, 20, 20, 0.7); + --hero-gradient-end: rgba(20, 20, 20, 1); + --surface-bg: #141414; + --text-primary: #fff; + --text-secondary: rgba(255, 255, 255, 0.7); + --text-muted: rgba(255, 255, 255, 0.5); + --accent-color: #e50914; + --tag-bg: rgba(255, 255, 255, 0.1); + --tag-border: rgba(255, 255, 255, 0.2); + --poster-shadow: 0 8px 40px rgba(0, 0, 0, 0.5); + --play-idle-bg: linear-gradient( + 135deg, + #fff5e6 0%, + #f6efe5 58%, + #e7dac9 100% + ); + --play-idle-fg: #15110d; + --play-idle-border: rgba(255, 255, 255, 0.18); + --play-idle-shadow: 0 20px 42px rgba(0, 0, 0, 0.36); + --play-launching-bg: linear-gradient( + 135deg, + #ffdd79 0%, + #ffb347 42%, + #ef6a3a 100% + ); + --play-launching-fg: #1f1105; + --play-launching-border: rgba(255, 223, 136, 0.26); + --play-launching-shadow: 0 24px 50px rgba(255, 153, 71, 0.3); + --play-stop-bg: linear-gradient( + 135deg, + #ffd7cf 0%, + #ff9b8b 40%, + #eb5961 100% + ); + --play-stop-fg: #2d1012; + --play-stop-border: rgba(255, 205, 205, 0.24); + --play-stop-shadow: 0 24px 50px rgba(235, 89, 97, 0.24); + --play-icon-shell-bg: rgba(255, 255, 255, 0.28); + --play-icon-shell-border: rgba(255, 255, 255, 0.24); + } + + // ============================================================================ + // Details Section - Content Elements + // ============================================================================ + + .details { + // Styles for projected content elements + + &__tag { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 12px; + font-size: 0.8125rem; + font-weight: 500; + background: var(--tag-bg); + border: 1px solid var(--tag-border); + border-radius: 4px; + backdrop-filter: blur(8px); + + &--rating { + color: #ffd700; + border-color: rgba(255, 215, 0, 0.3); + } + } + + &__meta-item { + font-size: 0.875rem; + + .label { + color: var(--text-muted); + font-weight: 400; + margin-bottom: 2px; + text-decoration: none; + display: block; + } + + .value { + color: var(--text-primary); + font-weight: 500; + } + } + } + + // ============================================================================ + // Action Buttons - Play, Favorite + // ============================================================================ + + .play-btn { + --play-btn-bg: var(--play-idle-bg); + --play-btn-fg: var(--play-idle-fg); + --play-btn-border: var(--play-idle-border); + --play-btn-shadow: var(--play-idle-shadow); + + position: relative; + isolation: isolate; + overflow: hidden; + display: inline-flex; + align-items: center; + gap: 12px; + padding: 10px 24px 10px 18px; + min-height: 64px; + font-size: 1rem; + font-weight: 600; + letter-spacing: 0; + background: var(--play-btn-bg); + background-size: 140% 140%; + background-position: 0% 50%; + color: var(--play-btn-fg); + border: 1px solid var(--play-btn-border); + border-radius: 16px; + box-shadow: var(--play-btn-shadow); + cursor: pointer; + transition: + transform 220ms cubic-bezier(0.22, 1, 0.36, 1), + background-position 280ms ease, + box-shadow 260ms ease, + border-color 220ms ease, + color 220ms ease, + filter 220ms ease; + + &::before { + content: ''; + position: absolute; + inset: 1px; + border-radius: inherit; + background: + linear-gradient( + 115deg, + rgba(255, 255, 255, 0.34), + transparent 36% + ), + linear-gradient( + 180deg, + rgba(255, 255, 255, 0.18), + transparent 62% + ); + opacity: 0.7; + transition: opacity 220ms ease; + z-index: -1; + } + + &:hover:not(:disabled) { + transform: translateY(-2px); + background-position: 100% 50%; + box-shadow: + var(--play-btn-shadow), + 0 10px 20px rgba(255, 255, 255, 0.16) inset; + } + + &:hover:not(:disabled)::before { + opacity: 0.95; + } + + &:active:not(:disabled) { + transform: translateY(0); + box-shadow: var(--play-btn-shadow); + } + + &:disabled { + cursor: progress; + opacity: 1; + filter: saturate(1.05); + } + + mat-icon { + font-size: 24px; + width: 24px; + height: 24px; + transition: + transform 220ms cubic-bezier(0.22, 1, 0.36, 1), + color 220ms ease, + filter 220ms ease; + } + + &:hover:not(:disabled) .play-btn__icon { + transform: scale(1.08); + } + + &__icon-shell { + flex: 0 0 auto; + display: inline-flex; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + border-radius: 999px; + background: var(--play-icon-shell-bg); + border: 1px solid var(--play-icon-shell-border); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.38), + 0 6px 14px rgba(0, 0, 0, 0.08); + backdrop-filter: blur(10px); + transition: + transform 220ms ease, + background-color 220ms ease, + border-color 220ms ease, + box-shadow 220ms ease; + } + + @include detail-actions.play-button-copy(var(--play-btn-fg)); + + &--state-launching, + &--state-stop { + color: var(--play-btn-fg); + } + + &--state-launching { + --play-btn-bg: var(--play-launching-bg); + --play-btn-fg: var(--play-launching-fg); + --play-btn-border: var(--play-launching-border); + --play-btn-shadow: var(--play-launching-shadow); + animation: playButtonPulse 1.8s ease-in-out infinite; + + .play-btn__icon-shell { + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.42), + 0 10px 22px rgba(184, 89, 26, 0.2); + } + + .play-btn__icon { + animation: playIconLaunch 1.15s cubic-bezier(0.55, 0, 0.45, 1) + infinite; + } + } + + &--state-stop { + --play-btn-bg: var(--play-stop-bg); + --play-btn-fg: var(--play-stop-fg); + --play-btn-border: var(--play-stop-border); + --play-btn-shadow: var(--play-stop-shadow); + + .play-btn__icon-shell { + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.4), + 0 12px 24px rgba(145, 38, 53, 0.18); + } + + .play-btn__icon { + animation: playIconStop 1.2s ease-in-out infinite; + } + } + } + + @keyframes playButtonPulse { + 0%, + 100% { + box-shadow: var(--play-btn-shadow); + } + + 50% { + box-shadow: + var(--play-btn-shadow), + 0 0 0 6px rgba(255, 190, 92, 0.14); + } + } + + @keyframes playIconLaunch { + 0%, + 100% { + transform: rotate(0deg) scale(1); + } + + 50% { + transform: rotate(18deg) scale(1.1); + } + } + + @keyframes playIconStop { + 0%, + 100% { + transform: scale(1); + } + + 50% { + transform: scale(1.08); + } + } + + @media (prefers-reduced-motion: reduce) { + .play-btn, + .play-btn::before, + .play-btn mat-icon, + .play-btn__icon-shell { + animation: none !important; + transition-duration: 1ms !important; + } + } + + .favorite-btn, + .download-btn { + @include detail-actions.secondary-action-button; + } + + .favorite-btn { + @include detail-actions.favorite-button-state; + } + + .download-btn { + @include detail-actions.download-button-state; + } + + // ============================================================================ + // Content Sections - Below the hero fold + // ============================================================================ + + .content-section { + padding: 20px 0; + width: 100%; + box-sizing: border-box; + + &__title { + font-size: $season-title-font-size; + font-weight: $season-title-font-weight; + letter-spacing: $season-title-letter-spacing; + margin: 0 0 20px 0; + color: var(--text-primary); + } + } + + // ============================================================================ + // Seasons & Episodes - Inside details section + // ============================================================================ + + .seasons { + margin-top: 24px; + width: 100%; + + .season-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 0; + cursor: pointer; + transition: opacity 0.2s ease; + + &:hover { + opacity: 0.8; + } + + h4 { + margin: 0; + font-size: 1rem; + font-weight: 600; + } + + .expand-icon { + font-size: 0.75rem; + color: var(--text-muted); + } + } + + .episodes { + display: flex; + flex-wrap: wrap; + gap: 10px; + padding: 8px 0 16px; + + button { + margin: 0; + font-weight: 500; + } + } + + .episodes-loading, + .seasons-loading { + display: flex; + align-items: center; + gap: 12px; + padding: 20px 0; + color: var(--text-secondary); + } + + .no-episodes { + color: var(--text-muted); + font-style: italic; + padding: 8px 0; + } + } + + // Seasons loading when inside details + .details .seasons-loading { + display: flex; + align-items: center; + gap: 12px; + padding: 20px 0; + color: var(--text-secondary); + margin-top: 24px; + } + + // Season container when inside details section + .details app-season-container { + display: block; + width: 100%; + margin-top: 24px; + box-sizing: border-box; + } + + // ============================================================================ + // YouTube Trailer Section - Inside details + // ============================================================================ + + .youtube-trailer { + margin-top: 24px; + width: 100%; + + h3 { + font-size: $episode-title-font-size; + font-weight: 600; + letter-spacing: $episode-title-letter-spacing; + margin: 0 0 12px 0; + color: var(--text-primary); + } + + .trailer-wrapper { + position: relative; + width: 100%; + max-width: 560px; + aspect-ratio: 16 / 9; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); + + iframe { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border: none; + } + } + } + + // ============================================================================ + // Legacy container support (for gradual migration) + // ============================================================================ + + .container { + display: contents; + } + + .image { + display: none; // Hidden - now using .poster in hero + } + + // ============================================================================ + // Mobile Responsive - Option A: Keep poster grid layout + // ============================================================================ + + @media (max-width: 800px) { + :host { + --poster-width: 120px; + } + + .action-buttons { + width: 100%; + // This class is in ContentHeroComponent now, but these rules apply to children? + // No, .play-btn is a child of the PROJECTED content. + // So we should target the buttons directly for mobile + } + + @include detail-actions.mobile-action-buttons; + + .youtube-trailer { + margin-top: 16px; + + .trailer-wrapper { + max-width: 100%; + border-radius: 4px; + } + } + + .seasons { + padding: 16px 0 0; + } + } + + // ============================================================================ + // Animations + // ============================================================================ + + .youtube-trailer { + animation: fadeInUp 0.5s ease-out 0.4s both; + } + + @keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + // ============================================================================ + // Utility - keep .label for backwards compatibility + // ============================================================================ + + .label { + font-weight: 400; + font-size: 0.8125rem; + color: var(--text-muted); + text-decoration: none; + padding: 0; + opacity: 1; + } +} diff --git a/libs/ui/styles/_index.scss b/libs/ui/styles/_index.scss index 7c1405365..06421af87 100644 --- a/libs/ui/styles/_index.scss +++ b/libs/ui/styles/_index.scss @@ -9,4 +9,5 @@ @forward 'portal-layout'; @forward 'content-grid'; @forward 'portal-sidebar'; +@forward 'detail-view'; @forward 'detail-view-actions'; diff --git a/test-signal.ts b/test-signal.ts deleted file mode 100644 index e54ee353c..000000000 --- a/test-signal.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { signal } from '@angular/core'; -const sig = signal(undefined); -console.log(!!sig());