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.
@@ -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
- }