@noya-app/noya-designsystem 0.1.45 → 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 +10 -10
- package/CHANGELOG.md +9 -0
- package/dist/index.css +1 -1
- package/dist/index.d.mts +1144 -1070
- package/dist/index.d.ts +1144 -1070
- package/dist/index.js +10127 -9653
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +10407 -9937
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/combobox.test.ts +137 -89
- package/src/components/Breadcrumbs.tsx +29 -0
- package/src/components/Collection.tsx +74 -0
- package/src/components/Combobox.tsx +69 -52
- package/src/components/ComboboxMenu.tsx +37 -19
- package/src/components/DropdownMenu.tsx +0 -1
- package/src/components/EditableText.tsx +203 -0
- package/src/components/Grid.tsx +243 -0
- package/src/components/GridView.tsx +86 -96
- package/src/components/List.tsx +268 -0
- package/src/components/ListView.tsx +10 -9
- package/src/components/SearchCompletionMenu.tsx +13 -9
- package/src/components/SegmentedControl.tsx +7 -4
- package/src/components/SelectMenu.tsx +9 -5
- package/src/components/Toolbar.tsx +5 -4
- package/src/components/TreeView.tsx +1 -0
- package/src/components/WorkspaceLayout.tsx +28 -4
- package/src/components/internal/Menu.tsx +55 -14
- package/src/components/internal/MenuViewport.tsx +78 -77
- package/src/index.tsx +6 -4
- package/src/utils/combobox.ts +115 -103
- package/src/utils/createSectionedMenu.ts +3 -9
- package/src/utils/fuzzyScorer.ts +11 -8
- package/src/utils/selection.ts +16 -4
- package/src/components/SidebarList.tsx +0 -252
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import {
|
|
2
|
+
forwardRefGeneric,
|
|
3
|
+
memoGeneric,
|
|
4
|
+
useFileDropTarget,
|
|
5
|
+
} from "@noya-app/react-utils";
|
|
6
|
+
import React, { useImperativeHandle, useRef, useState } from "react";
|
|
7
|
+
import { cssVars } from "../theme";
|
|
8
|
+
import { cx } from "../utils/classNames";
|
|
9
|
+
import { updateSelection } from "../utils/selection";
|
|
10
|
+
import { CollectionProps, CollectionRef } from "./Collection";
|
|
11
|
+
import { ListView } from "./ListView";
|
|
12
|
+
import { TreeView } from "./TreeView";
|
|
13
|
+
|
|
14
|
+
const cssVarOverrides = {
|
|
15
|
+
"--icon-selected": cssVars.colors.primary,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const emptyArray: string[] = [];
|
|
19
|
+
|
|
20
|
+
export const List = memoGeneric(
|
|
21
|
+
forwardRefGeneric(function List<T, M extends string = string>(
|
|
22
|
+
props: CollectionProps<T, M> & { ref?: React.ForwardedRef<CollectionRef> },
|
|
23
|
+
ref: React.ForwardedRef<CollectionRef>
|
|
24
|
+
) {
|
|
25
|
+
const {
|
|
26
|
+
className,
|
|
27
|
+
items,
|
|
28
|
+
getId,
|
|
29
|
+
getName,
|
|
30
|
+
expandable,
|
|
31
|
+
getExpanded,
|
|
32
|
+
getRenamable,
|
|
33
|
+
getDepth,
|
|
34
|
+
setExpanded,
|
|
35
|
+
menuItems,
|
|
36
|
+
onSelectMenuItem,
|
|
37
|
+
onRename,
|
|
38
|
+
renderThumbnail,
|
|
39
|
+
renderAction,
|
|
40
|
+
renderDetail,
|
|
41
|
+
onSelectionChange,
|
|
42
|
+
itemRoleDescription = "clickable item",
|
|
43
|
+
detailPosition = "end",
|
|
44
|
+
size = "medium",
|
|
45
|
+
testHoveredId,
|
|
46
|
+
testRenamingId,
|
|
47
|
+
scrollable = false,
|
|
48
|
+
acceptsDrop,
|
|
49
|
+
onMoveItem,
|
|
50
|
+
onFilesDrop,
|
|
51
|
+
onDoubleClickItem,
|
|
52
|
+
renamable,
|
|
53
|
+
selectedIds = emptyArray,
|
|
54
|
+
} = props;
|
|
55
|
+
|
|
56
|
+
const [internalHoveredId, setHoveredId] = useState<string>();
|
|
57
|
+
const [internalRenamingId, setRenamingId] = useState<string>();
|
|
58
|
+
|
|
59
|
+
const hoveredId = internalHoveredId ?? testHoveredId;
|
|
60
|
+
const renamingId = testRenamingId ?? internalRenamingId;
|
|
61
|
+
|
|
62
|
+
const handleSelect = (itemId: string, event?: ListView.ClickInfo) => {
|
|
63
|
+
if (!onSelectionChange) return;
|
|
64
|
+
|
|
65
|
+
setRenamingId(undefined);
|
|
66
|
+
const newSelectedIds = updateSelection(
|
|
67
|
+
items.map(getId),
|
|
68
|
+
selectedIds,
|
|
69
|
+
itemId,
|
|
70
|
+
event
|
|
71
|
+
);
|
|
72
|
+
onSelectionChange(newSelectedIds, event);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const handleMenuAction = (action: M) => {
|
|
76
|
+
if (!onSelectMenuItem) return;
|
|
77
|
+
|
|
78
|
+
const selectedItems = items.filter((item) =>
|
|
79
|
+
selectedIds.includes(getId(item))
|
|
80
|
+
);
|
|
81
|
+
onSelectMenuItem(action, selectedItems);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const ViewComponent = expandable ? TreeView : ListView;
|
|
85
|
+
|
|
86
|
+
const fileDropTargetRef = useRef<HTMLDivElement>(null);
|
|
87
|
+
const { isDropTargetActive, dropTargetProps } = useFileDropTarget(
|
|
88
|
+
fileDropTargetRef,
|
|
89
|
+
onFilesDrop
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
useImperativeHandle(ref, () => ({
|
|
93
|
+
editName: (id: string) => {
|
|
94
|
+
setRenamingId(id);
|
|
95
|
+
},
|
|
96
|
+
}));
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<ViewComponent.Root
|
|
100
|
+
variant="bare"
|
|
101
|
+
className={cx(className, isDropTargetActive && "bg-primary-pastel")}
|
|
102
|
+
containerRef={fileDropTargetRef}
|
|
103
|
+
scrollable={scrollable}
|
|
104
|
+
data={items}
|
|
105
|
+
keyExtractor={getId}
|
|
106
|
+
role="listbox"
|
|
107
|
+
aria-orientation="vertical"
|
|
108
|
+
aria-multiselectable={true}
|
|
109
|
+
expandable={expandable}
|
|
110
|
+
sortable={expandable}
|
|
111
|
+
acceptsDrop={acceptsDrop}
|
|
112
|
+
onMoveItem={onMoveItem}
|
|
113
|
+
{...dropTargetProps}
|
|
114
|
+
renderItem={(
|
|
115
|
+
item: T,
|
|
116
|
+
_: number,
|
|
117
|
+
{ isDragging }: { isDragging: boolean }
|
|
118
|
+
) => {
|
|
119
|
+
const id = getId(item);
|
|
120
|
+
const isHovered = hoveredId === id && !isDragging;
|
|
121
|
+
const expanded = getExpanded?.(item);
|
|
122
|
+
const itemIsRenamable = getRenamable?.(item) ?? renamable;
|
|
123
|
+
const isSelected = selectedIds.includes(id);
|
|
124
|
+
const isRenaming = itemIsRenamable && onRename && renamingId === id;
|
|
125
|
+
const isTestingRenaming = testRenamingId === id;
|
|
126
|
+
|
|
127
|
+
const thumbnail = renderThumbnail?.(item, isSelected);
|
|
128
|
+
const action =
|
|
129
|
+
(isHovered || isSelected) &&
|
|
130
|
+
!isRenaming &&
|
|
131
|
+
renderAction?.(item, isSelected);
|
|
132
|
+
const end =
|
|
133
|
+
action ||
|
|
134
|
+
(detailPosition === "end" && renderDetail?.(item, isSelected));
|
|
135
|
+
const below =
|
|
136
|
+
detailPosition === "below" && renderDetail?.(item, isSelected);
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<ViewComponent.Row
|
|
140
|
+
id={id}
|
|
141
|
+
key={id}
|
|
142
|
+
role="option"
|
|
143
|
+
sortable={expandable}
|
|
144
|
+
aria-roledescription={itemRoleDescription}
|
|
145
|
+
aria-label={getName(item)}
|
|
146
|
+
aria-selected={isSelected}
|
|
147
|
+
className={cx("cursor-pointer p-1 rounded")}
|
|
148
|
+
backgroundColor={
|
|
149
|
+
isSelected ? cssVars.colors.primaryPastel : undefined
|
|
150
|
+
}
|
|
151
|
+
chevronClassName={cx(expandable && "relative left-2")}
|
|
152
|
+
style={cssVarOverrides as React.CSSProperties}
|
|
153
|
+
hovered={isHovered}
|
|
154
|
+
selected={isSelected}
|
|
155
|
+
expanded={expanded}
|
|
156
|
+
depth={getDepth?.(item)}
|
|
157
|
+
onHoverChange={(hovered: boolean) =>
|
|
158
|
+
setHoveredId(hovered ? id : undefined)
|
|
159
|
+
}
|
|
160
|
+
onPress={(e: ListView.ClickInfo) => {
|
|
161
|
+
handleSelect(id, e);
|
|
162
|
+
}}
|
|
163
|
+
onDoubleClick={() => {
|
|
164
|
+
onDoubleClickItem?.(id);
|
|
165
|
+
}}
|
|
166
|
+
onKeyDown={(e: React.KeyboardEvent) => {
|
|
167
|
+
if (e.key !== "Enter" && e.key !== " ") return;
|
|
168
|
+
e.preventDefault();
|
|
169
|
+
handleSelect(id, e);
|
|
170
|
+
if (e.key === "Enter") {
|
|
171
|
+
setRenamingId(id);
|
|
172
|
+
}
|
|
173
|
+
}}
|
|
174
|
+
menuItems={menuItems}
|
|
175
|
+
onSelectMenuItem={handleMenuAction}
|
|
176
|
+
onContextMenu={() => {
|
|
177
|
+
if (!isSelected) {
|
|
178
|
+
handleSelect(id, {
|
|
179
|
+
shiftKey: false,
|
|
180
|
+
altKey: false,
|
|
181
|
+
metaKey: false,
|
|
182
|
+
ctrlKey: false,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}}
|
|
186
|
+
tabIndex={0}
|
|
187
|
+
onClickChevron={() => {
|
|
188
|
+
if (!expandable) return;
|
|
189
|
+
setExpanded?.(item, !expanded);
|
|
190
|
+
}}
|
|
191
|
+
>
|
|
192
|
+
<div className="flex flex-1 items-center px-2 gap-3">
|
|
193
|
+
{thumbnail && (
|
|
194
|
+
<div
|
|
195
|
+
className={cx(
|
|
196
|
+
"rounded overflow-hidden flex-none flex items-center justify-center",
|
|
197
|
+
size === "medium" ? "w-8 h-8" : "w-16 h-16",
|
|
198
|
+
isSelected && "text-primary"
|
|
199
|
+
)}
|
|
200
|
+
style={{
|
|
201
|
+
backgroundColor: isSelected
|
|
202
|
+
? cssVars.colors.primaryPastel
|
|
203
|
+
: cssVars.colors.inputBackground,
|
|
204
|
+
}}
|
|
205
|
+
tabIndex={-1}
|
|
206
|
+
>
|
|
207
|
+
{thumbnail}
|
|
208
|
+
</div>
|
|
209
|
+
)}
|
|
210
|
+
<div className="flex flex-col flex-1 w-0">
|
|
211
|
+
<div className="flex items-center min-h-[27px] flex-1 gap-2">
|
|
212
|
+
<div className="flex-1 w-0 flex items-center" tabIndex={-1}>
|
|
213
|
+
{isRenaming ? (
|
|
214
|
+
<ViewComponent.EditableRowTitle
|
|
215
|
+
value={getName(item)}
|
|
216
|
+
placeholder="Enter name"
|
|
217
|
+
className="font-medium"
|
|
218
|
+
autoFocus={!isTestingRenaming}
|
|
219
|
+
onSubmitEditing={(value: string) => {
|
|
220
|
+
if (value !== getName(item)) {
|
|
221
|
+
onRename?.(item, value);
|
|
222
|
+
}
|
|
223
|
+
setRenamingId(undefined);
|
|
224
|
+
}}
|
|
225
|
+
/>
|
|
226
|
+
) : (
|
|
227
|
+
<ViewComponent.RowTitle
|
|
228
|
+
className={cx(
|
|
229
|
+
"font-medium",
|
|
230
|
+
isSelected && "text-primary"
|
|
231
|
+
)}
|
|
232
|
+
>
|
|
233
|
+
{getName(item)}
|
|
234
|
+
</ViewComponent.RowTitle>
|
|
235
|
+
)}
|
|
236
|
+
</div>
|
|
237
|
+
{end && (
|
|
238
|
+
<div
|
|
239
|
+
className={cx(
|
|
240
|
+
"flex items-center gap-2",
|
|
241
|
+
isSelected && "text-primary"
|
|
242
|
+
)}
|
|
243
|
+
tabIndex={-1}
|
|
244
|
+
>
|
|
245
|
+
{end}
|
|
246
|
+
</div>
|
|
247
|
+
)}
|
|
248
|
+
</div>
|
|
249
|
+
{below && (
|
|
250
|
+
<div
|
|
251
|
+
className={cx(
|
|
252
|
+
"flex items-center",
|
|
253
|
+
isSelected && "text-primary"
|
|
254
|
+
)}
|
|
255
|
+
tabIndex={-1}
|
|
256
|
+
>
|
|
257
|
+
{below}
|
|
258
|
+
</div>
|
|
259
|
+
)}
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
</ViewComponent.Row>
|
|
263
|
+
);
|
|
264
|
+
}}
|
|
265
|
+
/>
|
|
266
|
+
);
|
|
267
|
+
})
|
|
268
|
+
);
|
|
@@ -147,9 +147,10 @@ function ListViewEditableRowTitle({
|
|
|
147
147
|
// the `onBlur` fires correctly.
|
|
148
148
|
element.focus();
|
|
149
149
|
|
|
150
|
+
// must be a delay of 1ms or more to ensure any radix ui components have finished any focus behaviors
|
|
150
151
|
setTimeout(() => {
|
|
151
152
|
element.select();
|
|
152
|
-
},
|
|
153
|
+
}, 1);
|
|
153
154
|
}, [autoFocus]);
|
|
154
155
|
|
|
155
156
|
return (
|
|
@@ -1124,15 +1125,15 @@ export namespace ListView {
|
|
|
1124
1125
|
export const DragIndicator = ListViewDragIndicatorElement;
|
|
1125
1126
|
export const rowHeight = ROW_HEIGHT;
|
|
1126
1127
|
export const sectionHeaderLabelHeight = SECTION_HEADER_LABEL_HEIGHT;
|
|
1127
|
-
export const calculateHeight = (
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1128
|
+
export const calculateHeight = ({
|
|
1129
|
+
menuItemsCount,
|
|
1130
|
+
sectionHeaderCount,
|
|
1131
|
+
}: {
|
|
1132
|
+
menuItemsCount: number;
|
|
1133
|
+
sectionHeaderCount: number;
|
|
1134
|
+
}) => {
|
|
1132
1135
|
return (
|
|
1133
|
-
|
|
1134
|
-
headerCount *
|
|
1135
|
-
(headerVariant === "label" ? sectionHeaderLabelHeight : rowHeight)
|
|
1136
|
+
menuItemsCount * rowHeight + sectionHeaderCount * sectionHeaderLabelHeight
|
|
1136
1137
|
);
|
|
1137
1138
|
};
|
|
1138
1139
|
export const normalizeDestinationIndex = normalizeListDestinationIndex;
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ComboboxMenu,
|
|
3
|
-
ComboboxOption,
|
|
4
3
|
Divider,
|
|
5
4
|
IScoredItem,
|
|
6
5
|
InputField,
|
|
7
6
|
ListView,
|
|
7
|
+
SelectableMenuItem,
|
|
8
8
|
Small,
|
|
9
9
|
fuzzyFilter,
|
|
10
10
|
getNextIndex,
|
|
11
|
+
isSelectableMenuItem,
|
|
11
12
|
} from "@noya-app/noya-designsystem";
|
|
12
13
|
import { getCurrentPlatform, handleKeyboardEvent } from "@noya-app/noya-keymap";
|
|
13
14
|
import React, {
|
|
@@ -32,10 +33,10 @@ export const SearchCompletionMenu = forwardRef(function SearchCompletionMenu(
|
|
|
32
33
|
items,
|
|
33
34
|
width = 200,
|
|
34
35
|
}: {
|
|
35
|
-
onSelect: (item:
|
|
36
|
-
onHover?: (item:
|
|
36
|
+
onSelect: (item: SelectableMenuItem<string>) => void;
|
|
37
|
+
onHover?: (item: SelectableMenuItem<string>) => void;
|
|
37
38
|
onClose: () => void;
|
|
38
|
-
items:
|
|
39
|
+
items: SelectableMenuItem<string>[];
|
|
39
40
|
width?: number;
|
|
40
41
|
},
|
|
41
42
|
forwardedRef: React.ForwardedRef<ISearchCompletionMenu>
|
|
@@ -56,12 +57,14 @@ export const SearchCompletionMenu = forwardRef(function SearchCompletionMenu(
|
|
|
56
57
|
const results = useMemo(
|
|
57
58
|
() =>
|
|
58
59
|
fuzzyFilter({
|
|
59
|
-
items: items.map((item) =>
|
|
60
|
+
items: items.map((item) =>
|
|
61
|
+
typeof item.title === "string" ? item.title : item.value
|
|
62
|
+
),
|
|
60
63
|
query: search,
|
|
61
64
|
}).map((item) => ({
|
|
62
65
|
...item,
|
|
63
66
|
...items[item.index],
|
|
64
|
-
})) as (
|
|
67
|
+
})) as (SelectableMenuItem<string> & IScoredItem)[],
|
|
65
68
|
[items, search]
|
|
66
69
|
);
|
|
67
70
|
|
|
@@ -96,7 +99,7 @@ export const SearchCompletionMenu = forwardRef(function SearchCompletionMenu(
|
|
|
96
99
|
};
|
|
97
100
|
|
|
98
101
|
const selectAndClose = useCallback(
|
|
99
|
-
(item:
|
|
102
|
+
(item: SelectableMenuItem<string>) => {
|
|
100
103
|
onSelect(item);
|
|
101
104
|
onClose();
|
|
102
105
|
},
|
|
@@ -188,8 +191,9 @@ export const SearchCompletionMenu = forwardRef(function SearchCompletionMenu(
|
|
|
188
191
|
items={results}
|
|
189
192
|
selectedIndex={index}
|
|
190
193
|
onSelectItem={(item) => {
|
|
191
|
-
if (item
|
|
192
|
-
|
|
194
|
+
if (isSelectableMenuItem(item)) {
|
|
195
|
+
selectAndClose(item);
|
|
196
|
+
}
|
|
193
197
|
}}
|
|
194
198
|
onHoverIndex={setIndex}
|
|
195
199
|
/>
|
|
@@ -2,16 +2,16 @@ import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
|
|
|
2
2
|
import React, {
|
|
3
3
|
createContext,
|
|
4
4
|
forwardRef,
|
|
5
|
-
memo,
|
|
6
5
|
ReactNode,
|
|
7
6
|
useCallback,
|
|
8
7
|
useContext,
|
|
9
8
|
useMemo,
|
|
10
9
|
} from "react";
|
|
10
|
+
import { memoGeneric } from "../../../noya-react-utils/src/utils/reactGenerics";
|
|
11
11
|
import { useLabel } from "../hooks/useLabel";
|
|
12
12
|
import { cx } from "../utils/classNames";
|
|
13
13
|
import { renderIcon } from "./Icons";
|
|
14
|
-
import {
|
|
14
|
+
import { SelectableMenuItem } from "./internal/Menu";
|
|
15
15
|
import { Tooltip } from "./Tooltip";
|
|
16
16
|
|
|
17
17
|
type SegmentedControlColorScheme = "primary" | "secondary";
|
|
@@ -43,7 +43,10 @@ export type SegmentedControlItemProps<T extends string> = {
|
|
|
43
43
|
className?: string;
|
|
44
44
|
style?: React.CSSProperties;
|
|
45
45
|
tooltip?: ReactNode;
|
|
46
|
-
} & Pick<
|
|
46
|
+
} & Pick<
|
|
47
|
+
SelectableMenuItem<T>,
|
|
48
|
+
"value" | "disabled" | "icon" | "role" | "title"
|
|
49
|
+
>;
|
|
47
50
|
|
|
48
51
|
const SegmentedControlItem = forwardRef(function SegmentedControlItem<
|
|
49
52
|
T extends string,
|
|
@@ -104,7 +107,7 @@ const Separator = ({ transparent }: { transparent: boolean }) => (
|
|
|
104
107
|
/>
|
|
105
108
|
);
|
|
106
109
|
|
|
107
|
-
export const SegmentedControl =
|
|
110
|
+
export const SegmentedControl = memoGeneric(function SegmentedControl<
|
|
108
111
|
T extends string,
|
|
109
112
|
>({
|
|
110
113
|
id: idProp,
|
|
@@ -14,9 +14,15 @@ import React, {
|
|
|
14
14
|
} from "react";
|
|
15
15
|
import { useLabel, useLabelPosition } from "../hooks/useLabel";
|
|
16
16
|
import { cx } from "../utils/classNames";
|
|
17
|
+
|
|
17
18
|
import { Button } from "./Button";
|
|
18
19
|
import { renderIcon } from "./Icons";
|
|
19
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
isSelectableMenuItem,
|
|
22
|
+
MenuItem,
|
|
23
|
+
SelectableMenuItem,
|
|
24
|
+
styles,
|
|
25
|
+
} from "./internal/Menu";
|
|
20
26
|
import { MenuComponents, MenuViewport } from "./internal/MenuViewport";
|
|
21
27
|
import { InsetLabel } from "./Label";
|
|
22
28
|
|
|
@@ -141,10 +147,8 @@ export const SelectMenu = memoGeneric(function SelectMenu<
|
|
|
141
147
|
...props
|
|
142
148
|
}: Props<T>) {
|
|
143
149
|
const selectedItem = items.find(
|
|
144
|
-
(item): item is
|
|
145
|
-
item.
|
|
146
|
-
item.type !== "sectionHeader" &&
|
|
147
|
-
item.value === value
|
|
150
|
+
(item): item is SelectableMenuItem<T> =>
|
|
151
|
+
isSelectableMenuItem(item) && item.value === value
|
|
148
152
|
);
|
|
149
153
|
const icon = selectedItem?.icon;
|
|
150
154
|
const [internalOpen, setInternalOpen] = useState(open);
|
|
@@ -1,14 +1,15 @@
|
|
|
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 type { MenuItem } from "./internal/Menu";
|
|
9
9
|
import {
|
|
10
10
|
getKeyboardShortcutsForMenuItems,
|
|
11
|
-
|
|
11
|
+
isSelectableMenuItem,
|
|
12
|
+
MenuItem,
|
|
12
13
|
} from "./internal/Menu";
|
|
13
14
|
import { Spacer } from "./Spacer";
|
|
14
15
|
|
|
@@ -86,9 +87,9 @@ export function ToolbarMenu<T extends string>({
|
|
|
86
87
|
<>
|
|
87
88
|
{items.map((item, i) => {
|
|
88
89
|
if (item.type === "separator") return null;
|
|
89
|
-
if (item.type === "sectionHeader") return
|
|
90
|
+
if (item.type === "sectionHeader") return null;
|
|
90
91
|
|
|
91
|
-
if (
|
|
92
|
+
if (isSelectableMenuItem(item)) {
|
|
92
93
|
return (
|
|
93
94
|
<Button
|
|
94
95
|
key={i}
|
|
@@ -73,17 +73,17 @@ export const WorkspaceLayout = forwardRef(function WorkspaceLayout(
|
|
|
73
73
|
children,
|
|
74
74
|
onChangeLayoutState,
|
|
75
75
|
leftOptions: {
|
|
76
|
-
initialSize: leftInitialSize = 280,
|
|
77
76
|
minSize: leftMinSize = 200,
|
|
78
77
|
maxSize: leftMaxSize = 500,
|
|
78
|
+
initialSize: leftInitialSize = 280,
|
|
79
79
|
resizable: leftResizable = true,
|
|
80
80
|
style: leftStyle,
|
|
81
81
|
className: leftClassName,
|
|
82
82
|
} = {},
|
|
83
83
|
rightOptions: {
|
|
84
|
-
initialSize: rightInitialSize = 280,
|
|
85
84
|
minSize: rightMinSize = 200,
|
|
86
85
|
maxSize: rightMaxSize = 500,
|
|
86
|
+
initialSize: rightInitialSize = 280,
|
|
87
87
|
resizable: rightResizable = true,
|
|
88
88
|
style: rightStyle,
|
|
89
89
|
className: rightClassName,
|
|
@@ -99,8 +99,21 @@ export const WorkspaceLayout = forwardRef(function WorkspaceLayout(
|
|
|
99
99
|
: (size / windowSize.width) * 100;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
|
|
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
|
+
|
|
104
117
|
const leftSidebarMinPercentage =
|
|
105
118
|
leftMinSize !== undefined ? getPercentage(leftMinSize) : undefined;
|
|
106
119
|
const rightSidebarMinPercentage =
|
|
@@ -110,6 +123,17 @@ export const WorkspaceLayout = forwardRef(function WorkspaceLayout(
|
|
|
110
123
|
const rightSidebarMaxPercentage =
|
|
111
124
|
rightMaxSize !== undefined ? getPercentage(rightMaxSize) : undefined;
|
|
112
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
|
+
);
|
|
136
|
+
|
|
113
137
|
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
|
|
114
138
|
const leftSidebarRef = useRef<ImperativePanelHandle>(null);
|
|
115
139
|
const rightSidebarRef = useRef<ImperativePanelHandle>(null);
|
|
@@ -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
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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:
|
|
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
|
-
|
|
|
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
|
|
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.
|
|
176
|
+
if (item.type === "submenu") return item.items.flatMap(getKeyboardShortcuts);
|
|
136
177
|
|
|
137
178
|
if (item.disabled || !item.value || !item.shortcut) return [];
|
|
138
179
|
|