@springbrand/message-panel 0.1.2 → 0.1.3-alpha.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/demo/camel-chat-showcase.tsx +1265 -0
- package/demo/fixtures.ts +292 -0
- package/demo/message-panel-chat-showcase.tsx +250 -0
- package/demo/message-panel-gallery.tsx +389 -0
- package/package.json +6 -16
- package/src/adapters/ai-sdk.ts +32 -0
- package/src/camel/camel-chat-messages.tsx +1423 -0
- package/src/camel/camel-model-select.tsx +88 -0
- package/src/camel/camel-plan-bar.tsx +115 -0
- package/src/camel/camel-prompt-input.tsx +547 -0
- package/src/camel/camel-tool-presentation.tsx +1270 -0
- package/src/camel/camel-turn.ts +316 -0
- package/src/camel/turn-summary-bar.tsx +81 -0
- package/src/chat-summary-panel.tsx +229 -0
- package/src/composer/chat-composer.tsx +216 -0
- package/src/composer/composer-attachments.tsx +138 -0
- package/src/composer/composer-trigger-popover.tsx +236 -0
- package/src/composer/composer.tsx +425 -0
- package/src/composer/drop-controller.ts +80 -0
- package/src/composer/types.ts +79 -0
- package/src/contracts.ts +138 -0
- package/src/conversation.tsx +76 -0
- package/src/internal/cn.ts +3 -0
- package/src/message-panel.tsx +368 -0
- package/src/message.tsx +254 -0
- package/src/parts/index.tsx +1149 -0
- package/src/parts/kind.ts +67 -0
- package/src/parts/plan.ts +63 -0
- package/src/parts/primitives.tsx +439 -0
- package/src/registry.ts +44 -0
- package/src/status.tsx +132 -0
- package/dist/demo/camel-chat-showcase.d.ts +0 -1
- package/dist/demo/camel-chat-showcase.js +0 -1007
- package/dist/demo/fixtures.d.ts +0 -32
- package/dist/demo/fixtures.js +0 -250
- package/dist/demo/index.js +0 -4
- package/dist/demo/message-panel-chat-showcase.d.ts +0 -1
- package/dist/demo/message-panel-chat-showcase.js +0 -114
- package/dist/demo/message-panel-gallery.d.ts +0 -1
- package/dist/demo/message-panel-gallery.js +0 -130
- package/dist/src/adapters/ai-sdk.d.ts +0 -4
- package/dist/src/adapters/ai-sdk.js +0 -25
- package/dist/src/camel/camel-chat-messages.d.ts +0 -44
- package/dist/src/camel/camel-chat-messages.js +0 -421
- package/dist/src/camel/camel-model-select.d.ts +0 -13
- package/dist/src/camel/camel-model-select.js +0 -7
- package/dist/src/camel/camel-plan-bar.d.ts +0 -5
- package/dist/src/camel/camel-plan-bar.js +0 -24
- package/dist/src/camel/camel-prompt-input.d.ts +0 -32
- package/dist/src/camel/camel-prompt-input.js +0 -182
- package/dist/src/camel/camel-tool-presentation.d.ts +0 -21
- package/dist/src/camel/camel-tool-presentation.js +0 -784
- package/dist/src/camel/camel-turn.d.ts +0 -57
- package/dist/src/camel/camel-turn.js +0 -203
- package/dist/src/camel/index.js +0 -7
- package/dist/src/camel/turn-summary-bar.d.ts +0 -8
- package/dist/src/camel/turn-summary-bar.js +0 -12
- package/dist/src/chat-summary-panel.d.ts +0 -22
- package/dist/src/chat-summary-panel.js +0 -26
- package/dist/src/composer/chat-composer.d.ts +0 -4
- package/dist/src/composer/chat-composer.js +0 -90
- package/dist/src/composer/composer-attachments.d.ts +0 -13
- package/dist/src/composer/composer-attachments.js +0 -47
- package/dist/src/composer/composer-trigger-popover.d.ts +0 -31
- package/dist/src/composer/composer-trigger-popover.js +0 -29
- package/dist/src/composer/composer.d.ts +0 -15
- package/dist/src/composer/composer.js +0 -180
- package/dist/src/composer/drop-controller.d.ts +0 -8
- package/dist/src/composer/drop-controller.js +0 -69
- package/dist/src/composer/index.js +0 -6
- package/dist/src/composer/types.d.ts +0 -62
- package/dist/src/composer/types.js +0 -1
- package/dist/src/contracts.d.ts +0 -94
- package/dist/src/contracts.js +0 -1
- package/dist/src/conversation.d.ts +0 -13
- package/dist/src/conversation.js +0 -18
- package/dist/src/index.js +0 -12
- package/dist/src/internal/cn.d.ts +0 -1
- package/dist/src/internal/cn.js +0 -3
- package/dist/src/message-panel.d.ts +0 -4
- package/dist/src/message-panel.js +0 -144
- package/dist/src/message.d.ts +0 -36
- package/dist/src/message.js +0 -83
- package/dist/src/parts/index.d.ts +0 -187
- package/dist/src/parts/index.js +0 -332
- package/dist/src/parts/kind.d.ts +0 -3
- package/dist/src/parts/kind.js +0 -69
- package/dist/src/parts/plan.d.ts +0 -10
- package/dist/src/parts/plan.js +0 -40
- package/dist/src/parts/primitives.d.ts +0 -70
- package/dist/src/parts/primitives.js +0 -153
- package/dist/src/registry.d.ts +0 -4
- package/dist/src/registry.js +0 -31
- package/dist/src/status.d.ts +0 -26
- package/dist/src/status.js +0 -74
- /package/{dist/demo/index.d.ts → demo/index.ts} +0 -0
- /package/{dist/src/camel/index.d.ts → src/camel/index.ts} +0 -0
- /package/{dist/src/composer/index.d.ts → src/composer/index.ts} +0 -0
- /package/{dist/src/index.d.ts → src/index.ts} +0 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PlusIcon,
|
|
3
|
+
SparklesIcon,
|
|
4
|
+
} from "lucide-react";
|
|
5
|
+
import {
|
|
6
|
+
forwardRef,
|
|
7
|
+
useEffect,
|
|
8
|
+
useMemo,
|
|
9
|
+
useState,
|
|
10
|
+
type KeyboardEvent,
|
|
11
|
+
} from "react";
|
|
12
|
+
import { cn } from "../internal/cn";
|
|
13
|
+
import { Composer } from "./composer";
|
|
14
|
+
import type {
|
|
15
|
+
ChatComposerProps,
|
|
16
|
+
ComposerCommand,
|
|
17
|
+
ComposerToolbarAction,
|
|
18
|
+
} from "./types";
|
|
19
|
+
|
|
20
|
+
const toolbarIcons = {
|
|
21
|
+
plus: PlusIcon,
|
|
22
|
+
sparkles: SparklesIcon,
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
function ToolbarAction({ action }: { action: ComposerToolbarAction }) {
|
|
26
|
+
const Icon = action.icon ? toolbarIcons[action.icon] : undefined;
|
|
27
|
+
return (
|
|
28
|
+
<button
|
|
29
|
+
type="button"
|
|
30
|
+
className="sb-chat-composer__action"
|
|
31
|
+
disabled={action.disabled}
|
|
32
|
+
onClick={action.onSelect}
|
|
33
|
+
>
|
|
34
|
+
{Icon && <Icon size={16} />}
|
|
35
|
+
<span>{action.label}</span>
|
|
36
|
+
</button>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function CommandMenu({
|
|
41
|
+
commands,
|
|
42
|
+
active,
|
|
43
|
+
label,
|
|
44
|
+
onActiveChange,
|
|
45
|
+
onPick,
|
|
46
|
+
}: {
|
|
47
|
+
commands: readonly ComposerCommand[];
|
|
48
|
+
active: number;
|
|
49
|
+
label: string;
|
|
50
|
+
onActiveChange: (index: number) => void;
|
|
51
|
+
onPick: (command: ComposerCommand) => void;
|
|
52
|
+
}) {
|
|
53
|
+
return (
|
|
54
|
+
<div
|
|
55
|
+
className="sb-command-menu"
|
|
56
|
+
role="listbox"
|
|
57
|
+
aria-label={label}
|
|
58
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
59
|
+
>
|
|
60
|
+
<div className="sb-command-menu__label">{label}</div>
|
|
61
|
+
<div className="sb-command-menu__list">
|
|
62
|
+
{commands.map((command, index) => (
|
|
63
|
+
<button
|
|
64
|
+
key={command.name}
|
|
65
|
+
type="button"
|
|
66
|
+
role="option"
|
|
67
|
+
aria-selected={index === active}
|
|
68
|
+
onMouseEnter={() => onActiveChange(index)}
|
|
69
|
+
onClick={() => onPick(command)}
|
|
70
|
+
>
|
|
71
|
+
<code>/{command.name}</code>
|
|
72
|
+
<span>{command.description}</span>
|
|
73
|
+
</button>
|
|
74
|
+
))}
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function ChatComposerInner<TAttachment>(
|
|
81
|
+
{
|
|
82
|
+
value,
|
|
83
|
+
defaultValue = "",
|
|
84
|
+
onValueChange,
|
|
85
|
+
commands = [],
|
|
86
|
+
toolbarActions = [],
|
|
87
|
+
variant = "docked",
|
|
88
|
+
commandMenuLabel = "命令(↑↓ 选择,回车填入)",
|
|
89
|
+
onCommandPick,
|
|
90
|
+
onSubmit,
|
|
91
|
+
tools,
|
|
92
|
+
textareaProps,
|
|
93
|
+
className,
|
|
94
|
+
...props
|
|
95
|
+
}: ChatComposerProps<TAttachment>,
|
|
96
|
+
ref: React.ForwardedRef<HTMLDivElement>,
|
|
97
|
+
) {
|
|
98
|
+
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
99
|
+
const resolvedValue = value ?? internalValue;
|
|
100
|
+
const [dismissed, setDismissed] = useState(false);
|
|
101
|
+
const [active, setActive] = useState(0);
|
|
102
|
+
const query =
|
|
103
|
+
resolvedValue.startsWith("/") && !resolvedValue.includes("\n")
|
|
104
|
+
? resolvedValue.slice(1).trim().toLowerCase()
|
|
105
|
+
: null;
|
|
106
|
+
const filtered = useMemo(() => {
|
|
107
|
+
if (query === null) return [];
|
|
108
|
+
if (!query) return [...commands];
|
|
109
|
+
return commands.filter(
|
|
110
|
+
(command) =>
|
|
111
|
+
command.name.toLowerCase().includes(query) ||
|
|
112
|
+
command.description.toLowerCase().includes(query),
|
|
113
|
+
);
|
|
114
|
+
}, [commands, query]);
|
|
115
|
+
const menuOpen = !dismissed && query !== null && filtered.length > 0;
|
|
116
|
+
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
setDismissed(false);
|
|
119
|
+
}, [resolvedValue]);
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (active >= filtered.length) setActive(0);
|
|
123
|
+
}, [active, filtered.length]);
|
|
124
|
+
|
|
125
|
+
const changeValue = (next: string) => {
|
|
126
|
+
if (value === undefined) setInternalValue(next);
|
|
127
|
+
onValueChange?.(next);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const pick = (command: ComposerCommand) => {
|
|
131
|
+
changeValue(command.prompt);
|
|
132
|
+
setDismissed(true);
|
|
133
|
+
onCommandPick?.(command);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
137
|
+
textareaProps?.onKeyDown?.(event);
|
|
138
|
+
if (event.defaultPrevented || !menuOpen) return;
|
|
139
|
+
if (event.key === "Escape") {
|
|
140
|
+
event.preventDefault();
|
|
141
|
+
setDismissed(true);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
145
|
+
event.preventDefault();
|
|
146
|
+
const delta = event.key === "ArrowDown" ? 1 : -1;
|
|
147
|
+
setActive(
|
|
148
|
+
(current) => (current + delta + filtered.length) % filtered.length,
|
|
149
|
+
);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (
|
|
153
|
+
(event.key === "Enter" &&
|
|
154
|
+
!event.shiftKey &&
|
|
155
|
+
!event.nativeEvent.isComposing) ||
|
|
156
|
+
event.key === "Tab"
|
|
157
|
+
) {
|
|
158
|
+
const command = filtered[active] ?? filtered[0];
|
|
159
|
+
if (command) {
|
|
160
|
+
event.preventDefault();
|
|
161
|
+
pick(command);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<div
|
|
168
|
+
ref={ref}
|
|
169
|
+
className={cn("sb-chat-composer", className)}
|
|
170
|
+
data-variant={variant}
|
|
171
|
+
>
|
|
172
|
+
<div className="sb-chat-composer__inner">
|
|
173
|
+
{menuOpen && (
|
|
174
|
+
<CommandMenu
|
|
175
|
+
commands={filtered}
|
|
176
|
+
active={active}
|
|
177
|
+
label={commandMenuLabel}
|
|
178
|
+
onActiveChange={setActive}
|
|
179
|
+
onPick={pick}
|
|
180
|
+
/>
|
|
181
|
+
)}
|
|
182
|
+
<Composer
|
|
183
|
+
{...props}
|
|
184
|
+
value={resolvedValue}
|
|
185
|
+
onValueChange={changeValue}
|
|
186
|
+
onSubmit={async (message) => {
|
|
187
|
+
await onSubmit(message);
|
|
188
|
+
if (value === undefined) setInternalValue("");
|
|
189
|
+
}}
|
|
190
|
+
tools={
|
|
191
|
+
<>
|
|
192
|
+
{tools}
|
|
193
|
+
{toolbarActions.map((action) => (
|
|
194
|
+
<ToolbarAction key={action.id} action={action} />
|
|
195
|
+
))}
|
|
196
|
+
</>
|
|
197
|
+
}
|
|
198
|
+
textareaProps={{
|
|
199
|
+
...textareaProps,
|
|
200
|
+
onKeyDown: handleKeyDown,
|
|
201
|
+
onBlur: (event) => {
|
|
202
|
+
textareaProps?.onBlur?.(event);
|
|
203
|
+
setDismissed(true);
|
|
204
|
+
},
|
|
205
|
+
}}
|
|
206
|
+
/>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export const ChatComposer = forwardRef(ChatComposerInner) as <TAttachment>(
|
|
213
|
+
props: ChatComposerProps<TAttachment> & {
|
|
214
|
+
ref?: React.ForwardedRef<HTMLDivElement>;
|
|
215
|
+
},
|
|
216
|
+
) => React.ReactElement;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ArrowLeftIcon,
|
|
3
|
+
ArrowRightIcon,
|
|
4
|
+
FileAudioIcon,
|
|
5
|
+
FileCodeIcon,
|
|
6
|
+
FileImageIcon,
|
|
7
|
+
FileSpreadsheetIcon,
|
|
8
|
+
FileTextIcon,
|
|
9
|
+
Loader2Icon,
|
|
10
|
+
RotateCcwIcon,
|
|
11
|
+
XIcon,
|
|
12
|
+
} from "lucide-react";
|
|
13
|
+
import {
|
|
14
|
+
forwardRef,
|
|
15
|
+
type ButtonHTMLAttributes,
|
|
16
|
+
type HTMLAttributes,
|
|
17
|
+
type ReactNode,
|
|
18
|
+
} from "react";
|
|
19
|
+
import { cn } from "../internal/cn";
|
|
20
|
+
import type { ComposerAttachmentView } from "./types";
|
|
21
|
+
|
|
22
|
+
function formatSize(bytes?: number): string {
|
|
23
|
+
if (!bytes) return "";
|
|
24
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
25
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
26
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function attachmentIcon(attachment: ComposerAttachmentView) {
|
|
30
|
+
const value = `${attachment.mediaType ?? ""} ${attachment.name}`;
|
|
31
|
+
if (/image/i.test(value)) return FileImageIcon;
|
|
32
|
+
if (/audio/i.test(value)) return FileAudioIcon;
|
|
33
|
+
if (/csv|sheet|excel/i.test(value)) return FileSpreadsheetIcon;
|
|
34
|
+
if (/code|javascript|typescript|json|html|xml/i.test(value)) return FileCodeIcon;
|
|
35
|
+
return FileTextIcon;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const Attachments = forwardRef<
|
|
39
|
+
HTMLDivElement,
|
|
40
|
+
HTMLAttributes<HTMLDivElement>
|
|
41
|
+
>(({ className, ...props }, ref) => (
|
|
42
|
+
<div ref={ref} className={cn("sb-attachments", className)} {...props} />
|
|
43
|
+
));
|
|
44
|
+
Attachments.displayName = "Attachments";
|
|
45
|
+
|
|
46
|
+
export interface AttachmentPreviewProps extends HTMLAttributes<HTMLDivElement> {
|
|
47
|
+
attachment: ComposerAttachmentView;
|
|
48
|
+
actions?: ReactNode;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const AttachmentPreview = forwardRef<
|
|
52
|
+
HTMLDivElement,
|
|
53
|
+
AttachmentPreviewProps
|
|
54
|
+
>(({ attachment, actions, className, ...props }, ref) => {
|
|
55
|
+
const Icon = attachmentIcon(attachment);
|
|
56
|
+
const isImage =
|
|
57
|
+
attachment.mediaType?.startsWith("image/") && Boolean(attachment.url);
|
|
58
|
+
const subtitle =
|
|
59
|
+
attachment.status === "uploading"
|
|
60
|
+
? "上传中…"
|
|
61
|
+
: attachment.status === "error"
|
|
62
|
+
? "上传失败"
|
|
63
|
+
: [attachment.note, formatSize(attachment.size)].filter(Boolean).join(" · ");
|
|
64
|
+
return (
|
|
65
|
+
<div
|
|
66
|
+
ref={ref}
|
|
67
|
+
className={cn("sb-attachment", className)}
|
|
68
|
+
data-status={attachment.status}
|
|
69
|
+
{...props}
|
|
70
|
+
>
|
|
71
|
+
<span className="sb-attachment__preview">
|
|
72
|
+
{attachment.status === "uploading" ? (
|
|
73
|
+
<Loader2Icon size={18} className="sb-spin" />
|
|
74
|
+
) : isImage ? (
|
|
75
|
+
<img src={attachment.url} alt="" />
|
|
76
|
+
) : (
|
|
77
|
+
<Icon size={20} />
|
|
78
|
+
)}
|
|
79
|
+
</span>
|
|
80
|
+
<span className="sb-attachment__text">
|
|
81
|
+
<strong title={attachment.name}>{attachment.name}</strong>
|
|
82
|
+
<small>{subtitle}</small>
|
|
83
|
+
</span>
|
|
84
|
+
{actions && <span className="sb-attachment__actions">{actions}</span>}
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
AttachmentPreview.displayName = "AttachmentPreview";
|
|
89
|
+
|
|
90
|
+
export const AttachmentRemove = forwardRef<
|
|
91
|
+
HTMLButtonElement,
|
|
92
|
+
ButtonHTMLAttributes<HTMLButtonElement>
|
|
93
|
+
>(({ className, children, ...props }, ref) => (
|
|
94
|
+
<button
|
|
95
|
+
ref={ref}
|
|
96
|
+
type="button"
|
|
97
|
+
className={cn("sb-attachment__button", className)}
|
|
98
|
+
aria-label="移除附件"
|
|
99
|
+
{...props}
|
|
100
|
+
>
|
|
101
|
+
{children ?? <XIcon size={13} />}
|
|
102
|
+
</button>
|
|
103
|
+
));
|
|
104
|
+
AttachmentRemove.displayName = "AttachmentRemove";
|
|
105
|
+
|
|
106
|
+
export function AttachmentRetry(
|
|
107
|
+
props: ButtonHTMLAttributes<HTMLButtonElement>,
|
|
108
|
+
) {
|
|
109
|
+
return (
|
|
110
|
+
<button
|
|
111
|
+
type="button"
|
|
112
|
+
className={cn("sb-attachment__button", props.className)}
|
|
113
|
+
aria-label="重试上传"
|
|
114
|
+
{...props}
|
|
115
|
+
>
|
|
116
|
+
<RotateCcwIcon size={13} />
|
|
117
|
+
</button>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function AttachmentReorder({
|
|
122
|
+
direction,
|
|
123
|
+
...props
|
|
124
|
+
}: ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
125
|
+
direction: "left" | "right";
|
|
126
|
+
}) {
|
|
127
|
+
const Icon = direction === "left" ? ArrowLeftIcon : ArrowRightIcon;
|
|
128
|
+
return (
|
|
129
|
+
<button
|
|
130
|
+
type="button"
|
|
131
|
+
className={cn("sb-attachment__button", props.className)}
|
|
132
|
+
aria-label={direction === "left" ? "附件左移" : "附件右移"}
|
|
133
|
+
{...props}
|
|
134
|
+
>
|
|
135
|
+
<Icon size={13} />
|
|
136
|
+
</button>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ComposerPrimitive,
|
|
3
|
+
unstable_defaultDirectiveFormatter,
|
|
4
|
+
unstable_useTriggerPopoverScopeContext,
|
|
5
|
+
type Unstable_DirectiveFormatter,
|
|
6
|
+
type Unstable_TriggerItem,
|
|
7
|
+
} from "@assistant-ui/react";
|
|
8
|
+
import {
|
|
9
|
+
ChevronLeftIcon,
|
|
10
|
+
ChevronRightIcon,
|
|
11
|
+
TerminalIcon,
|
|
12
|
+
} from "lucide-react";
|
|
13
|
+
import {
|
|
14
|
+
memo,
|
|
15
|
+
type ComponentPropsWithoutRef,
|
|
16
|
+
type FC,
|
|
17
|
+
} from "react";
|
|
18
|
+
import { cn } from "../internal/cn";
|
|
19
|
+
|
|
20
|
+
type IconComponent = FC<{ className?: string }>;
|
|
21
|
+
|
|
22
|
+
type DirectiveBehaviorProps = {
|
|
23
|
+
formatter?: Unstable_DirectiveFormatter;
|
|
24
|
+
onInserted?: (item: Unstable_TriggerItem) => void;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type ActionBehaviorProps = {
|
|
28
|
+
formatter?: Unstable_DirectiveFormatter;
|
|
29
|
+
onExecute: (item: Unstable_TriggerItem) => void;
|
|
30
|
+
removeOnExecute?: boolean;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type ComposerTriggerPopoverBaseProps = Omit<
|
|
34
|
+
ComponentPropsWithoutRef<
|
|
35
|
+
typeof ComposerPrimitive.Unstable_TriggerPopover
|
|
36
|
+
>,
|
|
37
|
+
"children"
|
|
38
|
+
> & {
|
|
39
|
+
iconMap?: Record<string, IconComponent>;
|
|
40
|
+
fallbackIcon?: IconComponent;
|
|
41
|
+
backLabel?: string;
|
|
42
|
+
emptyCategoriesLabel?: string;
|
|
43
|
+
emptyItemsLabel?: string;
|
|
44
|
+
loadingLabel?: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type ComposerTriggerPopoverProps =
|
|
48
|
+
ComposerTriggerPopoverBaseProps &
|
|
49
|
+
(
|
|
50
|
+
| {
|
|
51
|
+
directive: DirectiveBehaviorProps;
|
|
52
|
+
action?: never;
|
|
53
|
+
}
|
|
54
|
+
| {
|
|
55
|
+
action: ActionBehaviorProps;
|
|
56
|
+
directive?: never;
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
function resolveIcon(
|
|
61
|
+
iconKey: string | undefined,
|
|
62
|
+
iconMap: Record<string, IconComponent> | undefined,
|
|
63
|
+
fallback: IconComponent,
|
|
64
|
+
): IconComponent {
|
|
65
|
+
return (iconKey && iconMap?.[iconKey]) || fallback;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function Categories({
|
|
69
|
+
iconMap,
|
|
70
|
+
fallbackIcon,
|
|
71
|
+
emptyLabel,
|
|
72
|
+
}: {
|
|
73
|
+
iconMap?: Record<string, IconComponent>;
|
|
74
|
+
fallbackIcon: IconComponent;
|
|
75
|
+
emptyLabel: string;
|
|
76
|
+
}) {
|
|
77
|
+
return (
|
|
78
|
+
<ComposerPrimitive.Unstable_TriggerPopoverCategories>
|
|
79
|
+
{(categories) => (
|
|
80
|
+
<div className="flex flex-col py-1">
|
|
81
|
+
{categories.map((category) => {
|
|
82
|
+
const Icon = resolveIcon(
|
|
83
|
+
category.id,
|
|
84
|
+
iconMap,
|
|
85
|
+
fallbackIcon,
|
|
86
|
+
);
|
|
87
|
+
return (
|
|
88
|
+
<ComposerPrimitive.Unstable_TriggerPopoverCategoryItem
|
|
89
|
+
key={category.id}
|
|
90
|
+
categoryId={category.id}
|
|
91
|
+
className="flex cursor-pointer items-center justify-between gap-2 px-3 py-2 text-sm outline-none transition-colors hover:bg-accent focus:bg-accent data-[highlighted]:bg-accent"
|
|
92
|
+
>
|
|
93
|
+
<span className="flex items-center gap-2">
|
|
94
|
+
<Icon className="size-4 text-muted-foreground" />
|
|
95
|
+
{category.label}
|
|
96
|
+
</span>
|
|
97
|
+
<ChevronRightIcon className="size-4 text-muted-foreground" />
|
|
98
|
+
</ComposerPrimitive.Unstable_TriggerPopoverCategoryItem>
|
|
99
|
+
);
|
|
100
|
+
})}
|
|
101
|
+
{categories.length === 0 && (
|
|
102
|
+
<div className="px-3 py-2 text-sm text-muted-foreground">
|
|
103
|
+
{emptyLabel}
|
|
104
|
+
</div>
|
|
105
|
+
)}
|
|
106
|
+
</div>
|
|
107
|
+
)}
|
|
108
|
+
</ComposerPrimitive.Unstable_TriggerPopoverCategories>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function Items({
|
|
113
|
+
iconMap,
|
|
114
|
+
fallbackIcon,
|
|
115
|
+
backLabel,
|
|
116
|
+
emptyLabel,
|
|
117
|
+
loadingLabel,
|
|
118
|
+
}: {
|
|
119
|
+
iconMap?: Record<string, IconComponent>;
|
|
120
|
+
fallbackIcon: IconComponent;
|
|
121
|
+
backLabel: string;
|
|
122
|
+
emptyLabel: string;
|
|
123
|
+
loadingLabel: string;
|
|
124
|
+
}) {
|
|
125
|
+
const { isLoading } =
|
|
126
|
+
unstable_useTriggerPopoverScopeContext();
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<ComposerPrimitive.Unstable_TriggerPopoverItems>
|
|
130
|
+
{(items) => (
|
|
131
|
+
<div className="flex flex-col">
|
|
132
|
+
<ComposerPrimitive.Unstable_TriggerPopoverBack className="flex cursor-pointer items-center gap-1.5 border-b px-3 py-2 text-xs uppercase tracking-wide text-muted-foreground transition-colors hover:bg-accent">
|
|
133
|
+
<ChevronLeftIcon className="size-3.5" />
|
|
134
|
+
{backLabel}
|
|
135
|
+
</ComposerPrimitive.Unstable_TriggerPopoverBack>
|
|
136
|
+
<div className="py-1">
|
|
137
|
+
{items.map((item, index) => {
|
|
138
|
+
const iconKey =
|
|
139
|
+
typeof item.metadata?.icon === "string"
|
|
140
|
+
? item.metadata.icon
|
|
141
|
+
: undefined;
|
|
142
|
+
const Icon = resolveIcon(
|
|
143
|
+
iconKey,
|
|
144
|
+
iconMap,
|
|
145
|
+
fallbackIcon,
|
|
146
|
+
);
|
|
147
|
+
return (
|
|
148
|
+
<ComposerPrimitive.Unstable_TriggerPopoverItem
|
|
149
|
+
key={item.id}
|
|
150
|
+
item={item}
|
|
151
|
+
index={index}
|
|
152
|
+
className="flex w-full cursor-pointer flex-col items-start gap-0.5 px-3 py-2 text-start outline-none transition-colors hover:bg-accent focus:bg-accent data-[highlighted]:bg-accent"
|
|
153
|
+
>
|
|
154
|
+
<span className="flex items-center gap-2 text-sm font-medium">
|
|
155
|
+
<Icon className="size-3.5 text-primary" />
|
|
156
|
+
{item.label}
|
|
157
|
+
</span>
|
|
158
|
+
{item.description && (
|
|
159
|
+
<span className="ms-5.5 text-xs leading-tight text-muted-foreground">
|
|
160
|
+
{item.description}
|
|
161
|
+
</span>
|
|
162
|
+
)}
|
|
163
|
+
</ComposerPrimitive.Unstable_TriggerPopoverItem>
|
|
164
|
+
);
|
|
165
|
+
})}
|
|
166
|
+
{items.length === 0 && (
|
|
167
|
+
<div className="px-3 py-2 text-sm text-muted-foreground">
|
|
168
|
+
{isLoading ? loadingLabel : emptyLabel}
|
|
169
|
+
</div>
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
)}
|
|
174
|
+
</ComposerPrimitive.Unstable_TriggerPopoverItems>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const ComposerTriggerPopoverImpl: FC<
|
|
179
|
+
ComposerTriggerPopoverProps
|
|
180
|
+
> = ({
|
|
181
|
+
iconMap,
|
|
182
|
+
fallbackIcon = TerminalIcon,
|
|
183
|
+
backLabel = "Back",
|
|
184
|
+
emptyCategoriesLabel = "No items available",
|
|
185
|
+
emptyItemsLabel = "No matching items",
|
|
186
|
+
loadingLabel = "Loading…",
|
|
187
|
+
className,
|
|
188
|
+
directive,
|
|
189
|
+
action,
|
|
190
|
+
...props
|
|
191
|
+
}) => (
|
|
192
|
+
<ComposerPrimitive.Unstable_TriggerPopover
|
|
193
|
+
className={cn(
|
|
194
|
+
"absolute bottom-full start-0 z-50 mb-2 w-64 overflow-hidden rounded-xl border border-border bg-popover text-popover-foreground shadow-lg",
|
|
195
|
+
className,
|
|
196
|
+
)}
|
|
197
|
+
{...props}
|
|
198
|
+
>
|
|
199
|
+
{directive ? (
|
|
200
|
+
<ComposerPrimitive.Unstable_TriggerPopover.Directive
|
|
201
|
+
formatter={
|
|
202
|
+
directive.formatter ??
|
|
203
|
+
unstable_defaultDirectiveFormatter
|
|
204
|
+
}
|
|
205
|
+
onInserted={directive.onInserted}
|
|
206
|
+
/>
|
|
207
|
+
) : (
|
|
208
|
+
<ComposerPrimitive.Unstable_TriggerPopover.Action
|
|
209
|
+
formatter={
|
|
210
|
+
action.formatter ?? unstable_defaultDirectiveFormatter
|
|
211
|
+
}
|
|
212
|
+
onExecute={action.onExecute}
|
|
213
|
+
removeOnExecute={action.removeOnExecute}
|
|
214
|
+
/>
|
|
215
|
+
)}
|
|
216
|
+
<Categories
|
|
217
|
+
iconMap={iconMap}
|
|
218
|
+
fallbackIcon={fallbackIcon}
|
|
219
|
+
emptyLabel={emptyCategoriesLabel}
|
|
220
|
+
/>
|
|
221
|
+
<Items
|
|
222
|
+
iconMap={iconMap}
|
|
223
|
+
fallbackIcon={fallbackIcon}
|
|
224
|
+
backLabel={backLabel}
|
|
225
|
+
emptyLabel={emptyItemsLabel}
|
|
226
|
+
loadingLabel={loadingLabel}
|
|
227
|
+
/>
|
|
228
|
+
</ComposerPrimitive.Unstable_TriggerPopover>
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
ComposerTriggerPopoverImpl.displayName =
|
|
232
|
+
"ComposerTriggerPopover";
|
|
233
|
+
|
|
234
|
+
export const ComposerTriggerPopover = memo(
|
|
235
|
+
ComposerTriggerPopoverImpl,
|
|
236
|
+
) as FC<ComposerTriggerPopoverProps>;
|