@kushagradhawan/kookie-ui 0.1.49 → 0.1.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/components.css +880 -243
  2. package/dist/cjs/components/_internal/shell-bottom.d.ts +31 -5
  3. package/dist/cjs/components/_internal/shell-bottom.d.ts.map +1 -1
  4. package/dist/cjs/components/_internal/shell-bottom.js +1 -1
  5. package/dist/cjs/components/_internal/shell-bottom.js.map +3 -3
  6. package/dist/cjs/components/_internal/shell-handles.d.ts.map +1 -1
  7. package/dist/cjs/components/_internal/shell-handles.js +1 -1
  8. package/dist/cjs/components/_internal/shell-handles.js.map +3 -3
  9. package/dist/cjs/components/_internal/shell-inspector.d.ts +23 -5
  10. package/dist/cjs/components/_internal/shell-inspector.d.ts.map +1 -1
  11. package/dist/cjs/components/_internal/shell-inspector.js +1 -1
  12. package/dist/cjs/components/_internal/shell-inspector.js.map +3 -3
  13. package/dist/cjs/components/_internal/shell-sidebar.d.ts +24 -6
  14. package/dist/cjs/components/_internal/shell-sidebar.d.ts.map +1 -1
  15. package/dist/cjs/components/_internal/shell-sidebar.js +1 -1
  16. package/dist/cjs/components/_internal/shell-sidebar.js.map +3 -3
  17. package/dist/cjs/components/chatbar.d.ts +9 -2
  18. package/dist/cjs/components/chatbar.d.ts.map +1 -1
  19. package/dist/cjs/components/chatbar.js +1 -1
  20. package/dist/cjs/components/chatbar.js.map +3 -3
  21. package/dist/cjs/components/shell.context.d.ts +88 -0
  22. package/dist/cjs/components/shell.context.d.ts.map +1 -1
  23. package/dist/cjs/components/shell.context.js +1 -1
  24. package/dist/cjs/components/shell.context.js.map +3 -3
  25. package/dist/cjs/components/shell.d.ts +51 -13
  26. package/dist/cjs/components/shell.d.ts.map +1 -1
  27. package/dist/cjs/components/shell.hooks.d.ts +7 -1
  28. package/dist/cjs/components/shell.hooks.d.ts.map +1 -1
  29. package/dist/cjs/components/shell.hooks.js +1 -1
  30. package/dist/cjs/components/shell.hooks.js.map +3 -3
  31. package/dist/cjs/components/shell.js +1 -1
  32. package/dist/cjs/components/shell.js.map +3 -3
  33. package/dist/cjs/components/shell.types.d.ts +1 -0
  34. package/dist/cjs/components/shell.types.d.ts.map +1 -1
  35. package/dist/cjs/components/shell.types.js +1 -1
  36. package/dist/cjs/components/shell.types.js.map +2 -2
  37. package/dist/cjs/components/sidebar.d.ts +7 -1
  38. package/dist/cjs/components/sidebar.d.ts.map +1 -1
  39. package/dist/cjs/components/sidebar.js +1 -1
  40. package/dist/cjs/components/sidebar.js.map +3 -3
  41. package/dist/esm/components/_internal/shell-bottom.d.ts +31 -5
  42. package/dist/esm/components/_internal/shell-bottom.d.ts.map +1 -1
  43. package/dist/esm/components/_internal/shell-bottom.js +1 -1
  44. package/dist/esm/components/_internal/shell-bottom.js.map +3 -3
  45. package/dist/esm/components/_internal/shell-handles.d.ts.map +1 -1
  46. package/dist/esm/components/_internal/shell-handles.js +1 -1
  47. package/dist/esm/components/_internal/shell-handles.js.map +3 -3
  48. package/dist/esm/components/_internal/shell-inspector.d.ts +23 -5
  49. package/dist/esm/components/_internal/shell-inspector.d.ts.map +1 -1
  50. package/dist/esm/components/_internal/shell-inspector.js +1 -1
  51. package/dist/esm/components/_internal/shell-inspector.js.map +3 -3
  52. package/dist/esm/components/_internal/shell-sidebar.d.ts +24 -6
  53. package/dist/esm/components/_internal/shell-sidebar.d.ts.map +1 -1
  54. package/dist/esm/components/_internal/shell-sidebar.js +1 -1
  55. package/dist/esm/components/_internal/shell-sidebar.js.map +3 -3
  56. package/dist/esm/components/chatbar.d.ts +9 -2
  57. package/dist/esm/components/chatbar.d.ts.map +1 -1
  58. package/dist/esm/components/chatbar.js +1 -1
  59. package/dist/esm/components/chatbar.js.map +3 -3
  60. package/dist/esm/components/shell.context.d.ts +88 -0
  61. package/dist/esm/components/shell.context.d.ts.map +1 -1
  62. package/dist/esm/components/shell.context.js +1 -1
  63. package/dist/esm/components/shell.context.js.map +3 -3
  64. package/dist/esm/components/shell.d.ts +51 -13
  65. package/dist/esm/components/shell.d.ts.map +1 -1
  66. package/dist/esm/components/shell.hooks.d.ts +7 -1
  67. package/dist/esm/components/shell.hooks.d.ts.map +1 -1
  68. package/dist/esm/components/shell.hooks.js +1 -1
  69. package/dist/esm/components/shell.hooks.js.map +3 -3
  70. package/dist/esm/components/shell.js +1 -1
  71. package/dist/esm/components/shell.js.map +3 -3
  72. package/dist/esm/components/shell.types.d.ts +1 -0
  73. package/dist/esm/components/shell.types.d.ts.map +1 -1
  74. package/dist/esm/components/shell.types.js.map +2 -2
  75. package/dist/esm/components/sidebar.d.ts +7 -1
  76. package/dist/esm/components/sidebar.d.ts.map +1 -1
  77. package/dist/esm/components/sidebar.js +1 -1
  78. package/dist/esm/components/sidebar.js.map +3 -3
  79. package/package.json +14 -3
  80. package/schemas/base-button.json +1 -1
  81. package/schemas/button.json +1 -1
  82. package/schemas/icon-button.json +1 -1
  83. package/schemas/index.json +6 -6
  84. package/schemas/toggle-button.json +1 -1
  85. package/schemas/toggle-icon-button.json +1 -1
  86. package/src/components/_internal/base-menu.css +17 -18
  87. package/src/components/_internal/base-sidebar-menu.css +23 -21
  88. package/src/components/_internal/base-sidebar.css +20 -0
  89. package/src/components/_internal/shell-bottom.tsx +176 -49
  90. package/src/components/_internal/shell-handles.tsx +29 -4
  91. package/src/components/_internal/shell-inspector.tsx +175 -43
  92. package/src/components/_internal/shell-sidebar.tsx +176 -69
  93. package/src/components/chatbar.css +240 -21
  94. package/src/components/chatbar.tsx +246 -290
  95. package/src/components/sheet.css +8 -16
  96. package/src/components/shell.context.tsx +79 -0
  97. package/src/components/shell.css +28 -2
  98. package/src/components/shell.hooks.ts +35 -0
  99. package/src/components/shell.tsx +574 -214
  100. package/src/components/shell.types.ts +2 -0
  101. package/src/components/sidebar.css +233 -33
  102. package/src/components/sidebar.tsx +247 -213
  103. package/styles.css +841 -204
@@ -7,14 +7,15 @@ import { Flex } from './flex.js';
7
7
 
8
8
  import { ScrollArea } from './scroll-area.js';
9
9
  import { Slot } from './slot.js';
10
+ import { Box } from './box.js';
10
11
  import { Card } from './card.js';
11
12
  import { Text } from './text.js';
13
+ import { Inset } from './inset.js';
12
14
  import { useDropzone } from 'react-dropzone';
13
15
  import type { ComponentPropsWithout, RemovedProps } from '../helpers/component-props.js';
14
16
 
15
17
  // Avoid SSR warnings by using an isomorphic layout effect
16
- const useIsomorphicLayoutEffect =
17
- typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect;
18
+ const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect;
18
19
 
19
20
  type ExpandOn = 'none' | 'focus' | 'overflow' | 'both';
20
21
  type SendMode = 'always' | 'whenDirty' | 'never';
@@ -86,6 +87,9 @@ interface ChatbarContextValue {
86
87
  // Helpers
87
88
  appendFiles(files: File[]): void;
88
89
  appendFilesFromPaste(files: File[]): void;
90
+
91
+ // Guards
92
+ fileDialogOpenRef: React.MutableRefObject<boolean>;
89
93
  }
90
94
 
91
95
  const ChatbarContext = React.createContext<ChatbarContextValue | null>(null);
@@ -136,6 +140,13 @@ interface ChatbarRootBaseProps {
136
140
 
137
141
  size?: '1' | '2' | '3';
138
142
  variant?: 'surface' | 'outline' | 'classic' | 'ghost' | 'soft';
143
+ /** Accent color for the control (matches TextArea) */
144
+ color?: string;
145
+ /** Optional radius override (matches TextArea) */
146
+ radius?: string | number;
147
+ /** Panel/material translucency flags (matches TextArea) */
148
+ panelBackground?: 'solid' | 'translucent';
149
+ material?: 'solid' | 'translucent';
139
150
 
140
151
  width?: React.CSSProperties['width'];
141
152
  maxWidth?: React.CSSProperties['maxWidth'];
@@ -199,9 +210,7 @@ type RootElement = React.ElementRef<'div'>;
199
210
  * - `aria-expanded` on the root reflects the disclosure state of the input area.
200
211
  * - Dropzone provides proper ARIA attributes for drag and drop operations.
201
212
  */
202
- interface RootProps
203
- extends ComponentPropsWithout<'div', RemovedProps | 'onSubmit'>,
204
- ChatbarRootBaseProps {}
213
+ interface RootProps extends ComponentPropsWithout<'div', RemovedProps | 'onSubmit'>, ChatbarRootBaseProps {}
205
214
 
206
215
  const Root = React.forwardRef<RootElement, RootProps>((props, forwardedRef) => {
207
216
  const {
@@ -223,6 +232,10 @@ const Root = React.forwardRef<RootElement, RootProps>((props, forwardedRef) => {
223
232
  onSubmit,
224
233
  size = '2',
225
234
  variant,
235
+ color,
236
+ radius,
237
+ panelBackground,
238
+ material,
226
239
  width,
227
240
  maxWidth,
228
241
  asChild,
@@ -240,6 +253,7 @@ const Root = React.forwardRef<RootElement, RootProps>((props, forwardedRef) => {
240
253
  dropzone = true,
241
254
  ...divProps
242
255
  } = props;
256
+ const effectiveMaterial = material || panelBackground;
243
257
 
244
258
  const isValueControlled = valueProp != null;
245
259
  const [valueUncontrolled, setValueUncontrolled] = React.useState<string>(defaultValue);
@@ -251,23 +265,20 @@ const Root = React.forwardRef<RootElement, RootProps>((props, forwardedRef) => {
251
265
 
252
266
  const rootRef = React.useRef<HTMLDivElement>(null);
253
267
  const textareaRef = React.useRef<HTMLTextAreaElement>(null);
268
+ const fileDialogOpenRef = React.useRef<boolean>(false);
254
269
 
255
270
  // Attachments state
256
271
  // Treat `attachments` as controlled if the prop is provided, even if its value is `undefined`.
257
272
  // This avoids switching between controlled and uncontrolled when a consumer sets
258
273
  // `attachments={undefined}` to clear attachments. In that case we normalize to an empty array.
259
274
  const isAttachmentsControlled = 'attachments' in props;
260
- const [attachmentsUncontrolled, setAttachmentsUncontrolled] =
261
- React.useState<ChatbarAttachment[]>(defaultAttachments);
262
- const attachments = isAttachmentsControlled
263
- ? ((attachmentsProp ?? []) as ChatbarAttachment[])
264
- : attachmentsUncontrolled;
275
+ const [attachmentsUncontrolled, setAttachmentsUncontrolled] = React.useState<ChatbarAttachment[]>(defaultAttachments);
276
+ const attachments = isAttachmentsControlled ? ((attachmentsProp ?? []) as ChatbarAttachment[]) : attachmentsUncontrolled;
265
277
 
266
278
  // Track generated object URLs for cleanup
267
279
  const generatedUrlSetRef = React.useRef<Set<string>>(new Set());
268
280
 
269
- const toArray = (val: string | string[] | undefined) =>
270
- Array.isArray(val) ? val : typeof val === 'string' ? val.split(',').map((s) => s.trim()) : [];
281
+ const toArray = (val: string | string[] | undefined) => (Array.isArray(val) ? val : typeof val === 'string' ? val.split(',').map((s) => s.trim()) : []);
271
282
 
272
283
  const accepts = toArray(accept);
273
284
  const pasteAccepts = toArray(pasteAccept).length > 0 ? toArray(pasteAccept) : accepts;
@@ -282,8 +293,7 @@ const Root = React.forwardRef<RootElement, RootProps>((props, forwardedRef) => {
282
293
  // MIME pattern
283
294
  const [type, subtype] = pat.split('/');
284
295
  const [fmType, fmSubtype] = mime.split('/');
285
- if (type === '*' || (type === fmType && (subtype === '*' || subtype === fmSubtype)))
286
- return true;
296
+ if (type === '*' || (type === fmType && (subtype === '*' || subtype === fmSubtype))) return true;
287
297
  } else if (pat.startsWith('.')) {
288
298
  if (name.endsWith(pat)) return true;
289
299
  }
@@ -303,10 +313,7 @@ const Root = React.forwardRef<RootElement, RootProps>((props, forwardedRef) => {
303
313
  const next: ChatbarAttachment[] = [];
304
314
  const rejected: { file: File; reason: 'type' | 'size' | 'count' }[] = [];
305
315
 
306
- const remainingSlots =
307
- typeof maxAttachments === 'number'
308
- ? Math.max(maxAttachments - attachments.length, 0)
309
- : Infinity;
316
+ const remainingSlots = typeof maxAttachments === 'number' ? Math.max(maxAttachments - attachments.length, 0) : Infinity;
310
317
 
311
318
  for (const file of files) {
312
319
  if (next.length >= remainingSlots) {
@@ -345,6 +352,9 @@ const Root = React.forwardRef<RootElement, RootProps>((props, forwardedRef) => {
345
352
  const merged = attachments.concat(accepted);
346
353
  if (!isAttachmentsControlled) setAttachmentsUncontrolled(merged);
347
354
  onAttachmentsChange?.(merged);
355
+ // Ensure chatbar expands when attachments are added
356
+ if (!isOpenControlled) setOpenUncontrolled(true);
357
+ onOpenChangeProp?.(true);
348
358
  }
349
359
  if (rejected.length > 0) onAttachmentReject?.(rejected);
350
360
  };
@@ -359,10 +369,7 @@ const Root = React.forwardRef<RootElement, RootProps>((props, forwardedRef) => {
359
369
  const rejected: { file: File; reason: 'type' | 'size' | 'count' }[] = [];
360
370
 
361
371
  // Enforce maxAttachments and maxFileSize
362
- const remainingSlots =
363
- typeof maxAttachments === 'number'
364
- ? Math.max(maxAttachments - attachments.length, 0)
365
- : Infinity;
372
+ const remainingSlots = typeof maxAttachments === 'number' ? Math.max(maxAttachments - attachments.length, 0) : Infinity;
366
373
  for (const file of files) {
367
374
  if (acceptedFiles.length >= remainingSlots) {
368
375
  rejected.push({ file, reason: 'count' });
@@ -412,13 +419,16 @@ const Root = React.forwardRef<RootElement, RootProps>((props, forwardedRef) => {
412
419
  if (!rootEl) return;
413
420
  // If focus remains within root, ignore
414
421
  if (nextTarget && rootEl.contains(nextTarget)) return;
422
+ // If native file dialog is open, avoid collapsing on blur
423
+ if (fileDialogOpenRef.current) return;
415
424
  // Collapse when leaving the root if the value is empty
416
- if ((value?.trim?.() ?? '').length === 0) {
425
+ // Only collapse when both message and attachments are empty
426
+ if ((value?.trim?.() ?? '').length === 0 && attachments.length === 0) {
417
427
  if (!isOpenControlled) setOpenUncontrolled(false);
418
428
  onOpenChangeProp?.(false);
419
429
  }
420
430
  },
421
- [isOpenControlled, onOpenChangeProp, value],
431
+ [isOpenControlled, onOpenChangeProp, value, attachments],
422
432
  );
423
433
 
424
434
  // Dropzone functionality
@@ -429,12 +439,7 @@ const Root = React.forwardRef<RootElement, RootProps>((props, forwardedRef) => {
429
439
  }
430
440
  if (rejectedFiles.length > 0 && onAttachmentReject) {
431
441
  const rejections = rejectedFiles.map(({ file, errors }) => {
432
- const reason =
433
- errors[0]?.code === 'file-too-large'
434
- ? 'size'
435
- : errors[0]?.code === 'file-invalid-type'
436
- ? 'type'
437
- : 'count';
442
+ const reason = errors[0]?.code === 'file-too-large' ? 'size' : errors[0]?.code === 'file-invalid-type' ? 'type' : 'count';
438
443
  return { file, reason: reason as 'type' | 'size' | 'count' };
439
444
  });
440
445
  onAttachmentReject(rejections);
@@ -463,6 +468,27 @@ const Root = React.forwardRef<RootElement, RootProps>((props, forwardedRef) => {
463
468
  disabled: !dropzone || disabled,
464
469
  });
465
470
 
471
+ // Click-to-focus: focus textarea when clicking non-interactive areas inside the container
472
+ const isInteractiveTarget = React.useCallback((target: EventTarget | null) => {
473
+ if (!(target instanceof Element)) return false;
474
+ const el = target as Element;
475
+ if (el.closest('.rt-ChatbarDropOverlay')) return true;
476
+ return !!el.closest('button, [role="button"], a[href], input, textarea, select, [contenteditable], [tabindex]:not([tabindex="-1"])');
477
+ }, []);
478
+
479
+ const handleContainerPointerDown = React.useCallback(
480
+ (event: React.PointerEvent) => {
481
+ if (disabled) return;
482
+ if (isInteractiveTarget(event.target)) return;
483
+ if (event.pointerType === 'mouse' && event.button !== 0) return;
484
+ event.preventDefault();
485
+ textareaRef.current?.focus({ preventScroll: true });
486
+ },
487
+ [disabled, isInteractiveTarget, textareaRef],
488
+ );
489
+
490
+ // Clicking the label-wrapped Card will naturally focus the nested textarea.
491
+
466
492
  return (
467
493
  <ChatbarContext.Provider
468
494
  value={{
@@ -505,14 +531,14 @@ const Root = React.forwardRef<RootElement, RootProps>((props, forwardedRef) => {
505
531
  dropzone,
506
532
  appendFiles,
507
533
  appendFilesFromPaste,
534
+ fileDialogOpenRef,
508
535
  }}
509
536
  >
510
537
  <Comp
511
538
  {...divProps}
512
539
  ref={(node: HTMLDivElement) => {
513
540
  if (typeof forwardedRef === 'function') forwardedRef(node);
514
- else if (forwardedRef)
515
- (forwardedRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
541
+ else if (forwardedRef) (forwardedRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
516
542
  (rootRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
517
543
  }}
518
544
  className={classNames('rt-ChatbarRoot', `rt-r-size-${size}`, className)}
@@ -521,16 +547,22 @@ const Root = React.forwardRef<RootElement, RootProps>((props, forwardedRef) => {
521
547
  data-disabled={disabled ? '' : undefined}
522
548
  data-readonly={readOnly ? '' : undefined}
523
549
  data-drop-active={dropzone && isDragActive ? '' : undefined}
550
+ data-accent-color={color}
551
+ data-radius={radius as any}
552
+ data-panel-background={effectiveMaterial}
553
+ data-material={effectiveMaterial}
524
554
  aria-expanded={open}
525
555
  onBlurCapture={handleBlurCapture}
526
556
  >
527
557
  {dropzone && <input {...getInputProps()} />}
528
- <div {...(dropzone ? getRootProps() : {})} style={{ width: '100%', height: '100%' }}>
529
- <Card
530
- className="rt-ChatbarCard"
531
- size={Math.min(3, Number(size) + 1).toString() as '1' | '2' | '3'}
532
- variant={variant as any}
558
+ <div {...(dropzone ? getRootProps() : {})} style={{ width: '100%', height: '100%' }} onPointerDown={handleContainerPointerDown}>
559
+ <Box
560
+ className={classNames('rt-ChatbarBox', `rt-variant-${variant ?? 'surface'}`)}
533
561
  style={{ position: 'relative' }}
562
+ data-accent-color={color}
563
+ data-radius={radius as any}
564
+ data-panel-background={effectiveMaterial}
565
+ data-material={effectiveMaterial}
534
566
  >
535
567
  <div className="rt-ChatbarGrid">{children}</div>
536
568
  {dropzone && isDragActive && (
@@ -542,7 +574,7 @@ const Root = React.forwardRef<RootElement, RootProps>((props, forwardedRef) => {
542
574
  </div>
543
575
  </div>
544
576
  )}
545
- </Card>
577
+ </Box>
546
578
  </div>
547
579
  </Comp>
548
580
  </ChatbarContext.Provider>
@@ -588,37 +620,9 @@ interface TextareaProps extends Omit<React.ComponentPropsWithoutRef<'textarea'>,
588
620
  * on this component, as no implicit label is rendered.
589
621
  */
590
622
  const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>((props, forwardedRef) => {
591
- const {
592
- className,
593
- style,
594
- asChild,
595
- onFocus,
596
- onInput,
597
- onChange,
598
- onPaste,
599
- onKeyDown,
600
- submitOnEnter = false,
601
- rows,
602
- ...textareaProps
603
- } = props;
623
+ const { className, style, asChild, onFocus, onInput, onChange, onPaste, onKeyDown, submitOnEnter = false, rows, ...textareaProps } = props;
604
624
  const ctx = useChatbarContext();
605
- const {
606
- open,
607
- minLines,
608
- maxLines,
609
- expandOn,
610
- disabled,
611
- readOnly,
612
- setOpen,
613
- setValue,
614
- textareaRef,
615
- value,
616
- isValueControlled,
617
- sendMode,
618
- paste,
619
- appendFilesFromPaste,
620
- size,
621
- } = ctx;
625
+ const { open, minLines, maxLines, expandOn, disabled, readOnly, setOpen, setValue, textareaRef, value, isValueControlled, sendMode, paste, appendFilesFromPaste, size } = ctx;
622
626
 
623
627
  // Cached metrics to avoid repeated getComputedStyle calls
624
628
  const lineHeightRef = React.useRef<number>(0);
@@ -679,6 +683,26 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>((props, fo
679
683
  updateHeight();
680
684
  }, [updateHeight, value, open]);
681
685
 
686
+ // Auto-open on external value changes when content overflows one line
687
+ useIsomorphicLayoutEffect(() => {
688
+ if (!(expandOn === 'overflow' || expandOn === 'both')) return;
689
+ if (open) return;
690
+ const el = textareaRef.current;
691
+ if (!el) return;
692
+ // Measure overflow against compact (1-line) height
693
+ el.style.height = 'auto';
694
+ if (compactHeightRef.current === 0) {
695
+ recomputeMetrics();
696
+ }
697
+ const shouldExpand = el.scrollHeight > compactHeightRef.current + 1;
698
+ if (shouldExpand) {
699
+ setOpen(true);
700
+ // Immediately size for open state
701
+ updateHeight(true);
702
+ requestAnimationFrame(() => updateHeight(true));
703
+ }
704
+ }, [value, expandOn, open, setOpen, textareaRef, recomputeMetrics, updateHeight]);
705
+
682
706
  // Recompute metrics on mount and when size changes may affect typography
683
707
  useIsomorphicLayoutEffect(() => {
684
708
  recomputeMetrics();
@@ -706,13 +730,10 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>((props, fo
706
730
  // Dev-only warning if no accessible name is provided
707
731
  React.useEffect(() => {
708
732
  if (process.env.NODE_ENV === 'production') return;
709
- const hasLabel =
710
- textareaProps['aria-label'] != null || textareaProps['aria-labelledby'] != null;
733
+ const hasLabel = textareaProps['aria-label'] != null || textareaProps['aria-labelledby'] != null;
711
734
  if (!hasLabel) {
712
735
  // eslint-disable-next-line no-console
713
- console.warn(
714
- '[Chatbar.Textarea] Provide aria-label or aria-labelledby to ensure the control has an accessible name.',
715
- );
736
+ console.warn('[Chatbar.Textarea] Provide aria-label or aria-labelledby to ensure the control has an accessible name.');
716
737
  }
717
738
  // warn only on mount
718
739
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -768,6 +789,8 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>((props, fo
768
789
  .map((i) => i.getAsFile())
769
790
  .filter((f): f is File => !!f);
770
791
  if (files.length > 0) {
792
+ // Prevent pasting the file name or any text representation when files are present
793
+ event.preventDefault();
771
794
  appendFilesFromPaste(files);
772
795
  }
773
796
  }
@@ -790,14 +813,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>((props, fo
790
813
  onKeyDown?.(event);
791
814
  return;
792
815
  }
793
- if (
794
- event.key === 'Enter' &&
795
- !event.shiftKey &&
796
- !event.altKey &&
797
- !event.ctrlKey &&
798
- !event.metaKey &&
799
- !event.nativeEvent.isComposing
800
- ) {
816
+ if (event.key === 'Enter' && !event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey && !event.nativeEvent.isComposing) {
801
817
  if (disabled || readOnly) {
802
818
  onKeyDown?.(event);
803
819
  return;
@@ -821,17 +837,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>((props, fo
821
837
  }
822
838
  onKeyDown?.(event);
823
839
  },
824
- [
825
- submitOnEnter,
826
- disabled,
827
- readOnly,
828
- sendMode,
829
- value,
830
- isValueControlled,
831
- setValue,
832
- ctx,
833
- onKeyDown,
834
- ],
840
+ [submitOnEnter, disabled, readOnly, sendMode, value, isValueControlled, setValue, ctx, onKeyDown],
835
841
  );
836
842
 
837
843
  const Comp = asChild ? Slot : ('textarea' as any);
@@ -841,8 +847,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>((props, fo
841
847
  {...textareaProps}
842
848
  ref={(node: HTMLTextAreaElement) => {
843
849
  if (typeof forwardedRef === 'function') forwardedRef(node);
844
- else if (forwardedRef)
845
- (forwardedRef as React.MutableRefObject<HTMLTextAreaElement | null>).current = node;
850
+ else if (forwardedRef) (forwardedRef as React.MutableRefObject<HTMLTextAreaElement | null>).current = node;
846
851
  (textareaRef as React.MutableRefObject<HTMLTextAreaElement | null>).current = node;
847
852
  }}
848
853
  className="rt-ChatbarInput"
@@ -875,12 +880,7 @@ const InlineStart = React.forwardRef<HTMLDivElement, InlineSlotProps>((props, fo
875
880
  if (ctx.open) return null;
876
881
  const Comp = asChild ? Slot : ('div' as any);
877
882
  return (
878
- <Comp
879
- {...divProps}
880
- ref={forwardedRef}
881
- className={classNames('rt-ChatbarInlineStart', className)}
882
- style={style}
883
- >
883
+ <Comp {...divProps} ref={forwardedRef} className={classNames('rt-ChatbarInlineStart', className)} style={style}>
884
884
  {children}
885
885
  </Comp>
886
886
  );
@@ -893,12 +893,7 @@ const InlineEnd = React.forwardRef<HTMLDivElement, InlineSlotProps>((props, forw
893
893
  if (ctx.open) return null;
894
894
  const Comp = asChild ? Slot : ('div' as any);
895
895
  return (
896
- <Comp
897
- {...divProps}
898
- ref={forwardedRef}
899
- className={classNames('rt-ChatbarInlineEnd', className)}
900
- style={style}
901
- >
896
+ <Comp {...divProps} ref={forwardedRef} className={classNames('rt-ChatbarInlineEnd', className)} style={style}>
902
897
  {children}
903
898
  </Comp>
904
899
  );
@@ -916,35 +911,26 @@ interface AttachmentsRowProps extends Omit<React.ComponentPropsWithoutRef<'div'>
916
911
  renderAttachment?: (attachment: ChatbarAttachment) => React.ReactNode;
917
912
  }
918
913
 
919
- const AttachmentsRow = React.forwardRef<HTMLDivElement, AttachmentsRowProps>(
920
- (props, forwardedRef) => {
921
- const { asChild, forceMount, renderAttachment, className, style, ...divProps } = props;
922
- const ctx = useChatbarContext();
923
- const hasItems = ctx.attachments.length > 0;
924
- if (!hasItems && !forceMount) return null;
925
- const Comp = asChild ? Slot : ('div' as any);
926
- return (
927
- <Comp
928
- {...divProps}
929
- ref={forwardedRef}
930
- className={classNames('rt-ChatbarAttachmentsRow', className)}
931
- style={style}
932
- role="list"
933
- aria-label={divProps['aria-label'] ?? 'Attachments'}
934
- >
935
- <ScrollArea className="rt-ChatbarScrollArea" scrollbars="horizontal" size="1">
936
- <Flex align="center" gap="2" style={{ minWidth: 'fit-content' }}>
937
- {ctx.attachments.map((att) => (
938
- <Attachment key={att.id} attachment={att} asChild={!!renderAttachment}>
939
- {renderAttachment?.(att)}
940
- </Attachment>
941
- ))}
942
- </Flex>
943
- </ScrollArea>
944
- </Comp>
945
- );
946
- },
947
- );
914
+ const AttachmentsRow = React.forwardRef<HTMLDivElement, AttachmentsRowProps>((props, forwardedRef) => {
915
+ const { asChild, forceMount, renderAttachment, className, style, ...divProps } = props;
916
+ const ctx = useChatbarContext();
917
+ const hasItems = ctx.attachments.length > 0;
918
+ if (!hasItems && !forceMount) return null;
919
+ const Comp = asChild ? Slot : ('div' as any);
920
+ return (
921
+ <Comp {...divProps} ref={forwardedRef} className={classNames('rt-ChatbarAttachmentsRow', className)} style={style} role="list" aria-label={divProps['aria-label'] ?? 'Attachments'}>
922
+ <ScrollArea className="rt-ChatbarScrollArea" scrollbars="horizontal" size="1">
923
+ <Flex align="center" gap="2" style={{ minWidth: 'fit-content' }}>
924
+ {ctx.attachments.map((att) => (
925
+ <Attachment key={att.id} attachment={att} asChild={!!renderAttachment}>
926
+ {renderAttachment?.(att)}
927
+ </Attachment>
928
+ ))}
929
+ </Flex>
930
+ </ScrollArea>
931
+ </Comp>
932
+ );
933
+ });
948
934
  AttachmentsRow.displayName = 'Chatbar.AttachmentsRow';
949
935
 
950
936
  /** Default tile renderer for a single attachment. */
@@ -959,55 +945,37 @@ const Attachment = React.forwardRef<HTMLDivElement, AttachmentProps>((props, for
959
945
  const Comp = asChild ? Slot : ('div' as any);
960
946
  const isImage = !!attachment.url && attachment.type.startsWith('image/');
961
947
  return (
962
- <Comp
963
- {...divProps}
964
- ref={forwardedRef}
965
- className={classNames('rt-ChatbarAttachment', className)}
966
- style={style}
967
- role="listitem"
968
- data-kind={isImage ? 'image' : 'file'}
969
- title={attachment.name}
970
- >
948
+ <Comp {...divProps} ref={forwardedRef} className={classNames('rt-ChatbarAttachment', className)} style={style} role="listitem" data-kind={isImage ? 'image' : 'file'} title={attachment.name}>
971
949
  {children ?? (
972
- <Card size={ctx.size} variant="soft">
973
- <Flex align="center" gap="3" pr={!isImage ? '6' : undefined}>
974
- <div className="rt-ChatbarAttachmentPreview" aria-hidden>
975
- {isImage ? (
976
- <img className="rt-ChatbarAttachmentImage" src={attachment.url} alt="" />
977
- ) : (
978
- <FileTextIcon />
979
- )}
980
- </div>
981
- {!isImage && (
982
- <Flex direction="column" gap="0" style={{ minWidth: 0 }}>
983
- <Text
984
- size={ctx.size}
985
- weight="medium"
986
- style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}
987
- >
988
- {attachment.name}
989
- </Text>
990
- <Text size="1" color="gray">
991
- {Math.ceil(attachment.size / 1024)} KB
992
- </Text>
993
- </Flex>
994
- )}
995
- <IconButton
996
- className="rt-ChatbarAttachmentRemove"
997
- aria-label={`Remove ${attachment.name}`}
998
- size="1"
999
- // size={ctx.size}
1000
- variant="classic"
1001
- highContrast
1002
- color="gray"
1003
- onClick={() =>
1004
- ctx.setAttachments(ctx.attachments.filter((a) => a.id !== attachment.id))
1005
- }
1006
- >
1007
- <CloseIcon />
1008
- </IconButton>
1009
- </Flex>
1010
- </Card>
950
+ // <Card size={ctx.size} variant="surface">
951
+ <Flex align="center" gap="2" pr={!isImage ? '6' : undefined}>
952
+ <div className="rt-ChatbarAttachmentPreview" aria-hidden>
953
+ {isImage ? <img className="rt-ChatbarAttachmentImage" src={attachment.url} alt="" /> : <FileTextIcon />}
954
+ </div>
955
+ {!isImage && (
956
+ <Flex direction="column" gap="0" style={{ minWidth: 0 }}>
957
+ <Text size={ctx.size} weight="medium" style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
958
+ {attachment.name}
959
+ </Text>
960
+ <Text size="1" color="gray">
961
+ {Math.ceil(attachment.size / 1024)} KB
962
+ </Text>
963
+ </Flex>
964
+ )}
965
+ <IconButton
966
+ className="rt-ChatbarAttachmentRemove"
967
+ aria-label={`Remove ${attachment.name}`}
968
+ size="1"
969
+ // size={ctx.size}
970
+ variant="classic"
971
+ highContrast
972
+ color="gray"
973
+ onClick={() => ctx.setAttachments(ctx.attachments.filter((a) => a.id !== attachment.id))}
974
+ >
975
+ <CloseIcon />
976
+ </IconButton>
977
+ </Flex>
978
+ // </Card>
1011
979
  )}
1012
980
  </Comp>
1013
981
  );
@@ -1020,51 +988,72 @@ interface AttachTriggerProps extends React.ComponentPropsWithoutRef<'button'> {
1020
988
  multiple?: boolean;
1021
989
  }
1022
990
 
1023
- const AttachTrigger = React.forwardRef<HTMLButtonElement, AttachTriggerProps>(
1024
- (props, forwardedRef) => {
1025
- const { asChild, accept, multiple, className, style, ...buttonProps } = props;
1026
- const ctx = useChatbarContext();
1027
- const inputRef = React.useRef<HTMLInputElement | null>(null);
1028
- const Comp = asChild ? Slot : ('button' as any);
1029
- const actualAccept = (Array.isArray(accept) ? accept : (accept?.split(',') ?? [])).join(',');
1030
- return (
1031
- <>
1032
- <Comp
1033
- {...(buttonProps as any)}
1034
- ref={forwardedRef as any}
1035
- className={classNames('rt-ChatbarAttachTrigger', className)}
1036
- style={style}
1037
- type={buttonProps.type ?? 'button'}
1038
- aria-label={buttonProps['aria-label'] ?? 'Add attachments'}
1039
- onClick={(e: any) => {
1040
- // Ensure file input opens reliably by clicking it first
1041
- if (inputRef.current) {
1042
- inputRef.current.click();
1043
- }
1044
- // Then call user's onClick if provided
1045
- buttonProps.onClick?.(e);
1046
- }}
1047
- />
1048
- <input
1049
- ref={inputRef}
1050
- type="file"
1051
- accept={actualAccept}
1052
- multiple={multiple ?? ctx.multiple}
1053
- tabIndex={-1}
1054
- style={{ display: 'none' }}
1055
- onChange={(e) => {
1056
- const files = Array.from(e.currentTarget.files ?? []);
1057
- if (files.length > 0) {
1058
- ctx.appendFiles(files);
1059
- }
1060
- // Reset input value to allow selecting the same file again
1061
- e.currentTarget.value = '';
1062
- }}
1063
- />
1064
- </>
1065
- );
1066
- },
1067
- );
991
+ const AttachTrigger = React.forwardRef<HTMLButtonElement, AttachTriggerProps>((props, forwardedRef) => {
992
+ const { asChild, accept, multiple, className, style, ...buttonProps } = props;
993
+ const ctx = useChatbarContext();
994
+ const inputRef = React.useRef<HTMLInputElement | null>(null);
995
+ const Comp = asChild ? Slot : ('button' as any);
996
+ // Prefer Chatbar.Root's accept when a local accept is not provided
997
+ const mergedAccept = accept ?? ctx.accept;
998
+ const actualAccept = (Array.isArray(mergedAccept) ? mergedAccept : (mergedAccept?.split(',') ?? [])).join(',');
999
+ React.useEffect(() => {
1000
+ const handleWindowFocus = () => {
1001
+ // Reset guard when window regains focus after file dialog closes
1002
+ ctx.fileDialogOpenRef.current = false;
1003
+ };
1004
+ window.addEventListener('focus', handleWindowFocus);
1005
+ return () => window.removeEventListener('focus', handleWindowFocus);
1006
+ }, [ctx.fileDialogOpenRef]);
1007
+ return (
1008
+ <>
1009
+ <Comp
1010
+ {...(buttonProps as any)}
1011
+ ref={forwardedRef as any}
1012
+ className={classNames('rt-ChatbarAttachTrigger', className)}
1013
+ style={style}
1014
+ type={buttonProps.type ?? 'button'}
1015
+ aria-label={buttonProps['aria-label'] ?? 'Add attachments'}
1016
+ onPointerDown={(e: any) => {
1017
+ // Set guard before blur occurs (Safari fires blur before click)
1018
+ ctx.fileDialogOpenRef.current = true;
1019
+ buttonProps.onPointerDown?.(e);
1020
+ }}
1021
+ onMouseDown={(e: any) => {
1022
+ // Fallback for environments without Pointer Events
1023
+ ctx.fileDialogOpenRef.current = true;
1024
+ buttonProps.onMouseDown?.(e);
1025
+ }}
1026
+ onClick={(e: any) => {
1027
+ // Ensure file input opens reliably by clicking it first
1028
+ ctx.fileDialogOpenRef.current = true;
1029
+ if (inputRef.current) {
1030
+ inputRef.current.click();
1031
+ }
1032
+ // Then call user's onClick if provided
1033
+ buttonProps.onClick?.(e);
1034
+ }}
1035
+ />
1036
+ <input
1037
+ ref={inputRef}
1038
+ type="file"
1039
+ accept={actualAccept}
1040
+ multiple={multiple ?? ctx.multiple}
1041
+ tabIndex={-1}
1042
+ style={{ display: 'none' }}
1043
+ onChange={(e) => {
1044
+ const files = Array.from(e.currentTarget.files ?? []);
1045
+ if (files.length > 0) {
1046
+ ctx.appendFiles(files);
1047
+ }
1048
+ // File dialog closed; allow normal blur handling
1049
+ ctx.fileDialogOpenRef.current = false;
1050
+ // Reset input value to allow selecting the same file again
1051
+ e.currentTarget.value = '';
1052
+ }}
1053
+ />
1054
+ </>
1055
+ );
1056
+ });
1068
1057
  AttachTrigger.displayName = 'Chatbar.AttachTrigger';
1069
1058
  interface RowProps extends Omit<React.ComponentPropsWithoutRef<'div'>, 'children'> {
1070
1059
  asChild?: boolean;
@@ -1077,12 +1066,7 @@ const Row = React.forwardRef<HTMLDivElement, RowProps>((props, forwardedRef) =>
1077
1066
  if (!ctx.open) return null;
1078
1067
  const Comp = asChild ? Slot : ('div' as any);
1079
1068
  return (
1080
- <Comp
1081
- {...divProps}
1082
- ref={forwardedRef}
1083
- className={classNames('rt-ChatbarRow', className)}
1084
- style={style}
1085
- >
1069
+ <Comp {...divProps} ref={forwardedRef} className={classNames('rt-ChatbarRow', className)} style={style}>
1086
1070
  <Flex align="center" justify="between" width="100%">
1087
1071
  {children}
1088
1072
  </Flex>
@@ -1091,34 +1075,16 @@ const Row = React.forwardRef<HTMLDivElement, RowProps>((props, forwardedRef) =>
1091
1075
  });
1092
1076
  Row.displayName = 'Chatbar.Row';
1093
1077
 
1094
- const RowStart = React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<'div'>>(
1095
- (props, forwardedRef) => {
1096
- const { className, style, ...divProps } = props;
1097
- return (
1098
- <div
1099
- {...divProps}
1100
- ref={forwardedRef}
1101
- className={classNames('rt-ChatbarRowStart', className)}
1102
- style={style}
1103
- />
1104
- );
1105
- },
1106
- );
1078
+ const RowStart = React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<'div'>>((props, forwardedRef) => {
1079
+ const { className, style, ...divProps } = props;
1080
+ return <div {...divProps} ref={forwardedRef} className={classNames('rt-ChatbarRowStart', className)} style={style} />;
1081
+ });
1107
1082
  RowStart.displayName = 'Chatbar.RowStart';
1108
1083
 
1109
- const RowEnd = React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<'div'>>(
1110
- (props, forwardedRef) => {
1111
- const { className, style, ...divProps } = props;
1112
- return (
1113
- <div
1114
- {...divProps}
1115
- ref={forwardedRef}
1116
- className={classNames('rt-ChatbarRowEnd', className)}
1117
- style={style}
1118
- />
1119
- );
1120
- },
1121
- );
1084
+ const RowEnd = React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<'div'>>((props, forwardedRef) => {
1085
+ const { className, style, ...divProps } = props;
1086
+ return <div {...divProps} ref={forwardedRef} className={classNames('rt-ChatbarRowEnd', className)} style={style} />;
1087
+ });
1122
1088
  RowEnd.displayName = 'Chatbar.RowEnd';
1123
1089
 
1124
1090
  interface SendProps extends Omit<IconButtonProps, 'size'> {
@@ -1127,15 +1093,7 @@ interface SendProps extends Omit<IconButtonProps, 'size'> {
1127
1093
  }
1128
1094
 
1129
1095
  const Send = React.forwardRef<HTMLButtonElement, SendProps>((props, forwardedRef) => {
1130
- const {
1131
- asChild,
1132
- clearOnSend = true,
1133
- disabled,
1134
- children,
1135
- className,
1136
- style,
1137
- ...buttonProps
1138
- } = props;
1096
+ const { asChild, clearOnSend = true, disabled, children, className, style, ...buttonProps } = props;
1139
1097
  const ctx = useChatbarContext();
1140
1098
 
1141
1099
  const trimmed = ctx.value.trim();
@@ -1170,29 +1128,27 @@ const Send = React.forwardRef<HTMLButtonElement, SendProps>((props, forwardedRef
1170
1128
  onClick={handleClick}
1171
1129
  aria-label={buttonProps['aria-label'] ?? 'Send'}
1172
1130
  >
1173
- {children ?? 'Send'}
1131
+ {children ?? (
1132
+ <svg
1133
+ xmlns="http://www.w3.org/2000/svg"
1134
+ width="24"
1135
+ height="24"
1136
+ viewBox="0 0 24 24"
1137
+ fill="none"
1138
+ stroke="currentColor"
1139
+ strokeWidth="2"
1140
+ strokeLinecap="round"
1141
+ strokeLinejoin="round"
1142
+ className="lucide lucide-arrow-right-icon lucide-arrow-right"
1143
+ >
1144
+ <path d="M5 12h14" />
1145
+ <path d="m12 5 7 7-7 7" />
1146
+ </svg>
1147
+ )}
1174
1148
  </IconButton>
1175
1149
  );
1176
1150
  });
1177
1151
  Send.displayName = 'Chatbar.Send';
1178
1152
 
1179
- export {
1180
- Root,
1181
- Textarea,
1182
- InlineStart,
1183
- InlineEnd,
1184
- AttachmentsRow,
1185
- Attachment,
1186
- AttachTrigger,
1187
- Row,
1188
- RowStart,
1189
- RowEnd,
1190
- Send,
1191
- };
1192
- export type {
1193
- RootProps as ChatbarRootProps,
1194
- TextareaProps as ChatbarTextareaProps,
1195
- RowProps as ChatbarRowProps,
1196
- SendProps as ChatbarSendProps,
1197
- ChatbarAttachment,
1198
- };
1153
+ export { Root, Textarea, InlineStart, InlineEnd, AttachmentsRow, Attachment, AttachTrigger, Row, RowStart, RowEnd, Send };
1154
+ export type { RootProps as ChatbarRootProps, TextareaProps as ChatbarTextareaProps, RowProps as ChatbarRowProps, SendProps as ChatbarSendProps, ChatbarAttachment };