Files
iptvnator/libs/ui/components/src/lib/content-hero/content-hero.component.scss
T

627 lines
16 KiB
SCSS

// ============================================================================
// Netflix-style Detail View - Cinematic Streaming Design
// ============================================================================
@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);
width: 100%;
display: block;
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);
}
// ============================================================================
// Hero Section - Full container with backdrop
// ============================================================================
.hero {
position: relative;
width: 100%;
// Natural height: the page scroll container lives outside the hero
// (PortalDetailShellComponent). `overflow: hidden` clips the scaled
// blurred backdrop fallback.
overflow: hidden;
background: var(
--surface-bg
); // Solid background prevents any bleed-through
// Backdrop image layer - fixed at top, never repeats
&__backdrop {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: auto; // Only use height, not bottom
height: 60%;
background-size: cover;
background-position: center 20%;
background-repeat: no-repeat;
z-index: 0;
overflow: hidden;
&-image {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
object-position: center 20%;
}
// Fallback blur effect when using poster as backdrop
&--blurred {
filter: blur(40px) saturate(1.3);
transform: scale(1.4);
height: 65%;
}
}
// Gradient overlays for text legibility
&__vignette {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0; // Full height to cover everything
z-index: 1;
pointer-events: none;
background: linear-gradient(
to bottom,
transparent 0%,
transparent 35%,
var(--surface-bg) 55%,
var(--surface-bg) 100%
);
// Top area gradient for text legibility over backdrop
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 60%;
background: linear-gradient(
to top,
var(--hero-gradient-end) 0%,
var(--hero-gradient-mid) 50%,
var(--hero-gradient-start) 100%
);
}
// Side fade for poster area
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
height: 50%;
width: 40%;
background: linear-gradient(
to right,
var(--hero-gradient-mid) 0%,
var(--hero-gradient-start) 100%
);
}
}
// Content area (page scroll lives in the surrounding detail shell)
&__content {
position: relative;
z-index: 2;
display: flex;
gap: 32px;
align-items: flex-start;
padding: 140px 40px 60px;
// Reserved by the detail shell while its sticky Back control shows.
padding-inline-start: max(40px, var(--detail-back-lane, 0px));
max-width: var(--content-max-width);
margin: 0 auto;
box-sizing: border-box;
}
// Error state
&__error {
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
gap: 16px;
min-height: 50vh;
.error-icon {
font-size: 64px;
width: 64px;
height: 64px;
color: var(--text-muted);
}
.error-title {
font-size: 1.25rem;
font-weight: 500;
color: var(--text-secondary);
margin: 0;
}
}
}
// ============================================================================
// Poster Card - Floating with shadow
// ============================================================================
.poster {
flex-shrink: 0;
width: var(--poster-width);
align-self: flex-start;
&:hover .poster__image {
transform: scale(1.02);
}
&__image {
width: 100%;
aspect-ratio: 2 / 3;
object-fit: cover;
border-radius: 8px;
box-shadow: var(--poster-shadow);
background: linear-gradient(135deg, #2a2a2a 0%, #1a1a1a 100%);
transition: transform 0.3s ease;
}
&__placeholder {
width: 100%;
aspect-ratio: 2 / 3;
border-radius: 8px;
background: linear-gradient(135deg, #3a3a3a 0%, #2a2a2a 100%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: rgba(255, 255, 255, 0.8);
box-shadow: var(--poster-shadow);
overflow: hidden;
&-icon {
font-size: 48px;
width: 48px;
height: 48px;
opacity: 0.5;
margin-bottom: 12px;
}
&-text {
font-size: 1rem;
font-weight: 500;
text-align: center;
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
overflow: hidden;
overflow-wrap: anywhere;
padding: 16px;
}
}
}
// ============================================================================
// Details Section - Title, metadata, description
// ============================================================================
.details {
flex: 1;
min-width: 0;
padding-bottom: 24px;
&__title {
font-size: clamp(1.75rem, 4vw, 3rem);
font-weight: 700;
line-height: 1.1;
margin: 0 0 16px 0;
letter-spacing: -0.02em;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
overflow-wrap: anywhere;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
&__tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 16px;
}
// These styles are for Projected Content, we need to ensure they apply
// using ::ng-deep or just assume the global styles will handle them?
// Actually, since we project content, the styles defined here might NOT apply to the projected content
// unless we use ::ng-deep or the content itself has these styles.
// However, since we are moving the "shell", the container classes (.details__tags) are here.
// The inner items (.details__tag) will be in the projected content.
// So we need ::ng-deep here for .details__tag.
::ng-deep .details__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);
}
// Rendered as a <button> so the facet is keyboard-operable; the
// UA's own font and appearance have to be neutralized to keep it
// visually identical to the informational chips beside it.
&--clickable {
appearance: none;
cursor: pointer;
font-family: inherit;
letter-spacing: inherit;
line-height: inherit;
transition:
border-color 0.15s ease,
background 0.15s ease;
&:hover {
background: var(--tag-border);
border-color: var(--text-secondary, rgba(255, 255, 255, 0.5));
}
&:focus-visible {
outline: 2px solid var(--text-primary, #fff);
outline-offset: 2px;
}
}
}
&__description {
font-size: 0.9375rem;
line-height: 1.6;
color: var(--text-secondary);
margin-bottom: 6px;
max-width: 600px;
// Truncate long descriptions
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
overflow: hidden;
&--expanded {
display: block;
-webkit-line-clamp: unset;
overflow: visible;
}
}
&__description-toggle {
display: inline-flex;
align-items: center;
gap: 4px;
margin: 0 0 20px 0;
padding: 4px 0;
background: transparent;
border: 0;
cursor: pointer;
color: var(--text-secondary);
font: inherit;
font-size: 0.8125rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
transition:
color 180ms ease,
transform 180ms ease;
mat-icon {
font-size: 16px;
width: 16px;
height: 16px;
line-height: 16px;
transition: transform 180ms ease;
}
&:hover {
color: var(--text-primary);
mat-icon {
transform: translateY(2px);
}
}
&[aria-expanded='true']:hover mat-icon {
transform: translateY(-2px);
}
&:focus-visible {
outline: 2px solid var(--accent-color);
outline-offset: 2px;
border-radius: 2px;
}
}
&__meta {
display: flex;
flex-wrap: wrap;
gap: 16px 32px;
margin-bottom: 24px;
}
// Similar issue for meta items
::ng-deep .details__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
// ============================================================================
.action-buttons {
display: flex;
gap: 12px;
flex-wrap: wrap;
margin-top: 0;
// Use ::ng-deep to style the projected buttons
::ng-deep .play-btn {
display: inline-flex;
align-items: center;
gap: 12px;
min-height: 64px;
padding: 10px 24px 10px 18px;
font-size: 1rem;
font-weight: 600;
background: #fff;
color: #141414;
border: none;
border-radius: 16px;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background: rgba(255, 255, 255, 0.85);
transform: scale(1.02);
}
mat-icon {
font-size: 24px;
width: 24px;
height: 24px;
}
@include detail-actions.play-button-copy;
}
::ng-deep .favorite-btn,
::ng-deep .download-btn,
// The sources chip is a peer secondary action, so it wears the same
// surface, height and radius. Without this it reads as a link floating
// between three solid buttons.
::ng-deep app-vod-sources-chip .sources-chip {
@include detail-actions.secondary-action-button;
}
// Match the chip to its neighbouring icon buttons' tag surfaces. Its own
// pressed state (.sources-chip--open) is more specific and still wins.
::ng-deep app-vod-sources-chip .sources-chip {
background: var(--tag-bg);
border-color: var(--tag-border);
&:hover {
background: var(--tag-border);
}
}
::ng-deep .favorite-btn {
@include detail-actions.favorite-button-state;
}
::ng-deep .download-btn {
@include detail-actions.download-button-state;
}
}
// ============================================================================
// Mobile Responsive - Option A: Keep poster grid layout
// ============================================================================
@media (max-width: 800px) {
:host {
--poster-width: 120px;
}
.hero {
&__backdrop {
height: 45%;
&--blurred {
height: 50%;
}
}
&__vignette {
background: linear-gradient(
to bottom,
transparent 0%,
transparent 25%,
var(--surface-bg) 45%,
var(--surface-bg) 100%
);
&::before {
height: 50%;
}
&::after {
display: none;
}
}
&__content {
padding: 60px 20px 40px;
padding-inline-start: max(20px, var(--detail-back-lane, 0px));
gap: 16px;
}
}
.poster {
// Mobile poster adjustments - none needed
}
.details {
&__title {
font-size: 1.5rem;
}
&__description {
-webkit-line-clamp: 3;
&--expanded {
-webkit-line-clamp: unset;
}
}
&__meta {
gap: 12px 20px;
}
}
.action-buttons {
width: 100%;
@include detail-actions.projected-mobile-action-buttons;
}
}
// A phone cannot carry the poster and the details side by side: the details
// column drops to ~200px, and the action buttons are then squeezed below
// their own labels ("Play" clipped down to its icon). Stack them instead.
@media (max-width: 640px) {
.hero__content {
flex-direction: column;
align-items: stretch;
padding-inline: 16px;
padding-inline-start: max(16px, var(--detail-back-lane, 0px));
}
.poster {
width: 140px;
align-self: center;
}
.details__title {
text-align: center;
}
}
// ============================================================================
// Animations
// ============================================================================
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.hero__content {
animation: fadeInUp 0.6s ease-out 0.2s both;
}
// Skeleton Loader Overrides
ngx-skeleton-loader {
display: block;
width: 100%;
// Ensure the loader inside poster takes up the correct aspect ratio space
::ng-deep .loader {
display: block;
height: 100%;
}
}
:host-context(.dark-theme) {
ngx-skeleton-loader {
::ng-deep .loader {
background-color: rgba(255, 255, 255, 0.1);
}
}
}
.poster {
// ... existing styles ...
// Ensure poster container has height/ratio for skeleton
min-height: 330px; // Fallback
aspect-ratio: 2 / 3;
}
// After the rule above so it wins the cascade at equal specificity: that
// 330px skeleton fallback is sized for the desktop poster width, while the
// stacked phone hero renders the poster 140px wide — its aspect-ratio height
// is ~210px, and the leftover minimum was pure empty space pushing the title
// and actions ~120px down on every loaded detail page.
@media (max-width: 640px) {
.poster {
min-height: 0;
}
}