@noya-app/noya-designsystem 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 (49) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/CHANGELOG.md +19 -0
  3. package/dist/index.css +1 -1
  4. package/dist/index.d.mts +169 -205
  5. package/dist/index.d.ts +169 -205
  6. package/dist/index.js +5092 -13463
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +3501 -11912
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +9 -9
  11. package/src/components/ActionMenu.tsx +4 -2
  12. package/src/components/Avatar.tsx +2 -2
  13. package/src/components/Banner.tsx +29 -0
  14. package/src/components/Button.tsx +2 -2
  15. package/src/components/Chip.tsx +8 -8
  16. package/src/components/Collection.tsx +18 -4
  17. package/src/components/ComboboxMenu.tsx +10 -1
  18. package/src/components/Dialog.tsx +2 -2
  19. package/src/components/FillInputField.tsx +1 -1
  20. package/src/components/Grid.tsx +12 -11
  21. package/src/components/Label.tsx +11 -6
  22. package/src/components/LabeledField.tsx +6 -1
  23. package/src/components/List.tsx +30 -24
  24. package/src/components/ListView.tsx +10 -5
  25. package/src/components/MediaThumbnail.tsx +31 -9
  26. package/src/components/Popover.tsx +9 -6
  27. package/src/components/SearchCompletionMenu.tsx +12 -12
  28. package/src/components/Section.tsx +172 -0
  29. package/src/components/SegmentedControl.tsx +1 -1
  30. package/src/components/SelectMenu.tsx +1 -1
  31. package/src/components/Slider.tsx +1 -1
  32. package/src/components/Sortable.tsx +124 -22
  33. package/src/components/Toolbar.tsx +22 -10
  34. package/src/components/UserPointer.tsx +1 -1
  35. package/src/components/ai-assistant/AIAssistantLayout.tsx +102 -0
  36. package/src/components/connected-users-menu/ConnectedUsersMenuLayout.tsx +71 -0
  37. package/src/components/file-explorer/FileExplorerLayout.tsx +81 -0
  38. package/src/components/internal/Menu.tsx +12 -7
  39. package/src/components/internal/TextInput.tsx +7 -2
  40. package/src/components/pipeline/PipelineResultLayout.tsx +32 -0
  41. package/src/index.css +125 -118
  42. package/src/index.tsx +7 -0
  43. package/src/theme/index.ts +1 -1
  44. package/src/utils/classNames.ts +28 -3
  45. package/src/utils/combobox.ts +18 -13
  46. package/src/utils/moveTreeItem.ts +99 -0
  47. package/tailwind.config.ts +2 -239
  48. package/tailwind.d.ts +1 -1
  49. package/tsup.config.ts +0 -3
@@ -32,6 +32,12 @@ interface Props
32
32
  className?: string;
33
33
  }
34
34
 
35
+ export const popoverStyle = {
36
+ base: "rounded font-[14px] bg-popover-background shadow-[0_2px_4px_rgba(0,0,0,0.2),0_0_12px_rgba(0,0,0,0.1)] max-h-[600px] overflow-y-auto text-text-muted z-[1000]",
37
+ large: "w-[680px]",
38
+ normal: "w-[240px]",
39
+ };
40
+
35
41
  export function Popover({
36
42
  children,
37
43
  trigger,
@@ -56,12 +62,9 @@ export function Popover({
56
62
  <PopoverPrimitive.Portal>
57
63
  <PopoverPrimitive.Content
58
64
  className={cx(
59
- "rounded font-[14px] bg-popover-background shadow-[0_2px_4px_rgba(0,0,0,0.2),0_0_12px_rgba(0,0,0,0.1)] max-h-[600px] overflow-y-auto text-text-muted z-[1000]",
60
- variant === "large"
61
- ? "w-[680px]"
62
- : variant === "normal"
63
- ? "w-[240px]"
64
- : "",
65
+ popoverStyle.base,
66
+ variant === "large" && popoverStyle.large,
67
+ variant === "normal" && popoverStyle.normal,
65
68
  className
66
69
  )}
67
70
  side={side}
@@ -119,21 +119,21 @@ export const SearchCompletionMenu = forwardRef(function SearchCompletionMenu(
119
119
  (event: React.KeyboardEvent<HTMLInputElement>) => {
120
120
  handleKeyboardEvent(event.nativeEvent, getCurrentPlatform(navigator), {
121
121
  ArrowUp: () => {
122
- const nextIndex = getNextIndex(
123
- results,
124
- index,
125
- "previous",
126
- (item) => item.disabled ?? false
127
- );
122
+ const nextIndex = getNextIndex({
123
+ items: results,
124
+ currentIndex: index,
125
+ direction: "previous",
126
+ isDisabled: (item) => item.disabled ?? false,
127
+ });
128
128
  setIndex(nextIndex);
129
129
  },
130
130
  ArrowDown: () => {
131
- const nextIndex = getNextIndex(
132
- results,
133
- index,
134
- "next",
135
- (item) => item.disabled ?? false
136
- );
131
+ const nextIndex = getNextIndex({
132
+ items: results,
133
+ currentIndex: index,
134
+ direction: "next",
135
+ isDisabled: (item) => item.disabled ?? false,
136
+ });
137
137
  setIndex(nextIndex);
138
138
  },
139
139
  Tab: () => selectAndClose(results[index]!),
@@ -0,0 +1,172 @@
1
+ import {
2
+ Button,
3
+ IconButton,
4
+ IndentContext,
5
+ INPUT_HEIGHT,
6
+ InspectorPrimitives,
7
+ Spacer,
8
+ textStyles,
9
+ } from "@noya-app/noya-designsystem";
10
+ import { clientStorage, usePersistentStateString } from "@noya-app/react-utils";
11
+ import React, { CSSProperties, memo, useMemo } from "react";
12
+
13
+ type TitleIconOptions = {
14
+ inlineBlockWrapper?: boolean;
15
+ };
16
+
17
+ export type SectionProps = {
18
+ children: React.ReactNode;
19
+ title?: React.ReactNode;
20
+ onClickTitle?: () => void;
21
+ titleTextStyle?: "small" | "heading5" | "heading4" | "heading3";
22
+ right?: React.ReactNode;
23
+ hideRightWhenCollapsed?: boolean;
24
+ extraPadding?: boolean;
25
+ className?: string;
26
+ style?: CSSProperties;
27
+ titleIcon?: React.ReactNode;
28
+ titleIconOptions?: TitleIconOptions;
29
+ };
30
+
31
+ const titleIconStyle: CSSProperties = {
32
+ marginRight: "8px",
33
+ alignSelf: "flex-start",
34
+ position: "relative",
35
+ top: "2px",
36
+ };
37
+
38
+ const titleIconWrapperStyle: CSSProperties = {
39
+ display: "inline-block",
40
+ };
41
+
42
+ const SectionInternal = ({
43
+ children,
44
+ title,
45
+ onClickTitle,
46
+ titleTextStyle = "heading4",
47
+ right,
48
+ extraPadding,
49
+ className,
50
+ style: styleProp,
51
+ titleIcon,
52
+ titleIconOptions = {
53
+ inlineBlockWrapper: true,
54
+ },
55
+ }: SectionProps) => {
56
+ const style: CSSProperties = useMemo(
57
+ () => ({
58
+ display: "flex",
59
+ flexDirection: "column",
60
+ gap: "8px",
61
+ paddingTop:
62
+ titleTextStyle === "heading3"
63
+ ? "12px"
64
+ : title && extraPadding
65
+ ? "24px"
66
+ : "12px",
67
+ paddingBottom:
68
+ titleTextStyle === "heading3"
69
+ ? "12px"
70
+ : title && extraPadding
71
+ ? "12px"
72
+ : "12px",
73
+ ...styleProp,
74
+ }),
75
+ [titleTextStyle, title, extraPadding, styleProp]
76
+ );
77
+
78
+ const hasChildren = React.Children.toArray(children).some((child) => !!child);
79
+
80
+ const headerStyle: CSSProperties = useMemo(
81
+ () => ({
82
+ marginBottom: hasChildren ? "4px" : undefined,
83
+ minHeight: INPUT_HEIGHT,
84
+ }),
85
+ [hasChildren]
86
+ );
87
+
88
+ return (
89
+ <div style={style} className={className}>
90
+ {(title || titleIcon || right) && (
91
+ <InspectorPrimitives.SectionHeader style={headerStyle}>
92
+ {titleIcon && (
93
+ <div style={titleIconStyle} className={textStyles[titleTextStyle]}>
94
+ {titleIconOptions.inlineBlockWrapper ? (
95
+ <div style={titleIconWrapperStyle}>{titleIcon}</div>
96
+ ) : (
97
+ titleIcon
98
+ )}
99
+ </div>
100
+ )}
101
+ {title && (
102
+ <>
103
+ {onClickTitle ? (
104
+ <Button variant="none" onClick={onClickTitle}>
105
+ <InspectorPrimitives.Title $textStyle={titleTextStyle}>
106
+ {title}
107
+ </InspectorPrimitives.Title>
108
+ </Button>
109
+ ) : (
110
+ <InspectorPrimitives.Title $textStyle={titleTextStyle}>
111
+ {title}
112
+ </InspectorPrimitives.Title>
113
+ )}
114
+ </>
115
+ )}
116
+ <Spacer.Horizontal />
117
+ {right}
118
+ </InspectorPrimitives.SectionHeader>
119
+ )}
120
+ <IndentContext.Provider value={{ indentLevel: 1 }}>
121
+ {children}
122
+ </IndentContext.Provider>
123
+ </div>
124
+ );
125
+ };
126
+
127
+ type ExpandableSectionProps = {
128
+ storageKey: string;
129
+ initialVisibility?: "show" | "hide";
130
+ };
131
+
132
+ const ExpandableSection = ({
133
+ storageKey,
134
+ initialVisibility = "show",
135
+ ...props
136
+ }: SectionProps & ExpandableSectionProps) => {
137
+ const [visibility, setVisibility] = usePersistentStateString<"show" | "hide">(
138
+ clientStorage,
139
+ storageKey,
140
+ initialVisibility
141
+ );
142
+ const expanded = visibility === "show";
143
+
144
+ return (
145
+ <SectionInternal
146
+ {...props}
147
+ right={!props.hideRightWhenCollapsed || expanded ? props.right : null}
148
+ title={
149
+ <div className="flex gap-1 items-center">
150
+ {props.title}
151
+ <IconButton
152
+ iconName={expanded ? "ChevronDownIcon2" : "ChevronRightIcon2"}
153
+ onClick={() => setVisibility(expanded ? "hide" : "show")}
154
+ />
155
+ </div>
156
+ }
157
+ >
158
+ {expanded && props.children}
159
+ </SectionInternal>
160
+ );
161
+ };
162
+
163
+ export const Section = memo(function InspectorSection({
164
+ storageKey,
165
+ ...props
166
+ }: SectionProps & Partial<ExpandableSectionProps>) {
167
+ return storageKey ? (
168
+ <ExpandableSection {...props} storageKey={storageKey} />
169
+ ) : (
170
+ <SectionInternal {...props} />
171
+ );
172
+ });
@@ -1,3 +1,4 @@
1
+ import { memoGeneric } from "@noya-app/react-utils";
1
2
  import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
2
3
  import React, {
3
4
  createContext,
@@ -7,7 +8,6 @@ import React, {
7
8
  useContext,
8
9
  useMemo,
9
10
  } from "react";
10
- import { memoGeneric } from "../../../noya-react-utils/src/utils/reactGenerics";
11
11
  import { useLabel } from "../hooks/useLabel";
12
12
  import { cx } from "../utils/classNames";
13
13
  import { renderIcon } from "./Icons";
@@ -105,7 +105,7 @@ const SelectMenuTrigger = memoGeneric(function SelectMenuTrigger({
105
105
  {icon && <span className="pr-1.5">{renderIcon(icon)}</span>}
106
106
  {label && labelPosition === "inset" && (
107
107
  <div className={insetEndStyles}>
108
- <Label className="pr-4 !text-text-disabled" htmlFor={id}>
108
+ <Label className="pr-4 text-text-disabled" htmlFor={id}>
109
109
  {label}
110
110
  </Label>
111
111
  </div>
@@ -109,7 +109,7 @@ export const Slider = memo(function Slider({
109
109
  <RadixSlider.Track className="flex-grow h-full rounded overflow-hidden bg-input-background">
110
110
  {label && labelPosition === "inset" && (
111
111
  <div className={insetEndStyles}>
112
- <Label htmlFor={id} className="!text-text-disabled">
112
+ <Label htmlFor={id} className="text-text-disabled">
113
113
  {label}
114
114
  </Label>
115
115
  </div>
@@ -19,6 +19,7 @@ import {
19
19
  } from "@dnd-kit/sortable";
20
20
  import { memoGeneric } from "@noya-app/react-utils";
21
21
  import * as React from "react";
22
+ import { memo } from "react";
22
23
  import { createPortal } from "react-dom";
23
24
 
24
25
  export type RelativeDropPosition = "above" | "below" | "inside";
@@ -96,42 +97,67 @@ export function validateDropIndicator({
96
97
 
97
98
  const acceptsDropInside = acceptsDrop(activeIndex, overIndex, "inside");
98
99
 
99
- // If cursor is in the middle third of the element (between 1/3 and 2/3)
100
- // and dropping inside is allowed, show "inside" indicator
100
+ // Drop into the middle third if possible
101
101
  if (
102
- offsetStart >= elementStart + elementSize / 3 &&
103
- offsetStart <= elementStart + (elementSize * 2) / 3 &&
102
+ isContainedInMiddleThird(elementStart, elementSize, offsetStart) &&
104
103
  acceptsDropInside
105
- )
104
+ ) {
106
105
  return "inside";
106
+ }
107
+
108
+ const containingHalf = getContainingHalf(
109
+ elementStart,
110
+ elementSize,
111
+ offsetStart
112
+ );
107
113
 
108
- // Are we over the top or bottom half of the element?
109
- const indicator =
110
- offsetStart < elementStart + elementSize / 2 ? "above" : "below";
111
- // Drop above or below if possible, falling back to inside
112
- return acceptsDrop(activeIndex, overIndex, indicator)
113
- ? indicator
114
- : acceptsDropInside
114
+ // Drop above or below if possible
115
+ const acceptedHalf = acceptsDrop(activeIndex, overIndex, containingHalf);
116
+
117
+ return acceptedHalf
118
+ ? containingHalf
119
+ : // Lastly, check if inside but not middle third
120
+ acceptsDropInside
115
121
  ? "inside"
116
122
  : undefined;
117
123
  }
118
124
 
125
+ function isContainedInMiddleThird(
126
+ elementStart: number,
127
+ elementSize: number,
128
+ offsetStart: number
129
+ ): boolean {
130
+ return (
131
+ offsetStart >= elementStart + elementSize / 3 &&
132
+ offsetStart <= elementStart + (elementSize * 2) / 3
133
+ );
134
+ }
135
+
136
+ function getContainingHalf(
137
+ elementStart: number,
138
+ elementSize: number,
139
+ offsetStart: number
140
+ ): "above" | "below" {
141
+ if (offsetStart < elementStart + elementSize / 2) return "above";
142
+ return "below";
143
+ }
144
+
119
145
  /* ----------------------------------------------------------------------------
120
146
  * Item
121
147
  * ------------------------------------------------------------------------- */
122
148
 
123
149
  type UseSortableReturnType = ReturnType<typeof useSortable>;
124
150
 
151
+ type ItemChildrenProps<T> = {
152
+ ref: React.Ref<T>;
153
+ relativeDropPosition?: RelativeDropPosition;
154
+ style?: React.CSSProperties;
155
+ } & UseSortableReturnType["attributes"];
156
+
125
157
  interface ItemProps<T> {
126
158
  id: UniqueIdentifier;
127
159
  disabled?: boolean;
128
- children: (
129
- props: {
130
- ref: React.Ref<T>;
131
- relativeDropPosition?: RelativeDropPosition;
132
- style?: React.CSSProperties;
133
- } & UseSortableReturnType["attributes"]
134
- ) => JSX.Element;
160
+ children: (props: ItemChildrenProps<T>) => JSX.Element;
135
161
  }
136
162
 
137
163
  function SortableItem<T extends HTMLElement>({
@@ -360,7 +386,83 @@ function SortableRoot({
360
386
  );
361
387
  }
362
388
 
363
- export namespace Sortable {
364
- export const Item = memoGeneric(SortableItem);
365
- export const Root = memoGeneric(SortableRoot);
389
+ // Create a map of all subcomponents
390
+ const SortableSubcomponents = {
391
+ Root: memo(SortableRoot),
392
+ Item: memoGeneric(SortableItem),
393
+ } as const;
394
+
395
+ interface SortableProps<T, TElement extends HTMLElement>
396
+ extends Omit<RootProps, "children" | "keys" | "renderOverlay"> {
397
+ items: T[];
398
+ keyExtractor: (item: T) => UniqueIdentifier;
399
+ shouldRenderOverlay?: boolean;
400
+ renderItem: (
401
+ item: T,
402
+ props: ItemChildrenProps<TElement>,
403
+ { isOverlay }: { isOverlay: boolean }
404
+ ) => JSX.Element;
366
405
  }
406
+
407
+ const SortableComponent = memoGeneric(function Sortable<
408
+ TItem,
409
+ TElement extends HTMLElement,
410
+ >(props: SortableProps<TItem, TElement>) {
411
+ const {
412
+ renderItem,
413
+ items,
414
+ keyExtractor,
415
+ shouldRenderOverlay = true,
416
+ ...rest
417
+ } = props;
418
+
419
+ const keys = React.useMemo(
420
+ () => items.map(keyExtractor),
421
+ [items, keyExtractor]
422
+ );
423
+
424
+ const renderOverlay = React.useCallback(
425
+ (index: number) => {
426
+ return renderItem(
427
+ items[index],
428
+ {
429
+ ref: () => {},
430
+ style: {},
431
+ tabIndex: -1,
432
+ "aria-disabled": true,
433
+ "aria-pressed": false,
434
+ "aria-roledescription": "Item being dragged",
435
+ "aria-describedby": "Item being dragged",
436
+ role: "button",
437
+ },
438
+ { isOverlay: true }
439
+ );
440
+ },
441
+ [renderItem, items]
442
+ );
443
+
444
+ return (
445
+ <SortableSubcomponents.Root
446
+ {...rest}
447
+ keys={keys}
448
+ renderOverlay={shouldRenderOverlay ? renderOverlay : undefined}
449
+ >
450
+ {keys.map((key, index) => (
451
+ <SortableSubcomponents.Item key={key} id={key}>
452
+ {(props) => {
453
+ return renderItem(
454
+ items[index],
455
+ props as ItemChildrenProps<TElement>,
456
+ { isOverlay: false }
457
+ );
458
+ }}
459
+ </SortableSubcomponents.Item>
460
+ ))}
461
+ </SortableSubcomponents.Root>
462
+ );
463
+ });
464
+
465
+ // Export both the namespace and the component
466
+ const Sortable = Object.assign(SortableComponent, SortableSubcomponents);
467
+
468
+ export { Sortable };
@@ -2,8 +2,11 @@ import { DropdownChevronIcon } from "@noya-app/noya-icons";
2
2
  import { useKeyboardShortcuts } from "@noya-app/noya-keymap";
3
3
  import React from "react";
4
4
 
5
+ import { memoGeneric } from "@noya-app/react-utils";
6
+ import { cssVars } from "../theme";
5
7
  import { BaseToolbar } from "./BaseToolbar";
6
8
  import { Button } from "./Button";
9
+ import { DividerVertical } from "./Divider";
7
10
  import { DropdownMenu } from "./DropdownMenu";
8
11
  import { renderIcon } from "./Icons";
9
12
  import {
@@ -76,7 +79,11 @@ export function Toolbar<T extends string = string>({
76
79
  );
77
80
  }
78
81
 
79
- export function ToolbarMenu<T extends string>({
82
+ const activeButtonStyle = {
83
+ backgroundColor: cssVars.colors.primaryPastel,
84
+ };
85
+
86
+ export const ToolbarMenu = memoGeneric(function ToolbarMenu<T extends string>({
80
87
  items,
81
88
  onSelectMenuItem,
82
89
  }: {
@@ -86,14 +93,18 @@ export function ToolbarMenu<T extends string>({
86
93
  return (
87
94
  <>
88
95
  {items.map((item, i) => {
89
- if (item.type === "separator") return null;
90
96
  if (item.type === "sectionHeader") return null;
91
97
 
98
+ if (item.type === "separator") {
99
+ return <DividerVertical key={i} overflow={4} />;
100
+ }
101
+
92
102
  if (isSelectableMenuItem(item)) {
93
103
  return (
94
104
  <Button
95
105
  key={i}
96
106
  disabled={item.disabled}
107
+ style={item.checked ? activeButtonStyle : undefined}
97
108
  onClick={() => {
98
109
  if (onSelectMenuItem && item.value) {
99
110
  onSelectMenuItem(item.value);
@@ -101,12 +112,8 @@ export function ToolbarMenu<T extends string>({
101
112
  }}
102
113
  >
103
114
  {item.title}
104
- {item.icon && (
105
- <>
106
- <Spacer.Horizontal inline size={6} />
107
- {renderIcon(item.icon)}
108
- </>
109
- )}
115
+ {item.title && item.icon && <Spacer.Horizontal inline size={6} />}
116
+ {item.icon && renderIcon(item.icon)}
110
117
  </Button>
111
118
  );
112
119
  }
@@ -121,8 +128,13 @@ export function ToolbarMenu<T extends string>({
121
128
  }
122
129
  }}
123
130
  >
124
- <Button disabled={item.disabled}>
131
+ <Button
132
+ disabled={item.disabled}
133
+ style={item.checked ? activeButtonStyle : undefined}
134
+ >
125
135
  {item.title}
136
+ {item.title && item.icon && <Spacer.Horizontal inline size={6} />}
137
+ {item.icon && renderIcon(item.icon)}
126
138
  <Spacer.Horizontal size={6} />
127
139
  <DropdownChevronIcon />
128
140
  </Button>
@@ -131,4 +143,4 @@ export function ToolbarMenu<T extends string>({
131
143
  })}
132
144
  </>
133
145
  );
134
- }
146
+ });
@@ -94,7 +94,7 @@ const UserPointerIcon = memo(function PointerIcon({
94
94
  <polygon
95
95
  points={points}
96
96
  fill={color}
97
- stroke="var(--background)"
97
+ stroke="var(--n-background)"
98
98
  strokeWidth="0.5"
99
99
  />
100
100
  </svg>
@@ -0,0 +1,102 @@
1
+ import { IconButton } from "../IconButton";
2
+
3
+ import { cssVars } from "../../theme";
4
+
5
+ import { Avatar } from "../Avatar";
6
+ import { Logo } from "../NoyaLogo";
7
+
8
+ import React, { forwardRef } from "react";
9
+ import { INPUT_HEIGHT } from "../../theme";
10
+ import { cx } from "../../utils/classNames";
11
+ import { TextAreaRow } from "../TextArea";
12
+
13
+ export const AIAssistantLoadingIndicator = () => {
14
+ return (
15
+ <div className="flex gap-2">
16
+ <Avatar
17
+ name="Assistant"
18
+ size={INPUT_HEIGHT}
19
+ backgroundColor={cssVars.colors.primaryPastel}
20
+ >
21
+ <Logo style={{ width: 15 }} fill={cssVars.colors.primary} />
22
+ </Avatar>
23
+ <div className="animate-pulse">▋</div>
24
+ </div>
25
+ );
26
+ };
27
+
28
+ type AIAssistantInputProps = {
29
+ value: string;
30
+ onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
31
+ onSubmitClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
32
+ onKeyDown: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
33
+ disabled: boolean;
34
+ };
35
+
36
+ export const AIAssistantInput = forwardRef<
37
+ HTMLDivElement,
38
+ AIAssistantInputProps
39
+ >(({ value, onChange, onSubmitClick, onKeyDown, disabled }, ref) => (
40
+ <TextAreaRow
41
+ autoResize
42
+ autoFocus
43
+ value={value}
44
+ onChange={onChange}
45
+ ref={ref}
46
+ textAreaClassName="py-2 pl-3 !pr-9 w-full mt-3"
47
+ placeholder="Tell me what you'd like to do..."
48
+ onKeyDown={onKeyDown}
49
+ disabled={disabled}
50
+ end={
51
+ <IconButton
52
+ iconName="ArrowUpIcon"
53
+ onClick={onSubmitClick}
54
+ disabled={!value.trim() || disabled}
55
+ className="h-8 w-8 z-20 rounded-full"
56
+ size={20}
57
+ color={cssVars.colors.text}
58
+ style={{
59
+ backgroundColor: cssVars.colors.chipDefaultBg,
60
+ }}
61
+ />
62
+ }
63
+ />
64
+ ));
65
+
66
+ type AIAssistantLayoutProps = {
67
+ renderMessages: () => React.ReactNode;
68
+ renderInput: () => React.ReactNode;
69
+ isLoading: boolean;
70
+ renderLoadingIndicator?: () => React.ReactNode;
71
+ className?: string;
72
+ style?: React.CSSProperties;
73
+ };
74
+
75
+ export const AIAssistantLayout = forwardRef<
76
+ HTMLDivElement,
77
+ AIAssistantLayoutProps
78
+ >(
79
+ (
80
+ {
81
+ renderMessages,
82
+ renderInput,
83
+ isLoading,
84
+ renderLoadingIndicator,
85
+ className,
86
+ style,
87
+ }: AIAssistantLayoutProps,
88
+ ref
89
+ ) => {
90
+ return (
91
+ <div className={cx("flex flex-col flex-1", className)} style={style}>
92
+ <div className="flex-1 min-h-0 overflow-auto" ref={ref}>
93
+ <div className="flex flex-col gap-2 pb-4 pt-1">
94
+ {renderMessages()}
95
+ {isLoading ? renderLoadingIndicator?.() : null}
96
+ </div>
97
+ </div>
98
+ <div className="border-t border-divider pt-1 px-1">{renderInput()}</div>
99
+ </div>
100
+ );
101
+ }
102
+ );