11<template >
2- <component :is =" to ? 'router-link' : 'div'" :to =" to" class =" poster-wrap" :class =" sizeClass" >
3- <img
4- class =" poster"
5- :class =" { 'is-loaded': !isLoading, 'cursor-pointer': clickable }"
6- :src =" posterPath"
7- :alt =" item.title"
8- loading =" lazy"
9- decoding =" async"
10- @load =" onLoad"
11- @error =" onError"
12- />
13- <Skeleton v-if =" isLoading" class =" poster-skeleton" />
14- <!-- Show download status badge when downloading, otherwise show library badge -->
15- <div v-if =" isDownloading" class =" status-badge download-badge" >
16- <Loader2 class =" size-4 animate-spin" aria-hidden =" true" />
17- <span >Downloading</span >
2+ <component
3+ :is =" to ? 'router-link' : 'div'"
4+ :to =" to"
5+ class =" poster-outer"
6+ :class =" sizeClass"
7+ >
8+ <!-- Inner container: scales on hover, clips image to rounded corners -->
9+ <div class =" poster-inner" >
10+ <img
11+ class =" poster"
12+ :class =" { 'is-loaded': !isLoading, 'cursor-pointer': clickable }"
13+ :src =" posterPath"
14+ :alt =" item.title"
15+ loading =" lazy"
16+ decoding =" async"
17+ @load =" onLoad"
18+ @error =" onError"
19+ />
20+ <Skeleton v-if =" isLoading" class =" poster-skeleton" />
21+ <!-- Status badges stay with the image -->
22+ <div v-if =" isDownloading" class =" status-badge download-badge" >
23+ <Loader2 class =" size-4 animate-spin" aria-hidden =" true" />
24+ <span >Downloading</span >
25+ </div >
26+ <div v-else-if =" showLibraryBadge" class =" status-badge library-badge" >
27+ <CheckCircle2 class =" size-4" aria-hidden =" true" />
28+ <span >In library</span >
29+ </div >
1830 </div >
19- <div v-else-if =" showLibraryBadge" class =" status-badge library-badge" >
20- <CheckCircle2 class =" size-4" aria-hidden =" true" />
21- <span >In library</span >
31+ <!-- Overlay: sibling to inner, not clipped by it -->
32+ <div class =" poster-overlay" >
33+ <div class =" poster-info" >
34+ <span class =" poster-title" >{{ item.title }}</span >
35+ <span class =" poster-year" >{{ item.year }}</span >
36+ </div >
2237 </div >
2338 </component >
2439</template >
@@ -33,7 +48,6 @@ import {
3348 type ModelMovieDetail ,
3449 type ModelMovieRail ,
3550 type ModelSeriesDetail ,
36- type ModelSeriesRail ,
3751} from ' @/client/types.gen'
3852
3953/**
@@ -71,7 +85,7 @@ type PosterSize = keyof typeof POSTER_SIZES
7185
7286const props = withDefaults (
7387 defineProps <{
74- item: ModelMovieDetail | ModelSeriesDetail | ModelHydratedTitle | ModelMovieRail | ModelSeriesRail | ModelLibraryItem
88+ item: ModelMovieDetail | ModelSeriesDetail | ModelHydratedTitle | ModelMovieRail | ModelLibraryItem
7589 size? : PosterSize
7690 to? : { path: string } | string
7791 clickable? : boolean
@@ -118,18 +132,32 @@ const onError = () => {
118132 </script >
119133
120134<style scoped>
121- .poster-wrap {
135+ /* Outer container: handles sizing, is the clickable link, no clipping */
136+ .poster-outer {
122137 display : block ;
123138 width : 100% ;
124- aspect-ratio : 2 / 3 ; /* common movie/TV poster ratio */
139+ aspect-ratio : 2 / 3 ;
125140 position : relative ;
126- border-radius : 8px !important ;
127- overflow : hidden ;
128- background-color : #111827 ; /* neutral placeholder while loading */
129141 text-decoration : none ;
130142 color : inherit ;
131143}
132144
145+ /* Inner container: scales on hover, clips image to rounded corners */
146+ .poster-inner {
147+ position : absolute ;
148+ inset : 0 ;
149+ border-radius : 8px ;
150+ overflow : hidden ;
151+ background-color : #111827 ;
152+ transition : transform 0.2s ease , box-shadow 0.2s ease ;
153+ }
154+
155+ .poster-outer :hover .poster-inner {
156+ transform : scale (1.05 );
157+ box-shadow : 0 8px 24px rgba (0 , 0 , 0 , 0.4 );
158+ z-index : 10 ;
159+ }
160+
133161.poster {
134162 position : absolute ;
135163 inset : 0 ;
@@ -138,7 +166,6 @@ const onError = () => {
138166 object-fit : cover ;
139167 opacity : 0 ;
140168 transition : opacity 150ms ease ;
141- border-radius : 8px !important ;
142169}
143170
144171.poster.is-loaded {
@@ -168,36 +195,80 @@ const onError = () => {
168195}
169196
170197.library-badge svg {
171- color : #22c55e ; /* emerald-500 */
198+ color : #22c55e ;
172199}
173200
174201.download-badge svg {
175- color : #3b82f6 ; /* blue-500 */
202+ color : #3b82f6 ;
176203}
177204
178205.poster-skeleton {
179206 position : absolute ;
180207 inset : 0 ;
181208 width : 100% ;
182209 height : 100% ;
183- border-radius : 8px !important ;
184210}
185211
186212.poster--sm {
187- --poster-width : 8rem ; /* ~128px */
213+ --poster-width : 8rem ;
188214 width : var (--poster-width );
189215 max-width : var (--poster-width );
190216}
191217
192218.poster--md {
193- --poster-width : 11rem ; /* ~176px */
219+ --poster-width : 11rem ;
194220 width : var (--poster-width );
195221 max-width : var (--poster-width );
196222}
197223
198224.poster--lg {
199- --poster-width : 16rem ; /* ~256px */
225+ --poster-width : 16rem ;
200226 width : var (--poster-width );
201227 max-width : var (--poster-width );
202228}
229+
230+ /* Overlay: sibling to inner, not clipped by it */
231+ .poster-overlay {
232+ position : absolute ;
233+ inset : 0 ;
234+ border-radius : 8px ;
235+ background : linear-gradient (to top , rgba (0 , 0 , 0 , 0.8 ) 0% , rgba (0 , 0 , 0 , 0.4 ) 40% , transparent 100% );
236+ opacity : 0 ;
237+ transition : opacity 0.2s ease , transform 0.2s ease ;
238+ pointer-events : none ;
239+ display : flex ;
240+ flex-direction : column ;
241+ justify-content : flex-end ;
242+ padding : 0.75rem ;
243+ }
244+
245+ .poster-outer :hover .poster-overlay {
246+ opacity : 1 ;
247+ transform : scale (1.05 );
248+ z-index : 20 ;
249+ }
250+
251+ /* Info text at bottom */
252+ .poster-info {
253+ display : flex ;
254+ flex-direction : column ;
255+ gap : 0.125rem ;
256+ }
257+
258+ .poster-title {
259+ font-weight : 600 ;
260+ font-size : 0.875rem ;
261+ line-height : 1.2 ;
262+ color : white ;
263+ display : -webkit-box ;
264+ -webkit-line-clamp : 2 ;
265+ line-clamp : 2 ;
266+ -webkit-box-orient : vertical ;
267+ overflow : hidden ;
268+ }
269+
270+ .poster-year {
271+ font-size : 0.75rem ;
272+ color : rgba (255 , 255 , 255 , 0.7 );
273+ }
203274 </style >
0 commit comments