@nextclaw/ui 0.15.26 → 0.15.27
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 +16 -0
- package/dist/assets/{api-CK3Fro7z.js → api-a_GaOFc7.js} +4 -4
- package/dist/assets/{appearance-settings-page-zqwWO8Dy.js → appearance-settings-page-DlGXLgff.js} +1 -1
- package/dist/assets/{channels-list-page-CVCZBlCL.js → channels-list-page-BEEk4Uc9.js} +2 -2
- package/dist/assets/{chat-page-D4wehRTz.js → chat-page-DUQXXqQm.js} +1 -1
- package/dist/assets/{config-split-page-CceNFvlY.js → config-split-page-ps1Aod3g.js} +1 -1
- package/dist/assets/{confirm-dialog-CWeNYWZY.js → confirm-dialog-DJSUNG33.js} +2 -2
- package/dist/assets/desktop-update-config--zagjHic.js +1 -0
- package/dist/assets/{dist-D9xtGgdL.js → dist-BkTYNzm_.js} +1 -1
- package/dist/assets/{dist-AYxkFx2B.js → dist-DkR7_Dpl.js} +1 -1
- package/dist/assets/doc-browser-DahzJEWj.js +1 -0
- package/dist/assets/doc-browser-Q67TOGVS.js +1 -0
- package/dist/assets/{doc-browser-context-Cb7Z0j90.js → doc-browser-context-BQim8SqD.js} +1 -1
- package/dist/assets/form-actions-B2-bpFie.js +1 -0
- package/dist/assets/index-Ba_o5fu9.js +109 -0
- package/dist/assets/index-DSLK7zgR.css +1 -0
- package/dist/assets/loader-circle-DyXZU92l.js +1 -0
- package/dist/assets/mcp-marketplace-page-B0dBE5CV.js +1 -0
- package/dist/assets/mcp-marketplace-page-w8panjZG.js +9 -0
- package/dist/assets/{model-config-page-DfOLyQqR.js → model-config-page-rrhFaque.js} +1 -1
- package/dist/assets/{navigation-link-MiiyoIjH.js → navigation-link-mpHFXwRr.js} +2 -2
- package/dist/assets/plus-Dhb4KuMH.js +1 -0
- package/dist/assets/{provider-scoped-model-input-DXzjr5l0.js → provider-scoped-model-input-CFsfWEt9.js} +1 -1
- package/dist/assets/{providers-config-page-CaO8WfZH.js → providers-config-page-D-TYFZsF.js} +1 -1
- package/dist/assets/react-BlXwvOLo.js +8 -0
- package/dist/assets/{refresh-cw-a2VVeeAF.js → refresh-cw-JFuyDj_P.js} +1 -1
- package/dist/assets/remote-BwVv_Qq-.js +1 -0
- package/dist/assets/rotate-cw-DjbbiIW_.js +1 -0
- package/dist/assets/{runtime-config-page-BuA4A02z.js → runtime-config-page-Br9j9Vxe.js} +1 -1
- package/dist/assets/{save-CGm23Yww.js → save-BL8P8CLX.js} +1 -1
- package/dist/assets/{search-CH2t--sH.js → search-bZL75Pgc.js} +1 -1
- package/dist/assets/{search-config-page-BAmYqoAz.js → search-config-page-B3fvzMd1.js} +1 -1
- package/dist/assets/{secrets-config-page-BlPOuql_.js → secrets-config-page-ClwTxtXd.js} +2 -2
- package/dist/assets/security-config-DmJw9QZA.js +1 -0
- package/dist/assets/{select-DnxxDpcV.js → select-rUZ8nIZu.js} +2 -2
- package/dist/assets/{settings-2-agnMYc8a.js → settings-2-BI4SCkxQ.js} +1 -1
- package/dist/assets/skeleton-CVmEPG43.js +1 -0
- package/dist/assets/{tag-chip-CPOc8b2N.js → tag-chip-BdI_yIOA.js} +1 -1
- package/dist/assets/x-BcEayhV6.js +1 -0
- package/dist/index.html +19 -19
- package/package.json +7 -7
- package/src/app/components/layout/app-layout.tsx +38 -2
- package/src/app/hooks/__tests__/use-app-event-consumers.test.ts +20 -0
- package/src/app/hooks/use-app-event-consumers.ts +21 -0
- package/src/app/presenters/app.presenter.ts +6 -0
- package/src/features/apps/components/__tests__/app-packages-panel.test.tsx +92 -14
- package/src/features/apps/components/__tests__/apps-panel.test.tsx +58 -0
- package/src/features/apps/components/app-artwork.tsx +49 -0
- package/src/features/apps/components/app-marketplace-catalog.tsx +332 -0
- package/src/features/apps/components/app-marketplace-cover.tsx +64 -0
- package/src/features/apps/components/app-marketplace-detail.tsx +161 -0
- package/src/features/apps/components/app-marketplace-dialog.tsx +107 -217
- package/src/features/apps/components/app-marketplace-operation.tsx +84 -0
- package/src/features/apps/components/app-package-card.tsx +201 -52
- package/src/features/apps/components/app-packages-panel.tsx +98 -26
- package/src/features/apps/components/apps-panel.tsx +28 -27
- package/src/features/apps/hooks/use-app-marketplace.test.ts +144 -0
- package/src/features/apps/hooks/use-app-marketplace.ts +55 -85
- package/src/features/apps/hooks/use-app-packages.ts +59 -4
- package/src/features/apps/index.ts +1 -0
- package/src/features/apps/managers/__tests__/app-package-operation-settlement.manager.test.ts +81 -0
- package/src/features/apps/managers/app-package-operation-settlement.manager.ts +80 -0
- package/src/features/apps/services/app-marketplace.service.ts +223 -0
- package/src/features/apps/types/app-marketplace.types.ts +75 -0
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +64 -3
- package/src/features/chat/features/conversation/components/session-queued-input-rows.tsx +24 -14
- package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-controller.test.tsx +53 -0
- package/src/features/chat/features/conversation/hooks/__tests__/use-session-run-queue.test.tsx +18 -0
- package/src/features/chat/features/conversation/hooks/use-chat-composer-reference-intent.ts +4 -1
- package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +126 -32
- package/src/features/chat/features/conversation/hooks/use-session-run-queue.ts +12 -2
- package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +31 -0
- package/src/features/chat/features/input/utils/chat-composer-token-protocol.utils.ts +5 -0
- package/src/features/chat/features/input/utils/chat-inline-token-composer.utils.ts +14 -0
- package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +25 -0
- package/src/features/chat/features/message/hooks/use-chat-inline-token-actions.ts +8 -1
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +77 -20
- package/src/features/chat/features/workspace/components/chat-session-workspace-directory-browser.tsx +56 -50
- package/src/features/chat/features/workspace/components/project-files/workspace-directory-tree.tsx +33 -11
- package/src/features/chat/features/workspace/components/project-files/workspace-project-tree-view.tsx +93 -0
- package/src/features/chat/features/workspace/stores/__tests__/workspace-project-tree.store.test.ts +38 -0
- package/src/features/chat/features/workspace/stores/workspace-project-tree.store.ts +160 -0
- package/src/features/chat/index.ts +1 -0
- package/src/features/chat/managers/__tests__/chat-composer-intent.manager.test.ts +29 -0
- package/src/features/chat/managers/chat-composer-intent.manager.ts +29 -2
- package/src/features/chat/presenters/chat.presenter.ts +2 -2
- package/src/features/panel-apps/components/__tests__/panel-apps-list.test.tsx +1 -1
- package/src/features/panel-apps/components/panel-apps-list.tsx +1 -6
- package/src/features/right-panel-resources/utils/__tests__/right-panel-resource-uri.utils.test.ts +34 -0
- package/src/features/right-panel-resources/utils/right-panel-resource-uri.utils.ts +29 -0
- package/src/features/service-apps/components/service-apps-panel.tsx +3 -17
- package/src/platforms/desktop/components/desktop-app-shell.tsx +5 -0
- package/src/platforms/mobile/components/mobile-app-shell.tsx +9 -1
- package/src/shared/components/actions/__tests__/navigation-link.test.tsx +12 -0
- package/src/shared/components/actions/navigation-link.tsx +6 -3
- package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +34 -0
- package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +5 -0
- package/src/shared/components/doc-browser/doc-browser.tsx +11 -1
- package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +13 -0
- package/src/shared/components/ui/tabs.tsx +24 -1
- package/src/shared/hooks/use-server-path-watch.ts +42 -0
- package/src/shared/lib/api/server-path/server-path.types.ts +10 -0
- package/src/shared/lib/api/types.ts +3 -0
- package/src/shared/lib/api/utils/server-path.utils.ts +10 -0
- package/src/shared/lib/i18n/locales/en-US/chat.json +1 -0
- package/src/shared/lib/i18n/locales/en-US/doc-browser.json +49 -7
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +1 -0
- package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +49 -7
- package/vite.config.ts +32 -1
- package/dist/assets/desktop-update-config-Dirc3bpt.js +0 -1
- package/dist/assets/doc-browser-4Sud6dWw.js +0 -1
- package/dist/assets/doc-browser-D4x_yH5F.js +0 -1
- package/dist/assets/form-actions-B2q4jgjE.js +0 -1
- package/dist/assets/index-CdyTsSBu.css +0 -1
- package/dist/assets/index-Das_bw2h.js +0 -107
- package/dist/assets/loader-circle-CpmBdaon.js +0 -1
- package/dist/assets/mcp-marketplace-page-CdOY3Sop.js +0 -1
- package/dist/assets/mcp-marketplace-page-DvjrfqEt.js +0 -9
- package/dist/assets/plus-C8FkjpSZ.js +0 -1
- package/dist/assets/react-C8XFFvuz.js +0 -8
- package/dist/assets/remote-BJHXa4mB.js +0 -1
- package/dist/assets/rotate-cw-DJ7UF3F2.js +0 -1
- package/dist/assets/security-config-CbDiY2xr.js +0 -1
- package/dist/assets/skeleton-XpqeWL62.js +0 -1
- package/dist/assets/x-odTHAtLZ.js +0 -1
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import type { AppPackageOperationView, AppPackageView } from '@nextclaw/client-sdk';
|
|
2
|
+
import { ChevronRight, Search, Sparkles } from 'lucide-react';
|
|
3
|
+
import { AppArtwork } from '@/features/apps/components/app-artwork';
|
|
4
|
+
import { AppMarketplaceCover } from '@/features/apps/components/app-marketplace-cover';
|
|
5
|
+
import {
|
|
6
|
+
findLatestAppPackageOperation,
|
|
7
|
+
MarketplaceInstallButton,
|
|
8
|
+
OperationProgress,
|
|
9
|
+
} from '@/features/apps/components/app-marketplace-operation';
|
|
10
|
+
import { isAppPackageOperationActive } from '@/features/apps/hooks/use-app-packages';
|
|
11
|
+
import type { AppMarketplaceItemView } from '@/features/apps/types/app-marketplace.types';
|
|
12
|
+
import { pickLocalizedText } from '@/features/marketplace';
|
|
13
|
+
import {
|
|
14
|
+
DialogDescription,
|
|
15
|
+
DialogHeader,
|
|
16
|
+
DialogTitle,
|
|
17
|
+
} from '@/shared/components/ui/dialog';
|
|
18
|
+
import { Input } from '@/shared/components/ui/input';
|
|
19
|
+
import { Skeleton } from '@/shared/components/ui/skeleton';
|
|
20
|
+
import { getLanguage, t } from '@/shared/lib/i18n';
|
|
21
|
+
import { cn } from '@/shared/lib/utils';
|
|
22
|
+
|
|
23
|
+
export type MarketplaceFilter = 'all' | 'featured' | 'personal' | 'local';
|
|
24
|
+
|
|
25
|
+
export function AppMarketplaceCatalog({
|
|
26
|
+
error,
|
|
27
|
+
filter,
|
|
28
|
+
installedById,
|
|
29
|
+
isError,
|
|
30
|
+
isFetchingNextPage,
|
|
31
|
+
isLoading,
|
|
32
|
+
isStarting,
|
|
33
|
+
items,
|
|
34
|
+
localeFallbacks,
|
|
35
|
+
onFilterChange,
|
|
36
|
+
onInstall,
|
|
37
|
+
onLoadMore,
|
|
38
|
+
onSearchChange,
|
|
39
|
+
onSelect,
|
|
40
|
+
onUpdate,
|
|
41
|
+
operations,
|
|
42
|
+
hasNextPage,
|
|
43
|
+
search,
|
|
44
|
+
startingSource,
|
|
45
|
+
}: {
|
|
46
|
+
error: Error | null;
|
|
47
|
+
filter: MarketplaceFilter;
|
|
48
|
+
installedById: Map<string, AppPackageView>;
|
|
49
|
+
isError: boolean;
|
|
50
|
+
isFetchingNextPage: boolean;
|
|
51
|
+
isLoading: boolean;
|
|
52
|
+
isStarting: boolean;
|
|
53
|
+
items: AppMarketplaceItemView[];
|
|
54
|
+
localeFallbacks: string[];
|
|
55
|
+
onFilterChange: (value: MarketplaceFilter) => void;
|
|
56
|
+
onInstall: (source: string, registryUrl: string) => void;
|
|
57
|
+
onLoadMore: () => void;
|
|
58
|
+
onSearchChange: (value: string) => void;
|
|
59
|
+
onSelect: (slug: string) => void;
|
|
60
|
+
onUpdate: (appId: string) => void;
|
|
61
|
+
operations: AppPackageOperationView[];
|
|
62
|
+
hasNextPage: boolean;
|
|
63
|
+
search: string;
|
|
64
|
+
startingSource?: string;
|
|
65
|
+
}) {
|
|
66
|
+
return (
|
|
67
|
+
<>
|
|
68
|
+
<MarketplaceCatalogHeader
|
|
69
|
+
filter={filter}
|
|
70
|
+
onFilterChange={onFilterChange}
|
|
71
|
+
onSearchChange={onSearchChange}
|
|
72
|
+
search={search}
|
|
73
|
+
/>
|
|
74
|
+
<div className="custom-scrollbar min-h-0 flex-1 overflow-y-auto bg-background px-4 py-4 sm:px-6 sm:py-5">
|
|
75
|
+
{error ? (
|
|
76
|
+
<div role="alert" className="mb-2 rounded-xl bg-destructive/8 px-3 py-2.5 text-xs leading-5 text-destructive ring-1 ring-destructive/15">
|
|
77
|
+
{error.message || t('appPackagesActionFailed')}
|
|
78
|
+
</div>
|
|
79
|
+
) : null}
|
|
80
|
+
<MarketplaceCatalogContent
|
|
81
|
+
installedById={installedById}
|
|
82
|
+
isError={isError}
|
|
83
|
+
isFetchingNextPage={isFetchingNextPage}
|
|
84
|
+
isLoading={isLoading}
|
|
85
|
+
isStarting={isStarting}
|
|
86
|
+
items={items}
|
|
87
|
+
localeFallbacks={localeFallbacks}
|
|
88
|
+
onInstall={onInstall}
|
|
89
|
+
onLoadMore={onLoadMore}
|
|
90
|
+
onSelect={onSelect}
|
|
91
|
+
onUpdate={onUpdate}
|
|
92
|
+
operations={operations}
|
|
93
|
+
hasNextPage={hasNextPage}
|
|
94
|
+
startingSource={startingSource}
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
</>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function MarketplaceCatalogHeader({
|
|
102
|
+
filter,
|
|
103
|
+
onFilterChange,
|
|
104
|
+
onSearchChange,
|
|
105
|
+
search,
|
|
106
|
+
}: {
|
|
107
|
+
filter: MarketplaceFilter;
|
|
108
|
+
onFilterChange: (value: MarketplaceFilter) => void;
|
|
109
|
+
onSearchChange: (value: string) => void;
|
|
110
|
+
search: string;
|
|
111
|
+
}) {
|
|
112
|
+
const filters: Array<{ value: MarketplaceFilter; label: string }> = [
|
|
113
|
+
{ value: 'all', label: t('appPackagesFilterAll') },
|
|
114
|
+
{ value: 'featured', label: t('appPackagesMarketplaceFeatured') },
|
|
115
|
+
{ value: 'personal', label: t('appPackagesFilterPersonal') },
|
|
116
|
+
{ value: 'local', label: t('appPackagesFilterLocal') },
|
|
117
|
+
];
|
|
118
|
+
return (
|
|
119
|
+
<div className="shrink-0 border-b border-border/60 bg-card px-5 pb-3.5 pt-[max(1.25rem,env(safe-area-inset-top))] sm:px-6 sm:pt-5">
|
|
120
|
+
<DialogHeader className="pr-8">
|
|
121
|
+
<DialogTitle className="text-xl">{t('appPackagesMarketplaceTitle')}</DialogTitle>
|
|
122
|
+
<DialogDescription className="max-w-xl leading-5">
|
|
123
|
+
{t('appPackagesMarketplaceDescription')}
|
|
124
|
+
</DialogDescription>
|
|
125
|
+
</DialogHeader>
|
|
126
|
+
<div className="relative mt-4">
|
|
127
|
+
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
128
|
+
<Input
|
|
129
|
+
autoFocus
|
|
130
|
+
className="h-10 rounded-xl border-transparent bg-muted/65 pl-9 shadow-none focus-visible:ring-1"
|
|
131
|
+
value={search}
|
|
132
|
+
onChange={(event) => onSearchChange(event.target.value)}
|
|
133
|
+
placeholder={t('appPackagesMarketplaceSearch')}
|
|
134
|
+
aria-label={t('appPackagesMarketplaceSearch')}
|
|
135
|
+
/>
|
|
136
|
+
</div>
|
|
137
|
+
<div className="mt-3 flex gap-1 overflow-x-auto" aria-label={t('appPackagesFilterLabel')}>
|
|
138
|
+
{filters.map((entry) => (
|
|
139
|
+
<button
|
|
140
|
+
key={entry.value}
|
|
141
|
+
type="button"
|
|
142
|
+
onClick={() => onFilterChange(entry.value)}
|
|
143
|
+
className={cn(
|
|
144
|
+
'shrink-0 rounded-full px-3 py-1.5 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border',
|
|
145
|
+
filter === entry.value
|
|
146
|
+
? 'bg-foreground text-background'
|
|
147
|
+
: 'text-muted-foreground hover:bg-[var(--interaction-hover)] hover:text-foreground',
|
|
148
|
+
)}
|
|
149
|
+
>
|
|
150
|
+
{entry.label}
|
|
151
|
+
</button>
|
|
152
|
+
))}
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function MarketplaceCatalogContent({
|
|
159
|
+
installedById,
|
|
160
|
+
isError,
|
|
161
|
+
isFetchingNextPage,
|
|
162
|
+
isLoading,
|
|
163
|
+
isStarting,
|
|
164
|
+
items,
|
|
165
|
+
localeFallbacks,
|
|
166
|
+
onInstall,
|
|
167
|
+
onLoadMore,
|
|
168
|
+
onSelect,
|
|
169
|
+
onUpdate,
|
|
170
|
+
operations,
|
|
171
|
+
hasNextPage,
|
|
172
|
+
startingSource,
|
|
173
|
+
}: {
|
|
174
|
+
installedById: Map<string, AppPackageView>;
|
|
175
|
+
isError: boolean;
|
|
176
|
+
isFetchingNextPage: boolean;
|
|
177
|
+
isLoading: boolean;
|
|
178
|
+
isStarting: boolean;
|
|
179
|
+
items: AppMarketplaceItemView[];
|
|
180
|
+
localeFallbacks: string[];
|
|
181
|
+
onInstall: (source: string, registryUrl: string) => void;
|
|
182
|
+
onLoadMore: () => void;
|
|
183
|
+
onSelect: (slug: string) => void;
|
|
184
|
+
onUpdate: (appId: string) => void;
|
|
185
|
+
operations: AppPackageOperationView[];
|
|
186
|
+
hasNextPage: boolean;
|
|
187
|
+
startingSource?: string;
|
|
188
|
+
}) {
|
|
189
|
+
if (isLoading) {
|
|
190
|
+
return (
|
|
191
|
+
<div className="grid gap-3 sm:grid-cols-2 min-[820px]:grid-cols-3" aria-label={t('appPackagesMarketplaceLoading')}>
|
|
192
|
+
<Skeleton className="aspect-[16/9] w-full rounded-2xl" />
|
|
193
|
+
<Skeleton className="aspect-[16/9] w-full rounded-2xl" />
|
|
194
|
+
<Skeleton className="hidden aspect-[16/9] w-full rounded-2xl min-[820px]:block" />
|
|
195
|
+
</div>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
if (isError) {
|
|
199
|
+
return (
|
|
200
|
+
<div role="alert" className="rounded-xl border border-destructive/20 bg-destructive/5 px-4 py-5 text-sm text-destructive">
|
|
201
|
+
{t('appPackagesMarketplaceFailed')}
|
|
202
|
+
</div>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
if (items.length === 0) {
|
|
206
|
+
return (
|
|
207
|
+
<div className="flex flex-col items-center px-6 py-14 text-center">
|
|
208
|
+
<span className="flex h-11 w-11 items-center justify-center rounded-2xl bg-muted">
|
|
209
|
+
<Search className="h-5 w-5 text-muted-foreground" />
|
|
210
|
+
</span>
|
|
211
|
+
<p className="mt-3 text-sm font-medium text-foreground">{t('appPackagesMarketplaceEmpty')}</p>
|
|
212
|
+
<p className="mt-1 text-xs text-muted-foreground">{t('appPackagesMarketplaceEmptyHint')}</p>
|
|
213
|
+
</div>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
return (
|
|
217
|
+
<div>
|
|
218
|
+
<div className="grid gap-3 sm:grid-cols-2 min-[820px]:grid-cols-3">
|
|
219
|
+
{items.map((item) => (
|
|
220
|
+
<MarketplaceCatalogCard
|
|
221
|
+
key={item.id}
|
|
222
|
+
installedPackage={installedById.get(item.appId)}
|
|
223
|
+
isStarting={isStarting && startingSource === item.install.spec}
|
|
224
|
+
item={item}
|
|
225
|
+
localeFallbacks={localeFallbacks}
|
|
226
|
+
onInstall={onInstall}
|
|
227
|
+
onSelect={onSelect}
|
|
228
|
+
onUpdate={onUpdate}
|
|
229
|
+
operation={findLatestAppPackageOperation(operations, item.appId, item.install.spec)}
|
|
230
|
+
/>
|
|
231
|
+
))}
|
|
232
|
+
</div>
|
|
233
|
+
{hasNextPage ? (
|
|
234
|
+
<div className="flex justify-center pt-5">
|
|
235
|
+
<button
|
|
236
|
+
type="button"
|
|
237
|
+
disabled={isFetchingNextPage}
|
|
238
|
+
onClick={onLoadMore}
|
|
239
|
+
className="rounded-full border border-border bg-card px-4 py-2 text-xs font-medium text-foreground transition hover:bg-[var(--interaction-hover)] disabled:cursor-wait disabled:opacity-60"
|
|
240
|
+
>
|
|
241
|
+
{isFetchingNextPage ? t('appPackagesMarketplaceLoadingMore') : t('appPackagesMarketplaceLoadMore')}
|
|
242
|
+
</button>
|
|
243
|
+
</div>
|
|
244
|
+
) : null}
|
|
245
|
+
</div>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function MarketplaceCatalogCard({
|
|
250
|
+
installedPackage,
|
|
251
|
+
isStarting,
|
|
252
|
+
item,
|
|
253
|
+
localeFallbacks,
|
|
254
|
+
onInstall,
|
|
255
|
+
onSelect,
|
|
256
|
+
onUpdate,
|
|
257
|
+
operation,
|
|
258
|
+
}: {
|
|
259
|
+
installedPackage?: AppPackageView;
|
|
260
|
+
isStarting: boolean;
|
|
261
|
+
item: AppMarketplaceItemView;
|
|
262
|
+
localeFallbacks: string[];
|
|
263
|
+
onInstall: (source: string, registryUrl: string) => void;
|
|
264
|
+
onSelect: (slug: string) => void;
|
|
265
|
+
onUpdate: (appId: string) => void;
|
|
266
|
+
operation?: AppPackageOperationView;
|
|
267
|
+
}) {
|
|
268
|
+
const displayName = installedPackage
|
|
269
|
+
? readLocalizedText(installedPackage.name, installedPackage.nameI18n)
|
|
270
|
+
: item.name;
|
|
271
|
+
const summary = pickLocalizedText(item.summaryI18n, item.summary, localeFallbacks);
|
|
272
|
+
const active = operation ? isAppPackageOperationActive(operation.status) : false;
|
|
273
|
+
const failed = operation?.status === 'failed' || operation?.status === 'interrupted';
|
|
274
|
+
const canUpdate = Boolean(
|
|
275
|
+
installedPackage && installedPackage.activeVersion !== item.latestVersion,
|
|
276
|
+
);
|
|
277
|
+
return (
|
|
278
|
+
<article className="group relative overflow-hidden rounded-2xl border border-border/60 bg-card shadow-sm transition duration-200 hover:-translate-y-0.5 hover:border-border hover:shadow-lg">
|
|
279
|
+
<button
|
|
280
|
+
type="button"
|
|
281
|
+
onClick={() => onSelect(item.slug)}
|
|
282
|
+
className="block w-full text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
|
|
283
|
+
>
|
|
284
|
+
<AppMarketplaceCover accentColor={item.accentColor} coverPreview={item.coverPreview} coverUrl={item.coverUrl} name={displayName} className="aspect-[16/9] rounded-none ring-0" />
|
|
285
|
+
</button>
|
|
286
|
+
<div className="p-3.5">
|
|
287
|
+
<div className="flex items-start gap-3">
|
|
288
|
+
<AppArtwork icon={item.iconUrl ?? installedPackage?.icon} name={displayName} className="h-11 w-11 rounded-[13px]" />
|
|
289
|
+
<button
|
|
290
|
+
type="button"
|
|
291
|
+
onClick={() => onSelect(item.slug)}
|
|
292
|
+
className="min-w-0 flex-1 rounded-lg text-left focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
|
|
293
|
+
>
|
|
294
|
+
<span className="flex min-w-0 items-center gap-2">
|
|
295
|
+
<span className="truncate text-sm font-semibold text-foreground">{displayName}</span>
|
|
296
|
+
{item.featured ? <Sparkles className="h-3.5 w-3.5 shrink-0 text-amber-500" aria-label={t('appPackagesMarketplaceFeatured')} /> : null}
|
|
297
|
+
</span>
|
|
298
|
+
<span className="mt-1 line-clamp-2 block min-h-10 text-xs leading-5 text-muted-foreground">{summary}</span>
|
|
299
|
+
<span className="mt-2 flex items-center gap-1.5 text-[11px] text-muted-foreground/75">
|
|
300
|
+
<span>{item.publisher.name}</span>
|
|
301
|
+
<span aria-hidden="true">·</span>
|
|
302
|
+
<span>v{item.latestVersion}</span>
|
|
303
|
+
<ChevronRight className="ml-0.5 h-3 w-3 opacity-0 transition-opacity group-hover:opacity-60" />
|
|
304
|
+
</span>
|
|
305
|
+
</button>
|
|
306
|
+
</div>
|
|
307
|
+
<div className="mt-3 flex items-center justify-end border-t border-border/50 pt-3">
|
|
308
|
+
<MarketplaceInstallButton
|
|
309
|
+
active={active}
|
|
310
|
+
canUpdate={canUpdate}
|
|
311
|
+
failed={failed}
|
|
312
|
+
installed={Boolean(installedPackage)}
|
|
313
|
+
isStarting={isStarting}
|
|
314
|
+
onAction={() => canUpdate
|
|
315
|
+
? onUpdate(item.appId)
|
|
316
|
+
: onInstall(item.install.spec, item.install.registry)}
|
|
317
|
+
operation={operation}
|
|
318
|
+
/>
|
|
319
|
+
</div>
|
|
320
|
+
</div>
|
|
321
|
+
{operation && (active || failed) ? <div className="px-4 pb-4"><OperationProgress operation={operation} /></div> : null}
|
|
322
|
+
</article>
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function readLocalizedText(
|
|
327
|
+
fallback: string | undefined,
|
|
328
|
+
localized: Record<string, string> | undefined,
|
|
329
|
+
): string {
|
|
330
|
+
const languageTag = getLanguage() === 'zh' ? 'zh-CN' : 'en-US';
|
|
331
|
+
return localized?.[languageTag] ?? fallback ?? '';
|
|
332
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useState, type CSSProperties } from 'react';
|
|
2
|
+
import { cn } from '@/shared/lib/utils';
|
|
3
|
+
|
|
4
|
+
const COVER_PREVIEW_LABEL = import.meta.env.VITE_APP_MARKETPLACE_PREVIEW_LABEL;
|
|
5
|
+
|
|
6
|
+
export function AppMarketplaceCover({
|
|
7
|
+
accentColor,
|
|
8
|
+
className,
|
|
9
|
+
coverPreview,
|
|
10
|
+
coverUrl,
|
|
11
|
+
name,
|
|
12
|
+
}: {
|
|
13
|
+
accentColor?: string;
|
|
14
|
+
className?: string;
|
|
15
|
+
coverPreview?: boolean;
|
|
16
|
+
coverUrl?: string;
|
|
17
|
+
name: string;
|
|
18
|
+
}) {
|
|
19
|
+
const [failedUrl, setFailedUrl] = useState<string>();
|
|
20
|
+
const visibleCover = coverUrl && failedUrl !== coverUrl ? coverUrl : undefined;
|
|
21
|
+
return (
|
|
22
|
+
<span
|
|
23
|
+
className={cn(
|
|
24
|
+
'relative block aspect-[8/5] overflow-hidden rounded-2xl bg-muted ring-1 ring-black/[0.05] dark:ring-white/[0.07]',
|
|
25
|
+
className,
|
|
26
|
+
)}
|
|
27
|
+
style={{ '--app-accent': accentColor ?? '#78716c' } as CSSProperties}
|
|
28
|
+
>
|
|
29
|
+
{visibleCover ? (
|
|
30
|
+
<>
|
|
31
|
+
<img
|
|
32
|
+
src={visibleCover}
|
|
33
|
+
alt={`${name} app cover`}
|
|
34
|
+
className="h-full w-full object-cover transition-transform duration-500 group-hover:scale-[1.015]"
|
|
35
|
+
loading="lazy"
|
|
36
|
+
onError={() => setFailedUrl(visibleCover)}
|
|
37
|
+
/>
|
|
38
|
+
{coverPreview && COVER_PREVIEW_LABEL ? (
|
|
39
|
+
<span className="absolute bottom-2 right-2 rounded-full bg-background/75 px-2 py-1 text-[9px] font-medium text-foreground/70 shadow-sm ring-1 ring-black/[0.06] backdrop-blur-md">
|
|
40
|
+
{COVER_PREVIEW_LABEL}
|
|
41
|
+
</span>
|
|
42
|
+
) : null}
|
|
43
|
+
</>
|
|
44
|
+
) : (
|
|
45
|
+
<span
|
|
46
|
+
aria-label={`${name} 暂无封面`}
|
|
47
|
+
className="flex h-full w-full items-center justify-center bg-[radial-gradient(circle_at_30%_25%,color-mix(in_srgb,var(--app-accent)_28%,transparent),transparent_50%),linear-gradient(145deg,var(--muted),var(--background))]"
|
|
48
|
+
>
|
|
49
|
+
<span className="flex h-14 w-14 items-center justify-center rounded-2xl bg-background/65 text-xl font-semibold text-foreground/65 shadow-sm ring-1 ring-black/[0.05] backdrop-blur-sm">
|
|
50
|
+
{readCoverMonogram(name)}
|
|
51
|
+
</span>
|
|
52
|
+
</span>
|
|
53
|
+
)}
|
|
54
|
+
</span>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readCoverMonogram(name: string): string {
|
|
59
|
+
const words = name.trim().split(/\s+/).filter(Boolean);
|
|
60
|
+
if (words.length > 1) {
|
|
61
|
+
return words.slice(0, 2).map((word) => word[0]).join('').toLocaleUpperCase();
|
|
62
|
+
}
|
|
63
|
+
return [...(words[0] ?? '?')].slice(0, 1).join('').toLocaleUpperCase();
|
|
64
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import type { AppPackageOperationView, AppPackageView } from '@nextclaw/client-sdk';
|
|
2
|
+
import { ExternalLink, FileText, Server, ShieldCheck, type LucideIcon } from 'lucide-react';
|
|
3
|
+
import { AppArtwork } from '@/features/apps/components/app-artwork';
|
|
4
|
+
import { AppMarketplaceCover } from '@/features/apps/components/app-marketplace-cover';
|
|
5
|
+
import {
|
|
6
|
+
MarketplaceInstallButton,
|
|
7
|
+
OperationProgress,
|
|
8
|
+
} from '@/features/apps/components/app-marketplace-operation';
|
|
9
|
+
import { isAppPackageOperationActive } from '@/features/apps/hooks/use-app-packages';
|
|
10
|
+
import type { AppMarketplaceDetailView } from '@/features/apps/types/app-marketplace.types';
|
|
11
|
+
import { pickLocalizedText } from '@/features/marketplace';
|
|
12
|
+
import { Skeleton } from '@/shared/components/ui/skeleton';
|
|
13
|
+
import { t } from '@/shared/lib/i18n';
|
|
14
|
+
|
|
15
|
+
export function MarketplaceDetail({
|
|
16
|
+
detail,
|
|
17
|
+
error,
|
|
18
|
+
installedPackage,
|
|
19
|
+
isLoading,
|
|
20
|
+
isStarting,
|
|
21
|
+
localeFallbacks,
|
|
22
|
+
onInstall,
|
|
23
|
+
onUpdate,
|
|
24
|
+
operation,
|
|
25
|
+
}: {
|
|
26
|
+
detail?: AppMarketplaceDetailView;
|
|
27
|
+
error: Error | null;
|
|
28
|
+
installedPackage?: AppPackageView;
|
|
29
|
+
isLoading: boolean;
|
|
30
|
+
isStarting: boolean;
|
|
31
|
+
localeFallbacks: string[];
|
|
32
|
+
onInstall: (source: string, registryUrl: string) => void;
|
|
33
|
+
onUpdate: (appId: string) => void;
|
|
34
|
+
operation?: AppPackageOperationView;
|
|
35
|
+
}) {
|
|
36
|
+
if (isLoading) {
|
|
37
|
+
return <div className="space-y-4 p-6"><Skeleton className="h-24 w-full rounded-2xl" /><Skeleton className="h-40 w-full rounded-2xl" /></div>;
|
|
38
|
+
}
|
|
39
|
+
if (error || !detail) {
|
|
40
|
+
return <div role="alert" className="m-6 rounded-xl border border-destructive/20 bg-destructive/5 px-4 py-5 text-sm text-destructive">{error?.message ?? t('appPackagesMarketplaceFailed')}</div>;
|
|
41
|
+
}
|
|
42
|
+
const summary = pickLocalizedText(detail.summaryI18n, detail.summary, localeFallbacks);
|
|
43
|
+
const description = pickLocalizedText(detail.descriptionI18n, detail.description ?? summary, localeFallbacks);
|
|
44
|
+
const active = operation ? isAppPackageOperationActive(operation.status) : false;
|
|
45
|
+
const failed = operation?.status === 'failed' || operation?.status === 'interrupted';
|
|
46
|
+
const components = detail.manifest.components ?? [];
|
|
47
|
+
const panelCount = components.filter((entry) => entry.kind === 'panel').length;
|
|
48
|
+
const serviceCount = components.filter((entry) => entry.kind === 'service').length;
|
|
49
|
+
const canUpdate = Boolean(
|
|
50
|
+
installedPackage && installedPackage.activeVersion !== detail.latestVersion,
|
|
51
|
+
);
|
|
52
|
+
return (
|
|
53
|
+
<div className="mx-auto w-full max-w-3xl px-5 py-5 sm:px-7 sm:py-6">
|
|
54
|
+
<div className="grid items-stretch gap-5 sm:grid-cols-[minmax(0,1.18fr)_minmax(240px,0.82fr)]">
|
|
55
|
+
<AppMarketplaceCover accentColor={detail.accentColor} coverPreview={detail.coverPreview} coverUrl={detail.coverUrl} name={detail.name} className="aspect-[16/9] rounded-2xl" />
|
|
56
|
+
<div className="flex min-w-0 flex-col rounded-2xl border border-border/60 bg-card p-4">
|
|
57
|
+
<div className="flex items-start gap-3">
|
|
58
|
+
<AppArtwork icon={detail.iconUrl ?? installedPackage?.icon} name={detail.name} className="h-14 w-14 rounded-2xl" />
|
|
59
|
+
<div className="min-w-0 flex-1">
|
|
60
|
+
<h2 className="text-lg font-semibold tracking-tight text-foreground">{detail.name}</h2>
|
|
61
|
+
<div className="mt-1.5 flex flex-wrap items-center gap-2 text-[11px] text-muted-foreground">
|
|
62
|
+
<span className="inline-flex items-center gap-1"><ShieldCheck className="h-3.5 w-3.5" />{detail.publisher.name}</span>
|
|
63
|
+
<span>v{detail.latestVersion}</span>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
<p className="mt-3 text-sm leading-6 text-muted-foreground">{summary}</p>
|
|
68
|
+
<div className="mt-auto pt-4">
|
|
69
|
+
<MarketplaceInstallButton
|
|
70
|
+
active={active}
|
|
71
|
+
canUpdate={canUpdate}
|
|
72
|
+
failed={failed}
|
|
73
|
+
installed={Boolean(installedPackage)}
|
|
74
|
+
isStarting={isStarting}
|
|
75
|
+
onAction={() => canUpdate
|
|
76
|
+
? onUpdate(detail.appId)
|
|
77
|
+
: onInstall(detail.install.spec, detail.install.registry)}
|
|
78
|
+
operation={operation}
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
{operation && (active || failed) ? <OperationProgress operation={operation} /> : null}
|
|
84
|
+
|
|
85
|
+
<section className="mt-7">
|
|
86
|
+
<h3 className="text-sm font-semibold text-foreground">{t('appPackagesAboutApp')}</h3>
|
|
87
|
+
<p className="mt-2 text-sm leading-6 text-muted-foreground">{description}</p>
|
|
88
|
+
</section>
|
|
89
|
+
|
|
90
|
+
<div className="mt-6 grid gap-3 sm:grid-cols-2">
|
|
91
|
+
<DetailFact icon={FileText} label={t('appPackagesPanelsAdded')} value={panelCount > 0 ? String(panelCount) : t('appPackagesNone')} />
|
|
92
|
+
<DetailFact icon={Server} label={t('appPackagesServicesAdded')} value={serviceCount > 0 ? String(serviceCount) : t('appPackagesNone')} />
|
|
93
|
+
{detail.manifest.engines?.nextclaw ? (
|
|
94
|
+
<DetailFact
|
|
95
|
+
icon={ShieldCheck}
|
|
96
|
+
label={t('appPackagesNextClawRequirement')}
|
|
97
|
+
value={detail.manifest.engines.nextclaw}
|
|
98
|
+
/>
|
|
99
|
+
) : null}
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<section className="mt-7 border-t border-border/60 pt-6">
|
|
103
|
+
<h3 className="text-sm font-semibold text-foreground">{t('appPackagesAccessTitle')}</h3>
|
|
104
|
+
<div className="mt-3 space-y-2">
|
|
105
|
+
{readPermissionRows(detail).map((entry) => (
|
|
106
|
+
<div key={entry} className="flex items-start gap-2.5 rounded-xl bg-muted/45 px-3 py-2.5 text-xs leading-5 text-muted-foreground">
|
|
107
|
+
<ShieldCheck className="mt-0.5 h-3.5 w-3.5 shrink-0" />
|
|
108
|
+
<span>{entry}</span>
|
|
109
|
+
</div>
|
|
110
|
+
))}
|
|
111
|
+
</div>
|
|
112
|
+
</section>
|
|
113
|
+
|
|
114
|
+
<a
|
|
115
|
+
href={detail.webUrl}
|
|
116
|
+
target="_blank"
|
|
117
|
+
rel="noreferrer"
|
|
118
|
+
className="mt-6 inline-flex items-center gap-1.5 text-xs font-medium text-muted-foreground underline-offset-4 hover:text-foreground hover:underline focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
|
|
119
|
+
>
|
|
120
|
+
{t('appPackagesOpenPublicDetails')}
|
|
121
|
+
<ExternalLink className="h-3.5 w-3.5" />
|
|
122
|
+
</a>
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function DetailFact({
|
|
128
|
+
icon: Icon,
|
|
129
|
+
label,
|
|
130
|
+
value,
|
|
131
|
+
}: {
|
|
132
|
+
icon: LucideIcon;
|
|
133
|
+
label: string;
|
|
134
|
+
value: string;
|
|
135
|
+
}) {
|
|
136
|
+
return (
|
|
137
|
+
<div className="flex items-center gap-3 rounded-xl border border-border/60 px-3 py-3">
|
|
138
|
+
<span className="flex h-8 w-8 items-center justify-center rounded-lg bg-muted text-muted-foreground"><Icon className="h-4 w-4" /></span>
|
|
139
|
+
<div><p className="text-[11px] text-muted-foreground">{label}</p><p className="text-sm font-medium text-foreground">{value}</p></div>
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function readPermissionRows(detail: AppMarketplaceDetailView): string[] {
|
|
145
|
+
const rows: string[] = [];
|
|
146
|
+
const documentAccess = detail.permissions.documentAccess ?? [];
|
|
147
|
+
if (documentAccess.length > 0) {
|
|
148
|
+
rows.push(t('appPackagesDocumentAccess').replace('{count}', String(documentAccess.length)));
|
|
149
|
+
}
|
|
150
|
+
const domains = detail.permissions.allowedDomains ?? [];
|
|
151
|
+
if (domains.length > 0) {
|
|
152
|
+
rows.push(t('appPackagesNetworkAccess').replace('{count}', String(domains.length)));
|
|
153
|
+
}
|
|
154
|
+
if (detail.permissions.capabilities?.hostBridge) {
|
|
155
|
+
rows.push(t('appPackagesHostBridgeAccess'));
|
|
156
|
+
}
|
|
157
|
+
if (detail.permissions.storage?.namespace) {
|
|
158
|
+
rows.push(t('appPackagesLocalStorageAccess'));
|
|
159
|
+
}
|
|
160
|
+
return rows.length > 0 ? rows : [t('appPackagesNoExtraAccess')];
|
|
161
|
+
}
|