import * as React from 'react' import * as LucideIcons from 'lucide-react' import type { SAILLabelPosition, SAILMarginSize, SAILAlign, SAILShape, SAILColorInput } from '../../types/sail' import { mergeClasses } from '../../utils/classNames' import { resolveColorClass, isSemanticColor, isPaletteColor } from '../../utils/colorResolver' import { marginAboveMap, marginBelowMap, alignMap } from '../../utils/sailMaps' type StampSize = "TINY" | "SMALL" | "MEDIUM" | "LARGE" type StampBackgroundColor = SAILColorInput | "TRANSPARENT" type StampContentColor = SAILColorInput export interface StampFieldProps { /** Text to display as the field label */ label?: string /** Determines where the label appears */ labelPosition?: SAILLabelPosition /** Supplemental text about this field */ instructions?: string /** Displays a help icon with the specified text as a tooltip */ helpTooltip?: string /** Icon to display inside the stamp */ icon?: string /** Text to display within the stamp */ text?: string /** Determines the background color */ backgroundColor?: StampBackgroundColor /** Determines the icon color */ contentColor?: StampContentColor /** Determines the size of the stamp */ size?: StampSize /** Determines alignment of the stamp */ align?: SAILAlign /** Text to display on mouseover (web) or tap (mobile) */ tooltip?: string /** Determines whether the component is displayed on the interface */ showWhen?: boolean /** Additional text to be announced by screen readers */ accessibilityText?: string /** Link to apply to the stamp */ link?: any /** Determines how much space is added above the layout */ marginAbove?: SAILMarginSize /** Determines how much space is added below the layout */ marginBelow?: SAILMarginSize /** Determines the stamp shape */ shape?: SAILShape /** Additional Tailwind classes for prototype-specific styling (not part of SAIL API) */ className?: string } /** * Displays an icon and/or text on a colored circular background. * Best used as a decorative component to add visual interest to your page. */ export const StampField: React.FC = ({ label, labelPosition = "ABOVE", instructions, helpTooltip, icon, text, backgroundColor = "ACCENT", contentColor = "STANDARD", size = "MEDIUM", align = "CENTER", tooltip, showWhen = true, accessibilityText, link, marginAbove = "NONE", marginBelow = "NONE", shape = "ROUNDED", className: classNameProp }) => { // Visibility control if (!showWhen) return null const sizeMap: Record = { TINY: { container: 'w-6 h-6', text: 'text-xs', icon: 'text-xs' }, SMALL: { container: 'w-8 h-8', text: 'text-sm', icon: 'text-sm' }, MEDIUM: { container: 'w-12 h-12', text: 'text-base', icon: 'text-base' }, LARGE: { container: 'w-16 h-16', text: 'text-lg', icon: 'text-lg' } } const shapeMap: Record = { SQUARED: 'rounded-none', SEMI_ROUNDED: 'rounded-sm', ROUNDED: 'rounded-full' } // Background color mapping const getBackgroundColor = (): { className?: string; style?: React.CSSProperties } => { if (backgroundColor === "TRANSPARENT") { return { className: 'bg-transparent' } } // Semantic colors — curated mappings if (isSemanticColor(backgroundColor)) { return { className: resolveColorClass(backgroundColor, 'bg') } } // Palette colors — mechanical mapping if (isPaletteColor(backgroundColor)) { return { className: resolveColorClass(backgroundColor, 'bg') } } // Hex color - use inline style if (backgroundColor.startsWith('#')) { return { style: { backgroundColor } } } // Default fallback return { className: 'bg-blue-500' } } // Content color mapping const getContentColor = (): { className?: string; style?: React.CSSProperties } => { // Semantic colors if (isSemanticColor(contentColor)) { if (contentColor === 'STANDARD') { return { className: backgroundColor === "TRANSPARENT" ? 'text-gray-900' : 'text-white' } } return { className: resolveColorClass(contentColor, 'text') } } // Palette colors if (isPaletteColor(contentColor)) { return { className: resolveColorClass(contentColor, 'text') } } // Hex color - use inline style if (contentColor.startsWith('#')) { return { style: { color: contentColor } } } // Default fallback return { className: backgroundColor === "TRANSPARENT" ? 'text-gray-900' : 'text-white' } } // Border for transparent background const getBorderColor = (): string => { if (backgroundColor !== "TRANSPARENT") return '' if (isSemanticColor(contentColor)) { return resolveColorClass(contentColor, 'border') } if (isPaletteColor(contentColor)) { return resolveColorClass(contentColor, 'border') } return 'border-gray-900' } const backgroundStyles = getBackgroundColor() const contentStyles = getContentColor() const borderClass = backgroundColor === "TRANSPARENT" ? `border-2 ${getBorderColor()}` : '' const stampClasses = [ 'flex', 'items-center', 'justify-center', 'font-medium', sizeMap[size].container, shapeMap[shape], backgroundStyles.className, contentStyles.className, borderClass, alignMap[align] === 'justify-start' ? 'self-start' : alignMap[align] === 'justify-end' ? 'self-end' : 'self-center', link ? 'cursor-pointer hover:opacity-80 transition-opacity' : '' ].filter(Boolean).join(' ') const sailContainerClasses = [ marginAboveMap[marginAbove], marginBelowMap[marginBelow] ].filter(Boolean).join(' ') const containerClasses = mergeClasses(sailContainerClasses, classNameProp) const stampStyle = { ...backgroundStyles.style, ...contentStyles.style } // Map any Lucide icon name directly, with SAIL compatibility fallbacks const getIconComponent = (iconName: string) => { // First try direct Lucide icon name (kebab-case or PascalCase) const kebabToPascal = (str: string) => str.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join('') const pascalIconName = kebabToPascal(iconName) if (pascalIconName in LucideIcons) { return LucideIcons[pascalIconName as keyof typeof LucideIcons] as React.ComponentType<{ size?: number; className?: string; style?: React.CSSProperties }> } // Also try direct case-insensitive lookup const directIconName = iconName.charAt(0).toUpperCase() + iconName.slice(1).toLowerCase() if (directIconName in LucideIcons) { return LucideIcons[directIconName as keyof typeof LucideIcons] as React.ComponentType<{ size?: number; className?: string; style?: React.CSSProperties }> } // Fallback to SAIL compatibility mapping const sailIconMap: Record = { 'STAR': 'Star', 'HOME': 'Home', 'USER': 'User', 'PHONE': 'Phone', 'briefcase': 'Briefcase', 'tasks': 'ListTodo', 'paper-plane': 'Send', 'calendar': 'Calendar', 'clock-o': 'Clock', 'money': 'DollarSign' } const lucideIconName = sailIconMap[iconName] if (lucideIconName && lucideIconName in LucideIcons) { return LucideIcons[lucideIconName] as React.ComponentType<{ size?: number; className?: string; style?: React.CSSProperties }> } // Fallback to a generic icon return LucideIcons.Circle as React.ComponentType<{ size?: number; className?: string; style?: React.CSSProperties }> } const renderStampContent = () => { if (icon && text) { const IconComponent = getIconComponent(icon) return (
{text}
) } if (icon) { const IconComponent = getIconComponent(icon) return ( ) } if (text) { return ( {text} ) } return null } const stampElement = (
{renderStampContent()}
) const wrappedStamp = link ? (
link?.onClick?.()} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault() link?.onClick?.() } }}> {stampElement}
) : stampElement const renderLabel = () => { if (!label || labelPosition === "COLLAPSED") { return accessibilityText ? ( {label || accessibilityText} ) : null } return ( ) } return (
{renderLabel()} {wrappedStamp} {instructions && (

{instructions}

)}
) }