2026-03-05 08:05:23 +07:00
import React , { useState , useEffect , useCallback , useRef } from 'react' ;
2026-05-09 21:29:07 +07:00
import { useTranslation } from 'react-i18next' ;
2026-04-22 02:45:45 +07:00
import {
searchLibraries ,
installLibrary ,
getInstalledLibraries ,
2026-06-08 00:58:13 +07:00
getCustomLibraries ,
deleteCustomLibrary ,
2026-05-02 14:24:09 +07:00
uninstallLibrary ,
2026-04-22 02:45:45 +07:00
} from '../../services/libraryService' ;
2026-03-05 08:05:23 +07:00
import type { ArduinoLibrary , InstalledLibrary } from '../../services/libraryService' ;
2026-03-25 13:06:38 +07:00
import { trackInstallLibrary } from '../../utils/analytics' ;
2026-06-07 11:07:48 +07:00
import { useSimulatorStore } from '../../store/useSimulatorStore' ;
import { boardDisplayName } from '../../types/board' ;
2026-03-05 08:05:23 +07:00
import './LibraryManagerModal.css' ;
interface LibraryManagerModalProps {
2026-04-22 02:45:45 +07:00
isOpen : boolean ;
onClose : ( ) = > void ;
2026-03-05 08:05:23 +07:00
}
2026-06-07 10:08:34 +07:00
type Tab = 'project' | 'search' | 'installed' ;
/ * * C a s e / s e p a r a t o r - i n s e n s i t i v e n a m e m a t c h , m a t c h i n g t h e b a c k e n d ' s
* _norm_lib_name ( lowercased , alphanumerics only ) so the UI ' s "in project"
* state agrees with what the compiler scopes . * /
const normLib = ( s : string ) : string = >
( s || '' ) . toLowerCase ( ) . replace ( /[^a-z0-9]/g , '' ) ;
2026-03-05 08:05:23 +07:00
export const LibraryManagerModal : React.FC < LibraryManagerModalProps > = ( { isOpen , onClose } ) = > {
2026-05-09 21:29:07 +07:00
const { t } = useTranslation ( ) ;
2026-04-22 02:45:45 +07:00
const [ activeTab , setActiveTab ] = useState < Tab > ( 'search' ) ;
2026-06-07 11:07:48 +07:00
// P2.4 — manifests are PER-BOARD: the velxio.json edited here belongs to the
// ACTIVE board, so two boards in one project can scope to different libraries.
const boards = useSimulatorStore ( ( s ) = > s . boards ) ;
const activeBoardId = useSimulatorStore ( ( s ) = > s . activeBoardId ) ;
const updateBoard = useSimulatorStore ( ( s ) = > s . updateBoard ) ;
const activeBoard = boards . find ( ( b ) = > b . id === activeBoardId ) ? ? boards [ 0 ] ;
const manifestLibs = activeBoard ? . libraries ? ? null ;
const setLibraries = useCallback (
( libs : string [ ] | null ) = > {
if ( ! activeBoard ) return ;
updateBoard ( activeBoard . id , { libraries : libs && libs . length ? libs : undefined } ) ;
} ,
[ activeBoard , updateBoard ] ,
) ;
2026-06-07 10:08:34 +07:00
// Raw velxio.json editor draft + parse error (the Wokwi-style view).
const [ jsonDraft , setJsonDraft ] = useState ( '' ) ;
const [ jsonError , setJsonError ] = useState < string | null > ( null ) ;
const [ newLibName , setNewLibName ] = useState ( '' ) ;
2026-06-07 11:07:48 +07:00
// Autocomplete suggestions for the "add library" field (index search).
const [ addSuggestions , setAddSuggestions ] = useState < string [ ] > ( [ ] ) ;
const addDebounceRef = useRef < ReturnType < typeof setTimeout > | null > ( null ) ;
2026-04-22 02:45:45 +07:00
const [ searchQuery , setSearchQuery ] = useState ( '' ) ;
const [ searchResults , setSearchResults ] = useState < ArduinoLibrary [ ] > ( [ ] ) ;
const [ installedLibraries , setInstalledLibraries ] = useState < InstalledLibrary [ ] > ( [ ] ) ;
const [ loadingSearch , setLoadingSearch ] = useState ( false ) ;
const [ loadingInstalled , setLoadingInstalled ] = useState ( false ) ;
const [ installingLib , setInstallingLib ] = useState < string | null > ( null ) ;
2026-05-02 14:24:09 +07:00
const [ uninstallingLib , setUninstallingLib ] = useState < string | null > ( null ) ;
/** Track user-selected version per library name */
const [ selectedVersions , setSelectedVersions ] = useState < Record < string , string > > ( { } ) ;
2026-04-22 02:45:45 +07:00
const [ statusMsg , setStatusMsg ] = useState < { type : 'success' | 'error' ; text : string } | null > (
null ,
) ;
const debounceRef = useRef < ReturnType < typeof setTimeout > | null > ( null ) ;
2026-03-05 08:05:23 +07:00
2026-04-22 02:45:45 +07:00
const fetchInstalled = useCallback ( async ( ) = > {
setLoadingInstalled ( true ) ;
try {
2026-06-08 00:58:13 +07:00
// P2.2c — the shared global list (index libs) PLUS the user's per-user
// custom uploads (which live in their per-user store, not the global
// list). Custom first so they're easy to find; de-duped by name.
const [ libs , custom ] = await Promise . all ( [ getInstalledLibraries ( ) , getCustomLibraries ( ) ] ) ;
const customNames = new Set ( custom . map ( ( c ) = > ( c . name || '' ) . toLowerCase ( ) ) ) ;
const merged = [
. . . custom ,
. . . libs . filter ( ( l ) = > ! customNames . has ( ( l . library ? . name || l . name || '' ) . toLowerCase ( ) ) ) ,
] ;
setInstalledLibraries ( merged ) ;
2026-04-22 02:45:45 +07:00
} catch ( e : unknown ) {
setStatusMsg ( {
type : 'error' ,
text : e instanceof Error ? e . message : 'Failed to load installed libraries' ,
} ) ;
} finally {
setLoadingInstalled ( false ) ;
}
} , [ ] ) ;
2026-03-05 08:05:23 +07:00
2026-04-22 02:45:45 +07:00
// Reset state when modal closes
useEffect ( ( ) = > {
if ( ! isOpen ) {
setSearchQuery ( '' ) ;
setSearchResults ( [ ] ) ;
setStatusMsg ( null ) ;
}
} , [ isOpen ] ) ;
2026-03-05 11:23:59 +07:00
2026-04-22 02:45:45 +07:00
// Fetch installed list when modal opens or switching to installed tab
useEffect ( ( ) = > {
if ( isOpen && activeTab === 'installed' ) fetchInstalled ( ) ;
} , [ isOpen , activeTab , fetchInstalled ] ) ;
2026-03-05 08:05:23 +07:00
2026-04-22 02:45:45 +07:00
useEffect ( ( ) = > {
if ( isOpen ) fetchInstalled ( ) ;
} , [ isOpen , fetchInstalled ] ) ;
2026-03-06 07:07:10 +07:00
2026-04-22 02:45:45 +07:00
// Search: immediate on open (empty query), debounced when typing
useEffect ( ( ) = > {
if ( ! isOpen ) return ;
if ( debounceRef . current ) clearTimeout ( debounceRef . current ) ;
const delay = searchQuery ? 400 : 0 ;
debounceRef . current = setTimeout ( async ( ) = > {
setLoadingSearch ( true ) ;
setStatusMsg ( null ) ;
try {
const results = await searchLibraries ( searchQuery ) ;
setSearchResults ( results ) ;
} catch ( e : unknown ) {
setStatusMsg ( { type : 'error' , text : e instanceof Error ? e . message : 'Search failed' } ) ;
setSearchResults ( [ ] ) ;
} finally {
setLoadingSearch ( false ) ;
}
} , delay ) ;
2026-03-05 08:05:23 +07:00
2026-04-22 02:45:45 +07:00
return ( ) = > {
if ( debounceRef . current ) clearTimeout ( debounceRef . current ) ;
2026-03-05 08:05:23 +07:00
} ;
2026-04-22 02:45:45 +07:00
} , [ searchQuery , isOpen ] ) ;
2026-03-05 08:05:23 +07:00
2026-04-22 02:45:45 +07:00
const handleInstall = async ( libName : string ) = > {
setInstallingLib ( libName ) ;
setStatusMsg ( null ) ;
try {
2026-05-02 14:24:09 +07:00
const version = selectedVersions [ libName ] ;
const result = await installLibrary ( libName , version ) ;
2026-04-22 02:45:45 +07:00
if ( result . success ) {
trackInstallLibrary ( libName ) ;
2026-06-07 10:08:34 +07:00
// P2.4 — installing a library declares it for THIS project (adds it to
// velxio.json), so the compile is scoped to it and it never clashes
// with another project's libs. The user can remove it in the Project tab.
addToManifest ( libName ) ;
2026-05-02 14:24:09 +07:00
if ( result . fallback ) {
2026-06-07 10:08:34 +07:00
setStatusMsg ( { type : 'success' , text : ` " ${ libName } " installed and added to this project (latest — requested @ ${ result . requested_version } was not available) ` } ) ;
2026-05-02 14:24:09 +07:00
} else {
2026-06-07 10:08:34 +07:00
setStatusMsg ( { type : 'success' , text : ` " ${ libName } ${ version ? ' @' + version : '' } " installed and added to this project! ` } ) ;
2026-05-02 14:24:09 +07:00
}
fetchInstalled ( ) ;
2026-04-22 02:45:45 +07:00
} else {
setStatusMsg ( { type : 'error' , text : result.error || ` Failed to install " ${ libName } " ` } ) ;
}
} catch ( e : unknown ) {
setStatusMsg ( { type : 'error' , text : e instanceof Error ? e . message : 'Installation failed' } ) ;
} finally {
setInstallingLib ( null ) ;
}
} ;
2026-03-05 08:05:23 +07:00
2026-05-02 14:24:09 +07:00
const handleUninstall = async ( libName : string ) = > {
setUninstallingLib ( libName ) ;
setStatusMsg ( null ) ;
try {
const result = await uninstallLibrary ( libName ) ;
if ( result . success ) {
setStatusMsg ( { type : 'success' , text : ` " ${ libName } " uninstalled successfully! ` } ) ;
fetchInstalled ( ) ;
} else {
setStatusMsg ( { type : 'error' , text : result.error || ` Failed to uninstall " ${ libName } " ` } ) ;
}
} catch ( e : unknown ) {
setStatusMsg ( { type : 'error' , text : e instanceof Error ? e . message : 'Uninstall failed' } ) ;
} finally {
setUninstallingLib ( null ) ;
}
} ;
2026-06-08 00:58:13 +07:00
// P2.2c — a CUSTOM lib lives in the user's per-user store, not the shared
// arduino-cli dir, so removing it hits the per-user delete endpoint.
const handleRemoveCustom = async ( libName : string ) = > {
setUninstallingLib ( libName ) ;
setStatusMsg ( null ) ;
try {
const result = await deleteCustomLibrary ( libName ) ;
if ( result . success ) {
setStatusMsg ( { type : 'success' , text : ` Removed your custom " ${ libName } ". ` } ) ;
removeFromManifest ( libName ) ;
fetchInstalled ( ) ;
} else {
setStatusMsg ( { type : 'error' , text : result.error || ` Failed to remove " ${ libName } " ` } ) ;
}
} finally {
setUninstallingLib ( null ) ;
}
} ;
2026-06-07 10:08:34 +07:00
// ── Project manifest (velxio.json) editing ──────────────────────────────
const declared = manifestLibs ? ? [ ] ;
const inManifest = useCallback (
( name : string ) : boolean = > declared . some ( ( l ) = > normLib ( l ) === normLib ( name ) ) ,
[ declared ] ,
) ;
const addToManifest = useCallback (
( name : string ) = > {
const clean = name . trim ( ) ;
if ( ! clean ) return ;
2026-06-07 11:07:48 +07:00
const cur = manifestLibs ? ? [ ] ;
2026-06-07 10:08:34 +07:00
if ( cur . some ( ( l ) = > normLib ( l ) === normLib ( clean ) ) ) return ;
setLibraries ( [ . . . cur , clean ] ) ;
} ,
2026-06-07 11:07:48 +07:00
[ manifestLibs , setLibraries ] ,
2026-06-07 10:08:34 +07:00
) ;
const removeFromManifest = useCallback (
( name : string ) = > {
2026-06-07 11:07:48 +07:00
const cur = manifestLibs ? ? [ ] ;
2026-06-07 10:08:34 +07:00
const next = cur . filter ( ( l ) = > normLib ( l ) !== normLib ( name ) ) ;
setLibraries ( next . length ? next : null ) ;
} ,
2026-06-07 11:07:48 +07:00
[ manifestLibs , setLibraries ] ,
2026-06-07 10:08:34 +07:00
) ;
2026-06-07 11:07:48 +07:00
// Autocomplete: debounced index search for the "add library" field. Combined
// with instant matches from the installed list in `addOptions` below.
useEffect ( ( ) = > {
if ( addDebounceRef . current ) clearTimeout ( addDebounceRef . current ) ;
const q = newLibName . trim ( ) ;
if ( q . length < 2 ) {
setAddSuggestions ( [ ] ) ;
return ;
}
addDebounceRef . current = setTimeout ( async ( ) = > {
try {
const results = await searchLibraries ( q ) ;
setAddSuggestions ( results . map ( ( r ) = > r . name ) . filter ( Boolean ) ) ;
} catch {
setAddSuggestions ( [ ] ) ;
}
} , 300 ) ;
return ( ) = > {
if ( addDebounceRef . current ) clearTimeout ( addDebounceRef . current ) ;
} ;
} , [ newLibName ] ) ;
// Merged, de-duped suggestions: installed libs first (instant), then index
// results, excluding what's already declared. Capped for a tidy dropdown.
const addOptions = ( ( ) = > {
const q = normLib ( newLibName ) ;
if ( ! q ) return [ ] ;
const installedNames = installedLibraries
. map ( ( il ) = > il . library ? . name || il . name || '' )
. filter ( Boolean ) ;
const merged : string [ ] = [ ] ;
for ( const n of [ . . . installedNames , . . . addSuggestions ] ) {
if ( ! normLib ( n ) . includes ( q ) ) continue ;
if ( declared . some ( ( d ) = > normLib ( d ) === normLib ( n ) ) ) continue ;
if ( merged . some ( ( m ) = > normLib ( m ) === normLib ( n ) ) ) continue ;
merged . push ( n ) ;
}
return merged . slice ( 0 , 8 ) ;
} ) ( ) ;
2026-06-07 10:08:34 +07:00
const applyJsonDraft = useCallback ( ( ) = > {
try {
const parsed = JSON . parse ( jsonDraft || '{}' ) ;
const libs = Array . isArray ( parsed ) ? parsed : parsed.libraries ;
if ( ! Array . isArray ( libs ) || ! libs . every ( ( x ) = > typeof x === 'string' ) ) {
setJsonError ( 'Expected {"libraries": ["Name", ...]}' ) ;
return ;
}
setJsonError ( null ) ;
setLibraries ( libs . length ? ( libs as string [ ] ) : null ) ;
} catch ( e ) {
setJsonError ( e instanceof Error ? e . message : 'Invalid JSON' ) ;
}
} , [ jsonDraft , setLibraries ] ) ;
// Open the Project (velxio.json) tab when launched from the explorer entry.
useEffect ( ( ) = > {
const toProject = ( ) = > setActiveTab ( 'project' ) ;
window . addEventListener ( 'velxio-open-library-manager' , toProject ) ;
return ( ) = > window . removeEventListener ( 'velxio-open-library-manager' , toProject ) ;
} , [ ] ) ;
2026-06-07 22:20:12 +07:00
// P2.2 — a custom .zip upload lands in the user's PER-USER store (not the
// shared dir), so auto-declare it on the active board (velxio.json) and show
// the Project tab; the compile then resolves it via the owner per-user path.
useEffect ( ( ) = > {
const onUploaded = ( e : Event ) = > {
const name = ( e as CustomEvent ) . detail ? . library ;
if ( name ) {
addToManifest ( name ) ;
setActiveTab ( 'project' ) ;
}
fetchInstalled ( ) ;
} ;
window . addEventListener ( 'velxio-custom-library-installed' , onUploaded ) ;
return ( ) = > window . removeEventListener ( 'velxio-custom-library-installed' , onUploaded ) ;
} , [ addToManifest , fetchInstalled ] ) ;
2026-06-07 10:08:34 +07:00
// Keep the raw velxio.json draft in sync with the manifest while not editing.
useEffect ( ( ) = > {
setJsonDraft ( JSON . stringify ( { libraries : manifestLibs ? ? [ ] } , null , 2 ) ) ;
setJsonError ( null ) ;
} , [ manifestLibs , isOpen ] ) ;
2026-04-22 02:45:45 +07:00
const handleClose = ( ) = > {
onClose ( ) ;
} ;
2026-03-05 08:05:23 +07:00
2026-04-22 02:45:45 +07:00
if ( ! isOpen ) return null ;
2026-03-05 11:23:59 +07:00
2026-04-22 02:45:45 +07:00
const isInstalled = ( libName : string ) : boolean = >
installedLibraries . some (
( il ) = > ( il . library ? . name || il . name || '' ) . toLowerCase ( ) === libName . toLowerCase ( ) ,
) ;
2026-03-05 08:05:23 +07:00
2026-04-22 02:45:45 +07:00
const getLibName = ( lib : ArduinoLibrary ) : string = > lib . name || 'Unknown' ;
const getLibVersion = ( lib : ArduinoLibrary ) : string = > lib . latest ? . version || lib . version || '' ;
const getLibAuthor = ( lib : ArduinoLibrary ) : string = > lib . latest ? . author || lib . author || '' ;
const getLibDesc = ( lib : ArduinoLibrary ) : string = > lib . latest ? . sentence || lib . sentence || '' ;
2026-03-05 08:05:23 +07:00
2026-04-22 02:45:45 +07:00
const getInstalledName = ( lib : InstalledLibrary ) : string = >
lib . library ? . name || lib . name || 'Unknown' ;
const getInstalledVersion = ( lib : InstalledLibrary ) : string = >
lib . library ? . version || lib . version || '' ;
const getInstalledAuthor = ( lib : InstalledLibrary ) : string = >
lib . library ? . author || lib . author || '' ;
const getInstalledDesc = ( lib : InstalledLibrary ) : string = >
lib . library ? . sentence || lib . sentence || '' ;
2026-03-05 08:05:23 +07:00
2026-04-22 02:45:45 +07:00
return (
< div className = "lib-modal-overlay" onClick = { handleClose } >
< div className = "lib-modal" onClick = { ( e ) = > e . stopPropagation ( ) } >
{ /* Header */ }
< div className = "lib-modal-header" >
< div className = "lib-modal-title" >
< svg
className = "lib-icon"
viewBox = "0 0 24 24"
fill = "none"
stroke = "currentColor"
strokeWidth = "2"
strokeLinecap = "round"
strokeLinejoin = "round"
>
< path d = "M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z" / >
< path d = "m3.3 7 8.7 5 8.7-5" / >
< path d = "M12 22V12" / >
< / svg >
2026-05-09 21:29:07 +07:00
< span > { t ( 'editor.libraryManager.title' ) } < / span >
2026-04-22 02:45:45 +07:00
< / div >
< button className = "lib-close-btn" onClick = { handleClose } >
< svg
width = "14"
height = "14"
viewBox = "0 0 24 24"
fill = "none"
stroke = "currentColor"
strokeWidth = "2.5"
strokeLinecap = "round"
>
< line x1 = "18" y1 = "6" x2 = "6" y2 = "18" / >
< line x1 = "6" y1 = "6" x2 = "18" y2 = "18" / >
< / svg >
< / button >
< / div >
2026-03-05 08:05:23 +07:00
2026-04-22 02:45:45 +07:00
{ /* Tabs */ }
< div className = "lib-tabs" >
2026-06-07 10:08:34 +07:00
< button
className = { ` lib-tab ${ activeTab === 'project' ? 'active' : '' } ` }
onClick = { ( ) = > setActiveTab ( 'project' ) }
>
In project ( { declared . length } )
< / button >
2026-04-22 02:45:45 +07:00
< button
className = { ` lib-tab ${ activeTab === 'search' ? 'active' : '' } ` }
onClick = { ( ) = > setActiveTab ( 'search' ) }
>
2026-05-09 21:29:07 +07:00
{ t ( 'editor.libraryManager.searchTab' ) }
2026-04-22 02:45:45 +07:00
< / button >
< button
className = { ` lib-tab ${ activeTab === 'installed' ? 'active' : '' } ` }
onClick = { ( ) = > setActiveTab ( 'installed' ) }
>
2026-05-09 21:29:07 +07:00
{ t ( 'editor.libraryManager.installedTab' ) }
2026-04-22 02:45:45 +07:00
< / button >
< / div >
2026-03-05 08:05:23 +07:00
2026-04-22 02:45:45 +07:00
{ /* Status bar */ }
{ statusMsg && (
< div className = { ` lib-status ${ statusMsg . type } ` } >
{ statusMsg . type === 'success' ? (
< svg
width = "14"
height = "14"
viewBox = "0 0 24 24"
fill = "none"
stroke = "currentColor"
strokeWidth = "2.5"
strokeLinecap = "round"
strokeLinejoin = "round"
style = { { flexShrink : 0 } }
>
< polyline points = "20 6 9 17 4 12" / >
< / svg >
) : (
< svg
width = "14"
height = "14"
viewBox = "0 0 24 24"
fill = "none"
stroke = "currentColor"
strokeWidth = "2.5"
strokeLinecap = "round"
strokeLinejoin = "round"
style = { { flexShrink : 0 } }
>
< line x1 = "18" y1 = "6" x2 = "6" y2 = "18" / >
< line x1 = "6" y1 = "6" x2 = "18" y2 = "18" / >
< / svg >
) }
{ statusMsg . text }
< / div >
) }
2026-03-05 08:05:23 +07:00
2026-06-07 10:08:34 +07:00
{ / * P r o j e c t T a b — v e l x i o . j s o n : t h e l i b r a r i e s T H I S p r o j e c t d e c l a r e s .
These ( plus the arduino - esp32 core ) are the ESP32 compile scope , so
the project never picks up another project 's or user' s libraries . * / }
{ activeTab === 'project' && (
< div className = "lib-content" >
< div style = { { padding : '10px 14px' , color : '#9d9d9d' , fontSize : 12 , lineHeight : 1.5 } } >
2026-06-07 11:07:48 +07:00
Libraries used by { ' ' }
< strong style = { { color : '#a5d6a7' } } >
{ activeBoard ? boardDisplayName ( activeBoard ) : 'this board' }
< / strong > { ' ' }
( its < strong style = { { color : '#ffd60a' } } > velxio . json < / strong > ) . Each
board has its own list , so two boards can use different libraries
without clashing . Installing a library adds it here automatically ;
start typing below to add more .
2026-06-07 10:08:34 +07:00
< / div >
{ /* Declared libraries as removable rows */ }
< div className = "lib-list" style = { { maxHeight : 220 } } >
{ declared . length === 0 && (
< div className = "lib-empty" >
< p > No libraries declared . < / p >
< p className = "lib-empty-sub" >
Core libraries ( WiFi , Wire , SPI , WebServer … ) are always
available . Add external libraries from the Search tab or below .
< / p >
< / div >
) }
{ declared . map ( ( name , i ) = > (
< div key = { i } className = "lib-item" >
< div className = "lib-item-info" >
< div className = "lib-item-header" >
< span className = "lib-item-name" > { name } < / span >
< / div >
< / div >
< div className = "lib-item-actions" >
< button
className = "lib-uninstall-btn"
onClick = { ( ) = > removeFromManifest ( name ) }
title = "Remove from this project (velxio.json)"
>
Remove
< / button >
< / div >
< / div >
) ) }
< / div >
2026-06-07 11:07:48 +07:00
{ / * A d d a l i b r a r y — a u t o c o m p l e t e ( i n s t a l l e d + i n d e x s e a r c h ) s o t h e
user picks from a list instead of typing the exact name . * / }
< div style = { { position : 'relative' , marginTop : 8 } } >
< div className = "lib-search-bar" style = { { margin : 0 } } >
< input
type = "text"
placeholder = "Add a library — start typing to search…"
value = { newLibName }
onChange = { ( e ) = > setNewLibName ( e . target . value ) }
onKeyDown = { ( e ) = > {
if ( e . key === 'Enter' ) {
const pick = addOptions [ 0 ] ? ? newLibName ;
if ( pick . trim ( ) ) {
addToManifest ( pick ) ;
setNewLibName ( '' ) ;
setAddSuggestions ( [ ] ) ;
}
} else if ( e . key === 'Escape' ) {
setNewLibName ( '' ) ;
setAddSuggestions ( [ ] ) ;
}
} }
/ >
< / div >
{ addOptions . length > 0 && (
< div
style = { {
position : 'absolute' ,
left : 0 ,
right : 0 ,
zIndex : 5 ,
background : '#252526' ,
border : '1px solid #3c3c3c' ,
borderRadius : 4 ,
marginTop : 2 ,
maxHeight : 200 ,
overflowY : 'auto' ,
boxShadow : '0 6px 16px rgba(0,0,0,0.4)' ,
} }
>
{ addOptions . map ( ( opt ) = > (
< button
key = { opt }
onClick = { ( ) = > {
addToManifest ( opt ) ;
setNewLibName ( '' ) ;
setAddSuggestions ( [ ] ) ;
} }
style = { {
display : 'block' ,
width : '100%' ,
textAlign : 'left' ,
padding : '7px 12px' ,
background : 'transparent' ,
border : 'none' ,
color : '#d4d4d4' ,
fontSize : 13 ,
cursor : 'pointer' ,
} }
onMouseEnter = { ( e ) = > ( e . currentTarget . style . background = '#094771' ) }
onMouseLeave = { ( e ) = > ( e . currentTarget . style . background = 'transparent' ) }
>
{ opt }
< / button >
) ) }
< / div >
) }
2026-06-07 10:08:34 +07:00
< / div >
{ /* Raw velxio.json editor (Wokwi-style) for power users */ }
< details style = { { padding : '6px 14px 14px' , color : '#9d9d9d' , fontSize : 12 } } >
< summary style = { { cursor : 'pointer' , userSelect : 'none' } } >
Edit velxio . json directly
< / summary >
< textarea
value = { jsonDraft }
onChange = { ( e ) = > setJsonDraft ( e . target . value ) }
spellCheck = { false }
style = { {
width : '100%' ,
minHeight : 110 ,
marginTop : 8 ,
background : '#1e1e1e' ,
color : '#d4d4d4' ,
border : '1px solid #2d2d2d' ,
borderRadius : 4 ,
fontFamily : 'monospace' ,
fontSize : 12 ,
padding : 8 ,
boxSizing : 'border-box' ,
} }
/ >
{ jsonError && (
< div className = "lib-status error" style = { { marginTop : 6 } } >
{ jsonError }
< / div >
) }
< button className = "lib-install-btn" style = { { marginTop : 8 } } onClick = { applyJsonDraft } >
Apply velxio . json
< / button >
< / details >
< / div >
) }
2026-04-22 02:45:45 +07:00
{ /* Search Tab */ }
{ activeTab === 'search' && (
< div className = "lib-content" >
< div className = "lib-search-bar" >
< svg
className = "lib-search-icon"
viewBox = "0 0 24 24"
fill = "none"
stroke = "currentColor"
strokeWidth = "2"
strokeLinecap = "round"
strokeLinejoin = "round"
>
< circle cx = "11" cy = "11" r = "8" / >
< path d = "m21 21-4.3-4.3" / >
< / svg >
< input
type = "text"
2026-05-09 21:29:07 +07:00
placeholder = { t ( 'editor.libraryManager.filterPlaceholder' ) }
2026-04-22 02:45:45 +07:00
value = { searchQuery }
onChange = { ( e ) = > setSearchQuery ( e . target . value ) }
autoFocus
/ >
{ loadingSearch && (
< svg
className = "lib-spinner"
viewBox = "0 0 24 24"
fill = "none"
stroke = "currentColor"
strokeWidth = "2.5"
strokeLinecap = "round"
>
< path d = "M21 12a9 9 0 1 1-6.219-8.56" / >
< / svg >
) }
< / div >
< div className = "lib-list" >
{ loadingSearch && (
< div className = "lib-empty" >
< svg
className = "lib-spinner lib-spinner-center"
viewBox = "0 0 24 24"
fill = "none"
stroke = "currentColor"
strokeWidth = "2.5"
strokeLinecap = "round"
>
< path d = "M21 12a9 9 0 1 1-6.219-8.56" / >
< / svg >
< p className = "lib-empty-sub" >
2026-05-09 21:29:07 +07:00
{ searchQuery
? t ( 'editor.libraryManager.searchingFor' , { query : searchQuery } )
: t ( 'editor.libraryManager.loadingLibraries' ) }
2026-04-22 02:45:45 +07:00
< / p >
< / div >
) }
{ ! loadingSearch && searchResults . length === 0 && (
< div className = "lib-empty" >
< p >
{ searchQuery
2026-05-09 21:29:07 +07:00
? t ( 'editor.libraryManager.noResultsFor' , { query : searchQuery } )
: t ( 'editor.libraryManager.noResults' ) }
2026-04-22 02:45:45 +07:00
< / p >
< / div >
) }
{ ! loadingSearch &&
searchResults . map ( ( lib , i ) = > (
< div key = { i } className = "lib-item" >
< div className = "lib-item-info" >
< div className = "lib-item-header" >
< span className = "lib-item-name" > { getLibName ( lib ) } < / span >
{ getLibAuthor ( lib ) && (
2026-05-09 21:29:07 +07:00
< span className = "lib-item-author" >
{ t ( 'editor.libraryManager.byAuthor' , { author : getLibAuthor ( lib ) } ) }
< / span >
2026-04-22 02:45:45 +07:00
) }
< / div >
{ getLibDesc ( lib ) && < p className = "lib-item-desc" > { getLibDesc ( lib ) } < / p > }
< / div >
< div className = "lib-item-actions" >
{ isInstalled ( getLibName ( lib ) ) ? (
2026-05-02 14:24:09 +07:00
< >
< span className = "lib-item-version lib-installed-badge" >
{ selectedVersions [ lib . name ] ? ? lib . latest ? . version ? ? '' }
< svg style = { { display : 'inline' , marginLeft : '4px' , verticalAlign : 'middle' } } width = "12" height = "12" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "3" strokeLinecap = "round" strokeLinejoin = "round" > < polyline points = "20 6 9 17 4 12" / > < / svg >
< / span >
< button
className = "lib-uninstall-btn"
onClick = { ( ) = > handleUninstall ( getLibName ( lib ) ) }
disabled = { uninstallingLib !== null }
2026-04-22 02:45:45 +07:00
>
2026-05-09 21:29:07 +07:00
{ uninstallingLib === getLibName ( lib )
? t ( 'editor.libraryManager.uninstalling' )
: t ( 'editor.libraryManager.uninstall' ) }
2026-05-02 14:24:09 +07:00
< / button >
< / >
2026-04-22 02:45:45 +07:00
) : (
2026-05-02 14:24:09 +07:00
< >
{ lib . releases && Object . keys ( lib . releases ) . length > 1 && (
< select
className = "lib-version-select"
value = { selectedVersions [ lib . name ] ? ? lib . latest ? . version ? ? '' }
onChange = { ( e ) = > setSelectedVersions ( ( prev ) = > ( { . . . prev , [ lib . name ] : e . target . value } ) ) }
>
{ Object . entries ( lib . releases ) . map ( ( [ ver ] ) = > (
< option key = { ver } value = { ver } > { ver } < / option >
) ) }
< / select >
2026-04-22 02:45:45 +07:00
) }
2026-05-02 14:24:09 +07:00
< button
className = "lib-install-btn"
onClick = { ( ) = > handleInstall ( getLibName ( lib ) ) }
disabled = { installingLib !== null }
>
{ installingLib === getLibName ( lib ) ? (
2026-05-09 21:29:07 +07:00
< span className = "lib-installing" > { t ( 'editor.libraryManager.installing' ) } < / span >
2026-05-02 14:24:09 +07:00
) : (
2026-05-09 21:29:07 +07:00
t ( 'editor.libraryManager.install' )
2026-05-02 14:24:09 +07:00
) }
< / button >
< / >
2026-04-22 02:45:45 +07:00
) }
2026-03-05 08:05:23 +07:00
< / div >
2026-04-22 02:45:45 +07:00
< / div >
) ) }
< / div >
< / div >
) }
2026-03-05 08:05:23 +07:00
2026-04-22 02:45:45 +07:00
{ /* Installed Tab */ }
{ activeTab === 'installed' && (
< div className = "lib-content" >
< div className = "lib-list" >
{ loadingInstalled && (
< div className = "lib-empty" >
2026-05-09 21:29:07 +07:00
< p > { t ( 'editor.libraryManager.loadingInstalled' ) } < / p >
2026-04-22 02:45:45 +07:00
< / div >
) }
{ ! loadingInstalled && installedLibraries . length === 0 && (
< div className = "lib-empty" >
2026-05-09 21:29:07 +07:00
< p > { t ( 'editor.libraryManager.noneInstalled' ) } < / p >
< p className = "lib-empty-sub" > { t ( 'editor.libraryManager.useSearchTab' ) } < / p >
2026-04-22 02:45:45 +07:00
< / div >
) }
{ installedLibraries . map ( ( lib , i ) = > (
< div key = { i } className = "lib-item" >
< div className = "lib-item-info" >
< div className = "lib-item-header" >
< span className = "lib-item-name" > { getInstalledName ( lib ) } < / span >
{ getInstalledAuthor ( lib ) && (
2026-05-09 21:29:07 +07:00
< span className = "lib-item-author" >
{ t ( 'editor.libraryManager.byAuthor' , { author : getInstalledAuthor ( lib ) } ) }
< / span >
2026-04-22 02:45:45 +07:00
) }
2026-03-05 08:05:23 +07:00
< / div >
2026-04-22 02:45:45 +07:00
{ getInstalledDesc ( lib ) && (
< p className = "lib-item-desc" > { getInstalledDesc ( lib ) } < / p >
) }
< / div >
< div className = "lib-item-actions" >
{ getInstalledVersion ( lib ) && (
< span className = "lib-item-version lib-installed-badge" >
{ getInstalledVersion ( lib ) }
< svg
style = { { display : 'inline' , marginLeft : '4px' , verticalAlign : 'middle' } }
width = "12"
height = "12"
viewBox = "0 0 24 24"
fill = "none"
stroke = "currentColor"
strokeWidth = "3"
strokeLinecap = "round"
strokeLinejoin = "round"
>
< polyline points = "20 6 9 17 4 12" / >
< / svg >
< / span >
) }
2026-06-07 10:08:34 +07:00
{ inManifest ( getInstalledName ( lib ) ) ? (
< button
className = "lib-uninstall-btn"
onClick = { ( ) = > removeFromManifest ( getInstalledName ( lib ) ) }
title = "Remove from this project (velxio.json)"
>
In project ✓
< / button >
) : (
< button
className = "lib-install-btn"
onClick = { ( ) = > addToManifest ( getInstalledName ( lib ) ) }
title = "Add to this project (velxio.json)"
>
Add to project
< / button >
) }
2026-05-02 14:24:09 +07:00
< button
className = "lib-uninstall-btn"
2026-06-08 00:58:13 +07:00
onClick = { ( ) = >
lib . custom
? handleRemoveCustom ( getInstalledName ( lib ) )
: handleUninstall ( getInstalledName ( lib ) )
}
2026-05-02 14:24:09 +07:00
disabled = { uninstallingLib !== null }
2026-06-08 00:58:13 +07:00
title = { lib . custom ? 'Remove your custom upload' : 'Uninstall library' }
2026-05-02 14:24:09 +07:00
>
2026-06-08 00:58:13 +07:00
{ uninstallingLib === getInstalledName ( lib )
? 'Removing...'
: lib . custom
? 'Remove'
: 'UNINSTALL' }
2026-05-02 14:24:09 +07:00
< / button >
2026-04-22 02:45:45 +07:00
< / div >
< / div >
) ) }
2026-03-05 08:05:23 +07:00
< / div >
2026-04-22 02:45:45 +07:00
< / div >
) }
< / div >
< / div >
) ;
2026-03-05 08:05:23 +07:00
} ;