File size: 7,658 Bytes
5a0b87c 0b6a8e8 5a0b87c 0b6a8e8 5a0b87c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | "use client";
import { useState} from "react";
import { motion } from "framer-motion";
import {
Package, Search, Download,
ToggleLeft, ToggleRight, RefreshCw
} from "lucide-react";
import { BUILTIN_EXTENSIONS } from "@/constants/extensions";
import { toast } from "sonner";
type ExtCategory = "all" | "installed" | "Language" | "Snippets" | "Formatter" | "Linter" | "SCM" | "Tools";
const CATEGORIES: { id: ExtCategory; label: string }[] = [
{ id: "all", label: "All" },
{ id: "installed", label: "Installed" },
{ id: "Language", label: "Language" },
{ id: "Snippets", label: "Snippets" },
{ id: "Formatter", label: "Formatter" },
{ id: "Linter", label: "Linter" },
{ id: "SCM", label: "SCM" },
];
export default function ExtensionPanel() {
const [search, setSearch] = useState("");
const [category, setCategory] = useState<ExtCategory>("all");
const [enabled, setEnabled] = useState<Record<string, boolean>>(() =>
Object.fromEntries(BUILTIN_EXTENSIONS.map((e) => [e.id, e.enabled]))
);
const [installed, setInstalled] = useState<Record<string, boolean>>(() =>
Object.fromEntries(BUILTIN_EXTENSIONS.map((e) => [e.id, e.preinstalled]))
);
const [installing, setInstalling] = useState<string | null>(null);
const toggle = (id: string) => {
setEnabled((prev) => ({ ...prev, [id]: !prev[id] }));
toast.success(enabled[id] ? "Extension disabled" : "Extension enabled");
};
const install = async (id: string) => {
setInstalling(id);
await new Promise((r) => setTimeout(r, 1200));
setInstalled((prev) => ({ ...prev, [id]: true }));
setEnabled((prev) => ({ ...prev, [id]: true }));
setInstalling(null);
toast.success(`Extension installed!`);
};
const filtered = BUILTIN_EXTENSIONS.filter((ext) => {
const matchSearch = search
? ext.name.toLowerCase().includes(search.toLowerCase()) ||
ext.description.toLowerCase().includes(search.toLowerCase())
: true;
const matchCat =
category === "all" ? true :
category === "installed" ? installed[ext.id] :
ext.category === category;
return matchSearch && matchCat;
});
return (
<div className="sidebar h-full flex flex-col overflow-hidden">
{/* Header */}
<div className="sidebar-header">
<span>Extensions</span>
<button className="activity-btn w-6 h-6" title="Refresh">
<RefreshCw size={12} />
</button>
</div>
{/* Search */}
<div className="px-2 py-2">
<div className="relative">
<Search size={12} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-(--text-muted)" />
<input
className="input text-xs pl-7 py-1.5"
placeholder="Search extensions…"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
</div>
{/* Category tabs */}
<div className="flex gap-1 px-2 pb-2 overflow-x-auto shrink-0">
{CATEGORIES.map((cat) => (
<button
key={cat.id}
onClick={() => setCategory(cat.id)}
className={`px-2.5 py-0.5 rounded text-[10px] font-medium whitespace-nowrap transition-all shrink-0 ${category === cat.id ? "bg-(--accent) text-(--text-on-accent)" : "bg-(--bg-3) text-(--text-2) hover:bg-(--surface-hover)"}`}
>
{cat.label}
</button>
))}
</div>
{/* Extension list */}
<div className="flex-1 overflow-y-auto px-2 py-1 flex flex-col gap-1.5">
{filtered.map((ext) => (
<motion.div
key={ext.id}
layout
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="card p-3 gap-0"
>
<div className="flex items-start gap-2">
<div className="w-9 h-9 rounded-lg bg-(--bg-2) flex items-center justify-center text-lg shrink-0">
{ext.icon}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-1.5 flex-wrap">
<span className="font-medium text-xs text-(--text) truncate">{ext.name}</span>
{installed[ext.id] && (
<span className="badge badge-accent text-[8px]">Installed</span>
)}
</div>
<p className="text-(--text-muted) text-[10px] mt-0.5 line-clamp-2">{ext.description}</p>
<div className="flex items-center gap-1.5 mt-1.5">
<span className="text-[9px] text-(--text-muted)">{ext.publisher} · v{ext.version}</span>
<span className="badge bg-(--bg-3) text-(--text-muted) text-[8px]">{ext.category}</span>
</div>
</div>
</div>
{/* Actions */}
<div className="flex items-center justify-end gap-1.5 mt-2 pt-2 border-t border-(--border-subtle)">
{installed[ext.id] ? (
<button
onClick={() => toggle(ext.id)}
className={`flex items-center gap-1 text-[10px] px-2 py-1 rounded transition-all ${enabled[ext.id] ? "text-(--accent) bg-[rgba(57,211,83,0.08)]" : "text-(--text-muted) bg-(--bg-2)"}`}
>
{enabled[ext.id] ? <ToggleRight size={12} /> : <ToggleLeft size={12} />}
{enabled[ext.id] ? "Enabled" : "Disabled"}
</button>
) : (
<button
onClick={() => install(ext.id)}
disabled={installing === ext.id}
className="btn btn-primary py-1 px-2 text-[10px] gap-1"
>
{installing === ext.id ? (
<><span className="w-2.5 h-2.5 rounded-full border border-(--text-on-accent) border-t-transparent animate-spin" />Installing…</>
) : (
<><Download size={10} /> Install</>
)}
</button>
)}
</div>
</motion.div>
))}
{filtered.length === 0 && (
<div className="text-center py-8 text-(--text-muted) text-xs">
<Package size={24} className="mx-auto mb-2 opacity-30" />
<p>No extensions found</p>
</div>
)}
</div>
</div>
);
}
|