11import React , { useRef , useState , useCallback , lazy , Suspense } from 'react' ;
22import { Button , EmptyValue } from '@object-ui/components' ;
33import { useUpload } from '@object-ui/providers' ;
4- import { Upload , X , Image as ImageIcon , Crop as CropIcon , Loader2 } from 'lucide-react' ;
4+ import { useObjectTranslation } from '@object-ui/i18n' ;
5+ import { X , Image as ImageIcon , Crop as CropIcon , Loader2 } from 'lucide-react' ;
56import { FieldWidgetProps } from './types' ;
7+ import { ImageLightbox } from './ImageLightbox' ;
68import { useUploadingSignal } from './useUploadingSignal' ;
79import {
810 fileValueForSubmit ,
@@ -32,7 +34,9 @@ export function ImageField({ value, onChange, field, readonly, onUploadingChange
3234 */
3335 const cropEnabled = imageField ?. crop !== false ;
3436 const [ cropTarget , setCropTarget ] = useState < { index : number ; src : string ; name : string } | null > ( null ) ;
37+ const [ lightboxIndex , setLightboxIndex ] = useState < number | null > ( null ) ;
3538 const { upload } = useUpload ( ) ;
39+ const { t } = useObjectTranslation ( ) ;
3640 const [ uploading , setUploading ] = useState ( false ) ;
3741 // Display details of just-uploaded images, keyed by their new `sys_file` id.
3842 // Submitting the reference form means the field value no longer carries the
@@ -83,20 +87,41 @@ export function ImageField({ value, onChange, field, readonly, onUploadingChange
8387 [ views ] ,
8488 ) ;
8589
90+ const lightboxImages = views . filter ( ( v ) => v . url ) . map ( ( v ) => ( { url : v . url as string , name : v . name } ) ) ;
91+
8692 if ( readonly ) {
87- if ( ! value ) return < EmptyValue /> ;
93+ if ( ! value || lightboxImages . length === 0 ) return < EmptyValue /> ;
8894
8995 return (
90- < div className = "flex flex-wrap gap-2" >
91- { views . map ( ( img , idx ) => (
92- < img
93- key = { idx }
94- src = { img . url || '' }
95- alt = { img . name || `Image ${ idx + 1 } ` }
96- className = "size-20 rounded-md object-cover border border-gray-200"
96+ < >
97+ < div className = "flex flex-wrap gap-2" >
98+ { lightboxImages . map ( ( img , idx ) => (
99+ < button
100+ key = { idx }
101+ type = "button"
102+ onClick = { ( ) => setLightboxIndex ( idx ) }
103+ className = "group relative overflow-hidden rounded-md border border-border focus:outline-none focus:ring-2 focus:ring-ring"
104+ aria-label = { t ( 'fields.image.enlarge' , { name : img . name || t ( 'fields.image.imageAlt' , { index : idx + 1 } ) } ) }
105+ >
106+ < img
107+ src = { img . url }
108+ alt = { img . name || t ( 'fields.image.imageAlt' , { index : idx + 1 } ) }
109+ className = "size-20 object-cover transition-transform duration-150 group-hover:scale-105"
110+ />
111+ < span className = "pointer-events-none absolute inset-0 bg-black/0 transition-colors group-hover:bg-black/10" />
112+ </ button >
113+ ) ) }
114+ </ div >
115+ { lightboxIndex !== null && (
116+ < ImageLightbox
117+ images = { lightboxImages }
118+ index = { lightboxIndex }
119+ open
120+ onOpenChange = { ( o ) => ! o && setLightboxIndex ( null ) }
121+ onIndexChange = { setLightboxIndex }
97122 />
98- ) ) }
99- </ div >
123+ ) }
124+ </ >
100125 ) ;
101126 }
102127
@@ -153,7 +178,7 @@ export function ImageField({ value, onChange, field, readonly, onUploadingChange
153178 < div key = { idx } className = "relative group" >
154179 < img
155180 src = { img . url || '' }
156- alt = { img . name || `Image ${ idx + 1 } ` }
181+ alt = { img . name || t ( 'fields.image.imageAlt' , { index : idx + 1 } ) }
157182 className = "size-20 rounded-md object-cover border border-gray-200"
158183 />
159184 < div className = "absolute top-1 right-1 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity" >
@@ -164,7 +189,7 @@ export function ImageField({ value, onChange, field, readonly, onUploadingChange
164189 size = "sm"
165190 onClick = { ( ) => openCropper ( idx ) }
166191 className = "h-6 w-6 p-0"
167- aria-label = { `Crop image ${ idx + 1 } ` }
192+ aria-label = { t ( 'fields. image.crop' , { index : idx + 1 } ) }
168193 data-testid = { `image-field-crop-${ idx } ` }
169194 >
170195 < CropIcon className = "size-3" />
@@ -176,6 +201,7 @@ export function ImageField({ value, onChange, field, readonly, onUploadingChange
176201 size = "sm"
177202 onClick = { ( ) => handleRemove ( idx ) }
178203 className = "h-6 w-6 p-0"
204+ aria-label = { t ( 'fields.image.remove' , { index : idx + 1 } ) }
179205 >
180206 < X className = "size-3" />
181207 </ Button >
@@ -198,7 +224,11 @@ export function ImageField({ value, onChange, field, readonly, onUploadingChange
198224 ) : (
199225 < ImageIcon className = "size-4 mr-2" />
200226 ) }
201- { uploading ? 'Uploading…' : images . length > 0 ? 'Add More Images' : 'Upload Image' }
227+ { uploading
228+ ? t ( 'fields.image.uploading' )
229+ : images . length > 0
230+ ? t ( 'fields.image.addMore' )
231+ : t ( 'fields.image.upload' ) }
202232 </ Button >
203233 </ div >
204234
0 commit comments