@noya-app/noya-designsystem 0.1.52 → 0.1.54
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 +14 -0
- package/dist/index.css +1 -1
- package/dist/index.d.mts +38 -12
- package/dist/index.d.ts +38 -12
- package/dist/index.js +421 -326
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +900 -814
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/components/Button.tsx +1 -1
- package/src/components/ColorSwatch.tsx +5 -2
- 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/SegmentedControl.tsx +13 -42
- package/src/components/SelectMenu.tsx +7 -0
- package/src/components/SelectionToolbar.tsx +51 -45
- package/src/components/Slider.tsx +1 -1
- package/src/components/Toolbar.tsx +124 -76
- package/src/components/Tooltip.tsx +7 -0
- package/src/components/connected-users-menu/ConnectedUsersMenuLayout.tsx +2 -2
- package/src/components/internal/Menu.tsx +2 -2
- package/src/components/internal/MenuViewport.tsx +9 -1
- package/src/components/internal/SelectItem.tsx +24 -3
- package/src/contexts/PortalScopeContext.tsx +48 -0
- package/src/index.tsx +1 -0
|
@@ -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,134 @@ 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
|
+
const iconButtonStyle = {
|
|
22
|
+
height: 27,
|
|
23
|
+
width: 27,
|
|
24
|
+
minWidth: 27,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const ToolbarMenuDropdown = memoGeneric(function ToolbarMenuDropdown<
|
|
28
|
+
T extends string,
|
|
29
|
+
>({
|
|
30
|
+
item,
|
|
31
|
+
onSelectMenuItem,
|
|
32
|
+
}: {
|
|
33
|
+
item: SubMenuItem<T>;
|
|
34
|
+
onSelectMenuItem?: (value: T) => void;
|
|
35
|
+
}) {
|
|
36
|
+
const [open, setOpen] = React.useState(false);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<DropdownMenu
|
|
40
|
+
open={open}
|
|
41
|
+
onOpenChange={setOpen}
|
|
42
|
+
items={item.items}
|
|
43
|
+
onSelect={(value) => {
|
|
44
|
+
if (onSelectMenuItem && value) {
|
|
45
|
+
onSelectMenuItem(value);
|
|
46
|
+
}
|
|
47
|
+
}}
|
|
48
|
+
>
|
|
49
|
+
<Button disabled={item.disabled} active={item.checked || open}>
|
|
50
|
+
{item.icon && renderIcon(item.icon)}
|
|
51
|
+
{item.title && item.icon && <Spacer.Horizontal inline size={6} />}
|
|
52
|
+
{item.title}
|
|
53
|
+
<Spacer.Horizontal inline size={6} />
|
|
54
|
+
<DropdownChevronIcon />
|
|
55
|
+
</Button>
|
|
56
|
+
</DropdownMenu>
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export const ToolbarMenuButton = memoGeneric(function ToolbarMenuButton<
|
|
61
|
+
T extends string,
|
|
62
|
+
>({
|
|
63
|
+
item,
|
|
64
|
+
onSelectMenuItem,
|
|
65
|
+
}: {
|
|
66
|
+
item: SelectableMenuItem<T>;
|
|
67
|
+
onSelectMenuItem?: (value: T) => void;
|
|
68
|
+
}) {
|
|
69
|
+
const content = (
|
|
70
|
+
<Button
|
|
71
|
+
disabled={item.disabled}
|
|
72
|
+
active={item.checked}
|
|
73
|
+
style={item.icon && !item.title ? iconButtonStyle : undefined}
|
|
74
|
+
onClick={() => {
|
|
75
|
+
if (onSelectMenuItem && item.value) {
|
|
76
|
+
onSelectMenuItem(item.value);
|
|
77
|
+
}
|
|
78
|
+
}}
|
|
79
|
+
>
|
|
80
|
+
{item.icon && renderIcon(item.icon)}
|
|
81
|
+
{item.title && item.icon && <Spacer.Horizontal inline size={6} />}
|
|
82
|
+
{item.title}
|
|
83
|
+
</Button>
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
return item.tooltip ? (
|
|
87
|
+
<Tooltip sideOffset={10} content={item.tooltip}>
|
|
88
|
+
{content}
|
|
89
|
+
</Tooltip>
|
|
90
|
+
) : (
|
|
91
|
+
content
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
export const ToolbarMenuItem = memoGeneric(function ToolbarMenuItem<
|
|
96
|
+
T extends string,
|
|
97
|
+
>({
|
|
98
|
+
item,
|
|
99
|
+
onSelectMenuItem,
|
|
100
|
+
}: {
|
|
101
|
+
item: MenuItem<T>;
|
|
102
|
+
onSelectMenuItem?: (value: T) => void;
|
|
103
|
+
}) {
|
|
104
|
+
if (item.type === "sectionHeader") return null;
|
|
105
|
+
|
|
106
|
+
if (item.type === "separator") {
|
|
107
|
+
return <DividerVertical overflow={4} />;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (isSelectableMenuItem(item)) {
|
|
111
|
+
return (
|
|
112
|
+
<ToolbarMenuButton item={item} onSelectMenuItem={onSelectMenuItem} />
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<ToolbarMenuDropdown item={item} onSelectMenuItem={onSelectMenuItem} />
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
export const ToolbarMenu = memoGeneric(function ToolbarMenu<T extends string>({
|
|
122
|
+
items,
|
|
123
|
+
onSelectMenuItem,
|
|
124
|
+
}: {
|
|
125
|
+
items: MenuItem<T>[];
|
|
126
|
+
onSelectMenuItem?: (value: T) => void;
|
|
127
|
+
}) {
|
|
128
|
+
return (
|
|
129
|
+
<>
|
|
130
|
+
{items.map((item, i) => {
|
|
131
|
+
return (
|
|
132
|
+
<ToolbarMenuItem
|
|
133
|
+
key={i}
|
|
134
|
+
item={item}
|
|
135
|
+
onSelectMenuItem={onSelectMenuItem}
|
|
136
|
+
/>
|
|
137
|
+
);
|
|
138
|
+
})}
|
|
139
|
+
</>
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
20
143
|
export interface ToolbarProps<T extends string = string> {
|
|
21
144
|
children?: React.ReactNode;
|
|
22
145
|
logo?: React.ReactNode;
|
|
@@ -79,78 +202,3 @@ export function Toolbar<T extends string = string>({
|
|
|
79
202
|
</BaseToolbar>
|
|
80
203
|
);
|
|
81
204
|
}
|
|
82
|
-
|
|
83
|
-
const activeButtonStyle = {
|
|
84
|
-
backgroundColor: cssVars.colors.primaryPastel,
|
|
85
|
-
color: cssVars.colors.primary,
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
export const ToolbarMenu = memoGeneric(function ToolbarMenu<T extends string>({
|
|
89
|
-
items,
|
|
90
|
-
onSelectMenuItem,
|
|
91
|
-
}: {
|
|
92
|
-
items: MenuItem<T>[];
|
|
93
|
-
onSelectMenuItem?: (value: T) => void;
|
|
94
|
-
}) {
|
|
95
|
-
return (
|
|
96
|
-
<>
|
|
97
|
-
{items.map((item, i) => {
|
|
98
|
-
if (item.type === "sectionHeader") return null;
|
|
99
|
-
|
|
100
|
-
if (item.type === "separator") {
|
|
101
|
-
return <DividerVertical key={i} overflow={4} />;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (isSelectableMenuItem(item)) {
|
|
105
|
-
const content = (
|
|
106
|
-
<Button
|
|
107
|
-
key={i}
|
|
108
|
-
disabled={item.disabled}
|
|
109
|
-
style={item.checked ? activeButtonStyle : undefined}
|
|
110
|
-
onClick={() => {
|
|
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
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return (
|
|
132
|
-
<DropdownMenu
|
|
133
|
-
key={i}
|
|
134
|
-
items={item.items}
|
|
135
|
-
onSelect={(value) => {
|
|
136
|
-
if (onSelectMenuItem && value) {
|
|
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>
|
|
152
|
-
);
|
|
153
|
-
})}
|
|
154
|
-
</>
|
|
155
|
-
);
|
|
156
|
-
});
|
|
@@ -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}
|
|
@@ -60,11 +60,11 @@ export const ConnectedUsersMenuLayout = ({
|
|
|
60
60
|
: undefined,
|
|
61
61
|
}}
|
|
62
62
|
>
|
|
63
|
-
AI
|
|
64
|
-
<Spacer.Horizontal size={8} />
|
|
65
63
|
<ChatBubbleWithDotsIcon
|
|
66
64
|
color={isAssistantOpen ? undefined : cssVars.colors.icon}
|
|
67
65
|
/>
|
|
66
|
+
<Spacer.Horizontal inline size={6} />
|
|
67
|
+
AI
|
|
68
68
|
</Button>
|
|
69
69
|
</Tooltip>
|
|
70
70
|
)}
|
|
@@ -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;
|
|
@@ -159,7 +159,7 @@ export const getMenuItemKey = <T extends string>(
|
|
|
159
159
|
};
|
|
160
160
|
|
|
161
161
|
export const CHECKBOX_WIDTH = 15;
|
|
162
|
-
export const CHECKBOX_RIGHT_INSET =
|
|
162
|
+
export const CHECKBOX_RIGHT_INSET = 6;
|
|
163
163
|
export const CHECKBOX_INDENT_WIDTH = 6;
|
|
164
164
|
|
|
165
165
|
export const styles = {
|
|
@@ -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
|
>
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { renderIcon } from "../Icons";
|
|
2
2
|
|
|
3
|
-
import { CheckIcon } from "@noya-app/noya-icons";
|
|
4
3
|
import * as Select from "@radix-ui/react-select";
|
|
5
4
|
import React, { useCallback } from "react";
|
|
6
5
|
import { cx } from "../../utils/classNames";
|
|
@@ -68,7 +67,7 @@ export const SelectItem = React.forwardRef(
|
|
|
68
67
|
<Spacer.Horizontal size={CHECKBOX_INDENT_WIDTH} />
|
|
69
68
|
{Components.ItemIndicator && (
|
|
70
69
|
<Components.ItemIndicator {...styles.itemIndicator}>
|
|
71
|
-
<
|
|
70
|
+
<Checkmark />
|
|
72
71
|
</Components.ItemIndicator>
|
|
73
72
|
)}
|
|
74
73
|
{icon && (
|
|
@@ -109,7 +108,7 @@ export const SelectItem = React.forwardRef(
|
|
|
109
108
|
{indented && <Spacer.Horizontal size={CHECKBOX_INDENT_WIDTH} />}
|
|
110
109
|
{checked ? (
|
|
111
110
|
<div {...styles.itemIndicator}>
|
|
112
|
-
<
|
|
111
|
+
<Checkmark />
|
|
113
112
|
</div>
|
|
114
113
|
) : indented ? (
|
|
115
114
|
<Spacer.Horizontal size={CHECKBOX_WIDTH - CHECKBOX_RIGHT_INSET} />
|
|
@@ -134,3 +133,25 @@ export const SelectItem = React.forwardRef(
|
|
|
134
133
|
);
|
|
135
134
|
}
|
|
136
135
|
);
|
|
136
|
+
|
|
137
|
+
function Checkmark() {
|
|
138
|
+
return (
|
|
139
|
+
<svg
|
|
140
|
+
viewBox="0 0 15 15"
|
|
141
|
+
fill="none"
|
|
142
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
143
|
+
aria-hidden="true"
|
|
144
|
+
width={15}
|
|
145
|
+
height={15}
|
|
146
|
+
>
|
|
147
|
+
<path
|
|
148
|
+
className="scale-90 origin-center"
|
|
149
|
+
d="M3 8L6 11L12 5"
|
|
150
|
+
strokeWidth="1.1"
|
|
151
|
+
strokeLinecap="round"
|
|
152
|
+
strokeLinejoin="round"
|
|
153
|
+
stroke="currentColor"
|
|
154
|
+
/>
|
|
155
|
+
</svg>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
@@ -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";
|