@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.
- package/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +19 -0
- package/dist/index.css +1 -1
- package/dist/index.d.mts +169 -205
- package/dist/index.d.ts +169 -205
- package/dist/index.js +5092 -13463
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3501 -11912
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -9
- package/src/components/ActionMenu.tsx +4 -2
- package/src/components/Avatar.tsx +2 -2
- package/src/components/Banner.tsx +29 -0
- package/src/components/Button.tsx +2 -2
- package/src/components/Chip.tsx +8 -8
- package/src/components/Collection.tsx +18 -4
- package/src/components/ComboboxMenu.tsx +10 -1
- package/src/components/Dialog.tsx +2 -2
- package/src/components/FillInputField.tsx +1 -1
- package/src/components/Grid.tsx +12 -11
- package/src/components/Label.tsx +11 -6
- package/src/components/LabeledField.tsx +6 -1
- package/src/components/List.tsx +30 -24
- package/src/components/ListView.tsx +10 -5
- package/src/components/MediaThumbnail.tsx +31 -9
- package/src/components/Popover.tsx +9 -6
- package/src/components/SearchCompletionMenu.tsx +12 -12
- package/src/components/Section.tsx +172 -0
- package/src/components/SegmentedControl.tsx +1 -1
- package/src/components/SelectMenu.tsx +1 -1
- package/src/components/Slider.tsx +1 -1
- package/src/components/Sortable.tsx +124 -22
- package/src/components/Toolbar.tsx +22 -10
- package/src/components/UserPointer.tsx +1 -1
- package/src/components/ai-assistant/AIAssistantLayout.tsx +102 -0
- package/src/components/connected-users-menu/ConnectedUsersMenuLayout.tsx +71 -0
- package/src/components/file-explorer/FileExplorerLayout.tsx +81 -0
- package/src/components/internal/Menu.tsx +12 -7
- package/src/components/internal/TextInput.tsx +7 -2
- package/src/components/pipeline/PipelineResultLayout.tsx +32 -0
- package/src/index.css +125 -118
- package/src/index.tsx +7 -0
- package/src/theme/index.ts +1 -1
- package/src/utils/classNames.ts +28 -3
- package/src/utils/combobox.ts +18 -13
- package/src/utils/moveTreeItem.ts +99 -0
- package/tailwind.config.ts +2 -239
- package/tailwind.d.ts +1 -1
- 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
|
-
|
|
60
|
-
variant === "large"
|
|
61
|
-
|
|
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
|
|
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="
|
|
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
|
-
//
|
|
100
|
-
// and dropping inside is allowed, show "inside" indicator
|
|
100
|
+
// Drop into the middle third if possible
|
|
101
101
|
if (
|
|
102
|
-
|
|
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
|
-
//
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
});
|
|
@@ -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
|
+
);
|