@noya-app/noya-designsystem 0.1.51 → 0.1.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/.turbo/turbo-build.log +10 -13
  2. package/CHANGELOG.md +17 -0
  3. package/dist/index.css +1 -1
  4. package/dist/index.d.mts +307 -12
  5. package/dist/index.d.ts +307 -12
  6. package/dist/index.js +3408 -647
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +3660 -908
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +12 -9
  11. package/postcss.config.js +7 -0
  12. package/src/components/ActionMenu.tsx +8 -3
  13. package/src/components/Banner.tsx +2 -2
  14. package/src/components/BaseToolbar.tsx +6 -2
  15. package/src/components/Breadcrumbs.tsx +32 -2
  16. package/src/components/Button.tsx +30 -19
  17. package/src/components/Chip.tsx +3 -0
  18. package/src/components/ColorSwatch.tsx +42 -0
  19. package/src/components/ColorSwatchControl.tsx +90 -0
  20. package/src/components/ContextMenu.tsx +10 -2
  21. package/src/components/Dialog.tsx +1 -1
  22. package/src/components/DraggableMenuButton.tsx +17 -30
  23. package/src/components/DropdownMenu.tsx +7 -0
  24. package/src/components/FillInputField.tsx +1 -1
  25. package/src/components/Grid.tsx +11 -4
  26. package/src/components/GridView.tsx +1 -1
  27. package/src/components/LabeledField.tsx +4 -1
  28. package/src/components/List.tsx +12 -7
  29. package/src/components/ListView.tsx +7 -4
  30. package/src/components/MediaThumbnail.tsx +44 -10
  31. package/src/components/Popover.tsx +8 -1
  32. package/src/components/SegmentedControl.tsx +1 -1
  33. package/src/components/SelectMenu.tsx +7 -0
  34. package/src/components/SelectionToolbar.tsx +76 -0
  35. package/src/components/Switch.tsx +1 -1
  36. package/src/components/Text.tsx +2 -2
  37. package/src/components/Toast.tsx +13 -7
  38. package/src/components/Toolbar.tsx +78 -37
  39. package/src/components/Tooltip.tsx +11 -2
  40. package/src/components/catppuccin/fileIcons.ts +2430 -0
  41. package/src/components/connected-users-menu/ConnectedUsersMenuLayout.tsx +3 -1
  42. package/src/components/internal/Menu.tsx +6 -5
  43. package/src/components/internal/MenuViewport.tsx +11 -1
  44. package/src/components/pipeline/PipelineResultLayout.tsx +18 -0
  45. package/src/contexts/DialogContext.tsx +20 -5
  46. package/src/contexts/PortalScopeContext.tsx +48 -0
  47. package/src/index.css +101 -39
  48. package/src/index.tsx +4 -0
  49. package/src/theme/index.ts +6 -1
  50. package/src/theme/themeUtils.ts +10 -3
  51. package/src/utils/classNames.ts +3 -2
  52. package/src/utils/moveTreeItem.ts +4 -4
  53. package/tsup.config.ts +1 -1
@@ -29,11 +29,13 @@ export const LabeledField = memo(function LabeledField({
29
29
  minWidth = 100,
30
30
  className,
31
31
  style,
32
+ labelStyle: labelStyleProp,
32
33
  ...props
33
34
  }: {
34
35
  children: React.ReactNode;
35
36
  label: React.ReactNode;
36
37
  labelPosition?: LabelPosition;
38
+ labelStyle?: React.CSSProperties;
37
39
  /** Used for both the `id` of the field and the `htmlFor` of the label, unless `id` and `htmlFor` are explicitly provided */
38
40
  fieldId?: string;
39
41
  className?: string;
@@ -79,8 +81,9 @@ export const LabeledField = memo(function LabeledField({
79
81
  const labelStyle = useMemo(
80
82
  () => ({
81
83
  minWidth: `calc(${labelWidth}px - ${indentLevel * 8}px)`,
84
+ ...labelStyleProp,
82
85
  }),
83
- [labelWidth, indentLevel]
86
+ [labelWidth, indentLevel, labelStyleProp]
84
87
  );
85
88
 
86
89
  return (
@@ -17,7 +17,7 @@ import { ListView, ListViewItemInfo } from "./ListView";
17
17
  import { TreeView } from "./TreeView";
18
18
 
19
19
  const cssVarOverrides = {
20
- "--n-icon-selected": cssVars.colors.primary,
20
+ "--n-icon-selected": cssVars.colors.selectedListItemText,
21
21
  };
22
22
 
23
23
  const emptyArray: string[] = [];
@@ -117,7 +117,7 @@ export const List = memoGeneric(
117
117
  variant="bare"
118
118
  className={cx(
119
119
  className,
120
- isDropTargetActive && "bg-primary-pastel",
120
+ isDropTargetActive && "bg-selected-list-item-background",
121
121
  scrollable && "basis-0 min-h-0"
122
122
  )}
123
123
  containerRef={fileDropTargetRef}
@@ -174,6 +174,14 @@ export const List = memoGeneric(
174
174
  selected={isSelected}
175
175
  onOpenChange={onOpenChange}
176
176
  variant="bare"
177
+ style={{
178
+ backgroundColor: isSelected
179
+ ? cssVars.colors.selectedListItemBackground
180
+ : "transparent",
181
+ color: isSelected
182
+ ? cssVars.colors.selectedListItemText
183
+ : undefined,
184
+ }}
177
185
  />
178
186
  ));
179
187
 
@@ -193,9 +201,6 @@ export const List = memoGeneric(
193
201
  aria-label={getName(item)}
194
202
  aria-selected={isSelected}
195
203
  className={cx("cursor-pointer rounded px-1", pyMap[size])}
196
- backgroundColor={
197
- isSelected ? cssVars.colors.primaryPastel : undefined
198
- }
199
204
  testRelativeDropPosition={
200
205
  testShowDropIndicatorId === id ? "inside" : undefined
201
206
  }
@@ -245,7 +250,7 @@ export const List = memoGeneric(
245
250
  className={cx(
246
251
  "overflow-hidden flex-none flex items-center justify-center",
247
252
  thumbnailSize === "auto" && thumbnailSizeMap[size],
248
- isSelected && "text-primary",
253
+ isSelected && "text-selected-list-item-text",
249
254
  size === "small" ? "rounded-sm" : "rounded"
250
255
  )}
251
256
  tabIndex={-1}
@@ -274,7 +279,7 @@ export const List = memoGeneric(
274
279
  <ViewComponent.RowTitle
275
280
  className={cx(
276
281
  fontStyleMap[size],
277
- isSelected && "text-primary"
282
+ isSelected && "text-selected-list-item-text"
278
283
  )}
279
284
  >
280
285
  {getName(item)}
@@ -232,7 +232,7 @@ const RowContainer = forwardRef<
232
232
  : "font-sans text-heading5 font-normal",
233
233
  !$isSectionHeader &&
234
234
  $selected &&
235
- "text-white active:bg-primary-light",
235
+ "text-selected-list-item-text active:bg-selected-list-item-background",
236
236
  !$isSectionHeader && $disabled && "text-text-disabled",
237
237
  $clickable ? "cursor-pointer" : "cursor-default",
238
238
  $isSectionHeader && "bg-listview-raised-background",
@@ -254,7 +254,10 @@ const RowContainer = forwardRef<
254
254
  }),
255
255
  ...($selected &&
256
256
  !$isSectionHeader && {
257
- backgroundColor: cssVars.colors[$colorScheme],
257
+ backgroundColor:
258
+ $colorScheme === "primary"
259
+ ? cssVars.colors["selectedListItemBackground"]
260
+ : cssVars.colors[$colorScheme],
258
261
  }),
259
262
  ...($selected &&
260
263
  !$isSectionHeader &&
@@ -279,7 +282,7 @@ const RowContainer = forwardRef<
279
282
  backgroundColor: $selected
280
283
  ? $colorScheme === "secondary"
281
284
  ? cssVars.colors.secondaryLight
282
- : cssVars.colors.primaryLight
285
+ : cssVars.colors.selectedListItemBackground
283
286
  : cssVars.colors.activeBackground,
284
287
  },
285
288
  }),
@@ -358,7 +361,7 @@ const ListViewDragIndicatorElement = forwardRef<
358
361
  backgroundColor:
359
362
  $colorScheme === "secondary"
360
363
  ? cssVars.colors.secondary
361
- : cssVars.colors.primary,
364
+ : cssVars.colors.selectedListItemBackground,
362
365
  border: `2px solid white`,
363
366
  boxShadow: "0 0 2px rgba(0,0,0,0.5)",
364
367
  }),
@@ -1,9 +1,15 @@
1
- import { ImageIcon, SpeakerLoudIcon, VideoIcon } from "@noya-app/noya-icons";
1
+ import {
2
+ FileIcon,
3
+ ImageIcon,
4
+ SpeakerLoudIcon,
5
+ VideoIcon,
6
+ } from "@noya-app/noya-icons";
2
7
  import React, { memo, useMemo } from "react";
3
8
  import { cssVars } from "../theme";
4
9
  import { cx } from "../utils/classNames";
5
10
  import { CollectionItemSize } from "./Collection";
6
11
  import { IconName, Icons } from "./Icons";
12
+ import { fileExtensions, fileNames } from "./catppuccin/fileIcons";
7
13
 
8
14
  type ColorScheme = "primary" | "secondary" | "icon" | "warning";
9
15
 
@@ -31,6 +37,8 @@ type MediaThumbnailProps = {
31
37
  */
32
38
  iconName?: IconName;
33
39
  size?: CollectionItemSize;
40
+
41
+ fileName?: string;
34
42
  };
35
43
 
36
44
  export const getThumbnailColors = (
@@ -39,8 +47,8 @@ export const getThumbnailColors = (
39
47
  ) => {
40
48
  if (selected) {
41
49
  return {
42
- icon: cssVars.colors.primary,
43
- background: cssVars.colors.primaryPastel,
50
+ icon: cssVars.colors.selectedListItemText,
51
+ background: cssVars.colors.selectedListItemIconBackground,
44
52
  };
45
53
  }
46
54
 
@@ -75,8 +83,9 @@ export const MediaThumbnail = memo(function MediaThumbnail({
75
83
  className,
76
84
  colorScheme = "icon",
77
85
  url,
78
- iconName = "FileIcon",
86
+ iconName,
79
87
  size = "medium",
88
+ fileName,
80
89
  }: MediaThumbnailProps) {
81
90
  const { icon: iconColor, background: backgroundColor } = getThumbnailColors(
82
91
  colorScheme,
@@ -95,6 +104,7 @@ export const MediaThumbnail = memo(function MediaThumbnail({
95
104
  switch (contentType) {
96
105
  case "image":
97
106
  if (!url) return <ImageIcon color={iconColor} style={iconStyles} />;
107
+
98
108
  return (
99
109
  <img
100
110
  src={url}
@@ -103,18 +113,32 @@ export const MediaThumbnail = memo(function MediaThumbnail({
103
113
  tabIndex={-1}
104
114
  />
105
115
  );
106
-
107
116
  case "video":
108
117
  return <VideoIcon color={iconColor} style={iconStyles} />;
109
-
110
118
  case "audio":
111
119
  return <SpeakerLoudIcon color={iconColor} style={iconStyles} />;
112
-
113
120
  default:
114
- const Icon = Icons[iconName];
115
- return <Icon color={iconColor} style={iconStyles} />;
121
+ if (iconName) {
122
+ const Icon = Icons[iconName];
123
+ return <Icon color={iconColor} style={iconStyles} />;
124
+ }
125
+
126
+ if (fileName) {
127
+ const fileIcon = findFileIcon(fileName);
128
+ if (fileIcon) {
129
+ return (
130
+ <img
131
+ src={`https://api.iconify.design/catppuccin/${fileIcon}.svg?height=none&box=1`}
132
+ alt=""
133
+ className="w-4 h-4"
134
+ />
135
+ );
136
+ }
137
+ }
138
+
139
+ return <FileIcon color={iconColor} style={iconStyles} />;
116
140
  }
117
- }, [contentType, url, iconColor, iconStyles, iconName]);
141
+ }, [contentType, url, iconColor, iconStyles, iconName, fileName]);
118
142
 
119
143
  return (
120
144
  <div
@@ -137,3 +161,13 @@ const iconSizeMap: Record<CollectionItemSize, number | string> = {
137
161
  large: "max(15px, 20%)",
138
162
  xl: "max(15px, 20%)",
139
163
  };
164
+
165
+ function findFileIcon(fileName: string) {
166
+ const fileIcon = fileNames[fileName];
167
+ if (fileIcon) return fileIcon;
168
+ const fileExtensionIndex = fileName.lastIndexOf(".");
169
+ if (fileExtensionIndex === -1) return undefined;
170
+ const fileExtension = fileName.slice(fileExtensionIndex + 1);
171
+ const fileExtensionIcon = fileExtensions[fileExtension];
172
+ return fileExtensionIcon;
173
+ }
@@ -1,5 +1,9 @@
1
1
  import * as PopoverPrimitive from "@radix-ui/react-popover";
2
2
  import React, { ComponentProps } from "react";
3
+ import {
4
+ portalScopeProps,
5
+ usePortalScopeId,
6
+ } from "../contexts/PortalScopeContext";
3
7
  import { cx } from "../utils/classNames";
4
8
  import { IconButton } from "./IconButton";
5
9
 
@@ -33,7 +37,7 @@ interface Props
33
37
  }
34
38
 
35
39
  export const popoverStyle = {
36
- base: "rounded font-[14px] bg-popover-background shadow-[0_2px_4px_rgba(0,0,0,0.2),0_0_12px_rgba(0,0,0,0.1)] max-h-[600px] overflow-y-auto text-text-muted z-[1000]",
40
+ base: "rounded font-[14px] bg-popover-background shadow-popover max-h-[600px] overflow-y-auto text-text-muted z-[1000]",
37
41
  large: "w-[680px]",
38
42
  normal: "w-[240px]",
39
43
  };
@@ -56,11 +60,14 @@ export function Popover({
56
60
  onClickClose,
57
61
  className,
58
62
  }: Props) {
63
+ const portalScopeId = usePortalScopeId();
64
+
59
65
  return (
60
66
  <PopoverPrimitive.Root open={open} onOpenChange={onOpenChange}>
61
67
  <PopoverPrimitive.Trigger asChild>{trigger}</PopoverPrimitive.Trigger>
62
68
  <PopoverPrimitive.Portal>
63
69
  <PopoverPrimitive.Content
70
+ {...portalScopeProps(portalScopeId)}
64
71
  className={cx(
65
72
  popoverStyle.base,
66
73
  variant === "large" && popoverStyle.large,
@@ -80,7 +80,7 @@ const SegmentedControlItem = forwardRef(function SegmentedControlItem<
80
80
  ? "aria-checked:bg-primary aria-checked:text-white"
81
81
  : "aria-checked:bg-background aria-checked:text-text",
82
82
  "focus:z-interactable",
83
- "aria-checked:shadow-[0_1px_1px_rgba(0,0,0,0.1)]",
83
+ "aria-checked:shadow-segment",
84
84
  "whitespace-pre",
85
85
  className
86
86
  )}
@@ -15,6 +15,10 @@ import React, {
15
15
  import { useLabel, useLabelPosition } from "../hooks/useLabel";
16
16
  import { cx } from "../utils/classNames";
17
17
 
18
+ import {
19
+ portalScopeProps,
20
+ usePortalScopeId,
21
+ } from "../contexts/PortalScopeContext";
18
22
  import { getInsetEndStyles } from "../utils/inputs";
19
23
  import { Button } from "./Button";
20
24
  import { renderIcon } from "./Icons";
@@ -148,6 +152,8 @@ export const SelectMenu = memoGeneric(function SelectMenu<
148
152
  contentStyle: contentStyleProp,
149
153
  ...props
150
154
  }: Props<T>) {
155
+ const portalScopeId = usePortalScopeId();
156
+
151
157
  const selectedItem = items.find(
152
158
  (item): item is SelectableMenuItem<T> =>
153
159
  isSelectableMenuItem(item) && item.value === value
@@ -213,6 +219,7 @@ export const SelectMenu = memoGeneric(function SelectMenu<
213
219
  />
214
220
  <Select.Portal>
215
221
  <Select.Content
222
+ {...portalScopeProps(portalScopeId)}
216
223
  className={styles.contentStyle}
217
224
  position="popper"
218
225
  sideOffset={6}
@@ -0,0 +1,76 @@
1
+ import {
2
+ portalScopeProps,
3
+ usePortalScopeId,
4
+ } from "@noya-app/noya-designsystem";
5
+ import { Rect } from "@noya-app/noya-geometry";
6
+ import { useSize } from "@noya-app/react-utils";
7
+ import * as PopperPrimitive from "@radix-ui/react-popper";
8
+ import type { MeasurableElement } from "@radix-ui/utils";
9
+ import React, { CSSProperties, memo, useMemo } from "react";
10
+
11
+ // Create a Measurable from a Rect
12
+ const createVirtualElement = (rect: Rect): MeasurableElement => ({
13
+ getBoundingClientRect: () => ({
14
+ width: rect.width,
15
+ height: rect.height,
16
+ x: rect.x,
17
+ y: rect.y,
18
+ top: rect.y,
19
+ right: rect.x + rect.width,
20
+ bottom: rect.y + rect.height,
21
+ left: rect.x,
22
+ toJSON: () => null,
23
+ }),
24
+ });
25
+
26
+ type SelectionToolbarContainerProps = {
27
+ rect: Rect;
28
+ children: React.ReactNode;
29
+ };
30
+
31
+ export const SelectionToolbarContainer = memo(
32
+ function SelectionToolbarContainer({
33
+ rect,
34
+ children,
35
+ }: SelectionToolbarContainerProps) {
36
+ const portalScopeId = usePortalScopeId();
37
+ const containerRef = React.useRef<HTMLDivElement>(null);
38
+ const size = useSize(containerRef, "width");
39
+
40
+ // Create a virtual reference element from the rect
41
+ const virtualRef = useMemo(
42
+ () => ({
43
+ current: createVirtualElement(rect),
44
+ }),
45
+ [rect]
46
+ );
47
+
48
+ return (
49
+ <PopperPrimitive.Root>
50
+ <PopperPrimitive.Anchor virtualRef={virtualRef} />
51
+ <PopperPrimitive.Content
52
+ {...portalScopeProps(portalScopeId)}
53
+ side="top"
54
+ sideOffset={10}
55
+ align="center"
56
+ avoidCollisions
57
+ collisionPadding={10}
58
+ contentEditable={false}
59
+ style={{
60
+ zIndex: 1000,
61
+ opacity: size ? 1 : 0,
62
+ transition: "opacity 0.2s ease-in-out",
63
+ ["--n-input-background" as keyof CSSProperties]: "transparent",
64
+ }}
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>
73
+ </PopperPrimitive.Root>
74
+ );
75
+ }
76
+ );
@@ -38,7 +38,7 @@ export const Switch = function Switch({
38
38
  onChange(newValue);
39
39
  }}
40
40
  className={cx(
41
- "all-unset w-8 h-[19px] bg-active-background rounded-full relative cursor-pointer ring-offset-background [webkit-tap-highlight-color:rgba(0,0,0,0)] focus:ring-2 focus:ring-primary focus:ring-offset-[1px] outline-none data-[state=checked]:bg-primary transition-all",
41
+ "all-unset w-8 h-[19px] bg-active-background rounded-full relative cursor-pointer ring-offset-background [-webkit-tap-highlight-color:rgba(0,0,0,0)] focus:ring-2 focus:ring-primary focus:ring-offset-[1px] outline-none data-[state=checked]:bg-primary transition-all",
42
42
  colorScheme === "secondary" && "data-[state=checked]:bg-secondary",
43
43
  "focus:z-interactable",
44
44
  className
@@ -33,8 +33,8 @@ const elements: Record<Variant, keyof ReactHTML> = {
33
33
  };
34
34
 
35
35
  export const textStyles: Record<Variant, string> = {
36
- title: "font-sans text-title font-bold sm:text-[36px]",
37
- subtitle: "font-sans text-subtitle font-medium sm:text-[18px]",
36
+ title: "font-sans text-title font-bold",
37
+ subtitle: "font-sans text-subtitle font-medium",
38
38
  heading1: "font-sans text-heading1 font-semibold",
39
39
  heading2: "font-sans text-heading2 font-medium",
40
40
  heading3: "font-sans text-heading3 font-normal",
@@ -1,7 +1,7 @@
1
- import * as ToastPrimitive from '@radix-ui/react-toast';
2
- import React, { ReactNode } from 'react';
3
- import { IconButton } from './IconButton';
4
- import { textStyles } from './Text';
1
+ import * as ToastPrimitive from "@radix-ui/react-toast";
2
+ import React, { ReactNode } from "react";
3
+ import { IconButton } from "./IconButton";
4
+ import { textStyles } from "./Text";
5
5
 
6
6
  export const Toast = ({
7
7
  title,
@@ -15,15 +15,21 @@ export const Toast = ({
15
15
  }) => {
16
16
  return (
17
17
  <ToastPrimitive.Root
18
- className="px-2 py-2.5 grid grid-cols-[auto_max-content] gap-x-2.5 items-center rounded text-sm bg-popover-background shadow-[0_2px_4px_rgba(0,0,0,0.2),0_0_12px_rgba(0,0,0,0.1)] text-text-muted"
18
+ className="px-2 py-2.5 grid grid-cols-[auto_max-content] gap-x-2.5 items-center rounded text-sm bg-popover-background shadow-popover text-text-muted"
19
19
  {...props}
20
20
  >
21
21
  {title && (
22
- <ToastPrimitive.Title className={`mb-[5px] ${textStyles.label} font-bold text-text`}>
22
+ <ToastPrimitive.Title
23
+ className={`mb-[5px] ${textStyles.label} font-bold text-text`}
24
+ >
23
25
  {title}
24
26
  </ToastPrimitive.Title>
25
27
  )}
26
- <ToastPrimitive.Description className={`${textStyles.small} text-text-muted`}>{content}</ToastPrimitive.Description>
28
+ <ToastPrimitive.Description
29
+ className={`${textStyles.small} text-text-muted`}
30
+ >
31
+ {content}
32
+ </ToastPrimitive.Description>
27
33
  {children && (
28
34
  <ToastPrimitive.Action asChild altText="">
29
35
  {children}
@@ -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,8 +12,78 @@ 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";
19
+ import { Tooltip } from "./Tooltip";
20
+
21
+ export const ToolbarMenuDropdown = memoGeneric(function ToolbarMenuDropdown<
22
+ T extends string,
23
+ >({
24
+ item,
25
+ onSelectMenuItem,
26
+ }: {
27
+ item: SubMenuItem<T>;
28
+ onSelectMenuItem?: (value: T) => void;
29
+ }) {
30
+ const [open, setOpen] = React.useState(false);
31
+
32
+ return (
33
+ <DropdownMenu
34
+ open={open}
35
+ onOpenChange={setOpen}
36
+ items={item.items}
37
+ onSelect={(value) => {
38
+ if (onSelectMenuItem && value) {
39
+ onSelectMenuItem(value);
40
+ }
41
+ }}
42
+ >
43
+ <Button disabled={item.disabled} active={item.checked || open}>
44
+ {item.title}
45
+ {item.title && item.icon && <Spacer.Horizontal inline size={6} />}
46
+ {item.icon && renderIcon(item.icon)}
47
+ <Spacer.Horizontal size={6} />
48
+ <DropdownChevronIcon />
49
+ </Button>
50
+ </DropdownMenu>
51
+ );
52
+ });
53
+
54
+ export const ToolbarMenuButton = memoGeneric(function ToolbarMenuButton<
55
+ T extends string,
56
+ >({
57
+ item,
58
+ onSelectMenuItem,
59
+ }: {
60
+ item: SelectableMenuItem<T>;
61
+ onSelectMenuItem?: (value: T) => void;
62
+ }) {
63
+ const content = (
64
+ <Button
65
+ disabled={item.disabled}
66
+ active={item.checked}
67
+ onClick={() => {
68
+ if (onSelectMenuItem && item.value) {
69
+ onSelectMenuItem(item.value);
70
+ }
71
+ }}
72
+ >
73
+ {item.title}
74
+ {item.title && item.icon && <Spacer.Horizontal inline size={6} />}
75
+ {item.icon && renderIcon(item.icon)}
76
+ </Button>
77
+ );
78
+
79
+ return item.tooltip ? (
80
+ <Tooltip sideOffset={10} content={item.tooltip}>
81
+ {content}
82
+ </Tooltip>
83
+ ) : (
84
+ content
85
+ );
86
+ });
18
87
 
19
88
  export interface ToolbarProps<T extends string = string> {
20
89
  children?: React.ReactNode;
@@ -79,10 +148,6 @@ export function Toolbar<T extends string = string>({
79
148
  );
80
149
  }
81
150
 
82
- const activeButtonStyle = {
83
- backgroundColor: cssVars.colors.primaryPastel,
84
- };
85
-
86
151
  export const ToolbarMenu = memoGeneric(function ToolbarMenu<T extends string>({
87
152
  items,
88
153
  onSelectMenuItem,
@@ -101,44 +166,20 @@ export const ToolbarMenu = memoGeneric(function ToolbarMenu<T extends string>({
101
166
 
102
167
  if (isSelectableMenuItem(item)) {
103
168
  return (
104
- <Button
169
+ <ToolbarMenuButton
105
170
  key={i}
106
- disabled={item.disabled}
107
- style={item.checked ? activeButtonStyle : undefined}
108
- onClick={() => {
109
- if (onSelectMenuItem && item.value) {
110
- onSelectMenuItem(item.value);
111
- }
112
- }}
113
- >
114
- {item.title}
115
- {item.title && item.icon && <Spacer.Horizontal inline size={6} />}
116
- {item.icon && renderIcon(item.icon)}
117
- </Button>
171
+ item={item}
172
+ onSelectMenuItem={onSelectMenuItem}
173
+ />
118
174
  );
119
175
  }
120
176
 
121
177
  return (
122
- <DropdownMenu
178
+ <ToolbarMenuDropdown
123
179
  key={i}
124
- items={item.items}
125
- onSelect={(value) => {
126
- if (onSelectMenuItem && value) {
127
- onSelectMenuItem(value);
128
- }
129
- }}
130
- >
131
- <Button
132
- disabled={item.disabled}
133
- style={item.checked ? activeButtonStyle : undefined}
134
- >
135
- {item.title}
136
- {item.title && item.icon && <Spacer.Horizontal inline size={6} />}
137
- {item.icon && renderIcon(item.icon)}
138
- <Spacer.Horizontal size={6} />
139
- <DropdownChevronIcon />
140
- </Button>
141
- </DropdownMenu>
180
+ item={item}
181
+ onSelectMenuItem={onSelectMenuItem}
182
+ />
142
183
  );
143
184
  })}
144
185
  </>
@@ -1,5 +1,9 @@
1
1
  import * as TooltipPrimitive from "@radix-ui/react-tooltip";
2
2
  import * as React from "react";
3
+ import {
4
+ portalScopeProps,
5
+ usePortalScopeId,
6
+ } from "../contexts/PortalScopeContext";
3
7
  import { textStyles } from "./Text";
4
8
 
5
9
  // const Arrow = styled(TooltipPrimitive.Arrow)(({ theme }) => ({
@@ -9,23 +13,28 @@ import { textStyles } from "./Text";
9
13
  interface Props {
10
14
  children: React.ReactNode;
11
15
  content: React.ReactNode;
16
+ sideOffset?: number;
12
17
  }
13
18
 
14
19
  export const Tooltip = React.memo(function Tooltip({
15
20
  children,
16
21
  content,
22
+ sideOffset = 2,
17
23
  }: Props) {
24
+ const portalScopeId = usePortalScopeId();
25
+
18
26
  return (
19
27
  <TooltipPrimitive.Provider>
20
28
  <TooltipPrimitive.Root>
21
29
  <TooltipPrimitive.Trigger asChild>{children}</TooltipPrimitive.Trigger>
22
30
  <TooltipPrimitive.Portal>
23
31
  <TooltipPrimitive.Content
32
+ {...portalScopeProps(portalScopeId)}
24
33
  side="bottom"
25
34
  align="center"
26
- sideOffset={2}
35
+ sideOffset={sideOffset}
27
36
  collisionPadding={8}
28
- className={`${textStyles.small} text-text rounded-[3px] py-small px-medium bg-popover-background shadow-[0_2px_4px_rgba(0,0,0,0.2)_0_0_12px_rgba(0,0,0,0.1)] border border-solid border-divider-strong z-menu`}
37
+ className={`${textStyles.small} text-text rounded-[3px] py-small px-medium bg-popover-background shadow-popover border border-solid border-divider-strong z-menu`}
29
38
  >
30
39
  {content}
31
40
  {/* <Arrow /> */}