diff --git a/app/components/elements/Dialog.module.css b/app/components/elements/Dialog.module.css index 37c2037fd..4f7c35854 100644 --- a/app/components/elements/Dialog.module.css +++ b/app/components/elements/Dialog.module.css @@ -18,12 +18,12 @@ margin: auto; outline: none; animation: zoom-in-95 300ms ease-out; +} - &::backdrop { - background-color: rgba(0, 0, 0, 0.25); - backdrop-filter: blur(10px); - animation: fade-in 300ms ease-out; - } +.blurredBackdrop::backdrop { + background-color: rgba(0, 0, 0, 0.25); + backdrop-filter: blur(10px); + animation: fade-in 300ms ease-out; } @keyframes fade-in { diff --git a/app/components/elements/Dialog.tsx b/app/components/elements/Dialog.tsx index bfc69b631..5f6df54a7 100644 --- a/app/components/elements/Dialog.tsx +++ b/app/components/elements/Dialog.tsx @@ -13,6 +13,8 @@ import styles from "./Dialog.module.css"; interface DialogElementProps { id?: string; className?: string; + /** The standard dialog backdrop: dimmed and blurred page behind. */ + blurredBackdrop?: boolean; isDismissable?: boolean; onClose?: () => void; "aria-label"?: string; @@ -57,6 +59,7 @@ export function SendouModal({ ref, ...rest }: DialogElementProps) { function DialogElement({ id, className, + blurredBackdrop, isDismissable, onClose, "aria-label": ariaLabel, @@ -68,7 +71,9 @@ function DialogElement({ (); + export function ArtGrid({ arts, enablePreview = false, @@ -84,68 +86,99 @@ export function ArtGrid({ } function BigImageDialog({ close, art }: { close: () => void; art: ListedArt }) { + const dialogRef = React.useRef(null); + const [infoVisible, setInfoVisible] = React.useState(true); const [imageSettled, imageRef] = useImageSettled(); + const [aspectRatio, placeholderRef] = useImageAspectRatio(); const { formatter } = useDateTimeFormat({ year: "numeric", month: "numeric", day: "numeric", }); + const dateText = formatter.format(databaseTimestampToDate(art.createdAt)); + + const handleClick = (event: React.MouseEvent) => { + const target = event.target as HTMLElement; + if (target.closest("a")) return; + if (target.closest("figure") && !deviceCanHover()) { + setInfoVisible((visible) => !visible); + return; + } + dialogRef.current?.close(); + }; + return ( - - - {art.tags || art.linkedUsers ? ( -
- {art.linkedUsers?.map((user) => ( - - {user.username} - - ))} - {art.tags?.map((tag) => ( - - #{tag.name} - - ))} -
- ) : null} - {art.description ? ( -
+
- {art.description} -
- ) : null} - } - > - Close - -
+ + +
+ + {art.description ? ( +
+ {art.description} +
+ ) : null} + {art.tags || art.linkedUsers ? ( +
+ {art.linkedUsers?.map((user) => ( + + {user.username} + + ))} + {art.tags?.map((tag) => ( + + #{tag.name} + + ))} +
+ ) : null} +
+ + + ); } @@ -173,6 +206,7 @@ function ImagePreview({ src={previewUrl(art.url)} loading="lazy" onClick={onClick} + onPointerEnter={enablePreview ? () => preloadImage(art.url) : undefined} ref={imageRef} className={enablePreview ? styles.thumbnail : undefined} data-testid="art-image" @@ -329,3 +363,39 @@ function useImageSettled() { return [imageSettled, imageRef] as const; } + +/** Aspect ratio of the image once it has loaded, and the ref to give it. */ +function useImageAspectRatio() { + const [aspectRatio, setAspectRatio] = React.useState(null); + + const imageRef = (image: HTMLImageElement | null) => { + if (!image) return; + + const measure = () => { + if (image.naturalWidth > 0 && image.naturalHeight > 0) { + setAspectRatio(image.naturalWidth / image.naturalHeight); + } + }; + if (image.complete) { + measure(); + return; + } + + image.addEventListener("load", measure); + return () => image.removeEventListener("load", measure); + }; + + return [aspectRatio, imageRef] as const; +} + +/** Touch devices have no hover to reveal the lightbox info with, so a tap on the image toggles it instead. */ +function deviceCanHover() { + return window.matchMedia("(hover: hover)").matches; +} + +/** Warms the browser cache so the full-size image is ready by the time the lightbox opens. */ +function preloadImage(url: string) { + if (preloadedImageUrls.has(url)) return; + preloadedImageUrls.add(url); + new Image().src = url; +} diff --git a/changelog/2026-09-19-art-lightbox.md b/changelog/2026-09-19-art-lightbox.md new file mode 100644 index 000000000..2648736a4 --- /dev/null +++ b/changelog/2026-09-19-art-lightbox.md @@ -0,0 +1,5 @@ +--- +navItem: art +type: feature +--- +User art page big art view has been redesigned and speed improved.