@noya-app/noya-designsystem 0.1.44 → 0.1.46

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