@nextclaw/ui 0.12.24 → 0.12.25

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 (206) hide show
  1. package/CHANGELOG.md +68 -29
  2. package/dist/assets/api-DGD9_Bg4.js +15 -0
  3. package/dist/assets/app-manager-provider-oYdeYPSv.js +1 -0
  4. package/dist/assets/{book-open-DDlN5MvX.js → book-open-BcnAiKde.js} +1 -1
  5. package/dist/assets/channels-list-page-FJDuPwU6.js +8 -0
  6. package/dist/assets/chat-page-D1fMNBrT.js +1 -0
  7. package/dist/assets/config-split-page-CcrEUtwu.js +1 -0
  8. package/dist/assets/cpu-DPPwMzoC.js +3 -0
  9. package/dist/assets/{createLucideIcon-BLMK3QUd.js → createLucideIcon-DzY6wN61.js} +1 -1
  10. package/dist/assets/desktop-kk7qvZ-v.js +3 -0
  11. package/dist/assets/desktop-update-config-CP8dFYXK.js +1 -0
  12. package/dist/assets/{dialog-C3D7Be0p.js → dialog-BKo0RItd.js} +1 -1
  13. package/dist/assets/{dist-CPlbUgwU.js → dist-CFiwgaLs.js} +1 -1
  14. package/dist/assets/doc-browser-CAhfnm0D.js +1 -0
  15. package/dist/assets/{doc-browser-context-BJuMaI3o.js → doc-browser-context-FukQHvyo.js} +1 -1
  16. package/dist/assets/doc-browser-p9DDNPWB.js +1 -0
  17. package/dist/assets/doc-browser-rZIQIjuw.js +1 -0
  18. package/dist/assets/download-CMM8po31.js +1 -0
  19. package/dist/assets/{es2015-xqN1slyW.js → es2015-BhznEEyJ.js} +1 -1
  20. package/dist/assets/{external-link-DwfSfTLB.js → external-link-CpEvG65F.js} +1 -1
  21. package/dist/assets/i18n-D1144VAA.js +1 -0
  22. package/dist/assets/index-D-AAMKCt.js +103 -0
  23. package/dist/assets/index-DnBeV2Xm.css +1 -0
  24. package/dist/assets/{key-round-CJ5gDAAG.js → key-round-DUq47t0P.js} +1 -1
  25. package/dist/assets/marketplace-page-BrCLRIc4.js +105 -0
  26. package/dist/assets/marketplace-page-odDpPYEs.js +1 -0
  27. package/dist/assets/mcp-marketplace-page-CfbOBgKK.js +1 -0
  28. package/dist/assets/mcp-marketplace-page-DIq_SpMe.js +40 -0
  29. package/dist/assets/model-config-Bc6VVnxy.js +1 -0
  30. package/dist/assets/{notice-card-BFDbKQDA.js → notice-card-Dr6xCwva.js} +1 -1
  31. package/dist/assets/play-AqrNslHI.js +1 -0
  32. package/dist/assets/plus-B-YHtTNC.js +1 -0
  33. package/dist/assets/{popover-B86Dbfhf.js → popover-BDFNiLlg.js} +1 -1
  34. package/dist/assets/provider-scoped-model-input-BMTp4BEH.js +1 -0
  35. package/dist/assets/providers-list-DN0tvISH.js +1 -0
  36. package/dist/assets/refresh-cw-CrbD8EkT.js +1 -0
  37. package/dist/assets/remote-Dr3jcfWP.js +1 -0
  38. package/dist/assets/{rotate-cw-BZ2JObNs.js → rotate-cw-BN9yjccP.js} +1 -1
  39. package/dist/assets/runtime-config-page-CRWOwBbl.js +1 -0
  40. package/dist/assets/{save-euRxl8pI.js → save-CO_4qf6b.js} +1 -1
  41. package/dist/assets/{search-CLd7m0M7.js → search-CRtQwr-h.js} +1 -1
  42. package/dist/assets/search-config-C4c1yZSP.js +1 -0
  43. package/dist/assets/secrets-config-zAF30YfO.js +3 -0
  44. package/dist/assets/{select-CJ0wbo3D.js → select-BUTwE_lC.js} +1 -1
  45. package/dist/assets/{setting-row-D1Yygqp7.js → setting-row-BavcnXw1.js} +1 -1
  46. package/dist/assets/settings-MWL2SMyk.js +1 -0
  47. package/dist/assets/{sparkles-DVfeSVJQ.js → sparkles-BmgOD4nY.js} +1 -1
  48. package/dist/assets/{status-dot-ChvPCib9.js → status-dot-l3kPFdq_.js} +1 -1
  49. package/dist/assets/{tabs-custom-Hia_ong0.js → tabs-custom-D48zdZoc.js} +1 -1
  50. package/dist/assets/{tag-chip-FrkmkT8r.js → tag-chip-Dm2Lqnpu.js} +1 -1
  51. package/dist/assets/use-config-Cyv5IuSt.js +1 -0
  52. package/dist/assets/use-infinite-scroll-loader-Cvz8ZteY.js +1 -0
  53. package/dist/assets/x-BeyYA_h6.js +1 -0
  54. package/dist/index.html +29 -40
  55. package/package.json +9 -9
  56. package/src/app/components/layout/sidebar.layout.test.tsx +2 -4
  57. package/src/app/components/theme-provider.tsx +1 -0
  58. package/src/app/configs/app-navigation.config.ts +0 -6
  59. package/src/app/index.tsx +4 -7
  60. package/src/features/agents/components/agents-page.test.tsx +25 -15
  61. package/src/features/agents/components/agents-page.tsx +133 -172
  62. package/src/features/channels/components/config/channel-form.test.tsx +1 -0
  63. package/src/features/channels/components/config/channel-form.tsx +4 -3
  64. package/src/features/channels/components/config/weixin-channel-auth-section.test.tsx +38 -1
  65. package/src/features/channels/components/config/weixin-channel-auth-section.tsx +137 -40
  66. package/src/features/channels/index.ts +1 -1
  67. package/src/features/channels/utils/channel-form-fields.utils.test.ts +26 -0
  68. package/src/features/channels/utils/channel-form-fields.utils.ts +32 -18
  69. package/src/features/chat/components/chat-session-workspace-panel-nav.tsx +23 -4
  70. package/src/features/chat/components/chat-session-workspace-panel.tsx +34 -2
  71. package/src/features/chat/components/chat-sidebar-session-item.tsx +9 -3
  72. package/src/features/chat/components/conversation/chat-conversation-header.test.tsx +71 -0
  73. package/src/features/chat/components/conversation/chat-conversation-header.tsx +6 -0
  74. package/src/features/chat/components/conversation/chat-conversation-panel.test.tsx +181 -61
  75. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +54 -23
  76. package/src/features/chat/components/conversation/session-header/chat-session-header-actions.test.tsx +24 -0
  77. package/src/features/chat/components/conversation/session-header/chat-session-header-actions.tsx +26 -5
  78. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +174 -0
  79. package/src/features/chat/components/layout/chat-sidebar.test.tsx +45 -8
  80. package/src/features/chat/components/layout/chat-sidebar.tsx +29 -46
  81. package/src/features/chat/components/providers/chat-presenter.provider.tsx +2 -0
  82. package/src/features/chat/components/workspace/session-cron-job-content.tsx +103 -0
  83. package/src/features/chat/hooks/use-ncp-agent-runtime.test.tsx +14 -0
  84. package/src/features/chat/hooks/use-ncp-chat-page-data.test.tsx +70 -0
  85. package/src/features/chat/hooks/use-ncp-chat-page-data.ts +1 -1
  86. package/src/features/chat/hooks/use-ncp-child-session-tabs-view.ts +2 -8
  87. package/src/features/chat/hooks/use-ncp-session-list-view.ts +1 -2
  88. package/src/features/chat/managers/chat-session-list.manager.test.ts +7 -9
  89. package/src/features/chat/managers/chat-session-list.manager.ts +5 -10
  90. package/src/features/chat/managers/ncp-chat-input.manager.test.ts +0 -2
  91. package/src/features/chat/managers/ncp-chat-presenter.manager.ts +6 -0
  92. package/src/features/chat/managers/ncp-chat-thread.manager.test.ts +52 -1
  93. package/src/features/chat/managers/ncp-chat-thread.manager.ts +21 -0
  94. package/src/features/chat/pages/ncp-chat-page.tsx +5 -4
  95. package/src/features/chat/stores/chat-session-list.store.ts +0 -2
  96. package/src/features/chat/stores/chat-thread.store.ts +4 -0
  97. package/src/features/chat/utils/chat-session-display.utils.test.ts +83 -1
  98. package/src/features/chat/utils/chat-session-display.utils.ts +73 -0
  99. package/src/features/chat/utils/ncp-session-adapter.utils.test.ts +22 -0
  100. package/src/features/chat/utils/ncp-session-adapter.utils.ts +32 -0
  101. package/src/features/marketplace/components/curated-shelves/marketplace-curated-scene-route.test.tsx +235 -0
  102. package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.config.ts +162 -0
  103. package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.tsx +355 -0
  104. package/src/features/marketplace/components/curated-shelves/marketplace-shelf-card.tsx +118 -0
  105. package/src/features/marketplace/components/detail-doc/marketplace-detail-doc-renderer.ts +201 -0
  106. package/src/features/marketplace/components/detail-doc/marketplace-detail-doc.test.ts +40 -0
  107. package/src/features/marketplace/components/marketplace-catalog-grid.tsx +114 -0
  108. package/src/features/marketplace/components/marketplace-detail-doc.ts +73 -24
  109. package/src/features/marketplace/components/marketplace-item-icon.tsx +45 -0
  110. package/src/features/marketplace/components/marketplace-list-card.tsx +177 -93
  111. package/src/features/marketplace/components/marketplace-page-detail.test.tsx +9 -2
  112. package/src/features/marketplace/components/marketplace-page-parts.tsx +1 -1
  113. package/src/features/marketplace/components/marketplace-page.test.tsx +25 -6
  114. package/src/features/marketplace/components/marketplace-page.tsx +154 -132
  115. package/src/features/marketplace/hooks/use-marketplace-curated-scene-route.ts +97 -0
  116. package/src/features/marketplace/hooks/use-marketplace.ts +59 -3
  117. package/src/features/system-status/components/config/runtime-agent-list-card.tsx +4 -8
  118. package/src/features/system-status/components/config/runtime-binding-list-card.tsx +5 -7
  119. package/src/features/system-status/components/config/runtime-config-editor.tsx +1 -19
  120. package/src/features/system-status/components/config/runtime-entry-list-card.tsx +10 -11
  121. package/src/features/system-status/components/config/runtime-settings-card.tsx +15 -23
  122. package/src/features/system-status/components/runtime-control-card.test.tsx +8 -6
  123. package/src/features/system-status/components/runtime-control-card.tsx +7 -6
  124. package/src/features/system-status/pages/runtime-config-page.test.tsx +19 -9
  125. package/src/features/system-status/pages/runtime-config-page.tsx +2 -3
  126. package/src/features/system-status/utils/runtime-config-agent.utils.ts +4 -4
  127. package/src/features/system-status/utils/system-status.utils.ts +31 -6
  128. package/src/index.css +8 -0
  129. package/src/platforms/desktop/components/desktop-app-shell.test.tsx +67 -0
  130. package/src/platforms/desktop/components/desktop-app-shell.tsx +46 -18
  131. package/src/platforms/desktop/components/desktop-window-chrome.tsx +30 -0
  132. package/src/platforms/desktop/index.ts +6 -0
  133. package/src/platforms/desktop/types/desktop-update.types.ts +3 -0
  134. package/src/platforms/desktop/utils/desktop-host.utils.ts +56 -0
  135. package/src/shared/components/common/brand-header.tsx +36 -16
  136. package/src/shared/components/config/provider-form-support.ts +2 -22
  137. package/src/shared/components/cron-config.tsx +12 -58
  138. package/src/shared/components/doc-browser/doc-browser.tsx +4 -4
  139. package/src/shared/components/ui/select.tsx +19 -7
  140. package/src/shared/lib/api/channel-auth.types.ts +1 -0
  141. package/src/shared/lib/api/ncp-session.types.ts +9 -0
  142. package/src/shared/lib/api/types.ts +12 -1
  143. package/src/shared/lib/api/utils/marketplace.utils.ts +7 -1
  144. package/src/shared/lib/cron/cron-job-view.utils.ts +59 -0
  145. package/src/shared/lib/cron/index.ts +1 -0
  146. package/src/shared/lib/i18n/{channel-auth.ts → channel-auth.constants.ts} +31 -0
  147. package/src/shared/lib/i18n/chat-labels.utils.ts +3 -2
  148. package/src/shared/lib/i18n/index.ts +20 -59
  149. package/src/shared/lib/i18n/{runtime-control.ts → runtime-control-labels.utils.ts} +30 -1
  150. package/src/shared/lib/provider-models/index.test.ts +39 -0
  151. package/src/shared/lib/provider-models/index.ts +1 -3
  152. package/src/shared/lib/ui-document-title/index.ts +0 -1
  153. package/tsconfig.json +1 -0
  154. package/vite.config.ts +1 -1
  155. package/vitest.config.ts +1 -1
  156. package/dist/assets/api-D2xRKmZd.js +0 -15
  157. package/dist/assets/app-manager-provider-CNaZboG4.js +0 -1
  158. package/dist/assets/app-navigation.config-Ihhrrt--.js +0 -1
  159. package/dist/assets/channels-list-page-p26lgxLk.js +0 -8
  160. package/dist/assets/chat-Dkh2qtuz.js +0 -61
  161. package/dist/assets/chat-page-DoTmE2wx.js +0 -1
  162. package/dist/assets/chunk-JZWAC4HX-Kydj4yEz.js +0 -3
  163. package/dist/assets/config-split-page-DIOCjj2Q.js +0 -1
  164. package/dist/assets/desktop-update-config-DlpzDfKM.js +0 -1
  165. package/dist/assets/doc-browser-C8FM5fC0.js +0 -1
  166. package/dist/assets/doc-browser-RJUOL_GO.js +0 -1
  167. package/dist/assets/doc-browser-p82AdNO-.js +0 -1
  168. package/dist/assets/folder-CeJKPx5P.js +0 -1
  169. package/dist/assets/hash-BqxRTZW5.js +0 -1
  170. package/dist/assets/i18n-DnTGDIRw.js +0 -1
  171. package/dist/assets/index-D8MKmXtO.css +0 -1
  172. package/dist/assets/index-pBvbJ5Mt.js +0 -2
  173. package/dist/assets/loader-circle-fd-vQKtW.js +0 -1
  174. package/dist/assets/logo-badge-KAe-7d8c.js +0 -1
  175. package/dist/assets/logos-C4sYP1Vl.js +0 -1
  176. package/dist/assets/marketplace-page-Cql0kDi-.js +0 -1
  177. package/dist/assets/marketplace-page-m4P5g_Ht.js +0 -49
  178. package/dist/assets/mcp-marketplace-page-9WVKl1m1.js +0 -1
  179. package/dist/assets/mcp-marketplace-page-ByzBQZcx.js +0 -40
  180. package/dist/assets/message-square-z_osm9c0.js +0 -1
  181. package/dist/assets/model-config-Dbr_0APb.js +0 -1
  182. package/dist/assets/play-Dv6Nr1Ew.js +0 -1
  183. package/dist/assets/plus-D8eKFY7h.js +0 -1
  184. package/dist/assets/provider-scoped-model-input-DFm6N2f7.js +0 -1
  185. package/dist/assets/providers-list-BJcLOjun.js +0 -1
  186. package/dist/assets/refresh-ccw-ByVwmnN_.js +0 -1
  187. package/dist/assets/refresh-cw-PcqoYB3K.js +0 -1
  188. package/dist/assets/remote-BOxo9iwd.js +0 -1
  189. package/dist/assets/runtime-config-page-CjLhnbSl.js +0 -1
  190. package/dist/assets/search-config-J4Htco-P.js +0 -1
  191. package/dist/assets/secrets-config-CUdERjco.js +0 -3
  192. package/dist/assets/sessions-config-page-DpK991fs.js +0 -2
  193. package/dist/assets/settings-drbWqzA4.js +0 -1
  194. package/dist/assets/skeleton-BK1SOSRA.js +0 -1
  195. package/dist/assets/theme-provider-0hxjiPc_.js +0 -2
  196. package/dist/assets/tooltip-Cj4yA0gH.js +0 -1
  197. package/dist/assets/trash-2-CBsHCfqq.js +0 -1
  198. package/dist/assets/use-config-38Ur-89i.js +0 -1
  199. package/dist/assets/use-confirm-dialog-DPQThaeU.js +0 -1
  200. package/dist/assets/use-infinite-scroll-loader-5Gf1xQi7.js +0 -1
  201. package/dist/assets/use-viewport-layout-D1XzKeip.js +0 -1
  202. package/dist/assets/x-CM-XDMpk.js +0 -1
  203. package/src/features/chat/components/config/sessions-config-detail-pane.tsx +0 -244
  204. package/src/features/chat/pages/sessions-config-page.test.tsx +0 -152
  205. package/src/features/chat/pages/sessions-config-page.tsx +0 -192
  206. /package/dist/assets/{config-hints-MogHYQ8G.js → config-hints-BNfpOL4J.js} +0 -0
@@ -0,0 +1,40 @@
1
+ import { buildGenericDetailDataUrl } from "@/features/marketplace/components/marketplace-detail-doc";
2
+
3
+ function readDetailHtml(params: Parameters<typeof buildGenericDetailDataUrl>[0]) {
4
+ return decodeURIComponent(buildGenericDetailDataUrl(params));
5
+ }
6
+
7
+ describe("buildGenericDetailDataUrl", () => {
8
+ it("renders skill metadata and markdown content semantically", () => {
9
+ const html = readDetailHtml({
10
+ title: "Weather Skill",
11
+ typeLabel: "Skill",
12
+ spec: "@nextclaw/weather",
13
+ metadataRaw: "name: weather\ndescription: Local weather skill",
14
+ contentRaw: "# Weather Skill\n\nUse **weather** with `city`.\n\n- Local forecast\n- Severe alerts",
15
+ });
16
+
17
+ expect(html).toContain('<dl class="metadata-list">');
18
+ expect(html).toContain("<dt>name</dt><dd>weather</dd>");
19
+ expect(html).toContain("<h1>Weather Skill</h1>");
20
+ expect(html).toContain("<strong>weather</strong>");
21
+ expect(html).toContain("<code>city</code>");
22
+ expect(html).toContain("<li>Local forecast</li>");
23
+ expect(html).not.toContain('<pre class="code"># Weather Skill');
24
+ });
25
+
26
+ it("escapes marketplace content before rendering markdown", () => {
27
+ const html = readDetailHtml({
28
+ title: "Unsafe Skill",
29
+ typeLabel: "Skill",
30
+ spec: "@nextclaw/unsafe",
31
+ metadataRaw: '{"name":"unsafe","nested":{"script":"<script>alert(1)</script>"}}',
32
+ contentRaw: "[safe](https://example.com) <script>alert(1)</script>",
33
+ });
34
+
35
+ expect(html).toContain("<dt>nested</dt>");
36
+ expect(html).toContain("&lt;script&gt;alert(1)&lt;/script&gt;");
37
+ expect(html).toContain('<a href="https://example.com" target="_blank" rel="noopener noreferrer">safe</a>');
38
+ expect(html).not.toContain("<script>alert(1)</script>");
39
+ });
40
+ });
@@ -0,0 +1,114 @@
1
+ import type {
2
+ MarketplaceInstalledRecord,
3
+ MarketplaceItemSummary,
4
+ MarketplaceManageAction,
5
+ } from "@/shared/lib/api";
6
+ import {
7
+ MarketplaceListCard,
8
+ type InstallState,
9
+ type ManageState,
10
+ } from "@/features/marketplace/components/marketplace-list-card";
11
+ import { MarketplaceListSkeleton } from "@/features/marketplace/components/marketplace-page-parts";
12
+ import {
13
+ findInstalledRecordForItem,
14
+ type InstalledRenderEntry,
15
+ } from "@/features/marketplace/components/marketplace-page-data";
16
+ import { cn } from "@/shared/lib/utils";
17
+
18
+ type MarketplaceCatalogGridProps = {
19
+ scope: "all" | "installed";
20
+ title: string;
21
+ summary: string;
22
+ showTitle: boolean;
23
+ showListSkeleton: boolean;
24
+ skeletonCardCount: number;
25
+ allItems: MarketplaceItemSummary[];
26
+ installedEntries: InstalledRenderEntry[];
27
+ installedRecordLookup: Map<string, MarketplaceInstalledRecord>;
28
+ language: string;
29
+ installState: InstallState;
30
+ manageState: ManageState;
31
+ onOpen: (item?: MarketplaceItemSummary, record?: MarketplaceInstalledRecord) => void;
32
+ onInstall: (item: MarketplaceItemSummary) => void;
33
+ onManage: (
34
+ action: MarketplaceManageAction,
35
+ record: MarketplaceInstalledRecord,
36
+ ) => void;
37
+ };
38
+
39
+ export function MarketplaceCatalogGrid(props: MarketplaceCatalogGridProps) {
40
+ const {
41
+ scope,
42
+ title,
43
+ summary,
44
+ showTitle,
45
+ showListSkeleton,
46
+ skeletonCardCount,
47
+ allItems,
48
+ installedEntries,
49
+ installedRecordLookup,
50
+ language,
51
+ installState,
52
+ manageState,
53
+ onOpen,
54
+ onInstall,
55
+ onManage,
56
+ } = props;
57
+
58
+ return (
59
+ <section className={cn("flex min-h-full flex-col", showTitle && "gap-3")}>
60
+ {showTitle && (
61
+ <div className="flex items-center justify-between gap-3">
62
+ <h3 className="text-[14px] font-semibold text-gray-950">{title}</h3>
63
+ <span className="text-[12px] text-gray-500">{summary}</span>
64
+ </div>
65
+ )}
66
+
67
+ <div
68
+ data-testid={showListSkeleton ? "marketplace-list-skeleton" : undefined}
69
+ className={cn(
70
+ "grid grid-cols-1 gap-3 lg:grid-cols-2 2xl:grid-cols-3",
71
+ showListSkeleton && "min-h-0 flex-1 auto-rows-[104px] content-start",
72
+ )}
73
+ >
74
+ {showListSkeleton && (
75
+ <MarketplaceListSkeleton count={skeletonCardCount} />
76
+ )}
77
+
78
+ {!showListSkeleton &&
79
+ scope === "all" &&
80
+ allItems.map((item) => (
81
+ <MarketplaceListCard
82
+ key={item.id}
83
+ item={item}
84
+ record={findInstalledRecordForItem(item, installedRecordLookup)}
85
+ language={language}
86
+ installState={installState}
87
+ manageState={manageState}
88
+ onOpen={() =>
89
+ onOpen(item, findInstalledRecordForItem(item, installedRecordLookup))
90
+ }
91
+ onInstall={onInstall}
92
+ onManage={onManage}
93
+ />
94
+ ))}
95
+
96
+ {!showListSkeleton &&
97
+ scope === "installed" &&
98
+ installedEntries.map((entry) => (
99
+ <MarketplaceListCard
100
+ key={entry.key}
101
+ item={entry.item}
102
+ record={entry.record}
103
+ language={language}
104
+ installState={installState}
105
+ manageState={manageState}
106
+ onOpen={() => onOpen(entry.item, entry.record)}
107
+ onInstall={onInstall}
108
+ onManage={onManage}
109
+ />
110
+ ))}
111
+ </div>
112
+ </section>
113
+ );
114
+ }
@@ -1,11 +1,8 @@
1
- function escapeHtml(text: string): string {
2
- return text
3
- .replace(/&/g, "&amp;")
4
- .replace(/</g, "&lt;")
5
- .replace(/>/g, "&gt;")
6
- .replace(/"/g, "&quot;")
7
- .replace(/'/g, "&#39;");
8
- }
1
+ import {
2
+ escapeHtml,
3
+ renderDetailMarkdown,
4
+ renderDetailMetadata,
5
+ } from "@/features/marketplace/components/detail-doc/marketplace-detail-doc-renderer";
9
6
 
10
7
  export function buildGenericDetailDataUrl(params: {
11
8
  title: string;
@@ -19,6 +16,7 @@ export function buildGenericDetailDataUrl(params: {
19
16
  sourceLabel?: string;
20
17
  tags?: string[];
21
18
  author?: string;
19
+ loading?: boolean;
22
20
  }): string {
23
21
  const {
24
22
  title,
@@ -32,12 +30,15 @@ export function buildGenericDetailDataUrl(params: {
32
30
  sourceLabel,
33
31
  tags,
34
32
  author,
33
+ loading,
35
34
  } = params;
36
35
  const metadata = metadataRaw?.trim() || "-";
37
36
  const content = contentRaw?.trim() || "-";
38
37
  const summary = rawSummary?.trim();
39
38
  const description = rawDescription?.trim();
40
39
  const shouldShowDescription = Boolean(description) && description !== summary;
40
+ const renderedMetadata = renderDetailMetadata(metadata);
41
+ const renderedContent = renderDetailMarkdown(content);
41
42
 
42
43
  const html = `<!doctype html>
43
44
  <html>
@@ -47,25 +48,72 @@ export function buildGenericDetailDataUrl(params: {
47
48
  <title>${escapeHtml(title)}</title>
48
49
  <style>
49
50
  :root { color-scheme: light; }
50
- body { margin: 0; background: #f7f9fc; color: #0f172a; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
51
- .wrap { max-width: 980px; margin: 0 auto; padding: 28px 20px 40px; }
52
- .hero { border: 1px solid #dbeafe; border-radius: 16px; background: linear-gradient(180deg, #eff6ff, #ffffff); padding: 20px; box-shadow: 0 6px 20px rgba(30, 64, 175, 0.08); }
53
- .hero h1 { margin: 0; font-size: 26px; }
54
- .meta { margin-top: 8px; color: #475569; font-size: 13px; overflow-wrap: anywhere; word-break: break-word; }
55
- .summary { margin-top: 14px; font-size: 14px; line-height: 1.7; color: #334155; }
56
- .grid { display: grid; grid-template-columns: 260px 1fr; gap: 14px; margin-top: 16px; }
57
- .card { border: 1px solid #e2e8f0; background: #fff; border-radius: 14px; overflow: hidden; }
58
- .card h2 { margin: 0; padding: 12px 14px; font-size: 13px; font-weight: 700; color: #1d4ed8; border-bottom: 1px solid #e2e8f0; background: #f8fafc; }
59
- .card .body { padding: 12px 14px; font-size: 13px; color: #334155; line-height: 1.7; }
60
- .code { white-space: pre-wrap; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; line-height: 1.6; margin: 0; }
51
+ body { margin: 0; background: #f9f8f5; color: #2f2212; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
52
+ .wrap { max-width: 940px; margin: 0 auto; padding: 24px 20px 36px; }
53
+ .hero { border: 1px solid #f0e2c8; border-radius: 14px; background: linear-gradient(180deg, #fff9f1 0%, #ffffff 28%); padding: 18px; box-shadow: 0 1px 3px rgba(30, 20, 10, 0.05); }
54
+ .hero h1 { margin: 0; font-size: 22px; line-height: 1.2; letter-spacing: 0; }
55
+ .meta { margin-top: 7px; color: #78644d; font-size: 12px; overflow-wrap: anywhere; word-break: break-word; }
56
+ .summary { margin: 12px 0 0; font-size: 13px; line-height: 1.65; color: #5f5142; }
57
+ .grid { display: grid; grid-template-columns: minmax(220px, 0.42fr) minmax(0, 1fr); gap: 12px; margin-top: 12px; }
58
+ .card { border: 1px solid #eee3d1; background: #fffdf9; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 2px rgba(30, 20, 10, 0.035); }
59
+ .card h2 { margin: 0; padding: 11px 13px; font-size: 12px; font-weight: 650; color: #3f472f; border-bottom: 1px solid #f1e7d4; background: #fffaf2; }
60
+ .card .body { padding: 12px 13px; font-size: 12px; color: #4e463d; line-height: 1.65; }
61
+ .code { white-space: pre-wrap; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11.5px; line-height: 1.55; margin: 0; }
62
+ .metadata-list { margin: 0; }
63
+ .metadata-list div { display: grid; grid-template-columns: minmax(72px, 0.36fr) minmax(0, 1fr); gap: 10px; padding: 8px 0; border-bottom: 1px solid #f2eadc; }
64
+ .metadata-list div:last-child { border-bottom: 0; }
65
+ .metadata-list dt { color: #7a5a24; font-weight: 650; overflow-wrap: anywhere; }
66
+ .metadata-list dd { margin: 0; color: #4e463d; overflow-wrap: anywhere; }
67
+ .markdown { font-size: 13px; line-height: 1.68; }
68
+ .markdown > *:first-child { margin-top: 0; }
69
+ .markdown > *:last-child { margin-bottom: 0; }
70
+ .markdown h1, .markdown h2, .markdown h3, .markdown h4 { margin: 18px 0 8px; color: #2f2212; line-height: 1.25; letter-spacing: 0; }
71
+ .markdown h1 { font-size: 20px; }
72
+ .markdown h2 { font-size: 17px; }
73
+ .markdown h3 { font-size: 15px; }
74
+ .markdown h4 { font-size: 13px; }
75
+ .markdown p { margin: 10px 0; }
76
+ .markdown ul, .markdown ol { margin: 10px 0; padding-left: 20px; }
77
+ .markdown li { margin: 5px 0; }
78
+ .markdown blockquote { margin: 12px 0; padding: 8px 12px; border-left: 3px solid #d9b56f; border-radius: 8px; background: #fff7ea; color: #6d5841; }
79
+ .markdown code { border: 1px solid #eadcc6; border-radius: 5px; background: #fff7ea; padding: 1px 4px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11.5px; color: #6b4b16; }
80
+ .markdown a { color: #5f6b45; text-decoration: none; font-weight: 600; }
81
+ .markdown a:hover { text-decoration: underline; }
82
+ .code-block { position: relative; margin: 12px 0; overflow: hidden; border: 1px solid #eadcc6; border-radius: 10px; background: #2f2a24; }
83
+ .code-block pre { margin: 0; overflow-x: auto; padding: 13px; }
84
+ .code-block code { border: 0; border-radius: 0; background: transparent; padding: 0; color: #f7efe3; }
85
+ .code-language { position: absolute; right: 10px; top: 8px; color: #d8c3a0; font-size: 10px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
61
86
  .tags { margin-top: 10px; }
62
- .tag { display: inline-block; margin: 0 6px 6px 0; padding: 4px 9px; border-radius: 999px; background: #e0e7ff; color: #3730a3; font-size: 11px; }
63
- .source { color: #2563eb; text-decoration: none; overflow-wrap: anywhere; word-break: break-all; }
87
+ .tag { display: inline-block; margin: 0 6px 6px 0; padding: 4px 8px; border: 1px solid #ecd9b5; border-radius: 999px; background: #fff7ea; color: #7a5a24; font-size: 11px; }
88
+ .source { color: #5f6b45; text-decoration: none; overflow-wrap: anywhere; word-break: break-all; }
89
+ .source:hover { text-decoration: underline; }
90
+ .skeleton { display: block; border-radius: 8px; background: linear-gradient(90deg, #f0e6d6 0%, #fffaf2 42%, #f0e6d6 78%); background-size: 220% 100%; animation: shimmer 1.35s ease-in-out infinite; }
91
+ .detail-skeleton .hero { padding: 18px; }
92
+ .sk-title { width: 52%; height: 24px; }
93
+ .sk-meta { width: 78%; height: 12px; margin-top: 12px; }
94
+ .sk-line { height: 12px; margin-top: 12px; }
95
+ .sk-line.short { width: 62%; }
96
+ .sk-line.mid { width: 82%; }
97
+ .sk-body { height: 220px; margin: 13px; }
98
+ @keyframes shimmer { 0% { background-position: 120% 0; } 100% { background-position: -120% 0; } }
64
99
  @media (max-width: 860px) { .grid { grid-template-columns: 1fr; } }
65
100
  </style>
66
101
  </head>
67
102
  <body>
68
- <main class="wrap">
103
+ <main class="wrap${loading ? " detail-skeleton" : ""}"${loading ? ' aria-busy="true"' : ""}>
104
+ ${loading ? `
105
+ <section class="hero">
106
+ <span class="skeleton sk-title"></span>
107
+ <span class="skeleton sk-meta"></span>
108
+ <span class="skeleton sk-line mid"></span>
109
+ <span class="skeleton sk-line"></span>
110
+ <span class="skeleton sk-line short"></span>
111
+ </section>
112
+ <section class="grid">
113
+ <article class="card"><span class="skeleton sk-body"></span></article>
114
+ <article class="card"><span class="skeleton sk-body"></span></article>
115
+ </section>
116
+ ` : `
69
117
  <section class="hero">
70
118
  <h1>${escapeHtml(title)}</h1>
71
119
  <div class="meta">${escapeHtml(typeLabel)} · ${escapeHtml(spec)}${author ? ` · ${escapeHtml(author)}` : ""}</div>
@@ -78,13 +126,14 @@ export function buildGenericDetailDataUrl(params: {
78
126
  <section class="grid">
79
127
  <article class="card">
80
128
  <h2>Metadata</h2>
81
- <div class="body"><pre class="code">${escapeHtml(metadata)}</pre></div>
129
+ <div class="body">${renderedMetadata}</div>
82
130
  </article>
83
131
  <article class="card">
84
132
  <h2>Content</h2>
85
- <div class="body"><pre class="code">${escapeHtml(content)}</pre></div>
133
+ <div class="body markdown">${renderedContent}</div>
86
134
  </article>
87
135
  </section>
136
+ `}
88
137
  </main>
89
138
  </body>
90
139
  </html>`;
@@ -0,0 +1,45 @@
1
+ import { cn } from "@/shared/lib/utils";
2
+
3
+ const MARKETPLACE_ITEM_ICON_COLORS = [
4
+ "bg-amber-600",
5
+ "bg-orange-500",
6
+ "bg-yellow-600",
7
+ "bg-emerald-600",
8
+ "bg-teal-600",
9
+ "bg-cyan-600",
10
+ "bg-stone-600",
11
+ "bg-rose-500",
12
+ "bg-violet-500",
13
+ ] as const;
14
+
15
+ export function MarketplaceItemIcon(props: {
16
+ name?: string;
17
+ fallback: string;
18
+ className?: string;
19
+ }) {
20
+ const { name, fallback, className } = props;
21
+ const displayName = name || fallback;
22
+ const letters = displayName.substring(0, 2).toUpperCase();
23
+
24
+ return (
25
+ <div
26
+ className={cn(
27
+ "flex h-10 w-10 shrink-0 items-center justify-center rounded-xl text-sm font-semibold text-white",
28
+ getMarketplaceItemIconColor(displayName),
29
+ className,
30
+ )}
31
+ >
32
+ {letters}
33
+ </div>
34
+ );
35
+ }
36
+
37
+ function getMarketplaceItemIconColor(text: string) {
38
+ let hash = 0;
39
+ for (let index = 0; index < text.length; index++) {
40
+ hash = text.charCodeAt(index) + ((hash << 5) - hash);
41
+ }
42
+ return MARKETPLACE_ITEM_ICON_COLORS[
43
+ Math.abs(hash) % MARKETPLACE_ITEM_ICON_COLORS.length
44
+ ];
45
+ }
@@ -13,8 +13,16 @@ import {
13
13
  buildLocaleFallbacks,
14
14
  pickLocalizedText,
15
15
  } from "@/features/marketplace/components/marketplace-localization";
16
+ import { MarketplaceItemIcon } from "@/features/marketplace/components/marketplace-item-icon";
16
17
  import { t } from "@/shared/lib/i18n";
17
18
  import { cn } from "@/shared/lib/utils";
19
+ import {
20
+ CheckCircle2,
21
+ Download,
22
+ Power,
23
+ PowerOff,
24
+ Trash2,
25
+ } from "lucide-react";
18
26
 
19
27
  export type InstallState = {
20
28
  installingSpecs: ReadonlySet<string>;
@@ -24,42 +32,22 @@ export type ManageState = {
24
32
  actionsByTarget: ReadonlyMap<string, MarketplaceManageAction>;
25
33
  };
26
34
 
27
- const ITEM_ICON_COLORS = [
28
- "bg-amber-600",
29
- "bg-orange-500",
30
- "bg-yellow-600",
31
- "bg-emerald-600",
32
- "bg-teal-600",
33
- "bg-cyan-600",
34
- "bg-stone-600",
35
- "bg-rose-500",
36
- "bg-violet-500",
37
- ] as const;
38
-
39
- function getAvatarColor(text: string) {
40
- let hash = 0;
41
- for (let i = 0; i < text.length; i++) {
42
- hash = text.charCodeAt(i) + ((hash << 5) - hash);
43
- }
44
- return ITEM_ICON_COLORS[Math.abs(hash) % ITEM_ICON_COLORS.length];
45
- }
46
-
47
- function ItemIcon({ name, fallback }: { name?: string; fallback: string }) {
48
- const displayName = name || fallback;
49
- const letters = displayName.substring(0, 2).toUpperCase();
50
- const colorClass = getAvatarColor(displayName);
51
-
52
- return (
53
- <div
54
- className={cn(
55
- "flex h-10 w-10 shrink-0 items-center justify-center rounded-xl text-sm font-semibold text-white",
56
- colorClass,
57
- )}
58
- >
59
- {letters}
60
- </div>
61
- );
62
- }
35
+ type MarketplaceListCardActionProps = {
36
+ item?: MarketplaceItemSummary;
37
+ record?: MarketplaceInstalledRecord;
38
+ pluginRecord?: MarketplaceInstalledRecord;
39
+ isInstalling: boolean;
40
+ isDisabled: boolean;
41
+ canUninstall: boolean;
42
+ busyAction?: MarketplaceManageAction;
43
+ busyForRecord: boolean;
44
+ language: string;
45
+ onInstall: (item: MarketplaceItemSummary) => void;
46
+ onManage: (
47
+ action: MarketplaceManageAction,
48
+ record: MarketplaceInstalledRecord,
49
+ ) => void;
50
+ };
63
51
 
64
52
  function MarketplaceListCardMeta({
65
53
  title,
@@ -98,17 +86,144 @@ function MarketplaceListCardMeta({
98
86
  ) : null}
99
87
  </div>
100
88
 
89
+ <p className="line-clamp-2 text-left text-[12px] leading-relaxed text-gray-500/90">
90
+ {summary}
91
+ </p>
92
+ </TooltipProvider>
93
+ );
94
+ }
95
+
96
+ function MarketplaceListCardActions(props: MarketplaceListCardActionProps) {
97
+ const {
98
+ item,
99
+ record,
100
+ pluginRecord,
101
+ isInstalling,
102
+ isDisabled,
103
+ canUninstall,
104
+ busyAction,
105
+ busyForRecord,
106
+ language,
107
+ onInstall,
108
+ onManage,
109
+ } = props;
110
+ const hasActions = Boolean((item && !record) || pluginRecord || (record && canUninstall));
111
+
112
+ return (
113
+ <div
114
+ className={cn(
115
+ "relative flex h-8 shrink-0 items-center justify-end",
116
+ record ? "md:w-5" : "md:w-0",
117
+ )}
118
+ >
119
+ <div
120
+ className={cn(
121
+ "hidden items-center justify-end transition-opacity duration-150 md:flex",
122
+ hasActions && "group-hover:opacity-0 group-focus-within:opacity-0",
123
+ )}
124
+ >
125
+ {record ? (
126
+ <MarketplaceInstalledStatusIcon
127
+ disabled={isDisabled}
128
+ language={language}
129
+ />
130
+ ) : null}
131
+ </div>
132
+
133
+ <div
134
+ className={cn(
135
+ "flex w-max items-center justify-end gap-2 transition-opacity duration-150",
136
+ "opacity-100 md:pointer-events-none md:absolute md:right-0 md:opacity-0",
137
+ "md:group-hover:pointer-events-auto md:group-hover:opacity-100",
138
+ "md:group-focus-within:pointer-events-auto md:group-focus-within:opacity-100",
139
+ )}
140
+ >
141
+ {item && !record && (
142
+ <button
143
+ onClick={(event) => {
144
+ event.stopPropagation();
145
+ onInstall(item);
146
+ }}
147
+ disabled={isInstalling}
148
+ className="inline-flex h-8 items-center gap-1.5 whitespace-nowrap rounded-xl bg-primary px-3 text-xs font-medium text-white transition-colors hover:bg-primary-600 disabled:opacity-50"
149
+ >
150
+ <Download className="h-3.5 w-3.5" />
151
+ {isInstalling ? t("marketplaceInstalling") : t("marketplaceInstall")}
152
+ </button>
153
+ )}
154
+
155
+ {pluginRecord && (
156
+ <button
157
+ disabled={busyForRecord}
158
+ onClick={(event) => {
159
+ event.stopPropagation();
160
+ onManage(isDisabled ? "enable" : "disable", pluginRecord);
161
+ }}
162
+ className="inline-flex h-8 items-center gap-1.5 whitespace-nowrap rounded-xl border border-gray-200/80 bg-white px-3 text-xs font-medium text-gray-600 transition-colors hover:border-gray-300 hover:bg-gray-50 disabled:opacity-50"
163
+ >
164
+ {isDisabled ? (
165
+ <Power className="h-3.5 w-3.5" />
166
+ ) : (
167
+ <PowerOff className="h-3.5 w-3.5" />
168
+ )}
169
+ {busyAction && busyAction !== "uninstall"
170
+ ? busyAction === "enable"
171
+ ? t("marketplaceEnabling")
172
+ : t("marketplaceDisabling")
173
+ : isDisabled
174
+ ? t("marketplaceEnable")
175
+ : t("marketplaceDisable")}
176
+ </button>
177
+ )}
178
+
179
+ {record && canUninstall && (
180
+ <button
181
+ disabled={busyForRecord}
182
+ onClick={(event) => {
183
+ event.stopPropagation();
184
+ onManage("uninstall", record);
185
+ }}
186
+ className="inline-flex h-8 items-center gap-1.5 whitespace-nowrap rounded-xl border border-gray-200/80 bg-white px-3 text-xs font-medium text-gray-500 transition-colors hover:border-rose-200 hover:bg-rose-50 hover:text-rose-600 disabled:opacity-50"
187
+ >
188
+ <Trash2 className="h-3.5 w-3.5" />
189
+ {busyAction === "uninstall"
190
+ ? t("marketplaceRemoving")
191
+ : t("marketplaceUninstall")}
192
+ </button>
193
+ )}
194
+ </div>
195
+ </div>
196
+ );
197
+ }
198
+
199
+ function MarketplaceInstalledStatusIcon(props: {
200
+ disabled: boolean;
201
+ language: string;
202
+ }) {
203
+ const { disabled, language } = props;
204
+ const label = disabled
205
+ ? readLocalized({ zh: "已禁用", en: "Disabled" }, language)
206
+ : readLocalized({ zh: "已安装", en: "Installed" }, language);
207
+
208
+ return (
209
+ <TooltipProvider delayDuration={300}>
101
210
  <Tooltip>
102
211
  <TooltipTrigger asChild>
103
- <p className="line-clamp-1 text-left text-[12px] leading-relaxed text-gray-500/90">
104
- {summary}
105
- </p>
212
+ <span
213
+ aria-label={label}
214
+ className={cn(
215
+ "inline-flex h-5 w-5 items-center justify-center",
216
+ disabled ? "text-gray-400" : "text-emerald-700",
217
+ )}
218
+ >
219
+ {disabled ? (
220
+ <PowerOff className="h-4 w-4" />
221
+ ) : (
222
+ <CheckCircle2 className="h-4 w-4" />
223
+ )}
224
+ </span>
106
225
  </TooltipTrigger>
107
- {summary ? (
108
- <TooltipContent className="max-w-[400px] text-xs leading-relaxed">
109
- {summary}
110
- </TooltipContent>
111
- ) : null}
226
+ <TooltipContent className="text-xs">{label}</TooltipContent>
112
227
  </Tooltip>
113
228
  </TooltipProvider>
114
229
  );
@@ -171,7 +286,7 @@ export function MarketplaceListCard(props: {
171
286
  className="group flex cursor-pointer items-start justify-between gap-3.5 rounded-2xl border border-gray-200/40 bg-white px-5 py-4 shadow-sm transition-all hover:border-blue-300/80 hover:shadow-md"
172
287
  >
173
288
  <div className="flex min-w-0 flex-1 gap-3">
174
- <ItemIcon
289
+ <MarketplaceItemIcon
175
290
  name={title}
176
291
  fallback={spec || t("marketplaceTypeExtension")}
177
292
  />
@@ -180,54 +295,23 @@ export function MarketplaceListCard(props: {
180
295
  </div>
181
296
  </div>
182
297
 
183
- <div className="flex h-full shrink-0 items-center">
184
- {item && !record && (
185
- <button
186
- onClick={(event) => {
187
- event.stopPropagation();
188
- onInstall(item);
189
- }}
190
- disabled={isInstalling}
191
- className="inline-flex h-8 items-center gap-1.5 rounded-xl bg-primary px-4 text-xs font-medium text-white transition-colors hover:bg-primary-600 disabled:opacity-50"
192
- >
193
- {isInstalling ? t("marketplaceInstalling") : t("marketplaceInstall")}
194
- </button>
195
- )}
196
-
197
- {pluginRecord && (
198
- <button
199
- disabled={busyForRecord}
200
- onClick={(event) => {
201
- event.stopPropagation();
202
- onManage(isDisabled ? "enable" : "disable", pluginRecord);
203
- }}
204
- className="inline-flex h-8 items-center rounded-xl border border-gray-200/80 bg-white px-4 text-xs font-medium text-gray-600 transition-colors hover:border-gray-300 hover:bg-gray-50 disabled:opacity-50"
205
- >
206
- {busyAction && busyAction !== "uninstall"
207
- ? busyAction === "enable"
208
- ? t("marketplaceEnabling")
209
- : t("marketplaceDisabling")
210
- : isDisabled
211
- ? t("marketplaceEnable")
212
- : t("marketplaceDisable")}
213
- </button>
214
- )}
215
-
216
- {record && canUninstall && (
217
- <button
218
- disabled={busyForRecord}
219
- onClick={(event) => {
220
- event.stopPropagation();
221
- onManage("uninstall", record);
222
- }}
223
- className="inline-flex h-8 items-center rounded-xl border border-rose-100 bg-white px-4 text-xs font-medium text-rose-500 transition-colors hover:border-rose-200 hover:bg-rose-50 disabled:opacity-50"
224
- >
225
- {busyAction === "uninstall"
226
- ? t("marketplaceRemoving")
227
- : t("marketplaceUninstall")}
228
- </button>
229
- )}
230
- </div>
298
+ <MarketplaceListCardActions
299
+ item={item}
300
+ record={record}
301
+ pluginRecord={pluginRecord}
302
+ isInstalling={isInstalling}
303
+ isDisabled={isDisabled}
304
+ canUninstall={canUninstall}
305
+ busyAction={busyAction}
306
+ busyForRecord={busyForRecord}
307
+ language={language}
308
+ onInstall={onInstall}
309
+ onManage={onManage}
310
+ />
231
311
  </article>
232
312
  );
233
313
  }
314
+
315
+ function readLocalized(text: { zh: string; en: string }, language: string) {
316
+ return language.startsWith("zh") ? text.zh : text.en;
317
+ }