@noya-app/noya-designsystem 0.1.54 → 0.1.55
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 +8 -0
- package/dist/index.css +1 -1
- package/dist/index.d.mts +26 -5
- package/dist/index.d.ts +26 -5
- package/dist/index.js +363 -255
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +999 -894
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/components/Button.tsx +13 -0
- package/src/components/Dialog.tsx +3 -5
- package/src/components/Grid.tsx +3 -1
- package/src/components/GridView.tsx +1 -0
- package/src/components/InputField.tsx +10 -3
- package/src/components/Popover.tsx +68 -4
- package/src/components/SegmentedControl.tsx +41 -18
- package/src/components/SelectionToolbar.tsx +35 -22
- package/src/components/Toolbar.tsx +7 -10
- package/src/components/connected-users-menu/ConnectedUsersMenuLayout.tsx +3 -26
- package/src/contexts/DesignSystemConfiguration.tsx +8 -5
- package/src/contexts/OpenPortalsContext.tsx +67 -0
- package/src/index.css +2 -2
- package/src/index.tsx +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noya-app/noya-designsystem",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.55",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"module": "./dist/index.mjs",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"@noya-app/noya-colorpicker": "0.1.21",
|
|
26
26
|
"@noya-app/noya-utils": "0.1.5",
|
|
27
27
|
"@noya-app/noya-geometry": "0.1.11",
|
|
28
|
-
"@noya-app/noya-icons": "0.1.
|
|
28
|
+
"@noya-app/noya-icons": "0.1.9",
|
|
29
29
|
"@noya-app/noya-keymap": "0.1.3",
|
|
30
30
|
"@noya-app/noya-tailwind-config": "0.1.3",
|
|
31
31
|
"@radix-ui/primitive": "^1.0.0",
|
|
@@ -7,7 +7,10 @@ import React, {
|
|
|
7
7
|
useMemo,
|
|
8
8
|
} from "react";
|
|
9
9
|
import { cx, mergeConflictingClassNames } from "../utils/classNames";
|
|
10
|
+
import { renderIcon } from "./Icons";
|
|
11
|
+
import { Spacer } from "./Spacer";
|
|
10
12
|
import { Tooltip } from "./Tooltip";
|
|
13
|
+
import type { MenuItemIcon } from "./internal/Menu";
|
|
11
14
|
|
|
12
15
|
type ButtonVariant =
|
|
13
16
|
| "normal"
|
|
@@ -48,6 +51,8 @@ export interface ButtonRootProps {
|
|
|
48
51
|
className?: string;
|
|
49
52
|
style?: CSSProperties;
|
|
50
53
|
tabIndex?: number;
|
|
54
|
+
icon?: MenuItemIcon;
|
|
55
|
+
iconRight?: MenuItemIcon;
|
|
51
56
|
children: ReactNode;
|
|
52
57
|
active?: boolean;
|
|
53
58
|
disabled?: boolean;
|
|
@@ -71,6 +76,8 @@ export const Button = forwardRef(function Button(
|
|
|
71
76
|
className,
|
|
72
77
|
style,
|
|
73
78
|
tabIndex,
|
|
79
|
+
icon,
|
|
80
|
+
iconRight,
|
|
74
81
|
tooltip,
|
|
75
82
|
active = false,
|
|
76
83
|
disabled = false,
|
|
@@ -128,7 +135,13 @@ export const Button = forwardRef(function Button(
|
|
|
128
135
|
className="min-h-[19px] flex items-center flex-1 justify-center leading-[15px]"
|
|
129
136
|
style={contentStyle}
|
|
130
137
|
>
|
|
138
|
+
{icon && renderIcon(icon)}
|
|
139
|
+
{icon && children && <Spacer.Horizontal inline size={6} />}
|
|
131
140
|
{children}
|
|
141
|
+
{iconRight && (icon || children) && (
|
|
142
|
+
<Spacer.Horizontal inline size={6} />
|
|
143
|
+
)}
|
|
144
|
+
{iconRight && renderIcon(iconRight)}
|
|
132
145
|
</span>
|
|
133
146
|
</Component>
|
|
134
147
|
);
|
|
@@ -30,9 +30,8 @@ const StyledContent = forwardRef<
|
|
|
30
30
|
<DialogPrimitive.Content
|
|
31
31
|
ref={ref}
|
|
32
32
|
className={cx(
|
|
33
|
-
`
|
|
34
|
-
|
|
35
|
-
w-[90vw] max-w-[450px] max-h-[85vh] p-dialog-padding rounded-[2px]
|
|
33
|
+
`fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2
|
|
34
|
+
w-[90vw] max-w-[550px] max-h-[85vh] p-dialog-padding rounded-[2px]
|
|
36
35
|
font-sans text-heading5 font-normal leading-[19px] text-text-muted
|
|
37
36
|
bg-popover-background pointer-events-all z-[1000] focus:outline-none
|
|
38
37
|
shadow-dialog-shadow
|
|
@@ -54,7 +53,7 @@ const StyledTitle = forwardRef<
|
|
|
54
53
|
<DialogPrimitive.Title
|
|
55
54
|
ref={ref}
|
|
56
55
|
className={cx(
|
|
57
|
-
`m-0 font-sans text-
|
|
56
|
+
`m-0 font-sans text-heading3 font-medium leading-[1.4] text-text `,
|
|
58
57
|
className
|
|
59
58
|
)}
|
|
60
59
|
{...props}
|
|
@@ -152,7 +151,6 @@ export const Dialog = forwardRef(function Dialog(
|
|
|
152
151
|
)}
|
|
153
152
|
{description && (
|
|
154
153
|
<>
|
|
155
|
-
<Spacer.Vertical size={10} />
|
|
156
154
|
<StyledDescription>{description}</StyledDescription>
|
|
157
155
|
<Spacer.Vertical size={20} />
|
|
158
156
|
</>
|
package/src/components/Grid.tsx
CHANGED
|
@@ -239,7 +239,7 @@ const InputElement = forwardRef(
|
|
|
239
239
|
: size === "large"
|
|
240
240
|
? "py-2.5 text-heading5"
|
|
241
241
|
: size === "medium"
|
|
242
|
-
? "py-1
|
|
242
|
+
? "py-1 text-heading5"
|
|
243
243
|
: $variant === "bare" && "py-1 -m-1",
|
|
244
244
|
className
|
|
245
245
|
);
|
|
@@ -488,7 +488,9 @@ interface InputFieldRootProps {
|
|
|
488
488
|
id?: string;
|
|
489
489
|
children?: ReactNode;
|
|
490
490
|
start?: ReactNode;
|
|
491
|
+
startClassName?: string;
|
|
491
492
|
end?: ReactNode;
|
|
493
|
+
endClassName?: string;
|
|
492
494
|
label?: ReactNode;
|
|
493
495
|
width?: number;
|
|
494
496
|
startWidth?: number;
|
|
@@ -520,6 +522,8 @@ function InputFieldRoot({
|
|
|
520
522
|
className,
|
|
521
523
|
start,
|
|
522
524
|
end,
|
|
525
|
+
startClassName,
|
|
526
|
+
endClassName,
|
|
523
527
|
label: labelProp,
|
|
524
528
|
}: InputFieldRootProps) {
|
|
525
529
|
const randomId = useId();
|
|
@@ -577,12 +581,15 @@ function InputFieldRoot({
|
|
|
577
581
|
<RootContainer $width={width} style={style} className={className}>
|
|
578
582
|
{children}
|
|
579
583
|
{start && (
|
|
580
|
-
<span ref={startRef} className={startBaseStyles}>
|
|
584
|
+
<span ref={startRef} className={cx(startBaseStyles, startClassName)}>
|
|
581
585
|
{start}
|
|
582
586
|
</span>
|
|
583
587
|
)}
|
|
584
588
|
{(end || label) && (
|
|
585
|
-
<span
|
|
589
|
+
<span
|
|
590
|
+
ref={endRef}
|
|
591
|
+
className={cx(endStyles, label && end && "gap-1.5", endClassName)}
|
|
592
|
+
>
|
|
586
593
|
{labelProp
|
|
587
594
|
? insetLabel
|
|
588
595
|
: label && labelPosition === "inset" && insetLabel}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { useControlledOrUncontrolled } from "@noya-app/react-utils";
|
|
1
2
|
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
|
2
|
-
import React, { ComponentProps } from "react";
|
|
3
|
+
import React, { ComponentProps, useEffect, useId, useRef } from "react";
|
|
4
|
+
import { useOpenPortalsControls } from "../contexts/OpenPortalsContext";
|
|
3
5
|
import {
|
|
4
6
|
portalScopeProps,
|
|
5
7
|
usePortalScopeId,
|
|
@@ -34,6 +36,7 @@ interface Props
|
|
|
34
36
|
onClickClose?: () => void;
|
|
35
37
|
showArrow?: boolean;
|
|
36
38
|
className?: string;
|
|
39
|
+
portalContainer?: HTMLElement | null;
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
export const popoverStyle = {
|
|
@@ -59,15 +62,36 @@ export function Popover({
|
|
|
59
62
|
onFocusOutside,
|
|
60
63
|
onClickClose,
|
|
61
64
|
className,
|
|
65
|
+
portalContainer,
|
|
62
66
|
}: Props) {
|
|
67
|
+
const defaultId = useId();
|
|
63
68
|
const portalScopeId = usePortalScopeId();
|
|
69
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
70
|
+
const [isOpen, setIsOpen] = useControlledOrUncontrolled({
|
|
71
|
+
value: open,
|
|
72
|
+
onChange: onOpenChange,
|
|
73
|
+
defaultValue: false,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const { addOpenPortal, removeOpenPortal } = useOpenPortalsControls();
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (!isOpen) return;
|
|
80
|
+
|
|
81
|
+
addOpenPortal(portalScopeId || defaultId);
|
|
82
|
+
|
|
83
|
+
return () => {
|
|
84
|
+
removeOpenPortal(portalScopeId || defaultId);
|
|
85
|
+
};
|
|
86
|
+
}, [addOpenPortal, isOpen, portalScopeId, removeOpenPortal, defaultId]);
|
|
64
87
|
|
|
65
88
|
return (
|
|
66
|
-
<PopoverPrimitive.Root open={
|
|
89
|
+
<PopoverPrimitive.Root open={isOpen} onOpenChange={setIsOpen}>
|
|
67
90
|
<PopoverPrimitive.Trigger asChild>{trigger}</PopoverPrimitive.Trigger>
|
|
68
|
-
<PopoverPrimitive.Portal>
|
|
91
|
+
<PopoverPrimitive.Portal container={portalContainer}>
|
|
69
92
|
<PopoverPrimitive.Content
|
|
70
93
|
{...portalScopeProps(portalScopeId)}
|
|
94
|
+
ref={contentRef}
|
|
71
95
|
className={cx(
|
|
72
96
|
popoverStyle.base,
|
|
73
97
|
variant === "large" && popoverStyle.large,
|
|
@@ -81,7 +105,18 @@ export function Popover({
|
|
|
81
105
|
onCloseAutoFocus={onCloseAutoFocus}
|
|
82
106
|
onInteractOutside={onInteractOutside}
|
|
83
107
|
onFocusOutside={onFocusOutside}
|
|
84
|
-
onPointerDownOutside={
|
|
108
|
+
onPointerDownOutside={(event) => {
|
|
109
|
+
if (
|
|
110
|
+
isEventInsideElement(
|
|
111
|
+
event.detail.originalEvent,
|
|
112
|
+
contentRef.current
|
|
113
|
+
)
|
|
114
|
+
) {
|
|
115
|
+
event.preventDefault();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
onPointerDownOutside?.(event);
|
|
119
|
+
}}
|
|
85
120
|
collisionPadding={8}
|
|
86
121
|
arrowPadding={2}
|
|
87
122
|
// Don't propagate pointer down events to the canvas
|
|
@@ -109,3 +144,32 @@ export function Popover({
|
|
|
109
144
|
</PopoverPrimitive.Root>
|
|
110
145
|
);
|
|
111
146
|
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Radix ignores pointer-events on the body when a popover is open.
|
|
150
|
+
* This function checks if an event is inside an element by temporarily
|
|
151
|
+
* setting pointer-events to auto and then restoring it afterward.
|
|
152
|
+
*/
|
|
153
|
+
function isEventInsideElement(
|
|
154
|
+
event: PointerEvent,
|
|
155
|
+
element: HTMLElement | null
|
|
156
|
+
) {
|
|
157
|
+
if (!element) return false;
|
|
158
|
+
|
|
159
|
+
const body = document.body;
|
|
160
|
+
const originalPointerEvents = body.style.pointerEvents;
|
|
161
|
+
body.style.pointerEvents = "auto";
|
|
162
|
+
|
|
163
|
+
// Get all elements at the point
|
|
164
|
+
const elementsAtPoint = document.elementsFromPoint(
|
|
165
|
+
event.clientX,
|
|
166
|
+
event.clientY
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// Restore original pointer-events
|
|
170
|
+
body.style.pointerEvents = originalPointerEvents;
|
|
171
|
+
|
|
172
|
+
const target = elementsAtPoint.at(0);
|
|
173
|
+
|
|
174
|
+
return target && element.contains(target);
|
|
175
|
+
}
|
|
@@ -15,6 +15,7 @@ import { SelectableMenuItem } from "./internal/Menu";
|
|
|
15
15
|
import { Tooltip } from "./Tooltip";
|
|
16
16
|
|
|
17
17
|
type SegmentedControlColorScheme = "primary" | "secondary";
|
|
18
|
+
type SegmentedControlVariant = "default" | "tabs";
|
|
18
19
|
|
|
19
20
|
type SegmentedControlContextValue = {
|
|
20
21
|
/** @default primary */
|
|
@@ -35,6 +36,7 @@ export interface SegmentedControlProps<T extends string> {
|
|
|
35
36
|
items: SegmentedControlItemProps<T>[];
|
|
36
37
|
className?: string;
|
|
37
38
|
style?: React.CSSProperties;
|
|
39
|
+
variant?: SegmentedControlVariant;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
export type SegmentedControlItemProps<T extends string> = {
|
|
@@ -42,6 +44,7 @@ export type SegmentedControlItemProps<T extends string> = {
|
|
|
42
44
|
className?: string;
|
|
43
45
|
style?: React.CSSProperties;
|
|
44
46
|
tooltip?: ReactNode;
|
|
47
|
+
variant?: SegmentedControlVariant;
|
|
45
48
|
} & Pick<
|
|
46
49
|
SelectableMenuItem<T>,
|
|
47
50
|
"value" | "disabled" | "icon" | "role" | "title"
|
|
@@ -57,6 +60,7 @@ const SegmentedControlItem = forwardRef(function SegmentedControlItem<
|
|
|
57
60
|
disabled = false,
|
|
58
61
|
icon,
|
|
59
62
|
className,
|
|
63
|
+
variant = "default",
|
|
60
64
|
...props
|
|
61
65
|
}: SegmentedControlItemProps<T>,
|
|
62
66
|
forwardedRef: React.ForwardedRef<HTMLButtonElement>
|
|
@@ -69,27 +73,42 @@ const SegmentedControlItem = forwardRef(function SegmentedControlItem<
|
|
|
69
73
|
value={(value ?? "").toString()}
|
|
70
74
|
disabled={disabled}
|
|
71
75
|
className={cx(
|
|
72
|
-
"
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
? "
|
|
78
|
-
: colorScheme === "
|
|
79
|
-
? "
|
|
80
|
-
: "
|
|
76
|
+
"group",
|
|
77
|
+
"flex items-center justify-center",
|
|
78
|
+
"font-sans text-button relative flex-1 appearance-none text-segmented-control-item focus:outline-none transition-colors",
|
|
79
|
+
variant === "tabs" ? "font-semibold" : "font-normal",
|
|
80
|
+
variant === "tabs"
|
|
81
|
+
? ""
|
|
82
|
+
: colorScheme === "secondary"
|
|
83
|
+
? "focus:ring-2 focus:ring-secondary focus:ring-offset-1"
|
|
84
|
+
: "focus:ring-2 focus:ring-primary focus:ring-offset-1",
|
|
85
|
+
variant === "tabs"
|
|
86
|
+
? "aria-checked:text-primary"
|
|
87
|
+
: colorScheme === "secondary"
|
|
88
|
+
? "aria-checked:bg-secondary aria-checked:text-white"
|
|
89
|
+
: colorScheme === "primary"
|
|
90
|
+
? "aria-checked:bg-primary aria-checked:text-white"
|
|
91
|
+
: "aria-checked:bg-background aria-checked:text-text",
|
|
81
92
|
"focus:z-interactable",
|
|
82
|
-
"
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
"
|
|
86
|
-
|
|
93
|
+
variant === "default"
|
|
94
|
+
? "px-1 rounded aria-checked:shadow-segment"
|
|
95
|
+
: "px-1.5 border-y-2 border-y-transparent aria-checked:border-b-primary",
|
|
96
|
+
variant === "default" &&
|
|
97
|
+
"border-r border-divider last:border-r-0 aria-checked:border-r-transparent [&:has(+[aria-checked=true])]:border-r-transparent",
|
|
87
98
|
className
|
|
88
99
|
)}
|
|
89
100
|
{...props}
|
|
90
101
|
>
|
|
91
|
-
|
|
92
|
-
|
|
102
|
+
<span
|
|
103
|
+
className={cx(
|
|
104
|
+
"inline-flex whitespace-pre flex-nowrap gap-1.5 items-center justify-center align-middle text-center",
|
|
105
|
+
variant === "tabs" &&
|
|
106
|
+
"group-focus:ring-2 group-focus:ring-primary-pastel group-focus:ring-offset-2"
|
|
107
|
+
)}
|
|
108
|
+
>
|
|
109
|
+
{icon && renderIcon(icon)}
|
|
110
|
+
{title}
|
|
111
|
+
</span>
|
|
93
112
|
</ToggleGroupPrimitive.Item>
|
|
94
113
|
);
|
|
95
114
|
|
|
@@ -109,6 +128,7 @@ export const SegmentedControl = memoGeneric(function SegmentedControl<
|
|
|
109
128
|
colorScheme,
|
|
110
129
|
allowEmpty,
|
|
111
130
|
items,
|
|
131
|
+
variant = "default",
|
|
112
132
|
className,
|
|
113
133
|
style,
|
|
114
134
|
}: SegmentedControlProps<T>) {
|
|
@@ -131,7 +151,10 @@ export const SegmentedControl = memoGeneric(function SegmentedControl<
|
|
|
131
151
|
value={value}
|
|
132
152
|
onValueChange={handleValueChange}
|
|
133
153
|
className={cx(
|
|
134
|
-
`
|
|
154
|
+
`appearance-none relative outline-none`,
|
|
155
|
+
variant === "default" &&
|
|
156
|
+
"grid min-h-input-height bg-input-background rounded py-0.5 px-0.5",
|
|
157
|
+
variant === "tabs" && "flex gap-1.5 min-h-[33px]",
|
|
135
158
|
className
|
|
136
159
|
)}
|
|
137
160
|
style={{
|
|
@@ -140,7 +163,7 @@ export const SegmentedControl = memoGeneric(function SegmentedControl<
|
|
|
140
163
|
}}
|
|
141
164
|
>
|
|
142
165
|
{items.map((item, index) => (
|
|
143
|
-
<SegmentedControlItem key={index} {...item} />
|
|
166
|
+
<SegmentedControlItem key={index} {...item} variant={variant} />
|
|
144
167
|
))}
|
|
145
168
|
</ToggleGroupPrimitive.Root>
|
|
146
169
|
</SegmentedControlContext.Provider>
|
|
@@ -7,6 +7,7 @@ import { useSize } from "@noya-app/react-utils";
|
|
|
7
7
|
import * as PopperPrimitive from "@radix-ui/react-popper";
|
|
8
8
|
import type { MeasurableElement } from "@radix-ui/utils";
|
|
9
9
|
import React, { CSSProperties, memo, useMemo } from "react";
|
|
10
|
+
import { createPortal } from "react-dom";
|
|
10
11
|
|
|
11
12
|
// Create a Measurable from a Rect
|
|
12
13
|
const createVirtualElement = (rect: Rect): MeasurableElement => ({
|
|
@@ -26,12 +27,14 @@ const createVirtualElement = (rect: Rect): MeasurableElement => ({
|
|
|
26
27
|
type SelectionToolbarContainerProps = {
|
|
27
28
|
rect: Rect;
|
|
28
29
|
children: React.ReactNode;
|
|
30
|
+
portalContainer?: HTMLElement | null;
|
|
29
31
|
};
|
|
30
32
|
|
|
31
33
|
export const SelectionToolbarContainer = memo(
|
|
32
34
|
function SelectionToolbarContainer({
|
|
33
35
|
rect,
|
|
34
36
|
children,
|
|
37
|
+
portalContainer,
|
|
35
38
|
}: SelectionToolbarContainerProps) {
|
|
36
39
|
const portalScopeId = usePortalScopeId();
|
|
37
40
|
const containerRef = React.useRef<HTMLDivElement>(null);
|
|
@@ -45,31 +48,41 @@ export const SelectionToolbarContainer = memo(
|
|
|
45
48
|
[rect]
|
|
46
49
|
);
|
|
47
50
|
|
|
51
|
+
const popperContent = (
|
|
52
|
+
<PopperPrimitive.Content
|
|
53
|
+
{...portalScopeProps(portalScopeId)}
|
|
54
|
+
side="top"
|
|
55
|
+
sideOffset={10}
|
|
56
|
+
align="center"
|
|
57
|
+
avoidCollisions
|
|
58
|
+
collisionPadding={10}
|
|
59
|
+
contentEditable={false}
|
|
60
|
+
style={{
|
|
61
|
+
zIndex: 1000,
|
|
62
|
+
opacity: size ? 1 : 0,
|
|
63
|
+
transition: "opacity 0.2s ease-in-out",
|
|
64
|
+
["--n-input-background" as keyof CSSProperties]: "transparent",
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
<div
|
|
68
|
+
ref={containerRef}
|
|
69
|
+
className="flex gap-1 bg-popover-background rounded shadow-popover p-1"
|
|
70
|
+
onClick={(e) => e.stopPropagation()}
|
|
71
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
72
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
73
|
+
onWheel={(e) => e.stopPropagation()}
|
|
74
|
+
>
|
|
75
|
+
{children}
|
|
76
|
+
</div>
|
|
77
|
+
</PopperPrimitive.Content>
|
|
78
|
+
);
|
|
79
|
+
|
|
48
80
|
return (
|
|
49
81
|
<PopperPrimitive.Root>
|
|
50
82
|
<PopperPrimitive.Anchor virtualRef={virtualRef} />
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
}}
|
|
65
|
-
>
|
|
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>
|
|
83
|
+
{portalContainer
|
|
84
|
+
? createPortal(popperContent, portalContainer)
|
|
85
|
+
: popperContent}
|
|
73
86
|
</PopperPrimitive.Root>
|
|
74
87
|
);
|
|
75
88
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { DropdownChevronIcon } from "@noya-app/noya-icons";
|
|
2
1
|
import { useKeyboardShortcuts } from "@noya-app/noya-keymap";
|
|
3
2
|
import React from "react";
|
|
4
3
|
|
|
@@ -7,7 +6,6 @@ import { BaseToolbar } from "./BaseToolbar";
|
|
|
7
6
|
import { Button } from "./Button";
|
|
8
7
|
import { DividerVertical } from "./Divider";
|
|
9
8
|
import { DropdownMenu } from "./DropdownMenu";
|
|
10
|
-
import { renderIcon } from "./Icons";
|
|
11
9
|
import {
|
|
12
10
|
getKeyboardShortcutsForMenuItems,
|
|
13
11
|
isSelectableMenuItem,
|
|
@@ -15,7 +13,6 @@ import {
|
|
|
15
13
|
SelectableMenuItem,
|
|
16
14
|
SubMenuItem,
|
|
17
15
|
} from "./internal/Menu";
|
|
18
|
-
import { Spacer } from "./Spacer";
|
|
19
16
|
import { Tooltip } from "./Tooltip";
|
|
20
17
|
|
|
21
18
|
const iconButtonStyle = {
|
|
@@ -46,12 +43,13 @@ export const ToolbarMenuDropdown = memoGeneric(function ToolbarMenuDropdown<
|
|
|
46
43
|
}
|
|
47
44
|
}}
|
|
48
45
|
>
|
|
49
|
-
<Button
|
|
50
|
-
{item.
|
|
51
|
-
{item.
|
|
46
|
+
<Button
|
|
47
|
+
disabled={item.disabled}
|
|
48
|
+
active={item.checked || open}
|
|
49
|
+
icon={item.icon}
|
|
50
|
+
iconRight="DropdownChevronIcon"
|
|
51
|
+
>
|
|
52
52
|
{item.title}
|
|
53
|
-
<Spacer.Horizontal inline size={6} />
|
|
54
|
-
<DropdownChevronIcon />
|
|
55
53
|
</Button>
|
|
56
54
|
</DropdownMenu>
|
|
57
55
|
);
|
|
@@ -70,6 +68,7 @@ export const ToolbarMenuButton = memoGeneric(function ToolbarMenuButton<
|
|
|
70
68
|
<Button
|
|
71
69
|
disabled={item.disabled}
|
|
72
70
|
active={item.checked}
|
|
71
|
+
icon={item.icon}
|
|
73
72
|
style={item.icon && !item.title ? iconButtonStyle : undefined}
|
|
74
73
|
onClick={() => {
|
|
75
74
|
if (onSelectMenuItem && item.value) {
|
|
@@ -77,8 +76,6 @@ export const ToolbarMenuButton = memoGeneric(function ToolbarMenuButton<
|
|
|
77
76
|
}
|
|
78
77
|
}}
|
|
79
78
|
>
|
|
80
|
-
{item.icon && renderIcon(item.icon)}
|
|
81
|
-
{item.title && item.icon && <Spacer.Horizontal inline size={6} />}
|
|
82
79
|
{item.title}
|
|
83
80
|
</Button>
|
|
84
81
|
);
|
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
import { cssVars } from "../../theme";
|
|
2
|
-
|
|
3
|
-
import { ChatBubbleWithDotsIcon } from "@noya-app/noya-icons";
|
|
4
1
|
import React from "react";
|
|
5
2
|
import { INPUT_HEIGHT } from "../../theme";
|
|
6
|
-
import { colorFromString } from "../../utils/colorFromString";
|
|
7
3
|
import { Avatar, AvatarProps, AvatarStack } from "../Avatar";
|
|
8
4
|
import Button from "../Button";
|
|
9
|
-
import { Spacer } from "../Spacer";
|
|
10
5
|
import { Small } from "../Text";
|
|
11
6
|
import { Tooltip } from "../Tooltip";
|
|
12
7
|
|
|
@@ -25,14 +20,12 @@ export const UserAvatar = ({ userId, name, image }: AvatarProps) => (
|
|
|
25
20
|
|
|
26
21
|
type ConnectedUsersMenuLayoutProps = {
|
|
27
22
|
renderUsers: () => React.ReactNode;
|
|
28
|
-
currentUserId?: string;
|
|
29
23
|
launchAIAssistant?: () => void;
|
|
30
24
|
isAssistantOpen?: boolean;
|
|
31
25
|
};
|
|
32
26
|
|
|
33
27
|
export const ConnectedUsersMenuLayout = ({
|
|
34
28
|
renderUsers,
|
|
35
|
-
currentUserId,
|
|
36
29
|
launchAIAssistant,
|
|
37
30
|
isAssistantOpen,
|
|
38
31
|
}: ConnectedUsersMenuLayoutProps) => {
|
|
@@ -41,29 +34,13 @@ export const ConnectedUsersMenuLayout = ({
|
|
|
41
34
|
<AvatarStack size={INPUT_HEIGHT} className="mr-1">
|
|
42
35
|
{renderUsers()}
|
|
43
36
|
</AvatarStack>
|
|
44
|
-
|
|
45
|
-
{/* AI Assistant Avatar */}
|
|
46
37
|
{launchAIAssistant && (
|
|
47
|
-
<Tooltip
|
|
48
|
-
content={
|
|
49
|
-
<div className="flex flex-col">
|
|
50
|
-
<Small>AI Assistant</Small>
|
|
51
|
-
</div>
|
|
52
|
-
}
|
|
53
|
-
>
|
|
38
|
+
<Tooltip content="AI Assistant">
|
|
54
39
|
<Button
|
|
55
|
-
|
|
40
|
+
active={isAssistantOpen}
|
|
56
41
|
onClick={launchAIAssistant}
|
|
57
|
-
|
|
58
|
-
backgroundColor: isAssistantOpen
|
|
59
|
-
? colorFromString(currentUserId ?? "")
|
|
60
|
-
: undefined,
|
|
61
|
-
}}
|
|
42
|
+
icon="ChatBubbleWithDotsIcon"
|
|
62
43
|
>
|
|
63
|
-
<ChatBubbleWithDotsIcon
|
|
64
|
-
color={isAssistantOpen ? undefined : cssVars.colors.icon}
|
|
65
|
-
/>
|
|
66
|
-
<Spacer.Horizontal inline size={6} />
|
|
67
44
|
AI
|
|
68
45
|
</Button>
|
|
69
46
|
</Tooltip>
|
|
@@ -3,6 +3,7 @@ import * as React from "react";
|
|
|
3
3
|
import { ToastProvider } from "../components/Toast";
|
|
4
4
|
import { DialogProvider } from "./DialogContext";
|
|
5
5
|
import { FloatingWindowProvider } from "./FloatingWindowContext";
|
|
6
|
+
import { OpenPortalsProvider } from "./OpenPortalsContext";
|
|
6
7
|
|
|
7
8
|
export type DesignSystemConfigurationContextValue = {
|
|
8
9
|
platform: PlatformName;
|
|
@@ -39,11 +40,13 @@ export const DesignSystemConfigurationProvider = React.memo(
|
|
|
39
40
|
|
|
40
41
|
return (
|
|
41
42
|
<DesignSystemConfigurationContext.Provider value={contextValue}>
|
|
42
|
-
<
|
|
43
|
-
<
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
<OpenPortalsProvider>
|
|
44
|
+
<DialogProvider>
|
|
45
|
+
<ToastProvider>
|
|
46
|
+
<FloatingWindowProvider>{children}</FloatingWindowProvider>
|
|
47
|
+
</ToastProvider>
|
|
48
|
+
</DialogProvider>
|
|
49
|
+
</OpenPortalsProvider>
|
|
47
50
|
</DesignSystemConfigurationContext.Provider>
|
|
48
51
|
);
|
|
49
52
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
export type OpenPortalsContextValue = {
|
|
4
|
+
hasOpenPortals: boolean;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type OpenPortalControlsContextValue = {
|
|
8
|
+
addOpenPortal: (portal: string) => void;
|
|
9
|
+
removeOpenPortal: (portal: string) => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* This context is used to track the open portals in the design system.
|
|
14
|
+
*
|
|
15
|
+
* We currently use this so that we can cover up iframes when there are open portals.
|
|
16
|
+
* This lets us intercept click events that would otherwise be swallowed by the iframe.
|
|
17
|
+
*/
|
|
18
|
+
const OpenPortalsContext = React.createContext<OpenPortalsContextValue>({
|
|
19
|
+
hasOpenPortals: false,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const OpenPortalControlsContext =
|
|
23
|
+
React.createContext<OpenPortalControlsContextValue>({
|
|
24
|
+
addOpenPortal: () => {},
|
|
25
|
+
removeOpenPortal: () => {},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const OpenPortalsProvider = React.memo(function OpenPortalsProvider({
|
|
29
|
+
children,
|
|
30
|
+
}: {
|
|
31
|
+
children: React.ReactNode;
|
|
32
|
+
}) {
|
|
33
|
+
const [openPortals, setOpenPortals] = React.useState<string[]>([]);
|
|
34
|
+
|
|
35
|
+
const value = React.useMemo(
|
|
36
|
+
() => ({ hasOpenPortals: openPortals.length > 0 }),
|
|
37
|
+
[openPortals]
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const controlsValue = React.useMemo(
|
|
41
|
+
() => ({
|
|
42
|
+
addOpenPortal: (portal: string) => {
|
|
43
|
+
setOpenPortals((prev) => [...prev, portal]);
|
|
44
|
+
},
|
|
45
|
+
removeOpenPortal: (portal: string) => {
|
|
46
|
+
setOpenPortals((prev) => prev.filter((p) => p !== portal));
|
|
47
|
+
},
|
|
48
|
+
}),
|
|
49
|
+
[]
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<OpenPortalsContext.Provider value={value}>
|
|
54
|
+
<OpenPortalControlsContext.Provider value={controlsValue}>
|
|
55
|
+
{children}
|
|
56
|
+
</OpenPortalControlsContext.Provider>
|
|
57
|
+
</OpenPortalsContext.Provider>
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export function useHasOpenPortals(): boolean {
|
|
62
|
+
return React.useContext(OpenPortalsContext).hasOpenPortals;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function useOpenPortalsControls(): OpenPortalControlsContextValue {
|
|
66
|
+
return React.useContext(OpenPortalControlsContext);
|
|
67
|
+
}
|