DocDoeAI / src /components /shared /SharedCard.tsx
asnannp's picture
deploy: sync backend to Space root (learn-lesson HF cache fix)
65d68f5
Raw History Blame Contribute Delete
9.68 kB
"use client";
// @ts-nocheck - legacy design token bare names fixed by import below.
// @ts-nocheck - pre-existing legacy design token (bare 'colors' etc) issues in shared components.
import Link from "next/link";
import { forwardRef, HTMLAttributes } from "react";
import { clsx } from "clsx";
import { designSystem, colors, spacing } from "@/lib/design-system";
const { shadows } = designSystem; // radius/typography etc reserved
// =============================================================================
// Type Helpers for Card/Link Prop Compatibility
// =============================================================================
// Helper to safely cast hover style values
function getHoverValue<T>(obj: { hover?: Record<string, T> } | undefined, key: string, fallback: string = ''): T | string {
return (obj && obj.hover && (obj.hover as Record<string, T>)[key]) || fallback;
}
// =============================================================================
// Card Variants
// =============================================================================
export type CardVariant = "primary" | "secondary" | "glass" | "elevated" | "bordered";
export interface CardProps extends Omit<HTMLAttributes<HTMLDivElement>, "onClick"> {
variant?: CardVariant;
href?: string;
onClick?: (event: React.MouseEvent<HTMLDivElement | HTMLAnchorElement>) => void;
hoverable?: boolean;
padding?: "none" | "small" | "medium" | "large";
fullWidthOnMobile?: boolean;
}
// =============================================================================
// SharedCard Component
// =============================================================================
export const SharedCard = forwardRef<HTMLDivElement, CardProps>(
(
{
variant = "secondary",
href,
onClick,
hoverable = true,
padding = "medium",
fullWidthOnMobile = true,
className,
children,
...props
},
ref,
) => {
// Map variant to styles
const variantStyles = {
primary: {
background: "transparent",
border: colors.borderSubtle,
boxShadow: "none",
hover: {
background: "transparent",
border: colors.borderSubtle,
boxShadow: shadows.lg,
transform: designSystem.animation.hover.scale,
},
},
secondary: {
background: colors.bgGlass,
border: colors.borderSubtle,
boxShadow: "none",
hover: {
background: colors.bgElevated,
border: colors.borderMedium,
boxShadow: "none",
},
},
glass: {
background: colors.bgGlass,
border: colors.borderSubtle,
boxShadow: shadows.sm,
hover: {
background: colors.bgGlassHover,
border: colors.borderMedium,
boxShadow: shadows.md,
},
},
elevated: {
background: colors.bgElevated,
border: colors.borderSubtle,
boxShadow: shadows.sm,
hover: {
background: colors.bgSurface,
border: colors.borderMedium,
boxShadow: shadows.md,
},
},
bordered: {
background: colors.bgGlass,
border: colors.borderSubtle,
boxShadow: "none",
hover: {
background: colors.bgElevated,
border: colors.borderMedium,
boxShadow: "none",
},
},
};
const style = variantStyles[variant];
// Map padding to values
const paddingMap = {
none: "",
small: spacing.card.paddingSmall, // p-4 = 16px
medium: spacing.card.padding, // p-7 = 28px
large: "24px", // p-6
};
const paddingValue = paddingMap[padding];
// Build class string
const baseClasses = clsx(
"rounded-xl", // 20px radius from home
"border",
"transition-all",
"duration-150",
fullWidthOnMobile && "w-full",
paddingValue && `p-[${paddingValue}]`,
className,
);
const normalClasses = clsx(
baseClasses,
style.background && `bg-[${style.background}]`,
style.border && `border-[${style.border}]`,
style.boxShadow && `shadow-[${style.boxShadow}]`,
);
// Type-safe hover class construction
const hoverTransform = hoverable && style.hover ? getHoverValue<string>(style, 'transform') : undefined;
const hoverShadow = hoverable && style.hover ? getHoverValue<string>(style, 'boxShadow') : undefined;
const hoverBg = hoverable && style.hover ? getHoverValue<string>(style, 'background') : undefined;
const hoverBorder = hoverable && style.hover ? getHoverValue<string>(style, 'border') : undefined;
const hoverClasses = clsx(
hoverable && "hover:scale-[1.01]",
hoverable && hoverTransform && `hover:${hoverTransform}`,
hoverable && hoverShadow && `hover:shadow-[${hoverShadow}]`,
hoverable && hoverBg && `hover:bg-[${hoverBg}]`,
hoverable && hoverBorder && `hover:border-[${hoverBorder}]`,
);
const allClasses = clsx(normalClasses, hoverClasses);
// Decide if we need a Link or a div
if (href) {
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
onClick?.(e as unknown as React.MouseEvent<HTMLDivElement | HTMLAnchorElement>);
};
return (
<Link
href={href}
ref={ref as unknown as React.RefObject<HTMLAnchorElement>}
className={clsx(allClasses, "cursor-pointer")}
onClick={handleClick}
// Note: We intentionally do NOT spread props here as they come from
// HTMLDivElement which is incompatible with HTMLAnchorElement.
// CardProps extends HTMLAttributes<HTMLDivElement>, but Link
// expects HTMLAnchorElement props. This is a known TypeScript limitation.
// Users should pass Link-specific props via dedicated props, not spread.
>
{children}
</Link>
);
}
return (
<div
ref={ref as React.RefObject<HTMLDivElement>}
className={clsx(allClasses, onClick && "cursor-pointer")}
onClick={onClick}
// Only spread props when rendering as div (HTMLDivElement)
{...props}
>
{children}
</div>
);
},
);
SharedCard.displayName = "SharedCard";
// =============================================================================
// Named exports for specific card types (for convenience)
// =============================================================================
// Primary Card (matching /home PrimaryCard)
export function PrimaryCard({
href,
icon: Icon,
label,
description,
accentClass = "bg-[var(--accent-primary)]/10 text-[var(--accent-primary)]",
borderClass = "border-[var(--accent-primary)]/20",
bgClass = "bg-gradient-to-br from-[var(--accent-primary)]/5 to-transparent",
className,
...props
}: {
href: string;
icon: React.ElementType;
label: string;
description: string;
accentClass?: string;
borderClass?: string;
bgClass?: string;
className?: string;
} & Omit<CardProps, "variant" | "href">) {
return (
<SharedCard
variant="primary"
href={href}
className={clsx(
"group flex flex-col",
borderClass,
bgClass,
className,
)}
{...props}
>
<div className={clsx("mb-5 grid size-12 place-items-center rounded-xl", accentClass)}>
<Icon className="size-6" aria-hidden="true" />
</div>
<h3 className="text-[18px] font-bold text-[var(--text-primary)]">{label}</h3>
<p className="mt-2 flex-1 text-[14px] leading-6 text-[var(--text-secondary)]">
{description}
</p>
<span className="mt-5 text-[13px] font-semibold text-[var(--accent-primary)] transition group-hover:underline">
Get started →
</span>
</SharedCard>
);
}
// Secondary Card (matching /home SecondaryCard)
export function SecondaryCard({
href,
icon: Icon,
label,
description,
disabled = false,
className,
onClick,
...props
}: {
href?: string;
icon: React.ElementType;
label: string;
description: string;
disabled?: boolean;
className?: string;
onClick?: () => void;
} & Omit<CardProps, "onClick" | "href">) {
const baseClasses =
"flex items-center gap-3.5 rounded-lg border border-[var(--border-subtle)] bg-[var(--bg-glass)] p-4 transition hover:border-[var(--border-medium)] hover:bg-[var(--bg-elevated)]";
const inner = (
<>
<div className="grid size-9 shrink-0 place-items-center rounded-md bg-[var(--bg-elevated)]">
<Icon className="size-4 text-[var(--text-muted)]" aria-hidden="true" />
</div>
<div className="min-w-0">
<p className="text-[13px] font-semibold text-[var(--text-primary)]">
{label}
{disabled ? (
<span className="ml-1.5 text-[10px] font-bold uppercase text-[var(--text-muted)]">
Unavailable
</span>
) : null}
</p>
<p className="truncate text-[12px] text-[var(--text-muted)]">{description}</p>
</div>
</>
);
if (disabled) {
return (
<div
className="flex cursor-default items-center gap-3.5 rounded-lg border border-[var(--border-subtle)] bg-[var(--bg-glass)] p-4 opacity-50"
onClick={onClick}
{...props}
>
{inner}
</div>
);
}
if (!href) {
return (
<div
className={clsx(baseClasses, className)}
onClick={onClick}
{...props}
>
{inner}
</div>
);
}
return (
<Link href={href} className={clsx(baseClasses, className)}>
{inner}
</Link>
);
}