@noya-app/noya-designsystem 0.1.46 → 0.1.48
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 +16 -0
- package/dist/index.css +1 -1
- package/dist/index.d.mts +208 -98
- package/dist/index.d.ts +208 -98
- package/dist/index.js +2148 -1604
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2153 -1618
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -7
- package/src/__tests__/validateDropIndicator.test.ts +263 -0
- package/src/components/ActionMenu.tsx +40 -0
- package/src/components/ActivityIndicator.tsx +7 -1
- package/src/components/Collection.tsx +12 -2
- package/src/components/Combobox.tsx +14 -1
- package/src/components/ComboboxMenu.tsx +14 -5
- package/src/components/Dialog.tsx +28 -31
- package/src/components/Drawer.tsx +98 -0
- package/src/components/Grid.tsx +57 -27
- package/src/components/GridView.tsx +39 -25
- package/src/components/InputField.tsx +87 -92
- package/src/components/Label.tsx +7 -7
- package/src/components/List.tsx +42 -16
- package/src/components/ListView.tsx +21 -17
- package/src/components/MediaThumbnail.tsx +117 -0
- package/src/components/SearchCompletionMenu.tsx +31 -37
- package/src/components/SelectMenu.tsx +19 -17
- package/src/components/Sortable.tsx +70 -44
- package/src/components/Switch.tsx +1 -1
- package/src/components/internal/Menu.tsx +38 -23
- package/src/components/internal/MenuViewport.tsx +7 -1
- package/src/components/internal/SelectItem.tsx +51 -27
- package/src/components/internal/__tests__/Menu.test.tsx +12 -0
- package/src/components/workspace/DrawerWorkspaceLayout.tsx +86 -0
- package/src/components/workspace/PanelWorkspaceLayout.tsx +102 -0
- package/src/components/{WorkspaceLayout.tsx → workspace/WorkspaceLayout.tsx} +109 -106
- package/src/components/workspace/types.ts +31 -0
- package/src/contexts/DialogContext.tsx +91 -30
- package/src/hooks/usePreservePanelSize.tsx +1 -5
- package/src/index.css +3 -1
- package/src/index.tsx +4 -1
- package/tailwind.config.ts +1 -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.inputBackground,
|
|
47
|
+
};
|
|
48
|
+
case "warning":
|
|
49
|
+
return {
|
|
50
|
+
icon: cssVars.colors.warning,
|
|
51
|
+
background: cssVars.colors.inputBackground,
|
|
52
|
+
};
|
|
53
|
+
case "icon":
|
|
54
|
+
default:
|
|
55
|
+
return {
|
|
56
|
+
icon: cssVars.colors.icon,
|
|
57
|
+
background: cssVars.colors.inputBackground,
|
|
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,15 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ComboboxMenu,
|
|
3
|
-
Divider,
|
|
4
|
-
IScoredItem,
|
|
5
|
-
InputField,
|
|
6
|
-
ListView,
|
|
7
|
-
SelectableMenuItem,
|
|
8
|
-
Small,
|
|
9
|
-
fuzzyFilter,
|
|
10
|
-
getNextIndex,
|
|
11
|
-
isSelectableMenuItem,
|
|
12
|
-
} from "@noya-app/noya-designsystem";
|
|
13
1
|
import { getCurrentPlatform, handleKeyboardEvent } from "@noya-app/noya-keymap";
|
|
14
2
|
import React, {
|
|
15
3
|
forwardRef,
|
|
@@ -20,6 +8,21 @@ import React, {
|
|
|
20
8
|
useRef,
|
|
21
9
|
useState,
|
|
22
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";
|
|
23
26
|
|
|
24
27
|
export interface ISearchCompletionMenu {
|
|
25
28
|
focus: () => void;
|
|
@@ -32,17 +35,21 @@ export const SearchCompletionMenu = forwardRef(function SearchCompletionMenu(
|
|
|
32
35
|
onClose,
|
|
33
36
|
items,
|
|
34
37
|
width = 200,
|
|
38
|
+
testSelectedIndex,
|
|
39
|
+
testSearch,
|
|
35
40
|
}: {
|
|
36
41
|
onSelect: (item: SelectableMenuItem<string>) => void;
|
|
37
42
|
onHover?: (item: SelectableMenuItem<string>) => void;
|
|
38
43
|
onClose: () => void;
|
|
39
44
|
items: SelectableMenuItem<string>[];
|
|
40
45
|
width?: number;
|
|
46
|
+
testSelectedIndex?: number;
|
|
47
|
+
testSearch?: string;
|
|
41
48
|
},
|
|
42
49
|
forwardedRef: React.ForwardedRef<ISearchCompletionMenu>
|
|
43
50
|
) {
|
|
44
51
|
const [state, setState] = useState({
|
|
45
|
-
search: "",
|
|
52
|
+
search: testSearch ?? "",
|
|
46
53
|
index: 0,
|
|
47
54
|
});
|
|
48
55
|
|
|
@@ -50,6 +57,8 @@ export const SearchCompletionMenu = forwardRef(function SearchCompletionMenu(
|
|
|
50
57
|
|
|
51
58
|
const listRef = React.useRef<ListView.VirtualizedList>(null);
|
|
52
59
|
|
|
60
|
+
const hasCheckedItems = items.some((item) => item.checked);
|
|
61
|
+
|
|
53
62
|
useEffect(() => {
|
|
54
63
|
listRef.current?.scrollToIndex(index);
|
|
55
64
|
}, [index]);
|
|
@@ -155,32 +164,16 @@ export const SearchCompletionMenu = forwardRef(function SearchCompletionMenu(
|
|
|
155
164
|
borderRadius: 0,
|
|
156
165
|
outline: "none",
|
|
157
166
|
boxShadow: "none",
|
|
158
|
-
backgroundColor:
|
|
159
|
-
|
|
167
|
+
backgroundColor: cssVars.colors.inputBackground,
|
|
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",
|
|
160
174
|
}}
|
|
161
175
|
onChange={setSearch}
|
|
162
176
|
onKeyDown={handleKeyDown}
|
|
163
|
-
// onBlur={(event) => {
|
|
164
|
-
// const isWithinPopover =
|
|
165
|
-
// event.relatedTarget &&
|
|
166
|
-
// event.relatedTarget instanceof HTMLElement &&
|
|
167
|
-
// event.relatedTarget.role === 'dialog';
|
|
168
|
-
|
|
169
|
-
// if (isWithinPopover) {
|
|
170
|
-
// inputRef.current?.focus();
|
|
171
|
-
|
|
172
|
-
// event.stopPropagation();
|
|
173
|
-
// event.preventDefault();
|
|
174
|
-
// }
|
|
175
|
-
// }}
|
|
176
|
-
// onPointerDown={(event) => {
|
|
177
|
-
// event.stopPropagation();
|
|
178
|
-
// event.preventDefault();
|
|
179
|
-
// }}
|
|
180
|
-
// onFocusCapture={(event) => {
|
|
181
|
-
// event.stopPropagation();
|
|
182
|
-
// event.preventDefault();
|
|
183
|
-
// }}
|
|
184
177
|
/>
|
|
185
178
|
</InputField.Root>
|
|
186
179
|
<Divider />
|
|
@@ -189,13 +182,14 @@ export const SearchCompletionMenu = forwardRef(function SearchCompletionMenu(
|
|
|
189
182
|
ref={listRef}
|
|
190
183
|
listSize={listSize}
|
|
191
184
|
items={results}
|
|
192
|
-
selectedIndex={index}
|
|
185
|
+
selectedIndex={testSelectedIndex ?? index}
|
|
193
186
|
onSelectItem={(item) => {
|
|
194
187
|
if (isSelectableMenuItem(item)) {
|
|
195
188
|
selectAndClose(item);
|
|
196
189
|
}
|
|
197
190
|
}}
|
|
198
191
|
onHoverIndex={setIndex}
|
|
192
|
+
indented={hasCheckedItems}
|
|
199
193
|
/>
|
|
200
194
|
) : (
|
|
201
195
|
<div
|
|
@@ -93,7 +93,7 @@ const SelectMenuTrigger = memoGeneric(function SelectMenuTrigger({
|
|
|
93
93
|
id={id}
|
|
94
94
|
style={style}
|
|
95
95
|
className={cx(
|
|
96
|
-
className ?? "flex w-full
|
|
96
|
+
className ?? "flex w-full",
|
|
97
97
|
"flex-1 focus:z-interactable relative",
|
|
98
98
|
isOpen && "ring-2 ring-primary"
|
|
99
99
|
)}
|
|
@@ -108,15 +108,12 @@ const SelectMenuTrigger = memoGeneric(function SelectMenuTrigger({
|
|
|
108
108
|
<span className="flex-1 flex">
|
|
109
109
|
<Select.Value placeholder={placeholder} />
|
|
110
110
|
</span>
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
)}
|
|
118
|
-
/>
|
|
119
|
-
</span>
|
|
111
|
+
<DropdownChevronIcon
|
|
112
|
+
className={cx(
|
|
113
|
+
"transition-transform duration-100 ml-1.5",
|
|
114
|
+
isOpen ? "rotate-180" : "rotate-0"
|
|
115
|
+
)}
|
|
116
|
+
/>
|
|
120
117
|
</Button>
|
|
121
118
|
</Select.SelectTrigger>
|
|
122
119
|
);
|
|
@@ -143,7 +140,7 @@ export const SelectMenu = memoGeneric(function SelectMenu<
|
|
|
143
140
|
onFocus,
|
|
144
141
|
onBlur,
|
|
145
142
|
onOpenChange,
|
|
146
|
-
contentStyle,
|
|
143
|
+
contentStyle: contentStyleProp,
|
|
147
144
|
...props
|
|
148
145
|
}: Props<T>) {
|
|
149
146
|
const selectedItem = items.find(
|
|
@@ -181,6 +178,15 @@ export const SelectMenu = memoGeneric(function SelectMenu<
|
|
|
181
178
|
[icon, id, style, className, disabled, value, selectedItem]
|
|
182
179
|
);
|
|
183
180
|
|
|
181
|
+
const contentStyle = useMemo(() => {
|
|
182
|
+
return {
|
|
183
|
+
width: "var(--radix-select-trigger-width)",
|
|
184
|
+
minWidth: "max-content",
|
|
185
|
+
maxHeight: "500px",
|
|
186
|
+
...contentStyleProp,
|
|
187
|
+
};
|
|
188
|
+
}, [contentStyleProp]);
|
|
189
|
+
|
|
184
190
|
const content = (
|
|
185
191
|
<Select.Root
|
|
186
192
|
value={value}
|
|
@@ -206,12 +212,8 @@ export const SelectMenu = memoGeneric(function SelectMenu<
|
|
|
206
212
|
position="popper"
|
|
207
213
|
sideOffset={6}
|
|
208
214
|
collisionPadding={8}
|
|
209
|
-
align="
|
|
210
|
-
style={
|
|
211
|
-
width: "var(--radix-select-trigger-width)",
|
|
212
|
-
maxHeight: "500px",
|
|
213
|
-
...contentStyle,
|
|
214
|
-
}}
|
|
215
|
+
align="center"
|
|
216
|
+
style={contentStyle}
|
|
215
217
|
>
|
|
216
218
|
<Select.ScrollUpButton className={scrollButtonStyles}>
|
|
217
219
|
<ChevronUpIcon
|
|
@@ -6,11 +6,13 @@ import {
|
|
|
6
6
|
DragOverlay,
|
|
7
7
|
DragStartEvent,
|
|
8
8
|
PointerSensor,
|
|
9
|
-
|
|
9
|
+
UniqueIdentifier,
|
|
10
|
+
useDndContext,
|
|
10
11
|
useSensor,
|
|
11
12
|
useSensors,
|
|
12
13
|
} from "@dnd-kit/core";
|
|
13
14
|
import {
|
|
15
|
+
horizontalListSortingStrategy,
|
|
14
16
|
SortableContext,
|
|
15
17
|
useSortable,
|
|
16
18
|
verticalListSortingStrategy,
|
|
@@ -34,7 +36,7 @@ export const normalizeListDestinationIndex = (
|
|
|
34
36
|
return position === "above" ? index : index + 1;
|
|
35
37
|
};
|
|
36
38
|
|
|
37
|
-
const defaultAcceptsDrop: DropValidator = (
|
|
39
|
+
export const defaultAcceptsDrop: DropValidator = (
|
|
38
40
|
sourceIndex,
|
|
39
41
|
destinationIndex,
|
|
40
42
|
position
|
|
@@ -51,34 +53,47 @@ const defaultAcceptsDrop: DropValidator = (
|
|
|
51
53
|
};
|
|
52
54
|
|
|
53
55
|
const SortableItemContext = React.createContext<{
|
|
54
|
-
keys:
|
|
55
|
-
position: Translate;
|
|
56
|
+
keys: UniqueIdentifier[];
|
|
56
57
|
acceptsDrop: DropValidator;
|
|
57
|
-
setActivatorEvent: (event: PointerEvent) => void;
|
|
58
58
|
axis: "x" | "y";
|
|
59
|
+
position: { x: number; y: number };
|
|
60
|
+
setActivatorEvent: (event: PointerEvent) => void;
|
|
59
61
|
}>({
|
|
60
62
|
keys: [],
|
|
61
|
-
position: { x: 0, y: 0 },
|
|
62
63
|
acceptsDrop: defaultAcceptsDrop,
|
|
63
|
-
setActivatorEvent: () => {},
|
|
64
64
|
axis: "y",
|
|
65
|
+
position: { x: 0, y: 0 },
|
|
66
|
+
setActivatorEvent: () => {},
|
|
65
67
|
});
|
|
66
68
|
|
|
67
|
-
|
|
68
|
-
acceptsDrop: DropValidator
|
|
69
|
-
keys:
|
|
70
|
-
activeId:
|
|
71
|
-
overId:
|
|
72
|
-
offsetStart: number
|
|
73
|
-
elementStart: number
|
|
74
|
-
elementSize: number
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
69
|
+
type ValidateDropIndicatorParams = {
|
|
70
|
+
acceptsDrop: DropValidator; // Function that validates if a drop is allowed
|
|
71
|
+
keys: UniqueIdentifier[]; // Array of all sortable item IDs
|
|
72
|
+
activeId: UniqueIdentifier; // ID of item being dragged
|
|
73
|
+
overId: UniqueIdentifier; // ID of item being dragged over
|
|
74
|
+
offsetStart: number; // Current drag position (x or y coordinate)
|
|
75
|
+
elementStart: number; // Start position of target element (left or top)
|
|
76
|
+
elementSize: number; // Size of target element (width or height)
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export function validateDropIndicator({
|
|
80
|
+
acceptsDrop,
|
|
81
|
+
keys,
|
|
82
|
+
activeId,
|
|
83
|
+
overId,
|
|
84
|
+
offsetStart,
|
|
85
|
+
elementStart,
|
|
86
|
+
elementSize,
|
|
87
|
+
}: ValidateDropIndicatorParams): RelativeDropPosition | undefined {
|
|
88
|
+
const activeIndex = keys.findIndex((id) => id === activeId); // index of item being dragged
|
|
89
|
+
const overIndex = keys.findIndex((id) => id === overId); // index of item being dragged over
|
|
90
|
+
|
|
91
|
+
if (activeIndex === -1 || overIndex === -1) return undefined;
|
|
78
92
|
|
|
79
93
|
const acceptsDropInside = acceptsDrop(activeIndex, overIndex, "inside");
|
|
80
94
|
|
|
81
|
-
// If
|
|
95
|
+
// If cursor is in the middle third of the element (between 1/3 and 2/3)
|
|
96
|
+
// and dropping inside is allowed, show "inside" indicator
|
|
82
97
|
if (
|
|
83
98
|
offsetStart >= elementStart + elementSize / 3 &&
|
|
84
99
|
offsetStart <= elementStart + (elementSize * 2) / 3 &&
|
|
@@ -89,7 +104,6 @@ function validateDropIndicator(
|
|
|
89
104
|
// Are we over the top or bottom half of the element?
|
|
90
105
|
const indicator =
|
|
91
106
|
offsetStart < elementStart + elementSize / 2 ? "above" : "below";
|
|
92
|
-
|
|
93
107
|
// Drop above or below if possible, falling back to inside
|
|
94
108
|
return acceptsDrop(activeIndex, overIndex, indicator)
|
|
95
109
|
? indicator
|
|
@@ -105,13 +119,13 @@ function validateDropIndicator(
|
|
|
105
119
|
type UseSortableReturnType = ReturnType<typeof useSortable>;
|
|
106
120
|
|
|
107
121
|
interface ItemProps<T> {
|
|
108
|
-
id:
|
|
122
|
+
id: UniqueIdentifier;
|
|
109
123
|
disabled?: boolean;
|
|
110
124
|
children: (
|
|
111
125
|
props: {
|
|
112
126
|
ref: React.Ref<T>;
|
|
113
127
|
relativeDropPosition?: RelativeDropPosition;
|
|
114
|
-
|
|
128
|
+
style?: React.CSSProperties;
|
|
115
129
|
} & UseSortableReturnType["attributes"]
|
|
116
130
|
) => JSX.Element;
|
|
117
131
|
}
|
|
@@ -124,10 +138,10 @@ function SortableItem<T extends HTMLElement>({
|
|
|
124
138
|
const { keys, position, acceptsDrop, setActivatorEvent, axis } =
|
|
125
139
|
React.useContext(SortableItemContext);
|
|
126
140
|
const sortable = useSortable({ id, disabled });
|
|
141
|
+
const { activatorEvent } = useDndContext();
|
|
127
142
|
|
|
128
143
|
const {
|
|
129
144
|
active,
|
|
130
|
-
activatorEvent,
|
|
131
145
|
attributes,
|
|
132
146
|
listeners,
|
|
133
147
|
setNodeRef,
|
|
@@ -154,15 +168,15 @@ function SortableItem<T extends HTMLElement>({
|
|
|
154
168
|
...listeners,
|
|
155
169
|
relativeDropPosition:
|
|
156
170
|
index >= 0 && index === overIndex && !isDragging && active && over
|
|
157
|
-
? validateDropIndicator(
|
|
171
|
+
? validateDropIndicator({
|
|
158
172
|
acceptsDrop,
|
|
159
173
|
keys,
|
|
160
|
-
active.id,
|
|
161
|
-
over.id,
|
|
162
|
-
axis === "x" ? offsetLeft : offsetTop,
|
|
163
|
-
axis === "x" ? over.rect.
|
|
164
|
-
axis === "x" ? over.rect.width : over.rect.height
|
|
165
|
-
)
|
|
174
|
+
activeId: active.id,
|
|
175
|
+
overId: over.id,
|
|
176
|
+
offsetStart: axis === "x" ? offsetLeft : offsetTop,
|
|
177
|
+
elementStart: axis === "x" ? over.rect.left : over.rect.top,
|
|
178
|
+
elementSize: axis === "x" ? over.rect.width : over.rect.height,
|
|
179
|
+
})
|
|
166
180
|
: undefined,
|
|
167
181
|
});
|
|
168
182
|
}
|
|
@@ -172,7 +186,7 @@ function SortableItem<T extends HTMLElement>({
|
|
|
172
186
|
* ------------------------------------------------------------------------- */
|
|
173
187
|
|
|
174
188
|
interface RootProps {
|
|
175
|
-
keys:
|
|
189
|
+
keys: UniqueIdentifier[];
|
|
176
190
|
children: React.ReactNode;
|
|
177
191
|
renderOverlay?: (index: number) => React.ReactNode;
|
|
178
192
|
onMoveItem?: (
|
|
@@ -202,16 +216,19 @@ function SortableRoot({
|
|
|
202
216
|
|
|
203
217
|
const [activeIndex, setActiveIndex] = React.useState<number | undefined>();
|
|
204
218
|
const activatorEvent = React.useRef<PointerEvent | null>(null);
|
|
219
|
+
const [position, setPosition] = React.useState<{ x: number; y: number }>({
|
|
220
|
+
x: 0,
|
|
221
|
+
y: 0,
|
|
222
|
+
});
|
|
205
223
|
|
|
206
224
|
const setActivatorEvent = React.useCallback((event: PointerEvent) => {
|
|
207
225
|
activatorEvent.current = event;
|
|
208
226
|
}, []);
|
|
209
227
|
|
|
210
|
-
const [position, setPosition] = React.useState<Translate>({ x: 0, y: 0 });
|
|
211
|
-
|
|
212
228
|
const handleDragStart = React.useCallback(
|
|
213
229
|
(event: DragStartEvent) => {
|
|
214
|
-
|
|
230
|
+
const index = keys.findIndex((id) => id === event.active.id);
|
|
231
|
+
setActiveIndex(index);
|
|
215
232
|
},
|
|
216
233
|
[keys]
|
|
217
234
|
);
|
|
@@ -227,23 +244,25 @@ function SortableRoot({
|
|
|
227
244
|
setActiveIndex(undefined);
|
|
228
245
|
|
|
229
246
|
if (over && active.id !== over.id) {
|
|
230
|
-
const oldIndex = keys.
|
|
231
|
-
const newIndex = keys.
|
|
247
|
+
const oldIndex = keys.findIndex((id) => id === active.id);
|
|
248
|
+
const newIndex = keys.findIndex((id) => id === over.id);
|
|
249
|
+
|
|
250
|
+
if (oldIndex === -1 || newIndex === -1) return;
|
|
232
251
|
|
|
233
252
|
const eventX = activatorEvent.current?.clientX ?? 0;
|
|
234
253
|
const eventY = activatorEvent.current?.clientY ?? 0;
|
|
235
254
|
const offsetLeft = eventX + position.x;
|
|
236
255
|
const offsetTop = eventY + position.y;
|
|
237
256
|
|
|
238
|
-
const indicator = validateDropIndicator(
|
|
257
|
+
const indicator = validateDropIndicator({
|
|
239
258
|
acceptsDrop,
|
|
240
259
|
keys,
|
|
241
|
-
active.id,
|
|
242
|
-
over.id,
|
|
243
|
-
axis === "x" ? offsetLeft : offsetTop,
|
|
244
|
-
axis === "x" ? over.rect.
|
|
245
|
-
axis === "x" ? over.rect.width : over.rect.height
|
|
246
|
-
);
|
|
260
|
+
activeId: active.id,
|
|
261
|
+
overId: over.id,
|
|
262
|
+
offsetStart: axis === "x" ? offsetLeft : offsetTop,
|
|
263
|
+
elementStart: axis === "x" ? over.rect.left : over.rect.top,
|
|
264
|
+
elementSize: axis === "x" ? over.rect.width : over.rect.height,
|
|
265
|
+
});
|
|
247
266
|
|
|
248
267
|
if (!indicator) return;
|
|
249
268
|
|
|
@@ -279,7 +298,14 @@ function SortableRoot({
|
|
|
279
298
|
onDragMove={handleDragMove}
|
|
280
299
|
onDragEnd={handleDragEnd}
|
|
281
300
|
>
|
|
282
|
-
<SortableContext
|
|
301
|
+
<SortableContext
|
|
302
|
+
items={keys}
|
|
303
|
+
strategy={
|
|
304
|
+
axis === "x"
|
|
305
|
+
? horizontalListSortingStrategy
|
|
306
|
+
: verticalListSortingStrategy
|
|
307
|
+
}
|
|
308
|
+
>
|
|
283
309
|
{children}
|
|
284
310
|
</SortableContext>
|
|
285
311
|
{mounted &&
|
|
@@ -38,7 +38,7 @@ export const Switch = function Switch({
|
|
|
38
38
|
onChange(newValue);
|
|
39
39
|
}}
|
|
40
40
|
className={cx(
|
|
41
|
-
"all-unset w-8 h-[19px] bg-active-background rounded-full relative cursor-pointer [webkit-tap-highlight-color:rgba(0,0,0,0)] focus:ring-2 focus:ring-primary focus:ring-offset-[1px] outline-none data-[state=checked]:bg-primary transition-all",
|
|
41
|
+
"all-unset w-8 h-[19px] bg-active-background rounded-full relative cursor-pointer ring-offset-background [webkit-tap-highlight-color:rgba(0,0,0,0)] focus:ring-2 focus:ring-primary focus:ring-offset-[1px] outline-none data-[state=checked]:bg-primary transition-all",
|
|
42
42
|
colorScheme === "secondary" && "data-[state=checked]:bg-secondary",
|
|
43
43
|
"focus:z-interactable",
|
|
44
44
|
className
|
|
@@ -62,10 +62,18 @@ export type SeparatorItem = {
|
|
|
62
62
|
type: "separator";
|
|
63
63
|
};
|
|
64
64
|
|
|
65
|
+
export const separator: SeparatorItem = {
|
|
66
|
+
type: "separator",
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
type FalsyReactNode = false | "" | null | undefined;
|
|
70
|
+
|
|
71
|
+
export type MenuItemIcon = IconName | React.ReactElement | FalsyReactNode;
|
|
72
|
+
|
|
65
73
|
type BaseMenuItem = {
|
|
66
74
|
checked?: boolean;
|
|
67
75
|
disabled?: boolean;
|
|
68
|
-
icon?:
|
|
76
|
+
icon?: MenuItemIcon;
|
|
69
77
|
role?: MenuItemRole;
|
|
70
78
|
alwaysInclude?: boolean;
|
|
71
79
|
title: NonNullable<ReactNode>;
|
|
@@ -83,6 +91,7 @@ export type SelectableMenuItem<T extends string> = BaseMenuItem & {
|
|
|
83
91
|
type?: undefined;
|
|
84
92
|
shortcut?: string;
|
|
85
93
|
value: T;
|
|
94
|
+
testSelected?: boolean;
|
|
86
95
|
};
|
|
87
96
|
|
|
88
97
|
type SubMenuItem<T extends string> = BaseMenuItem & {
|
|
@@ -132,39 +141,43 @@ export const isSelectableMenuItemWithScore = (
|
|
|
132
141
|
): item is ScoredSelectableMenuItem<string> =>
|
|
133
142
|
isSelectableMenuItem(item) && "index" in item && "score" in item;
|
|
134
143
|
|
|
144
|
+
export const getMenuItemKey = <T extends string>(
|
|
145
|
+
item: MenuItem<T>,
|
|
146
|
+
index: number
|
|
147
|
+
): string => {
|
|
148
|
+
switch (item.type) {
|
|
149
|
+
case "separator":
|
|
150
|
+
return index.toString();
|
|
151
|
+
case "sectionHeader":
|
|
152
|
+
return item.id;
|
|
153
|
+
case "submenu":
|
|
154
|
+
return item.id;
|
|
155
|
+
default:
|
|
156
|
+
return item.value;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
135
160
|
export const CHECKBOX_WIDTH = 16;
|
|
136
161
|
export const CHECKBOX_RIGHT_INSET = 8;
|
|
162
|
+
export const CHECKBOX_INDENT_WIDTH = 6;
|
|
137
163
|
|
|
138
164
|
export const styles = {
|
|
139
165
|
separatorStyle: "h-px bg-divider mx-4 my-1",
|
|
140
|
-
|
|
166
|
+
selectedItemStyle:
|
|
167
|
+
"focus:outline-none focus:text-white focus:bg-primary focus:kbd:text-white",
|
|
168
|
+
testSelectedItemStyle: "outline-none text-white bg-primary kbd:text-white",
|
|
141
169
|
itemStyle: ({ disabled }: { disabled?: boolean }) => `
|
|
142
170
|
flex-none select-none cursor-pointer rounded
|
|
143
|
-
py-1.5 px-
|
|
144
|
-
focus:outline-none focus:text-white focus:bg-primary
|
|
145
|
-
focus:kbd:text-white
|
|
171
|
+
py-1.5 px-2
|
|
146
172
|
active:bg-primary-light
|
|
147
173
|
transition-colors
|
|
148
174
|
flex items-center
|
|
149
175
|
font-sans text-button font-medium
|
|
150
176
|
${disabled ? "text-text-disabled" : ""}
|
|
151
177
|
`,
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
relative
|
|
156
|
-
-left-2.5
|
|
157
|
-
-mr-2
|
|
158
|
-
`,
|
|
159
|
-
|
|
160
|
-
contentStyle: `
|
|
161
|
-
rounded
|
|
162
|
-
bg-popover-background
|
|
163
|
-
text-text
|
|
164
|
-
shadow-[0_2px_4px_rgba(0,0,0,0.2),_0_0_12px_rgba(0,0,0,0.1)]
|
|
165
|
-
z-menu
|
|
166
|
-
py-1
|
|
167
|
-
`,
|
|
178
|
+
itemIndicatorStyle: "flex items-center relative -left-2.5 -mr-2",
|
|
179
|
+
contentStyle:
|
|
180
|
+
"rounded bg-popover-background text-text shadow-[0_2px_4px_rgba(0,0,0,0.2),_0_0_12px_rgba(0,0,0,0.1)] z-menu py-1",
|
|
168
181
|
};
|
|
169
182
|
|
|
170
183
|
function getKeyboardShortcuts<T extends string>(
|
|
@@ -256,12 +269,14 @@ export const SectionHeader = memo(function SectionHeader({
|
|
|
256
269
|
variant === "label"
|
|
257
270
|
? `text-label ${textStyles.label} !font-bold text-text-disabled`
|
|
258
271
|
: "font-sans text-heading5 font-normal",
|
|
259
|
-
"bg-listview-raised-background flex items-center py-1.5 px-
|
|
272
|
+
"bg-listview-raised-background flex items-center py-1.5 px-3",
|
|
260
273
|
isFirst && "-mt-1"
|
|
261
274
|
)}
|
|
262
275
|
>
|
|
263
276
|
{indented && (
|
|
264
|
-
<Spacer.Horizontal
|
|
277
|
+
<Spacer.Horizontal
|
|
278
|
+
size={CHECKBOX_WIDTH - CHECKBOX_RIGHT_INSET + CHECKBOX_INDENT_WIDTH}
|
|
279
|
+
/>
|
|
265
280
|
)}
|
|
266
281
|
{title}
|
|
267
282
|
</span>
|
|
@@ -2,6 +2,7 @@ import { ChevronRightIcon } from "@noya-app/noya-icons";
|
|
|
2
2
|
import * as RadixContextMenu from "@radix-ui/react-context-menu";
|
|
3
3
|
import * as RadixDropdownMenu from "@radix-ui/react-dropdown-menu";
|
|
4
4
|
import React from "react";
|
|
5
|
+
import { cx } from "../..";
|
|
5
6
|
import { MenuItem, SectionHeader, styles } from "./Menu";
|
|
6
7
|
import { SelectItem } from "./SelectItem";
|
|
7
8
|
|
|
@@ -15,6 +16,7 @@ type MenuItemComponent = React.ForwardRefExoticComponent<
|
|
|
15
16
|
children?: React.ReactNode;
|
|
16
17
|
value: string;
|
|
17
18
|
onSelect?: (event: any) => void;
|
|
19
|
+
onClick?: (event: React.MouseEvent<any>) => void;
|
|
18
20
|
}>
|
|
19
21
|
>;
|
|
20
22
|
type MenuCheckboxItemComponent = React.FC<{
|
|
@@ -94,7 +96,10 @@ export const MenuViewport = <T extends string>({
|
|
|
94
96
|
return (
|
|
95
97
|
<Components.Sub key={item.id}>
|
|
96
98
|
<Components.SubTrigger
|
|
97
|
-
className={
|
|
99
|
+
className={cx(
|
|
100
|
+
styles.itemStyle({ disabled: item.disabled }),
|
|
101
|
+
styles.selectedItemStyle
|
|
102
|
+
)}
|
|
98
103
|
asChild
|
|
99
104
|
onClick={(e) => {
|
|
100
105
|
e.stopPropagation();
|
|
@@ -141,6 +146,7 @@ export const MenuViewport = <T extends string>({
|
|
|
141
146
|
onSelect={onSelect}
|
|
142
147
|
Components={Components}
|
|
143
148
|
indented={hasCheckedItem}
|
|
149
|
+
testSelected={item.testSelected}
|
|
144
150
|
>
|
|
145
151
|
{item.title ?? item.value}
|
|
146
152
|
</SelectItem>
|