@noya-app/noya-designsystem 0.1.47 → 0.1.49

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 (55) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/CHANGELOG.md +18 -0
  3. package/dist/index.css +1 -1
  4. package/dist/index.d.mts +389 -241
  5. package/dist/index.d.ts +389 -241
  6. package/dist/index.js +4802 -3990
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +4715 -3915
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +7 -7
  11. package/src/__tests__/validateDropIndicator.test.ts +263 -0
  12. package/src/components/ActionMenu.tsx +51 -0
  13. package/src/components/ActivityIndicator.tsx +7 -1
  14. package/src/components/BaseToolbar.tsx +2 -2
  15. package/src/components/Checkbox.tsx +2 -2
  16. package/src/components/Chip.tsx +7 -6
  17. package/src/components/Collection.tsx +30 -2
  18. package/src/components/Combobox.tsx +27 -15
  19. package/src/components/ComboboxMenu.tsx +15 -6
  20. package/src/components/Dialog.tsx +17 -12
  21. package/src/components/DimensionInput.tsx +14 -12
  22. package/src/components/Drawer.tsx +98 -0
  23. package/src/components/EditableText.tsx +3 -1
  24. package/src/components/FillInputField.tsx +2 -2
  25. package/src/components/Grid.tsx +161 -111
  26. package/src/components/GridView.tsx +73 -41
  27. package/src/components/InputField.tsx +148 -208
  28. package/src/components/Label.tsx +4 -68
  29. package/src/components/LabeledField.tsx +7 -1
  30. package/src/components/List.tsx +135 -50
  31. package/src/components/ListView.tsx +63 -43
  32. package/src/components/MediaThumbnail.tsx +117 -0
  33. package/src/components/Message.tsx +8 -8
  34. package/src/components/NoyaLogo.tsx +41 -0
  35. package/src/components/SearchCompletionMenu.tsx +30 -16
  36. package/src/components/SegmentedControl.tsx +1 -1
  37. package/src/components/SelectMenu.tsx +28 -21
  38. package/src/components/Slider.tsx +16 -7
  39. package/src/components/Sortable.tsx +125 -62
  40. package/src/components/internal/Menu.tsx +45 -25
  41. package/src/components/internal/MenuViewport.tsx +7 -1
  42. package/src/components/internal/SelectItem.tsx +53 -28
  43. package/src/components/internal/__tests__/Menu.test.tsx +12 -0
  44. package/src/components/workspace/DrawerWorkspaceLayout.tsx +86 -0
  45. package/src/components/workspace/PanelWorkspaceLayout.tsx +102 -0
  46. package/src/components/{WorkspaceLayout.tsx → workspace/WorkspaceLayout.tsx} +109 -106
  47. package/src/components/workspace/types.ts +31 -0
  48. package/src/contexts/DialogContext.tsx +49 -34
  49. package/src/hooks/usePreservePanelSize.tsx +1 -5
  50. package/src/index.css +8 -1
  51. package/src/index.tsx +6 -1
  52. package/src/theme/index.ts +2 -0
  53. package/src/utils/classNames.ts +51 -3
  54. package/src/utils/inputs.ts +21 -0
  55. package/tailwind.config.ts +8 -0
@@ -0,0 +1,117 @@
1
+ import { ImageIcon, SpeakerLoudIcon, VideoIcon } from "@noya-app/noya-icons";
2
+ import React, { memo, useMemo } from "react";
3
+ import { cssVars } from "../theme";
4
+ import { cx } from "../utils/classNames";
5
+ import { IconName, Icons } from "./Icons";
6
+
7
+ type ColorScheme = "primary" | "icon" | "warning";
8
+
9
+ type AssetType = "image" | "video" | "audio";
10
+
11
+ const getAssetType = (contentType?: string): AssetType | undefined => {
12
+ if (!contentType) return undefined;
13
+ if (contentType.startsWith("image/")) return "image";
14
+ if (contentType.startsWith("video/")) return "video";
15
+ if (contentType.startsWith("audio/")) return "audio";
16
+ return undefined;
17
+ };
18
+
19
+ type MediaThumbnailProps = {
20
+ contentType?: string;
21
+ url?: string;
22
+ selected?: boolean;
23
+ className?: string;
24
+ /** Color scheme for the thumbnail. Affects icon color and background.
25
+ * @default "icon"
26
+ */
27
+ colorScheme?: ColorScheme;
28
+ /**
29
+ * If provided, use this icon instead of the default one for the asset type
30
+ */
31
+ iconName?: IconName;
32
+ };
33
+
34
+ const getColors = (colorScheme: ColorScheme, selected: boolean) => {
35
+ if (selected) {
36
+ return {
37
+ icon: cssVars.colors.primary,
38
+ background: cssVars.colors.primaryPastel,
39
+ };
40
+ }
41
+
42
+ switch (colorScheme) {
43
+ case "primary":
44
+ return {
45
+ icon: cssVars.colors.primary,
46
+ background: cssVars.colors.listViewThumbnailBackground,
47
+ };
48
+ case "warning":
49
+ return {
50
+ icon: cssVars.colors.warning,
51
+ background: cssVars.colors.listViewThumbnailBackground,
52
+ };
53
+ case "icon":
54
+ default:
55
+ return {
56
+ icon: cssVars.colors.icon,
57
+ background: cssVars.colors.listViewThumbnailBackground,
58
+ };
59
+ }
60
+ };
61
+
62
+ const iconStyles = {
63
+ width: "max(15px, 20%)",
64
+ height: "max(15px, 20%)",
65
+ };
66
+
67
+ export const MediaThumbnail = memo(function MediaThumbnail({
68
+ contentType: contentTypeProp,
69
+ selected = false,
70
+ className,
71
+ colorScheme = "icon",
72
+ url,
73
+ iconName = "FileIcon",
74
+ }: MediaThumbnailProps) {
75
+ const { icon: iconColor, background: backgroundColor } = getColors(
76
+ colorScheme,
77
+ selected
78
+ );
79
+ const contentType = getAssetType(contentTypeProp);
80
+
81
+ const content = useMemo(() => {
82
+ switch (contentType) {
83
+ case "image":
84
+ if (!url) return <ImageIcon color={iconColor} style={iconStyles} />;
85
+ return (
86
+ <img
87
+ src={url}
88
+ alt=""
89
+ className="object-contain w-full h-full"
90
+ tabIndex={-1}
91
+ />
92
+ );
93
+
94
+ case "video":
95
+ return <VideoIcon color={iconColor} style={iconStyles} />;
96
+
97
+ case "audio":
98
+ return <SpeakerLoudIcon color={iconColor} style={iconStyles} />;
99
+
100
+ default:
101
+ const Icon = Icons[iconName];
102
+ return <Icon color={iconColor} style={iconStyles} />;
103
+ }
104
+ }, [contentType, url, iconColor, iconName]);
105
+
106
+ return (
107
+ <div
108
+ className={cx(
109
+ "w-full h-full justify-center flex items-center aspect-square",
110
+ className
111
+ )}
112
+ style={{ backgroundColor }}
113
+ >
114
+ {content}
115
+ </div>
116
+ );
117
+ });
@@ -1,11 +1,11 @@
1
+ import { MultiplayerUser } from "@noya-app/noya-multiplayer-react";
1
2
  import React, { forwardRef, memo, useMemo } from "react";
2
- import { Icons } from "./Icons";
3
- import { Avatar } from "./Avatar";
3
+ import { cssVars, INPUT_HEIGHT } from "../theme";
4
4
  import { cx } from "../utils/classNames";
5
- import { Text } from "./Text";
6
- import { MultiplayerUser } from "@noya-app/noya-multiplayer-react";
7
5
  import { colorFromString } from "../utils/colorFromString";
8
- import { cssVars } from "../theme";
6
+ import { Avatar } from "./Avatar";
7
+ import { Logo } from "./NoyaLogo";
8
+ import { Text } from "./Text";
9
9
 
10
10
  export type MessageProps = {
11
11
  role: "user" | "assistant" | "system" | "data";
@@ -17,7 +17,7 @@ export type MessageProps = {
17
17
 
18
18
  export const Message = memo(
19
19
  forwardRef(function Message(
20
- { role, children, user, avatarSize = 27 }: MessageProps,
20
+ { role, children, user, avatarSize = INPUT_HEIGHT }: MessageProps,
21
21
  ref: React.ForwardedRef<HTMLDivElement>
22
22
  ) {
23
23
  const randomId = crypto.randomUUID();
@@ -42,9 +42,9 @@ export const Message = memo(
42
42
  <Avatar
43
43
  name="Assistant"
44
44
  size={avatarSize}
45
- backgroundColor={cssVars.colors.inputBackground}
45
+ backgroundColor={cssVars.colors.primaryPastel}
46
46
  >
47
- <Icons.PersonIcon fontSize={22} color={cssVars.colors.text} />
47
+ <Logo style={{ width: 15 }} fill={cssVars.colors.primary} />
48
48
  </Avatar>
49
49
  ) : (
50
50
  <Avatar
@@ -0,0 +1,41 @@
1
+ import { cssVars } from "@noya-app/noya-designsystem";
2
+ import React, { forwardRef } from "react";
3
+
4
+ export const Logo = forwardRef(function Logo(
5
+ props: React.ComponentProps<"svg">,
6
+ ref: React.Ref<SVGSVGElement>
7
+ ) {
8
+ const fill = props.fill || cssVars.colors.logoFill;
9
+
10
+ return (
11
+ <svg
12
+ ref={ref}
13
+ viewBox="0 0 24 26"
14
+ fill="none"
15
+ xmlns="http://www.w3.org/2000/svg"
16
+ {...props}
17
+ className="w-6 aspect-[24/26]"
18
+ >
19
+ <path
20
+ d="M6.5 3H2V12H5V6H6.5C7.32843 6 8 6.67157 8 7.5V12H11V7.5C11 5.01472 8.98528 3 6.5 3Z"
21
+ fill={fill}
22
+ />
23
+ <path
24
+ fillRule="evenodd"
25
+ clipRule="evenodd"
26
+ d="M17.5 12C19.9853 12 22 9.98528 22 7.5C22 5.01472 19.9853 3 17.5 3C15.0147 3 13 5.01472 13 7.5C13 9.98528 15.0147 12 17.5 12ZM17.5 9C18.3284 9 19 8.32843 19 7.5C19 6.67157 18.3284 6 17.5 6C16.6716 6 16 6.67157 16 7.5C16 8.32843 16.6716 9 17.5 9Z"
27
+ fill={fill}
28
+ />
29
+ <path
30
+ d="M5 14H2V18.5C2 20.9853 4.01472 23 6.5 23H8V26H11V17H8V20H6.5C5.67157 20 5 19.3284 5 18.5V14Z"
31
+ fill={fill}
32
+ />
33
+ <path
34
+ fillRule="evenodd"
35
+ clipRule="evenodd"
36
+ d="M17.5 23C15.0147 23 13 20.9853 13 18.5C13 16.0147 15.0147 14 17.5 14C19.9853 14 22 16.0147 22 18.5V23H17.5ZM17.5 20C18.3284 20 19 19.3284 19 18.5C19 17.6716 18.3284 17 17.5 17C16.6716 17 16 17.6716 16 18.5C16 19.3284 16.6716 20 17.5 20Z"
37
+ fill={fill}
38
+ />
39
+ </svg>
40
+ );
41
+ });
@@ -1,16 +1,3 @@
1
- import {
2
- ComboboxMenu,
3
- Divider,
4
- IScoredItem,
5
- InputField,
6
- ListView,
7
- SelectableMenuItem,
8
- Small,
9
- cssVars,
10
- fuzzyFilter,
11
- getNextIndex,
12
- isSelectableMenuItem,
13
- } from "@noya-app/noya-designsystem";
14
1
  import { getCurrentPlatform, handleKeyboardEvent } from "@noya-app/noya-keymap";
15
2
  import React, {
16
3
  forwardRef,
@@ -21,6 +8,21 @@ import React, {
21
8
  useRef,
22
9
  useState,
23
10
  } from "react";
11
+ import { cssVars } from "../theme";
12
+ import { getNextIndex } from "../utils/combobox";
13
+ import { fuzzyFilter, IScoredItem } from "../utils/fuzzyScorer";
14
+ import { ComboboxMenu } from "./ComboboxMenu";
15
+ import { Divider } from "./Divider";
16
+ import { InputField } from "./InputField";
17
+ import {
18
+ CHECKBOX_INDENT_WIDTH,
19
+ CHECKBOX_RIGHT_INSET,
20
+ CHECKBOX_WIDTH,
21
+ isSelectableMenuItem,
22
+ SelectableMenuItem,
23
+ } from "./internal/Menu";
24
+ import { ListView } from "./ListView";
25
+ import { Small } from "./Text";
24
26
 
25
27
  export interface ISearchCompletionMenu {
26
28
  focus: () => void;
@@ -33,17 +35,21 @@ export const SearchCompletionMenu = forwardRef(function SearchCompletionMenu(
33
35
  onClose,
34
36
  items,
35
37
  width = 200,
38
+ testSelectedIndex,
39
+ testSearch,
36
40
  }: {
37
41
  onSelect: (item: SelectableMenuItem<string>) => void;
38
42
  onHover?: (item: SelectableMenuItem<string>) => void;
39
43
  onClose: () => void;
40
44
  items: SelectableMenuItem<string>[];
41
45
  width?: number;
46
+ testSelectedIndex?: number;
47
+ testSearch?: string;
42
48
  },
43
49
  forwardedRef: React.ForwardedRef<ISearchCompletionMenu>
44
50
  ) {
45
51
  const [state, setState] = useState({
46
- search: "",
52
+ search: testSearch ?? "",
47
53
  index: 0,
48
54
  });
49
55
 
@@ -51,6 +57,8 @@ export const SearchCompletionMenu = forwardRef(function SearchCompletionMenu(
51
57
 
52
58
  const listRef = React.useRef<ListView.VirtualizedList>(null);
53
59
 
60
+ const hasCheckedItems = items.some((item) => item.checked);
61
+
54
62
  useEffect(() => {
55
63
  listRef.current?.scrollToIndex(index);
56
64
  }, [index]);
@@ -157,7 +165,12 @@ export const SearchCompletionMenu = forwardRef(function SearchCompletionMenu(
157
165
  outline: "none",
158
166
  boxShadow: "none",
159
167
  backgroundColor: cssVars.colors.inputBackground,
160
- padding: "4px 12px",
168
+ paddingTop: "4px",
169
+ paddingBottom: "4px",
170
+ paddingLeft: hasCheckedItems
171
+ ? `${CHECKBOX_WIDTH - CHECKBOX_RIGHT_INSET + 12 + CHECKBOX_INDENT_WIDTH}px`
172
+ : "12px",
173
+ paddingRight: "12px",
161
174
  }}
162
175
  onChange={setSearch}
163
176
  onKeyDown={handleKeyDown}
@@ -169,13 +182,14 @@ export const SearchCompletionMenu = forwardRef(function SearchCompletionMenu(
169
182
  ref={listRef}
170
183
  listSize={listSize}
171
184
  items={results}
172
- selectedIndex={index}
185
+ selectedIndex={testSelectedIndex ?? index}
173
186
  onSelectItem={(item) => {
174
187
  if (isSelectableMenuItem(item)) {
175
188
  selectAndClose(item);
176
189
  }
177
190
  }}
178
191
  onHoverIndex={setIndex}
192
+ indented={hasCheckedItems}
179
193
  />
180
194
  ) : (
181
195
  <div
@@ -141,7 +141,7 @@ export const SegmentedControl = memoGeneric(function SegmentedControl<
141
141
  value={value}
142
142
  onValueChange={handleValueChange}
143
143
  className={cx(
144
- `flex items-stretch flex-auto appearance-none relative outline-none min-h-[27px] rounded bg-input-background py-[2px] px-[2px]`,
144
+ `flex items-stretch flex-auto appearance-none relative outline-none min-h-input-height rounded bg-input-background py-[2px] px-[2px]`,
145
145
  className
146
146
  )}
147
147
  style={style}
@@ -15,6 +15,7 @@ import React, {
15
15
  import { useLabel, useLabelPosition } from "../hooks/useLabel";
16
16
  import { cx } from "../utils/classNames";
17
17
 
18
+ import { getInsetEndStyles } from "../utils/inputs";
18
19
  import { Button } from "./Button";
19
20
  import { renderIcon } from "./Icons";
20
21
  import {
@@ -24,7 +25,7 @@ import {
24
25
  styles,
25
26
  } from "./internal/Menu";
26
27
  import { MenuComponents, MenuViewport } from "./internal/MenuViewport";
27
- import { InsetLabel } from "./Label";
28
+ import { Label } from "./Label";
28
29
 
29
30
  type Props<T extends string> = {
30
31
  id?: string;
@@ -57,6 +58,8 @@ const scrollButtonStyles = `
57
58
  hover:text-text
58
59
  `;
59
60
 
61
+ const insetEndStyles = getInsetEndStyles();
62
+
60
63
  interface SelectMenuTriggerProps {
61
64
  id?: string;
62
65
  style?: React.CSSProperties;
@@ -93,7 +96,7 @@ const SelectMenuTrigger = memoGeneric(function SelectMenuTrigger({
93
96
  id={id}
94
97
  style={style}
95
98
  className={cx(
96
- className ?? "flex w-full gap-1.5",
99
+ className ?? "flex w-full",
97
100
  "flex-1 focus:z-interactable relative",
98
101
  isOpen && "ring-2 ring-primary"
99
102
  )}
@@ -101,22 +104,21 @@ const SelectMenuTrigger = memoGeneric(function SelectMenuTrigger({
101
104
  >
102
105
  {icon && <span className="pr-1.5">{renderIcon(icon)}</span>}
103
106
  {label && labelPosition === "inset" && (
104
- <InsetLabel className="pr-4" htmlFor={id}>
105
- {label}
106
- </InsetLabel>
107
+ <div className={insetEndStyles}>
108
+ <Label className="pr-4 !text-text-disabled" htmlFor={id}>
109
+ {label}
110
+ </Label>
111
+ </div>
107
112
  )}
108
113
  <span className="flex-1 flex">
109
114
  <Select.Value placeholder={placeholder} />
110
115
  </span>
111
-
112
- <span className="px-0.5">
113
- <DropdownChevronIcon
114
- className={cx(
115
- "transition-transform duration-100",
116
- isOpen ? "rotate-180" : "rotate-0"
117
- )}
118
- />
119
- </span>
116
+ <DropdownChevronIcon
117
+ className={cx(
118
+ "transition-transform duration-100 ml-1.5",
119
+ isOpen ? "rotate-180" : "rotate-0"
120
+ )}
121
+ />
120
122
  </Button>
121
123
  </Select.SelectTrigger>
122
124
  );
@@ -143,7 +145,7 @@ export const SelectMenu = memoGeneric(function SelectMenu<
143
145
  onFocus,
144
146
  onBlur,
145
147
  onOpenChange,
146
- contentStyle,
148
+ contentStyle: contentStyleProp,
147
149
  ...props
148
150
  }: Props<T>) {
149
151
  const selectedItem = items.find(
@@ -181,6 +183,15 @@ export const SelectMenu = memoGeneric(function SelectMenu<
181
183
  [icon, id, style, className, disabled, value, selectedItem]
182
184
  );
183
185
 
186
+ const contentStyle = useMemo(() => {
187
+ return {
188
+ width: "var(--radix-select-trigger-width)",
189
+ minWidth: "max-content",
190
+ maxHeight: "500px",
191
+ ...contentStyleProp,
192
+ };
193
+ }, [contentStyleProp]);
194
+
184
195
  const content = (
185
196
  <Select.Root
186
197
  value={value}
@@ -206,12 +217,8 @@ export const SelectMenu = memoGeneric(function SelectMenu<
206
217
  position="popper"
207
218
  sideOffset={6}
208
219
  collisionPadding={8}
209
- align="end"
210
- style={{
211
- width: "var(--radix-select-trigger-width)",
212
- maxHeight: "500px",
213
- ...contentStyle,
214
- }}
220
+ align="center"
221
+ style={contentStyle}
215
222
  >
216
223
  <Select.ScrollUpButton className={scrollButtonStyles}>
217
224
  <ChevronUpIcon
@@ -2,7 +2,8 @@ import * as RadixSlider from "@radix-ui/react-slider";
2
2
  import React, { FocusEventHandler, memo, useCallback, useMemo } from "react";
3
3
  import { useLabel, useLabelPosition } from "../hooks/useLabel";
4
4
  import { cx } from "../utils/classNames";
5
- import { InsetLabel } from "./Label";
5
+ import { getInsetEndStyles } from "../utils/inputs";
6
+ import { Label } from "./Label";
6
7
 
7
8
  type ColorScheme = "primary" | "secondary";
8
9
 
@@ -28,6 +29,8 @@ const thumbStyle = {
28
29
  width: THUMB_WIDTH,
29
30
  };
30
31
 
32
+ const insetEndStyles = getInsetEndStyles();
33
+
31
34
  export const Slider = memo(function Slider({
32
35
  style,
33
36
  className,
@@ -95,7 +98,7 @@ export const Slider = memo(function Slider({
95
98
  value={arrayValue}
96
99
  onValueChange={handleValueChange}
97
100
  className={cx(
98
- "flex relative items-center select-none touch-none h-[27px] rounded overflow-hidden flex-grow max-h-[27px]",
101
+ "flex relative items-center select-none touch-none h-input-height rounded overflow-hidden flex-grow max-h-input-height",
99
102
  className
100
103
  )}
101
104
  style={style}
@@ -105,7 +108,11 @@ export const Slider = memo(function Slider({
105
108
  >
106
109
  <RadixSlider.Track className="flex-grow h-full rounded overflow-hidden bg-input-background">
107
110
  {label && labelPosition === "inset" && (
108
- <InsetLabel htmlFor={id}>{label}</InsetLabel>
111
+ <div className={insetEndStyles}>
112
+ <Label htmlFor={id} className="!text-text-disabled">
113
+ {label}
114
+ </Label>
115
+ </div>
109
116
  )}
110
117
  <div
111
118
  style={trackFillStyle}
@@ -117,14 +124,16 @@ export const Slider = memo(function Slider({
117
124
  />
118
125
  </RadixSlider.Track>
119
126
  {label && labelPosition === "inset" && colorScheme !== undefined && (
120
- <InsetLabel className="text-white overflow-hidden" style={labelStyle}>
121
- {label}
122
- </InsetLabel>
127
+ <div className={insetEndStyles}>
128
+ <Label className="text-white overflow-hidden" style={labelStyle}>
129
+ {label}
130
+ </Label>
131
+ </div>
123
132
  )}
124
133
  <RadixSlider.Thumb
125
134
  style={thumbStyle}
126
135
  className={cx(
127
- "block h-[27px] rounded border border-solid bg-slider-thumb-background transition-colors focus:border-primary focus:border-2 outline-none",
136
+ "block h-input-height rounded border border-solid bg-slider-thumb-background transition-colors focus:border-primary focus:border-2 outline-none",
128
137
  colorScheme === undefined && "border-slider-border",
129
138
  colorScheme === "primary" && "border-primary",
130
139
  colorScheme === "secondary" && "border-secondary"