@bitrise/bitkit 13.324.0 → 13.326.0

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bitrise/bitkit",
3
3
  "description": "Bitrise React component library",
4
- "version": "13.324.0",
4
+ "version": "13.326.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+ssh://git@github.com/bitrise-io/bitkit.git"
@@ -38,10 +38,10 @@
38
38
  "chakra-ui-2--theme": "npm:@chakra-ui/theme@3.4.9",
39
39
  "chakra-ui-2--theme-tools": "npm:@chakra-ui/theme-tools@2.2.9",
40
40
  "clsx": "^2.1.1",
41
- "framer-motion": "^12.23.24",
41
+ "framer-motion": "^12.25.0",
42
42
  "luxon": "^3.7.2",
43
- "react": "^18.3.1",
44
- "react-dom": "^18.3.1",
43
+ "react": "^18.2.0",
44
+ "react-dom": "^18.2.0",
45
45
  "react-focus-lock": "2.13.7",
46
46
  "react-imask": "^7.6.1",
47
47
  "react-markdown": "^10.1.0"
@@ -56,14 +56,14 @@
56
56
  "@babel/preset-react": "^7.28.5",
57
57
  "@babel/preset-typescript": "^7.28.5",
58
58
  "@bitrise/eslint-plugin": "^2.12.0",
59
- "@google-cloud/storage": "^7.17.3",
60
- "@storybook/addon-docs": "^9.1.16",
61
- "@storybook/addon-links": "^9.1.16",
59
+ "@google-cloud/storage": "^7.18.0",
60
+ "@storybook/addon-docs": "^9.1.17",
61
+ "@storybook/addon-links": "^9.1.17",
62
62
  "@storybook/addon-webpack5-compiler-swc": "^3.0.0",
63
- "@storybook/react-webpack5": "^9.1.16",
63
+ "@storybook/react-webpack5": "^9.1.17",
64
64
  "@testing-library/dom": "^10.4.1",
65
65
  "@testing-library/jest-dom": "6.9.1",
66
- "@testing-library/react": "16.3.0",
66
+ "@testing-library/react": "16.3.1",
67
67
  "@testing-library/user-event": "^14.6.1",
68
68
  "@types/jest": "^29.5.14",
69
69
  "@types/luxon": "^3.7.1",
@@ -79,11 +79,11 @@
79
79
  "jest-environment-jsdom": "^29.7.0",
80
80
  "jsdom": "26.1.0",
81
81
  "lodash": "^4.17.21",
82
- "prettier": "^3.7.3",
83
- "react-hook-form": "^7.67.0",
84
- "release-it": "^19.0.6",
85
- "storybook": "^9.1.16",
86
- "ts-jest": "^29.4.5",
82
+ "prettier": "^3.7.4",
83
+ "react-hook-form": "^7.70.0",
84
+ "release-it": "^19.2.3",
85
+ "storybook": "^9.1.17",
86
+ "ts-jest": "^29.4.6",
87
87
  "typescript": "^5.9.3"
88
88
  },
89
89
  "files": [
@@ -4,8 +4,8 @@ import { BadgeColorScheme, BadgeProps } from './Badge';
4
4
  const baseStyle: SystemStyleObject = {
5
5
  borderRadius: '4',
6
6
  display: 'inline-flex',
7
+ alignItems: 'center',
7
8
  gap: '4',
8
- padding: '4',
9
9
  textStyle: 'comp/badge/sm',
10
10
  };
11
11
 
@@ -87,15 +87,62 @@ const getVariant = (props: BadgeProps) => {
87
87
  return colors[variant || 'subtle'][colorScheme || 'neutral'];
88
88
  };
89
89
 
90
+ const sizes: Record<'xs' | 'xxs', SystemStyleObject> = {
91
+ xs: {
92
+ height: 'fit-content',
93
+ width: 'fit-content',
94
+ padding: '4px',
95
+ display: 'flex',
96
+ justifyContent: 'center',
97
+ alignItems: 'center',
98
+ '&.has-children': {
99
+ paddingInlineStart: '8px',
100
+ paddingInlineEnd: '8px',
101
+ },
102
+ '&.has-icon': {
103
+ paddingInlineStart: '4px',
104
+ },
105
+ '&.has-icon:not(.has-children)': {
106
+ paddingInlineEnd: '4px',
107
+ },
108
+ '&.single-char': {
109
+ width: '24px',
110
+ height: '24px',
111
+ padding: '4px 0',
112
+ },
113
+ },
114
+ xxs: {
115
+ height: 'fit-content',
116
+ width: 'fit-content',
117
+ display: 'flex',
118
+ justifyContent: 'center',
119
+ alignItems: 'center',
120
+ padding: '2px 6px 2px 4px',
121
+ '&.single-char:not(.has-icon)': {
122
+ width: '20px',
123
+ height: '20px',
124
+ padding: '2px 0',
125
+ },
126
+ '&.has-icon:not(.has-children)': {
127
+ padding: '2px',
128
+ },
129
+ '&.has-icon.has-children': {
130
+ padding: '2px 6px 2px 4px',
131
+ },
132
+ },
133
+ };
134
+
90
135
  const BadgeTheme = {
91
136
  baseStyle,
92
137
  variants: {
93
138
  bold: getVariant,
94
139
  subtle: getVariant,
95
140
  },
141
+ sizes,
96
142
  defaultProps: {
97
143
  variant: 'subtle',
98
144
  colorScheme: 'neutral',
145
+ size: 'xs',
99
146
  },
100
147
  };
101
148
 
@@ -11,22 +11,33 @@ export type BadgeColorScheme =
11
11
  | 'progress'
12
12
  | 'orange'
13
13
  | 'turquoise';
14
+
14
15
  export interface BadgeProps extends ChakraBadgeProps {
15
16
  children?: string | number;
16
17
  colorScheme?: BadgeColorScheme;
17
18
  iconName?: TypeIconName;
18
19
  variant?: 'bold' | 'subtle';
20
+ size?: 'xs' | 'xxs';
19
21
  }
20
22
 
21
23
  /**
22
24
  * Badges are used to highlight an item's status for quick recognition.
23
25
  */
24
26
  const Badge = forwardRef<BadgeProps, 'span'>((props, ref) => {
25
- const { children, iconName, ...rest } = props;
27
+ const { children, iconName, size = 'xs', className, ...rest } = props;
28
+
29
+ const hasIcon = !!iconName;
30
+ const hasChildren = !!children;
31
+ const isSingleChar = hasChildren && String(children).length === 1;
32
+
33
+ const classNames = [className, hasIcon && 'has-icon', hasChildren && 'has-children', isSingleChar && 'single-char']
34
+ .filter(Boolean)
35
+ .join(' ');
36
+
26
37
  return (
27
- <ChakraBadge {...rest} ref={ref} paddingInlineStart={iconName ? '4' : '8'} paddingInlineEnd={children ? '8' : '4'}>
28
- {!!iconName && <Icon size="16" name={iconName} />}
29
- {!!children && (
38
+ <ChakraBadge {...rest} size={size} ref={ref} className={classNames || undefined}>
39
+ {hasIcon && <Icon size="16" name={iconName} />}
40
+ {hasChildren && (
30
41
  <Text hasEllipsis as="span">
31
42
  {children}
32
43
  </Text>
@@ -94,7 +94,7 @@ export const FILTER_STORY_DATA: FilterData = {
94
94
  }, 2000);
95
95
  });
96
96
  },
97
- options: ['default 1', 'default 2', 'default 3'],
97
+ options: ['default 1', 'default 2', 'test case with a very long name that might overflow'],
98
98
  type: 'tag',
99
99
  },
100
100
  workflow: {
@@ -1,3 +1,4 @@
1
+ import Divider from '../../Divider/Divider';
1
2
  import Badge from '../../Badge/Badge';
2
3
  import Text from '../../Text/Text';
3
4
  import Button from '../../Button/Button';
@@ -6,12 +7,29 @@ import { useFilterContext } from '../Filter.context';
6
7
  import { FilterProps } from '../Filter.types';
7
8
  import FilterDrawer from './FilterDrawer';
8
9
  import FilterForm from './FilterForm';
10
+ import FilterSearch from './FilterSearch';
11
+
12
+ const Filter = (props: FilterProps) => {
13
+ const {
14
+ data: initialData,
15
+ defaultState,
16
+ filtersDependOn,
17
+ isLoading,
18
+ isMobile,
19
+ onChange,
20
+ searchTooltip,
21
+ showAdd = true,
22
+ showFilterIcon = true,
23
+ showClearFilters = true,
24
+ showSearch,
25
+ ...rest
26
+ } = props;
9
27
 
10
- const Filter = ({ isLoading, showAdd = true }: FilterProps) => {
11
28
  const {
12
29
  data,
13
30
  isPopoverOpen,
14
31
  onClearFilters,
32
+ onFilterChange,
15
33
  selectedCategory,
16
34
  setSelectedCategory,
17
35
  setPopoverOpen,
@@ -20,14 +38,14 @@ const Filter = ({ isLoading, showAdd = true }: FilterProps) => {
20
38
  } = useFilterContext();
21
39
 
22
40
  const isAllOptionForSwitch = (category: string) => {
23
- return data[category].type === 'switch' && state[category]?.[0] === 'all';
41
+ return data[category]?.type === 'switch' && state[category]?.[0] === 'all';
24
42
  };
25
43
 
26
44
  const count = Object.entries(state).filter(([name, item]) => item?.length > 0 && !isAllOptionForSwitch(name)).length;
27
45
 
28
46
  return (
29
- <>
30
- <Box alignItems="center" display="flex" justifyContent="space-between">
47
+ <Box display="flex" flexDirection="column" {...rest}>
48
+ <Box alignItems="center" display="flex" justifyContent="space-between" padding="12">
31
49
  <Button leftIconName="Filter" onClick={() => setPopoverOpen(true)} size="sm" variant="secondary">
32
50
  <Text alignItems="center" display="flex" gap="8">
33
51
  Filters
@@ -49,13 +67,20 @@ const Filter = ({ isLoading, showAdd = true }: FilterProps) => {
49
67
  </Button>
50
68
  )}
51
69
  </Box>
70
+ {showSearch && (
71
+ <>
72
+ <Divider color="border/minimal" />
73
+ <FilterSearch onChange={onFilterChange} value={state.search?.length ? state.search[0] : ''} />
74
+ <Divider color="border/minimal" />
75
+ </>
76
+ )}
52
77
  <FilterDrawer isOpen={isPopoverOpen} onClose={() => setPopoverOpen(false)} showAdd={showAdd} />
53
78
  <FilterForm
54
79
  isOpen={Boolean(selectedCategory)}
55
80
  onClose={() => setSelectedCategory(undefined)}
56
81
  selectedCategory={selectedCategory || ''}
57
82
  />
58
- </>
83
+ </Box>
59
84
  );
60
85
  };
61
86
 
@@ -97,8 +97,8 @@ const FilterForm = ({ isOpen, onClose, selectedCategory }: FilterFormProps) => {
97
97
  bodyPadding="0"
98
98
  bodyProps={{ overflowY: 'auto' }}
99
99
  bodyRef={bodyRef}
100
- contentProps={{ zIndex: 'dialog', top: '0' }}
101
- headerPadding={16}
100
+ contentProps={{ gap: '0', zIndex: 'dialog', top: '0' }}
101
+ headerPadding="12"
102
102
  hideCloseButton
103
103
  isOpen={isOpen}
104
104
  maxWidth="100%"
@@ -118,7 +118,7 @@ const FilterForm = ({ isOpen, onClose, selectedCategory }: FilterFormProps) => {
118
118
  <Text as="h6" color="text/tertiary" textStyle="heading/h6">
119
119
  Filter
120
120
  </Text>
121
- <Text as="h3" color="text/primary" textStyle="heading/mobile/h3">
121
+ <Text as="h3" color="text/primary" textStyle="heading/mobile/h3" textTransform="initial">
122
122
  {name}
123
123
  </Text>
124
124
  </Box>
@@ -162,21 +162,20 @@ const FilterForm = ({ isOpen, onClose, selectedCategory }: FilterFormProps) => {
162
162
  ) : null
163
163
  }
164
164
  >
165
- <Box display="flex" flexDirection="column" gap="16">
165
+ <Box display="flex" flexDirection="column" gap="8">
166
166
  <Box
167
167
  backgroundColor="background/primary"
168
168
  display="flex"
169
169
  flexDirection="column"
170
170
  gap="8"
171
- marginBottom="8"
172
171
  position="sticky"
173
172
  top="0"
174
173
  >
175
174
  {(withSearch || isAsync) && (
176
175
  <>
177
- <Divider />
176
+ <Divider color="border/minimal" />
178
177
  <SearchInput
179
- marginInline="12"
178
+ marginInline="8"
180
179
  onChange={onSearchChange}
181
180
  placeholder="Search for options"
182
181
  value={searchValue}
@@ -184,7 +183,7 @@ const FilterForm = ({ isOpen, onClose, selectedCategory }: FilterFormProps) => {
184
183
  />
185
184
  </>
186
185
  )}
187
- <Divider />
186
+ <Divider color="border/minimal" />
188
187
  </Box>
189
188
  <Box display="flex" flexDirection="column" gap="8">
190
189
  {isLoading && (
@@ -4,6 +4,7 @@ import FormLabel from '../../Form/FormLabel';
4
4
  import Button from '../../Button/Button';
5
5
  import Box from '../../Box/Box';
6
6
  import Icon from '../../Icon/Icon';
7
+ import Text from '../../Text/Text';
7
8
  import IconButton from '../../IconButton/IconButton';
8
9
  import { useFilterContext } from '../Filter.context';
9
10
  import { getDateRangeLabel, getOptionLabel } from '../Filter.utils';
@@ -59,7 +60,9 @@ const FilterItem = ({ category }: FilterItemProps) => {
59
60
  variant="secondary"
60
61
  width="100%"
61
62
  >
62
- {getText()}
63
+ <Text overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap" width="100%" textAlign="left" minW="0">
64
+ {getText()}
65
+ </Text>
63
66
  </Button>
64
67
  <IconButton
65
68
  aria-label="Clear"
@@ -88,7 +91,9 @@ const FilterItem = ({ category }: FilterItemProps) => {
88
91
  {getText()}
89
92
  </Box>
90
93
  ) : (
91
- getText()
94
+ <Text overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap" minW="0">
95
+ {getText()}
96
+ </Text>
92
97
  )}
93
98
  </Button>
94
99
  )}
@@ -0,0 +1,36 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { InputProps } from 'chakra-ui-2--react';
3
+ import { useDebounce } from '../../../utils/utils';
4
+ import SearchInput from '../../SearchInput/SearchInput';
5
+ import { FilterValue } from '../Filter.types';
6
+
7
+ export interface FilterSearchProps extends Omit<InputProps, 'onChange' | 'value'> {
8
+ onChange: (category: string, selected: FilterValue) => void;
9
+ value: string;
10
+ }
11
+
12
+ const FilterSearch = (props: FilterSearchProps) => {
13
+ const { onChange, value } = props;
14
+
15
+ const [searchValue, setSearchValue] = useState(value);
16
+
17
+ const onInputChange = (newSearchValue: string) => {
18
+ setSearchValue(newSearchValue);
19
+ };
20
+
21
+ const debouncedSearchValue = useDebounce<string>(searchValue, 500);
22
+
23
+ useEffect(() => {
24
+ onChange('search', debouncedSearchValue.length ? [debouncedSearchValue] : []);
25
+ }, [debouncedSearchValue]);
26
+
27
+ useEffect(() => {
28
+ setSearchValue(value);
29
+ }, [value]);
30
+
31
+ return (
32
+ <SearchInput onChange={onInputChange} paddingBlock="4" placeholder="Search" value={searchValue} variant="mobile" />
33
+ );
34
+ };
35
+
36
+ export default FilterSearch;
@@ -0,0 +1,24 @@
1
+ import { forwardRef, Icon, IconProps } from 'chakra-ui-2--react';
2
+
3
+ const BuildEnvSetup = forwardRef<IconProps, 'svg'>((props, ref) => (
4
+ <Icon ref={ref} viewBox="0 0 16 16" {...props}>
5
+ <path
6
+ fillRule="evenodd"
7
+ clipRule="evenodd"
8
+ d="M12.3721 6.71387C12.9165 6.88799 13.4161 7.16252 13.8486 7.5166L14.0918 7.71484L13.8047 8.91211C13.9455 9.10106 14.0663 9.3049 14.167 9.52051L15.3682 9.87891L15.4238 10.1797C15.4725 10.4434 15.5 10.7176 15.5 11C15.5 11.281 15.4735 11.5549 15.4248 11.8193L15.3691 12.1211L14.168 12.4756C14.0672 12.692 13.9448 12.8954 13.8037 13.085L14.0918 14.2842L13.8486 14.4824C13.4164 14.8364 12.9171 15.1118 12.3721 15.2861L12.0898 15.376L11.876 15.1719L11.1621 14.4951L11 14.5C10.8755 14.5 10.7532 14.4913 10.6328 14.4785L9.96094 15.1162L9.73535 15.3291L9.44434 15.2217C8.88338 15.0149 8.37465 14.7011 7.94434 14.3027L7.72949 14.1035L8.03027 12.8477C7.97055 12.7524 7.91507 12.6537 7.86426 12.5518L6.93652 12.2764L6.64844 12.1904L6.58887 11.8955C6.53026 11.6054 6.5 11.3058 6.5 11C6.5 10.6938 6.52983 10.3929 6.58887 10.1016L6.64844 9.80762L7.86523 9.44531C7.91585 9.34408 7.97091 9.246 8.03027 9.15137L7.79785 8.17871L7.72949 7.89453L7.94434 7.69531C8.37475 7.29715 8.88402 6.98386 9.44434 6.77734L9.73535 6.66992L9.96094 6.88281L10.6328 7.51953C10.7537 7.50693 10.8762 7.5 11 7.5L11.1602 7.50391L12.0908 6.62402L12.3721 6.71387ZM11 9.75C10.3097 9.75006 9.75 10.3097 9.75 11C9.75 11.6903 10.3097 12.2499 11 12.25C11.6904 12.25 12.25 11.6904 12.25 11C12.25 10.3096 11.6904 9.75 11 9.75Z"
9
+ fill="currentColor"
10
+ />
11
+ <path
12
+ fillRule="evenodd"
13
+ clipRule="evenodd"
14
+ d="M13.5 1C14.3284 1 15 1.67157 15 2.5V7.23047C14.57 6.77408 14.0635 6.39196 13.5 6.10352V5.25H2.5V12H5.59473C5.69238 12.5309 5.8651 13.0343 6.10352 13.5H2.5C1.67157 13.5 1 12.8284 1 12V2.5C1 1.67157 1.67157 1 2.5 1H13.5ZM2.5 3.75H13.5V2.5H2.5V3.75Z"
15
+ fill="currentColor"
16
+ />
17
+ <path
18
+ d="M6.45801 7.89746C6.04014 8.50802 5.74276 9.20722 5.59863 9.96094L4.53027 11.0303L3.46973 9.96973L4.93945 8.5L3.46973 7.03027L4.53027 5.96973L6.45801 7.89746Z"
19
+ fill="currentColor"
20
+ />
21
+ </Icon>
22
+ ));
23
+
24
+ export default BuildEnvSetup;
@@ -0,0 +1,14 @@
1
+ import { forwardRef, Icon, IconProps } from 'chakra-ui-2--react';
2
+
3
+ const FolderEmpty = forwardRef<IconProps, 'svg'>((props, ref) => (
4
+ <Icon ref={ref} viewBox="0 0 16 16" {...props}>
5
+ <path
6
+ fillRule="evenodd"
7
+ clipRule="evenodd"
8
+ d="M6.37868 2.5C6.7765 2.5 7.15804 2.65804 7.43934 2.93934L8 3.5H13.5C14.3284 3.5 15 4.17157 15 5V12C15 12.8284 14.3284 13.5 13.5 13.5H2.5C1.67157 13.5 1 12.8284 1 12V4C1 3.17157 1.67157 2.5 2.5 2.5H6.37868ZM7.37868 5L6.37868 4H2.5V12H13.5V5L7.37868 5Z"
9
+ fill="currentColor"
10
+ />
11
+ </Icon>
12
+ ));
13
+
14
+ export default FolderEmpty;
@@ -46,6 +46,7 @@ export { default as Bug } from './Bug';
46
46
  export { default as Build } from './Build';
47
47
  export { default as BuildCache } from './BuildCache';
48
48
  export { default as BuildCacheSolid } from './BuildCacheSolid';
49
+ export { default as BuildEnvSetup } from './BuildEnvSetup';
49
50
  export { default as AbortCircle } from './AbortCircle';
50
51
  export { default as AbortCircleFilled } from './AbortCircleFilled';
51
52
  export { default as CrossCircleFilled } from './CrossCircleFilled';
@@ -108,6 +109,7 @@ export { default as Filter } from './Filter';
108
109
  export { default as Flag } from './Flag';
109
110
  export { default as Flutter } from './Flutter';
110
111
  export { default as Folder } from './Folder';
112
+ export { default as FolderEmpty } from './FolderEmpty';
111
113
  export { default as Fullscreen } from './Fullscreen';
112
114
  export { default as FullscreenExit } from './FullscreenExit';
113
115
  export { default as Gauge } from './Gauge';
@@ -0,0 +1,24 @@
1
+ import { forwardRef, Icon, IconProps } from 'chakra-ui-2--react';
2
+
3
+ const BuildEnvSetup = forwardRef<IconProps, 'svg'>((props, ref) => (
4
+ <Icon ref={ref} viewBox="0 0 24 24" {...props}>
5
+ <path
6
+ fillRule="evenodd"
7
+ clipRule="evenodd"
8
+ d="M19.165 12.2578C19.9325 12.5013 20.6274 12.9074 21.208 13.4375L21.4258 13.6367L21.3574 13.9238L21.0566 15.1729C21.1424 15.3038 21.2209 15.4397 21.292 15.5801L22.8076 16.0303L22.8711 16.3184C22.9543 16.6981 23 17.0936 23 17.5C23 17.9061 22.954 18.3006 22.8711 18.6797L22.8086 18.9688L21.292 19.418C21.2212 19.5581 21.1432 19.6942 21.0576 19.8252L21.4258 21.3623L21.208 21.5615C20.6276 22.0916 19.933 22.4985 19.165 22.7422L18.8838 22.8311L18.6699 22.6289L17.7344 21.7422L17.501 21.75C17.4213 21.75 17.3428 21.7468 17.2656 21.7422L16.3311 22.6289L16.1172 22.8311L15.8359 22.7422C15.068 22.4985 14.3728 22.0921 13.792 21.5615L13.5742 21.3623L13.9414 19.8242C13.856 19.6937 13.7779 19.558 13.707 19.418L12.1914 18.9678L12.1289 18.6787C12.0462 18.3002 12 17.9059 12 17.5C12 17.0939 12.046 16.6993 12.1289 16.3203L12.1914 16.0312L12.4746 15.9473L13.707 15.5801C13.7779 15.4402 13.856 15.3044 13.9414 15.1738L13.5742 13.6367L13.792 13.4375C14.3729 12.9069 15.0685 12.5013 15.8359 12.2578L16.1172 12.1689L17.2656 13.2559C17.3433 13.2515 17.4218 13.25 17.501 13.25C17.5795 13.25 17.6574 13.2515 17.7344 13.2559L18.8838 12.1689L19.165 12.2578ZM17.501 16C16.6725 16 16.001 16.6716 16.001 17.5C16.001 18.3284 16.6725 19 17.501 19C18.3292 18.9997 19.001 18.3283 19.001 17.5C19.001 16.6717 18.3292 16.0003 17.501 16Z"
9
+ fill="currentColor"
10
+ />
11
+ <path
12
+ fillRule="evenodd"
13
+ clipRule="evenodd"
14
+ d="M20 3C21.1046 3 22 3.89543 22 5V12.8115C21.4243 12.2588 20.748 11.81 20 11.498V9H4V19H11.1758C11.3461 19.7207 11.6364 20.3943 12.0244 21H4C2.89543 21 2 20.1046 2 19V5C2 3.89543 2.89543 3 4 3H20ZM4 7H20V5H4V7Z"
15
+ fill="currentColor"
16
+ />
17
+ <path
18
+ d="M11.4141 14L7.70703 17.707L6.29297 16.293L8.58594 14L6.29297 11.707L7.70703 10.293L11.4141 14Z"
19
+ fill="currentColor"
20
+ />
21
+ </Icon>
22
+ ));
23
+
24
+ export default BuildEnvSetup;
@@ -0,0 +1,14 @@
1
+ import { forwardRef, Icon, IconProps } from 'chakra-ui-2--react';
2
+
3
+ const FolderEmpty = forwardRef<IconProps, 'svg'>((props, ref) => (
4
+ <Icon ref={ref} viewBox="0 0 24 24" {...props}>
5
+ <path
6
+ fillRule="evenodd"
7
+ clipRule="evenodd"
8
+ d="M10.5858 4.58579C10.2107 4.21071 9.70201 4 9.17157 4H4C2.89543 4 2 4.89543 2 6V18C2 19.1046 2.89543 20 4 20H20C21.1046 20 22 19.1046 22 18V8C22 6.89543 21.1046 6 20 6H12L10.5858 4.58579ZM4 6V18H20V8H11.1716L9.17157 6H4Z"
9
+ fill="currentColor"
10
+ />
11
+ </Icon>
12
+ ));
13
+
14
+ export default FolderEmpty;
@@ -46,6 +46,7 @@ export { default as Bug } from './Bug';
46
46
  export { default as Build } from './Build';
47
47
  export { default as BuildCache } from './BuildCache';
48
48
  export { default as BuildCacheSolid } from './BuildCacheSolid';
49
+ export { default as BuildEnvSetup } from './BuildEnvSetup';
49
50
  export { default as AbortCircle } from './AbortCircle';
50
51
  export { default as AbortCircleFilled } from './AbortCircleFilled';
51
52
  export { default as DoubleCircle } from './DoubleCircle';
@@ -106,6 +107,7 @@ export { default as Filter } from './Filter';
106
107
  export { default as Flag } from './Flag';
107
108
  export { default as Flutter } from './Flutter';
108
109
  export { default as Folder } from './Folder';
110
+ export { default as FolderEmpty } from './FolderEmpty';
109
111
  export { default as Fullscreen } from './Fullscreen';
110
112
  export { default as FullscreenExit } from './FullscreenExit';
111
113
  export { default as Gauge } from './Gauge';
@@ -3,6 +3,7 @@ import { useControllableState } from 'chakra-ui-2--react';
3
3
  import { NodeCallback, TreeViewAction, TreeViewState } from './TreeView.types';
4
4
 
5
5
  const TreeViewContext = createContext<TreeViewState & TreeViewAction>({
6
+ variant: undefined,
6
7
  selectedId: undefined,
7
8
  expandedIds: new Set<string>(),
8
9
  disabledIds: new Set<string>(),
@@ -10,7 +11,7 @@ const TreeViewContext = createContext<TreeViewState & TreeViewAction>({
10
11
  onExpandedChange: () => undefined,
11
12
  });
12
13
 
13
- export type TreeViewContenxtProviderProps = PropsWithChildren<
14
+ export type TreeViewContextProviderProps = PropsWithChildren<
14
15
  Partial<TreeViewState> &
15
16
  Partial<TreeViewAction> & {
16
17
  defaultSelectedId?: string;
@@ -19,6 +20,7 @@ export type TreeViewContenxtProviderProps = PropsWithChildren<
19
20
  }
20
21
  >;
21
22
  export const TreeViewContextProvider = ({
23
+ variant = 'navigation',
22
24
  selectedId: controlledSelectedId,
23
25
  defaultSelectedId,
24
26
  expandedIds: controlledExpandedIds,
@@ -28,7 +30,7 @@ export const TreeViewContextProvider = ({
28
30
  onSelectionChange,
29
31
  onExpandedChange,
30
32
  children,
31
- }: TreeViewContenxtProviderProps) => {
33
+ }: TreeViewContextProviderProps) => {
32
34
  const [selectedId, setSelectedId] = useControllableState({
33
35
  value: controlledSelectedId,
34
36
  defaultValue: defaultSelectedId,
@@ -71,8 +73,15 @@ export const TreeViewContextProvider = ({
71
73
  );
72
74
 
73
75
  const contextValue = useMemo(
74
- () => ({ selectedId, expandedIds, disabledIds, onSelectionChange: handleSelect, onExpandedChange: handleExpand }),
75
- [selectedId, expandedIds, disabledIds, handleSelect, handleExpand],
76
+ () => ({
77
+ variant,
78
+ selectedId,
79
+ expandedIds,
80
+ disabledIds,
81
+ onSelectionChange: handleSelect,
82
+ onExpandedChange: handleExpand,
83
+ }),
84
+ [variant, selectedId, expandedIds, disabledIds, handleSelect, handleExpand],
76
85
  );
77
86
 
78
87
  return <TreeViewContext.Provider value={contextValue}>{children}</TreeViewContext.Provider>;
@@ -1,28 +1,63 @@
1
1
  import { createMultiStyleConfigHelpers } from 'chakra-ui-2--styled-system';
2
2
 
3
3
  const { defineMultiStyleConfig, definePartsStyle } = createMultiStyleConfigHelpers([
4
+ 'listItem',
4
5
  'icon',
6
+ 'iconWrapper',
5
7
  'button',
6
8
  'buttonContent',
7
9
  'textBlock',
8
10
  'suffixBlock',
9
11
  'selectionIndicator',
12
+ 'collapseContent',
13
+ 'hoverActionsWrapper',
14
+ 'hoverActionButton',
15
+ 'leafBadge',
10
16
  ]);
11
17
 
12
18
  const baseStyle = definePartsStyle({
19
+ listItem: {
20
+ overflow: 'hidden',
21
+ },
13
22
  icon: {
14
23
  marginTop: '10px',
15
24
  marginBottom: '2px',
16
25
  color: 'icon/primary',
17
26
  },
27
+ leafBadge: {
28
+ marginTop: '10px',
29
+ marginBottom: '2px',
30
+ },
31
+ iconWrapper: {
32
+ position: 'relative',
33
+ display: 'flex',
34
+ alignSelf: 'flex-start',
35
+ zIndex: 1,
36
+ '&.show-connector::after': {
37
+ content: '""',
38
+ position: 'absolute',
39
+ left: '50%',
40
+ top: '100%',
41
+ bottom: '-100px',
42
+ width: '1px',
43
+ backgroundColor: 'border/regular',
44
+ },
45
+ },
18
46
  button: {
19
47
  pos: 'relative',
20
48
  py: '0',
21
49
  px: '16',
22
50
  pl: 'calc(16px + 24px * var(--level))',
23
51
  w: '100%',
52
+ overflow: 'hidden',
24
53
  _hover: {
25
54
  bgColor: 'background/secondary',
55
+ '& .hover-actions': {
56
+ display: 'flex',
57
+ },
58
+ '& .hide-on-hover': {
59
+ display: 'none',
60
+ },
26
61
  },
27
62
  _active: {
28
63
  bgColor: 'background/tertiary',
@@ -37,6 +72,15 @@ const baseStyle = definePartsStyle({
37
72
  bgColor: 'background/selected-hover',
38
73
  },
39
74
  },
75
+ '&.show-ancestor-connectors::before': {
76
+ content: '""',
77
+ position: 'absolute',
78
+ top: 0,
79
+ bottom: 0,
80
+ left: '24px',
81
+ width: 'calc(var(--level) * 24px)',
82
+ zIndex: 1,
83
+ },
40
84
  },
41
85
  buttonContent: {
42
86
  gap: '8',
@@ -63,9 +107,17 @@ const baseStyle = definePartsStyle({
63
107
  display: 'flex',
64
108
  alignItems: 'center',
65
109
  justifyContent: 'flex-end',
66
- paddingTop: '8',
67
110
  paddingLeft: '16',
68
111
  gap: '8',
112
+ '& > .hide-on-hover': {
113
+ display: 'flex',
114
+ },
115
+ 'button:hover > div > div > &': {
116
+ pt: '0',
117
+ },
118
+ 'button:hover > div > div > & > .hide-on-hover': {
119
+ display: 'none',
120
+ },
69
121
  },
70
122
  selectionIndicator: {
71
123
  width: '3px',
@@ -75,6 +127,23 @@ const baseStyle = definePartsStyle({
75
127
  bgColor: 'border/selected',
76
128
  borderLeftRadius: '2',
77
129
  },
130
+ collapseContent: {
131
+ position: 'relative',
132
+ '&.show-connector::before': {
133
+ content: '""',
134
+ position: 'absolute',
135
+ top: 0,
136
+ bottom: 0,
137
+ left: 'calc(16px + var(--level) * 24px + 8px)',
138
+ width: '1px',
139
+ backgroundColor: 'border/regular',
140
+ zIndex: 1,
141
+ },
142
+ },
143
+ hoverActionsWrapper: {
144
+ display: 'none',
145
+ alignItems: 'center',
146
+ },
78
147
  });
79
148
 
80
149
  const TreeViewTheme = defineMultiStyleConfig({
@@ -1,81 +1,36 @@
1
1
  import { PropsWithChildren } from 'react';
2
- import { TreeViewContenxtProviderProps, TreeViewContextProvider } from './TreeView.context';
2
+ import { TreeViewContextProviderProps, TreeViewContextProvider } from './TreeView.context';
3
3
  import TreeViewGroup from './TreeViewGroup';
4
4
  import TreeViewNode from './TreeViewNode';
5
- import { NodeData } from './TreeView.types';
5
+ import { NodeData, TreeViewVariant, BranchNodeData, LeafNodeData } from './TreeView.types';
6
6
 
7
- const TreeViewLeaf = (props: Omit<NodeData, 'type' | 'children'>) => (
7
+ const TreeViewLeaf = (props: Omit<LeafNodeData, 'type' | 'children'>) => (
8
8
  <TreeViewNode type="leaf" level={-1} indexPath={[]} titlePath={[]} {...props} />
9
9
  );
10
- const TreeViewBranch = (props: PropsWithChildren<Omit<NodeData, 'type' | 'children'>>) => (
10
+ const TreeViewBranch = (props: PropsWithChildren<Omit<BranchNodeData, 'type' | 'children'>>) => (
11
11
  <TreeViewNode type="branch" level={-1} indexPath={[]} titlePath={[]} {...props} />
12
12
  );
13
13
 
14
14
  const renderTreeNodes = (nodes: NodeData[]) => {
15
15
  return nodes.map((node) => {
16
- const {
17
- id,
18
- type,
19
- title,
20
- description,
21
- iconName,
22
- iconColor,
23
- label,
24
- labelIconName,
25
- labelIconColor,
26
- children,
27
- onClick,
28
- } = node;
16
+ const { id, type, children, ...rest } = node;
29
17
 
30
18
  if (type === 'branch') {
31
19
  return (
32
- <TreeViewNode
33
- key={id}
34
- id={id}
35
- type="branch"
36
- title={title}
37
- description={description}
38
- iconName={iconName}
39
- iconColor={iconColor}
40
- label={label}
41
- labelIconName={labelIconName}
42
- labelIconColor={labelIconColor}
43
- // Thes props will be overridden by the TreeViewGroup
44
- level={-1}
45
- indexPath={[]}
46
- titlePath={[]}
47
- onClick={onClick}
48
- >
20
+ <TreeViewNode key={id} id={id} type="branch" level={-1} indexPath={[]} titlePath={[]} {...rest}>
49
21
  {children && renderTreeNodes(children)}
50
22
  </TreeViewNode>
51
23
  );
52
24
  }
53
25
 
54
- return (
55
- <TreeViewNode
56
- key={id}
57
- id={id}
58
- type="leaf"
59
- title={title}
60
- description={description}
61
- iconName={iconName}
62
- iconColor={iconColor}
63
- label={label}
64
- labelIconName={labelIconName}
65
- labelIconColor={labelIconColor}
66
- // Thes props will be overridden by the TreeViewGroup
67
- level={-1}
68
- indexPath={[]}
69
- titlePath={[]}
70
- onClick={onClick}
71
- />
72
- );
26
+ return <TreeViewNode key={id} id={id} type="leaf" level={-1} indexPath={[]} titlePath={[]} {...rest} />;
73
27
  });
74
28
  };
75
29
 
76
- export type TreeViewProps = TreeViewContenxtProviderProps & {
30
+ export type TreeViewProps = TreeViewContextProviderProps & {
77
31
  label: string;
78
32
  data?: NodeData[];
33
+ variant?: TreeViewVariant;
79
34
  };
80
35
 
81
36
  const TreeView = ({ data, label, children, ...rest }: PropsWithChildren<TreeViewProps>) => {
@@ -1,21 +1,87 @@
1
+ import { PropsWithChildren } from 'react';
2
+ import { UseFloatingProps } from '@floating-ui/react-dom-interactions';
1
3
  import { TypeIconName } from '../Icon/Icon';
4
+ import { BadgeColorScheme, BadgeProps } from '../Badge/Badge';
2
5
 
3
6
  export type NodeType = 'branch' | 'leaf';
4
7
 
5
- export type NodeData = {
8
+ export type TreeViewVariant = 'navigation' | 'files';
9
+
10
+ export type HoverControlButton = {
11
+ iconName: TypeIconName;
12
+ label: string;
13
+ isDanger?: boolean;
14
+ onClick: (e: React.MouseEvent) => void;
15
+ };
16
+
17
+ type LeafIndicator =
18
+ | {
19
+ iconName?: TypeIconName;
20
+ iconColor?: string;
21
+ badgeLabel?: never;
22
+ badgeVariant?: never;
23
+ badgeColorScheme?: never;
24
+ badgeTooltip?: never;
25
+ }
26
+ | {
27
+ iconName?: never;
28
+ iconColor?: never;
29
+ badgeLabel?: string;
30
+ badgeVariant?: BadgeProps['variant'];
31
+ badgeColorScheme?: BadgeColorScheme;
32
+ badgeTooltip?: {
33
+ label: string;
34
+ placement?: UseFloatingProps['placement'];
35
+ };
36
+ };
37
+
38
+ type NodeBase = {
6
39
  id: string;
7
- type: NodeType;
8
40
  title: string;
9
41
  description?: string;
10
- iconName?: TypeIconName;
11
- iconColor?: string;
42
+ hasWarning?: boolean;
12
43
  label?: string;
13
44
  labelIconName?: TypeIconName;
14
45
  labelIconColor?: string;
15
- children?: NodeData[];
16
46
  onClick?: VoidFunction;
47
+ hoverActions?: [HoverControlButton] | [HoverControlButton, HoverControlButton];
48
+ children?: NodeData[];
17
49
  };
18
50
 
51
+ type LeafNodeData = NodeBase & { type: 'leaf' } & LeafIndicator;
52
+
53
+ type BranchNodeData = NodeBase & {
54
+ type: 'branch';
55
+ iconName?: TypeIconName;
56
+ iconColor?: string;
57
+ branchIconTooltip?: {
58
+ label: string;
59
+ placement?: UseFloatingProps['placement'];
60
+ };
61
+ };
62
+
63
+ export type NodeData = LeafNodeData | BranchNodeData;
64
+ export type { LeafNodeData, BranchNodeData };
65
+
66
+ type AllNodeProps = NodeBase & {
67
+ type: NodeType;
68
+ iconName?: TypeIconName;
69
+ iconColor?: string;
70
+ badgeLabel?: string;
71
+ badgeVariant?: BadgeProps['variant'];
72
+ badgeColorScheme?: BadgeColorScheme;
73
+ badgeTooltip?: {
74
+ label: string;
75
+ placement?: UseFloatingProps['placement'];
76
+ };
77
+ branchIconTooltip?: {
78
+ label: string;
79
+ placement?: UseFloatingProps['placement'];
80
+ };
81
+ };
82
+
83
+ export type TreeViewNodeProps = PropsWithChildren<Omit<AllNodeProps, 'children'> & NodePath>;
84
+
19
85
  export type NodePath = {
20
86
  level: number;
21
87
  indexPath: number[];
@@ -32,6 +98,7 @@ export type NodeState = {
32
98
  };
33
99
 
34
100
  export type TreeViewState = {
101
+ variant?: TreeViewVariant;
35
102
  selectedId?: string;
36
103
  expandedIds: Set<string>;
37
104
  disabledIds: Set<string>;
@@ -1,12 +1,15 @@
1
- import { memo, PropsWithChildren, useCallback, useMemo } from 'react';
1
+ import { memo, useCallback, useMemo, MouseEvent } from 'react';
2
2
  import { useMultiStyleConfig } from 'chakra-ui-2--react';
3
3
  import Icon from '../Icon/Icon';
4
+ import Badge from '../Badge/Badge';
4
5
  import Box from '../Box/Box';
5
6
  import Text from '../Text/Text';
6
7
  import Collapse from '../Collapse/Collapse';
8
+ import ControlButton from '../ControlButton/ControlButton';
9
+ import Tooltip from '../Tooltip/Tooltip';
7
10
  import TreeViewGroup from './TreeViewGroup';
8
11
  import { useTreeViewContext } from './TreeView.context';
9
- import { NodeData, NodePath } from './TreeView.types';
12
+ import { HoverControlButton, TreeViewNodeProps } from './TreeView.types';
10
13
 
11
14
  type TreeViewNodeContentProps = TreeViewNodeProps & {
12
15
  isBranch: boolean;
@@ -22,11 +25,18 @@ const TreeViewNodeContent = memo(
22
25
  id,
23
26
  title,
24
27
  description,
28
+ hasWarning,
25
29
  iconName,
26
30
  iconColor,
31
+ badgeLabel,
32
+ badgeVariant,
33
+ badgeColorScheme,
34
+ badgeTooltip,
27
35
  label,
28
36
  labelIconName,
29
37
  labelIconColor,
38
+ hoverActions,
39
+ branchIconTooltip,
30
40
  // NodePath
31
41
  level,
32
42
  indexPath,
@@ -40,6 +50,29 @@ const TreeViewNodeContent = memo(
40
50
  children,
41
51
  }: TreeViewNodeContentProps) => {
42
52
  const styles = useMultiStyleConfig('TreeView', {});
53
+ const { variant } = useTreeViewContext();
54
+
55
+ const isFilesVariant = variant === 'files';
56
+ const hasChildren = Boolean(children);
57
+ const isSelectable = !(isBranch && !hasChildren);
58
+ const isEmptyBranch = isBranch && !hasChildren;
59
+ const chevronIcon = isExpanded ? 'ChevronDown' : 'ChevronRight';
60
+ const primaryIconName = isFilesVariant ? 'FolderEmpty' : chevronIcon;
61
+
62
+ const showAncestorConnectors = isFilesVariant && level > 1;
63
+ const showOwnConnector = isFilesVariant && isBranch && isExpanded;
64
+
65
+ const hasHoverActions = hoverActions && hoverActions.length > 0;
66
+ const hasLabelContent = label || labelIconName || hasWarning;
67
+
68
+ const showIcon = !isBranch && iconName;
69
+ const showBadge = !isBranch && !iconName && badgeLabel;
70
+
71
+ const handleActionClick = useCallback((e: MouseEvent, action: HoverControlButton) => {
72
+ e.stopPropagation();
73
+ e.preventDefault();
74
+ action.onClick(e);
75
+ }, []);
43
76
 
44
77
  return (
45
78
  <Box
@@ -47,27 +80,59 @@ const TreeViewNodeContent = memo(
47
80
  id={id}
48
81
  role="treeitem"
49
82
  aria-level={level}
50
- aria-disabled={isDisabled}
83
+ aria-disabled={isDisabled || !isSelectable}
51
84
  aria-selected={isSelected}
52
- {...(isBranch ? { 'aria-expanded': isExpanded } : {})}
85
+ {...(isBranch ? { 'aria-expanded': hasChildren ? isExpanded : undefined } : {})}
86
+ sx={styles.listItem}
53
87
  >
54
88
  <Box
55
89
  as="button"
56
- tabIndex={isSelected ? 0 : -1}
57
- data-selected={isSelected ? '' : undefined}
58
- sx={{ '--level': `${level - 1}`, ...styles.button }}
90
+ tabIndex={!isSelectable || !isSelected ? -1 : 0}
91
+ data-selected={isSelectable && isSelected ? '' : undefined}
92
+ className={`${showAncestorConnectors ? 'show-ancestor-connectors' : ''}`}
93
+ sx={{
94
+ '--level': `${level - 1}`,
95
+ '--connector-color': 'var(--chakra-colors-border-regular)',
96
+ cursor: isEmptyBranch ? 'default' : 'pointer',
97
+ ...styles.button,
98
+ }}
59
99
  onClick={onClick}
60
100
  >
61
101
  <Box sx={{ ...styles.buttonContent }}>
62
- {isBranch && <Icon size="16" name={isExpanded ? 'ChevronDown' : 'ChevronRight'} sx={styles.icon} />}
102
+ {isBranch && (
103
+ <Box sx={styles.iconWrapper} className={showOwnConnector ? 'show-connector' : ''}>
104
+ {isFilesVariant && isEmptyBranch ? (
105
+ <Tooltip
106
+ label={branchIconTooltip?.label || 'Empty branch'}
107
+ placement={branchIconTooltip?.placement || 'top'}
108
+ >
109
+ <Icon size="16" name={primaryIconName} sx={styles.icon} />
110
+ </Tooltip>
111
+ ) : (
112
+ <Icon size="16" name={primaryIconName} sx={styles.icon} />
113
+ )}
114
+ </Box>
115
+ )}
63
116
  <Box sx={styles.borderBox}>
64
- {iconName && (
117
+ {showIcon && (
65
118
  <Icon
66
119
  size="16"
67
120
  name={iconName}
68
- sx={{ ...styles.icon, ...{ color: iconColor || (isSelected ? 'icon/primary' : 'icon/secondary') } }}
121
+ sx={{
122
+ ...styles.icon,
123
+ color: iconColor || (isSelected ? 'icon/primary' : 'icon/secondary'),
124
+ }}
69
125
  />
70
126
  )}
127
+
128
+ {showBadge && (
129
+ <Tooltip shouldWrapChildren label={badgeTooltip?.label} placement={badgeTooltip?.placement || 'top'}>
130
+ <Badge variant={badgeVariant} colorScheme={badgeColorScheme} size="xxs" sx={styles.leafBadge}>
131
+ {badgeLabel}
132
+ </Badge>
133
+ </Tooltip>
134
+ )}
135
+
71
136
  <Box sx={styles.textBlock}>
72
137
  <Text fontWeight={isExpanded ? 'semibold' : 'normal'} overflowWrap="break-word" wordBreak="break-word">
73
138
  {title}
@@ -83,25 +148,60 @@ const TreeViewNodeContent = memo(
83
148
  </Text>
84
149
  )}
85
150
  </Box>
86
- {(label || labelIconName) && (
151
+
152
+ {(hasLabelContent || hasHoverActions) && (
87
153
  <Box sx={styles.suffixBlock} color={isSelected || isExpanded ? 'text/primary' : 'text/secondary'}>
88
- {labelIconName && <Icon name={labelIconName} size="16" color={labelIconColor} />}
89
- {label && (
90
- <Text textStyle="body/md/regular" textAlign="right">
91
- {label}
92
- </Text>
154
+ {hasHoverActions && (
155
+ <Box sx={styles.hoverActionsWrapper} className="hover-actions">
156
+ {hoverActions.map((action, index) => (
157
+ <ControlButton
158
+ /* eslint-disable-next-line react/no-array-index-key */
159
+ key={index}
160
+ aria-label={action.label}
161
+ iconName={action.iconName}
162
+ isDanger={action.isDanger}
163
+ onClick={(e: MouseEvent) => handleActionClick(e, action)}
164
+ size="xs"
165
+ />
166
+ ))}
167
+ </Box>
168
+ )}
169
+ {hasLabelContent && (
170
+ <Box
171
+ display="flex"
172
+ alignItems="center"
173
+ gap="8"
174
+ pt="8px"
175
+ className={hasHoverActions ? 'hide-on-hover' : undefined}
176
+ >
177
+ {hasWarning && <Icon name="WarningYellow" size="16" />}
178
+ {labelIconName && <Icon name={labelIconName} size="16" color={labelIconColor} />}
179
+ {label && (
180
+ <Text textStyle="body/md/regular" textAlign="right">
181
+ {label}
182
+ </Text>
183
+ )}
184
+ </Box>
93
185
  )}
94
186
  </Box>
95
187
  )}
96
188
  </Box>
97
- {isSelected && <Box sx={styles.selectionIndicator} />}
189
+ {!isFilesVariant && isSelected && <Box sx={styles.selectionIndicator} />}
98
190
  </Box>
99
191
  </Box>
100
192
  {isBranch && children && (
101
193
  <Collapse in={isExpanded} unmountOnExit>
102
- <TreeViewGroup role="group" level={level + 1} indexPath={indexPath} titlePath={titlePath}>
103
- {children}
104
- </TreeViewGroup>
194
+ <Box
195
+ className={showOwnConnector ? 'show-connector' : undefined}
196
+ sx={{
197
+ '--level': `${level - 1}`,
198
+ ...styles.collapseContent,
199
+ }}
200
+ >
201
+ <TreeViewGroup role="group" level={level + 1} indexPath={indexPath} titlePath={titlePath}>
202
+ {children}
203
+ </TreeViewGroup>
204
+ </Box>
105
205
  </Collapse>
106
206
  )}
107
207
  </Box>
@@ -109,19 +209,24 @@ const TreeViewNodeContent = memo(
109
209
  },
110
210
  );
111
211
 
112
- export type TreeViewNodeProps = PropsWithChildren<Omit<NodeData, 'children'> & NodePath>;
113
-
114
212
  const TreeViewNode = ({
115
213
  // NodeData
116
214
  id,
117
215
  type,
118
216
  title,
119
217
  description,
218
+ hasWarning,
120
219
  iconName,
121
220
  iconColor,
221
+ badgeLabel,
222
+ badgeVariant,
223
+ badgeColorScheme,
224
+ badgeTooltip,
122
225
  label,
123
226
  labelIconName,
124
227
  labelIconColor,
228
+ hoverActions,
229
+ branchIconTooltip,
125
230
  onClick,
126
231
  // NodePath
127
232
  level,
@@ -133,12 +238,14 @@ const TreeViewNode = ({
133
238
  const { selectedId, disabledIds, expandedIds, onSelectionChange, onExpandedChange } = useTreeViewContext();
134
239
 
135
240
  const isBranch = type === 'branch';
241
+ const hasChildren = Boolean(children);
242
+ const isSelectable = !(isBranch && !hasChildren);
136
243
  const isDisabled = useMemo(() => disabledIds.has(id), [id, disabledIds]);
137
244
  const isExpanded = useMemo(() => (isBranch ? expandedIds.has(id) : undefined), [id, isBranch, expandedIds]);
138
- const isSelected = useMemo(() => id === selectedId, [id, selectedId]);
245
+ const isSelected = useMemo(() => (isSelectable ? id === selectedId : false), [id, selectedId, isSelectable]);
139
246
 
140
247
  const handleClick = useCallback(() => {
141
- if (isDisabled) {
248
+ if (isDisabled || !isSelectable) {
142
249
  return;
143
250
  }
144
251
 
@@ -158,7 +265,7 @@ const TreeViewNode = ({
158
265
  });
159
266
  }
160
267
 
161
- if (isBranch) {
268
+ if (isBranch && hasChildren) {
162
269
  onExpandedChange({
163
270
  node: {
164
271
  id,
@@ -198,11 +305,18 @@ const TreeViewNode = ({
198
305
  type={type}
199
306
  title={title}
200
307
  description={description}
308
+ hasWarning={hasWarning}
201
309
  iconName={iconName}
202
310
  iconColor={iconColor}
311
+ badgeLabel={badgeLabel}
312
+ badgeVariant={badgeVariant}
313
+ badgeColorScheme={badgeColorScheme}
314
+ badgeTooltip={badgeTooltip}
203
315
  label={label}
204
316
  labelIconName={labelIconName}
205
317
  labelIconColor={labelIconColor}
318
+ hoverActions={hoverActions}
319
+ branchIconTooltip={branchIconTooltip}
206
320
  level={level}
207
321
  indexPath={indexPath}
208
322
  titlePath={titlePath}