@aiready/components 0.1.21 → 0.1.24

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiready/components",
3
- "version": "0.1.21",
3
+ "version": "0.1.24",
4
4
  "description": "Unified shared components library (UI, charts, hooks, utilities) for AIReady",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -62,8 +62,8 @@
62
62
  "clsx": "^2.1.1",
63
63
  "d3": "^7.9.0",
64
64
  "d3-force": "^3.0.0",
65
- "tailwind-merge": "^2.6.1",
66
- "@aiready/core": "0.9.22"
65
+ "tailwind-merge": "^3.0.0",
66
+ "@aiready/core": "0.9.25"
67
67
  },
68
68
  "devDependencies": {
69
69
  "@testing-library/jest-dom": "^6.6.5",
@@ -74,7 +74,7 @@
74
74
  "tailwindcss": "^4.1.14",
75
75
  "tsup": "^8.3.5",
76
76
  "typescript": "^5.7.2",
77
- "vitest": "^2.1.8"
77
+ "vitest": "^4.0.0"
78
78
  },
79
79
  "scripts": {
80
80
  "dev": "tsup --watch",
@@ -128,8 +128,8 @@ export const ForceDirectedGraph = forwardRef<ForceDirectedGraphHandle, ForceDire
128
128
  const nodes = React.useMemo(() => {
129
129
  if (!initialNodes || !initialNodes.length) return initialNodes;
130
130
 
131
- const cx = width / 2;
132
- const cy = height / 2;
131
+ const centerX = width / 2;
132
+ const centerY = height / 2;
133
133
 
134
134
  // For force layout, use random positions but don't animate
135
135
  if (layout === 'force') {
@@ -145,8 +145,8 @@ export const ForceDirectedGraph = forwardRef<ForceDirectedGraphHandle, ForceDire
145
145
  const radius = Math.min(width, height) * 0.35;
146
146
  return initialNodes.map((n: any, i: number) => ({
147
147
  ...n,
148
- x: cx + Math.cos((2 * Math.PI * i) / initialNodes.length) * radius,
149
- y: cy + Math.sin((2 * Math.PI * i) / initialNodes.length) * radius,
148
+ x: centerX + Math.cos((2 * Math.PI * i) / initialNodes.length) * radius,
149
+ y: centerY + Math.sin((2 * Math.PI * i) / initialNodes.length) * radius,
150
150
  }));
151
151
  }
152
152
 
@@ -188,16 +188,16 @@ export const ForceDirectedGraph = forwardRef<ForceDirectedGraphHandle, ForceDire
188
188
  if (!nodes || nodes.length === 0) return;
189
189
 
190
190
  const applyLayout = () => {
191
- const cx = width / 2;
192
- const cy = height / 2;
191
+ const centerX = width / 2;
192
+ const centerY = height / 2;
193
193
 
194
194
  if (layout === 'circular') {
195
195
  // Place all nodes in a circle
196
196
  const radius = Math.min(width, height) * 0.35;
197
197
  nodes.forEach((node, i) => {
198
198
  const angle = (2 * Math.PI * i) / nodes.length;
199
- node.fx = cx + Math.cos(angle) * radius;
200
- node.fy = cy + Math.sin(angle) * radius;
199
+ node.fx = centerX + Math.cos(angle) * radius;
200
+ node.fy = centerY + Math.sin(angle) * radius;
201
201
  });
202
202
  } else if (layout === 'hierarchical') {
203
203
  // Place packages in rows, files within packages in columns
@@ -0,0 +1,147 @@
1
+ 'use client';
2
+
3
+ import React, { useState, useCallback, useMemo } from 'react';
4
+
5
+ export interface CodeBlockProps {
6
+ children: React.ReactNode;
7
+ language?: string;
8
+ showCopy?: boolean;
9
+ showHeader?: boolean;
10
+ className?: string;
11
+ }
12
+
13
+ // Dedent helper - removes common leading indentation
14
+ function dedentCode(code: string): string {
15
+ // Normalize tabs to two spaces
16
+ let normalized = code.replace(/\t/g, ' ').replace(/[ \t]+$/gm, '');
17
+
18
+ const lines = normalized.split('\n');
19
+ if (lines.length <= 1) return normalized.trim();
20
+
21
+ // Remove leading/trailing empty lines
22
+ let start = 0;
23
+ while (start < lines.length && lines[start].trim() === '') start++;
24
+ let end = lines.length - 1;
25
+ while (end >= 0 && lines[end].trim() === '') end--;
26
+
27
+ if (start > end) return '';
28
+ const relevantLines = lines.slice(start, end + 1);
29
+
30
+ // Find minimum indent across non-empty lines
31
+ const nonEmpty = relevantLines.filter((l) => l.trim() !== '');
32
+ const minIndent = nonEmpty.reduce((min, line) => {
33
+ const m = line.match(/^\s*/)?.[0].length ?? 0;
34
+ return Math.min(min, m);
35
+ }, Infinity);
36
+
37
+ // Remove common indentation
38
+ const dedented = minIndent === Infinity || minIndent === 0
39
+ ? relevantLines.join('\n')
40
+ : relevantLines.map((l) => (l.startsWith(' '.repeat(minIndent)) ? l.slice(minIndent) : l)).join('\n');
41
+
42
+ return dedented;
43
+ }
44
+
45
+ // Simple Copy Button
46
+ function CopyButton({ code }: { code: string }) {
47
+ const [copied, setCopied] = useState(false);
48
+
49
+ const handleCopy = useCallback(async () => {
50
+ try {
51
+ await navigator.clipboard.writeText(code);
52
+ setCopied(true);
53
+ setTimeout(() => setCopied(false), 2000);
54
+ } catch {
55
+ // Fallback for older browsers
56
+ const textarea = document.createElement('textarea');
57
+ textarea.value = code;
58
+ textarea.style.position = 'fixed';
59
+ textarea.style.opacity = '0';
60
+ document.body.appendChild(textarea);
61
+ textarea.select();
62
+ document.execCommand('copy');
63
+ document.body.removeChild(textarea);
64
+ setCopied(true);
65
+ setTimeout(() => setCopied(false), 2000);
66
+ }
67
+ }, [code]);
68
+
69
+ return (
70
+ <button
71
+ onClick={handleCopy}
72
+ className="rounded-md p-1.5 text-slate-400 hover:text-slate-200 hover:bg-slate-700/50 transition-colors"
73
+ title={copied ? 'Copied!' : 'Copy code'}
74
+ >
75
+ {copied ? (
76
+ <svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
77
+ <path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
78
+ </svg>
79
+ ) : (
80
+ <svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
81
+ <path strokeLinecap="round" strokeLinejoin="round" d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184" />
82
+ </svg>
83
+ )}
84
+ </button>
85
+ );
86
+ }
87
+
88
+ export function CodeBlock({
89
+ children,
90
+ language = 'typescript',
91
+ showCopy = true,
92
+ showHeader = true,
93
+ className = '',
94
+ }: CodeBlockProps) {
95
+ // Get code string from children
96
+ const codeString = useMemo(() => {
97
+ if (typeof children === 'string') {
98
+ return dedentCode(children);
99
+ }
100
+ // Handle template literal children
101
+ try {
102
+ const raw = React.Children.toArray(children)
103
+ .map((c) => (typeof c === 'string' ? c : typeof c === 'number' ? String(c) : ''))
104
+ .join('');
105
+ return dedentCode(raw);
106
+ } catch {
107
+ return '';
108
+ }
109
+ }, [children]);
110
+
111
+ return (
112
+ <div className={`group relative my-4 overflow-hidden rounded-xl border border-slate-700 bg-slate-900 shadow-lg ${className}`}>
113
+ {/* Header bar */}
114
+ {showHeader && (
115
+ <div className="flex items-center justify-between border-b border-slate-700 bg-slate-800/50 px-4 py-2">
116
+ <div className="flex items-center gap-2">
117
+ <div className="flex gap-1.5">
118
+ <div className="h-3 w-3 rounded-full bg-red-500/50" />
119
+ <div className="h-3 w-3 rounded-full bg-amber-500/50" />
120
+ <div className="h-3 w-3 rounded-full bg-emerald-500/50" />
121
+ </div>
122
+ <span className="ml-2 text-xs font-semibold uppercase tracking-wider text-slate-500 font-mono">
123
+ {language}
124
+ </span>
125
+ </div>
126
+ {showCopy && <CopyButton code={codeString} />}
127
+ </div>
128
+ )}
129
+
130
+ {/* Code body */}
131
+ <pre className="overflow-x-auto p-4 text-sm leading-relaxed">
132
+ <code className="font-mono block whitespace-pre text-slate-300">
133
+ {codeString}
134
+ </code>
135
+ </pre>
136
+ </div>
137
+ );
138
+ }
139
+
140
+ // Inline code component
141
+ export function InlineCode({ children, className = '' }: { children: React.ReactNode; className?: string }) {
142
+ return (
143
+ <code className={`rounded-md bg-slate-100 px-1.5 py-0.5 text-sm font-mono text-slate-800 ${className}`}>
144
+ {children}
145
+ </code>
146
+ );
147
+ }
@@ -0,0 +1 @@
1
+ export { CodeBlock, InlineCode, type CodeBlockProps } from './CodeBlock';
@@ -0,0 +1,124 @@
1
+ 'use client';
2
+
3
+ import { getRating as getCoreRating } from '@aiready/core/client';
4
+ import { cn } from '../utils/cn';
5
+
6
+ export type ScoreRating = 'excellent' | 'good' | 'fair' | 'needs-work' | 'critical';
7
+
8
+ export interface ScoreBarProps {
9
+ score: number;
10
+ maxScore?: number;
11
+ label: string;
12
+ showScore?: boolean;
13
+ size?: 'sm' | 'md' | 'lg';
14
+ className?: string;
15
+ }
16
+
17
+ const ratingConfig: Record<ScoreRating, { color: string; bgColor: string; label: string }> = {
18
+ excellent: { color: 'bg-green-500', bgColor: 'bg-green-100', label: 'Excellent' },
19
+ good: { color: 'bg-emerald-500', bgColor: 'bg-emerald-100', label: 'Good' },
20
+ fair: { color: 'bg-amber-500', bgColor: 'bg-amber-100', label: 'Fair' },
21
+ 'needs-work': { color: 'bg-orange-500', bgColor: 'bg-orange-100', label: 'Needs Work' },
22
+ critical: { color: 'bg-red-500', bgColor: 'bg-red-100', label: 'Critical' },
23
+ };
24
+
25
+ // Convert Title Case rating from core to lowercase for component use
26
+ function getRating(score: number): ScoreRating {
27
+ const coreRating = getCoreRating(score);
28
+ const ratingMap: Record<string, ScoreRating> = {
29
+ 'Excellent': 'excellent',
30
+ 'Good': 'good',
31
+ 'Fair': 'fair',
32
+ 'Needs Work': 'needs-work',
33
+ 'Critical': 'critical',
34
+ };
35
+ return ratingMap[coreRating] || 'critical';
36
+ }
37
+
38
+ const sizeConfig = {
39
+ sm: { height: 'h-1.5', text: 'text-xs', score: 'text-sm' },
40
+ md: { height: 'h-2', text: 'text-sm', score: 'text-base' },
41
+ lg: { height: 'h-3', text: 'text-base', score: 'text-lg' },
42
+ };
43
+
44
+ export function ScoreBar({
45
+ score,
46
+ maxScore = 100,
47
+ label,
48
+ showScore = true,
49
+ size = 'md',
50
+ className,
51
+ }: ScoreBarProps) {
52
+ const percentage = Math.min(100, Math.max(0, (score / maxScore) * 100));
53
+ const rating = getRating(percentage);
54
+ const config = ratingConfig[rating];
55
+ const sizes = sizeConfig[size];
56
+
57
+ return (
58
+ <div className={cn('space-y-1', className)}>
59
+ <div className="flex items-center justify-between">
60
+ <span className={cn('text-slate-700', sizes.text)}>{label}</span>
61
+ {showScore && (
62
+ <span className={cn('font-bold text-slate-900', sizes.score)}>
63
+ {score}/{maxScore}
64
+ </span>
65
+ )}
66
+ </div>
67
+ <div className={cn('w-full rounded-full bg-slate-200', sizes.height)}>
68
+ <div
69
+ className={cn('rounded-full transition-all duration-500', config.color, sizes.height)}
70
+ style={{ width: `${percentage}%` }}
71
+ />
72
+ </div>
73
+ </div>
74
+ );
75
+ }
76
+
77
+ export interface ScoreCardProps {
78
+ score: number;
79
+ title?: string;
80
+ breakdown?: Array<{
81
+ label: string;
82
+ score: number;
83
+ weight?: number;
84
+ }>;
85
+ className?: string;
86
+ }
87
+
88
+ export function ScoreCard({ score, title, breakdown, className }: ScoreCardProps) {
89
+ const rating = getRating(score);
90
+ const config = ratingConfig[rating];
91
+
92
+ return (
93
+ <div className={cn('rounded-xl border-2 border-slate-200 bg-white p-6 shadow-lg', className)}>
94
+ <div className="mb-4">
95
+ <div className="text-4xl font-black text-slate-900">{score}/100</div>
96
+ <div className={cn('text-lg font-bold', `text-${rating === 'excellent' ? 'green' : rating === 'good' ? 'emerald' : rating === 'fair' ? 'amber' : rating === 'needs-work' ? 'orange' : 'red'}-600`)}>
97
+ {config.label} Rating
98
+ </div>
99
+ {title && <p className="text-sm text-slate-600 mt-1">{title}</p>}
100
+ </div>
101
+
102
+ {breakdown && breakdown.length > 0 && (
103
+ <div className="space-y-3">
104
+ {breakdown.map((item, index) => (
105
+ <ScoreBar
106
+ key={index}
107
+ score={item.score}
108
+ label={item.label}
109
+ size="sm"
110
+ />
111
+ ))}
112
+ </div>
113
+ )}
114
+
115
+ {breakdown && breakdown.length > 0 && (
116
+ <div className="mt-4 text-xs text-slate-600 bg-slate-50 p-3 rounded-lg">
117
+ <strong>Formula:</strong> {breakdown.map(item =>
118
+ `${item.score}×${item.weight || 1}`
119
+ ).join(' + ')} / 100 = {score}
120
+ </div>
121
+ )}
122
+ </div>
123
+ );
124
+ }
@@ -0,0 +1 @@
1
+ export { ScoreBar, ScoreCard, type ScoreBarProps, type ScoreCardProps, type ScoreRating } from './ScoreBar';
@@ -0,0 +1,86 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+
5
+ export interface ErrorDisplayProps {
6
+ title?: string;
7
+ message: string;
8
+ retry?: () => void;
9
+ retryLabel?: string;
10
+ }
11
+
12
+ export function ErrorDisplay({
13
+ title = 'Something went wrong',
14
+ message,
15
+ retry,
16
+ retryLabel = 'Try again',
17
+ }: ErrorDisplayProps) {
18
+ return (
19
+ <div className="flex flex-col items-center justify-center min-h-[200px] gap-4 p-8">
20
+ <div className="rounded-full bg-red-100 p-3">
21
+ <svg
22
+ className="h-6 w-6 text-red-600"
23
+ fill="none"
24
+ viewBox="0 0 24 24"
25
+ strokeWidth="1.5"
26
+ stroke="currentColor"
27
+ >
28
+ <path
29
+ strokeLinecap="round"
30
+ strokeLinejoin="round"
31
+ d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"
32
+ />
33
+ </svg>
34
+ </div>
35
+ <div className="text-center">
36
+ <h3 className="text-lg font-semibold text-slate-900">{title}</h3>
37
+ <p className="mt-2 text-sm text-slate-500">{message}</p>
38
+ </div>
39
+ {retry && (
40
+ <button
41
+ onClick={retry}
42
+ className="mt-2 inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 transition-colors"
43
+ >
44
+ <svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
45
+ <path
46
+ strokeLinecap="round"
47
+ strokeLinejoin="round"
48
+ d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
49
+ />
50
+ </svg>
51
+ {retryLabel}
52
+ </button>
53
+ )}
54
+ </div>
55
+ );
56
+ }
57
+
58
+ export interface EmptyStateProps {
59
+ title: string;
60
+ description?: string;
61
+ icon?: React.ReactNode;
62
+ action?: {
63
+ label: string;
64
+ onClick: () => void;
65
+ };
66
+ }
67
+
68
+ export function EmptyState({ title, description, icon, action }: EmptyStateProps) {
69
+ return (
70
+ <div className="flex flex-col items-center justify-center min-h-[200px] gap-4 p-8">
71
+ {icon && <div className="rounded-full bg-slate-100 p-3">{icon}</div>}
72
+ <div className="text-center">
73
+ <h3 className="text-lg font-semibold text-slate-900">{title}</h3>
74
+ {description && <p className="mt-2 text-sm text-slate-500">{description}</p>}
75
+ </div>
76
+ {action && (
77
+ <button
78
+ onClick={action.onClick}
79
+ className="mt-2 inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 transition-colors"
80
+ >
81
+ {action.label}
82
+ </button>
83
+ )}
84
+ </div>
85
+ );
86
+ }
@@ -0,0 +1,35 @@
1
+ 'use client';
2
+
3
+ export interface LoadingSpinnerProps {
4
+ size?: 'sm' | 'md' | 'lg';
5
+ className?: string;
6
+ }
7
+
8
+ const sizeClasses = {
9
+ sm: 'h-4 w-4',
10
+ md: 'h-8 w-8',
11
+ lg: 'h-12 w-12',
12
+ };
13
+
14
+ export function LoadingSpinner({ size = 'md', className = '' }: LoadingSpinnerProps) {
15
+ return (
16
+ <div className={`flex items-center justify-center ${className}`}>
17
+ <div
18
+ className={`${sizeClasses[size]} animate-spin rounded-full border-2 border-slate-300 border-t-blue-600`}
19
+ />
20
+ </div>
21
+ );
22
+ }
23
+
24
+ export interface LoadingOverlayProps {
25
+ message?: string;
26
+ }
27
+
28
+ export function LoadingOverlay({ message = 'Loading...' }: LoadingOverlayProps) {
29
+ return (
30
+ <div className="flex flex-col items-center justify-center min-h-[200px] gap-4">
31
+ <LoadingSpinner size="lg" />
32
+ <p className="text-sm text-slate-500 animate-pulse">{message}</p>
33
+ </div>
34
+ );
35
+ }
@@ -0,0 +1,2 @@
1
+ export { LoadingSpinner, LoadingOverlay, type LoadingSpinnerProps, type LoadingOverlayProps } from './LoadingSpinner';
2
+ export { ErrorDisplay, EmptyState, type ErrorDisplayProps, type EmptyStateProps } from './ErrorDisplay';
package/src/index.ts CHANGED
@@ -25,10 +25,35 @@ export { Switch, type SwitchProps } from './components/switch';
25
25
  export { Textarea, type TextareaProps } from './components/textarea';
26
26
  export { Select, type SelectProps, type SelectOption } from './components/select';
27
27
 
28
+ // Code Block
29
+ export { CodeBlock, InlineCode, type CodeBlockProps } from './code-block';
30
+
31
+ // Navigation
32
+ export { Breadcrumb, type BreadcrumbProps, type BreadcrumbItem } from './navigation';
33
+
34
+ // Data Display
35
+ export { ScoreBar, ScoreCard, type ScoreBarProps, type ScoreCardProps, type ScoreRating } from './data-display';
36
+
37
+ // Feedback
38
+ export {
39
+ LoadingSpinner,
40
+ LoadingOverlay,
41
+ ErrorDisplay,
42
+ EmptyState,
43
+ type LoadingSpinnerProps,
44
+ type LoadingOverlayProps,
45
+ type ErrorDisplayProps,
46
+ type EmptyStateProps,
47
+ } from './feedback';
48
+
49
+ // Theme
50
+ export { ThemeProvider, useTheme, type Theme, type EffectiveTheme } from './theme';
51
+
28
52
  // Utils
29
53
  export { cn } from './utils/cn';
30
54
  export * from './utils/colors';
31
55
  export * from './utils/formatters';
56
+ export * from './utils/score';
32
57
 
33
58
  // Hooks
34
59
  export { useDebounce } from './hooks/useDebounce';
@@ -0,0 +1,61 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import { cn } from '../utils/cn';
5
+
6
+ export interface BreadcrumbItem {
7
+ label: string;
8
+ href?: string;
9
+ }
10
+
11
+ export interface BreadcrumbProps {
12
+ items: BreadcrumbItem[];
13
+ separator?: React.ReactNode;
14
+ className?: string;
15
+ }
16
+
17
+ const DefaultSeparator = () => (
18
+ <svg
19
+ className="h-4 w-4 text-slate-400"
20
+ fill="none"
21
+ viewBox="0 0 24 24"
22
+ strokeWidth="1.5"
23
+ stroke="currentColor"
24
+ >
25
+ <path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
26
+ </svg>
27
+ );
28
+
29
+ export function Breadcrumb({ items, separator, className }: BreadcrumbProps) {
30
+ const Separator = separator || <DefaultSeparator />;
31
+
32
+ return (
33
+ <nav className={cn('flex items-center gap-1 text-sm', className)} aria-label="Breadcrumb">
34
+ <ol className="flex items-center gap-1">
35
+ {items.map((item, index) => {
36
+ const isLast = index === items.length - 1;
37
+
38
+ return (
39
+ <li key={index} className="flex items-center gap-1">
40
+ {index > 0 && (
41
+ <span className="flex-shrink-0">{Separator}</span>
42
+ )}
43
+ {item.href && !isLast ? (
44
+ <a
45
+ href={item.href}
46
+ className="text-slate-600 hover:text-slate-900 transition-colors"
47
+ >
48
+ {item.label}
49
+ </a>
50
+ ) : (
51
+ <span className={isLast ? 'font-medium text-slate-900' : 'text-slate-600'}>
52
+ {item.label}
53
+ </span>
54
+ )}
55
+ </li>
56
+ );
57
+ })}
58
+ </ol>
59
+ </nav>
60
+ );
61
+ }
@@ -0,0 +1 @@
1
+ export { Breadcrumb, type BreadcrumbProps, type BreadcrumbItem } from './Breadcrumb';