create-mcp-use-app 2.0.0-beta.3 → 2.0.0-beta.5

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 (45) hide show
  1. package/README.md +68 -37
  2. package/dist/index.js +10 -8
  3. package/dist/templates/blank/README.md +1 -1
  4. package/dist/templates/blank/tsconfig.json +2 -1
  5. package/dist/templates/mcp-apps/README.md +1 -1
  6. package/dist/templates/mcp-apps/index.ts +71 -101
  7. package/dist/templates/mcp-apps/mcp-env.d.ts +1 -1
  8. package/dist/templates/mcp-apps/public/logo-mcp-use.svg +8 -0
  9. package/dist/templates/mcp-apps/tsconfig.json +2 -1
  10. package/dist/templates/mcp-apps/views/my-view/MeshGradient.tsx +299 -0
  11. package/dist/templates/mcp-apps/views/my-view/Tooltip.tsx +75 -0
  12. package/dist/templates/mcp-apps/views/my-view/icons.tsx +142 -0
  13. package/dist/templates/mcp-apps/views/my-view/view.css +5 -0
  14. package/dist/templates/mcp-apps/views/my-view/view.tsx +396 -0
  15. package/dist/templates/{starter → mcp-server}/README.md +2 -2
  16. package/dist/templates/{starter → mcp-server}/index.ts +0 -18
  17. package/dist/templates/{starter → mcp-server}/tsconfig.json +2 -1
  18. package/package.json +2 -2
  19. package/dist/templates/mcp-apps/public/fruits/apple.webp +0 -0
  20. package/dist/templates/mcp-apps/public/fruits/apricot.webp +0 -0
  21. package/dist/templates/mcp-apps/public/fruits/avocado.webp +0 -0
  22. package/dist/templates/mcp-apps/public/fruits/banana.webp +0 -0
  23. package/dist/templates/mcp-apps/public/fruits/blueberry.webp +0 -0
  24. package/dist/templates/mcp-apps/public/fruits/cherries.webp +0 -0
  25. package/dist/templates/mcp-apps/public/fruits/coconut.webp +0 -0
  26. package/dist/templates/mcp-apps/public/fruits/grapes.webp +0 -0
  27. package/dist/templates/mcp-apps/public/fruits/lemon.webp +0 -0
  28. package/dist/templates/mcp-apps/public/fruits/mango.webp +0 -0
  29. package/dist/templates/mcp-apps/public/fruits/orange.webp +0 -0
  30. package/dist/templates/mcp-apps/public/fruits/pear.webp +0 -0
  31. package/dist/templates/mcp-apps/public/fruits/pineapple.webp +0 -0
  32. package/dist/templates/mcp-apps/public/fruits/plum.webp +0 -0
  33. package/dist/templates/mcp-apps/public/fruits/strawberry.webp +0 -0
  34. package/dist/templates/mcp-apps/public/fruits/watermelon.webp +0 -0
  35. package/dist/templates/mcp-apps/views/product-search-result/components/Accordion.tsx +0 -35
  36. package/dist/templates/mcp-apps/views/product-search-result/components/AccordionItem.tsx +0 -35
  37. package/dist/templates/mcp-apps/views/product-search-result/components/Carousel.tsx +0 -52
  38. package/dist/templates/mcp-apps/views/product-search-result/components/CarouselItem.tsx +0 -91
  39. package/dist/templates/mcp-apps/views/product-search-result/components/CarouselSkeleton.tsx +0 -18
  40. package/dist/templates/mcp-apps/views/product-search-result/hooks/useCarouselAnimation.ts +0 -72
  41. package/dist/templates/mcp-apps/views/product-search-result/view.css +0 -81
  42. package/dist/templates/mcp-apps/views/product-search-result/view.tsx +0 -371
  43. /package/dist/templates/{starter → mcp-server}/gitignore +0 -0
  44. /package/dist/templates/{starter → mcp-server}/package.json +0 -0
  45. /package/dist/templates/{starter → mcp-server}/public/icon.svg +0 -0
@@ -0,0 +1,396 @@
1
+ /** MCP App starter view — edit this file to build your UI. */
2
+ import {
3
+ Image,
4
+ ModelContext,
5
+ ThemeProvider,
6
+ useCallTool,
7
+ useDisplayMode,
8
+ useHostContext,
9
+ useOpenExternal,
10
+ useSendFollowUp,
11
+ useToolContext,
12
+ useViewTheme,
13
+ useViewTool,
14
+ } from "mcp-use/react";
15
+ import { useState } from "react";
16
+ import { z } from "zod";
17
+
18
+ import {
19
+ CloseIcon,
20
+ ExpandIcon,
21
+ ExternalLinkIcon,
22
+ MonitorIcon,
23
+ MoonIcon,
24
+ PipIcon,
25
+ SmartphoneIcon,
26
+ SunIcon,
27
+ } from "./icons.js";
28
+ import { HeroMeshOrb } from "./MeshGradient.js";
29
+ import { Tooltip } from "./Tooltip.js";
30
+ import "./view.css";
31
+
32
+ export default function McpApp() {
33
+ // useToolContext — tool lifecycle (pending / error / result)
34
+ const view = useToolContext<"show-app">();
35
+ // useHostContext — locale, timezone, platform, capabilities
36
+ const { hostCapabilities, platform, displayMode, locale, timeZone } =
37
+ useHostContext();
38
+ const theme = useViewTheme();
39
+ // useDisplayMode — request pip / fullscreen / inline
40
+ const { availableDisplayModes, requestDisplayMode } = useDisplayMode();
41
+ // useOpenExternal — open URLs via the host
42
+ const openExternal = useOpenExternal();
43
+ // useSendFollowUp — send a prompt back to the host
44
+ const sendFollowUp = useSendFollowUp();
45
+ // useCallTool — invoke server tools from the view
46
+ const sayHello = useCallTool("say-hello");
47
+
48
+ const [count, setCount] = useState(0);
49
+ const [highlighted, setHighlighted] = useState(false);
50
+
51
+ // useViewTool — register a tool callable from the host
52
+ useViewTool(
53
+ {
54
+ name: "set-counter",
55
+ description: "Set the counter displayed in the MCP App card",
56
+ inputSchema: z.object({
57
+ value: z.number().int().min(0).describe("Counter value to display"),
58
+ }),
59
+ },
60
+ async ({ value }) => {
61
+ setCount(value);
62
+ setHighlighted(true);
63
+ window.setTimeout(() => setHighlighted(false), 1200);
64
+ return { content: [{ type: "text", text: `Counter set to ${value}` }] };
65
+ }
66
+ );
67
+
68
+ const isMobile = platform === "mobile";
69
+ const isFullscreen = displayMode === "fullscreen";
70
+ const isPip = displayMode === "pip";
71
+ const pending = view.status === "pending";
72
+ const canOpenExternal = hostCapabilities?.openLinks !== undefined;
73
+ const canSendFollowUp = hostCapabilities?.message !== undefined;
74
+ const callResult = sayHello.data?.structuredContent?.greeting;
75
+
76
+ return (
77
+ <ThemeProvider>
78
+ <div
79
+ data-card-shell
80
+ className={
81
+ isFullscreen
82
+ ? "relative flex h-full min-h-0 w-full flex-1 flex-col overflow-hidden rounded-none border-0 bg-white dark:bg-black"
83
+ : `relative overflow-hidden rounded-3xl border border-zinc-200 bg-white dark:border-white/10 dark:bg-black${
84
+ !pending && highlighted
85
+ ? " ring-2 ring-blue-500 ring-offset-2 dark:ring-offset-black"
86
+ : ""
87
+ }`
88
+ }
89
+ >
90
+ {!isMobile && <HeroMeshOrb />}
91
+ {!isFullscreen && (
92
+ <div className="absolute inset-x-4 top-4 z-20 flex items-center justify-between">
93
+ <div className="flex items-center gap-2">
94
+ <Tooltip label={`MCP Host sending: Theme: ${theme}`} multiline>
95
+ <span
96
+ className="inline-flex items-center gap-1 rounded-full px-2.5 py-1 text-xs font-medium tracking-wide text-neutral-500 dark:text-neutral-400"
97
+ aria-label={`Theme: ${theme}`}
98
+ >
99
+ {theme === "dark" ? <MoonIcon /> : <SunIcon />}
100
+ </span>
101
+ </Tooltip>
102
+ <Tooltip
103
+ label={`MCP Host sending: Platform: ${isMobile ? "mobile" : "web"}`}
104
+ multiline
105
+ >
106
+ <span
107
+ className="inline-flex items-center gap-1 rounded-full px-2.5 py-1 text-xs font-medium tracking-wide text-neutral-500 dark:text-neutral-400"
108
+ aria-label={`Platform: ${isMobile ? "mobile" : "web"}`}
109
+ >
110
+ {isMobile ? <SmartphoneIcon /> : <MonitorIcon />}
111
+ </span>
112
+ </Tooltip>
113
+ <Tooltip label={`MCP Host sending: Locale: ${locale}`} multiline>
114
+ <span className="inline-flex items-center gap-1 rounded-full px-2.5 py-1 text-xs font-medium tracking-wide text-neutral-500 dark:text-neutral-400">
115
+ {locale}
116
+ </span>
117
+ </Tooltip>
118
+ <Tooltip
119
+ label={`MCP Host sending: Timezone: ${timeZone}`}
120
+ multiline
121
+ >
122
+ <span className="inline-flex items-center gap-1 rounded-full px-2.5 py-1 text-xs font-medium tracking-wide text-neutral-500 dark:text-neutral-400">
123
+ {timeZone.split("/").pop()?.replace(/_/g, " ") ?? timeZone}
124
+ </span>
125
+ </Tooltip>
126
+ </div>
127
+ <div className="flex items-center gap-2">
128
+ {!isFullscreen && !isPip && (
129
+ <>
130
+ {availableDisplayModes.includes("pip") && (
131
+ <Tooltip
132
+ label="MCP Apps display mode: picture-in-picture"
133
+ multiline
134
+ >
135
+ <button
136
+ type="button"
137
+ className="cursor-pointer rounded-full border border-neutral-300 p-2 text-neutral-900 transition-colors hover:bg-neutral-100 dark:border-neutral-600 dark:text-neutral-100 dark:hover:bg-neutral-800"
138
+ aria-label="Picture-in-picture"
139
+ onClick={() => void requestDisplayMode({ mode: "pip" })}
140
+ >
141
+ <PipIcon />
142
+ </button>
143
+ </Tooltip>
144
+ )}
145
+ {availableDisplayModes.includes("fullscreen") && (
146
+ <Tooltip
147
+ label="MCP Apps display mode: fullscreen"
148
+ multiline
149
+ >
150
+ <button
151
+ type="button"
152
+ className="cursor-pointer rounded-full border border-neutral-300 p-2 text-neutral-900 transition-colors hover:bg-neutral-100 dark:border-neutral-600 dark:text-neutral-100 dark:hover:bg-neutral-800"
153
+ aria-label="Fullscreen"
154
+ onClick={() =>
155
+ void requestDisplayMode({ mode: "fullscreen" })
156
+ }
157
+ >
158
+ <ExpandIcon />
159
+ </button>
160
+ </Tooltip>
161
+ )}
162
+ </>
163
+ )}
164
+ {(isFullscreen || isPip) && (
165
+ <Tooltip
166
+ label="MCP Apps display mode: return to inline"
167
+ multiline
168
+ >
169
+ <button
170
+ type="button"
171
+ className="cursor-pointer rounded-full border border-neutral-300 p-2 text-neutral-900 transition-colors hover:bg-neutral-100 dark:border-neutral-600 dark:text-neutral-100 dark:hover:bg-neutral-800"
172
+ aria-label="Exit"
173
+ onClick={() => void requestDisplayMode({ mode: "inline" })}
174
+ >
175
+ <CloseIcon />
176
+ </button>
177
+ </Tooltip>
178
+ )}
179
+ </div>
180
+ </div>
181
+ )}
182
+ {!isMobile && (
183
+ <button
184
+ type="button"
185
+ className="absolute bottom-6 left-1/2 z-20 flex -translate-x-1/2 cursor-pointer flex-col items-center gap-1 border-0 bg-transparent p-0 text-neutral-900 transition-opacity hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-50"
186
+ disabled={!canOpenExternal}
187
+ title="mcp-use on GitHub"
188
+ onClick={() =>
189
+ void openExternal({ url: "https://github.com/mcp-use/mcp-use" })
190
+ }
191
+ >
192
+ <span className="text-[10px] font-medium tracking-wide uppercase">
193
+ Built with
194
+ </span>
195
+ <Image
196
+ src="/logo-mcp-use.svg"
197
+ alt="mcp-use"
198
+ className="h-[1.4rem] w-auto max-w-[115px] object-contain"
199
+ />
200
+ </button>
201
+ )}
202
+ <div
203
+ className={`relative z-10 flex flex-col items-center text-center ${
204
+ isFullscreen
205
+ ? "min-h-0 flex-1 justify-center overflow-y-auto px-6 py-8"
206
+ : isMobile
207
+ ? "px-6 py-10"
208
+ : "min-h-[400px] px-8 py-8 pb-16 sm:min-h-[440px] sm:px-10 sm:py-10 sm:pb-16"
209
+ }`}
210
+ >
211
+ {view.status === "error" ? (
212
+ <>
213
+ <h1 className="m-0 text-2xl font-semibold tracking-tight text-neutral-900 dark:text-neutral-100">
214
+ Failed to load MCP App
215
+ </h1>
216
+ <p className="mt-4 mb-0 text-sm text-neutral-600 dark:text-neutral-400">
217
+ {view.error.message}
218
+ </p>
219
+ </>
220
+ ) : (
221
+ <>
222
+ {/* ModelContext — push state the host model can read */}
223
+ {!pending && (
224
+ <ModelContext content={`MCP App counter: ${count}`} />
225
+ )}
226
+ <div
227
+ className={`relative z-10 mb-0 flex flex-col items-center gap-3 ${isMobile ? "mt-10" : "mt-3"}`}
228
+ >
229
+ {pending ? (
230
+ <span
231
+ aria-hidden
232
+ className="inline-block h-7 w-[4.75rem] animate-pulse rounded-full bg-zinc-200 dark:bg-zinc-800"
233
+ />
234
+ ) : (
235
+ <button
236
+ type="button"
237
+ className="inline-flex cursor-pointer items-center gap-1 rounded-full border border-neutral-300 bg-transparent px-3 py-1 text-xs font-medium text-neutral-600 transition-colors hover:bg-neutral-100 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-600 dark:text-neutral-300 dark:hover:bg-neutral-800"
238
+ disabled={!canOpenExternal}
239
+ onClick={() =>
240
+ void openExternal({
241
+ url: "https://mcp-use.com/docs/typescript",
242
+ })
243
+ }
244
+ >
245
+ Docs
246
+ <ExternalLinkIcon />
247
+ </button>
248
+ )}
249
+ <h1
250
+ className={`hero-title relative z-10 mx-auto mb-0 max-w-md text-center font-medium leading-tight text-neutral-900 dark:text-neutral-100 ${
251
+ isMobile ? "text-2xl" : "text-4xl sm:text-5xl"
252
+ }`}
253
+ >
254
+ {pending ? (
255
+ <span className="inline-block animate-pulse rounded-sm text-transparent select-none bg-zinc-200 dark:bg-zinc-800">
256
+ {view.toolInput?.appName?.trim() || "MCP App"}
257
+ </span>
258
+ ) : (
259
+ view.toolInput?.appName?.trim() || "MCP App"
260
+ )}
261
+ </h1>
262
+ </div>
263
+ <p
264
+ className={`relative z-10 mx-auto mt-4 mb-6 max-w-md text-center leading-relaxed text-neutral-600 dark:text-neutral-400 ${isMobile ? "text-sm" : "text-base"}`}
265
+ >
266
+ {pending ? (
267
+ <span className="inline-block animate-pulse rounded-sm text-transparent select-none bg-zinc-200 dark:bg-zinc-800">
268
+ Get started by editing{" "}
269
+ <code className="inline-flex align-middle rounded-md bg-zinc-100 px-1.5 py-0.5 font-mono text-[13px] leading-none text-transparent dark:bg-zinc-800">
270
+ views/my-view/view.tsx
271
+ </code>
272
+ </span>
273
+ ) : (
274
+ <>
275
+ Get started by editing{" "}
276
+ <code className="inline-flex align-middle rounded-md bg-zinc-100 px-1.5 py-0.5 font-mono text-[13px] leading-none text-neutral-700 dark:bg-zinc-800 dark:text-neutral-200">
277
+ views/my-view/view.tsx
278
+ </code>
279
+ </>
280
+ )}
281
+ </p>
282
+ <div
283
+ className={`relative z-10 flex w-full flex-wrap items-center justify-center ${isMobile ? "gap-1.5" : "gap-2"}`}
284
+ >
285
+ <Tooltip
286
+ label="Widget state: updates UI and Model Context for the host"
287
+ multiline
288
+ >
289
+ <button
290
+ type="button"
291
+ disabled={pending}
292
+ aria-hidden={pending}
293
+ tabIndex={pending ? -1 : 0}
294
+ className={`inline-flex cursor-pointer items-center gap-1.5 rounded-full bg-black font-normal text-white transition-colors hover:bg-black/90 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-white dark:text-black dark:hover:bg-white/90 ${
295
+ isMobile ? "px-3 py-1.5 text-xs" : "px-4 py-2 text-sm"
296
+ } ${pending ? "animate-pulse opacity-70" : ""}`}
297
+ onClick={() => setCount((c) => c + 1)}
298
+ >
299
+ <span className={pending ? "text-transparent" : undefined}>
300
+ Update State
301
+ </span>
302
+ <span
303
+ className={`rounded-full border tabular-nums ${
304
+ isMobile
305
+ ? "px-1 py-0.5 text-[10px]"
306
+ : "px-1.5 py-0.5 text-xs"
307
+ } border-white/35 dark:border-black/25 ${pending ? "text-transparent" : ""}`}
308
+ >
309
+ {count}
310
+ </span>
311
+ </button>
312
+ </Tooltip>
313
+ <Tooltip
314
+ label="In-app tool calls: invoke MCP tools from the view"
315
+ multiline
316
+ >
317
+ <button
318
+ type="button"
319
+ disabled={pending || sayHello.isPending}
320
+ aria-hidden={pending}
321
+ tabIndex={pending ? -1 : 0}
322
+ className={`inline-flex cursor-pointer items-center gap-1.5 rounded-full border border-neutral-300 bg-transparent font-normal text-neutral-900 transition-colors hover:bg-neutral-100 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-600 dark:text-neutral-100 dark:hover:bg-neutral-800 ${
323
+ isMobile ? "px-3 py-1.5 text-xs" : "px-4 py-2 text-sm"
324
+ } ${pending ? "animate-pulse opacity-70" : ""}`}
325
+ onClick={() => void sayHello.callTool({ name: "mcp-use" })}
326
+ >
327
+ <span className={pending ? "text-transparent" : undefined}>
328
+ {sayHello.isPending ? "Saying…" : "Say Hello"}
329
+ </span>
330
+ </button>
331
+ </Tooltip>
332
+ <Tooltip
333
+ label="Follow-up messages: send a prompt back to the MCP host"
334
+ multiline
335
+ >
336
+ <button
337
+ type="button"
338
+ disabled={pending || !canSendFollowUp}
339
+ aria-hidden={pending}
340
+ tabIndex={pending ? -1 : 0}
341
+ className={`inline-flex cursor-pointer items-center gap-1.5 rounded-full border border-neutral-300 bg-transparent font-normal text-neutral-900 transition-colors hover:bg-neutral-100 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-600 dark:text-neutral-100 dark:hover:bg-neutral-800 ${
342
+ isMobile ? "px-3 py-1.5 text-xs" : "px-4 py-2 text-sm"
343
+ } ${pending ? "animate-pulse opacity-70" : ""}`}
344
+ onClick={() =>
345
+ void sendFollowUp({
346
+ prompt:
347
+ "Tell me more about MCP Apps and views in mcp-use",
348
+ }).catch(() => {})
349
+ }
350
+ >
351
+ <span className={pending ? "text-transparent" : undefined}>
352
+ Send Followup
353
+ </span>
354
+ </button>
355
+ </Tooltip>
356
+ </div>
357
+ {!pending && callResult !== undefined && (
358
+ <p className="mx-auto mt-4 mb-0 text-center text-sm text-neutral-600 dark:text-neutral-400">
359
+ Tool Response: {callResult}
360
+ </p>
361
+ )}
362
+ {!pending && sayHello.error !== undefined && (
363
+ <p className="mt-4 mb-0 text-sm text-red-500" role="alert">
364
+ {sayHello.error.message}
365
+ </p>
366
+ )}
367
+ </>
368
+ )}
369
+ </div>
370
+ {isMobile && !isFullscreen && (
371
+ <div className="relative w-full">
372
+ <HeroMeshOrb flow />
373
+ <button
374
+ type="button"
375
+ className="absolute bottom-3 left-1/2 z-20 flex -translate-x-1/2 cursor-pointer flex-col items-center gap-1 border-0 bg-transparent p-0 text-neutral-900 transition-opacity hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-50"
376
+ disabled={!canOpenExternal}
377
+ title="mcp-use on GitHub"
378
+ onClick={() =>
379
+ void openExternal({ url: "https://github.com/mcp-use/mcp-use" })
380
+ }
381
+ >
382
+ <span className="text-[10px] font-medium tracking-wide uppercase">
383
+ Built with
384
+ </span>
385
+ <Image
386
+ src="/logo-mcp-use.svg"
387
+ alt="mcp-use"
388
+ className="h-[1.15rem] w-auto max-w-[98px] object-contain"
389
+ />
390
+ </button>
391
+ </div>
392
+ )}
393
+ </div>
394
+ </ThemeProvider>
395
+ );
396
+ }
@@ -11,9 +11,9 @@ npm install
11
11
  npm run dev
12
12
  ```
13
13
 
14
- Open [http://localhost:3000/inspector](http://localhost:3000/inspector) with your browser to test your server.
14
+ Open [http://localhost:3000/mcp/inspector](http://localhost:3000/mcp/inspector) with your browser to test your server.
15
15
 
16
- You can start building by editing the entry file. Add tools, resources, and prompts — the server auto-reloads as you edit.
16
+ You can start building by editing the entry file. Add tools and prompts — the server auto-reloads as you edit.
17
17
 
18
18
  ## Learn More
19
19
 
@@ -29,24 +29,6 @@ server.tool(
29
29
  }
30
30
  );
31
31
 
32
- server.resource(
33
- {
34
- name: "settings",
35
- uri: "config://settings",
36
- description: "Example application settings",
37
- mimeType: "application/json",
38
- },
39
- async (uri) => ({
40
- contents: [
41
- {
42
- uri: uri.href,
43
- mimeType: "application/json",
44
- text: JSON.stringify({ theme: "dark", language: "en" }),
45
- },
46
- ],
47
- })
48
- );
49
-
50
32
  server.prompt(
51
33
  {
52
34
  name: "review-code",
@@ -9,7 +9,8 @@
9
9
  "esModuleInterop": true,
10
10
  "forceConsistentCasingInFileNames": true,
11
11
  "skipLibCheck": true,
12
- "noEmit": true
12
+ "noEmit": true,
13
+ "noUncheckedSideEffectImports": false
13
14
  },
14
15
  "include": [
15
16
  "index.ts",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-mcp-use-app",
3
- "version": "2.0.0-beta.3",
3
+ "version": "2.0.0-beta.5",
4
4
  "type": "module",
5
5
  "description": "Create MCP-Use apps with one command",
6
6
  "author": "mcp-use, Inc.",
@@ -22,7 +22,7 @@
22
22
  "scaffold",
23
23
  "cli",
24
24
  "init",
25
- "starter",
25
+ "mcp-server",
26
26
  "template"
27
27
  ],
28
28
  "bin": {
@@ -1,35 +0,0 @@
1
- import { useState } from "react";
2
- import { AccordionItem } from "./AccordionItem.js";
3
-
4
- export interface AccordionItemData {
5
- question: string;
6
- answer: string;
7
- }
8
-
9
- interface AccordionProps {
10
- items: AccordionItemData[];
11
- }
12
-
13
- export function Accordion({ items }: AccordionProps) {
14
- const [openAccordionIndex, setOpenAccordionIndex] = useState<number | null>(
15
- null
16
- );
17
-
18
- return (
19
- <div className="mt-4 border-t border-neutral-200 p-8 pt-4 dark:border-neutral-700">
20
- <div className="overflow-hidden rounded-lg border border-neutral-200 dark:border-neutral-700">
21
- {items.map((item, index) => (
22
- <AccordionItem
23
- key={index}
24
- question={item.question}
25
- answer={item.answer}
26
- isOpen={openAccordionIndex === index}
27
- onToggle={() =>
28
- setOpenAccordionIndex(openAccordionIndex === index ? null : index)
29
- }
30
- />
31
- ))}
32
- </div>
33
- </div>
34
- );
35
- }
@@ -1,35 +0,0 @@
1
- export interface AccordionItemProps {
2
- question: string;
3
- answer: string;
4
- isOpen: boolean;
5
- onToggle: () => void;
6
- }
7
-
8
- export function AccordionItem({
9
- question,
10
- answer,
11
- isOpen,
12
- onToggle,
13
- }: AccordionItemProps) {
14
- return (
15
- <div className="border-b border-neutral-200 last:border-b-0 dark:border-neutral-700">
16
- <button
17
- type="button"
18
- onClick={onToggle}
19
- className="flex w-full items-center justify-between p-4 text-left transition-colors hover:bg-neutral-50 dark:hover:bg-neutral-900"
20
- >
21
- <span className="font-medium text-neutral-900 dark:text-neutral-100">
22
- {question}
23
- </span>
24
- <span className="text-xl text-neutral-500 transition-transform duration-200 dark:text-neutral-400">
25
- {isOpen ? "−" : "+"}
26
- </span>
27
- </button>
28
- {isOpen && (
29
- <div className="px-4 pb-4 text-neutral-600 dark:text-neutral-400">
30
- {answer}
31
- </div>
32
- )}
33
- </div>
34
- );
35
- }
@@ -1,52 +0,0 @@
1
- import { useRef } from "react";
2
- import { CarouselItem } from "./CarouselItem.js";
3
- import { useCarouselAnimation } from "../hooks/useCarouselAnimation.js";
4
-
5
- interface CarouselProps {
6
- results: Array<{ fruit: string; color: string }>;
7
- favorites?: string[];
8
- highlighted?: string | null;
9
- onSelectFruit: (fruit: string) => void;
10
- onToggleFavorite?: (fruit: string) => void;
11
- }
12
-
13
- export function Carousel({
14
- results,
15
- favorites = [],
16
- highlighted = null,
17
- onSelectFruit,
18
- onToggleFavorite,
19
- }: CarouselProps) {
20
- const carouselContainerRef = useRef<HTMLDivElement>(null);
21
- const scrollContainerRef = useRef<HTMLDivElement>(null);
22
-
23
- useCarouselAnimation(carouselContainerRef, scrollContainerRef);
24
-
25
- return (
26
- <div
27
- ref={scrollContainerRef}
28
- className="carousel-scroll-container w-full overflow-x-auto overflow-y-visible pl-8"
29
- >
30
- <div ref={carouselContainerRef} className="overflow-visible">
31
- <div className="flex gap-4">
32
- {results.map((item, index) => {
33
- const fruit = item.fruit;
34
- return (
35
- <CarouselItem
36
- key={`${fruit}-${index}`}
37
- fruit={fruit}
38
- color={item.color}
39
- isFavorite={favorites.includes(fruit)}
40
- isHighlighted={highlighted === fruit}
41
- onClick={() => onSelectFruit(fruit)}
42
- {...(onToggleFavorite !== undefined && {
43
- onToggleFavorite: () => onToggleFavorite(fruit),
44
- })}
45
- />
46
- );
47
- })}
48
- </div>
49
- </div>
50
- </div>
51
- );
52
- }