@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noya-app/noya-designsystem",
3
- "version": "0.1.39",
3
+ "version": "0.1.41",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.ts",
@@ -23,9 +23,9 @@
23
23
  "@dnd-kit/core": "3.1.1",
24
24
  "@dnd-kit/modifiers": "3.0.0",
25
25
  "@dnd-kit/sortable": "4.0.0",
26
- "@noya-app/noya-colorpicker": "0.1.13",
27
- "@noya-app/noya-utils": "0.1.2",
28
- "@noya-app/noya-geometry": "0.1.5",
26
+ "@noya-app/noya-colorpicker": "0.1.14",
27
+ "@noya-app/noya-utils": "0.1.3",
28
+ "@noya-app/noya-geometry": "0.1.6",
29
29
  "@noya-app/noya-icons": "0.1.4",
30
30
  "@noya-app/noya-keymap": "0.1.2",
31
31
  "@radix-ui/primitive": "^1.0.0",
@@ -38,7 +38,7 @@
38
38
  "@radix-ui/react-progress": "^1.0.1",
39
39
  "@radix-ui/react-radio-group": "^1.1.0",
40
40
  "@radix-ui/react-scroll-area": "^1.0.2",
41
- "@radix-ui/react-slider": "^1.1.0",
41
+ "@radix-ui/react-slider": "^1.2.2",
42
42
  "@radix-ui/react-switch": "^1.0.1",
43
43
  "@radix-ui/react-toast": "^1.1.4",
44
44
  "@radix-ui/react-toggle-group": "^1.0.1",
@@ -1,5 +1,4 @@
1
- import React, { memo } from "react";
2
- import { cn } from "../utils/tailwind";
1
+ import React, { memo, useMemo } from "react";
3
2
 
4
3
  interface Props {
5
4
  size?: number;
@@ -14,9 +13,19 @@ export const ActivityIndicator = memo(function ActivityIndicator({
14
13
  color = "black",
15
14
  trackColor = "rgba(0, 0, 0, 0.1)",
16
15
  }: Props) {
16
+ const opacityStyle = useMemo(() => ({ opacity }), [opacity]);
17
+ const dynamicStyles = useMemo(
18
+ () => ({
19
+ border: `2px solid ${trackColor}`,
20
+ borderTopColor: color,
21
+ width: `${size}px`,
22
+ height: `${size}px`,
23
+ }),
24
+ [size, color, trackColor]
25
+ );
17
26
  return (
18
- <div className={cn("flex justify-center items-center", `opacity-[${opacity}]`)}>
19
- <div className={cn("animate-spin", `border-[2px_solid_${trackColor}] border-t-[2px_solid_${color}] rounded-[50%] w-[${size}px] h-[${size}px]`)} />
27
+ <div className="flex justify-center items-center" style={opacityStyle}>
28
+ <div style={dynamicStyles} className="animate-spin rounded-[50%]" />
20
29
  </div>
21
30
  );
22
31
  });
@@ -0,0 +1,261 @@
1
+ import { isDeepEqual } from "@noya-app/noya-utils";
2
+ import React, {
3
+ useEffect,
4
+ useState,
5
+ useRef,
6
+ Children,
7
+ isValidElement,
8
+ useCallback,
9
+ useLayoutEffect,
10
+ } from "react";
11
+
12
+ type AnimatePresenceProps = {
13
+ children: React.ReactNode;
14
+ /**
15
+ * Duration of enter/exit animations in milliseconds
16
+ * @default 200
17
+ */
18
+ duration?: number;
19
+ /**
20
+ * Custom styles to apply during animations
21
+ */
22
+ animationStyles?: {
23
+ entering?: React.CSSProperties;
24
+ entered?: React.CSSProperties;
25
+ exiting?: React.CSSProperties;
26
+ };
27
+ /** @default false */
28
+ skipInitialAnimation?: boolean;
29
+ /**
30
+ * @default "wait"
31
+ * "wait" - wait for the previous animation to finish before starting the next one
32
+ * "crossfade" - crossfade between the previous and next animation
33
+ */
34
+ mode?: "wait" | "crossfade";
35
+ };
36
+
37
+ const defaultDuration = 150;
38
+
39
+ type AnimationState = "entering" | "entered" | "exiting" | "exited";
40
+
41
+ type PresenceChildProps = {
42
+ isPresent: boolean;
43
+ onExitComplete: () => void;
44
+ waitingToEnter?: boolean;
45
+ } & Pick<
46
+ AnimatePresenceProps,
47
+ "animationStyles" | "children" | "duration" | "skipInitialAnimation"
48
+ >;
49
+
50
+ const defaultAnimationStyles: Required<
51
+ AnimatePresenceProps["animationStyles"]
52
+ > = {
53
+ entering: {
54
+ opacity: 0,
55
+ transition: `opacity ${defaultDuration}ms ease-in`,
56
+ },
57
+ entered: {
58
+ opacity: 1,
59
+ transition: `opacity ${defaultDuration}ms ease-in`,
60
+ },
61
+ exiting: {
62
+ opacity: 0,
63
+ transition: `opacity ${defaultDuration}ms ease-out`,
64
+ },
65
+ };
66
+
67
+ const PresenceChild = ({
68
+ children,
69
+ duration,
70
+ isPresent,
71
+ animationStyles,
72
+ onExitComplete,
73
+ skipInitialAnimation,
74
+ }: PresenceChildProps) => {
75
+ const [animationState, setAnimationState] = useState<AnimationState>(() =>
76
+ skipInitialAnimation && isPresent
77
+ ? "entered"
78
+ : isPresent
79
+ ? "entering"
80
+ : "exited"
81
+ );
82
+ const timeoutRef = useRef<NodeJS.Timeout>();
83
+ const isInitialMount = useRef(true);
84
+
85
+ const mergedAnimationStyles = {
86
+ ...defaultAnimationStyles,
87
+ ...animationStyles,
88
+ exited: {},
89
+ willChange: "opacity",
90
+ backfaceVisibility: "hidden",
91
+ } as Record<AnimationState, React.CSSProperties>;
92
+
93
+ useLayoutEffect(() => {
94
+ let rafId: number;
95
+
96
+ if (isPresent && animationState !== "entered") {
97
+ if (isInitialMount.current && skipInitialAnimation) {
98
+ setAnimationState("entered");
99
+ isInitialMount.current = false;
100
+ return;
101
+ }
102
+
103
+ // Use RAF for better timing
104
+ rafId = requestAnimationFrame(() => {
105
+ setAnimationState("entering");
106
+ rafId = requestAnimationFrame(() => {
107
+ setAnimationState("entered");
108
+ });
109
+ });
110
+ } else if (!isPresent && animationState !== "exited") {
111
+ setAnimationState("exiting");
112
+ timeoutRef.current = setTimeout(() => {
113
+ setAnimationState("exited");
114
+ onExitComplete();
115
+ }, duration);
116
+ }
117
+
118
+ return () => {
119
+ if (rafId) cancelAnimationFrame(rafId);
120
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
121
+ };
122
+ }, [
123
+ isPresent,
124
+ duration,
125
+ onExitComplete,
126
+ animationState,
127
+ skipInitialAnimation,
128
+ ]);
129
+
130
+ if (animationState === "exited" && !isPresent) {
131
+ return null;
132
+ }
133
+
134
+ return <div style={mergedAnimationStyles[animationState]}>{children}</div>;
135
+ };
136
+
137
+ type MountedChild = {
138
+ child: React.ReactElement;
139
+ isPresent: boolean;
140
+ skipInitialAnimation?: boolean;
141
+ };
142
+
143
+ export const AnimatePresence = ({
144
+ children,
145
+ duration = defaultDuration,
146
+ animationStyles,
147
+ skipInitialAnimation,
148
+ mode = "wait",
149
+ }: AnimatePresenceProps) => {
150
+ const [mountedChildren, setMountedChildren] = useState<MountedChild[]>([]);
151
+ const previousChildrenRef = useRef<React.ReactElement[]>([]);
152
+ const removingChildrenRef = useRef<Set<string>>(new Set());
153
+ const waitingToEnterRef = useRef<MountedChild[]>([]);
154
+
155
+ useEffect(() => {
156
+ const newChildren = Children.toArray(children).filter(
157
+ isValidElement
158
+ ) as React.ReactElement[];
159
+
160
+ setMountedChildren((prev) => {
161
+ if (!prev.length && skipInitialAnimation) {
162
+ return newChildren.map((child) => ({
163
+ child,
164
+ isPresent: true,
165
+ skipInitialAnimation: skipInitialAnimation,
166
+ }));
167
+ }
168
+
169
+ // Update presence for existing children
170
+ const updated = prev.map((mounted) => {
171
+ const isStillPresent = newChildren.some(
172
+ (child) =>
173
+ isDeepEqual(child.key, mounted.child.key) ||
174
+ isDeepEqual(child, mounted.child)
175
+ );
176
+
177
+ if (!isStillPresent) {
178
+ removingChildrenRef.current.add(mounted.child.key?.toString() ?? "");
179
+ }
180
+
181
+ return {
182
+ ...mounted,
183
+ isPresent: isStillPresent,
184
+ removing: !isStillPresent
185
+ ? true
186
+ : removingChildrenRef.current.has(
187
+ mounted.child.key?.toString() ?? ""
188
+ ),
189
+ };
190
+ });
191
+
192
+ // Store new children in waitingToEnterRef if we're in wait mode and have removing children
193
+ const newMounted = newChildren
194
+ .filter(
195
+ (child) =>
196
+ !prev.some(
197
+ (p) =>
198
+ isDeepEqual(p.child.key, child.key) ||
199
+ isDeepEqual(p.child, child)
200
+ )
201
+ )
202
+ .map((child) => ({
203
+ child,
204
+ isPresent: mode === "wait" ? false : true,
205
+ }));
206
+
207
+ previousChildrenRef.current = newChildren;
208
+
209
+ if (mode === "wait" && removingChildrenRef.current.size > 0) {
210
+ waitingToEnterRef.current = newMounted;
211
+ return updated;
212
+ }
213
+
214
+ return [...updated, ...newMounted];
215
+ });
216
+ }, [children, skipInitialAnimation, mode]);
217
+
218
+ const removeChild = useCallback((childKey: string) => {
219
+ if (!removingChildrenRef.current.has(childKey)) return;
220
+ removingChildrenRef.current.delete(childKey);
221
+
222
+ setMountedChildren((prev) => {
223
+ const filtered = prev.filter(
224
+ (mounted) => mounted.child.key?.toString() !== childKey
225
+ );
226
+
227
+ if (
228
+ removingChildrenRef.current.size === 0 &&
229
+ waitingToEnterRef.current.length > 0
230
+ ) {
231
+ const enteringChildren = waitingToEnterRef.current.map((mounted) => ({
232
+ ...mounted,
233
+ isPresent: true,
234
+ }));
235
+ waitingToEnterRef.current = [];
236
+ return [...filtered, ...enteringChildren];
237
+ }
238
+
239
+ return filtered;
240
+ });
241
+ }, []);
242
+
243
+ return (
244
+ <>
245
+ {mountedChildren.map(({ child, isPresent, skipInitialAnimation }) => (
246
+ <PresenceChild
247
+ key={child.key}
248
+ duration={duration}
249
+ animationStyles={animationStyles}
250
+ isPresent={isPresent}
251
+ skipInitialAnimation={skipInitialAnimation}
252
+ onExitComplete={() => removeChild(child.key?.toString() ?? "")}
253
+ >
254
+ {child}
255
+ </PresenceChild>
256
+ ))}
257
+ </>
258
+ );
259
+ };
260
+
261
+ export type { AnimatePresenceProps };
@@ -1,110 +1,232 @@
1
- import React, { forwardRef } from "react";
2
1
  import * as AvatarPrimitive from "@radix-ui/react-avatar";
2
+ import React, { forwardRef, useMemo } from "react";
3
+ import { cx } from "../utils/classNames";
4
+ import { colorFromString } from "../utils/colorFromString";
5
+
6
+ const getInnerAvatarStyles = (
7
+ size: number,
8
+ variant: "normal" | "bare"
9
+ ): React.CSSProperties => {
10
+ const borderSize = variant === "bare" ? 4 : size > 36 ? 8 : 6;
11
+ const dimension = `${size - borderSize}px`;
12
+ return {
13
+ width: dimension,
14
+ height: dimension,
15
+ minWidth: dimension,
16
+ minHeight: dimension,
17
+ };
18
+ };
3
19
 
4
20
  const AvatarRoot = React.forwardRef<
5
21
  React.ElementRef<typeof AvatarPrimitive.Root>,
6
- React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> & {
7
- size: number;
8
- overflow: number;
22
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> &
23
+ Pick<AvatarProps, "size" | "variant" | "backgroundColor" | "disabled">
24
+ >(
25
+ (
26
+ {
27
+ className,
28
+ size = 21,
29
+ style,
30
+ backgroundColor,
31
+ disabled,
32
+ variant = "normal",
33
+ ...props
34
+ },
35
+ ref
36
+ ) => {
37
+ const dynamicStyles = useMemo(
38
+ () => ({
39
+ width: `${size}px`,
40
+ height: `${size}px`,
41
+ backgroundColor,
42
+ ...style,
43
+ }),
44
+ [backgroundColor, size, style]
45
+ );
46
+
47
+ return (
48
+ <AvatarPrimitive.Root
49
+ ref={ref}
50
+ style={dynamicStyles}
51
+ className={cx(
52
+ "flex items-center justify-center align-middle overflow-hidden select-none cursor-pointer rounded-full",
53
+ !disabled && "hover:translate-y-[-1px]",
54
+ variant !== "bare" && "border border-background",
55
+ className
56
+ )}
57
+ {...props}
58
+ />
59
+ );
9
60
  }
10
- >(({ className, size, overflow, ...props }, ref) => (
11
- <AvatarPrimitive.Root
12
- ref={ref}
13
- className={`
14
- inline-flex items-center justify-center
15
- align-middle overflow-hidden select-none
16
- w-[${size}px] h-[${size}px]
17
- rounded-full bg-input-background
18
- ${overflow ? `m-[-${overflow}px]` : ""}
19
- hover:shadow-[0_0_0_2px_var(--sidebar-background)] hover:opacity-90
20
- ${className ?? ""}
21
- `}
22
- {...props}
23
- />
24
- ));
61
+ );
25
62
 
26
63
  const AvatarImage = React.forwardRef<
27
64
  React.ElementRef<typeof AvatarPrimitive.Image>,
28
- React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
29
- >(({ className, ...props }, ref) => (
30
- <AvatarPrimitive.Image
31
- ref={ref}
32
- className={`
33
- w-full h-full
34
- object-cover
35
- rounded-inherit
36
- ${className ?? ""}
37
- `}
38
- {...props}
39
- />
40
- ));
65
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> &
66
+ Pick<AvatarProps, "size" | "variant">
67
+ >(({ className, size = 21, variant = "normal", ...props }, ref) => {
68
+ return (
69
+ <AvatarPrimitive.Image
70
+ ref={ref}
71
+ className={cx(
72
+ "object-cover overflow-hidden rounded-full w-full h-full",
73
+ className
74
+ )}
75
+ style={getInnerAvatarStyles(size, variant)}
76
+ {...props}
77
+ />
78
+ );
79
+ });
80
+
81
+ function getMonogram(text?: string) {
82
+ return text?.at(0)?.toLocaleUpperCase();
83
+ }
41
84
 
42
85
  const AvatarFallback = React.forwardRef<
43
86
  React.ElementRef<typeof AvatarPrimitive.Fallback>,
44
- React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> & {
45
- size: number;
87
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> &
88
+ Pick<AvatarProps, "size" | "variant" | "backgroundColor">
89
+ >(
90
+ (
91
+ { className, children, style, size = 21, backgroundColor, ...props },
92
+ ref
93
+ ) => {
94
+ return (
95
+ <AvatarPrimitive.Fallback
96
+ ref={ref}
97
+ style={{
98
+ backgroundColor,
99
+ fontSize: `${size * 0.5}px`,
100
+ lineHeight: `${size}px`, // This ensures perfect vertical centering
101
+ ...style,
102
+ }}
103
+ className={cx(
104
+ "font-medium flex items-center justify-center text-white",
105
+ className
106
+ )}
107
+ {...props}
108
+ >
109
+ {children}
110
+ </AvatarPrimitive.Fallback>
111
+ );
46
112
  }
47
- >(({ className, size, ...props }, ref) => (
48
- <AvatarPrimitive.Fallback
49
- ref={ref}
50
- className={`
51
- text-[0.8rem] font-medium
52
- w-full h-full
53
- flex items-center justify-center
54
- text-white
55
- leading-[${size}px]
56
- ${className ?? ""}
57
- `}
58
- {...props}
59
- />
60
- ));
113
+ );
114
+
115
+ export type AvatarProps = {
116
+ image?: string;
117
+ name?: string;
118
+ /** string used to generate a fallback monogram. more unique strings are better. */
119
+ userId?: string;
120
+ size?: number;
121
+ overflow?: number;
122
+ style?: React.CSSProperties;
123
+ className?: string;
124
+ /** colors are generated from the fallback string or title, but you can override it here */
125
+ backgroundColor?: string;
126
+ /** @default normal */
127
+ variant?: "normal" | "bare";
128
+ disabled?: boolean;
129
+ };
61
130
 
62
- export const Avatar = forwardRef(function Avatar(
63
- {
64
- image,
65
- name,
66
- fallback,
67
- size = 19,
68
- overflow = 0,
69
- ...props
70
- }: {
71
- image?: string;
72
- name?: string;
73
- fallback?: string;
74
- size?: number;
75
- overflow?: number;
76
- style?: React.CSSProperties;
77
- className?: string;
78
- },
79
- forwardedRef: React.ForwardedRef<HTMLDivElement>
80
- ) {
131
+ export const Avatar = React.memo(
132
+ forwardRef(function Avatar(
133
+ {
134
+ image,
135
+ name,
136
+ userId,
137
+ size = 21,
138
+ backgroundColor,
139
+ variant = "normal",
140
+ ...props
141
+ }: AvatarProps,
142
+ forwardedRef: React.ForwardedRef<HTMLDivElement>
143
+ ) {
144
+ const getBackgroundColor =
145
+ backgroundColor ?? colorFromString(userId ?? name ?? "");
146
+
147
+ return (
148
+ <AvatarRoot
149
+ ref={forwardedRef}
150
+ size={size}
151
+ backgroundColor={getBackgroundColor}
152
+ variant={variant}
153
+ {...props}
154
+ >
155
+ {image ? (
156
+ <AvatarImage src={image} alt={name} size={size} variant={variant} />
157
+ ) : (
158
+ <AvatarFallback
159
+ backgroundColor={getBackgroundColor}
160
+ size={size}
161
+ variant={variant}
162
+ >
163
+ {getMonogram(name)}
164
+ </AvatarFallback>
165
+ )}
166
+ </AvatarRoot>
167
+ );
168
+ })
169
+ );
170
+
171
+ const OverflowAvatar = React.memo(function OverflowAvatar({
172
+ size = 21,
173
+ count,
174
+ }: {
175
+ size?: number;
176
+ count: number;
177
+ }) {
81
178
  return (
82
- <AvatarRoot ref={forwardedRef} size={size} overflow={overflow} {...props}>
83
- <AvatarImage src={image} alt={name} />
84
- <AvatarFallback size={size}>{fallback}</AvatarFallback>
179
+ <AvatarRoot size={size} backgroundColor="var(--text-disabled)" disabled>
180
+ <AvatarFallback backgroundColor="var(--text-disabled)" size={size}>
181
+ +{count}
182
+ </AvatarFallback>
85
183
  </AvatarRoot>
86
184
  );
87
185
  });
88
186
 
89
- export function AvatarStack({
90
- size = 19,
187
+ export const AvatarStack = React.memo(function AvatarStack({
188
+ size = 21,
91
189
  children,
190
+ max = 3,
191
+ className,
92
192
  }: {
93
193
  size?: number;
194
+ /** @default 3 */
195
+ max?: number;
94
196
  children: React.ReactNode;
197
+ className?: string;
95
198
  }) {
96
- const childrenArray = React.Children.toArray(children).map((child) => {
97
- return React.cloneElement(child as React.ReactElement, { size });
98
- });
199
+ const avatarCount = React.Children.count(children);
200
+ // can add logic later to shorten values over 2 digits, etc.
201
+ const overflowCount = avatarCount - max;
202
+
203
+ const childrenArray = useMemo(() => {
204
+ const processedChildren = React.Children.toArray(children)
205
+ .map((child) => React.cloneElement(child as React.ReactElement, { size }))
206
+ .slice(0, max);
207
+
208
+ if (avatarCount > max) {
209
+ return [
210
+ <OverflowAvatar key="overflow" size={size} count={overflowCount} />,
211
+ ...processedChildren,
212
+ ];
213
+ }
214
+
215
+ return processedChildren;
216
+ }, [children, max, size, avatarCount, overflowCount]);
99
217
 
100
218
  return (
101
- <div className="flex">
219
+ <div
220
+ className={cx(
221
+ "flex flex-row-reverse",
222
+ size > 36 ? "pl-3" : "pl-1.5",
223
+ className
224
+ )}
225
+ >
102
226
  {childrenArray.map((child, index) => {
103
- if (index === 0) return child;
104
-
105
227
  return (
106
228
  <div
107
- className={`relative w-[${size}px] h-[${size}px] -ml-2 rounded-full overflow-hidden [clip-path:circle(50%)]`}
229
+ className={cx("relative", size > 36 ? "-ml-3" : "-ml-1.5")}
108
230
  key={index}
109
231
  >
110
232
  {child}
@@ -113,4 +235,4 @@ export function AvatarStack({
113
235
  })}
114
236
  </div>
115
237
  );
116
- }
238
+ });
@@ -6,6 +6,7 @@ import React, {
6
6
  useCallback,
7
7
  } from "react";
8
8
  import { Tooltip } from "./Tooltip";
9
+ import { cx } from "../utils/classNames";
9
10
 
10
11
  type ButtonVariant =
11
12
  | "normal"
@@ -27,7 +28,7 @@ const variantStyles: Record<ButtonVariant, string> = {
27
28
  "bg-secondary text-white hover:bg-secondary-light active:bg-secondary min-w-[31px]",
28
29
  secondaryBright: "bg-[#0ab557] text-white hover:opacity-80 active:opacity-90",
29
30
  floating:
30
- "bg-white text-text shadow-[0_1px_2px_rgba(0,0,0,0.1)] hover:opacity-80 active:opacity-90",
31
+ "bg-floating-button text-text shadow-[0_1px_2px_rgba(0,0,0,0.1)] hover:opacity-80 active:opacity-90",
31
32
  thin: "bg-transparent text-text hover:bg-input-background-light",
32
33
  none: "bg-transparent text-text hover:bg-input-background-light",
33
34
  };
@@ -85,11 +86,13 @@ export const Button = forwardRef(function Button(
85
86
  <Component
86
87
  ref={forwardedRef}
87
88
  id={id}
88
- className={`no-underline leading-[1] relative border-0 outline-none select-none text-left rounded flex items-center justify-center [&>*]:pointer-events-none [-webkit-app-region:no-drag] focus:ring-2 focus:ring-primary focus:shadow-[0_0_0_1px_var(--popover-background)_inset] ${disabled ? "opacity-25" : ""} ${!className?.includes("flex") ? "flex-none" : ""} ${
89
+ className={cx(
90
+ "no-underline leading-[1] relative border-0 outline-none select-none text-left rounded flex items-center justify-center [&>*]:pointer-events-none [-webkit-app-region:no-drag] focus:ring-2 focus:ring-primary focus:shadow-[0_0_0_1px_var(--popover-background)_inset]",
91
+ disabled ? "opacity-25" : "",
92
+ !className?.includes("flex") ? "flex-none" : "",
89
93
  active
90
94
  ? "bg-primary text-white hover:bg-primary-light active:bg-primary"
91
- : variantStyles[variant]
92
- } ${
95
+ : variantStyles[variant],
93
96
  sizeStyles[
94
97
  variant === "thin"
95
98
  ? "thin"
@@ -98,8 +101,9 @@ export const Button = forwardRef(function Button(
98
101
  : variant === "none"
99
102
  ? "none"
100
103
  : size
101
- ]
102
- } ${className ?? ""}`}
104
+ ],
105
+ className
106
+ )}
103
107
  style={style}
104
108
  tabIndex={tabIndex}
105
109
  disabled={disabled}