@fanvue/ui 2.5.2 → 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.
@@ -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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"}
@@ -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;
@@ -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":"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"}
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,
@@ -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.5.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",