@umituz/web-design-system 2.7.2 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Responsive Utility Functions
3
+ * @description Centralized functions for building responsive Tailwind classes
4
+ * All responsive class construction logic lives here - components should only call these functions
5
+ */
6
+
7
+ import { cn } from './cn.util';
8
+ import type { SizeVariant, Breakpoint } from '../../domain/types';
9
+ import {
10
+ RESPONSIVE_SPACING,
11
+ RESPONSIVE_ICON_SIZE,
12
+ RESPONSIVE_CONTAINER_SIZE,
13
+ RESPONSIVE_TEXT_SIZE,
14
+ RESPONSIVE_GAP,
15
+ } from '../../domain/tokens/responsive-map.tokens';
16
+
17
+ /**
18
+ * Build responsive padding/margin/gap classes
19
+ * @param property - Tailwind spacing property ('p', 'm', 'px', 'py', 'gap', 'space-y', 'space-x')
20
+ * @param size - Size variant (sm, md, lg, xl)
21
+ * @returns Complete responsive class string
22
+ *
23
+ * @example
24
+ * getSpacing('p', 'md') // "p-4 sm:p-5 md:p-5 lg:p-6 xl:p-8 2xl:p-10"
25
+ */
26
+ export function getSpacing(
27
+ property: 'p' | 'm' | 'px' | 'py' | 'pt' | 'pb' | 'pl' | 'pr' | 'gap' | 'space-y' | 'space-x',
28
+ size: SizeVariant
29
+ ): string {
30
+ const token = RESPONSIVE_SPACING[size];
31
+
32
+ // Start with base (xs/mobile) value
33
+ const classes: string[] = [`${property}-${token.xs}`];
34
+
35
+ // Add breakpoint-specific classes
36
+ const breakpoints: Breakpoint[] = ['sm', 'md', 'lg', 'xl', '2xl'];
37
+ breakpoints.forEach((bp) => {
38
+ if (token[bp] !== token.xs) {
39
+ classes.push(`${bp}:${property}-${token[bp]}`);
40
+ }
41
+ });
42
+
43
+ return classes.join(' ');
44
+ }
45
+
46
+ /**
47
+ * Build responsive icon size classes (height + width)
48
+ * @param size - Size variant
49
+ * @returns Combined height and width classes
50
+ *
51
+ * @example
52
+ * getIconSize('md') // "h-5 w-5 sm:h-6 sm:w-6 md:h-7 md:w-7 lg:h-8 lg:w-8 xl:h-9 xl:w-9 2xl:h-10 2xl:w-10"
53
+ */
54
+ export function getIconSize(size: SizeVariant): string {
55
+ const token = RESPONSIVE_ICON_SIZE[size];
56
+ const classes: string[] = [`h-${token.xs}`, `w-${token.xs}`];
57
+
58
+ const breakpoints: Breakpoint[] = ['sm', 'md', 'lg', 'xl', '2xl'];
59
+ breakpoints.forEach((bp) => {
60
+ if (token[bp] !== token.xs) {
61
+ classes.push(`${bp}:h-${token[bp]}`, `${bp}:w-${token[bp]}`);
62
+ }
63
+ });
64
+
65
+ return classes.join(' ');
66
+ }
67
+
68
+ /**
69
+ * Build responsive container size classes (for icon wrappers)
70
+ * @param size - Size variant
71
+ * @returns Combined height, width, and flex center classes
72
+ *
73
+ * @example
74
+ * getContainerSize('md') // "w-10 h-10 sm:w-12 sm:h-12 md:w-12 md:h-12..."
75
+ */
76
+ export function getContainerSize(size: SizeVariant): string {
77
+ const token = RESPONSIVE_CONTAINER_SIZE[size];
78
+ const classes: string[] = [`w-${token.xs}`, `h-${token.xs}`];
79
+
80
+ const breakpoints: Breakpoint[] = ['sm', 'md', 'lg', 'xl', '2xl'];
81
+ breakpoints.forEach((bp) => {
82
+ if (token[bp] !== token.xs) {
83
+ classes.push(`${bp}:w-${token[bp]}`, `${bp}:h-${token[bp]}`);
84
+ }
85
+ });
86
+
87
+ return classes.join(' ');
88
+ }
89
+
90
+ /**
91
+ * Get responsive text size classes
92
+ * @param size - Size variant
93
+ * @returns Text size class string
94
+ *
95
+ * @example
96
+ * getTextSize('md') // "text-sm sm:text-base"
97
+ */
98
+ export function getTextSize(size: SizeVariant): string {
99
+ const token = RESPONSIVE_TEXT_SIZE[size];
100
+ return cn(token.mobile, token.tablet);
101
+ }
102
+
103
+ /**
104
+ * Build responsive gap/spacing classes for layout
105
+ * @param size - Size variant
106
+ * @returns Gap class string
107
+ *
108
+ * @example
109
+ * getGap('md') // "gap-3 sm:gap-4 md:gap-4 lg:gap-5 xl:gap-5 2xl:gap-6"
110
+ */
111
+ export function getGap(size: SizeVariant): string {
112
+ return getSpacing('gap', size);
113
+ }
114
+
115
+ /**
116
+ * Build responsive space-y classes for vertical stacking
117
+ * @param size - Size variant
118
+ * @returns Space-y class string
119
+ *
120
+ * @example
121
+ * getSpaceY('md') // "space-y-3 sm:space-y-4 md:space-y-4 lg:space-y-5..."
122
+ */
123
+ export function getSpaceY(size: SizeVariant): string {
124
+ return getSpacing('space-y', size);
125
+ }
126
+
127
+ /**
128
+ * Get complete responsive styles for common component patterns
129
+ * @param size - Size variant
130
+ * @param options - Which styles to include
131
+ * @returns Combined class string
132
+ *
133
+ * @example
134
+ * getResponsiveStyles('md', { padding: true, gap: true })
135
+ * // "p-4 sm:p-5 md:p-5 lg:p-6 xl:p-8 2xl:p-10 gap-3 sm:gap-4..."
136
+ */
137
+ export function getResponsiveStyles(
138
+ size: SizeVariant,
139
+ options: {
140
+ padding?: boolean;
141
+ gap?: boolean;
142
+ iconSize?: boolean;
143
+ textSize?: boolean;
144
+ containerSize?: boolean;
145
+ } = {}
146
+ ): string {
147
+ const classes: string[] = [];
148
+
149
+ if (options.padding) {
150
+ classes.push(getSpacing('p', size));
151
+ }
152
+ if (options.gap) {
153
+ classes.push(getGap(size));
154
+ }
155
+ if (options.iconSize) {
156
+ classes.push(getIconSize(size));
157
+ }
158
+ if (options.textSize) {
159
+ classes.push(getTextSize(size));
160
+ }
161
+ if (options.containerSize) {
162
+ classes.push(getContainerSize(size));
163
+ }
164
+
165
+ return classes.join(' ');
166
+ }
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * FormField Component (Molecule)
3
- * @description Label + Input combination
3
+ * @description Label + Input combination (Responsive)
4
4
  */
5
5
 
6
6
  import { forwardRef } from 'react';
7
- import { cn } from '../../infrastructure/utils';
7
+ import { cn, getSpaceY } from '../../infrastructure/utils';
8
8
  import type { BaseProps } from '../../domain/types';
9
9
  import { Input } from '../atoms/Input';
10
10
  import { Text } from '../atoms/Text';
@@ -16,20 +16,21 @@ export interface FormFieldProps extends BaseProps {
16
16
  required?: boolean;
17
17
  id?: string;
18
18
  inputProps?: React.ComponentProps<typeof Input>;
19
+ fullWidth?: boolean; // Enable responsive full width
19
20
  }
20
21
 
21
22
  export const FormField = forwardRef<HTMLInputElement, FormFieldProps>(
22
- ({ label, error, helperText, required, id, className, inputProps, ...props }, ref) => {
23
+ ({ label, error, helperText, required, id, className, inputProps, fullWidth = true, ...props }, ref) => {
23
24
  const fieldId = id || inputProps?.id || inputProps?.name;
24
25
  const errorId = error ? `${fieldId}-error` : undefined;
25
26
  const helperId = helperText ? `${fieldId}-helper` : undefined;
26
27
 
27
28
  return (
28
- <div className={cn('space-y-1.5', className)} {...props}>
29
+ <div className={cn(getSpaceY('sm'), fullWidth && 'w-full', className)} {...props}>
29
30
  {label && (
30
- <label htmlFor={fieldId} className="text-sm font-medium">
31
+ <label htmlFor={fieldId} className="text-sm font-medium block">
31
32
  {label}
32
- {required && <span className="text-destructive ml-1">*</span>}
33
+ {required && <span className="text-destructive ml-1" aria-label="required">*</span>}
33
34
  </label>
34
35
  )}
35
36
 
@@ -21,7 +21,7 @@ const SelectTrigger = React.forwardRef<
21
21
  <SelectPrimitive.Trigger
22
22
  ref={ref}
23
23
  className={cn(
24
- 'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
24
+ 'flex h-9 sm:h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
25
25
  className
26
26
  )}
27
27
  {...props}
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * EmptyState Component (Organism)
3
- * @description Component for displaying empty states with optional actions
3
+ * @description Component for displaying empty states with optional actions (Responsive)
4
4
  */
5
5
 
6
6
  import { forwardRef } from 'react';
7
- import { cn } from '../../infrastructure/utils';
7
+ import { cn, getSpacing, getGap } from '../../infrastructure/utils';
8
8
  import type { BaseProps, SizeVariant } from '../../domain/types';
9
9
 
10
10
  export interface EmptyStateProps extends BaseProps {
@@ -16,36 +16,9 @@ export interface EmptyStateProps extends BaseProps {
16
16
  onClick: () => void;
17
17
  };
18
18
  size?: Extract<SizeVariant, 'sm' | 'md' | 'lg' | 'xl'>;
19
+ centered?: boolean;
19
20
  }
20
21
 
21
- const sizeStyles = {
22
- sm: 'p-6 gap-3',
23
- md: 'p-8 gap-4',
24
- lg: 'p-12 gap-5',
25
- xl: 'p-16 gap-6',
26
- };
27
-
28
- const iconSizeStyles = {
29
- sm: 'h-8 w-8',
30
- md: 'h-12 w-12',
31
- lg: 'h-16 w-16',
32
- xl: 'h-20 w-20',
33
- };
34
-
35
- const titleSizeStyles = {
36
- sm: 'text-sm',
37
- md: 'text-base',
38
- lg: 'text-lg',
39
- xl: 'text-xl',
40
- };
41
-
42
- const descriptionSizeStyles = {
43
- sm: 'text-xs',
44
- md: 'text-sm',
45
- lg: 'text-base',
46
- xl: 'text-lg',
47
- };
48
-
49
22
  export const EmptyState = forwardRef<HTMLDivElement, EmptyStateProps>(
50
23
  (
51
24
  {
@@ -64,17 +37,18 @@ export const EmptyState = forwardRef<HTMLDivElement, EmptyStateProps>(
64
37
  ref={ref}
65
38
  className={cn(
66
39
  'flex flex-col items-center justify-center text-center',
67
- sizeStyles[size],
40
+ getSpacing('p', size),
41
+ getGap(size === 'xl' ? 'lg' : size),
68
42
  className
69
43
  )}
70
44
  {...props}
71
45
  >
72
46
  {Icon && (
73
- <Icon className={cn(iconSizeStyles[size], 'text-muted-foreground/50 mx-auto mb-2')} />
47
+ <Icon className={cn(size === 'sm' ? 'h-8 w-8 sm:h-10 sm:w-10' : size === 'md' ? 'h-12 w-12 sm:h-14 sm:w-14' : size === 'lg' ? 'h-16 w-16 sm:h-20 sm:w-20' : 'h-20 w-20 sm:h-24 sm:w-24', 'text-muted-foreground/50 mx-auto mb-2')} />
74
48
  )}
75
- <h3 className={cn('font-semibold text-foreground', titleSizeStyles[size])}>{title}</h3>
49
+ <h3 className={cn('font-semibold text-foreground', size === 'sm' ? 'text-sm sm:text-base' : size === 'md' ? 'text-base sm:text-lg' : size === 'lg' ? 'text-lg sm:text-xl' : 'text-xl sm:text-2xl')}>{title}</h3>
76
50
  {description && (
77
- <p className={cn('text-muted-foreground max-w-md', descriptionSizeStyles[size])}>
51
+ <p className={cn('text-muted-foreground max-w-md', size === 'sm' ? 'text-xs sm:text-sm' : size === 'md' ? 'text-sm sm:text-base' : size === 'lg' ? 'text-base sm:text-lg' : 'text-lg sm:text-xl')}>
78
52
  {description}
79
53
  </p>
80
54
  )}
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Grid Component (Organism)
3
+ * @description Responsive grid system with configurable columns
4
+ */
5
+
6
+ import { cn } from '../../infrastructure/utils';
7
+ import type { BaseProps } from '../../domain/types';
8
+ import type { Breakpoint } from '../../domain/types/breakpoint.types';
9
+
10
+ export interface GridProps extends BaseProps {
11
+ children: React.ReactNode;
12
+ cols?: {
13
+ xs?: number;
14
+ sm?: number;
15
+ md?: number;
16
+ lg?: number;
17
+ xl?: number;
18
+ '2xl'?: number;
19
+ };
20
+ gap?: number | string;
21
+ className?: string;
22
+ }
23
+
24
+ /**
25
+ * Responsive grid system that adjusts columns based on breakpoint
26
+ * @param cols - Number of columns per breakpoint
27
+ * @param gap - Gap between items (spacing unit or custom value)
28
+ * @param children - Grid items
29
+ *
30
+ * @example
31
+ * ```tsx
32
+ * // Auto responsive: 1 col mobile, 2 tablet, 3 desktop
33
+ * <Grid cols={{ xs: 1, sm: 2, lg: 3 }}>
34
+ * <div>Item 1</div>
35
+ * <div>Item 2</div>
36
+ * </Grid>
37
+ *
38
+ * // Fixed 4 columns
39
+ * <Grid cols={{ lg: 4 }}>
40
+ * {items.map(item => <Card key={item.id}>{item}</Card>)}
41
+ * </Grid>
42
+ * ```
43
+ */
44
+ export const Grid = ({ children, cols = {}, gap = 4, className }: GridProps) => {
45
+ const {
46
+ xs = 1,
47
+ sm = xs,
48
+ md = sm,
49
+ lg = md,
50
+ xl = lg,
51
+ '2xl': xl2 = xl
52
+ } = cols;
53
+
54
+ const gridCols = cn(
55
+ 'grid',
56
+ `grid-cols-${xs}`, // xs always applies (mobile-first)
57
+ sm !== xs && `sm:grid-cols-${sm}`,
58
+ md !== sm && `md:grid-cols-${md}`,
59
+ lg !== md && `lg:grid-cols-${lg}`,
60
+ xl !== lg && `xl:grid-cols-${xl}`,
61
+ xl2 !== xl && `2xl:grid-cols-${xl2}`
62
+ );
63
+
64
+ const gapClass = typeof gap === 'number' ? `gap-${gap}` : gap;
65
+
66
+ return (
67
+ <div className={cn(gridCols, gapClass, className)}>
68
+ {children}
69
+ </div>
70
+ );
71
+ };
72
+
73
+ Grid.displayName = 'Grid';
74
+
75
+ /**
76
+ * Grid Item component for custom column spans
77
+ */
78
+ export interface GridItemProps extends BaseProps {
79
+ children: React.ReactNode;
80
+ span?: {
81
+ xs?: number;
82
+ sm?: number;
83
+ md?: number;
84
+ lg?: number;
85
+ xl?: number;
86
+ '2xl'?: number;
87
+ };
88
+ className?: string;
89
+ }
90
+
91
+ /**
92
+ * Grid item with custom column span per breakpoint
93
+ * @param span - Number of columns to span (1-12)
94
+ *
95
+ * @example
96
+ * ```tsx
97
+ * <Grid cols={{ lg: 4 }}>
98
+ * <GridItem span={{ lg: 2 }}>Spans 2 columns</GridItem>
99
+ * <GridItem>Regular 1 column</GridItem>
100
+ * </Grid>
101
+ * ```
102
+ */
103
+ export const GridItem = ({ children, span = {}, className }: GridItemProps) => {
104
+ const {
105
+ xs = 1,
106
+ sm = xs,
107
+ md = sm,
108
+ lg = md,
109
+ xl = lg,
110
+ '2xl': xl2 = xl
111
+ } = span;
112
+
113
+ const colSpan = cn(
114
+ 'col-span-1',
115
+ `sm:col-span-${sm}`,
116
+ `md:col-span-${md}`,
117
+ `lg:col-span-${lg}`,
118
+ `xl:col-span-${xl}`,
119
+ `2xl:col-span-${xl2}`
120
+ );
121
+
122
+ return (
123
+ <div className={cn(colSpan, className)}>
124
+ {children}
125
+ </div>
126
+ );
127
+ };
128
+
129
+ GridItem.displayName = 'GridItem';
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * LoadingState Component (Organism)
3
- * @description Component for displaying loading states
3
+ * @description Component for displaying loading states (Responsive)
4
4
  */
5
5
 
6
6
  import { forwardRef } from 'react';
7
- import { cn } from '../../infrastructure/utils';
7
+ import { cn, getSpacing, getGap } from '../../infrastructure/utils';
8
8
  import { Spinner } from '../atoms/Spinner';
9
9
  import type { BaseProps, SizeVariant } from '../../domain/types';
10
10
 
@@ -12,26 +12,9 @@ export interface LoadingStateProps extends BaseProps {
12
12
  message?: string;
13
13
  size?: Extract<SizeVariant, 'sm' | 'md' | 'lg'>;
14
14
  variant?: 'spinner' | 'skeleton' | 'dots';
15
+ centered?: boolean;
15
16
  }
16
17
 
17
- const sizeStyles = {
18
- sm: 'gap-3',
19
- md: 'gap-4',
20
- lg: 'gap-5',
21
- };
22
-
23
- const spinnerSizeStyles = {
24
- sm: 'sm' as const,
25
- md: 'md' as const,
26
- lg: 'lg' as const,
27
- };
28
-
29
- const messageSizeStyles = {
30
- sm: 'text-xs',
31
- md: 'text-sm',
32
- lg: 'text-base',
33
- };
34
-
35
18
  export const LoadingState = forwardRef<HTMLDivElement, LoadingStateProps>(
36
19
  (
37
20
  {
@@ -39,30 +22,34 @@ export const LoadingState = forwardRef<HTMLDivElement, LoadingStateProps>(
39
22
  message,
40
23
  size = 'md',
41
24
  variant = 'spinner',
25
+ centered = true,
42
26
  ...props
43
27
  },
44
28
  ref
45
29
  ) => {
30
+ const spinnerSize: 'sm' | 'md' | 'lg' = size;
46
31
  return (
47
32
  <div
48
33
  ref={ref}
49
34
  className={cn(
50
- 'flex flex-col items-center justify-center p-8',
51
- sizeStyles[size],
35
+ 'flex flex-col items-center justify-center',
36
+ getGap(size),
37
+ getSpacing('p', size),
38
+ centered && 'min-h-[200px] sm:min-h-[300px]',
52
39
  className
53
40
  )}
54
41
  {...props}
55
42
  >
56
- {variant === 'spinner' && <Spinner size={spinnerSizeStyles[size]} />}
43
+ {variant === 'spinner' && <Spinner size={spinnerSize} />}
57
44
  {variant === 'dots' && (
58
- <div className="flex gap-1">
45
+ <div className="flex gap-1 sm:gap-2">
59
46
  {[0, 1, 2].map((i) => (
60
47
  <div
61
48
  key={i}
62
49
  className={cn(
63
50
  'w-2 h-2 bg-primary rounded-full animate-bounce',
64
- size === 'sm' && 'w-1.5 h-1.5',
65
- size === 'lg' && 'w-3 h-3'
51
+ 'w-1.5 h-1.5 sm:w-2 sm:h-2',
52
+ 'lg:w-2.5 h-2.5'
66
53
  )}
67
54
  style={{ animationDelay: `${i * 0.15}s` }}
68
55
  />
@@ -70,14 +57,14 @@ export const LoadingState = forwardRef<HTMLDivElement, LoadingStateProps>(
70
57
  </div>
71
58
  )}
72
59
  {variant === 'skeleton' && (
73
- <div className="w-full space-y-3">
74
- <div className="h-4 bg-muted animate-pulse rounded" style={{ width: '40%' }} />
75
- <div className="h-3 bg-muted animate-pulse rounded" style={{ width: '70%' }} />
76
- <div className="h-3 bg-muted animate-pulse rounded" style={{ width: '60%' }} />
60
+ <div className="w-full space-y-2 sm:space-y-3">
61
+ <div className="h-3 sm:h-4 bg-muted animate-pulse rounded w-2/3 sm:w-1/2" />
62
+ <div className="h-2.5 sm:h-3 bg-muted animate-pulse rounded w-full" />
63
+ <div className="h-2.5 sm:h-3 bg-muted animate-pulse rounded w-3/4" />
77
64
  </div>
78
65
  )}
79
66
  {message && variant !== 'skeleton' && (
80
- <p className={cn('text-muted-foreground mt-2', messageSizeStyles[size])}>{message}</p>
67
+ <p className={cn('text-muted-foreground mt-2 sm:mt-3', size === 'sm' ? 'text-xs sm:text-sm' : size === 'md' ? 'text-sm sm:text-base' : 'text-base sm:text-lg', 'text-center max-w-md')}>{message}</p>
81
68
  )}
82
69
  </div>
83
70
  );
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * MetricCard Component (Organism)
3
- * @description Card component for displaying metrics and statistics with trend indicators
3
+ * @description Card component for displaying metrics and statistics with trend indicators (Responsive)
4
4
  */
5
5
 
6
6
  import { forwardRef } from 'react';
7
- import { cn } from '../../infrastructure/utils';
7
+ import { cn, getSpacing, getIconSize, getTextSize, getGap } from '../../infrastructure/utils';
8
8
  import { Card, CardContent } from './Card';
9
9
  import type { BaseProps, ColorVariant, SizeVariant } from '../../domain/types';
10
10
 
@@ -21,32 +21,9 @@ export interface MetricCardProps extends BaseProps {
21
21
  variant?: ColorVariant;
22
22
  trendValueFormatter?: (value: number) => string;
23
23
  onClick?: () => void;
24
+ responsiveLayout?: boolean; // Enable responsive layout changes
24
25
  }
25
26
 
26
- const sizeStyles = {
27
- sm: 'p-3',
28
- md: 'p-4',
29
- lg: 'p-5',
30
- };
31
-
32
- const valueSizeStyles = {
33
- sm: 'text-lg',
34
- md: 'text-2xl',
35
- lg: 'text-3xl',
36
- };
37
-
38
- const iconSizeStyles = {
39
- sm: 'h-4 w-4',
40
- md: 'h-8 w-8',
41
- lg: 'h-10 w-10',
42
- };
43
-
44
- const labelSizeStyles = {
45
- sm: 'text-xs',
46
- md: 'text-sm',
47
- lg: 'text-base',
48
- };
49
-
50
27
  const trendColors: Record<ColorVariant, { positive: string; negative: string; bg: string }> = {
51
28
  primary: { positive: 'text-primary', negative: 'text-destructive', bg: 'bg-primary' },
52
29
  secondary: { positive: 'text-secondary', negative: 'text-destructive', bg: 'bg-secondary' },
@@ -68,6 +45,7 @@ export const MetricCard = forwardRef<HTMLDivElement, MetricCardProps>(
68
45
  variant = 'primary',
69
46
  trendValueFormatter = (v) => `${v > 0 ? '+' : ''}${v}%`,
70
47
  onClick,
48
+ responsiveLayout = true,
71
49
  ...props
72
50
  },
73
51
  ref
@@ -87,34 +65,48 @@ export const MetricCard = forwardRef<HTMLDivElement, MetricCardProps>(
87
65
  onClick={onClick}
88
66
  {...props}
89
67
  >
90
- <CardContent className={cn(sizeStyles[size])}>
91
- <div className="flex items-center justify-between">
92
- <div className="flex-1">
93
- <p className={cn('text-muted-foreground font-medium', labelSizeStyles[size])}>{title}</p>
94
- <p className={cn('font-bold text-foreground', valueSizeStyles[size])}>
68
+ <CardContent className={getSpacing('p', size)}>
69
+ <div className={cn(
70
+ 'flex items-center justify-between',
71
+ getGap('md'),
72
+ responsiveLayout && 'flex-col sm:flex-row'
73
+ )}>
74
+ <div className={cn(
75
+ 'flex-1',
76
+ responsiveLayout && 'text-center sm:text-left w-full sm:w-auto'
77
+ )}>
78
+ <p className={cn('text-muted-foreground font-medium', getTextSize(size))}>{title}</p>
79
+ <p className={cn('font-bold text-foreground mt-1', size === 'sm' ? 'text-lg sm:text-xl' : size === 'md' ? 'text-2xl' : 'text-3xl sm:text-4xl')}>
95
80
  {typeof value === 'number' ? value.toLocaleString() : value}
96
81
  </p>
97
82
  {trend && (
98
- <div className="flex items-center gap-1 mt-1">
83
+ <div className={cn(
84
+ 'flex items-center gap-1 mt-1',
85
+ responsiveLayout ? 'justify-center sm:justify-start' : ''
86
+ )}>
99
87
  <span
100
88
  className={cn(
101
- 'text-xs font-medium',
89
+ 'text-xs sm:text-sm font-medium',
102
90
  isPositive ? colors.positive : colors.negative
103
91
  )}
104
92
  >
105
93
  {TrendIcon}
106
94
  </span>
107
- <p className={cn('text-xs', isPositive ? colors.positive : colors.negative)}>
95
+ <p className={cn('text-xs sm:text-sm', isPositive ? colors.positive : colors.negative)}>
108
96
  {trendValueFormatter(trend.value)}
109
97
  </p>
110
98
  {trend.label && (
111
- <p className="text-xs text-muted-foreground ml-1">{trend.label}</p>
99
+ <p className="text-xs text-muted-foreground ml-1 hidden sm:inline">{trend.label}</p>
112
100
  )}
113
101
  </div>
114
102
  )}
115
103
  </div>
116
104
  {Icon && (
117
- <Icon className={cn(iconSizeStyles[size], iconColor, 'flex-shrink-0')} />
105
+ <div className={cn(
106
+ responsiveLayout ? 'sm:self-auto' : ''
107
+ )}>
108
+ <Icon className={cn(getIconSize(size), iconColor, 'flex-shrink-0')} />
109
+ </div>
118
110
  )}
119
111
  </div>
120
112
  </CardContent>