@noya-app/noya-designsystem 0.1.77 → 0.1.78

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 (81) hide show
  1. package/.turbo/turbo-build.log +20 -12
  2. package/CHANGELOG.md +9 -0
  3. package/dist/chunk-D57E6H3M.mjs +36 -0
  4. package/dist/chunk-D57E6H3M.mjs.map +1 -0
  5. package/dist/emojis.d.mts +1 -0
  6. package/dist/emojis.d.ts +1 -0
  7. package/dist/emojis.js +31 -0
  8. package/dist/emojis.js.map +1 -0
  9. package/dist/emojis.mjs +8 -0
  10. package/dist/emojis.mjs.map +1 -0
  11. package/dist/index.css +1 -1
  12. package/dist/index.d.mts +142 -90
  13. package/dist/index.d.ts +142 -90
  14. package/dist/index.js +3171 -1745
  15. package/dist/index.js.map +1 -1
  16. package/dist/index.mjs +3349 -1918
  17. package/dist/index.mjs.map +1 -1
  18. package/package.json +11 -7
  19. package/src/__tests__/__snapshots__/fuzzyScorer.test.ts.snap +1 -1
  20. package/src/components/Avatar.tsx +10 -10
  21. package/src/components/Banner.tsx +1 -1
  22. package/src/components/BaseToolbar.tsx +1 -1
  23. package/src/components/Button.tsx +2 -1
  24. package/src/components/Chip.tsx +1 -1
  25. package/src/components/ColorSwatchControl.tsx +22 -14
  26. package/src/components/CommandPalette.tsx +10 -6
  27. package/src/components/ContextMenu.tsx +249 -38
  28. package/src/components/Dialog.tsx +70 -67
  29. package/src/components/Drawer.tsx +56 -17
  30. package/src/components/DropdownMenu.tsx +305 -46
  31. package/src/components/EditableText.tsx +1 -1
  32. package/src/components/EmojiPicker.tsx +645 -0
  33. package/src/components/GridView.tsx +5 -1
  34. package/src/components/IVirtualizedList.tsx +21 -1
  35. package/src/components/InputField.tsx +2 -10
  36. package/src/components/InspectorContainer.tsx +4 -2
  37. package/src/components/Message.tsx +5 -16
  38. package/src/components/Popover.tsx +73 -107
  39. package/src/components/Progress.tsx +18 -18
  40. package/src/components/ScrollArea.tsx +66 -31
  41. package/src/components/ScrollableSidebar.tsx +1 -1
  42. package/src/components/SegmentedControl.tsx +166 -38
  43. package/src/components/SelectMenu.tsx +193 -101
  44. package/src/components/Slider.tsx +40 -38
  45. package/src/components/Switch.tsx +4 -4
  46. package/src/components/TextArea.tsx +1 -1
  47. package/src/components/Toast.tsx +99 -26
  48. package/src/components/Toolbar.tsx +114 -16
  49. package/src/components/Tooltip.tsx +18 -8
  50. package/src/components/Virtualized.tsx +193 -14
  51. package/src/components/VisuallyHidden.tsx +20 -0
  52. package/src/components/__tests__/Virtualized.math.test.ts +426 -1
  53. package/src/components/__tests__/Virtualized.test.tsx +129 -1
  54. package/src/components/ai-assistant/AIAssistantLayout.tsx +11 -6
  55. package/src/components/internal/Menu.tsx +4 -0
  56. package/src/components/listView/ListViewEditableRowTitle.tsx +1 -1
  57. package/src/components/listView/ListViewRoot.tsx +5 -1
  58. package/src/components/resizablePanels/Panel.tsx +94 -0
  59. package/src/components/resizablePanels/PanelGroup.tsx +498 -0
  60. package/src/components/resizablePanels/PanelGroupContext.tsx +14 -0
  61. package/src/components/resizablePanels/PanelResizeHandle.tsx +61 -0
  62. package/src/components/resizablePanels/index.ts +7 -0
  63. package/src/components/resizablePanels/types.ts +65 -0
  64. package/src/components/workspace/DrawerWorkspaceLayout.tsx +22 -7
  65. package/src/components/workspace/PanelWorkspaceLayout.tsx +30 -84
  66. package/src/components/workspace/WorkspaceLayout.tsx +28 -58
  67. package/src/components/workspace/types.ts +6 -4
  68. package/src/contexts/DialogContext.tsx +15 -24
  69. package/src/emojis.ts +1 -0
  70. package/src/hooks/useTriggerToggle.ts +95 -0
  71. package/src/index.css +2 -0
  72. package/src/index.tsx +1 -2
  73. package/src/theme/proseTheme.ts +2 -0
  74. package/src/utils/mergeProps.ts +57 -0
  75. package/src/utils/skinTone.ts +90 -0
  76. package/tsup.config.ts +1 -1
  77. package/src/__tests__/workspaceLayout.test.ts +0 -281
  78. package/src/components/ScrollArea2.tsx +0 -76
  79. package/src/components/internal/MenuViewport.tsx +0 -178
  80. package/src/components/internal/SelectItem.tsx +0 -138
  81. package/src/components/workspace/panelStorage.ts +0 -216
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noya-app/noya-designsystem",
3
- "version": "0.1.77",
3
+ "version": "0.1.78",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.ts",
@@ -11,7 +11,12 @@
11
11
  "require": "./dist/index.js",
12
12
  "types": "./dist/index.d.ts"
13
13
  },
14
- "./index.css": "./dist/index.css"
14
+ "./index.css": "./dist/index.css",
15
+ "./emojis": {
16
+ "import": "./dist/emojis.mjs",
17
+ "require": "./dist/emojis.js",
18
+ "types": "./dist/emojis.d.ts"
19
+ }
15
20
  },
16
21
  "scripts": {
17
22
  "build:css": "tailwindcss -i ./src/index.css -o ./dist/index.css --postcss",
@@ -20,24 +25,23 @@
20
25
  "lint": "eslint . --max-warnings 0"
21
26
  },
22
27
  "dependencies": {
23
- "@base-ui-components/react": "1.0.0-beta.4",
28
+ "@base-ui/react": "1.0.0",
24
29
  "@dnd-kit/core": "6.3.1",
25
30
  "@dnd-kit/sortable": "10.0.0",
26
31
  "@noya-app/noya-colorpicker": "0.1.33",
27
32
  "@noya-app/noya-utils": "0.1.9",
28
33
  "@noya-app/noya-geometry": "0.1.17",
29
- "@noya-app/noya-icons": "0.1.17",
34
+ "@noya-app/noya-icons": "0.1.18",
30
35
  "@noya-app/noya-keymap": "0.1.4",
31
- "@noya-app/noya-tailwind-config": "0.1.9",
32
- "radix-ui": "1.4.2",
36
+ "@noya-app/noya-tailwind-config": "0.1.10",
33
37
  "@radix-ui/react-compose-refs": "^1.0.0",
34
38
  "@radix-ui/primitive": "^1.0.1",
35
39
  "@radix-ui/react-utils": "^0.0.5",
36
40
  "@radix-ui/react-popper": "1.2.3",
37
41
  "@radix-ui/utils": "0.0.3",
42
+ "gemoji": "8.1.0",
38
43
  "immer": "9.0.5",
39
44
  "kiwi.js": "^1.1.3",
40
- "react-resizable-panels": "3.0.3",
41
45
  "vscode-fuzzy-scorer": "^0.0.4"
42
46
  },
43
47
  "devDependencies": {
@@ -1,4 +1,4 @@
1
- // Bun Snapshot v1, https://goo.gl/fbAQLP
1
+ // Bun Snapshot v1, https://bun.sh/docs/test/snapshots
2
2
 
3
3
  exports[`filter single match 1`] = `
4
4
  [
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { Avatar as AvatarPrimitive } from "radix-ui";
3
+ import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar";
4
4
  import React, { forwardRef, useMemo } from "react";
5
5
  import { cx } from "../utils/classNames";
6
6
  import { colorFromString } from "../utils/colorFromString";
@@ -21,9 +21,9 @@ const getInnerAvatarStyles = (
21
21
  };
22
22
 
23
23
  const AvatarRoot = React.forwardRef<
24
- React.ComponentRef<typeof AvatarPrimitive.Root>,
25
- React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> &
26
- Pick<AvatarProps, "size" | "variant" | "backgroundColor" | "disabled">
24
+ HTMLSpanElement,
25
+ Omit<React.HTMLAttributes<HTMLSpanElement>, "className"> &
26
+ Pick<AvatarProps, "size" | "variant" | "backgroundColor" | "disabled" | "className">
27
27
  >(
28
28
  (
29
29
  {
@@ -63,9 +63,9 @@ const AvatarRoot = React.forwardRef<
63
63
  );
64
64
 
65
65
  const AvatarImage = React.forwardRef<
66
- React.ComponentRef<typeof AvatarPrimitive.Image>,
67
- React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> &
68
- Pick<AvatarProps, "size" | "variant">
66
+ HTMLImageElement,
67
+ Omit<React.ImgHTMLAttributes<HTMLImageElement>, "className"> &
68
+ Pick<AvatarProps, "size" | "variant" | "className">
69
69
  >(({ className, size = 21, variant = "normal", ...props }, ref) => {
70
70
  return (
71
71
  <AvatarPrimitive.Image
@@ -85,9 +85,9 @@ function getMonogram(text?: string) {
85
85
  }
86
86
 
87
87
  const AvatarFallback = React.forwardRef<
88
- React.ComponentRef<typeof AvatarPrimitive.Fallback>,
89
- React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> &
90
- Pick<AvatarProps, "size" | "variant" | "backgroundColor">
88
+ HTMLSpanElement,
89
+ Omit<React.HTMLAttributes<HTMLSpanElement>, "className"> &
90
+ Pick<AvatarProps, "size" | "variant" | "backgroundColor" | "className">
91
91
  >(
92
92
  (
93
93
  { className, children, style, size = 21, backgroundColor, ...props },
@@ -40,7 +40,7 @@ export function Banner({
40
40
  return (
41
41
  <div
42
42
  className={cx(
43
- "n-flex n-py-1 n-px-1.5 n-items-center n-justify-center n-select-none n-rounded n-gap-1.5 n-min-h-input-height",
43
+ "n-flex n-py-input-padding-y n-px-input-padding-x n-items-center n-justify-center n-select-none n-rounded n-gap-1.5 n-min-h-input-height",
44
44
  variant === "muted" && "n-bg-input-background",
45
45
  variant === "info" &&
46
46
  "n-bg-banner-info-background n-border n-border-banner-info-border",
@@ -114,9 +114,9 @@ export function BaseToolbar({
114
114
  <>
115
115
  <Spacer.Horizontal size={10} className="@xl/toolbar:!n-hidden" />
116
116
  <DividerVertical className="@xl/toolbar:!n-hidden" />
117
+ <Spacer.Horizontal size={10} className="@xl/toolbar:!n-hidden" />
117
118
  </>
118
119
  )}
119
- <Spacer.Horizontal size={10} className="@xl/toolbar:!n-hidden" />
120
120
  {leftElement}
121
121
  <Spacer.Horizontal size={10} />
122
122
  </>
@@ -41,7 +41,8 @@ const variantStyles: Record<ButtonVariant, string> = {
41
41
 
42
42
  const sizeStyles: Record<ButtonSize | "thin" | "none", string> = {
43
43
  small: "n-px-[2px] n-font-sans n-text-[10px] n-font-medium",
44
- normal: "n-px-[6px] n-py-1 n-font-sans n-text-button n-font-medium",
44
+ normal:
45
+ "n-px-input-padding-x n-py-input-padding-y n-font-sans n-text-button n-font-medium",
45
46
  large: "n-p-3 n-px-4 n-font-sans n-text-heading4 n-font-medium",
46
47
  thin: "n-px-[1px] n-py-1 n-font-sans n-text-button n-font-medium",
47
48
  floating: "n-px-1 n-py-0 n-font-sans n-text-[12px] n-font-medium",
@@ -17,7 +17,7 @@ const BASE_STYLES =
17
17
  const SIZE_STYLES = {
18
18
  xl: "n-text-[12px] n-p-[6px_12px]",
19
19
  large: "n-text-[11px] n-p-[4px_8px]",
20
- medium: "n-text-[10px] n-p-[2px_6px]",
20
+ medium: "n-text-[10px] n-py-0.5 n-px-input-padding-x",
21
21
  small: "n-text-[9px] n-p-[0px_4px]",
22
22
  } as const;
23
23
 
@@ -1,7 +1,8 @@
1
1
  "use client";
2
2
 
3
- import { ToggleGroup as ToggleGroupPrimitive } from "radix-ui";
4
- import React, { useMemo } from "react";
3
+ import { Toggle } from "@base-ui/react/toggle";
4
+ import { ToggleGroup } from "@base-ui/react/toggle-group";
5
+ import React, { useCallback, useMemo } from "react";
5
6
  import { cx } from "../utils/classNames";
6
7
  import {
7
8
  ColorSwatch,
@@ -40,6 +41,7 @@ export function ColorSwatchControl(props: ColorSwatchControlProps) {
40
41
  id,
41
42
  style,
42
43
  } = props;
44
+
43
45
  const styles = useMemo(() => {
44
46
  return {
45
47
  display: "grid",
@@ -49,6 +51,15 @@ export function ColorSwatchControl(props: ColorSwatchControlProps) {
49
51
  };
50
52
  }, [size, style]);
51
53
 
54
+ const handleValueChange = useCallback(
55
+ (values: string[]) => {
56
+ if (values.length > 0 && onValueChange) {
57
+ onValueChange(values[0] as TokenValue);
58
+ }
59
+ },
60
+ [onValueChange]
61
+ );
62
+
52
63
  const content = (
53
64
  <div className={className} id={id} style={styles}>
54
65
  {options.map((option) => {
@@ -60,33 +71,30 @@ export function ColorSwatchControl(props: ColorSwatchControlProps) {
60
71
  onFocus={onFocus}
61
72
  onBlur={onBlur}
62
73
  disabled={disabled}
63
- // TODO: decide on "checked" visual treatment
64
74
  className={cx(readOnly ? "n-cursor-default" : "n-cursor-pointer")}
65
75
  checked={value === option}
66
76
  />
67
77
  );
68
78
  return (
69
- <ToggleGroupPrimitive.Item
70
- asChild
71
- value={option as string}
79
+ <Toggle
72
80
  key={`${option as string}-toggle-group-item`}
81
+ value={option as string}
73
82
  disabled={readOnly}
74
- >
75
- {item}
76
- </ToggleGroupPrimitive.Item>
83
+ render={item}
84
+ />
77
85
  );
78
86
  })}
79
87
  </div>
80
88
  );
89
+
81
90
  return (
82
- <ToggleGroupPrimitive.Root
83
- type="single"
84
- value={value as string}
85
- onValueChange={onValueChange}
91
+ <ToggleGroup
92
+ value={value ? [value as string] : []}
93
+ onValueChange={handleValueChange}
86
94
  className="n-w-full"
87
95
  disabled={readOnly}
88
96
  >
89
97
  {content}
90
- </ToggleGroupPrimitive.Root>
98
+ </ToggleGroup>
91
99
  );
92
100
  }
@@ -27,6 +27,16 @@ export const CommandPalette = memo(function CommandPalette({
27
27
  setShowCommandPalette(false);
28
28
  }, [setShowCommandPalette]);
29
29
 
30
+ // Focus the menu when the dialog opens
31
+ React.useEffect(() => {
32
+ if (!showCommandPalette) return;
33
+
34
+ menuRef.current?.focus();
35
+ setTimeout(() => {
36
+ menuRef.current?.focus();
37
+ }, 0);
38
+ }, [showCommandPalette]);
39
+
30
40
  return (
31
41
  <Dialog
32
42
  style={{
@@ -45,12 +55,6 @@ export const CommandPalette = memo(function CommandPalette({
45
55
  setShowCommandPalette(value);
46
56
  }}
47
57
  closeOnInteractOutside
48
- onOpenAutoFocus={() => {
49
- menuRef.current?.focus();
50
- setTimeout(() => {
51
- menuRef.current?.focus();
52
- }, 0);
53
- }}
54
58
  >
55
59
  <div className="n-flex n-flex-col n-flex-1">
56
60
  <AutoSizer>
@@ -1,19 +1,31 @@
1
1
  "use client";
2
2
 
3
+ import { ContextMenu as BaseContextMenu } from "@base-ui/react/context-menu";
4
+ import { CheckIcon, ChevronRightIcon } from "@noya-app/noya-icons";
3
5
  import { useKeyboardShortcuts } from "@noya-app/noya-keymap";
4
6
  import { memoGeneric } from "@noya-app/react-utils";
5
- import { ContextMenu as RadixContextMenu } from "radix-ui";
6
- import React, { ReactNode, useCallback, useMemo } from "react";
7
+ import React, { ReactNode, useCallback, useMemo, useState } from "react";
7
8
  import {
8
9
  portalScopeProps,
9
10
  usePortalScopeId,
10
11
  } from "../contexts/PortalScopeContext";
12
+ import { cx } from "../utils/classNames";
13
+ import { Icon, IconName } from "./Icons";
11
14
  import {
15
+ CHECKBOX_INDENT_WIDTH,
16
+ CHECKBOX_RIGHT_INSET,
17
+ CHECKBOX_WIDTH,
12
18
  MenuItem,
19
+ MenuItemIcon,
13
20
  getKeyboardShortcutsForMenuItems,
21
+ isSelectableMenuItem,
22
+ KeyboardShortcut,
23
+ SectionHeaderMenuItem as SectionHeaderMenuItemComponent,
14
24
  styles,
15
25
  } from "./internal/Menu";
16
- import { MenuViewport } from "./internal/MenuViewport";
26
+ import { Spacer } from "./Spacer";
27
+ import { cloneElementWithMergedProps } from "../utils/mergeProps";
28
+
17
29
  export interface MenuItemProps<T extends string> {
18
30
  value?: T;
19
31
  children: ReactNode;
@@ -26,17 +38,210 @@ export interface MenuItemProps<T extends string> {
26
38
  items?: MenuItem<T>[];
27
39
  }
28
40
 
29
- const Components = {
30
- Separator: RadixContextMenu.Separator,
31
- ItemText: React.Fragment,
32
- Item: RadixContextMenu.Item,
33
- CheckboxItem: RadixContextMenu.CheckboxItem,
34
- ItemIndicator: RadixContextMenu.ItemIndicator,
35
- SubTrigger: RadixContextMenu.SubTrigger,
36
- SubContent: RadixContextMenu.SubContent,
37
- Sub: RadixContextMenu.Sub,
38
- Portal: RadixContextMenu.Portal,
39
- };
41
+ const itemStyle = (state: { disabled: boolean; highlighted: boolean }) =>
42
+ cx(
43
+ "n-flex-none n-select-none n-cursor-pointer n-rounded",
44
+ "n-py-1.5 n-px-2",
45
+ "n-transition-colors",
46
+ "n-flex n-items-center",
47
+ "n-font-sans n-text-button n-font-medium",
48
+ "focus:n-outline-none",
49
+ state.disabled && "n-text-text-disabled",
50
+ state.highlighted &&
51
+ "n-text-selected-list-item-text n-bg-selected-list-item-background"
52
+ );
53
+
54
+ function MenuItemContent({
55
+ icon,
56
+ checked,
57
+ indented,
58
+ children,
59
+ shortcut,
60
+ isSubmenuTrigger,
61
+ }: {
62
+ icon?: MenuItemIcon;
63
+ checked?: boolean;
64
+ indented?: boolean;
65
+ children: ReactNode;
66
+ shortcut?: string;
67
+ isSubmenuTrigger?: boolean;
68
+ }) {
69
+ return (
70
+ <>
71
+ {indented && <Spacer.Horizontal size={CHECKBOX_INDENT_WIDTH} />}
72
+ {checked ? (
73
+ <span
74
+ className="n-flex n-items-center n-relative -n-left-2.5"
75
+ style={{ marginRight: -CHECKBOX_RIGHT_INSET }}
76
+ >
77
+ <CheckIcon />
78
+ </span>
79
+ ) : indented ? (
80
+ <Spacer.Horizontal size={CHECKBOX_WIDTH - CHECKBOX_RIGHT_INSET} />
81
+ ) : null}
82
+ {icon &&
83
+ (typeof icon === "string" ? (
84
+ <Icon
85
+ name={icon as IconName}
86
+ className="n-mr-2 n-w-4 n-h-4 n-flex-shrink-0"
87
+ />
88
+ ) : (
89
+ <span className="n-mr-2 n-w-4 n-h-4 n-flex-shrink-0 n-flex n-items-center n-justify-center">
90
+ {icon}
91
+ </span>
92
+ ))}
93
+ <div className="n-flex n-items-center n-flex-1">{children}</div>
94
+ {shortcut && (
95
+ <>
96
+ <Spacer.Horizontal size={16} />
97
+ <KeyboardShortcut shortcut={shortcut} />
98
+ </>
99
+ )}
100
+ {isSubmenuTrigger && <ChevronRightIcon className="-n-mr-1" />}
101
+ </>
102
+ );
103
+ }
104
+
105
+ function SubmenuItem<T extends string>({
106
+ item,
107
+ onSelect,
108
+ hasCheckedItem,
109
+ portalScopeId,
110
+ }: {
111
+ item: Extract<MenuItem<T>, { type: "submenu" }>;
112
+ onSelect: ((value: T) => void) | undefined;
113
+ hasCheckedItem: boolean;
114
+ portalScopeId: string | undefined;
115
+ }) {
116
+ const [open, setOpen] = useState(false);
117
+
118
+ const submenuHasCheckedItem = item.items.some(
119
+ (subItem) => isSelectableMenuItem(subItem) && subItem.checked
120
+ );
121
+
122
+ return (
123
+ <BaseContextMenu.SubmenuRoot
124
+ key={item.id}
125
+ open={open}
126
+ onOpenChange={(newOpen, event) => {
127
+ if (event.reason === "focus-out") {
128
+ return;
129
+ }
130
+ setOpen(newOpen);
131
+ }}
132
+ >
133
+ <div className="n-px-1">
134
+ <BaseContextMenu.SubmenuTrigger
135
+ disabled={item.disabled}
136
+ className={itemStyle}
137
+ >
138
+ <MenuItemContent
139
+ icon={item.icon}
140
+ checked={item.checked}
141
+ indented={hasCheckedItem}
142
+ isSubmenuTrigger
143
+ >
144
+ {item.title}
145
+ </MenuItemContent>
146
+ </BaseContextMenu.SubmenuTrigger>
147
+ </div>
148
+ <BaseContextMenu.Portal>
149
+ <BaseContextMenu.Positioner
150
+ {...portalScopeProps(portalScopeId)}
151
+ alignOffset={-5}
152
+ className="n-z-menu n-outline-none"
153
+ >
154
+ <BaseContextMenu.Popup className={styles.contentStyle}>
155
+ <ContextMenuViewport
156
+ items={item.items}
157
+ onSelect={onSelect}
158
+ hasCheckedItem={submenuHasCheckedItem}
159
+ />
160
+ </BaseContextMenu.Popup>
161
+ </BaseContextMenu.Positioner>
162
+ </BaseContextMenu.Portal>
163
+ </BaseContextMenu.SubmenuRoot>
164
+ );
165
+ }
166
+
167
+ function ContextMenuViewport<T extends string>({
168
+ items,
169
+ onSelect,
170
+ hasCheckedItem,
171
+ }: {
172
+ items: MenuItem<T>[];
173
+ onSelect?: (value: T) => void;
174
+ hasCheckedItem: boolean;
175
+ }) {
176
+ const portalScopeId = usePortalScopeId();
177
+
178
+ return (
179
+ <>
180
+ {items.map((item, index) => {
181
+ switch (item.type) {
182
+ case "separator":
183
+ return (
184
+ <BaseContextMenu.Separator
185
+ key={index}
186
+ className={styles.separatorStyle}
187
+ />
188
+ );
189
+
190
+ case "sectionHeader":
191
+ return (
192
+ <SectionHeaderMenuItemComponent
193
+ isFirst={index === 0}
194
+ key={item.id}
195
+ {...item}
196
+ indented={hasCheckedItem}
197
+ />
198
+ );
199
+
200
+ case "popover":
201
+ return null;
202
+
203
+ case "submenu":
204
+ if (item.items) {
205
+ return (
206
+ <SubmenuItem
207
+ key={item.id}
208
+ item={item as Extract<MenuItem<string>, { type: "submenu" }>}
209
+ onSelect={onSelect as ((value: string) => void) | undefined}
210
+ hasCheckedItem={hasCheckedItem}
211
+ portalScopeId={portalScopeId}
212
+ />
213
+ );
214
+ }
215
+ return null;
216
+
217
+ default:
218
+ return (
219
+ <div key={item.value} className="n-px-1">
220
+ <BaseContextMenu.Item
221
+ disabled={item.disabled}
222
+ className={itemStyle}
223
+ onClick={() => {
224
+ if (onSelect && item.value) {
225
+ setTimeout(() => onSelect(item.value), 0);
226
+ }
227
+ }}
228
+ >
229
+ <MenuItemContent
230
+ icon={item.icon}
231
+ checked={item.checked}
232
+ indented={hasCheckedItem}
233
+ shortcut={item.shortcut}
234
+ >
235
+ {item.title ?? item.value}
236
+ </MenuItemContent>
237
+ </BaseContextMenu.Item>
238
+ </div>
239
+ );
240
+ }
241
+ })}
242
+ </>
243
+ );
244
+ }
40
245
 
41
246
  /* ----------------------------------------------------------------------------
42
247
  * Root
@@ -69,41 +274,47 @@ function ContextMenuRoot<T extends string>({
69
274
 
70
275
  useKeyboardShortcuts(keymap);
71
276
 
72
- // We call preventDefault both to:
73
- // - Disable radix-ui's long-press-to-open behavior
74
- // https://github.com/radix-ui/primitives/issues/748#issuecomment-869502837
75
- // - Mark the event as handled, so our ListView root doesn't handle it (a hack)
76
- const onPointerDown = useCallback((event: React.PointerEvent) => {
77
- event.preventDefault();
78
- }, []);
79
-
80
277
  const handleSelectItem = useCallback(
81
278
  (value: T) => {
82
279
  if (!value) return;
83
-
84
280
  onSelect(value);
85
281
  },
86
282
  [onSelect]
87
283
  );
88
284
 
285
+ const handleOpenChange = useCallback(
286
+ (open: boolean) => {
287
+ onOpenChange?.(open);
288
+ },
289
+ [onOpenChange]
290
+ );
291
+
292
+ const hasCheckedItem = items.some(
293
+ (item) => isSelectableMenuItem(item) && item.checked
294
+ );
295
+
89
296
  return (
90
- <RadixContextMenu.Root onOpenChange={onOpenChange}>
91
- <RadixContextMenu.Trigger asChild onPointerDown={onPointerDown}>
92
- {children}
93
- </RadixContextMenu.Trigger>
94
- <RadixContextMenu.Portal>
95
- <RadixContextMenu.Content
297
+ <BaseContextMenu.Root onOpenChange={handleOpenChange}>
298
+ <BaseContextMenu.Trigger
299
+ render={(triggerProps) =>
300
+ cloneElementWithMergedProps(children, triggerProps)
301
+ }
302
+ />
303
+ <BaseContextMenu.Portal>
304
+ <BaseContextMenu.Positioner
96
305
  {...portalScopeProps(portalScopeId)}
97
- className={styles.contentStyle}
306
+ className="n-z-menu n-outline-none"
98
307
  >
99
- <MenuViewport
100
- items={items}
101
- Components={Components}
102
- onSelect={handleSelectItem}
103
- />
104
- </RadixContextMenu.Content>
105
- </RadixContextMenu.Portal>
106
- </RadixContextMenu.Root>
308
+ <BaseContextMenu.Popup className={styles.contentStyle}>
309
+ <ContextMenuViewport
310
+ items={items}
311
+ onSelect={handleSelectItem}
312
+ hasCheckedItem={hasCheckedItem}
313
+ />
314
+ </BaseContextMenu.Popup>
315
+ </BaseContextMenu.Positioner>
316
+ </BaseContextMenu.Portal>
317
+ </BaseContextMenu.Root>
107
318
  );
108
319
  }
109
320