@noya-app/noya-designsystem 0.1.39 → 0.1.41

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 (50) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/CHANGELOG.md +16 -0
  3. package/dist/index.css +1 -1
  4. package/dist/index.d.mts +419 -323
  5. package/dist/index.d.ts +419 -323
  6. package/dist/index.js +6583 -8325
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +5721 -7468
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +5 -5
  11. package/src/components/ActivityIndicator.tsx +13 -4
  12. package/src/components/AnimatePresence.tsx +261 -0
  13. package/src/components/Avatar.tsx +202 -80
  14. package/src/components/Button.tsx +10 -6
  15. package/src/components/Checkbox.tsx +7 -2
  16. package/src/components/Chip.tsx +88 -132
  17. package/src/components/{InputFieldWithCompletions.tsx → Combobox.tsx} +10 -3
  18. package/src/components/ContextMenu.tsx +9 -5
  19. package/src/components/Dialog.tsx +20 -23
  20. package/src/components/Divider.tsx +15 -7
  21. package/src/components/DraggableMenuButton.tsx +9 -5
  22. package/src/components/DropdownMenu.tsx +8 -5
  23. package/src/components/FillInputField.tsx +5 -1
  24. package/src/components/FillPreviewBackground.tsx +1 -5
  25. package/src/components/GridView.tsx +71 -46
  26. package/src/components/InputField.tsx +140 -75
  27. package/src/components/InspectorPrimitives.tsx +17 -11
  28. package/src/components/Label.tsx +5 -1
  29. package/src/components/ListView.tsx +28 -15
  30. package/src/components/Progress.tsx +10 -7
  31. package/src/components/SelectMenu.tsx +20 -8
  32. package/src/components/Slider.tsx +104 -10
  33. package/src/components/Sortable.tsx +3 -3
  34. package/src/components/Switch.tsx +19 -2
  35. package/src/components/Text.tsx +5 -1
  36. package/src/components/TextArea.tsx +5 -1
  37. package/src/components/TreeView.tsx +4 -10
  38. package/src/components/UserPointer.tsx +170 -0
  39. package/src/components/WorkspaceLayout.tsx +5 -0
  40. package/src/components/internal/Menu.tsx +14 -5
  41. package/src/components/internal/TextInput.tsx +13 -0
  42. package/src/contexts/DialogContext.tsx +30 -18
  43. package/src/hooks/useTheme.ts +50 -0
  44. package/src/index.css +19 -16
  45. package/src/index.tsx +14 -26
  46. package/src/utils/classNames.ts +5 -0
  47. package/src/utils/colorFromString.ts +44 -0
  48. package/tailwind.config.ts +17 -14
  49. package/src/hooks/useDarkMode.ts +0 -14
  50. package/src/utils/tailwind.ts +0 -9
@@ -1,4 +1,5 @@
1
1
  import React, { forwardRef, InputHTMLAttributes, memo } from "react";
2
+ import { cx } from "../utils/classNames";
2
3
 
3
4
  interface CheckboxProps
4
5
  extends Omit<InputHTMLAttributes<HTMLInputElement>, "type"> {
@@ -18,7 +19,11 @@ export const Checkbox = memo(
18
19
  ) {
19
20
  return (
20
21
  <div
21
- className={`relative inline-flex w-[19px] h-[19px] ${colorSchemeStyles[colorScheme]} ${className ?? ""}`}
22
+ className={cx(
23
+ "flex-none relative inline-flex w-[19px] h-[19px]",
24
+ colorSchemeStyles[colorScheme],
25
+ className
26
+ )}
22
27
  style={style}
23
28
  >
24
29
  <input
@@ -34,7 +39,7 @@ export const Checkbox = memo(
34
39
  bg-input-background
35
40
  transition-colors
36
41
  border
37
- border-input-border
42
+ border-divider
38
43
  disabled:opacity-50
39
44
  disabled:cursor-not-allowed
40
45
  focus:ring-2
@@ -2,119 +2,68 @@ import { Cross1Icon, PlusIcon } from "@noya-app/noya-icons";
2
2
  import React, { forwardRef, memo } from "react";
3
3
  import { useHover } from "../hooks/useHover";
4
4
  import { cssVars } from "../theme";
5
+ import { cx } from "../utils/classNames";
5
6
 
6
7
  type ChipColorScheme = "primary" | "secondary" | "error";
7
8
  type ChipSize = "small" | "medium" | "large";
8
9
  type ChipVariant = "solid" | "outlined" | "ghost";
9
10
 
10
- function getChipStyles({
11
- $colorScheme,
12
- $size,
13
- $variant,
14
- $monospace,
15
- $isInteractive,
16
- }: {
17
- $colorScheme?: ChipColorScheme;
18
- $size: ChipSize;
19
- $variant: ChipVariant;
20
- $monospace: boolean;
21
- $isInteractive: boolean;
22
- }) {
23
- // Base styles
24
- const baseStyles = `
25
- inline-flex items-center
26
- whitespace-pre rounded
27
- select-none
28
- leading-[15px]
29
- `;
30
-
31
- // Size styles
32
- const sizeStyles = {
33
- large: "text-[11px] p-[4px_8px]",
34
- medium: "text-[10px] p-[2px_6px]",
35
- small: "text-[9px] p-[0px_4px]",
36
- }[$size];
37
-
38
- // Font family
39
- const fontStyles = $monospace ? "font-mono" : "";
40
- const shadowStyles = {
41
- primary: "shadow-[0_0_0_1px_var(--chip-primary-shadow)_inset]",
42
- secondary: "shadow-[0_0_0_1px_var(--chip-secondary-shadow)_inset]",
43
- error: "shadow-[0_0_0_1px_var(--chip-error-shadow)_inset]",
44
- default: "shadow-[0_0_0_1px_var(--chip-default-shadow)_inset]",
45
- };
46
-
47
- // Color and background based on scheme and variant
48
- const variantStyles = (() => {
49
- const colors = {
50
- primary: {
51
- text: "text-primary",
52
- bg: "bg-primary-pastel",
53
- subtle: "bg-[var(--chip-primary-bg)]",
54
- },
55
- secondary: {
56
- text: "text-secondary",
57
- bg: "bg-secondary-pastel",
58
- subtle: "bg-[var(--chip-secondary-bg)]",
59
- },
60
- error: {
61
- text: "text-text",
62
- bg: "bg-[rgb(255,219,219)]",
63
- subtle: "bg-[var(--chip-error-bg)]",
64
- },
65
- default: {
66
- text: "text-text",
67
- bg: "bg-input-background",
68
- subtle: "bg-[var(--chip-default-bg)]",
69
- },
70
- }[$colorScheme ?? "default"];
71
-
72
- if ($variant === "solid") {
73
- return `${colors.text} ${colors.bg}`;
74
- }
75
- if ($variant === "outlined") {
76
- return `
77
- ${colors.text} bg-transparent
78
- ${shadowStyles[$colorScheme ?? "default"]}
79
- hover:${colors.subtle} hover:shadow-none
80
- `;
81
- }
82
- return "";
83
- })();
84
-
85
- // Interactive states
86
- const interactiveStyles = $isInteractive
87
- ? "cursor-pointer hover:opacity-85"
88
- : "";
89
-
90
- return `
91
- ${baseStyles}
92
- ${sizeStyles}
93
- ${fontStyles}
94
- ${variantStyles}
95
- ${interactiveStyles}
96
- `
97
- .replace(/\s+/g, " ")
98
- .trim();
99
- }
100
-
101
- function getIconStyles(size: ChipSize, isAdd: boolean) {
102
- const baseStyles = "relative opacity-50 cursor-pointer hover:opacity-85";
103
-
104
- if (size === "large") {
105
- return `
106
- ${baseStyles}
107
- ${isAdd ? "ml-[-2px] mr-[2px] -top-[1px]" : "mr-[-2px] ml-[2px]"}
108
- scale-75
109
- `;
110
- }
111
-
112
- return `
113
- ${baseStyles}
114
- ${isAdd ? "ml-[-2px] mr-[-2px]" : "mr-[-2px]"}
115
- scale-60
116
- `;
117
- }
11
+ // Hoisted constants for styles
12
+ const BASE_STYLES =
13
+ "inline-flex items-center whitespace-pre rounded select-none leading-[15px]";
14
+
15
+ const SIZE_STYLES = {
16
+ large: "text-[11px] p-[4px_8px]",
17
+ medium: "text-[10px] p-[2px_6px]",
18
+ small: "text-[9px] p-[0px_4px]",
19
+ } as const;
20
+
21
+ const SHADOW_STYLES = {
22
+ primary: "shadow-[0_0_0_1px_var(--chip-primary-shadow)_inset]",
23
+ secondary: "shadow-[0_0_0_1px_var(--chip-secondary-shadow)_inset]",
24
+ error: "shadow-[0_0_0_1px_var(--chip-error-shadow)_inset]",
25
+ default: "shadow-[0_0_0_1px_var(--chip-default-shadow)_inset]",
26
+ } as const;
27
+
28
+ const COLOR_STYLES = {
29
+ primary: {
30
+ text: "text-primary",
31
+ bg: "bg-primary-pastel",
32
+ subtle: "bg-[var(--chip-primary-bg)]",
33
+ },
34
+ secondary: {
35
+ text: "text-secondary",
36
+ bg: "bg-secondary-pastel",
37
+ subtle: "bg-[var(--chip-secondary-bg)]",
38
+ },
39
+ error: {
40
+ text: "text-text",
41
+ bg: "bg-[rgb(255,219,219)]",
42
+ subtle: "bg-[var(--chip-error-bg)]",
43
+ },
44
+ default: {
45
+ text: "text-text",
46
+ bg: "bg-input-background",
47
+ subtle: "bg-[var(--chip-default-bg)]",
48
+ },
49
+ } as const;
50
+
51
+ const ICON_BASE_STYLES = "relative opacity-50 cursor-pointer hover:opacity-85";
52
+
53
+ const ICON_SIZE_STYLES = {
54
+ large: {
55
+ add: "ml-[-2px] mr-[2px] -top-[1px] scale-75",
56
+ delete: "mr-[-2px] ml-[2px] scale-75",
57
+ },
58
+ medium: {
59
+ add: "ml-[-2px] mr-[-2px] scale-60",
60
+ delete: "mr-[-2px] scale-60",
61
+ },
62
+ small: {
63
+ add: "ml-[-2px] mr-[-2px] scale-60",
64
+ delete: "mr-[-2px] scale-60",
65
+ },
66
+ } as const;
118
67
 
119
68
  export interface ChipProps {
120
69
  id?: string;
@@ -162,6 +111,24 @@ export const Chip = memo(
162
111
  });
163
112
 
164
113
  const handleClick = !children && !deletable && addable ? onAdd : onClick;
114
+ const isInteractive = !!handleClick || clickable === true;
115
+
116
+ const colors = COLOR_STYLES[colorScheme ?? "default"];
117
+ const variantClasses =
118
+ variant === "solid"
119
+ ? `${colors.text} ${colors.bg}`
120
+ : variant === "outlined"
121
+ ? `${colors.text} bg-transparent ${SHADOW_STYLES[colorScheme ?? "default"]} hover:${colors.subtle} hover:shadow-none`
122
+ : "";
123
+
124
+ const chipClasses = cx(
125
+ BASE_STYLES,
126
+ SIZE_STYLES[size],
127
+ monospace && "font-mono",
128
+ variantClasses,
129
+ isInteractive && "cursor-pointer hover:opacity-85",
130
+ className
131
+ );
165
132
 
166
133
  const color =
167
134
  colorScheme === "primary"
@@ -173,16 +140,7 @@ export const Chip = memo(
173
140
  return (
174
141
  <span
175
142
  ref={forwardedRef}
176
- className={`
177
- ${getChipStyles({
178
- $colorScheme: colorScheme,
179
- $size: size,
180
- $variant: variant,
181
- $monospace: monospace,
182
- $isInteractive: !!handleClick || clickable === true,
183
- })}
184
- ${className ?? ""}
185
- `}
143
+ className={chipClasses}
186
144
  style={style}
187
145
  onClick={handleClick}
188
146
  tabIndex={tabIndex}
@@ -190,14 +148,13 @@ export const Chip = memo(
190
148
  >
191
149
  {addable && (
192
150
  <PlusIcon
193
- className={getIconStyles(size, true)}
151
+ className={cx(ICON_BASE_STYLES, ICON_SIZE_STYLES[size].add)}
194
152
  onClick={
195
- onAdd
196
- ? (event) => {
197
- event.stopPropagation();
198
- onAdd?.();
199
- }
200
- : undefined
153
+ onAdd &&
154
+ ((event) => {
155
+ event.stopPropagation();
156
+ onAdd();
157
+ })
201
158
  }
202
159
  color={color}
203
160
  />
@@ -205,15 +162,14 @@ export const Chip = memo(
205
162
  {children}
206
163
  {deletable && (
207
164
  <Cross1Icon
208
- className={getIconStyles(size, false)}
165
+ className={cx(ICON_BASE_STYLES, ICON_SIZE_STYLES[size].delete)}
209
166
  {...(hoverDeleteProps as any)}
210
167
  onClick={
211
- onDelete
212
- ? (event) => {
213
- event.stopPropagation();
214
- onDelete?.();
215
- }
216
- : undefined
168
+ onDelete &&
169
+ ((event) => {
170
+ event.stopPropagation();
171
+ onDelete();
172
+ })
217
173
  }
218
174
  color={color}
219
175
  />
@@ -140,6 +140,7 @@ export const CompletionMenu = memo(
140
140
  );
141
141
 
142
142
  export type InputFieldWithCompletionsProps = {
143
+ id?: string;
143
144
  loading?: boolean;
144
145
  initialValue?: string;
145
146
  placeholder?: string;
@@ -153,6 +154,7 @@ export type InputFieldWithCompletionsProps = {
153
154
  size?: InputFieldSize;
154
155
  style?: React.CSSProperties;
155
156
  className?: string;
157
+ label?: React.ReactNode;
156
158
  children?: React.ReactNode;
157
159
  hideChildrenWhenFocused?: boolean;
158
160
  hideMenuWhenEmptyValue?: boolean;
@@ -164,9 +166,10 @@ export interface InputFieldWithCompletionsRef {
164
166
  selectAllInputText(): void;
165
167
  }
166
168
 
167
- export const InputFieldWithCompletions = memo(
168
- forwardRef(function InputFieldWithCompletions(
169
+ export const Combobox = memo(
170
+ forwardRef(function Combobox(
169
171
  {
172
+ id,
170
173
  loading,
171
174
  initialValue = "",
172
175
  placeholder,
@@ -180,6 +183,7 @@ export const InputFieldWithCompletions = memo(
180
183
  onDeleteWhenEmpty,
181
184
  style,
182
185
  className,
186
+ label,
183
187
  children,
184
188
  hideChildrenWhenFocused = false,
185
189
  hideMenuWhenEmptyValue = false,
@@ -409,7 +413,9 @@ export const InputFieldWithCompletions = memo(
409
413
 
410
414
  return (
411
415
  <InputField.Root
416
+ id={id}
412
417
  size={size}
418
+ labelPosition="end"
413
419
  renderPopoverContent={({ width }) => {
414
420
  const listSize = { width, height };
415
421
 
@@ -417,7 +423,7 @@ export const InputFieldWithCompletions = memo(
417
423
  <div
418
424
  className={`flex flex-col ${!filter && hideMenuWhenEmptyValue ? "none" : "flex"}`}
419
425
  style={{
420
- flex: `0 0 ${height}px`
426
+ flex: `0 0 ${height}px`,
421
427
  }}
422
428
  >
423
429
  {filteredItems.length > 0 ? (
@@ -472,6 +478,7 @@ export const InputFieldWithCompletions = memo(
472
478
  <ActivityIndicator />
473
479
  </InputField.Label>
474
480
  )}
481
+ {label && <InputField.Label>{label}</InputField.Label>}
475
482
  </InputField.Root>
476
483
  );
477
484
  })
@@ -1,7 +1,8 @@
1
1
  import { CheckIcon, ChevronRightIcon } from "@noya-app/noya-icons";
2
2
  import { useKeyboardShortcuts } from "@noya-app/noya-keymap";
3
+ import { memoGeneric } from "@noya-app/react-utils";
3
4
  import * as RadixContextMenu from "@radix-ui/react-context-menu";
4
- import React, { ReactNode, memo, useCallback, useMemo } from "react";
5
+ import React, { ReactNode, useCallback, useMemo } from "react";
5
6
  import { renderIcon } from "./Icons";
6
7
  import { Spacer } from "./Spacer";
7
8
  import {
@@ -26,7 +27,7 @@ export interface MenuItemProps<T extends string> {
26
27
  items?: MenuItem<T>[];
27
28
  }
28
29
 
29
- const ContextMenuItem = memo(function ContextMenuItem<T extends string>({
30
+ const ContextMenuItem = memoGeneric(function ContextMenuItem<T extends string>({
30
31
  value,
31
32
  children,
32
33
  onSelect,
@@ -68,7 +69,7 @@ const ContextMenuItem = memo(function ContextMenuItem<T extends string>({
68
69
 
69
70
  const element = (
70
71
  <RadixContextMenu.Item
71
- className={styles.itemStyle({ disabled })}
72
+ className={styles.itemStyle({ disabled })}
72
73
  disabled={disabled}
73
74
  onSelect={handleSelectItem}
74
75
  onPointerDown={handlePointerDown}
@@ -171,7 +172,10 @@ function ContextMenuRoot<T extends string>({
171
172
  <ContentComponent className={styles.contentStyle}>
172
173
  {items.map((item, index) =>
173
174
  item === SEPARATOR_ITEM ? (
174
- <RadixContextMenu.Separator key={index} className={styles.separatorStyle} />
175
+ <RadixContextMenu.Separator
176
+ key={index}
177
+ className={styles.separatorStyle}
178
+ />
175
179
  ) : (
176
180
  <ContextMenuItem
177
181
  key={item.value ?? index}
@@ -194,4 +198,4 @@ function ContextMenuRoot<T extends string>({
194
198
  );
195
199
  }
196
200
 
197
- export const ContextMenu = memo(ContextMenuRoot);
201
+ export const ContextMenu = memoGeneric(ContextMenuRoot);
@@ -9,7 +9,7 @@ import React, {
9
9
  } from "react";
10
10
  import { IconButton } from "./IconButton";
11
11
  import { Spacer } from "./Spacer";
12
- import { cn } from "../utils/tailwind";
12
+ import { cx } from "../utils/classNames";
13
13
 
14
14
  const StyledOverlay = forwardRef<
15
15
  React.ElementRef<typeof DialogPrimitive.Overlay>,
@@ -17,7 +17,7 @@ const StyledOverlay = forwardRef<
17
17
  >(({ className, ...props }, ref) => (
18
18
  <DialogPrimitive.Overlay
19
19
  ref={ref}
20
- className={cn("fixed inset-0 bg-[rgba(0,0,0,0.5)]", className)}
20
+ className={cx(`fixed inset-0 bg-[rgba(0,0,0,0.5)] `, className)}
21
21
  {...props}
22
22
  />
23
23
  ));
@@ -28,14 +28,16 @@ const StyledContent = forwardRef<
28
28
  >(({ className, ...props }, ref) => (
29
29
  <DialogPrimitive.Content
30
30
  ref={ref}
31
- className={`
31
+ className={cx(
32
+ `
32
33
  fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2
33
34
  w-[90vw] max-w-[450px] max-h-[85vh] p-dialog-padding rounded-[2px]
34
35
  font-sans text-heading5 font-normal leading-[19px] text-text-muted
35
36
  bg-popover-background overflow-y-auto pointer-events-all z-[1000] focus:outline-none
36
37
  shadow-[0_10px_38px_-10px_hsla(206,22%,7%,.35),0_10px_20px_-15px_hsla(206,22%,7%,.2)]
37
- ${className}
38
- `}
38
+ `,
39
+ className
40
+ )}
39
41
  {...props}
40
42
  />
41
43
  ));
@@ -46,8 +48,8 @@ const StyledTitle = forwardRef<
46
48
  >(({ className, ...props }, ref) => (
47
49
  <DialogPrimitive.Title
48
50
  ref={ref}
49
- className={cn(
50
- "m-0 font-sans text-base font-normal leading-[1.4] text-text",
51
+ className={cx(
52
+ `m-0 font-sans text-base font-normal leading-[1.4] text-text `,
51
53
  className
52
54
  )}
53
55
  {...props}
@@ -60,8 +62,8 @@ const StyledDescription = forwardRef<
60
62
  >(({ className, ...props }, ref) => (
61
63
  <DialogPrimitive.Description
62
64
  ref={ref}
63
- className={cn(
64
- "m-0 font-sans text-heading5 font-normal leading-[19px] text-text-muted",
65
+ className={cx(
66
+ `m-0 font-sans text-heading5 font-normal leading-[19px] text-text-muted `,
65
67
  className
66
68
  )}
67
69
  {...props}
@@ -74,7 +76,10 @@ const CloseButtonContainer = forwardRef<
74
76
  >(({ className, ...props }, ref) => (
75
77
  <div
76
78
  ref={ref}
77
- className={`absolute top-dialog-padding right-dialog-padding z-[1] bg-popover-background p-[4px_6px] rounded-[2px] border border-[rgba(128,128,128,0.2)] ${className}`}
79
+ className={cx(
80
+ `absolute top-dialog-padding right-dialog-padding z-[1] bg-popover-background p-[4px_6px] rounded-[2px] border border-[rgba(128,128,128,0.2)] `,
81
+ className
82
+ )}
78
83
  {...props}
79
84
  />
80
85
  ));
@@ -94,6 +99,7 @@ interface Props {
94
99
  typeof DialogPrimitive.Content
95
100
  >["onOpenAutoFocus"];
96
101
  closeOnInteractOutside?: boolean;
102
+ className?: string;
97
103
  }
98
104
 
99
105
  export const Dialog = forwardRef(function Dialog(
@@ -106,6 +112,7 @@ export const Dialog = forwardRef(function Dialog(
106
112
  onOpenChange,
107
113
  onOpenAutoFocus,
108
114
  closeOnInteractOutside = true,
115
+ className,
109
116
  }: Props,
110
117
  forwardedRef: ForwardedRef<IDialog>
111
118
  ) {
@@ -125,6 +132,7 @@ export const Dialog = forwardRef(function Dialog(
125
132
  ref={contentRef}
126
133
  onOpenAutoFocus={onOpenAutoFocus}
127
134
  style={style}
135
+ className={className}
128
136
  {...(closeOnInteractOutside === false && {
129
137
  onPointerDownOutside: (event) => event.preventDefault(),
130
138
  onInteractOutside: (event) => event.preventDefault(),
@@ -161,19 +169,8 @@ export const FullscreenDialog = forwardRef(function FullscreenDialog(
161
169
  return (
162
170
  <Dialog
163
171
  ref={forwardedRef}
164
- style={{
165
- width: "100%",
166
- height: "100%",
167
- maxWidth: "100%",
168
- maxHeight: "100%",
169
- top: "0",
170
- left: "0",
171
- transform: "none",
172
- display: "flex",
173
- flexDirection: "column",
174
- padding: 0,
175
- ...style,
176
- }}
172
+ className="w-full h-full max-w-full max-h-full top-0 left-0 translate-none flex flex-col p-0"
173
+ style={style}
177
174
  {...rest}
178
175
  />
179
176
  );
@@ -1,4 +1,5 @@
1
1
  import React, { memo } from "react";
2
+ import { cx } from "../utils/classNames";
2
3
 
3
4
  type DividerVariant = "normal" | "subtle" | "strong";
4
5
 
@@ -18,12 +19,14 @@ export const Divider = memo(function Divider({
18
19
  }: DividerProps) {
19
20
  return (
20
21
  <div
21
- className={`
22
+ className={cx(
23
+ `
22
24
  h-px min-h-px
23
25
  ${variant === "strong" ? "bg-divider-strong" : variant === "subtle" ? "bg-divider-subtle" : "bg-divider"}
24
26
  self-stretch
25
- ${className ?? ""}
26
- `}
27
+ `,
28
+ className
29
+ )}
27
30
  style={{
28
31
  marginLeft: `-${overflow}px`,
29
32
  marginRight: `-${overflow}px`,
@@ -38,17 +41,22 @@ export const DividerVertical = memo(function DividerVertical({
38
41
  variant = "normal",
39
42
  overflow = 0,
40
43
  className,
44
+ style,
41
45
  ...props
42
46
  }: DividerProps) {
43
47
  return (
44
48
  <div
45
- className={`
49
+ className={cx(`
46
50
  w-px min-w-px
47
- my-[-${overflow}px]
48
51
  ${variant === "strong" ? "bg-divider-strong" : variant === "subtle" ? "bg-divider-subtle" : "bg-divider"}
49
52
  self-stretch
50
- ${className ?? ""}
51
- `}
53
+ ${className}
54
+ `)}
55
+ style={{
56
+ marginTop: `-${overflow}px`,
57
+ marginBottom: `-${overflow}px`,
58
+ ...style,
59
+ }}
52
60
  {...props}
53
61
  />
54
62
  );
@@ -1,7 +1,8 @@
1
1
  import { DragHandleDots2Icon } from "@noya-app/noya-icons";
2
- import React, { forwardRef, memo, useCallback, useState } from "react";
2
+ import { memoGeneric } from "@noya-app/react-utils";
3
+ import React, { forwardRef, useCallback, useState } from "react";
3
4
  import { cssVars } from "../theme";
4
- import { cn } from "../utils/tailwind";
5
+ import { cx } from "../utils/classNames";
5
6
  import { DropdownMenu } from "./DropdownMenu";
6
7
  import { MenuItem } from "./internal/Menu";
7
8
 
@@ -11,7 +12,10 @@ const DotButton = forwardRef<
11
12
  >(({ className, ...props }, ref) => (
12
13
  <div
13
14
  ref={ref}
14
- className={`cursor-pointer rounded flex items-center justify-center py-[2px] hover:bg-input-background-light active:bg-active-background ${className ?? ""}`}
15
+ className={cx(
16
+ `cursor-pointer rounded flex items-center justify-center py-[2px] hover:bg-input-background-light active:bg-active-background `,
17
+ className
18
+ )}
15
19
  {...props}
16
20
  />
17
21
  ));
@@ -22,7 +26,7 @@ const Trigger = forwardRef<
22
26
  >(({ className, ...props }, ref) => (
23
27
  <div
24
28
  ref={ref}
25
- className={cn("pointer-events-none h-[15px]", className)}
29
+ className={cx(`pointer-events-none h-[15px] `, className)}
26
30
  {...props}
27
31
  />
28
32
  ));
@@ -30,7 +34,7 @@ const Trigger = forwardRef<
30
34
  /**
31
35
  * A button that opens a menu when clicked, but also allows dragging the
32
36
  */
33
- export const DraggableMenuButton = memo(function DraggableMenuButton<
37
+ export const DraggableMenuButton = memoGeneric(function DraggableMenuButton<
34
38
  T extends string,
35
39
  >({
36
40
  items,
@@ -1,12 +1,11 @@
1
1
  import { CheckIcon, ChevronRightIcon } from "@noya-app/noya-icons";
2
2
  import { useKeyboardShortcuts } from "@noya-app/noya-keymap";
3
+ import { forwardRefGeneric, memoGeneric } from "@noya-app/react-utils";
3
4
  import * as RadixDropdownMenu from "@radix-ui/react-dropdown-menu";
4
5
  import React, {
5
6
  ComponentProps,
6
7
  ForwardedRef,
7
8
  SyntheticEvent,
8
- forwardRef,
9
- memo,
10
9
  useCallback,
11
10
  useMemo,
12
11
  } from "react";
@@ -22,7 +21,9 @@ import {
22
21
  styles,
23
22
  } from "./internal/Menu";
24
23
 
25
- const DropdownMenuItem = memo(function ContextMenuItem<T extends string>({
24
+ const DropdownMenuItem = memoGeneric(function ContextMenuItem<
25
+ T extends string,
26
+ >({
26
27
  value,
27
28
  children,
28
29
  onSelect,
@@ -123,7 +124,9 @@ type DropdownRootProps<T extends string> = MenuProps<T> & {
123
124
  "side" | "sideOffset" | "align" | "alignOffset" | "collisionPadding"
124
125
  >;
125
126
 
126
- const DropdownMenuRoot = forwardRef(function DropdownMenuRoot<T extends string>(
127
+ const DropdownMenuRoot = forwardRefGeneric(function DropdownMenuRoot<
128
+ T extends string,
129
+ >(
127
130
  {
128
131
  items,
129
132
  children,
@@ -213,4 +216,4 @@ const DropdownMenuRoot = forwardRef(function DropdownMenuRoot<T extends string>(
213
216
  );
214
217
  });
215
218
 
216
- export const DropdownMenu = memo(DropdownMenuRoot);
219
+ export const DropdownMenu = memoGeneric(DropdownMenuRoot);
@@ -2,6 +2,7 @@ import { Sketch } from "@noya-app/noya-file-format";
2
2
  import React, { forwardRef, memo } from "react";
3
3
  import { SketchPattern } from "../utils/sketchPattern";
4
4
  import { FillPreviewBackground } from "./FillPreviewBackground";
5
+ import { cx } from "../utils/classNames";
5
6
 
6
7
  const FillButton = forwardRef<
7
8
  HTMLButtonElement,
@@ -9,7 +10,10 @@ const FillButton = forwardRef<
9
10
  >(({ className, ...props }, ref) => (
10
11
  <button
11
12
  ref={ref}
12
- className={`outline-none p-0 w-[50px] h-[27px] rounded overflow-hidden border-none shadow-[0_0_0_1px_var(--divider)_inset] bg-transparent relative focus:shadow-[0_0_0_1px_var(--sidebar-background),0_0_0_3px_var(--primary)] ${className ?? ""}`}
13
+ className={cx(
14
+ `outline-none p-0 w-[50px] h-[27px] rounded overflow-hidden border-none shadow-[0_0_0_1px_var(--divider)_inset] bg-transparent relative focus:shadow-[0_0_0_1px_var(--sidebar-background),0_0_0_3px_var(--primary)] `,
15
+ className
16
+ )}
13
17
  {...props}
14
18
  />
15
19
  ));