@vvedantb/ui 0.17.1 → 0.18.0
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.
- package/README.md +251 -3
- package/package.json +13 -5
- package/shell/src/SidebarResize.tsx +1 -4
- package/src/ai-elements/__snapshots__/queue.test.tsx.snap +11 -0
- package/src/ai-elements/activity-question-utils.ts +26 -0
- package/src/ai-elements/activity-question.tsx +159 -0
- package/src/ai-elements/activity-shared.tsx +459 -0
- package/src/ai-elements/activity-step-detail.tsx +115 -0
- package/src/ai-elements/activity-step-label.ts +981 -0
- package/src/ai-elements/activity-tasks-utils.ts +199 -0
- package/src/ai-elements/activity-tasks.tsx +770 -0
- package/src/ai-elements/artifact.tsx +162 -52
- package/src/ai-elements/attachments.tsx +62 -32
- package/src/ai-elements/code-block.tsx +76 -22
- package/src/ai-elements/context.tsx +370 -0
- package/src/ai-elements/conversation.tsx +186 -62
- package/src/ai-elements/file-chip.tsx +53 -0
- package/src/ai-elements/message.tsx +161 -50
- package/src/ai-elements/model-picker-content.tsx +505 -0
- package/src/ai-elements/model-picker-types.ts +56 -0
- package/src/ai-elements/model-picker.tsx +301 -0
- package/src/ai-elements/model-selector.tsx +102 -21
- package/src/ai-elements/plan.tsx +130 -0
- package/src/ai-elements/prompt-input-settings.tsx +144 -0
- package/src/ai-elements/prompt-input-speech.tsx +189 -0
- package/src/ai-elements/prompt-input-traits.tsx +281 -0
- package/src/ai-elements/prompt-input.tsx +320 -134
- package/src/ai-elements/provider-icon.tsx +133 -0
- package/src/ai-elements/queue.tsx +293 -9
- package/src/ai-elements/shimmer.tsx +72 -1
- package/src/ai-elements/simple-model-ladder.tsx +108 -0
- package/src/ai-elements/test-results.tsx +510 -0
- package/src/ai-elements/trace.tsx +243 -0
- package/src/ai-elements/use-streaming-disclosure.ts +50 -0
- package/src/ai-elements/web-preview.tsx +349 -0
- package/src/composer/input-area/ComposerSlashMenu.tsx +1 -1
- package/src/index.ts +66 -12
- package/src/kibo/avatar-stack.tsx +40 -0
- package/src/kibo/contribution-graph.tsx +539 -0
- package/src/kibo/cursor.tsx +72 -0
- package/src/kibo/gantt-features.tsx +439 -0
- package/src/kibo/gantt-header.tsx +181 -0
- package/src/kibo/gantt-provider.tsx +657 -0
- package/src/kibo/gantt-sidebar.tsx +157 -0
- package/src/kibo/gantt-timeline.tsx +154 -0
- package/src/kibo/index.ts +151 -0
- package/src/kibo/kanban.tsx +338 -0
- package/src/kibo/kanbanDropTarget.ts +25 -0
- package/src/kibo/list.tsx +162 -0
- package/src/kibo/theme-switcher.tsx +109 -0
- package/src/kibo/video-player.tsx +237 -0
- package/src/markdown/Markdown.tsx +112 -0
- package/src/markdown/MarkdownBlockquote.tsx +45 -0
- package/src/markdown/MarkdownCodeBlock.tsx +197 -0
- package/src/markdown/MarkdownInlineCode.tsx +40 -0
- package/src/markdown/MarkdownLink.tsx +92 -0
- package/src/markdown/codeFence.ts +57 -0
- package/src/markdown/codeHighlight.ts +138 -0
- package/src/markdown/incrementalBlocks.ts +182 -0
- package/src/markdown/index.ts +3 -0
- package/src/markdown/markdownClipboard.ts +178 -0
- package/src/markdown/mermaid.ts +155 -0
- package/src/markdown/rehypeGithubAlerts.ts +80 -0
- package/src/motion/presets.ts +24 -0
- package/src/ui/__snapshots__/alt-wrappers.test.tsx.snap +19 -0
- package/src/ui/__snapshots__/sonner-dom.test.tsx.snap +5 -0
- package/src/ui/_menu-parts.tsx +29 -0
- package/src/ui/accordion.alt.tsx +46 -53
- package/src/ui/accordion.tsx +59 -10
- package/src/ui/avatar.tsx +56 -0
- package/src/ui/badge.tsx +45 -21
- package/src/ui/border-beam.tsx +97 -0
- package/src/ui/button-group.tsx +50 -21
- package/src/ui/button.tsx +67 -45
- package/src/ui/calendar.tsx +48 -5
- package/src/ui/card.tsx +77 -8
- package/src/ui/carousel.tsx +60 -0
- package/src/ui/centered-spinner.tsx +30 -0
- package/src/ui/checkbox.tsx +34 -16
- package/src/ui/clear-input.alt.tsx +33 -75
- package/src/ui/clear-input.tsx +140 -15
- package/src/ui/collapsible.tsx +40 -2
- package/src/ui/command.tsx +143 -70
- package/src/ui/context-menu.tsx +159 -93
- package/src/ui/crossfade-icon.tsx +123 -0
- package/src/ui/dialog.tsx +154 -70
- package/src/ui/dropdown-menu.tsx +177 -117
- package/src/ui/hover-card.tsx +59 -17
- package/src/ui/input-group.alt.tsx +83 -83
- package/src/ui/input-group.tsx +129 -58
- package/src/ui/input.tsx +13 -4
- package/src/ui/label.tsx +11 -8
- package/src/ui/list-row.tsx +232 -0
- package/src/ui/loading-state.tsx +215 -0
- package/src/ui/popover.tsx +56 -23
- package/src/ui/progress.tsx +28 -16
- package/src/ui/refresh-spin-icon.tsx +41 -0
- package/src/ui/search-input.tsx +66 -15
- package/src/ui/select.tsx +171 -100
- package/src/ui/separator.tsx +23 -14
- package/src/ui/slider.tsx +21 -7
- package/src/ui/sonner.tsx +68 -30
- package/src/ui/spinner.tsx +229 -11
- package/src/ui/status-dot.tsx +84 -0
- package/src/ui/surface.tsx +49 -0
- package/src/ui/switch.tsx +29 -15
- package/src/ui/table.tsx +38 -16
- package/src/ui/tabs.tsx +123 -35
- package/src/ui/tabsSliding.ts +33 -1
- package/src/ui/textarea.tsx +13 -4
- package/src/ui/tooltip.tsx +50 -14
- package/src/ui/ui-config.tsx +149 -0
- package/src/utils/cat-color.ts +123 -0
- package/src/utils/dayjs.ts +6 -0
- package/src/utils/gesture.ts +59 -0
- package/src/utils/runtimeVisibility.ts +167 -0
- package/src/utils/sharedClock.ts +96 -0
- package/src/utils/surface-radius.ts +16 -0
- package/src/utils/useDragSensors.ts +58 -0
- package/tokens/examples/tokens-moved/Fixture.tsx +283 -0
- package/tokens/examples/tokens-moved/README.md +20 -0
- package/tokens/examples/tokens-moved/index.html +12 -0
- package/tokens/examples/tokens-moved/main.tsx +6 -0
- package/tokens/examples/tokens-moved/package.json +14 -0
- package/tokens/examples/tokens-moved/styles.css +12 -0
- package/tokens/examples/tokens-moved/tsconfig.json +14 -0
- package/tokens/examples/tokens-moved/vite.config.ts +11 -0
- package/tokens/src/css/border-beam.css +116 -0
- 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 {
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
63
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
}:
|
|
76
|
-
<
|
|
77
|
-
<
|
|
78
|
-
|
|
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
|
-
|
|
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={
|
|
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={
|
|
195
|
+
className={merge(
|
|
180
196
|
"group relative",
|
|
181
|
-
|
|
182
|
-
variant === "
|
|
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={
|
|
248
|
+
className={merge(
|
|
230
249
|
"flex shrink-0 items-center justify-center overflow-hidden",
|
|
231
|
-
|
|
232
|
-
variant === "
|
|
233
|
-
variant === "
|
|
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={
|
|
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={
|
|
293
|
-
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
className
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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={
|
|
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
|
};
|