@noya-app/noya-designsystem 0.1.54 → 0.1.56
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 +16 -0
- package/dist/index.css +1 -1
- package/dist/index.d.mts +136 -44
- package/dist/index.d.ts +136 -44
- package/dist/index.js +1240 -802
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1484 -1054
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/validateDropIndicator.test.ts +35 -9
- 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/ListView.tsx +5 -8
- 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 +22 -12
- package/src/components/connected-users-menu/ConnectedUsersMenuLayout.tsx +3 -26
- package/src/components/sorting/DragRegistration.tsx +110 -0
- package/src/components/sorting/SharedDragProvider.tsx +307 -0
- package/src/components/sorting/Sortable.tsx +404 -0
- package/src/components/sorting/createSharedDrag.tsx +46 -0
- package/src/components/sorting/sorting.ts +180 -0
- package/src/contexts/DesignSystemConfiguration.tsx +8 -5
- package/src/contexts/OpenPortalsContext.tsx +67 -0
- package/src/index.css +2 -2
- package/src/index.tsx +5 -2
- package/src/utils/moveTreeItem.ts +4 -2
- package/src/components/Sortable.tsx +0 -468
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.56",
|
|
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.10",
|
|
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",
|
|
@@ -2,46 +2,46 @@ import { describe, expect, it } from "bun:test";
|
|
|
2
2
|
import {
|
|
3
3
|
defaultAcceptsDrop,
|
|
4
4
|
validateDropIndicator,
|
|
5
|
-
} from "../components/
|
|
5
|
+
} from "../components/sorting/sorting";
|
|
6
6
|
|
|
7
7
|
describe("defaultAcceptsDrop", () => {
|
|
8
8
|
it("should return false if the active item is the same as the over item", () => {
|
|
9
|
-
const result = defaultAcceptsDrop(0, 0, "inside");
|
|
9
|
+
const result = defaultAcceptsDrop(0, 0, "inside", "", "");
|
|
10
10
|
expect(result).toBe(false);
|
|
11
11
|
});
|
|
12
12
|
|
|
13
13
|
it("should return false for 'inside' position", () => {
|
|
14
|
-
const result = defaultAcceptsDrop(0, 1, "inside");
|
|
14
|
+
const result = defaultAcceptsDrop(0, 1, "inside", "", "");
|
|
15
15
|
expect(result).toBe(false);
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
it("should return false when dropping above the same item", () => {
|
|
19
|
-
const result = defaultAcceptsDrop(0, 0, "above");
|
|
19
|
+
const result = defaultAcceptsDrop(0, 0, "above", "", "");
|
|
20
20
|
expect(result).toBe(false);
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
it("should return false when dropping below the same item", () => {
|
|
24
|
-
const result = defaultAcceptsDrop(0, 0, "below");
|
|
24
|
+
const result = defaultAcceptsDrop(0, 0, "below", "", "");
|
|
25
25
|
expect(result).toBe(false);
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
it("should return false when dropping above the item below the source", () => {
|
|
29
|
-
const result = defaultAcceptsDrop(0, 1, "above");
|
|
29
|
+
const result = defaultAcceptsDrop(0, 1, "above", "", "");
|
|
30
30
|
expect(result).toBe(false);
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
it("should return false when dropping below the item above the source", () => {
|
|
34
|
-
const result = defaultAcceptsDrop(1, 0, "below");
|
|
34
|
+
const result = defaultAcceptsDrop(1, 0, "below", "", "");
|
|
35
35
|
expect(result).toBe(false);
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
it("should return true when dropping above a valid target", () => {
|
|
39
|
-
const result = defaultAcceptsDrop(0, 2, "above");
|
|
39
|
+
const result = defaultAcceptsDrop(0, 2, "above", "", "");
|
|
40
40
|
expect(result).toBe(true);
|
|
41
41
|
});
|
|
42
42
|
|
|
43
43
|
it("should return true when dropping below a valid target", () => {
|
|
44
|
-
const result = defaultAcceptsDrop(2, 0, "below");
|
|
44
|
+
const result = defaultAcceptsDrop(2, 0, "below", "", "");
|
|
45
45
|
expect(result).toBe(true);
|
|
46
46
|
});
|
|
47
47
|
});
|
|
@@ -97,6 +97,8 @@ describe("validateDropIndicator", () => {
|
|
|
97
97
|
offsetStart: 50, // offsetStart in middle third
|
|
98
98
|
elementStart: 0, // elementStart
|
|
99
99
|
elementSize: 100, // elementSize
|
|
100
|
+
sourceListId: "",
|
|
101
|
+
targetListId: "",
|
|
100
102
|
});
|
|
101
103
|
expect(result).toBe("inside");
|
|
102
104
|
});
|
|
@@ -110,6 +112,8 @@ describe("validateDropIndicator", () => {
|
|
|
110
112
|
offsetStart: 20, // offsetStart in top third
|
|
111
113
|
elementStart: 0, // elementStart
|
|
112
114
|
elementSize: 100, // elementSize
|
|
115
|
+
sourceListId: "",
|
|
116
|
+
targetListId: "",
|
|
113
117
|
});
|
|
114
118
|
expect(result).toBe("above");
|
|
115
119
|
});
|
|
@@ -123,6 +127,8 @@ describe("validateDropIndicator", () => {
|
|
|
123
127
|
offsetStart: 80, // offsetStart in bottom third
|
|
124
128
|
elementStart: 0, // elementStart
|
|
125
129
|
elementSize: 100, // elementSize
|
|
130
|
+
sourceListId: "",
|
|
131
|
+
targetListId: "",
|
|
126
132
|
});
|
|
127
133
|
expect(result).toBe("inside");
|
|
128
134
|
});
|
|
@@ -138,6 +144,8 @@ describe("validateDropIndicator", () => {
|
|
|
138
144
|
offsetStart: 50, // offsetStart in middle third
|
|
139
145
|
elementStart: 0, // elementStart
|
|
140
146
|
elementSize: 100, // elementSize
|
|
147
|
+
sourceListId: "",
|
|
148
|
+
targetListId: "",
|
|
141
149
|
});
|
|
142
150
|
expect(result).toBe("inside");
|
|
143
151
|
});
|
|
@@ -151,6 +159,8 @@ describe("validateDropIndicator", () => {
|
|
|
151
159
|
offsetStart: 20, // offsetStart in top third
|
|
152
160
|
elementStart: 0, // elementStart
|
|
153
161
|
elementSize: 100, // elementSize
|
|
162
|
+
sourceListId: "",
|
|
163
|
+
targetListId: "",
|
|
154
164
|
});
|
|
155
165
|
expect(result).toBe("above");
|
|
156
166
|
});
|
|
@@ -164,6 +174,8 @@ describe("validateDropIndicator", () => {
|
|
|
164
174
|
offsetStart: 80, // offsetStart in bottom third
|
|
165
175
|
elementStart: 0, // elementStart
|
|
166
176
|
elementSize: 100, // elementSize
|
|
177
|
+
sourceListId: "",
|
|
178
|
+
targetListId: "",
|
|
167
179
|
});
|
|
168
180
|
expect(result).toBe("below");
|
|
169
181
|
});
|
|
@@ -177,6 +189,8 @@ describe("validateDropIndicator", () => {
|
|
|
177
189
|
offsetStart: 120, // offsetStart in middle third relative to elementStart
|
|
178
190
|
elementStart: 100, // elementStart
|
|
179
191
|
elementSize: 60, // elementSize
|
|
192
|
+
sourceListId: "",
|
|
193
|
+
targetListId: "",
|
|
180
194
|
});
|
|
181
195
|
expect(result).toBe("inside");
|
|
182
196
|
});
|
|
@@ -191,6 +205,8 @@ describe("validateDropIndicator", () => {
|
|
|
191
205
|
offsetStart: 50, // offsetStart in middle third
|
|
192
206
|
elementStart: 0, // elementStart
|
|
193
207
|
elementSize: 100, // elementSize
|
|
208
|
+
sourceListId: "",
|
|
209
|
+
targetListId: "",
|
|
194
210
|
});
|
|
195
211
|
expect(result).toBe("inside");
|
|
196
212
|
});
|
|
@@ -204,6 +220,8 @@ describe("validateDropIndicator", () => {
|
|
|
204
220
|
offsetStart: 20, // offsetStart in top half
|
|
205
221
|
elementStart: 0, // elementStart
|
|
206
222
|
elementSize: 100, // elementSize
|
|
223
|
+
sourceListId: "",
|
|
224
|
+
targetListId: "",
|
|
207
225
|
});
|
|
208
226
|
expect(result).toBe("above");
|
|
209
227
|
});
|
|
@@ -217,6 +235,8 @@ describe("validateDropIndicator", () => {
|
|
|
217
235
|
offsetStart: 70, // offsetStart in bottom half
|
|
218
236
|
elementStart: 0, // elementStart
|
|
219
237
|
elementSize: 100, // elementSize
|
|
238
|
+
sourceListId: "",
|
|
239
|
+
targetListId: "",
|
|
220
240
|
});
|
|
221
241
|
expect(result).toBe("below");
|
|
222
242
|
});
|
|
@@ -230,6 +250,8 @@ describe("validateDropIndicator", () => {
|
|
|
230
250
|
offsetStart: 20, // offsetStart in top half
|
|
231
251
|
elementStart: 0, // elementStart
|
|
232
252
|
elementSize: 100, // elementSize
|
|
253
|
+
sourceListId: "",
|
|
254
|
+
targetListId: "",
|
|
233
255
|
});
|
|
234
256
|
expect(result).toBe("inside");
|
|
235
257
|
});
|
|
@@ -244,6 +266,8 @@ describe("validateDropIndicator", () => {
|
|
|
244
266
|
offsetStart: 50,
|
|
245
267
|
elementStart: 0,
|
|
246
268
|
elementSize: 100,
|
|
269
|
+
sourceListId: "",
|
|
270
|
+
targetListId: "",
|
|
247
271
|
});
|
|
248
272
|
expect(result).toBeUndefined();
|
|
249
273
|
});
|
|
@@ -257,6 +281,8 @@ describe("validateDropIndicator", () => {
|
|
|
257
281
|
offsetStart: 50,
|
|
258
282
|
elementStart: 0,
|
|
259
283
|
elementSize: 100,
|
|
284
|
+
sourceListId: "",
|
|
285
|
+
targetListId: "",
|
|
260
286
|
});
|
|
261
287
|
expect(result).toBeUndefined();
|
|
262
288
|
});
|
|
@@ -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}
|
|
@@ -31,13 +31,14 @@ import { ContextMenu } from "./ContextMenu";
|
|
|
31
31
|
import { InputField } from "./InputField";
|
|
32
32
|
import { MenuItem } from "./internal/Menu";
|
|
33
33
|
import { ScrollArea } from "./ScrollArea";
|
|
34
|
+
import { Sortable } from "./sorting/Sortable";
|
|
34
35
|
import {
|
|
35
36
|
DropValidator,
|
|
37
|
+
MoveDragItemHandler,
|
|
36
38
|
normalizeListTargetIndex,
|
|
37
39
|
RelativeDropPosition,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
} from "./Sortable";
|
|
40
|
+
SortableItemContextProps,
|
|
41
|
+
} from "./sorting/sorting";
|
|
41
42
|
import { Spacer } from "./Spacer";
|
|
42
43
|
|
|
43
44
|
export type ListRowMarginType = "none" | "top" | "bottom" | "vertical";
|
|
@@ -776,11 +777,7 @@ export type ListViewRootProps = {
|
|
|
776
777
|
onPress?: () => void;
|
|
777
778
|
scrollable?: boolean;
|
|
778
779
|
expandable?: boolean;
|
|
779
|
-
onMoveItem?:
|
|
780
|
-
sourceIndex: number,
|
|
781
|
-
targetIndex: number,
|
|
782
|
-
position: RelativeDropPosition
|
|
783
|
-
) => void;
|
|
780
|
+
onMoveItem?: MoveDragItemHandler;
|
|
784
781
|
indentation?: number;
|
|
785
782
|
acceptsDrop?: DropValidator;
|
|
786
783
|
pressEventName?: PressEventName;
|
|
@@ -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
|
}
|