From 821d3296ea4b6bde499e5497d258f15ab8dd1dbc Mon Sep 17 00:00:00 2001 From: Paul Nothaft Date: Thu, 29 Jan 2026 23:16:14 +0100 Subject: [PATCH] fix: use CSS Columns for gap-free mosaic layout (#146) Replaced CSS Grid with span rules approach with CSS Columns to eliminate gaps and white spaces in the mosaic layout. Images now flow vertically within columns, maintaining their natural aspect ratios without gaps. --- .../gallery/layouts/MosaicGalleryLayout.tsx | 83 ++++++------------- 1 file changed, 26 insertions(+), 57 deletions(-) diff --git a/frontend/src/components/gallery/layouts/MosaicGalleryLayout.tsx b/frontend/src/components/gallery/layouts/MosaicGalleryLayout.tsx index 357b1782..341abe49 100644 --- a/frontend/src/components/gallery/layouts/MosaicGalleryLayout.tsx +++ b/frontend/src/components/gallery/layouts/MosaicGalleryLayout.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { Download, Maximize2, Check, Heart, MessageSquare } from 'lucide-react'; import { AuthenticatedImage } from '../../common'; import { FeedbackIdentityModal } from '../../gallery/FeedbackIdentityModal'; @@ -9,45 +9,17 @@ import type { Photo } from '../../../types'; /** * Mosaic Gallery Layout * - * Uses CSS Grid with span rules based on photo aspect ratios to create - * a visually appealing mosaic layout. Based on best practices from: - * - https://www.30secondsofcode.org/css/s/image-mosaic/ - * - https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_grid_layout + * Uses CSS Columns for a gap-free masonry/mosaic effect. + * Images flow vertically within columns, maintaining their natural aspect ratios. + * This approach eliminates gaps that occur with CSS Grid span rules. * - * Portrait photos span 2 rows, wide landscape photos span 2 columns. + * Based on: + * - https://css-tricks.com/seamless-responsive-photo-grid/ + * - https://www.30secondsofcode.org/css/s/image-mosaic/ */ -// Determine grid span based on aspect ratio -type SpanType = 'normal' | 'tall' | 'wide'; - -const getSpanType = (photo: Photo): SpanType => { - const width = photo.width || 1; - const height = photo.height || 1; - const ratio = width / height; - - // Very tall portrait (aspect ratio < 0.7) - span 2 rows - if (ratio < 0.75) return 'tall'; - // Very wide landscape (aspect ratio > 1.6) - span 2 columns - if (ratio > 1.6) return 'wide'; - // Normal aspect ratio - return 'normal'; -}; - -// Get CSS classes for grid item based on span type -const getGridItemClasses = (spanType: SpanType): string => { - switch (spanType) { - case 'tall': - return 'row-span-2'; - case 'wide': - return 'col-span-2'; - default: - return ''; - } -}; - interface MosaicPhotoProps { photo: Photo; - spanType: SpanType; isSelected: boolean; isSelectionMode: boolean; onClick: (e: React.MouseEvent) => void; @@ -66,7 +38,6 @@ interface MosaicPhotoProps { const MosaicPhoto: React.FC = ({ photo, - spanType, isSelected, isSelectionMode, onClick, @@ -84,12 +55,11 @@ const MosaicPhoto: React.FC = ({ const [likedLocal, setLikedLocal] = React.useState(false); const canComment = Boolean(feedbackEnabled && feedbackOptions?.allowComments && onQuickComment); - const gridItemClasses = getGridItemClasses(spanType); - return ( <>
{ e.stopPropagation(); onClick(e); @@ -98,7 +68,7 @@ const MosaicPhoto: React.FC = ({ = ({ feedbackEnabled = false, feedbackOptions }) => { - // Pre-compute photos with their span types - const photosWithSpans = useMemo(() => { - return photos.map((photo, index) => ({ - photo, - originalIndex: index, - spanType: getSpanType(photo) - })); - }, [photos]); - return (
- {photosWithSpans.map(({ photo, originalIndex, spanType }) => ( + + {photos.map((photo, index) => ( { if (isSelectionMode && onPhotoSelect) { onPhotoSelect(photo.id); } else { - onPhotoClick(originalIndex); + onPhotoClick(index); } }} onDownload={(e) => onDownload(photo, e)} @@ -275,7 +244,7 @@ export const MosaicGalleryLayout: React.FC = ({ feedbackOptions={feedbackOptions} onQuickComment={() => { if (typeof onOpenPhotoWithFeedback !== 'undefined' && onOpenPhotoWithFeedback) { - onOpenPhotoWithFeedback(originalIndex); + onOpenPhotoWithFeedback(index); } }} />