@cloudbase/agent-react-ui 1.0.1-alpha.32

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 (109) hide show
  1. package/README.md +123 -0
  2. package/components.json +21 -0
  3. package/dist/index.css +4241 -0
  4. package/dist/index.css.map +1 -0
  5. package/dist/index.d.mts +59 -0
  6. package/dist/index.d.ts +59 -0
  7. package/dist/index.js +2169 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/index.mjs +2182 -0
  10. package/dist/index.mjs.map +1 -0
  11. package/example/.env.sample +2 -0
  12. package/example/App.tsx +368 -0
  13. package/example/app.css +1 -0
  14. package/example/index.html +12 -0
  15. package/example/main.tsx +9 -0
  16. package/example/vite.config.ts +34 -0
  17. package/package.json +75 -0
  18. package/postcss.config.cjs +3 -0
  19. package/src/components/ai-elements/agent.tsx +140 -0
  20. package/src/components/ai-elements/artifact.tsx +147 -0
  21. package/src/components/ai-elements/attachments.tsx +421 -0
  22. package/src/components/ai-elements/audio-player.tsx +228 -0
  23. package/src/components/ai-elements/canvas.tsx +22 -0
  24. package/src/components/ai-elements/chain-of-thought.tsx +228 -0
  25. package/src/components/ai-elements/checkpoint.tsx +71 -0
  26. package/src/components/ai-elements/code-block.tsx +532 -0
  27. package/src/components/ai-elements/commit.tsx +448 -0
  28. package/src/components/ai-elements/confirmation.tsx +176 -0
  29. package/src/components/ai-elements/connection.tsx +28 -0
  30. package/src/components/ai-elements/context.tsx +408 -0
  31. package/src/components/ai-elements/controls.tsx +18 -0
  32. package/src/components/ai-elements/conversation.tsx +100 -0
  33. package/src/components/ai-elements/edge.tsx +140 -0
  34. package/src/components/ai-elements/environment-variables.tsx +295 -0
  35. package/src/components/ai-elements/file-tree.tsx +258 -0
  36. package/src/components/ai-elements/image.tsx +24 -0
  37. package/src/components/ai-elements/inline-citation.tsx +287 -0
  38. package/src/components/ai-elements/message.tsx +336 -0
  39. package/src/components/ai-elements/mic-selector.tsx +370 -0
  40. package/src/components/ai-elements/model-selector.tsx +211 -0
  41. package/src/components/ai-elements/node.tsx +71 -0
  42. package/src/components/ai-elements/open-in-chat.tsx +365 -0
  43. package/src/components/ai-elements/package-info.tsx +233 -0
  44. package/src/components/ai-elements/panel.tsx +15 -0
  45. package/src/components/ai-elements/persona.tsx +270 -0
  46. package/src/components/ai-elements/plan.tsx +142 -0
  47. package/src/components/ai-elements/prompt-input.tsx +1263 -0
  48. package/src/components/ai-elements/queue.tsx +274 -0
  49. package/src/components/ai-elements/reasoning.tsx +193 -0
  50. package/src/components/ai-elements/sandbox.tsx +126 -0
  51. package/src/components/ai-elements/schema-display.tsx +458 -0
  52. package/src/components/ai-elements/shimmer.tsx +64 -0
  53. package/src/components/ai-elements/snippet.tsx +139 -0
  54. package/src/components/ai-elements/sources.tsx +77 -0
  55. package/src/components/ai-elements/speech-input.tsx +301 -0
  56. package/src/components/ai-elements/stack-trace.tsx +482 -0
  57. package/src/components/ai-elements/suggestion.tsx +53 -0
  58. package/src/components/ai-elements/task.tsx +87 -0
  59. package/src/components/ai-elements/terminal.tsx +261 -0
  60. package/src/components/ai-elements/test-results.tsx +485 -0
  61. package/src/components/ai-elements/tool.tsx +174 -0
  62. package/src/components/ai-elements/toolbar.tsx +16 -0
  63. package/src/components/ai-elements/transcription.tsx +124 -0
  64. package/src/components/ai-elements/voice-selector.tsx +479 -0
  65. package/src/components/ai-elements/web-preview.tsx +263 -0
  66. package/src/components/chat/Chat.tsx +178 -0
  67. package/src/components/chat/Input.tsx +98 -0
  68. package/src/components/chat/Message.tsx +276 -0
  69. package/src/components/chat/index.ts +2 -0
  70. package/src/components/index.ts +1 -0
  71. package/src/components/ui/accordion.tsx +64 -0
  72. package/src/components/ui/alert.tsx +66 -0
  73. package/src/components/ui/avatar.tsx +107 -0
  74. package/src/components/ui/badge.tsx +48 -0
  75. package/src/components/ui/button-group.tsx +83 -0
  76. package/src/components/ui/button.tsx +64 -0
  77. package/src/components/ui/card.tsx +92 -0
  78. package/src/components/ui/carousel.tsx +239 -0
  79. package/src/components/ui/collapsible.tsx +31 -0
  80. package/src/components/ui/command.tsx +184 -0
  81. package/src/components/ui/dialog.tsx +158 -0
  82. package/src/components/ui/dropdown-menu.tsx +257 -0
  83. package/src/components/ui/hover-card.tsx +42 -0
  84. package/src/components/ui/input-group.tsx +168 -0
  85. package/src/components/ui/input.tsx +21 -0
  86. package/src/components/ui/popover.tsx +87 -0
  87. package/src/components/ui/progress.tsx +31 -0
  88. package/src/components/ui/scroll-area.tsx +56 -0
  89. package/src/components/ui/select.tsx +190 -0
  90. package/src/components/ui/separator.tsx +28 -0
  91. package/src/components/ui/spinner.tsx +16 -0
  92. package/src/components/ui/switch.tsx +33 -0
  93. package/src/components/ui/tabs.tsx +91 -0
  94. package/src/components/ui/textarea.tsx +18 -0
  95. package/src/components/ui/tooltip.tsx +61 -0
  96. package/src/css/global.css +123 -0
  97. package/src/css/index.css +1 -0
  98. package/src/hooks/index.ts +1 -0
  99. package/src/hooks/use-copy-to-clipboard.ts +31 -0
  100. package/src/index.ts +4 -0
  101. package/src/lib/utils.ts +6 -0
  102. package/src/locales/context.ts +8 -0
  103. package/src/locales/hooks.ts +20 -0
  104. package/src/locales/index.ts +3 -0
  105. package/src/locales/langs/en.ts +17 -0
  106. package/src/locales/langs/index.ts +12 -0
  107. package/src/locales/langs/zh-cn.ts +18 -0
  108. package/tsconfig.json +21 -0
  109. package/tsup.config.ts +21 -0
@@ -0,0 +1,458 @@
1
+ "use client";
2
+
3
+ import { Badge } from "@/components/ui/badge";
4
+ import {
5
+ Collapsible,
6
+ CollapsibleContent,
7
+ CollapsibleTrigger,
8
+ } from "@/components/ui/collapsible";
9
+ import { cn } from "@/lib/utils";
10
+ import { ChevronRightIcon } from "lucide-react";
11
+ import {
12
+ type ComponentProps,
13
+ createContext,
14
+ type HTMLAttributes,
15
+ useContext,
16
+ } from "react";
17
+
18
+ type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
19
+
20
+ interface SchemaParameter {
21
+ name: string;
22
+ type: string;
23
+ required?: boolean;
24
+ description?: string;
25
+ location?: "path" | "query" | "header";
26
+ }
27
+
28
+ interface SchemaProperty {
29
+ name: string;
30
+ type: string;
31
+ required?: boolean;
32
+ description?: string;
33
+ properties?: SchemaProperty[];
34
+ items?: SchemaProperty;
35
+ }
36
+
37
+ interface SchemaDisplayContextType {
38
+ method: HttpMethod;
39
+ path: string;
40
+ description?: string;
41
+ parameters?: SchemaParameter[];
42
+ requestBody?: SchemaProperty[];
43
+ responseBody?: SchemaProperty[];
44
+ }
45
+
46
+ const SchemaDisplayContext = createContext<SchemaDisplayContextType>({
47
+ method: "GET",
48
+ path: "",
49
+ });
50
+
51
+ export type SchemaDisplayProps = HTMLAttributes<HTMLDivElement> & {
52
+ method: HttpMethod;
53
+ path: string;
54
+ description?: string;
55
+ parameters?: SchemaParameter[];
56
+ requestBody?: SchemaProperty[];
57
+ responseBody?: SchemaProperty[];
58
+ };
59
+
60
+ export const SchemaDisplay = ({
61
+ method,
62
+ path,
63
+ description,
64
+ parameters,
65
+ requestBody,
66
+ responseBody,
67
+ className,
68
+ children,
69
+ ...props
70
+ }: SchemaDisplayProps) => (
71
+ <SchemaDisplayContext.Provider
72
+ value={{ method, path, description, parameters, requestBody, responseBody }}
73
+ >
74
+ <div
75
+ className={cn(
76
+ "overflow-hidden rounded-lg border bg-background",
77
+ className
78
+ )}
79
+ {...props}
80
+ >
81
+ {children ?? (
82
+ <>
83
+ <SchemaDisplayHeader>
84
+ <div className="flex items-center gap-3">
85
+ <SchemaDisplayMethod />
86
+ <SchemaDisplayPath />
87
+ </div>
88
+ </SchemaDisplayHeader>
89
+ {description && <SchemaDisplayDescription />}
90
+ <SchemaDisplayContent>
91
+ {parameters && parameters.length > 0 && <SchemaDisplayParameters />}
92
+ {requestBody && requestBody.length > 0 && <SchemaDisplayRequest />}
93
+ {responseBody && responseBody.length > 0 && (
94
+ <SchemaDisplayResponse />
95
+ )}
96
+ </SchemaDisplayContent>
97
+ </>
98
+ )}
99
+ </div>
100
+ </SchemaDisplayContext.Provider>
101
+ );
102
+
103
+ export type SchemaDisplayHeaderProps = HTMLAttributes<HTMLDivElement>;
104
+
105
+ export const SchemaDisplayHeader = ({
106
+ className,
107
+ children,
108
+ ...props
109
+ }: SchemaDisplayHeaderProps) => (
110
+ <div
111
+ className={cn("flex items-center gap-3 border-b px-4 py-3", className)}
112
+ {...props}
113
+ >
114
+ {children}
115
+ </div>
116
+ );
117
+
118
+ const methodStyles: Record<HttpMethod, string> = {
119
+ GET: "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400",
120
+ POST: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400",
121
+ PUT: "bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400",
122
+ PATCH:
123
+ "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400",
124
+ DELETE: "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400",
125
+ };
126
+
127
+ export type SchemaDisplayMethodProps = ComponentProps<typeof Badge>;
128
+
129
+ export const SchemaDisplayMethod = ({
130
+ className,
131
+ children,
132
+ ...props
133
+ }: SchemaDisplayMethodProps) => {
134
+ const { method } = useContext(SchemaDisplayContext);
135
+
136
+ return (
137
+ <Badge
138
+ className={cn("font-mono text-xs", methodStyles[method], className)}
139
+ variant="secondary"
140
+ {...props}
141
+ >
142
+ {children ?? method}
143
+ </Badge>
144
+ );
145
+ };
146
+
147
+ export type SchemaDisplayPathProps = HTMLAttributes<HTMLSpanElement>;
148
+
149
+ export const SchemaDisplayPath = ({
150
+ className,
151
+ children,
152
+ ...props
153
+ }: SchemaDisplayPathProps) => {
154
+ const { path } = useContext(SchemaDisplayContext);
155
+
156
+ // Highlight path parameters
157
+ const highlightedPath = path.replace(
158
+ /\{([^}]+)\}/g,
159
+ '<span class="text-blue-600 dark:text-blue-400">{$1}</span>'
160
+ );
161
+
162
+ return (
163
+ <span
164
+ className={cn("font-mono text-sm", className)}
165
+ // biome-ignore lint/security/noDangerouslySetInnerHtml: "needed for parameter highlighting"
166
+ dangerouslySetInnerHTML={{ __html: children ?? highlightedPath }}
167
+ {...props}
168
+ />
169
+ );
170
+ };
171
+
172
+ export type SchemaDisplayDescriptionProps =
173
+ HTMLAttributes<HTMLParagraphElement>;
174
+
175
+ export const SchemaDisplayDescription = ({
176
+ className,
177
+ children,
178
+ ...props
179
+ }: SchemaDisplayDescriptionProps) => {
180
+ const { description } = useContext(SchemaDisplayContext);
181
+
182
+ return (
183
+ <p
184
+ className={cn(
185
+ "border-b px-4 py-3 text-muted-foreground text-sm",
186
+ className
187
+ )}
188
+ {...props}
189
+ >
190
+ {children ?? description}
191
+ </p>
192
+ );
193
+ };
194
+
195
+ export type SchemaDisplayContentProps = HTMLAttributes<HTMLDivElement>;
196
+
197
+ export const SchemaDisplayContent = ({
198
+ className,
199
+ children,
200
+ ...props
201
+ }: SchemaDisplayContentProps) => (
202
+ <div className={cn("divide-y", className)} {...props}>
203
+ {children}
204
+ </div>
205
+ );
206
+
207
+ export type SchemaDisplayParametersProps = ComponentProps<typeof Collapsible>;
208
+
209
+ export const SchemaDisplayParameters = ({
210
+ className,
211
+ children,
212
+ ...props
213
+ }: SchemaDisplayParametersProps) => {
214
+ const { parameters } = useContext(SchemaDisplayContext);
215
+
216
+ return (
217
+ <Collapsible className={cn(className)} defaultOpen {...props}>
218
+ <CollapsibleTrigger className="group flex w-full items-center gap-2 px-4 py-3 text-left transition-colors hover:bg-muted/50">
219
+ <ChevronRightIcon className="size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90" />
220
+ <span className="font-medium text-sm">Parameters</span>
221
+ <Badge className="ml-auto text-xs" variant="secondary">
222
+ {parameters?.length}
223
+ </Badge>
224
+ </CollapsibleTrigger>
225
+ <CollapsibleContent>
226
+ <div className="divide-y border-t">
227
+ {children ??
228
+ parameters?.map((param) => (
229
+ <SchemaDisplayParameter key={param.name} {...param} />
230
+ ))}
231
+ </div>
232
+ </CollapsibleContent>
233
+ </Collapsible>
234
+ );
235
+ };
236
+
237
+ export type SchemaDisplayParameterProps = HTMLAttributes<HTMLDivElement> &
238
+ SchemaParameter;
239
+
240
+ export const SchemaDisplayParameter = ({
241
+ name,
242
+ type,
243
+ required,
244
+ description,
245
+ location,
246
+ className,
247
+ ...props
248
+ }: SchemaDisplayParameterProps) => (
249
+ <div className={cn("px-4 py-3 pl-10", className)} {...props}>
250
+ <div className="flex items-center gap-2">
251
+ <span className="font-mono text-sm">{name}</span>
252
+ <Badge className="text-xs" variant="outline">
253
+ {type}
254
+ </Badge>
255
+ {location && (
256
+ <Badge className="text-xs" variant="secondary">
257
+ {location}
258
+ </Badge>
259
+ )}
260
+ {required && (
261
+ <Badge
262
+ className="bg-red-100 text-red-700 text-xs dark:bg-red-900/30 dark:text-red-400"
263
+ variant="secondary"
264
+ >
265
+ required
266
+ </Badge>
267
+ )}
268
+ </div>
269
+ {description && (
270
+ <p className="mt-1 text-muted-foreground text-sm">{description}</p>
271
+ )}
272
+ </div>
273
+ );
274
+
275
+ export type SchemaDisplayRequestProps = ComponentProps<typeof Collapsible>;
276
+
277
+ export const SchemaDisplayRequest = ({
278
+ className,
279
+ children,
280
+ ...props
281
+ }: SchemaDisplayRequestProps) => {
282
+ const { requestBody } = useContext(SchemaDisplayContext);
283
+
284
+ return (
285
+ <Collapsible className={cn(className)} defaultOpen {...props}>
286
+ <CollapsibleTrigger className="group flex w-full items-center gap-2 px-4 py-3 text-left transition-colors hover:bg-muted/50">
287
+ <ChevronRightIcon className="size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90" />
288
+ <span className="font-medium text-sm">Request Body</span>
289
+ </CollapsibleTrigger>
290
+ <CollapsibleContent>
291
+ <div className="border-t">
292
+ {children ??
293
+ requestBody?.map((prop) => (
294
+ <SchemaDisplayProperty key={prop.name} {...prop} depth={0} />
295
+ ))}
296
+ </div>
297
+ </CollapsibleContent>
298
+ </Collapsible>
299
+ );
300
+ };
301
+
302
+ export type SchemaDisplayResponseProps = ComponentProps<typeof Collapsible>;
303
+
304
+ export const SchemaDisplayResponse = ({
305
+ className,
306
+ children,
307
+ ...props
308
+ }: SchemaDisplayResponseProps) => {
309
+ const { responseBody } = useContext(SchemaDisplayContext);
310
+
311
+ return (
312
+ <Collapsible className={cn(className)} defaultOpen {...props}>
313
+ <CollapsibleTrigger className="group flex w-full items-center gap-2 px-4 py-3 text-left transition-colors hover:bg-muted/50">
314
+ <ChevronRightIcon className="size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90" />
315
+ <span className="font-medium text-sm">Response</span>
316
+ </CollapsibleTrigger>
317
+ <CollapsibleContent>
318
+ <div className="border-t">
319
+ {children ??
320
+ responseBody?.map((prop) => (
321
+ <SchemaDisplayProperty key={prop.name} {...prop} depth={0} />
322
+ ))}
323
+ </div>
324
+ </CollapsibleContent>
325
+ </Collapsible>
326
+ );
327
+ };
328
+
329
+ export type SchemaDisplayBodyProps = HTMLAttributes<HTMLDivElement>;
330
+
331
+ export const SchemaDisplayBody = ({
332
+ className,
333
+ children,
334
+ ...props
335
+ }: SchemaDisplayBodyProps) => (
336
+ <div className={cn("divide-y", className)} {...props}>
337
+ {children}
338
+ </div>
339
+ );
340
+
341
+ export type SchemaDisplayPropertyProps = HTMLAttributes<HTMLDivElement> &
342
+ SchemaProperty & {
343
+ depth?: number;
344
+ };
345
+
346
+ export const SchemaDisplayProperty = ({
347
+ name,
348
+ type,
349
+ required,
350
+ description,
351
+ properties,
352
+ items,
353
+ depth = 0,
354
+ className,
355
+ ...props
356
+ }: SchemaDisplayPropertyProps) => {
357
+ const hasChildren = properties || items;
358
+ const paddingLeft = 40 + depth * 16;
359
+
360
+ if (hasChildren) {
361
+ return (
362
+ <Collapsible defaultOpen={depth < 2}>
363
+ <CollapsibleTrigger
364
+ className={cn(
365
+ "group flex w-full items-center gap-2 py-3 text-left transition-colors hover:bg-muted/50",
366
+ className
367
+ )}
368
+ style={{ paddingLeft }}
369
+ >
370
+ <ChevronRightIcon className="size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90" />
371
+ <span className="font-mono text-sm">{name}</span>
372
+ <Badge className="text-xs" variant="outline">
373
+ {type}
374
+ </Badge>
375
+ {required && (
376
+ <Badge
377
+ className="bg-red-100 text-red-700 text-xs dark:bg-red-900/30 dark:text-red-400"
378
+ variant="secondary"
379
+ >
380
+ required
381
+ </Badge>
382
+ )}
383
+ </CollapsibleTrigger>
384
+ {description && (
385
+ <p
386
+ className="pb-2 text-muted-foreground text-sm"
387
+ style={{ paddingLeft: paddingLeft + 24 }}
388
+ >
389
+ {description}
390
+ </p>
391
+ )}
392
+ <CollapsibleContent>
393
+ <div className="divide-y border-t">
394
+ {properties?.map((prop) => (
395
+ <SchemaDisplayProperty
396
+ key={prop.name}
397
+ {...prop}
398
+ depth={depth + 1}
399
+ />
400
+ ))}
401
+ {items && (
402
+ <SchemaDisplayProperty
403
+ {...items}
404
+ depth={depth + 1}
405
+ name={`${name}[]`}
406
+ />
407
+ )}
408
+ </div>
409
+ </CollapsibleContent>
410
+ </Collapsible>
411
+ );
412
+ }
413
+
414
+ return (
415
+ <div
416
+ className={cn("py-3 pr-4", className)}
417
+ style={{ paddingLeft }}
418
+ {...props}
419
+ >
420
+ <div className="flex items-center gap-2">
421
+ <span className="size-4" /> {/* Spacer for alignment */}
422
+ <span className="font-mono text-sm">{name}</span>
423
+ <Badge className="text-xs" variant="outline">
424
+ {type}
425
+ </Badge>
426
+ {required && (
427
+ <Badge
428
+ className="bg-red-100 text-red-700 text-xs dark:bg-red-900/30 dark:text-red-400"
429
+ variant="secondary"
430
+ >
431
+ required
432
+ </Badge>
433
+ )}
434
+ </div>
435
+ {description && (
436
+ <p className="mt-1 pl-6 text-muted-foreground text-sm">{description}</p>
437
+ )}
438
+ </div>
439
+ );
440
+ };
441
+
442
+ export type SchemaDisplayExampleProps = HTMLAttributes<HTMLPreElement>;
443
+
444
+ export const SchemaDisplayExample = ({
445
+ className,
446
+ children,
447
+ ...props
448
+ }: SchemaDisplayExampleProps) => (
449
+ <pre
450
+ className={cn(
451
+ "mx-4 mb-4 overflow-auto rounded-md bg-muted p-4 font-mono text-sm",
452
+ className
453
+ )}
454
+ {...props}
455
+ >
456
+ {children}
457
+ </pre>
458
+ );
@@ -0,0 +1,64 @@
1
+ "use client";
2
+
3
+ import { cn } from "@/lib/utils";
4
+ import { motion } from "motion/react";
5
+ import {
6
+ type CSSProperties,
7
+ type ElementType,
8
+ type JSX,
9
+ memo,
10
+ useMemo,
11
+ } from "react";
12
+
13
+ export interface TextShimmerProps {
14
+ children: string;
15
+ as?: ElementType;
16
+ className?: string;
17
+ duration?: number;
18
+ spread?: number;
19
+ }
20
+
21
+ const ShimmerComponent = ({
22
+ children,
23
+ as: Component = "p",
24
+ className,
25
+ duration = 2,
26
+ spread = 2,
27
+ }: TextShimmerProps) => {
28
+ const MotionComponent = motion.create(
29
+ Component as keyof JSX.IntrinsicElements
30
+ );
31
+
32
+ const dynamicSpread = useMemo(
33
+ () => (children?.length ?? 0) * spread,
34
+ [children, spread]
35
+ );
36
+
37
+ return (
38
+ <MotionComponent
39
+ animate={{ backgroundPosition: "0% center" }}
40
+ className={cn(
41
+ "relative inline-block bg-[length:250%_100%,auto] bg-clip-text text-transparent",
42
+ "[--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--color-background),#0000_calc(50%+var(--spread)))] [background-repeat:no-repeat,padding-box]",
43
+ className
44
+ )}
45
+ initial={{ backgroundPosition: "100% center" }}
46
+ style={
47
+ {
48
+ "--spread": `${dynamicSpread}px`,
49
+ backgroundImage:
50
+ "var(--bg), linear-gradient(var(--color-muted-foreground), var(--color-muted-foreground))",
51
+ } as CSSProperties
52
+ }
53
+ transition={{
54
+ repeat: Number.POSITIVE_INFINITY,
55
+ duration,
56
+ ease: "linear",
57
+ }}
58
+ >
59
+ {children}
60
+ </MotionComponent>
61
+ );
62
+ };
63
+
64
+ export const Shimmer = memo(ShimmerComponent);
@@ -0,0 +1,139 @@
1
+ "use client";
2
+
3
+ import {
4
+ InputGroup,
5
+ InputGroupAddon,
6
+ InputGroupButton,
7
+ InputGroupInput,
8
+ InputGroupText,
9
+ } from "@/components/ui/input-group";
10
+ import { cn } from "@/lib/utils";
11
+ import { CheckIcon, CopyIcon } from "lucide-react";
12
+ import {
13
+ type ComponentProps,
14
+ createContext,
15
+ useContext,
16
+ useEffect,
17
+ useRef,
18
+ useState,
19
+ } from "react";
20
+
21
+ interface SnippetContextType {
22
+ code: string;
23
+ }
24
+
25
+ const SnippetContext = createContext<SnippetContextType>({
26
+ code: "",
27
+ });
28
+
29
+ export type SnippetProps = ComponentProps<typeof InputGroup> & {
30
+ code: string;
31
+ };
32
+
33
+ export const Snippet = ({
34
+ code,
35
+ className,
36
+ children,
37
+ ...props
38
+ }: SnippetProps) => (
39
+ <SnippetContext.Provider value={{ code }}>
40
+ <InputGroup className={cn("font-mono", className)} {...props}>
41
+ {children}
42
+ </InputGroup>
43
+ </SnippetContext.Provider>
44
+ );
45
+
46
+ export type SnippetAddonProps = ComponentProps<typeof InputGroupAddon>;
47
+
48
+ export const SnippetAddon = (props: SnippetAddonProps) => (
49
+ <InputGroupAddon {...props} />
50
+ );
51
+
52
+ export type SnippetTextProps = ComponentProps<typeof InputGroupText>;
53
+
54
+ export const SnippetText = ({ className, ...props }: SnippetTextProps) => (
55
+ <InputGroupText
56
+ className={cn("pl-2 font-normal text-muted-foreground", className)}
57
+ {...props}
58
+ />
59
+ );
60
+
61
+ export type SnippetInputProps = Omit<
62
+ ComponentProps<typeof InputGroupInput>,
63
+ "readOnly" | "value"
64
+ >;
65
+
66
+ export const SnippetInput = ({ className, ...props }: SnippetInputProps) => {
67
+ const { code } = useContext(SnippetContext);
68
+
69
+ return (
70
+ <InputGroupInput
71
+ className={cn("text-foreground", className)}
72
+ readOnly
73
+ value={code}
74
+ {...props}
75
+ />
76
+ );
77
+ };
78
+
79
+ export type SnippetCopyButtonProps = ComponentProps<typeof InputGroupButton> & {
80
+ onCopy?: () => void;
81
+ onError?: (error: Error) => void;
82
+ timeout?: number;
83
+ };
84
+
85
+ export const SnippetCopyButton = ({
86
+ onCopy,
87
+ onError,
88
+ timeout = 2000,
89
+ children,
90
+ className,
91
+ ...props
92
+ }: SnippetCopyButtonProps) => {
93
+ const [isCopied, setIsCopied] = useState(false);
94
+ const timeoutRef = useRef<number>(0);
95
+ const { code } = useContext(SnippetContext);
96
+
97
+ const copyToClipboard = async () => {
98
+ if (typeof window === "undefined" || !navigator?.clipboard?.writeText) {
99
+ onError?.(new Error("Clipboard API not available"));
100
+ return;
101
+ }
102
+
103
+ try {
104
+ if (!isCopied) {
105
+ await navigator.clipboard.writeText(code);
106
+ setIsCopied(true);
107
+ onCopy?.();
108
+ timeoutRef.current = window.setTimeout(
109
+ () => setIsCopied(false),
110
+ timeout
111
+ );
112
+ }
113
+ } catch (error) {
114
+ onError?.(error as Error);
115
+ }
116
+ };
117
+
118
+ useEffect(
119
+ () => () => {
120
+ window.clearTimeout(timeoutRef.current);
121
+ },
122
+ []
123
+ );
124
+
125
+ const Icon = isCopied ? CheckIcon : CopyIcon;
126
+
127
+ return (
128
+ <InputGroupButton
129
+ aria-label="Copy"
130
+ className={className}
131
+ onClick={copyToClipboard}
132
+ size="icon-sm"
133
+ title="Copy"
134
+ {...props}
135
+ >
136
+ {children ?? <Icon className="size-3.5" size={14} />}
137
+ </InputGroupButton>
138
+ );
139
+ };