@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.
Files changed (55) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/package.json +3 -3
  3. package/scripts/generate-icons.mjs +15 -1
  4. package/src/actions-menu/actions-menu-item.tsx +2 -2
  5. package/src/actions-menu/actions-menu-sub-command-menu.tsx +3 -0
  6. package/src/avatar.tsx +26 -12
  7. package/src/beta-badge.tsx +17 -0
  8. package/src/button/base-button.tsx +3 -3
  9. package/src/command-menu/index.tsx +15 -21
  10. package/src/command-menu/nested-command-menu.tsx +356 -0
  11. package/src/context-menu/index.tsx +3 -3
  12. package/src/date-picker/relative-date-picker.tsx +20 -9
  13. package/src/design-system.ts +18 -14
  14. package/src/dropdown-menu/index.tsx +1 -1
  15. package/src/format-date-from-now.ts +1 -1
  16. package/src/icons/ast/Anthropic.ts +8 -0
  17. package/src/icons/ast/Csv.ts +8 -0
  18. package/src/icons/ast/DateRange.ts +8 -0
  19. package/src/icons/ast/FiberyMono.ts +1 -1
  20. package/src/icons/ast/OpenAi.ts +8 -0
  21. package/src/icons/ast/ReadOnly.ts +8 -0
  22. package/src/icons/ast/SpaceList.ts +8 -0
  23. package/src/icons/ast/SuggestIntegration.ts +8 -0
  24. package/src/icons/ast/index.tsx +7 -0
  25. package/src/icons/react/Anthropic.tsx +13 -0
  26. package/src/icons/react/Csv.tsx +13 -0
  27. package/src/icons/react/DateRange.tsx +13 -0
  28. package/src/icons/react/OpenAi.tsx +13 -0
  29. package/src/icons/react/ReadOnly.tsx +13 -0
  30. package/src/icons/react/SpaceList.tsx +13 -0
  31. package/src/icons/react/SuggestIntegration.tsx +13 -0
  32. package/src/icons/react/index.tsx +7 -0
  33. package/src/icons/svg/anthropic.svg +15 -0
  34. package/src/icons/svg/csv.svg +3 -0
  35. package/src/icons/svg/date-range.svg +4 -0
  36. package/src/icons/svg/fibery-mono.svg +6 -1
  37. package/src/icons/svg/open-ai.svg +8 -0
  38. package/src/icons/svg/read-only.svg +6 -0
  39. package/src/icons/svg/space-list.svg +4 -0
  40. package/src/icons/svg/suggest-integration.svg +4 -0
  41. package/src/icons/types.ts +2 -2
  42. package/src/loaders.tsx +28 -23
  43. package/src/logo.tsx +13 -45
  44. package/src/new-badge.tsx +8 -6
  45. package/src/number-input/number-input.tsx +37 -56
  46. package/src/select/custom-select-partials/group-heading.tsx +24 -1
  47. package/src/select/custom-select-partials/option.tsx +17 -11
  48. package/src/select/select-in-popover.tsx +3 -1
  49. package/src/select/util.ts +2 -1
  50. package/src/toast/utils/toastify-item-name.ts +1 -1
  51. package/src/type-badge-box.tsx +4 -2
  52. package/src/type-badge.tsx +20 -12
  53. package/src/unit/primitive.tsx +3 -0
  54. package/src/unit/styles.ts +4 -0
  55. 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.0",
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.1",
48
- "@fibery/react": "1.4.2"
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(Object.entries(rest).map(([key, value]) => [_.camelCase(key), value])),
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
- interface AvatarImageProps {
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 [fetchState, setFetchState] = useState(":fetch-state/start");
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
- interface AvatarPlaceholderProps {
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
- interface NullUserAvatarProps {
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: inset 0 0 0 0.5px var(--fibery-button-border-color);
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` as const],
93
+ "--fibery-button-border-color": themeVars[`colorBorderButton${variantMod}${colorMod}Default`],
94
94
  };
95
95
  }
96
96
 
@@ -1,12 +1,12 @@
1
- import {CommandInput, CommandItem, CommandList, CommandGroup, CommandRoot, CommandEmpty, CommandSeparator} from "cmdk";
2
- import {border, layout, space, textStyles, themeVars} from "../design-system";
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 Search from "../icons/react/Search";
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 {useComposedRefs} from "@fibery/react/src/use-composed-refs";
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.s8}px;
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.s12}px;
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.s12}px`,
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.separatorColor,
105
- height: 1,
104
+ backgroundColor: themeVars.actionMenuSeparatorColor,
105
+ height: 0.5,
106
106
  margin: `${space.s4}px 0`,
107
107
  }}
108
108
  `;