@noya-app/noya-designsystem 0.1.44 → 0.1.46
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 +13 -10
- package/CHANGELOG.md +18 -0
- package/dist/index.css +1 -1
- package/dist/index.d.mts +1194 -878
- package/dist/index.d.ts +1194 -878
- package/dist/index.js +11432 -10219
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +11556 -10360
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/combobox.test.ts +137 -89
- package/src/components/AnimatePresence.tsx +10 -1
- package/src/components/Avatar.tsx +2 -0
- package/src/components/Breadcrumbs.tsx +29 -0
- package/src/components/Checkbox.tsx +6 -1
- package/src/components/Collection.tsx +74 -0
- package/src/components/Combobox.tsx +75 -56
- package/src/components/ComboboxMenu.tsx +61 -28
- package/src/components/CommandPalette.tsx +69 -0
- package/src/components/ContextMenu.tsx +33 -134
- package/src/components/DropdownMenu.tsx +46 -155
- package/src/components/EditableText.tsx +203 -0
- package/src/components/Fade.tsx +62 -0
- package/src/components/Grid.tsx +243 -0
- package/src/components/GridView.tsx +86 -96
- package/src/components/InputField.tsx +109 -133
- package/src/components/Label.tsx +81 -7
- package/src/components/LabeledField.tsx +112 -0
- package/src/components/List.tsx +268 -0
- package/src/components/ListView.tsx +65 -61
- package/src/components/Popover.tsx +12 -1
- package/src/components/SearchCompletionMenu.tsx +210 -0
- package/src/components/SegmentedControl.tsx +12 -6
- package/src/components/SelectMenu.tsx +110 -126
- package/src/components/Slider.tsx +18 -26
- package/src/components/Text.tsx +1 -0
- package/src/components/TextArea.tsx +4 -1
- package/src/components/Toolbar.tsx +9 -4
- package/src/components/TreeView.tsx +4 -0
- package/src/components/WorkspaceLayout.tsx +64 -20
- package/src/components/internal/Menu.tsx +107 -18
- package/src/components/internal/MenuViewport.tsx +152 -0
- package/src/components/internal/SelectItem.tsx +111 -0
- package/src/hooks/useIndent.ts +12 -0
- package/src/hooks/useLabel.ts +51 -0
- package/src/hooks/usePreservePanelSize.tsx +50 -19
- package/src/index.tsx +15 -6
- package/src/utils/combobox.ts +116 -101
- package/src/utils/createSectionedMenu.ts +11 -14
- package/src/utils/fuzzyScorer.ts +11 -8
- package/src/utils/selection.ts +48 -0
|
@@ -7,6 +7,7 @@ import React, {
|
|
|
7
7
|
useEffect,
|
|
8
8
|
useRef,
|
|
9
9
|
} from "react";
|
|
10
|
+
import { useLabel } from "../hooks/useLabel";
|
|
10
11
|
import { cx } from "../utils/classNames";
|
|
11
12
|
|
|
12
13
|
export const useAutoResize = (value: string | undefined) => {
|
|
@@ -39,6 +40,7 @@ export const TextArea = memo(
|
|
|
39
40
|
} & React.TextareaHTMLAttributes<HTMLTextAreaElement>,
|
|
40
41
|
forwardedRef: React.ForwardedRef<HTMLTextAreaElement>
|
|
41
42
|
) {
|
|
43
|
+
const { fieldId: id } = useLabel({ fieldId: rest.id });
|
|
42
44
|
const autoResizeRef = useAutoResize(
|
|
43
45
|
autoResize ? value || rest.placeholder || "" : undefined
|
|
44
46
|
);
|
|
@@ -67,8 +69,9 @@ export const TextArea = memo(
|
|
|
67
69
|
`font-sans text-heading5 font-normal text-text bg-input-background flex-1 py-1 px-1.5 border-none outline-none min-h-6 w-full rounded resize-none placeholder:text-text-disabled focus:ring-2 focus:ring-primary read-only:text-text-disabled focus:z-interactable transition-all`,
|
|
68
70
|
className
|
|
69
71
|
)}
|
|
70
|
-
ref={handleRef}
|
|
71
72
|
{...rest}
|
|
73
|
+
id={id}
|
|
74
|
+
ref={handleRef}
|
|
72
75
|
onChange={handleChange}
|
|
73
76
|
value={value}
|
|
74
77
|
/>
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
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 { BaseToolbar } from "./BaseToolbar";
|
|
5
6
|
import { Button } from "./Button";
|
|
6
7
|
import { DropdownMenu } from "./DropdownMenu";
|
|
7
8
|
import { renderIcon } from "./Icons";
|
|
8
|
-
import
|
|
9
|
-
|
|
9
|
+
import {
|
|
10
|
+
getKeyboardShortcutsForMenuItems,
|
|
11
|
+
isSelectableMenuItem,
|
|
12
|
+
MenuItem,
|
|
13
|
+
} from "./internal/Menu";
|
|
10
14
|
import { Spacer } from "./Spacer";
|
|
11
15
|
|
|
12
16
|
export interface ToolbarProps<T extends string = string> {
|
|
@@ -82,9 +86,10 @@ export function ToolbarMenu<T extends string>({
|
|
|
82
86
|
return (
|
|
83
87
|
<>
|
|
84
88
|
{items.map((item, i) => {
|
|
85
|
-
if (
|
|
89
|
+
if (item.type === "separator") return null;
|
|
90
|
+
if (item.type === "sectionHeader") return null;
|
|
86
91
|
|
|
87
|
-
if (
|
|
92
|
+
if (isSelectableMenuItem(item)) {
|
|
88
93
|
return (
|
|
89
94
|
<Button
|
|
90
95
|
key={i}
|
|
@@ -12,7 +12,9 @@ import { Spacer } from "./Spacer";
|
|
|
12
12
|
type TreeRowBaseProps = {
|
|
13
13
|
icon?: Exclude<ReactNode, string> | IconName;
|
|
14
14
|
expanded?: boolean;
|
|
15
|
+
chevronClassName?: string;
|
|
15
16
|
onClickChevron?: ({ altKey }: { altKey: boolean }) => void;
|
|
17
|
+
onDoubleClick?: () => void;
|
|
16
18
|
};
|
|
17
19
|
|
|
18
20
|
type TreeViewRowProps<MenuItemType extends string> =
|
|
@@ -23,6 +25,7 @@ const TreeRow = forwardRefGeneric(function TreeRow<MenuItemType extends string>(
|
|
|
23
25
|
icon,
|
|
24
26
|
expanded,
|
|
25
27
|
onClickChevron,
|
|
28
|
+
chevronClassName,
|
|
26
29
|
children,
|
|
27
30
|
...rest
|
|
28
31
|
}: TreeViewRowProps<MenuItemType>,
|
|
@@ -46,6 +49,7 @@ const TreeRow = forwardRefGeneric(function TreeRow<MenuItemType extends string>(
|
|
|
46
49
|
<Spacer.Horizontal size={19} />
|
|
47
50
|
) : (
|
|
48
51
|
<IconButton
|
|
52
|
+
className={chevronClassName}
|
|
49
53
|
iconName={expanded ? "ChevronDownIcon2" : "ChevronRightIcon2"}
|
|
50
54
|
onClick={handleClickChevron}
|
|
51
55
|
selected={rest.selected}
|
|
@@ -25,10 +25,11 @@ import {
|
|
|
25
25
|
import { useWindowSize } from "../hooks/useWindowSize";
|
|
26
26
|
import { cx } from "../utils/classNames";
|
|
27
27
|
|
|
28
|
-
interface
|
|
28
|
+
export interface WorkspaceLayoutProps {
|
|
29
29
|
id?: string;
|
|
30
30
|
className?: string;
|
|
31
31
|
style?: React.CSSProperties;
|
|
32
|
+
theme?: "light" | "dark";
|
|
32
33
|
autoSavePrefix?: string;
|
|
33
34
|
left?: React.ReactNode;
|
|
34
35
|
leftOptions?: SideOptions;
|
|
@@ -57,6 +58,7 @@ export type SideOptions = {
|
|
|
57
58
|
*/
|
|
58
59
|
initialSize?: number | string;
|
|
59
60
|
minSize?: number | string;
|
|
61
|
+
maxSize?: number | string;
|
|
60
62
|
};
|
|
61
63
|
|
|
62
64
|
export const WorkspaceLayout = forwardRef(function WorkspaceLayout(
|
|
@@ -71,20 +73,23 @@ export const WorkspaceLayout = forwardRef(function WorkspaceLayout(
|
|
|
71
73
|
children,
|
|
72
74
|
onChangeLayoutState,
|
|
73
75
|
leftOptions: {
|
|
76
|
+
minSize: leftMinSize = 200,
|
|
77
|
+
maxSize: leftMaxSize = 500,
|
|
74
78
|
initialSize: leftInitialSize = 280,
|
|
75
|
-
minSize: leftMinSize,
|
|
76
79
|
resizable: leftResizable = true,
|
|
77
80
|
style: leftStyle,
|
|
78
81
|
className: leftClassName,
|
|
79
82
|
} = {},
|
|
80
83
|
rightOptions: {
|
|
84
|
+
minSize: rightMinSize = 200,
|
|
85
|
+
maxSize: rightMaxSize = 500,
|
|
81
86
|
initialSize: rightInitialSize = 280,
|
|
82
|
-
minSize: rightMinSize,
|
|
83
87
|
resizable: rightResizable = true,
|
|
84
88
|
style: rightStyle,
|
|
85
89
|
className: rightClassName,
|
|
86
90
|
} = {},
|
|
87
|
-
|
|
91
|
+
theme,
|
|
92
|
+
}: WorkspaceLayoutProps,
|
|
88
93
|
forwardedRef: React.ForwardedRef<IWorkspaceLayout>
|
|
89
94
|
) {
|
|
90
95
|
const windowSize = useWindowSize();
|
|
@@ -94,12 +99,40 @@ export const WorkspaceLayout = forwardRef(function WorkspaceLayout(
|
|
|
94
99
|
: (size / windowSize.width) * 100;
|
|
95
100
|
}
|
|
96
101
|
|
|
97
|
-
|
|
98
|
-
|
|
102
|
+
function clampPercentage(
|
|
103
|
+
value: number,
|
|
104
|
+
min: number | undefined,
|
|
105
|
+
max: number | undefined
|
|
106
|
+
) {
|
|
107
|
+
let result = value;
|
|
108
|
+
if (min !== undefined) {
|
|
109
|
+
result = Math.max(result, min);
|
|
110
|
+
}
|
|
111
|
+
if (max !== undefined) {
|
|
112
|
+
result = Math.min(result, max);
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
|
|
99
117
|
const leftSidebarMinPercentage =
|
|
100
118
|
leftMinSize !== undefined ? getPercentage(leftMinSize) : undefined;
|
|
101
119
|
const rightSidebarMinPercentage =
|
|
102
120
|
rightMinSize !== undefined ? getPercentage(rightMinSize) : undefined;
|
|
121
|
+
const leftSidebarMaxPercentage =
|
|
122
|
+
leftMaxSize !== undefined ? getPercentage(leftMaxSize) : undefined;
|
|
123
|
+
const rightSidebarMaxPercentage =
|
|
124
|
+
rightMaxSize !== undefined ? getPercentage(rightMaxSize) : undefined;
|
|
125
|
+
|
|
126
|
+
const leftSidebarPercentage = clampPercentage(
|
|
127
|
+
getPercentage(leftInitialSize),
|
|
128
|
+
leftSidebarMinPercentage,
|
|
129
|
+
leftSidebarMaxPercentage
|
|
130
|
+
);
|
|
131
|
+
const rightSidebarPercentage = clampPercentage(
|
|
132
|
+
getPercentage(rightInitialSize),
|
|
133
|
+
rightSidebarMinPercentage,
|
|
134
|
+
rightSidebarMaxPercentage
|
|
135
|
+
);
|
|
103
136
|
|
|
104
137
|
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
|
|
105
138
|
const leftSidebarRef = useRef<ImperativePanelHandle>(null);
|
|
@@ -189,6 +222,7 @@ export const WorkspaceLayout = forwardRef(function WorkspaceLayout(
|
|
|
189
222
|
id={id}
|
|
190
223
|
className={cx("flex flex-col bg-canvas-background", className)}
|
|
191
224
|
style={style}
|
|
225
|
+
data-theme={theme}
|
|
192
226
|
>
|
|
193
227
|
{toolbar}
|
|
194
228
|
<div className="flex flex-row flex-1">
|
|
@@ -212,11 +246,20 @@ export const WorkspaceLayout = forwardRef(function WorkspaceLayout(
|
|
|
212
246
|
ref={leftSidebarRef}
|
|
213
247
|
defaultSize={leftSidebarPercentage}
|
|
214
248
|
minSize={leftSidebarMinPercentage ?? leftSidebarPercentage}
|
|
215
|
-
{
|
|
249
|
+
maxSize={
|
|
250
|
+
leftSidebarMaxPercentage ??
|
|
251
|
+
(!leftResizable ? leftSidebarPercentage : undefined)
|
|
252
|
+
}
|
|
216
253
|
collapsible
|
|
217
254
|
>
|
|
218
255
|
{internalLayoutState?.leftSidebarCollapsed ? null : (
|
|
219
|
-
<PanelInner
|
|
256
|
+
<PanelInner
|
|
257
|
+
style={leftStyle}
|
|
258
|
+
className={cx(
|
|
259
|
+
"bg-sidebar-background flex-col",
|
|
260
|
+
leftClassName
|
|
261
|
+
)}
|
|
262
|
+
>
|
|
220
263
|
{left}
|
|
221
264
|
</PanelInner>
|
|
222
265
|
)}
|
|
@@ -231,7 +274,7 @@ export const WorkspaceLayout = forwardRef(function WorkspaceLayout(
|
|
|
231
274
|
minSize={10}
|
|
232
275
|
className="flex relative"
|
|
233
276
|
>
|
|
234
|
-
{children}
|
|
277
|
+
<PanelInner className="bg-canvas-background">{children}</PanelInner>
|
|
235
278
|
</Panel>
|
|
236
279
|
{right && (
|
|
237
280
|
<>
|
|
@@ -243,13 +286,20 @@ export const WorkspaceLayout = forwardRef(function WorkspaceLayout(
|
|
|
243
286
|
ref={rightSidebarRef}
|
|
244
287
|
minSize={rightSidebarMinPercentage ?? rightSidebarPercentage}
|
|
245
288
|
defaultSize={rightSidebarPercentage}
|
|
246
|
-
{
|
|
247
|
-
|
|
248
|
-
|
|
289
|
+
maxSize={
|
|
290
|
+
rightSidebarMaxPercentage ??
|
|
291
|
+
(!rightResizable ? rightSidebarPercentage : undefined)
|
|
292
|
+
}
|
|
249
293
|
collapsible
|
|
250
294
|
>
|
|
251
295
|
{internalLayoutState?.rightSidebarCollapsed ? null : (
|
|
252
|
-
<PanelInner
|
|
296
|
+
<PanelInner
|
|
297
|
+
style={rightStyle}
|
|
298
|
+
className={cx(
|
|
299
|
+
"bg-sidebar-background flex-col",
|
|
300
|
+
rightClassName
|
|
301
|
+
)}
|
|
302
|
+
>
|
|
253
303
|
{right}
|
|
254
304
|
</PanelInner>
|
|
255
305
|
)}
|
|
@@ -272,13 +322,7 @@ const PanelInner = memo(function PanelInner({
|
|
|
272
322
|
className?: string;
|
|
273
323
|
}) {
|
|
274
324
|
return (
|
|
275
|
-
<div
|
|
276
|
-
style={style}
|
|
277
|
-
className={cx(
|
|
278
|
-
"absolute inset-0 flex flex-col bg-sidebar-background",
|
|
279
|
-
className
|
|
280
|
-
)}
|
|
281
|
-
>
|
|
325
|
+
<div style={style} className={cx("absolute inset-0 flex", className)}>
|
|
282
326
|
{children}
|
|
283
327
|
</div>
|
|
284
328
|
);
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { getShortcutDisplayParts } from "@noya-app/noya-keymap";
|
|
2
2
|
import React, { memo, ReactNode } from "react";
|
|
3
3
|
import { useDesignSystemConfiguration } from "../../contexts/DesignSystemConfiguration";
|
|
4
|
+
import { cx } from "../../utils/classNames";
|
|
5
|
+
import { IScoredItem } from "../../utils/fuzzyScorer";
|
|
4
6
|
import withSeparatorElements from "../../utils/withSeparatorElements";
|
|
5
7
|
import { IconName } from "../Icons";
|
|
8
|
+
import { Spacer } from "../Spacer";
|
|
6
9
|
import { textStyles } from "../Text";
|
|
7
10
|
|
|
8
|
-
export const SEPARATOR_ITEM = "separator";
|
|
9
|
-
|
|
10
11
|
// From electron
|
|
11
12
|
export type MenuItemRole =
|
|
12
13
|
| "undo"
|
|
@@ -57,40 +58,95 @@ export type MenuItemRole =
|
|
|
57
58
|
| "moveTabToNewWindow"
|
|
58
59
|
| "windowMenu";
|
|
59
60
|
|
|
60
|
-
export type
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
export type SeparatorItem = {
|
|
62
|
+
type: "separator";
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
type BaseMenuItem = {
|
|
64
66
|
checked?: boolean;
|
|
65
67
|
disabled?: boolean;
|
|
66
68
|
icon?: Exclude<ReactNode, string> | IconName;
|
|
67
|
-
items?: MenuItem<T>[];
|
|
68
69
|
role?: MenuItemRole;
|
|
70
|
+
alwaysInclude?: boolean;
|
|
71
|
+
title: NonNullable<ReactNode>;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export type SectionHeaderMenuItem = {
|
|
75
|
+
type: "sectionHeader";
|
|
76
|
+
id: string;
|
|
77
|
+
title: ReactNode;
|
|
78
|
+
maxVisibleItems?: number;
|
|
79
|
+
variant?: "normal" | "label";
|
|
69
80
|
};
|
|
70
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
|
+
|
|
71
99
|
export type MenuItem<T extends string> =
|
|
72
|
-
|
|
|
73
|
-
|
|
|
100
|
+
| SeparatorItem
|
|
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>
|
|
110
|
+
| SectionHeaderMenuItem;
|
|
74
111
|
|
|
75
112
|
// Extract type T of RegularMenuItem<T> within MenuItem<T>
|
|
76
113
|
export type ExtractMenuItemType<T> =
|
|
77
|
-
T extends
|
|
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;
|
|
78
134
|
|
|
79
135
|
export const CHECKBOX_WIDTH = 16;
|
|
80
136
|
export const CHECKBOX_RIGHT_INSET = 8;
|
|
81
137
|
|
|
82
138
|
export const styles = {
|
|
83
|
-
separatorStyle: "h-px bg-divider mx-
|
|
139
|
+
separatorStyle: "h-px bg-divider mx-4 my-1",
|
|
84
140
|
|
|
85
141
|
itemStyle: ({ disabled }: { disabled?: boolean }) => `
|
|
86
142
|
flex-none select-none cursor-pointer rounded
|
|
87
|
-
py-1.5 px-
|
|
143
|
+
py-1.5 px-3
|
|
88
144
|
focus:outline-none focus:text-white focus:bg-primary
|
|
89
145
|
focus:kbd:text-white
|
|
90
146
|
active:bg-primary-light
|
|
91
147
|
transition-colors
|
|
92
148
|
flex items-center
|
|
93
|
-
font-sans text-button font-medium
|
|
149
|
+
font-sans text-button font-medium
|
|
94
150
|
${disabled ? "text-text-disabled" : ""}
|
|
95
151
|
`,
|
|
96
152
|
|
|
@@ -106,18 +162,18 @@ export const styles = {
|
|
|
106
162
|
bg-popover-background
|
|
107
163
|
text-text
|
|
108
164
|
shadow-[0_2px_4px_rgba(0,0,0,0.2),_0_0_12px_rgba(0,0,0,0.1)]
|
|
109
|
-
p-1
|
|
110
|
-
border border-popover-divider
|
|
111
165
|
z-menu
|
|
166
|
+
py-1
|
|
112
167
|
`,
|
|
113
168
|
};
|
|
114
169
|
|
|
115
170
|
function getKeyboardShortcuts<T extends string>(
|
|
116
171
|
item: MenuItem<T>
|
|
117
172
|
): [string, T][] {
|
|
118
|
-
if (item ===
|
|
173
|
+
if (item.type === "separator") return [];
|
|
174
|
+
if (item.type === "sectionHeader") return [];
|
|
119
175
|
|
|
120
|
-
if (item.
|
|
176
|
+
if (item.type === "submenu") return item.items.flatMap(getKeyboardShortcuts);
|
|
121
177
|
|
|
122
178
|
if (item.disabled || !item.value || !item.shortcut) return [];
|
|
123
179
|
|
|
@@ -143,7 +199,11 @@ const ShortcutElement = ({
|
|
|
143
199
|
fixedWidth?: boolean;
|
|
144
200
|
}) => (
|
|
145
201
|
<kbd
|
|
146
|
-
className={
|
|
202
|
+
className={cx(
|
|
203
|
+
textStyles.small,
|
|
204
|
+
"text-inherit opacity-60 tabular-nums",
|
|
205
|
+
fixedWidth && "w-[0.9rem] text-center"
|
|
206
|
+
)}
|
|
147
207
|
>
|
|
148
208
|
{children}
|
|
149
209
|
</kbd>
|
|
@@ -178,3 +238,32 @@ export const KeyboardShortcut = memo(function KeyboardShortcut({
|
|
|
178
238
|
</>
|
|
179
239
|
);
|
|
180
240
|
});
|
|
241
|
+
|
|
242
|
+
export const SectionHeader = memo(function SectionHeader({
|
|
243
|
+
title,
|
|
244
|
+
variant = "normal",
|
|
245
|
+
id,
|
|
246
|
+
isFirst,
|
|
247
|
+
indented,
|
|
248
|
+
}: Omit<SectionHeaderMenuItem, "maxVisibleItems" | "type"> & {
|
|
249
|
+
isFirst?: boolean;
|
|
250
|
+
indented?: boolean;
|
|
251
|
+
}) {
|
|
252
|
+
return (
|
|
253
|
+
<span
|
|
254
|
+
id={id}
|
|
255
|
+
className={cx(
|
|
256
|
+
variant === "label"
|
|
257
|
+
? `text-label ${textStyles.label} !font-bold text-text-disabled`
|
|
258
|
+
: "font-sans text-heading5 font-normal",
|
|
259
|
+
"bg-listview-raised-background flex items-center py-1.5 px-4",
|
|
260
|
+
isFirst && "-mt-1"
|
|
261
|
+
)}
|
|
262
|
+
>
|
|
263
|
+
{indented && (
|
|
264
|
+
<Spacer.Horizontal size={CHECKBOX_WIDTH - CHECKBOX_RIGHT_INSET} />
|
|
265
|
+
)}
|
|
266
|
+
{title}
|
|
267
|
+
</span>
|
|
268
|
+
);
|
|
269
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { ChevronRightIcon } from "@noya-app/noya-icons";
|
|
2
|
+
import * as RadixContextMenu from "@radix-ui/react-context-menu";
|
|
3
|
+
import * as RadixDropdownMenu from "@radix-ui/react-dropdown-menu";
|
|
4
|
+
import React from "react";
|
|
5
|
+
import { MenuItem, SectionHeader, styles } from "./Menu";
|
|
6
|
+
import { SelectItem } from "./SelectItem";
|
|
7
|
+
|
|
8
|
+
type MenuSeparatorComponent = React.FC<{ className?: string }>;
|
|
9
|
+
type MenuItemTextComponent = React.FC<{ children?: React.ReactNode }>;
|
|
10
|
+
type MenuItemComponent = React.ForwardRefExoticComponent<
|
|
11
|
+
React.RefAttributes<HTMLDivElement> &
|
|
12
|
+
React.PropsWithoutRef<{
|
|
13
|
+
className?: string;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
children?: React.ReactNode;
|
|
16
|
+
value: string;
|
|
17
|
+
onSelect?: (event: any) => void;
|
|
18
|
+
}>
|
|
19
|
+
>;
|
|
20
|
+
type MenuCheckboxItemComponent = React.FC<{
|
|
21
|
+
checked?: boolean | "indeterminate";
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
onSelect?: (event: any) => void;
|
|
24
|
+
className?: string;
|
|
25
|
+
children?: React.ReactNode;
|
|
26
|
+
}>;
|
|
27
|
+
|
|
28
|
+
type MenuItemIndicatorComponent = React.FC<{
|
|
29
|
+
className?: string;
|
|
30
|
+
children?: React.ReactNode;
|
|
31
|
+
}>;
|
|
32
|
+
|
|
33
|
+
export type MenuComponents = {
|
|
34
|
+
Separator: MenuSeparatorComponent;
|
|
35
|
+
ItemText: MenuItemTextComponent;
|
|
36
|
+
Item: MenuItemComponent;
|
|
37
|
+
CheckboxItem?: MenuCheckboxItemComponent;
|
|
38
|
+
ItemIndicator?: MenuItemIndicatorComponent;
|
|
39
|
+
Sub?: typeof RadixDropdownMenu.Sub | typeof RadixContextMenu.Sub;
|
|
40
|
+
SubTrigger?:
|
|
41
|
+
| typeof RadixDropdownMenu.SubTrigger
|
|
42
|
+
| typeof RadixContextMenu.SubTrigger;
|
|
43
|
+
SubContent?:
|
|
44
|
+
| typeof RadixDropdownMenu.SubContent
|
|
45
|
+
| typeof RadixContextMenu.SubContent;
|
|
46
|
+
Portal?: typeof RadixDropdownMenu.Portal | typeof RadixContextMenu.Portal;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
type MenuViewportProps<T extends string> = {
|
|
50
|
+
items: MenuItem<T>[];
|
|
51
|
+
Components: MenuComponents;
|
|
52
|
+
onSelect?: (value: any) => void;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const MenuViewport = <T extends string>({
|
|
56
|
+
items,
|
|
57
|
+
Components,
|
|
58
|
+
onSelect,
|
|
59
|
+
}: MenuViewportProps<T>) => {
|
|
60
|
+
const hasCheckedItem = items.some(
|
|
61
|
+
(item) =>
|
|
62
|
+
item.type !== "separator" && item.type !== "sectionHeader" && item.checked
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<>
|
|
67
|
+
{items.map((item, index) => {
|
|
68
|
+
switch (item.type) {
|
|
69
|
+
case "separator":
|
|
70
|
+
return (
|
|
71
|
+
<Components.Separator
|
|
72
|
+
key={index}
|
|
73
|
+
className={styles.separatorStyle}
|
|
74
|
+
/>
|
|
75
|
+
);
|
|
76
|
+
|
|
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}
|
|
144
|
+
>
|
|
145
|
+
{item.title ?? item.value}
|
|
146
|
+
</SelectItem>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
})}
|
|
150
|
+
</>
|
|
151
|
+
);
|
|
152
|
+
};
|