@openconsole/shadcn 0.2.2 → 0.2.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 (118) hide show
  1. package/README.md +460 -380
  2. package/components/ai-elements/agent.tsx +141 -0
  3. package/components/ai-elements/artifact.tsx +148 -0
  4. package/components/ai-elements/attachments.tsx +426 -0
  5. package/components/ai-elements/audio-player.tsx +231 -0
  6. package/components/ai-elements/canvas.tsx +26 -0
  7. package/components/ai-elements/chain-of-thought.tsx +222 -0
  8. package/components/ai-elements/checkpoint.tsx +71 -0
  9. package/components/ai-elements/code-block.tsx +562 -0
  10. package/components/ai-elements/commit.tsx +458 -0
  11. package/components/ai-elements/confirmation.tsx +174 -0
  12. package/components/ai-elements/connection.tsx +28 -0
  13. package/components/ai-elements/context.tsx +409 -0
  14. package/components/ai-elements/controls.tsx +18 -0
  15. package/components/ai-elements/conversation.tsx +168 -0
  16. package/components/ai-elements/edge.tsx +143 -0
  17. package/components/ai-elements/environment-variables.tsx +324 -0
  18. package/components/ai-elements/file-tree.tsx +304 -0
  19. package/components/ai-elements/image.tsx +24 -0
  20. package/components/ai-elements/index.ts +51 -0
  21. package/components/ai-elements/inline-citation.tsx +296 -0
  22. package/components/ai-elements/jsx-preview.tsx +310 -0
  23. package/components/ai-elements/message.tsx +360 -0
  24. package/components/ai-elements/mic-selector.tsx +375 -0
  25. package/components/ai-elements/model-selector.tsx +213 -0
  26. package/components/ai-elements/node.tsx +71 -0
  27. package/components/ai-elements/open-in-chat.tsx +370 -0
  28. package/components/ai-elements/package-info.tsx +239 -0
  29. package/components/ai-elements/panel.tsx +15 -0
  30. package/components/ai-elements/persona.tsx +306 -0
  31. package/components/ai-elements/plan.tsx +147 -0
  32. package/components/ai-elements/prompt-input.tsx +1463 -0
  33. package/components/ai-elements/queue.tsx +274 -0
  34. package/components/ai-elements/reasoning.tsx +228 -0
  35. package/components/ai-elements/sandbox.tsx +132 -0
  36. package/components/ai-elements/schema-display.tsx +471 -0
  37. package/components/ai-elements/shimmer.tsx +77 -0
  38. package/components/ai-elements/snippet.tsx +145 -0
  39. package/components/ai-elements/sources.tsx +77 -0
  40. package/components/ai-elements/speech-input.tsx +323 -0
  41. package/components/ai-elements/stack-trace.tsx +528 -0
  42. package/components/ai-elements/suggestion.tsx +57 -0
  43. package/components/ai-elements/task.tsx +87 -0
  44. package/components/ai-elements/terminal.tsx +273 -0
  45. package/components/ai-elements/test-results.tsx +496 -0
  46. package/components/ai-elements/tool.tsx +173 -0
  47. package/components/ai-elements/toolbar.tsx +16 -0
  48. package/components/ai-elements/transcription.tsx +125 -0
  49. package/components/ai-elements/voice-selector.tsx +524 -0
  50. package/components/ai-elements/web-preview.tsx +281 -0
  51. package/components/index.ts +3 -0
  52. package/{accordion.tsx → components/ui/accordion.tsx} +66 -66
  53. package/{alert-dialog.tsx → components/ui/alert-dialog.tsx} +196 -196
  54. package/{alert.tsx → components/ui/alert.tsx} +66 -66
  55. package/{aspect-ratio.tsx → components/ui/aspect-ratio.tsx} +11 -11
  56. package/{avatar.tsx → components/ui/avatar.tsx} +53 -53
  57. package/{badge.tsx → components/ui/badge.tsx} +46 -46
  58. package/{breadcrumb.tsx → components/ui/breadcrumb.tsx} +109 -109
  59. package/{button-group.tsx → components/ui/button-group.tsx} +83 -83
  60. package/{button.tsx → components/ui/button.tsx} +60 -60
  61. package/{calendar.tsx → components/ui/calendar.tsx} +219 -219
  62. package/{card.tsx → components/ui/card.tsx} +92 -92
  63. package/{carousel.tsx → components/ui/carousel.tsx} +241 -241
  64. package/{chart.tsx → components/ui/chart.tsx} +374 -374
  65. package/{checkbox.tsx → components/ui/checkbox.tsx} +32 -32
  66. package/{collapsible.tsx → components/ui/collapsible.tsx} +33 -33
  67. package/{command.tsx → components/ui/command.tsx} +184 -184
  68. package/{context-menu.tsx → components/ui/context-menu.tsx} +252 -252
  69. package/{dialog.tsx → components/ui/dialog.tsx} +143 -143
  70. package/{direction.tsx → components/ui/direction.tsx} +22 -22
  71. package/{drawer.tsx → components/ui/drawer.tsx} +135 -135
  72. package/{dropdown-menu.tsx → components/ui/dropdown-menu.tsx} +257 -257
  73. package/{empty.tsx → components/ui/empty.tsx} +104 -104
  74. package/{field.tsx → components/ui/field.tsx} +248 -248
  75. package/{form.tsx → components/ui/form.tsx} +167 -167
  76. package/{hover-card.tsx → components/ui/hover-card.tsx} +44 -44
  77. package/components/ui/icon.tsx +55 -0
  78. package/components/ui/index.ts +59 -0
  79. package/{input-group.tsx → components/ui/input-group.tsx} +170 -170
  80. package/{input-otp.tsx → components/ui/input-otp.tsx} +77 -77
  81. package/{input.tsx → components/ui/input.tsx} +21 -21
  82. package/{item.tsx → components/ui/item.tsx} +193 -193
  83. package/{kbd.tsx → components/ui/kbd.tsx} +28 -28
  84. package/{label.tsx → components/ui/label.tsx} +24 -24
  85. package/{menubar.tsx → components/ui/menubar.tsx} +276 -276
  86. package/{native-select.tsx → components/ui/native-select.tsx} +62 -62
  87. package/{navigation-menu.tsx → components/ui/navigation-menu.tsx} +168 -168
  88. package/{pagination.tsx → components/ui/pagination.tsx} +127 -127
  89. package/{popover.tsx → components/ui/popover.tsx} +89 -89
  90. package/{progress.tsx → components/ui/progress.tsx} +31 -31
  91. package/{radio-group.tsx → components/ui/radio-group.tsx} +45 -45
  92. package/{resizable.tsx → components/ui/resizable.tsx} +53 -53
  93. package/{scroll-area.tsx → components/ui/scroll-area.tsx} +58 -58
  94. package/{select.tsx → components/ui/select.tsx} +187 -187
  95. package/{separator.tsx → components/ui/separator.tsx} +28 -28
  96. package/{sheet.tsx → components/ui/sheet.tsx} +139 -139
  97. package/{sidebar.tsx → components/ui/sidebar.tsx} +724 -724
  98. package/{skeleton.tsx → components/ui/skeleton.tsx} +13 -13
  99. package/{slider.tsx → components/ui/slider.tsx} +63 -63
  100. package/{sonner.tsx → components/ui/sonner.tsx} +40 -40
  101. package/{spinner.tsx → components/ui/spinner.tsx} +16 -16
  102. package/{switch.tsx → components/ui/switch.tsx} +35 -35
  103. package/{table.tsx → components/ui/table.tsx} +116 -116
  104. package/{tabs.tsx → components/ui/tabs.tsx} +66 -66
  105. package/{textarea.tsx → components/ui/textarea.tsx} +18 -18
  106. package/{toggle-group.tsx → components/ui/toggle-group.tsx} +83 -83
  107. package/{toggle.tsx → components/ui/toggle.tsx} +47 -47
  108. package/{tooltip.tsx → components/ui/tooltip.tsx} +61 -61
  109. package/hooks/index.ts +1 -1
  110. package/hooks/use-mobile.ts +19 -19
  111. package/index.ts +3 -59
  112. package/lib/index.ts +1 -1
  113. package/lib/utils.ts +6 -6
  114. package/package.json +79 -1
  115. package/styles.css +124 -124
  116. package/icon.tsx +0 -21
  117. package/tsconfig.json +0 -12
  118. package/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,141 @@
1
+ "use client";
2
+
3
+ import {
4
+ Accordion,
5
+ AccordionContent,
6
+ AccordionItem,
7
+ AccordionTrigger,
8
+ } from "../ui/accordion";
9
+ import { Badge } from "../ui/badge";
10
+ import { cn } from "../../lib/utils";
11
+ import type { Tool } from "ai";
12
+ import { BotIcon } from "lucide-react";
13
+ import type { ComponentProps } from "react";
14
+ import { memo } from "react";
15
+
16
+ import { CodeBlock } from "./code-block";
17
+
18
+ export type AgentProps = ComponentProps<"div">;
19
+
20
+ export const Agent = memo(({ className, ...props }: AgentProps) => (
21
+ <div
22
+ className={cn("not-prose w-full rounded-md border", className)}
23
+ {...props}
24
+ />
25
+ ));
26
+
27
+ export type AgentHeaderProps = ComponentProps<"div"> & {
28
+ name: string;
29
+ model?: string;
30
+ };
31
+
32
+ export const AgentHeader = memo(
33
+ ({ className, name, model, ...props }: AgentHeaderProps) => (
34
+ <div
35
+ className={cn(
36
+ "flex w-full items-center justify-between gap-4 p-3",
37
+ className
38
+ )}
39
+ {...props}
40
+ >
41
+ <div className="flex items-center gap-2">
42
+ <BotIcon className="size-4 text-muted-foreground" />
43
+ <span className="font-medium text-sm">{name}</span>
44
+ {model && (
45
+ <Badge className="font-mono text-xs" variant="secondary">
46
+ {model}
47
+ </Badge>
48
+ )}
49
+ </div>
50
+ </div>
51
+ )
52
+ );
53
+
54
+ export type AgentContentProps = ComponentProps<"div">;
55
+
56
+ export const AgentContent = memo(
57
+ ({ className, ...props }: AgentContentProps) => (
58
+ <div className={cn("space-y-4 p-4 pt-0", className)} {...props} />
59
+ )
60
+ );
61
+
62
+ export type AgentInstructionsProps = ComponentProps<"div"> & {
63
+ children: string;
64
+ };
65
+
66
+ export const AgentInstructions = memo(
67
+ ({ className, children, ...props }: AgentInstructionsProps) => (
68
+ <div className={cn("space-y-2", className)} {...props}>
69
+ <span className="font-medium text-muted-foreground text-sm">
70
+ Instructions
71
+ </span>
72
+ <div className="rounded-md bg-muted/50 p-3 text-muted-foreground text-sm">
73
+ <p>{children}</p>
74
+ </div>
75
+ </div>
76
+ )
77
+ );
78
+
79
+ export type AgentToolsProps = ComponentProps<typeof Accordion>;
80
+
81
+ export const AgentTools = memo(({ className, ...props }: AgentToolsProps) => (
82
+ <div className={cn("space-y-2", className)}>
83
+ <span className="font-medium text-muted-foreground text-sm">Tools</span>
84
+ <Accordion className="rounded-md border" {...props} />
85
+ </div>
86
+ ));
87
+
88
+ export type AgentToolProps = ComponentProps<typeof AccordionItem> & {
89
+ tool: Tool;
90
+ };
91
+
92
+ export const AgentTool = memo(
93
+ ({ className, tool, value, ...props }: AgentToolProps) => {
94
+ const schema =
95
+ "jsonSchema" in tool && tool.jsonSchema
96
+ ? tool.jsonSchema
97
+ : tool.inputSchema;
98
+
99
+ return (
100
+ <AccordionItem
101
+ className={cn("border-b last:border-b-0", className)}
102
+ value={value}
103
+ {...props}
104
+ >
105
+ <AccordionTrigger className="px-3 py-2 text-sm hover:no-underline">
106
+ {tool.description ?? "No description"}
107
+ </AccordionTrigger>
108
+ <AccordionContent className="px-3 pb-3">
109
+ <div className="rounded-md bg-muted/50">
110
+ <CodeBlock code={JSON.stringify(schema, null, 2)} language="json" />
111
+ </div>
112
+ </AccordionContent>
113
+ </AccordionItem>
114
+ );
115
+ }
116
+ );
117
+
118
+ export type AgentOutputProps = ComponentProps<"div"> & {
119
+ schema: string;
120
+ };
121
+
122
+ export const AgentOutput = memo(
123
+ ({ className, schema, ...props }: AgentOutputProps) => (
124
+ <div className={cn("space-y-2", className)} {...props}>
125
+ <span className="font-medium text-muted-foreground text-sm">
126
+ Output Schema
127
+ </span>
128
+ <div className="rounded-md bg-muted/50">
129
+ <CodeBlock code={schema} language="typescript" />
130
+ </div>
131
+ </div>
132
+ )
133
+ );
134
+
135
+ Agent.displayName = "Agent";
136
+ AgentHeader.displayName = "AgentHeader";
137
+ AgentContent.displayName = "AgentContent";
138
+ AgentInstructions.displayName = "AgentInstructions";
139
+ AgentTools.displayName = "AgentTools";
140
+ AgentTool.displayName = "AgentTool";
141
+ AgentOutput.displayName = "AgentOutput";
@@ -0,0 +1,148 @@
1
+ "use client";
2
+
3
+ import { Button } from "../ui/button";
4
+ import {
5
+ Tooltip,
6
+ TooltipContent,
7
+ TooltipProvider,
8
+ TooltipTrigger,
9
+ } from "../ui/tooltip";
10
+ import { cn } from "../../lib/utils";
11
+ import type { LucideIcon } from "lucide-react";
12
+ import { XIcon } from "lucide-react";
13
+ import type { ComponentProps, HTMLAttributes } from "react";
14
+
15
+ export type ArtifactProps = HTMLAttributes<HTMLDivElement>;
16
+
17
+ export const Artifact = ({ className, ...props }: ArtifactProps) => (
18
+ <div
19
+ className={cn(
20
+ "flex flex-col overflow-hidden rounded-lg border bg-background shadow-sm",
21
+ className
22
+ )}
23
+ {...props}
24
+ />
25
+ );
26
+
27
+ export type ArtifactHeaderProps = HTMLAttributes<HTMLDivElement>;
28
+
29
+ export const ArtifactHeader = ({
30
+ className,
31
+ ...props
32
+ }: ArtifactHeaderProps) => (
33
+ <div
34
+ className={cn(
35
+ "flex items-center justify-between border-b bg-muted/50 px-4 py-3",
36
+ className
37
+ )}
38
+ {...props}
39
+ />
40
+ );
41
+
42
+ export type ArtifactCloseProps = ComponentProps<typeof Button>;
43
+
44
+ export const ArtifactClose = ({
45
+ className,
46
+ children,
47
+ size = "sm",
48
+ variant = "ghost",
49
+ ...props
50
+ }: ArtifactCloseProps) => (
51
+ <Button
52
+ className={cn(
53
+ "size-8 p-0 text-muted-foreground hover:text-foreground",
54
+ className
55
+ )}
56
+ size={size}
57
+ type="button"
58
+ variant={variant}
59
+ {...props}
60
+ >
61
+ {children ?? <XIcon className="size-4" />}
62
+ <span className="sr-only">Close</span>
63
+ </Button>
64
+ );
65
+
66
+ export type ArtifactTitleProps = HTMLAttributes<HTMLParagraphElement>;
67
+
68
+ export const ArtifactTitle = ({ className, ...props }: ArtifactTitleProps) => (
69
+ <p
70
+ className={cn("font-medium text-foreground text-sm", className)}
71
+ {...props}
72
+ />
73
+ );
74
+
75
+ export type ArtifactDescriptionProps = HTMLAttributes<HTMLParagraphElement>;
76
+
77
+ export const ArtifactDescription = ({
78
+ className,
79
+ ...props
80
+ }: ArtifactDescriptionProps) => (
81
+ <p className={cn("text-muted-foreground text-sm", className)} {...props} />
82
+ );
83
+
84
+ export type ArtifactActionsProps = HTMLAttributes<HTMLDivElement>;
85
+
86
+ export const ArtifactActions = ({
87
+ className,
88
+ ...props
89
+ }: ArtifactActionsProps) => (
90
+ <div className={cn("flex items-center gap-1", className)} {...props} />
91
+ );
92
+
93
+ export type ArtifactActionProps = ComponentProps<typeof Button> & {
94
+ tooltip?: string;
95
+ label?: string;
96
+ icon?: LucideIcon;
97
+ };
98
+
99
+ export const ArtifactAction = ({
100
+ tooltip,
101
+ label,
102
+ icon: Icon,
103
+ children,
104
+ className,
105
+ size = "sm",
106
+ variant = "ghost",
107
+ ...props
108
+ }: ArtifactActionProps) => {
109
+ const button = (
110
+ <Button
111
+ className={cn(
112
+ "size-8 p-0 text-muted-foreground hover:text-foreground",
113
+ className
114
+ )}
115
+ size={size}
116
+ type="button"
117
+ variant={variant}
118
+ {...props}
119
+ >
120
+ {Icon ? <Icon className="size-4" /> : children}
121
+ <span className="sr-only">{label || tooltip}</span>
122
+ </Button>
123
+ );
124
+
125
+ if (tooltip) {
126
+ return (
127
+ <TooltipProvider>
128
+ <Tooltip>
129
+ <TooltipTrigger asChild>{button}</TooltipTrigger>
130
+ <TooltipContent>
131
+ <p>{tooltip}</p>
132
+ </TooltipContent>
133
+ </Tooltip>
134
+ </TooltipProvider>
135
+ );
136
+ }
137
+
138
+ return button;
139
+ };
140
+
141
+ export type ArtifactContentProps = HTMLAttributes<HTMLDivElement>;
142
+
143
+ export const ArtifactContent = ({
144
+ className,
145
+ ...props
146
+ }: ArtifactContentProps) => (
147
+ <div className={cn("flex-1 overflow-auto p-4", className)} {...props} />
148
+ );
@@ -0,0 +1,426 @@
1
+ "use client";
2
+
3
+ import { Button } from "../ui/button";
4
+ import {
5
+ HoverCard,
6
+ HoverCardContent,
7
+ HoverCardTrigger,
8
+ } from "../ui/hover-card";
9
+ import { cn } from "../../lib/utils";
10
+ import type { FileUIPart, SourceDocumentUIPart } from "ai";
11
+ import {
12
+ FileTextIcon,
13
+ GlobeIcon,
14
+ ImageIcon,
15
+ Music2Icon,
16
+ PaperclipIcon,
17
+ VideoIcon,
18
+ XIcon,
19
+ } from "lucide-react";
20
+ import type { ComponentProps, HTMLAttributes, ReactNode } from "react";
21
+ import { createContext, useCallback, useContext, useMemo } from "react";
22
+
23
+ // ============================================================================
24
+ // Types
25
+ // ============================================================================
26
+
27
+ export type AttachmentData =
28
+ | (FileUIPart & { id: string })
29
+ | (SourceDocumentUIPart & { id: string });
30
+
31
+ export type AttachmentMediaCategory =
32
+ | "image"
33
+ | "video"
34
+ | "audio"
35
+ | "document"
36
+ | "source"
37
+ | "unknown";
38
+
39
+ export type AttachmentVariant = "grid" | "inline" | "list";
40
+
41
+ const mediaCategoryIcons: Record<AttachmentMediaCategory, typeof ImageIcon> = {
42
+ audio: Music2Icon,
43
+ document: FileTextIcon,
44
+ image: ImageIcon,
45
+ source: GlobeIcon,
46
+ unknown: PaperclipIcon,
47
+ video: VideoIcon,
48
+ };
49
+
50
+ // ============================================================================
51
+ // Utility Functions
52
+ // ============================================================================
53
+
54
+ export const getMediaCategory = (
55
+ data: AttachmentData
56
+ ): AttachmentMediaCategory => {
57
+ if (data.type === "source-document") {
58
+ return "source";
59
+ }
60
+
61
+ const mediaType = data.mediaType ?? "";
62
+
63
+ if (mediaType.startsWith("image/")) {
64
+ return "image";
65
+ }
66
+ if (mediaType.startsWith("video/")) {
67
+ return "video";
68
+ }
69
+ if (mediaType.startsWith("audio/")) {
70
+ return "audio";
71
+ }
72
+ if (mediaType.startsWith("application/") || mediaType.startsWith("text/")) {
73
+ return "document";
74
+ }
75
+
76
+ return "unknown";
77
+ };
78
+
79
+ export const getAttachmentLabel = (data: AttachmentData): string => {
80
+ if (data.type === "source-document") {
81
+ return data.title || data.filename || "Source";
82
+ }
83
+
84
+ const category = getMediaCategory(data);
85
+ return data.filename || (category === "image" ? "Image" : "Attachment");
86
+ };
87
+
88
+ const renderAttachmentImage = (
89
+ url: string,
90
+ filename: string | undefined,
91
+ isGrid: boolean
92
+ ) =>
93
+ isGrid ? (
94
+ <img
95
+ alt={filename || "Image"}
96
+ className="size-full object-cover"
97
+ height={96}
98
+ src={url}
99
+ width={96}
100
+ />
101
+ ) : (
102
+ <img
103
+ alt={filename || "Image"}
104
+ className="size-full rounded object-cover"
105
+ height={20}
106
+ src={url}
107
+ width={20}
108
+ />
109
+ );
110
+
111
+ // ============================================================================
112
+ // Contexts
113
+ // ============================================================================
114
+
115
+ interface AttachmentsContextValue {
116
+ variant: AttachmentVariant;
117
+ }
118
+
119
+ const AttachmentsContext = createContext<AttachmentsContextValue | null>(null);
120
+
121
+ interface AttachmentContextValue {
122
+ data: AttachmentData;
123
+ mediaCategory: AttachmentMediaCategory;
124
+ onRemove?: () => void;
125
+ variant: AttachmentVariant;
126
+ }
127
+
128
+ const AttachmentContext = createContext<AttachmentContextValue | null>(null);
129
+
130
+ // ============================================================================
131
+ // Hooks
132
+ // ============================================================================
133
+
134
+ export const useAttachmentsContext = () =>
135
+ useContext(AttachmentsContext) ?? { variant: "grid" as const };
136
+
137
+ export const useAttachmentContext = () => {
138
+ const ctx = useContext(AttachmentContext);
139
+ if (!ctx) {
140
+ throw new Error("Attachment components must be used within <Attachment>");
141
+ }
142
+ return ctx;
143
+ };
144
+
145
+ // ============================================================================
146
+ // Attachments - Container
147
+ // ============================================================================
148
+
149
+ export type AttachmentsProps = HTMLAttributes<HTMLDivElement> & {
150
+ variant?: AttachmentVariant;
151
+ };
152
+
153
+ export const Attachments = ({
154
+ variant = "grid",
155
+ className,
156
+ children,
157
+ ...props
158
+ }: AttachmentsProps) => {
159
+ const contextValue = useMemo(() => ({ variant }), [variant]);
160
+
161
+ return (
162
+ <AttachmentsContext.Provider value={contextValue}>
163
+ <div
164
+ className={cn(
165
+ "flex items-start",
166
+ variant === "list" ? "flex-col gap-2" : "flex-wrap gap-2",
167
+ variant === "grid" && "ml-auto w-fit",
168
+ className
169
+ )}
170
+ {...props}
171
+ >
172
+ {children}
173
+ </div>
174
+ </AttachmentsContext.Provider>
175
+ );
176
+ };
177
+
178
+ // ============================================================================
179
+ // Attachment - Item
180
+ // ============================================================================
181
+
182
+ export type AttachmentProps = HTMLAttributes<HTMLDivElement> & {
183
+ data: AttachmentData;
184
+ onRemove?: () => void;
185
+ };
186
+
187
+ export const Attachment = ({
188
+ data,
189
+ onRemove,
190
+ className,
191
+ children,
192
+ ...props
193
+ }: AttachmentProps) => {
194
+ const { variant } = useAttachmentsContext();
195
+ const mediaCategory = getMediaCategory(data);
196
+
197
+ const contextValue = useMemo<AttachmentContextValue>(
198
+ () => ({ data, mediaCategory, onRemove, variant }),
199
+ [data, mediaCategory, onRemove, variant]
200
+ );
201
+
202
+ return (
203
+ <AttachmentContext.Provider value={contextValue}>
204
+ <div
205
+ className={cn(
206
+ "group relative",
207
+ variant === "grid" && "size-24 overflow-hidden rounded-lg",
208
+ variant === "inline" && [
209
+ "flex h-8 cursor-pointer select-none items-center gap-1.5",
210
+ "rounded-md border border-border px-1.5",
211
+ "font-medium text-sm transition-all",
212
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
213
+ ],
214
+ variant === "list" && [
215
+ "flex w-full items-center gap-3 rounded-lg border p-3",
216
+ "hover:bg-accent/50",
217
+ ],
218
+ className
219
+ )}
220
+ {...props}
221
+ >
222
+ {children}
223
+ </div>
224
+ </AttachmentContext.Provider>
225
+ );
226
+ };
227
+
228
+ // ============================================================================
229
+ // AttachmentPreview - Media preview
230
+ // ============================================================================
231
+
232
+ export type AttachmentPreviewProps = HTMLAttributes<HTMLDivElement> & {
233
+ fallbackIcon?: ReactNode;
234
+ };
235
+
236
+ export const AttachmentPreview = ({
237
+ fallbackIcon,
238
+ className,
239
+ ...props
240
+ }: AttachmentPreviewProps) => {
241
+ const { data, mediaCategory, variant } = useAttachmentContext();
242
+
243
+ const iconSize = variant === "inline" ? "size-3" : "size-4";
244
+
245
+ const renderIcon = (Icon: typeof ImageIcon) => (
246
+ <Icon className={cn(iconSize, "text-muted-foreground")} />
247
+ );
248
+
249
+ const renderContent = () => {
250
+ if (mediaCategory === "image" && data.type === "file" && data.url) {
251
+ return renderAttachmentImage(data.url, data.filename, variant === "grid");
252
+ }
253
+
254
+ if (mediaCategory === "video" && data.type === "file" && data.url) {
255
+ return <video className="size-full object-cover" muted src={data.url} />;
256
+ }
257
+
258
+ const Icon = mediaCategoryIcons[mediaCategory];
259
+ return fallbackIcon ?? renderIcon(Icon);
260
+ };
261
+
262
+ return (
263
+ <div
264
+ className={cn(
265
+ "flex shrink-0 items-center justify-center overflow-hidden",
266
+ variant === "grid" && "size-full bg-muted",
267
+ variant === "inline" && "size-5 rounded bg-background",
268
+ variant === "list" && "size-12 rounded bg-muted",
269
+ className
270
+ )}
271
+ {...props}
272
+ >
273
+ {renderContent()}
274
+ </div>
275
+ );
276
+ };
277
+
278
+ // ============================================================================
279
+ // AttachmentInfo - Name and type display
280
+ // ============================================================================
281
+
282
+ export type AttachmentInfoProps = HTMLAttributes<HTMLDivElement> & {
283
+ showMediaType?: boolean;
284
+ };
285
+
286
+ export const AttachmentInfo = ({
287
+ showMediaType = false,
288
+ className,
289
+ ...props
290
+ }: AttachmentInfoProps) => {
291
+ const { data, variant } = useAttachmentContext();
292
+ const label = getAttachmentLabel(data);
293
+
294
+ if (variant === "grid") {
295
+ return null;
296
+ }
297
+
298
+ return (
299
+ <div className={cn("min-w-0 flex-1", className)} {...props}>
300
+ <span className="block truncate">{label}</span>
301
+ {showMediaType && data.mediaType && (
302
+ <span className="block truncate text-muted-foreground text-xs">
303
+ {data.mediaType}
304
+ </span>
305
+ )}
306
+ </div>
307
+ );
308
+ };
309
+
310
+ // ============================================================================
311
+ // AttachmentRemove - Remove button
312
+ // ============================================================================
313
+
314
+ export type AttachmentRemoveProps = ComponentProps<typeof Button> & {
315
+ label?: string;
316
+ };
317
+
318
+ export const AttachmentRemove = ({
319
+ label = "Remove",
320
+ className,
321
+ children,
322
+ ...props
323
+ }: AttachmentRemoveProps) => {
324
+ const { onRemove, variant } = useAttachmentContext();
325
+
326
+ const handleClick = useCallback(
327
+ (e: React.MouseEvent) => {
328
+ e.stopPropagation();
329
+ onRemove?.();
330
+ },
331
+ [onRemove]
332
+ );
333
+
334
+ if (!onRemove) {
335
+ return null;
336
+ }
337
+
338
+ return (
339
+ <Button
340
+ aria-label={label}
341
+ className={cn(
342
+ variant === "grid" && [
343
+ "absolute top-2 right-2 size-6 rounded-full p-0",
344
+ "bg-background/80 backdrop-blur-sm",
345
+ "opacity-0 transition-opacity group-hover:opacity-100",
346
+ "hover:bg-background",
347
+ "[&>svg]:size-3",
348
+ ],
349
+ variant === "inline" && [
350
+ "size-5 rounded p-0",
351
+ "opacity-0 transition-opacity group-hover:opacity-100",
352
+ "[&>svg]:size-2.5",
353
+ ],
354
+ variant === "list" && ["size-8 shrink-0 rounded p-0", "[&>svg]:size-4"],
355
+ className
356
+ )}
357
+ onClick={handleClick}
358
+ type="button"
359
+ variant="ghost"
360
+ {...props}
361
+ >
362
+ {children ?? <XIcon />}
363
+ <span className="sr-only">{label}</span>
364
+ </Button>
365
+ );
366
+ };
367
+
368
+ // ============================================================================
369
+ // AttachmentHoverCard - Hover preview
370
+ // ============================================================================
371
+
372
+ export type AttachmentHoverCardProps = ComponentProps<typeof HoverCard>;
373
+
374
+ export const AttachmentHoverCard = ({
375
+ openDelay = 0,
376
+ closeDelay = 0,
377
+ ...props
378
+ }: AttachmentHoverCardProps) => (
379
+ <HoverCard closeDelay={closeDelay} openDelay={openDelay} {...props} />
380
+ );
381
+
382
+ export type AttachmentHoverCardTriggerProps = ComponentProps<
383
+ typeof HoverCardTrigger
384
+ >;
385
+
386
+ export const AttachmentHoverCardTrigger = (
387
+ props: AttachmentHoverCardTriggerProps
388
+ ) => <HoverCardTrigger {...props} />;
389
+
390
+ export type AttachmentHoverCardContentProps = ComponentProps<
391
+ typeof HoverCardContent
392
+ >;
393
+
394
+ export const AttachmentHoverCardContent = ({
395
+ align = "start",
396
+ className,
397
+ ...props
398
+ }: AttachmentHoverCardContentProps) => (
399
+ <HoverCardContent
400
+ align={align}
401
+ className={cn("w-auto p-2", className)}
402
+ {...props}
403
+ />
404
+ );
405
+
406
+ // ============================================================================
407
+ // AttachmentEmpty - Empty state
408
+ // ============================================================================
409
+
410
+ export type AttachmentEmptyProps = HTMLAttributes<HTMLDivElement>;
411
+
412
+ export const AttachmentEmpty = ({
413
+ className,
414
+ children,
415
+ ...props
416
+ }: AttachmentEmptyProps) => (
417
+ <div
418
+ className={cn(
419
+ "flex items-center justify-center p-4 text-muted-foreground text-sm",
420
+ className
421
+ )}
422
+ {...props}
423
+ >
424
+ {children ?? "No attachments"}
425
+ </div>
426
+ );