import React from "react"; import { Button } from "../button"; import BlogCard from "../cards/blog-card"; import FloatingImageCard from "../cards/floating-image-card"; import FullImageCard from "../cards/full-image-card"; import SimpleCard from "../cards/simple-card"; import { CalloutCardType, CalloutItem, CalloutProps } from "./types"; import { AnimationWrapper } from "@shared/components/animation-wrapper"; import { Text } from "@shared/components/text"; import { cx } from "@shared/utils"; const backgroundClassMap: Record = { cream500: "bg-[#FFFEEF]", gray100: "bg-fill-secondary", white: "bg-white", transparent: "", blue: "bg-fill-brand", green: "bg-fill-brand-accent", navy: "bg-fill-inverse", purple: "bg-fill-brand-tertiary", yellow: "bg-[#F5FF1E]", }; // Literal class strings (Tailwind JIT only picks up literal tokens; do not // build these by concatenation at runtime). // Per-card responsive width classes for the flex-wrap layout. Mobile is // always full-width; md renders 1 or 2 columns; lg renders up to 6. // The calc() values subtract a portion of the gap so cards fit cleanly. const mdWidthMap: Record = { 1: "md:w-full", 2: "md:w-[calc(50%-1rem)]", }; const lgWidthMap: Record = { 1: "lg:w-full", 2: "lg:w-[calc(50%-0.75rem)]", 3: "lg:w-[calc(33.333%-1rem)]", 4: "lg:w-[calc(25%-1.125rem)]", 5: "lg:w-[calc(20%-1.2rem)]", 6: "lg:w-[calc(16.666%-1.25rem)]", }; /** * Mirrors the local @ui Callout `calculateOptimalColumns` logic: * - ≤4 cards: one per column * - divisible by 3: 3 cols * - divisible by 4: 4 cols * - >6 cards: 4 cols * - else: 3 cols */ const calculateOptimalColumns = (count: number): number => { if (count <= 4) return count || 1; if (count % 3 === 0) return 3; if (count % 4 === 0) return 4; if (count > 6) return 4; return 3; }; const clampCol = (n: number) => Math.max(1, Math.min(6, n)); export const Callout: React.FC = ({ anchorId, title, items, enableHeading = false, subtitle, description, finePrint, cta, color = "dark", maxWidth = true, maxCardsPerRow, cardType = "simple", backgroundColor, background, textColor, containerClassName, innerClassName, applyBoxShadow = false, cardStackingMobile = true, cardsWidth = true, noGutter = false, disableAnimation, animationType = "lift", }) => { const itemCount = items?.length ?? 0; const desktopCols = clampCol( maxCardsPerRow ?? calculateOptimalColumns(itemCount) ); const lgCols = clampCol(Math.min(desktopCols, itemCount || desktopCols)); // Layout selection: // - cardsWidth === false → explicit vertical stack (single column) // - any other value (true / undefined / string / etc.) → legacy // flex-wrap layout that always renders multi-column on md+ and // full-width on mobile. This matches the pre-0.1.79 DOM and keeps // consumers like SMB-browse rendering correctly without changes. const isStackMode = cardsWidth === false && cardType !== "blog"; const gridClass = isStackMode ? cx( "flex flex-col items-stretch self-stretch", noGutter ? "gap-0" : "gap-6" ) : cx( "flex flex-wrap items-stretch justify-center self-stretch", noGutter ? "gap-0" : "gap-6", noGutter ? "md:gap-0" : "md:gap-6" ); // Per-card width classes — only used for flex-wrap layout. Mobile is // full-width; md respects `cardStackingMobile` (1 col when true, 2 cols // when false); lg uses the optimal column count. Stack mode skips this. const cardWidthClass = isStackMode ? "" : cx( "w-full", mdWidthMap[clampCol(cardStackingMobile ? 1 : Math.min(2, desktopCols))], lgWidthMap[lgCols] ); const renderCard = (item: CalloutItem, index: number) => { const itemCardType: CalloutCardType = item.cardType ?? cardType; // Stack mode preserves any legacy lgWidth/mdWidth on the card itself; // flex-wrap mode controls width via the wrapper div, so strip them. const widthProps = isStackMode ? { lgWidth: undefined, mdWidth: undefined } : {}; switch (itemCardType) { case "blog": { const blogItem = item as any; return ( ); } case "fullImage": return ( ); case "floatingImage": return ( ); case "simple": default: return ( ); } }; const sectionStyle = background ? { background } : undefined; const headingStyle = textColor ? { color: textColor } : undefined; const sectionBgClass = background ? "" : backgroundColor ? (backgroundClassMap[backgroundColor] ?? "") : ""; return (
{title && ( {title} )} {subtitle && ( {subtitle} )} {description && ( {description} )}
{items.map((item, index: number) => isStackMode ? ( {renderCard(item, index)} ) : (
{renderCard(item, index)}
) )}
{(cta || finePrint) && (
{cta ? ( ) : null} {finePrint ? ( {finePrint} ) : null}
)}
); }; export default Callout;