@noya-app/noya-designsystem 0.1.76 → 0.1.78

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 (86) hide show
  1. package/.turbo/turbo-build.log +20 -12
  2. package/CHANGELOG.md +19 -0
  3. package/dist/chunk-D57E6H3M.mjs +36 -0
  4. package/dist/chunk-D57E6H3M.mjs.map +1 -0
  5. package/dist/emojis.d.mts +1 -0
  6. package/dist/emojis.d.ts +1 -0
  7. package/dist/emojis.js +31 -0
  8. package/dist/emojis.js.map +1 -0
  9. package/dist/emojis.mjs +8 -0
  10. package/dist/emojis.mjs.map +1 -0
  11. package/dist/index.css +1 -1
  12. package/dist/index.d.mts +192 -93
  13. package/dist/index.d.ts +192 -93
  14. package/dist/index.js +3355 -1763
  15. package/dist/index.js.map +1 -1
  16. package/dist/index.mjs +3622 -2030
  17. package/dist/index.mjs.map +1 -1
  18. package/package.json +13 -9
  19. package/src/__tests__/__snapshots__/fuzzyScorer.test.ts.snap +1 -1
  20. package/src/components/Avatar.tsx +10 -10
  21. package/src/components/Banner.tsx +1 -1
  22. package/src/components/BaseToolbar.tsx +20 -6
  23. package/src/components/Button.tsx +2 -1
  24. package/src/components/Chip.tsx +1 -1
  25. package/src/components/ColorSwatchControl.tsx +22 -14
  26. package/src/components/CommandPalette.tsx +10 -6
  27. package/src/components/ContextMenu.tsx +249 -38
  28. package/src/components/Dialog.tsx +70 -67
  29. package/src/components/Drawer.tsx +56 -17
  30. package/src/components/DropdownMenu.tsx +307 -46
  31. package/src/components/EditableText.tsx +1 -1
  32. package/src/components/EmojiPicker.tsx +645 -0
  33. package/src/components/GridBackground.tsx +40 -0
  34. package/src/components/GridView.tsx +5 -1
  35. package/src/components/IVirtualizedList.tsx +21 -1
  36. package/src/components/InputField.tsx +2 -10
  37. package/src/components/InspectorContainer.tsx +4 -2
  38. package/src/components/Message.tsx +5 -16
  39. package/src/components/OverlayToolbar.tsx +97 -0
  40. package/src/components/Popover.tsx +73 -107
  41. package/src/components/Progress.tsx +18 -18
  42. package/src/components/ScrollArea.tsx +66 -31
  43. package/src/components/ScrollableSidebar.tsx +1 -1
  44. package/src/components/SegmentedControl.tsx +166 -38
  45. package/src/components/SelectMenu.tsx +193 -101
  46. package/src/components/Slider.tsx +40 -38
  47. package/src/components/StackNavigator.tsx +1 -0
  48. package/src/components/Switch.tsx +4 -4
  49. package/src/components/TextArea.tsx +1 -1
  50. package/src/components/Toast.tsx +99 -26
  51. package/src/components/Toolbar.tsx +133 -24
  52. package/src/components/Tooltip.tsx +18 -8
  53. package/src/components/UserPointer.tsx +5 -3
  54. package/src/components/Virtualized.tsx +193 -14
  55. package/src/components/VisuallyHidden.tsx +20 -0
  56. package/src/components/__tests__/Virtualized.math.test.ts +426 -1
  57. package/src/components/__tests__/Virtualized.test.tsx +129 -1
  58. package/src/components/ai-assistant/AIAssistantLayout.tsx +11 -6
  59. package/src/components/internal/Menu.tsx +4 -0
  60. package/src/components/listView/ListViewEditableRowTitle.tsx +1 -1
  61. package/src/components/listView/ListViewRoot.tsx +5 -1
  62. package/src/components/resizablePanels/Panel.tsx +94 -0
  63. package/src/components/resizablePanels/PanelGroup.tsx +498 -0
  64. package/src/components/resizablePanels/PanelGroupContext.tsx +14 -0
  65. package/src/components/resizablePanels/PanelResizeHandle.tsx +61 -0
  66. package/src/components/resizablePanels/index.ts +7 -0
  67. package/src/components/resizablePanels/types.ts +65 -0
  68. package/src/components/workspace/DrawerWorkspaceLayout.tsx +22 -7
  69. package/src/components/workspace/PanelWorkspaceLayout.tsx +30 -84
  70. package/src/components/workspace/WorkspaceLayout.tsx +82 -76
  71. package/src/components/workspace/types.ts +6 -4
  72. package/src/contexts/DialogContext.tsx +15 -24
  73. package/src/emojis.ts +1 -0
  74. package/src/hooks/useTriggerToggle.ts +95 -0
  75. package/src/index.css +2 -0
  76. package/src/index.tsx +3 -2
  77. package/src/theme/proseTheme.ts +22 -0
  78. package/src/utils/mergeProps.ts +57 -0
  79. package/src/utils/skinTone.ts +90 -0
  80. package/tailwind.config.ts +6 -1
  81. package/tsup.config.ts +1 -1
  82. package/src/__tests__/workspaceLayout.test.ts +0 -281
  83. package/src/components/ScrollArea2.tsx +0 -76
  84. package/src/components/internal/MenuViewport.tsx +0 -178
  85. package/src/components/internal/SelectItem.tsx +0 -138
  86. package/src/components/workspace/panelStorage.ts +0 -216
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
- import { Slider as RadixSlider } from "radix-ui";
4
- import React, { FocusEventHandler, memo, useCallback, useMemo } from "react";
3
+ import { Slider as BaseSlider } from "@base-ui/react/slider";
4
+ import React, { FocusEventHandler, memo, useCallback } from "react";
5
5
  import { useLabel } from "../hooks/useLabel";
6
6
  import { cx } from "../utils/classNames";
7
7
 
@@ -43,31 +43,28 @@ export const Slider = memo(function Slider({
43
43
  readOnly = false,
44
44
  ...props
45
45
  }: Props) {
46
- const arrayValue = useMemo(
47
- () => [Math.min(Math.max(value, min), max)],
48
- [value, min, max]
49
- );
46
+ const clampedValue = Math.min(Math.max(value, min), max);
50
47
 
51
48
  const { fieldId: id } = useLabel({
52
49
  fieldId: props.id,
53
50
  });
54
51
 
55
52
  const handleValueChange = useCallback(
56
- (arrayValue: number[]) => {
57
- onValueChange(arrayValue[0]);
53
+ (newValue: number) => {
54
+ onValueChange(newValue);
58
55
  },
59
56
  [onValueChange]
60
57
  );
61
58
 
62
- const ratio = (arrayValue[0] - min) / (max - min);
59
+ const ratio = (clampedValue - min) / (max - min);
63
60
 
64
61
  return (
65
- <RadixSlider.Root
62
+ <BaseSlider.Root
66
63
  min={min}
67
64
  max={max}
68
65
  step={step}
69
66
  id={id}
70
- value={arrayValue}
67
+ value={clampedValue}
71
68
  onValueChange={handleValueChange}
72
69
  className={cx(
73
70
  "n-flex n-relative n-items-center n-select-none n-touch-none n-h-input-height n-rounded n-flex-grow n-max-h-input-height n-cursor-pointer",
@@ -77,34 +74,39 @@ export const Slider = memo(function Slider({
77
74
  onFocus={onFocus}
78
75
  onBlur={onBlur}
79
76
  disabled={readOnly}
77
+ thumbAlignment="edge"
80
78
  >
81
- <div className="n-relative n-flex-grow n-h-[3px] n-overflow-hidden">
82
- <div
83
- className="n-absolute n-h-full n-rounded-l n-bg-slider-active-background"
84
- style={{
85
- left: 0,
86
- width: `calc(${ratio * 100}% - ${ratio * 8}px - 2px)`,
87
- }}
88
- />
89
- <div
90
- className="n-absolute n-h-full n-rounded-r n-bg-input-background"
91
- style={{
92
- right: 0,
93
- width: `calc(100% - ${ratio * 100}% + ${ratio * 8}px - 10px)`,
94
- }}
79
+ <BaseSlider.Control className="n-relative n-flex-grow n-h-[21px] n-flex n-items-center">
80
+ <BaseSlider.Track className="n-relative n-flex-grow n-h-[3px] n-overflow-hidden">
81
+ <div
82
+ className="n-absolute n-h-full n-rounded-l n-bg-slider-active-background"
83
+ style={{
84
+ left: 0,
85
+ // With thumbAlignment="edge", thumb ranges from 0 to (100% - THUMB_WIDTH)
86
+ // Active track fills from 0 to thumb left edge, minus a small gap
87
+ width: `calc(${ratio * 100}% - ${ratio * THUMB_WIDTH + 2}px)`,
88
+ }}
89
+ />
90
+ <div
91
+ className="n-absolute n-h-full n-rounded-r n-bg-input-background"
92
+ style={{
93
+ right: 0,
94
+ // Inactive track fills from thumb right edge to 100%, minus a small gap
95
+ width: `calc(${(1 - ratio) * 100}% - ${(1 - ratio) * THUMB_WIDTH + 2}px)`,
96
+ }}
97
+ />
98
+ </BaseSlider.Track>
99
+ <BaseSlider.Thumb
100
+ style={thumbStyle}
101
+ className={cx(
102
+ "n-block n-h-[21px] n-rounded-sm n-bg-slider-thumb-background n-shadow-sliderThumb n-transition-colors focus:n-outline-primary focus:n-outline-2",
103
+ "n-cursor-ew-resize",
104
+ colorScheme === undefined && "n-border-slider-border",
105
+ colorScheme === "primary" && "n-border-primary",
106
+ colorScheme === "secondary" && "n-border-secondary"
107
+ )}
95
108
  />
96
- </div>
97
- <RadixSlider.Thumb
98
- style={thumbStyle}
99
- className={cx(
100
- "n-block n-h-[21px] n-rounded-sm n-bg-slider-thumb-background n-shadow-sliderThumb n-transition-colors focus:n-outline-primary focus:n-outline-2",
101
- "n-cursor-ew-resize",
102
- // "outline-none outline outline-2 outline-offset-[0px] outline-background",
103
- colorScheme === undefined && "n-border-slider-border",
104
- colorScheme === "primary" && "n-border-primary",
105
- colorScheme === "secondary" && "n-border-secondary"
106
- )}
107
- />
108
- </RadixSlider.Root>
109
+ </BaseSlider.Control>
110
+ </BaseSlider.Root>
109
111
  );
110
112
  });
@@ -356,6 +356,7 @@ function defaultRenderHeader({
356
356
  innerClassName="n-px-1"
357
357
  showDivider={showDivider}
358
358
  enableAbsoluteBreakpoint={false}
359
+ enableContainerQuery={false}
359
360
  left={
360
361
  showBackButton && (
361
362
  <Button
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { Switch as SwitchPrimitive } from "radix-ui";
3
+ import { Switch as SwitchPrimitive } from "@base-ui/react/switch";
4
4
  import * as React from "react";
5
5
  import { cx } from "../utils/classNames";
6
6
 
@@ -40,15 +40,15 @@ export const Switch = function Switch({
40
40
  onChange(newValue);
41
41
  }}
42
42
  className={cx(
43
- "n-all-unset n-w-8 n-h-[19px] n-bg-active-background n-rounded-full n-relative n-cursor-pointer n-ring-offset-background n-[-webkit-tap-highlight-color:rgba(0,0,0,0)] focus:n-ring-2 focus:n-ring-primary focus:n-ring-offset-[1px] n-outline-none data-[state=checked]:n-bg-primary n-transition-all",
44
- colorScheme === "secondary" && "data-[state=checked]:n-bg-secondary",
43
+ "n-all-unset n-w-8 n-h-[19px] n-bg-active-background n-rounded-full n-relative n-cursor-pointer n-ring-offset-background n-[-webkit-tap-highlight-color:rgba(0,0,0,0)] focus:n-ring-2 focus:n-ring-primary focus:n-ring-offset-[1px] n-outline-none data-[checked]:n-bg-primary n-transition-all",
44
+ colorScheme === "secondary" && "data-[checked]:n-bg-secondary",
45
45
  "focus:n-z-interactable",
46
46
  className
47
47
  )}
48
48
  onFocus={onFocus}
49
49
  onBlur={onBlur}
50
50
  >
51
- <SwitchPrimitive.Thumb className="n-block n-w-[15px] n-h-[15px] n-bg-background n-rounded-full n-transition-transform n-translate-x-[2px] data-[state=checked]:n-translate-x-[15px]" />
51
+ <SwitchPrimitive.Thumb className="n-block n-w-[15px] n-h-[15px] n-bg-background n-rounded-full n-transition-transform n-translate-x-[2px] n-translate-y-[2px] data-[checked]:n-translate-x-[15px]" />
52
52
  </SwitchPrimitive.Root>
53
53
  );
54
54
  };
@@ -68,7 +68,7 @@ export const TextArea = memo(
68
68
  return (
69
69
  <textarea
70
70
  className={cx(
71
- `n-font-sans n-text-heading5 n-font-normal n-text-text n-bg-input-background n-flex-1 n-py-1 n-px-1.5 n-border-none n-outline-none n-min-h-6 n-w-full n-rounded n-resize-none placeholder:n-text-text-disabled focus:n-ring-2 focus:n-ring-primary read-only:n-text-text-disabled focus:n-z-interactable n-transition-[box-shadow]`,
71
+ `n-font-sans n-text-heading5 n-font-normal n-text-text n-bg-input-background n-flex-1 n-py-1 n-px-input-padding-x n-border-none n-outline-none n-min-h-6 n-w-full n-rounded n-resize-none placeholder:n-text-text-disabled focus:n-ring-2 focus:n-ring-primary read-only:n-text-text-disabled focus:n-z-interactable n-transition-[box-shadow]`,
72
72
  className
73
73
  )}
74
74
  {...rest}
@@ -1,50 +1,123 @@
1
- import { Toast as ToastPrimitive } from "radix-ui";
2
- import React, { ReactNode } from "react";
1
+ "use client";
2
+
3
+ import { Toast as ToastPrimitive } from "@base-ui/react/toast";
4
+ import React, { ReactNode, createContext, useCallback, useContext } from "react";
3
5
  import { IconButton } from "./IconButton";
4
6
  import { textStyles } from "./Text";
5
7
 
6
- export const Toast = ({
7
- title,
8
- content,
9
- children,
10
- ...props
11
- }: {
8
+ // Types for toast data
9
+ interface ToastData {
12
10
  title?: string;
13
11
  content: ReactNode;
14
- children?: React.ReactNode;
15
- }) => {
12
+ action?: ReactNode;
13
+ }
14
+
15
+ // Context for toast functionality
16
+ interface ToastContextValue {
17
+ showToast: (data: ToastData) => void;
18
+ }
19
+
20
+ const ToastContext = createContext<ToastContextValue | null>(null);
21
+
22
+ export function useToast() {
23
+ const context = useContext(ToastContext);
24
+ if (!context) {
25
+ throw new Error("useToast must be used within ToastProvider");
26
+ }
27
+ return context;
28
+ }
29
+
30
+ // Toast object type (inferred from toast manager)
31
+ type ToastObject = {
32
+ id: string;
33
+ data?: ToastData;
34
+ };
35
+
36
+ // Individual toast content renderer
37
+ function ToastContent({ toast }: { toast: ToastObject }) {
16
38
  return (
17
39
  <ToastPrimitive.Root
40
+ toast={toast}
18
41
  className="n-px-2 n-py-2.5 n-grid n-grid-cols-[auto_max-content] n-gap-x-2.5 n-items-center n-rounded n-text-sm n-bg-popover-background n-shadow-popover n-text-text-muted"
19
- {...props}
20
42
  >
21
- {title && (
43
+ {toast.data?.title && (
22
44
  <ToastPrimitive.Title
23
45
  className={`n-mb-[5px] ${textStyles.label} n-font-bold n-text-text`}
24
46
  >
25
- {title}
47
+ {toast.data.title}
26
48
  </ToastPrimitive.Title>
27
49
  )}
28
50
  <ToastPrimitive.Description
29
51
  className={`${textStyles.small} n-text-text-muted`}
30
52
  >
31
- {content}
53
+ {toast.data?.content}
32
54
  </ToastPrimitive.Description>
33
- {children && (
34
- <ToastPrimitive.Action asChild altText="">
35
- {children}
55
+ {toast.data?.action && (
56
+ <ToastPrimitive.Action render={<span />}>
57
+ {toast.data.action}
36
58
  </ToastPrimitive.Action>
37
59
  )}
38
- <ToastPrimitive.Close aria-label="Close" asChild>
39
- <IconButton iconName="Cross1Icon" />
40
- </ToastPrimitive.Close>
60
+ <ToastPrimitive.Close
61
+ aria-label="Close"
62
+ render={<IconButton iconName="Cross1Icon" />}
63
+ />
41
64
  </ToastPrimitive.Root>
42
65
  );
66
+ }
67
+
68
+ // Inner component that uses the toast manager hook (must be inside Provider)
69
+ function ToastProviderInner({ children }: { children: React.ReactNode }) {
70
+ const toastManager = ToastPrimitive.useToastManager();
71
+
72
+ const showToast = useCallback(
73
+ (data: ToastData) => {
74
+ toastManager.add({ data });
75
+ },
76
+ [toastManager]
77
+ );
78
+
79
+ return (
80
+ <ToastContext.Provider value={{ showToast }}>
81
+ {children}
82
+ <ToastPrimitive.Viewport className="n-fixed n-bottom-0 n-right-0 n-flex n-flex-col n-p-5 n-gap-2.5 n-min-w-[200px] n-max-w-[100vw] n-m-0 n-list-none n-z-[2147483647] n-outline-none">
83
+ {toastManager.toasts.map((toast) => (
84
+ <ToastContent key={toast.id} toast={toast} />
85
+ ))}
86
+ </ToastPrimitive.Viewport>
87
+ </ToastContext.Provider>
88
+ );
89
+ }
90
+
91
+ // Provider component that maintains backwards compatibility
92
+ export const ToastProvider = ({ children }: { children: React.ReactNode }) => {
93
+ return (
94
+ <ToastPrimitive.Provider>
95
+ <ToastProviderInner>{children}</ToastProviderInner>
96
+ </ToastPrimitive.Provider>
97
+ );
43
98
  };
44
99
 
45
- export const ToastProvider = ({ children }: { children: React.ReactNode }) => (
46
- <ToastPrimitive.Provider>
47
- {children}
48
- <ToastPrimitive.Viewport className="n-fixed n-bottom-0 n-right-0 n-flex n-flex-col n-p-5 n-gap-2.5 n-min-w-[200px] n-max-w-[100vw] n-m-0 n-list-none n-z-[2147483647] n-outline-none" />
49
- </ToastPrimitive.Provider>
50
- );
100
+ // Legacy Toast component for direct rendering (if needed)
101
+ export const Toast = ({
102
+ title,
103
+ content,
104
+ children,
105
+ }: {
106
+ title?: string;
107
+ content: ReactNode;
108
+ children?: React.ReactNode;
109
+ }) => {
110
+ // This is a simplified version for backwards compatibility
111
+ // In most cases, useToast().showToast() should be used instead
112
+ return (
113
+ <div className="n-px-2 n-py-2.5 n-grid n-grid-cols-[auto_max-content] n-gap-x-2.5 n-items-center n-rounded n-text-sm n-bg-popover-background n-shadow-popover n-text-text-muted">
114
+ {title && (
115
+ <div className={`n-mb-[5px] ${textStyles.label} n-font-bold n-text-text`}>
116
+ {title}
117
+ </div>
118
+ )}
119
+ <div className={`${textStyles.small} n-text-text-muted`}>{content}</div>
120
+ {children}
121
+ </div>
122
+ );
123
+ };
@@ -31,6 +31,16 @@ const SIDE_OFFSET = 6;
31
31
 
32
32
  export const ToolbarMenuContext = createContext<{
33
33
  dividerOverflow?: number;
34
+ /** When true, PopoverMenuItem items show as dialogs on small screens */
35
+ useDialogOnSmallScreen?: boolean;
36
+ /** Function to detect if screen is small. Required when useDialogOnSmallScreen is true */
37
+ isSmallScreen?: boolean;
38
+ /** Function to open a dialog. Required when useDialogOnSmallScreen is true */
39
+ openDialog?: (options: {
40
+ title: ReactNode;
41
+ className?: string;
42
+ children: ReactNode | ((props: { close: () => void }) => ReactNode);
43
+ }) => Promise<void> & { close: () => void };
34
44
  }>({});
35
45
 
36
46
  export const ToolbarMenuDropdown = memoGeneric(function ToolbarMenuDropdown<
@@ -38,17 +48,28 @@ export const ToolbarMenuDropdown = memoGeneric(function ToolbarMenuDropdown<
38
48
  >({
39
49
  item,
40
50
  onSelectMenuItem,
51
+ onPopoverOpenChange,
41
52
  }: {
42
53
  item: SubMenuItem<T>;
43
54
  onSelectMenuItem?: (value: T) => void;
55
+ onPopoverOpenChange?: (open: boolean) => void;
44
56
  }) {
45
57
  const [open, setOpen] = React.useState(false);
46
58
 
59
+ const handleOpenChange = React.useCallback(
60
+ (newOpen: boolean) => {
61
+ setOpen(newOpen);
62
+ onPopoverOpenChange?.(newOpen);
63
+ },
64
+ [onPopoverOpenChange]
65
+ );
66
+
47
67
  return (
48
68
  <DropdownMenu
49
69
  open={open}
50
- onOpenChange={setOpen}
70
+ onOpenChange={handleOpenChange}
51
71
  items={item.items}
72
+ modal={false}
52
73
  onSelect={(value) => {
53
74
  if (onSelectMenuItem && value) {
54
75
  onSelectMenuItem(value);
@@ -59,7 +80,11 @@ export const ToolbarMenuDropdown = memoGeneric(function ToolbarMenuDropdown<
59
80
  disabled={item.disabled}
60
81
  active={item.checked || open}
61
82
  icon={item.icon}
62
- iconRight="DropdownChevronIcon"
83
+ iconRight={
84
+ item.icon === "DotsVerticalIcon" || item.icon === "DotsHorizontalIcon"
85
+ ? undefined
86
+ : "DropdownChevronIcon"
87
+ }
63
88
  >
64
89
  {item.title}
65
90
  </Button>
@@ -86,9 +111,11 @@ export const ToolbarShortcut = ({
86
111
  export const ToolbarMenuPopover = memoGeneric(function ToolbarMenuPopover({
87
112
  item,
88
113
  portalContainer,
114
+ onPopoverOpenChange,
89
115
  }: {
90
116
  item: PopoverMenuItem;
91
117
  portalContainer?: HTMLElement | null;
118
+ onPopoverOpenChange?: (open: boolean) => void;
92
119
  }) {
93
120
  const {
94
121
  disabled,
@@ -97,31 +124,88 @@ export const ToolbarMenuPopover = memoGeneric(function ToolbarMenuPopover({
97
124
  tooltip,
98
125
  content: itemContent,
99
126
  checked,
127
+ popoverClassName,
128
+ triggerProps: buttonProps,
100
129
  } = item;
101
130
  const [open, setOpen] = React.useState(false);
102
131
 
132
+ const handleOpenChange = React.useCallback(
133
+ (newOpen: boolean) => {
134
+ setOpen(newOpen);
135
+ onPopoverOpenChange?.(newOpen);
136
+ },
137
+ [onPopoverOpenChange]
138
+ );
139
+
140
+ const { useDialogOnSmallScreen, isSmallScreen, openDialog } =
141
+ useContext(ToolbarMenuContext);
142
+
143
+ const shouldUseDialog = useDialogOnSmallScreen && isSmallScreen && openDialog;
144
+
145
+ const handleOpenDialog = React.useCallback(() => {
146
+ if (!openDialog) return;
147
+
148
+ handleOpenChange(true);
149
+ const dialogContent =
150
+ typeof itemContent === "function" ? (
151
+ (props: { close: () => void }) => (
152
+ <div className="-n-m-3">{itemContent(props)}</div>
153
+ )
154
+ ) : (
155
+ <div className="-n-m-3">{itemContent}</div>
156
+ );
157
+
158
+ const dialog = openDialog({
159
+ title,
160
+ className: "n-w-[90vw] n-max-w-[360px]",
161
+ children: dialogContent,
162
+ });
163
+ dialog.then(() => handleOpenChange(false));
164
+ }, [openDialog, itemContent, title, handleOpenChange]);
165
+
166
+ const trigger = (
167
+ <Button
168
+ disabled={disabled}
169
+ active={checked || open}
170
+ icon={icon}
171
+ iconRight="DropdownChevronIcon"
172
+ onClick={shouldUseDialog ? handleOpenDialog : undefined}
173
+ {...buttonProps}
174
+ >
175
+ {title}
176
+ </Button>
177
+ );
178
+
179
+ // On small screens with dialog mode enabled, just render the trigger button
180
+ if (shouldUseDialog) {
181
+ return tooltip && !open ? (
182
+ <Tooltip
183
+ sideOffset={SIDE_OFFSET}
184
+ side="top"
185
+ content={tooltip}
186
+ portalContainer={portalContainer}
187
+ >
188
+ {trigger}
189
+ </Tooltip>
190
+ ) : (
191
+ trigger
192
+ );
193
+ }
194
+
103
195
  const content = (
104
196
  <span>
105
197
  <Popover
106
198
  open={open}
107
- onOpenChange={setOpen}
199
+ onOpenChange={handleOpenChange}
108
200
  side="top"
109
201
  showArrow={false}
110
202
  sideOffset={SIDE_OFFSET}
111
203
  portalContainer={portalContainer}
112
- trigger={
113
- <Button
114
- disabled={disabled}
115
- active={checked || open}
116
- icon={icon}
117
- iconRight="DropdownChevronIcon"
118
- >
119
- {title}
120
- </Button>
121
- }
204
+ trigger={trigger}
205
+ className={popoverClassName}
122
206
  >
123
207
  {typeof itemContent === "function"
124
- ? itemContent({ close: () => setOpen(false) })
208
+ ? itemContent({ close: () => handleOpenChange(false) })
125
209
  : itemContent}
126
210
  </Popover>
127
211
  </span>
@@ -203,6 +287,7 @@ export const ToolbarMenuItem = memoGeneric(function ToolbarMenuItem<
203
287
  portalContainer,
204
288
  displayFilter,
205
289
  dividerOverflow: dividerOverflowProp,
290
+ onPopoverOpenChange,
206
291
  }: {
207
292
  item: MenuItem<T>;
208
293
  onSelectMenuItem?: (value: T) => void;
@@ -212,6 +297,7 @@ export const ToolbarMenuItem = memoGeneric(function ToolbarMenuItem<
212
297
  portalContainer?: HTMLElement | null;
213
298
  displayFilter?: DisplayFilter;
214
299
  dividerOverflow?: number;
300
+ onPopoverOpenChange?: (open: boolean) => void;
215
301
  }) {
216
302
  const { dividerOverflow: contextDividerOverflow } =
217
303
  useContext(ToolbarMenuContext);
@@ -225,7 +311,13 @@ export const ToolbarMenuItem = memoGeneric(function ToolbarMenuItem<
225
311
  }
226
312
 
227
313
  if (item.type === "popover") {
228
- return <ToolbarMenuPopover item={item} portalContainer={portalContainer} />;
314
+ return (
315
+ <ToolbarMenuPopover
316
+ item={item}
317
+ portalContainer={portalContainer}
318
+ onPopoverOpenChange={onPopoverOpenChange}
319
+ />
320
+ );
229
321
  }
230
322
 
231
323
  if (isSelectableMenuItem(item)) {
@@ -242,7 +334,11 @@ export const ToolbarMenuItem = memoGeneric(function ToolbarMenuItem<
242
334
  }
243
335
 
244
336
  return (
245
- <ToolbarMenuDropdown item={item} onSelectMenuItem={onSelectMenuItem} />
337
+ <ToolbarMenuDropdown
338
+ item={item}
339
+ onSelectMenuItem={onSelectMenuItem}
340
+ onPopoverOpenChange={onPopoverOpenChange}
341
+ />
246
342
  );
247
343
  });
248
344
 
@@ -257,6 +353,7 @@ export const ToolbarMenu = memoGeneric(function ToolbarMenu<T extends string>({
257
353
  portalContainer,
258
354
  displayFilter = "iconAndText",
259
355
  dividerOverflow,
356
+ onPopoverOpenChange,
260
357
  }: {
261
358
  items: MenuItem<T>[];
262
359
  onSelectMenuItem?: (value: T) => void;
@@ -266,6 +363,7 @@ export const ToolbarMenu = memoGeneric(function ToolbarMenu<T extends string>({
266
363
  portalContainer?: HTMLElement | null;
267
364
  displayFilter?: DisplayFilter;
268
365
  dividerOverflow?: number;
366
+ onPopoverOpenChange?: (open: boolean) => void;
269
367
  }) {
270
368
  return (
271
369
  <>
@@ -281,6 +379,7 @@ export const ToolbarMenu = memoGeneric(function ToolbarMenu<T extends string>({
281
379
  portalContainer={portalContainer}
282
380
  displayFilter={displayFilter}
283
381
  dividerOverflow={dividerOverflow}
382
+ onPopoverOpenChange={onPopoverOpenChange}
284
383
  />
285
384
  );
286
385
  })}
@@ -299,6 +398,21 @@ export interface ToolbarProps<T extends string = string> {
299
398
  dividerOverflow?: number;
300
399
  }
301
400
 
401
+ export function useBindKeyboardShortcutsForMenuItems<T extends string>(
402
+ items: MenuItem<T>[],
403
+ onSelectMenuItem?: (value: T) => void
404
+ ) {
405
+ return useKeyboardShortcuts(
406
+ React.useMemo(
407
+ () =>
408
+ onSelectMenuItem
409
+ ? getKeyboardShortcutsForMenuItems(items, onSelectMenuItem)
410
+ : {},
411
+ [items, onSelectMenuItem]
412
+ )
413
+ );
414
+ }
415
+
302
416
  export function Toolbar<T extends string = string>({
303
417
  children,
304
418
  logo,
@@ -314,14 +428,9 @@ export function Toolbar<T extends string = string>({
314
428
  [leftMenuItems, rightMenuItems]
315
429
  );
316
430
 
317
- useKeyboardShortcuts(
318
- React.useMemo(
319
- () =>
320
- onSelectMenuItem && shouldBindKeyboardShortcuts
321
- ? getKeyboardShortcutsForMenuItems(allMenuItems, onSelectMenuItem)
322
- : {},
323
- [allMenuItems, onSelectMenuItem, shouldBindKeyboardShortcuts]
324
- )
431
+ useBindKeyboardShortcutsForMenuItems(
432
+ allMenuItems,
433
+ shouldBindKeyboardShortcuts ? onSelectMenuItem : undefined
325
434
  );
326
435
 
327
436
  return (
@@ -1,17 +1,19 @@
1
- import { Tooltip as TooltipPrimitive } from "radix-ui";
1
+ "use client";
2
+
3
+ import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip";
2
4
  import * as React from "react";
3
- import { ComponentProps } from "react";
4
5
  import {
5
6
  portalScopeProps,
6
7
  usePortalScopeId,
7
8
  } from "../contexts/PortalScopeContext";
9
+ import { cloneElementWithMergedProps } from "../utils/mergeProps";
8
10
  import { textStyles } from "./Text";
9
11
 
10
12
  interface Props {
11
13
  children: React.ReactNode;
12
14
  content: React.ReactNode;
13
15
  sideOffset?: number;
14
- side?: ComponentProps<typeof TooltipPrimitive.Content>["side"];
16
+ side?: "top" | "bottom" | "left" | "right";
15
17
  portalContainer?: HTMLElement | null;
16
18
  }
17
19
 
@@ -27,18 +29,26 @@ export const Tooltip = React.memo(function Tooltip({
27
29
  return (
28
30
  <TooltipPrimitive.Provider>
29
31
  <TooltipPrimitive.Root>
30
- <TooltipPrimitive.Trigger asChild>{children}</TooltipPrimitive.Trigger>
32
+ <TooltipPrimitive.Trigger
33
+ render={(triggerProps) =>
34
+ cloneElementWithMergedProps(children, triggerProps)
35
+ }
36
+ />
31
37
  <TooltipPrimitive.Portal container={portalContainer}>
32
- <TooltipPrimitive.Content
38
+ <TooltipPrimitive.Positioner
33
39
  {...portalScopeProps(portalScopeId)}
34
40
  side={side}
35
41
  align="center"
36
42
  sideOffset={sideOffset}
37
43
  collisionPadding={8}
38
- className={`${textStyles.small} n-text-text n-rounded-sm n-p-2 n-bg-popover-background n-shadow-tooltip n-z-menu`}
44
+ className="n-z-menu"
39
45
  >
40
- {content}
41
- </TooltipPrimitive.Content>
46
+ <TooltipPrimitive.Popup
47
+ className={`${textStyles.small} n-text-text n-rounded-sm n-p-2 n-bg-popover-background n-shadow-tooltip n-z-menu`}
48
+ >
49
+ {content}
50
+ </TooltipPrimitive.Popup>
51
+ </TooltipPrimitive.Positioner>
42
52
  </TooltipPrimitive.Portal>
43
53
  </TooltipPrimitive.Root>
44
54
  </TooltipPrimitive.Provider>
@@ -13,7 +13,7 @@ const UserPointerContainer = memo(function UserPointerContainer({
13
13
  className,
14
14
  style,
15
15
  }: {
16
- point: Point;
16
+ point?: Point;
17
17
  visible?: boolean;
18
18
  children: React.ReactNode;
19
19
  className?: string;
@@ -22,7 +22,9 @@ const UserPointerContainer = memo(function UserPointerContainer({
22
22
  return (
23
23
  <div
24
24
  style={{
25
- transform: `translate(${Math.round(point.x)}px, ${Math.round(point.y)}px)`,
25
+ ...(point && {
26
+ transform: `translate(${Math.round(point.x)}px, ${Math.round(point.y)}px)`,
27
+ }),
26
28
  transition: "transform 0.075s, opacity 0.2s",
27
29
  ...style,
28
30
  }}
@@ -117,7 +119,7 @@ export type UserPointerProps = {
117
119
  name?: string;
118
120
  /** label to display in the tooltip next to the Pointer */
119
121
  visible?: boolean;
120
- point: Point;
122
+ point?: Point;
121
123
  /** defaults to a random color based on the key, but can optionally be overridden */
122
124
  backgroundColor?: string;
123
125
  style?: React.CSSProperties;