@nextclaw/ui 0.3.16 → 0.3.17
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 +10 -0
- package/dist/assets/index-CPXV1dWr.js +337 -0
- package/dist/assets/index-Wn63frSd.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/App.tsx +2 -0
- package/src/api/marketplace.ts +110 -0
- package/src/api/types.ts +85 -0
- package/src/components/config/ChannelsList.tsx +2 -2
- package/src/components/config/ModelConfig.tsx +2 -2
- package/src/components/config/ProvidersList.tsx +3 -3
- package/src/components/config/SessionsConfig.tsx +93 -81
- package/src/components/doc-browser/DocBrowser.tsx +272 -0
- package/src/components/doc-browser/DocBrowserContext.tsx +134 -0
- package/src/components/doc-browser/index.ts +3 -0
- package/src/components/doc-browser/useDocLinkInterceptor.ts +33 -0
- package/src/components/layout/AppLayout.tsx +25 -8
- package/src/components/layout/Sidebar.tsx +32 -5
- package/src/components/marketplace/MarketplacePage.tsx +408 -0
- package/src/hooks/useMarketplace.ts +59 -0
- package/src/index.css +11 -4
- package/src/lib/i18n.ts +10 -1
- package/src/styles/design-system.css +256 -214
- package/dist/assets/index-DuW0OWcM.js +0 -298
- package/dist/assets/index-xwCviEXg.css +0 -1
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
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 { useInstallMarketplaceItem, useMarketplaceInstalled, useMarketplaceItems, useMarketplaceRecommendations } from '@/hooks/useMarketplace';
|
|
6
|
+
import type { MarketplaceItemSummary, MarketplaceSort } from '@/api/types';
|
|
7
|
+
|
|
8
|
+
const PAGE_SIZE = 12;
|
|
9
|
+
|
|
10
|
+
type FilterType = 'all' | 'plugin' | 'skill';
|
|
11
|
+
type ScopeType = 'all' | 'installed';
|
|
12
|
+
|
|
13
|
+
type InstallState = {
|
|
14
|
+
isPending: boolean;
|
|
15
|
+
installingSpec?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type InstalledSpecSets = {
|
|
19
|
+
plugin: Set<string>;
|
|
20
|
+
skill: Set<string>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function buildInstalledSpecSets(records: { pluginSpecs: string[]; skillSpecs: string[] } | undefined): InstalledSpecSets {
|
|
24
|
+
return {
|
|
25
|
+
plugin: new Set(records?.pluginSpecs ?? []),
|
|
26
|
+
skill: new Set(records?.skillSpecs ?? [])
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isInstalled(item: MarketplaceItemSummary, sets: InstalledSpecSets): boolean {
|
|
31
|
+
return item.type === 'plugin'
|
|
32
|
+
? sets.plugin.has(item.install.spec)
|
|
33
|
+
: sets.skill.has(item.install.spec);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function TypeBadge({ type }: { type: MarketplaceItemSummary['type'] }) {
|
|
37
|
+
return (
|
|
38
|
+
<span
|
|
39
|
+
className={cn(
|
|
40
|
+
'text-[11px] uppercase px-2 py-1 rounded-full font-semibold',
|
|
41
|
+
type === 'plugin' ? 'bg-blue-50 text-blue-600' : 'bg-emerald-50 text-emerald-600'
|
|
42
|
+
)}
|
|
43
|
+
>
|
|
44
|
+
{type}
|
|
45
|
+
</span>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function InstalledBadge() {
|
|
50
|
+
return <span className="text-[11px] px-2 py-1 rounded-full font-semibold bg-indigo-50 text-indigo-600">Installed</span>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function FilterPanel(props: {
|
|
54
|
+
searchText: string;
|
|
55
|
+
typeFilter: FilterType;
|
|
56
|
+
sort: MarketplaceSort;
|
|
57
|
+
onSearchTextChange: (value: string) => void;
|
|
58
|
+
onTypeFilterChange: (value: FilterType) => void;
|
|
59
|
+
onSortChange: (value: MarketplaceSort) => void;
|
|
60
|
+
}) {
|
|
61
|
+
return (
|
|
62
|
+
<div className="bg-white border border-gray-200 rounded-2xl p-4 mb-5">
|
|
63
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
|
64
|
+
<div className="md:col-span-2 relative">
|
|
65
|
+
<PackageSearch className="h-4 w-4 text-gray-400 absolute left-3 top-1/2 -translate-y-1/2" />
|
|
66
|
+
<input
|
|
67
|
+
value={props.searchText}
|
|
68
|
+
onChange={(event) => props.onSearchTextChange(event.target.value)}
|
|
69
|
+
placeholder="Search by name, slug, tags..."
|
|
70
|
+
className="w-full h-10 border border-gray-200 rounded-lg pl-9 pr-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary/30"
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<div className="flex gap-2">
|
|
75
|
+
<select
|
|
76
|
+
className="flex-1 h-10 border border-gray-200 rounded-lg px-3 text-sm bg-white"
|
|
77
|
+
value={props.typeFilter}
|
|
78
|
+
onChange={(event) => props.onTypeFilterChange(event.target.value as FilterType)}
|
|
79
|
+
>
|
|
80
|
+
<option value="all">All</option>
|
|
81
|
+
<option value="plugin">Plugins</option>
|
|
82
|
+
<option value="skill">Skills</option>
|
|
83
|
+
</select>
|
|
84
|
+
<select
|
|
85
|
+
className="flex-1 h-10 border border-gray-200 rounded-lg px-3 text-sm bg-white"
|
|
86
|
+
value={props.sort}
|
|
87
|
+
onChange={(event) => props.onSortChange(event.target.value as MarketplaceSort)}
|
|
88
|
+
>
|
|
89
|
+
<option value="relevance">Relevance</option>
|
|
90
|
+
<option value="updated">Recently Updated</option>
|
|
91
|
+
<option value="downloads">Downloads</option>
|
|
92
|
+
</select>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function InstallButton(props: {
|
|
100
|
+
item: MarketplaceItemSummary;
|
|
101
|
+
installState: InstallState;
|
|
102
|
+
installed: boolean;
|
|
103
|
+
onInstall: (item: MarketplaceItemSummary) => void;
|
|
104
|
+
}) {
|
|
105
|
+
const isInstalling = props.installState.isPending && props.installState.installingSpec === props.item.install.spec;
|
|
106
|
+
|
|
107
|
+
if (props.installed) {
|
|
108
|
+
return (
|
|
109
|
+
<button
|
|
110
|
+
disabled
|
|
111
|
+
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"
|
|
112
|
+
>
|
|
113
|
+
Installed
|
|
114
|
+
</button>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<button
|
|
120
|
+
onClick={() => props.onInstall(props.item)}
|
|
121
|
+
disabled={props.installState.isPending}
|
|
122
|
+
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"
|
|
123
|
+
>
|
|
124
|
+
<Download className="h-3.5 w-3.5" />
|
|
125
|
+
{isInstalling ? 'Installing...' : 'Install'}
|
|
126
|
+
</button>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function RecommendationSection(props: {
|
|
131
|
+
items: MarketplaceItemSummary[];
|
|
132
|
+
loading: boolean;
|
|
133
|
+
installState: InstallState;
|
|
134
|
+
installedSets: InstalledSpecSets;
|
|
135
|
+
onInstall: (item: MarketplaceItemSummary) => void;
|
|
136
|
+
}) {
|
|
137
|
+
return (
|
|
138
|
+
<section className="mb-6">
|
|
139
|
+
<div className="flex items-center gap-2 mb-3">
|
|
140
|
+
<Sparkles className="h-4 w-4 text-amber-500" />
|
|
141
|
+
<h3 className="text-[15px] font-bold text-gray-900">Recommended</h3>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
145
|
+
{props.items.map((item) => {
|
|
146
|
+
const installed = isInstalled(item, props.installedSets);
|
|
147
|
+
return (
|
|
148
|
+
<div key={item.id} className="bg-white border border-gray-200 rounded-xl p-4">
|
|
149
|
+
<div className="flex items-start justify-between gap-3">
|
|
150
|
+
<div>
|
|
151
|
+
<div className="text-[14px] font-semibold text-gray-900">{item.name}</div>
|
|
152
|
+
<div className="text-[12px] text-gray-500 mt-0.5">{item.summary}</div>
|
|
153
|
+
</div>
|
|
154
|
+
<div className="flex items-center gap-2">
|
|
155
|
+
<TypeBadge type={item.type} />
|
|
156
|
+
{installed && <InstalledBadge />}
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
<div className="mt-3 flex items-center justify-between">
|
|
160
|
+
<code className="text-[11px] text-gray-500 bg-gray-100 rounded px-2 py-1">{item.install.spec}</code>
|
|
161
|
+
<InstallButton
|
|
162
|
+
item={item}
|
|
163
|
+
installed={installed}
|
|
164
|
+
installState={props.installState}
|
|
165
|
+
onInstall={props.onInstall}
|
|
166
|
+
/>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
})}
|
|
171
|
+
|
|
172
|
+
{props.loading && <div className="text-[13px] text-gray-500">Loading recommendations...</div>}
|
|
173
|
+
{!props.loading && props.items.length === 0 && <div className="text-[13px] text-gray-500">No recommendations yet.</div>}
|
|
174
|
+
</div>
|
|
175
|
+
</section>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function MarketplaceItemCard(props: {
|
|
180
|
+
item: MarketplaceItemSummary;
|
|
181
|
+
installState: InstallState;
|
|
182
|
+
installed: boolean;
|
|
183
|
+
onInstall: (item: MarketplaceItemSummary) => void;
|
|
184
|
+
}) {
|
|
185
|
+
const downloads = props.item.metrics?.downloads30d;
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<article className="bg-white border border-gray-200 rounded-xl p-4 hover:shadow-sm transition-shadow">
|
|
189
|
+
<div className="flex items-start justify-between gap-2">
|
|
190
|
+
<h4 className="text-[14px] font-semibold text-gray-900">{props.item.name}</h4>
|
|
191
|
+
<div className="flex items-center gap-2">
|
|
192
|
+
<TypeBadge type={props.item.type} />
|
|
193
|
+
{props.installed && <InstalledBadge />}
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<p className="text-[12px] text-gray-500 mt-1 min-h-10">{props.item.summary}</p>
|
|
198
|
+
|
|
199
|
+
<div className="flex flex-wrap gap-1 mt-2">
|
|
200
|
+
{props.item.tags.slice(0, 3).map((tag) => (
|
|
201
|
+
<span key={`${props.item.id}-${tag}`} className="text-[11px] px-2 py-0.5 rounded-full bg-gray-100 text-gray-600">
|
|
202
|
+
{tag}
|
|
203
|
+
</span>
|
|
204
|
+
))}
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<div className="mt-3 text-[11px] text-gray-500">By {props.item.author}</div>
|
|
208
|
+
<div className="mt-1 text-[11px] text-gray-500">{downloads ? `${downloads} downloads / 30d` : 'No metrics'}</div>
|
|
209
|
+
|
|
210
|
+
<div className="mt-3 pt-3 border-t border-gray-100 flex items-center justify-between gap-2">
|
|
211
|
+
<code className="text-[11px] text-gray-500 bg-gray-100 rounded px-2 py-1 truncate">{props.item.install.spec}</code>
|
|
212
|
+
<InstallButton
|
|
213
|
+
item={props.item}
|
|
214
|
+
installed={props.installed}
|
|
215
|
+
installState={props.installState}
|
|
216
|
+
onInstall={props.onInstall}
|
|
217
|
+
/>
|
|
218
|
+
</div>
|
|
219
|
+
</article>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function PaginationBar(props: {
|
|
224
|
+
page: number;
|
|
225
|
+
totalPages: number;
|
|
226
|
+
busy: boolean;
|
|
227
|
+
onPrev: () => void;
|
|
228
|
+
onNext: () => void;
|
|
229
|
+
}) {
|
|
230
|
+
return (
|
|
231
|
+
<div className="mt-5 flex items-center justify-end gap-2">
|
|
232
|
+
<button
|
|
233
|
+
className="h-8 px-3 rounded-lg border border-gray-200 text-sm text-gray-700 disabled:opacity-40"
|
|
234
|
+
onClick={props.onPrev}
|
|
235
|
+
disabled={props.page <= 1 || props.busy}
|
|
236
|
+
>
|
|
237
|
+
Prev
|
|
238
|
+
</button>
|
|
239
|
+
<div className="text-sm text-gray-600 min-w-20 text-center">
|
|
240
|
+
{props.totalPages === 0 ? '0 / 0' : `${props.page} / ${props.totalPages}`}
|
|
241
|
+
</div>
|
|
242
|
+
<button
|
|
243
|
+
className="h-8 px-3 rounded-lg border border-gray-200 text-sm text-gray-700 disabled:opacity-40"
|
|
244
|
+
onClick={props.onNext}
|
|
245
|
+
disabled={props.totalPages === 0 || props.page >= props.totalPages || props.busy}
|
|
246
|
+
>
|
|
247
|
+
Next
|
|
248
|
+
</button>
|
|
249
|
+
</div>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function MarketplacePage() {
|
|
254
|
+
const [searchText, setSearchText] = useState('');
|
|
255
|
+
const [query, setQuery] = useState('');
|
|
256
|
+
const [scope, setScope] = useState<ScopeType>('all');
|
|
257
|
+
const [typeFilter, setTypeFilter] = useState<FilterType>('all');
|
|
258
|
+
const [sort, setSort] = useState<MarketplaceSort>('relevance');
|
|
259
|
+
const [page, setPage] = useState(1);
|
|
260
|
+
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
const timer = setTimeout(() => {
|
|
263
|
+
setPage(1);
|
|
264
|
+
setQuery(searchText.trim());
|
|
265
|
+
}, 250);
|
|
266
|
+
return () => clearTimeout(timer);
|
|
267
|
+
}, [searchText]);
|
|
268
|
+
|
|
269
|
+
const installedQuery = useMarketplaceInstalled();
|
|
270
|
+
const requestPage = scope === 'installed' ? 1 : page;
|
|
271
|
+
const requestPageSize = scope === 'installed' ? 100 : PAGE_SIZE;
|
|
272
|
+
|
|
273
|
+
const itemsQuery = useMarketplaceItems({
|
|
274
|
+
q: query || undefined,
|
|
275
|
+
type: typeFilter === 'all' ? undefined : typeFilter,
|
|
276
|
+
sort,
|
|
277
|
+
page: requestPage,
|
|
278
|
+
pageSize: requestPageSize
|
|
279
|
+
});
|
|
280
|
+
const recommendationsQuery = useMarketplaceRecommendations({ scene: 'default', limit: 4 });
|
|
281
|
+
const installMutation = useInstallMarketplaceItem();
|
|
282
|
+
|
|
283
|
+
const installedSets = buildInstalledSpecSets(installedQuery.data);
|
|
284
|
+
const allItems = itemsQuery.data?.items ?? [];
|
|
285
|
+
const items = scope === 'installed'
|
|
286
|
+
? allItems.filter((item) => isInstalled(item, installedSets))
|
|
287
|
+
: allItems;
|
|
288
|
+
|
|
289
|
+
const recommendations = recommendationsQuery.data?.items ?? [];
|
|
290
|
+
const total = scope === 'installed'
|
|
291
|
+
? items.length
|
|
292
|
+
: (itemsQuery.data?.total ?? 0);
|
|
293
|
+
const totalPages = scope === 'installed' ? 1 : (itemsQuery.data?.totalPages ?? 0);
|
|
294
|
+
|
|
295
|
+
const listSummary = useMemo(() => {
|
|
296
|
+
if (!itemsQuery.data) {
|
|
297
|
+
return 'Loading...';
|
|
298
|
+
}
|
|
299
|
+
if (items.length === 0) {
|
|
300
|
+
return scope === 'installed' ? 'No installed items on this page' : 'No results';
|
|
301
|
+
}
|
|
302
|
+
return `Showing ${items.length} / ${total}`;
|
|
303
|
+
}, [items.length, itemsQuery.data, scope, total]);
|
|
304
|
+
|
|
305
|
+
const installState: InstallState = {
|
|
306
|
+
isPending: installMutation.isPending,
|
|
307
|
+
installingSpec: installMutation.variables?.spec
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const tabs = [
|
|
311
|
+
{ id: 'all', label: 'Marketplace' },
|
|
312
|
+
{ id: 'installed', label: 'Installed', count: installedQuery.data?.total ?? 0 }
|
|
313
|
+
];
|
|
314
|
+
|
|
315
|
+
const handleInstall = (item: MarketplaceItemSummary) => {
|
|
316
|
+
if (installMutation.isPending) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
installMutation.mutate({ type: item.type, spec: item.install.spec });
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
return (
|
|
323
|
+
<div className="animate-fade-in pb-20">
|
|
324
|
+
<div className="flex items-center justify-between mb-6">
|
|
325
|
+
<div>
|
|
326
|
+
<h2 className="text-2xl font-bold text-gray-900">Marketplace</h2>
|
|
327
|
+
<p className="text-[13px] text-gray-500 mt-1">Search, discover and install plugins/skills.</p>
|
|
328
|
+
</div>
|
|
329
|
+
<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">
|
|
330
|
+
<Store className="h-3.5 w-3.5" />
|
|
331
|
+
Read-only Catalog
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
|
|
335
|
+
<Tabs
|
|
336
|
+
tabs={tabs}
|
|
337
|
+
activeTab={scope}
|
|
338
|
+
onChange={(value) => {
|
|
339
|
+
setScope(value as ScopeType);
|
|
340
|
+
setPage(1);
|
|
341
|
+
}}
|
|
342
|
+
className="mb-5"
|
|
343
|
+
/>
|
|
344
|
+
|
|
345
|
+
<FilterPanel
|
|
346
|
+
searchText={searchText}
|
|
347
|
+
typeFilter={typeFilter}
|
|
348
|
+
sort={sort}
|
|
349
|
+
onSearchTextChange={setSearchText}
|
|
350
|
+
onTypeFilterChange={(value) => {
|
|
351
|
+
setPage(1);
|
|
352
|
+
setTypeFilter(value);
|
|
353
|
+
}}
|
|
354
|
+
onSortChange={(value) => {
|
|
355
|
+
setPage(1);
|
|
356
|
+
setSort(value);
|
|
357
|
+
}}
|
|
358
|
+
/>
|
|
359
|
+
|
|
360
|
+
<RecommendationSection
|
|
361
|
+
items={recommendations}
|
|
362
|
+
loading={recommendationsQuery.isLoading}
|
|
363
|
+
installState={installState}
|
|
364
|
+
installedSets={installedSets}
|
|
365
|
+
onInstall={handleInstall}
|
|
366
|
+
/>
|
|
367
|
+
|
|
368
|
+
<section>
|
|
369
|
+
<div className="flex items-center justify-between mb-3">
|
|
370
|
+
<h3 className="text-[15px] font-bold text-gray-900">{scope === 'installed' ? 'Installed Items' : 'All Items'}</h3>
|
|
371
|
+
<span className="text-[12px] text-gray-500">{listSummary}</span>
|
|
372
|
+
</div>
|
|
373
|
+
|
|
374
|
+
{itemsQuery.isError && (
|
|
375
|
+
<div className="p-4 rounded-xl bg-rose-50 border border-rose-200 text-rose-700 text-sm">
|
|
376
|
+
Failed to load marketplace data: {itemsQuery.error.message}
|
|
377
|
+
</div>
|
|
378
|
+
)}
|
|
379
|
+
|
|
380
|
+
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3">
|
|
381
|
+
{items.map((item) => (
|
|
382
|
+
<MarketplaceItemCard
|
|
383
|
+
key={item.id}
|
|
384
|
+
item={item}
|
|
385
|
+
installed={isInstalled(item, installedSets)}
|
|
386
|
+
installState={installState}
|
|
387
|
+
onInstall={handleInstall}
|
|
388
|
+
/>
|
|
389
|
+
))}
|
|
390
|
+
</div>
|
|
391
|
+
|
|
392
|
+
{!itemsQuery.isLoading && !itemsQuery.isError && items.length === 0 && (
|
|
393
|
+
<div className="text-[13px] text-gray-500 py-8 text-center">No items found.</div>
|
|
394
|
+
)}
|
|
395
|
+
</section>
|
|
396
|
+
|
|
397
|
+
{scope === 'all' && (
|
|
398
|
+
<PaginationBar
|
|
399
|
+
page={page}
|
|
400
|
+
totalPages={totalPages}
|
|
401
|
+
busy={itemsQuery.isFetching}
|
|
402
|
+
onPrev={() => setPage((current) => Math.max(1, current - 1))}
|
|
403
|
+
onNext={() => setPage((current) => (totalPages > 0 ? Math.min(totalPages, current + 1) : current + 1))}
|
|
404
|
+
/>
|
|
405
|
+
)}
|
|
406
|
+
</div>
|
|
407
|
+
);
|
|
408
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
2
|
+
import { toast } from 'sonner';
|
|
3
|
+
import {
|
|
4
|
+
fetchMarketplaceItem,
|
|
5
|
+
fetchMarketplaceInstalled,
|
|
6
|
+
fetchMarketplaceItems,
|
|
7
|
+
fetchMarketplaceRecommendations,
|
|
8
|
+
installMarketplaceItem,
|
|
9
|
+
type MarketplaceListParams
|
|
10
|
+
} from '@/api/marketplace';
|
|
11
|
+
import type { MarketplaceInstallRequest, MarketplaceItemType } from '@/api/types';
|
|
12
|
+
|
|
13
|
+
export function useMarketplaceItems(params: MarketplaceListParams) {
|
|
14
|
+
return useQuery({
|
|
15
|
+
queryKey: ['marketplace-items', params],
|
|
16
|
+
queryFn: () => fetchMarketplaceItems(params),
|
|
17
|
+
staleTime: 15_000
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function useMarketplaceRecommendations(params: { scene?: string; limit?: number }) {
|
|
22
|
+
return useQuery({
|
|
23
|
+
queryKey: ['marketplace-recommendations', params],
|
|
24
|
+
queryFn: () => fetchMarketplaceRecommendations(params),
|
|
25
|
+
staleTime: 30_000
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function useMarketplaceItem(slug: string | null, type?: MarketplaceItemType) {
|
|
30
|
+
return useQuery({
|
|
31
|
+
queryKey: ['marketplace-item', slug, type],
|
|
32
|
+
queryFn: () => fetchMarketplaceItem(slug as string, type),
|
|
33
|
+
enabled: Boolean(slug),
|
|
34
|
+
staleTime: 30_000
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function useMarketplaceInstalled() {
|
|
39
|
+
return useQuery({
|
|
40
|
+
queryKey: ['marketplace-installed'],
|
|
41
|
+
queryFn: fetchMarketplaceInstalled,
|
|
42
|
+
staleTime: 10_000
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function useInstallMarketplaceItem() {
|
|
47
|
+
const queryClient = useQueryClient();
|
|
48
|
+
|
|
49
|
+
return useMutation({
|
|
50
|
+
mutationFn: (request: MarketplaceInstallRequest) => installMarketplaceItem(request),
|
|
51
|
+
onSuccess: (result) => {
|
|
52
|
+
queryClient.invalidateQueries({ queryKey: ['marketplace-installed'] });
|
|
53
|
+
toast.success(result.message || `${result.type} installed`);
|
|
54
|
+
},
|
|
55
|
+
onError: (error: Error) => {
|
|
56
|
+
toast.error(error.message || 'Install failed');
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
package/src/index.css
CHANGED
|
@@ -84,6 +84,7 @@
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
@layer utilities {
|
|
87
|
+
|
|
87
88
|
/* ========================================
|
|
88
89
|
SCROLLBAR
|
|
89
90
|
======================================== */
|
|
@@ -126,11 +127,11 @@
|
|
|
126
127
|
SHADOWS
|
|
127
128
|
======================================== */
|
|
128
129
|
.shadow-card {
|
|
129
|
-
box-shadow: 0
|
|
130
|
+
box-shadow: 0 2px 8px -2px rgba(0, 0, 0, 0.04), 0 1px 2px -1px rgba(0, 0, 0, 0.02);
|
|
130
131
|
}
|
|
131
132
|
|
|
132
133
|
.shadow-card-hover {
|
|
133
|
-
box-shadow: 0
|
|
134
|
+
box-shadow: 0 12px 24px -4px rgba(0, 0, 0, 0.06), 0 4px 8px -4px rgba(0, 0, 0, 0.03);
|
|
134
135
|
}
|
|
135
136
|
|
|
136
137
|
.shadow-premium {
|
|
@@ -170,6 +171,7 @@
|
|
|
170
171
|
opacity: 0;
|
|
171
172
|
transform: translateY(12px);
|
|
172
173
|
}
|
|
174
|
+
|
|
173
175
|
to {
|
|
174
176
|
opacity: 1;
|
|
175
177
|
transform: translateY(0);
|
|
@@ -181,6 +183,7 @@
|
|
|
181
183
|
opacity: 0;
|
|
182
184
|
transform: translateX(-12px);
|
|
183
185
|
}
|
|
186
|
+
|
|
184
187
|
to {
|
|
185
188
|
opacity: 1;
|
|
186
189
|
transform: translateX(0);
|
|
@@ -192,6 +195,7 @@
|
|
|
192
195
|
opacity: 0;
|
|
193
196
|
transform: scale(0.97);
|
|
194
197
|
}
|
|
198
|
+
|
|
195
199
|
to {
|
|
196
200
|
opacity: 1;
|
|
197
201
|
transform: scale(1);
|
|
@@ -199,9 +203,12 @@
|
|
|
199
203
|
}
|
|
200
204
|
|
|
201
205
|
@keyframes pulse-soft {
|
|
202
|
-
|
|
206
|
+
|
|
207
|
+
0%,
|
|
208
|
+
100% {
|
|
203
209
|
opacity: 1;
|
|
204
210
|
}
|
|
211
|
+
|
|
205
212
|
50% {
|
|
206
213
|
opacity: 0.8;
|
|
207
214
|
}
|
|
@@ -221,4 +228,4 @@
|
|
|
221
228
|
|
|
222
229
|
.animate-pulse-soft {
|
|
223
230
|
animation: pulse-soft 3s ease-in-out infinite;
|
|
224
|
-
}
|
|
231
|
+
}
|
package/src/lib/i18n.ts
CHANGED
|
@@ -159,7 +159,16 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
|
|
|
159
159
|
feishuVerifyFailed: { zh: '验证失败', en: 'Verification failed' },
|
|
160
160
|
enterTag: { zh: '输入后按回车...', en: 'Type and press Enter...' },
|
|
161
161
|
headerName: { zh: 'Header 名称', en: 'Header Name' },
|
|
162
|
-
headerValue: { zh: 'Header 值', en: 'Header Value' }
|
|
162
|
+
headerValue: { zh: 'Header 值', en: 'Header Value' },
|
|
163
|
+
|
|
164
|
+
// Doc Browser
|
|
165
|
+
docBrowserTitle: { zh: '帮助文档', en: 'Help Docs' },
|
|
166
|
+
docBrowserSearchPlaceholder: { zh: '搜索,也可以输入文档地址直接打开', en: 'Search, or enter a doc URL to open' },
|
|
167
|
+
docBrowserOpenExternal: { zh: '文档中心打开', en: 'Open in Docs' },
|
|
168
|
+
docBrowserFloatMode: { zh: '悬浮窗口', en: 'Float Window' },
|
|
169
|
+
docBrowserDockMode: { zh: '固定到侧栏', en: 'Dock to Sidebar' },
|
|
170
|
+
docBrowserClose: { zh: '关闭', en: 'Close' },
|
|
171
|
+
docBrowserHelp: { zh: '帮助文档', en: 'Help Docs' },
|
|
163
172
|
};
|
|
164
173
|
|
|
165
174
|
export function t(key: string, lang: 'zh' | 'en' = 'en'): string {
|