Coverage for src / lexigram / ui / molecules / metric_card.py: 26%

31 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-08-10 04:11 +0800

1""" 

2MetricCard component for displaying key metrics. 

3 

4Shows a number with label, optional trend indicator, and icon. 

5Perfect for dashboard statistics. 

6""" 

7 

8from __future__ import annotations 

9 

10from typing import Any, Literal 

11 

12from lexigram.ui.core.base import Component, el 

13 

14MetricCardVariant = Literal["default", "success", "warning", "danger", "info"] 

15TrendDirection = Literal["up", "down"] 

16 

17 

18class MetricCard(Component): 

19 """ 

20 MetricProtocol card for dashboard statistics. 

21 

22 Example: 

23 MetricCard( 

24 value="1,234", 

25 label="Total Users", 

26 trend="+12%", 

27 trend_direction="up", 

28 icon="👥", 

29 color="success" 

30 ) 

31 """ 

32 

33 def __init__( 

34 self, 

35 value: str | float, 

36 label: str, 

37 trend: str | None = None, 

38 trend_direction: TrendDirection | None = None, 

39 icon: str | None = None, 

40 variant: MetricCardVariant = "default", 

41 **props, 

42 ): 

43 """ 

44 Initialize metric card. 

45 

46 Args: 

47 value: The metric value (number or formatted string) 

48 label: Description label 

49 trend: Trend indicator (e.g., "+12%", "-5%") 

50 trend_direction: Direction of trend ("up" or "down") 

51 icon: Optional icon (emoji or icon class) 

52 variant: Color variant 

53 **props: Additional properties 

54 """ 

55 super().__init__( 

56 value=value, 

57 label=label, 

58 trend=trend, 

59 trend_direction=trend_direction, 

60 icon=icon, 

61 color=variant, 

62 **props, 

63 ) 

64 self.value = value 

65 self.label = label 

66 self.trend = trend 

67 self.trend_direction = trend_direction 

68 self.icon = icon 

69 self.variant = variant 

70 

71 def render(self) -> Any: 

72 """Render the metric card.""" 

73 # Variant color classes 

74 variant_classes = { 

75 "default": "border-border", 

76 "success": "border-success/30 bg-success/10", 

77 "warning": "border-warning/30 bg-warning/10", 

78 "danger": "border-destructive/30 bg-destructive/10", 

79 "info": "border-info/30 bg-info/10", 

80 } 

81 

82 variant_text_classes = { 

83 "default": "text-foreground", 

84 "success": "text-success", 

85 "warning": "text-warning", 

86 "danger": "text-destructive", 

87 "info": "text-info", 

88 } 

89 

90 card_classes = f"bg-card rounded-lg border-2 {variant_classes.get(self.variant, variant_classes['default'])} p-6 shadow-sm hover:shadow-md transition-shadow" 

91 value_classes = f"text-3xl font-bold {variant_text_classes.get(self.variant, variant_text_classes['default'])}" 

92 

93 # Icon element 

94 icon_el = "" 

95 if self.icon: 

96 icon_el = el("div", self.icon, class_="text-3xl mb-2 opacity-50") 

97 

98 # Trend element 

99 trend_el = "" 

100 if self.trend: 

101 trend_color = "text-success" 

102 trend_arrow = "↑" 

103 

104 if self.trend_direction == "down": 

105 trend_color = "text-destructive" 

106 trend_arrow = "↓" 

107 

108 trend_el = el( 

109 "div", 

110 el("span", trend_arrow, class_="font-bold"), 

111 " ", 

112 el("span", self.trend), 

113 class_=f"text-sm font-medium {trend_color} mt-1", 

114 ) 

115 

116 return el( 

117 "div", 

118 icon_el, 

119 el("div", str(self.value), class_=value_classes), 

120 el( 

121 "div", 

122 self.label, 

123 class_="text-sm font-medium text-muted-foreground mt-1", 

124 ), 

125 trend_el, 

126 class_=card_classes, 

127 )