@sudobility/devops-components-rn 1.0.0 → 1.0.2

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.
Files changed (65) hide show
  1. package/dist/AlertDialog.d.ts.map +1 -1
  2. package/dist/ApiPlayground.d.ts.map +1 -1
  3. package/dist/ApiReference.d.ts.map +1 -1
  4. package/dist/AuditLog.d.ts +26 -4
  5. package/dist/AuditLog.d.ts.map +1 -1
  6. package/dist/BodyMetrics.d.ts.map +1 -1
  7. package/dist/BuildLog.d.ts +16 -5
  8. package/dist/BuildLog.d.ts.map +1 -1
  9. package/dist/ChangelogDisplay.d.ts.map +1 -1
  10. package/dist/CodePlayground.d.ts.map +1 -1
  11. package/dist/ConflictResolver.d.ts.map +1 -1
  12. package/dist/DealPipeline.d.ts.map +1 -1
  13. package/dist/DeploymentStatus.d.ts +11 -4
  14. package/dist/DeploymentStatus.d.ts.map +1 -1
  15. package/dist/DriverLog.d.ts.map +1 -1
  16. package/dist/MemoryUsage.d.ts.map +1 -1
  17. package/dist/MetricsGrid.d.ts +12 -19
  18. package/dist/MetricsGrid.d.ts.map +1 -1
  19. package/dist/PipelineView.d.ts +21 -5
  20. package/dist/PipelineView.d.ts.map +1 -1
  21. package/dist/RegressionTest.d.ts.map +1 -1
  22. package/dist/SystemStatusIndicator.d.ts +9 -8
  23. package/dist/SystemStatusIndicator.d.ts.map +1 -1
  24. package/dist/TestResult.d.ts.map +1 -1
  25. package/dist/TestRunner.d.ts.map +1 -1
  26. package/dist/WebhookLogger.d.ts.map +1 -1
  27. package/dist/WorkflowBuilder.d.ts.map +1 -1
  28. package/dist/WorkflowTemplate.d.ts.map +1 -1
  29. package/dist/XmlParser.d.ts.map +1 -1
  30. package/dist/index.cjs +1002 -0
  31. package/dist/index.cjs.map +1 -0
  32. package/dist/index.d.ts +6 -27
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.mjs +1002 -0
  35. package/dist/index.mjs.map +1 -0
  36. package/package.json +9 -10
  37. package/src/AlertDialog.tsx +21 -16
  38. package/src/ApiPlayground.tsx +7 -3
  39. package/src/ApiReference.tsx +6 -2
  40. package/src/AuditLog.tsx +244 -21
  41. package/src/BodyMetrics.tsx +6 -2
  42. package/src/BuildLog.tsx +132 -26
  43. package/src/ChangelogDisplay.tsx +6 -2
  44. package/src/CodePlayground.tsx +7 -3
  45. package/src/ConflictResolver.tsx +7 -3
  46. package/src/DealPipeline.tsx +6 -2
  47. package/src/DeploymentStatus.tsx +159 -25
  48. package/src/DriverLog.tsx +4 -2
  49. package/src/MemoryUsage.tsx +6 -2
  50. package/src/MetricsGrid.tsx +99 -94
  51. package/src/PipelineView.tsx +225 -26
  52. package/src/RegressionTest.tsx +6 -2
  53. package/src/SystemStatusIndicator.tsx +70 -47
  54. package/src/TestResult.tsx +6 -2
  55. package/src/TestRunner.tsx +7 -3
  56. package/src/WebhookLogger.tsx +6 -2
  57. package/src/WorkflowBuilder.tsx +7 -3
  58. package/src/WorkflowTemplate.tsx +7 -3
  59. package/src/XmlParser.tsx +4 -2
  60. package/src/index.ts +41 -30
  61. package/src/nativewind.d.ts +3 -0
  62. package/dist/index.cjs.js +0 -1593
  63. package/dist/index.cjs.js.map +0 -1
  64. package/dist/index.esm.js +0 -1593
  65. package/dist/index.esm.js.map +0 -1
package/src/BuildLog.tsx CHANGED
@@ -1,32 +1,138 @@
1
1
  import React from 'react';
2
- import { Text, Pressable, type ViewProps } from 'react-native';
3
- import { cn } from '@sudobility/components-rn';
2
+ import { View, Text, ScrollView } from 'react-native';
3
+ import { cn, Card } from '@sudobility/components-rn';
4
4
 
5
- export interface BuildLogProps extends ViewProps {
6
- disabled?: boolean;
7
- onPress?: () => void;
8
- children?: React.ReactNode;
5
+ export type LogLevel = 'info' | 'warn' | 'error' | 'debug' | 'success';
6
+
7
+ export interface LogEntry {
8
+ id: string;
9
+ level: LogLevel;
10
+ message: string;
11
+ timestamp: Date;
12
+ source?: string;
13
+ }
14
+
15
+ export interface BuildLogProps {
16
+ entries: LogEntry[];
17
+ title?: string;
18
+ maxHeight?: number;
19
+ showTimestamp?: boolean;
20
+ showSource?: boolean;
21
+ className?: string;
9
22
  }
10
23
 
24
+ const levelConfig: Record<
25
+ LogLevel,
26
+ { color: string; bgColor: string; darkBgColor: string; prefix: string }
27
+ > = {
28
+ info: {
29
+ color: 'text-blue-600 dark:text-blue-400',
30
+ bgColor: 'bg-blue-50',
31
+ darkBgColor: 'dark:bg-blue-950',
32
+ prefix: 'INFO',
33
+ },
34
+ warn: {
35
+ color: 'text-yellow-600 dark:text-yellow-400',
36
+ bgColor: 'bg-yellow-50',
37
+ darkBgColor: 'dark:bg-yellow-950',
38
+ prefix: 'WARN',
39
+ },
40
+ error: {
41
+ color: 'text-red-600 dark:text-red-400',
42
+ bgColor: 'bg-red-50',
43
+ darkBgColor: 'dark:bg-red-950',
44
+ prefix: 'ERROR',
45
+ },
46
+ debug: {
47
+ color: 'text-gray-600 dark:text-gray-400',
48
+ bgColor: 'bg-gray-50',
49
+ darkBgColor: 'dark:bg-gray-900',
50
+ prefix: 'DEBUG',
51
+ },
52
+ success: {
53
+ color: 'text-green-600 dark:text-green-400',
54
+ bgColor: 'bg-green-50',
55
+ darkBgColor: 'dark:bg-green-950',
56
+ prefix: 'SUCCESS',
57
+ },
58
+ };
59
+
60
+ const formatTime = (date: Date): string => {
61
+ return date.toLocaleTimeString('en-US', {
62
+ hour12: false,
63
+ hour: '2-digit',
64
+ minute: '2-digit',
65
+ second: '2-digit',
66
+ });
67
+ };
68
+
11
69
  export const BuildLog: React.FC<BuildLogProps> = ({
70
+ entries,
71
+ title,
72
+ maxHeight = 400,
73
+ showTimestamp = true,
74
+ showSource = false,
12
75
  className,
13
- children,
14
- disabled = false,
15
- onPress,
16
- ...props
17
- }) => (
18
- <Pressable
19
- onPress={disabled ? undefined : onPress}
20
- disabled={disabled}
21
- accessibilityRole="button"
22
- accessibilityLabel="Build Log"
23
- className={cn(
24
- 'p-4 rounded-lg border bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700',
25
- disabled && 'opacity-50',
26
- className
27
- )}
28
- {...props}
29
- >
30
- {children || <Text className="text-gray-900 dark:text-white">BuildLog Component</Text>}
31
- </Pressable>
32
- );
76
+ }) => {
77
+ return (
78
+ <Card className={cn('overflow-hidden', className)}>
79
+ {title && (
80
+ <View className='px-4 py-3 border-b border-gray-200 dark:border-gray-700'>
81
+ <Text className='text-base font-semibold text-gray-900 dark:text-gray-100'>
82
+ {title}
83
+ </Text>
84
+ </View>
85
+ )}
86
+ <ScrollView
87
+ style={{ maxHeight }}
88
+ className='bg-gray-900 dark:bg-black'
89
+ showsVerticalScrollIndicator={true}
90
+ >
91
+ <View className='p-3'>
92
+ {entries.map(entry => {
93
+ const config = levelConfig[entry.level];
94
+ return (
95
+ <View
96
+ key={entry.id}
97
+ className={cn(
98
+ 'flex-row flex-wrap py-1 px-2 mb-1 rounded',
99
+ config.bgColor,
100
+ config.darkBgColor
101
+ )}
102
+ >
103
+ {showTimestamp && (
104
+ <Text className='font-mono text-xs text-gray-500 dark:text-gray-500 mr-2'>
105
+ [{formatTime(entry.timestamp)}]
106
+ </Text>
107
+ )}
108
+ <Text
109
+ className={cn(
110
+ 'font-mono text-xs font-bold mr-2',
111
+ config.color
112
+ )}
113
+ >
114
+ [{config.prefix}]
115
+ </Text>
116
+ {showSource && entry.source && (
117
+ <Text className='font-mono text-xs text-gray-400 dark:text-gray-600 mr-2'>
118
+ [{entry.source}]
119
+ </Text>
120
+ )}
121
+ <Text className='font-mono text-xs text-gray-200 dark:text-gray-300 flex-1'>
122
+ {entry.message}
123
+ </Text>
124
+ </View>
125
+ );
126
+ })}
127
+ </View>
128
+ </ScrollView>
129
+ <View className='px-4 py-2 bg-gray-800 dark:bg-gray-950 border-t border-gray-700'>
130
+ <Text className='text-xs text-gray-400 dark:text-gray-500'>
131
+ {entries.length} log entries
132
+ </Text>
133
+ </View>
134
+ </Card>
135
+ );
136
+ };
137
+
138
+ export default BuildLog;
@@ -19,9 +19,13 @@ export const ChangelogDisplay: React.FC<ChangelogDisplayProps> = ({
19
19
  disabled && 'opacity-50',
20
20
  className
21
21
  )}
22
- accessibilityLabel="Changelog Display"
22
+ accessibilityLabel='Changelog Display'
23
23
  {...props}
24
24
  >
25
- {children || <Text className="text-gray-900 dark:text-white">ChangelogDisplay Component</Text>}
25
+ {children || (
26
+ <Text className='text-gray-900 dark:text-white'>
27
+ ChangelogDisplay Component
28
+ </Text>
29
+ )}
26
30
  </View>
27
31
  );
@@ -18,8 +18,8 @@ export const CodePlayground: React.FC<CodePlaygroundProps> = ({
18
18
  <Pressable
19
19
  onPress={disabled ? undefined : onPress}
20
20
  disabled={disabled}
21
- accessibilityRole="button"
22
- accessibilityLabel="Code Playground"
21
+ accessibilityRole='button'
22
+ accessibilityLabel='Code Playground'
23
23
  className={cn(
24
24
  'p-4 rounded-lg border bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700',
25
25
  disabled && 'opacity-50',
@@ -27,6 +27,10 @@ export const CodePlayground: React.FC<CodePlaygroundProps> = ({
27
27
  )}
28
28
  {...props}
29
29
  >
30
- {children || <Text className="text-gray-900 dark:text-white">CodePlayground Component</Text>}
30
+ {children || (
31
+ <Text className='text-gray-900 dark:text-white'>
32
+ CodePlayground Component
33
+ </Text>
34
+ )}
31
35
  </Pressable>
32
36
  );
@@ -18,8 +18,8 @@ export const ConflictResolver: React.FC<ConflictResolverProps> = ({
18
18
  <Pressable
19
19
  onPress={disabled ? undefined : onPress}
20
20
  disabled={disabled}
21
- accessibilityRole="button"
22
- accessibilityLabel="Conflict Resolver"
21
+ accessibilityRole='button'
22
+ accessibilityLabel='Conflict Resolver'
23
23
  className={cn(
24
24
  'p-4 rounded-lg border bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700',
25
25
  disabled && 'opacity-50',
@@ -27,6 +27,10 @@ export const ConflictResolver: React.FC<ConflictResolverProps> = ({
27
27
  )}
28
28
  {...props}
29
29
  >
30
- {children || <Text className="text-gray-900 dark:text-white">ConflictResolver Component</Text>}
30
+ {children || (
31
+ <Text className='text-gray-900 dark:text-white'>
32
+ ConflictResolver Component
33
+ </Text>
34
+ )}
31
35
  </Pressable>
32
36
  );
@@ -19,9 +19,13 @@ export const DealPipeline: React.FC<DealPipelineProps> = ({
19
19
  disabled && 'opacity-50',
20
20
  className
21
21
  )}
22
- accessibilityLabel="Deal Pipeline"
22
+ accessibilityLabel='Deal Pipeline'
23
23
  {...props}
24
24
  >
25
- {children || <Text className="text-gray-900 dark:text-white">DealPipeline Component</Text>}
25
+ {children || (
26
+ <Text className='text-gray-900 dark:text-white'>
27
+ DealPipeline Component
28
+ </Text>
29
+ )}
26
30
  </View>
27
31
  );
@@ -1,32 +1,166 @@
1
1
  import React from 'react';
2
- import { Text, Pressable, type ViewProps } from 'react-native';
3
- import { cn } from '@sudobility/components-rn';
2
+ import { View, Text, Pressable } from 'react-native';
3
+ import { cn, Card } from '@sudobility/components-rn';
4
4
 
5
- export interface DeploymentStatusProps extends ViewProps {
6
- disabled?: boolean;
5
+ export type DeploymentState =
6
+ | 'pending'
7
+ | 'building'
8
+ | 'deploying'
9
+ | 'success'
10
+ | 'failed'
11
+ | 'cancelled';
12
+
13
+ export interface DeploymentStatusProps {
14
+ state: DeploymentState;
15
+ environment: string;
16
+ version: string;
17
+ timestamp: Date;
18
+ commitHash?: string;
19
+ commitMessage?: string;
20
+ duration?: number;
7
21
  onPress?: () => void;
8
- children?: React.ReactNode;
22
+ className?: string;
9
23
  }
10
24
 
25
+ const stateConfig: Record<
26
+ DeploymentState,
27
+ {
28
+ color: string;
29
+ bgColor: string;
30
+ darkBgColor: string;
31
+ label: string;
32
+ icon: string;
33
+ }
34
+ > = {
35
+ pending: {
36
+ color: 'text-gray-600 dark:text-gray-400',
37
+ bgColor: 'bg-gray-100',
38
+ darkBgColor: 'dark:bg-gray-800',
39
+ label: 'Pending',
40
+ icon: '⏳',
41
+ },
42
+ building: {
43
+ color: 'text-blue-600 dark:text-blue-400',
44
+ bgColor: 'bg-blue-100',
45
+ darkBgColor: 'dark:bg-blue-900',
46
+ label: 'Building',
47
+ icon: '🔨',
48
+ },
49
+ deploying: {
50
+ color: 'text-purple-600 dark:text-purple-400',
51
+ bgColor: 'bg-purple-100',
52
+ darkBgColor: 'dark:bg-purple-900',
53
+ label: 'Deploying',
54
+ icon: '🚀',
55
+ },
56
+ success: {
57
+ color: 'text-green-600 dark:text-green-400',
58
+ bgColor: 'bg-green-100',
59
+ darkBgColor: 'dark:bg-green-900',
60
+ label: 'Success',
61
+ icon: '✓',
62
+ },
63
+ failed: {
64
+ color: 'text-red-600 dark:text-red-400',
65
+ bgColor: 'bg-red-100',
66
+ darkBgColor: 'dark:bg-red-900',
67
+ label: 'Failed',
68
+ icon: '✗',
69
+ },
70
+ cancelled: {
71
+ color: 'text-orange-600 dark:text-orange-400',
72
+ bgColor: 'bg-orange-100',
73
+ darkBgColor: 'dark:bg-orange-900',
74
+ label: 'Cancelled',
75
+ icon: '⊘',
76
+ },
77
+ };
78
+
79
+ const formatDuration = (seconds: number): string => {
80
+ if (seconds < 60) return `${seconds}s`;
81
+ const minutes = Math.floor(seconds / 60);
82
+ const remainingSeconds = seconds % 60;
83
+ if (minutes < 60) return `${minutes}m ${remainingSeconds}s`;
84
+ const hours = Math.floor(minutes / 60);
85
+ const remainingMinutes = minutes % 60;
86
+ return `${hours}h ${remainingMinutes}m`;
87
+ };
88
+
11
89
  export const DeploymentStatus: React.FC<DeploymentStatusProps> = ({
12
- className,
13
- children,
14
- disabled = false,
90
+ state,
91
+ environment,
92
+ version,
93
+ timestamp,
94
+ commitHash,
95
+ commitMessage,
96
+ duration,
15
97
  onPress,
16
- ...props
17
- }) => (
18
- <Pressable
19
- onPress={disabled ? undefined : onPress}
20
- disabled={disabled}
21
- accessibilityRole="button"
22
- accessibilityLabel="Deployment Status"
23
- className={cn(
24
- 'p-4 rounded-lg border bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700',
25
- disabled && 'opacity-50',
26
- className
27
- )}
28
- {...props}
29
- >
30
- {children || <Text className="text-gray-900 dark:text-white">DeploymentStatus Component</Text>}
31
- </Pressable>
32
- );
98
+ className,
99
+ }) => {
100
+ const config = stateConfig[state];
101
+
102
+ const content = (
103
+ <Card className={cn('p-4', className)}>
104
+ <View className='flex-row items-start justify-between'>
105
+ <View className='flex-1'>
106
+ <View className='flex-row items-center'>
107
+ <View
108
+ className={cn(
109
+ 'px-2 py-1 rounded-md mr-2',
110
+ config.bgColor,
111
+ config.darkBgColor
112
+ )}
113
+ >
114
+ <Text className={cn('text-xs font-medium', config.color)}>
115
+ {config.icon} {config.label}
116
+ </Text>
117
+ </View>
118
+ <View className='bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded-md'>
119
+ <Text className='text-xs font-medium text-gray-700 dark:text-gray-300'>
120
+ {environment}
121
+ </Text>
122
+ </View>
123
+ </View>
124
+ <Text className='mt-2 text-base font-semibold text-gray-900 dark:text-gray-100'>
125
+ {version}
126
+ </Text>
127
+ {commitHash && (
128
+ <Text className='mt-1 text-sm font-mono text-gray-600 dark:text-gray-400'>
129
+ {commitHash.substring(0, 7)}
130
+ </Text>
131
+ )}
132
+ {commitMessage && (
133
+ <Text
134
+ className='mt-1 text-sm text-gray-600 dark:text-gray-400'
135
+ numberOfLines={2}
136
+ >
137
+ {commitMessage}
138
+ </Text>
139
+ )}
140
+ </View>
141
+ </View>
142
+ <View className='flex-row items-center justify-between mt-3 pt-3 border-t border-gray-200 dark:border-gray-700'>
143
+ <Text className='text-xs text-gray-500 dark:text-gray-500'>
144
+ {timestamp.toLocaleString()}
145
+ </Text>
146
+ {duration !== undefined && (
147
+ <Text className='text-xs text-gray-500 dark:text-gray-500'>
148
+ Duration: {formatDuration(duration)}
149
+ </Text>
150
+ )}
151
+ </View>
152
+ </Card>
153
+ );
154
+
155
+ if (onPress) {
156
+ return (
157
+ <Pressable onPress={onPress} accessibilityRole='button'>
158
+ {content}
159
+ </Pressable>
160
+ );
161
+ }
162
+
163
+ return content;
164
+ };
165
+
166
+ export default DeploymentStatus;
package/src/DriverLog.tsx CHANGED
@@ -19,9 +19,11 @@ export const DriverLog: React.FC<DriverLogProps> = ({
19
19
  disabled && 'opacity-50',
20
20
  className
21
21
  )}
22
- accessibilityLabel="Driver Log"
22
+ accessibilityLabel='Driver Log'
23
23
  {...props}
24
24
  >
25
- {children || <Text className="text-gray-900 dark:text-white">DriverLog Component</Text>}
25
+ {children || (
26
+ <Text className='text-gray-900 dark:text-white'>DriverLog Component</Text>
27
+ )}
26
28
  </View>
27
29
  );
@@ -19,9 +19,13 @@ export const MemoryUsage: React.FC<MemoryUsageProps> = ({
19
19
  disabled && 'opacity-50',
20
20
  className
21
21
  )}
22
- accessibilityLabel="Memory Usage"
22
+ accessibilityLabel='Memory Usage'
23
23
  {...props}
24
24
  >
25
- {children || <Text className="text-gray-900 dark:text-white">MemoryUsage Component</Text>}
25
+ {children || (
26
+ <Text className='text-gray-900 dark:text-white'>
27
+ MemoryUsage Component
28
+ </Text>
29
+ )}
26
30
  </View>
27
31
  );
@@ -1,115 +1,120 @@
1
1
  import React from 'react';
2
- import { View, Text, FlatList, type ViewProps } from 'react-native';
3
- import { cn } from '@sudobility/components-rn';
2
+ import { View, Text, Pressable } from 'react-native';
3
+ import { cn, Card } from '@sudobility/components-rn';
4
4
 
5
- type MetricColor = 'blue' | 'green' | 'purple' | 'orange' | 'pink' | 'gray';
6
-
7
- export interface MetricItem {
8
- value: string | number;
5
+ export interface Metric {
6
+ id: string;
9
7
  label: string;
10
- color?: MetricColor;
8
+ value: number | string;
9
+ unit?: string;
10
+ change?: number;
11
+ changeLabel?: string;
11
12
  icon?: React.ReactNode;
12
- trend?: {
13
- direction: 'up' | 'down';
14
- value: string;
15
- };
16
13
  }
17
14
 
18
- export interface MetricsGridProps extends ViewProps {
19
- title?: string;
20
- description?: string;
21
- metrics: MetricItem[];
22
- columns?: 2 | 3 | 4;
15
+ export interface MetricsGridProps {
16
+ metrics: Metric[];
17
+ columns?: 1 | 2 | 3 | 4;
18
+ onMetricPress?: (metric: Metric) => void;
19
+ className?: string;
23
20
  }
24
21
 
25
- const colorClasses: Record<MetricColor, string> = {
26
- blue: 'text-blue-600 dark:text-blue-400',
27
- green: 'text-green-600 dark:text-green-400',
28
- purple: 'text-purple-600 dark:text-purple-400',
29
- orange: 'text-orange-600 dark:text-orange-400',
30
- pink: 'text-pink-600 dark:text-pink-400',
31
- gray: 'text-gray-600 dark:text-gray-400',
22
+ const formatChange = (change: number): string => {
23
+ const sign = change >= 0 ? '+' : '';
24
+ return sign + change.toFixed(1) + '%';
32
25
  };
33
26
 
34
- interface MetricCardProps {
35
- metric: MetricItem;
36
- }
37
-
38
- const MetricCard: React.FC<MetricCardProps> = ({ metric }) => {
39
- const colorClass = metric.color ? colorClasses[metric.color] : colorClasses.blue;
40
-
41
- return (
42
- <View className="bg-white dark:bg-gray-800 rounded-xl p-6 border border-gray-200 dark:border-gray-700 items-center mb-4 mx-2">
43
- {metric.icon && (
44
- <View className={cn('mb-4', colorClass)}>{metric.icon}</View>
45
- )}
46
-
47
- <View className="gap-2 items-center">
48
- <Text className={cn('text-3xl font-bold', colorClass)}>
49
- {metric.value}
50
- </Text>
51
-
52
- <Text className="text-gray-600 dark:text-gray-400 font-medium">
53
- {metric.label}
54
- </Text>
55
-
56
- {metric.trend && (
57
- <Text
58
- className={cn(
59
- 'text-sm font-semibold',
60
- metric.trend.direction === 'up'
61
- ? 'text-green-600 dark:text-green-400'
62
- : 'text-red-600 dark:text-red-400'
63
- )}
64
- >
65
- {metric.trend.direction === 'up' ? '↑' : '↓'} {metric.trend.value}
66
- </Text>
67
- )}
68
- </View>
69
- </View>
70
- );
71
- };
72
-
73
- /**
74
- * MetricsGrid component for React Native
75
- * Grid display of metric cards
76
- */
77
27
  export const MetricsGrid: React.FC<MetricsGridProps> = ({
78
- title,
79
- description,
80
28
  metrics,
81
29
  columns = 2,
30
+ onMetricPress,
82
31
  className,
83
- ...props
84
32
  }) => {
33
+ const columnWidthClass = {
34
+ 1: 'w-full',
35
+ 2: 'w-1/2',
36
+ 3: 'w-1/3',
37
+ 4: 'w-1/4',
38
+ }[columns];
39
+
85
40
  return (
86
- <View className={cn('py-8 px-4', className)} {...props}>
87
- {(title || description) && (
88
- <View className="items-center mb-8">
89
- {title && (
90
- <Text className="text-2xl font-bold text-gray-900 dark:text-white mb-4 text-center">
91
- {title}
92
- </Text>
93
- )}
94
- {description && (
95
- <Text className="text-lg text-gray-600 dark:text-gray-300 text-center max-w-lg">
96
- {description}
97
- </Text>
98
- )}
99
- </View>
100
- )}
41
+ <View className={cn('flex-row flex-wrap -m-1', className)}>
42
+ {metrics.map(metric => {
43
+ const content = (
44
+ <Card className='p-4 m-1 flex-1'>
45
+ <View className='flex-row items-start justify-between'>
46
+ <View className='flex-1'>
47
+ <Text className='text-sm text-gray-600 dark:text-gray-400 mb-1'>
48
+ {metric.label}
49
+ </Text>
50
+ <View className='flex-row items-baseline'>
51
+ <Text className='text-2xl font-bold text-gray-900 dark:text-gray-100'>
52
+ {metric.value}
53
+ </Text>
54
+ {metric.unit && (
55
+ <Text className='text-sm text-gray-500 dark:text-gray-500 ml-1'>
56
+ {metric.unit}
57
+ </Text>
58
+ )}
59
+ </View>
60
+ {metric.change !== undefined && (
61
+ <View className='flex-row items-center mt-2'>
62
+ <View
63
+ className={cn(
64
+ 'px-1.5 py-0.5 rounded',
65
+ metric.change >= 0
66
+ ? 'bg-green-100 dark:bg-green-900'
67
+ : 'bg-red-100 dark:bg-red-900'
68
+ )}
69
+ >
70
+ <Text
71
+ className={cn(
72
+ 'text-xs font-medium',
73
+ metric.change >= 0
74
+ ? 'text-green-700 dark:text-green-300'
75
+ : 'text-red-700 dark:text-red-300'
76
+ )}
77
+ >
78
+ {formatChange(metric.change)}
79
+ </Text>
80
+ </View>
81
+ {metric.changeLabel && (
82
+ <Text className='text-xs text-gray-500 dark:text-gray-500 ml-2'>
83
+ {metric.changeLabel}
84
+ </Text>
85
+ )}
86
+ </View>
87
+ )}
88
+ </View>
89
+ {metric.icon && <View className='ml-2'>{metric.icon}</View>}
90
+ </View>
91
+ </Card>
92
+ );
93
+
94
+ if (onMetricPress) {
95
+ return (
96
+ <View key={metric.id} className={columnWidthClass}>
97
+ <Pressable
98
+ onPress={() => onMetricPress(metric)}
99
+ accessibilityRole='button'
100
+ accessibilityLabel={
101
+ metric.label + ': ' + metric.value + (metric.unit || '')
102
+ }
103
+ >
104
+ {content}
105
+ </Pressable>
106
+ </View>
107
+ );
108
+ }
101
109
 
102
- <FlatList
103
- data={metrics}
104
- keyExtractor={(_, index) => index.toString()}
105
- numColumns={columns > 2 ? 2 : columns}
106
- renderItem={({ item }) => (
107
- <View className="flex-1">
108
- <MetricCard metric={item} />
110
+ return (
111
+ <View key={metric.id} className={columnWidthClass}>
112
+ {content}
109
113
  </View>
110
- )}
111
- scrollEnabled={false}
112
- />
114
+ );
115
+ })}
113
116
  </View>
114
117
  );
115
118
  };
119
+
120
+ export default MetricsGrid;