diff --git a/frontend/src/components/editor/EditorToolbar.tsx b/frontend/src/components/editor/EditorToolbar.tsx index d67a4e8c..df83d1ba 100644 --- a/frontend/src/components/editor/EditorToolbar.tsx +++ b/frontend/src/components/editor/EditorToolbar.tsx @@ -3,6 +3,7 @@ import { useEditorStore } from '../../store/useEditorStore'; import { useSimulatorStore, BOARD_FQBN, BOARD_LABELS } from '../../store/useSimulatorStore'; import { compileCode } from '../../services/compilation'; import { LibraryManagerModal } from '../simulator/LibraryManagerModal'; +import { InstallLibrariesModal } from '../simulator/InstallLibrariesModal'; import { parseCompileResult } from '../../utils/compilationLogger'; import type { CompilationLog } from '../../utils/compilationLogger'; import { exportToWokwiZip, importFromWokwiZip } from '../../utils/wokwiZip'; @@ -30,6 +31,8 @@ export const EditorToolbar = ({ consoleOpen, setConsoleOpen, compileLogs: _compi const [compiling, setCompiling] = useState(false); const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); const [libManagerOpen, setLibManagerOpen] = useState(false); + const [pendingLibraries, setPendingLibraries] = useState([]); + const [installModalOpen, setInstallModalOpen] = useState(false); const importInputRef = useRef(null); const addLog = useCallback((log: CompilationLog) => { @@ -121,6 +124,10 @@ export const EditorToolbar = ({ consoleOpen, setConsoleOpen, compileLogs: _compi setWires(result.wires); if (result.files.length > 0) loadFiles(result.files); setMessage({ type: 'success', text: `Imported ${file.name}` }); + if (result.libraries.length > 0) { + setPendingLibraries(result.libraries); + setInstallModalOpen(true); + } } catch (err: any) { setMessage({ type: 'error', text: err?.message || 'Import failed.' }); } @@ -275,6 +282,11 @@ export const EditorToolbar = ({ consoleOpen, setConsoleOpen, compileLogs: _compi )} setLibManagerOpen(false)} /> + setInstallModalOpen(false)} + libraries={pendingLibraries} + /> ); }; diff --git a/frontend/src/components/simulator/InstallLibrariesModal.css b/frontend/src/components/simulator/InstallLibrariesModal.css new file mode 100644 index 00000000..f5670c90 --- /dev/null +++ b/frontend/src/components/simulator/InstallLibrariesModal.css @@ -0,0 +1,246 @@ +.ilib-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.75); + display: flex; + align-items: center; + justify-content: center; + z-index: 1100; + backdrop-filter: blur(4px); + animation: ilib-fadeIn 0.15s ease; +} + +@keyframes ilib-fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.ilib-modal { + background: #1a1a1a; + border: 1px solid #333; + border-radius: 8px; + width: 480px; + max-width: 95vw; + max-height: 80vh; + display: flex; + flex-direction: column; + box-shadow: 0 24px 64px rgba(0, 0, 0, 0.6); + animation: ilib-slideIn 0.2s ease; + overflow: hidden; +} + +@keyframes ilib-slideIn { + from { transform: translateY(-16px); opacity: 0; } + to { transform: translateY(0); opacity: 1; } +} + +/* Header */ +.ilib-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 18px; + background: #111; + border-bottom: 1px solid #2a2a2a; +} + +.ilib-title { + display: flex; + align-items: center; + gap: 10px; + font-size: 13px; + font-weight: 700; + letter-spacing: 0.12em; + color: #ccc; + text-transform: uppercase; +} + +.ilib-close-btn { + background: none; + border: none; + color: #888; + cursor: pointer; + padding: 4px 8px; + border-radius: 4px; + transition: all 0.15s; + line-height: 1; +} + +.ilib-close-btn:hover:not(:disabled) { + background: #2a2a2a; + color: #fff; +} + +.ilib-close-btn:disabled { + opacity: 0.3; + cursor: not-allowed; +} + +/* Subtitle */ +.ilib-subtitle { + padding: 10px 18px; + font-size: 13px; + color: #999; + border-bottom: 1px solid #222; + background: #161616; + min-height: 38px; + display: flex; + align-items: center; +} + +.ilib-subtitle-installing { + display: flex; + align-items: center; + gap: 8px; + color: #00b8d4; + font-weight: 500; +} + +.ilib-subtitle-done { + color: #4ade80; + font-weight: 500; +} + +/* Library list */ +.ilib-list { + flex: 1; + overflow-y: auto; + padding: 6px 0; +} + +.ilib-list::-webkit-scrollbar { width: 6px; } +.ilib-list::-webkit-scrollbar-track { background: #111; } +.ilib-list::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; } + +.ilib-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 18px; + border-bottom: 1px solid #1f1f1f; + gap: 12px; + transition: background 0.1s; +} + +.ilib-item:hover { background: #1f1f1f; } + +.ilib-item-name { + font-size: 13px; + color: #ddd; + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ilib-item--done .ilib-item-name { color: #888; } +.ilib-item--error .ilib-item-name { color: #888; } + +.ilib-item-status { + flex-shrink: 0; +} + +/* Status badges */ +.ilib-badge { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.04em; + padding: 3px 8px; + border-radius: 3px; + white-space: nowrap; +} + +.ilib-badge--pending { + color: #666; + background: #1a1a1a; + border: 1px solid #333; +} + +.ilib-badge--installing { + color: #00b8d4; + background: #0a1f24; + border: 1px solid #005f7a; +} + +.ilib-badge--done { + color: #4ade80; + background: #0d2b1e; + border: 1px solid #1a4731; +} + +.ilib-badge--error { + color: #f87171; + background: #2b0d0d; + border: 1px solid #4a1a1a; + cursor: help; +} + +/* Spinner */ +.ilib-spinner { + flex-shrink: 0; + animation: ilib-spin 0.8s linear infinite; +} + +@keyframes ilib-spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +/* Footer */ +.ilib-footer { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 10px; + padding: 14px 18px; + background: #111; + border-top: 1px solid #2a2a2a; +} + +.ilib-btn { + display: inline-flex; + align-items: center; + gap: 6px; + border: none; + border-radius: 20px; + padding: 8px 22px; + font-size: 13px; + font-weight: 700; + letter-spacing: 0.05em; + cursor: pointer; + transition: all 0.15s; + white-space: nowrap; +} + +.ilib-btn:disabled { + opacity: 0.45; + cursor: not-allowed; + transform: none !important; + box-shadow: none !important; +} + +.ilib-btn--primary { + background: #00b8d4; + color: #000; +} + +.ilib-btn--primary:hover:not(:disabled) { + background: #00d4f0; + box-shadow: 0 0 12px rgba(0, 184, 212, 0.4); + transform: translateY(-1px); +} + +.ilib-btn--ghost { + background: transparent; + color: #888; + border: 1px solid #333; +} + +.ilib-btn--ghost:hover:not(:disabled) { + background: #2a2a2a; + color: #ccc; +} diff --git a/frontend/src/components/simulator/InstallLibrariesModal.tsx b/frontend/src/components/simulator/InstallLibrariesModal.tsx new file mode 100644 index 00000000..958fa549 --- /dev/null +++ b/frontend/src/components/simulator/InstallLibrariesModal.tsx @@ -0,0 +1,197 @@ +import React, { useState, useCallback } from 'react'; +import { installLibrary } from '../../services/libraryService'; +import './InstallLibrariesModal.css'; + +interface InstallLibrariesModalProps { + isOpen: boolean; + onClose: () => void; + libraries: string[]; +} + +type ItemStatus = 'pending' | 'installing' | 'done' | 'error'; + +interface LibItem { + name: string; + status: ItemStatus; + error?: string; +} + +const Spinner: React.FC<{ size?: number }> = ({ size = 16 }) => ( + + + +); + +export const InstallLibrariesModal: React.FC = ({ + isOpen, + onClose, + libraries, +}) => { + const [items, setItems] = useState(() => + libraries.map((name) => ({ name, status: 'pending' })), + ); + const [running, setRunning] = useState(false); + const [doneCount, setDoneCount] = useState(0); + + // Sync items when the libraries prop changes (new import) + React.useEffect(() => { + setItems(libraries.map((name) => ({ name, status: 'pending' }))); + setDoneCount(0); + setRunning(false); + }, [libraries]); + + const setItemStatus = useCallback( + (name: string, status: ItemStatus, error?: string) => { + setItems((prev) => + prev.map((it) => (it.name === name ? { ...it, status, error } : it)), + ); + }, + [], + ); + + const handleInstallAll = useCallback(async () => { + setRunning(true); + let completed = 0; + for (const item of items) { + if (item.status === 'done') { completed++; continue; } + setItemStatus(item.name, 'installing'); + try { + const result = await installLibrary(item.name); + if (result.success) { + setItemStatus(item.name, 'done'); + } else { + setItemStatus(item.name, 'error', result.error || 'Install failed'); + } + } catch (e) { + setItemStatus(item.name, 'error', e instanceof Error ? e.message : 'Install failed'); + } + completed++; + setDoneCount(completed); + } + setRunning(false); + }, [items, setItemStatus]); + + if (!isOpen) return null; + + const pendingCount = items.filter((i) => i.status === 'pending').length; + const installedCount = items.filter((i) => i.status === 'done').length; + const allDone = items.length > 0 && pendingCount === 0 && !running; + + return ( +
+
e.stopPropagation()}> + {/* Header */} +
+
+ + + + + + REQUIRED LIBRARIES +
+ +
+ + {/* Subtitle */} +
+ {running ? ( + + + Installing {doneCount + 1} of {items.length}… + + ) : allDone ? ( + All libraries installed successfully + ) : ( + + This project requires {items.length} {items.length === 1 ? 'library' : 'libraries'}. + Install them to compile correctly. + + )} +
+ + {/* Library list */} +
+ {items.map((item) => ( +
+ {item.name} + + {item.status === 'pending' && pending} + {item.status === 'installing' && ( + + + installing… + + )} + {item.status === 'done' && ( + + + + + installed + + )} + {item.status === 'error' && ( + + + + + + error + + )} + +
+ ))} +
+ + {/* Footer */} +
+ {allDone ? ( + + ) : ( + <> + + + + )} +
+
+
+ ); +}; diff --git a/frontend/src/utils/wokwiZip.ts b/frontend/src/utils/wokwiZip.ts index 307c15b5..59efc482 100644 --- a/frontend/src/utils/wokwiZip.ts +++ b/frontend/src/utils/wokwiZip.ts @@ -48,6 +48,8 @@ export interface ImportResult { components: VelxioComponent[]; wires: Wire[]; files: Array<{ name: string; content: string }>; + /** Standard Arduino library names parsed from libraries.txt (Wokwi-only @wokwi: entries are excluded). */ + libraries: string[]; } // ── Board mappings ──────────────────────────────────────────────────────────── @@ -303,5 +305,17 @@ export async function importFromWokwiZip(file: File): Promise { return a.name.localeCompare(b.name); }); - return { boardType, boardPosition, components, wires, files }; + // Parse libraries.txt — skip blank lines, comments (#), and Wokwi-only entries (name@wokwi:hash) + const libraries: string[] = []; + const libEntry = zip.file('libraries.txt'); + if (libEntry) { + const libText = await libEntry.async('string'); + for (const raw of libText.split('\n')) { + const line = raw.trim(); + if (!line || line.startsWith('#') || line.includes('@wokwi:')) continue; + libraries.push(line); + } + } + + return { boardType, boardPosition, components, wires, files, libraries }; }