feat: add border to image hash placeholder

This commit is contained in:
codytseng
2025-12-24 22:48:38 +08:00
parent 41a65338b5
commit 526b64aec0
9 changed files with 28 additions and 26 deletions

View File

@@ -73,13 +73,13 @@ export default function Image({
} }
return ( return (
<div className={cn('relative overflow-hidden', classNames.wrapper)} {...props}> <div className={cn('relative overflow-hidden rounded-xl', classNames.wrapper)} {...props}>
{/* Spacer: transparent image to maintain dimensions when image is loading */} {/* Spacer: transparent image to maintain dimensions when image is loading */}
{isLoading && dim?.width && dim?.height && ( {isLoading && dim?.width && dim?.height && (
<img <img
src={`data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='${dim.width}' height='${dim.height}'%3E%3C/svg%3E`} src={`data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='${dim.width}' height='${dim.height}'%3E%3C/svg%3E`}
className={cn( className={cn(
'object-cover rounded-xl transition-opacity pointer-events-none w-full h-full', 'object-cover transition-opacity pointer-events-none w-full h-full',
className className
)} )}
alt="" alt=""
@@ -91,7 +91,7 @@ export default function Image({
<ThumbHashPlaceholder <ThumbHashPlaceholder
thumbHash={thumbHash} thumbHash={thumbHash}
className={cn( className={cn(
'w-full h-full transition-opacity rounded-xl', 'w-full h-full transition-opacity',
isLoading ? 'opacity-100' : 'opacity-0' isLoading ? 'opacity-100' : 'opacity-0'
)} )}
/> />
@@ -99,14 +99,14 @@ export default function Image({
<BlurHashCanvas <BlurHashCanvas
blurHash={blurHash} blurHash={blurHash}
className={cn( className={cn(
'w-full h-full transition-opacity rounded-xl', 'w-full h-full transition-opacity',
isLoading ? 'opacity-100' : 'opacity-0' isLoading ? 'opacity-100' : 'opacity-0'
)} )}
/> />
) : ( ) : (
<Skeleton <Skeleton
className={cn( className={cn(
'w-full h-full transition-opacity rounded-xl', 'w-full h-full transition-opacity',
isLoading ? 'opacity-100' : 'opacity-0', isLoading ? 'opacity-100' : 'opacity-0',
classNames.skeleton classNames.skeleton
)} )}
@@ -124,7 +124,7 @@ export default function Image({
onLoad={handleLoad} onLoad={handleLoad}
onError={handleError} onError={handleError}
className={cn( className={cn(
'object-cover rounded-xl transition-opacity pointer-events-none w-full h-full', 'object-cover transition-opacity pointer-events-none w-full h-full',
isLoading ? 'opacity-0 absolute inset-0' : '', isLoading ? 'opacity-0 absolute inset-0' : '',
className className
)} )}
@@ -137,7 +137,7 @@ export default function Image({
alt={alt} alt={alt}
decoding="async" decoding="async"
loading="lazy" loading="lazy"
className={cn('object-cover rounded-xl w-full h-full transition-opacity', className)} className={cn('object-cover w-full h-full transition-opacity', className)}
/> />
) : ( ) : (
<div <div

View File

@@ -94,9 +94,9 @@ export default function ImageGallery({
<ImageWithLightbox <ImageWithLightbox
key={i} key={i}
image={image} image={image}
className="max-h-[80vh] sm:max-h-[50vh] object-contain border" className="max-h-[80vh] sm:max-h-[50vh] object-contain"
classNames={{ classNames={{
wrapper: cn('w-fit max-w-full', className) wrapper: cn('w-fit max-w-full border', className)
}} }}
/> />
)) ))
@@ -107,10 +107,10 @@ export default function ImageGallery({
imageContent = ( imageContent = (
<Image <Image
key={0} key={0}
className="max-h-[80vh] sm:max-h-[50vh] object-contain border" className="max-h-[80vh] sm:max-h-[50vh] object-contain"
classNames={{ classNames={{
errorPlaceholder: 'aspect-square h-[30vh]', errorPlaceholder: 'aspect-square h-[30vh]',
wrapper: 'cursor-zoom-in' wrapper: 'cursor-zoom-in border'
}} }}
image={displayImages[0]} image={displayImages[0]}
onClick={(e) => handlePhotoClick(e, 0)} onClick={(e) => handlePhotoClick(e, 0)}
@@ -122,8 +122,8 @@ export default function ImageGallery({
{displayImages.map((image, i) => ( {displayImages.map((image, i) => (
<Image <Image
key={i} key={i}
className="aspect-square w-full border" className="aspect-square w-full"
classNames={{ wrapper: 'cursor-zoom-in' }} classNames={{ wrapper: 'cursor-zoom-in border' }}
image={image} image={image}
onClick={(e) => handlePhotoClick(e, i)} onClick={(e) => handlePhotoClick(e, i)}
/> />
@@ -136,8 +136,8 @@ export default function ImageGallery({
{displayImages.map((image, i) => ( {displayImages.map((image, i) => (
<Image <Image
key={i} key={i}
className="aspect-square w-full border" className="aspect-square w-full"
classNames={{ wrapper: 'cursor-zoom-in' }} classNames={{ wrapper: 'cursor-zoom-in border' }}
image={image} image={image}
onClick={(e) => handlePhotoClick(e, i)} onClick={(e) => handlePhotoClick(e, i)}
/> />

View File

@@ -67,7 +67,7 @@ export default function ImageWithLightbox({
key={0} key={0}
className={className} className={className}
classNames={{ classNames={{
wrapper: cn('rounded-lg border cursor-zoom-in', classNames.wrapper), wrapper: cn('border cursor-zoom-in', classNames.wrapper),
errorPlaceholder: 'aspect-square h-[30vh]', errorPlaceholder: 'aspect-square h-[30vh]',
skeleton: classNames.skeleton skeleton: classNames.skeleton
}} }}

View File

@@ -26,7 +26,7 @@ export default function FollowPack({ event, className }: { event: Event; classNa
{image && ( {image && (
<Image <Image
image={{ url: image, pubkey: event.pubkey }} image={{ url: image, pubkey: event.pubkey }}
className="w-24 h-20 object-cover rounded-lg" className="w-24 h-20 object-cover"
classNames={{ classNames={{
wrapper: 'w-24 h-20 flex-shrink-0', wrapper: 'w-24 h-20 flex-shrink-0',
errorPlaceholder: 'w-24 h-20' errorPlaceholder: 'w-24 h-20'

View File

@@ -67,7 +67,7 @@ export default function LongFormArticlePreview({
{metadata.image && autoLoadMedia && ( {metadata.image && autoLoadMedia && (
<Image <Image
image={{ url: metadata.image, pubkey: event.pubkey }} image={{ url: metadata.image, pubkey: event.pubkey }}
className="rounded-lg aspect-[4/3] xl:aspect-video object-cover bg-foreground h-44" className="aspect-[4/3] xl:aspect-video object-cover bg-foreground h-44"
hideIfError hideIfError
/> />
)} )}

View File

@@ -40,8 +40,8 @@ export function ReactionNotification({
<Image <Image
image={{ url: emojiUrl, pubkey: notification.pubkey }} image={{ url: emojiUrl, pubkey: notification.pubkey }}
alt={emojiName} alt={emojiName}
className="w-6 h-6 rounded-md" className="w-6 h-6"
classNames={{ errorPlaceholder: 'bg-transparent' }} classNames={{ errorPlaceholder: 'bg-transparent', wrapper: 'rounded-md' }}
errorPlaceholder={<Heart size={24} className="text-red-400" />} errorPlaceholder={<Heart size={24} className="text-red-400" />}
/> />
) )

View File

@@ -1,5 +1,4 @@
import { generateImageByPubkey } from '@/lib/pubkey' import { generateImageByPubkey } from '@/lib/pubkey'
import { cn } from '@/lib/utils'
import { useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
import Image from '../Image' import Image from '../Image'
@@ -27,7 +26,10 @@ export default function ProfileBanner({
<Image <Image
image={{ url: bannerUrl, pubkey }} image={{ url: bannerUrl, pubkey }}
alt={`${pubkey} banner`} alt={`${pubkey} banner`}
className={cn('rounded-none', className)} className={className}
classNames={{
wrapper: 'rounded-none'
}}
errorPlaceholder={defaultBanner} errorPlaceholder={defaultBanner}
/> />
) )

View File

@@ -36,9 +36,9 @@ export default function RelayInfo({ url, className }: { url: string; className?:
<div className="px-4 space-y-4"> <div className="px-4 space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center gap-2 justify-between"> <div className="flex items-center gap-2 justify-between">
<div className="flex gap-2 items-center truncate"> <div className="flex gap-2 items-center flex-1">
<RelayIcon url={url} className="w-8 h-8" /> <RelayIcon url={url} className="w-8 h-8" />
<div className="text-2xl font-semibold truncate select-text"> <div className="text-2xl font-semibold truncate select-text flex-1 w-0">
{relayInfo.name || relayInfo.shortUrl} {relayInfo.name || relayInfo.shortUrl}
</div> </div>
</div> </div>

View File

@@ -68,9 +68,9 @@ export default function WebPreview({
{image && ( {image && (
<Image <Image
image={{ url: image }} image={{ url: image }}
className="aspect-[4/3] xl:aspect-video bg-foreground h-44 rounded-none border-r" className="aspect-[4/3] xl:aspect-video bg-foreground h-44"
classNames={{ classNames={{
skeleton: 'rounded-none border-r' wrapper: 'rounded-none border-r'
}} }}
hideIfError hideIfError
/> />