@nextclaw/ui 0.5.2 → 0.5.3
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 +7 -0
- package/dist/assets/index-BHGBLfqi.css +1 -0
- package/dist/assets/index-CAFcPSll.js +332 -0
- package/dist/index.html +2 -2
- package/package.json +2 -1
- package/src/components/marketplace/MarketplacePage.tsx +186 -351
- package/src/components/ui/tooltip.tsx +30 -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,126 @@ 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
|
-
<TypeBadge type={props.item.type} />
|
|
389
|
-
{props.installed && <InstalledBadge />}
|
|
390
|
-
{props.installed && <EnabledStateBadge enabled={props.installedRecord?.enabled} />}
|
|
391
|
-
</div>
|
|
392
|
-
</div>
|
|
233
|
+
const record = props.record;
|
|
234
|
+
const type = props.item?.type ?? record?.type;
|
|
235
|
+
const title = props.item?.name ?? record?.label ?? record?.id ?? record?.spec ?? 'Unknown Item';
|
|
236
|
+
const summary = props.item?.summary ?? (record ? 'Installed locally. Details are currently unavailable from marketplace.' : '');
|
|
237
|
+
const spec = props.item?.install.spec ?? record?.spec ?? '';
|
|
393
238
|
|
|
394
|
-
|
|
239
|
+
const targetId = record?.id || record?.spec;
|
|
240
|
+
const busyForRecord = Boolean(targetId) && props.manageState.isPending && props.manageState.targetId === targetId;
|
|
395
241
|
|
|
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>
|
|
242
|
+
const canToggle = record?.type === 'plugin';
|
|
243
|
+
const canUninstallPlugin = record?.type === 'plugin' && record.origin !== 'bundled';
|
|
244
|
+
const canUninstallSkill = record?.type === 'skill' && record.source === 'workspace';
|
|
245
|
+
const canUninstall = Boolean(canUninstallPlugin || canUninstallSkill);
|
|
405
246
|
|
|
406
|
-
|
|
407
|
-
<code className="text-[11px] text-gray-500 bg-gray-100 rounded px-2 py-1 truncate">{props.item.install.spec}</code>
|
|
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
|
-
}
|
|
247
|
+
const isInstalling = props.installState.isPending && props.item && props.installState.installingSpec === props.item.install.spec;
|
|
426
248
|
|
|
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;
|
|
249
|
+
const displayType = type === 'plugin' ? 'Plugin' : type === 'skill' ? 'Skill' : 'Extension';
|
|
434
250
|
|
|
435
251
|
return (
|
|
436
|
-
<article className="bg-white border border-gray-200 rounded-xl
|
|
437
|
-
<div className="flex
|
|
438
|
-
<
|
|
439
|
-
<div className="flex
|
|
440
|
-
<
|
|
441
|
-
|
|
442
|
-
|
|
252
|
+
<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]">
|
|
253
|
+
<div className="flex gap-3 min-w-0 flex-1 h-full items-start">
|
|
254
|
+
<ItemIcon name={title} fallback={spec || 'Ext'} />
|
|
255
|
+
<div className="min-w-0 flex-1 flex flex-col justify-center h-full">
|
|
256
|
+
<TooltipProvider delayDuration={400}>
|
|
257
|
+
<Tooltip>
|
|
258
|
+
<TooltipTrigger asChild>
|
|
259
|
+
<div className="text-[14px] font-semibold text-gray-900 truncate leading-tight cursor-default">{title}</div>
|
|
260
|
+
</TooltipTrigger>
|
|
261
|
+
<TooltipContent className="max-w-[300px] text-xs">
|
|
262
|
+
{title}
|
|
263
|
+
</TooltipContent>
|
|
264
|
+
</Tooltip>
|
|
265
|
+
|
|
266
|
+
<div className="flex items-center gap-1.5 mt-0.5 mb-1.5">
|
|
267
|
+
<span className="text-[11px] text-gray-500 font-medium">{displayType}</span>
|
|
268
|
+
{spec && (
|
|
269
|
+
<>
|
|
270
|
+
<span className="text-[10px] text-gray-300">•</span>
|
|
271
|
+
<Tooltip>
|
|
272
|
+
<TooltipTrigger asChild>
|
|
273
|
+
<span className="text-[11px] text-gray-400 truncate max-w-full font-mono cursor-default">{spec}</span>
|
|
274
|
+
</TooltipTrigger>
|
|
275
|
+
<TooltipContent className="max-w-[300px] text-xs font-mono break-all">
|
|
276
|
+
{spec}
|
|
277
|
+
</TooltipContent>
|
|
278
|
+
</Tooltip>
|
|
279
|
+
</>
|
|
280
|
+
)}
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
<Tooltip>
|
|
284
|
+
<TooltipTrigger asChild>
|
|
285
|
+
<p className="text-[12px] text-gray-500/90 line-clamp-1 transition-colors leading-relaxed text-left cursor-default">{summary}</p>
|
|
286
|
+
</TooltipTrigger>
|
|
287
|
+
{summary && (
|
|
288
|
+
<TooltipContent className="max-w-[400px] text-xs leading-relaxed">
|
|
289
|
+
{summary}
|
|
290
|
+
</TooltipContent>
|
|
291
|
+
)}
|
|
292
|
+
</Tooltip>
|
|
293
|
+
</TooltipProvider>
|
|
443
294
|
</div>
|
|
444
295
|
</div>
|
|
445
296
|
|
|
446
|
-
<
|
|
297
|
+
<div className="shrink-0 flex items-center h-full">
|
|
298
|
+
{props.item && !record && (
|
|
299
|
+
<button
|
|
300
|
+
onClick={() => props.onInstall(props.item as MarketplaceItemSummary)}
|
|
301
|
+
disabled={props.installState.isPending}
|
|
302
|
+
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"
|
|
303
|
+
>
|
|
304
|
+
{isInstalling ? 'Installing...' : 'Install'}
|
|
305
|
+
</button>
|
|
306
|
+
)}
|
|
447
307
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
308
|
+
{record && canToggle && (
|
|
309
|
+
<button
|
|
310
|
+
disabled={props.manageState.isPending}
|
|
311
|
+
onClick={() => props.onManage(record.enabled === false ? 'enable' : 'disable', record)}
|
|
312
|
+
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"
|
|
313
|
+
>
|
|
314
|
+
{busyForRecord && props.manageState.action !== 'uninstall'
|
|
315
|
+
? (props.manageState.action === 'enable' ? 'Enabling...' : 'Disabling...')
|
|
316
|
+
: (record.enabled === false ? 'Enable' : 'Disable')}
|
|
317
|
+
</button>
|
|
318
|
+
)}
|
|
451
319
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
320
|
+
{record && canUninstall && (
|
|
321
|
+
<button
|
|
322
|
+
disabled={props.manageState.isPending}
|
|
323
|
+
onClick={() => props.onManage('uninstall', record)}
|
|
324
|
+
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"
|
|
325
|
+
>
|
|
326
|
+
{busyForRecord && props.manageState.action === 'uninstall' ? 'Removing...' : 'Uninstall'}
|
|
327
|
+
</button>
|
|
328
|
+
)}
|
|
459
329
|
</div>
|
|
460
330
|
</article>
|
|
461
331
|
);
|
|
@@ -469,7 +339,7 @@ function PaginationBar(props: {
|
|
|
469
339
|
onNext: () => void;
|
|
470
340
|
}) {
|
|
471
341
|
return (
|
|
472
|
-
<div className="mt-
|
|
342
|
+
<div className="mt-4 flex items-center justify-end gap-2">
|
|
473
343
|
<button
|
|
474
344
|
className="h-8 px-3 rounded-lg border border-gray-200 text-sm text-gray-700 disabled:opacity-40"
|
|
475
345
|
onClick={props.onPrev}
|
|
@@ -516,7 +386,7 @@ export function MarketplacePage() {
|
|
|
516
386
|
page,
|
|
517
387
|
pageSize: PAGE_SIZE
|
|
518
388
|
});
|
|
519
|
-
|
|
389
|
+
|
|
520
390
|
const installMutation = useInstallMarketplaceItem();
|
|
521
391
|
const manageMutation = useManageMarketplaceItem();
|
|
522
392
|
|
|
@@ -524,36 +394,30 @@ export function MarketplacePage() {
|
|
|
524
394
|
() => installedQuery.data?.records ?? [],
|
|
525
395
|
[installedQuery.data?.records]
|
|
526
396
|
);
|
|
527
|
-
|
|
528
|
-
const installedRecordLookup = useMemo(
|
|
529
|
-
() => buildInstalledRecordLookup(installedRecords),
|
|
530
|
-
[installedRecords]
|
|
531
|
-
);
|
|
397
|
+
|
|
532
398
|
const allItems = useMemo(
|
|
533
399
|
() => itemsQuery.data?.items ?? [],
|
|
534
400
|
[itemsQuery.data?.items]
|
|
535
401
|
);
|
|
536
|
-
const recommendations = useMemo(
|
|
537
|
-
() => recommendationsQuery.data?.items ?? [],
|
|
538
|
-
[recommendationsQuery.data?.items]
|
|
539
|
-
);
|
|
540
402
|
|
|
541
403
|
const catalogLookup = useMemo(
|
|
542
|
-
() => buildCatalogLookup(
|
|
543
|
-
[allItems
|
|
404
|
+
() => buildCatalogLookup(allItems),
|
|
405
|
+
[allItems]
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
const installedRecordLookup = useMemo(
|
|
409
|
+
() => buildInstalledRecordLookup(installedRecords),
|
|
410
|
+
[installedRecords]
|
|
544
411
|
);
|
|
545
412
|
|
|
546
413
|
const installedEntries = useMemo<InstalledRenderEntry[]>(() => {
|
|
547
414
|
const entries = installedRecords
|
|
548
415
|
.filter((record) => (typeFilter === 'all' ? true : record.type === typeFilter))
|
|
549
|
-
.map((record) => {
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
item
|
|
555
|
-
};
|
|
556
|
-
})
|
|
416
|
+
.map((record) => ({
|
|
417
|
+
key: `${record.type}:${record.spec}:${record.id ?? ''}`,
|
|
418
|
+
record,
|
|
419
|
+
item: findCatalogItemForRecord(record, catalogLookup)
|
|
420
|
+
}))
|
|
557
421
|
.filter((entry) => matchInstalledSearch(entry.record, entry.item, query));
|
|
558
422
|
|
|
559
423
|
entries.sort((left, right) => {
|
|
@@ -572,9 +436,7 @@ export function MarketplacePage() {
|
|
|
572
436
|
return entries;
|
|
573
437
|
}, [installedRecords, typeFilter, catalogLookup, query]);
|
|
574
438
|
|
|
575
|
-
const total = scope === 'installed'
|
|
576
|
-
? installedEntries.length
|
|
577
|
-
: (itemsQuery.data?.total ?? 0);
|
|
439
|
+
const total = scope === 'installed' ? installedEntries.length : (itemsQuery.data?.total ?? 0);
|
|
578
440
|
const totalPages = scope === 'installed' ? 1 : (itemsQuery.data?.totalPages ?? 0);
|
|
579
441
|
|
|
580
442
|
const listSummary = useMemo(() => {
|
|
@@ -582,21 +444,15 @@ export function MarketplacePage() {
|
|
|
582
444
|
if (installedQuery.isLoading) {
|
|
583
445
|
return 'Loading...';
|
|
584
446
|
}
|
|
585
|
-
|
|
586
|
-
return 'No installed items';
|
|
587
|
-
}
|
|
588
|
-
const installedTotal = installedQuery.data?.total ?? installedEntries.length;
|
|
589
|
-
return `Showing ${installedEntries.length} / ${installedTotal}`;
|
|
447
|
+
return `${installedEntries.length} installed`;
|
|
590
448
|
}
|
|
591
449
|
|
|
592
450
|
if (!itemsQuery.data) {
|
|
593
451
|
return 'Loading...';
|
|
594
452
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
return `Showing ${allItems.length} / ${total}`;
|
|
599
|
-
}, [scope, installedQuery.isLoading, installedQuery.data, installedEntries.length, itemsQuery.data, allItems.length, total]);
|
|
453
|
+
|
|
454
|
+
return `${allItems.length} / ${total}`;
|
|
455
|
+
}, [scope, installedQuery.isLoading, installedEntries.length, itemsQuery.data, allItems.length, total]);
|
|
600
456
|
|
|
601
457
|
const installState: InstallState = {
|
|
602
458
|
isPending: installMutation.isPending,
|
|
@@ -648,15 +504,9 @@ export function MarketplacePage() {
|
|
|
648
504
|
|
|
649
505
|
return (
|
|
650
506
|
<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>
|
|
507
|
+
<div className="mb-5">
|
|
508
|
+
<h2 className="text-2xl font-bold text-gray-900">Marketplace</h2>
|
|
509
|
+
<p className="text-[13px] text-gray-500 mt-1">A cleaner extension list focused on install / enable / disable.</p>
|
|
660
510
|
</div>
|
|
661
511
|
|
|
662
512
|
<Tabs
|
|
@@ -666,10 +516,11 @@ export function MarketplacePage() {
|
|
|
666
516
|
setScope(value as ScopeType);
|
|
667
517
|
setPage(1);
|
|
668
518
|
}}
|
|
669
|
-
className="mb-
|
|
519
|
+
className="mb-4"
|
|
670
520
|
/>
|
|
671
521
|
|
|
672
522
|
<FilterPanel
|
|
523
|
+
scope={scope}
|
|
673
524
|
searchText={searchText}
|
|
674
525
|
typeFilter={typeFilter}
|
|
675
526
|
sort={sort}
|
|
@@ -684,19 +535,9 @@ export function MarketplacePage() {
|
|
|
684
535
|
}}
|
|
685
536
|
/>
|
|
686
537
|
|
|
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
538
|
<section>
|
|
698
539
|
<div className="flex items-center justify-between mb-3">
|
|
699
|
-
<h3 className="text-[15px] font-bold text-gray-900">{scope === 'installed' ? 'Installed
|
|
540
|
+
<h3 className="text-[15px] font-bold text-gray-900">{scope === 'installed' ? 'Installed' : 'Extensions'}</h3>
|
|
700
541
|
<span className="text-[12px] text-gray-500">{listSummary}</span>
|
|
701
542
|
</div>
|
|
702
543
|
|
|
@@ -711,13 +552,12 @@ export function MarketplacePage() {
|
|
|
711
552
|
</div>
|
|
712
553
|
)}
|
|
713
554
|
|
|
714
|
-
<div className="grid grid-cols-1
|
|
555
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 gap-3">
|
|
715
556
|
{scope === 'all' && allItems.map((item) => (
|
|
716
|
-
<
|
|
557
|
+
<MarketplaceListCard
|
|
717
558
|
key={item.id}
|
|
718
559
|
item={item}
|
|
719
|
-
|
|
720
|
-
installed={isInstalled(item, installedSets)}
|
|
560
|
+
record={findInstalledRecordForItem(item, installedRecordLookup)}
|
|
721
561
|
installState={installState}
|
|
722
562
|
manageState={manageState}
|
|
723
563
|
onInstall={handleInstall}
|
|
@@ -726,20 +566,15 @@ export function MarketplacePage() {
|
|
|
726
566
|
))}
|
|
727
567
|
|
|
728
568
|
{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} />
|
|
569
|
+
<MarketplaceListCard
|
|
570
|
+
key={entry.key}
|
|
571
|
+
item={entry.item}
|
|
572
|
+
record={entry.record}
|
|
573
|
+
installState={installState}
|
|
574
|
+
manageState={manageState}
|
|
575
|
+
onInstall={handleInstall}
|
|
576
|
+
onManage={handleManage}
|
|
577
|
+
/>
|
|
743
578
|
))}
|
|
744
579
|
</div>
|
|
745
580
|
|