import * as React from 'react'; import { Check } from 'lucide-react'; import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter, } from '../../ui/card'; import { Button } from '../../ui/button'; import { cn } from '../../shared/utils'; export type BlockColor = | 'primary' | 'chart-1' | 'chart-2' | 'chart-3' | 'chart-4' | 'chart-5' | 'success' | 'info' | 'warning' | 'destructive'; const colorTokens: Record = { primary: { border: 'border-primary', tint: 'bg-primary/5', icon: 'text-primary' }, 'chart-1': { border: 'border-[var(--chart-1)]', tint: 'bg-[var(--chart-1)]/5', icon: 'text-[var(--chart-1)]', }, 'chart-2': { border: 'border-[var(--chart-2)]', tint: 'bg-[var(--chart-2)]/5', icon: 'text-[var(--chart-2)]', }, 'chart-3': { border: 'border-[var(--chart-3)]', tint: 'bg-[var(--chart-3)]/5', icon: 'text-[var(--chart-3)]', }, 'chart-4': { border: 'border-[var(--chart-4)]', tint: 'bg-[var(--chart-4)]/5', icon: 'text-[var(--chart-4)]', }, 'chart-5': { border: 'border-[var(--chart-5)]', tint: 'bg-[var(--chart-5)]/5', icon: 'text-[var(--chart-5)]', }, success: { border: 'border-success', tint: 'bg-success/5', icon: 'text-success' }, info: { border: 'border-info', tint: 'bg-info/5', icon: 'text-info' }, warning: { border: 'border-warning', tint: 'bg-warning/5', icon: 'text-warning' }, destructive: { border: 'border-destructive', tint: 'bg-destructive/5', icon: 'text-destructive' }, }; export interface ServiceCardItem { /** Omit to render the card without an icon chip. */ icon?: React.ReactNode; color?: BlockColor; category?: string; title: string; description: string; checklist?: string[]; bullets?: string[]; cta?: { label: string; href?: string; onClick?: () => void }; /** Card surface tone — only applies when the grid's `variant` is `"card"`. */ background?: 'default' | 'white'; className?: string; } export interface ServiceCardGridProps { items: ServiceCardItem[]; variant?: 'card' | 'plain'; columns?: 2 | 3 | 4; className?: string; } const columnClasses: Record<2 | 3 | 4, string> = { 2: 'md:grid-cols-2', 3: 'md:grid-cols-3', 4: 'md:grid-cols-4', }; /** * Responsive grid of service/feature cards. * * @description * Renders a list of `ServiceCardItem`s as either bordered `Card`s with an * icon chip, checklist and CTA (`variant="card"`, e.g. a 3-up services * section) or a borderless icon + title + description layout for compact * 4-up feature grids (`variant="plain"`). * * @ai-rules * 1. Use `variant="card"` for services that need a checklist/bullets/CTA; use `variant="plain"` for simple icon-grid feature lists. * 2. `columns` sets the desktop column count (grid is always 1 column on mobile); it defaults to 3. * 3. Each item's `color` maps to `colorTokens` — never hardcode chip colors. * 4. `icon` is optional — omit it for an icon-less card (title + description, optionally with checklist/bullets/CTA). * 5. Each item's `background` (`"default"` | `"white"`) only applies in `variant="card"`. Keep it uniform within a single grid — vary it across separate sections/blocks instead, e.g. by pairing a `bg-muted` wrapper section with `"default"` cards, rather than mixing tones inside one grid. * 6. Pass `className: 'border'` on an item when its card sits directly on `bg-background` (the theme's `--card` and `--background` are identical in light mode, so without a border the card's own background is invisible there). */ export function ServiceCardGrid({ items, variant = 'card', columns = 3, className, }: ServiceCardGridProps) { return (
{items.map((item, index) => ( ))}
); } export interface ServiceCardProps extends ServiceCardItem { variant?: 'card' | 'plain'; className?: string; } /** * Single service/feature card, used standalone or via `ServiceCardGrid`. * * @description * Pairs a token-tinted icon chip with a title and description. In * `variant="card"` it renders inside a bordered `Card` with an optional * category label, checklist, bullet list and footer CTA button. In * `variant="plain"` it renders as a minimal chip + text stack with no * chrome, checklist or CTA — for dense icon-grid layouts. * * @ai-rules * 1. `checklist` and `bullets`/`cta` only render in `variant="card"`; they are ignored in `variant="plain"`. * 2. Pass `icon` as a `lucide-react` element; it is sized automatically inside the chip. Omit it entirely for an icon-less card. * 3. If `cta.href` is set the button renders as a link (`asChild`); otherwise it fires `cta.onClick`. * 4. `background` is ignored in `variant="plain"` (no surface to color); it maps directly to the underlying `Card`'s `variant`. */ export function ServiceCard({ icon, color = 'primary', category, title, description, checklist, bullets, cta, variant = 'card', background = 'default', className, }: ServiceCardProps) { const { border, tint, icon: iconColor } = colorTokens[color]; const iconChip = icon && (
svg]:size-6', iconColor)}>{icon}
); if (variant === 'plain') { return (
{iconChip}
{category && (

{category}

)}

{title}

{description}

); } return ( {iconChip}
{category && (

{category}

)} {title}
{description} {checklist && checklist.length > 0 && (
{checklist.map(item => (
{item}
))}
)} {bullets && bullets.length > 0 && (
    {bullets.map(bullet => (
  • {bullet}
  • ))}
)}
{cta && ( )}
); }