skillverse 0.1.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/.prettierrc +10 -0
- package/README.md +369 -0
- package/client/README.md +73 -0
- package/client/eslint.config.js +23 -0
- package/client/index.html +13 -0
- package/client/package.json +41 -0
- package/client/postcss.config.js +6 -0
- package/client/public/vite.svg +1 -0
- package/client/src/App.css +42 -0
- package/client/src/App.tsx +26 -0
- package/client/src/assets/react.svg +1 -0
- package/client/src/components/AddSkillDialog.tsx +249 -0
- package/client/src/components/Layout.tsx +134 -0
- package/client/src/components/LinkWorkspaceDialog.tsx +196 -0
- package/client/src/components/LoadingSpinner.tsx +57 -0
- package/client/src/components/SkillCard.tsx +269 -0
- package/client/src/components/Toast.tsx +44 -0
- package/client/src/components/Tooltip.tsx +132 -0
- package/client/src/index.css +168 -0
- package/client/src/lib/api.ts +196 -0
- package/client/src/main.tsx +10 -0
- package/client/src/pages/Dashboard.tsx +209 -0
- package/client/src/pages/Marketplace.tsx +282 -0
- package/client/src/pages/Settings.tsx +136 -0
- package/client/src/pages/SkillLibrary.tsx +163 -0
- package/client/src/pages/Workspaces.tsx +662 -0
- package/client/src/stores/appStore.ts +222 -0
- package/client/tailwind.config.js +82 -0
- package/client/tsconfig.app.json +28 -0
- package/client/tsconfig.json +7 -0
- package/client/tsconfig.node.json +26 -0
- package/client/vite.config.ts +26 -0
- package/package.json +34 -0
- package/registry/.env.example +5 -0
- package/registry/Dockerfile +42 -0
- package/registry/docker-compose.yml +33 -0
- package/registry/package.json +37 -0
- package/registry/prisma/schema.prisma +59 -0
- package/registry/src/index.ts +34 -0
- package/registry/src/lib/db.ts +3 -0
- package/registry/src/middleware/errorHandler.ts +35 -0
- package/registry/src/routes/auth.ts +152 -0
- package/registry/src/routes/skills.ts +295 -0
- package/registry/tsconfig.json +23 -0
- package/server/.env.example +11 -0
- package/server/package.json +60 -0
- package/server/prisma/schema.prisma +73 -0
- package/server/public/assets/index-BsYtpZSa.css +1 -0
- package/server/public/assets/index-Dfr_6UV8.js +20 -0
- package/server/public/index.html +14 -0
- package/server/public/vite.svg +1 -0
- package/server/src/bin.ts +428 -0
- package/server/src/config.ts +39 -0
- package/server/src/index.ts +112 -0
- package/server/src/lib/db.ts +14 -0
- package/server/src/middleware/errorHandler.ts +40 -0
- package/server/src/middleware/logger.ts +12 -0
- package/server/src/routes/dashboard.ts +102 -0
- package/server/src/routes/marketplace.ts +273 -0
- package/server/src/routes/skills.ts +294 -0
- package/server/src/routes/workspaces.ts +168 -0
- package/server/src/services/bundleService.ts +123 -0
- package/server/src/services/skillService.ts +722 -0
- package/server/src/services/workspaceService.ts +521 -0
- package/server/src/verify-sync.ts +91 -0
- package/server/tsconfig.json +19 -0
- package/server/tsup.config.ts +18 -0
- package/shared/package.json +21 -0
- package/shared/pnpm-lock.yaml +24 -0
- package/shared/src/index.ts +169 -0
- package/shared/tsconfig.json +10 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { clsx } from 'clsx';
|
|
2
|
+
import { GitBranch, Upload, Link2, ExternalLink, MoreVertical, Trash2, RefreshCw, ArrowUpCircle, FileText, X, Loader2 } from 'lucide-react';
|
|
3
|
+
import type { Skill } from '@skillverse/shared';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import { useAppStore } from '../stores/appStore';
|
|
6
|
+
import { skillsApi } from '../lib/api';
|
|
7
|
+
import Tooltip from './Tooltip';
|
|
8
|
+
|
|
9
|
+
interface SkillCardProps {
|
|
10
|
+
skill: Skill;
|
|
11
|
+
onLink?: () => void;
|
|
12
|
+
onDelete?: () => void;
|
|
13
|
+
onView?: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default function SkillCard({ skill, onLink, onDelete, onView }: SkillCardProps) {
|
|
17
|
+
const [showMenu, setShowMenu] = useState(false);
|
|
18
|
+
const [showSkillMdModal, setShowSkillMdModal] = useState(false);
|
|
19
|
+
const [skillMdContent, setSkillMdContent] = useState<string | null>(null);
|
|
20
|
+
const [skillMdLoading, setSkillMdLoading] = useState(false);
|
|
21
|
+
const { checkUpdate, upgradeSkill, skillUpdates, skillUpdateChecking } = useAppStore();
|
|
22
|
+
|
|
23
|
+
const linkedCount = (skill as any).linkedWorkspaces?.length || 0;
|
|
24
|
+
// Use store state for real-time updates, fallback to persisted state
|
|
25
|
+
const hasUpdate = skillUpdates[skill.id] || skill.updateAvailable;
|
|
26
|
+
const isChecking = skillUpdateChecking[skill.id];
|
|
27
|
+
const isGit = skill.source === 'git';
|
|
28
|
+
|
|
29
|
+
const handleCheckUpdate = async () => {
|
|
30
|
+
setShowMenu(false);
|
|
31
|
+
await checkUpdate(skill.id);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const handleUpgrade = async () => {
|
|
35
|
+
if (confirm(`Are you sure you want to upgrade "${skill.name}"? This will overwrite existing files.`)) {
|
|
36
|
+
await upgradeSkill(skill.id);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const handleViewSkillMd = async () => {
|
|
41
|
+
setShowMenu(false);
|
|
42
|
+
setSkillMdLoading(true);
|
|
43
|
+
setShowSkillMdModal(true);
|
|
44
|
+
try {
|
|
45
|
+
const result = await skillsApi.getSkillMd(skill.id);
|
|
46
|
+
setSkillMdContent(result.exists ? result.content : null);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.error('Failed to load SKILL.md:', err);
|
|
49
|
+
setSkillMdContent(null);
|
|
50
|
+
} finally {
|
|
51
|
+
setSkillMdLoading(false);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<>
|
|
57
|
+
<div className="card group relative h-full flex flex-col">
|
|
58
|
+
{/* Header */}
|
|
59
|
+
<div className="flex items-start justify-between gap-2">
|
|
60
|
+
<div className="flex items-center gap-3 min-w-0 flex-1">
|
|
61
|
+
<div
|
|
62
|
+
className={clsx(
|
|
63
|
+
'w-10 h-10 rounded-xl flex items-center justify-center relative shrink-0',
|
|
64
|
+
skill.source === 'git'
|
|
65
|
+
? 'bg-primary-500/10 text-primary-400'
|
|
66
|
+
: 'bg-accent-500/10 text-accent-400'
|
|
67
|
+
)}
|
|
68
|
+
>
|
|
69
|
+
{skill.source === 'git' ? (
|
|
70
|
+
<GitBranch className="w-5 h-5" />
|
|
71
|
+
) : (
|
|
72
|
+
<Upload className="w-5 h-5" />
|
|
73
|
+
)}
|
|
74
|
+
{hasUpdate && (
|
|
75
|
+
<span className="absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full border-2 border-dark-800" />
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
<div className="min-w-0 flex-1">
|
|
79
|
+
<h3 className="font-semibold text-dark-100 group-hover:text-primary-400 transition-colors flex items-center gap-2">
|
|
80
|
+
<Tooltip content={skill.name} position="top">
|
|
81
|
+
{isGit && (skill.sourceUrl || skill.repoUrl) ? (
|
|
82
|
+
<a
|
|
83
|
+
href={skill.sourceUrl || skill.repoUrl}
|
|
84
|
+
target="_blank"
|
|
85
|
+
rel="noopener noreferrer"
|
|
86
|
+
className="truncate max-w-[160px] sm:max-w-[200px] lg:max-w-[220px] hover:underline cursor-pointer"
|
|
87
|
+
onClick={(e) => e.stopPropagation()}
|
|
88
|
+
>
|
|
89
|
+
{skill.name}
|
|
90
|
+
</a>
|
|
91
|
+
) : (
|
|
92
|
+
<span className="truncate max-w-[160px] sm:max-w-[200px] lg:max-w-[220px]">
|
|
93
|
+
{skill.name}
|
|
94
|
+
</span>
|
|
95
|
+
)}
|
|
96
|
+
</Tooltip>
|
|
97
|
+
{hasUpdate && (
|
|
98
|
+
<span className="text-[10px] bg-red-500/10 text-red-400 px-1.5 py-0.5 rounded uppercase font-bold tracking-wide shrink-0 group-hover:hidden">
|
|
99
|
+
Update
|
|
100
|
+
</span>
|
|
101
|
+
)}
|
|
102
|
+
</h3>
|
|
103
|
+
<p className="text-xs text-dark-500 mt-0.5">
|
|
104
|
+
{skill.source === 'git' ? 'Git Repository' : 'Local Upload'}
|
|
105
|
+
</p>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
{/* Actions Menu */}
|
|
110
|
+
<div className="relative shrink-0">
|
|
111
|
+
<button
|
|
112
|
+
onClick={() => setShowMenu(!showMenu)}
|
|
113
|
+
className="btn-icon opacity-0 group-hover:opacity-100 transition-opacity"
|
|
114
|
+
>
|
|
115
|
+
<MoreVertical className="w-4 h-4" />
|
|
116
|
+
</button>
|
|
117
|
+
|
|
118
|
+
{showMenu && (
|
|
119
|
+
<>
|
|
120
|
+
<div
|
|
121
|
+
className="fixed inset-0 z-10"
|
|
122
|
+
onClick={() => setShowMenu(false)}
|
|
123
|
+
/>
|
|
124
|
+
<div className="absolute right-0 top-full mt-1 w-48 py-1 bg-dark-700 border border-dark-600 rounded-lg shadow-xl z-20 animate-scale-in">
|
|
125
|
+
{/* Preview SKILL.md */}
|
|
126
|
+
<button
|
|
127
|
+
onClick={handleViewSkillMd}
|
|
128
|
+
className="w-full px-3 py-2 text-left text-sm text-dark-200 hover:bg-dark-600 flex items-center gap-2"
|
|
129
|
+
>
|
|
130
|
+
<FileText className="w-4 h-4" />
|
|
131
|
+
View SKILL.md
|
|
132
|
+
</button>
|
|
133
|
+
{onView && (
|
|
134
|
+
<button
|
|
135
|
+
onClick={() => {
|
|
136
|
+
setShowMenu(false);
|
|
137
|
+
onView();
|
|
138
|
+
}}
|
|
139
|
+
className="w-full px-3 py-2 text-left text-sm text-dark-200 hover:bg-dark-600 flex items-center gap-2"
|
|
140
|
+
>
|
|
141
|
+
<ExternalLink className="w-4 h-4" />
|
|
142
|
+
View Details
|
|
143
|
+
</button>
|
|
144
|
+
)}
|
|
145
|
+
{isGit && (
|
|
146
|
+
<button
|
|
147
|
+
onClick={handleCheckUpdate}
|
|
148
|
+
disabled={isChecking}
|
|
149
|
+
className="w-full px-3 py-2 text-left text-sm text-dark-200 hover:bg-dark-600 flex items-center gap-2"
|
|
150
|
+
>
|
|
151
|
+
<RefreshCw className={clsx("w-4 h-4", isChecking && "animate-spin")} />
|
|
152
|
+
{isChecking ? 'Checking...' : 'Check for Updates'}
|
|
153
|
+
</button>
|
|
154
|
+
)}
|
|
155
|
+
{onLink && (
|
|
156
|
+
<button
|
|
157
|
+
onClick={() => {
|
|
158
|
+
setShowMenu(false);
|
|
159
|
+
onLink();
|
|
160
|
+
}}
|
|
161
|
+
className="w-full px-3 py-2 text-left text-sm text-dark-200 hover:bg-dark-600 flex items-center gap-2"
|
|
162
|
+
>
|
|
163
|
+
<Link2 className="w-4 h-4" />
|
|
164
|
+
Link to Workspace
|
|
165
|
+
</button>
|
|
166
|
+
)}
|
|
167
|
+
{onDelete && (
|
|
168
|
+
<button
|
|
169
|
+
onClick={() => {
|
|
170
|
+
setShowMenu(false);
|
|
171
|
+
onDelete();
|
|
172
|
+
}}
|
|
173
|
+
className="w-full px-3 py-2 text-left text-sm text-red-400 hover:bg-dark-600 flex items-center gap-2"
|
|
174
|
+
>
|
|
175
|
+
<Trash2 className="w-4 h-4" />
|
|
176
|
+
Delete
|
|
177
|
+
</button>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
</>
|
|
181
|
+
)}
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
{/* Description with Popover */}
|
|
186
|
+
<div className="mt-3 relative group/desc flex-1">
|
|
187
|
+
{skill.description ? (
|
|
188
|
+
<>
|
|
189
|
+
<Tooltip content={skill.description} position="bottom">
|
|
190
|
+
<p className="text-sm text-dark-400 line-clamp-2 text-justified">
|
|
191
|
+
{skill.description}
|
|
192
|
+
</p>
|
|
193
|
+
</Tooltip>
|
|
194
|
+
</>
|
|
195
|
+
) : (
|
|
196
|
+
<p className="text-sm text-dark-500 italic">No description</p>
|
|
197
|
+
)}
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
{/* Upgrade Button - pushed down if needed, but flex-col handles it */}
|
|
201
|
+
{hasUpdate && (
|
|
202
|
+
<div className="mt-4">
|
|
203
|
+
<button
|
|
204
|
+
onClick={handleUpgrade}
|
|
205
|
+
className="w-full btn-primary py-1.5 text-xs flex items-center justify-center gap-2"
|
|
206
|
+
>
|
|
207
|
+
<ArrowUpCircle className="w-3.5 h-3.5" />
|
|
208
|
+
Upgrade to Latest Version
|
|
209
|
+
</button>
|
|
210
|
+
</div>
|
|
211
|
+
)}
|
|
212
|
+
|
|
213
|
+
{/* Footer */}
|
|
214
|
+
<div className={clsx("pt-4 border-t border-dark-700 flex items-center justify-between mt-4")}>
|
|
215
|
+
<div className="flex items-center gap-2 text-xs text-dark-500">
|
|
216
|
+
<Link2 className="w-3.5 h-3.5" />
|
|
217
|
+
<span>{linkedCount} workspace{linkedCount !== 1 ? 's' : ''}</span>
|
|
218
|
+
</div>
|
|
219
|
+
<div className="text-xs text-dark-500">
|
|
220
|
+
{new Date(skill.installDate).toLocaleDateString()}
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
{/* SKILL.md Preview Modal */}
|
|
226
|
+
{showSkillMdModal && (
|
|
227
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center p-2 sm:p-4">
|
|
228
|
+
<div
|
|
229
|
+
className="absolute inset-0 bg-dark-950/80 backdrop-blur-sm"
|
|
230
|
+
onClick={() => setShowSkillMdModal(false)}
|
|
231
|
+
/>
|
|
232
|
+
<div className="relative w-full max-w-2xl bg-dark-800 border border-dark-700 rounded-2xl shadow-2xl animate-scale-in max-h-[85vh] flex flex-col">
|
|
233
|
+
<div className="flex items-center justify-between px-4 sm:px-6 py-4 border-b border-dark-700 shrink-0">
|
|
234
|
+
<div className="flex items-center gap-3">
|
|
235
|
+
<FileText className="w-5 h-5 text-primary-400" />
|
|
236
|
+
<div>
|
|
237
|
+
<h2 className="text-lg font-semibold text-dark-100">SKILL.md</h2>
|
|
238
|
+
<p className="text-xs text-dark-500">{skill.name}</p>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
<button onClick={() => setShowSkillMdModal(false)} className="btn-icon">
|
|
242
|
+
<X className="w-5 h-5" />
|
|
243
|
+
</button>
|
|
244
|
+
</div>
|
|
245
|
+
<div className="flex-1 overflow-y-auto p-4 sm:p-6">
|
|
246
|
+
{skillMdLoading ? (
|
|
247
|
+
<div className="flex items-center justify-center py-12">
|
|
248
|
+
<Loader2 className="w-6 h-6 animate-spin text-primary-400" />
|
|
249
|
+
</div>
|
|
250
|
+
) : skillMdContent ? (
|
|
251
|
+
<pre className="text-sm text-dark-200 whitespace-pre-wrap font-mono bg-dark-750 rounded-lg p-4 overflow-x-auto">
|
|
252
|
+
{skillMdContent}
|
|
253
|
+
</pre>
|
|
254
|
+
) : (
|
|
255
|
+
<div className="text-center py-12">
|
|
256
|
+
<FileText className="w-12 h-12 mx-auto text-dark-600 mb-3" />
|
|
257
|
+
<p className="text-dark-400">No SKILL.md found</p>
|
|
258
|
+
<p className="text-sm text-dark-500 mt-1">
|
|
259
|
+
This skill doesn't have a SKILL.md file
|
|
260
|
+
</p>
|
|
261
|
+
</div>
|
|
262
|
+
)}
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
)}
|
|
267
|
+
</>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useAppStore } from '../stores/appStore';
|
|
2
|
+
import { CheckCircle, XCircle, Info, X } from 'lucide-react';
|
|
3
|
+
import { clsx } from 'clsx';
|
|
4
|
+
|
|
5
|
+
export default function Toast() {
|
|
6
|
+
const { toast, hideToast } = useAppStore();
|
|
7
|
+
|
|
8
|
+
if (!toast) return null;
|
|
9
|
+
|
|
10
|
+
const icons = {
|
|
11
|
+
success: CheckCircle,
|
|
12
|
+
error: XCircle,
|
|
13
|
+
info: Info,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const colors = {
|
|
17
|
+
success: 'from-green-500/20 to-green-500/5 border-green-500/30 text-green-400',
|
|
18
|
+
error: 'from-red-500/20 to-red-500/5 border-red-500/30 text-red-400',
|
|
19
|
+
info: 'from-primary-500/20 to-primary-500/5 border-primary-500/30 text-primary-400',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const Icon = icons[toast.type];
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className="fixed bottom-6 right-6 z-50 animate-slide-in">
|
|
26
|
+
<div
|
|
27
|
+
className={clsx(
|
|
28
|
+
'flex items-center gap-3 px-4 py-3 rounded-xl border',
|
|
29
|
+
'bg-gradient-to-r backdrop-blur-lg shadow-lg',
|
|
30
|
+
colors[toast.type]
|
|
31
|
+
)}
|
|
32
|
+
>
|
|
33
|
+
<Icon className="w-5 h-5 flex-shrink-0" />
|
|
34
|
+
<span className="text-sm font-medium text-dark-100">{toast.message}</span>
|
|
35
|
+
<button
|
|
36
|
+
onClick={hideToast}
|
|
37
|
+
className="ml-2 p-1 rounded-lg hover:bg-dark-700/50 transition-colors"
|
|
38
|
+
>
|
|
39
|
+
<X className="w-4 h-4" />
|
|
40
|
+
</button>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { clsx } from 'clsx';
|
|
2
|
+
import { useState, useRef, useEffect, type ReactNode } from 'react';
|
|
3
|
+
import { createPortal } from 'react-dom';
|
|
4
|
+
|
|
5
|
+
interface TooltipProps {
|
|
6
|
+
content: string;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
position?: 'top' | 'bottom' | 'left' | 'right';
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Reusable Tooltip component using Portal to avoid overflow clipping
|
|
14
|
+
*/
|
|
15
|
+
export default function Tooltip({
|
|
16
|
+
content,
|
|
17
|
+
children,
|
|
18
|
+
position = 'top',
|
|
19
|
+
className,
|
|
20
|
+
}: TooltipProps) {
|
|
21
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
22
|
+
const [coords, setCoords] = useState({ top: 0, left: 0 });
|
|
23
|
+
const triggerRef = useRef<HTMLDivElement>(null);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (isVisible && triggerRef.current) {
|
|
27
|
+
const rect = triggerRef.current.getBoundingClientRect();
|
|
28
|
+
const tooltipPadding = 8;
|
|
29
|
+
|
|
30
|
+
let top = 0;
|
|
31
|
+
let left = 0;
|
|
32
|
+
|
|
33
|
+
switch (position) {
|
|
34
|
+
case 'top':
|
|
35
|
+
top = rect.top - tooltipPadding;
|
|
36
|
+
left = rect.left + rect.width / 2;
|
|
37
|
+
break;
|
|
38
|
+
case 'bottom':
|
|
39
|
+
top = rect.bottom + tooltipPadding;
|
|
40
|
+
left = rect.left + rect.width / 2;
|
|
41
|
+
break;
|
|
42
|
+
case 'left':
|
|
43
|
+
top = rect.top + rect.height / 2;
|
|
44
|
+
left = rect.left - tooltipPadding;
|
|
45
|
+
break;
|
|
46
|
+
case 'right':
|
|
47
|
+
top = rect.top + rect.height / 2;
|
|
48
|
+
left = rect.right + tooltipPadding;
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
setCoords({ top, left });
|
|
53
|
+
}
|
|
54
|
+
}, [isVisible, position]);
|
|
55
|
+
|
|
56
|
+
if (!content) {
|
|
57
|
+
return <>{children}</>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const tooltipContent = isVisible && createPortal(
|
|
61
|
+
<div
|
|
62
|
+
className={clsx(
|
|
63
|
+
'fixed z-[9999] px-2.5 py-1.5 text-xs rounded-lg',
|
|
64
|
+
'bg-dark-700 text-dark-100 border border-dark-600 shadow-xl',
|
|
65
|
+
'animate-fade-in pointer-events-none'
|
|
66
|
+
)}
|
|
67
|
+
style={{
|
|
68
|
+
top: position === 'top' ? coords.top : position === 'bottom' ? coords.top : coords.top,
|
|
69
|
+
left: coords.left,
|
|
70
|
+
transform: position === 'top'
|
|
71
|
+
? 'translate(-50%, -100%)'
|
|
72
|
+
: position === 'bottom'
|
|
73
|
+
? 'translate(-50%, 0)'
|
|
74
|
+
: position === 'left'
|
|
75
|
+
? 'translate(-100%, -50%)'
|
|
76
|
+
: 'translate(0, -50%)',
|
|
77
|
+
maxWidth: '320px',
|
|
78
|
+
wordBreak: 'break-all',
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
{content}
|
|
82
|
+
</div>,
|
|
83
|
+
document.body
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<>
|
|
88
|
+
<div
|
|
89
|
+
ref={triggerRef}
|
|
90
|
+
className={clsx('inline-flex', className)}
|
|
91
|
+
onMouseEnter={() => setIsVisible(true)}
|
|
92
|
+
onMouseLeave={() => setIsVisible(false)}
|
|
93
|
+
>
|
|
94
|
+
{children}
|
|
95
|
+
</div>
|
|
96
|
+
{tooltipContent}
|
|
97
|
+
</>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* TruncatedText component - shows text with ellipsis and tooltip on hover
|
|
103
|
+
*/
|
|
104
|
+
interface TruncatedTextProps {
|
|
105
|
+
text: string;
|
|
106
|
+
className?: string;
|
|
107
|
+
maxWidth?: string;
|
|
108
|
+
lines?: number;
|
|
109
|
+
tooltipPosition?: 'top' | 'bottom' | 'left' | 'right';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function TruncatedText({
|
|
113
|
+
text,
|
|
114
|
+
className,
|
|
115
|
+
maxWidth = '100%',
|
|
116
|
+
lines = 1,
|
|
117
|
+
tooltipPosition = 'top',
|
|
118
|
+
}: TruncatedTextProps) {
|
|
119
|
+
return (
|
|
120
|
+
<Tooltip content={text} position={tooltipPosition}>
|
|
121
|
+
<span
|
|
122
|
+
className={clsx(
|
|
123
|
+
lines === 1 ? 'truncate' : `line-clamp-${lines}`,
|
|
124
|
+
className
|
|
125
|
+
)}
|
|
126
|
+
style={{ maxWidth, display: 'inline-block' }}
|
|
127
|
+
>
|
|
128
|
+
{text}
|
|
129
|
+
</span>
|
|
130
|
+
</Tooltip>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
/* Base styles */
|
|
6
|
+
:root {
|
|
7
|
+
--color-bg: #0f172a;
|
|
8
|
+
--color-surface: #1e293b;
|
|
9
|
+
--color-surface-hover: #334155;
|
|
10
|
+
--color-border: #334155;
|
|
11
|
+
--color-text: #f8fafc;
|
|
12
|
+
--color-text-muted: #94a3b8;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
* {
|
|
16
|
+
box-sizing: border-box;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
body {
|
|
20
|
+
margin: 0;
|
|
21
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
22
|
+
background-color: var(--color-bg);
|
|
23
|
+
color: var(--color-text);
|
|
24
|
+
-webkit-font-smoothing: antialiased;
|
|
25
|
+
-moz-osx-font-smoothing: grayscale;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* Custom scrollbar */
|
|
29
|
+
::-webkit-scrollbar {
|
|
30
|
+
width: 8px;
|
|
31
|
+
height: 8px;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
::-webkit-scrollbar-track {
|
|
35
|
+
background: var(--color-surface);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
::-webkit-scrollbar-thumb {
|
|
39
|
+
background: var(--color-border);
|
|
40
|
+
border-radius: 4px;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
::-webkit-scrollbar-thumb:hover {
|
|
44
|
+
background: #475569;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* Glass effect */
|
|
48
|
+
@layer utilities {
|
|
49
|
+
.glass {
|
|
50
|
+
background: rgba(30, 41, 59, 0.8);
|
|
51
|
+
backdrop-filter: blur(12px);
|
|
52
|
+
border: 1px solid rgba(51, 65, 85, 0.5);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.glass-light {
|
|
56
|
+
background: rgba(248, 250, 252, 0.05);
|
|
57
|
+
backdrop-filter: blur(8px);
|
|
58
|
+
border: 1px solid rgba(148, 163, 184, 0.1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* Button base */
|
|
63
|
+
@layer components {
|
|
64
|
+
.btn {
|
|
65
|
+
@apply px-4 py-2 rounded-lg font-medium transition-all duration-200 inline-flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.btn-primary {
|
|
69
|
+
@apply btn bg-gradient-to-r from-primary-500 to-primary-600 text-white hover:from-primary-600 hover:to-primary-700 shadow-lg hover:shadow-glow active:scale-[0.98];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.btn-secondary {
|
|
73
|
+
@apply btn bg-dark-700 text-dark-100 border border-dark-600 hover:bg-dark-600 hover:border-dark-500;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.btn-ghost {
|
|
77
|
+
@apply btn text-dark-300 hover:text-dark-100 hover:bg-dark-700;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.btn-danger {
|
|
81
|
+
@apply btn bg-red-500/10 text-red-400 border border-red-500/30 hover:bg-red-500/20 hover:border-red-500/50;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.btn-icon {
|
|
85
|
+
@apply p-2 rounded-lg transition-colors duration-200 text-dark-400 hover:text-dark-100 hover:bg-dark-700;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* Input styles */
|
|
89
|
+
.input {
|
|
90
|
+
@apply w-full px-4 py-2.5 rounded-lg bg-dark-800 border border-dark-600 text-dark-100 placeholder-dark-400 focus:outline-none focus:border-primary-500 focus:ring-1 focus:ring-primary-500/50 transition-all duration-200;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* Card styles */
|
|
94
|
+
.card {
|
|
95
|
+
@apply bg-dark-800 border border-dark-700 rounded-xl p-6 transition-all duration-200 hover:border-dark-600;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.card-interactive {
|
|
99
|
+
@apply card cursor-pointer hover:bg-dark-750 hover:shadow-lg;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* Text utilities */
|
|
104
|
+
@layer utilities {
|
|
105
|
+
|
|
106
|
+
/* Multi-line text justified alignment for clean edges */
|
|
107
|
+
.text-justified {
|
|
108
|
+
text-align: justify;
|
|
109
|
+
text-justify: inter-word;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* Tooltip component styles */
|
|
114
|
+
.tooltip-wrapper {
|
|
115
|
+
@apply relative inline-flex;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.tooltip-wrapper .tooltip-content {
|
|
119
|
+
@apply absolute z-50 px-2.5 py-1.5 text-xs rounded-lg bg-dark-700 text-dark-100 border border-dark-600 opacity-0 invisible transition-all duration-200 pointer-events-none shadow-xl;
|
|
120
|
+
max-width: 320px;
|
|
121
|
+
width: max-content;
|
|
122
|
+
white-space: normal;
|
|
123
|
+
word-wrap: break-word;
|
|
124
|
+
word-break: break-all;
|
|
125
|
+
transform: translateY(4px);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.tooltip-wrapper:hover .tooltip-content {
|
|
129
|
+
@apply opacity-100 visible;
|
|
130
|
+
transform: translateY(0);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* Tooltip positions */
|
|
134
|
+
.tooltip-content.tooltip-top {
|
|
135
|
+
@apply bottom-full left-1/2 mb-2;
|
|
136
|
+
transform: translateX(-50%) translateY(-4px);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.tooltip-wrapper:hover .tooltip-content.tooltip-top {
|
|
140
|
+
transform: translateX(-50%) translateY(0);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.tooltip-content.tooltip-bottom {
|
|
144
|
+
@apply top-full left-1/2 mt-2;
|
|
145
|
+
transform: translateX(-50%) translateY(4px);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.tooltip-wrapper:hover .tooltip-content.tooltip-bottom {
|
|
149
|
+
transform: translateX(-50%) translateY(0);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.tooltip-content.tooltip-left {
|
|
153
|
+
@apply right-full top-1/2 mr-2;
|
|
154
|
+
transform: translateY(-50%) translateX(4px);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.tooltip-wrapper:hover .tooltip-content.tooltip-left {
|
|
158
|
+
transform: translateY(-50%) translateX(0);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.tooltip-content.tooltip-right {
|
|
162
|
+
@apply left-full top-1/2 ml-2;
|
|
163
|
+
transform: translateY(-50%) translateX(-4px);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.tooltip-wrapper:hover .tooltip-content.tooltip-right {
|
|
167
|
+
transform: translateY(-50%) translateX(0);
|
|
168
|
+
}
|