@fibery/ui-kit 1.36.0 → 1.36.2
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/CHANGELOG.md +17 -0
- package/package.json +3 -3
- package/scripts/generate-icons.mjs +15 -1
- package/src/actions-menu/actions-menu-item.tsx +2 -2
- package/src/actions-menu/actions-menu-sub-command-menu.tsx +3 -0
- package/src/avatar.tsx +26 -12
- package/src/beta-badge.tsx +17 -0
- package/src/button/base-button.tsx +3 -3
- package/src/command-menu/index.tsx +15 -21
- package/src/command-menu/nested-command-menu.tsx +356 -0
- package/src/context-menu/index.tsx +3 -3
- package/src/date-picker/relative-date-picker.tsx +20 -9
- package/src/design-system.ts +18 -14
- package/src/dropdown-menu/index.tsx +1 -1
- package/src/format-date-from-now.ts +1 -1
- package/src/icons/ast/Anthropic.ts +8 -0
- package/src/icons/ast/Csv.ts +8 -0
- package/src/icons/ast/DateRange.ts +8 -0
- package/src/icons/ast/FiberyMono.ts +1 -1
- package/src/icons/ast/OpenAi.ts +8 -0
- package/src/icons/ast/ReadOnly.ts +8 -0
- package/src/icons/ast/SpaceList.ts +8 -0
- package/src/icons/ast/SuggestIntegration.ts +8 -0
- package/src/icons/ast/index.tsx +7 -0
- package/src/icons/react/Anthropic.tsx +13 -0
- package/src/icons/react/Csv.tsx +13 -0
- package/src/icons/react/DateRange.tsx +13 -0
- package/src/icons/react/OpenAi.tsx +13 -0
- package/src/icons/react/ReadOnly.tsx +13 -0
- package/src/icons/react/SpaceList.tsx +13 -0
- package/src/icons/react/SuggestIntegration.tsx +13 -0
- package/src/icons/react/index.tsx +7 -0
- package/src/icons/svg/anthropic.svg +15 -0
- package/src/icons/svg/csv.svg +3 -0
- package/src/icons/svg/date-range.svg +4 -0
- package/src/icons/svg/fibery-mono.svg +6 -1
- package/src/icons/svg/open-ai.svg +8 -0
- package/src/icons/svg/read-only.svg +6 -0
- package/src/icons/svg/space-list.svg +4 -0
- package/src/icons/svg/suggest-integration.svg +4 -0
- package/src/icons/types.ts +2 -2
- package/src/loaders.tsx +28 -23
- package/src/logo.tsx +13 -45
- package/src/new-badge.tsx +8 -6
- package/src/number-input/number-input.tsx +37 -56
- package/src/select/custom-select-partials/group-heading.tsx +24 -1
- package/src/select/custom-select-partials/option.tsx +17 -11
- package/src/select/select-in-popover.tsx +3 -1
- package/src/select/util.ts +2 -1
- package/src/toast/utils/toastify-item-name.ts +1 -1
- package/src/type-badge-box.tsx +4 -2
- package/src/type-badge.tsx +20 -12
- package/src/unit/primitive.tsx +3 -0
- package/src/unit/styles.ts +4 -0
- package/src/unit/types.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @fibery/ui-kit
|
|
2
2
|
|
|
3
|
+
## 1.36.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 651e6f2: add noColor option to TypeBadge
|
|
8
|
+
- Updated dependencies [7976d88]
|
|
9
|
+
- @fibery/helpers@1.3.3
|
|
10
|
+
- @fibery/react@1.4.4
|
|
11
|
+
|
|
12
|
+
## 1.36.1
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- Updated dependencies [ec46f77]
|
|
17
|
+
- @fibery/helpers@1.3.2
|
|
18
|
+
- @fibery/react@1.4.3
|
|
19
|
+
|
|
3
20
|
## 1.36.0
|
|
4
21
|
|
|
5
22
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fibery/ui-kit",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"dependencies": {
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"tabbable": "5.2.1",
|
|
45
45
|
"ua-parser-js": "1.0.39",
|
|
46
46
|
"@fibery/emoji-data": "2.6.1",
|
|
47
|
-
"@fibery/helpers": "1.3.
|
|
48
|
-
"@fibery/react": "1.4.
|
|
47
|
+
"@fibery/helpers": "1.3.3",
|
|
48
|
+
"@fibery/react": "1.4.4"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"react": "^18.2.0",
|
|
@@ -82,7 +82,21 @@ for (const iconPath of iconPaths) {
|
|
|
82
82
|
const {class: className, xmlns, ...rest} = node.properties ?? {};
|
|
83
83
|
return {
|
|
84
84
|
...node,
|
|
85
|
-
properties: Object.fromEntries(
|
|
85
|
+
properties: Object.fromEntries(
|
|
86
|
+
Object.entries(rest).map(([key, value]) => {
|
|
87
|
+
if (key === "style") {
|
|
88
|
+
const style = {};
|
|
89
|
+
value.split(";").forEach((styleItem) => {
|
|
90
|
+
const [styleKey, styleValue] = styleItem.split(":").map((s) => s.trim());
|
|
91
|
+
if (styleKey && styleValue) {
|
|
92
|
+
style[_.camelCase(styleKey)] = styleValue;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
return [_.camelCase(key), style];
|
|
96
|
+
}
|
|
97
|
+
return [_.camelCase(key), value];
|
|
98
|
+
})
|
|
99
|
+
),
|
|
86
100
|
};
|
|
87
101
|
}),
|
|
88
102
|
name: _.kebabCase(name),
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import {stopPropagation} from "@fibery/react/src/stop-propagation";
|
|
2
2
|
import {useCallbackRef} from "@fibery/react/src/use-callback-ref";
|
|
3
|
-
import CheckedIcon from "../icons/react/Checked";
|
|
4
3
|
import {css, cx} from "@linaria/core";
|
|
5
4
|
import {FunctionComponent, memo, ReactNode, useId} from "react";
|
|
6
5
|
import {border, colors, layout, lineHeight, space, textStyles, themeVars} from "../design-system";
|
|
6
|
+
import CheckedIcon from "../icons/react/Checked";
|
|
7
7
|
import {IconBaseProps} from "../icons/types";
|
|
8
8
|
import {Spinner} from "../loaders";
|
|
9
9
|
import {ShortcutBadge} from "../shortcut-badge";
|
|
10
10
|
import {useTheme} from "../theme-provider";
|
|
11
|
-
import {useActiveDangerousRow, useSetActiveDangerousRow} from "./contexts/actions-menu-dangerous-rows";
|
|
12
11
|
import {useActionsMenuContext} from "./contexts/actions-menu-context";
|
|
12
|
+
import {useActiveDangerousRow, useSetActiveDangerousRow} from "./contexts/actions-menu-dangerous-rows";
|
|
13
13
|
|
|
14
14
|
const hiddenTextCss = css`
|
|
15
15
|
visibility: hidden;
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
CommandMenuItem,
|
|
11
11
|
CommandMenuList,
|
|
12
12
|
CommandMenuRoot,
|
|
13
|
+
CommandMenuSeparator,
|
|
13
14
|
} from "../command-menu";
|
|
14
15
|
import {ActionsMenuSubMenu} from "./actions-menu-sub-menu";
|
|
15
16
|
import {useActionsMenuContext} from "./contexts/actions-menu-context";
|
|
@@ -159,3 +160,5 @@ export const ActionsMenuSubCommandMenuItem: React.FC<React.ComponentPropsWithout
|
|
|
159
160
|
export const ActionsMenuSubCommandMenuEmpty = CommandMenuEmpty;
|
|
160
161
|
|
|
161
162
|
export const ActionsMenuSubCommandMenuGroup = CommandMenuGroup;
|
|
163
|
+
|
|
164
|
+
export const ActionsMenuSubCommandMenuSeparator = CommandMenuSeparator;
|
package/src/avatar.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {css} from "@linaria/core";
|
|
2
2
|
import _ from "lodash";
|
|
3
|
-
import {CSSProperties, FC, memo, useCallback, useState} from "react";
|
|
3
|
+
import {CSSProperties, FC, memo, useCallback, useMemo, useState} from "react";
|
|
4
4
|
import {getDarkenColor, getObjectColorMemoized, textStyles, themeVars} from "./design-system";
|
|
5
5
|
import {FibermojiPlaceholder} from "./fibermoji-placeholder";
|
|
6
6
|
import {getShiftStyle} from "./icons/get-shift-style";
|
|
@@ -82,6 +82,7 @@ const EMOJI_BY_BADGE = {
|
|
|
82
82
|
vacation: "🌴",
|
|
83
83
|
dayon: "💻",
|
|
84
84
|
};
|
|
85
|
+
|
|
85
86
|
function getAvatarUrl(url: string, size: number) {
|
|
86
87
|
if (!url) {
|
|
87
88
|
return url;
|
|
@@ -92,7 +93,17 @@ function getAvatarUrl(url: string, size: number) {
|
|
|
92
93
|
return urlObject.toString();
|
|
93
94
|
}
|
|
94
95
|
|
|
95
|
-
|
|
96
|
+
/**
|
|
97
|
+
* This triggers a network request if the image is not cached.
|
|
98
|
+
* Avoid using in render or performance-critical code.
|
|
99
|
+
*/
|
|
100
|
+
const isImageCached = (src: string): boolean => {
|
|
101
|
+
const image = new Image();
|
|
102
|
+
image.src = src;
|
|
103
|
+
return image.complete && image.naturalWidth !== 0;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
type AvatarImageProps = {
|
|
96
107
|
size: number;
|
|
97
108
|
containerSize?: number;
|
|
98
109
|
title?: string;
|
|
@@ -102,11 +113,8 @@ interface AvatarImageProps {
|
|
|
102
113
|
backgroundColor?: string;
|
|
103
114
|
disableNativeTitle?: boolean;
|
|
104
115
|
placeholder?: React.ReactNode;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
*
|
|
108
|
-
* @type {React.NamedExoticComponent<{readonly badges?: *, readonly backgroundColor?: *, readonly size?: *, readonly avatarUrl?: *, readonly disableNativeTitle?: *, readonly title?: *, readonly radius?: *, readonly containerSize?: *}>}
|
|
109
|
-
*/
|
|
116
|
+
};
|
|
117
|
+
|
|
110
118
|
export const AvatarImage: FC<AvatarImageProps> = memo(function AvatarImage({
|
|
111
119
|
size,
|
|
112
120
|
containerSize,
|
|
@@ -118,7 +126,13 @@ export const AvatarImage: FC<AvatarImageProps> = memo(function AvatarImage({
|
|
|
118
126
|
disableNativeTitle = false,
|
|
119
127
|
placeholder,
|
|
120
128
|
}) {
|
|
121
|
-
const
|
|
129
|
+
const initialFetchState = useMemo(() => {
|
|
130
|
+
if (avatarUrl && isImageCached(avatarUrl)) {
|
|
131
|
+
return ":fetch-state/success";
|
|
132
|
+
}
|
|
133
|
+
return ":fetch-state/start";
|
|
134
|
+
}, [avatarUrl]);
|
|
135
|
+
const [fetchState, setFetchState] = useState(initialFetchState);
|
|
122
136
|
const onError = useCallback(() => {
|
|
123
137
|
setFetchState(":fetch-state/error");
|
|
124
138
|
}, []);
|
|
@@ -193,12 +207,12 @@ const getAvatarPlaceholderStyle = ({
|
|
|
193
207
|
borderRadius: radius ?? "50%",
|
|
194
208
|
});
|
|
195
209
|
|
|
196
|
-
|
|
210
|
+
type AvatarPlaceholderProps = {
|
|
197
211
|
size: number;
|
|
198
212
|
title?: string;
|
|
199
213
|
radius?: number;
|
|
200
214
|
backgroundColor?: string;
|
|
201
|
-
}
|
|
215
|
+
};
|
|
202
216
|
|
|
203
217
|
export const AvatarPlaceholder: FC<AvatarPlaceholderProps> = memo(function AvatarPlaceholder(props) {
|
|
204
218
|
const features = useFeatures<{enableFibermojiAvatars: boolean}>();
|
|
@@ -237,10 +251,10 @@ const getNullUserAvatarStyle = ({size, containerSize}: {size: number; containerS
|
|
|
237
251
|
|
|
238
252
|
const magicCoefficient = 4.5;
|
|
239
253
|
|
|
240
|
-
|
|
254
|
+
type NullUserAvatarProps = {
|
|
241
255
|
size: number;
|
|
242
256
|
containerSize: number;
|
|
243
|
-
}
|
|
257
|
+
};
|
|
244
258
|
|
|
245
259
|
export const NullUserAvatar: FC<NullUserAvatarProps> = memo(function NullUserAvatar({size, containerSize}) {
|
|
246
260
|
const dashSize = (Math.PI * size) / magicCoefficient / 12;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {css} from "@linaria/core";
|
|
2
|
+
import {fontWeight, space, textStyles, themeVars} from "./design-system";
|
|
3
|
+
|
|
4
|
+
const betaBadgeStyle = css`
|
|
5
|
+
${textStyles.heading7}
|
|
6
|
+
text-transform: none;
|
|
7
|
+
font-weight: ${fontWeight.medium};
|
|
8
|
+
display: inline-block;
|
|
9
|
+
padding: ${space.s2}px 4.5px ${space.s2}px 5px;
|
|
10
|
+
color: ${themeVars.colorAI};
|
|
11
|
+
background-color: ${themeVars.colorBgAI};
|
|
12
|
+
vertical-align: text-top;
|
|
13
|
+
border-radius: 4px;
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
type Props = {children: React.ReactNode};
|
|
17
|
+
export const BetaBadge: React.FC<Props> = ({children}) => <span className={betaBadgeStyle}>{children}</span>;
|
|
@@ -21,7 +21,7 @@ export const baseButton = css`
|
|
|
21
21
|
cursor: pointer;
|
|
22
22
|
user-select: none;
|
|
23
23
|
|
|
24
|
-
transition-property: border, background-color, color, opacity;
|
|
24
|
+
transition-property: border, background-color, color, opacity, box-shadow;
|
|
25
25
|
transition-duration: 0.15s;
|
|
26
26
|
|
|
27
27
|
color: var(--fibery-button-text-color);
|
|
@@ -29,7 +29,7 @@ export const baseButton = css`
|
|
|
29
29
|
|
|
30
30
|
background-color: var(--fibery-button-color);
|
|
31
31
|
|
|
32
|
-
box-shadow:
|
|
32
|
+
box-shadow: 0 0 0 0.5px var(--fibery-button-border-color);
|
|
33
33
|
|
|
34
34
|
&:hover:not(:disabled) {
|
|
35
35
|
background-color: var(--fibery-button-hover-color);
|
|
@@ -90,7 +90,7 @@ const getColors = (variant: ButtonVariant, color: ButtonColor) => {
|
|
|
90
90
|
|
|
91
91
|
if (variantMod === "Outline") {
|
|
92
92
|
borderColors = {
|
|
93
|
-
"--fibery-button-border-color": themeVars[`colorBorderButton${variantMod}${colorMod}Default`
|
|
93
|
+
"--fibery-button-border-color": themeVars[`colorBorderButton${variantMod}${colorMod}Default`],
|
|
94
94
|
};
|
|
95
95
|
}
|
|
96
96
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import {useComposedRefs} from "@fibery/react/src/use-composed-refs";
|
|
2
|
+
import {useControllableState} from "@fibery/react/src/use-controllable-state";
|
|
3
|
+
import {css, cx} from "@linaria/core";
|
|
3
4
|
import {styled} from "@linaria/react";
|
|
5
|
+
import {CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandRoot, CommandSeparator} from "cmdk";
|
|
4
6
|
import {forwardRef, useRef, useState} from "react";
|
|
5
|
-
import
|
|
6
|
-
import {css, cx} from "@linaria/core";
|
|
7
|
+
import {border, layout, space, textStyles, themeVars} from "../design-system";
|
|
7
8
|
import ClearInputFilled from "../icons/react/ClearInputFilled";
|
|
8
|
-
import
|
|
9
|
-
import {useControllableState} from "@fibery/react/src/use-controllable-state";
|
|
9
|
+
import Search from "../icons/react/Search";
|
|
10
10
|
|
|
11
11
|
export const CommandMenuRoot = styled(CommandRoot)`
|
|
12
12
|
display: flex;
|
|
@@ -101,7 +101,7 @@ export const CommandMenuGroup = styled(CommandGroup)`
|
|
|
101
101
|
${textStyles.small}
|
|
102
102
|
display: flex;
|
|
103
103
|
align-items: center;
|
|
104
|
-
padding: 0 ${space.
|
|
104
|
+
padding: 0 ${space.s12}px;
|
|
105
105
|
min-height: ${layout.groupTitleHeight}px;
|
|
106
106
|
color: ${themeVars.accentTextColor};
|
|
107
107
|
}
|
|
@@ -113,11 +113,11 @@ export const CommandMenuSeparator = styled(CommandSeparator)`
|
|
|
113
113
|
margin: ${space.s4}px 0;
|
|
114
114
|
`;
|
|
115
115
|
|
|
116
|
-
const commandItemCss = css`
|
|
116
|
+
export const commandItemCss = css`
|
|
117
117
|
display: flex;
|
|
118
118
|
align-items: center;
|
|
119
119
|
gap: ${space.s6}px;
|
|
120
|
-
padding: 0 ${space.
|
|
120
|
+
padding: 0 ${space.s8}px;
|
|
121
121
|
cursor: pointer;
|
|
122
122
|
min-height: ${layout.menuItemHeight}px;
|
|
123
123
|
margin: 0 ${space.s4}px;
|
|
@@ -128,6 +128,11 @@ const commandItemCss = css`
|
|
|
128
128
|
background-color: ${themeVars.colorBgActionsMenuItemHover};
|
|
129
129
|
}
|
|
130
130
|
`;
|
|
131
|
+
|
|
132
|
+
const disabledItemCss = css`
|
|
133
|
+
cursor: not-allowed;
|
|
134
|
+
`;
|
|
135
|
+
|
|
131
136
|
const commandItemTitleCss = css`
|
|
132
137
|
flex: 1 1 0;
|
|
133
138
|
white-space: nowrap;
|
|
@@ -135,17 +140,6 @@ const commandItemTitleCss = css`
|
|
|
135
140
|
overflow: hidden;
|
|
136
141
|
`;
|
|
137
142
|
|
|
138
|
-
export const CommandExpressionSelectItem = forwardRef<
|
|
139
|
-
React.ElementRef<typeof CommandItem>,
|
|
140
|
-
React.ComponentPropsWithoutRef<typeof CommandItem>
|
|
141
|
-
>(function CommandMenuItem({className, children, ...props}, ref) {
|
|
142
|
-
return (
|
|
143
|
-
<CommandItem ref={ref} className={cx(commandItemCss, className)} {...props}>
|
|
144
|
-
{children}
|
|
145
|
-
</CommandItem>
|
|
146
|
-
);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
143
|
export const CommandMenuItem = forwardRef<
|
|
150
144
|
React.ElementRef<typeof CommandItem>,
|
|
151
145
|
React.ComponentPropsWithoutRef<typeof CommandItem> & {
|
|
@@ -156,7 +150,7 @@ export const CommandMenuItem = forwardRef<
|
|
|
156
150
|
const [showTitle, setShowTitle] = useState(false);
|
|
157
151
|
|
|
158
152
|
return (
|
|
159
|
-
<CommandItem ref={ref} className={cx(commandItemCss, className)} {...props}>
|
|
153
|
+
<CommandItem ref={ref} className={cx(commandItemCss, className, props.disabled && disabledItemCss)} {...props}>
|
|
160
154
|
{icon}
|
|
161
155
|
<span
|
|
162
156
|
title={showTitle ? (children as string) : ""}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import {CommandItem} from "cmdk";
|
|
2
|
+
import React, {forwardRef, ReactNode, useCallback, useEffect, useMemo, useRef, useState} from "react";
|
|
3
|
+
import {css, cx} from "@linaria/core";
|
|
4
|
+
import {border, space, themeVars} from "../design-system";
|
|
5
|
+
import Back from "../icons/react/Back";
|
|
6
|
+
import {
|
|
7
|
+
commandItemCss,
|
|
8
|
+
CommandMenuEmpty,
|
|
9
|
+
CommandMenuGroup,
|
|
10
|
+
CommandMenuInput,
|
|
11
|
+
CommandMenuItem,
|
|
12
|
+
CommandMenuList,
|
|
13
|
+
CommandMenuRoot,
|
|
14
|
+
CommandMenuSeparator,
|
|
15
|
+
} from "./index";
|
|
16
|
+
import cn from "classnames";
|
|
17
|
+
import {Tooltip} from "../tooltip";
|
|
18
|
+
import {IconButton} from "../button/icon-button";
|
|
19
|
+
import ArrowRight from "../icons/react/ArrowRight";
|
|
20
|
+
|
|
21
|
+
const selectedItemClassName = css`
|
|
22
|
+
background-color: ${themeVars.colorBgListItemGeneralSelected};
|
|
23
|
+
|
|
24
|
+
&[data-selected="true"] {
|
|
25
|
+
background-color: ${themeVars.colorBgListItemGeneralSelectedHover};
|
|
26
|
+
}
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
const arrowWrapperClassName = css`
|
|
30
|
+
flex-grow: 0;
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
export type NestedMenuItem<T> = {
|
|
34
|
+
key: string;
|
|
35
|
+
title?: string;
|
|
36
|
+
icon?: ReactNode;
|
|
37
|
+
value: T;
|
|
38
|
+
groupItems?: Array<NestedMenuItem<T>>;
|
|
39
|
+
filterPlaceholder?: string;
|
|
40
|
+
emptyMessage?: string;
|
|
41
|
+
calculateNestedItems?: () => Array<NestedMenuItem<T>>;
|
|
42
|
+
kind?: "separator" | "group";
|
|
43
|
+
showNestedItemsOnClick?: boolean;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type NestedMenuStack<T> = Array<{
|
|
47
|
+
item: NestedMenuItem<T>;
|
|
48
|
+
nestedItems: NestedMenuItem<T>[];
|
|
49
|
+
}>;
|
|
50
|
+
|
|
51
|
+
type Props<T> = {
|
|
52
|
+
defaultStack?: NestedMenuStack<T>;
|
|
53
|
+
defaultValue?: string;
|
|
54
|
+
rootItems: Array<NestedMenuItem<T>>;
|
|
55
|
+
onChange: (item: NestedMenuItem<T>) => void;
|
|
56
|
+
filterPlaceholder: string;
|
|
57
|
+
emptyMessage: string;
|
|
58
|
+
getArrowTooltipMessage: (item: NestedMenuItem<T>) => string;
|
|
59
|
+
maxDepthLevel?: number;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const NestedCommandMenuItem = forwardRef<
|
|
63
|
+
React.ElementRef<typeof CommandItem>,
|
|
64
|
+
React.ComponentPropsWithoutRef<typeof CommandItem>
|
|
65
|
+
>(function CommandMenuItem({className, children, ...props}, ref) {
|
|
66
|
+
return (
|
|
67
|
+
<CommandItem ref={ref} className={cx(commandItemCss, className)} {...props}>
|
|
68
|
+
{children}
|
|
69
|
+
</CommandItem>
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
function isItemMatchFilter<T>(item: NestedMenuItem<T>, lowerCaseFilterValue: string) {
|
|
74
|
+
return item.title?.toLowerCase()?.includes(lowerCaseFilterValue);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function NestedCommandMenu<T>({
|
|
78
|
+
defaultStack,
|
|
79
|
+
defaultValue,
|
|
80
|
+
rootItems,
|
|
81
|
+
onChange,
|
|
82
|
+
filterPlaceholder,
|
|
83
|
+
emptyMessage,
|
|
84
|
+
getArrowTooltipMessage,
|
|
85
|
+
maxDepthLevel = Number.MAX_VALUE,
|
|
86
|
+
}: Props<T>) {
|
|
87
|
+
const [stack, setStack] = React.useState<NestedMenuStack<T>>(defaultStack || []);
|
|
88
|
+
const currentStackItem = stack[stack.length - 1];
|
|
89
|
+
|
|
90
|
+
const currentItems = currentStackItem?.nestedItems || rootItems;
|
|
91
|
+
|
|
92
|
+
const onGoToRelation = useCallback(
|
|
93
|
+
(key: string) => {
|
|
94
|
+
const item = currentItems.find((item) => item.key === key)!;
|
|
95
|
+
setStack((items) => [...items, {item, nestedItems: item.calculateNestedItems?.() || []}]);
|
|
96
|
+
setFilter("");
|
|
97
|
+
},
|
|
98
|
+
[currentItems]
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const onCmdkItemSelect = useCallback(
|
|
102
|
+
(item: NestedMenuItem<T>) => {
|
|
103
|
+
onChange(item);
|
|
104
|
+
},
|
|
105
|
+
[onChange]
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const [filter, setFilter] = useState("");
|
|
109
|
+
|
|
110
|
+
const filteredOptions = useMemo(() => {
|
|
111
|
+
const lowerCaseFilterValue = filter.toLowerCase();
|
|
112
|
+
if (!filter) {
|
|
113
|
+
return currentItems;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return currentItems
|
|
117
|
+
.map((item) => {
|
|
118
|
+
if (item.kind === "group") {
|
|
119
|
+
const groupItems = (item.groupItems || []).filter((groupItem) =>
|
|
120
|
+
isItemMatchFilter(groupItem, lowerCaseFilterValue)
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
return groupItems.length > 0 ? {...item, groupItems} : null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return isItemMatchFilter(item, lowerCaseFilterValue) ? item : null;
|
|
127
|
+
})
|
|
128
|
+
.filter(Boolean) as Array<NestedMenuItem<T>>;
|
|
129
|
+
}, [filter, currentItems]);
|
|
130
|
+
|
|
131
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
132
|
+
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
setTimeout(() => {
|
|
135
|
+
inputRef.current?.focus();
|
|
136
|
+
}, 0);
|
|
137
|
+
}, [stack]);
|
|
138
|
+
|
|
139
|
+
const menuListRef = useRef<HTMLDivElement>(null);
|
|
140
|
+
const scrollToTop = useCallback(() => {
|
|
141
|
+
menuListRef.current?.scrollTo({top: 0, behavior: "instant"});
|
|
142
|
+
}, []);
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
scrollToTop();
|
|
145
|
+
}, [stack, scrollToTop]);
|
|
146
|
+
|
|
147
|
+
const [value, setValue] = React.useState(defaultValue);
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<>
|
|
151
|
+
{stack.length > 0 && (
|
|
152
|
+
<>
|
|
153
|
+
<div
|
|
154
|
+
className={css`
|
|
155
|
+
cursor: pointer;
|
|
156
|
+
|
|
157
|
+
&:hover {
|
|
158
|
+
background-color: ${themeVars.colorBgActionsMenuItemHover};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
display: flex;
|
|
162
|
+
padding: ${space.s6}px ${space.s8}px;
|
|
163
|
+
border-radius: ${border.radius6}px;
|
|
164
|
+
margin: 0 ${space.s4}px;
|
|
165
|
+
align-items: center;
|
|
166
|
+
gap: ${space.s8}px;
|
|
167
|
+
`}
|
|
168
|
+
onClick={() => {
|
|
169
|
+
setStack((relations) => relations.slice(0, -1));
|
|
170
|
+
setFilter("");
|
|
171
|
+
}}
|
|
172
|
+
>
|
|
173
|
+
<div>
|
|
174
|
+
<Back iconSize={16} aria-hidden />
|
|
175
|
+
</div>
|
|
176
|
+
<div>
|
|
177
|
+
Back
|
|
178
|
+
{stack.length > 1 ? ` to ${stack[stack.length - 2].item.title}` : ""}
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
<div
|
|
182
|
+
className={css`
|
|
183
|
+
background-color: ${themeVars.separatorColor};
|
|
184
|
+
height: 1px;
|
|
185
|
+
margin-top: ${space.s4}px;
|
|
186
|
+
`}
|
|
187
|
+
/>
|
|
188
|
+
</>
|
|
189
|
+
)}
|
|
190
|
+
|
|
191
|
+
<CommandMenuRoot
|
|
192
|
+
shouldFilter={false}
|
|
193
|
+
value={value}
|
|
194
|
+
onValueChange={setValue}
|
|
195
|
+
onKeyDown={(e) => {
|
|
196
|
+
if (e.key === "Escape" || (e.key === "Backspace" && !filter && stack.length > 0)) {
|
|
197
|
+
e.preventDefault();
|
|
198
|
+
setStack((relations) => relations.slice(0, -1));
|
|
199
|
+
} else if (e.key === "ArrowRight") {
|
|
200
|
+
const currentItem = currentItems.find((item) => item.key === value);
|
|
201
|
+
if (currentItem && currentItem.calculateNestedItems) {
|
|
202
|
+
e.preventDefault();
|
|
203
|
+
onGoToRelation(currentItem.key);
|
|
204
|
+
}
|
|
205
|
+
} else if (e.key === "ArrowLeft" && stack.length > 0) {
|
|
206
|
+
e.preventDefault();
|
|
207
|
+
setStack((relations) => relations.slice(0, -1));
|
|
208
|
+
}
|
|
209
|
+
}}
|
|
210
|
+
>
|
|
211
|
+
<CommandMenuInput
|
|
212
|
+
ref={inputRef}
|
|
213
|
+
value={filter}
|
|
214
|
+
onValueChange={setFilter}
|
|
215
|
+
placeholder={currentStackItem?.item?.filterPlaceholder || filterPlaceholder}
|
|
216
|
+
/>
|
|
217
|
+
<CommandMenuList
|
|
218
|
+
ref={menuListRef}
|
|
219
|
+
className={css`
|
|
220
|
+
&& {
|
|
221
|
+
transition: none;
|
|
222
|
+
max-height: 300px;
|
|
223
|
+
}
|
|
224
|
+
`}
|
|
225
|
+
>
|
|
226
|
+
<CommandMenuEmpty>{currentStackItem?.item?.emptyMessage || emptyMessage}</CommandMenuEmpty>
|
|
227
|
+
{renderLevel({
|
|
228
|
+
options: filteredOptions,
|
|
229
|
+
stack,
|
|
230
|
+
maxDepthLevel,
|
|
231
|
+
onCmdkItemSelect,
|
|
232
|
+
onGoToRelation,
|
|
233
|
+
getArrowTooltipMessage,
|
|
234
|
+
defaultValue,
|
|
235
|
+
})}
|
|
236
|
+
</CommandMenuList>
|
|
237
|
+
</CommandMenuRoot>
|
|
238
|
+
</>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function renderLevel<T>({
|
|
243
|
+
options,
|
|
244
|
+
defaultValue,
|
|
245
|
+
stack,
|
|
246
|
+
maxDepthLevel,
|
|
247
|
+
onCmdkItemSelect,
|
|
248
|
+
onGoToRelation,
|
|
249
|
+
getArrowTooltipMessage,
|
|
250
|
+
}: {
|
|
251
|
+
options: Array<NestedMenuItem<T>>;
|
|
252
|
+
defaultValue?: string;
|
|
253
|
+
stack: NestedMenuStack<T>;
|
|
254
|
+
maxDepthLevel: number;
|
|
255
|
+
onCmdkItemSelect: (item: NestedMenuItem<T>) => void;
|
|
256
|
+
onGoToRelation: (key: string) => void;
|
|
257
|
+
getArrowTooltipMessage: (item: NestedMenuItem<T>) => string;
|
|
258
|
+
}) {
|
|
259
|
+
return options.map((item) => {
|
|
260
|
+
if (item.kind === "separator") {
|
|
261
|
+
return <CommandMenuSeparator key={item.key} />;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (item.kind === "group") {
|
|
265
|
+
const nestedItems = item.groupItems || [];
|
|
266
|
+
if (nestedItems.length > 0) {
|
|
267
|
+
return (
|
|
268
|
+
<CommandMenuGroup heading={item.title} key={item.key}>
|
|
269
|
+
{renderLevel({
|
|
270
|
+
options: nestedItems,
|
|
271
|
+
defaultValue,
|
|
272
|
+
stack,
|
|
273
|
+
maxDepthLevel,
|
|
274
|
+
onCmdkItemSelect,
|
|
275
|
+
onGoToRelation,
|
|
276
|
+
getArrowTooltipMessage,
|
|
277
|
+
})}
|
|
278
|
+
</CommandMenuGroup>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const isSelected = item.key === defaultValue;
|
|
286
|
+
if (item.calculateNestedItems && stack.length < maxDepthLevel) {
|
|
287
|
+
return (
|
|
288
|
+
<NestedCommandMenuItem
|
|
289
|
+
key={item.key}
|
|
290
|
+
value={item.key}
|
|
291
|
+
onSelect={() => {
|
|
292
|
+
if (item.showNestedItemsOnClick) {
|
|
293
|
+
onGoToRelation(item.key);
|
|
294
|
+
} else {
|
|
295
|
+
onCmdkItemSelect(item);
|
|
296
|
+
}
|
|
297
|
+
}}
|
|
298
|
+
asChild
|
|
299
|
+
className={cn(
|
|
300
|
+
css`
|
|
301
|
+
padding-right: 0;
|
|
302
|
+
`,
|
|
303
|
+
isSelected ? selectedItemClassName : undefined
|
|
304
|
+
)}
|
|
305
|
+
>
|
|
306
|
+
<div
|
|
307
|
+
className={css`
|
|
308
|
+
display: flex;
|
|
309
|
+
align-items: center;
|
|
310
|
+
justify-content: space-between;
|
|
311
|
+
gap: ${space.s6}px;
|
|
312
|
+
|
|
313
|
+
&[data-selected="true"]:has(.${arrowWrapperClassName}:hover) {
|
|
314
|
+
background-color: transparent;
|
|
315
|
+
}
|
|
316
|
+
`}
|
|
317
|
+
>
|
|
318
|
+
{item.icon}
|
|
319
|
+
<div
|
|
320
|
+
className={css`
|
|
321
|
+
flex: 1;
|
|
322
|
+
`}
|
|
323
|
+
>
|
|
324
|
+
{item.title}
|
|
325
|
+
</div>
|
|
326
|
+
<div className={arrowWrapperClassName}>
|
|
327
|
+
<Tooltip title={getArrowTooltipMessage(item)}>
|
|
328
|
+
<IconButton
|
|
329
|
+
size={"medium"}
|
|
330
|
+
onClick={(e) => {
|
|
331
|
+
e.stopPropagation();
|
|
332
|
+
onGoToRelation(item.key);
|
|
333
|
+
}}
|
|
334
|
+
>
|
|
335
|
+
<ArrowRight color={themeVars.iconColor} containerSize={10} aria-hidden />
|
|
336
|
+
</IconButton>
|
|
337
|
+
</Tooltip>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
</NestedCommandMenuItem>
|
|
341
|
+
);
|
|
342
|
+
} else {
|
|
343
|
+
return (
|
|
344
|
+
<CommandMenuItem
|
|
345
|
+
key={item.key}
|
|
346
|
+
value={item.key}
|
|
347
|
+
onSelect={() => onCmdkItemSelect(item)}
|
|
348
|
+
icon={item.icon}
|
|
349
|
+
className={isSelected ? selectedItemClassName : undefined}
|
|
350
|
+
>
|
|
351
|
+
{item.title}
|
|
352
|
+
</CommandMenuItem>
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
}
|
|
@@ -18,7 +18,7 @@ export const rowItemStyles = {
|
|
|
18
18
|
minHeight: layout.menuItemHeight,
|
|
19
19
|
outline: "none",
|
|
20
20
|
borderRadius: border.radius0,
|
|
21
|
-
padding: `0px ${space.
|
|
21
|
+
padding: `0px ${space.s8}px`,
|
|
22
22
|
margin: `0px ${space.s4}px`,
|
|
23
23
|
cursor: "pointer",
|
|
24
24
|
|
|
@@ -101,8 +101,8 @@ export const Item = styled(ContextMenuPrimitive.Item)`
|
|
|
101
101
|
|
|
102
102
|
export const Separator = styled(ContextMenuPrimitive.Separator)`
|
|
103
103
|
${{
|
|
104
|
-
backgroundColor: themeVars.
|
|
105
|
-
height:
|
|
104
|
+
backgroundColor: themeVars.actionMenuSeparatorColor,
|
|
105
|
+
height: 0.5,
|
|
106
106
|
margin: `${space.s4}px 0`,
|
|
107
107
|
}}
|
|
108
108
|
`;
|