@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.
@@ -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
- }, 0);
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
- items: number,
1129
- headerCount: number,
1130
- headerVariant: ListViewSectionHeaderVariant
1131
- ) => {
1128
+ export const calculateHeight = ({
1129
+ menuItemsCount,
1130
+ sectionHeaderCount,
1131
+ }: {
1132
+ menuItemsCount: number;
1133
+ sectionHeaderCount: number;
1134
+ }) => {
1132
1135
  return (
1133
- items * rowHeight +
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: ComboboxOption) => void;
36
- onHover?: (item: ComboboxOption) => void;
36
+ onSelect: (item: SelectableMenuItem<string>) => void;
37
+ onHover?: (item: SelectableMenuItem<string>) => void;
37
38
  onClose: () => void;
38
- items: ComboboxOption[];
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) => item.name),
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 (ComboboxOption & IScoredItem)[],
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: ComboboxOption) => {
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.type === "sectionHeader") return;
192
- selectAndClose(item);
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 { RegularMenuItem } from "./internal/Menu";
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<RegularMenuItem<T>, "value" | "disabled" | "icon" | "role">;
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 = memo(function 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 { MenuItem, RegularMenuItem, styles } from "./internal/Menu";
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 RegularMenuItem<T> =>
145
- item.type !== "separator" &&
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
- SectionHeader,
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 <SectionHeader {...item} />;
90
+ if (item.type === "sectionHeader") return null;
90
91
 
91
- if (!item.items) {
92
+ if (isSelectableMenuItem(item)) {
92
93
  return (
93
94
  <Button
94
95
  key={i}
@@ -14,6 +14,7 @@ type TreeRowBaseProps = {
14
14
  expanded?: boolean;
15
15
  chevronClassName?: string;
16
16
  onClickChevron?: ({ altKey }: { altKey: boolean }) => void;
17
+ onDoubleClick?: () => void;
17
18
  };
18
19
 
19
20
  type TreeViewRowProps<MenuItemType extends string> =
@@ -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
- const leftSidebarPercentage = getPercentage(leftInitialSize);
103
- const rightSidebarPercentage = getPercentage(rightInitialSize);
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 RegularMenuItem<T extends string> = {
65
- value?: T;
66
- title: ReactNode;
67
- shortcut?: string;
61
+ export type SeparatorItem = {
62
+ type: "separator";
63
+ };
64
+
65
+ type BaseMenuItem = {
68
66
  checked?: boolean;
69
67
  disabled?: boolean;
70
68
  icon?: Exclude<ReactNode, string> | IconName;
71
- items?: MenuItem<T>[];
72
69
  role?: MenuItemRole;
73
- type?: undefined;
74
70
  alwaysInclude?: boolean;
71
+ title: NonNullable<ReactNode>;
75
72
  };
76
73
 
77
74
  export type SectionHeaderMenuItem = {
78
75
  type: "sectionHeader";
79
76
  id: string;
80
- title: string;
77
+ title: ReactNode;
81
78
  maxVisibleItems?: number;
82
79
  variant?: "normal" | "label";
83
80
  };
84
81
 
82
+ export type SelectableMenuItem<T extends string> = BaseMenuItem & {
83
+ type?: undefined;
84
+ shortcut?: string;
85
+ value: T;
86
+ };
87
+
88
+ type SubMenuItem<T extends string> = BaseMenuItem & {
89
+ type: "submenu";
90
+ items: MenuItem<T>[];
91
+ id: string;
92
+ };
93
+
94
+ export type NonSelectableMenuItem<T extends string> =
95
+ | SeparatorItem
96
+ | SectionHeaderMenuItem
97
+ | SubMenuItem<T>;
98
+
85
99
  export type MenuItem<T extends string> =
86
100
  | SeparatorItem
87
- | RegularMenuItem<T>
101
+ | SelectableMenuItem<T>
102
+ | SectionHeaderMenuItem
103
+ | SubMenuItem<T>;
104
+
105
+ type ScoredSelectableMenuItem<T extends string> = SelectableMenuItem<T> &
106
+ IScoredItem;
107
+
108
+ export type ScoredMenuItem<T extends string> =
109
+ | ScoredSelectableMenuItem<T>
88
110
  | SectionHeaderMenuItem;
89
111
 
90
112
  // Extract type T of RegularMenuItem<T> within MenuItem<T>
91
113
  export type ExtractMenuItemType<T> =
92
- T extends RegularMenuItem<infer U> ? U : never;
114
+ T extends SelectableMenuItem<infer U> ? U : never;
115
+
116
+ export const isSelectableMenuItem = <T extends string>(
117
+ item: MenuItem<T>
118
+ ): item is SelectableMenuItem<T> => {
119
+ return item.type === undefined;
120
+ };
121
+
122
+ export const isNonSelectableMenuItem = <T extends string>(
123
+ item: MenuItem<T>
124
+ ): item is NonSelectableMenuItem<T> => !isSelectableMenuItem(item);
125
+
126
+ export const isMenuItemSectionHeader = (
127
+ item: MenuItem<string>
128
+ ): item is SectionHeaderMenuItem => item.type === "sectionHeader";
129
+
130
+ export const isSelectableMenuItemWithScore = (
131
+ item: MenuItem<string>
132
+ ): item is ScoredSelectableMenuItem<string> =>
133
+ isSelectableMenuItem(item) && "index" in item && "score" in item;
93
134
 
94
135
  export const CHECKBOX_WIDTH = 16;
95
136
  export const CHECKBOX_RIGHT_INSET = 8;
@@ -132,7 +173,7 @@ function getKeyboardShortcuts<T extends string>(
132
173
  if (item.type === "separator") return [];
133
174
  if (item.type === "sectionHeader") return [];
134
175
 
135
- if (item.items) return item.items.flatMap(getKeyboardShortcuts);
176
+ if (item.type === "submenu") return item.items.flatMap(getKeyboardShortcuts);
136
177
 
137
178
  if (item.disabled || !item.value || !item.shortcut) return [];
138
179