@springbrand/message-panel 0.1.2 → 0.1.3-alpha.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 (99) hide show
  1. package/demo/camel-chat-showcase.tsx +1265 -0
  2. package/demo/fixtures.ts +292 -0
  3. package/demo/message-panel-chat-showcase.tsx +250 -0
  4. package/demo/message-panel-gallery.tsx +389 -0
  5. package/package.json +6 -16
  6. package/src/adapters/ai-sdk.ts +32 -0
  7. package/src/camel/camel-chat-messages.tsx +1435 -0
  8. package/src/camel/camel-model-select.tsx +88 -0
  9. package/src/camel/camel-plan-bar.tsx +115 -0
  10. package/src/camel/camel-prompt-input.tsx +547 -0
  11. package/src/camel/camel-tool-presentation.tsx +1270 -0
  12. package/src/camel/camel-turn.ts +334 -0
  13. package/src/camel/turn-summary-bar.tsx +81 -0
  14. package/src/chat-summary-panel.tsx +229 -0
  15. package/src/composer/chat-composer.tsx +216 -0
  16. package/src/composer/composer-attachments.tsx +138 -0
  17. package/src/composer/composer-trigger-popover.tsx +236 -0
  18. package/src/composer/composer.tsx +425 -0
  19. package/src/composer/drop-controller.ts +80 -0
  20. package/src/composer/types.ts +79 -0
  21. package/src/contracts.ts +138 -0
  22. package/src/conversation.tsx +76 -0
  23. package/src/internal/cn.ts +3 -0
  24. package/src/message-panel.tsx +368 -0
  25. package/src/message.tsx +254 -0
  26. package/src/parts/index.tsx +1149 -0
  27. package/src/parts/kind.ts +67 -0
  28. package/src/parts/plan.ts +64 -0
  29. package/src/parts/primitives.tsx +439 -0
  30. package/src/registry.ts +44 -0
  31. package/src/status.tsx +132 -0
  32. package/dist/demo/camel-chat-showcase.d.ts +0 -1
  33. package/dist/demo/camel-chat-showcase.js +0 -1007
  34. package/dist/demo/fixtures.d.ts +0 -32
  35. package/dist/demo/fixtures.js +0 -250
  36. package/dist/demo/index.js +0 -4
  37. package/dist/demo/message-panel-chat-showcase.d.ts +0 -1
  38. package/dist/demo/message-panel-chat-showcase.js +0 -114
  39. package/dist/demo/message-panel-gallery.d.ts +0 -1
  40. package/dist/demo/message-panel-gallery.js +0 -130
  41. package/dist/src/adapters/ai-sdk.d.ts +0 -4
  42. package/dist/src/adapters/ai-sdk.js +0 -25
  43. package/dist/src/camel/camel-chat-messages.d.ts +0 -44
  44. package/dist/src/camel/camel-chat-messages.js +0 -421
  45. package/dist/src/camel/camel-model-select.d.ts +0 -13
  46. package/dist/src/camel/camel-model-select.js +0 -7
  47. package/dist/src/camel/camel-plan-bar.d.ts +0 -5
  48. package/dist/src/camel/camel-plan-bar.js +0 -24
  49. package/dist/src/camel/camel-prompt-input.d.ts +0 -32
  50. package/dist/src/camel/camel-prompt-input.js +0 -182
  51. package/dist/src/camel/camel-tool-presentation.d.ts +0 -21
  52. package/dist/src/camel/camel-tool-presentation.js +0 -784
  53. package/dist/src/camel/camel-turn.d.ts +0 -57
  54. package/dist/src/camel/camel-turn.js +0 -203
  55. package/dist/src/camel/index.js +0 -7
  56. package/dist/src/camel/turn-summary-bar.d.ts +0 -8
  57. package/dist/src/camel/turn-summary-bar.js +0 -12
  58. package/dist/src/chat-summary-panel.d.ts +0 -22
  59. package/dist/src/chat-summary-panel.js +0 -26
  60. package/dist/src/composer/chat-composer.d.ts +0 -4
  61. package/dist/src/composer/chat-composer.js +0 -90
  62. package/dist/src/composer/composer-attachments.d.ts +0 -13
  63. package/dist/src/composer/composer-attachments.js +0 -47
  64. package/dist/src/composer/composer-trigger-popover.d.ts +0 -31
  65. package/dist/src/composer/composer-trigger-popover.js +0 -29
  66. package/dist/src/composer/composer.d.ts +0 -15
  67. package/dist/src/composer/composer.js +0 -180
  68. package/dist/src/composer/drop-controller.d.ts +0 -8
  69. package/dist/src/composer/drop-controller.js +0 -69
  70. package/dist/src/composer/index.js +0 -6
  71. package/dist/src/composer/types.d.ts +0 -62
  72. package/dist/src/composer/types.js +0 -1
  73. package/dist/src/contracts.d.ts +0 -94
  74. package/dist/src/contracts.js +0 -1
  75. package/dist/src/conversation.d.ts +0 -13
  76. package/dist/src/conversation.js +0 -18
  77. package/dist/src/index.js +0 -12
  78. package/dist/src/internal/cn.d.ts +0 -1
  79. package/dist/src/internal/cn.js +0 -3
  80. package/dist/src/message-panel.d.ts +0 -4
  81. package/dist/src/message-panel.js +0 -144
  82. package/dist/src/message.d.ts +0 -36
  83. package/dist/src/message.js +0 -83
  84. package/dist/src/parts/index.d.ts +0 -187
  85. package/dist/src/parts/index.js +0 -332
  86. package/dist/src/parts/kind.d.ts +0 -3
  87. package/dist/src/parts/kind.js +0 -69
  88. package/dist/src/parts/plan.d.ts +0 -10
  89. package/dist/src/parts/plan.js +0 -40
  90. package/dist/src/parts/primitives.d.ts +0 -70
  91. package/dist/src/parts/primitives.js +0 -153
  92. package/dist/src/registry.d.ts +0 -4
  93. package/dist/src/registry.js +0 -31
  94. package/dist/src/status.d.ts +0 -26
  95. package/dist/src/status.js +0 -74
  96. /package/{dist/demo/index.d.ts → demo/index.ts} +0 -0
  97. /package/{dist/src/camel/index.d.ts → src/camel/index.ts} +0 -0
  98. /package/{dist/src/composer/index.d.ts → src/composer/index.ts} +0 -0
  99. /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>;