@vvedantb/ui 0.17.1 → 0.18.1

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 (130) hide show
  1. package/README.md +251 -3
  2. package/package.json +13 -5
  3. package/shell/src/SidebarResize.tsx +1 -4
  4. package/src/ai-elements/__snapshots__/queue.test.tsx.snap +11 -0
  5. package/src/ai-elements/activity-question-utils.ts +26 -0
  6. package/src/ai-elements/activity-question.tsx +159 -0
  7. package/src/ai-elements/activity-shared.tsx +459 -0
  8. package/src/ai-elements/activity-step-detail.tsx +115 -0
  9. package/src/ai-elements/activity-step-label.ts +981 -0
  10. package/src/ai-elements/activity-tasks-utils.ts +199 -0
  11. package/src/ai-elements/activity-tasks.tsx +770 -0
  12. package/src/ai-elements/artifact.tsx +162 -52
  13. package/src/ai-elements/attachments.tsx +62 -32
  14. package/src/ai-elements/code-block.tsx +76 -22
  15. package/src/ai-elements/context.tsx +370 -0
  16. package/src/ai-elements/conversation.tsx +186 -62
  17. package/src/ai-elements/file-chip.tsx +53 -0
  18. package/src/ai-elements/message.tsx +161 -50
  19. package/src/ai-elements/model-picker-content.tsx +505 -0
  20. package/src/ai-elements/model-picker-types.ts +56 -0
  21. package/src/ai-elements/model-picker.tsx +301 -0
  22. package/src/ai-elements/model-selector.tsx +102 -21
  23. package/src/ai-elements/plan.tsx +130 -0
  24. package/src/ai-elements/prompt-input-settings.tsx +144 -0
  25. package/src/ai-elements/prompt-input-speech.tsx +189 -0
  26. package/src/ai-elements/prompt-input-traits.tsx +281 -0
  27. package/src/ai-elements/prompt-input.tsx +320 -134
  28. package/src/ai-elements/provider-icon.tsx +133 -0
  29. package/src/ai-elements/queue.tsx +293 -9
  30. package/src/ai-elements/shimmer.tsx +72 -1
  31. package/src/ai-elements/simple-model-ladder.tsx +108 -0
  32. package/src/ai-elements/speech-input.tsx +5 -3
  33. package/src/ai-elements/test-results.tsx +510 -0
  34. package/src/ai-elements/trace.tsx +243 -0
  35. package/src/ai-elements/use-streaming-disclosure.ts +50 -0
  36. package/src/ai-elements/web-preview.tsx +349 -0
  37. package/src/composer/input-area/ComposerSlashMenu.tsx +1 -1
  38. package/src/index.ts +66 -12
  39. package/src/kibo/avatar-stack.tsx +40 -0
  40. package/src/kibo/contribution-graph.tsx +539 -0
  41. package/src/kibo/cursor.tsx +72 -0
  42. package/src/kibo/gantt-features.tsx +439 -0
  43. package/src/kibo/gantt-header.tsx +181 -0
  44. package/src/kibo/gantt-provider.tsx +657 -0
  45. package/src/kibo/gantt-sidebar.tsx +157 -0
  46. package/src/kibo/gantt-timeline.tsx +154 -0
  47. package/src/kibo/index.ts +151 -0
  48. package/src/kibo/kanban.tsx +338 -0
  49. package/src/kibo/kanbanDropTarget.ts +25 -0
  50. package/src/kibo/list.tsx +162 -0
  51. package/src/kibo/theme-switcher.tsx +109 -0
  52. package/src/kibo/video-player.tsx +237 -0
  53. package/src/markdown/Markdown.tsx +112 -0
  54. package/src/markdown/MarkdownBlockquote.tsx +45 -0
  55. package/src/markdown/MarkdownCodeBlock.tsx +197 -0
  56. package/src/markdown/MarkdownInlineCode.tsx +40 -0
  57. package/src/markdown/MarkdownLink.tsx +92 -0
  58. package/src/markdown/codeFence.ts +57 -0
  59. package/src/markdown/codeHighlight.ts +138 -0
  60. package/src/markdown/incrementalBlocks.ts +182 -0
  61. package/src/markdown/index.ts +3 -0
  62. package/src/markdown/markdownClipboard.ts +178 -0
  63. package/src/markdown/mermaid.ts +155 -0
  64. package/src/markdown/rehypeGithubAlerts.ts +80 -0
  65. package/src/motion/presets.ts +24 -0
  66. package/src/ui/__snapshots__/alt-wrappers.test.tsx.snap +19 -0
  67. package/src/ui/__snapshots__/sonner-dom.test.tsx.snap +5 -0
  68. package/src/ui/_menu-parts.tsx +29 -0
  69. package/src/ui/accordion.alt.tsx +46 -53
  70. package/src/ui/accordion.tsx +59 -10
  71. package/src/ui/avatar.tsx +56 -0
  72. package/src/ui/badge.tsx +45 -21
  73. package/src/ui/border-beam.tsx +97 -0
  74. package/src/ui/button-group.tsx +50 -21
  75. package/src/ui/button.tsx +67 -45
  76. package/src/ui/calendar.tsx +48 -5
  77. package/src/ui/card.tsx +77 -8
  78. package/src/ui/carousel.tsx +60 -0
  79. package/src/ui/centered-spinner.tsx +30 -0
  80. package/src/ui/checkbox.tsx +34 -16
  81. package/src/ui/clear-input.alt.tsx +33 -75
  82. package/src/ui/clear-input.tsx +140 -15
  83. package/src/ui/collapsible.tsx +40 -2
  84. package/src/ui/command.tsx +143 -70
  85. package/src/ui/context-menu.tsx +159 -93
  86. package/src/ui/crossfade-icon.tsx +123 -0
  87. package/src/ui/dialog.tsx +154 -70
  88. package/src/ui/dropdown-menu.tsx +177 -117
  89. package/src/ui/hover-card.tsx +59 -17
  90. package/src/ui/input-group.alt.tsx +83 -83
  91. package/src/ui/input-group.tsx +129 -58
  92. package/src/ui/input.tsx +13 -4
  93. package/src/ui/label.tsx +11 -8
  94. package/src/ui/list-row.tsx +232 -0
  95. package/src/ui/loading-state.tsx +215 -0
  96. package/src/ui/popover.tsx +56 -23
  97. package/src/ui/progress.tsx +28 -16
  98. package/src/ui/refresh-spin-icon.tsx +41 -0
  99. package/src/ui/search-input.tsx +66 -15
  100. package/src/ui/select.tsx +171 -100
  101. package/src/ui/separator.tsx +23 -14
  102. package/src/ui/slider.tsx +21 -7
  103. package/src/ui/sonner.tsx +68 -30
  104. package/src/ui/spinner.tsx +229 -11
  105. package/src/ui/status-dot.tsx +84 -0
  106. package/src/ui/surface.tsx +49 -0
  107. package/src/ui/switch.tsx +29 -15
  108. package/src/ui/table.tsx +38 -16
  109. package/src/ui/tabs.tsx +123 -35
  110. package/src/ui/tabsSliding.ts +33 -1
  111. package/src/ui/textarea.tsx +13 -4
  112. package/src/ui/tooltip.tsx +50 -14
  113. package/src/ui/ui-config.tsx +149 -0
  114. package/src/utils/cat-color.ts +123 -0
  115. package/src/utils/dayjs.ts +6 -0
  116. package/src/utils/gesture.ts +59 -0
  117. package/src/utils/runtimeVisibility.ts +167 -0
  118. package/src/utils/sharedClock.ts +96 -0
  119. package/src/utils/surface-radius.ts +16 -0
  120. package/src/utils/useDragSensors.ts +58 -0
  121. package/tokens/examples/tokens-moved/Fixture.tsx +283 -0
  122. package/tokens/examples/tokens-moved/README.md +20 -0
  123. package/tokens/examples/tokens-moved/index.html +12 -0
  124. package/tokens/examples/tokens-moved/main.tsx +6 -0
  125. package/tokens/examples/tokens-moved/package.json +14 -0
  126. package/tokens/examples/tokens-moved/styles.css +12 -0
  127. package/tokens/examples/tokens-moved/tsconfig.json +14 -0
  128. package/tokens/examples/tokens-moved/vite.config.ts +11 -0
  129. package/tokens/src/css/border-beam.css +116 -0
  130. package/tokens/src/css/markdown.css +318 -0
@@ -1,66 +1,157 @@
1
1
  "use client";
2
2
 
3
- import type { ComponentProps, ReactNode } from "react";
3
+ import type { ComponentProps, ComponentType, ReactNode } from "react";
4
+ import { isValidElement } from "react";
5
+ import { IconX } from "@tabler/icons-react";
4
6
 
5
7
  import { Button } from "../ui/button";
6
8
  import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
7
- import { cn } from "../utils/cn";
9
+ import { useCn, useUiConfig } from "../ui/ui-config";
8
10
  import { flatElevatedSurfaceClass } from "../ui/surface-classes";
9
11
 
12
+ /**
13
+ * `UiConfig.Artifact`: classes replace each part's built-in classes
14
+ * (`action` covers `ArtifactAction` and `ArtifactClose`). `actionSize` is the
15
+ * actions' default button size; `actionLabel: "sr-only"` names an action with
16
+ * a visually hidden span instead of `aria-label`.
17
+ */
18
+ export interface ArtifactConfig {
19
+ actionSize?: ComponentProps<typeof Button>["size"];
20
+ actionLabel?: "aria-label" | "sr-only";
21
+ classes?: {
22
+ root?: string;
23
+ header?: string;
24
+ title?: string;
25
+ description?: string;
26
+ actions?: string;
27
+ action?: string;
28
+ /** Class of an icon given as a component (`icon={IconCopy}`). */
29
+ actionIcon?: string;
30
+ content?: string;
31
+ };
32
+ }
33
+
10
34
  export type ArtifactProps = ComponentProps<"div">;
11
35
 
12
- export const Artifact = ({ className, ...props }: ArtifactProps) => (
13
- <div
14
- className={cn(flatElevatedSurfaceClass, "rounded-lg", className)}
15
- {...props}
16
- />
17
- );
36
+ export const Artifact = ({ className, ...props }: ArtifactProps) => {
37
+ const root = useUiConfig("Artifact")?.classes?.root;
38
+ const merge = useCn();
39
+ return (
40
+ <div
41
+ className={merge(root ?? [flatElevatedSurfaceClass, "rounded-lg"], className)}
42
+ {...props}
43
+ />
44
+ );
45
+ };
18
46
 
19
47
  export type ArtifactHeaderProps = ComponentProps<"div">;
20
48
 
21
49
  export const ArtifactHeader = ({
22
50
  className,
23
51
  ...props
24
- }: ArtifactHeaderProps) => (
25
- <div
26
- className={cn(
27
- "flex items-start justify-between gap-2 px-4 py-3",
28
- className,
29
- )}
30
- {...props}
31
- />
32
- );
52
+ }: ArtifactHeaderProps) => {
53
+ const header = useUiConfig("Artifact")?.classes?.header;
54
+ const merge = useCn();
55
+ return (
56
+ <div
57
+ className={merge(
58
+ header ?? "flex items-start justify-between gap-2 px-4 py-3",
59
+ className,
60
+ )}
61
+ {...props}
62
+ />
63
+ );
64
+ };
33
65
 
34
66
  export type ArtifactTitleProps = ComponentProps<"p">;
35
67
 
36
- export const ArtifactTitle = ({ className, ...props }: ArtifactTitleProps) => (
37
- <p
38
- className={cn("text-sm font-medium text-foreground", className)}
39
- {...props}
40
- />
41
- );
68
+ export const ArtifactTitle = ({ className, ...props }: ArtifactTitleProps) => {
69
+ const title = useUiConfig("Artifact")?.classes?.title;
70
+ const merge = useCn();
71
+ return (
72
+ <p
73
+ className={merge(title ?? "text-sm font-medium text-foreground", className)}
74
+ {...props}
75
+ />
76
+ );
77
+ };
42
78
 
43
79
  export type ArtifactDescriptionProps = ComponentProps<"p">;
44
80
 
45
81
  export const ArtifactDescription = ({
46
82
  className,
47
83
  ...props
48
- }: ArtifactDescriptionProps) => (
49
- <p className={cn("text-xs text-muted-foreground", className)} {...props} />
50
- );
84
+ }: ArtifactDescriptionProps) => {
85
+ const description = useUiConfig("Artifact")?.classes?.description;
86
+ const merge = useCn();
87
+ return (
88
+ <p
89
+ className={merge(description ?? "text-xs text-muted-foreground", className)}
90
+ {...props}
91
+ />
92
+ );
93
+ };
51
94
 
52
95
  export type ArtifactActionsProps = ComponentProps<"div">;
53
96
 
54
97
  export const ArtifactActions = ({
55
98
  className,
56
99
  ...props
57
- }: ArtifactActionsProps) => (
58
- <div className={cn("flex items-center gap-1", className)} {...props} />
59
- );
100
+ }: ArtifactActionsProps) => {
101
+ const actions = useUiConfig("Artifact")?.classes?.actions;
102
+ const merge = useCn();
103
+ return (
104
+ <div className={merge(actions ?? "flex items-center gap-1", className)} {...props} />
105
+ );
106
+ };
107
+
108
+ type ArtifactIcon = ComponentType<{ className?: string }>;
109
+
110
+ /** A component (`IconCopy`) rather than a rendered node (`<IconCopy />`). */
111
+ function isIconComponent(icon: ReactNode | ArtifactIcon): icon is ArtifactIcon {
112
+ if (typeof icon === "function") return true;
113
+ // forwardRef / memo components are objects with `render` / `type`.
114
+ return (
115
+ typeof icon === "object" &&
116
+ icon !== null &&
117
+ !isValidElement(icon) &&
118
+ ("render" in icon || "type" in icon)
119
+ );
120
+ }
121
+
122
+ const renderIcon = (Icon: ArtifactIcon, className: string) => <Icon className={className} />;
123
+
124
+ /** Button shared by `ArtifactAction` and `ArtifactClose`. */
125
+ function ArtifactActionButton({
126
+ label,
127
+ className,
128
+ variant = "ghost",
129
+ size,
130
+ children,
131
+ ...props
132
+ }: Omit<ComponentProps<typeof Button>, "aria-label"> & { label?: string }) {
133
+ const config = useUiConfig("Artifact");
134
+ const merge = useCn();
135
+ const srOnly = config?.actionLabel === "sr-only";
136
+ return (
137
+ <Button
138
+ variant={variant}
139
+ size={size ?? config?.actionSize ?? "icon"}
140
+ className={merge(config?.classes?.action ?? "min-w-6 h-6", className)}
141
+ {...(srOnly ? { type: "button" } : { "aria-label": label })}
142
+ {...props}
143
+ >
144
+ {children}
145
+ {srOnly ? <span className="sr-only">{label}</span> : null}
146
+ </Button>
147
+ );
148
+ }
60
149
 
61
150
  export type ArtifactActionProps = ComponentProps<typeof Button> & {
62
- tooltip: string;
63
- icon: ReactNode;
151
+ /** Without it the button renders alone, with no tooltip. */
152
+ tooltip?: string;
153
+ /** A node (`<IconCopy />`) or an icon component (`IconCopy`). */
154
+ icon?: ReactNode | ArtifactIcon;
64
155
  label?: string;
65
156
  };
66
157
 
@@ -68,25 +159,42 @@ export const ArtifactAction = ({
68
159
  tooltip,
69
160
  icon,
70
161
  label,
71
- className,
72
- variant = "ghost",
73
- size = "icon",
162
+ children,
163
+ "aria-label": ariaLabel,
164
+ ...props
165
+ }: ArtifactActionProps) => {
166
+ const actionIcon = useUiConfig("Artifact")?.classes?.actionIcon;
167
+ const content = isIconComponent(icon)
168
+ ? renderIcon(icon, actionIcon ?? "size-4")
169
+ : (icon ?? children);
170
+ const button = (
171
+ <ArtifactActionButton label={ariaLabel ?? label ?? tooltip} {...props}>
172
+ {content}
173
+ </ArtifactActionButton>
174
+ );
175
+ if (!tooltip) return button;
176
+ return (
177
+ <Tooltip>
178
+ <TooltipTrigger asChild>{button}</TooltipTrigger>
179
+ <TooltipContent>{tooltip}</TooltipContent>
180
+ </Tooltip>
181
+ );
182
+ };
183
+
184
+ export type ArtifactCloseProps = ComponentProps<typeof Button> & {
185
+ /** Accessible name. Default `"Close"`. */
186
+ label?: string;
187
+ };
188
+
189
+ /** Close button for the artifact header, an `ArtifactAction` with an X. */
190
+ export const ArtifactClose = ({
191
+ label = "Close",
192
+ children,
74
193
  ...props
75
- }: ArtifactActionProps) => (
76
- <Tooltip>
77
- <TooltipTrigger asChild>
78
- <Button
79
- variant={variant}
80
- size={size}
81
- className={cn("min-w-6 h-6", className)}
82
- aria-label={label ?? tooltip}
83
- {...props}
84
- >
85
- {icon}
86
- </Button>
87
- </TooltipTrigger>
88
- <TooltipContent>{tooltip}</TooltipContent>
89
- </Tooltip>
194
+ }: ArtifactCloseProps) => (
195
+ <ArtifactActionButton label={label} {...props}>
196
+ {children ?? <IconX className="size-4" />}
197
+ </ArtifactActionButton>
90
198
  );
91
199
 
92
200
  export type ArtifactContentProps = ComponentProps<"div">;
@@ -94,6 +202,8 @@ export type ArtifactContentProps = ComponentProps<"div">;
94
202
  export const ArtifactContent = ({
95
203
  className,
96
204
  ...props
97
- }: ArtifactContentProps) => (
98
- <div className={cn("px-4 pb-4", className)} {...props} />
99
- );
205
+ }: ArtifactContentProps) => {
206
+ const content = useUiConfig("Artifact")?.classes?.content;
207
+ const merge = useCn();
208
+ return <div className={merge(content ?? "px-4 pb-4", className)} {...props} />;
209
+ };
@@ -12,6 +12,7 @@ import {
12
12
  flatElevatedSurfaceHoverClass,
13
13
  flatPanelSubtleClass,
14
14
  } from "../ui/surface-classes";
15
+ import { useCn, useUiConfig } from "../ui/ui-config";
15
16
  import {
16
17
  IconFileText,
17
18
  IconMusic,
@@ -40,6 +41,18 @@ export type AttachmentMediaCategory =
40
41
  | "source"
41
42
  | "unknown";
42
43
  export type AttachmentVariant = "grid" | "inline" | "list";
44
+ /**
45
+ * `UiConfig.Attachments`: per-variant classes that replace the variant part
46
+ * of an `Attachment`, its `AttachmentPreview` and its `AttachmentRemove`.
47
+ * The shared base classes stay.
48
+ */
49
+ export interface AttachmentsConfig {
50
+ classes?: {
51
+ item?: Partial<Record<AttachmentVariant, string>>;
52
+ preview?: Partial<Record<AttachmentVariant, string>>;
53
+ remove?: Partial<Record<AttachmentVariant, string>>;
54
+ };
55
+ }
43
56
  const mediaCategoryIcons: Record<AttachmentMediaCategory, typeof IconPhoto> = {
44
57
  audio: IconMusic,
45
58
  document: IconFileText,
@@ -139,11 +152,12 @@ export const Attachments = ({
139
152
  children,
140
153
  ...props
141
154
  }: AttachmentsProps) => {
155
+ const merge = useCn();
142
156
  const contextValue = { variant };
143
157
  return (
144
158
  <AttachmentsContext.Provider value={contextValue}>
145
159
  <div
146
- className={cn(
160
+ className={merge(
147
161
  "flex items-start",
148
162
  variant === "list" ? "flex-col gap-2" : "flex-wrap gap-2",
149
163
  variant === "grid" && "ml-auto w-fit",
@@ -171,21 +185,24 @@ export const Attachment = ({
171
185
  ...props
172
186
  }: AttachmentProps) => {
173
187
  const { variant } = useAttachmentsContext();
188
+ const item = useUiConfig("Attachments")?.classes?.item?.[variant];
189
+ const merge = useCn();
174
190
  const mediaCategory = getMediaCategory(data);
175
191
  const contextValue = { data, mediaCategory, onRemove, variant };
176
192
  return (
177
193
  <AttachmentContext.Provider value={contextValue}>
178
194
  <div
179
- className={cn(
195
+ className={merge(
180
196
  "group relative",
181
- variant === "grid" && "size-24 overflow-hidden rounded-lg",
182
- variant === "inline" && [
197
+ item,
198
+ item === undefined && variant === "grid" && "size-24 overflow-hidden rounded-lg",
199
+ item === undefined && variant === "inline" && [
183
200
  "flex h-8 cursor-pointer select-none items-center gap-1.5",
184
201
  "rounded-md px-1.5",
185
202
  "font-medium text-sm transition-[background-color,color,transform] duration-150 ease-out",
186
203
  "active:scale-[0.97] hover:bg-surface-secondary hover:text-foreground",
187
204
  ],
188
- variant === "list" && [
205
+ item === undefined && variant === "list" && [
189
206
  "flex w-full items-center gap-3 rounded-lg p-3",
190
207
  "transition-[background-color,transform] duration-150 ease-out active:scale-[0.99] hover:bg-surface-secondary",
191
208
  ],
@@ -210,6 +227,8 @@ export const AttachmentPreview = ({
210
227
  ...props
211
228
  }: AttachmentPreviewProps) => {
212
229
  const { data, mediaCategory, variant } = useAttachmentContext();
230
+ const preview = useUiConfig("Attachments")?.classes?.preview?.[variant];
231
+ const merge = useCn();
213
232
  const iconSize = variant === "inline" ? "size-3" : "size-4";
214
233
  const renderIcon = (Icon: typeof IconPhoto) => (
215
234
  <Icon className={cn(iconSize, "text-muted-foreground")} />
@@ -226,11 +245,12 @@ export const AttachmentPreview = ({
226
245
  };
227
246
  return (
228
247
  <div
229
- className={cn(
248
+ className={merge(
230
249
  "flex shrink-0 items-center justify-center overflow-hidden",
231
- variant === "grid" && cn("size-full", flatPanelSubtleClass),
232
- variant === "inline" && cn("size-5 rounded", flatPanelSubtleClass),
233
- variant === "list" && cn("size-12 rounded", flatPanelSubtleClass),
250
+ preview,
251
+ preview === undefined && variant === "grid" && cn("size-full", flatPanelSubtleClass),
252
+ preview === undefined && variant === "inline" && cn("size-5 rounded", flatPanelSubtleClass),
253
+ preview === undefined && variant === "list" && cn("size-12 rounded", flatPanelSubtleClass),
234
254
  className,
235
255
  )}
236
256
  {...props}
@@ -251,12 +271,13 @@ export const AttachmentInfo = ({
251
271
  ...props
252
272
  }: AttachmentInfoProps) => {
253
273
  const { data, variant } = useAttachmentContext();
274
+ const merge = useCn();
254
275
  const label = getAttachmentLabel(data);
255
276
  if (variant === "grid") {
256
277
  return null;
257
278
  }
258
279
  return (
259
- <div className={cn("min-w-0 flex-1", className)} {...props}>
280
+ <div className={merge("min-w-0 flex-1", className)} {...props}>
260
281
  <span className="block truncate">{label}</span>
261
282
  {showMediaType && data.mediaType && (
262
283
  <span className="block truncate text-muted-foreground text-xs">
@@ -279,6 +300,8 @@ export const AttachmentRemove = ({
279
300
  ...props
280
301
  }: AttachmentRemoveProps) => {
281
302
  const { onRemove, variant } = useAttachmentContext();
303
+ const remove = useUiConfig("Attachments")?.classes?.remove?.[variant];
304
+ const merge = useCn();
282
305
  const handleClick = (e: React.MouseEvent) => {
283
306
  e.stopPropagation();
284
307
  onRemove?.();
@@ -289,19 +312,20 @@ export const AttachmentRemove = ({
289
312
  return (
290
313
  <Button
291
314
  aria-label={label}
292
- className={cn(
293
- variant === "grid" && [
315
+ className={merge(
316
+ remove,
317
+ remove === undefined && variant === "grid" && [
294
318
  "absolute top-2 right-2 size-6 rounded-full p-0",
295
319
  cn(flatPanelSubtleClass, flatElevatedSurfaceHoverClass),
296
320
  "opacity-0 transition-opacity group-hover:opacity-100",
297
321
  "[&>svg]:size-3",
298
322
  ],
299
- variant === "inline" && [
323
+ remove === undefined && variant === "inline" && [
300
324
  "size-5 rounded p-0",
301
325
  "opacity-0 transition-opacity group-hover:opacity-100",
302
326
  "[&>svg]:size-2.5",
303
327
  ],
304
- variant === "list" && ["size-8 shrink-0 rounded p-0", "[&>svg]:size-4"],
328
+ remove === undefined && variant === "list" && ["size-8 shrink-0 rounded p-0", "[&>svg]:size-4"],
305
329
  className,
306
330
  )}
307
331
  onClick={handleClick}
@@ -338,13 +362,16 @@ export const AttachmentHoverCardContent = ({
338
362
  align = "start",
339
363
  className,
340
364
  ...props
341
- }: AttachmentHoverCardContentProps) => (
342
- <HoverCardContent
343
- align={align}
344
- className={cn("w-auto p-2", className)}
345
- {...props}
346
- />
347
- );
365
+ }: AttachmentHoverCardContentProps) => {
366
+ const merge = useCn();
367
+ return (
368
+ <HoverCardContent
369
+ align={align}
370
+ className={merge("w-auto p-2", className)}
371
+ {...props}
372
+ />
373
+ );
374
+ };
348
375
  // ============================================================================
349
376
  // AttachmentEmpty - Empty state
350
377
  // ============================================================================
@@ -353,14 +380,17 @@ export const AttachmentEmpty = ({
353
380
  className,
354
381
  children,
355
382
  ...props
356
- }: AttachmentEmptyProps) => (
357
- <div
358
- className={cn(
359
- "flex items-center justify-center p-4 text-muted-foreground text-sm",
360
- className,
361
- )}
362
- {...props}
363
- >
364
- {children ?? "No attachments"}
365
- </div>
366
- );
383
+ }: AttachmentEmptyProps) => {
384
+ const merge = useCn();
385
+ return (
386
+ <div
387
+ className={merge(
388
+ "flex items-center justify-center p-4 text-muted-foreground text-sm",
389
+ className,
390
+ )}
391
+ {...props}
392
+ >
393
+ {children ?? "No attachments"}
394
+ </div>
395
+ );
396
+ };
@@ -16,6 +16,7 @@ import {
16
16
  } from "../ui/radix-select";
17
17
  import { cn } from "../utils/cn";
18
18
  import { flatPanelSubtleClass } from "../ui/surface-classes";
19
+ import { useCn, useUiConfig } from "../ui/ui-config";
19
20
  import { IconCheck, IconCopy } from "@tabler/icons-react";
20
21
  import { createContext, useContext, useEffect, useRef, useState } from "react";
21
22
  import { bundledLanguages, createHighlighter } from "shiki";
@@ -87,9 +88,38 @@ const LineSpan = ({
87
88
  // Types
88
89
  type CodeBlockProps = HTMLAttributes<HTMLDivElement> & {
89
90
  code: string;
90
- language: string;
91
+ /** Shiki language id. Without it the code renders unhighlighted. */
92
+ language?: string;
91
93
  showLineNumbers?: boolean;
94
+ /**
95
+ * Plain text, no shiki: a `pre` with the code. `children`, when given,
96
+ * replace that `pre` (and carry their own copy button) instead of sitting
97
+ * above the highlighted body.
98
+ */
99
+ plain?: boolean;
92
100
  };
101
+ /**
102
+ * `UiConfig.CodeBlock`: `plain` default, and classes that replace the
103
+ * container, the plain-mode `pre`, the copy button and its icon. A
104
+ * `copyLabel` adds a screen-reader label to the copy button.
105
+ * `copyEveryClick` makes the copy button copy again while it still shows
106
+ * the check; by default those clicks do nothing until it resets.
107
+ */
108
+ export interface CodeBlockConfig {
109
+ plain?: boolean;
110
+ copyEveryClick?: boolean;
111
+ copyButtonSize?: ComponentProps<typeof Button>["size"];
112
+ copyLabel?: string;
113
+ classes?: {
114
+ container?: string;
115
+ pre?: string;
116
+ copyButton?: string;
117
+ /** Replaces the icon's `size={14}` with a class. */
118
+ copyIcon?: string;
119
+ };
120
+ }
121
+ const containerClasses =
122
+ "relative w-full overflow-hidden rounded-lg bg-surface-secondary/60 text-foreground";
93
123
  interface TokenizedCode {
94
124
  tokens: ThemedToken[][];
95
125
  fg: string;
@@ -261,21 +291,22 @@ export const CodeBlockContainer = ({
261
291
  ...props
262
292
  }: HTMLAttributes<HTMLDivElement> & {
263
293
  language: string;
264
- }) => (
265
- <div
266
- className={cn(
267
- "relative w-full overflow-hidden rounded-lg bg-surface-secondary/60 text-foreground",
268
- className,
269
- )}
270
- data-language={language}
271
- style={{
272
- containIntrinsicSize: "auto 200px",
273
- contentVisibility: "auto",
274
- ...style,
275
- }}
276
- {...props}
277
- />
278
- );
294
+ }) => {
295
+ const container = useUiConfig("CodeBlock")?.classes?.container;
296
+ const merge = useCn();
297
+ return (
298
+ <div
299
+ className={merge(container ?? containerClasses, className)}
300
+ data-language={language}
301
+ style={{
302
+ containIntrinsicSize: "auto 200px",
303
+ contentVisibility: "auto",
304
+ ...style,
305
+ }}
306
+ {...props}
307
+ />
308
+ );
309
+ };
279
310
  export const CodeBlockHeader = ({
280
311
  children,
281
312
  className,
@@ -363,18 +394,37 @@ export const CodeBlock = ({
363
394
  code,
364
395
  language,
365
396
  showLineNumbers = false,
397
+ plain,
366
398
  className,
367
399
  children,
368
400
  ...props
369
401
  }: CodeBlockProps) => {
402
+ const config = useUiConfig("CodeBlock");
403
+ const merge = useCn();
370
404
  const contextValue = { code };
405
+ if (plain ?? config?.plain) {
406
+ return (
407
+ <CodeBlockContext.Provider value={contextValue}>
408
+ <div
409
+ className={merge(config?.classes?.container ?? containerClasses, className)}
410
+ {...props}
411
+ >
412
+ {children ?? (
413
+ <pre className={config?.classes?.pre ?? "m-0 overflow-x-auto p-4 text-sm"}>
414
+ <code data-language={language}>{code}</code>
415
+ </pre>
416
+ )}
417
+ </div>
418
+ </CodeBlockContext.Provider>
419
+ );
420
+ }
371
421
  return (
372
422
  <CodeBlockContext.Provider value={contextValue}>
373
- <CodeBlockContainer className={className} language={language} {...props}>
423
+ <CodeBlockContainer className={className} language={language ?? "text"} {...props}>
374
424
  {children}
375
425
  <CodeBlockContent
376
426
  code={code}
377
- language={language}
427
+ language={language ?? "text"}
378
428
  showLineNumbers={showLineNumbers}
379
429
  />
380
430
  </CodeBlockContainer>
@@ -397,13 +447,16 @@ export const CodeBlockCopyButton = ({
397
447
  const [isCopied, setIsCopied] = useState(false);
398
448
  const timeoutRef = useRef<number>(0);
399
449
  const { code } = useContext(CodeBlockContext);
450
+ const config = useUiConfig("CodeBlock");
451
+ const merge = useCn();
452
+ const iconClass = config?.classes?.copyIcon;
400
453
  const copyToClipboard = async () => {
401
454
  if (typeof window === "undefined" || !navigator?.clipboard?.writeText) {
402
455
  onError?.(new Error("Clipboard API not available"));
403
456
  return;
404
457
  }
405
458
  try {
406
- if (!isCopied) {
459
+ if (!isCopied || config?.copyEveryClick) {
407
460
  await navigator.clipboard.writeText(code);
408
461
  setIsCopied(true);
409
462
  onCopy?.();
@@ -425,13 +478,14 @@ export const CodeBlockCopyButton = ({
425
478
  const Icon = isCopied ? IconCheck : IconCopy;
426
479
  return (
427
480
  <Button
428
- className={cn("shrink-0", className)}
481
+ className={merge(config?.classes?.copyButton ?? "shrink-0", className)}
429
482
  onClick={copyToClipboard}
430
- size="icon"
483
+ size={config?.copyButtonSize ?? "icon"}
431
484
  variant="ghost"
432
485
  {...props}
433
486
  >
434
- {children ?? <Icon size={14} />}
487
+ {children ?? (iconClass === undefined ? <Icon size={14} /> : <Icon className={iconClass} />)}
488
+ {config?.copyLabel ? <span className="sr-only">{config.copyLabel}</span> : null}
435
489
  </Button>
436
490
  );
437
491
  };