@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noya-app/noya-designsystem",
3
- "version": "0.1.54",
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.8",
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/Sortable";
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
- fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2
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-base font-normal leading-[1.4] text-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
  </>
@@ -235,7 +235,9 @@ export const Grid = memoGeneric(
235
235
  <span
236
236
  {...props}
237
237
  className={cx(
238
- isSelected && "text-selected-list-item-text",
238
+ isSelected
239
+ ? "text-selected-list-item-text"
240
+ : "text-text",
239
241
  "truncate select-none"
240
242
  )}
241
243
  />
@@ -194,6 +194,7 @@ const GridViewItem = forwardRefGeneric(function GridViewItem<
194
194
  <>
195
195
  <Spacer.Vertical size={8} />
196
196
  <ItemTitle>{title || " "}</ItemTitle>
197
+ <Spacer.Vertical size={2} />
197
198
  <ItemDescription>{subtitle || " "}</ItemDescription>
198
199
  </>
199
200
  )}
@@ -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 text-heading5"
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 ref={endRef} className={cx(endStyles, label && end && "gap-1.5")}>
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
- Sortable,
39
- type SortableItemContextProps,
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={open} onOpenChange={onOpenChange}>
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={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
- "font-sans text-button font-normal relative flex-1 appearance-none text-segmented-control-item rounded inline-flex gap-1.5 items-center justify-center align-middle text-center focus:outline-none transition-colors",
73
- colorScheme === "secondary"
74
- ? "focus:ring-2 focus:ring-secondary focus:ring-offset-1"
75
- : "focus:ring-2 focus:ring-primary focus:ring-offset-1",
76
- colorScheme === "secondary"
77
- ? "aria-checked:bg-secondary aria-checked:text-white"
78
- : colorScheme === "primary"
79
- ? "aria-checked:bg-primary aria-checked:text-white"
80
- : "aria-checked:bg-background aria-checked:text-text",
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
- "aria-checked:shadow-segment",
83
- "whitespace-pre px-1",
84
- "border-r border-divider last:border-r-0",
85
- "aria-checked:border-r-transparent",
86
- "[&:has(+[aria-checked=true])]:border-r-transparent",
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
- {icon && renderIcon(icon)}
92
- {title}
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
- `grid appearance-none relative outline-none min-h-input-height rounded bg-input-background py-0.5 px-0.5`,
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
- <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
- }}
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
  }