@noya-app/noya-designsystem 0.1.39 → 0.1.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +10 -0
- package/dist/index.css +1 -1
- package/dist/index.d.mts +416 -321
- package/dist/index.d.ts +416 -321
- package/dist/index.js +9558 -11324
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +5694 -7465
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/components/ActivityIndicator.tsx +13 -4
- package/src/components/AnimatePresence.tsx +261 -0
- package/src/components/Avatar.tsx +202 -80
- package/src/components/Button.tsx +10 -6
- package/src/components/Checkbox.tsx +6 -1
- package/src/components/Chip.tsx +88 -132
- package/src/components/{InputFieldWithCompletions.tsx → Combobox.tsx} +10 -3
- package/src/components/ContextMenu.tsx +9 -5
- package/src/components/Dialog.tsx +20 -23
- package/src/components/Divider.tsx +15 -7
- package/src/components/DraggableMenuButton.tsx +9 -5
- package/src/components/DropdownMenu.tsx +8 -5
- package/src/components/FillInputField.tsx +5 -1
- package/src/components/FillPreviewBackground.tsx +1 -5
- package/src/components/GridView.tsx +71 -46
- package/src/components/InputField.tsx +140 -75
- package/src/components/InspectorPrimitives.tsx +17 -11
- package/src/components/Label.tsx +5 -1
- package/src/components/ListView.tsx +28 -15
- package/src/components/Progress.tsx +10 -7
- package/src/components/SelectMenu.tsx +20 -8
- package/src/components/Slider.tsx +70 -8
- package/src/components/Sortable.tsx +3 -3
- package/src/components/Switch.tsx +19 -2
- package/src/components/Text.tsx +5 -1
- package/src/components/TextArea.tsx +5 -1
- package/src/components/TreeView.tsx +4 -10
- package/src/components/UserPointer.tsx +170 -0
- package/src/components/WorkspaceLayout.tsx +5 -0
- package/src/components/internal/Menu.tsx +14 -5
- package/src/components/internal/TextInput.tsx +13 -0
- package/src/contexts/DialogContext.tsx +30 -18
- package/src/hooks/useTheme.ts +50 -0
- package/src/index.css +16 -13
- package/src/index.tsx +14 -26
- package/src/utils/classNames.ts +5 -0
- package/src/utils/colorFromString.ts +44 -0
- package/tailwind.config.ts +16 -13
- package/src/hooks/useDarkMode.ts +0 -14
- 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.
|
|
3
|
+
"version": "0.1.40",
|
|
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.
|
|
27
|
-
"@noya-app/noya-utils": "0.1.
|
|
28
|
-
"@noya-app/noya-geometry": "0.1.
|
|
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",
|
|
@@ -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=
|
|
19
|
-
<div className=
|
|
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
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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 =
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
83
|
-
<
|
|
84
|
-
|
|
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 =
|
|
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
|
|
97
|
-
|
|
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
|
|
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={
|
|
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-
|
|
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={
|
|
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
|
-
|
|
104
|
+
],
|
|
105
|
+
className
|
|
106
|
+
)}
|
|
103
107
|
style={style}
|
|
104
108
|
tabIndex={tabIndex}
|
|
105
109
|
disabled={disabled}
|
|
@@ -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={
|
|
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
|