@@ -204,177 +204,180 @@ function ImagePickerPopoverComponent<TFieldValues extends FieldValues = FieldVal
204204 >
205205 < div
206206 ref = { imagePickerRef }
207- className = "flex h-96 w-80 flex-col overflow-auto rounded border border-subtle bg-surface-1 shadow-raised-200 md:h-[36rem] md:w-[36rem]"
207+ className = "flex h-96 w-80 flex-col overflow-hidden rounded border border-subtle bg-surface-1 shadow-raised-200 md:h-[36rem] md:w-[36rem]"
208208 >
209- { /* Row wrapper: Propel's Tabs root omits className, so it takes its full height by
210- stretching as a flex item. */ }
211- < div className = "flex h-full p-3" >
209+ { /* Row wrapper: published Tabs omits className, so fill height comes from stretch. */ }
210+ < div className = "flex h-full min-h-0 w-full p-3" >
212211 < Tabs variant = "contained" defaultValue = { enabledTabs [ 0 ] ?. key || "images" } >
213- < TabsList >
214- { enabledTabs . map ( ( tab ) => (
215- < Tab key = { tab . key } value = { tab . key } label = { tab . title } />
216- ) ) }
217- </ TabsList >
218- { /* Grid wrapper: Propel's TabsPanel omits className, so the single mounted panel
219- gets its fill height from a one-row grid instead. */ }
220- < div className = "vertical-scrollbar scrollbar-sm grid min-h-0 w-full flex-1 grid-rows-1 overflow-x-hidden overflow-y-auto p-3" >
221- < TabsPanel value = "unsplash" >
222- < div className = "space-y-4" >
223- { ( unsplashImages || ! unsplashError ) && (
224- < >
225- < div className = "flex items-center gap-x-2" >
226- < Controller
227- control = { control }
228- name = { "search" as FieldPath < TFieldValues > }
229- render = { ( { field : { value, ref } } ) => (
230- < InputGroup size = "2xl" >
231- < Input
232- size = "2xl"
233- id = "search"
234- name = "search"
235- type = "text"
236- onKeyDown = { ( e ) => {
237- if ( e . key === "Enter" ) {
238- e . preventDefault ( ) ;
239- setSearchParams ( formData . search ) ;
240- }
241- } }
242- value = { value }
243- onChange = { ( e ) => setFormData ( { ...formData , search : e . target . value } ) }
244- ref = { ref }
245- placeholder = "Search for images"
246- />
247- </ InputGroup >
248- ) }
249- />
250- < Button variant = "primary" size = "xl" onClick = { ( ) => setSearchParams ( formData . search ) } >
251- Search
252- </ Button >
253- </ div >
254- { unsplashImages ? (
255- unsplashImages . length > 0 ? (
256- < div className = "grid grid-cols-4 gap-4" >
257- { unsplashImages . map ( ( image ) => (
258- < div
259- key = { image . id }
260- className = "relative col-span-2 aspect-video md:col-span-1"
261- onClick = { ( ) => {
262- setIsOpen ( false ) ;
263- onChange ( image . urls . regular ) ;
264- } }
265- >
266- < img
267- src = { image . urls . small }
268- alt = { image . alt_description }
269- className = "absolute top-0 left-0 h-full w-full cursor-pointer rounded-sm object-cover"
270- />
271- </ div >
272- ) ) }
273- </ div >
274- ) : (
275- < p className = "pt-7 text-center text-11 text-secondary" > No images found.</ p >
276- )
277- ) : (
278- < Loader className = "grid grid-cols-4 gap-4" >
279- < Loader . Item height = "80px" width = "100%" />
280- < Loader . Item height = "80px" width = "100%" />
281- < Loader . Item height = "80px" width = "100%" />
282- < Loader . Item height = "80px" width = "100%" />
283- < Loader . Item height = "80px" width = "100%" />
284- < Loader . Item height = "80px" width = "100%" />
285- < Loader . Item height = "80px" width = "100%" />
286- < Loader . Item height = "80px" width = "100%" />
287- </ Loader >
288- ) }
289- </ >
290- ) }
291- </ div >
292- </ TabsPanel >
293- < TabsPanel value = "images" >
294- < div className = "grid grid-cols-4 gap-4" >
295- { Object . values ( STATIC_COVER_IMAGES ) . map ( ( imageUrl , index ) => (
296- < div
297- key = { imageUrl }
298- className = "relative col-span-2 aspect-video md:col-span-1"
299- onClick = { ( ) => handleStaticImageSelect ( imageUrl ) }
300- >
301- < img
302- src = { imageUrl }
303- alt = { `Cover image ${ index + 1 } ` }
304- className = "absolute top-0 left-0 h-full w-full cursor-pointer rounded-sm object-cover transition-opacity hover:opacity-80"
305- />
306- </ div >
212+ < div className = "flex h-full min-h-0 w-full flex-col" >
213+ { /* Published TabsList is inline-flex; stretch it to the previous full-width bar. */ }
214+ < div className = "w-full min-w-0 [&_[role=tab]]:min-w-0 [&_[role=tab]]:flex-1 [&_[role=tablist]]:flex [&_[role=tablist]]:w-full" >
215+ < TabsList >
216+ { enabledTabs . map ( ( tab ) => (
217+ < Tab key = { tab . key } value = { tab . key } label = { tab . title } />
307218 ) ) }
308- </ div >
309- </ TabsPanel >
310- < TabsPanel value = "upload" >
311- < div className = "flex h-full w-full flex-col gap-y-2" >
312- < div className = "flex w-full flex-1 items-center gap-3" >
313- < div
314- { ...getRootProps ( ) }
315- className = { `relative grid h-full w-full cursor-pointer place-items-center rounded-lg p-12 text-center focus:ring-2 focus:ring-accent-strong focus:ring-offset-2 focus:outline-none ${
316- ( image === null && isDragActive ) || ! value
317- ? "border-2 border-dashed border-subtle hover:bg-surface-2"
318- : ""
319- } `}
320- >
321- < button
322- type = "button"
323- className = "absolute top-0 right-0 z-40 -translate-y-1/2 rounded-sm bg-surface-2 px-2 py-0.5 text-11 font-medium text-secondary"
324- >
325- Edit
326- </ button >
327- { image !== null || ( value && value !== "" ) ? (
328- < >
329- < img
330- src = { image ? URL . createObjectURL ( image ) : getCoverImageDisplayURL ( value , "" ) }
331- alt = "image"
332- className = "h-full w-full rounded-lg object-cover"
219+ </ TabsList >
220+ </ div >
221+ { /* Grid wrapper: published TabsPanel omits className, so fill height comes from a one-row grid. */ }
222+ < div className = "vertical-scrollbar scrollbar-sm grid min-h-0 w-full flex-1 grid-rows-1 overflow-x-hidden overflow-y-auto pt-1" >
223+ < TabsPanel value = "unsplash" >
224+ < div className = "space-y-4" >
225+ { ( unsplashImages || ! unsplashError ) && (
226+ < >
227+ < div className = "flex items-center gap-x-2" >
228+ < Controller
229+ control = { control }
230+ name = { "search" as FieldPath < TFieldValues > }
231+ render = { ( { field : { value, ref } } ) => (
232+ < InputGroup size = "2xl" >
233+ < Input
234+ size = "2xl"
235+ id = "search"
236+ name = "search"
237+ type = "text"
238+ onKeyDown = { ( e ) => {
239+ if ( e . key === "Enter" ) {
240+ e . preventDefault ( ) ;
241+ setSearchParams ( formData . search ) ;
242+ }
243+ } }
244+ value = { value }
245+ onChange = { ( e ) => setFormData ( { ...formData , search : e . target . value } ) }
246+ ref = { ref }
247+ placeholder = "Search for images"
248+ />
249+ </ InputGroup >
250+ ) }
333251 />
334- </ >
335- ) : (
336- < div >
337- < span className = "mt-2 block text-13 font-medium text-secondary" >
338- { isDragActive ? "Drop image here to upload" : "Drag & drop image here" }
339- </ span >
252+ < Button variant = "primary" size = "xl" onClick = { ( ) => setSearchParams ( formData . search ) } >
253+ Search
254+ </ Button >
340255 </ div >
341- ) }
256+ { unsplashImages ? (
257+ unsplashImages . length > 0 ? (
258+ < div className = "grid grid-cols-4 gap-4" >
259+ { unsplashImages . map ( ( image ) => (
260+ < div
261+ key = { image . id }
262+ className = "relative col-span-2 aspect-video md:col-span-1"
263+ onClick = { ( ) => {
264+ setIsOpen ( false ) ;
265+ onChange ( image . urls . regular ) ;
266+ } }
267+ >
268+ < img
269+ src = { image . urls . small }
270+ alt = { image . alt_description }
271+ className = "absolute top-0 left-0 h-full w-full cursor-pointer rounded-sm object-cover"
272+ />
273+ </ div >
274+ ) ) }
275+ </ div >
276+ ) : (
277+ < p className = "pt-7 text-center text-11 text-secondary" > No images found.</ p >
278+ )
279+ ) : (
280+ < Loader className = "grid grid-cols-4 gap-4" >
281+ < Loader . Item height = "80px" width = "100%" />
282+ < Loader . Item height = "80px" width = "100%" />
283+ < Loader . Item height = "80px" width = "100%" />
284+ < Loader . Item height = "80px" width = "100%" />
285+ < Loader . Item height = "80px" width = "100%" />
286+ < Loader . Item height = "80px" width = "100%" />
287+ < Loader . Item height = "80px" width = "100%" />
288+ < Loader . Item height = "80px" width = "100%" />
289+ </ Loader >
290+ ) }
291+ </ >
292+ ) }
293+ </ div >
294+ </ TabsPanel >
295+ < TabsPanel value = "images" >
296+ < div className = "grid grid-cols-4 gap-4" >
297+ { Object . values ( STATIC_COVER_IMAGES ) . map ( ( imageUrl , index ) => (
298+ < div
299+ key = { imageUrl }
300+ className = "relative col-span-2 aspect-video md:col-span-1"
301+ onClick = { ( ) => handleStaticImageSelect ( imageUrl ) }
302+ >
303+ < img
304+ src = { imageUrl }
305+ alt = { `Cover image ${ index + 1 } ` }
306+ className = "absolute top-0 left-0 h-full w-full cursor-pointer rounded-sm object-cover transition-opacity hover:opacity-80"
307+ />
308+ </ div >
309+ ) ) }
310+ </ div >
311+ </ TabsPanel >
312+ < TabsPanel value = "upload" >
313+ < div className = "flex h-full w-full flex-col gap-y-2" >
314+ < div className = "flex w-full flex-1 items-center gap-3" >
315+ < div
316+ { ...getRootProps ( ) }
317+ className = { `relative grid h-full w-full cursor-pointer place-items-center rounded-lg p-12 text-center focus:ring-2 focus:ring-accent-strong focus:ring-offset-2 focus:outline-none ${
318+ ( image === null && isDragActive ) || ! value
319+ ? "border-2 border-dashed border-subtle hover:bg-surface-2"
320+ : ""
321+ } `}
322+ >
323+ < button
324+ type = "button"
325+ className = "absolute top-0 right-0 z-40 -translate-y-1/2 rounded-sm bg-surface-2 px-2 py-0.5 text-11 font-medium text-secondary"
326+ >
327+ Edit
328+ </ button >
329+ { image !== null || ( value && value !== "" ) ? (
330+ < >
331+ < img
332+ src = { image ? URL . createObjectURL ( image ) : getCoverImageDisplayURL ( value , "" ) }
333+ alt = "image"
334+ className = "h-full w-full rounded-lg object-cover"
335+ />
336+ </ >
337+ ) : (
338+ < div >
339+ < span className = "mt-2 block text-13 font-medium text-secondary" >
340+ { isDragActive ? "Drop image here to upload" : "Drag & drop image here" }
341+ </ span >
342+ </ div >
343+ ) }
342344
343- < input { ...getInputProps ( ) } />
345+ < input { ...getInputProps ( ) } />
346+ </ div >
344347 </ div >
345- </ div >
346- { fileRejections . length > 0 && (
347- < p className = "text-13 text-danger-primary" >
348- { fileRejections [ 0 ] . errors [ 0 ] . code === "file-too-large"
349- ? "The image size cannot exceed 5 MB."
350- : "Please upload a file in a valid format." }
351- </ p >
352- ) }
348+ { fileRejections . length > 0 && (
349+ < p className = "text-13 text-danger-primary" >
350+ { fileRejections [ 0 ] . errors [ 0 ] . code === "file-too-large"
351+ ? "The image size cannot exceed 5 MB."
352+ : "Please upload a file in a valid format." }
353+ </ p >
354+ ) }
353355
354- < p className = "text-13 text-secondary" > File formats supported- .jpeg, .jpg, .png, .webp</ p >
356+ < p className = "text-13 text-secondary" > File formats supported- .jpeg, .jpg, .png, .webp</ p >
355357
356- < div className = "flex h-12 items-start justify-end gap-2" >
357- < Button
358- variant = "secondary"
359- onClick = { ( ) => {
360- setIsOpen ( false ) ;
361- setImage ( null ) ;
362- } }
363- >
364- Cancel
365- </ Button >
366- < Button
367- variant = "primary"
368- className = "w-full"
369- onClick = { handleSubmit }
370- disabled = { ! image }
371- loading = { isImageUploading }
372- >
373- { isImageUploading ? "Uploading" : "Upload & Save" }
374- </ Button >
358+ < div className = "flex h-12 items-start justify-end gap-2" >
359+ < Button
360+ variant = "secondary"
361+ onClick = { ( ) => {
362+ setIsOpen ( false ) ;
363+ setImage ( null ) ;
364+ } }
365+ >
366+ Cancel
367+ </ Button >
368+ < Button
369+ variant = "primary"
370+ className = "w-full"
371+ onClick = { handleSubmit }
372+ disabled = { ! image }
373+ loading = { isImageUploading }
374+ >
375+ { isImageUploading ? "Uploading" : "Upload & Save" }
376+ </ Button >
377+ </ div >
375378 </ div >
376- </ div >
377- </ TabsPanel >
379+ </ TabsPanel >
380+ </ div >
378381 </ div >
379382 </ Tabs >
380383 </ div >
0 commit comments