@noya-app/noya-designsystem 0.1.45 → 0.1.47

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 (39) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/CHANGELOG.md +15 -0
  3. package/dist/index.css +1 -1
  4. package/dist/index.d.mts +1149 -1064
  5. package/dist/index.d.ts +1149 -1064
  6. package/dist/index.js +10161 -9662
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +10465 -9972
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +3 -3
  11. package/src/__tests__/combobox.test.ts +137 -89
  12. package/src/components/Breadcrumbs.tsx +29 -0
  13. package/src/components/Collection.tsx +74 -0
  14. package/src/components/Combobox.tsx +69 -52
  15. package/src/components/ComboboxMenu.tsx +37 -19
  16. package/src/components/Dialog.tsx +11 -19
  17. package/src/components/DropdownMenu.tsx +0 -1
  18. package/src/components/EditableText.tsx +203 -0
  19. package/src/components/Grid.tsx +243 -0
  20. package/src/components/GridView.tsx +86 -96
  21. package/src/components/List.tsx +268 -0
  22. package/src/components/ListView.tsx +10 -9
  23. package/src/components/SearchCompletionMenu.tsx +15 -31
  24. package/src/components/SegmentedControl.tsx +7 -4
  25. package/src/components/SelectMenu.tsx +9 -5
  26. package/src/components/Switch.tsx +1 -1
  27. package/src/components/Toolbar.tsx +5 -4
  28. package/src/components/TreeView.tsx +1 -0
  29. package/src/components/WorkspaceLayout.tsx +28 -4
  30. package/src/components/internal/Menu.tsx +55 -14
  31. package/src/components/internal/MenuViewport.tsx +78 -77
  32. package/src/contexts/DialogContext.tsx +53 -7
  33. package/src/index.css +2 -1
  34. package/src/index.tsx +6 -4
  35. package/src/utils/combobox.ts +115 -103
  36. package/src/utils/createSectionedMenu.ts +3 -9
  37. package/src/utils/fuzzyScorer.ts +11 -8
  38. package/src/utils/selection.ts +16 -4
  39. package/src/components/SidebarList.tsx +0 -252
@@ -2,15 +2,12 @@ import { getShortcutDisplayParts } from "@noya-app/noya-keymap";
2
2
  import React, { memo, ReactNode } from "react";
3
3
  import { useDesignSystemConfiguration } from "../../contexts/DesignSystemConfiguration";
4
4
  import { cx } from "../../utils/classNames";
5
+ import { IScoredItem } from "../../utils/fuzzyScorer";
5
6
  import withSeparatorElements from "../../utils/withSeparatorElements";
6
7
  import { IconName } from "../Icons";
7
8
  import { Spacer } from "../Spacer";
8
9
  import { textStyles } from "../Text";
9
10
 
10
- export type SeparatorItem = {
11
- type: "separator";
12
- };
13
-
14
11
  // From electron
15
12
  export type MenuItemRole =
16
13
  | "undo"
@@ -61,35 +58,79 @@ export type MenuItemRole =
61
58
  | "moveTabToNewWindow"
62
59
  | "windowMenu";
63
60
 
64
- export type RegularMenuItem<T extends string> = {
65
- value?: T;
66
- title: ReactNode;
67
- shortcut?: string;
61
+ export type SeparatorItem = {
62
+ type: "separator";
63
+ };
64
+
65
+ type BaseMenuItem = {
68
66
  checked?: boolean;
69
67
  disabled?: boolean;
70
68
  icon?: Exclude<ReactNode, string> | IconName;
71
- items?: MenuItem<T>[];
72
69
  role?: MenuItemRole;
73
- type?: undefined;
74
70
  alwaysInclude?: boolean;
71
+ title: NonNullable<ReactNode>;
75
72
  };
76
73
 
77
74
  export type SectionHeaderMenuItem = {
78
75
  type: "sectionHeader";
79
76
  id: string;
80
- title: string;
77
+ title: ReactNode;
81
78
  maxVisibleItems?: number;
82
79
  variant?: "normal" | "label";
83
80
  };
84
81
 
82
+ export type SelectableMenuItem<T extends string> = BaseMenuItem & {
83
+ type?: undefined;
84
+ shortcut?: string;
85
+ value: T;
86
+ };
87
+
88
+ type SubMenuItem<T extends string> = BaseMenuItem & {
89
+ type: "submenu";
90
+ items: MenuItem<T>[];
91
+ id: string;
92
+ };
93
+
94
+ export type NonSelectableMenuItem<T extends string> =
95
+ | SeparatorItem
96
+ | SectionHeaderMenuItem
97
+ | SubMenuItem<T>;
98
+
85
99
  export type MenuItem<T extends string> =
86
100
  | SeparatorItem
87
- | RegularMenuItem<T>
101
+ | SelectableMenuItem<T>
102
+ | SectionHeaderMenuItem
103
+ | SubMenuItem<T>;
104
+
105
+ type ScoredSelectableMenuItem<T extends string> = SelectableMenuItem<T> &
106
+ IScoredItem;
107
+
108
+ export type ScoredMenuItem<T extends string> =
109
+ | ScoredSelectableMenuItem<T>
88
110
  | SectionHeaderMenuItem;
89
111
 
90
112
  // Extract type T of RegularMenuItem<T> within MenuItem<T>
91
113
  export type ExtractMenuItemType<T> =
92
- T extends RegularMenuItem<infer U> ? U : never;
114
+ T extends SelectableMenuItem<infer U> ? U : never;
115
+
116
+ export const isSelectableMenuItem = <T extends string>(
117
+ item: MenuItem<T>
118
+ ): item is SelectableMenuItem<T> => {
119
+ return item.type === undefined;
120
+ };
121
+
122
+ export const isNonSelectableMenuItem = <T extends string>(
123
+ item: MenuItem<T>
124
+ ): item is NonSelectableMenuItem<T> => !isSelectableMenuItem(item);
125
+
126
+ export const isMenuItemSectionHeader = (
127
+ item: MenuItem<string>
128
+ ): item is SectionHeaderMenuItem => item.type === "sectionHeader";
129
+
130
+ export const isSelectableMenuItemWithScore = (
131
+ item: MenuItem<string>
132
+ ): item is ScoredSelectableMenuItem<string> =>
133
+ isSelectableMenuItem(item) && "index" in item && "score" in item;
93
134
 
94
135
  export const CHECKBOX_WIDTH = 16;
95
136
  export const CHECKBOX_RIGHT_INSET = 8;
@@ -132,7 +173,7 @@ function getKeyboardShortcuts<T extends string>(
132
173
  if (item.type === "separator") return [];
133
174
  if (item.type === "sectionHeader") return [];
134
175
 
135
- if (item.items) return item.items.flatMap(getKeyboardShortcuts);
176
+ if (item.type === "submenu") return item.items.flatMap(getKeyboardShortcuts);
136
177
 
137
178
  if (item.disabled || !item.value || !item.shortcut) return [];
138
179
 
@@ -65,86 +65,87 @@ export const MenuViewport = <T extends string>({
65
65
  return (
66
66
  <>
67
67
  {items.map((item, index) => {
68
- if (item.type === "separator") {
69
- return (
70
- <Components.Separator
71
- key={index}
72
- className={styles.separatorStyle}
73
- />
74
- );
75
- }
76
-
77
- if (item.type === "sectionHeader") {
78
- return (
79
- <SectionHeader
80
- isFirst={index === 0}
81
- key={item.id}
82
- {...item}
83
- indented={hasCheckedItem}
84
- />
85
- );
86
- }
68
+ switch (item.type) {
69
+ case "separator":
70
+ return (
71
+ <Components.Separator
72
+ key={index}
73
+ className={styles.separatorStyle}
74
+ />
75
+ );
87
76
 
88
- const value = item.value ?? "";
89
-
90
- if (
91
- item.items &&
92
- Components.Sub &&
93
- Components.SubTrigger &&
94
- Components.SubContent &&
95
- Components.Portal
96
- ) {
97
- return (
98
- <Components.Sub key={value}>
99
- <Components.SubTrigger
100
- className={styles.itemStyle({ disabled: item.disabled })}
101
- asChild
77
+ case "sectionHeader":
78
+ return (
79
+ <SectionHeader
80
+ isFirst={index === 0}
81
+ key={item.id}
82
+ {...item}
83
+ indented={hasCheckedItem}
84
+ />
85
+ );
86
+ case "submenu":
87
+ if (
88
+ item.items &&
89
+ Components.Sub &&
90
+ Components.SubTrigger &&
91
+ Components.SubContent &&
92
+ Components.Portal
93
+ ) {
94
+ return (
95
+ <Components.Sub key={item.id}>
96
+ <Components.SubTrigger
97
+ className={styles.itemStyle({ disabled: item.disabled })}
98
+ asChild
99
+ onClick={(e) => {
100
+ e.stopPropagation();
101
+ e.preventDefault();
102
+ }}
103
+ >
104
+ <SelectItem
105
+ value={item.id}
106
+ icon={item.icon}
107
+ checked={item.checked}
108
+ onSelect={onSelect}
109
+ Components={Components}
110
+ indented={hasCheckedItem}
111
+ >
112
+ <div className="flex items-center flex-1">
113
+ {item.title}
114
+ </div>
115
+ <ChevronRightIcon className="-mr-1" />
116
+ </SelectItem>
117
+ </Components.SubTrigger>
118
+ <Components.Portal>
119
+ <Components.SubContent
120
+ alignOffset={-5}
121
+ className={styles.contentStyle}
122
+ >
123
+ <MenuViewport
124
+ items={item.items}
125
+ Components={Components}
126
+ onSelect={onSelect}
127
+ />
128
+ </Components.SubContent>
129
+ </Components.Portal>
130
+ </Components.Sub>
131
+ );
132
+ } else return null;
133
+ default:
134
+ return (
135
+ <SelectItem
136
+ key={item.value}
137
+ value={item.value}
138
+ icon={item.icon}
139
+ checked={item.checked}
140
+ shortcut={item.shortcut}
141
+ onSelect={onSelect}
142
+ Components={Components}
143
+ indented={hasCheckedItem}
102
144
  >
103
- <SelectItem
104
- value={value}
105
- icon={item.icon}
106
- checked={item.checked}
107
- shortcut={item.shortcut}
108
- onSelect={onSelect}
109
- Components={Components}
110
- indented={hasCheckedItem}
111
- >
112
- <div className="flex items-center flex-1">
113
- {item.title ?? value}
114
- </div>
115
- <ChevronRightIcon className="-mr-1" />
116
- </SelectItem>
117
- </Components.SubTrigger>
118
- <Components.Portal>
119
- <Components.SubContent
120
- alignOffset={-5}
121
- className={styles.contentStyle}
122
- >
123
- <MenuViewport
124
- items={item.items}
125
- Components={Components}
126
- onSelect={onSelect}
127
- />
128
- </Components.SubContent>
129
- </Components.Portal>
130
- </Components.Sub>
131
- );
145
+ {item.title ?? item.value}
146
+ </SelectItem>
147
+ );
132
148
  }
133
-
134
- return (
135
- <SelectItem
136
- key={value}
137
- value={value}
138
- icon={item.icon}
139
- checked={item.checked}
140
- shortcut={item.shortcut}
141
- onSelect={onSelect}
142
- Components={Components}
143
- indented={hasCheckedItem}
144
- >
145
- {item.title ?? value}
146
- </SelectItem>
147
- );
148
149
  })}
149
150
  </>
150
151
  );
@@ -25,6 +25,12 @@ function createDeferredPromise<T>() {
25
25
  }
26
26
 
27
27
  export type DialogContextValue = {
28
+ openDialog(options: {
29
+ title: string;
30
+ description?: ReactNode;
31
+ children: ReactNode;
32
+ }): Promise<void>;
33
+
28
34
  openInputDialog(
29
35
  options:
30
36
  | string
@@ -66,7 +72,18 @@ type ConfirmationDialogContents = {
66
72
  resolve: (value: boolean) => void;
67
73
  };
68
74
 
69
- type DialogContents = InputDialogContents | ConfirmationDialogContents;
75
+ type CustomDialogContents = {
76
+ type: "custom";
77
+ title: string;
78
+ description?: ReactNode;
79
+ children: ReactNode;
80
+ resolve: () => void;
81
+ };
82
+
83
+ type DialogContents =
84
+ | InputDialogContents
85
+ | ConfirmationDialogContents
86
+ | CustomDialogContents;
70
87
 
71
88
  export const DialogProvider = function DialogProvider({
72
89
  children,
@@ -136,6 +153,26 @@ export const DialogProvider = function DialogProvider({
136
153
  return promise;
137
154
  }, []);
138
155
 
156
+ const openCustomDialog: DialogContextValue["openDialog"] = useCallback(
157
+ (options) => {
158
+ const { promise, resolve } = createDeferredPromise<void>();
159
+
160
+ setContents({
161
+ type: "custom",
162
+ title: options.title,
163
+ description: options.description,
164
+ children: options.children,
165
+ resolve: () => {
166
+ resolve();
167
+ setContents(undefined);
168
+ },
169
+ });
170
+
171
+ return promise;
172
+ },
173
+ []
174
+ );
175
+
139
176
  const handleKeyDown = useCallback(
140
177
  (event: React.KeyboardEvent<HTMLInputElement>) => {
141
178
  if (event.key !== "Enter") return;
@@ -162,11 +199,12 @@ export const DialogProvider = function DialogProvider({
162
199
  <DialogContext.Provider
163
200
  value={useMemo(
164
201
  () => ({
202
+ openDialog: openCustomDialog,
165
203
  openInputDialog: open,
166
204
  openConfirmationDialog: openConfirmation,
167
205
  containsElement,
168
206
  }),
169
- [containsElement, open, openConfirmation]
207
+ [containsElement, open, openConfirmation, openCustomDialog]
170
208
  )}
171
209
  >
172
210
  {children}
@@ -221,12 +259,16 @@ export const DialogProvider = function DialogProvider({
221
259
  OK
222
260
  </Button>
223
261
  </div>
262
+ ) : contents?.type === "custom" ? (
263
+ <>{contents.children}</>
224
264
  ) : (
225
265
  <>
226
266
  <InputField.Root>
227
267
  <InputField.Input
228
268
  ref={inputRef}
229
- placeholder={contents?.placeholder}
269
+ placeholder={
270
+ contents?.type === "input" ? contents.placeholder : undefined
271
+ }
230
272
  value={contents?.type === "input" ? contents.inputValue : ""}
231
273
  onChange={(value: string) => {
232
274
  setContents((contents) =>
@@ -262,7 +304,7 @@ export const DialogProvider = function DialogProvider({
262
304
  );
263
305
  };
264
306
 
265
- function useDialog(): DialogContextValue {
307
+ export function useDialogContext(): DialogContextValue {
266
308
  const value = useContext(DialogContext);
267
309
 
268
310
  if (!value) {
@@ -273,13 +315,17 @@ function useDialog(): DialogContextValue {
273
315
  }
274
316
 
275
317
  export function useOpenInputDialog() {
276
- return useDialog().openInputDialog;
318
+ return useDialogContext().openInputDialog;
277
319
  }
278
320
 
279
321
  export function useDialogContainsElement() {
280
- return useDialog().containsElement;
322
+ return useDialogContext().containsElement;
281
323
  }
282
324
 
283
325
  export function useOpenConfirmationDialog() {
284
- return useDialog().openConfirmationDialog;
326
+ return useDialogContext().openConfirmationDialog;
327
+ }
328
+
329
+ export function useOpenDialog() {
330
+ return useDialogContext().openDialog;
285
331
  }
package/src/index.css CHANGED
@@ -104,7 +104,8 @@
104
104
  --popover-divider: rgba(255, 255, 255, 0.08);
105
105
  --listview-raised-background: rgba(181, 178, 255, 0.1);
106
106
  --listview-editing-background: #000;
107
- --slider-thumb-background: rgb(248, 248, 250);
107
+ --slider-thumb-background: var(--input-background-light);
108
+ --slider-border: var(--divider);
108
109
  --mask: rgb(102, 187, 106);
109
110
  --transparent-checker: rgba(255, 255, 255, 0.3);
110
111
  --scrollbar: rgba(199, 199, 199, 0.2);
package/src/index.tsx CHANGED
@@ -2,9 +2,11 @@
2
2
  export * from "./components/ActivityIndicator";
3
3
  export * from "./components/AnimatePresence";
4
4
  export * from "./components/Avatar";
5
+ export * from "./components/Breadcrumbs";
5
6
  export * from "./components/Button";
6
7
  export * from "./components/Checkbox";
7
8
  export * from "./components/Chip";
9
+ export * from "./components/Collection";
8
10
  export * from "./components/Combobox";
9
11
  export * from "./components/ComboboxMenu";
10
12
  export * from "./components/CommandPalette";
@@ -13,11 +15,13 @@ export * from "./components/Dialog";
13
15
  export * from "./components/Divider";
14
16
  export * from "./components/DraggableMenuButton";
15
17
  export * from "./components/DropdownMenu";
18
+ export * from "./components/EditableText";
16
19
  export * from "./components/Fade";
17
20
  export * from "./components/FillInputField";
18
21
  export * from "./components/FillPreviewBackground";
19
22
  export * from "./components/FloatingWindow";
20
23
  export * from "./components/GradientPicker";
24
+ export * from "./components/Grid";
21
25
  export * from "./components/GridView";
22
26
  export * from "./components/IconButton";
23
27
  export * from "./components/Icons";
@@ -27,14 +31,13 @@ export * from "./components/internal/Menu";
27
31
  export type {
28
32
  ExtractMenuItemType,
29
33
  MenuItem,
30
- RegularMenuItem,
31
- SectionHeaderMenuItem,
32
- SeparatorItem,
34
+ SelectableMenuItem,
33
35
  } from "./components/internal/Menu";
34
36
  export * from "./components/internal/MenuViewport";
35
37
  export * from "./components/Label";
36
38
  export * from "./components/LabeledElementView";
37
39
  export * from "./components/LabeledField";
40
+ export * from "./components/List";
38
41
  export * from "./components/ListView";
39
42
  export * from "./components/Message";
40
43
  export * from "./components/Popover";
@@ -43,7 +46,6 @@ export * from "./components/ScrollArea";
43
46
  export * from "./components/SearchCompletionMenu";
44
47
  export * from "./components/SegmentedControl";
45
48
  export * from "./components/SelectMenu";
46
- export * from "./components/SidebarList";
47
49
  export * from "./components/Slider";
48
50
  export * from "./components/Sortable";
49
51
  export type { RelativeDropPosition } from "./components/Sortable";