@@ -80,6 +80,15 @@ function formatMetricValue(
8080 const decimalsMatch = trimmed . match ( / 0 \. ( 0 + ) / ) ;
8181 const decimals = decimalsMatch ? decimalsMatch [ 1 ] . length : 0 ;
8282
83+ // Compact / abbreviated notation (numeral.js 'a' convention, e.g. '0.0a' →
84+ // "1.1M", '$0a' → "$1M"). Keeps big KPI numbers from overflowing a tile.
85+ const isCompact = / a / i. test ( trimmed ) ;
86+ if ( isCompact ) {
87+ const opts : Intl . NumberFormatOptions = { notation : 'compact' , maximumFractionDigits : decimals || 1 } ;
88+ if ( isCurrency ) { opts . style = 'currency' ; opts . currency = currency || symbolMap [ trimmed [ 0 ] ] || 'USD' ; }
89+ try { return new Intl . NumberFormat ( 'en-US' , opts ) . format ( value as number ) ; } catch { /* fall through */ }
90+ }
91+
8392 if ( isCurrency ) {
8493 const code = currency || symbolMap [ trimmed [ 0 ] ] || 'USD' ;
8594 try {
@@ -133,6 +142,19 @@ const VARIANT_ICON_CLASSES: Record<MetricColorVariant, string> = {
133142 danger : 'bg-rose-500/10 text-rose-600 dark:text-rose-400' ,
134143} ;
135144
145+ /** Text-colour per variant — used by the `bare` layout to tint the big number
146+ * (data-screen KPIs) instead of an icon chip. */
147+ const VARIANT_TEXT_CLASSES : Record < MetricColorVariant , string > = {
148+ default : 'text-foreground' ,
149+ blue : 'text-blue-600 dark:text-blue-400' ,
150+ teal : 'text-teal-600 dark:text-teal-400' ,
151+ orange : 'text-orange-600 dark:text-orange-400' ,
152+ purple : 'text-purple-600 dark:text-purple-400' ,
153+ success : 'text-emerald-600 dark:text-emerald-400' ,
154+ warning : 'text-amber-600 dark:text-amber-400' ,
155+ danger : 'text-rose-600 dark:text-rose-400' ,
156+ } ;
157+
136158export interface MetricWidgetProps {
137159 label : string | { key ?: string ; defaultValue ?: string } ;
138160 value : string | number ;
@@ -160,6 +182,12 @@ export interface MetricWidgetProps {
160182 suffix ?: string ;
161183 /** When set, the entire card becomes clickable and emits this handler. */
162184 onClick ?: ( ) => void ;
185+ /**
186+ * Layout variant. `'card'` (default) is the bordered KPI card; `'bare'` drops
187+ * all card chrome and renders a large tinted number + label — the dense
188+ * big-number look data-screens (大屏) want, while staying data-bound.
189+ */
190+ variant ?: 'card' | 'bare' ;
163191}
164192
165193export const MetricWidget = ( {
@@ -177,6 +205,7 @@ export const MetricWidget = ({
177205 prefix,
178206 suffix,
179207 onClick,
208+ variant = 'card' ,
180209 ...props
181210} : MetricWidgetProps ) => {
182211 const iconClasses = VARIANT_ICON_CLASSES [ colorVariant ] || VARIANT_ICON_CLASSES . default ;
@@ -211,6 +240,34 @@ export const MetricWidget = ({
211240 return icon ;
212241 } , [ icon ] ) ;
213242
243+ if ( variant === 'bare' ) {
244+ const textClasses = VARIANT_TEXT_CLASSES [ colorVariant ] || VARIANT_TEXT_CLASSES . default ;
245+ return (
246+ < div
247+ className = { cn ( 'flex flex-col gap-1 min-w-0' , onClick && 'cursor-pointer' , className ) }
248+ onClick = { onClick }
249+ role = { onClick ? 'button' : undefined }
250+ tabIndex = { onClick ? 0 : undefined }
251+ onKeyDown = { onClick ? ( e ) => { if ( e . key === 'Enter' || e . key === ' ' ) { e . preventDefault ( ) ; onClick ( ) ; } } : undefined }
252+ >
253+ { loading ? (
254+ < div className = "flex items-center gap-2 text-muted-foreground" data-testid = "metric-loading" >
255+ < Loader2 className = "h-4 w-4 animate-spin" /> < span className = "text-sm" > Loading…</ span >
256+ </ div >
257+ ) : error ? (
258+ < div className = "flex items-center gap-2" data-testid = "metric-error" role = "alert" >
259+ < AlertCircle className = "h-4 w-4 text-destructive shrink-0" /> < span className = "text-xs text-destructive truncate" > { error } </ span >
260+ </ div >
261+ ) : (
262+ < >
263+ < div className = { cn ( 'text-[2rem] leading-none font-extrabold tabular-nums tracking-tight truncate' , textClasses ) } > { displayValue } </ div >
264+ < div className = "text-xs font-medium text-muted-foreground truncate" > { resolveLabel ( label ) } </ div >
265+ </ >
266+ ) }
267+ </ div >
268+ ) ;
269+ }
270+
214271 return (
215272 < Card
216273 className = { cn (
0 commit comments