feat(RichText): render gallery as 3-column grid for compact display

- Add new 'grid' preset to MediaEmbed with square aspect ratio and object-cover
- Update Gallery component to use CSS grid (3 cols) for images/videos
- Separate audio items into stacked layout below the grid
- Grid adapts to container width for varied event card sizes
This commit is contained in:
Claude
2026-01-07 09:22:59 +00:00
parent 6707e2d7ba
commit 160e888cf5
2 changed files with 32 additions and 10 deletions

View File

@@ -13,7 +13,7 @@ interface MediaEmbedProps {
url: string;
type?: "image" | "video" | "audio" | "auto";
alt?: string;
preset?: "inline" | "thumbnail" | "preview" | "banner";
preset?: "inline" | "thumbnail" | "grid" | "preview" | "banner";
className?: string;
// Image-specific
@@ -42,6 +42,12 @@ const PRESETS = {
maxWidthClass: "max-w-[120px]",
roundedClass: "rounded-md",
},
grid: {
maxHeightClass: "",
maxWidthClass: "w-full",
roundedClass: "rounded-md",
objectFit: "cover" as const,
},
preview: {
maxHeightClass: "max-h-[500px]",
maxWidthClass: "max-w-full",
@@ -61,7 +67,7 @@ const getDefaultAspectRatio = (
mediaType: string,
preset: string,
): string | undefined => {
if (preset === "thumbnail") return "1/1";
if (preset === "thumbnail" || preset === "grid") return "1/1";
if (mediaType === "video") return "16/9";
return undefined; // auto for images
};
@@ -256,7 +262,8 @@ export function MediaEmbed({
alt={alt || "Image"}
loading="lazy"
className={cn(
"w-full h-full object-contain",
"w-full h-full",
preset === "grid" ? "object-cover" : "object-contain",
presetStyles.roundedClass,
enableZoom && "cursor-zoom-in",
fadeIn && "transition-opacity duration-300",

View File

@@ -36,13 +36,13 @@ export function Gallery({ node }: GalleryNodeProps) {
if (isImageURL(url)) {
if (shouldShowMedia && options.showImages) {
return <MediaEmbed url={url} type="image" preset="inline" enableZoom />;
return <MediaEmbed url={url} type="image" preset="grid" enableZoom />;
}
return <MediaPlaceholder type="image" />;
}
if (isVideoURL(url)) {
if (shouldShowMedia && options.showVideos) {
return <MediaEmbed url={url} type="video" preset="inline" />;
return <MediaEmbed url={url} type="video" preset="grid" />;
}
return <MediaPlaceholder type="video" />;
}
@@ -65,13 +65,28 @@ export function Gallery({ node }: GalleryNodeProps) {
// Only show dialog for audio files
const audioLinks = links.filter((url) => isAudioURL(url));
// Separate media types for layout
const imageLinks = links.filter((url) => isImageURL(url) || isVideoURL(url));
const audioOnlyLinks = links.filter((url) => isAudioURL(url));
return (
<>
<div className="my-2 flex flex-wrap gap-2">
{links.map((url: string, i: number) => (
<div key={i}>{renderLink(url, i)}</div>
))}
</div>
{/* Grid layout for images/videos */}
{imageLinks.length > 0 && (
<div className="my-2 grid grid-cols-3 gap-1.5">
{imageLinks.map((url: string, i: number) => (
<div key={`${url}-${i}`}>{renderLink(url, links.indexOf(url))}</div>
))}
</div>
)}
{/* Stack layout for audio */}
{audioOnlyLinks.length > 0 && (
<div className="my-2 flex flex-col gap-2">
{audioOnlyLinks.map((url: string, i: number) => (
<div key={`${url}-${i}`}>{renderLink(url, links.indexOf(url))}</div>
))}
</div>
)}
{audioLinks.length > 0 && (
<MediaDialog
open={dialogOpen}