{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "metric-card-glass", "type": "registry:block", "title": "Metric Card Glass", "description": "Rich metric display card with progress bars, sparklines, and trend indicators.\n * Follows shadcn/ui variant system with Glass UI extensions for data visualization.\n *\n * ## Features", "dependencies": [ "lucide-react", "react", "shadcn-glass-ui" ], "registryDependencies": [ "cn", "variants" ], "files": [ { "path": "components/glass/composite/metric-card-glass.tsx", "type": "registry:component", "content": "/**\n * MetricCardGlass Component\n *\n * Rich metric display card with progress bars, sparklines, and trend indicators.\n * Follows shadcn/ui variant system with Glass UI extensions for data visualization.\n *\n * ## Features\n * - 5 semantic variants (default, secondary, success, warning, destructive)\n * - Trend indicators with directional arrows (up/down/neutral)\n * - Optional progress bar with gradient colors matched to variant\n * - Optional sparkline chart for trend visualization\n * - Flexible change display (string, number, or detailed MetricChange object)\n * - Score ratio display (e.g., \"85/100\") via maxScore prop\n * - Optional explain button with HelpCircle icon\n * - Hover lift effect via InteractiveCard primitive\n * - Responsive sizing (sm, md, lg text scales)\n * - Theme-aware with per-variant CSS variables\n *\n * ## CSS Variables\n * Per-variant variables (5 variants × 4 properties = 20 variables):\n * - `--metric-default-bg`, `--metric-default-text`, `--metric-default-border`, `--metric-default-glow`\n * - `--metric-secondary-bg`, `--metric-secondary-text`, `--metric-secondary-border`, `--metric-secondary-glow`\n * - `--metric-success-bg`, `--metric-success-text`, `--metric-success-border`, `--metric-success-glow`\n * - `--metric-warning-bg`, `--metric-warning-text`, `--metric-warning-border`, `--metric-warning-glow`\n * - `--metric-destructive-bg`, `--metric-destructive-text`, `--metric-destructive-border`, `--metric-destructive-glow`\n *\n * @example Basic metric card (shadcn/ui style)\n * ```tsx\n * import { MetricCardGlass } from 'shadcn-glass-ui'\n * import { DollarSign } from 'lucide-react'\n *\n * function Dashboard() {\n * return (\n * }\n * />\n * )\n * }\n * ```\n *\n * @example With progress bar and sparkline\n * ```tsx\n * \n * ```\n *\n * @example Score ratio display\n * ```tsx\n * \n * // Displays: \"92/100\"\n * ```\n *\n * @example With explain button\n * ```tsx\n * setShowExplanationModal(true)}\n * variant=\"default\"\n * />\n * // Shows HelpCircle icon button next to value\n * ```\n *\n * @accessibility\n * - Trend icons include `aria-hidden=\"true\"` (direction conveyed via text and color)\n * - Explain button has descriptive `aria-label`: \"Explain {title} metric\"\n * - Sparkline includes `aria-label` with metric title context\n * - High contrast text colors meet WCAG 2.1 AA (4.5:1 ratio)\n * - Trend colors use semantic alert CSS variables for consistency\n *\n * @since v1.0.0\n */\n\nimport { forwardRef, type CSSProperties, type ReactNode } from 'react';\nimport { TrendingUp, TrendingDown, Minus, HelpCircle } from 'lucide-react';\nimport { cn } from '@/lib/utils';\nimport { ProgressGlass } from '../specialized/progress-glass';\nimport { SparklineGlass } from '../specialized/sparkline-glass';\nimport { InteractiveCard } from '../primitives';\nimport '@/glass-theme.css';\n\nimport type { ProgressGradient } from '@/lib/variants/progress-glass-variants';\n\n// ========================================\n// TYPES\n// ========================================\n\n/**\n * Metric variant system (following AlertGlass, BadgeGlass pattern)\n * - default: Blue (primary metric)\n * - secondary: Gray (neutral metric)\n * - success: Green (positive metric)\n * - warning: Yellow (caution metric)\n * - destructive: Red (negative metric)\n */\nexport type MetricVariant =\n | 'default' // shadcn/ui base (blue)\n | 'secondary' // shadcn/ui base (gray)\n | 'success' // Glass UI extension (green)\n | 'warning' // Glass UI extension (yellow)\n | 'destructive'; // shadcn/ui base (red)\n\n/** @deprecated Use MetricVariant instead */\nexport type MetricColor = 'emerald' | 'amber' | 'blue' | 'red';\n\nexport type TrendDirection = 'up' | 'down' | 'neutral';\n\n/**\n * Detailed change object with trend information\n */\nexport interface MetricChange {\n /** Change value (e.g., 12.5 for +12.5%) */\n readonly value: number;\n /** Trend direction (auto-detected from value if not provided) */\n readonly direction?: TrendDirection;\n /** Optional period label (e.g., \"vs last month\") */\n readonly period?: string;\n}\n\n/** @deprecated Use MetricChange instead */\nexport interface MetricTrend {\n readonly value: number;\n readonly direction: TrendDirection;\n readonly label?: string;\n}\n\n/**\n * Props for MetricCardGlass component.\n *\n * Extends standard div attributes with metric-specific props.\n * Follows shadcn/ui Card pattern with Glass UI extensions for data visualization.\n *\n * @example\n * ```tsx\n * const props: MetricCardGlassProps = {\n * title: \"Active Users\",\n * value: \"12,345\",\n * change: { value: 8.2, direction: 'up', period: 'vs last week' },\n * variant: \"success\",\n * progress: 75,\n * showProgress: true,\n * };\n * ```\n */\nexport interface MetricCardGlassProps extends React.HTMLAttributes {\n // ========================================\n // CORE PROPS (shadcn/ui compatible)\n // ========================================\n\n /**\n * Metric title displayed above the value.\n *\n * @example\n * ```tsx\n * \n * ```\n */\n readonly title: string;\n\n /**\n * Primary display value for the metric.\n *\n * Can be a string (e.g., \"$45,231\") or number (e.g., 85).\n * If `maxScore` is provided, formats as ratio (e.g., \"85/100\").\n *\n * @example\n * ```tsx\n * \n * \n * // Second example displays: \"85/100\"\n * ```\n */\n readonly value: string | number;\n\n /**\n * Optional description or subtitle displayed below the title.\n *\n * @example\n * ```tsx\n * \n * ```\n */\n readonly description?: string;\n\n /**\n * Change indicator showing metric trend.\n *\n * Accepts three formats:\n * - String: \"+12.5%\", \"-5.3\", etc.\n * - Number: 12.5, -5.3, etc.\n * - MetricChange object: `{ value: 12.5, direction: 'up', period: 'vs last month' }`\n *\n * Direction is auto-detected from value if not explicitly set.\n *\n * @example\n * ```tsx\n * \n * \n * \n * ```\n */\n readonly change?: string | number | MetricChange;\n\n /**\n * Semantic variant defining color scheme.\n *\n * Follows shadcn/ui + Glass UI variant system:\n * - `default`: Blue (primary metrics)\n * - `secondary`: Gray (neutral metrics)\n * - `success`: Green (positive metrics)\n * - `warning`: Yellow (caution metrics)\n * - `destructive`: Red (negative metrics)\n *\n * @default \"default\"\n *\n * @example\n * ```tsx\n * \n * \n * ```\n */\n readonly variant?: MetricVariant;\n\n /**\n * Icon displayed next to the title.\n *\n * @example\n * ```tsx\n * import { DollarSign } from 'lucide-react'\n *\n * }\n * />\n * ```\n */\n readonly icon?: ReactNode;\n\n // ========================================\n // GLASS UI EXTENSIONS\n // ========================================\n\n /**\n * Data points for sparkline visualization.\n *\n * Array of numeric values rendered as a mini bar chart.\n * Automatically highlights the maximum value.\n *\n * @example\n * ```tsx\n * \n * ```\n */\n readonly sparklineData?: readonly number[];\n\n /**\n * Whether to display the sparkline chart.\n *\n * Requires `sparklineData` prop to be provided.\n *\n * @default true\n *\n * @example\n * ```tsx\n * \n * ```\n */\n readonly showSparkline?: boolean;\n\n /**\n * Whether to display the progress bar.\n *\n * Requires `progress` prop to be provided.\n * Progress bar uses gradient colors matched to the variant.\n *\n * @default true\n *\n * @example\n * ```tsx\n * \n * ```\n */\n readonly showProgress?: boolean;\n\n /**\n * Progress percentage for the progress bar.\n *\n * Value between 0 and 100. If not provided, the component attempts\n * to infer from `value` prop if it's a number in range 0-100.\n *\n * @example\n * ```tsx\n * \n * ```\n */\n readonly progress?: number;\n\n // ========================================\n // SCORE DISPLAY (Issue #15)\n // ========================================\n\n /**\n * Max score for ratio display\n *\n * When provided, formats the value as a ratio (e.g., \"85/100\").\n * Useful for displaying scores, completion rates, or progress out of a maximum value.\n *\n * @example\n * ```tsx\n * \n * // Displays: \"85/100\"\n * ```\n */\n readonly maxScore?: number;\n\n /**\n * Callback when explain button is clicked\n *\n * Enables an interactive \"explain\" button (HelpCircle icon) next to the metric value.\n * Use to show tooltips, modals, or contextual help about the metric's meaning.\n * The button includes an accessible aria-label: \"Explain {title} metric\"\n *\n * @example\n * ```tsx\n * setShowExplanationModal(true)}\n * />\n * ```\n */\n readonly onExplain?: () => void;\n\n /**\n * Control explain button visibility\n *\n * Defaults to `true` if `onExplain` is provided. Set to `false` to hide the button\n * even when `onExplain` callback exists.\n *\n * @default true (when onExplain is provided)\n *\n * @example\n * ```tsx\n * // Conditionally show explain button\n * \n * ```\n */\n readonly showExplain?: boolean;\n\n // ========================================\n // DEPRECATED (backward compatibility)\n // ========================================\n\n /** @deprecated Use `title` instead. Will be removed in v2.0 */\n readonly label?: string;\n\n /** @deprecated Use `variant` instead. Mapping: emerald→success, amber→warning, blue→default, red→destructive. Will be removed in v2.0 */\n readonly color?: MetricColor;\n\n /** @deprecated Format value before passing. Use `value` prop directly. Will be removed in v2.0 */\n readonly valueFormatter?: (value: number) => string;\n\n /** @deprecated Use `description` instead. Will be removed in v2.0 */\n readonly valueSuffix?: string;\n\n /** @deprecated Use `change` instead. Will be removed in v2.0 */\n readonly trend?: MetricTrend;\n}\n\n// ========================================\n// VARIANT SYSTEM (following AlertGlass, BadgeGlass pattern)\n// ========================================\n\ntype VariantStyle = { bg: string; text: string; border: string; glow: string };\n\n// New variant-based system (shadcn/ui compatible)\nconst variantStyles: Record = {\n default: {\n bg: 'var(--metric-default-bg)',\n text: 'var(--metric-default-text)',\n border: 'var(--metric-default-border)',\n glow: 'var(--metric-default-glow)',\n },\n secondary: {\n bg: 'var(--metric-secondary-bg)',\n text: 'var(--metric-secondary-text)',\n border: 'var(--metric-secondary-border)',\n glow: 'var(--metric-secondary-glow)',\n },\n success: {\n bg: 'var(--metric-success-bg)',\n text: 'var(--metric-success-text)',\n border: 'var(--metric-success-border)',\n glow: 'var(--metric-success-glow)',\n },\n warning: {\n bg: 'var(--metric-warning-bg)',\n text: 'var(--metric-warning-text)',\n border: 'var(--metric-warning-border)',\n glow: 'var(--metric-warning-glow)',\n },\n destructive: {\n bg: 'var(--metric-destructive-bg)',\n text: 'var(--metric-destructive-text)',\n border: 'var(--metric-destructive-border)',\n glow: 'var(--metric-destructive-glow)',\n },\n};\n\n// Map MetricVariant to ProgressGradient\nconst variantToGradient: Record = {\n default: 'blue',\n secondary: 'cyan',\n success: 'emerald',\n warning: 'amber',\n destructive: 'rose',\n};\n\n// ========================================\n// DEPRECATED: Old color system (backward compatibility)\n// ========================================\n\n/** @deprecated Use variantStyles instead */\nconst colorToVariant: Record = {\n emerald: 'success',\n amber: 'warning',\n blue: 'default',\n red: 'destructive',\n};\n\n// Trend direction colors - using existing alert CSS variables\nconst trendColors: Record = {\n up: 'text-[var(--alert-success-text)]',\n down: 'text-[var(--alert-destructive-text)]',\n neutral: 'text-[var(--text-muted)]',\n};\n\n// Trend icons\nconst TrendIcons: Record = {\n up: TrendingUp,\n down: TrendingDown,\n neutral: Minus,\n};\n\n// ========================================\n// COMPONENT\n// ========================================\n\nexport const MetricCardGlass = forwardRef(\n (\n {\n // New API\n title,\n value,\n description,\n change,\n variant,\n progress,\n // Deprecated API (backward compatibility)\n label,\n color,\n valueFormatter,\n valueSuffix,\n trend,\n // Score display (Issue #15)\n maxScore,\n onExplain,\n showExplain,\n // Common props\n icon,\n sparklineData,\n showSparkline = true,\n showProgress = true,\n className,\n ...props\n },\n ref\n ) => {\n // ========================================\n // BACKWARD COMPATIBILITY LAYER\n // ========================================\n\n // Support old `label` prop\n const actualTitle = title || label;\n if (!actualTitle) {\n console.warn('[MetricCardGlass] Missing required prop: `title` (or deprecated `label`)');\n }\n if (label && !title) {\n console.warn(\n '[MetricCardGlass] Deprecated prop `label` used. Please use `title` instead. Will be removed in v2.0'\n );\n }\n\n // Support old `color` prop → `variant`\n const actualVariant: MetricVariant = variant || (color ? colorToVariant[color] : 'default');\n if (color && !variant) {\n console.warn(\n `[MetricCardGlass] Deprecated prop \\`color=\"${color}\"\\` used. Please use \\`variant=\"${colorToVariant[color]}\"\\` instead. Will be removed in v2.0`\n );\n }\n\n // Support old `valueSuffix` → `description`\n const actualDescription = description || valueSuffix;\n if (valueSuffix && !description) {\n console.warn(\n '[MetricCardGlass] Deprecated prop `valueSuffix` used. Please use `description` instead. Will be removed in v2.0'\n );\n }\n\n // Support old `trend` → `change`\n const actualChange =\n change ||\n (trend\n ? {\n value: trend.value,\n direction: trend.direction,\n period: trend.label,\n }\n : undefined);\n if (trend && !change) {\n console.warn(\n '[MetricCardGlass] Deprecated prop `trend` used. Please use `change` instead. Will be removed in v2.0'\n );\n }\n\n // Support old `valueFormatter`\n let displayValue =\n typeof value === 'number' && valueFormatter ? valueFormatter(value) : String(value);\n if (valueFormatter) {\n console.warn(\n '[MetricCardGlass] Deprecated prop `valueFormatter` used. Please format value before passing. Will be removed in v2.0'\n );\n }\n\n // Format as ratio if maxScore is provided (e.g., \"85/100\")\n if (maxScore !== undefined) {\n displayValue = `${displayValue}/${maxScore}`;\n }\n\n // Determine if explain button should be shown\n const shouldShowExplain = showExplain ?? onExplain !== undefined;\n\n // Get actual progress value (use prop or infer from value if it's 0-100)\n const actualProgress =\n progress ?? (typeof value === 'number' && value >= 0 && value <= 100 ? value : undefined);\n\n // ========================================\n // COMPONENT LOGIC\n // ========================================\n\n const variantVars = variantStyles[actualVariant];\n const hasSparkline = showSparkline && sparklineData && sparklineData.length > 0;\n\n const valueStyles: CSSProperties = {\n color: variantVars.text,\n textShadow: variantVars.glow,\n };\n\n // Parse and render change indicator\n const renderChange = () => {\n if (!actualChange) return null;\n\n // Handle simple string or number\n if (typeof actualChange === 'string' || typeof actualChange === 'number') {\n const changeStr = String(actualChange);\n const isPositive =\n changeStr.startsWith('+') || (!changeStr.startsWith('-') && parseFloat(changeStr) > 0);\n const isNegative = changeStr.startsWith('-') || parseFloat(changeStr) < 0;\n const direction: TrendDirection = isPositive ? 'up' : isNegative ? 'down' : 'neutral';\n const TrendIcon = TrendIcons[direction];\n\n return (\n
\n \n {changeStr}\n
\n );\n }\n\n // Handle detailed MetricChange object\n const changeValue = actualChange.value;\n const direction =\n actualChange.direction || (changeValue > 0 ? 'up' : changeValue < 0 ? 'down' : 'neutral');\n const TrendIcon = TrendIcons[direction];\n const displayChange = direction === 'down' ? `-${Math.abs(changeValue)}` : `+${changeValue}`;\n\n return (\n
\n \n {displayChange}%\n {actualChange.period && (\n {actualChange.period}\n )}\n
\n );\n };\n\n return (\n \n {/* Header with icon and change indicator */}\n
\n
\n {icon && (\n
\n {icon}\n
\n )}\n \n {actualTitle}\n \n
\n {renderChange()}\n
\n\n {/* Value display */}\n
\n
\n \n {displayValue}\n \n {shouldShowExplain && onExplain && (\n \n \n \n )}\n
\n {actualDescription && (\n \n {actualDescription}\n \n )}\n
\n\n {/* Progress and Sparkline */}\n {hasSparkline ? (\n
\n {showProgress && actualProgress !== undefined && (\n \n )}\n \n
\n ) : showProgress && actualProgress !== undefined ? (\n \n ) : null}\n \n );\n }\n);\n\nMetricCardGlass.displayName = 'MetricCardGlass';\n" } ], "categories": [ "composite" ] }