Sei un senior frontend engineer. Stai lavorando su un sito Next.js 16 + React 19 + Tailwind v4 in italiano, look chanhdai-inspired: colonna stretta 672px, Geist Sans + Geist Mono, hairline 1px, divisori a stripe diagonale, palette zinc. Token CSS disponibili: --bg, --bg-alt, --fg, --fg-muted, --fg-soft, --border, --border-strong, --accent. Usa SEMPRE queste variabili tramite le utility tailwind generate (bg-bg, text-fg-muted, border-border, ecc.). Helper "cn" da "@/lib/utils". Niente librerie UI extra: solo lucide-react e tailwind-merge. Genera un componente <BentoTile>: card decorativa con due render path. Props: - icon?: ComponentType — lucide - name: string - group?: string — kicker mono uppercase - variant?: "minimal" | "accent" | "mono" (default "minimal") - size?: "sm" | "md" | "lg" (default "md") — heights 80/128/176px - isNew?: boolean - preview?: ReactNode — preview live, frozen - previewFit?: "scale" | "cover" (default "scale") - previewInnerWidth?: number (default 320) — width pre-scale del wrapper - previewScale?: number (default 0.5) - className?: string Render path 1 — con preview: - div rounded-md border, h size, overflow-hidden - absolute inset-0 con [&_*]:!animate-none [&_*]:!transition-none [&_*]:!will-change-auto - Se previewFit "cover": absolute inset-0 con il preview dentro - Altrimenti: wrapper assoluto centrato (left-1/2 top-1/2, transform: translate(-50%,-50%) scale(N), width: previewInnerWidth) con il preview dentro - Optional accent dot top-right per isNew (con ring) - Niente icon, niente name, niente fog overlay Render path 2 — senza preview: - div rounded-md border + p-3 + h size, flex-col justify-between - Top: icon chip 28x28 (border + bg) + accent dot top-right se isNew - Bottom: kicker mono small + name 12.5px font-medium Varianti: - minimal: border-border bg-bg, label text-fg - accent: border-accent/40 bg-accent/[0.04], kicker text-accent - mono: border-fg bg-fg text-bg Esporta bentoSizeForSlug(slug), bentoVariantForSlug(slug, isNew?) per pick deterministici da char-code-sum. Constraints: server component, aria-hidden default. Output: file completo .tsx.
Component
LiveNewBento Tile
Bento card a doppia identità. Con `preview` set: la preview riempie la tile, frozen (animation off), niente overlay — la preview È la tile. Senza preview: layout etichettato classico (icon top-left, kicker + name bottom). Due fit per la preview: scale (centrata) o cover (fill edge-to-edge).
Esempio01
import type { ComponentType, ReactNode, SVGProps } from "react";
import { cn } from "@/lib/utils";
export type BentoTileSize = "sm" | "md" | "lg";
export type BentoTileVariant = "minimal" | "accent" | "mono";
export type BentoTilePreviewFit = "scale" | "cover";
export type BentoTileProps = {
/** Lucide icon component, rendered top-left when no preview is set. */
icon?: ComponentType<SVGProps<SVGSVGElement>>;
/** Component name. */
name: string;
/** Optional kicker shown above the name (mono small caps). */
group?: string;
/** Visual variant. Default "minimal". */
variant?: BentoTileVariant;
/** Vertical size. Default "md". */
size?: BentoTileSize;
/** Mark the tile as a "new" — shows an accent dot. */
isNew?: boolean;
/**
* Live preview rendered without a fog overlay. The whole subtree gets
* `animation: none !important` and `transition: none !important` applied
* so 24+ tiles in side gutters don't tank performance.
*
* When provided, the icon/name labels are NOT rendered — the preview is
* the tile. Use the parent context (catalog row, detail page) for the
* canonical label.
*/
preview?: ReactNode;
/**
* How the preview fits inside the tile.
* - "scale" (default): preview rendered in a 320px-wide wrapper, scaled
* by `previewScale`, centred. Best for component-style previews that
* are already laid out.
* - "cover": preview fills the tile (consumer controls sizing). Use for
* imagery that should fill edge-to-edge (e.g. `<img object-cover/>`).
*/
previewFit?: BentoTilePreviewFit;
/** Width of the rendered preview before it's scaled down. Default 320. */
previewInnerWidth?: number;
/** Scale factor applied to the preview. Default 0.5. */
previewScale?: number;
className?: string;
};
const SIZE_HEIGHT: Record<BentoTileSize, string> = {
sm: "h-20", // 80px
md: "h-32", // 128px
lg: "h-44", // 176px
};
const VARIANT_FRAME: Record<BentoTileVariant, string> = {
minimal: "border-border bg-bg",
accent: "border-accent/40 bg-accent/[0.04]",
mono: "border-fg bg-fg text-bg",
};
const VARIANT_ICON_CHIP: Record<BentoTileVariant, string> = {
minimal: "border-border bg-bg-alt text-fg",
accent: "border-accent/30 bg-bg text-accent",
mono: "border-bg/30 bg-bg/10 text-bg",
};
const VARIANT_LABEL: Record<BentoTileVariant, string> = {
minimal: "text-fg",
accent: "text-fg",
mono: "text-bg",
};
const VARIANT_KICKER: Record<BentoTileVariant, string> = {
minimal: "text-fg-soft",
accent: "text-accent",
mono: "text-bg/60",
};
/**
* Decorative bento card. Two render paths:
*
* - **with preview**: the preview fills the tile, no overlay, no labels.
* Optional accent dot (top-right) for `isNew`. The preview subtree is
* frozen — animations + transitions disabled — so dozens of tiles in
* side gutters don't tank performance.
* - **without preview**: labelled card (icon top-left, kicker + name
* bottom). The icon-only fallback is what the gutters show for the few
* components that don't ship a `preview()` (e.g. interactive demos that
* don't snapshot well).
*
* Three variants for visual rhythm without resorting to randomness — pick
* one deterministically (e.g. via slug hash) so SSR stays stable.
*/
export function BentoTile({
icon: Icon,
name,
group,
variant = "minimal",
size = "md",
isNew = false,
preview,
previewFit = "scale",
previewInnerWidth = 320,
previewScale = 0.5,
className,
}: BentoTileProps) {
if (preview) {
return (
<div
aria-hidden
className={cn(
"relative w-full overflow-hidden rounded-md border",
SIZE_HEIGHT[size],
VARIANT_FRAME[variant],
className,
)}
>
<div className="pointer-events-none absolute inset-0 select-none [&_*]:!animate-none [&_*]:!transition-none [&_*]:!will-change-auto">
{previewFit === "cover" ? (
<div className="absolute inset-0">{preview}</div>
) : (
<div
className="absolute left-1/2 top-1/2"
style={{
width: `${previewInnerWidth}px`,
transform: `translate(-50%, -50%) scale(${previewScale})`,
transformOrigin: "center",
}}
>
{preview}
</div>
)}
</div>
{isNew ? (
<span
aria-hidden
className={cn(
"absolute right-1.5 top-1.5 h-1.5 w-1.5 rounded-full ring-2",
variant === "mono" ? "bg-bg ring-fg" : "bg-accent ring-bg",
)}
/>
) : null}
</div>
);
}
return (
<div
aria-hidden
className={cn(
"relative flex w-full flex-col justify-between overflow-hidden rounded-md border p-3",
SIZE_HEIGHT[size],
VARIANT_FRAME[variant],
className,
)}
>
<div className="flex items-start justify-between">
{Icon ? (
<span
className={cn(
"grid h-7 w-7 place-items-center rounded-md border",
VARIANT_ICON_CHIP[variant],
)}
>
<Icon className="h-3.5 w-3.5" aria-hidden />
</span>
) : <span aria-hidden />}
{isNew ? (
<span
className={cn(
"h-1.5 w-1.5 rounded-full",
variant === "mono" ? "bg-bg" : "bg-accent",
)}
/>
) : null}
</div>
<div className="flex flex-col gap-0.5">
{group ? (
<span
className={cn(
"font-mono text-[9.5px] uppercase tracking-[0.08em]",
VARIANT_KICKER[variant],
)}
>
{group}
</span>
) : null}
<span
className={cn(
"text-[12.5px] font-medium leading-[1.3]",
VARIANT_LABEL[variant],
)}
>
{name}
</span>
</div>
</div>
);
}
/**
* Stable visual rhythm helpers — pick a size and variant from the slug so
* the bento layout is identical between SSR and client hydration.
*/
export function bentoSizeForSlug(slug: string): BentoTileSize {
const sum = [...slug].reduce((acc, ch) => acc + ch.charCodeAt(0), 0);
return (["sm", "md", "lg"] as const)[sum % 3];
}
export function bentoVariantForSlug(slug: string, isNew: boolean | undefined): BentoTileVariant {
if (isNew) return "accent";
const sum = [...slug].reduce((acc, ch) => acc + ch.charCodeAt(0), 0);
return sum % 5 === 0 ? "mono" : "minimal";
}
Note — Quando preview è settato il render path diventa solo-preview: niente icon, niente name, niente fog. Il subtree riceve [&_*]:!animate-none [&_*]:!transition-none [&_*]:!will-change-auto così 24+ tile nei gutter restano economiche. previewFit='cover' bypassa la scala e fa riempire alla preview tutta la tile (caso tipico: <img object-cover/>).
Prompt LLM02
Incolla in Claude o ChatGPT per generare la tua variante. Include il contesto del brand, i token e i vincoli del progetto.
Uso tipico03
// Preview path — preview fills the tile, no labels.
<BentoTile size="lg" isNew preview={<MyComponentPreview />} />
// Image-fill variant.
<BentoTile size="lg" previewFit="cover" preview={<img src={src} className="h-full w-full object-cover" />} />
// Label path — icon + kicker + name, no preview.
<BentoTile icon={Wind} name="Marquee" group="Motion" variant="accent" />Dipendenze04
- tailwind-merge
- clsx
- @/lib/utils#cn
Ti è servito? Dimmelo, oppure proponi il prossimo componente.
