@aiready/components 0.13.18 → 0.13.20

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,215 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { ScoreBar, ScoreCard } from '../ScoreBar';
4
+
5
+ // Mock the @aiready/core/client module
6
+ vi.mock('@aiready/core/client', async (importOriginal) => {
7
+ const actual = (await importOriginal()) as any;
8
+ return {
9
+ ...actual,
10
+ getRating: vi.fn((score: number) => {
11
+ if (score >= 80) return 'Excellent';
12
+ if (score >= 60) return 'Good';
13
+ if (score >= 40) return 'Fair';
14
+ if (score >= 20) return 'Needs Work';
15
+ return 'Critical';
16
+ }),
17
+ };
18
+ });
19
+
20
+ describe('ScoreBar', () => {
21
+ beforeEach(() => {
22
+ vi.clearAllMocks();
23
+ });
24
+
25
+ describe('rendering', () => {
26
+ it('should render with default props', () => {
27
+ render(<ScoreBar score={75} label="Test Score" />);
28
+
29
+ expect(screen.getByText('Test Score')).toBeInTheDocument();
30
+ expect(screen.getByText('75/100')).toBeInTheDocument();
31
+ });
32
+
33
+ it('should render without score when showScore is false', () => {
34
+ render(<ScoreBar score={75} label="Test Score" showScore={false} />);
35
+
36
+ expect(screen.getByText('Test Score')).toBeInTheDocument();
37
+ expect(screen.queryByText('75/100')).not.toBeInTheDocument();
38
+ });
39
+
40
+ it('should apply custom className', () => {
41
+ const { container } = render(
42
+ <ScoreBar score={75} label="Test Score" className="custom-class" />
43
+ );
44
+
45
+ expect(container.firstChild).toHaveClass('custom-class');
46
+ });
47
+
48
+ it('should render with different sizes', () => {
49
+ const { container, rerender } = render(
50
+ <ScoreBar score={75} label="Test Score" size="sm" />
51
+ );
52
+
53
+ // Small size should have h-1.5 for the bar
54
+ expect(container.querySelector('.bg-slate-200')).toHaveClass('h-1.5');
55
+
56
+ rerender(<ScoreBar score={75} label="Test Score" size="lg" />);
57
+ expect(container.querySelector('.bg-slate-200')).toHaveClass('h-3');
58
+ });
59
+ });
60
+
61
+ describe('score prop', () => {
62
+ it('should handle score of 0', () => {
63
+ const { container } = render(<ScoreBar score={0} label="Zero Score" />);
64
+
65
+ expect(screen.getByText('0/100')).toBeInTheDocument();
66
+ // Should show critical rating (red)
67
+ expect(container.querySelector('.bg-red-500')).toBeInTheDocument();
68
+ });
69
+
70
+ it('should handle perfect score of 100', () => {
71
+ const { container } = render(
72
+ <ScoreBar score={100} label="Perfect Score" />
73
+ );
74
+
75
+ expect(screen.getByText('100/100')).toBeInTheDocument();
76
+ // Should show excellent rating (green)
77
+ expect(container.querySelector('.bg-green-500')).toBeInTheDocument();
78
+ });
79
+
80
+ it('should handle custom maxScore', () => {
81
+ const { container } = render(
82
+ <ScoreBar score={150} maxScore={200} label="Custom Max" />
83
+ );
84
+
85
+ expect(screen.getByText('150/200')).toBeInTheDocument();
86
+ // 150/200 = 75% which should be good (emerald)
87
+ expect(container.querySelector('.bg-emerald-500')).toBeInTheDocument();
88
+ });
89
+
90
+ it('should clamp bar width to 0-100% range', () => {
91
+ const { container, rerender } = render(
92
+ <ScoreBar score={-10} label="Negative Score" />
93
+ );
94
+
95
+ // The bar width should be clamped to 0%
96
+ const barElement = container.querySelector('[style*="width"]');
97
+ expect(barElement).toHaveStyle('width: 0%');
98
+
99
+ rerender(<ScoreBar score={150} label="Over Max" />);
100
+ // The bar width should be clamped to 100%
101
+ expect(barElement).toHaveStyle('width: 100%');
102
+ });
103
+ });
104
+
105
+ describe('label display', () => {
106
+ it('should render the provided label', () => {
107
+ render(<ScoreBar score={75} label="Custom Label" />);
108
+
109
+ expect(screen.getByText('Custom Label')).toBeInTheDocument();
110
+ });
111
+
112
+ it('should display label in correct size based on size prop', () => {
113
+ const { rerender } = render(
114
+ <ScoreBar score={75} label="Test" size="sm" />
115
+ );
116
+
117
+ expect(screen.getByText('Test')).toHaveClass('text-xs');
118
+
119
+ rerender(<ScoreBar score={75} label="Test" size="lg" />);
120
+ expect(screen.getByText('Test')).toHaveClass('text-base');
121
+ });
122
+ });
123
+
124
+ describe('color based on score', () => {
125
+ it('should show green color for excellent scores (>=80)', () => {
126
+ const { container } = render(<ScoreBar score={85} label="Excellent" />);
127
+
128
+ // Check for green-500 background class
129
+ expect(container.querySelector('.bg-green-500')).toBeInTheDocument();
130
+ });
131
+
132
+ it('should show emerald color for good scores (>=60)', () => {
133
+ const { container } = render(<ScoreBar score={70} label="Good" />);
134
+
135
+ // Check for emerald-500 background class
136
+ expect(container.querySelector('.bg-emerald-500')).toBeInTheDocument();
137
+ });
138
+
139
+ it('should show amber color for fair scores (>=40)', () => {
140
+ const { container } = render(<ScoreBar score={50} label="Fair" />);
141
+
142
+ // Check for amber-500 background class
143
+ expect(container.querySelector('.bg-amber-500')).toBeInTheDocument();
144
+ });
145
+
146
+ it('should show orange color for needs-work scores (>=20)', () => {
147
+ const { container } = render(<ScoreBar score={30} label="Needs Work" />);
148
+
149
+ // Check for orange-500 background class
150
+ expect(container.querySelector('.bg-orange-500')).toBeInTheDocument();
151
+ });
152
+
153
+ it('should show red color for critical scores (<20)', () => {
154
+ const { container } = render(<ScoreBar score={10} label="Critical" />);
155
+
156
+ // Check for red-500 background class
157
+ expect(container.querySelector('.bg-red-500')).toBeInTheDocument();
158
+ });
159
+ });
160
+
161
+ describe('accessibility', () => {
162
+ it('should have proper ARIA structure', () => {
163
+ render(<ScoreBar score={75} label="Accessibility Test" />);
164
+
165
+ // The component should have proper div structure for screen readers
166
+ // The label and score should be visible to screen readers
167
+ expect(screen.getByText('Accessibility Test')).toBeInTheDocument();
168
+ expect(screen.getByText('75/100')).toBeInTheDocument();
169
+ });
170
+ });
171
+ });
172
+
173
+ describe('ScoreCard', () => {
174
+ it('should render with score and rating', () => {
175
+ render(<ScoreCard score={85} title="Test Card" />);
176
+
177
+ expect(screen.getByText('85/100')).toBeInTheDocument();
178
+ expect(screen.getByText('Excellent Rating')).toBeInTheDocument();
179
+ expect(screen.getByText('Test Card')).toBeInTheDocument();
180
+ });
181
+
182
+ it('should render breakdown when provided', () => {
183
+ const breakdown = [
184
+ { label: 'Security', score: 90 },
185
+ { label: 'Performance', score: 80 },
186
+ ];
187
+
188
+ render(<ScoreCard score={85} breakdown={breakdown} />);
189
+
190
+ expect(screen.getByText('Security')).toBeInTheDocument();
191
+ expect(screen.getByText('Performance')).toBeInTheDocument();
192
+ expect(screen.getByText('90/100')).toBeInTheDocument();
193
+ expect(screen.getByText('80/100')).toBeInTheDocument();
194
+ });
195
+
196
+ it('should calculate and display formula when breakdown has weights', () => {
197
+ const breakdown = [
198
+ { label: 'Security', score: 90, weight: 2 },
199
+ { label: 'Performance', score: 80, weight: 1 },
200
+ ];
201
+
202
+ render(<ScoreCard score={87} breakdown={breakdown} />);
203
+
204
+ // Formula: 90×2 + 80×1 / 100 = 87
205
+ expect(screen.getByText('90×2 + 80×1 / 100 = 87')).toBeInTheDocument();
206
+ });
207
+
208
+ it('should apply custom className', () => {
209
+ const { container } = render(
210
+ <ScoreCard score={75} className="custom-class" />
211
+ );
212
+
213
+ expect(container.firstChild).toHaveClass('custom-class');
214
+ });
215
+ });
package/src/index.ts CHANGED
@@ -114,4 +114,7 @@ export {
114
114
  type ForceDirectedGraphHandle,
115
115
  type LayoutType,
116
116
  } from './charts/ForceDirectedGraph';
117
- export { GraphControls, type GraphControlsProps } from './charts/GraphControls';
117
+ export {
118
+ GraphControls,
119
+ type GraphControlsProps,
120
+ } from './charts/force-directed/GraphControls';
@@ -1,28 +1,49 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { scoreColor, scoreBg, scoreLabel, getScoreRating } from '../score';
2
+ import {
3
+ scoreColor,
4
+ scoreBg,
5
+ scoreLabel,
6
+ getScoreRating,
7
+ scoreGlow,
8
+ } from '../score';
3
9
 
4
10
  describe('Score Utilities', () => {
5
11
  it('should return correct color for scores', () => {
12
+ expect(scoreColor(95)).toBe('text-emerald-400');
6
13
  expect(scoreColor(80)).toBe('text-emerald-400');
7
- expect(scoreColor(60)).toBe('text-amber-400');
8
- expect(scoreColor(30)).toBe('text-red-400');
14
+ expect(scoreColor(65)).toBe('text-amber-400');
15
+ expect(scoreColor(45)).toBe('text-red-400');
16
+ expect(scoreColor(20)).toBe('text-red-400');
9
17
  expect(scoreColor(null)).toBe('text-slate-400');
10
18
  });
11
19
 
12
20
  it('should return correct background for scores', () => {
21
+ expect(scoreBg(95)).toContain('emerald');
13
22
  expect(scoreBg(80)).toContain('emerald');
14
- expect(scoreBg(60)).toContain('amber');
15
- expect(scoreBg(30)).toContain('red');
23
+ expect(scoreBg(65)).toContain('amber');
24
+ expect(scoreBg(45)).toContain('red');
25
+ expect(scoreBg(20)).toContain('red');
16
26
  expect(scoreBg(null)).toContain('slate');
17
27
  });
18
28
 
19
29
  it('should return correct labels', () => {
30
+ expect(scoreLabel(95)).toBe('Excellent');
20
31
  expect(scoreLabel(80)).toBe('AI-Ready');
21
- expect(scoreLabel(60)).toBe('Needs Improvement');
22
- expect(scoreLabel(30)).toBe('Critical Issues');
32
+ expect(scoreLabel(65)).toBe('Fair');
33
+ expect(scoreLabel(45)).toBe('Needs Improvement');
34
+ expect(scoreLabel(20)).toBe('Critical Issues');
23
35
  expect(scoreLabel(null)).toBe('Not analyzed');
24
36
  });
25
37
 
38
+ it('should return correct glow for scores', () => {
39
+ expect(scoreGlow(95)).toContain('emerald');
40
+ expect(scoreGlow(80)).toContain('emerald');
41
+ expect(scoreGlow(65)).toContain('amber');
42
+ expect(scoreGlow(45)).toContain('red');
43
+ expect(scoreGlow(20)).toContain('red');
44
+ expect(scoreGlow(null)).toBe('');
45
+ });
46
+
26
47
  it('should return correct rating strings', () => {
27
48
  expect(getScoreRating(95)).toBe('excellent');
28
49
  expect(getScoreRating(80)).toBe('good');
@@ -1,57 +1,95 @@
1
+ import { getRatingSlug } from '@aiready/core/client';
2
+
3
+ type RatingSlug = 'excellent' | 'good' | 'fair' | 'needs-work' | 'critical';
4
+
5
+ interface ScoreMetadata {
6
+ color: string;
7
+ bg: string;
8
+ label: string;
9
+ glow: string;
10
+ }
11
+
12
+ const SCORE_METADATA: Record<RatingSlug, ScoreMetadata> = {
13
+ excellent: {
14
+ color: 'text-emerald-400',
15
+ bg: 'bg-emerald-900/30 border-emerald-500/30',
16
+ label: 'Excellent',
17
+ glow: 'shadow-emerald-500/20',
18
+ },
19
+ good: {
20
+ color: 'text-emerald-400',
21
+ bg: 'bg-emerald-900/30 border-emerald-500/30',
22
+ label: 'AI-Ready',
23
+ glow: 'shadow-emerald-500/20',
24
+ },
25
+ fair: {
26
+ color: 'text-amber-400',
27
+ bg: 'bg-amber-900/30 border-amber-500/30',
28
+ label: 'Fair',
29
+ glow: 'shadow-amber-500/20',
30
+ },
31
+ 'needs-work': {
32
+ color: 'text-red-400',
33
+ bg: 'bg-red-900/30 border-red-500/30',
34
+ label: 'Needs Improvement',
35
+ glow: 'shadow-red-500/20',
36
+ },
37
+ critical: {
38
+ color: 'text-red-400',
39
+ bg: 'bg-red-900/30 border-red-500/30',
40
+ label: 'Critical Issues',
41
+ glow: 'shadow-red-500/20',
42
+ },
43
+ };
44
+
45
+ const DEFAULT_METADATA: ScoreMetadata = {
46
+ color: 'text-slate-400',
47
+ bg: 'bg-slate-800/50 border-slate-700',
48
+ label: 'Not analyzed',
49
+ glow: '',
50
+ };
51
+
1
52
  /**
2
- * Score utility functions for AI readiness scoring
3
- * Provides color, background, glow, and label helpers for score display
53
+ * Get the metadata for a score using core rating system
4
54
  */
55
+ function getMetadata(score: number | null | undefined): ScoreMetadata {
56
+ if (score == null) return DEFAULT_METADATA;
57
+ const rating = getRatingSlug(score) as RatingSlug;
58
+ return SCORE_METADATA[rating] || SCORE_METADATA.critical;
59
+ }
5
60
 
6
61
  /**
7
- * Get the Tailwind color class for a score
62
+ * Get the Tailwind color class for a score using core rating system
8
63
  */
9
64
  export function scoreColor(score: number | null | undefined): string {
10
- if (score == null) return 'text-slate-400';
11
- if (score >= 75) return 'text-emerald-400';
12
- if (score >= 50) return 'text-amber-400';
13
- return 'text-red-400';
65
+ return getMetadata(score).color;
14
66
  }
15
67
 
16
68
  /**
17
- * Get the Tailwind background/border class for a score
69
+ * Get the Tailwind background/border class for a score using core rating system
18
70
  */
19
71
  export function scoreBg(score: number | null | undefined): string {
20
- if (score == null) return 'bg-slate-800/50 border-slate-700';
21
- if (score >= 75) return 'bg-emerald-900/30 border-emerald-500/30';
22
- if (score >= 50) return 'bg-amber-900/30 border-amber-500/30';
23
- return 'bg-red-900/30 border-red-500/30';
72
+ return getMetadata(score).bg;
24
73
  }
25
74
 
26
75
  /**
27
- * Get the display label for a score
76
+ * Get the display label for a score using core rating system
28
77
  */
29
78
  export function scoreLabel(score: number | null | undefined): string {
30
- if (score == null) return 'Not analyzed';
31
- if (score >= 75) return 'AI-Ready';
32
- if (score >= 50) return 'Needs Improvement';
33
- return 'Critical Issues';
79
+ return getMetadata(score).label;
34
80
  }
35
81
 
36
82
  /**
37
- * Get the Tailwind shadow glow class for a score
83
+ * Get the Tailwind shadow glow class for a score using core rating system
38
84
  */
39
85
  export function scoreGlow(score: number | null | undefined): string {
40
- if (score == null) return '';
41
- if (score >= 75) return 'shadow-emerald-500/20';
42
- if (score >= 50) return 'shadow-amber-500/20';
43
- return 'shadow-red-500/20';
86
+ return getMetadata(score).glow;
44
87
  }
45
88
 
46
- import { getRatingSlug } from '@aiready/core/client';
47
-
48
89
  /**
49
90
  * Get rating from score (for use with ScoreBar component)
50
91
  */
51
- export function getScoreRating(
52
- score: number | null | undefined
53
- ): 'excellent' | 'good' | 'fair' | 'needs-work' | 'critical' {
92
+ export function getScoreRating(score: number | null | undefined): RatingSlug {
54
93
  if (score == null) return 'critical';
55
- // Use core implementation to resolve duplication
56
- return getRatingSlug(score) as any;
94
+ return getRatingSlug(score) as RatingSlug;
57
95
  }
@@ -1,24 +0,0 @@
1
- export interface GraphNode {
2
- id: string;
3
- label?: string;
4
- color?: string;
5
- size?: number;
6
- group?: string;
7
- kind?: 'file' | 'package';
8
- packageGroup?: string;
9
- x?: number;
10
- y?: number;
11
- fx?: number | null;
12
- fy?: number | null;
13
- }
14
-
15
- export interface GraphLink {
16
- source: string | GraphNode;
17
- target: string | GraphNode;
18
- color?: string;
19
- width?: number;
20
- label?: string;
21
- type?: string;
22
- }
23
-
24
- export type LayoutType = 'force' | 'hierarchical' | 'circular';