@fanvue/ui 2.5.1 → 2.6.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/dist/cjs/components/ChatInput/ChatInput.cjs +289 -0
- package/dist/cjs/components/ChatInput/ChatInput.cjs.map +1 -0
- package/dist/cjs/index.cjs +2 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/components/ChatInput/ChatInput.mjs +272 -0
- package/dist/components/ChatInput/ChatInput.mjs.map +1 -0
- package/dist/index.d.ts +92 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -2
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
4
|
+
const jsxRuntime = require("react/jsx-runtime");
|
|
5
|
+
const React = require("react");
|
|
6
|
+
const cn = require("../../utils/cn.cjs");
|
|
7
|
+
const IconButton = require("../IconButton/IconButton.cjs");
|
|
8
|
+
const AddIcon = require("../Icons/AddIcon.cjs");
|
|
9
|
+
const ArrowUpIcon = require("../Icons/ArrowUpIcon.cjs");
|
|
10
|
+
const ChevronDownIcon = require("../Icons/ChevronDownIcon.cjs");
|
|
11
|
+
function _interopNamespaceDefault(e) {
|
|
12
|
+
const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
|
|
13
|
+
if (e) {
|
|
14
|
+
for (const k in e) {
|
|
15
|
+
if (k !== "default") {
|
|
16
|
+
const d = Object.getOwnPropertyDescriptor(e, k);
|
|
17
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
18
|
+
enumerable: true,
|
|
19
|
+
get: () => e[k]
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
n.default = e;
|
|
25
|
+
return Object.freeze(n);
|
|
26
|
+
}
|
|
27
|
+
const React__namespace = /* @__PURE__ */ _interopNamespaceDefault(React);
|
|
28
|
+
const LINE_HEIGHT = 18;
|
|
29
|
+
const TEXTAREA_PY = 12;
|
|
30
|
+
function calculateHeight(rows) {
|
|
31
|
+
return LINE_HEIGHT * rows + TEXTAREA_PY * 2;
|
|
32
|
+
}
|
|
33
|
+
const ChatInput = React__namespace.forwardRef(
|
|
34
|
+
({
|
|
35
|
+
className,
|
|
36
|
+
minRows = 1,
|
|
37
|
+
maxRows = 6,
|
|
38
|
+
disabled = false,
|
|
39
|
+
loading = false,
|
|
40
|
+
value,
|
|
41
|
+
defaultValue,
|
|
42
|
+
placeholder,
|
|
43
|
+
maxLength,
|
|
44
|
+
"aria-label": ariaLabel,
|
|
45
|
+
onChange,
|
|
46
|
+
onKeyDown,
|
|
47
|
+
onSubmit,
|
|
48
|
+
showFileButton = false,
|
|
49
|
+
onFileClick,
|
|
50
|
+
fileButtonAriaLabel = "Attach file",
|
|
51
|
+
submitAriaLabel = "Send message",
|
|
52
|
+
submitIcon,
|
|
53
|
+
toolbarRight,
|
|
54
|
+
selectOptions,
|
|
55
|
+
selectValue,
|
|
56
|
+
onSelectChange,
|
|
57
|
+
style,
|
|
58
|
+
...textareaProps
|
|
59
|
+
}, ref) => {
|
|
60
|
+
const internalRef = React__namespace.useRef(null);
|
|
61
|
+
const [internalValue, setInternalValue] = React__namespace.useState(defaultValue ?? "");
|
|
62
|
+
const resolvedValue = value !== void 0 ? value : internalValue;
|
|
63
|
+
const isControlled = value !== void 0;
|
|
64
|
+
const mergedRef = React__namespace.useCallback(
|
|
65
|
+
(node) => {
|
|
66
|
+
internalRef.current = node;
|
|
67
|
+
if (typeof ref === "function") {
|
|
68
|
+
ref(node);
|
|
69
|
+
} else if (ref) {
|
|
70
|
+
ref.current = node;
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
[ref]
|
|
74
|
+
);
|
|
75
|
+
const adjustHeight = React__namespace.useCallback(() => {
|
|
76
|
+
const textarea = internalRef.current;
|
|
77
|
+
if (!textarea) return;
|
|
78
|
+
const minHeight2 = calculateHeight(minRows);
|
|
79
|
+
const maxHeight2 = calculateHeight(maxRows);
|
|
80
|
+
textarea.style.height = `${minHeight2}px`;
|
|
81
|
+
const desired = Math.min(Math.max(textarea.scrollHeight, minHeight2), maxHeight2);
|
|
82
|
+
textarea.style.height = `${desired}px`;
|
|
83
|
+
}, [minRows, maxRows]);
|
|
84
|
+
React__namespace.useEffect(() => {
|
|
85
|
+
adjustHeight();
|
|
86
|
+
}, [resolvedValue, adjustHeight]);
|
|
87
|
+
const handleChange = (e) => {
|
|
88
|
+
if (!isControlled) {
|
|
89
|
+
setInternalValue(e.target.value);
|
|
90
|
+
}
|
|
91
|
+
onChange?.(e);
|
|
92
|
+
};
|
|
93
|
+
const canSubmit = !!String(resolvedValue).trim() && !disabled && !loading;
|
|
94
|
+
const handleSubmit = () => {
|
|
95
|
+
const text = String(resolvedValue).trim();
|
|
96
|
+
if (!text || !canSubmit) return;
|
|
97
|
+
onSubmit?.(text);
|
|
98
|
+
if (!isControlled) {
|
|
99
|
+
setInternalValue("");
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const handleKeyDown = (e) => {
|
|
103
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
104
|
+
e.preventDefault();
|
|
105
|
+
handleSubmit();
|
|
106
|
+
}
|
|
107
|
+
onKeyDown?.(e);
|
|
108
|
+
};
|
|
109
|
+
const minHeight = calculateHeight(minRows);
|
|
110
|
+
const maxHeight = calculateHeight(maxRows);
|
|
111
|
+
const selectedOption = selectOptions?.find((o) => o.value === selectValue) ?? selectOptions?.[0];
|
|
112
|
+
const resolvedToolbarRight = toolbarRight ?? (selectOptions && selectOptions.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
113
|
+
InlineSelect,
|
|
114
|
+
{
|
|
115
|
+
options: selectOptions,
|
|
116
|
+
value: selectValue,
|
|
117
|
+
onChange: onSelectChange,
|
|
118
|
+
disabled,
|
|
119
|
+
selectedOption
|
|
120
|
+
}
|
|
121
|
+
) : null);
|
|
122
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
123
|
+
"div",
|
|
124
|
+
{
|
|
125
|
+
className: cn.cn(
|
|
126
|
+
"relative flex flex-col gap-6 rounded-lg border border-border-primary bg-surface-primary",
|
|
127
|
+
"has-focus-visible:border-neutral-alphas-400 has-focus-visible:outline-none",
|
|
128
|
+
"motion-safe:transition-colors",
|
|
129
|
+
disabled && "opacity-50",
|
|
130
|
+
className
|
|
131
|
+
),
|
|
132
|
+
children: [
|
|
133
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
134
|
+
"textarea",
|
|
135
|
+
{
|
|
136
|
+
...textareaProps,
|
|
137
|
+
ref: mergedRef,
|
|
138
|
+
value: isControlled ? value : internalValue,
|
|
139
|
+
placeholder,
|
|
140
|
+
maxLength,
|
|
141
|
+
disabled,
|
|
142
|
+
"aria-label": ariaLabel ?? placeholder,
|
|
143
|
+
onChange: handleChange,
|
|
144
|
+
onKeyDown: handleKeyDown,
|
|
145
|
+
rows: minRows,
|
|
146
|
+
className: cn.cn(
|
|
147
|
+
"w-full resize-none bg-transparent px-4 pt-4",
|
|
148
|
+
"typography-regular-body-md text-content-primary",
|
|
149
|
+
"placeholder:text-content-tertiary",
|
|
150
|
+
"focus:outline-none disabled:cursor-not-allowed",
|
|
151
|
+
"overflow-y-auto"
|
|
152
|
+
),
|
|
153
|
+
style: {
|
|
154
|
+
minHeight: `${minHeight}px`,
|
|
155
|
+
maxHeight: `${maxHeight}px`,
|
|
156
|
+
...style
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
),
|
|
160
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-2 px-4 pb-4", children: [
|
|
161
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-1", children: showFileButton && /* @__PURE__ */ jsxRuntime.jsx(
|
|
162
|
+
IconButton.IconButton,
|
|
163
|
+
{
|
|
164
|
+
variant: "tertiary",
|
|
165
|
+
size: "32",
|
|
166
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(AddIcon.AddIcon, {}),
|
|
167
|
+
"aria-label": fileButtonAriaLabel,
|
|
168
|
+
onClick: onFileClick,
|
|
169
|
+
disabled,
|
|
170
|
+
className: "sm:border sm:border-border-primary max-sm:-ml-2"
|
|
171
|
+
}
|
|
172
|
+
) }),
|
|
173
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
|
|
174
|
+
resolvedToolbarRight,
|
|
175
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
176
|
+
IconButton.IconButton,
|
|
177
|
+
{
|
|
178
|
+
variant: "primary",
|
|
179
|
+
size: "32",
|
|
180
|
+
icon: submitIcon ?? /* @__PURE__ */ jsxRuntime.jsx(ArrowUpIcon.ArrowUpIcon, {}),
|
|
181
|
+
"aria-label": submitAriaLabel,
|
|
182
|
+
onClick: handleSubmit,
|
|
183
|
+
disabled: !canSubmit,
|
|
184
|
+
className: "disabled:bg-surface-secondary disabled:opacity-100 disabled:text-icons-primary"
|
|
185
|
+
}
|
|
186
|
+
)
|
|
187
|
+
] })
|
|
188
|
+
] })
|
|
189
|
+
]
|
|
190
|
+
}
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
ChatInput.displayName = "ChatInput";
|
|
195
|
+
function InlineSelect({ options, value, onChange, disabled, selectedOption }) {
|
|
196
|
+
const [open, setOpen] = React__namespace.useState(false);
|
|
197
|
+
const containerRef = React__namespace.useRef(null);
|
|
198
|
+
React__namespace.useEffect(() => {
|
|
199
|
+
if (!open) return;
|
|
200
|
+
const handleClick = (e) => {
|
|
201
|
+
if (containerRef.current && !containerRef.current.contains(e.target)) {
|
|
202
|
+
setOpen(false);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
document.addEventListener("mousedown", handleClick);
|
|
206
|
+
return () => document.removeEventListener("mousedown", handleClick);
|
|
207
|
+
}, [open]);
|
|
208
|
+
React__namespace.useEffect(() => {
|
|
209
|
+
if (!open) return;
|
|
210
|
+
const handleKey = (e) => {
|
|
211
|
+
if (e.key === "Escape") setOpen(false);
|
|
212
|
+
};
|
|
213
|
+
document.addEventListener("keydown", handleKey);
|
|
214
|
+
return () => document.removeEventListener("keydown", handleKey);
|
|
215
|
+
}, [open]);
|
|
216
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: containerRef, className: "relative", children: [
|
|
217
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
218
|
+
"button",
|
|
219
|
+
{
|
|
220
|
+
type: "button",
|
|
221
|
+
role: "combobox",
|
|
222
|
+
"aria-expanded": open,
|
|
223
|
+
"aria-haspopup": "listbox",
|
|
224
|
+
"aria-label": "Select model",
|
|
225
|
+
disabled,
|
|
226
|
+
onClick: () => setOpen((prev) => !prev),
|
|
227
|
+
className: cn.cn(
|
|
228
|
+
"typography-semibold-body-sm text-content-primary",
|
|
229
|
+
"flex items-center gap-1 rounded-sm px-2 py-1",
|
|
230
|
+
"hover:bg-neutral-alphas-50 focus-visible:shadow-focus-ring focus-visible:outline-none",
|
|
231
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
232
|
+
"motion-safe:transition-colors"
|
|
233
|
+
),
|
|
234
|
+
children: [
|
|
235
|
+
selectedOption?.icon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex shrink-0 items-center [&>svg]:size-4", children: selectedOption.icon }),
|
|
236
|
+
selectedOption?.label ?? options[0]?.label ?? "Select",
|
|
237
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
238
|
+
ChevronDownIcon.ChevronDownIcon,
|
|
239
|
+
{
|
|
240
|
+
className: cn.cn("size-4 motion-safe:transition-transform", open && "rotate-180")
|
|
241
|
+
}
|
|
242
|
+
)
|
|
243
|
+
]
|
|
244
|
+
}
|
|
245
|
+
),
|
|
246
|
+
open && /* @__PURE__ */ jsxRuntime.jsx(
|
|
247
|
+
"div",
|
|
248
|
+
{
|
|
249
|
+
role: "listbox",
|
|
250
|
+
className: cn.cn(
|
|
251
|
+
"absolute right-0 bottom-full z-10 mb-1 min-w-[140px]",
|
|
252
|
+
"overflow-hidden rounded-xs border border-border-primary bg-surface-primary p-1 shadow-lg"
|
|
253
|
+
),
|
|
254
|
+
children: options.map((option) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
255
|
+
"div",
|
|
256
|
+
{
|
|
257
|
+
role: "option",
|
|
258
|
+
tabIndex: 0,
|
|
259
|
+
"aria-selected": option.value === value,
|
|
260
|
+
className: cn.cn(
|
|
261
|
+
"typography-regular-body-md flex cursor-pointer items-center gap-2 rounded-xs px-3 py-1.5",
|
|
262
|
+
"text-content-primary hover:bg-neutral-alphas-50",
|
|
263
|
+
"focus-visible:shadow-focus-ring focus-visible:outline-none",
|
|
264
|
+
option.value === value && "bg-neutral-alphas-50"
|
|
265
|
+
),
|
|
266
|
+
onClick: () => {
|
|
267
|
+
onChange?.(option.value);
|
|
268
|
+
setOpen(false);
|
|
269
|
+
},
|
|
270
|
+
onKeyDown: (e) => {
|
|
271
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
272
|
+
e.preventDefault();
|
|
273
|
+
onChange?.(option.value);
|
|
274
|
+
setOpen(false);
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
children: [
|
|
278
|
+
option.icon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex shrink-0 items-center [&>svg]:size-4", children: option.icon }),
|
|
279
|
+
option.label
|
|
280
|
+
]
|
|
281
|
+
},
|
|
282
|
+
option.value
|
|
283
|
+
))
|
|
284
|
+
}
|
|
285
|
+
)
|
|
286
|
+
] });
|
|
287
|
+
}
|
|
288
|
+
exports.ChatInput = ChatInput;
|
|
289
|
+
//# sourceMappingURL=ChatInput.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChatInput.cjs","sources":["../../../../src/components/ChatInput/ChatInput.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { IconButton } from \"../IconButton/IconButton\";\nimport { AddIcon } from \"../Icons/AddIcon\";\nimport { ArrowUpIcon } from \"../Icons/ArrowUpIcon\";\nimport { ChevronDownIcon } from \"../Icons/ChevronDownIcon\";\n\n/** A single option for the inline model/dropdown selector. */\nexport interface ChatInputSelectOption {\n /** Unique value for this option. */\n value: string;\n /** Display label. */\n label: string;\n /** Optional icon rendered to the left of the label. */\n icon?: React.ReactNode;\n}\n\n/**\n * Props for {@link ChatInput}. Standard textarea HTML attributes are forwarded to the inner\n * `<textarea>` except `className` (applied to the outer container), `rows` (use `minRows`), and\n * `onSubmit` (replaced by the chat message submit callback).\n */\nexport interface ChatInputProps\n extends Omit<\n React.TextareaHTMLAttributes<HTMLTextAreaElement>,\n \"className\" | \"rows\" | \"onSubmit\"\n > {\n /** Minimum number of visible rows. @default 1 */\n minRows?: number;\n /** Maximum number of visible rows before scrolling. @default 6 */\n maxRows?: number;\n /** Whether a submission is in progress (disables submit, shows visual feedback). @default false */\n loading?: boolean;\n /**\n * Callback fired when the user submits (clicks the send button or presses Enter without Shift).\n * Receives the current trimmed text value.\n */\n onSubmit?: (value: string) => void;\n /**\n * When `true`, renders an \"attach file\" button in the bottom-left toolbar.\n * @default false\n */\n showFileButton?: boolean;\n /** Callback fired when the attach-file button is clicked. Only relevant when `showFileButton` is `true`. */\n onFileClick?: () => void;\n /** Accessible label for the attach-file button. @default \"Attach file\" */\n fileButtonAriaLabel?: string;\n /** Accessible label for the submit button. @default \"Send message\" */\n submitAriaLabel?: string;\n /** Icon element for the submit button. @default `<ArrowUpIcon />` */\n submitIcon?: React.ReactNode;\n /**\n * Optional content rendered in the bottom-right toolbar, to the left of the submit button.\n * When provided, takes precedence over the built-in `selectOptions` dropdown.\n */\n toolbarRight?: React.ReactNode;\n /**\n * Options for the built-in inline dropdown selector (e.g. model picker).\n * Ignored when `toolbarRight` is provided.\n */\n selectOptions?: ChatInputSelectOption[];\n /** Currently selected value for the built-in dropdown. Should match one of `selectOptions[].value`. */\n selectValue?: string;\n /** Callback fired when the user picks a different dropdown option. */\n onSelectChange?: (value: string) => void;\n /** Additional className applied to the outermost container. */\n className?: string;\n}\n\nconst LINE_HEIGHT = 18;\nconst TEXTAREA_PY = 12;\n\nfunction calculateHeight(rows: number): number {\n return LINE_HEIGHT * rows + TEXTAREA_PY * 2;\n}\n\n/**\n * A chat-style multi-line input with an integrated toolbar containing an\n * optional file-attach button, optional right-side controls (e.g. a model\n * selector), and a submit button — all inside a single bordered container.\n *\n * Designed to behave like modern AI chat inputs: auto-grows with content,\n * submits on Enter (Shift+Enter for newlines), and keeps controls inline.\n *\n * @example\n * ```tsx\n * <ChatInput\n * placeholder=\"Type a message...\"\n * onSubmit={(text) => send(text)}\n * />\n * ```\n *\n * @example\n * ```tsx\n * <ChatInput\n * placeholder=\"Ask the agent...\"\n * showFileButton\n * onFileClick={() => openFilePicker()}\n * selectOptions={[\n * { value: \"fanvue-ai\", label: \"Fanvue AI\", icon: <AIIcon className=\"size-4\" /> },\n * { value: \"example\", label: \"Example\", icon: <BulbIcon className=\"size-4\" /> },\n * ]}\n * selectValue=\"fanvue-ai\"\n * onSelectChange={(v) => setModel(v)}\n * onSubmit={(text) => send(text)}\n * />\n * ```\n */\nexport const ChatInput = React.forwardRef<HTMLTextAreaElement, ChatInputProps>(\n (\n {\n className,\n minRows = 1,\n maxRows = 6,\n disabled = false,\n loading = false,\n value,\n defaultValue,\n placeholder,\n maxLength,\n \"aria-label\": ariaLabel,\n onChange,\n onKeyDown,\n onSubmit,\n showFileButton = false,\n onFileClick,\n fileButtonAriaLabel = \"Attach file\",\n submitAriaLabel = \"Send message\",\n submitIcon,\n toolbarRight,\n selectOptions,\n selectValue,\n onSelectChange,\n style,\n ...textareaProps\n },\n ref,\n ) => {\n const internalRef = React.useRef<HTMLTextAreaElement>(null);\n const [internalValue, setInternalValue] = React.useState(defaultValue ?? \"\");\n const resolvedValue = value !== undefined ? value : internalValue;\n const isControlled = value !== undefined;\n\n const mergedRef = React.useCallback(\n (node: HTMLTextAreaElement | null) => {\n (internalRef as React.MutableRefObject<HTMLTextAreaElement | null>).current = node;\n if (typeof ref === \"function\") {\n ref(node);\n } else if (ref) {\n (ref as React.MutableRefObject<HTMLTextAreaElement | null>).current = node;\n }\n },\n [ref],\n );\n\n const adjustHeight = React.useCallback(() => {\n const textarea = internalRef.current;\n if (!textarea) return;\n\n const minHeight = calculateHeight(minRows);\n const maxHeight = calculateHeight(maxRows);\n\n textarea.style.height = `${minHeight}px`;\n const desired = Math.min(Math.max(textarea.scrollHeight, minHeight), maxHeight);\n textarea.style.height = `${desired}px`;\n }, [minRows, maxRows]);\n\n React.useEffect(() => {\n adjustHeight();\n }, [resolvedValue, adjustHeight]);\n\n const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n if (!isControlled) {\n setInternalValue(e.target.value);\n }\n onChange?.(e);\n };\n\n const canSubmit = !!String(resolvedValue).trim() && !disabled && !loading;\n\n const handleSubmit = () => {\n const text = String(resolvedValue).trim();\n if (!text || !canSubmit) return;\n onSubmit?.(text);\n if (!isControlled) {\n setInternalValue(\"\");\n }\n };\n\n const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n handleSubmit();\n }\n onKeyDown?.(e);\n };\n\n const minHeight = calculateHeight(minRows);\n const maxHeight = calculateHeight(maxRows);\n\n const selectedOption =\n selectOptions?.find((o) => o.value === selectValue) ?? selectOptions?.[0];\n const resolvedToolbarRight =\n toolbarRight ??\n (selectOptions && selectOptions.length > 0 ? (\n <InlineSelect\n options={selectOptions}\n value={selectValue}\n onChange={onSelectChange}\n disabled={disabled}\n selectedOption={selectedOption}\n />\n ) : null);\n\n return (\n <div\n className={cn(\n \"relative flex flex-col gap-6 rounded-lg border border-border-primary bg-surface-primary\",\n \"has-focus-visible:border-neutral-alphas-400 has-focus-visible:outline-none\",\n \"motion-safe:transition-colors\",\n disabled && \"opacity-50\",\n className,\n )}\n >\n <textarea\n {...textareaProps}\n ref={mergedRef}\n value={isControlled ? value : internalValue}\n placeholder={placeholder}\n maxLength={maxLength}\n disabled={disabled}\n aria-label={ariaLabel ?? placeholder}\n onChange={handleChange}\n onKeyDown={handleKeyDown}\n rows={minRows}\n className={cn(\n \"w-full resize-none bg-transparent px-4 pt-4\",\n \"typography-regular-body-md text-content-primary\",\n \"placeholder:text-content-tertiary\",\n \"focus:outline-none disabled:cursor-not-allowed\",\n \"overflow-y-auto\",\n )}\n style={{\n minHeight: `${minHeight}px`,\n maxHeight: `${maxHeight}px`,\n ...style,\n }}\n />\n\n <div className=\"flex items-center justify-between gap-2 px-4 pb-4\">\n <div className=\"flex items-center gap-1\">\n {showFileButton && (\n <IconButton\n variant=\"tertiary\"\n size=\"32\"\n icon={<AddIcon />}\n aria-label={fileButtonAriaLabel}\n onClick={onFileClick}\n disabled={disabled}\n className=\"sm:border sm:border-border-primary max-sm:-ml-2\"\n />\n )}\n </div>\n\n <div className=\"flex items-center gap-1\">\n {resolvedToolbarRight}\n <IconButton\n variant=\"primary\"\n size=\"32\"\n icon={submitIcon ?? <ArrowUpIcon />}\n aria-label={submitAriaLabel}\n onClick={handleSubmit}\n disabled={!canSubmit}\n className=\"disabled:bg-surface-secondary disabled:opacity-100 disabled:text-icons-primary\"\n />\n </div>\n </div>\n </div>\n );\n },\n);\n\nChatInput.displayName = \"ChatInput\";\n\ninterface InlineSelectProps {\n options: ChatInputSelectOption[];\n value?: string;\n onChange?: (value: string) => void;\n disabled?: boolean;\n selectedOption?: ChatInputSelectOption;\n}\n\nfunction InlineSelect({ options, value, onChange, disabled, selectedOption }: InlineSelectProps) {\n const [open, setOpen] = React.useState(false);\n const containerRef = React.useRef<HTMLDivElement>(null);\n\n React.useEffect(() => {\n if (!open) return;\n const handleClick = (e: MouseEvent) => {\n if (containerRef.current && !containerRef.current.contains(e.target as Node)) {\n setOpen(false);\n }\n };\n document.addEventListener(\"mousedown\", handleClick);\n return () => document.removeEventListener(\"mousedown\", handleClick);\n }, [open]);\n\n React.useEffect(() => {\n if (!open) return;\n const handleKey = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") setOpen(false);\n };\n document.addEventListener(\"keydown\", handleKey);\n return () => document.removeEventListener(\"keydown\", handleKey);\n }, [open]);\n\n return (\n <div ref={containerRef} className=\"relative\">\n <button\n type=\"button\"\n role=\"combobox\"\n aria-expanded={open}\n aria-haspopup=\"listbox\"\n aria-label=\"Select model\"\n disabled={disabled}\n onClick={() => setOpen((prev) => !prev)}\n className={cn(\n \"typography-semibold-body-sm text-content-primary\",\n \"flex items-center gap-1 rounded-sm px-2 py-1\",\n \"hover:bg-neutral-alphas-50 focus-visible:shadow-focus-ring focus-visible:outline-none\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n \"motion-safe:transition-colors\",\n )}\n >\n {selectedOption?.icon && (\n <span className=\"flex shrink-0 items-center [&>svg]:size-4\">{selectedOption.icon}</span>\n )}\n {selectedOption?.label ?? options[0]?.label ?? \"Select\"}\n <ChevronDownIcon\n className={cn(\"size-4 motion-safe:transition-transform\", open && \"rotate-180\")}\n />\n </button>\n\n {open && (\n <div\n role=\"listbox\"\n className={cn(\n \"absolute right-0 bottom-full z-10 mb-1 min-w-[140px]\",\n \"overflow-hidden rounded-xs border border-border-primary bg-surface-primary p-1 shadow-lg\",\n )}\n >\n {options.map((option) => (\n <div\n key={option.value}\n role=\"option\"\n tabIndex={0}\n aria-selected={option.value === value}\n className={cn(\n \"typography-regular-body-md flex cursor-pointer items-center gap-2 rounded-xs px-3 py-1.5\",\n \"text-content-primary hover:bg-neutral-alphas-50\",\n \"focus-visible:shadow-focus-ring focus-visible:outline-none\",\n option.value === value && \"bg-neutral-alphas-50\",\n )}\n onClick={() => {\n onChange?.(option.value);\n setOpen(false);\n }}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onChange?.(option.value);\n setOpen(false);\n }\n }}\n >\n {option.icon && (\n <span className=\"flex shrink-0 items-center [&>svg]:size-4\">{option.icon}</span>\n )}\n {option.label}\n </div>\n ))}\n </div>\n )}\n </div>\n );\n}\n"],"names":["React","minHeight","maxHeight","jsx","jsxs","cn","IconButton","AddIcon","ArrowUpIcon","ChevronDownIcon"],"mappings":"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"}
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -17,6 +17,7 @@ const BottomNavigationAction = require("./components/BottomNavigation/BottomNavi
|
|
|
17
17
|
const Breadcrumb = require("./components/Breadcrumb/Breadcrumb.cjs");
|
|
18
18
|
const Button = require("./components/Button/Button.cjs");
|
|
19
19
|
const Card = require("./components/Card/Card.cjs");
|
|
20
|
+
const ChatInput = require("./components/ChatInput/ChatInput.cjs");
|
|
20
21
|
const Checkbox = require("./components/Checkbox/Checkbox.cjs");
|
|
21
22
|
const Chip = require("./components/Chip/Chip.cjs");
|
|
22
23
|
const Count = require("./components/Count/Count.cjs");
|
|
@@ -214,6 +215,7 @@ exports.CardDescription = Card.CardDescription;
|
|
|
214
215
|
exports.CardFooter = Card.CardFooter;
|
|
215
216
|
exports.CardHeader = Card.CardHeader;
|
|
216
217
|
exports.CardTitle = Card.CardTitle;
|
|
218
|
+
exports.ChatInput = ChatInput.ChatInput;
|
|
217
219
|
exports.Checkbox = Checkbox.Checkbox;
|
|
218
220
|
exports.Chip = Chip.Chip;
|
|
219
221
|
exports.Count = Count.Count;
|
package/dist/cjs/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../../utils/cn.mjs";
|
|
5
|
+
import { IconButton } from "../IconButton/IconButton.mjs";
|
|
6
|
+
import { AddIcon } from "../Icons/AddIcon.mjs";
|
|
7
|
+
import { ArrowUpIcon } from "../Icons/ArrowUpIcon.mjs";
|
|
8
|
+
import { ChevronDownIcon } from "../Icons/ChevronDownIcon.mjs";
|
|
9
|
+
const LINE_HEIGHT = 18;
|
|
10
|
+
const TEXTAREA_PY = 12;
|
|
11
|
+
function calculateHeight(rows) {
|
|
12
|
+
return LINE_HEIGHT * rows + TEXTAREA_PY * 2;
|
|
13
|
+
}
|
|
14
|
+
const ChatInput = React.forwardRef(
|
|
15
|
+
({
|
|
16
|
+
className,
|
|
17
|
+
minRows = 1,
|
|
18
|
+
maxRows = 6,
|
|
19
|
+
disabled = false,
|
|
20
|
+
loading = false,
|
|
21
|
+
value,
|
|
22
|
+
defaultValue,
|
|
23
|
+
placeholder,
|
|
24
|
+
maxLength,
|
|
25
|
+
"aria-label": ariaLabel,
|
|
26
|
+
onChange,
|
|
27
|
+
onKeyDown,
|
|
28
|
+
onSubmit,
|
|
29
|
+
showFileButton = false,
|
|
30
|
+
onFileClick,
|
|
31
|
+
fileButtonAriaLabel = "Attach file",
|
|
32
|
+
submitAriaLabel = "Send message",
|
|
33
|
+
submitIcon,
|
|
34
|
+
toolbarRight,
|
|
35
|
+
selectOptions,
|
|
36
|
+
selectValue,
|
|
37
|
+
onSelectChange,
|
|
38
|
+
style,
|
|
39
|
+
...textareaProps
|
|
40
|
+
}, ref) => {
|
|
41
|
+
const internalRef = React.useRef(null);
|
|
42
|
+
const [internalValue, setInternalValue] = React.useState(defaultValue ?? "");
|
|
43
|
+
const resolvedValue = value !== void 0 ? value : internalValue;
|
|
44
|
+
const isControlled = value !== void 0;
|
|
45
|
+
const mergedRef = React.useCallback(
|
|
46
|
+
(node) => {
|
|
47
|
+
internalRef.current = node;
|
|
48
|
+
if (typeof ref === "function") {
|
|
49
|
+
ref(node);
|
|
50
|
+
} else if (ref) {
|
|
51
|
+
ref.current = node;
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
[ref]
|
|
55
|
+
);
|
|
56
|
+
const adjustHeight = React.useCallback(() => {
|
|
57
|
+
const textarea = internalRef.current;
|
|
58
|
+
if (!textarea) return;
|
|
59
|
+
const minHeight2 = calculateHeight(minRows);
|
|
60
|
+
const maxHeight2 = calculateHeight(maxRows);
|
|
61
|
+
textarea.style.height = `${minHeight2}px`;
|
|
62
|
+
const desired = Math.min(Math.max(textarea.scrollHeight, minHeight2), maxHeight2);
|
|
63
|
+
textarea.style.height = `${desired}px`;
|
|
64
|
+
}, [minRows, maxRows]);
|
|
65
|
+
React.useEffect(() => {
|
|
66
|
+
adjustHeight();
|
|
67
|
+
}, [resolvedValue, adjustHeight]);
|
|
68
|
+
const handleChange = (e) => {
|
|
69
|
+
if (!isControlled) {
|
|
70
|
+
setInternalValue(e.target.value);
|
|
71
|
+
}
|
|
72
|
+
onChange?.(e);
|
|
73
|
+
};
|
|
74
|
+
const canSubmit = !!String(resolvedValue).trim() && !disabled && !loading;
|
|
75
|
+
const handleSubmit = () => {
|
|
76
|
+
const text = String(resolvedValue).trim();
|
|
77
|
+
if (!text || !canSubmit) return;
|
|
78
|
+
onSubmit?.(text);
|
|
79
|
+
if (!isControlled) {
|
|
80
|
+
setInternalValue("");
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const handleKeyDown = (e) => {
|
|
84
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
handleSubmit();
|
|
87
|
+
}
|
|
88
|
+
onKeyDown?.(e);
|
|
89
|
+
};
|
|
90
|
+
const minHeight = calculateHeight(minRows);
|
|
91
|
+
const maxHeight = calculateHeight(maxRows);
|
|
92
|
+
const selectedOption = selectOptions?.find((o) => o.value === selectValue) ?? selectOptions?.[0];
|
|
93
|
+
const resolvedToolbarRight = toolbarRight ?? (selectOptions && selectOptions.length > 0 ? /* @__PURE__ */ jsx(
|
|
94
|
+
InlineSelect,
|
|
95
|
+
{
|
|
96
|
+
options: selectOptions,
|
|
97
|
+
value: selectValue,
|
|
98
|
+
onChange: onSelectChange,
|
|
99
|
+
disabled,
|
|
100
|
+
selectedOption
|
|
101
|
+
}
|
|
102
|
+
) : null);
|
|
103
|
+
return /* @__PURE__ */ jsxs(
|
|
104
|
+
"div",
|
|
105
|
+
{
|
|
106
|
+
className: cn(
|
|
107
|
+
"relative flex flex-col gap-6 rounded-lg border border-border-primary bg-surface-primary",
|
|
108
|
+
"has-focus-visible:border-neutral-alphas-400 has-focus-visible:outline-none",
|
|
109
|
+
"motion-safe:transition-colors",
|
|
110
|
+
disabled && "opacity-50",
|
|
111
|
+
className
|
|
112
|
+
),
|
|
113
|
+
children: [
|
|
114
|
+
/* @__PURE__ */ jsx(
|
|
115
|
+
"textarea",
|
|
116
|
+
{
|
|
117
|
+
...textareaProps,
|
|
118
|
+
ref: mergedRef,
|
|
119
|
+
value: isControlled ? value : internalValue,
|
|
120
|
+
placeholder,
|
|
121
|
+
maxLength,
|
|
122
|
+
disabled,
|
|
123
|
+
"aria-label": ariaLabel ?? placeholder,
|
|
124
|
+
onChange: handleChange,
|
|
125
|
+
onKeyDown: handleKeyDown,
|
|
126
|
+
rows: minRows,
|
|
127
|
+
className: cn(
|
|
128
|
+
"w-full resize-none bg-transparent px-4 pt-4",
|
|
129
|
+
"typography-regular-body-md text-content-primary",
|
|
130
|
+
"placeholder:text-content-tertiary",
|
|
131
|
+
"focus:outline-none disabled:cursor-not-allowed",
|
|
132
|
+
"overflow-y-auto"
|
|
133
|
+
),
|
|
134
|
+
style: {
|
|
135
|
+
minHeight: `${minHeight}px`,
|
|
136
|
+
maxHeight: `${maxHeight}px`,
|
|
137
|
+
...style
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
),
|
|
141
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 px-4 pb-4", children: [
|
|
142
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-center gap-1", children: showFileButton && /* @__PURE__ */ jsx(
|
|
143
|
+
IconButton,
|
|
144
|
+
{
|
|
145
|
+
variant: "tertiary",
|
|
146
|
+
size: "32",
|
|
147
|
+
icon: /* @__PURE__ */ jsx(AddIcon, {}),
|
|
148
|
+
"aria-label": fileButtonAriaLabel,
|
|
149
|
+
onClick: onFileClick,
|
|
150
|
+
disabled,
|
|
151
|
+
className: "sm:border sm:border-border-primary max-sm:-ml-2"
|
|
152
|
+
}
|
|
153
|
+
) }),
|
|
154
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
155
|
+
resolvedToolbarRight,
|
|
156
|
+
/* @__PURE__ */ jsx(
|
|
157
|
+
IconButton,
|
|
158
|
+
{
|
|
159
|
+
variant: "primary",
|
|
160
|
+
size: "32",
|
|
161
|
+
icon: submitIcon ?? /* @__PURE__ */ jsx(ArrowUpIcon, {}),
|
|
162
|
+
"aria-label": submitAriaLabel,
|
|
163
|
+
onClick: handleSubmit,
|
|
164
|
+
disabled: !canSubmit,
|
|
165
|
+
className: "disabled:bg-surface-secondary disabled:opacity-100 disabled:text-icons-primary"
|
|
166
|
+
}
|
|
167
|
+
)
|
|
168
|
+
] })
|
|
169
|
+
] })
|
|
170
|
+
]
|
|
171
|
+
}
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
);
|
|
175
|
+
ChatInput.displayName = "ChatInput";
|
|
176
|
+
function InlineSelect({ options, value, onChange, disabled, selectedOption }) {
|
|
177
|
+
const [open, setOpen] = React.useState(false);
|
|
178
|
+
const containerRef = React.useRef(null);
|
|
179
|
+
React.useEffect(() => {
|
|
180
|
+
if (!open) return;
|
|
181
|
+
const handleClick = (e) => {
|
|
182
|
+
if (containerRef.current && !containerRef.current.contains(e.target)) {
|
|
183
|
+
setOpen(false);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
document.addEventListener("mousedown", handleClick);
|
|
187
|
+
return () => document.removeEventListener("mousedown", handleClick);
|
|
188
|
+
}, [open]);
|
|
189
|
+
React.useEffect(() => {
|
|
190
|
+
if (!open) return;
|
|
191
|
+
const handleKey = (e) => {
|
|
192
|
+
if (e.key === "Escape") setOpen(false);
|
|
193
|
+
};
|
|
194
|
+
document.addEventListener("keydown", handleKey);
|
|
195
|
+
return () => document.removeEventListener("keydown", handleKey);
|
|
196
|
+
}, [open]);
|
|
197
|
+
return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: "relative", children: [
|
|
198
|
+
/* @__PURE__ */ jsxs(
|
|
199
|
+
"button",
|
|
200
|
+
{
|
|
201
|
+
type: "button",
|
|
202
|
+
role: "combobox",
|
|
203
|
+
"aria-expanded": open,
|
|
204
|
+
"aria-haspopup": "listbox",
|
|
205
|
+
"aria-label": "Select model",
|
|
206
|
+
disabled,
|
|
207
|
+
onClick: () => setOpen((prev) => !prev),
|
|
208
|
+
className: cn(
|
|
209
|
+
"typography-semibold-body-sm text-content-primary",
|
|
210
|
+
"flex items-center gap-1 rounded-sm px-2 py-1",
|
|
211
|
+
"hover:bg-neutral-alphas-50 focus-visible:shadow-focus-ring focus-visible:outline-none",
|
|
212
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
213
|
+
"motion-safe:transition-colors"
|
|
214
|
+
),
|
|
215
|
+
children: [
|
|
216
|
+
selectedOption?.icon && /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center [&>svg]:size-4", children: selectedOption.icon }),
|
|
217
|
+
selectedOption?.label ?? options[0]?.label ?? "Select",
|
|
218
|
+
/* @__PURE__ */ jsx(
|
|
219
|
+
ChevronDownIcon,
|
|
220
|
+
{
|
|
221
|
+
className: cn("size-4 motion-safe:transition-transform", open && "rotate-180")
|
|
222
|
+
}
|
|
223
|
+
)
|
|
224
|
+
]
|
|
225
|
+
}
|
|
226
|
+
),
|
|
227
|
+
open && /* @__PURE__ */ jsx(
|
|
228
|
+
"div",
|
|
229
|
+
{
|
|
230
|
+
role: "listbox",
|
|
231
|
+
className: cn(
|
|
232
|
+
"absolute right-0 bottom-full z-10 mb-1 min-w-[140px]",
|
|
233
|
+
"overflow-hidden rounded-xs border border-border-primary bg-surface-primary p-1 shadow-lg"
|
|
234
|
+
),
|
|
235
|
+
children: options.map((option) => /* @__PURE__ */ jsxs(
|
|
236
|
+
"div",
|
|
237
|
+
{
|
|
238
|
+
role: "option",
|
|
239
|
+
tabIndex: 0,
|
|
240
|
+
"aria-selected": option.value === value,
|
|
241
|
+
className: cn(
|
|
242
|
+
"typography-regular-body-md flex cursor-pointer items-center gap-2 rounded-xs px-3 py-1.5",
|
|
243
|
+
"text-content-primary hover:bg-neutral-alphas-50",
|
|
244
|
+
"focus-visible:shadow-focus-ring focus-visible:outline-none",
|
|
245
|
+
option.value === value && "bg-neutral-alphas-50"
|
|
246
|
+
),
|
|
247
|
+
onClick: () => {
|
|
248
|
+
onChange?.(option.value);
|
|
249
|
+
setOpen(false);
|
|
250
|
+
},
|
|
251
|
+
onKeyDown: (e) => {
|
|
252
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
253
|
+
e.preventDefault();
|
|
254
|
+
onChange?.(option.value);
|
|
255
|
+
setOpen(false);
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
children: [
|
|
259
|
+
option.icon && /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center [&>svg]:size-4", children: option.icon }),
|
|
260
|
+
option.label
|
|
261
|
+
]
|
|
262
|
+
},
|
|
263
|
+
option.value
|
|
264
|
+
))
|
|
265
|
+
}
|
|
266
|
+
)
|
|
267
|
+
] });
|
|
268
|
+
}
|
|
269
|
+
export {
|
|
270
|
+
ChatInput
|
|
271
|
+
};
|
|
272
|
+
//# sourceMappingURL=ChatInput.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChatInput.mjs","sources":["../../../src/components/ChatInput/ChatInput.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { IconButton } from \"../IconButton/IconButton\";\nimport { AddIcon } from \"../Icons/AddIcon\";\nimport { ArrowUpIcon } from \"../Icons/ArrowUpIcon\";\nimport { ChevronDownIcon } from \"../Icons/ChevronDownIcon\";\n\n/** A single option for the inline model/dropdown selector. */\nexport interface ChatInputSelectOption {\n /** Unique value for this option. */\n value: string;\n /** Display label. */\n label: string;\n /** Optional icon rendered to the left of the label. */\n icon?: React.ReactNode;\n}\n\n/**\n * Props for {@link ChatInput}. Standard textarea HTML attributes are forwarded to the inner\n * `<textarea>` except `className` (applied to the outer container), `rows` (use `minRows`), and\n * `onSubmit` (replaced by the chat message submit callback).\n */\nexport interface ChatInputProps\n extends Omit<\n React.TextareaHTMLAttributes<HTMLTextAreaElement>,\n \"className\" | \"rows\" | \"onSubmit\"\n > {\n /** Minimum number of visible rows. @default 1 */\n minRows?: number;\n /** Maximum number of visible rows before scrolling. @default 6 */\n maxRows?: number;\n /** Whether a submission is in progress (disables submit, shows visual feedback). @default false */\n loading?: boolean;\n /**\n * Callback fired when the user submits (clicks the send button or presses Enter without Shift).\n * Receives the current trimmed text value.\n */\n onSubmit?: (value: string) => void;\n /**\n * When `true`, renders an \"attach file\" button in the bottom-left toolbar.\n * @default false\n */\n showFileButton?: boolean;\n /** Callback fired when the attach-file button is clicked. Only relevant when `showFileButton` is `true`. */\n onFileClick?: () => void;\n /** Accessible label for the attach-file button. @default \"Attach file\" */\n fileButtonAriaLabel?: string;\n /** Accessible label for the submit button. @default \"Send message\" */\n submitAriaLabel?: string;\n /** Icon element for the submit button. @default `<ArrowUpIcon />` */\n submitIcon?: React.ReactNode;\n /**\n * Optional content rendered in the bottom-right toolbar, to the left of the submit button.\n * When provided, takes precedence over the built-in `selectOptions` dropdown.\n */\n toolbarRight?: React.ReactNode;\n /**\n * Options for the built-in inline dropdown selector (e.g. model picker).\n * Ignored when `toolbarRight` is provided.\n */\n selectOptions?: ChatInputSelectOption[];\n /** Currently selected value for the built-in dropdown. Should match one of `selectOptions[].value`. */\n selectValue?: string;\n /** Callback fired when the user picks a different dropdown option. */\n onSelectChange?: (value: string) => void;\n /** Additional className applied to the outermost container. */\n className?: string;\n}\n\nconst LINE_HEIGHT = 18;\nconst TEXTAREA_PY = 12;\n\nfunction calculateHeight(rows: number): number {\n return LINE_HEIGHT * rows + TEXTAREA_PY * 2;\n}\n\n/**\n * A chat-style multi-line input with an integrated toolbar containing an\n * optional file-attach button, optional right-side controls (e.g. a model\n * selector), and a submit button — all inside a single bordered container.\n *\n * Designed to behave like modern AI chat inputs: auto-grows with content,\n * submits on Enter (Shift+Enter for newlines), and keeps controls inline.\n *\n * @example\n * ```tsx\n * <ChatInput\n * placeholder=\"Type a message...\"\n * onSubmit={(text) => send(text)}\n * />\n * ```\n *\n * @example\n * ```tsx\n * <ChatInput\n * placeholder=\"Ask the agent...\"\n * showFileButton\n * onFileClick={() => openFilePicker()}\n * selectOptions={[\n * { value: \"fanvue-ai\", label: \"Fanvue AI\", icon: <AIIcon className=\"size-4\" /> },\n * { value: \"example\", label: \"Example\", icon: <BulbIcon className=\"size-4\" /> },\n * ]}\n * selectValue=\"fanvue-ai\"\n * onSelectChange={(v) => setModel(v)}\n * onSubmit={(text) => send(text)}\n * />\n * ```\n */\nexport const ChatInput = React.forwardRef<HTMLTextAreaElement, ChatInputProps>(\n (\n {\n className,\n minRows = 1,\n maxRows = 6,\n disabled = false,\n loading = false,\n value,\n defaultValue,\n placeholder,\n maxLength,\n \"aria-label\": ariaLabel,\n onChange,\n onKeyDown,\n onSubmit,\n showFileButton = false,\n onFileClick,\n fileButtonAriaLabel = \"Attach file\",\n submitAriaLabel = \"Send message\",\n submitIcon,\n toolbarRight,\n selectOptions,\n selectValue,\n onSelectChange,\n style,\n ...textareaProps\n },\n ref,\n ) => {\n const internalRef = React.useRef<HTMLTextAreaElement>(null);\n const [internalValue, setInternalValue] = React.useState(defaultValue ?? \"\");\n const resolvedValue = value !== undefined ? value : internalValue;\n const isControlled = value !== undefined;\n\n const mergedRef = React.useCallback(\n (node: HTMLTextAreaElement | null) => {\n (internalRef as React.MutableRefObject<HTMLTextAreaElement | null>).current = node;\n if (typeof ref === \"function\") {\n ref(node);\n } else if (ref) {\n (ref as React.MutableRefObject<HTMLTextAreaElement | null>).current = node;\n }\n },\n [ref],\n );\n\n const adjustHeight = React.useCallback(() => {\n const textarea = internalRef.current;\n if (!textarea) return;\n\n const minHeight = calculateHeight(minRows);\n const maxHeight = calculateHeight(maxRows);\n\n textarea.style.height = `${minHeight}px`;\n const desired = Math.min(Math.max(textarea.scrollHeight, minHeight), maxHeight);\n textarea.style.height = `${desired}px`;\n }, [minRows, maxRows]);\n\n React.useEffect(() => {\n adjustHeight();\n }, [resolvedValue, adjustHeight]);\n\n const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n if (!isControlled) {\n setInternalValue(e.target.value);\n }\n onChange?.(e);\n };\n\n const canSubmit = !!String(resolvedValue).trim() && !disabled && !loading;\n\n const handleSubmit = () => {\n const text = String(resolvedValue).trim();\n if (!text || !canSubmit) return;\n onSubmit?.(text);\n if (!isControlled) {\n setInternalValue(\"\");\n }\n };\n\n const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n handleSubmit();\n }\n onKeyDown?.(e);\n };\n\n const minHeight = calculateHeight(minRows);\n const maxHeight = calculateHeight(maxRows);\n\n const selectedOption =\n selectOptions?.find((o) => o.value === selectValue) ?? selectOptions?.[0];\n const resolvedToolbarRight =\n toolbarRight ??\n (selectOptions && selectOptions.length > 0 ? (\n <InlineSelect\n options={selectOptions}\n value={selectValue}\n onChange={onSelectChange}\n disabled={disabled}\n selectedOption={selectedOption}\n />\n ) : null);\n\n return (\n <div\n className={cn(\n \"relative flex flex-col gap-6 rounded-lg border border-border-primary bg-surface-primary\",\n \"has-focus-visible:border-neutral-alphas-400 has-focus-visible:outline-none\",\n \"motion-safe:transition-colors\",\n disabled && \"opacity-50\",\n className,\n )}\n >\n <textarea\n {...textareaProps}\n ref={mergedRef}\n value={isControlled ? value : internalValue}\n placeholder={placeholder}\n maxLength={maxLength}\n disabled={disabled}\n aria-label={ariaLabel ?? placeholder}\n onChange={handleChange}\n onKeyDown={handleKeyDown}\n rows={minRows}\n className={cn(\n \"w-full resize-none bg-transparent px-4 pt-4\",\n \"typography-regular-body-md text-content-primary\",\n \"placeholder:text-content-tertiary\",\n \"focus:outline-none disabled:cursor-not-allowed\",\n \"overflow-y-auto\",\n )}\n style={{\n minHeight: `${minHeight}px`,\n maxHeight: `${maxHeight}px`,\n ...style,\n }}\n />\n\n <div className=\"flex items-center justify-between gap-2 px-4 pb-4\">\n <div className=\"flex items-center gap-1\">\n {showFileButton && (\n <IconButton\n variant=\"tertiary\"\n size=\"32\"\n icon={<AddIcon />}\n aria-label={fileButtonAriaLabel}\n onClick={onFileClick}\n disabled={disabled}\n className=\"sm:border sm:border-border-primary max-sm:-ml-2\"\n />\n )}\n </div>\n\n <div className=\"flex items-center gap-1\">\n {resolvedToolbarRight}\n <IconButton\n variant=\"primary\"\n size=\"32\"\n icon={submitIcon ?? <ArrowUpIcon />}\n aria-label={submitAriaLabel}\n onClick={handleSubmit}\n disabled={!canSubmit}\n className=\"disabled:bg-surface-secondary disabled:opacity-100 disabled:text-icons-primary\"\n />\n </div>\n </div>\n </div>\n );\n },\n);\n\nChatInput.displayName = \"ChatInput\";\n\ninterface InlineSelectProps {\n options: ChatInputSelectOption[];\n value?: string;\n onChange?: (value: string) => void;\n disabled?: boolean;\n selectedOption?: ChatInputSelectOption;\n}\n\nfunction InlineSelect({ options, value, onChange, disabled, selectedOption }: InlineSelectProps) {\n const [open, setOpen] = React.useState(false);\n const containerRef = React.useRef<HTMLDivElement>(null);\n\n React.useEffect(() => {\n if (!open) return;\n const handleClick = (e: MouseEvent) => {\n if (containerRef.current && !containerRef.current.contains(e.target as Node)) {\n setOpen(false);\n }\n };\n document.addEventListener(\"mousedown\", handleClick);\n return () => document.removeEventListener(\"mousedown\", handleClick);\n }, [open]);\n\n React.useEffect(() => {\n if (!open) return;\n const handleKey = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") setOpen(false);\n };\n document.addEventListener(\"keydown\", handleKey);\n return () => document.removeEventListener(\"keydown\", handleKey);\n }, [open]);\n\n return (\n <div ref={containerRef} className=\"relative\">\n <button\n type=\"button\"\n role=\"combobox\"\n aria-expanded={open}\n aria-haspopup=\"listbox\"\n aria-label=\"Select model\"\n disabled={disabled}\n onClick={() => setOpen((prev) => !prev)}\n className={cn(\n \"typography-semibold-body-sm text-content-primary\",\n \"flex items-center gap-1 rounded-sm px-2 py-1\",\n \"hover:bg-neutral-alphas-50 focus-visible:shadow-focus-ring focus-visible:outline-none\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n \"motion-safe:transition-colors\",\n )}\n >\n {selectedOption?.icon && (\n <span className=\"flex shrink-0 items-center [&>svg]:size-4\">{selectedOption.icon}</span>\n )}\n {selectedOption?.label ?? options[0]?.label ?? \"Select\"}\n <ChevronDownIcon\n className={cn(\"size-4 motion-safe:transition-transform\", open && \"rotate-180\")}\n />\n </button>\n\n {open && (\n <div\n role=\"listbox\"\n className={cn(\n \"absolute right-0 bottom-full z-10 mb-1 min-w-[140px]\",\n \"overflow-hidden rounded-xs border border-border-primary bg-surface-primary p-1 shadow-lg\",\n )}\n >\n {options.map((option) => (\n <div\n key={option.value}\n role=\"option\"\n tabIndex={0}\n aria-selected={option.value === value}\n className={cn(\n \"typography-regular-body-md flex cursor-pointer items-center gap-2 rounded-xs px-3 py-1.5\",\n \"text-content-primary hover:bg-neutral-alphas-50\",\n \"focus-visible:shadow-focus-ring focus-visible:outline-none\",\n option.value === value && \"bg-neutral-alphas-50\",\n )}\n onClick={() => {\n onChange?.(option.value);\n setOpen(false);\n }}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onChange?.(option.value);\n setOpen(false);\n }\n }}\n >\n {option.icon && (\n <span className=\"flex shrink-0 items-center [&>svg]:size-4\">{option.icon}</span>\n )}\n {option.label}\n </div>\n ))}\n </div>\n )}\n </div>\n );\n}\n"],"names":["minHeight","maxHeight"],"mappings":";;;;;;;;AAqEA,MAAM,cAAc;AACpB,MAAM,cAAc;AAEpB,SAAS,gBAAgB,MAAsB;AAC7C,SAAO,cAAc,OAAO,cAAc;AAC5C;AAkCO,MAAM,YAAY,MAAM;AAAA,EAC7B,CACE;AAAA,IACE;AAAA,IACA,UAAU;AAAA,IACV,UAAU;AAAA,IACV,WAAW;AAAA,IACX,UAAU;AAAA,IACV;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,cAAc;AAAA,IACd;AAAA,IACA;AAAA,IACA;AAAA,IACA,iBAAiB;AAAA,IACjB;AAAA,IACA,sBAAsB;AAAA,IACtB,kBAAkB;AAAA,IAClB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAG;AAAA,EAAA,GAEL,QACG;AACH,UAAM,cAAc,MAAM,OAA4B,IAAI;AAC1D,UAAM,CAAC,eAAe,gBAAgB,IAAI,MAAM,SAAS,gBAAgB,EAAE;AAC3E,UAAM,gBAAgB,UAAU,SAAY,QAAQ;AACpD,UAAM,eAAe,UAAU;AAE/B,UAAM,YAAY,MAAM;AAAA,MACtB,CAAC,SAAqC;AACnC,oBAAmE,UAAU;AAC9E,YAAI,OAAO,QAAQ,YAAY;AAC7B,cAAI,IAAI;AAAA,QACV,WAAW,KAAK;AACb,cAA2D,UAAU;AAAA,QACxE;AAAA,MACF;AAAA,MACA,CAAC,GAAG;AAAA,IAAA;AAGN,UAAM,eAAe,MAAM,YAAY,MAAM;AAC3C,YAAM,WAAW,YAAY;AAC7B,UAAI,CAAC,SAAU;AAEf,YAAMA,aAAY,gBAAgB,OAAO;AACzC,YAAMC,aAAY,gBAAgB,OAAO;AAEzC,eAAS,MAAM,SAAS,GAAGD,UAAS;AACpC,YAAM,UAAU,KAAK,IAAI,KAAK,IAAI,SAAS,cAAcA,UAAS,GAAGC,UAAS;AAC9E,eAAS,MAAM,SAAS,GAAG,OAAO;AAAA,IACpC,GAAG,CAAC,SAAS,OAAO,CAAC;AAErB,UAAM,UAAU,MAAM;AACpB,mBAAA;AAAA,IACF,GAAG,CAAC,eAAe,YAAY,CAAC;AAEhC,UAAM,eAAe,CAAC,MAA8C;AAClE,UAAI,CAAC,cAAc;AACjB,yBAAiB,EAAE,OAAO,KAAK;AAAA,MACjC;AACA,iBAAW,CAAC;AAAA,IACd;AAEA,UAAM,YAAY,CAAC,CAAC,OAAO,aAAa,EAAE,KAAA,KAAU,CAAC,YAAY,CAAC;AAElE,UAAM,eAAe,MAAM;AACzB,YAAM,OAAO,OAAO,aAAa,EAAE,KAAA;AACnC,UAAI,CAAC,QAAQ,CAAC,UAAW;AACzB,iBAAW,IAAI;AACf,UAAI,CAAC,cAAc;AACjB,yBAAiB,EAAE;AAAA,MACrB;AAAA,IACF;AAEA,UAAM,gBAAgB,CAAC,MAAgD;AACrE,UAAI,EAAE,QAAQ,WAAW,CAAC,EAAE,UAAU;AACpC,UAAE,eAAA;AACF,qBAAA;AAAA,MACF;AACA,kBAAY,CAAC;AAAA,IACf;AAEA,UAAM,YAAY,gBAAgB,OAAO;AACzC,UAAM,YAAY,gBAAgB,OAAO;AAEzC,UAAM,iBACJ,eAAe,KAAK,CAAC,MAAM,EAAE,UAAU,WAAW,KAAK,gBAAgB,CAAC;AAC1E,UAAM,uBACJ,iBACC,iBAAiB,cAAc,SAAS,IACvC;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,SAAS;AAAA,QACT,OAAO;AAAA,QACP,UAAU;AAAA,QACV;AAAA,QACA;AAAA,MAAA;AAAA,IAAA,IAEA;AAEN,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,WAAW;AAAA,UACT;AAAA,UACA;AAAA,UACA;AAAA,UACA,YAAY;AAAA,UACZ;AAAA,QAAA;AAAA,QAGF,UAAA;AAAA,UAAA;AAAA,YAAC;AAAA,YAAA;AAAA,cACE,GAAG;AAAA,cACJ,KAAK;AAAA,cACL,OAAO,eAAe,QAAQ;AAAA,cAC9B;AAAA,cACA;AAAA,cACA;AAAA,cACA,cAAY,aAAa;AAAA,cACzB,UAAU;AAAA,cACV,WAAW;AAAA,cACX,MAAM;AAAA,cACN,WAAW;AAAA,gBACT;AAAA,gBACA;AAAA,gBACA;AAAA,gBACA;AAAA,gBACA;AAAA,cAAA;AAAA,cAEF,OAAO;AAAA,gBACL,WAAW,GAAG,SAAS;AAAA,gBACvB,WAAW,GAAG,SAAS;AAAA,gBACvB,GAAG;AAAA,cAAA;AAAA,YACL;AAAA,UAAA;AAAA,UAGF,qBAAC,OAAA,EAAI,WAAU,qDACb,UAAA;AAAA,YAAA,oBAAC,OAAA,EAAI,WAAU,2BACZ,UAAA,kBACC;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,SAAQ;AAAA,gBACR,MAAK;AAAA,gBACL,0BAAO,SAAA,EAAQ;AAAA,gBACf,cAAY;AAAA,gBACZ,SAAS;AAAA,gBACT;AAAA,gBACA,WAAU;AAAA,cAAA;AAAA,YAAA,GAGhB;AAAA,YAEA,qBAAC,OAAA,EAAI,WAAU,2BACZ,UAAA;AAAA,cAAA;AAAA,cACD;AAAA,gBAAC;AAAA,gBAAA;AAAA,kBACC,SAAQ;AAAA,kBACR,MAAK;AAAA,kBACL,MAAM,cAAc,oBAAC,aAAA,CAAA,CAAY;AAAA,kBACjC,cAAY;AAAA,kBACZ,SAAS;AAAA,kBACT,UAAU,CAAC;AAAA,kBACX,WAAU;AAAA,gBAAA;AAAA,cAAA;AAAA,YACZ,EAAA,CACF;AAAA,UAAA,EAAA,CACF;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,EAGN;AACF;AAEA,UAAU,cAAc;AAUxB,SAAS,aAAa,EAAE,SAAS,OAAO,UAAU,UAAU,kBAAqC;AAC/F,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,SAAS,KAAK;AAC5C,QAAM,eAAe,MAAM,OAAuB,IAAI;AAEtD,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,KAAM;AACX,UAAM,cAAc,CAAC,MAAkB;AACrC,UAAI,aAAa,WAAW,CAAC,aAAa,QAAQ,SAAS,EAAE,MAAc,GAAG;AAC5E,gBAAQ,KAAK;AAAA,MACf;AAAA,IACF;AACA,aAAS,iBAAiB,aAAa,WAAW;AAClD,WAAO,MAAM,SAAS,oBAAoB,aAAa,WAAW;AAAA,EACpE,GAAG,CAAC,IAAI,CAAC;AAET,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,KAAM;AACX,UAAM,YAAY,CAAC,MAAqB;AACtC,UAAI,EAAE,QAAQ,SAAU,SAAQ,KAAK;AAAA,IACvC;AACA,aAAS,iBAAiB,WAAW,SAAS;AAC9C,WAAO,MAAM,SAAS,oBAAoB,WAAW,SAAS;AAAA,EAChE,GAAG,CAAC,IAAI,CAAC;AAET,SACE,qBAAC,OAAA,EAAI,KAAK,cAAc,WAAU,YAChC,UAAA;AAAA,IAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,MAAK;AAAA,QACL,MAAK;AAAA,QACL,iBAAe;AAAA,QACf,iBAAc;AAAA,QACd,cAAW;AAAA,QACX;AAAA,QACA,SAAS,MAAM,QAAQ,CAAC,SAAS,CAAC,IAAI;AAAA,QACtC,WAAW;AAAA,UACT;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QAAA;AAAA,QAGD,UAAA;AAAA,UAAA,gBAAgB,QACf,oBAAC,QAAA,EAAK,WAAU,6CAA6C,yBAAe,MAAK;AAAA,UAElF,gBAAgB,SAAS,QAAQ,CAAC,GAAG,SAAS;AAAA,UAC/C;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,WAAW,GAAG,2CAA2C,QAAQ,YAAY;AAAA,YAAA;AAAA,UAAA;AAAA,QAC/E;AAAA,MAAA;AAAA,IAAA;AAAA,IAGD,QACC;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,MAAK;AAAA,QACL,WAAW;AAAA,UACT;AAAA,UACA;AAAA,QAAA;AAAA,QAGD,UAAA,QAAQ,IAAI,CAAC,WACZ;AAAA,UAAC;AAAA,UAAA;AAAA,YAEC,MAAK;AAAA,YACL,UAAU;AAAA,YACV,iBAAe,OAAO,UAAU;AAAA,YAChC,WAAW;AAAA,cACT;AAAA,cACA;AAAA,cACA;AAAA,cACA,OAAO,UAAU,SAAS;AAAA,YAAA;AAAA,YAE5B,SAAS,MAAM;AACb,yBAAW,OAAO,KAAK;AACvB,sBAAQ,KAAK;AAAA,YACf;AAAA,YACA,WAAW,CAAC,MAAM;AAChB,kBAAI,EAAE,QAAQ,WAAW,EAAE,QAAQ,KAAK;AACtC,kBAAE,eAAA;AACF,2BAAW,OAAO,KAAK;AACvB,wBAAQ,KAAK;AAAA,cACf;AAAA,YACF;AAAA,YAEC,UAAA;AAAA,cAAA,OAAO,QACN,oBAAC,QAAA,EAAK,WAAU,6CAA6C,iBAAO,MAAK;AAAA,cAE1E,OAAO;AAAA,YAAA;AAAA,UAAA;AAAA,UAzBH,OAAO;AAAA,QAAA,CA2Bf;AAAA,MAAA;AAAA,IAAA;AAAA,EACH,GAEJ;AAEJ;"}
|
package/dist/index.d.ts
CHANGED
|
@@ -651,6 +651,98 @@ export declare const ChartIcon: React_2.ForwardRefExoticComponent<React_2.SVGAtt
|
|
|
651
651
|
className?: string;
|
|
652
652
|
} & React_2.RefAttributes<SVGSVGElement>>;
|
|
653
653
|
|
|
654
|
+
/**
|
|
655
|
+
* A chat-style multi-line input with an integrated toolbar containing an
|
|
656
|
+
* optional file-attach button, optional right-side controls (e.g. a model
|
|
657
|
+
* selector), and a submit button — all inside a single bordered container.
|
|
658
|
+
*
|
|
659
|
+
* Designed to behave like modern AI chat inputs: auto-grows with content,
|
|
660
|
+
* submits on Enter (Shift+Enter for newlines), and keeps controls inline.
|
|
661
|
+
*
|
|
662
|
+
* @example
|
|
663
|
+
* ```tsx
|
|
664
|
+
* <ChatInput
|
|
665
|
+
* placeholder="Type a message..."
|
|
666
|
+
* onSubmit={(text) => send(text)}
|
|
667
|
+
* />
|
|
668
|
+
* ```
|
|
669
|
+
*
|
|
670
|
+
* @example
|
|
671
|
+
* ```tsx
|
|
672
|
+
* <ChatInput
|
|
673
|
+
* placeholder="Ask the agent..."
|
|
674
|
+
* showFileButton
|
|
675
|
+
* onFileClick={() => openFilePicker()}
|
|
676
|
+
* selectOptions={[
|
|
677
|
+
* { value: "fanvue-ai", label: "Fanvue AI", icon: <AIIcon className="size-4" /> },
|
|
678
|
+
* { value: "example", label: "Example", icon: <BulbIcon className="size-4" /> },
|
|
679
|
+
* ]}
|
|
680
|
+
* selectValue="fanvue-ai"
|
|
681
|
+
* onSelectChange={(v) => setModel(v)}
|
|
682
|
+
* onSubmit={(text) => send(text)}
|
|
683
|
+
* />
|
|
684
|
+
* ```
|
|
685
|
+
*/
|
|
686
|
+
export declare const ChatInput: React_2.ForwardRefExoticComponent<ChatInputProps & React_2.RefAttributes<HTMLTextAreaElement>>;
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Props for {@link ChatInput}. Standard textarea HTML attributes are forwarded to the inner
|
|
690
|
+
* `<textarea>` except `className` (applied to the outer container), `rows` (use `minRows`), and
|
|
691
|
+
* `onSubmit` (replaced by the chat message submit callback).
|
|
692
|
+
*/
|
|
693
|
+
export declare interface ChatInputProps extends Omit<React_2.TextareaHTMLAttributes<HTMLTextAreaElement>, "className" | "rows" | "onSubmit"> {
|
|
694
|
+
/** Minimum number of visible rows. @default 1 */
|
|
695
|
+
minRows?: number;
|
|
696
|
+
/** Maximum number of visible rows before scrolling. @default 6 */
|
|
697
|
+
maxRows?: number;
|
|
698
|
+
/** Whether a submission is in progress (disables submit, shows visual feedback). @default false */
|
|
699
|
+
loading?: boolean;
|
|
700
|
+
/**
|
|
701
|
+
* Callback fired when the user submits (clicks the send button or presses Enter without Shift).
|
|
702
|
+
* Receives the current trimmed text value.
|
|
703
|
+
*/
|
|
704
|
+
onSubmit?: (value: string) => void;
|
|
705
|
+
/**
|
|
706
|
+
* When `true`, renders an "attach file" button in the bottom-left toolbar.
|
|
707
|
+
* @default false
|
|
708
|
+
*/
|
|
709
|
+
showFileButton?: boolean;
|
|
710
|
+
/** Callback fired when the attach-file button is clicked. Only relevant when `showFileButton` is `true`. */
|
|
711
|
+
onFileClick?: () => void;
|
|
712
|
+
/** Accessible label for the attach-file button. @default "Attach file" */
|
|
713
|
+
fileButtonAriaLabel?: string;
|
|
714
|
+
/** Accessible label for the submit button. @default "Send message" */
|
|
715
|
+
submitAriaLabel?: string;
|
|
716
|
+
/** Icon element for the submit button. @default `<ArrowUpIcon />` */
|
|
717
|
+
submitIcon?: React_2.ReactNode;
|
|
718
|
+
/**
|
|
719
|
+
* Optional content rendered in the bottom-right toolbar, to the left of the submit button.
|
|
720
|
+
* When provided, takes precedence over the built-in `selectOptions` dropdown.
|
|
721
|
+
*/
|
|
722
|
+
toolbarRight?: React_2.ReactNode;
|
|
723
|
+
/**
|
|
724
|
+
* Options for the built-in inline dropdown selector (e.g. model picker).
|
|
725
|
+
* Ignored when `toolbarRight` is provided.
|
|
726
|
+
*/
|
|
727
|
+
selectOptions?: ChatInputSelectOption[];
|
|
728
|
+
/** Currently selected value for the built-in dropdown. Should match one of `selectOptions[].value`. */
|
|
729
|
+
selectValue?: string;
|
|
730
|
+
/** Callback fired when the user picks a different dropdown option. */
|
|
731
|
+
onSelectChange?: (value: string) => void;
|
|
732
|
+
/** Additional className applied to the outermost container. */
|
|
733
|
+
className?: string;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/** A single option for the inline model/dropdown selector. */
|
|
737
|
+
export declare interface ChatInputSelectOption {
|
|
738
|
+
/** Unique value for this option. */
|
|
739
|
+
value: string;
|
|
740
|
+
/** Display label. */
|
|
741
|
+
label: string;
|
|
742
|
+
/** Optional icon rendered to the left of the label. */
|
|
743
|
+
icon?: React_2.ReactNode;
|
|
744
|
+
}
|
|
745
|
+
|
|
654
746
|
/**
|
|
655
747
|
* A checkbox input with optional label and helper text. Supports checked,
|
|
656
748
|
* unchecked, and indeterminate states.
|
package/dist/index.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import { BottomNavigationAction } from "./components/BottomNavigation/BottomNavi
|
|
|
15
15
|
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from "./components/Breadcrumb/Breadcrumb.mjs";
|
|
16
16
|
import { Button } from "./components/Button/Button.mjs";
|
|
17
17
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./components/Card/Card.mjs";
|
|
18
|
+
import { ChatInput } from "./components/ChatInput/ChatInput.mjs";
|
|
18
19
|
import { Checkbox } from "./components/Checkbox/Checkbox.mjs";
|
|
19
20
|
import { Chip } from "./components/Chip/Chip.mjs";
|
|
20
21
|
import { Count } from "./components/Count/Count.mjs";
|
|
@@ -229,6 +230,7 @@ export {
|
|
|
229
230
|
CardHeader,
|
|
230
231
|
CardTitle,
|
|
231
232
|
ChartIcon,
|
|
233
|
+
ChatInput,
|
|
232
234
|
CheckCircleIcon,
|
|
233
235
|
CheckIcon,
|
|
234
236
|
CheckOutlineIcon,
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fanvue/ui",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "React component library built with Tailwind CSS for Fanvue ecosystem",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org",
|
|
@@ -160,7 +160,9 @@
|
|
|
160
160
|
"rollup": ">=4.59.0",
|
|
161
161
|
"storybook": ">=10.2.10",
|
|
162
162
|
"picomatch": ">=4.0.4",
|
|
163
|
-
"micromatch>picomatch": "2.3.2"
|
|
163
|
+
"micromatch>picomatch": "2.3.2",
|
|
164
|
+
"lodash": ">=4.18.0",
|
|
165
|
+
"lodash-es": ">=4.18.0"
|
|
164
166
|
}
|
|
165
167
|
},
|
|
166
168
|
"size-limit": [
|