@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.
Files changed (125) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/assets/{api-CK3Fro7z.js → api-a_GaOFc7.js} +4 -4
  3. package/dist/assets/{appearance-settings-page-zqwWO8Dy.js → appearance-settings-page-DlGXLgff.js} +1 -1
  4. package/dist/assets/{channels-list-page-CVCZBlCL.js → channels-list-page-BEEk4Uc9.js} +2 -2
  5. package/dist/assets/{chat-page-D4wehRTz.js → chat-page-DUQXXqQm.js} +1 -1
  6. package/dist/assets/{config-split-page-CceNFvlY.js → config-split-page-ps1Aod3g.js} +1 -1
  7. package/dist/assets/{confirm-dialog-CWeNYWZY.js → confirm-dialog-DJSUNG33.js} +2 -2
  8. package/dist/assets/desktop-update-config--zagjHic.js +1 -0
  9. package/dist/assets/{dist-D9xtGgdL.js → dist-BkTYNzm_.js} +1 -1
  10. package/dist/assets/{dist-AYxkFx2B.js → dist-DkR7_Dpl.js} +1 -1
  11. package/dist/assets/doc-browser-DahzJEWj.js +1 -0
  12. package/dist/assets/doc-browser-Q67TOGVS.js +1 -0
  13. package/dist/assets/{doc-browser-context-Cb7Z0j90.js → doc-browser-context-BQim8SqD.js} +1 -1
  14. package/dist/assets/form-actions-B2-bpFie.js +1 -0
  15. package/dist/assets/index-Ba_o5fu9.js +109 -0
  16. package/dist/assets/index-DSLK7zgR.css +1 -0
  17. package/dist/assets/loader-circle-DyXZU92l.js +1 -0
  18. package/dist/assets/mcp-marketplace-page-B0dBE5CV.js +1 -0
  19. package/dist/assets/mcp-marketplace-page-w8panjZG.js +9 -0
  20. package/dist/assets/{model-config-page-DfOLyQqR.js → model-config-page-rrhFaque.js} +1 -1
  21. package/dist/assets/{navigation-link-MiiyoIjH.js → navigation-link-mpHFXwRr.js} +2 -2
  22. package/dist/assets/plus-Dhb4KuMH.js +1 -0
  23. package/dist/assets/{provider-scoped-model-input-DXzjr5l0.js → provider-scoped-model-input-CFsfWEt9.js} +1 -1
  24. package/dist/assets/{providers-config-page-CaO8WfZH.js → providers-config-page-D-TYFZsF.js} +1 -1
  25. package/dist/assets/react-BlXwvOLo.js +8 -0
  26. package/dist/assets/{refresh-cw-a2VVeeAF.js → refresh-cw-JFuyDj_P.js} +1 -1
  27. package/dist/assets/remote-BwVv_Qq-.js +1 -0
  28. package/dist/assets/rotate-cw-DjbbiIW_.js +1 -0
  29. package/dist/assets/{runtime-config-page-BuA4A02z.js → runtime-config-page-Br9j9Vxe.js} +1 -1
  30. package/dist/assets/{save-CGm23Yww.js → save-BL8P8CLX.js} +1 -1
  31. package/dist/assets/{search-CH2t--sH.js → search-bZL75Pgc.js} +1 -1
  32. package/dist/assets/{search-config-page-BAmYqoAz.js → search-config-page-B3fvzMd1.js} +1 -1
  33. package/dist/assets/{secrets-config-page-BlPOuql_.js → secrets-config-page-ClwTxtXd.js} +2 -2
  34. package/dist/assets/security-config-DmJw9QZA.js +1 -0
  35. package/dist/assets/{select-DnxxDpcV.js → select-rUZ8nIZu.js} +2 -2
  36. package/dist/assets/{settings-2-agnMYc8a.js → settings-2-BI4SCkxQ.js} +1 -1
  37. package/dist/assets/skeleton-CVmEPG43.js +1 -0
  38. package/dist/assets/{tag-chip-CPOc8b2N.js → tag-chip-BdI_yIOA.js} +1 -1
  39. package/dist/assets/x-BcEayhV6.js +1 -0
  40. package/dist/index.html +19 -19
  41. package/package.json +7 -7
  42. package/src/app/components/layout/app-layout.tsx +38 -2
  43. package/src/app/hooks/__tests__/use-app-event-consumers.test.ts +20 -0
  44. package/src/app/hooks/use-app-event-consumers.ts +21 -0
  45. package/src/app/presenters/app.presenter.ts +6 -0
  46. package/src/features/apps/components/__tests__/app-packages-panel.test.tsx +92 -14
  47. package/src/features/apps/components/__tests__/apps-panel.test.tsx +58 -0
  48. package/src/features/apps/components/app-artwork.tsx +49 -0
  49. package/src/features/apps/components/app-marketplace-catalog.tsx +332 -0
  50. package/src/features/apps/components/app-marketplace-cover.tsx +64 -0
  51. package/src/features/apps/components/app-marketplace-detail.tsx +161 -0
  52. package/src/features/apps/components/app-marketplace-dialog.tsx +107 -217
  53. package/src/features/apps/components/app-marketplace-operation.tsx +84 -0
  54. package/src/features/apps/components/app-package-card.tsx +201 -52
  55. package/src/features/apps/components/app-packages-panel.tsx +98 -26
  56. package/src/features/apps/components/apps-panel.tsx +28 -27
  57. package/src/features/apps/hooks/use-app-marketplace.test.ts +144 -0
  58. package/src/features/apps/hooks/use-app-marketplace.ts +55 -85
  59. package/src/features/apps/hooks/use-app-packages.ts +59 -4
  60. package/src/features/apps/index.ts +1 -0
  61. package/src/features/apps/managers/__tests__/app-package-operation-settlement.manager.test.ts +81 -0
  62. package/src/features/apps/managers/app-package-operation-settlement.manager.ts +80 -0
  63. package/src/features/apps/services/app-marketplace.service.ts +223 -0
  64. package/src/features/apps/types/app-marketplace.types.ts +75 -0
  65. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +64 -3
  66. package/src/features/chat/features/conversation/components/session-queued-input-rows.tsx +24 -14
  67. package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-controller.test.tsx +53 -0
  68. package/src/features/chat/features/conversation/hooks/__tests__/use-session-run-queue.test.tsx +18 -0
  69. package/src/features/chat/features/conversation/hooks/use-chat-composer-reference-intent.ts +4 -1
  70. package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +126 -32
  71. package/src/features/chat/features/conversation/hooks/use-session-run-queue.ts +12 -2
  72. package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +31 -0
  73. package/src/features/chat/features/input/utils/chat-composer-token-protocol.utils.ts +5 -0
  74. package/src/features/chat/features/input/utils/chat-inline-token-composer.utils.ts +14 -0
  75. package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +25 -0
  76. package/src/features/chat/features/message/hooks/use-chat-inline-token-actions.ts +8 -1
  77. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +77 -20
  78. package/src/features/chat/features/workspace/components/chat-session-workspace-directory-browser.tsx +56 -50
  79. package/src/features/chat/features/workspace/components/project-files/workspace-directory-tree.tsx +33 -11
  80. package/src/features/chat/features/workspace/components/project-files/workspace-project-tree-view.tsx +93 -0
  81. package/src/features/chat/features/workspace/stores/__tests__/workspace-project-tree.store.test.ts +38 -0
  82. package/src/features/chat/features/workspace/stores/workspace-project-tree.store.ts +160 -0
  83. package/src/features/chat/index.ts +1 -0
  84. package/src/features/chat/managers/__tests__/chat-composer-intent.manager.test.ts +29 -0
  85. package/src/features/chat/managers/chat-composer-intent.manager.ts +29 -2
  86. package/src/features/chat/presenters/chat.presenter.ts +2 -2
  87. package/src/features/panel-apps/components/__tests__/panel-apps-list.test.tsx +1 -1
  88. package/src/features/panel-apps/components/panel-apps-list.tsx +1 -6
  89. package/src/features/right-panel-resources/utils/__tests__/right-panel-resource-uri.utils.test.ts +34 -0
  90. package/src/features/right-panel-resources/utils/right-panel-resource-uri.utils.ts +29 -0
  91. package/src/features/service-apps/components/service-apps-panel.tsx +3 -17
  92. package/src/platforms/desktop/components/desktop-app-shell.tsx +5 -0
  93. package/src/platforms/mobile/components/mobile-app-shell.tsx +9 -1
  94. package/src/shared/components/actions/__tests__/navigation-link.test.tsx +12 -0
  95. package/src/shared/components/actions/navigation-link.tsx +6 -3
  96. package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +34 -0
  97. package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +5 -0
  98. package/src/shared/components/doc-browser/doc-browser.tsx +11 -1
  99. package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +13 -0
  100. package/src/shared/components/ui/tabs.tsx +24 -1
  101. package/src/shared/hooks/use-server-path-watch.ts +42 -0
  102. package/src/shared/lib/api/server-path/server-path.types.ts +10 -0
  103. package/src/shared/lib/api/types.ts +3 -0
  104. package/src/shared/lib/api/utils/server-path.utils.ts +10 -0
  105. package/src/shared/lib/i18n/locales/en-US/chat.json +1 -0
  106. package/src/shared/lib/i18n/locales/en-US/doc-browser.json +49 -7
  107. package/src/shared/lib/i18n/locales/zh-CN/chat.json +1 -0
  108. package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +49 -7
  109. package/vite.config.ts +32 -1
  110. package/dist/assets/desktop-update-config-Dirc3bpt.js +0 -1
  111. package/dist/assets/doc-browser-4Sud6dWw.js +0 -1
  112. package/dist/assets/doc-browser-D4x_yH5F.js +0 -1
  113. package/dist/assets/form-actions-B2q4jgjE.js +0 -1
  114. package/dist/assets/index-CdyTsSBu.css +0 -1
  115. package/dist/assets/index-Das_bw2h.js +0 -107
  116. package/dist/assets/loader-circle-CpmBdaon.js +0 -1
  117. package/dist/assets/mcp-marketplace-page-CdOY3Sop.js +0 -1
  118. package/dist/assets/mcp-marketplace-page-DvjrfqEt.js +0 -9
  119. package/dist/assets/plus-C8FkjpSZ.js +0 -1
  120. package/dist/assets/react-C8XFFvuz.js +0 -8
  121. package/dist/assets/remote-BJHXa4mB.js +0 -1
  122. package/dist/assets/rotate-cw-DJ7UF3F2.js +0 -1
  123. package/dist/assets/security-config-CbDiY2xr.js +0 -1
  124. package/dist/assets/skeleton-XpqeWL62.js +0 -1
  125. 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
+ }