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.
Files changed (72) hide show
  1. package/.prettierrc +10 -0
  2. package/README.md +369 -0
  3. package/client/README.md +73 -0
  4. package/client/eslint.config.js +23 -0
  5. package/client/index.html +13 -0
  6. package/client/package.json +41 -0
  7. package/client/postcss.config.js +6 -0
  8. package/client/public/vite.svg +1 -0
  9. package/client/src/App.css +42 -0
  10. package/client/src/App.tsx +26 -0
  11. package/client/src/assets/react.svg +1 -0
  12. package/client/src/components/AddSkillDialog.tsx +249 -0
  13. package/client/src/components/Layout.tsx +134 -0
  14. package/client/src/components/LinkWorkspaceDialog.tsx +196 -0
  15. package/client/src/components/LoadingSpinner.tsx +57 -0
  16. package/client/src/components/SkillCard.tsx +269 -0
  17. package/client/src/components/Toast.tsx +44 -0
  18. package/client/src/components/Tooltip.tsx +132 -0
  19. package/client/src/index.css +168 -0
  20. package/client/src/lib/api.ts +196 -0
  21. package/client/src/main.tsx +10 -0
  22. package/client/src/pages/Dashboard.tsx +209 -0
  23. package/client/src/pages/Marketplace.tsx +282 -0
  24. package/client/src/pages/Settings.tsx +136 -0
  25. package/client/src/pages/SkillLibrary.tsx +163 -0
  26. package/client/src/pages/Workspaces.tsx +662 -0
  27. package/client/src/stores/appStore.ts +222 -0
  28. package/client/tailwind.config.js +82 -0
  29. package/client/tsconfig.app.json +28 -0
  30. package/client/tsconfig.json +7 -0
  31. package/client/tsconfig.node.json +26 -0
  32. package/client/vite.config.ts +26 -0
  33. package/package.json +34 -0
  34. package/registry/.env.example +5 -0
  35. package/registry/Dockerfile +42 -0
  36. package/registry/docker-compose.yml +33 -0
  37. package/registry/package.json +37 -0
  38. package/registry/prisma/schema.prisma +59 -0
  39. package/registry/src/index.ts +34 -0
  40. package/registry/src/lib/db.ts +3 -0
  41. package/registry/src/middleware/errorHandler.ts +35 -0
  42. package/registry/src/routes/auth.ts +152 -0
  43. package/registry/src/routes/skills.ts +295 -0
  44. package/registry/tsconfig.json +23 -0
  45. package/server/.env.example +11 -0
  46. package/server/package.json +60 -0
  47. package/server/prisma/schema.prisma +73 -0
  48. package/server/public/assets/index-BsYtpZSa.css +1 -0
  49. package/server/public/assets/index-Dfr_6UV8.js +20 -0
  50. package/server/public/index.html +14 -0
  51. package/server/public/vite.svg +1 -0
  52. package/server/src/bin.ts +428 -0
  53. package/server/src/config.ts +39 -0
  54. package/server/src/index.ts +112 -0
  55. package/server/src/lib/db.ts +14 -0
  56. package/server/src/middleware/errorHandler.ts +40 -0
  57. package/server/src/middleware/logger.ts +12 -0
  58. package/server/src/routes/dashboard.ts +102 -0
  59. package/server/src/routes/marketplace.ts +273 -0
  60. package/server/src/routes/skills.ts +294 -0
  61. package/server/src/routes/workspaces.ts +168 -0
  62. package/server/src/services/bundleService.ts +123 -0
  63. package/server/src/services/skillService.ts +722 -0
  64. package/server/src/services/workspaceService.ts +521 -0
  65. package/server/src/verify-sync.ts +91 -0
  66. package/server/tsconfig.json +19 -0
  67. package/server/tsup.config.ts +18 -0
  68. package/shared/package.json +21 -0
  69. package/shared/pnpm-lock.yaml +24 -0
  70. package/shared/src/index.ts +169 -0
  71. package/shared/tsconfig.json +10 -0
  72. 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
+ }