@noya-app/noya-designsystem 0.1.52 → 0.1.53
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 +6 -0
- package/dist/index.d.mts +29 -6
- package/dist/index.d.ts +29 -6
- package/dist/index.js +324 -249
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +838 -771
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/Button.tsx +1 -1
- package/src/components/ColorSwatchControl.tsx +4 -6
- package/src/components/ContextMenu.tsx +10 -2
- package/src/components/DraggableMenuButton.tsx +17 -30
- package/src/components/DropdownMenu.tsx +7 -0
- package/src/components/Popover.tsx +7 -0
- package/src/components/SelectMenu.tsx +7 -0
- package/src/components/SelectionToolbar.tsx +51 -45
- package/src/components/Toolbar.tsx +78 -47
- package/src/components/Tooltip.tsx +7 -0
- package/src/components/internal/Menu.tsx +1 -1
- package/src/components/internal/MenuViewport.tsx +9 -1
- package/src/contexts/PortalScopeContext.tsx +48 -0
- package/src/index.tsx +1 -0
package/package.json
CHANGED
|
@@ -89,7 +89,7 @@ export const Button = forwardRef(function Button(
|
|
|
89
89
|
disabled ? "opacity-25" : "",
|
|
90
90
|
!className?.includes("flex") ? "flex-none" : "",
|
|
91
91
|
active
|
|
92
|
-
? "bg-primary text-
|
|
92
|
+
? "bg-primary-pastel text-primary hover:bg-primary-pastel-light min-w-[31px]"
|
|
93
93
|
: variantStyles[variant],
|
|
94
94
|
sizeStyles[
|
|
95
95
|
variant === "thin"
|
|
@@ -63,13 +63,12 @@ export function ColorSwatchControl(props: ColorSwatchControlProps) {
|
|
|
63
63
|
checked={value === option}
|
|
64
64
|
/>
|
|
65
65
|
);
|
|
66
|
-
return
|
|
67
|
-
item
|
|
68
|
-
) : (
|
|
66
|
+
return (
|
|
69
67
|
<ToggleGroupPrimitive.Item
|
|
70
68
|
asChild
|
|
71
69
|
value={option as string}
|
|
72
70
|
key={option as string}
|
|
71
|
+
disabled={readOnly}
|
|
73
72
|
>
|
|
74
73
|
{item}
|
|
75
74
|
</ToggleGroupPrimitive.Item>
|
|
@@ -77,14 +76,13 @@ export function ColorSwatchControl(props: ColorSwatchControlProps) {
|
|
|
77
76
|
})}
|
|
78
77
|
</div>
|
|
79
78
|
);
|
|
80
|
-
return
|
|
81
|
-
content
|
|
82
|
-
) : (
|
|
79
|
+
return (
|
|
83
80
|
<ToggleGroupPrimitive.Root
|
|
84
81
|
type="single"
|
|
85
82
|
value={value as string}
|
|
86
83
|
onValueChange={onValueChange}
|
|
87
84
|
className="w-full"
|
|
85
|
+
disabled={readOnly}
|
|
88
86
|
>
|
|
89
87
|
{content}
|
|
90
88
|
</ToggleGroupPrimitive.Root>
|
|
@@ -2,13 +2,16 @@ import { useKeyboardShortcuts } from "@noya-app/noya-keymap";
|
|
|
2
2
|
import { memoGeneric } from "@noya-app/react-utils";
|
|
3
3
|
import * as RadixContextMenu from "@radix-ui/react-context-menu";
|
|
4
4
|
import React, { ReactNode, useCallback, useMemo } from "react";
|
|
5
|
+
import {
|
|
6
|
+
portalScopeProps,
|
|
7
|
+
usePortalScopeId,
|
|
8
|
+
} from "../contexts/PortalScopeContext";
|
|
5
9
|
import {
|
|
6
10
|
MenuItem,
|
|
7
11
|
getKeyboardShortcutsForMenuItems,
|
|
8
12
|
styles,
|
|
9
13
|
} from "./internal/Menu";
|
|
10
14
|
import { MenuViewport } from "./internal/MenuViewport";
|
|
11
|
-
|
|
12
15
|
export interface MenuItemProps<T extends string> {
|
|
13
16
|
value?: T;
|
|
14
17
|
children: ReactNode;
|
|
@@ -60,6 +63,8 @@ function ContextMenuRoot<T extends string>({
|
|
|
60
63
|
[items, onSelect, shouldBindKeyboardShortcuts]
|
|
61
64
|
);
|
|
62
65
|
|
|
66
|
+
const portalScopeId = usePortalScopeId();
|
|
67
|
+
|
|
63
68
|
useKeyboardShortcuts(keymap);
|
|
64
69
|
|
|
65
70
|
// We call preventDefault both to:
|
|
@@ -85,7 +90,10 @@ function ContextMenuRoot<T extends string>({
|
|
|
85
90
|
{children}
|
|
86
91
|
</RadixContextMenu.Trigger>
|
|
87
92
|
<RadixContextMenu.Portal>
|
|
88
|
-
<RadixContextMenu.Content
|
|
93
|
+
<RadixContextMenu.Content
|
|
94
|
+
{...portalScopeProps(portalScopeId)}
|
|
95
|
+
className={styles.contentStyle}
|
|
96
|
+
>
|
|
89
97
|
<MenuViewport
|
|
90
98
|
items={items}
|
|
91
99
|
Components={Components}
|
|
@@ -1,36 +1,11 @@
|
|
|
1
1
|
import { DragHandleDots2Icon } from "@noya-app/noya-icons";
|
|
2
2
|
import { memoGeneric } from "@noya-app/react-utils";
|
|
3
|
-
import React, {
|
|
3
|
+
import React, { useCallback, useState } from "react";
|
|
4
4
|
import { cssVars } from "../theme";
|
|
5
5
|
import { cx } from "../utils/classNames";
|
|
6
6
|
import { DropdownMenu } from "./DropdownMenu";
|
|
7
7
|
import { MenuItem } from "./internal/Menu";
|
|
8
8
|
|
|
9
|
-
const DotButton = forwardRef<
|
|
10
|
-
HTMLDivElement,
|
|
11
|
-
React.HTMLAttributes<HTMLDivElement>
|
|
12
|
-
>(({ className, ...props }, ref) => (
|
|
13
|
-
<div
|
|
14
|
-
ref={ref}
|
|
15
|
-
className={cx(
|
|
16
|
-
`cursor-pointer rounded flex items-center justify-center py-[2px] hover:bg-input-background-light active:bg-active-background `,
|
|
17
|
-
className
|
|
18
|
-
)}
|
|
19
|
-
{...props}
|
|
20
|
-
/>
|
|
21
|
-
));
|
|
22
|
-
|
|
23
|
-
const Trigger = forwardRef<
|
|
24
|
-
HTMLDivElement,
|
|
25
|
-
React.HTMLAttributes<HTMLDivElement>
|
|
26
|
-
>(({ className, ...props }, ref) => (
|
|
27
|
-
<div
|
|
28
|
-
ref={ref}
|
|
29
|
-
className={cx(`pointer-events-none h-[15px] `, className)}
|
|
30
|
-
{...props}
|
|
31
|
-
/>
|
|
32
|
-
));
|
|
33
|
-
|
|
34
9
|
/**
|
|
35
10
|
* A button that opens a menu when clicked, but also allows dragging the
|
|
36
11
|
*/
|
|
@@ -39,11 +14,17 @@ export const DraggableMenuButton = memoGeneric(function DraggableMenuButton<
|
|
|
39
14
|
>({
|
|
40
15
|
items,
|
|
41
16
|
onSelect,
|
|
17
|
+
onClick,
|
|
42
18
|
isVisible = true,
|
|
19
|
+
className,
|
|
20
|
+
style,
|
|
43
21
|
}: {
|
|
44
22
|
items?: MenuItem<T>[];
|
|
45
23
|
onSelect?: (value: T) => void;
|
|
24
|
+
onClick?: () => void;
|
|
46
25
|
isVisible?: boolean;
|
|
26
|
+
className?: string;
|
|
27
|
+
style?: React.CSSProperties;
|
|
47
28
|
}) {
|
|
48
29
|
const [open, setOpen] = useState(false);
|
|
49
30
|
const [downPosition, setDownPosition] = useState<{
|
|
@@ -94,12 +75,18 @@ export const DraggableMenuButton = memoGeneric(function DraggableMenuButton<
|
|
|
94
75
|
);
|
|
95
76
|
|
|
96
77
|
return (
|
|
97
|
-
<
|
|
78
|
+
<div
|
|
79
|
+
className={cx(
|
|
80
|
+
"cursor-pointer rounded flex items-center justify-center py-[2px] hover:bg-input-background-light active:bg-active-background ",
|
|
81
|
+
className
|
|
82
|
+
)}
|
|
83
|
+
style={style}
|
|
98
84
|
onPointerDownCapture={handlePointerDownCapture}
|
|
99
85
|
onPointerUp={handlePointerUp}
|
|
100
86
|
onClick={(event) => {
|
|
101
87
|
event.stopPropagation();
|
|
102
88
|
event.preventDefault();
|
|
89
|
+
onClick?.();
|
|
103
90
|
}}
|
|
104
91
|
>
|
|
105
92
|
{items && onSelect ? (
|
|
@@ -110,17 +97,17 @@ export const DraggableMenuButton = memoGeneric(function DraggableMenuButton<
|
|
|
110
97
|
onSelect={onSelect}
|
|
111
98
|
shouldBindKeyboardShortcuts={false}
|
|
112
99
|
>
|
|
113
|
-
<
|
|
100
|
+
<div className="pointer-events-none h-[15px]">
|
|
114
101
|
<DragHandleDots2Icon
|
|
115
102
|
color={isVisible || open ? cssVars.colors.icon : "transparent"}
|
|
116
103
|
/>
|
|
117
|
-
</
|
|
104
|
+
</div>
|
|
118
105
|
</DropdownMenu>
|
|
119
106
|
) : (
|
|
120
107
|
<DragHandleDots2Icon
|
|
121
108
|
color={isVisible || open ? cssVars.colors.icon : "transparent"}
|
|
122
109
|
/>
|
|
123
110
|
)}
|
|
124
|
-
</
|
|
111
|
+
</div>
|
|
125
112
|
);
|
|
126
113
|
});
|
|
@@ -7,6 +7,10 @@ import React, {
|
|
|
7
7
|
SyntheticEvent,
|
|
8
8
|
useMemo,
|
|
9
9
|
} from "react";
|
|
10
|
+
import {
|
|
11
|
+
portalScopeProps,
|
|
12
|
+
usePortalScopeId,
|
|
13
|
+
} from "../contexts/PortalScopeContext";
|
|
10
14
|
import { MenuProps } from "./ContextMenu";
|
|
11
15
|
import { getKeyboardShortcutsForMenuItems, styles } from "./internal/Menu";
|
|
12
16
|
import { MenuViewport } from "./internal/MenuViewport";
|
|
@@ -57,6 +61,8 @@ const DropdownMenuRoot = forwardRefGeneric(function DropdownMenuRoot<
|
|
|
57
61
|
contentStyle,
|
|
58
62
|
} = props;
|
|
59
63
|
|
|
64
|
+
const portalScopeId = usePortalScopeId();
|
|
65
|
+
|
|
60
66
|
const keymap = useMemo(
|
|
61
67
|
() =>
|
|
62
68
|
shouldBindKeyboardShortcuts === false
|
|
@@ -86,6 +92,7 @@ const DropdownMenuRoot = forwardRefGeneric(function DropdownMenuRoot<
|
|
|
86
92
|
</RadixDropdownMenu.Trigger>
|
|
87
93
|
<RadixDropdownMenu.Portal>
|
|
88
94
|
<RadixDropdownMenu.Content
|
|
95
|
+
{...portalScopeProps(portalScopeId)}
|
|
89
96
|
className={styles.contentStyle}
|
|
90
97
|
side={side}
|
|
91
98
|
sideOffset={sideOffset}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
|
2
2
|
import React, { ComponentProps } from "react";
|
|
3
|
+
import {
|
|
4
|
+
portalScopeProps,
|
|
5
|
+
usePortalScopeId,
|
|
6
|
+
} from "../contexts/PortalScopeContext";
|
|
3
7
|
import { cx } from "../utils/classNames";
|
|
4
8
|
import { IconButton } from "./IconButton";
|
|
5
9
|
|
|
@@ -56,11 +60,14 @@ export function Popover({
|
|
|
56
60
|
onClickClose,
|
|
57
61
|
className,
|
|
58
62
|
}: Props) {
|
|
63
|
+
const portalScopeId = usePortalScopeId();
|
|
64
|
+
|
|
59
65
|
return (
|
|
60
66
|
<PopoverPrimitive.Root open={open} onOpenChange={onOpenChange}>
|
|
61
67
|
<PopoverPrimitive.Trigger asChild>{trigger}</PopoverPrimitive.Trigger>
|
|
62
68
|
<PopoverPrimitive.Portal>
|
|
63
69
|
<PopoverPrimitive.Content
|
|
70
|
+
{...portalScopeProps(portalScopeId)}
|
|
64
71
|
className={cx(
|
|
65
72
|
popoverStyle.base,
|
|
66
73
|
variant === "large" && popoverStyle.large,
|
|
@@ -15,6 +15,10 @@ import React, {
|
|
|
15
15
|
import { useLabel, useLabelPosition } from "../hooks/useLabel";
|
|
16
16
|
import { cx } from "../utils/classNames";
|
|
17
17
|
|
|
18
|
+
import {
|
|
19
|
+
portalScopeProps,
|
|
20
|
+
usePortalScopeId,
|
|
21
|
+
} from "../contexts/PortalScopeContext";
|
|
18
22
|
import { getInsetEndStyles } from "../utils/inputs";
|
|
19
23
|
import { Button } from "./Button";
|
|
20
24
|
import { renderIcon } from "./Icons";
|
|
@@ -148,6 +152,8 @@ export const SelectMenu = memoGeneric(function SelectMenu<
|
|
|
148
152
|
contentStyle: contentStyleProp,
|
|
149
153
|
...props
|
|
150
154
|
}: Props<T>) {
|
|
155
|
+
const portalScopeId = usePortalScopeId();
|
|
156
|
+
|
|
151
157
|
const selectedItem = items.find(
|
|
152
158
|
(item): item is SelectableMenuItem<T> =>
|
|
153
159
|
isSelectableMenuItem(item) && item.value === value
|
|
@@ -213,6 +219,7 @@ export const SelectMenu = memoGeneric(function SelectMenu<
|
|
|
213
219
|
/>
|
|
214
220
|
<Select.Portal>
|
|
215
221
|
<Select.Content
|
|
222
|
+
{...portalScopeProps(portalScopeId)}
|
|
216
223
|
className={styles.contentStyle}
|
|
217
224
|
position="popper"
|
|
218
225
|
sideOffset={6}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
portalScopeProps,
|
|
3
|
+
usePortalScopeId,
|
|
4
|
+
} from "@noya-app/noya-designsystem";
|
|
2
5
|
import { Rect } from "@noya-app/noya-geometry";
|
|
3
|
-
import {
|
|
6
|
+
import { useSize } from "@noya-app/react-utils";
|
|
4
7
|
import * as PopperPrimitive from "@radix-ui/react-popper";
|
|
5
8
|
import type { MeasurableElement } from "@radix-ui/utils";
|
|
6
|
-
import React, { CSSProperties, useMemo } from "react";
|
|
9
|
+
import React, { CSSProperties, memo, useMemo } from "react";
|
|
7
10
|
|
|
8
11
|
// Create a Measurable from a Rect
|
|
9
12
|
const createVirtualElement = (rect: Rect): MeasurableElement => ({
|
|
@@ -20,51 +23,54 @@ const createVirtualElement = (rect: Rect): MeasurableElement => ({
|
|
|
20
23
|
}),
|
|
21
24
|
});
|
|
22
25
|
|
|
23
|
-
type
|
|
26
|
+
type SelectionToolbarContainerProps = {
|
|
24
27
|
rect: Rect;
|
|
25
|
-
|
|
26
|
-
onSelectMenuItem: (item: T) => void;
|
|
28
|
+
children: React.ReactNode;
|
|
27
29
|
};
|
|
28
30
|
|
|
29
|
-
export const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
export const SelectionToolbarContainer = memo(
|
|
32
|
+
function SelectionToolbarContainer({
|
|
33
|
+
rect,
|
|
34
|
+
children,
|
|
35
|
+
}: SelectionToolbarContainerProps) {
|
|
36
|
+
const portalScopeId = usePortalScopeId();
|
|
37
|
+
const containerRef = React.useRef<HTMLDivElement>(null);
|
|
38
|
+
const size = useSize(containerRef, "width");
|
|
34
39
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
// Create a virtual reference element from the rect
|
|
41
|
+
const virtualRef = useMemo(
|
|
42
|
+
() => ({
|
|
43
|
+
current: createVirtualElement(rect),
|
|
44
|
+
}),
|
|
45
|
+
[rect]
|
|
46
|
+
);
|
|
42
47
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
>
|
|
61
|
-
<div
|
|
62
|
-
ref={containerRef}
|
|
63
|
-
className="flex gap-1 bg-popover-background rounded shadow-popover p-1"
|
|
48
|
+
return (
|
|
49
|
+
<PopperPrimitive.Root>
|
|
50
|
+
<PopperPrimitive.Anchor virtualRef={virtualRef} />
|
|
51
|
+
<PopperPrimitive.Content
|
|
52
|
+
{...portalScopeProps(portalScopeId)}
|
|
53
|
+
side="top"
|
|
54
|
+
sideOffset={10}
|
|
55
|
+
align="center"
|
|
56
|
+
avoidCollisions
|
|
57
|
+
collisionPadding={10}
|
|
58
|
+
contentEditable={false}
|
|
59
|
+
style={{
|
|
60
|
+
zIndex: 1000,
|
|
61
|
+
opacity: size ? 1 : 0,
|
|
62
|
+
transition: "opacity 0.2s ease-in-out",
|
|
63
|
+
["--n-input-background" as keyof CSSProperties]: "transparent",
|
|
64
|
+
}}
|
|
64
65
|
>
|
|
65
|
-
<
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
66
|
+
<div
|
|
67
|
+
ref={containerRef}
|
|
68
|
+
className="flex gap-1 bg-popover-background rounded shadow-popover p-1"
|
|
69
|
+
>
|
|
70
|
+
{children}
|
|
71
|
+
</div>
|
|
72
|
+
</PopperPrimitive.Content>
|
|
73
|
+
</PopperPrimitive.Root>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
);
|
|
@@ -3,7 +3,6 @@ import { useKeyboardShortcuts } from "@noya-app/noya-keymap";
|
|
|
3
3
|
import React from "react";
|
|
4
4
|
|
|
5
5
|
import { memoGeneric } from "@noya-app/react-utils";
|
|
6
|
-
import { cssVars } from "../theme";
|
|
7
6
|
import { BaseToolbar } from "./BaseToolbar";
|
|
8
7
|
import { Button } from "./Button";
|
|
9
8
|
import { DividerVertical } from "./Divider";
|
|
@@ -13,10 +12,79 @@ import {
|
|
|
13
12
|
getKeyboardShortcutsForMenuItems,
|
|
14
13
|
isSelectableMenuItem,
|
|
15
14
|
MenuItem,
|
|
15
|
+
SelectableMenuItem,
|
|
16
|
+
SubMenuItem,
|
|
16
17
|
} from "./internal/Menu";
|
|
17
18
|
import { Spacer } from "./Spacer";
|
|
18
19
|
import { Tooltip } from "./Tooltip";
|
|
19
20
|
|
|
21
|
+
export const ToolbarMenuDropdown = memoGeneric(function ToolbarMenuDropdown<
|
|
22
|
+
T extends string,
|
|
23
|
+
>({
|
|
24
|
+
item,
|
|
25
|
+
onSelectMenuItem,
|
|
26
|
+
}: {
|
|
27
|
+
item: SubMenuItem<T>;
|
|
28
|
+
onSelectMenuItem?: (value: T) => void;
|
|
29
|
+
}) {
|
|
30
|
+
const [open, setOpen] = React.useState(false);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<DropdownMenu
|
|
34
|
+
open={open}
|
|
35
|
+
onOpenChange={setOpen}
|
|
36
|
+
items={item.items}
|
|
37
|
+
onSelect={(value) => {
|
|
38
|
+
if (onSelectMenuItem && value) {
|
|
39
|
+
onSelectMenuItem(value);
|
|
40
|
+
}
|
|
41
|
+
}}
|
|
42
|
+
>
|
|
43
|
+
<Button disabled={item.disabled} active={item.checked || open}>
|
|
44
|
+
{item.title}
|
|
45
|
+
{item.title && item.icon && <Spacer.Horizontal inline size={6} />}
|
|
46
|
+
{item.icon && renderIcon(item.icon)}
|
|
47
|
+
<Spacer.Horizontal size={6} />
|
|
48
|
+
<DropdownChevronIcon />
|
|
49
|
+
</Button>
|
|
50
|
+
</DropdownMenu>
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export const ToolbarMenuButton = memoGeneric(function ToolbarMenuButton<
|
|
55
|
+
T extends string,
|
|
56
|
+
>({
|
|
57
|
+
item,
|
|
58
|
+
onSelectMenuItem,
|
|
59
|
+
}: {
|
|
60
|
+
item: SelectableMenuItem<T>;
|
|
61
|
+
onSelectMenuItem?: (value: T) => void;
|
|
62
|
+
}) {
|
|
63
|
+
const content = (
|
|
64
|
+
<Button
|
|
65
|
+
disabled={item.disabled}
|
|
66
|
+
active={item.checked}
|
|
67
|
+
onClick={() => {
|
|
68
|
+
if (onSelectMenuItem && item.value) {
|
|
69
|
+
onSelectMenuItem(item.value);
|
|
70
|
+
}
|
|
71
|
+
}}
|
|
72
|
+
>
|
|
73
|
+
{item.title}
|
|
74
|
+
{item.title && item.icon && <Spacer.Horizontal inline size={6} />}
|
|
75
|
+
{item.icon && renderIcon(item.icon)}
|
|
76
|
+
</Button>
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return item.tooltip ? (
|
|
80
|
+
<Tooltip sideOffset={10} content={item.tooltip}>
|
|
81
|
+
{content}
|
|
82
|
+
</Tooltip>
|
|
83
|
+
) : (
|
|
84
|
+
content
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
20
88
|
export interface ToolbarProps<T extends string = string> {
|
|
21
89
|
children?: React.ReactNode;
|
|
22
90
|
logo?: React.ReactNode;
|
|
@@ -80,11 +148,6 @@ export function Toolbar<T extends string = string>({
|
|
|
80
148
|
);
|
|
81
149
|
}
|
|
82
150
|
|
|
83
|
-
const activeButtonStyle = {
|
|
84
|
-
backgroundColor: cssVars.colors.primaryPastel,
|
|
85
|
-
color: cssVars.colors.primary,
|
|
86
|
-
};
|
|
87
|
-
|
|
88
151
|
export const ToolbarMenu = memoGeneric(function ToolbarMenu<T extends string>({
|
|
89
152
|
items,
|
|
90
153
|
onSelectMenuItem,
|
|
@@ -102,53 +165,21 @@ export const ToolbarMenu = memoGeneric(function ToolbarMenu<T extends string>({
|
|
|
102
165
|
}
|
|
103
166
|
|
|
104
167
|
if (isSelectableMenuItem(item)) {
|
|
105
|
-
|
|
106
|
-
<
|
|
168
|
+
return (
|
|
169
|
+
<ToolbarMenuButton
|
|
107
170
|
key={i}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (onSelectMenuItem && item.value) {
|
|
112
|
-
onSelectMenuItem(item.value);
|
|
113
|
-
}
|
|
114
|
-
}}
|
|
115
|
-
>
|
|
116
|
-
{item.title}
|
|
117
|
-
{item.title && item.icon && <Spacer.Horizontal inline size={6} />}
|
|
118
|
-
{item.icon && renderIcon(item.icon)}
|
|
119
|
-
</Button>
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
return item.tooltip ? (
|
|
123
|
-
<Tooltip sideOffset={10} content={item.tooltip} key={i}>
|
|
124
|
-
{content}
|
|
125
|
-
</Tooltip>
|
|
126
|
-
) : (
|
|
127
|
-
content
|
|
171
|
+
item={item}
|
|
172
|
+
onSelectMenuItem={onSelectMenuItem}
|
|
173
|
+
/>
|
|
128
174
|
);
|
|
129
175
|
}
|
|
130
176
|
|
|
131
177
|
return (
|
|
132
|
-
<
|
|
178
|
+
<ToolbarMenuDropdown
|
|
133
179
|
key={i}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
onSelectMenuItem(value);
|
|
138
|
-
}
|
|
139
|
-
}}
|
|
140
|
-
>
|
|
141
|
-
<Button
|
|
142
|
-
disabled={item.disabled}
|
|
143
|
-
style={item.checked ? activeButtonStyle : undefined}
|
|
144
|
-
>
|
|
145
|
-
{item.title}
|
|
146
|
-
{item.title && item.icon && <Spacer.Horizontal inline size={6} />}
|
|
147
|
-
{item.icon && renderIcon(item.icon)}
|
|
148
|
-
<Spacer.Horizontal size={6} />
|
|
149
|
-
<DropdownChevronIcon />
|
|
150
|
-
</Button>
|
|
151
|
-
</DropdownMenu>
|
|
180
|
+
item={item}
|
|
181
|
+
onSelectMenuItem={onSelectMenuItem}
|
|
182
|
+
/>
|
|
152
183
|
);
|
|
153
184
|
})}
|
|
154
185
|
</>
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
2
2
|
import * as React from "react";
|
|
3
|
+
import {
|
|
4
|
+
portalScopeProps,
|
|
5
|
+
usePortalScopeId,
|
|
6
|
+
} from "../contexts/PortalScopeContext";
|
|
3
7
|
import { textStyles } from "./Text";
|
|
4
8
|
|
|
5
9
|
// const Arrow = styled(TooltipPrimitive.Arrow)(({ theme }) => ({
|
|
@@ -17,12 +21,15 @@ export const Tooltip = React.memo(function Tooltip({
|
|
|
17
21
|
content,
|
|
18
22
|
sideOffset = 2,
|
|
19
23
|
}: Props) {
|
|
24
|
+
const portalScopeId = usePortalScopeId();
|
|
25
|
+
|
|
20
26
|
return (
|
|
21
27
|
<TooltipPrimitive.Provider>
|
|
22
28
|
<TooltipPrimitive.Root>
|
|
23
29
|
<TooltipPrimitive.Trigger asChild>{children}</TooltipPrimitive.Trigger>
|
|
24
30
|
<TooltipPrimitive.Portal>
|
|
25
31
|
<TooltipPrimitive.Content
|
|
32
|
+
{...portalScopeProps(portalScopeId)}
|
|
26
33
|
side="bottom"
|
|
27
34
|
align="center"
|
|
28
35
|
sideOffset={sideOffset}
|
|
@@ -95,7 +95,7 @@ export type SelectableMenuItem<T extends string> = BaseMenuItem & {
|
|
|
95
95
|
tooltip?: ReactNode;
|
|
96
96
|
};
|
|
97
97
|
|
|
98
|
-
type SubMenuItem<T extends string> = BaseMenuItem & {
|
|
98
|
+
export type SubMenuItem<T extends string> = BaseMenuItem & {
|
|
99
99
|
type: "submenu";
|
|
100
100
|
items: MenuItem<T>[];
|
|
101
101
|
id: string;
|
|
@@ -2,7 +2,11 @@ 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 {
|
|
5
|
+
import {
|
|
6
|
+
portalScopeProps,
|
|
7
|
+
usePortalScopeId,
|
|
8
|
+
} from "../../contexts/PortalScopeContext";
|
|
9
|
+
import { cx } from "../../utils/classNames";
|
|
6
10
|
import { MenuItem, SectionHeader, styles } from "./Menu";
|
|
7
11
|
import { SelectItem } from "./SelectItem";
|
|
8
12
|
|
|
@@ -59,6 +63,8 @@ export const MenuViewport = <T extends string>({
|
|
|
59
63
|
Components,
|
|
60
64
|
onSelect,
|
|
61
65
|
}: MenuViewportProps<T>) => {
|
|
66
|
+
const portalScopeId = usePortalScopeId();
|
|
67
|
+
|
|
62
68
|
const hasCheckedItem = items.some(
|
|
63
69
|
(item) =>
|
|
64
70
|
item.type !== "separator" && item.type !== "sectionHeader" && item.checked
|
|
@@ -105,6 +111,7 @@ export const MenuViewport = <T extends string>({
|
|
|
105
111
|
e.stopPropagation();
|
|
106
112
|
e.preventDefault();
|
|
107
113
|
}}
|
|
114
|
+
disabled={item.disabled}
|
|
108
115
|
>
|
|
109
116
|
<SelectItem
|
|
110
117
|
value={item.id}
|
|
@@ -123,6 +130,7 @@ export const MenuViewport = <T extends string>({
|
|
|
123
130
|
</Components.SubTrigger>
|
|
124
131
|
<Components.Portal>
|
|
125
132
|
<Components.SubContent
|
|
133
|
+
{...portalScopeProps(portalScopeId)}
|
|
126
134
|
alignOffset={-5}
|
|
127
135
|
className={styles.contentStyle}
|
|
128
136
|
>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
export type PortalScopeContextValue = string;
|
|
4
|
+
|
|
5
|
+
const PortalScopeContext = React.createContext<
|
|
6
|
+
PortalScopeContextValue | undefined
|
|
7
|
+
>(undefined);
|
|
8
|
+
|
|
9
|
+
export const PortalScopeProvider = React.memo(function PortalScopeProvider({
|
|
10
|
+
children,
|
|
11
|
+
portalScopeId,
|
|
12
|
+
}: {
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
portalScopeId: string;
|
|
15
|
+
}) {
|
|
16
|
+
return (
|
|
17
|
+
<PortalScopeContext.Provider value={portalScopeId}>
|
|
18
|
+
{children}
|
|
19
|
+
</PortalScopeContext.Provider>
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export function usePortalScopeId(): string {
|
|
24
|
+
return React.useContext(PortalScopeContext) ?? "";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const portalScopeDataSetName = "noyaPortalScope";
|
|
28
|
+
export const portalScopePropName = `data-noya-portal-scope`;
|
|
29
|
+
|
|
30
|
+
export function portalScopeProps(id?: string) {
|
|
31
|
+
return {
|
|
32
|
+
[portalScopePropName]: id,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getClosestPortalScope(element: EventTarget) {
|
|
37
|
+
if (!(element instanceof HTMLElement)) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const closest = element.closest(`[${portalScopePropName}]`);
|
|
42
|
+
|
|
43
|
+
if (!closest || !(closest instanceof HTMLElement)) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return closest.dataset[portalScopeDataSetName];
|
|
48
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -78,6 +78,7 @@ export * from "./contexts/DialogContext";
|
|
|
78
78
|
export * from "./contexts/FloatingWindowContext";
|
|
79
79
|
export * from "./contexts/GlobalInputBlurContext";
|
|
80
80
|
export * from "./contexts/ImageDataContext";
|
|
81
|
+
export * from "./contexts/PortalScopeContext";
|
|
81
82
|
// Hooks
|
|
82
83
|
export * from "./hooks/useHover";
|
|
83
84
|
export * from "./hooks/useIndent";
|