@fanvue/ui 3.5.0 → 3.7.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/charts.d.ts +1 -1
- package/dist/cjs/components/Chart/ChartCard.cjs +2 -2
- package/dist/cjs/components/Chart/ChartCard.cjs.map +1 -1
- package/dist/cjs/components/ChatInput/ChatInput.cjs +128 -92
- package/dist/cjs/components/ChatInput/ChatInput.cjs.map +1 -1
- package/dist/cjs/components/Chip/Chip.cjs +46 -10
- package/dist/cjs/components/Chip/Chip.cjs.map +1 -1
- package/dist/cjs/components/Drawer/Drawer.cjs +6 -3
- package/dist/cjs/components/Drawer/Drawer.cjs.map +1 -1
- package/dist/cjs/components/DropdownMenu/DropdownMenu.cjs +20 -4
- package/dist/cjs/components/DropdownMenu/DropdownMenu.cjs.map +1 -1
- package/dist/cjs/components/Icons/TickIcon.cjs +3 -1
- package/dist/cjs/components/Icons/TickIcon.cjs.map +1 -1
- package/dist/cjs/components/Logo/Logo.cjs +107 -33
- package/dist/cjs/components/Logo/Logo.cjs.map +1 -1
- package/dist/cjs/components/Logo/agenciesIcon.cjs +6 -0
- package/dist/cjs/components/Logo/agenciesIcon.cjs.map +1 -0
- package/dist/cjs/components/Tabs/TabsList.cjs +20 -12
- package/dist/cjs/components/Tabs/TabsList.cjs.map +1 -1
- package/dist/cjs/components/Tabs/TabsTrigger.cjs +8 -7
- package/dist/cjs/components/Tabs/TabsTrigger.cjs.map +1 -1
- package/dist/components/Chart/ChartCard.mjs +2 -2
- package/dist/components/Chart/ChartCard.mjs.map +1 -1
- package/dist/components/ChatInput/ChatInput.mjs +128 -92
- package/dist/components/ChatInput/ChatInput.mjs.map +1 -1
- package/dist/components/Chip/Chip.mjs +46 -10
- package/dist/components/Chip/Chip.mjs.map +1 -1
- package/dist/components/Drawer/Drawer.mjs +6 -3
- package/dist/components/Drawer/Drawer.mjs.map +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.mjs +20 -4
- package/dist/components/DropdownMenu/DropdownMenu.mjs.map +1 -1
- package/dist/components/Icons/TickIcon.mjs +3 -1
- package/dist/components/Icons/TickIcon.mjs.map +1 -1
- package/dist/components/Logo/Logo.mjs +107 -33
- package/dist/components/Logo/Logo.mjs.map +1 -1
- package/dist/components/Logo/agenciesIcon.mjs +6 -0
- package/dist/components/Logo/agenciesIcon.mjs.map +1 -0
- package/dist/components/Tabs/TabsList.mjs +20 -12
- package/dist/components/Tabs/TabsList.mjs.map +1 -1
- package/dist/components/Tabs/TabsTrigger.mjs +8 -7
- package/dist/components/Tabs/TabsTrigger.mjs.map +1 -1
- package/dist/index.d.ts +61 -6
- package/package.json +8 -4
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { cn } from "../../utils/cn.mjs";
|
|
5
|
+
import { Drawer, DrawerTrigger, DrawerContent, DrawerHeader, DrawerTitle } from "../Drawer/Drawer.mjs";
|
|
6
|
+
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from "../DropdownMenu/DropdownMenu.mjs";
|
|
5
7
|
import { IconButton } from "../IconButton/IconButton.mjs";
|
|
6
8
|
import { AddIcon } from "../Icons/AddIcon.mjs";
|
|
7
9
|
import { ArrowUpIcon } from "../Icons/ArrowUpIcon.mjs";
|
|
8
|
-
import { CheckIcon } from "../Icons/CheckIcon.mjs";
|
|
9
10
|
import { ChevronDownIcon } from "../Icons/ChevronDownIcon.mjs";
|
|
10
11
|
import { CloseIcon } from "../Icons/CloseIcon.mjs";
|
|
12
|
+
import { TickIcon } from "../Icons/TickIcon.mjs";
|
|
11
13
|
const LINE_HEIGHT = 18;
|
|
12
14
|
const TEXTAREA_PY = 12;
|
|
13
15
|
function calculateHeight(rows) {
|
|
@@ -63,6 +65,8 @@ const ChatInput = React.forwardRef(
|
|
|
63
65
|
submitIcon,
|
|
64
66
|
toolbarRight,
|
|
65
67
|
selectOptions,
|
|
68
|
+
selectVariant = "menu",
|
|
69
|
+
selectMenuTitle,
|
|
66
70
|
selectValue,
|
|
67
71
|
selectDisabled = false,
|
|
68
72
|
onSelectChange,
|
|
@@ -98,7 +102,7 @@ const ChatInput = React.forwardRef(
|
|
|
98
102
|
}, [minRows, maxRows]);
|
|
99
103
|
React.useEffect(() => {
|
|
100
104
|
adjustHeight();
|
|
101
|
-
}, [
|
|
105
|
+
}, [adjustHeight]);
|
|
102
106
|
const handleChange = (e) => {
|
|
103
107
|
if (!isControlled) {
|
|
104
108
|
setInternalValue(e.target.value);
|
|
@@ -143,7 +147,9 @@ const ChatInput = React.forwardRef(
|
|
|
143
147
|
value: selectValue,
|
|
144
148
|
onChange: onSelectChange,
|
|
145
149
|
disabled: disabled || selectDisabled,
|
|
146
|
-
selectedOption
|
|
150
|
+
selectedOption,
|
|
151
|
+
variant: selectVariant,
|
|
152
|
+
menuTitle: selectMenuTitle
|
|
147
153
|
}
|
|
148
154
|
) : null);
|
|
149
155
|
return /* @__PURE__ */ jsxs(
|
|
@@ -198,7 +204,7 @@ const ChatInput = React.forwardRef(
|
|
|
198
204
|
"aria-label": fileButtonAriaLabel,
|
|
199
205
|
onClick: onFileClick,
|
|
200
206
|
disabled,
|
|
201
|
-
className: "sm:border sm:border-border-primary
|
|
207
|
+
className: "max-sm:-ml-2 sm:border sm:border-border-primary"
|
|
202
208
|
}
|
|
203
209
|
) }),
|
|
204
210
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
@@ -212,7 +218,7 @@ const ChatInput = React.forwardRef(
|
|
|
212
218
|
"aria-label": submitAriaLabel,
|
|
213
219
|
onClick: handleSubmit,
|
|
214
220
|
disabled: !canSubmit,
|
|
215
|
-
className: "disabled:bg-surface-secondary disabled:
|
|
221
|
+
className: "disabled:bg-surface-secondary disabled:text-icons-primary disabled:opacity-100"
|
|
216
222
|
}
|
|
217
223
|
)
|
|
218
224
|
] })
|
|
@@ -223,98 +229,128 @@ const ChatInput = React.forwardRef(
|
|
|
223
229
|
}
|
|
224
230
|
);
|
|
225
231
|
ChatInput.displayName = "ChatInput";
|
|
226
|
-
|
|
232
|
+
const SelectTriggerButton = React.forwardRef(
|
|
233
|
+
({ open, selectedOption, fallbackLabel, className, ...props }, ref) => /* @__PURE__ */ jsxs(
|
|
234
|
+
"button",
|
|
235
|
+
{
|
|
236
|
+
ref,
|
|
237
|
+
type: "button",
|
|
238
|
+
"aria-label": "Select model",
|
|
239
|
+
className: cn(
|
|
240
|
+
"typography-description-12px-semibold text-content-primary",
|
|
241
|
+
"flex items-center gap-1 rounded-md px-2 py-2",
|
|
242
|
+
"hover:bg-neutral-alphas-50 focus-visible:shadow-focus-ring focus-visible:outline-none",
|
|
243
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
244
|
+
"motion-safe:transition-colors",
|
|
245
|
+
open && "bg-neutral-alphas-50",
|
|
246
|
+
className
|
|
247
|
+
),
|
|
248
|
+
...props,
|
|
249
|
+
children: [
|
|
250
|
+
selectedOption?.icon && /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center [&>svg]:size-4", children: selectedOption.icon }),
|
|
251
|
+
selectedOption?.label ?? fallbackLabel ?? "Select",
|
|
252
|
+
/* @__PURE__ */ jsx(
|
|
253
|
+
ChevronDownIcon,
|
|
254
|
+
{
|
|
255
|
+
className: cn("size-4 motion-safe:transition-transform", open && "rotate-180")
|
|
256
|
+
}
|
|
257
|
+
)
|
|
258
|
+
]
|
|
259
|
+
}
|
|
260
|
+
)
|
|
261
|
+
);
|
|
262
|
+
SelectTriggerButton.displayName = "SelectTriggerButton";
|
|
263
|
+
function SelectedTick() {
|
|
264
|
+
return /* @__PURE__ */ jsx(TickIcon, { size: 16, className: "text-success-negative-content", "aria-hidden": "true" });
|
|
265
|
+
}
|
|
266
|
+
function InlineSelect({
|
|
267
|
+
options,
|
|
268
|
+
value,
|
|
269
|
+
onChange,
|
|
270
|
+
disabled,
|
|
271
|
+
selectedOption,
|
|
272
|
+
variant,
|
|
273
|
+
menuTitle
|
|
274
|
+
}) {
|
|
227
275
|
const [open, setOpen] = React.useState(false);
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
if (
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
type: "button",
|
|
252
|
-
role: "combobox",
|
|
253
|
-
"aria-expanded": open,
|
|
254
|
-
"aria-haspopup": "listbox",
|
|
255
|
-
"aria-label": "Select model",
|
|
256
|
-
disabled,
|
|
257
|
-
onClick: () => setOpen((prev) => !prev),
|
|
258
|
-
className: cn(
|
|
259
|
-
"typography-description-12px-semibold text-content-primary",
|
|
260
|
-
"flex items-center gap-1 rounded-md px-2 py-2",
|
|
261
|
-
"hover:bg-neutral-alphas-50 focus-visible:shadow-focus-ring focus-visible:outline-none",
|
|
262
|
-
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
263
|
-
"motion-safe:transition-colors",
|
|
264
|
-
open && "bg-neutral-alphas-50"
|
|
265
|
-
),
|
|
266
|
-
children: [
|
|
267
|
-
selectedOption?.icon && /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center [&>svg]:size-4", children: selectedOption.icon }),
|
|
268
|
-
selectedOption?.label ?? options[0]?.label ?? "Select",
|
|
269
|
-
/* @__PURE__ */ jsx(
|
|
270
|
-
ChevronDownIcon,
|
|
276
|
+
const fallbackLabel = options[0]?.label;
|
|
277
|
+
const handleOpenChange = (next) => {
|
|
278
|
+
if (disabled && next) return;
|
|
279
|
+
setOpen(next);
|
|
280
|
+
};
|
|
281
|
+
const trigger = /* @__PURE__ */ jsx(
|
|
282
|
+
SelectTriggerButton,
|
|
283
|
+
{
|
|
284
|
+
open,
|
|
285
|
+
selectedOption,
|
|
286
|
+
fallbackLabel,
|
|
287
|
+
disabled
|
|
288
|
+
}
|
|
289
|
+
);
|
|
290
|
+
if (variant === "sheet") {
|
|
291
|
+
return /* @__PURE__ */ jsxs(Drawer, { open, onOpenChange: handleOpenChange, children: [
|
|
292
|
+
/* @__PURE__ */ jsx(DrawerTrigger, { asChild: true, children: trigger }),
|
|
293
|
+
/* @__PURE__ */ jsxs(DrawerContent, { position: "bottom", variant: "sheet", children: [
|
|
294
|
+
/* @__PURE__ */ jsx(DrawerHeader, { children: /* @__PURE__ */ jsx(DrawerTitle, { className: "typography-header-heading-xs", children: menuTitle ?? "Select an option" }) }),
|
|
295
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-col gap-1 overflow-y-auto px-4 pb-4", role: "listbox", children: options.map((option) => {
|
|
296
|
+
const isSelected = option.value === value;
|
|
297
|
+
return /* @__PURE__ */ jsxs(
|
|
298
|
+
"button",
|
|
271
299
|
{
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
277
|
-
),
|
|
278
|
-
open && /* @__PURE__ */ jsx(
|
|
279
|
-
"div",
|
|
280
|
-
{
|
|
281
|
-
role: "listbox",
|
|
282
|
-
className: cn(
|
|
283
|
-
"absolute right-0 bottom-full z-10 mb-1 min-w-[180px]",
|
|
284
|
-
"overflow-hidden rounded-xs border border-border-primary bg-surface-primary p-1.5 shadow-lg"
|
|
285
|
-
),
|
|
286
|
-
children: options.map((option) => /* @__PURE__ */ jsxs(
|
|
287
|
-
"div",
|
|
288
|
-
{
|
|
289
|
-
role: "option",
|
|
290
|
-
tabIndex: 0,
|
|
291
|
-
"aria-selected": option.value === value,
|
|
292
|
-
className: cn(
|
|
293
|
-
"typography-body-small-14px-regular flex cursor-pointer items-center gap-2 rounded-xs py-2.5 pr-2 pl-3",
|
|
294
|
-
"text-content-primary hover:bg-neutral-alphas-50",
|
|
295
|
-
"focus-visible:shadow-focus-ring focus-visible:outline-none"
|
|
296
|
-
),
|
|
297
|
-
onClick: () => {
|
|
298
|
-
onChange?.(option.value);
|
|
299
|
-
setOpen(false);
|
|
300
|
-
},
|
|
301
|
-
onKeyDown: (e) => {
|
|
302
|
-
if (e.key === "Enter" || e.key === " ") {
|
|
303
|
-
e.preventDefault();
|
|
300
|
+
type: "button",
|
|
301
|
+
role: "option",
|
|
302
|
+
"aria-selected": isSelected,
|
|
303
|
+
onClick: () => {
|
|
304
304
|
onChange?.(option.value);
|
|
305
305
|
setOpen(false);
|
|
306
|
-
}
|
|
306
|
+
},
|
|
307
|
+
className: cn(
|
|
308
|
+
"flex w-full items-start gap-2 rounded-sm px-3 py-2 text-left outline-none",
|
|
309
|
+
"focus-visible:shadow-focus-ring",
|
|
310
|
+
isSelected ? "typography-body-default-16px-semibold bg-buttons-primary-default text-content-primary-inverted" : "typography-body-default-16px-regular text-content-primary hover:bg-neutral-alphas-50"
|
|
311
|
+
),
|
|
312
|
+
children: [
|
|
313
|
+
option.icon && /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center pt-1 [&>svg]:size-4", children: option.icon }),
|
|
314
|
+
/* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [
|
|
315
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: option.menuLabel ?? option.label }),
|
|
316
|
+
option.description && /* @__PURE__ */ jsx(
|
|
317
|
+
"span",
|
|
318
|
+
{
|
|
319
|
+
className: cn(
|
|
320
|
+
"typography-body-small-14px-regular truncate",
|
|
321
|
+
isSelected ? "text-content-primary-inverted" : "text-content-secondary"
|
|
322
|
+
),
|
|
323
|
+
children: option.description
|
|
324
|
+
}
|
|
325
|
+
)
|
|
326
|
+
] }),
|
|
327
|
+
isSelected && /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center pt-1", children: /* @__PURE__ */ jsx(SelectedTick, {}) })
|
|
328
|
+
]
|
|
307
329
|
},
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
330
|
+
option.value
|
|
331
|
+
);
|
|
332
|
+
}) })
|
|
333
|
+
] })
|
|
334
|
+
] });
|
|
335
|
+
}
|
|
336
|
+
return /* @__PURE__ */ jsxs(DropdownMenu, { open, onOpenChange: handleOpenChange, children: [
|
|
337
|
+
/* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: trigger }),
|
|
338
|
+
/* @__PURE__ */ jsx(DropdownMenuContent, { side: "top", align: "end", className: "min-w-[244px]", children: options.map((option) => {
|
|
339
|
+
const isSelected = option.value === value;
|
|
340
|
+
return /* @__PURE__ */ jsx(
|
|
341
|
+
DropdownMenuItem,
|
|
342
|
+
{
|
|
343
|
+
size: "40",
|
|
344
|
+
selected: isSelected,
|
|
345
|
+
description: option.description,
|
|
346
|
+
leadingIcon: option.icon ? /* @__PURE__ */ jsx("span", { className: "flex size-4 items-center [&>svg]:size-4", children: option.icon }) : void 0,
|
|
347
|
+
trailingIcon: isSelected ? /* @__PURE__ */ jsx(SelectedTick, {}) : void 0,
|
|
348
|
+
onSelect: () => onChange?.(option.value),
|
|
349
|
+
children: option.menuLabel ?? option.label
|
|
350
|
+
},
|
|
351
|
+
option.value
|
|
352
|
+
);
|
|
353
|
+
}) })
|
|
318
354
|
] });
|
|
319
355
|
}
|
|
320
356
|
export {
|
|
@@ -1 +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 { CheckIcon } from \"../Icons/CheckIcon\";\nimport { ChevronDownIcon } from \"../Icons/ChevronDownIcon\";\nimport { CloseIcon } from \"../Icons/CloseIcon\";\n\n/** A single image thumbnail in the built-in attachment strip. */\nexport interface ChatInputAttachmentItem {\n /** Stable id passed to {@link ChatInputProps.onAttachmentRemove} and used as React `key`. */\n id: string;\n /** Image URL for the thumbnail. */\n src: string;\n /** Optional value passed to the remove control `aria-label`. */\n ariaLabel?: string;\n}\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 /** When `true`, disables only the built-in dropdown selector. @default false */\n selectDisabled?: boolean;\n /** Callback fired when the user picks a different dropdown option. */\n onSelectChange?: (value: string) => void;\n /**\n * Image attachments shown in the built-in thumbnail strip. Ignored when {@link ChatInputProps.attachmentPreviews}\n * is provided (including `null`).\n */\n attachments?: ChatInputAttachmentItem[];\n /**\n * Called when the user removes a built-in thumbnail. The remove button is disabled when this is\n * omitted or the input is {@link ChatInputProps.disabled}.\n */\n onAttachmentRemove?: (id: string) => void;\n /**\n * Replaces the built-in attachment strip entirely. When set to any value other than `undefined`\n * (including `null` or `[]`), {@link ChatInputProps.attachments} is ignored.\n */\n attachmentPreviews?: React.ReactNode;\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\ninterface ChatInputDefaultAttachmentThumbnailsProps {\n attachments: ChatInputAttachmentItem[];\n onAttachmentRemove?: (id: string) => void;\n disabled?: boolean;\n}\n\nfunction ChatInputDefaultAttachmentThumbnails({\n attachments,\n onAttachmentRemove,\n disabled = false,\n}: ChatInputDefaultAttachmentThumbnailsProps) {\n return attachments.map((item) => (\n <div\n key={item.id}\n className=\"relative size-16 shrink-0 overflow-hidden rounded-sm border border-neutral-200 bg-background-secondary\"\n >\n <img src={item.src} alt=\"\" className=\"size-full object-cover\" />\n <IconButton\n variant=\"tertiary\"\n size=\"24\"\n aria-label={item.ariaLabel ? `Remove ${item.ariaLabel}` : \"Remove attachment\"}\n icon={<CloseIcon className=\"!size-3\" />}\n disabled={disabled || !onAttachmentRemove}\n onClick={() => onAttachmentRemove?.(item.id)}\n className=\"absolute top-0.5 right-0.5 size-5 bg-neutral-900/40 p-1 text-white hover:bg-neutral-900/55\"\n />\n </div>\n ));\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 *\n * @example\n * ```tsx\n * <ChatInput\n * showFileButton\n * onFileClick={() => openPicker()}\n * attachments={files}\n * onAttachmentRemove={(id) => setFiles((prev) => prev.filter((f) => f.id !== id))}\n * />\n * ```\n *\n * @example\n * ```tsx\n * <ChatInput\n * showFileButton\n * onFileClick={() => openPicker()}\n * attachmentPreviews={<CustomVideoStrip items={items} />}\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 selectDisabled = false,\n onSelectChange,\n attachments,\n onAttachmentRemove,\n attachmentPreviews,\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 useCustomAttachmentPreviews = attachmentPreviews !== undefined;\n const customAttachmentStrip = useCustomAttachmentPreviews ? attachmentPreviews : null;\n const defaultAttachmentStrip =\n !useCustomAttachmentPreviews && !!attachments?.length ? (\n <ChatInputDefaultAttachmentThumbnails\n attachments={attachments ?? []}\n disabled={disabled}\n onAttachmentRemove={onAttachmentRemove}\n />\n ) : null;\n const resolvedAttachmentStrip = customAttachmentStrip ?? defaultAttachmentStrip;\n const hasAttachmentStrip = resolvedAttachmentStrip != null;\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 || selectDisabled}\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:outline-none\",\n \"motion-safe:transition-colors\",\n disabled && \"opacity-50\",\n className,\n )}\n >\n <div className=\"flex flex-col\">\n {hasAttachmentStrip ? (\n <div className=\"flex gap-2 overflow-x-auto px-4 pt-4 pb-2 [scrollbar-width:thin]\">\n {resolvedAttachmentStrip}\n </div>\n ) : null}\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\",\n hasAttachmentStrip ? \"pt-0\" : \"pt-4\",\n \"typography-body-small-14px-regular 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 </div>\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-description-12px-semibold text-content-primary\",\n \"flex items-center gap-1 rounded-md px-2 py-2\",\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 open && \"bg-neutral-alphas-50\",\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-[180px]\",\n \"overflow-hidden rounded-xs border border-border-primary bg-surface-primary p-1.5 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-body-small-14px-regular flex cursor-pointer items-center gap-2 rounded-xs py-2.5 pr-2 pl-3\",\n \"text-content-primary hover:bg-neutral-alphas-50\",\n \"focus-visible:shadow-focus-ring focus-visible:outline-none\",\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 <span className=\"min-w-0 flex-1 truncate\">{option.label}</span>\n {option.value === value && (\n <span className=\"ml-auto flex size-4 shrink-0 items-center justify-center\">\n <CheckIcon className=\"size-4 text-content-primary\" aria-hidden=\"true\" />\n </span>\n )}\n </div>\n ))}\n </div>\n )}\n </div>\n );\n}\n"],"names":["minHeight","maxHeight"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ChatInput.mjs","sources":["../../../src/components/ChatInput/ChatInput.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerTrigger } from \"../Drawer/Drawer\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from \"../DropdownMenu/DropdownMenu\";\nimport { IconButton } from \"../IconButton/IconButton\";\nimport { AddIcon } from \"../Icons/AddIcon\";\nimport { ArrowUpIcon } from \"../Icons/ArrowUpIcon\";\nimport { ChevronDownIcon } from \"../Icons/ChevronDownIcon\";\nimport { CloseIcon } from \"../Icons/CloseIcon\";\nimport { TickIcon } from \"../Icons/TickIcon\";\n\n/** A single image thumbnail in the built-in attachment strip. */\nexport interface ChatInputAttachmentItem {\n /** Stable id passed to {@link ChatInputProps.onAttachmentRemove} and used as React `key`. */\n id: string;\n /** Image URL for the thumbnail. */\n src: string;\n /** Optional value passed to the remove control `aria-label`. */\n ariaLabel?: string;\n}\n\n/** A single option for the inline model/dropdown selector. */\nexport interface ChatInputSelectOption {\n /** Unique value for this option. */\n value: string;\n /** Short label shown on the collapsed trigger button (e.g. \"Sonnet 4.6\"). */\n label: string;\n /**\n * Optional longer title shown on the option's row inside the open menu/sheet\n * (e.g. \"Claude Sonnet 4.6\"). Falls back to {@link label} when omitted.\n */\n menuLabel?: string;\n /** Optional secondary text shown below the label in the dropdown menu. */\n description?: 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 /**\n * How the built-in selector presents its options:\n * - `\"menu\"` (default) — a dropdown anchored to the trigger, for pointer/desktop.\n * - `\"sheet\"` — a bottom sheet, for mobile/touch viewports.\n *\n * The viewport decision belongs to the consumer (it owns the breakpoint\n * source of truth), so pass e.g. `selectVariant={isDesktop ? \"menu\" : \"sheet\"}`.\n * @default \"menu\"\n */\n selectVariant?: \"menu\" | \"sheet\";\n /**\n * Title shown at the top of the `\"sheet\"` variant of the built-in selector\n * (e.g. \"Switch AI Model\"). @default \"Select an option\"\n */\n selectMenuTitle?: string;\n /** Currently selected value for the built-in dropdown. Should match one of `selectOptions[].value`. */\n selectValue?: string;\n /** When `true`, disables only the built-in dropdown selector. @default false */\n selectDisabled?: boolean;\n /** Callback fired when the user picks a different dropdown option. */\n onSelectChange?: (value: string) => void;\n /**\n * Image attachments shown in the built-in thumbnail strip. Ignored when {@link ChatInputProps.attachmentPreviews}\n * is provided (including `null`).\n */\n attachments?: ChatInputAttachmentItem[];\n /**\n * Called when the user removes a built-in thumbnail. The remove button is disabled when this is\n * omitted or the input is {@link ChatInputProps.disabled}.\n */\n onAttachmentRemove?: (id: string) => void;\n /**\n * Replaces the built-in attachment strip entirely. When set to any value other than `undefined`\n * (including `null` or `[]`), {@link ChatInputProps.attachments} is ignored.\n */\n attachmentPreviews?: React.ReactNode;\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\ninterface ChatInputDefaultAttachmentThumbnailsProps {\n attachments: ChatInputAttachmentItem[];\n onAttachmentRemove?: (id: string) => void;\n disabled?: boolean;\n}\n\nfunction ChatInputDefaultAttachmentThumbnails({\n attachments,\n onAttachmentRemove,\n disabled = false,\n}: ChatInputDefaultAttachmentThumbnailsProps) {\n return attachments.map((item) => (\n <div\n key={item.id}\n className=\"relative size-16 shrink-0 overflow-hidden rounded-sm border border-neutral-200 bg-background-secondary\"\n >\n <img src={item.src} alt=\"\" className=\"size-full object-cover\" />\n <IconButton\n variant=\"tertiary\"\n size=\"24\"\n aria-label={item.ariaLabel ? `Remove ${item.ariaLabel}` : \"Remove attachment\"}\n icon={<CloseIcon className=\"!size-3\" />}\n disabled={disabled || !onAttachmentRemove}\n onClick={() => onAttachmentRemove?.(item.id)}\n className=\"absolute top-0.5 right-0.5 size-5 bg-neutral-900/40 p-1 text-white hover:bg-neutral-900/55\"\n />\n </div>\n ));\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 *\n * @example\n * ```tsx\n * <ChatInput\n * showFileButton\n * onFileClick={() => openPicker()}\n * attachments={files}\n * onAttachmentRemove={(id) => setFiles((prev) => prev.filter((f) => f.id !== id))}\n * />\n * ```\n *\n * @example\n * ```tsx\n * <ChatInput\n * showFileButton\n * onFileClick={() => openPicker()}\n * attachmentPreviews={<CustomVideoStrip items={items} />}\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 selectVariant = \"menu\",\n selectMenuTitle,\n selectValue,\n selectDisabled = false,\n onSelectChange,\n attachments,\n onAttachmentRemove,\n attachmentPreviews,\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 }, [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 useCustomAttachmentPreviews = attachmentPreviews !== undefined;\n const customAttachmentStrip = useCustomAttachmentPreviews ? attachmentPreviews : null;\n const defaultAttachmentStrip =\n !useCustomAttachmentPreviews && !!attachments?.length ? (\n <ChatInputDefaultAttachmentThumbnails\n attachments={attachments ?? []}\n disabled={disabled}\n onAttachmentRemove={onAttachmentRemove}\n />\n ) : null;\n const resolvedAttachmentStrip = customAttachmentStrip ?? defaultAttachmentStrip;\n const hasAttachmentStrip = resolvedAttachmentStrip != null;\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 || selectDisabled}\n selectedOption={selectedOption}\n variant={selectVariant}\n menuTitle={selectMenuTitle}\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:outline-none\",\n \"motion-safe:transition-colors\",\n disabled && \"opacity-50\",\n className,\n )}\n >\n <div className=\"flex flex-col\">\n {hasAttachmentStrip ? (\n <div className=\"flex gap-2 overflow-x-auto px-4 pt-4 pb-2 [scrollbar-width:thin]\">\n {resolvedAttachmentStrip}\n </div>\n ) : null}\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\",\n hasAttachmentStrip ? \"pt-0\" : \"pt-4\",\n \"typography-body-small-14px-regular 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 </div>\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=\"max-sm:-ml-2 sm:border sm:border-border-primary\"\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:text-icons-primary disabled:opacity-100\"\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 /** Presentation: anchored dropdown (`\"menu\"`) or bottom sheet (`\"sheet\"`). */\n variant: \"menu\" | \"sheet\";\n /** Title for the bottom-sheet header. */\n menuTitle?: string;\n}\n\ninterface SelectTriggerButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n /** Whether the menu/sheet is open (drives chevron rotation + active background). */\n open: boolean;\n selectedOption?: ChatInputSelectOption;\n /** Label shown when no option is selected. */\n fallbackLabel?: string;\n}\n\n/**\n * The collapsed pill trigger (icon + short label + chevron). Shared by the\n * desktop menu and mobile sheet; spreads Radix-injected trigger props via\n * `asChild`.\n */\nconst SelectTriggerButton = React.forwardRef<HTMLButtonElement, SelectTriggerButtonProps>(\n ({ open, selectedOption, fallbackLabel, className, ...props }, ref) => (\n <button\n ref={ref}\n type=\"button\"\n aria-label=\"Select model\"\n className={cn(\n \"typography-description-12px-semibold text-content-primary\",\n \"flex items-center gap-1 rounded-md px-2 py-2\",\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 open && \"bg-neutral-alphas-50\",\n className,\n )}\n {...props}\n >\n {selectedOption?.icon && (\n <span className=\"flex shrink-0 items-center [&>svg]:size-4\">{selectedOption.icon}</span>\n )}\n {selectedOption?.label ?? fallbackLabel ?? \"Select\"}\n <ChevronDownIcon\n className={cn(\"size-4 motion-safe:transition-transform\", open && \"rotate-180\")}\n />\n </button>\n ),\n);\nSelectTriggerButton.displayName = \"SelectTriggerButton\";\n\n/** The green tick shown on the selected option (matches DropDown V2). */\nfunction SelectedTick() {\n return <TickIcon size={16} className=\"text-success-negative-content\" aria-hidden=\"true\" />;\n}\n\n/**\n * Inline model/option selector for the ChatInput toolbar. Renders the\n * design-system dropdown (DropDown V2) on desktop and a bottom sheet on mobile.\n */\nfunction InlineSelect({\n options,\n value,\n onChange,\n disabled,\n selectedOption,\n variant,\n menuTitle,\n}: InlineSelectProps) {\n const [open, setOpen] = React.useState(false);\n const fallbackLabel = options[0]?.label;\n\n // Never allow the menu/sheet to open while disabled, regardless of how the\n // open request originates (click, keyboard, programmatic).\n const handleOpenChange = (next: boolean) => {\n if (disabled && next) return;\n setOpen(next);\n };\n\n const trigger = (\n <SelectTriggerButton\n open={open}\n selectedOption={selectedOption}\n fallbackLabel={fallbackLabel}\n disabled={disabled}\n />\n );\n\n if (variant === \"sheet\") {\n return (\n <Drawer open={open} onOpenChange={handleOpenChange}>\n <DrawerTrigger asChild>{trigger}</DrawerTrigger>\n <DrawerContent position=\"bottom\" variant=\"sheet\">\n <DrawerHeader>\n <DrawerTitle className=\"typography-header-heading-xs\">\n {menuTitle ?? \"Select an option\"}\n </DrawerTitle>\n </DrawerHeader>\n {/* Mirrors DropdownMenuItem's two-line layout, which can't be reused\n here because it requires a DropdownMenu (Radix Menu) context. */}\n <div className=\"flex flex-col gap-1 overflow-y-auto px-4 pb-4\" role=\"listbox\">\n {options.map((option) => {\n const isSelected = option.value === value;\n return (\n <button\n key={option.value}\n type=\"button\"\n role=\"option\"\n aria-selected={isSelected}\n onClick={() => {\n onChange?.(option.value);\n setOpen(false);\n }}\n className={cn(\n \"flex w-full items-start gap-2 rounded-sm px-3 py-2 text-left outline-none\",\n \"focus-visible:shadow-focus-ring\",\n isSelected\n ? \"typography-body-default-16px-semibold bg-buttons-primary-default text-content-primary-inverted\"\n : \"typography-body-default-16px-regular text-content-primary hover:bg-neutral-alphas-50\",\n )}\n >\n {option.icon && (\n <span className=\"flex shrink-0 items-center pt-1 [&>svg]:size-4\">\n {option.icon}\n </span>\n )}\n <span className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n <span className=\"truncate\">{option.menuLabel ?? option.label}</span>\n {option.description && (\n <span\n className={cn(\n \"typography-body-small-14px-regular truncate\",\n isSelected ? \"text-content-primary-inverted\" : \"text-content-secondary\",\n )}\n >\n {option.description}\n </span>\n )}\n </span>\n {isSelected && (\n <span className=\"flex shrink-0 items-center pt-1\">\n <SelectedTick />\n </span>\n )}\n </button>\n );\n })}\n </div>\n </DrawerContent>\n </Drawer>\n );\n }\n\n return (\n <DropdownMenu open={open} onOpenChange={handleOpenChange}>\n <DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>\n <DropdownMenuContent side=\"top\" align=\"end\" className=\"min-w-[244px]\">\n {options.map((option) => {\n const isSelected = option.value === value;\n return (\n <DropdownMenuItem\n key={option.value}\n size=\"40\"\n selected={isSelected}\n description={option.description}\n leadingIcon={\n option.icon ? (\n <span className=\"flex size-4 items-center [&>svg]:size-4\">{option.icon}</span>\n ) : undefined\n }\n trailingIcon={isSelected ? <SelectedTick /> : undefined}\n onSelect={() => onChange?.(option.value)}\n >\n {option.menuLabel ?? option.label}\n </DropdownMenuItem>\n );\n })}\n </DropdownMenuContent>\n </DropdownMenu>\n );\n}\n"],"names":["minHeight","maxHeight"],"mappings":"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"}
|
|
@@ -23,32 +23,67 @@ const Chip = React.forwardRef(
|
|
|
23
23
|
const isInteractive = !!onClick && !asChild;
|
|
24
24
|
const Comp = asChild ? Slot : isInteractive ? "button" : "span";
|
|
25
25
|
const isDark = variant === "dark";
|
|
26
|
+
const showDottedBorder = !isDark && dotted && !selected && !asChild;
|
|
27
|
+
const dottedBorder = showDottedBorder ? /* @__PURE__ */ jsx(
|
|
28
|
+
"svg",
|
|
29
|
+
{
|
|
30
|
+
"aria-hidden": "true",
|
|
31
|
+
className: cn(
|
|
32
|
+
"pointer-events-none absolute inset-0 size-full overflow-visible",
|
|
33
|
+
disabled ? "text-buttons-chip-disabled" : "text-buttons-chip-dotted-default",
|
|
34
|
+
isInteractive && !disabled && "group-hover:text-buttons-chip-dotted-hover-stroke group-active:text-buttons-chip-dotted-hover-stroke"
|
|
35
|
+
),
|
|
36
|
+
children: /* @__PURE__ */ jsx(
|
|
37
|
+
"rect",
|
|
38
|
+
{
|
|
39
|
+
x: "0.5",
|
|
40
|
+
y: "0.5",
|
|
41
|
+
rx: variant === "square" ? 7.5 : size === "40" ? 19.5 : 15.5,
|
|
42
|
+
fill: "none",
|
|
43
|
+
stroke: "currentColor",
|
|
44
|
+
strokeWidth: "1",
|
|
45
|
+
strokeDasharray: variant === "square" && size === "40" ? "8 8" : "6 6",
|
|
46
|
+
style: { width: "calc(100% - 1px)", height: "calc(100% - 1px)" }
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
) : null;
|
|
26
51
|
return /* @__PURE__ */ jsx(
|
|
27
52
|
Comp,
|
|
28
53
|
{
|
|
29
54
|
ref,
|
|
30
55
|
"data-testid": "chip",
|
|
31
56
|
className: cn(
|
|
32
|
-
"
|
|
57
|
+
"relative inline-flex min-w-0 items-center justify-center whitespace-nowrap motion-safe:transition-colors motion-safe:duration-150",
|
|
33
58
|
// Shape
|
|
34
59
|
variant === "square" ? "rounded-xs" : "rounded-full",
|
|
35
|
-
// Size
|
|
36
|
-
size === "32" && "h-8 py-1",
|
|
37
|
-
size === "40" && "h-10 py-2.5",
|
|
60
|
+
// Size (32px uses 12px text, 40px uses 14px text per the design spec)
|
|
61
|
+
size === "32" && "typography-description-12px-semibold h-8 py-1",
|
|
62
|
+
size === "40" && "typography-body-small-14px-semibold h-10 py-2.5",
|
|
38
63
|
// Variant colors
|
|
39
64
|
isDark && "bg-neutral-alphas-600 text-content-always-white",
|
|
40
|
-
!isDark && selected && "bg-
|
|
41
|
-
|
|
42
|
-
!isDark &&
|
|
65
|
+
!isDark && selected && !dotted && "bg-buttons-chip-active text-content-primary-inverted",
|
|
66
|
+
// Active + dotted is a subtle filled state with a solid (non-dashed) border.
|
|
67
|
+
!isDark && selected && dotted && "border border-border-primary border-solid bg-inputs-inputs-primary text-content-primary",
|
|
68
|
+
!isDark && !selected && !dotted && "bg-buttons-chip-default text-content-primary",
|
|
69
|
+
// Dotted (non-selected): the dashed border is drawn via SVG (`dottedBorder`).
|
|
70
|
+
// `group` lets that SVG react to hover/active. `asChild` keeps a CSS dashed
|
|
71
|
+
// border fallback since the SVG is only rendered in the default path.
|
|
72
|
+
!isDark && !selected && dotted && "bg-transparent text-content-primary",
|
|
73
|
+
!isDark && !selected && dotted && !asChild && "group",
|
|
74
|
+
asChild && !isDark && !selected && dotted && (disabled ? "border border-buttons-chip-disabled border-dashed" : "border border-buttons-chip-dotted-default border-dashed"),
|
|
43
75
|
// Interactive
|
|
44
76
|
isInteractive && !disabled && "cursor-pointer",
|
|
45
|
-
isInteractive && !disabled && !isDark && !selected && !dotted && "hover:bg-
|
|
46
|
-
isInteractive && !disabled && !isDark && !selected && dotted && "hover:
|
|
77
|
+
isInteractive && !disabled && !isDark && !selected && !dotted && "hover:bg-buttons-chip-hover",
|
|
78
|
+
isInteractive && !disabled && !isDark && !selected && dotted && "hover:bg-neutral-alphas-50 active:bg-neutral-alphas-50",
|
|
47
79
|
// Focus
|
|
48
80
|
"focus-visible:shadow-focus-ring focus-visible:outline-none",
|
|
49
81
|
// Disabled
|
|
50
82
|
disabled && isDark && "pointer-events-none opacity-50",
|
|
51
|
-
disabled && !isDark && "pointer-events-none text-
|
|
83
|
+
disabled && !isDark && "pointer-events-none text-content-disabled",
|
|
84
|
+
// Solid (non-dotted) disabled chips get a muted fill; dotted ones stay
|
|
85
|
+
// transparent with their dashed border (drawn via SVG).
|
|
86
|
+
disabled && !isDark && !dotted && "bg-buttons-chip-disabled",
|
|
52
87
|
className
|
|
53
88
|
),
|
|
54
89
|
...isInteractive && {
|
|
@@ -61,6 +96,7 @@ const Chip = React.forwardRef(
|
|
|
61
96
|
...selected && { "data-selected": "" },
|
|
62
97
|
...props,
|
|
63
98
|
children: asChild ? /* @__PURE__ */ jsx(Slottable, { children }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
99
|
+
dottedBorder,
|
|
64
100
|
/* @__PURE__ */ jsxs("span", { className: "flex min-w-0 items-center gap-0.5 overflow-hidden px-3", children: [
|
|
65
101
|
leftDot && /* @__PURE__ */ jsx("span", { className: "size-2 shrink-0 rounded-full bg-current", "aria-hidden": "true" }),
|
|
66
102
|
leftIcon && /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center justify-center", "aria-hidden": "true", children: leftIcon }),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Chip.mjs","sources":["../../../src/components/Chip/Chip.tsx"],"sourcesContent":["import { Slot, Slottable } from \"@radix-ui/react-slot\";\nimport * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\n\n/** Visual variant of the chip. */\nexport type ChipVariant = \"rounded\" | \"square\" | \"dark\";\n/** Height of the chip in pixels. */\nexport type ChipSize = \"32\" | \"40\";\n\nexport interface ChipProps extends React.HTMLAttributes<HTMLElement> {\n /** Visual variant of the chip. @default \"rounded\" */\n variant?: ChipVariant;\n /** Height of the chip in pixels. @default \"32\" */\n size?: ChipSize;\n /** Whether the chip is in a selected (pressed) state. @default false */\n selected?: boolean;\n /** Whether the chip is disabled. @default false */\n disabled?: boolean;\n /** Whether to show a coloured status dot at the leading edge. @default false */\n leftDot?: boolean;\n /**\n * Whether the chip uses a dashed border for add/create affordances.\n * Has no effect when `variant=\"dark\"` or `selected` is `true`.\n * @default false\n */\n dotted?: boolean;\n /** Icon element displayed before the label. */\n leftIcon?: React.ReactNode;\n /** Icon element displayed after the label. */\n rightIcon?: React.ReactNode;\n /** Notification badge content (e.g. `\"99+\"`). Passed as a string for i18n support. */\n notificationLabel?: string;\n /** Click handler — when provided, the chip renders as a `<button>` for accessibility. */\n onClick?: React.MouseEventHandler<HTMLElement>;\n /** Merge props onto a child element instead of rendering a wrapper. @default false */\n asChild?: boolean;\n}\n\n/**\n * A compact element for filters, tags, or toggleable actions. When an `onClick`\n * handler is provided, the chip renders as an interactive `<button>` with\n * `aria-pressed` support.\n *\n * @example\n * ```tsx\n * <Chip selected onClick={toggle}>Music</Chip>\n * ```\n */\nexport const Chip = React.forwardRef<HTMLButtonElement, ChipProps>(\n (\n {\n className,\n variant = \"rounded\",\n size = \"32\",\n selected = false,\n disabled = false,\n leftDot = false,\n dotted = false,\n leftIcon,\n rightIcon,\n notificationLabel,\n onClick,\n asChild = false,\n children,\n ...props\n },\n ref,\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Variant-heavy UI component\n ) => {\n const isInteractive = !!onClick && !asChild;\n const Comp = asChild ? Slot : isInteractive ? \"button\" : \"span\";\n const isDark = variant === \"dark\";\n\n return (\n <Comp\n ref={ref}\n data-testid=\"chip\"\n className={cn(\n \"typography-description-12px-semibold relative inline-flex min-w-0 items-center justify-center whitespace-nowrap motion-safe:transition-colors motion-safe:duration-150\",\n // Shape\n variant === \"square\" ? \"rounded-xs\" : \"rounded-full\",\n // Size\n size === \"32\" && \"h-8 py-1\",\n size === \"40\" && \"h-10 py-2.5\",\n // Variant colors\n isDark && \"bg-neutral-alphas-600 text-content-always-white\",\n !isDark && selected && \"bg-brand-primary-muted text-content-primary\",\n !isDark && !selected && !dotted && \"bg-neutral-alphas-50 text-content-primary\",\n !isDark &&\n !selected &&\n dotted &&\n \"border border-buttons-chip-dotted-default border-dashed bg-transparent text-content-primary\",\n // Interactive\n isInteractive && !disabled && \"cursor-pointer\",\n isInteractive &&\n !disabled &&\n !isDark &&\n !selected &&\n !dotted &&\n \"hover:bg-brand-primary-muted active:bg-brand-primary-muted\",\n isInteractive &&\n !disabled &&\n !isDark &&\n !selected &&\n dotted &&\n \"hover:border-buttons-chip-dotted-hover-stroke hover:bg-neutral-alphas-50 active:border-buttons-chip-dotted-hover-stroke active:bg-neutral-alphas-50\",\n // Focus\n \"focus-visible:shadow-focus-ring focus-visible:outline-none\",\n // Disabled\n disabled && isDark && \"pointer-events-none opacity-50\",\n disabled && !isDark && \"pointer-events-none text-neutral-alphas-400\",\n className,\n )}\n {...(isInteractive && {\n type: \"button\" as const,\n disabled,\n \"aria-pressed\": selected,\n onClick,\n })}\n {...(!isInteractive && disabled && { \"aria-disabled\": true })}\n {...(selected && { \"data-selected\": \"\" })}\n {...props}\n >\n {asChild ? (\n <Slottable>{children}</Slottable>\n ) : (\n <>\n <span className=\"flex min-w-0 items-center gap-0.5 overflow-hidden px-3\">\n {leftDot && (\n <span className=\"size-2 shrink-0 rounded-full bg-current\" aria-hidden=\"true\" />\n )}\n {leftIcon && (\n <span className=\"flex shrink-0 items-center justify-center\" aria-hidden=\"true\">\n {leftIcon}\n </span>\n )}\n <span className=\"min-w-0 truncate\">{children}</span>\n {rightIcon && (\n <span\n className=\"flex size-5 shrink-0 items-center justify-center\"\n aria-hidden=\"true\"\n >\n {rightIcon}\n </span>\n )}\n </span>\n {notificationLabel && (\n <span className=\"typography-description-12px-semibold absolute -top-1 -right-1 flex h-4 min-w-4 items-center justify-center rounded-full bg-content-primary px-1 text-content-primary-inverted\">\n {notificationLabel}\n </span>\n )}\n </>\n )}\n </Comp>\n );\n },\n);\n\nChip.displayName = \"Chip\";\n"],"names":[],"mappings":";;;;;AAgDO,MAAM,OAAO,MAAM;AAAA,EACxB,CACE;AAAA,IACE;AAAA,IACA,UAAU;AAAA,IACV,OAAO;AAAA,IACP,WAAW;AAAA,IACX,WAAW;AAAA,IACX,UAAU;AAAA,IACV,SAAS;AAAA,IACT;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,UAAU;AAAA,IACV;AAAA,IACA,GAAG;AAAA,EAAA,GAEL,QAEG;AACH,UAAM,gBAAgB,CAAC,CAAC,WAAW,CAAC;AACpC,UAAM,OAAO,UAAU,OAAO,gBAAgB,WAAW;AACzD,UAAM,SAAS,YAAY;AAE3B,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA,eAAY;AAAA,QACZ,WAAW;AAAA,UACT;AAAA;AAAA,UAEA,YAAY,WAAW,eAAe;AAAA;AAAA,UAEtC,SAAS,QAAQ;AAAA,UACjB,SAAS,QAAQ;AAAA;AAAA,UAEjB,UAAU;AAAA,UACV,CAAC,UAAU,YAAY;AAAA,UACvB,CAAC,UAAU,CAAC,YAAY,CAAC,UAAU;AAAA,UACnC,CAAC,UACC,CAAC,YACD,UACA;AAAA;AAAA,UAEF,iBAAiB,CAAC,YAAY;AAAA,UAC9B,iBACE,CAAC,YACD,CAAC,UACD,CAAC,YACD,CAAC,UACD;AAAA,UACF,iBACE,CAAC,YACD,CAAC,UACD,CAAC,YACD,UACA;AAAA;AAAA,UAEF;AAAA;AAAA,UAEA,YAAY,UAAU;AAAA,UACtB,YAAY,CAAC,UAAU;AAAA,UACvB;AAAA,QAAA;AAAA,QAED,GAAI,iBAAiB;AAAA,UACpB,MAAM;AAAA,UACN;AAAA,UACA,gBAAgB;AAAA,UAChB;AAAA,QAAA;AAAA,QAED,GAAI,CAAC,iBAAiB,YAAY,EAAE,iBAAiB,KAAA;AAAA,QACrD,GAAI,YAAY,EAAE,iBAAiB,GAAA;AAAA,QACnC,GAAG;AAAA,QAEH,UAAA,UACC,oBAAC,WAAA,EAAW,SAAA,CAAS,IAErB,qBAAA,UAAA,EACE,UAAA;AAAA,UAAA,qBAAC,QAAA,EAAK,WAAU,0DACb,UAAA;AAAA,YAAA,WACC,oBAAC,QAAA,EAAK,WAAU,2CAA0C,eAAY,QAAO;AAAA,YAE9E,YACC,oBAAC,QAAA,EAAK,WAAU,6CAA4C,eAAY,QACrE,UAAA,UACH;AAAA,YAEF,oBAAC,QAAA,EAAK,WAAU,oBAAoB,SAAA,CAAS;AAAA,YAC5C,aACC;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,WAAU;AAAA,gBACV,eAAY;AAAA,gBAEX,UAAA;AAAA,cAAA;AAAA,YAAA;AAAA,UACH,GAEJ;AAAA,UACC,qBACC,oBAAC,QAAA,EAAK,WAAU,iLACb,UAAA,kBAAA,CACH;AAAA,QAAA,EAAA,CAEJ;AAAA,MAAA;AAAA,IAAA;AAAA,EAIR;AACF;AAEA,KAAK,cAAc;"}
|
|
1
|
+
{"version":3,"file":"Chip.mjs","sources":["../../../src/components/Chip/Chip.tsx"],"sourcesContent":["import { Slot, Slottable } from \"@radix-ui/react-slot\";\nimport * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\n\n/** Visual variant of the chip. */\nexport type ChipVariant = \"rounded\" | \"square\" | \"dark\";\n/** Height of the chip in pixels. */\nexport type ChipSize = \"32\" | \"40\";\n\nexport interface ChipProps extends React.HTMLAttributes<HTMLElement> {\n /** Visual variant of the chip. @default \"rounded\" */\n variant?: ChipVariant;\n /** Height of the chip in pixels. @default \"32\" */\n size?: ChipSize;\n /** Whether the chip is in a selected (pressed) state. @default false */\n selected?: boolean;\n /** Whether the chip is disabled. @default false */\n disabled?: boolean;\n /** Whether to show a coloured status dot at the leading edge. @default false */\n leftDot?: boolean;\n /**\n * Whether the chip uses a dashed border for add/create affordances.\n * When `selected`, it becomes a subtle filled state with a solid border.\n * Has no effect when `variant=\"dark\"`.\n * @default false\n */\n dotted?: boolean;\n /** Icon element displayed before the label. */\n leftIcon?: React.ReactNode;\n /** Icon element displayed after the label. */\n rightIcon?: React.ReactNode;\n /** Notification badge content (e.g. `\"99+\"`). Passed as a string for i18n support. */\n notificationLabel?: string;\n /** Click handler — when provided, the chip renders as a `<button>` for accessibility. */\n onClick?: React.MouseEventHandler<HTMLElement>;\n /** Merge props onto a child element instead of rendering a wrapper. @default false */\n asChild?: boolean;\n}\n\n/**\n * A compact element for filters, tags, or toggleable actions. When an `onClick`\n * handler is provided, the chip renders as an interactive `<button>` with\n * `aria-pressed` support.\n *\n * @example\n * ```tsx\n * <Chip selected onClick={toggle}>Music</Chip>\n * ```\n */\nexport const Chip = React.forwardRef<HTMLButtonElement, ChipProps>(\n (\n {\n className,\n variant = \"rounded\",\n size = \"32\",\n selected = false,\n disabled = false,\n leftDot = false,\n dotted = false,\n leftIcon,\n rightIcon,\n notificationLabel,\n onClick,\n asChild = false,\n children,\n ...props\n },\n ref,\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Variant-heavy UI component\n ) => {\n const isInteractive = !!onClick && !asChild;\n const Comp = asChild ? Slot : isInteractive ? \"button\" : \"span\";\n const isDark = variant === \"dark\";\n\n // The dashed border is drawn as an SVG so the dash length matches the design\n // spec (8/8 for the 40px square, 6/6 otherwise). CSS `border-dashed` only\n // renders browser-default dash lengths, which are too short. The stroke colour\n // is driven by `currentColor`, and `group-hover`/`group-active` react to the\n // chip's interactive states. Rendered only in the default (non-`asChild`) path.\n const showDottedBorder = !isDark && dotted && !selected && !asChild;\n const dottedBorder = showDottedBorder ? (\n <svg\n aria-hidden=\"true\"\n className={cn(\n \"pointer-events-none absolute inset-0 size-full overflow-visible\",\n disabled ? \"text-buttons-chip-disabled\" : \"text-buttons-chip-dotted-default\",\n isInteractive &&\n !disabled &&\n \"group-hover:text-buttons-chip-dotted-hover-stroke group-active:text-buttons-chip-dotted-hover-stroke\",\n )}\n >\n <rect\n x=\"0.5\"\n y=\"0.5\"\n rx={variant === \"square\" ? 7.5 : size === \"40\" ? 19.5 : 15.5}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1\"\n strokeDasharray={variant === \"square\" && size === \"40\" ? \"8 8\" : \"6 6\"}\n style={{ width: \"calc(100% - 1px)\", height: \"calc(100% - 1px)\" }}\n />\n </svg>\n ) : null;\n\n return (\n <Comp\n ref={ref}\n data-testid=\"chip\"\n className={cn(\n \"relative inline-flex min-w-0 items-center justify-center whitespace-nowrap motion-safe:transition-colors motion-safe:duration-150\",\n // Shape\n variant === \"square\" ? \"rounded-xs\" : \"rounded-full\",\n // Size (32px uses 12px text, 40px uses 14px text per the design spec)\n size === \"32\" && \"typography-description-12px-semibold h-8 py-1\",\n size === \"40\" && \"typography-body-small-14px-semibold h-10 py-2.5\",\n // Variant colors\n isDark && \"bg-neutral-alphas-600 text-content-always-white\",\n !isDark && selected && !dotted && \"bg-buttons-chip-active text-content-primary-inverted\",\n // Active + dotted is a subtle filled state with a solid (non-dashed) border.\n !isDark &&\n selected &&\n dotted &&\n \"border border-border-primary border-solid bg-inputs-inputs-primary text-content-primary\",\n !isDark && !selected && !dotted && \"bg-buttons-chip-default text-content-primary\",\n // Dotted (non-selected): the dashed border is drawn via SVG (`dottedBorder`).\n // `group` lets that SVG react to hover/active. `asChild` keeps a CSS dashed\n // border fallback since the SVG is only rendered in the default path.\n !isDark && !selected && dotted && \"bg-transparent text-content-primary\",\n !isDark && !selected && dotted && !asChild && \"group\",\n asChild &&\n !isDark &&\n !selected &&\n dotted &&\n (disabled\n ? \"border border-buttons-chip-disabled border-dashed\"\n : \"border border-buttons-chip-dotted-default border-dashed\"),\n // Interactive\n isInteractive && !disabled && \"cursor-pointer\",\n isInteractive &&\n !disabled &&\n !isDark &&\n !selected &&\n !dotted &&\n \"hover:bg-buttons-chip-hover\",\n isInteractive &&\n !disabled &&\n !isDark &&\n !selected &&\n dotted &&\n \"hover:bg-neutral-alphas-50 active:bg-neutral-alphas-50\",\n // Focus\n \"focus-visible:shadow-focus-ring focus-visible:outline-none\",\n // Disabled\n disabled && isDark && \"pointer-events-none opacity-50\",\n disabled && !isDark && \"pointer-events-none text-content-disabled\",\n // Solid (non-dotted) disabled chips get a muted fill; dotted ones stay\n // transparent with their dashed border (drawn via SVG).\n disabled && !isDark && !dotted && \"bg-buttons-chip-disabled\",\n className,\n )}\n {...(isInteractive && {\n type: \"button\" as const,\n disabled,\n \"aria-pressed\": selected,\n onClick,\n })}\n {...(!isInteractive && disabled && { \"aria-disabled\": true })}\n {...(selected && { \"data-selected\": \"\" })}\n {...props}\n >\n {asChild ? (\n <Slottable>{children}</Slottable>\n ) : (\n <>\n {dottedBorder}\n <span className=\"flex min-w-0 items-center gap-0.5 overflow-hidden px-3\">\n {leftDot && (\n <span className=\"size-2 shrink-0 rounded-full bg-current\" aria-hidden=\"true\" />\n )}\n {leftIcon && (\n <span className=\"flex shrink-0 items-center justify-center\" aria-hidden=\"true\">\n {leftIcon}\n </span>\n )}\n <span className=\"min-w-0 truncate\">{children}</span>\n {rightIcon && (\n <span\n className=\"flex size-5 shrink-0 items-center justify-center\"\n aria-hidden=\"true\"\n >\n {rightIcon}\n </span>\n )}\n </span>\n {notificationLabel && (\n <span className=\"typography-description-12px-semibold absolute -top-1 -right-1 flex h-4 min-w-4 items-center justify-center rounded-full bg-content-primary px-1 text-content-primary-inverted\">\n {notificationLabel}\n </span>\n )}\n </>\n )}\n </Comp>\n );\n },\n);\n\nChip.displayName = \"Chip\";\n"],"names":[],"mappings":"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"}
|
|
@@ -6,6 +6,7 @@ import { cn } from "../../utils/cn.mjs";
|
|
|
6
6
|
import { useSuppressClickAfterDrag } from "../../utils/useSuppressClickAfterDrag.mjs";
|
|
7
7
|
import { IconButton } from "../IconButton/IconButton.mjs";
|
|
8
8
|
import { CloseIcon } from "../Icons/CloseIcon.mjs";
|
|
9
|
+
const SHEET_CLASSES = "rounded-t-xl border border-modal-stroke bg-modal-background shadow-blur-menu backdrop-blur-[4px]";
|
|
9
10
|
const DrawerContext = React.createContext({
|
|
10
11
|
overlay: true
|
|
11
12
|
});
|
|
@@ -45,6 +46,7 @@ const DrawerContent = React.forwardRef(
|
|
|
45
46
|
className,
|
|
46
47
|
position = "right",
|
|
47
48
|
size = "sm",
|
|
49
|
+
variant = "panel",
|
|
48
50
|
overlay: overlayProp,
|
|
49
51
|
overlayProps,
|
|
50
52
|
style,
|
|
@@ -82,6 +84,7 @@ const DrawerContent = React.forwardRef(
|
|
|
82
84
|
"data-[state=open]:duration-200 data-[state=open]:ease-out",
|
|
83
85
|
SLIDE_CLASSES[position],
|
|
84
86
|
sizeClass,
|
|
87
|
+
variant === "sheet" && SHEET_CLASSES,
|
|
85
88
|
className
|
|
86
89
|
),
|
|
87
90
|
...props,
|
|
@@ -93,15 +96,15 @@ const DrawerContent = React.forwardRef(
|
|
|
93
96
|
);
|
|
94
97
|
DrawerContent.displayName = "DrawerContent";
|
|
95
98
|
const DrawerHeader = React.forwardRef(
|
|
96
|
-
({ className, showClose = true, closeLabel = "Close drawer", children, ...props }, ref) => /* @__PURE__ */ jsxs("div", { ref, className: cn("flex items-
|
|
99
|
+
({ className, showClose = true, closeLabel = "Close drawer", children, ...props }, ref) => /* @__PURE__ */ jsxs("div", { ref, className: cn("flex items-center gap-2 p-4", className), ...props, children: [
|
|
97
100
|
/* @__PURE__ */ jsx("div", { className: "flex min-w-0 flex-1 flex-col gap-1.5", children }),
|
|
98
101
|
showClose && /* @__PURE__ */ jsx(DialogPrimitive.Close, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
99
102
|
IconButton,
|
|
100
103
|
{
|
|
101
104
|
icon: /* @__PURE__ */ jsx(CloseIcon, {}),
|
|
102
105
|
"aria-label": closeLabel,
|
|
103
|
-
variant: "
|
|
104
|
-
size: "
|
|
106
|
+
variant: "secondary",
|
|
107
|
+
size: "32",
|
|
105
108
|
className: "shrink-0"
|
|
106
109
|
}
|
|
107
110
|
) })
|