@nextclaw/ui 0.5.2 → 0.5.4
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.
- package/CHANGELOG.md +13 -0
- package/dist/assets/index-BHGBLfqi.css +1 -0
- package/dist/assets/index-CWt_NEq3.js +332 -0
- package/dist/index.html +2 -2
- package/package.json +2 -1
- package/src/components/marketplace/MarketplacePage.tsx +188 -351
- package/src/components/ui/tooltip.tsx +30 -0
- package/src/hooks/useMarketplace.ts +4 -0
- package/dist/assets/index-CKTsCtI-.css +0 -1
- package/dist/assets/index-D8W5lAHk.js +0 -337
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
import { Download, PackageSearch, Sparkles, Store } from 'lucide-react';
|
|
3
|
-
import { cn } from '@/lib/utils';
|
|
4
|
-
import { Tabs } from '@/components/ui/tabs-custom';
|
|
5
|
-
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
6
|
-
import { useInstallMarketplaceItem, useManageMarketplaceItem, useMarketplaceInstalled, useMarketplaceItems, useMarketplaceRecommendations } from '@/hooks/useMarketplace';
|
|
1
|
+
/* eslint-disable max-lines-per-function */
|
|
7
2
|
import type { MarketplaceInstalledRecord, MarketplaceItemSummary, MarketplaceManageAction, MarketplaceSort } from '@/api/types';
|
|
3
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
4
|
+
import { Tabs } from '@/components/ui/tabs-custom';
|
|
5
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
6
|
+
import {
|
|
7
|
+
useInstallMarketplaceItem,
|
|
8
|
+
useManageMarketplaceItem,
|
|
9
|
+
useMarketplaceInstalled,
|
|
10
|
+
useMarketplaceItems
|
|
11
|
+
} from '@/hooks/useMarketplace';
|
|
12
|
+
import { cn } from '@/lib/utils';
|
|
13
|
+
import { PackageSearch } from 'lucide-react';
|
|
14
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
8
15
|
|
|
9
16
|
const PAGE_SIZE = 12;
|
|
10
17
|
|
|
@@ -22,11 +29,6 @@ type ManageState = {
|
|
|
22
29
|
action?: MarketplaceManageAction;
|
|
23
30
|
};
|
|
24
31
|
|
|
25
|
-
type InstalledSpecSets = {
|
|
26
|
-
plugin: Set<string>;
|
|
27
|
-
skill: Set<string>;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
32
|
type InstalledRenderEntry = {
|
|
31
33
|
key: string;
|
|
32
34
|
record: MarketplaceInstalledRecord;
|
|
@@ -42,37 +44,6 @@ function toLookupKey(type: MarketplaceItemSummary['type'], value: string | undef
|
|
|
42
44
|
return normalized.length > 0 ? `${type}:${normalized}` : '';
|
|
43
45
|
}
|
|
44
46
|
|
|
45
|
-
function buildInstalledSpecSets(
|
|
46
|
-
records: { pluginSpecs: string[]; skillSpecs: string[]; records: MarketplaceInstalledRecord[] } | undefined
|
|
47
|
-
): InstalledSpecSets {
|
|
48
|
-
const plugin = new Set((records?.pluginSpecs ?? []).map((value) => normalizeMarketplaceKey(value)).filter(Boolean));
|
|
49
|
-
const skill = new Set((records?.skillSpecs ?? []).map((value) => normalizeMarketplaceKey(value)).filter(Boolean));
|
|
50
|
-
|
|
51
|
-
for (const record of records?.records ?? []) {
|
|
52
|
-
const target = record.type === 'plugin' ? plugin : skill;
|
|
53
|
-
const specKey = normalizeMarketplaceKey(record.spec);
|
|
54
|
-
if (specKey) {
|
|
55
|
-
target.add(specKey);
|
|
56
|
-
}
|
|
57
|
-
const labelKey = normalizeMarketplaceKey(record.label);
|
|
58
|
-
if (labelKey) {
|
|
59
|
-
target.add(labelKey);
|
|
60
|
-
}
|
|
61
|
-
const idKey = normalizeMarketplaceKey(record.id);
|
|
62
|
-
if (idKey) {
|
|
63
|
-
target.add(idKey);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return { plugin, skill };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function isInstalled(item: MarketplaceItemSummary, sets: InstalledSpecSets): boolean {
|
|
71
|
-
const target = item.type === 'plugin' ? sets.plugin : sets.skill;
|
|
72
|
-
const candidateKeys = [item.install.spec, item.slug, item.id].map((value) => normalizeMarketplaceKey(value)).filter(Boolean);
|
|
73
|
-
return candidateKeys.some((key) => target.has(key));
|
|
74
|
-
}
|
|
75
|
-
|
|
76
47
|
function buildCatalogLookup(items: MarketplaceItemSummary[]): Map<string, MarketplaceItemSummary> {
|
|
77
48
|
const lookup = new Map<string, MarketplaceItemSummary>();
|
|
78
49
|
|
|
@@ -94,7 +65,7 @@ function buildInstalledRecordLookup(records: MarketplaceInstalledRecord[]): Map<
|
|
|
94
65
|
const lookup = new Map<string, MarketplaceInstalledRecord>();
|
|
95
66
|
|
|
96
67
|
for (const record of records) {
|
|
97
|
-
const candidates = [record.spec, record.
|
|
68
|
+
const candidates = [record.spec, record.id, record.label];
|
|
98
69
|
for (const candidate of candidates) {
|
|
99
70
|
const lookupKey = toLookupKey(record.type, candidate);
|
|
100
71
|
if (!lookupKey || lookup.has(lookupKey)) {
|
|
@@ -107,21 +78,6 @@ function buildInstalledRecordLookup(records: MarketplaceInstalledRecord[]): Map<
|
|
|
107
78
|
return lookup;
|
|
108
79
|
}
|
|
109
80
|
|
|
110
|
-
function findCatalogItemForRecord(
|
|
111
|
-
record: MarketplaceInstalledRecord,
|
|
112
|
-
catalogLookup: Map<string, MarketplaceItemSummary>
|
|
113
|
-
): MarketplaceItemSummary | undefined {
|
|
114
|
-
const bySpec = catalogLookup.get(toLookupKey(record.type, record.spec));
|
|
115
|
-
if (bySpec) {
|
|
116
|
-
return bySpec;
|
|
117
|
-
}
|
|
118
|
-
const byId = catalogLookup.get(toLookupKey(record.type, record.id));
|
|
119
|
-
if (byId) {
|
|
120
|
-
return byId;
|
|
121
|
-
}
|
|
122
|
-
return catalogLookup.get(toLookupKey(record.type, record.label));
|
|
123
|
-
}
|
|
124
|
-
|
|
125
81
|
function findInstalledRecordForItem(
|
|
126
82
|
item: MarketplaceItemSummary,
|
|
127
83
|
installedRecordLookup: Map<string, MarketplaceInstalledRecord>
|
|
@@ -140,6 +96,23 @@ function findInstalledRecordForItem(
|
|
|
140
96
|
return undefined;
|
|
141
97
|
}
|
|
142
98
|
|
|
99
|
+
function findCatalogItemForRecord(
|
|
100
|
+
record: MarketplaceInstalledRecord,
|
|
101
|
+
catalogLookup: Map<string, MarketplaceItemSummary>
|
|
102
|
+
): MarketplaceItemSummary | undefined {
|
|
103
|
+
const bySpec = catalogLookup.get(toLookupKey(record.type, record.spec));
|
|
104
|
+
if (bySpec) {
|
|
105
|
+
return bySpec;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const byId = catalogLookup.get(toLookupKey(record.type, record.id));
|
|
109
|
+
if (byId) {
|
|
110
|
+
return byId;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return catalogLookup.get(toLookupKey(record.type, record.label));
|
|
114
|
+
}
|
|
115
|
+
|
|
143
116
|
function matchInstalledSearch(
|
|
144
117
|
record: MarketplaceInstalledRecord,
|
|
145
118
|
item: MarketplaceItemSummary | undefined,
|
|
@@ -154,8 +127,6 @@ function matchInstalledSearch(
|
|
|
154
127
|
record.id,
|
|
155
128
|
record.spec,
|
|
156
129
|
record.label,
|
|
157
|
-
record.source,
|
|
158
|
-
record.runtimeStatus,
|
|
159
130
|
item?.name,
|
|
160
131
|
item?.slug,
|
|
161
132
|
item?.summary,
|
|
@@ -168,72 +139,31 @@ function matchInstalledSearch(
|
|
|
168
139
|
.some((value) => value.includes(normalizedQuery));
|
|
169
140
|
}
|
|
170
141
|
|
|
171
|
-
function
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
{type}
|
|
180
|
-
</span>
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function InstalledBadge() {
|
|
185
|
-
return <span className="text-[11px] px-2 py-1 rounded-full font-semibold bg-indigo-50 text-indigo-600">Installed</span>;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function EnabledStateBadge(props: { enabled?: boolean }) {
|
|
189
|
-
if (props.enabled === undefined) {
|
|
190
|
-
return null;
|
|
142
|
+
function getAvatarColor(text: string) {
|
|
143
|
+
const colors = [
|
|
144
|
+
'bg-blue-500', 'bg-indigo-500', 'bg-purple-500', 'bg-pink-500',
|
|
145
|
+
'bg-rose-500', 'bg-orange-500', 'bg-emerald-500', 'bg-teal-500', 'bg-cyan-500'
|
|
146
|
+
];
|
|
147
|
+
let hash = 0;
|
|
148
|
+
for (let i = 0; i < text.length; i++) {
|
|
149
|
+
hash = text.charCodeAt(i) + ((hash << 5) - hash);
|
|
191
150
|
}
|
|
192
|
-
|
|
193
|
-
return props.enabled
|
|
194
|
-
? <span className="text-[11px] px-2 py-1 rounded-full font-semibold bg-emerald-50 text-emerald-600">Enabled</span>
|
|
195
|
-
: <span className="text-[11px] px-2 py-1 rounded-full font-semibold bg-amber-50 text-amber-700">Disabled</span>;
|
|
151
|
+
return colors[Math.abs(hash) % colors.length];
|
|
196
152
|
}
|
|
197
153
|
|
|
198
|
-
function
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}) {
|
|
203
|
-
const targetId = props.record.id || props.record.spec;
|
|
204
|
-
const busyForRecord = props.state.isPending && props.state.targetId === targetId;
|
|
205
|
-
const canToggle = props.record.type === 'plugin';
|
|
206
|
-
const canUninstallPlugin = props.record.type === 'plugin' && props.record.origin !== 'bundled';
|
|
207
|
-
const canUninstallSkill = props.record.type === 'skill' && props.record.source === 'workspace';
|
|
208
|
-
const canUninstall = canUninstallPlugin || canUninstallSkill;
|
|
209
|
-
|
|
154
|
+
function ItemIcon({ name, fallback }: { name?: string; fallback: string }) {
|
|
155
|
+
const displayName = name || fallback;
|
|
156
|
+
const letters = displayName.substring(0, 2).toUpperCase();
|
|
157
|
+
const colorClass = getAvatarColor(displayName);
|
|
210
158
|
return (
|
|
211
|
-
<div className="flex items-center
|
|
212
|
-
{
|
|
213
|
-
<button
|
|
214
|
-
disabled={props.state.isPending}
|
|
215
|
-
onClick={() => props.onAction(props.record.enabled === false ? 'enable' : 'disable', props.record)}
|
|
216
|
-
className="inline-flex items-center h-8 px-3 rounded-lg text-xs font-semibold border border-gray-200 text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50"
|
|
217
|
-
>
|
|
218
|
-
{busyForRecord && props.state.action !== 'uninstall'
|
|
219
|
-
? (props.state.action === 'enable' ? 'Enabling...' : 'Disabling...')
|
|
220
|
-
: (props.record.enabled === false ? 'Enable' : 'Disable')}
|
|
221
|
-
</button>
|
|
222
|
-
)}
|
|
223
|
-
{canUninstall && (
|
|
224
|
-
<button
|
|
225
|
-
disabled={props.state.isPending}
|
|
226
|
-
onClick={() => props.onAction('uninstall', props.record)}
|
|
227
|
-
className="inline-flex items-center h-8 px-3 rounded-lg text-xs font-semibold border border-rose-200 text-rose-600 bg-white hover:bg-rose-50 disabled:opacity-50"
|
|
228
|
-
>
|
|
229
|
-
{busyForRecord && props.state.action === 'uninstall' ? 'Uninstalling...' : 'Uninstall'}
|
|
230
|
-
</button>
|
|
231
|
-
)}
|
|
159
|
+
<div className={cn("flex items-center justify-center w-11 h-11 rounded-2xl text-white font-bold text-base shrink-0 shadow-sm", colorClass)}>
|
|
160
|
+
{letters}
|
|
232
161
|
</div>
|
|
233
162
|
);
|
|
234
163
|
}
|
|
235
164
|
|
|
236
165
|
function FilterPanel(props: {
|
|
166
|
+
scope: ScopeType;
|
|
237
167
|
searchText: string;
|
|
238
168
|
typeFilter: FilterType;
|
|
239
169
|
sort: MarketplaceSort;
|
|
@@ -242,19 +172,18 @@ function FilterPanel(props: {
|
|
|
242
172
|
onSortChange: (value: MarketplaceSort) => void;
|
|
243
173
|
}) {
|
|
244
174
|
return (
|
|
245
|
-
<div className="bg-white border border-gray-200 rounded-
|
|
175
|
+
<div className="bg-white border border-gray-200 rounded-xl p-4 mb-4">
|
|
246
176
|
<div className="flex gap-3 items-center">
|
|
247
177
|
<div className="flex-1 min-w-0 relative">
|
|
248
178
|
<PackageSearch className="h-4 w-4 text-gray-400 absolute left-3 top-1/2 -translate-y-1/2" />
|
|
249
179
|
<input
|
|
250
180
|
value={props.searchText}
|
|
251
181
|
onChange={(event) => props.onSearchTextChange(event.target.value)}
|
|
252
|
-
placeholder="Search
|
|
182
|
+
placeholder="Search extensions..."
|
|
253
183
|
className="w-full h-9 border border-gray-200 rounded-lg pl-9 pr-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary/30"
|
|
254
184
|
/>
|
|
255
185
|
</div>
|
|
256
186
|
|
|
257
|
-
{/* Segmented type filter */}
|
|
258
187
|
<div className="inline-flex h-9 rounded-lg bg-gray-100 p-0.5 shrink-0">
|
|
259
188
|
{([
|
|
260
189
|
{ value: 'all', label: 'All' },
|
|
@@ -277,185 +206,128 @@ function FilterPanel(props: {
|
|
|
277
206
|
))}
|
|
278
207
|
</div>
|
|
279
208
|
|
|
280
|
-
|
|
281
|
-
<
|
|
282
|
-
<
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
<
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
209
|
+
{props.scope === 'all' && (
|
|
210
|
+
<Select value={props.sort} onValueChange={(v) => props.onSortChange(v as MarketplaceSort)}>
|
|
211
|
+
<SelectTrigger className="h-9 w-[150px] shrink-0 rounded-lg">
|
|
212
|
+
<SelectValue />
|
|
213
|
+
</SelectTrigger>
|
|
214
|
+
<SelectContent>
|
|
215
|
+
<SelectItem value="relevance">Relevance</SelectItem>
|
|
216
|
+
<SelectItem value="updated">Recently Updated</SelectItem>
|
|
217
|
+
</SelectContent>
|
|
218
|
+
</Select>
|
|
219
|
+
)}
|
|
289
220
|
</div>
|
|
290
221
|
</div>
|
|
291
222
|
);
|
|
292
223
|
}
|
|
293
224
|
|
|
294
|
-
function
|
|
295
|
-
item
|
|
296
|
-
|
|
297
|
-
installed: boolean;
|
|
298
|
-
onInstall: (item: MarketplaceItemSummary) => void;
|
|
299
|
-
}) {
|
|
300
|
-
const isInstalling = props.installState.isPending && props.installState.installingSpec === props.item.install.spec;
|
|
301
|
-
|
|
302
|
-
if (props.installed) {
|
|
303
|
-
return (
|
|
304
|
-
<button
|
|
305
|
-
disabled
|
|
306
|
-
className="inline-flex items-center gap-1.5 h-8 px-3 rounded-lg text-xs font-semibold bg-gray-100 text-gray-500 cursor-not-allowed"
|
|
307
|
-
>
|
|
308
|
-
Installed
|
|
309
|
-
</button>
|
|
310
|
-
);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
return (
|
|
314
|
-
<button
|
|
315
|
-
onClick={() => props.onInstall(props.item)}
|
|
316
|
-
disabled={props.installState.isPending}
|
|
317
|
-
className="inline-flex items-center gap-1.5 h-8 px-3 rounded-lg text-xs font-semibold bg-gray-900 text-white hover:bg-black disabled:opacity-50"
|
|
318
|
-
>
|
|
319
|
-
<Download className="h-3.5 w-3.5" />
|
|
320
|
-
{isInstalling ? 'Installing...' : 'Install'}
|
|
321
|
-
</button>
|
|
322
|
-
);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
function RecommendationSection(props: {
|
|
326
|
-
items: MarketplaceItemSummary[];
|
|
327
|
-
loading: boolean;
|
|
328
|
-
installState: InstallState;
|
|
329
|
-
installedSets: InstalledSpecSets;
|
|
330
|
-
onInstall: (item: MarketplaceItemSummary) => void;
|
|
331
|
-
}) {
|
|
332
|
-
return (
|
|
333
|
-
<section className="mb-6">
|
|
334
|
-
<div className="flex items-center gap-2 mb-3">
|
|
335
|
-
<Sparkles className="h-4 w-4 text-amber-500" />
|
|
336
|
-
<h3 className="text-[15px] font-bold text-gray-900">Recommended</h3>
|
|
337
|
-
</div>
|
|
338
|
-
|
|
339
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
340
|
-
{props.items.map((item) => {
|
|
341
|
-
const installed = isInstalled(item, props.installedSets);
|
|
342
|
-
return (
|
|
343
|
-
<div key={item.id} className="bg-white border border-gray-200 rounded-xl p-4">
|
|
344
|
-
<div className="flex items-start justify-between gap-3">
|
|
345
|
-
<div>
|
|
346
|
-
<div className="text-[14px] font-semibold text-gray-900">{item.name}</div>
|
|
347
|
-
<div className="text-[12px] text-gray-500 mt-0.5">{item.summary}</div>
|
|
348
|
-
</div>
|
|
349
|
-
<div className="flex items-center gap-2">
|
|
350
|
-
<TypeBadge type={item.type} />
|
|
351
|
-
{installed && <InstalledBadge />}
|
|
352
|
-
</div>
|
|
353
|
-
</div>
|
|
354
|
-
<div className="mt-3 flex items-center justify-between">
|
|
355
|
-
<code className="text-[11px] text-gray-500 bg-gray-100 rounded px-2 py-1">{item.install.spec}</code>
|
|
356
|
-
<InstallButton
|
|
357
|
-
item={item}
|
|
358
|
-
installed={installed}
|
|
359
|
-
installState={props.installState}
|
|
360
|
-
onInstall={props.onInstall}
|
|
361
|
-
/>
|
|
362
|
-
</div>
|
|
363
|
-
</div>
|
|
364
|
-
);
|
|
365
|
-
})}
|
|
366
|
-
|
|
367
|
-
{props.loading && <div className="text-[13px] text-gray-500">Loading recommendations...</div>}
|
|
368
|
-
{!props.loading && props.items.length === 0 && <div className="text-[13px] text-gray-500">No recommendations yet.</div>}
|
|
369
|
-
</div>
|
|
370
|
-
</section>
|
|
371
|
-
);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function MarketplaceItemCard(props: {
|
|
375
|
-
item: MarketplaceItemSummary;
|
|
376
|
-
installedRecord?: MarketplaceInstalledRecord;
|
|
225
|
+
function MarketplaceListCard(props: {
|
|
226
|
+
item?: MarketplaceItemSummary;
|
|
227
|
+
record?: MarketplaceInstalledRecord;
|
|
377
228
|
installState: InstallState;
|
|
378
229
|
manageState: ManageState;
|
|
379
|
-
installed: boolean;
|
|
380
230
|
onInstall: (item: MarketplaceItemSummary) => void;
|
|
381
231
|
onManage: (action: MarketplaceManageAction, record: MarketplaceInstalledRecord) => void;
|
|
382
232
|
}) {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
{props.installed && <InstalledBadge />}
|
|
390
|
-
{props.installed && <EnabledStateBadge enabled={props.installedRecord?.enabled} />}
|
|
391
|
-
</div>
|
|
392
|
-
</div>
|
|
233
|
+
const record = props.record;
|
|
234
|
+
const pluginRecord = record?.type === 'plugin' ? record : undefined;
|
|
235
|
+
const type = props.item?.type ?? record?.type;
|
|
236
|
+
const title = props.item?.name ?? record?.label ?? record?.id ?? record?.spec ?? 'Unknown Item';
|
|
237
|
+
const summary = props.item?.summary ?? (record ? 'Installed locally. Details are currently unavailable from marketplace.' : '');
|
|
238
|
+
const spec = props.item?.install.spec ?? record?.spec ?? '';
|
|
393
239
|
|
|
394
|
-
|
|
240
|
+
const targetId = record?.id || record?.spec;
|
|
241
|
+
const busyForRecord = Boolean(targetId) && props.manageState.isPending && props.manageState.targetId === targetId;
|
|
395
242
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
</span>
|
|
401
|
-
))}
|
|
402
|
-
</div>
|
|
403
|
-
|
|
404
|
-
<div className="mt-3 text-[11px] text-gray-500">By {props.item.author}</div>
|
|
243
|
+
const canToggle = Boolean(pluginRecord);
|
|
244
|
+
const canUninstallPlugin = record?.type === 'plugin' && record.origin !== 'bundled';
|
|
245
|
+
const canUninstallSkill = record?.type === 'skill' && record.source === 'workspace';
|
|
246
|
+
const canUninstall = Boolean(canUninstallPlugin || canUninstallSkill);
|
|
405
247
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
{props.installed && props.installedRecord ? (
|
|
409
|
-
<RecordActionButtons
|
|
410
|
-
record={props.installedRecord}
|
|
411
|
-
state={props.manageState}
|
|
412
|
-
onAction={props.onManage}
|
|
413
|
-
/>
|
|
414
|
-
) : (
|
|
415
|
-
<InstallButton
|
|
416
|
-
item={props.item}
|
|
417
|
-
installed={props.installed}
|
|
418
|
-
installState={props.installState}
|
|
419
|
-
onInstall={props.onInstall}
|
|
420
|
-
/>
|
|
421
|
-
)}
|
|
422
|
-
</div>
|
|
423
|
-
</article>
|
|
424
|
-
);
|
|
425
|
-
}
|
|
248
|
+
const isDisabled = record ? (record.enabled === false || record.runtimeStatus === 'disabled') : false;
|
|
249
|
+
const isInstalling = props.installState.isPending && props.item && props.installState.installingSpec === props.item.install.spec;
|
|
426
250
|
|
|
427
|
-
|
|
428
|
-
record: MarketplaceInstalledRecord;
|
|
429
|
-
manageState: ManageState;
|
|
430
|
-
onManage: (action: MarketplaceManageAction, record: MarketplaceInstalledRecord) => void;
|
|
431
|
-
}) {
|
|
432
|
-
const installedAt = props.record.installedAt ? new Date(props.record.installedAt).toLocaleString() : undefined;
|
|
433
|
-
const sourceHint = props.record.source ? `source: ${props.record.source}` : undefined;
|
|
251
|
+
const displayType = type === 'plugin' ? 'Plugin' : type === 'skill' ? 'Skill' : 'Extension';
|
|
434
252
|
|
|
435
253
|
return (
|
|
436
|
-
<article className="bg-white border border-gray-200 rounded-xl
|
|
437
|
-
<div className="flex
|
|
438
|
-
<
|
|
439
|
-
<div className="flex
|
|
440
|
-
<
|
|
441
|
-
|
|
442
|
-
|
|
254
|
+
<article className="group bg-white border border-transparent hover:border-gray-200 rounded-xl px-4 py-3 hover:shadow-sm transition-all flex items-start gap-4 justify-between cursor-default h-[90px]">
|
|
255
|
+
<div className="flex gap-3 min-w-0 flex-1 h-full items-start">
|
|
256
|
+
<ItemIcon name={title} fallback={spec || 'Ext'} />
|
|
257
|
+
<div className="min-w-0 flex-1 flex flex-col justify-center h-full">
|
|
258
|
+
<TooltipProvider delayDuration={400}>
|
|
259
|
+
<Tooltip>
|
|
260
|
+
<TooltipTrigger asChild>
|
|
261
|
+
<div className="text-[14px] font-semibold text-gray-900 truncate leading-tight cursor-default">{title}</div>
|
|
262
|
+
</TooltipTrigger>
|
|
263
|
+
<TooltipContent className="max-w-[300px] text-xs">
|
|
264
|
+
{title}
|
|
265
|
+
</TooltipContent>
|
|
266
|
+
</Tooltip>
|
|
267
|
+
|
|
268
|
+
<div className="flex items-center gap-1.5 mt-0.5 mb-1.5">
|
|
269
|
+
<span className="text-[11px] text-gray-500 font-medium">{displayType}</span>
|
|
270
|
+
{spec && (
|
|
271
|
+
<>
|
|
272
|
+
<span className="text-[10px] text-gray-300">•</span>
|
|
273
|
+
<Tooltip>
|
|
274
|
+
<TooltipTrigger asChild>
|
|
275
|
+
<span className="text-[11px] text-gray-400 truncate max-w-full font-mono cursor-default">{spec}</span>
|
|
276
|
+
</TooltipTrigger>
|
|
277
|
+
<TooltipContent className="max-w-[300px] text-xs font-mono break-all">
|
|
278
|
+
{spec}
|
|
279
|
+
</TooltipContent>
|
|
280
|
+
</Tooltip>
|
|
281
|
+
</>
|
|
282
|
+
)}
|
|
283
|
+
</div>
|
|
284
|
+
|
|
285
|
+
<Tooltip>
|
|
286
|
+
<TooltipTrigger asChild>
|
|
287
|
+
<p className="text-[12px] text-gray-500/90 line-clamp-1 transition-colors leading-relaxed text-left cursor-default">{summary}</p>
|
|
288
|
+
</TooltipTrigger>
|
|
289
|
+
{summary && (
|
|
290
|
+
<TooltipContent className="max-w-[400px] text-xs leading-relaxed">
|
|
291
|
+
{summary}
|
|
292
|
+
</TooltipContent>
|
|
293
|
+
)}
|
|
294
|
+
</Tooltip>
|
|
295
|
+
</TooltipProvider>
|
|
443
296
|
</div>
|
|
444
297
|
</div>
|
|
445
298
|
|
|
446
|
-
<
|
|
299
|
+
<div className="shrink-0 flex items-center h-full">
|
|
300
|
+
{props.item && !record && (
|
|
301
|
+
<button
|
|
302
|
+
onClick={() => props.onInstall(props.item as MarketplaceItemSummary)}
|
|
303
|
+
disabled={props.installState.isPending}
|
|
304
|
+
className="inline-flex items-center gap-1.5 h-8 px-4 rounded-full text-xs font-semibold bg-gray-900 text-white hover:bg-black disabled:opacity-50 transition-colors"
|
|
305
|
+
>
|
|
306
|
+
{isInstalling ? 'Installing...' : 'Install'}
|
|
307
|
+
</button>
|
|
308
|
+
)}
|
|
447
309
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
310
|
+
{pluginRecord && canToggle && (
|
|
311
|
+
<button
|
|
312
|
+
disabled={props.manageState.isPending}
|
|
313
|
+
onClick={() => props.onManage(isDisabled ? 'enable' : 'disable', pluginRecord)}
|
|
314
|
+
className="inline-flex items-center h-8 px-4 rounded-full text-xs font-semibold border border-gray-200 text-gray-700 bg-white hover:bg-gray-50 hover:border-gray-300 disabled:opacity-50 transition-colors"
|
|
315
|
+
>
|
|
316
|
+
{busyForRecord && props.manageState.action !== 'uninstall'
|
|
317
|
+
? (props.manageState.action === 'enable' ? 'Enabling...' : 'Disabling...')
|
|
318
|
+
: (isDisabled ? 'Enable' : 'Disable')}
|
|
319
|
+
</button>
|
|
320
|
+
)}
|
|
451
321
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
322
|
+
{record && canUninstall && (
|
|
323
|
+
<button
|
|
324
|
+
disabled={props.manageState.isPending}
|
|
325
|
+
onClick={() => props.onManage('uninstall', record)}
|
|
326
|
+
className="inline-flex items-center h-8 px-4 rounded-full text-xs font-semibold border border-rose-100 text-rose-600 bg-white hover:bg-rose-50 hover:border-rose-200 disabled:opacity-50 transition-colors"
|
|
327
|
+
>
|
|
328
|
+
{busyForRecord && props.manageState.action === 'uninstall' ? 'Removing...' : 'Uninstall'}
|
|
329
|
+
</button>
|
|
330
|
+
)}
|
|
459
331
|
</div>
|
|
460
332
|
</article>
|
|
461
333
|
);
|
|
@@ -469,7 +341,7 @@ function PaginationBar(props: {
|
|
|
469
341
|
onNext: () => void;
|
|
470
342
|
}) {
|
|
471
343
|
return (
|
|
472
|
-
<div className="mt-
|
|
344
|
+
<div className="mt-4 flex items-center justify-end gap-2">
|
|
473
345
|
<button
|
|
474
346
|
className="h-8 px-3 rounded-lg border border-gray-200 text-sm text-gray-700 disabled:opacity-40"
|
|
475
347
|
onClick={props.onPrev}
|
|
@@ -516,7 +388,7 @@ export function MarketplacePage() {
|
|
|
516
388
|
page,
|
|
517
389
|
pageSize: PAGE_SIZE
|
|
518
390
|
});
|
|
519
|
-
|
|
391
|
+
|
|
520
392
|
const installMutation = useInstallMarketplaceItem();
|
|
521
393
|
const manageMutation = useManageMarketplaceItem();
|
|
522
394
|
|
|
@@ -524,36 +396,30 @@ export function MarketplacePage() {
|
|
|
524
396
|
() => installedQuery.data?.records ?? [],
|
|
525
397
|
[installedQuery.data?.records]
|
|
526
398
|
);
|
|
527
|
-
|
|
528
|
-
const installedRecordLookup = useMemo(
|
|
529
|
-
() => buildInstalledRecordLookup(installedRecords),
|
|
530
|
-
[installedRecords]
|
|
531
|
-
);
|
|
399
|
+
|
|
532
400
|
const allItems = useMemo(
|
|
533
401
|
() => itemsQuery.data?.items ?? [],
|
|
534
402
|
[itemsQuery.data?.items]
|
|
535
403
|
);
|
|
536
|
-
const recommendations = useMemo(
|
|
537
|
-
() => recommendationsQuery.data?.items ?? [],
|
|
538
|
-
[recommendationsQuery.data?.items]
|
|
539
|
-
);
|
|
540
404
|
|
|
541
405
|
const catalogLookup = useMemo(
|
|
542
|
-
() => buildCatalogLookup(
|
|
543
|
-
[allItems
|
|
406
|
+
() => buildCatalogLookup(allItems),
|
|
407
|
+
[allItems]
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
const installedRecordLookup = useMemo(
|
|
411
|
+
() => buildInstalledRecordLookup(installedRecords),
|
|
412
|
+
[installedRecords]
|
|
544
413
|
);
|
|
545
414
|
|
|
546
415
|
const installedEntries = useMemo<InstalledRenderEntry[]>(() => {
|
|
547
416
|
const entries = installedRecords
|
|
548
417
|
.filter((record) => (typeFilter === 'all' ? true : record.type === typeFilter))
|
|
549
|
-
.map((record) => {
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
item
|
|
555
|
-
};
|
|
556
|
-
})
|
|
418
|
+
.map((record) => ({
|
|
419
|
+
key: `${record.type}:${record.spec}:${record.id ?? ''}`,
|
|
420
|
+
record,
|
|
421
|
+
item: findCatalogItemForRecord(record, catalogLookup)
|
|
422
|
+
}))
|
|
557
423
|
.filter((entry) => matchInstalledSearch(entry.record, entry.item, query));
|
|
558
424
|
|
|
559
425
|
entries.sort((left, right) => {
|
|
@@ -572,9 +438,7 @@ export function MarketplacePage() {
|
|
|
572
438
|
return entries;
|
|
573
439
|
}, [installedRecords, typeFilter, catalogLookup, query]);
|
|
574
440
|
|
|
575
|
-
const total = scope === 'installed'
|
|
576
|
-
? installedEntries.length
|
|
577
|
-
: (itemsQuery.data?.total ?? 0);
|
|
441
|
+
const total = scope === 'installed' ? installedEntries.length : (itemsQuery.data?.total ?? 0);
|
|
578
442
|
const totalPages = scope === 'installed' ? 1 : (itemsQuery.data?.totalPages ?? 0);
|
|
579
443
|
|
|
580
444
|
const listSummary = useMemo(() => {
|
|
@@ -582,21 +446,15 @@ export function MarketplacePage() {
|
|
|
582
446
|
if (installedQuery.isLoading) {
|
|
583
447
|
return 'Loading...';
|
|
584
448
|
}
|
|
585
|
-
|
|
586
|
-
return 'No installed items';
|
|
587
|
-
}
|
|
588
|
-
const installedTotal = installedQuery.data?.total ?? installedEntries.length;
|
|
589
|
-
return `Showing ${installedEntries.length} / ${installedTotal}`;
|
|
449
|
+
return `${installedEntries.length} installed`;
|
|
590
450
|
}
|
|
591
451
|
|
|
592
452
|
if (!itemsQuery.data) {
|
|
593
453
|
return 'Loading...';
|
|
594
454
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
return `Showing ${allItems.length} / ${total}`;
|
|
599
|
-
}, [scope, installedQuery.isLoading, installedQuery.data, installedEntries.length, itemsQuery.data, allItems.length, total]);
|
|
455
|
+
|
|
456
|
+
return `${allItems.length} / ${total}`;
|
|
457
|
+
}, [scope, installedQuery.isLoading, installedEntries.length, itemsQuery.data, allItems.length, total]);
|
|
600
458
|
|
|
601
459
|
const installState: InstallState = {
|
|
602
460
|
isPending: installMutation.isPending,
|
|
@@ -648,15 +506,9 @@ export function MarketplacePage() {
|
|
|
648
506
|
|
|
649
507
|
return (
|
|
650
508
|
<div className="animate-fade-in pb-20">
|
|
651
|
-
<div className="
|
|
652
|
-
<
|
|
653
|
-
|
|
654
|
-
<p className="text-[13px] text-gray-500 mt-1">Search, discover and install plugins/skills.</p>
|
|
655
|
-
</div>
|
|
656
|
-
<div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-indigo-50 text-indigo-600 text-xs font-semibold">
|
|
657
|
-
<Store className="h-3.5 w-3.5" />
|
|
658
|
-
Read-only Catalog
|
|
659
|
-
</div>
|
|
509
|
+
<div className="mb-5">
|
|
510
|
+
<h2 className="text-2xl font-bold text-gray-900">Marketplace</h2>
|
|
511
|
+
<p className="text-[13px] text-gray-500 mt-1">A cleaner extension list focused on install / enable / disable.</p>
|
|
660
512
|
</div>
|
|
661
513
|
|
|
662
514
|
<Tabs
|
|
@@ -666,10 +518,11 @@ export function MarketplacePage() {
|
|
|
666
518
|
setScope(value as ScopeType);
|
|
667
519
|
setPage(1);
|
|
668
520
|
}}
|
|
669
|
-
className="mb-
|
|
521
|
+
className="mb-4"
|
|
670
522
|
/>
|
|
671
523
|
|
|
672
524
|
<FilterPanel
|
|
525
|
+
scope={scope}
|
|
673
526
|
searchText={searchText}
|
|
674
527
|
typeFilter={typeFilter}
|
|
675
528
|
sort={sort}
|
|
@@ -684,19 +537,9 @@ export function MarketplacePage() {
|
|
|
684
537
|
}}
|
|
685
538
|
/>
|
|
686
539
|
|
|
687
|
-
{scope === 'all' && (
|
|
688
|
-
<RecommendationSection
|
|
689
|
-
items={recommendations}
|
|
690
|
-
loading={recommendationsQuery.isLoading}
|
|
691
|
-
installState={installState}
|
|
692
|
-
installedSets={installedSets}
|
|
693
|
-
onInstall={handleInstall}
|
|
694
|
-
/>
|
|
695
|
-
)}
|
|
696
|
-
|
|
697
540
|
<section>
|
|
698
541
|
<div className="flex items-center justify-between mb-3">
|
|
699
|
-
<h3 className="text-[15px] font-bold text-gray-900">{scope === 'installed' ? 'Installed
|
|
542
|
+
<h3 className="text-[15px] font-bold text-gray-900">{scope === 'installed' ? 'Installed' : 'Extensions'}</h3>
|
|
700
543
|
<span className="text-[12px] text-gray-500">{listSummary}</span>
|
|
701
544
|
</div>
|
|
702
545
|
|
|
@@ -711,13 +554,12 @@ export function MarketplacePage() {
|
|
|
711
554
|
</div>
|
|
712
555
|
)}
|
|
713
556
|
|
|
714
|
-
<div className="grid grid-cols-1
|
|
557
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 gap-3">
|
|
715
558
|
{scope === 'all' && allItems.map((item) => (
|
|
716
|
-
<
|
|
559
|
+
<MarketplaceListCard
|
|
717
560
|
key={item.id}
|
|
718
561
|
item={item}
|
|
719
|
-
|
|
720
|
-
installed={isInstalled(item, installedSets)}
|
|
562
|
+
record={findInstalledRecordForItem(item, installedRecordLookup)}
|
|
721
563
|
installState={installState}
|
|
722
564
|
manageState={manageState}
|
|
723
565
|
onInstall={handleInstall}
|
|
@@ -726,20 +568,15 @@ export function MarketplacePage() {
|
|
|
726
568
|
))}
|
|
727
569
|
|
|
728
570
|
{scope === 'installed' && installedEntries.map((entry) => (
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
onInstall={handleInstall}
|
|
739
|
-
onManage={handleManage}
|
|
740
|
-
/>
|
|
741
|
-
)
|
|
742
|
-
: <InstalledRecordCard key={`local:${entry.key}`} record={entry.record} manageState={manageState} onManage={handleManage} />
|
|
571
|
+
<MarketplaceListCard
|
|
572
|
+
key={entry.key}
|
|
573
|
+
item={entry.item}
|
|
574
|
+
record={entry.record}
|
|
575
|
+
installState={installState}
|
|
576
|
+
manageState={manageState}
|
|
577
|
+
onInstall={handleInstall}
|
|
578
|
+
onManage={handleManage}
|
|
579
|
+
/>
|
|
743
580
|
))}
|
|
744
581
|
</div>
|
|
745
582
|
|