flashts 1.0.0 → 1.0.2
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/CHANGELOG.md +25 -0
- package/CONTRIBUTING.md +60 -0
- package/README.md +36 -66
- package/bin/cli.ts +2 -2
- package/client/dist/assets/index-BAlIRq-u.css +1 -0
- package/client/dist/assets/index-DM0EVhuB.js +21 -0
- package/client/{index.html → dist/index.html} +2 -1
- package/package.json +76 -66
- package/server/index.ts +11 -9
- package/client/README.md +0 -73
- package/client/bun.lock +0 -613
- package/client/eslint.config.js +0 -23
- package/client/package-lock.json +0 -3174
- package/client/package.json +0 -43
- package/client/postcss.config.js +0 -6
- package/client/src/App.css +0 -42
- package/client/src/App.tsx +0 -247
- package/client/src/assets/react.svg +0 -1
- package/client/src/components/CodeEditor.tsx +0 -206
- package/client/src/components/Console.tsx +0 -52
- package/client/src/components/FileTabs.tsx +0 -195
- package/client/src/components/LightningLogo.tsx +0 -33
- package/client/src/components/PackageInstaller.tsx +0 -300
- package/client/src/components/WelcomeScreen.tsx +0 -171
- package/client/src/index.css +0 -142
- package/client/src/main.tsx +0 -10
- package/client/tailwind.config.js +0 -32
- package/client/tsconfig.app.json +0 -28
- package/client/tsconfig.json +0 -7
- package/client/tsconfig.node.json +0 -26
- package/client/vite.config.ts +0 -7
- package/tmp/e656e349-2c39-4bc3-b334-6b5eb957e306/main.ts +0 -2
- /package/client/{public → dist}/favicon.svg +0 -0
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
import { Plus, X, FileCode, FileJson, PlusCircle } from "lucide-react";
|
|
2
|
-
import { useState, useRef, useEffect } from "react";
|
|
3
|
-
import { createPortal } from "react-dom";
|
|
4
|
-
import clsx from "clsx";
|
|
5
|
-
|
|
6
|
-
export interface ProjectFile {
|
|
7
|
-
id: string;
|
|
8
|
-
name: string;
|
|
9
|
-
content: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
interface FileTabsProps {
|
|
13
|
-
files: ProjectFile[];
|
|
14
|
-
activeFileId: string;
|
|
15
|
-
onSelect: (id: string) => void;
|
|
16
|
-
onAdd: (name: string) => void;
|
|
17
|
-
onClose: (id: string) => void;
|
|
18
|
-
onRename: (id: string, newName: string) => void;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function FileTabs({ files, activeFileId, onSelect, onAdd, onClose, onRename }: FileTabsProps) {
|
|
22
|
-
const [editingId, setEditingId] = useState<string | null>(null);
|
|
23
|
-
const [editValue, setEditValue] = useState("");
|
|
24
|
-
const [isAdding, setIsAdding] = useState(false);
|
|
25
|
-
const [newName, setNewName] = useState("");
|
|
26
|
-
|
|
27
|
-
const inputRef = useRef<HTMLInputElement>(null);
|
|
28
|
-
const addInputRef = useRef<HTMLInputElement>(null);
|
|
29
|
-
const containerRef = useRef<HTMLDivElement>(null);
|
|
30
|
-
|
|
31
|
-
const activeFile = files.find(f => f.id === activeFileId);
|
|
32
|
-
const activeLanguage = activeFile?.name.endsWith('.ts') || activeFile?.name.endsWith('.tsx') ? 'TypeScript' : 'JavaScript';
|
|
33
|
-
|
|
34
|
-
const handleStartRename = (e: React.MouseEvent, file: ProjectFile) => {
|
|
35
|
-
e.stopPropagation();
|
|
36
|
-
setEditingId(file.id);
|
|
37
|
-
setEditValue(file.name);
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
const handleConfirmRename = () => {
|
|
41
|
-
if (editingId && editValue.trim()) {
|
|
42
|
-
let validatedName = editValue.trim();
|
|
43
|
-
if (!validatedName.includes('.')) validatedName += '.ts';
|
|
44
|
-
onRename(editingId, validatedName);
|
|
45
|
-
}
|
|
46
|
-
setEditingId(null);
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
const handleConfirmAdd = () => {
|
|
50
|
-
if (newName.trim()) {
|
|
51
|
-
let validatedName = newName.trim();
|
|
52
|
-
if (!validatedName.includes('.')) validatedName += '.ts';
|
|
53
|
-
onAdd(validatedName);
|
|
54
|
-
}
|
|
55
|
-
setIsAdding(false);
|
|
56
|
-
setNewName("");
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const handleKeyDown = (e: React.KeyboardEvent, type: 'rename' | 'add') => {
|
|
60
|
-
if (e.key === 'Enter') type === 'rename' ? handleConfirmRename() : handleConfirmAdd();
|
|
61
|
-
if (e.key === 'Escape') type === 'rename' ? setEditingId(null) : setIsAdding(false);
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
useEffect(() => {
|
|
65
|
-
if (editingId && inputRef.current) {
|
|
66
|
-
inputRef.current.focus();
|
|
67
|
-
inputRef.current.select();
|
|
68
|
-
}
|
|
69
|
-
}, [editingId]);
|
|
70
|
-
|
|
71
|
-
useEffect(() => {
|
|
72
|
-
if (isAdding && addInputRef.current) {
|
|
73
|
-
addInputRef.current.focus();
|
|
74
|
-
}
|
|
75
|
-
}, [isAdding]);
|
|
76
|
-
|
|
77
|
-
return (
|
|
78
|
-
<div className="flex items-center bg-bg-secondary w-full border-b border-border-color h-11 group/tabs overflow-hidden">
|
|
79
|
-
<div
|
|
80
|
-
ref={containerRef}
|
|
81
|
-
className="flex items-center overflow-x-auto no-scrollbar flex-1 h-full scroll-smooth select-none px-2 gap-1"
|
|
82
|
-
>
|
|
83
|
-
{files.map((file) => (
|
|
84
|
-
<div
|
|
85
|
-
key={file.id}
|
|
86
|
-
data-id={file.id}
|
|
87
|
-
onClick={() => onSelect(file.id)}
|
|
88
|
-
onDoubleClick={(e) => handleStartRename(e, file)}
|
|
89
|
-
className={clsx(
|
|
90
|
-
"flex items-center gap-2 px-3 h-8 rounded-t-lg cursor-pointer transition-[background-color,color] relative group flex-shrink-0 mt-2 outline-none select-none",
|
|
91
|
-
file.id === activeFileId
|
|
92
|
-
? "bg-bg-primary text-accent-primary border-x border-t border-border-color"
|
|
93
|
-
: "text-text-secondary hover:bg-white/5 hover:text-text-primary border-x border-t border-transparent"
|
|
94
|
-
)}
|
|
95
|
-
>
|
|
96
|
-
{file.name.endsWith('.json') ? <FileJson size={14} className="opacity-70" /> : <FileCode size={14} className="opacity-70" />}
|
|
97
|
-
|
|
98
|
-
{editingId === file.id ? (
|
|
99
|
-
<input
|
|
100
|
-
ref={inputRef}
|
|
101
|
-
value={editValue}
|
|
102
|
-
onChange={(e) => setEditValue(e.target.value)}
|
|
103
|
-
onBlur={handleConfirmRename}
|
|
104
|
-
onKeyDown={(e) => handleKeyDown(e, 'rename')}
|
|
105
|
-
className="bg-bg-tertiary text-white text-xs px-1 rounded outline-none w-24 border border-accent-primary/50"
|
|
106
|
-
onClick={e => e.stopPropagation()}
|
|
107
|
-
/>
|
|
108
|
-
) : (
|
|
109
|
-
<span className="text-xs font-medium truncate max-w-[120px]">
|
|
110
|
-
{file.name}
|
|
111
|
-
</span>
|
|
112
|
-
)}
|
|
113
|
-
|
|
114
|
-
{files.length > 1 && (
|
|
115
|
-
<button
|
|
116
|
-
onClick={(e) => {
|
|
117
|
-
e.stopPropagation();
|
|
118
|
-
onClose(file.id);
|
|
119
|
-
}}
|
|
120
|
-
className={clsx(
|
|
121
|
-
"p-0.5 hover:bg-red-500/20 rounded text-text-secondary hover:text-red-400 transition-opacity",
|
|
122
|
-
file.id === activeFileId ? "opacity-100" : "opacity-0 group-hover:opacity-100"
|
|
123
|
-
)}
|
|
124
|
-
title="Close file"
|
|
125
|
-
>
|
|
126
|
-
<X size={12} />
|
|
127
|
-
</button>
|
|
128
|
-
)}
|
|
129
|
-
</div>
|
|
130
|
-
))}
|
|
131
|
-
|
|
132
|
-
<button
|
|
133
|
-
onClick={() => setIsAdding(true)}
|
|
134
|
-
className="p-1.5 ml-1 rounded-lg text-text-secondary hover:text-accent-primary hover:bg-white/5 transition-colors mt-2"
|
|
135
|
-
title="New File"
|
|
136
|
-
>
|
|
137
|
-
<Plus size={16} />
|
|
138
|
-
</button>
|
|
139
|
-
</div>
|
|
140
|
-
|
|
141
|
-
{/* Language Label */}
|
|
142
|
-
<div className="px-4 h-full flex items-center border-l border-border-color bg-bg-secondary hidden md:flex">
|
|
143
|
-
<span className="text-[10px] font-bold uppercase tracking-widest text-text-secondary opacity-50">
|
|
144
|
-
{activeLanguage}
|
|
145
|
-
</span>
|
|
146
|
-
</div>
|
|
147
|
-
|
|
148
|
-
{/* New File Modal */}
|
|
149
|
-
{isAdding && createPortal(
|
|
150
|
-
<div className="fixed inset-0 z-[10000] flex items-center justify-center bg-black/60 backdrop-blur-sm" onClick={() => setIsAdding(false)}>
|
|
151
|
-
<div
|
|
152
|
-
className="w-full max-w-sm bg-bg-secondary border border-border-color rounded-xl shadow-2xl p-6 overflow-hidden"
|
|
153
|
-
onClick={e => e.stopPropagation()}
|
|
154
|
-
>
|
|
155
|
-
<div className="flex items-center gap-3 mb-4">
|
|
156
|
-
<div className="p-2 rounded-lg bg-accent-primary/10 text-accent-primary">
|
|
157
|
-
<PlusCircle size={24} />
|
|
158
|
-
</div>
|
|
159
|
-
<div>
|
|
160
|
-
<h3 className="text-lg font-bold text-white">Create New File</h3>
|
|
161
|
-
<p className="text-xs text-text-secondary">Enter a name ending in .ts or .js</p>
|
|
162
|
-
</div>
|
|
163
|
-
</div>
|
|
164
|
-
|
|
165
|
-
<input
|
|
166
|
-
ref={addInputRef}
|
|
167
|
-
type="text"
|
|
168
|
-
placeholder="e.g. utils.ts"
|
|
169
|
-
className="w-full bg-bg-primary border border-border-color rounded-lg px-4 py-3 text-white outline-none focus:border-accent-primary transition-colors mb-6"
|
|
170
|
-
value={newName}
|
|
171
|
-
onChange={e => setNewName(e.target.value)}
|
|
172
|
-
onKeyDown={(e) => handleKeyDown(e, 'add')}
|
|
173
|
-
/>
|
|
174
|
-
|
|
175
|
-
<div className="flex gap-3">
|
|
176
|
-
<button
|
|
177
|
-
onClick={() => setIsAdding(false)}
|
|
178
|
-
className="flex-1 px-4 py-2 rounded-lg bg-white/5 hover:bg-white/10 text-white font-medium transition-colors"
|
|
179
|
-
>
|
|
180
|
-
Cancel
|
|
181
|
-
</button>
|
|
182
|
-
<button
|
|
183
|
-
onClick={handleConfirmAdd}
|
|
184
|
-
className="flex-1 btn-primary justify-center"
|
|
185
|
-
>
|
|
186
|
-
Create
|
|
187
|
-
</button>
|
|
188
|
-
</div>
|
|
189
|
-
</div>
|
|
190
|
-
</div>,
|
|
191
|
-
document.body
|
|
192
|
-
)}
|
|
193
|
-
</div>
|
|
194
|
-
);
|
|
195
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
export const LightningLogo = ({ size = 24, className = "" }: { size?: number, className?: string }) => (
|
|
2
|
-
<svg
|
|
3
|
-
width={size}
|
|
4
|
-
height={size}
|
|
5
|
-
viewBox="0 0 24 24"
|
|
6
|
-
fill="none"
|
|
7
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
8
|
-
className={className}
|
|
9
|
-
>
|
|
10
|
-
<path
|
|
11
|
-
d="M13 3L4 14H11L9 21L18 10H11L13 3Z"
|
|
12
|
-
stroke="url(#paint0_linear)"
|
|
13
|
-
strokeWidth="2"
|
|
14
|
-
strokeLinecap="round"
|
|
15
|
-
strokeLinejoin="round"
|
|
16
|
-
fill="url(#paint0_linear)"
|
|
17
|
-
fillOpacity="0.2"
|
|
18
|
-
/>
|
|
19
|
-
<defs>
|
|
20
|
-
<linearGradient
|
|
21
|
-
id="paint0_linear"
|
|
22
|
-
x1="4"
|
|
23
|
-
y1="3"
|
|
24
|
-
x2="18"
|
|
25
|
-
y2="21"
|
|
26
|
-
gradientUnits="userSpaceOnUse"
|
|
27
|
-
>
|
|
28
|
-
<stop stopColor="#facc15" /> {/* Yellow 400 */}
|
|
29
|
-
<stop offset="1" stopColor="#fb923c" /> {/* Orange 400 */}
|
|
30
|
-
</linearGradient>
|
|
31
|
-
</defs>
|
|
32
|
-
</svg>
|
|
33
|
-
);
|
|
@@ -1,300 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import { useState, useEffect, useRef } from "react";
|
|
3
|
-
import { createPortal } from "react-dom";
|
|
4
|
-
import { Package, Search, Download, X, Loader2, Trash2, Check, Shield } from "lucide-react";
|
|
5
|
-
import clsx from "clsx";
|
|
6
|
-
|
|
7
|
-
interface NpmPackage {
|
|
8
|
-
package: {
|
|
9
|
-
name: string;
|
|
10
|
-
version: string;
|
|
11
|
-
description: string;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
interface PackageInstallerProps {
|
|
16
|
-
dependencies: Record<string, string>;
|
|
17
|
-
onRefresh: () => void;
|
|
18
|
-
}
|
|
19
|
-
const BASE_DEPS = ["hono", "react", "@types/react"];
|
|
20
|
-
|
|
21
|
-
export function PackageInstaller({ dependencies, onRefresh }: PackageInstallerProps) {
|
|
22
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
23
|
-
const [query, setQuery] = useState("");
|
|
24
|
-
const [results, setResults] = useState<NpmPackage[]>([]);
|
|
25
|
-
const [isSearching, setIsSearching] = useState(false);
|
|
26
|
-
const [installing, setInstalling] = useState<string | null>(null);
|
|
27
|
-
const [uninstalling, setUninstalling] = useState<string | null>(null);
|
|
28
|
-
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
29
|
-
const inputRef = useRef<HTMLInputElement>(null);
|
|
30
|
-
const listRef = useRef<HTMLDivElement>(null);
|
|
31
|
-
|
|
32
|
-
// Focus input when opened
|
|
33
|
-
useEffect(() => {
|
|
34
|
-
if (isOpen) {
|
|
35
|
-
setTimeout(() => inputRef.current?.focus(), 50);
|
|
36
|
-
onRefresh();
|
|
37
|
-
} else {
|
|
38
|
-
setQuery("");
|
|
39
|
-
setResults([]);
|
|
40
|
-
}
|
|
41
|
-
}, [isOpen]);
|
|
42
|
-
|
|
43
|
-
// Debounced search
|
|
44
|
-
useEffect(() => {
|
|
45
|
-
if (!query) {
|
|
46
|
-
setResults([]);
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const timer = setTimeout(async () => {
|
|
51
|
-
setIsSearching(true);
|
|
52
|
-
try {
|
|
53
|
-
const res = await fetch(`/search?q=${encodeURIComponent(query)}`);
|
|
54
|
-
const data = await res.json();
|
|
55
|
-
setResults(data.objects || []);
|
|
56
|
-
setSelectedIndex(0);
|
|
57
|
-
} catch (e) {
|
|
58
|
-
console.error(e);
|
|
59
|
-
} finally {
|
|
60
|
-
setIsSearching(false);
|
|
61
|
-
}
|
|
62
|
-
}, 300);
|
|
63
|
-
|
|
64
|
-
return () => clearTimeout(timer);
|
|
65
|
-
}, [query]);
|
|
66
|
-
|
|
67
|
-
// Global Hotkey for Package Manager (Ctrl + Shift + L)
|
|
68
|
-
useEffect(() => {
|
|
69
|
-
const handleGlobalKeyDown = (e: KeyboardEvent) => {
|
|
70
|
-
if ((e.ctrlKey || e.metaKey) && e.shiftKey && (e.key === 'l' || e.key === 'L')) {
|
|
71
|
-
e.preventDefault();
|
|
72
|
-
setIsOpen(prev => !prev);
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
window.addEventListener("keydown", handleGlobalKeyDown);
|
|
76
|
-
return () => window.removeEventListener("keydown", handleGlobalKeyDown);
|
|
77
|
-
}, []);
|
|
78
|
-
|
|
79
|
-
// Keyboard navigation (when open)
|
|
80
|
-
useEffect(() => {
|
|
81
|
-
if (!isOpen) return;
|
|
82
|
-
|
|
83
|
-
const handleKeyDown = (e: KeyboardEvent) => {
|
|
84
|
-
if (e.key === "ArrowDown") {
|
|
85
|
-
setSelectedIndex(i => Math.min(i + 1, results.length - 1));
|
|
86
|
-
e.preventDefault();
|
|
87
|
-
} else if (e.key === "ArrowUp") {
|
|
88
|
-
setSelectedIndex(i => Math.max(i - 1, 0));
|
|
89
|
-
e.preventDefault();
|
|
90
|
-
} else if (e.key === "Enter") {
|
|
91
|
-
if (results[selectedIndex]) {
|
|
92
|
-
installPackage(results[selectedIndex].package.name);
|
|
93
|
-
}
|
|
94
|
-
} else if (e.key === "Escape") {
|
|
95
|
-
setIsOpen(false);
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
window.addEventListener("keydown", handleKeyDown);
|
|
100
|
-
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
101
|
-
}, [isOpen, results, selectedIndex]);
|
|
102
|
-
|
|
103
|
-
const installPackage = async (name: string) => {
|
|
104
|
-
if (installing) return;
|
|
105
|
-
setInstalling(name);
|
|
106
|
-
try {
|
|
107
|
-
const res = await fetch("/install", {
|
|
108
|
-
method: "POST",
|
|
109
|
-
headers: { "Content-Type": "application/json" },
|
|
110
|
-
body: JSON.stringify({ package: name })
|
|
111
|
-
});
|
|
112
|
-
const data = await res.json();
|
|
113
|
-
if (data.success) {
|
|
114
|
-
await onRefresh();
|
|
115
|
-
setIsOpen(false);
|
|
116
|
-
} else {
|
|
117
|
-
alert(`Failed to install ${name}: ${data.output}`);
|
|
118
|
-
}
|
|
119
|
-
} catch (e) {
|
|
120
|
-
alert("Installation request failed");
|
|
121
|
-
} finally {
|
|
122
|
-
setInstalling(null);
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
const uninstallPackage = async (name: string) => {
|
|
127
|
-
if (uninstalling) return;
|
|
128
|
-
setUninstalling(name);
|
|
129
|
-
try {
|
|
130
|
-
const res = await fetch("/uninstall", {
|
|
131
|
-
method: "POST",
|
|
132
|
-
headers: { "Content-Type": "application/json" },
|
|
133
|
-
body: JSON.stringify({ package: name })
|
|
134
|
-
});
|
|
135
|
-
const data = await res.json();
|
|
136
|
-
if (data.success) {
|
|
137
|
-
await onRefresh();
|
|
138
|
-
} else {
|
|
139
|
-
alert(`Failed to uninstall ${name}: ${data.output}`);
|
|
140
|
-
}
|
|
141
|
-
} catch (e) {
|
|
142
|
-
alert("Uninstallation request failed");
|
|
143
|
-
} finally {
|
|
144
|
-
setUninstalling(null);
|
|
145
|
-
}
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
return (
|
|
149
|
-
<>
|
|
150
|
-
<button
|
|
151
|
-
onClick={() => setIsOpen(true)}
|
|
152
|
-
className="flex items-center gap-2 px-3 py-1.5 hover:bg-white/10 rounded-lg text-text-secondary hover:text-accent-primary transition-all active:scale-95 outline-none"
|
|
153
|
-
title="Install NPM Package (Ctrl + Shift + L)"
|
|
154
|
-
>
|
|
155
|
-
<Package size={18} />
|
|
156
|
-
<span className="text-[10px] font-bold uppercase tracking-wider">NPM Packages</span>
|
|
157
|
-
</button>
|
|
158
|
-
|
|
159
|
-
{isOpen && createPortal(
|
|
160
|
-
<div className="fixed inset-0 z-[9999] flex items-start justify-center pt-24 bg-black/50 backdrop-blur-sm" onClick={() => setIsOpen(false)}>
|
|
161
|
-
<div
|
|
162
|
-
className="w-[600px] bg-[#1e1e20] border border-border-color rounded-xl shadow-2xl overflow-hidden flex flex-col max-h-[500px]"
|
|
163
|
-
onClick={e => e.stopPropagation()}
|
|
164
|
-
>
|
|
165
|
-
{/* Search Header */}
|
|
166
|
-
<div className="p-4 border-b border-white/5 flex items-center gap-3">
|
|
167
|
-
<Search className="text-text-secondary" size={20} />
|
|
168
|
-
<input
|
|
169
|
-
ref={inputRef}
|
|
170
|
-
type="text"
|
|
171
|
-
className="bg-transparent border-none outline-none text-white flex-1 text-lg placeholder:text-white/20"
|
|
172
|
-
placeholder="Search NPM packages..."
|
|
173
|
-
value={query}
|
|
174
|
-
onChange={e => setQuery(e.target.value)}
|
|
175
|
-
/>
|
|
176
|
-
{isSearching && <Loader2 className="animate-spin text-accent-primary" size={20} />}
|
|
177
|
-
<button onClick={() => setIsOpen(false)} className="text-text-secondary hover:text-white">
|
|
178
|
-
<X size={20} />
|
|
179
|
-
</button>
|
|
180
|
-
</div>
|
|
181
|
-
|
|
182
|
-
{/* Results List */}
|
|
183
|
-
<div className="flex-1 overflow-y-auto p-2" ref={listRef}>
|
|
184
|
-
{/* Search Results */}
|
|
185
|
-
{query && (
|
|
186
|
-
<>
|
|
187
|
-
<div className="px-3 py-2 text-xs font-bold text-text-secondary uppercase tracking-wider">Search Results</div>
|
|
188
|
-
{results.length === 0 && !isSearching && (
|
|
189
|
-
<div className="text-center py-4 text-text-secondary">No packages found</div>
|
|
190
|
-
)}
|
|
191
|
-
{results.map((pkg, idx) => (
|
|
192
|
-
<div
|
|
193
|
-
key={pkg.package.name}
|
|
194
|
-
className={clsx(
|
|
195
|
-
"p-3 rounded-lg flex items-center justify-between cursor-pointer group transition-all",
|
|
196
|
-
idx === selectedIndex ? "bg-accent-primary text-white" : "hover:bg-white/5 text-text-primary"
|
|
197
|
-
)}
|
|
198
|
-
onMouseEnter={() => setSelectedIndex(idx)}
|
|
199
|
-
onClick={() => !dependencies[pkg.package.name] && installPackage(pkg.package.name)}
|
|
200
|
-
>
|
|
201
|
-
<div className="flex-1 min-w-0 mr-4">
|
|
202
|
-
<div className="flex items-center gap-2 font-medium">
|
|
203
|
-
<span>{pkg.package.name}</span>
|
|
204
|
-
<span className={clsx("text-xs px-1.5 py-0.5 rounded bg-black/20", idx === selectedIndex ? "text-white/80" : "text-text-secondary")}>v{pkg.package.version}</span>
|
|
205
|
-
</div>
|
|
206
|
-
<p className={clsx("text-sm truncate", idx === selectedIndex ? "text-white/80" : "text-text-secondary")}>
|
|
207
|
-
{pkg.package.description}
|
|
208
|
-
</p>
|
|
209
|
-
</div>
|
|
210
|
-
|
|
211
|
-
{installing === pkg.package.name ? (
|
|
212
|
-
<div className={clsx(
|
|
213
|
-
"p-2 rounded-md",
|
|
214
|
-
idx === selectedIndex ? "bg-white/20" : "bg-white/10"
|
|
215
|
-
)}>
|
|
216
|
-
<Loader2 className="animate-spin" size={18} />
|
|
217
|
-
</div>
|
|
218
|
-
) : dependencies[pkg.package.name] ? (
|
|
219
|
-
<div className="flex items-center gap-2">
|
|
220
|
-
<Check size={16} className={clsx(idx === selectedIndex ? "text-white" : "text-green-500")} />
|
|
221
|
-
<span className={clsx(
|
|
222
|
-
"text-[10px] font-bold uppercase tracking-wider px-2 py-1 rounded",
|
|
223
|
-
idx === selectedIndex ? "bg-white/20 text-white" : "bg-white/5 text-text-secondary"
|
|
224
|
-
)}>
|
|
225
|
-
Installed
|
|
226
|
-
</span>
|
|
227
|
-
</div>
|
|
228
|
-
) : (
|
|
229
|
-
<button
|
|
230
|
-
onClick={(e) => { e.stopPropagation(); installPackage(pkg.package.name); }}
|
|
231
|
-
className={clsx(
|
|
232
|
-
"p-2 rounded-md transition-colors",
|
|
233
|
-
"opacity-0 group-hover:opacity-100",
|
|
234
|
-
idx === selectedIndex ? "bg-white/20 hover:bg-white/30" : "bg-white/10 hover:bg-white/20"
|
|
235
|
-
)}
|
|
236
|
-
>
|
|
237
|
-
<Download size={18} />
|
|
238
|
-
</button>
|
|
239
|
-
)}
|
|
240
|
-
</div>
|
|
241
|
-
))}
|
|
242
|
-
<div className="h-px bg-white/5 my-2" />
|
|
243
|
-
</>
|
|
244
|
-
)}
|
|
245
|
-
|
|
246
|
-
{/* Installed Dependencies */}
|
|
247
|
-
<div className="px-3 py-2 text-xs font-bold text-text-secondary uppercase tracking-wider">Installed Packages</div>
|
|
248
|
-
{Object.keys(dependencies).length === 0 ? (
|
|
249
|
-
<div className="text-center py-4 text-xs text-text-secondary">No packages installed</div>
|
|
250
|
-
) : (
|
|
251
|
-
Object.entries(dependencies).map(([name, version]) => (
|
|
252
|
-
<div
|
|
253
|
-
key={name}
|
|
254
|
-
className="p-3 rounded-lg flex items-center justify-between hover:bg-white/5 text-text-primary group transition-all"
|
|
255
|
-
>
|
|
256
|
-
<div className="flex-1 min-w-0 mr-4">
|
|
257
|
-
<div className="flex items-center gap-2 font-medium">
|
|
258
|
-
<span>{name}</span>
|
|
259
|
-
<span className="text-xs px-1.5 py-0.5 rounded bg-black/20 text-text-secondary">{typeof version === 'string' ? version : 'latest'}</span>
|
|
260
|
-
</div>
|
|
261
|
-
</div>
|
|
262
|
-
|
|
263
|
-
{!BASE_DEPS.includes(name) ? (
|
|
264
|
-
<button
|
|
265
|
-
onClick={() => uninstallPackage(name)}
|
|
266
|
-
disabled={!!uninstalling}
|
|
267
|
-
className="p-2 rounded-md bg-red-500/10 hover:bg-red-500/20 text-red-500 opacity-0 group-hover:opacity-100 transition-all disabled:opacity-50"
|
|
268
|
-
title="Uninstall"
|
|
269
|
-
>
|
|
270
|
-
{uninstalling === name ? (
|
|
271
|
-
<Loader2 className="animate-spin" size={18} />
|
|
272
|
-
) : (
|
|
273
|
-
<Trash2 size={18} />
|
|
274
|
-
)}
|
|
275
|
-
</button>
|
|
276
|
-
) : (
|
|
277
|
-
<div className="flex items-center gap-2 px-2 py-1 rounded bg-white/5">
|
|
278
|
-
<Shield size={12} className="text-accent-primary/60" />
|
|
279
|
-
<span className="text-[10px] font-bold uppercase tracking-tight text-text-secondary opacity-60">
|
|
280
|
-
Core
|
|
281
|
-
</span>
|
|
282
|
-
</div>
|
|
283
|
-
)}
|
|
284
|
-
</div>
|
|
285
|
-
))
|
|
286
|
-
)}
|
|
287
|
-
</div>
|
|
288
|
-
|
|
289
|
-
{/* Footer */}
|
|
290
|
-
<div className="p-2 border-t border-white/5 bg-black/20 flex justify-between px-4 text-xs text-text-secondary">
|
|
291
|
-
<span><span className="font-bold">Enter</span> to install</span>
|
|
292
|
-
<span><span className="font-bold">↑↓</span> to navigate</span>
|
|
293
|
-
</div>
|
|
294
|
-
</div>
|
|
295
|
-
</div>,
|
|
296
|
-
document.body
|
|
297
|
-
)}
|
|
298
|
-
</>
|
|
299
|
-
);
|
|
300
|
-
}
|