import { ExternalLinkIcon, PaletteIcon, PlusIcon, RefreshCwIcon, SearchIcon, ShieldCheckIcon, } from "lucide-react"; import type { FormEvent } from "react"; import { useCallback, useEffect, useRef, useState } from "react"; import { importOpenVsxThemeExtension, searchOpenVsxThemes, type OpenVsxThemeExtension, type OpenVsxThemeSort, } from "../../openVsxThemes"; import { getCustomThemes, getStoredCustomThemeCollection, replaceCustomThemeCollection, type ThemeDefinition, } from "../../themePalette"; import { AlertDialog, AlertDialogClose, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogPopup, AlertDialogTitle, } from "../ui/alert-dialog"; import { Button } from "../ui/button"; import { InputGroup, InputGroupAddon, InputGroupInput } from "../ui/input-group"; import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select"; import { Spinner } from "../ui/spinner"; const DOWNLOAD_FORMAT = new Intl.NumberFormat(undefined, { notation: "compact", maximumFractionDigits: 1, }); const SUGGESTED_SEARCHES = ["Dracula", "Catppuccin", "Nord", "Tokyo Night"]; const SORT_OPTIONS: ReadonlyArray<{ value: OpenVsxThemeSort; label: string }> = [ { value: "downloadCount", label: "Most downloaded" }, { value: "rating", label: "Best rated" }, { value: "timestamp", label: "Newest" }, { value: "relevance", label: "Most relevant" }, ]; function ThemeExtensionIcon({ extension }: { extension: OpenVsxThemeExtension }) { const [failed, setFailed] = useState(false); return (
{extension.iconUrl && !failed ? ( setFailed(true)} /> ) : ( )}
); } export function ThemeSearchSection({ open, onInstalled, }: { open: boolean; onInstalled: (themes: ReadonlyArray, context: { updated: boolean }) => void; }) { const [query, setQuery] = useState(""); const [sortBy, setSortBy] = useState("downloadCount"); const [results, setResults] = useState | null>(null); const [error, setError] = useState(null); const [isSearching, setIsSearching] = useState(false); const [installingId, setInstallingId] = useState(null); const [pendingUpdate, setPendingUpdate] = useState(null); const requestRef = useRef(null); useEffect(() => { requestRef.current?.abort(); requestRef.current = null; if (open) { setQuery(""); setSortBy("downloadCount"); setResults(null); setError(null); setIsSearching(false); setInstallingId(null); setPendingUpdate(null); } return () => { requestRef.current?.abort(); requestRef.current = null; }; }, [open]); const runSearch = useCallback( async (searchText: string, nextSort = sortBy) => { const trimmed = searchText.trim(); if (!trimmed) return; requestRef.current?.abort(); const controller = new AbortController(); requestRef.current = controller; setQuery(trimmed); setIsSearching(true); setError(null); setResults(null); try { const nextResults = await searchOpenVsxThemes(trimmed, { signal: controller.signal, sortBy: nextSort, }); if (!controller.signal.aborted) setResults(nextResults); } catch (cause) { if (!controller.signal.aborted) { setError(cause instanceof Error ? cause.message : "Open VSX search failed."); } } if (requestRef.current === controller) { requestRef.current = null; setIsSearching(false); } }, [sortBy], ); const handleSearch = useCallback( (event: FormEvent) => { event.preventDefault(); void runSearch(query); }, [query, runSearch], ); const handleSortChange = useCallback( (value: OpenVsxThemeSort | null) => { const nextSort = SORT_OPTIONS.find((option) => option.value === value)?.value; if (!nextSort) return; setSortBy(nextSort); if ((results !== null || isSearching) && query.trim()) { void runSearch(query, nextSort); } }, [isSearching, query, results, runSearch], ); const handleInstall = useCallback( async (extension: OpenVsxThemeExtension, allowUpdate: boolean) => { setError(null); let installedCollection: ReadonlyArray; try { installedCollection = getStoredCustomThemeCollection(extension.collectionId); } catch (cause) { setError(cause instanceof Error ? cause.message : "Installed themes could not be read."); return; } const updated = installedCollection.length > 0; if (updated && !allowUpdate) { setPendingUpdate(extension); return; } requestRef.current?.abort(); const controller = new AbortController(); requestRef.current = controller; setInstallingId(extension.id); try { const themes = await importOpenVsxThemeExtension(extension, controller.signal); if (!controller.signal.aborted) { const imported = replaceCustomThemeCollection(extension.collectionId, themes, { expectedCollection: installedCollection, }); onInstalled(imported, { updated }); } } catch (cause) { if (!controller.signal.aborted) { setError(cause instanceof Error ? cause.message : "That theme could not be added."); } } if (requestRef.current === controller) { requestRef.current = null; setInstallingId(null); } }, [onInstalled], ); return (

Search community themes

Find open-source themes from Open VSX.

setQuery(event.currentTarget.value)} placeholder="try dracula, nord, catppuccin..." size="lg" type="search" value={query} />
{!isSearching ? (

Popular

{SUGGESTED_SEARCHES.map((suggestion) => ( ))}
{results && results.length > 0 ? (

Sort

) : null}
) : null}
{isSearching ? "Finding themes..." : results ? `${results.length} supported ${results.length === 1 ? "theme" : "themes"} found.` : ""}
{error ? (
{error}
) : null} {isSearching ? (
Finding themes...
) : null} {results ? ( results.length === 0 ? (

No supported open-source themes found

Try a broader search.

) : (
{results.map((extension) => { const isInstalling = installingId === extension.id; const isInstalled = getCustomThemes().some( (theme) => theme.collection?.id === extension.collectionId, ); const action = isInstalled ? "Update" : "Add"; const progressAction = isInstalled ? "Updating" : "Adding"; return (

{extension.name}

{extension.publisher} · {DOWNLOAD_FORMAT.format(extension.downloadCount)}{" "} downloads

{extension.description || "A community color theme for your editor."}

{extension.license} {extension.sourceUrl ? ( Source ) : null}
); })}
) ) : null} { if (!nextOpen) setPendingUpdate(null); }} > Update “{pendingUpdate?.name}”? This replaces its installed variants, including any local edits. Variants no longer in the extension will be removed. }>Cancel
); }