@mg21st/dev-assist 1.0.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.
- package/.eslintrc.json +17 -0
- package/.github/workflows/ci.yml +42 -0
- package/.github/workflows/docs.yml +49 -0
- package/.github/workflows/publish.yml +49 -0
- package/README.md +117 -0
- package/bin/dev-assist.js +4 -0
- package/dev-assist.config.js +10 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +133 -0
- package/dist/cli/wizard.d.ts +5 -0
- package/dist/cli/wizard.d.ts.map +1 -0
- package/dist/cli/wizard.js +66 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +62 -0
- package/dist/generators/docsGenerator.d.ts +15 -0
- package/dist/generators/docsGenerator.d.ts.map +1 -0
- package/dist/generators/docsGenerator.js +186 -0
- package/dist/generators/testGenerator.d.ts +12 -0
- package/dist/generators/testGenerator.d.ts.map +1 -0
- package/dist/generators/testGenerator.js +185 -0
- package/dist/parser/astParser.d.ts +7 -0
- package/dist/parser/astParser.d.ts.map +1 -0
- package/dist/parser/astParser.js +194 -0
- package/dist/server/index.d.ts +5 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +247 -0
- package/dist/shared/types.d.ts +77 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +3 -0
- package/docs/_config.yml +22 -0
- package/docs/api-reference.md +173 -0
- package/docs/architecture.md +90 -0
- package/docs/configuration.md +52 -0
- package/docs/contributing.md +101 -0
- package/docs/index.md +50 -0
- package/docs/installation.md +95 -0
- package/docs/usage.md +107 -0
- package/package.json +58 -0
- package/src/cli/index.ts +108 -0
- package/src/cli/wizard.ts +63 -0
- package/src/config.ts +29 -0
- package/src/generators/docsGenerator.ts +192 -0
- package/src/generators/testGenerator.ts +174 -0
- package/src/parser/astParser.ts +172 -0
- package/src/server/index.ts +238 -0
- package/src/shared/types.ts +83 -0
- package/tsconfig.build.json +8 -0
- package/tsconfig.json +19 -0
- package/ui/index.html +13 -0
- package/ui/package-lock.json +3086 -0
- package/ui/package.json +31 -0
- package/ui/postcss.config.js +6 -0
- package/ui/src/App.tsx +36 -0
- package/ui/src/components/ApiDocsTab.tsx +184 -0
- package/ui/src/components/ApiTestingTab.tsx +363 -0
- package/ui/src/components/Dashboard.tsx +128 -0
- package/ui/src/components/Layout.tsx +76 -0
- package/ui/src/components/TestsTab.tsx +149 -0
- package/ui/src/main.tsx +10 -0
- package/ui/src/styles/index.css +41 -0
- package/ui/tailwind.config.js +20 -0
- package/ui/tsconfig.json +19 -0
- package/ui/vite.config.ts +19 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
import { FileCode2, FunctionSquare, Route, TestTube2, RefreshCw } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
interface Summary {
|
|
6
|
+
totalFiles: number;
|
|
7
|
+
totalFunctions: number;
|
|
8
|
+
totalRoutes: number;
|
|
9
|
+
totalTests: number;
|
|
10
|
+
generatedAt: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface StatCardProps {
|
|
14
|
+
label: string;
|
|
15
|
+
value: number;
|
|
16
|
+
icon: React.ReactNode;
|
|
17
|
+
color: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function StatCard({ label, value, icon, color }: StatCardProps) {
|
|
21
|
+
return (
|
|
22
|
+
<div className="bg-white dark:bg-gray-800 rounded-xl p-5 border border-gray-200 dark:border-gray-700 shadow-sm flex items-center gap-4">
|
|
23
|
+
<div className={`p-3 rounded-lg ${color}`}>
|
|
24
|
+
{icon}
|
|
25
|
+
</div>
|
|
26
|
+
<div>
|
|
27
|
+
<p className="text-2xl font-bold">{value}</p>
|
|
28
|
+
<p className="text-sm text-gray-500 dark:text-gray-400">{label}</p>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function Dashboard() {
|
|
35
|
+
const [summary, setSummary] = useState<Summary | null>(null);
|
|
36
|
+
const [loading, setLoading] = useState(true);
|
|
37
|
+
const [error, setError] = useState<string | null>(null);
|
|
38
|
+
|
|
39
|
+
const fetchSummary = useCallback(async () => {
|
|
40
|
+
try {
|
|
41
|
+
setLoading(true);
|
|
42
|
+
setError(null);
|
|
43
|
+
const { data } = await axios.get<Summary>('/api/summary');
|
|
44
|
+
setSummary(data);
|
|
45
|
+
} catch {
|
|
46
|
+
setError('Failed to fetch project summary. Make sure the server is running.');
|
|
47
|
+
} finally {
|
|
48
|
+
setLoading(false);
|
|
49
|
+
}
|
|
50
|
+
}, []);
|
|
51
|
+
|
|
52
|
+
useEffect(() => { fetchSummary(); }, [fetchSummary]);
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div className="space-y-6">
|
|
56
|
+
<div className="flex items-center justify-between">
|
|
57
|
+
<div>
|
|
58
|
+
<h1 className="text-2xl font-bold">Dashboard</h1>
|
|
59
|
+
<p className="text-gray-500 dark:text-gray-400 text-sm mt-1">Project overview and statistics</p>
|
|
60
|
+
</div>
|
|
61
|
+
<button
|
|
62
|
+
onClick={fetchSummary}
|
|
63
|
+
className="flex items-center gap-2 px-4 py-2 bg-sky-500 text-white rounded-lg hover:bg-sky-600 transition-colors text-sm font-medium"
|
|
64
|
+
>
|
|
65
|
+
<RefreshCw size={15} className={loading ? 'animate-spin' : ''} />
|
|
66
|
+
Refresh
|
|
67
|
+
</button>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
{error && (
|
|
71
|
+
<div className="p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg text-red-700 dark:text-red-400 text-sm">
|
|
72
|
+
{error}
|
|
73
|
+
</div>
|
|
74
|
+
)}
|
|
75
|
+
|
|
76
|
+
{loading && !summary ? (
|
|
77
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
78
|
+
{[...Array(4)].map((_, i) => (
|
|
79
|
+
<div key={i} className="bg-white dark:bg-gray-800 rounded-xl p-5 border border-gray-200 dark:border-gray-700 h-24 animate-pulse" />
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
) : summary ? (
|
|
83
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
84
|
+
<StatCard label="Files Scanned" value={summary.totalFiles} icon={<FileCode2 size={20} className="text-blue-500" />} color="bg-blue-50 dark:bg-blue-900/30" />
|
|
85
|
+
<StatCard label="Functions Found" value={summary.totalFunctions} icon={<FunctionSquare size={20} className="text-purple-500" />} color="bg-purple-50 dark:bg-purple-900/30" />
|
|
86
|
+
<StatCard label="API Routes" value={summary.totalRoutes} icon={<Route size={20} className="text-green-500" />} color="bg-green-50 dark:bg-green-900/30" />
|
|
87
|
+
<StatCard label="Tests Generated" value={summary.totalTests} icon={<TestTube2 size={20} className="text-orange-500" />} color="bg-orange-50 dark:bg-orange-900/30" />
|
|
88
|
+
</div>
|
|
89
|
+
) : null}
|
|
90
|
+
|
|
91
|
+
{summary && (
|
|
92
|
+
<div className="bg-white dark:bg-gray-800 rounded-xl p-5 border border-gray-200 dark:border-gray-700">
|
|
93
|
+
<h2 className="font-semibold mb-3">Project Information</h2>
|
|
94
|
+
<div className="space-y-2 text-sm">
|
|
95
|
+
<div className="flex justify-between">
|
|
96
|
+
<span className="text-gray-500 dark:text-gray-400">Last scanned</span>
|
|
97
|
+
<span>{new Date(summary.generatedAt).toLocaleString()}</span>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
|
|
103
|
+
<div className="bg-white dark:bg-gray-800 rounded-xl p-5 border border-gray-200 dark:border-gray-700">
|
|
104
|
+
<h2 className="font-semibold mb-3">Quick Actions</h2>
|
|
105
|
+
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
|
106
|
+
<button
|
|
107
|
+
onClick={() => axios.post('/api/generate/tests', {}).then(fetchSummary)}
|
|
108
|
+
className="p-3 border-2 border-dashed border-sky-300 dark:border-sky-700 rounded-lg text-sky-600 dark:text-sky-400 hover:border-sky-500 hover:bg-sky-50 dark:hover:bg-sky-900/20 transition-colors text-sm font-medium"
|
|
109
|
+
>
|
|
110
|
+
🧪 Generate Tests
|
|
111
|
+
</button>
|
|
112
|
+
<button
|
|
113
|
+
onClick={() => axios.post('/api/generate/docs', {}).then(fetchSummary)}
|
|
114
|
+
className="p-3 border-2 border-dashed border-green-300 dark:border-green-700 rounded-lg text-green-600 dark:text-green-400 hover:border-green-500 hover:bg-green-50 dark:hover:bg-green-900/20 transition-colors text-sm font-medium"
|
|
115
|
+
>
|
|
116
|
+
📚 Generate API Docs
|
|
117
|
+
</button>
|
|
118
|
+
<button
|
|
119
|
+
onClick={fetchSummary}
|
|
120
|
+
className="p-3 border-2 border-dashed border-purple-300 dark:border-purple-700 rounded-lg text-purple-600 dark:text-purple-400 hover:border-purple-500 hover:bg-purple-50 dark:hover:bg-purple-900/20 transition-colors text-sm font-medium"
|
|
121
|
+
>
|
|
122
|
+
🔄 Re-scan Project
|
|
123
|
+
</button>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { TabId } from '../App';
|
|
3
|
+
import {
|
|
4
|
+
LayoutDashboard,
|
|
5
|
+
TestTube2,
|
|
6
|
+
BookOpen,
|
|
7
|
+
Zap,
|
|
8
|
+
Moon,
|
|
9
|
+
Sun,
|
|
10
|
+
Terminal
|
|
11
|
+
} from 'lucide-react';
|
|
12
|
+
|
|
13
|
+
interface LayoutProps {
|
|
14
|
+
children: React.ReactNode;
|
|
15
|
+
activeTab: TabId;
|
|
16
|
+
onTabChange: (tab: TabId) => void;
|
|
17
|
+
darkMode: boolean;
|
|
18
|
+
onToggleDark: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const tabs = [
|
|
22
|
+
{ id: 'dashboard' as TabId, label: 'Dashboard', icon: LayoutDashboard },
|
|
23
|
+
{ id: 'tests' as TabId, label: 'Tests', icon: TestTube2 },
|
|
24
|
+
{ id: 'docs' as TabId, label: 'API Docs', icon: BookOpen },
|
|
25
|
+
{ id: 'testing' as TabId, label: 'API Testing', icon: Zap },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export function Layout({ children, activeTab, onTabChange, darkMode, onToggleDark }: LayoutProps) {
|
|
29
|
+
return (
|
|
30
|
+
<div className={`min-h-screen flex flex-col ${darkMode ? 'dark bg-gray-900 text-white' : 'bg-gray-50 text-gray-900'}`}>
|
|
31
|
+
<header className={`h-14 flex items-center justify-between px-6 border-b ${darkMode ? 'bg-gray-800 border-gray-700' : 'bg-white border-gray-200'} shadow-sm`}>
|
|
32
|
+
<div className="flex items-center gap-3">
|
|
33
|
+
<Terminal className="text-sky-500" size={22} />
|
|
34
|
+
<span className="font-bold text-lg tracking-tight">DevAssist</span>
|
|
35
|
+
<span className={`text-xs px-2 py-0.5 rounded-full ${darkMode ? 'bg-sky-900 text-sky-300' : 'bg-sky-100 text-sky-700'}`}>v1.0.0</span>
|
|
36
|
+
</div>
|
|
37
|
+
<button
|
|
38
|
+
onClick={onToggleDark}
|
|
39
|
+
className={`p-2 rounded-lg transition-colors ${darkMode ? 'hover:bg-gray-700' : 'hover:bg-gray-100'}`}
|
|
40
|
+
title="Toggle dark mode"
|
|
41
|
+
>
|
|
42
|
+
{darkMode ? <Sun size={18} /> : <Moon size={18} />}
|
|
43
|
+
</button>
|
|
44
|
+
</header>
|
|
45
|
+
|
|
46
|
+
<div className="flex flex-1">
|
|
47
|
+
<aside className={`w-56 flex-shrink-0 border-r ${darkMode ? 'bg-gray-800 border-gray-700' : 'bg-white border-gray-200'}`}>
|
|
48
|
+
<nav className="p-3 space-y-1 pt-4">
|
|
49
|
+
{tabs.map(({ id, label, icon: Icon }) => (
|
|
50
|
+
<button
|
|
51
|
+
key={id}
|
|
52
|
+
onClick={() => onTabChange(id)}
|
|
53
|
+
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all ${
|
|
54
|
+
activeTab === id
|
|
55
|
+
? 'bg-sky-500 text-white shadow-md'
|
|
56
|
+
: darkMode
|
|
57
|
+
? 'text-gray-300 hover:bg-gray-700'
|
|
58
|
+
: 'text-gray-600 hover:bg-gray-100'
|
|
59
|
+
}`}
|
|
60
|
+
>
|
|
61
|
+
<Icon size={17} />
|
|
62
|
+
{label}
|
|
63
|
+
</button>
|
|
64
|
+
))}
|
|
65
|
+
</nav>
|
|
66
|
+
</aside>
|
|
67
|
+
|
|
68
|
+
<main className="flex-1 overflow-auto p-6">
|
|
69
|
+
<div className="fade-in">
|
|
70
|
+
{children}
|
|
71
|
+
</div>
|
|
72
|
+
</main>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
import { TestTube2, Download, RefreshCw, FileCode2, ChevronRight, Loader } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
interface TestFile {
|
|
6
|
+
filePath: string;
|
|
7
|
+
content: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function TestsTab() {
|
|
11
|
+
const [tests, setTests] = useState<TestFile[]>([]);
|
|
12
|
+
const [loading, setLoading] = useState(true);
|
|
13
|
+
const [generating, setGenerating] = useState(false);
|
|
14
|
+
const [selectedTest, setSelectedTest] = useState<TestFile | null>(null);
|
|
15
|
+
const [error, setError] = useState<string | null>(null);
|
|
16
|
+
|
|
17
|
+
const fetchTests = useCallback(async () => {
|
|
18
|
+
try {
|
|
19
|
+
setLoading(true);
|
|
20
|
+
const { data } = await axios.get<TestFile[]>('/api/tests');
|
|
21
|
+
setTests(data);
|
|
22
|
+
if (data.length > 0 && !selectedTest) setSelectedTest(data[0]);
|
|
23
|
+
} catch {
|
|
24
|
+
setError('Failed to fetch tests');
|
|
25
|
+
} finally {
|
|
26
|
+
setLoading(false);
|
|
27
|
+
}
|
|
28
|
+
}, [selectedTest]);
|
|
29
|
+
|
|
30
|
+
const generateTests = async () => {
|
|
31
|
+
try {
|
|
32
|
+
setGenerating(true);
|
|
33
|
+
setError(null);
|
|
34
|
+
await axios.post('/api/generate/tests', {});
|
|
35
|
+
await fetchTests();
|
|
36
|
+
} catch {
|
|
37
|
+
setError('Failed to generate tests');
|
|
38
|
+
} finally {
|
|
39
|
+
setGenerating(false);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const downloadTest = (test: TestFile) => {
|
|
44
|
+
const blob = new Blob([test.content], { type: 'text/plain' });
|
|
45
|
+
const url = URL.createObjectURL(blob);
|
|
46
|
+
const a = document.createElement('a');
|
|
47
|
+
a.href = url;
|
|
48
|
+
a.download = test.filePath.split('/').pop() || 'test.ts';
|
|
49
|
+
a.click();
|
|
50
|
+
URL.revokeObjectURL(url);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
useEffect(() => { fetchTests(); }, [fetchTests]);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className="space-y-4">
|
|
57
|
+
<div className="flex items-center justify-between">
|
|
58
|
+
<div>
|
|
59
|
+
<h1 className="text-2xl font-bold">Generated Tests</h1>
|
|
60
|
+
<p className="text-gray-500 dark:text-gray-400 text-sm mt-1">{tests.length} test files</p>
|
|
61
|
+
</div>
|
|
62
|
+
<div className="flex gap-2">
|
|
63
|
+
<button
|
|
64
|
+
onClick={fetchTests}
|
|
65
|
+
className="flex items-center gap-2 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors text-sm"
|
|
66
|
+
>
|
|
67
|
+
<RefreshCw size={14} />
|
|
68
|
+
Refresh
|
|
69
|
+
</button>
|
|
70
|
+
<button
|
|
71
|
+
onClick={generateTests}
|
|
72
|
+
disabled={generating}
|
|
73
|
+
className="flex items-center gap-2 px-4 py-2 bg-sky-500 text-white rounded-lg hover:bg-sky-600 disabled:opacity-50 transition-colors text-sm font-medium"
|
|
74
|
+
>
|
|
75
|
+
{generating ? <Loader size={14} className="animate-spin" /> : <TestTube2 size={14} />}
|
|
76
|
+
{generating ? 'Generating...' : 'Generate Tests'}
|
|
77
|
+
</button>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
{error && (
|
|
82
|
+
<div className="p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg text-red-700 dark:text-red-400 text-sm">
|
|
83
|
+
{error}
|
|
84
|
+
</div>
|
|
85
|
+
)}
|
|
86
|
+
|
|
87
|
+
{loading ? (
|
|
88
|
+
<div className="flex items-center justify-center h-64">
|
|
89
|
+
<Loader className="animate-spin text-sky-500" size={32} />
|
|
90
|
+
</div>
|
|
91
|
+
) : tests.length === 0 ? (
|
|
92
|
+
<div className="flex flex-col items-center justify-center h-64 text-gray-500 dark:text-gray-400">
|
|
93
|
+
<TestTube2 size={48} className="mb-4 opacity-30" />
|
|
94
|
+
<p className="text-lg font-medium">No tests generated yet</p>
|
|
95
|
+
<p className="text-sm mt-1">Click "Generate Tests" to create test stubs</p>
|
|
96
|
+
</div>
|
|
97
|
+
) : (
|
|
98
|
+
<div className="grid grid-cols-3 gap-4 h-[calc(100vh-200px)]">
|
|
99
|
+
<div className="col-span-1 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 overflow-auto">
|
|
100
|
+
<div className="p-3 border-b border-gray-200 dark:border-gray-700 text-sm font-medium text-gray-500 dark:text-gray-400">
|
|
101
|
+
Test Files
|
|
102
|
+
</div>
|
|
103
|
+
<div className="divide-y divide-gray-100 dark:divide-gray-700">
|
|
104
|
+
{tests.map((test) => (
|
|
105
|
+
<button
|
|
106
|
+
key={test.filePath}
|
|
107
|
+
onClick={() => setSelectedTest(test)}
|
|
108
|
+
className={`w-full flex items-center gap-2 px-3 py-2.5 text-left text-sm transition-colors ${
|
|
109
|
+
selectedTest?.filePath === test.filePath
|
|
110
|
+
? 'bg-sky-50 dark:bg-sky-900/30 text-sky-700 dark:text-sky-300'
|
|
111
|
+
: 'hover:bg-gray-50 dark:hover:bg-gray-700'
|
|
112
|
+
}`}
|
|
113
|
+
>
|
|
114
|
+
<FileCode2 size={14} className="flex-shrink-0" />
|
|
115
|
+
<span className="truncate">{test.filePath.split('/').pop()}</span>
|
|
116
|
+
<ChevronRight size={12} className="ml-auto flex-shrink-0" />
|
|
117
|
+
</button>
|
|
118
|
+
))}
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<div className="col-span-2 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 flex flex-col">
|
|
123
|
+
{selectedTest ? (
|
|
124
|
+
<>
|
|
125
|
+
<div className="p-3 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
|
126
|
+
<span className="text-sm font-medium text-gray-600 dark:text-gray-300">{selectedTest.filePath}</span>
|
|
127
|
+
<button
|
|
128
|
+
onClick={() => downloadTest(selectedTest)}
|
|
129
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
|
130
|
+
>
|
|
131
|
+
<Download size={12} />
|
|
132
|
+
Download
|
|
133
|
+
</button>
|
|
134
|
+
</div>
|
|
135
|
+
<pre className="flex-1 overflow-auto p-4 text-xs font-mono text-gray-800 dark:text-gray-200 scrollbar-thin">
|
|
136
|
+
<code>{selectedTest.content}</code>
|
|
137
|
+
</pre>
|
|
138
|
+
</>
|
|
139
|
+
) : (
|
|
140
|
+
<div className="flex items-center justify-center h-full text-gray-400">
|
|
141
|
+
Select a test file to view
|
|
142
|
+
</div>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
)}
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
}
|
package/ui/src/main.tsx
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
* {
|
|
6
|
+
box-sizing: border-box;
|
|
7
|
+
margin: 0;
|
|
8
|
+
padding: 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
body {
|
|
12
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
|
13
|
+
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
pre, code {
|
|
17
|
+
font-family: 'Consolas', 'Monaco', 'Andale Mono', 'Ubuntu Mono', monospace;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.scrollbar-thin::-webkit-scrollbar {
|
|
21
|
+
width: 6px;
|
|
22
|
+
height: 6px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.scrollbar-thin::-webkit-scrollbar-track {
|
|
26
|
+
background: transparent;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.scrollbar-thin::-webkit-scrollbar-thumb {
|
|
30
|
+
background: #4b5563;
|
|
31
|
+
border-radius: 3px;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.fade-in {
|
|
35
|
+
animation: fadeIn 0.3s ease-in-out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@keyframes fadeIn {
|
|
39
|
+
from { opacity: 0; transform: translateY(4px); }
|
|
40
|
+
to { opacity: 1; transform: translateY(0); }
|
|
41
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** @type {import('tailwindcss').Config} */
|
|
2
|
+
export default {
|
|
3
|
+
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
|
4
|
+
darkMode: 'class',
|
|
5
|
+
theme: {
|
|
6
|
+
extend: {
|
|
7
|
+
colors: {
|
|
8
|
+
primary: {
|
|
9
|
+
50: '#f0f9ff',
|
|
10
|
+
100: '#e0f2fe',
|
|
11
|
+
500: '#0ea5e9',
|
|
12
|
+
600: '#0284c7',
|
|
13
|
+
700: '#0369a1',
|
|
14
|
+
900: '#0c4a6e',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
plugins: [],
|
|
20
|
+
};
|
package/ui/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"moduleResolution": "bundler",
|
|
9
|
+
"allowImportingTsExtensions": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"isolatedModules": true,
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"jsx": "react-jsx",
|
|
14
|
+
"strict": true,
|
|
15
|
+
"noUnusedLocals": false,
|
|
16
|
+
"noUnusedParameters": false
|
|
17
|
+
},
|
|
18
|
+
"include": ["src"]
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
plugins: [react()],
|
|
6
|
+
build: {
|
|
7
|
+
outDir: 'dist',
|
|
8
|
+
emptyOutDir: true,
|
|
9
|
+
},
|
|
10
|
+
server: {
|
|
11
|
+
port: 5173,
|
|
12
|
+
proxy: {
|
|
13
|
+
'/api': {
|
|
14
|
+
target: 'http://localhost:3000',
|
|
15
|
+
changeOrigin: true,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
});
|