@noya-app/noya-designsystem 0.1.44 → 0.1.45
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 +9 -0
- package/dist/index.css +1 -1
- package/dist/index.d.mts +320 -78
- package/dist/index.d.ts +320 -78
- package/dist/index.js +2113 -1374
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2000 -1274
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/components/AnimatePresence.tsx +10 -1
- package/src/components/Avatar.tsx +2 -0
- package/src/components/Checkbox.tsx +6 -1
- package/src/components/Combobox.tsx +6 -4
- package/src/components/ComboboxMenu.tsx +30 -15
- package/src/components/CommandPalette.tsx +69 -0
- package/src/components/ContextMenu.tsx +33 -134
- package/src/components/DropdownMenu.tsx +47 -155
- package/src/components/Fade.tsx +62 -0
- package/src/components/InputField.tsx +109 -133
- package/src/components/Label.tsx +81 -7
- package/src/components/LabeledField.tsx +112 -0
- package/src/components/ListView.tsx +55 -52
- package/src/components/Popover.tsx +12 -1
- package/src/components/SearchCompletionMenu.tsx +206 -0
- package/src/components/SegmentedControl.tsx +5 -2
- package/src/components/SelectMenu.tsx +104 -124
- package/src/components/SidebarList.tsx +252 -0
- 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 +6 -2
- package/src/components/TreeView.tsx +3 -0
- package/src/components/WorkspaceLayout.tsx +38 -18
- package/src/components/internal/Menu.tsx +58 -10
- package/src/components/internal/MenuViewport.tsx +151 -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 +12 -5
- package/src/utils/combobox.ts +4 -1
- package/src/utils/createSectionedMenu.ts +11 -8
- package/src/utils/selection.ts +36 -0
|
@@ -6,7 +6,10 @@ import { Button } from "./Button";
|
|
|
6
6
|
import { DropdownMenu } from "./DropdownMenu";
|
|
7
7
|
import { renderIcon } from "./Icons";
|
|
8
8
|
import type { MenuItem } from "./internal/Menu";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
getKeyboardShortcutsForMenuItems,
|
|
11
|
+
SectionHeader,
|
|
12
|
+
} from "./internal/Menu";
|
|
10
13
|
import { Spacer } from "./Spacer";
|
|
11
14
|
|
|
12
15
|
export interface ToolbarProps<T extends string = string> {
|
|
@@ -82,7 +85,8 @@ export function ToolbarMenu<T extends string>({
|
|
|
82
85
|
return (
|
|
83
86
|
<>
|
|
84
87
|
{items.map((item, i) => {
|
|
85
|
-
if (
|
|
88
|
+
if (item.type === "separator") return null;
|
|
89
|
+
if (item.type === "sectionHeader") return <SectionHeader {...item} />;
|
|
86
90
|
|
|
87
91
|
if (!item.items) {
|
|
88
92
|
return (
|
|
@@ -12,6 +12,7 @@ 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;
|
|
16
17
|
};
|
|
17
18
|
|
|
@@ -23,6 +24,7 @@ const TreeRow = forwardRefGeneric(function TreeRow<MenuItemType extends string>(
|
|
|
23
24
|
icon,
|
|
24
25
|
expanded,
|
|
25
26
|
onClickChevron,
|
|
27
|
+
chevronClassName,
|
|
26
28
|
children,
|
|
27
29
|
...rest
|
|
28
30
|
}: TreeViewRowProps<MenuItemType>,
|
|
@@ -46,6 +48,7 @@ const TreeRow = forwardRefGeneric(function TreeRow<MenuItemType extends string>(
|
|
|
46
48
|
<Spacer.Horizontal size={19} />
|
|
47
49
|
) : (
|
|
48
50
|
<IconButton
|
|
51
|
+
className={chevronClassName}
|
|
49
52
|
iconName={expanded ? "ChevronDownIcon2" : "ChevronRightIcon2"}
|
|
50
53
|
onClick={handleClickChevron}
|
|
51
54
|
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(
|
|
@@ -72,19 +74,22 @@ export const WorkspaceLayout = forwardRef(function WorkspaceLayout(
|
|
|
72
74
|
onChangeLayoutState,
|
|
73
75
|
leftOptions: {
|
|
74
76
|
initialSize: leftInitialSize = 280,
|
|
75
|
-
minSize: leftMinSize,
|
|
77
|
+
minSize: leftMinSize = 200,
|
|
78
|
+
maxSize: leftMaxSize = 500,
|
|
76
79
|
resizable: leftResizable = true,
|
|
77
80
|
style: leftStyle,
|
|
78
81
|
className: leftClassName,
|
|
79
82
|
} = {},
|
|
80
83
|
rightOptions: {
|
|
81
84
|
initialSize: rightInitialSize = 280,
|
|
82
|
-
minSize: rightMinSize,
|
|
85
|
+
minSize: rightMinSize = 200,
|
|
86
|
+
maxSize: rightMaxSize = 500,
|
|
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();
|
|
@@ -100,6 +105,10 @@ export const WorkspaceLayout = forwardRef(function WorkspaceLayout(
|
|
|
100
105
|
leftMinSize !== undefined ? getPercentage(leftMinSize) : undefined;
|
|
101
106
|
const rightSidebarMinPercentage =
|
|
102
107
|
rightMinSize !== undefined ? getPercentage(rightMinSize) : undefined;
|
|
108
|
+
const leftSidebarMaxPercentage =
|
|
109
|
+
leftMaxSize !== undefined ? getPercentage(leftMaxSize) : undefined;
|
|
110
|
+
const rightSidebarMaxPercentage =
|
|
111
|
+
rightMaxSize !== undefined ? getPercentage(rightMaxSize) : undefined;
|
|
103
112
|
|
|
104
113
|
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
|
|
105
114
|
const leftSidebarRef = useRef<ImperativePanelHandle>(null);
|
|
@@ -189,6 +198,7 @@ export const WorkspaceLayout = forwardRef(function WorkspaceLayout(
|
|
|
189
198
|
id={id}
|
|
190
199
|
className={cx("flex flex-col bg-canvas-background", className)}
|
|
191
200
|
style={style}
|
|
201
|
+
data-theme={theme}
|
|
192
202
|
>
|
|
193
203
|
{toolbar}
|
|
194
204
|
<div className="flex flex-row flex-1">
|
|
@@ -212,11 +222,20 @@ export const WorkspaceLayout = forwardRef(function WorkspaceLayout(
|
|
|
212
222
|
ref={leftSidebarRef}
|
|
213
223
|
defaultSize={leftSidebarPercentage}
|
|
214
224
|
minSize={leftSidebarMinPercentage ?? leftSidebarPercentage}
|
|
215
|
-
{
|
|
225
|
+
maxSize={
|
|
226
|
+
leftSidebarMaxPercentage ??
|
|
227
|
+
(!leftResizable ? leftSidebarPercentage : undefined)
|
|
228
|
+
}
|
|
216
229
|
collapsible
|
|
217
230
|
>
|
|
218
231
|
{internalLayoutState?.leftSidebarCollapsed ? null : (
|
|
219
|
-
<PanelInner
|
|
232
|
+
<PanelInner
|
|
233
|
+
style={leftStyle}
|
|
234
|
+
className={cx(
|
|
235
|
+
"bg-sidebar-background flex-col",
|
|
236
|
+
leftClassName
|
|
237
|
+
)}
|
|
238
|
+
>
|
|
220
239
|
{left}
|
|
221
240
|
</PanelInner>
|
|
222
241
|
)}
|
|
@@ -231,7 +250,7 @@ export const WorkspaceLayout = forwardRef(function WorkspaceLayout(
|
|
|
231
250
|
minSize={10}
|
|
232
251
|
className="flex relative"
|
|
233
252
|
>
|
|
234
|
-
{children}
|
|
253
|
+
<PanelInner className="bg-canvas-background">{children}</PanelInner>
|
|
235
254
|
</Panel>
|
|
236
255
|
{right && (
|
|
237
256
|
<>
|
|
@@ -243,13 +262,20 @@ export const WorkspaceLayout = forwardRef(function WorkspaceLayout(
|
|
|
243
262
|
ref={rightSidebarRef}
|
|
244
263
|
minSize={rightSidebarMinPercentage ?? rightSidebarPercentage}
|
|
245
264
|
defaultSize={rightSidebarPercentage}
|
|
246
|
-
{
|
|
247
|
-
|
|
248
|
-
|
|
265
|
+
maxSize={
|
|
266
|
+
rightSidebarMaxPercentage ??
|
|
267
|
+
(!rightResizable ? rightSidebarPercentage : undefined)
|
|
268
|
+
}
|
|
249
269
|
collapsible
|
|
250
270
|
>
|
|
251
271
|
{internalLayoutState?.rightSidebarCollapsed ? null : (
|
|
252
|
-
<PanelInner
|
|
272
|
+
<PanelInner
|
|
273
|
+
style={rightStyle}
|
|
274
|
+
className={cx(
|
|
275
|
+
"bg-sidebar-background flex-col",
|
|
276
|
+
rightClassName
|
|
277
|
+
)}
|
|
278
|
+
>
|
|
253
279
|
{right}
|
|
254
280
|
</PanelInner>
|
|
255
281
|
)}
|
|
@@ -272,13 +298,7 @@ const PanelInner = memo(function PanelInner({
|
|
|
272
298
|
className?: string;
|
|
273
299
|
}) {
|
|
274
300
|
return (
|
|
275
|
-
<div
|
|
276
|
-
style={style}
|
|
277
|
-
className={cx(
|
|
278
|
-
"absolute inset-0 flex flex-col bg-sidebar-background",
|
|
279
|
-
className
|
|
280
|
-
)}
|
|
281
|
-
>
|
|
301
|
+
<div style={style} className={cx("absolute inset-0 flex", className)}>
|
|
282
302
|
{children}
|
|
283
303
|
</div>
|
|
284
304
|
);
|
|
@@ -1,11 +1,15 @@
|
|
|
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";
|
|
4
5
|
import withSeparatorElements from "../../utils/withSeparatorElements";
|
|
5
6
|
import { IconName } from "../Icons";
|
|
7
|
+
import { Spacer } from "../Spacer";
|
|
6
8
|
import { textStyles } from "../Text";
|
|
7
9
|
|
|
8
|
-
export
|
|
10
|
+
export type SeparatorItem = {
|
|
11
|
+
type: "separator";
|
|
12
|
+
};
|
|
9
13
|
|
|
10
14
|
// From electron
|
|
11
15
|
export type MenuItemRole =
|
|
@@ -66,11 +70,22 @@ export type RegularMenuItem<T extends string> = {
|
|
|
66
70
|
icon?: Exclude<ReactNode, string> | IconName;
|
|
67
71
|
items?: MenuItem<T>[];
|
|
68
72
|
role?: MenuItemRole;
|
|
73
|
+
type?: undefined;
|
|
74
|
+
alwaysInclude?: boolean;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export type SectionHeaderMenuItem = {
|
|
78
|
+
type: "sectionHeader";
|
|
79
|
+
id: string;
|
|
80
|
+
title: string;
|
|
81
|
+
maxVisibleItems?: number;
|
|
82
|
+
variant?: "normal" | "label";
|
|
69
83
|
};
|
|
70
84
|
|
|
71
85
|
export type MenuItem<T extends string> =
|
|
72
|
-
|
|
|
73
|
-
| RegularMenuItem<T
|
|
86
|
+
| SeparatorItem
|
|
87
|
+
| RegularMenuItem<T>
|
|
88
|
+
| SectionHeaderMenuItem;
|
|
74
89
|
|
|
75
90
|
// Extract type T of RegularMenuItem<T> within MenuItem<T>
|
|
76
91
|
export type ExtractMenuItemType<T> =
|
|
@@ -80,17 +95,17 @@ export const CHECKBOX_WIDTH = 16;
|
|
|
80
95
|
export const CHECKBOX_RIGHT_INSET = 8;
|
|
81
96
|
|
|
82
97
|
export const styles = {
|
|
83
|
-
separatorStyle: "h-px bg-divider mx-
|
|
98
|
+
separatorStyle: "h-px bg-divider mx-4 my-1",
|
|
84
99
|
|
|
85
100
|
itemStyle: ({ disabled }: { disabled?: boolean }) => `
|
|
86
101
|
flex-none select-none cursor-pointer rounded
|
|
87
|
-
py-1.5 px-
|
|
102
|
+
py-1.5 px-3
|
|
88
103
|
focus:outline-none focus:text-white focus:bg-primary
|
|
89
104
|
focus:kbd:text-white
|
|
90
105
|
active:bg-primary-light
|
|
91
106
|
transition-colors
|
|
92
107
|
flex items-center
|
|
93
|
-
font-sans text-button font-medium
|
|
108
|
+
font-sans text-button font-medium
|
|
94
109
|
${disabled ? "text-text-disabled" : ""}
|
|
95
110
|
`,
|
|
96
111
|
|
|
@@ -106,16 +121,16 @@ export const styles = {
|
|
|
106
121
|
bg-popover-background
|
|
107
122
|
text-text
|
|
108
123
|
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
124
|
z-menu
|
|
125
|
+
py-1
|
|
112
126
|
`,
|
|
113
127
|
};
|
|
114
128
|
|
|
115
129
|
function getKeyboardShortcuts<T extends string>(
|
|
116
130
|
item: MenuItem<T>
|
|
117
131
|
): [string, T][] {
|
|
118
|
-
if (item ===
|
|
132
|
+
if (item.type === "separator") return [];
|
|
133
|
+
if (item.type === "sectionHeader") return [];
|
|
119
134
|
|
|
120
135
|
if (item.items) return item.items.flatMap(getKeyboardShortcuts);
|
|
121
136
|
|
|
@@ -143,7 +158,11 @@ const ShortcutElement = ({
|
|
|
143
158
|
fixedWidth?: boolean;
|
|
144
159
|
}) => (
|
|
145
160
|
<kbd
|
|
146
|
-
className={
|
|
161
|
+
className={cx(
|
|
162
|
+
textStyles.small,
|
|
163
|
+
"text-inherit opacity-60 tabular-nums",
|
|
164
|
+
fixedWidth && "w-[0.9rem] text-center"
|
|
165
|
+
)}
|
|
147
166
|
>
|
|
148
167
|
{children}
|
|
149
168
|
</kbd>
|
|
@@ -178,3 +197,32 @@ export const KeyboardShortcut = memo(function KeyboardShortcut({
|
|
|
178
197
|
</>
|
|
179
198
|
);
|
|
180
199
|
});
|
|
200
|
+
|
|
201
|
+
export const SectionHeader = memo(function SectionHeader({
|
|
202
|
+
title,
|
|
203
|
+
variant = "normal",
|
|
204
|
+
id,
|
|
205
|
+
isFirst,
|
|
206
|
+
indented,
|
|
207
|
+
}: Omit<SectionHeaderMenuItem, "maxVisibleItems" | "type"> & {
|
|
208
|
+
isFirst?: boolean;
|
|
209
|
+
indented?: boolean;
|
|
210
|
+
}) {
|
|
211
|
+
return (
|
|
212
|
+
<span
|
|
213
|
+
id={id}
|
|
214
|
+
className={cx(
|
|
215
|
+
variant === "label"
|
|
216
|
+
? `text-label ${textStyles.label} !font-bold text-text-disabled`
|
|
217
|
+
: "font-sans text-heading5 font-normal",
|
|
218
|
+
"bg-listview-raised-background flex items-center py-1.5 px-4",
|
|
219
|
+
isFirst && "-mt-1"
|
|
220
|
+
)}
|
|
221
|
+
>
|
|
222
|
+
{indented && (
|
|
223
|
+
<Spacer.Horizontal size={CHECKBOX_WIDTH - CHECKBOX_RIGHT_INSET} />
|
|
224
|
+
)}
|
|
225
|
+
{title}
|
|
226
|
+
</span>
|
|
227
|
+
);
|
|
228
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
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
|
+
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
|
+
}
|
|
87
|
+
|
|
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
|
|
102
|
+
>
|
|
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
|
+
);
|
|
132
|
+
}
|
|
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
|
+
</>
|
|
150
|
+
);
|
|
151
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { renderIcon } from "../Icons";
|
|
2
|
+
|
|
3
|
+
import { CheckIcon } from "@noya-app/noya-icons";
|
|
4
|
+
import * as Select from "@radix-ui/react-select";
|
|
5
|
+
import React, { useCallback } from "react";
|
|
6
|
+
import { Spacer } from "../Spacer";
|
|
7
|
+
import {
|
|
8
|
+
CHECKBOX_RIGHT_INSET,
|
|
9
|
+
CHECKBOX_WIDTH,
|
|
10
|
+
KeyboardShortcut,
|
|
11
|
+
styles,
|
|
12
|
+
} from "./Menu";
|
|
13
|
+
import { MenuComponents } from "./MenuViewport";
|
|
14
|
+
|
|
15
|
+
export const SelectItem = React.forwardRef(
|
|
16
|
+
(
|
|
17
|
+
{
|
|
18
|
+
children,
|
|
19
|
+
icon,
|
|
20
|
+
disabled,
|
|
21
|
+
checked,
|
|
22
|
+
Components,
|
|
23
|
+
value,
|
|
24
|
+
onSelect,
|
|
25
|
+
shortcut,
|
|
26
|
+
indented,
|
|
27
|
+
...props
|
|
28
|
+
}: Select.SelectItemProps & {
|
|
29
|
+
icon?: React.ReactNode;
|
|
30
|
+
checked?: boolean | "indeterminate";
|
|
31
|
+
Components: MenuComponents;
|
|
32
|
+
onSelect?: (value: any) => void;
|
|
33
|
+
shortcut?: string;
|
|
34
|
+
indented?: boolean;
|
|
35
|
+
},
|
|
36
|
+
forwardedRef: React.ForwardedRef<HTMLDivElement>
|
|
37
|
+
) => {
|
|
38
|
+
const handleSelectItem = useCallback(() => {
|
|
39
|
+
if (!value) return;
|
|
40
|
+
onSelect?.(value);
|
|
41
|
+
}, [onSelect, value]);
|
|
42
|
+
|
|
43
|
+
if (checked && Components.CheckboxItem) {
|
|
44
|
+
return (
|
|
45
|
+
<Components.CheckboxItem
|
|
46
|
+
checked={checked}
|
|
47
|
+
disabled={disabled}
|
|
48
|
+
onSelect={handleSelectItem}
|
|
49
|
+
className={styles.itemStyle({ disabled })}
|
|
50
|
+
>
|
|
51
|
+
{Components.ItemIndicator && (
|
|
52
|
+
<Components.ItemIndicator className={styles.itemIndicatorStyle}>
|
|
53
|
+
<CheckIcon />
|
|
54
|
+
</Components.ItemIndicator>
|
|
55
|
+
)}
|
|
56
|
+
{icon && (
|
|
57
|
+
<>
|
|
58
|
+
{renderIcon(icon)}
|
|
59
|
+
<Spacer.Horizontal size={8} />
|
|
60
|
+
</>
|
|
61
|
+
)}
|
|
62
|
+
{children}
|
|
63
|
+
{shortcut && (
|
|
64
|
+
<>
|
|
65
|
+
<Spacer.Horizontal />
|
|
66
|
+
<Spacer.Horizontal size={24} />
|
|
67
|
+
<KeyboardShortcut shortcut={shortcut} />
|
|
68
|
+
</>
|
|
69
|
+
)}
|
|
70
|
+
</Components.CheckboxItem>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div className="px-1">
|
|
76
|
+
<Components.Item
|
|
77
|
+
className={styles.itemStyle({ disabled })}
|
|
78
|
+
disabled={disabled}
|
|
79
|
+
value={value}
|
|
80
|
+
ref={forwardedRef}
|
|
81
|
+
onSelect={handleSelectItem}
|
|
82
|
+
{...props}
|
|
83
|
+
>
|
|
84
|
+
<div className="flex flex-1 items-center">
|
|
85
|
+
{checked ? (
|
|
86
|
+
<div className={styles.itemIndicatorStyle}>
|
|
87
|
+
<CheckIcon />
|
|
88
|
+
</div>
|
|
89
|
+
) : indented ? (
|
|
90
|
+
<Spacer.Horizontal size={CHECKBOX_WIDTH - CHECKBOX_RIGHT_INSET} />
|
|
91
|
+
) : null}
|
|
92
|
+
{icon && (
|
|
93
|
+
<>
|
|
94
|
+
{renderIcon(icon)}
|
|
95
|
+
<Spacer.Horizontal size={8} />
|
|
96
|
+
</>
|
|
97
|
+
)}
|
|
98
|
+
<Components.ItemText>{children}</Components.ItemText>
|
|
99
|
+
{shortcut && (
|
|
100
|
+
<>
|
|
101
|
+
<Spacer.Horizontal />
|
|
102
|
+
<Spacer.Horizontal size={24} />
|
|
103
|
+
<KeyboardShortcut shortcut={shortcut} />
|
|
104
|
+
</>
|
|
105
|
+
)}
|
|
106
|
+
</div>
|
|
107
|
+
</Components.Item>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export const IndentContext = React.createContext<{
|
|
4
|
+
indentLevel: number;
|
|
5
|
+
}>({
|
|
6
|
+
indentLevel: 0,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export const useIndent = () => {
|
|
10
|
+
const { indentLevel } = React.useContext(IndentContext);
|
|
11
|
+
return indentLevel;
|
|
12
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React, { ReactNode } from "react";
|
|
2
|
+
import { LabelPosition } from "../components/Label";
|
|
3
|
+
|
|
4
|
+
export const LabelContext = React.createContext<{
|
|
5
|
+
fieldId?: string;
|
|
6
|
+
label?: React.ReactNode;
|
|
7
|
+
}>({
|
|
8
|
+
fieldId: "",
|
|
9
|
+
label: undefined,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
type UseLabelParams = {
|
|
13
|
+
label?: ReactNode;
|
|
14
|
+
fieldId?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const useLabel = ({ label, fieldId }: UseLabelParams) => {
|
|
18
|
+
const { label: labelFromContext, fieldId: fieldIdFromContext } =
|
|
19
|
+
React.useContext(LabelContext);
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
label: label ?? labelFromContext,
|
|
23
|
+
fieldId: fieldId ?? fieldIdFromContext,
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const LabelPositionContext =
|
|
28
|
+
React.createContext<LabelPositionContextValue>({
|
|
29
|
+
position: "start",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const useLabelPosition = () => {
|
|
33
|
+
const { position } = React.useContext(LabelPositionContext);
|
|
34
|
+
return position;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type LabelPositionContextValue = {
|
|
38
|
+
position: LabelPosition;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type LabelWidthContextValue = {
|
|
42
|
+
width: number;
|
|
43
|
+
};
|
|
44
|
+
export const LabelWidthContext = React.createContext<LabelWidthContextValue>({
|
|
45
|
+
width: 100,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export const useLabelWidth = () => {
|
|
49
|
+
const { width } = React.useContext(LabelWidthContext);
|
|
50
|
+
return width;
|
|
51
|
+
};
|