@nextclaw/ui 0.12.26 → 0.12.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 (26) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/assets/{channels-list-page-HgLgrEg4.js → channels-list-page-DkPvpAqc.js} +1 -1
  3. package/dist/assets/{chat-page-DAKMFDrS.js → chat-page-b7Zf32fF.js} +1 -1
  4. package/dist/assets/index-DmWo8dX2.css +1 -0
  5. package/dist/assets/{index-Cuwst6cc.js → index-DqJ3CYwi.js} +2 -2
  6. package/dist/assets/marketplace-page-BVqFjnEB.js +105 -0
  7. package/dist/assets/marketplace-page-DkQ2hTs1.js +1 -0
  8. package/dist/assets/{mcp-marketplace-page-DwnaLNTx.js → mcp-marketplace-page-BOYJO0kp.js} +1 -1
  9. package/dist/assets/mcp-marketplace-page-DSML7NN0.js +1 -0
  10. package/dist/assets/{model-config-L2l6YAlQ.js → model-config-Bg2yycmn.js} +1 -1
  11. package/dist/assets/{providers-list-DYAEunOp.js → providers-list-DC1q3fvC.js} +1 -1
  12. package/dist/assets/{runtime-config-page-BdeU8PEK.js → runtime-config-page-q-nC0C5i.js} +1 -1
  13. package/dist/assets/{search-config-CQUhd5RU.js → search-config-CcKHif8O.js} +1 -1
  14. package/dist/assets/{secrets-config-D-NWlW9q.js → secrets-config-DSg6O92a.js} +1 -1
  15. package/dist/assets/{use-infinite-scroll-loader-CFVdPpNv.js → use-infinite-scroll-loader-DF2e6nQ2.js} +1 -1
  16. package/dist/index.html +2 -2
  17. package/package.json +6 -6
  18. package/src/features/marketplace/components/curated-shelves/marketplace-curated-scene-route.test.tsx +54 -16
  19. package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.tsx +96 -24
  20. package/src/features/marketplace/components/marketplace-page.test.tsx +4 -0
  21. package/src/features/marketplace/components/marketplace-page.tsx +16 -12
  22. package/src/features/marketplace/hooks/use-marketplace-curated-scene-route.ts +14 -5
  23. package/dist/assets/index-dlcqieQ0.css +0 -1
  24. package/dist/assets/marketplace-page-BeFbwxR-.js +0 -105
  25. package/dist/assets/marketplace-page-CR4xq-TM.js +0 -1
  26. package/dist/assets/mcp-marketplace-page-DlRrSCj3.js +0 -1
@@ -1 +1 @@
1
- import{_ as e,m as t,p as n,r}from"./i18n-D1144VAA.js";import{$t as i,Bt as a,Qt as o,Zt as s,en as c}from"./api-DGD9_Bg4.js";import{t as l}from"./createLucideIcon-DzY6wN61.js";import{a as u,i as d,n as f,r as p,t as m}from"./select-BUTwE_lC.js";import{u as h}from"./index-Cuwst6cc.js";var g=class extends c{constructor(e,t){super(e,t)}bindMethods(){super.bindMethods(),this.fetchNextPage=this.fetchNextPage.bind(this),this.fetchPreviousPage=this.fetchPreviousPage.bind(this)}setOptions(e){super.setOptions({...e,behavior:i()})}getOptimisticResult(e){return e.behavior=i(),super.getOptimisticResult(e)}fetchNextPage(e){return this.fetch({...e,meta:{fetchMore:{direction:`forward`}}})}fetchPreviousPage(e){return this.fetch({...e,meta:{fetchMore:{direction:`backward`}}})}createResult(e,t){let{state:n}=e,r=super.createResult(e,t),{isFetching:i,isRefetching:a,isError:c,isRefetchError:l}=r,u=n.fetchMeta?.fetchMore?.direction,d=c&&u===`forward`,f=i&&u===`forward`,p=c&&u===`backward`,m=i&&u===`backward`;return{...r,fetchNextPage:this.fetchNextPage,fetchPreviousPage:this.fetchPreviousPage,hasNextPage:s(t,n.data),hasPreviousPage:o(t,n.data),isFetchNextPageError:d,isFetchingNextPage:f,isFetchPreviousPageError:p,isFetchingPreviousPage:m,isRefetchError:l&&!d&&!p,isRefetching:a&&!f&&!m}}};function _(e,t){return a(e,g,t)}var v=l(`PackageSearch`,[[`path`,{d:`M21 10V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l2-1.14`,key:`e7tb2h`}],[`path`,{d:`m7.5 4.27 9 5.15`,key:`1c824w`}],[`polyline`,{points:`3.29 7 12 12 20.71 7`,key:`ousv84`}],[`line`,{x1:`12`,x2:`12`,y1:`22`,y2:`12`,key:`a4e8g8`}],[`circle`,{cx:`18.5`,cy:`15.5`,r:`2.5`,key:`b5zd12`}],[`path`,{d:`M20.27 17.27 22 19`,key:`1l4muz`}]]);function y(e){if(!e||e.pages.length===0)return;let t=e.pages.flatMap(e=>e.items);return{...e.pages[e.pages.length-1],items:t,pages:e.pages,loadedItems:t.length,loadedPages:e.pages.length}}var b=n();function x({scope:e,searchText:t,searchPlaceholder:n,sort:i,onSearchTextChange:a,onSortChange:o}){return(0,b.jsx)(`div`,{className:`mb-4`,children:(0,b.jsxs)(`div`,{className:`flex items-center gap-3`,children:[(0,b.jsxs)(`div`,{className:`relative min-w-0 flex-1`,children:[(0,b.jsx)(v,{className:`absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400`}),(0,b.jsx)(`input`,{value:t,onChange:e=>a(e.target.value),placeholder:n,className:`h-9 w-full rounded-xl border border-gray-200/80 pl-9 pr-3 text-sm focus:outline-none focus:ring-1 focus:ring-primary/40`})]}),e===`all`&&(0,b.jsxs)(m,{value:i,onValueChange:e=>o(e),children:[(0,b.jsx)(d,{className:`h-9 w-[150px] shrink-0 rounded-lg`,children:(0,b.jsx)(u,{})}),(0,b.jsxs)(f,{children:[(0,b.jsx)(p,{value:`relevance`,children:r(`marketplaceSortRelevance`)}),(0,b.jsx)(p,{value:`updated`,children:r(`marketplaceSortUpdated`)})]})]})]})})}function S({count:e}){return(0,b.jsx)(b.Fragment,{children:Array.from({length:e},(e,t)=>(0,b.jsx)(`article`,{className:`h-full rounded-2xl border border-gray-200/40 bg-white px-5 py-4 shadow-sm`,children:(0,b.jsxs)(`div`,{className:`flex items-start justify-between gap-3.5`,children:[(0,b.jsxs)(`div`,{className:`flex min-w-0 flex-1 gap-3`,children:[(0,b.jsx)(h,{className:`h-10 w-10 shrink-0 rounded-xl`}),(0,b.jsxs)(`div`,{className:`min-w-0 flex-1 space-y-2 pt-0.5`,children:[(0,b.jsx)(h,{className:`h-4 w-32 max-w-[70%]`}),(0,b.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,b.jsx)(h,{className:`h-3 w-12`}),(0,b.jsx)(h,{className:`h-3 w-24`})]}),(0,b.jsx)(h,{className:`h-3 w-full`})]})]}),(0,b.jsx)(h,{className:`h-8 w-20 shrink-0 rounded-xl`})]})},`marketplace-skeleton-${t}`))})}function C({hasMore:e,loading:t,sentinelRef:n}){return!e&&!t?null:(0,b.jsxs)(`div`,{className:`py-4`,children:[e&&(0,b.jsx)(`div`,{ref:n,className:`h-1 w-full`,"aria-hidden":`true`}),t&&(0,b.jsx)(`div`,{"data-testid":`marketplace-loading-more`,className:`pt-3 text-center text-xs text-gray-500`,children:r(`loading`)})]})}function w(e){let t=e.trim().toLowerCase().replace(/_/g,`-`),n=[t,t.split(`-`)[0],`en`];return Array.from(new Set(n.filter(Boolean)))}function T(e){return e.trim().toLowerCase().replace(/_/g,`-`)}function E(e,t,n){if(e){let t=Object.entries(e).map(([e,t])=>({locale:T(e),text:typeof t==`string`?t.trim():``})).filter(e=>e.text.length>0);if(t.length>0){let e=new Map(t.map(e=>[e.locale,e.text]));for(let t of n){let n=T(t),r=e.get(n);if(r)return r}for(let e of n){let n=T(e).split(`-`)[0];if(!n)continue;let r=t.find(e=>e.locale===n||e.locale.startsWith(`${n}-`));if(r)return r.text}return t[0]?.text??``}}return t?.trim()??``}function D(e,t){if(!e)return``;for(let n of t)if(T(n).split(`-`)[0]===`zh`&&e.descriptionZh?.trim())return e.descriptionZh.trim();return e.description?.trim()?e.description.trim():e.descriptionZh?.trim()?e.descriptionZh.trim():``}var O=e(t(),1),k=160;function A(e){let t=(0,O.useRef)(null),n=(0,O.useRef)(null),r=(0,O.useRef)(e.onLoadMore),i=(0,O.useRef)(!1);return(0,O.useEffect)(()=>{r.current=e.onLoadMore},[e.onLoadMore]),(0,O.useEffect)(()=>{e.disabled&&(i.current=!1)},[e.disabled]),(0,O.useEffect)(()=>{let a=t.current,o=n.current,s=e.thresholdPx??k;if(e.disabled||!a||!o)return;let c=()=>{i.current||e.disabled||(i.current=!0,Promise.resolve(r.current()).finally(()=>{i.current=!1}))},l=()=>{o.getBoundingClientRect().top-a.getBoundingClientRect().bottom<=s&&c()};if(typeof IntersectionObserver==`function`){let e=new IntersectionObserver(e=>{e.some(e=>e.isIntersecting)&&c()},{root:a,rootMargin:`0px 0px ${s}px 0px`});return e.observe(o),l(),()=>{e.disconnect()}}return a.addEventListener(`scroll`,l,{passive:!0}),l(),()=>{a.removeEventListener(`scroll`,l)}},[e.disabled,e.thresholdPx,e.watchValue]),{containerRef:t,sentinelRef:n}}export{x as a,y as c,E as i,_ as l,w as n,C as o,D as r,S as s,A as t};
1
+ import{_ as e,m as t,p as n,r}from"./i18n-D1144VAA.js";import{$t as i,Bt as a,Qt as o,Zt as s,en as c}from"./api-DGD9_Bg4.js";import{t as l}from"./createLucideIcon-DzY6wN61.js";import{a as u,i as d,n as f,r as p,t as m}from"./select-BUTwE_lC.js";import{u as h}from"./index-DqJ3CYwi.js";var g=class extends c{constructor(e,t){super(e,t)}bindMethods(){super.bindMethods(),this.fetchNextPage=this.fetchNextPage.bind(this),this.fetchPreviousPage=this.fetchPreviousPage.bind(this)}setOptions(e){super.setOptions({...e,behavior:i()})}getOptimisticResult(e){return e.behavior=i(),super.getOptimisticResult(e)}fetchNextPage(e){return this.fetch({...e,meta:{fetchMore:{direction:`forward`}}})}fetchPreviousPage(e){return this.fetch({...e,meta:{fetchMore:{direction:`backward`}}})}createResult(e,t){let{state:n}=e,r=super.createResult(e,t),{isFetching:i,isRefetching:a,isError:c,isRefetchError:l}=r,u=n.fetchMeta?.fetchMore?.direction,d=c&&u===`forward`,f=i&&u===`forward`,p=c&&u===`backward`,m=i&&u===`backward`;return{...r,fetchNextPage:this.fetchNextPage,fetchPreviousPage:this.fetchPreviousPage,hasNextPage:s(t,n.data),hasPreviousPage:o(t,n.data),isFetchNextPageError:d,isFetchingNextPage:f,isFetchPreviousPageError:p,isFetchingPreviousPage:m,isRefetchError:l&&!d&&!p,isRefetching:a&&!f&&!m}}};function _(e,t){return a(e,g,t)}var v=l(`PackageSearch`,[[`path`,{d:`M21 10V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l2-1.14`,key:`e7tb2h`}],[`path`,{d:`m7.5 4.27 9 5.15`,key:`1c824w`}],[`polyline`,{points:`3.29 7 12 12 20.71 7`,key:`ousv84`}],[`line`,{x1:`12`,x2:`12`,y1:`22`,y2:`12`,key:`a4e8g8`}],[`circle`,{cx:`18.5`,cy:`15.5`,r:`2.5`,key:`b5zd12`}],[`path`,{d:`M20.27 17.27 22 19`,key:`1l4muz`}]]);function y(e){if(!e||e.pages.length===0)return;let t=e.pages.flatMap(e=>e.items);return{...e.pages[e.pages.length-1],items:t,pages:e.pages,loadedItems:t.length,loadedPages:e.pages.length}}var b=n();function x({scope:e,searchText:t,searchPlaceholder:n,sort:i,onSearchTextChange:a,onSortChange:o}){return(0,b.jsx)(`div`,{className:`mb-4`,children:(0,b.jsxs)(`div`,{className:`flex items-center gap-3`,children:[(0,b.jsxs)(`div`,{className:`relative min-w-0 flex-1`,children:[(0,b.jsx)(v,{className:`absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400`}),(0,b.jsx)(`input`,{value:t,onChange:e=>a(e.target.value),placeholder:n,className:`h-9 w-full rounded-xl border border-gray-200/80 pl-9 pr-3 text-sm focus:outline-none focus:ring-1 focus:ring-primary/40`})]}),e===`all`&&(0,b.jsxs)(m,{value:i,onValueChange:e=>o(e),children:[(0,b.jsx)(d,{className:`h-9 w-[150px] shrink-0 rounded-lg`,children:(0,b.jsx)(u,{})}),(0,b.jsxs)(f,{children:[(0,b.jsx)(p,{value:`relevance`,children:r(`marketplaceSortRelevance`)}),(0,b.jsx)(p,{value:`updated`,children:r(`marketplaceSortUpdated`)})]})]})]})})}function S({count:e}){return(0,b.jsx)(b.Fragment,{children:Array.from({length:e},(e,t)=>(0,b.jsx)(`article`,{className:`h-full rounded-2xl border border-gray-200/40 bg-white px-5 py-4 shadow-sm`,children:(0,b.jsxs)(`div`,{className:`flex items-start justify-between gap-3.5`,children:[(0,b.jsxs)(`div`,{className:`flex min-w-0 flex-1 gap-3`,children:[(0,b.jsx)(h,{className:`h-10 w-10 shrink-0 rounded-xl`}),(0,b.jsxs)(`div`,{className:`min-w-0 flex-1 space-y-2 pt-0.5`,children:[(0,b.jsx)(h,{className:`h-4 w-32 max-w-[70%]`}),(0,b.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,b.jsx)(h,{className:`h-3 w-12`}),(0,b.jsx)(h,{className:`h-3 w-24`})]}),(0,b.jsx)(h,{className:`h-3 w-full`})]})]}),(0,b.jsx)(h,{className:`h-8 w-20 shrink-0 rounded-xl`})]})},`marketplace-skeleton-${t}`))})}function C({hasMore:e,loading:t,sentinelRef:n}){return!e&&!t?null:(0,b.jsxs)(`div`,{className:`py-4`,children:[e&&(0,b.jsx)(`div`,{ref:n,className:`h-1 w-full`,"aria-hidden":`true`}),t&&(0,b.jsx)(`div`,{"data-testid":`marketplace-loading-more`,className:`pt-3 text-center text-xs text-gray-500`,children:r(`loading`)})]})}function w(e){let t=e.trim().toLowerCase().replace(/_/g,`-`),n=[t,t.split(`-`)[0],`en`];return Array.from(new Set(n.filter(Boolean)))}function T(e){return e.trim().toLowerCase().replace(/_/g,`-`)}function E(e,t,n){if(e){let t=Object.entries(e).map(([e,t])=>({locale:T(e),text:typeof t==`string`?t.trim():``})).filter(e=>e.text.length>0);if(t.length>0){let e=new Map(t.map(e=>[e.locale,e.text]));for(let t of n){let n=T(t),r=e.get(n);if(r)return r}for(let e of n){let n=T(e).split(`-`)[0];if(!n)continue;let r=t.find(e=>e.locale===n||e.locale.startsWith(`${n}-`));if(r)return r.text}return t[0]?.text??``}}return t?.trim()??``}function D(e,t){if(!e)return``;for(let n of t)if(T(n).split(`-`)[0]===`zh`&&e.descriptionZh?.trim())return e.descriptionZh.trim();return e.description?.trim()?e.description.trim():e.descriptionZh?.trim()?e.descriptionZh.trim():``}var O=e(t(),1),k=160;function A(e){let t=(0,O.useRef)(null),n=(0,O.useRef)(null),r=(0,O.useRef)(e.onLoadMore),i=(0,O.useRef)(!1);return(0,O.useEffect)(()=>{r.current=e.onLoadMore},[e.onLoadMore]),(0,O.useEffect)(()=>{e.disabled&&(i.current=!1)},[e.disabled]),(0,O.useEffect)(()=>{let a=t.current,o=n.current,s=e.thresholdPx??k;if(e.disabled||!a||!o)return;let c=()=>{i.current||e.disabled||(i.current=!0,Promise.resolve(r.current()).finally(()=>{i.current=!1}))},l=()=>{o.getBoundingClientRect().top-a.getBoundingClientRect().bottom<=s&&c()};if(typeof IntersectionObserver==`function`){let e=new IntersectionObserver(e=>{e.some(e=>e.isIntersecting)&&c()},{root:a,rootMargin:`0px 0px ${s}px 0px`});return e.observe(o),l(),()=>{e.disconnect()}}return a.addEventListener(`scroll`,l,{passive:!0}),l(),()=>{a.removeEventListener(`scroll`,l)}},[e.disabled,e.thresholdPx,e.watchValue]),{containerRef:t,sentinelRef:n}}export{x as a,y as c,E as i,_ as l,w as n,C as o,D as r,S as s,A as t};
package/dist/index.html CHANGED
@@ -78,7 +78,7 @@
78
78
  })();
79
79
  </script>
80
80
  <title>NextClaw</title>
81
- <script type="module" crossorigin src="/assets/index-Cuwst6cc.js"></script>
81
+ <script type="module" crossorigin src="/assets/index-DqJ3CYwi.js"></script>
82
82
  <link rel="modulepreload" crossorigin href="/assets/i18n-D1144VAA.js">
83
83
  <link rel="modulepreload" crossorigin href="/assets/createLucideIcon-DzY6wN61.js">
84
84
  <link rel="modulepreload" crossorigin href="/assets/cpu-DPPwMzoC.js">
@@ -106,7 +106,7 @@
106
106
  <link rel="modulepreload" crossorigin href="/assets/doc-browser-CAhfnm0D.js">
107
107
  <link rel="modulepreload" crossorigin href="/assets/use-config-Cyv5IuSt.js">
108
108
  <link rel="modulepreload" crossorigin href="/assets/desktop-DVUbOWbR.js">
109
- <link rel="stylesheet" crossorigin href="/assets/index-dlcqieQ0.css">
109
+ <link rel="stylesheet" crossorigin href="/assets/index-DmWo8dX2.css">
110
110
  </head>
111
111
 
112
112
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/ui",
3
- "version": "0.12.26",
3
+ "version": "0.12.27",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -28,14 +28,14 @@
28
28
  "tailwind-merge": "^2.5.4",
29
29
  "zod": "^3.23.8",
30
30
  "zustand": "^5.0.2",
31
- "@nextclaw/client-sdk": "0.1.6",
32
- "@nextclaw/ncp": "0.5.11",
31
+ "@nextclaw/agent-chat": "0.1.16",
32
+ "@nextclaw/agent-chat-ui": "0.3.18",
33
33
  "@nextclaw/ncp-http-agent-client": "0.3.23",
34
+ "@nextclaw/ncp": "0.5.11",
35
+ "@nextclaw/ncp-react": "0.4.31",
34
36
  "@nextclaw/server": "0.12.18",
35
37
  "@nextclaw/shared": "0.1.5",
36
- "@nextclaw/agent-chat-ui": "0.3.18",
37
- "@nextclaw/agent-chat": "0.1.16",
38
- "@nextclaw/ncp-react": "0.4.31"
38
+ "@nextclaw/client-sdk": "0.1.6"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@testing-library/react": "^16.3.0",
@@ -18,11 +18,27 @@ type ItemsQueryState = {
18
18
  fetchNextPage?: () => Promise<unknown>;
19
19
  };
20
20
 
21
+ type ScenesQueryState = {
22
+ data?: {
23
+ scenes: Array<{
24
+ scene: string;
25
+ title: string;
26
+ description?: string;
27
+ count?: number;
28
+ }>;
29
+ };
30
+ isLoading: boolean;
31
+ isFetching: boolean;
32
+ isError: boolean;
33
+ error: Error | null;
34
+ };
35
+
21
36
  const mocks = vi.hoisted(() => ({
22
37
  navigate: vi.fn(),
23
38
  docOpen: vi.fn(),
24
39
  routeParams: {} as { scene?: string },
25
40
  itemsQuery: null as unknown as ItemsQueryState,
41
+ scenesQuery: null as unknown as ScenesQueryState,
26
42
  }));
27
43
 
28
44
  vi.mock("react-router-dom", async () => {
@@ -55,21 +71,7 @@ vi.mock("@/shared/hooks/use-confirm-dialog", () => ({
55
71
 
56
72
  vi.mock("@/features/marketplace/hooks/use-marketplace", () => ({
57
73
  useMarketplaceItems: () => mocks.itemsQuery,
58
- useMarketplaceSkillScenes: () => ({
59
- data: {
60
- scenes: [
61
- {
62
- scene: "development-debugging",
63
- title: "Development",
64
- description: "Review, debug, analyze, and verify delivery work.",
65
- },
66
- ],
67
- },
68
- isLoading: false,
69
- isFetching: false,
70
- isError: false,
71
- error: null,
72
- }),
74
+ useMarketplaceSkillScenes: () => mocks.scenesQuery,
73
75
  useMarketplaceSkillSceneCounts: () => new Map([["development-debugging", 2]]),
74
76
  useMarketplaceInstalled: () => ({
75
77
  data: {
@@ -165,12 +167,32 @@ function createItemsQuery(items: MarketplaceItemSummary[]) {
165
167
  };
166
168
  }
167
169
 
170
+ function createScenesQuery(overrides: Partial<ScenesQueryState> = {}) {
171
+ return {
172
+ data: {
173
+ scenes: [
174
+ {
175
+ scene: "development-debugging",
176
+ title: "Development",
177
+ description: "Review, debug, analyze, and verify delivery work.",
178
+ },
179
+ ],
180
+ },
181
+ isLoading: false,
182
+ isFetching: false,
183
+ isError: false,
184
+ error: null,
185
+ ...overrides,
186
+ };
187
+ }
188
+
168
189
  describe("Marketplace curated scene routes", () => {
169
190
  beforeEach(() => {
170
191
  mocks.navigate.mockReset();
171
192
  mocks.docOpen.mockReset();
172
193
  mocks.routeParams = {};
173
194
  mocks.itemsQuery = createItemsQuery(createSceneItems());
195
+ mocks.scenesQuery = createScenesQuery();
174
196
  });
175
197
 
176
198
  it("opens curated goals through a scene route", async () => {
@@ -183,6 +205,7 @@ describe("Marketplace curated scene routes", () => {
183
205
  expect(mocks.navigate).toHaveBeenCalledWith(
184
206
  "/skills/scenes/development-debugging",
185
207
  );
208
+ expect(screen.getByText("All Skills")).toBeTruthy();
186
209
  expect(container.querySelector("input")?.getAttribute("value") ?? "").toBe("");
187
210
  });
188
211
 
@@ -196,7 +219,7 @@ describe("Marketplace curated scene routes", () => {
196
219
  expect(screen.getByRole("button", { name: "Back" })).toBeTruthy();
197
220
  expect(screen.getByText("Code Review")).toBeTruthy();
198
221
  expect(screen.queryByText("Calendar Sync")).toBeNull();
199
- expect(screen.queryByText("Skill Catalog")).toBeNull();
222
+ expect(screen.queryByText("All Skills")).toBeNull();
200
223
  expect(screen.queryByPlaceholderText("Search skills...")).toBeNull();
201
224
 
202
225
  await user.click(screen.getByRole("button", { name: "Back" }));
@@ -232,4 +255,19 @@ describe("Marketplace curated scene routes", () => {
232
255
 
233
256
  expect(screen.getByTestId("marketplace-loading-more")).toBeTruthy();
234
257
  });
258
+
259
+ it("keeps the shelf layout stable while scenes are still loading", () => {
260
+ mocks.scenesQuery = createScenesQuery({
261
+ data: undefined,
262
+ isLoading: true,
263
+ isFetching: true,
264
+ });
265
+
266
+ render(<MarketplacePage forcedType="skills" />);
267
+
268
+ expect(screen.getByTestId("marketplace-scenes-skeleton")).toBeTruthy();
269
+ expect(screen.getByText("Recently updated")).toBeTruthy();
270
+ expect(screen.getByText("All Skills")).toBeTruthy();
271
+ expect(screen.queryByRole("button", { name: /Development/ })).toBeNull();
272
+ });
235
273
  });
@@ -23,6 +23,7 @@ import {
23
23
  type MarketplaceShelfLocalizedText,
24
24
  type MarketplaceShelfSceneVisual,
25
25
  } from "@/features/marketplace/components/curated-shelves/marketplace-curated-shelves.config";
26
+ import { Skeleton } from "@/shared/components/ui/skeleton";
26
27
 
27
28
  const SCENE_CARD_GRID_CLASS =
28
29
  "grid grid-cols-[repeat(auto-fill,minmax(240px,320px))] justify-start gap-3";
@@ -36,6 +37,8 @@ export type MarketplaceShelfEntry = {
36
37
  export function MarketplaceCuratedShelves(props: {
37
38
  entries: MarketplaceShelfEntry[];
38
39
  scenes: MarketplaceSceneView[];
40
+ isScenesLoading: boolean;
41
+ isItemsLoading: boolean;
39
42
  language: string;
40
43
  installState: InstallState;
41
44
  onOpen: (entry: MarketplaceShelfEntry) => void;
@@ -45,6 +48,8 @@ export function MarketplaceCuratedShelves(props: {
45
48
  const {
46
49
  entries,
47
50
  scenes,
51
+ isScenesLoading,
52
+ isItemsLoading,
48
53
  language,
49
54
  installState,
50
55
  onOpen,
@@ -70,19 +75,23 @@ export function MarketplaceCuratedShelves(props: {
70
75
  language,
71
76
  )}
72
77
  />
73
- <div className="grid grid-cols-2 gap-2 md:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5">
74
- {scenes.map((scene) => (
75
- <GoalCard
76
- key={scene.scene}
77
- scene={scene}
78
- language={language}
79
- onSelect={onOpenScene}
80
- />
81
- ))}
82
- </div>
78
+ {isScenesLoading ? (
79
+ <SceneGoalSkeletons />
80
+ ) : (
81
+ <div className="grid grid-cols-2 gap-2 md:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5">
82
+ {scenes.map((scene) => (
83
+ <GoalCard
84
+ key={scene.scene}
85
+ scene={scene}
86
+ language={language}
87
+ onSelect={onOpenScene}
88
+ />
89
+ ))}
90
+ </div>
91
+ )}
83
92
  </section>
84
93
 
85
- {recentEntries.length > 0 && (
94
+ {(isItemsLoading || recentEntries.length > 0) && (
86
95
  <ShelfItemRow
87
96
  icon={Clock3}
88
97
  title={readLocalized({ zh: "最近更新", en: "Recently updated" }, language)}
@@ -96,6 +105,7 @@ export function MarketplaceCuratedShelves(props: {
96
105
  entries={recentEntries}
97
106
  language={language}
98
107
  localeFallbacks={localeFallbacks}
108
+ isLoading={isItemsLoading}
99
109
  installState={installState}
100
110
  onOpen={onOpen}
101
111
  onInstall={onInstall}
@@ -105,6 +115,29 @@ export function MarketplaceCuratedShelves(props: {
105
115
  );
106
116
  }
107
117
 
118
+ function SceneGoalSkeletons() {
119
+ return (
120
+ <div
121
+ data-testid="marketplace-scenes-skeleton"
122
+ className="grid grid-cols-2 gap-2 md:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5"
123
+ >
124
+ {MARKETPLACE_SHELF_SCENE_VISUALS.map((scene) => (
125
+ <div
126
+ key={scene.scene}
127
+ className="flex min-h-[74px] flex-col justify-center rounded-lg border border-gray-200/70 bg-white px-3 py-2.5 shadow-sm"
128
+ >
129
+ <div className="flex min-w-0 items-center gap-2">
130
+ <Skeleton className="h-7 w-7 shrink-0 rounded-md" />
131
+ <Skeleton className="h-3.5 min-w-0 flex-1" />
132
+ <Skeleton className="h-3 w-8 shrink-0" />
133
+ </div>
134
+ <Skeleton className="mt-2 h-3 w-4/5" />
135
+ </div>
136
+ ))}
137
+ </div>
138
+ );
139
+ }
140
+
108
141
  function GoalCard(props: {
109
142
  scene: MarketplaceSceneView;
110
143
  language: string;
@@ -259,6 +292,7 @@ function ShelfItemRow(props: {
259
292
  entries: MarketplaceShelfEntry[];
260
293
  language: string;
261
294
  localeFallbacks: string[];
295
+ isLoading: boolean;
262
296
  installState: InstallState;
263
297
  onOpen: (entry: MarketplaceShelfEntry) => void;
264
298
  onInstall: (item: MarketplaceItemSummary) => void;
@@ -270,6 +304,7 @@ function ShelfItemRow(props: {
270
304
  entries,
271
305
  language,
272
306
  localeFallbacks,
307
+ isLoading,
273
308
  installState,
274
309
  onOpen,
275
310
  onInstall,
@@ -278,23 +313,60 @@ function ShelfItemRow(props: {
278
313
  return (
279
314
  <section className="space-y-2.5">
280
315
  <ShelfHeader icon={Icon} title={title} description={description} />
281
- <div className="-mx-1 flex gap-2.5 overflow-x-auto px-1 pb-1.5 custom-scrollbar">
282
- {entries.map((entry) => (
283
- <SkillShelfCard
284
- key={entry.item.id}
285
- entry={entry}
286
- language={language}
287
- localeFallbacks={localeFallbacks}
288
- installState={installState}
289
- onOpen={onOpen}
290
- onInstall={onInstall}
291
- />
292
- ))}
293
- </div>
316
+ {isLoading ? (
317
+ <RecentShelfSkeletons />
318
+ ) : (
319
+ <div className="-mx-1 flex gap-2.5 overflow-x-auto px-1 pb-1.5 custom-scrollbar">
320
+ {entries.map((entry) => (
321
+ <SkillShelfCard
322
+ key={entry.item.id}
323
+ entry={entry}
324
+ language={language}
325
+ localeFallbacks={localeFallbacks}
326
+ installState={installState}
327
+ onOpen={onOpen}
328
+ onInstall={onInstall}
329
+ />
330
+ ))}
331
+ </div>
332
+ )}
294
333
  </section>
295
334
  );
296
335
  }
297
336
 
337
+ function RecentShelfSkeletons() {
338
+ return (
339
+ <div
340
+ data-testid="marketplace-recent-skeleton"
341
+ className="-mx-1 flex gap-2.5 overflow-hidden px-1 pb-1.5"
342
+ >
343
+ {Array.from({ length: 4 }, (_, index) => (
344
+ <article
345
+ key={`marketplace-recent-skeleton-${index}`}
346
+ className="flex min-h-[166px] w-[260px] shrink-0 flex-col justify-between rounded-xl border border-gray-200/70 bg-white p-3 shadow-sm"
347
+ >
348
+ <div>
349
+ <div className="mb-2.5 flex min-w-0 items-start gap-2.5">
350
+ <Skeleton className="h-10 w-10 shrink-0 rounded-xl" />
351
+ <div className="min-w-0 flex-1 space-y-1.5 pt-0.5">
352
+ <Skeleton className="h-3.5 w-32 max-w-full" />
353
+ <Skeleton className="h-3 w-24 max-w-full" />
354
+ </div>
355
+ </div>
356
+ <Skeleton className="h-3 w-full" />
357
+ <Skeleton className="mt-1.5 h-3 w-4/5" />
358
+ <Skeleton className="mt-2 h-3 w-24" />
359
+ </div>
360
+ <div className="mt-3 flex items-center justify-between gap-3 border-t border-gray-100 pt-2.5">
361
+ <Skeleton className="h-3 w-20" />
362
+ <Skeleton className="h-7 w-16 rounded-md" />
363
+ </div>
364
+ </article>
365
+ ))}
366
+ </div>
367
+ );
368
+ }
369
+
298
370
  function ShelfHeader({
299
371
  icon: Icon,
300
372
  title,
@@ -207,7 +207,11 @@ describe("MarketplacePage", () => {
207
207
 
208
208
  const { container } = render(<MarketplacePage forcedType="skills" />);
209
209
 
210
+ expect(screen.getByText("Recently updated")).toBeTruthy();
211
+ expect(screen.getByTestId("marketplace-recent-skeleton")).toBeTruthy();
212
+ expect(screen.getByText("All Skills")).toBeTruthy();
210
213
  expect(screen.getByTestId("marketplace-list-skeleton")).toBeTruthy();
214
+ expect(screen.queryByText("Skill Catalog")).toBeNull();
211
215
  expect(
212
216
  container.querySelectorAll(
213
217
  '[data-testid="marketplace-list-skeleton"] > article',
@@ -504,16 +504,18 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
504
504
  )}
505
505
 
506
506
  <section className="flex min-h-0 flex-1 flex-col">
507
- {!curatedSceneRoute.isSceneRoute && !curatedSceneRoute.showShelves && (
508
- <div className="mb-3 flex items-center justify-between">
509
- <h3 className="text-[14px] font-semibold text-gray-900">
510
- {scope === "installed"
511
- ? t(copyKeys.sectionInstalled)
512
- : t(copyKeys.sectionCatalog)}
513
- </h3>
514
- <span className="text-[12px] text-gray-500">{listSummary}</span>
515
- </div>
516
- )}
507
+ {!curatedSceneRoute.isSceneRoute &&
508
+ !curatedSceneRoute.showShelves &&
509
+ !showListSkeleton && (
510
+ <div className="mb-3 flex items-center justify-between">
511
+ <h3 className="text-[14px] font-semibold text-gray-900">
512
+ {scope === "installed"
513
+ ? t(copyKeys.sectionInstalled)
514
+ : language.startsWith("zh") ? "全部技能" : "All Skills"}
515
+ </h3>
516
+ <span className="text-[12px] text-gray-500">{listSummary}</span>
517
+ </div>
518
+ )}
517
519
 
518
520
  {scope === "all" && itemsQuery.isError && (
519
521
  <div className="rounded-xl border border-rose-200 bg-rose-50 p-4 text-sm text-rose-700">
@@ -549,6 +551,8 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
549
551
  <MarketplaceCuratedShelves
550
552
  entries={curatedSceneRoute.entries}
551
553
  scenes={curatedSceneRoute.scenes}
554
+ isScenesLoading={curatedSceneRoute.isScenesLoading}
555
+ isItemsLoading={showListSkeleton}
552
556
  language={language}
553
557
  installState={installState}
554
558
  onOpen={(entry) => void openItemDetail(entry.item, entry.record)}
@@ -565,7 +569,7 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
565
569
  title={
566
570
  scope === "installed"
567
571
  ? t(copyKeys.sectionInstalled)
568
- : t(copyKeys.sectionCatalog)
572
+ : language.startsWith("zh") ? "全部技能" : "All Skills"
569
573
  }
570
574
  summary={listSummary}
571
575
  showTitle={curatedSceneRoute.showShelves}
@@ -603,7 +607,7 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
603
607
  )}
604
608
 
605
609
  {scope === "all" &&
606
- !skeletonState.showCatalog &&
610
+ !showListSkeleton &&
607
611
  !itemsQuery.isError && (
608
612
  <MarketplaceInfiniteScrollStatus
609
613
  hasMore={Boolean(itemsQuery.hasNextPage)}
@@ -73,16 +73,24 @@ export function useMarketplaceCuratedSceneRoute(
73
73
  };
74
74
  }, [scene, scenes]);
75
75
  const isSceneRoute = typeFilter === "skill" && Boolean(scene?.trim());
76
- const showShelves =
76
+ const isShelfHome =
77
77
  typeFilter === "skill" &&
78
78
  scope === "all" &&
79
79
  !searchText.trim() &&
80
80
  !query &&
81
- !showListSkeleton &&
82
- !hasCatalogError &&
83
- scenes.length > 0 &&
84
- items.length >= 4 &&
85
81
  !isSceneRoute;
82
+ const hasShelfCatalog = showListSkeleton || items.length >= 4;
83
+ const isScenesLoading =
84
+ isShelfHome &&
85
+ !hasCatalogError &&
86
+ hasShelfCatalog &&
87
+ scenes.length === 0 &&
88
+ scenesQuery.isLoading;
89
+ const showShelves =
90
+ isShelfHome &&
91
+ !hasCatalogError &&
92
+ hasShelfCatalog &&
93
+ (showListSkeleton || scenes.length > 0 || isScenesLoading);
86
94
 
87
95
  return {
88
96
  entries,
@@ -90,6 +98,7 @@ export function useMarketplaceCuratedSceneRoute(
90
98
  selectedScene,
91
99
  sceneEntries: entries,
92
100
  isSceneRoute,
101
+ isScenesLoading,
93
102
  showShelves,
94
103
  backPath: forcedType ? "/skills" : "/marketplace/skills",
95
104
  pathPrefix: forcedType ? "/skills/scenes" : "/marketplace/skills/scenes",