import type * as React from 'react'
import Link from '../../embed-shims/next-link'
import { cn } from '../../utils/cn'
import { QuestionCircleIcon } from '../icons-v2-generated/signs-and-symbols/question-circle-icon'
import { CircularProgress, type CircularProgressOverflow, type CircularProgressVariant } from './circular-progress'
import { FloatingTooltip } from './floating-tooltip'
import { Tag } from './tag'
/**
* How the percentage is rendered next to the value:
* - `'auto'` (default): a colored `Tag` for `warning`/`error` variants, plain
* "(NN%)" text otherwise. Preserves the original coupling to `progressVariant`.
* - `'plain'`: always plain "(NN%)" text, regardless of `progressVariant`. Use
* when you want a colored progress ring but a neutral percentage (Figma).
* - `'tag'`: always a colored `Tag`.
*/
export type DashboardInfoCardPercentageDisplay = 'auto' | 'plain' | 'tag'
/**
* Size of the progress ring in px. A single number sets the ring size from the
* Tailwind `md` (800px) breakpoint up — on mobile the ring always shrinks to
* the 24px Figma spec. The responsive form sets `base` (mobile) explicitly,
* plus optional `md` / `lg` (1280px) overrides — e.g. `{ base: 24, lg: 56 }`
* keeps the small ring through tablet, and `{ base: 40 }` pins 40px at every
* breakpoint.
*/
export type DashboardInfoCardProgressSize =
| number
| { base: number; md?: number; lg?: number }
export interface DashboardInfoCardProps {
title?: string
/** Node rendered in place of the title text. Takes precedence over `title`. */
titleSlot?: React.ReactNode
/**
* Tag rendered in the title row, after the title (Figma "status" variant) —
* e.g. `` both work.
*/
icon?: React.ReactNode
value: string | number
percentage?: number
showProgress?: boolean
progressVariant?: CircularProgressVariant
/**
* Controls how `percentage` is rendered (Tag vs plain text) independently of
* the progress-ring color. Default `'auto'`. See {@link DashboardInfoCardPercentageDisplay}.
*/
percentageDisplay?: DashboardInfoCardPercentageDisplay
/**
* How the progress ring treats values over 100. Forwarded to `CircularProgress`.
* Default: `'clamp'` — existing behavior (clamped to 0–100).
* Use `'wrap'` for overage/overflow semantics (excess rendered as a red arc).
*/
progressOverflow?: CircularProgressOverflow
/**
* Size of the circular progress ring. Defaults to the Figma spec
* `{ base: 24, md: 56 }` — 24px on mobile, 56px on tablet and desktop. A
* number changes only the tablet/desktop size (mobile stays 24px); use
* `{ base, md?, lg? }` for full control. Stroke width scales proportionally
* with the size.
*/
progressSize?: DashboardInfoCardProgressSize
className?: string
/**
* Navigation URL — renders the card as a Next.js Link
* When provided, cursor becomes pointer and hover accent styles are applied
*/
href?: string
/** Tooltip content shown on a question-mark icon next to the value */
tooltip?: React.ReactNode
/** Override the value text className (default: `text-h3 md:text-h2` per Figma) */
valueClassName?: string
/** Secondary text rendered beside the value (e.g. an entry/item count). */
subValue?: React.ReactNode
}
export function DashboardInfoCard({
title,
titleSlot,
titleTag,
icon,
value,
percentage,
showProgress = false,
progressVariant,
percentageDisplay = 'auto',
progressOverflow,
progressSize,
className,
href,
tooltip,
valueClassName,
subValue
}: DashboardInfoCardProps) {
const formattedValue = typeof value === 'number'
? value.toLocaleString()
: value
const renderPercentage = () => {
if (percentage === undefined) return null
const asTag =
percentageDisplay === 'tag' ||
(percentageDisplay === 'auto' && (progressVariant === 'warning' || progressVariant === 'error'))
if (asTag) {
const tagVariant =
progressVariant === 'warning' || progressVariant === 'error'
? progressVariant
: progressVariant === 'success'
? 'success'
: 'grey'
return
({percentage}%)
) } const renderProgress = () => { if (!showProgress || percentage === undefined) return null // Keep the ring proportionally weighted (10px stroke at the 56px default). const strokeFor = (size: number) => Math.max(2, Math.round((size * 10) / 56)) const common = { percentage, variant: progressVariant, overflow: progressOverflow, showLabel: false, } // Figma spec: the mobile ring is always 24px. The default is 56px from // `md` (800px) up; a numeric `progressSize` overrides only that md+ size. const size = typeof progressSize === 'number' ? { base: 24, md: progressSize } : progressSize ?? { base: 24, md: 56 } // Responsive: one ring per provided breakpoint, toggled with Tailwind // display utilities so only one shows at a time. The class names must stay // static string literals — Tailwind's JIT scanner can't see // dynamically-built names (e.g. `${bp}:hidden`). const { base, md, lg } = size const rings: Array<{ key: string; size: number; className: string }> = [ { key: 'base', size: base, className: md !== undefined ? 'md:hidden' : lg !== undefined ? 'lg:hidden' : '' }, ] if (md !== undefined) { rings.push({ key: 'md', size: md, className: lg !== undefined ? 'hidden md:block lg:hidden' : 'hidden md:block' }) } if (lg !== undefined) { rings.push({ key: 'lg', size: lg, className: 'hidden lg:block' }) } return ( <> {rings.map(ring => ({title}
))} {titleTag}{formattedValue}
{subValue && ({subValue}
)} {renderPercentage()} {tooltip && (