@capillarytech/blaze-ui 0.1.6-alpha.15 → 0.1.6-alpha.17

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/.DS_Store CHANGED
Binary file
@@ -1,10 +1,11 @@
1
- // CapUnifiedSelect component using Ant Design v5 Select and TreeSelect directly
2
- import React from 'react';
1
+ // Enhanced CapUnifiedSelect supporting 4 scenarios with advanced features
2
+ import React, { useState, useEffect } from 'react';
3
3
  import PropTypes from 'prop-types';
4
- import { Select, TreeSelect } from 'antd';
5
- import { SelectWrapper, HeaderWrapper, StyledInfoIcon } from './styles';
6
- import CapLabel from '../CapLabel';
7
- import CapTooltip from '../CapTooltip';
4
+ import classnames from 'classnames';
5
+ import { Select, TreeSelect, Tooltip, Input, Button } from 'antd';
6
+ import { InfoCircleOutlined, SearchOutlined, WarningFilled, DownOutlined } from '@ant-design/icons';
7
+ import { SelectWrapper, HeaderWrapper, selectStyles } from './styles';
8
+ import withStyles from './withStyles';
8
9
 
9
10
  const CapUnifiedSelect = ({
10
11
  type,
@@ -15,12 +16,26 @@ const CapUnifiedSelect = ({
15
16
  placeholder = 'Select an option',
16
17
  className,
17
18
  style,
19
+ status,
20
+ statusMessage,
18
21
  allowClear = false,
19
- showSearch = false,
20
22
  label,
21
23
  tooltip,
22
24
  disabled = false,
25
+ treeCheckable = false,
26
+ customPopupRender = false,
27
+ onConfirm,
28
+ onCancel
23
29
  }) => {
30
+ const [searchText, setSearchText] = useState('');
31
+ const [tempValue, setTempValue] = useState(value);
32
+ const [allSelected, setAllSelected] = useState(false);
33
+
34
+ // Update tempValue when value changes
35
+ useEffect(() => {
36
+ setTempValue(value);
37
+ }, [value]);
38
+
24
39
  const selectVirtualizationProps = {
25
40
  listHeight: 256,
26
41
  };
@@ -30,66 +45,261 @@ const CapUnifiedSelect = ({
30
45
  listItemHeight: 32,
31
46
  };
32
47
 
48
+ // No results component
49
+ const NoResult = () => (
50
+ <div style={{
51
+ display: 'flex',
52
+ flexDirection: 'column',
53
+ alignItems: 'center',
54
+ justifyContent: 'center',
55
+ height: 200,
56
+ color: '#8c8c8c',
57
+ fontSize: 14
58
+ }}>
59
+ <WarningFilled style={{ fontSize: 36, marginBottom: 8, color: '#bfbfbf' }} />
60
+ <div style={{ fontWeight: 500 }}>No results found</div>
61
+ </div>
62
+ );
63
+
64
+ // Filter tree data based on search text
65
+ const getFilteredTreeData = (data, search) => {
66
+ if (!search) return data;
67
+
68
+ const filterNode = node => {
69
+ const titleText = node.title?.toLowerCase() || '';
70
+ const labelText = node.label?.toLowerCase() || '';
71
+ return titleText.includes(search.toLowerCase()) || labelText.includes(search.toLowerCase());
72
+ };
73
+
74
+ const loop = data =>
75
+ data.map(item => {
76
+ const children = item.children ? loop(item.children) : [];
77
+ if (filterNode(item) || children.length) {
78
+ return { ...item, children };
79
+ }
80
+ return null;
81
+ }).filter(Boolean);
82
+
83
+ return loop(data);
84
+ };
85
+
86
+ // Get all leaf node values from tree data
87
+ const flattenedLeafValues = (nodes) =>
88
+ nodes?.flatMap((node) =>
89
+ node.children ? flattenedLeafValues(node.children) : [node.value]
90
+ ) || [];
91
+
92
+ const filteredTree = getFilteredTreeData(treeData || options, searchText);
93
+ const filteredOptions = !searchText ? options : options.filter(opt =>
94
+ opt.label?.toLowerCase().includes(searchText.toLowerCase())
95
+ );
96
+
97
+ // All available leaf values for the current filtered tree data
98
+ const leafValues = flattenedLeafValues(filteredTree);
99
+
100
+ // Handles select all checkbox click
101
+ const handleSelectAll = (isTree = false) => {
102
+ const availableValues = isTree ? leafValues : filteredOptions.map(opt => opt.value);
103
+
104
+ if (allSelected) {
105
+ setTempValue([]);
106
+ } else {
107
+ setTempValue(availableValues);
108
+ }
109
+
110
+ setAllSelected(!allSelected);
111
+ };
112
+
113
+ // Update allSelected state when value changes
114
+ useEffect(() => {
115
+ const isMultipleMode = type === 'multiSelect' || type === 'multiTreeSelect';
116
+ if (isMultipleMode && Array.isArray(tempValue)) {
117
+ const availableValues = type.includes('tree') ? leafValues : filteredOptions.map(opt => opt.value);
118
+ setAllSelected(tempValue.length > 0 && tempValue.length === availableValues.length);
119
+ }
120
+ }, [tempValue, filteredOptions, leafValues, type]);
121
+
122
+ // Handle confirmation
123
+ const handleConfirm = () => {
124
+ if (onConfirm) {
125
+ onConfirm(tempValue);
126
+ } else {
127
+ onChange(tempValue);
128
+ }
129
+ setSearchText('');
130
+ };
131
+
132
+ // Handle cancellation
133
+ const handleCancel = () => {
134
+ if (onCancel) {
135
+ onCancel();
136
+ }
137
+ setTempValue(value);
138
+ setSearchText('');
139
+ };
140
+
141
+ // Handle temporary value change
142
+ const handleTempChange = (newValue) => {
143
+ setTempValue(newValue);
144
+ };
145
+
146
+ const suffix =
147
+ type === "multiTreeSelect" && type === "multiSelect" && value.length > 0 ? (
148
+ <>
149
+ <span>
150
+ +{value.length} more
151
+ <DownOutlined />
152
+ </span>
153
+ </>
154
+ ) : (
155
+ <DownOutlined />
156
+ );
157
+
33
158
  const renderHeader = () => {
34
159
  if (!label && !tooltip) return null;
35
-
36
160
  return (
37
161
  <HeaderWrapper className={disabled ? 'disabled' : ''}>
38
- {label && (
39
- <CapLabel type="label16" className={disabled ? 'disabled' : ''}>
40
- {label}
41
- </CapLabel>
42
- )}
162
+ {label && <label type="label16" className={disabled ? 'disabled' : ''}>{label}</label>}
43
163
  {tooltip && (
44
- <CapTooltip title={tooltip}>
45
- <StyledInfoIcon className={disabled ? 'disabled' : ''} />
46
- </CapTooltip>
164
+ <Tooltip title={tooltip} style={{ color: '#B3BAC5' }}>
165
+ <InfoCircleOutlined className={disabled ? 'disabled' : ''} />
166
+ </Tooltip>
47
167
  )}
48
168
  </HeaderWrapper>
49
169
  );
50
170
  };
51
171
 
52
172
  const renderDropdown = () => {
173
+ const isMultipleSelect = type === 'multiSelect' || type === 'multiTreeSelect';
174
+ const isTreeMode = type === 'treeSelect' || type === 'multiTreeSelect';
175
+ const currentItems = isTreeMode ? filteredTree : filteredOptions;
176
+ const selectedCount = Array.isArray(tempValue) ? tempValue.length : (tempValue ? 1 : 0);
177
+
178
+ // Custom dropdown render for both Select and TreeSelect
179
+ const renderCustomDropdown = (menu) => {
180
+ if (!customPopupRender) return menu;
181
+
182
+ return (
183
+ <>
184
+ <div style={{ borderBottom: '1px solid #f0f0f0', padding: '8px' }}>
185
+ <Input
186
+ prefix={<SearchOutlined style={{ color: "#B3BAC5" }} />}
187
+ placeholder="Search"
188
+ variant="borderless"
189
+ value={searchText}
190
+ onChange={(e) => setSearchText(e.target.value)}
191
+ onKeyDown={(e) => e.stopPropagation()}
192
+ />
193
+ </div>
194
+
195
+ {isMultipleSelect && currentItems.length > 0 && (
196
+ <div style={{
197
+ padding: '8px 12px',
198
+ cursor: 'pointer',
199
+ display: 'flex',
200
+ alignItems: 'center',
201
+ borderBottom: '1px solid #f0f0f0'
202
+ }}
203
+ onClick={() => handleSelectAll(isTreeMode)}>
204
+ <input
205
+ type="checkbox"
206
+ checked={allSelected}
207
+ readOnly
208
+ style={{ cursor: 'pointer' }}
209
+ />
210
+ <label style={{ marginLeft: 8, cursor: 'pointer' }}>Select all</label>
211
+ </div>
212
+ )}
213
+
214
+ {currentItems.length === 0 ? <NoResult /> : menu}
215
+
216
+ {currentItems.length > 0 && isMultipleSelect && (
217
+ <div style={{
218
+ display: 'flex',
219
+ justifyContent: 'space-between',
220
+ alignItems: 'center',
221
+ padding: '8px 12px',
222
+ borderTop: '1px solid #f0f0f0'
223
+ }}>
224
+ <div>
225
+ <Button type="primary" size="small" style={{ marginRight: 8 }} onClick={handleConfirm}>
226
+ Confirm
227
+ </Button>
228
+ <Button type="text" size="small" onClick={handleCancel}>
229
+ Cancel
230
+ </Button>
231
+ </div>
232
+ {selectedCount > 0 && (
233
+ <span style={{ color: '#8c8c8c', fontSize: '14px' }}>
234
+ {selectedCount} selected
235
+ </span>
236
+ )}
237
+ </div>
238
+ )}
239
+ </>
240
+ );
241
+ };
242
+
53
243
  if (type === 'treeSelect' || type === 'multiTreeSelect') {
54
244
  return (
55
- <TreeSelect
56
- treeData={treeData || options}
57
- value={value}
58
- onChange={onChange}
245
+ <>
246
+ <TreeSelect
247
+ treeData={filteredTree}
248
+ value={customPopupRender ? tempValue : value}
249
+ onChange={customPopupRender ? handleTempChange : onChange}
250
+ placeholder={placeholder}
251
+ maxTagCount={0} // hides all tags
252
+ maxTagPlaceholder={() => null} // hides default `+N more` placeholder
253
+ prefix={type === "multiTreeSelect" && value.length > 0 && <>{placeholder}</>}
254
+ suffixIcon={suffix}
255
+ className={classnames(`cap-unified-tree-select ${className || ''}`)}
256
+ style={style}
257
+ status={status}
258
+ allowClear={allowClear}
259
+ showSearch={false}
260
+ multiple={type === 'multiTreeSelect'}
261
+ treeCheckable={treeCheckable}
262
+ showCheckedStrategy={TreeSelect.SHOW_PARENT}
263
+ virtual
264
+ disabled={disabled}
265
+ filterTreeNode={false}
266
+ {...treeSelectVirtualizationProps}
267
+ popupRender={renderCustomDropdown}
268
+ />
269
+ {status === 'error' && <div style={{ color: '#E83135' }} className="cap-unified-select-status">{statusMessage}</div>}
270
+ </>
271
+ );
272
+ }
273
+
274
+ return (
275
+ <>
276
+ <Select
277
+ options={filteredOptions}
278
+ value={customPopupRender ? tempValue : value}
279
+ onChange={customPopupRender ? handleTempChange : onChange}
59
280
  placeholder={placeholder}
60
- className={className}
281
+ maxTagCount={0}
282
+ maxTagPlaceholder={() => null}
283
+ prefix={type === "multiSelect" && value.length > 0 && <>{placeholder}</>}
284
+ suffixIcon={suffix}
285
+ className={classnames(`cap-unified-select ${className || ''}`)}
61
286
  style={style}
62
287
  allowClear={allowClear}
63
- showSearch={showSearch}
64
- multiple={type === 'multiTreeSelect' ? true : false}
288
+ showSearch={false}
289
+ mode={type === 'multiSelect' ? 'multiple' : undefined}
65
290
  virtual
66
- treeDefaultExpandAll
67
291
  disabled={disabled}
68
- {...treeSelectVirtualizationProps}
292
+ status={status}
293
+ {...selectVirtualizationProps}
294
+ popupRender={renderCustomDropdown}
69
295
  />
70
- );
71
- }
72
-
73
- return (
74
- <Select
75
- value={value}
76
- onChange={onChange}
77
- placeholder={placeholder}
78
- className={className}
79
- style={style}
80
- allowClear={allowClear}
81
- showSearch={showSearch}
82
- options={options}
83
- mode={type === 'multiSelect' ? 'multiple' : undefined}
84
- virtual
85
- disabled={disabled}
86
- {...selectVirtualizationProps}
87
- />
296
+ {status === 'error' && <div style={{ color: '#E83135' }} className="cap-unified-select-status">{statusMessage}</div>}
297
+ </>
88
298
  );
89
- };
299
+ };
90
300
 
91
301
  return (
92
- <SelectWrapper>
302
+ <SelectWrapper className={classnames(`cap-unified-select-container ${className || ''}`)}>
93
303
  {renderHeader()}
94
304
  {renderDropdown()}
95
305
  </SelectWrapper>
@@ -106,16 +316,20 @@ CapUnifiedSelect.propTypes = {
106
316
  className: PropTypes.string,
107
317
  style: PropTypes.object,
108
318
  allowClear: PropTypes.bool,
109
- showSearch: PropTypes.bool,
110
319
  label: PropTypes.string,
111
320
  tooltip: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
112
321
  disabled: PropTypes.bool,
322
+ treeCheckable: PropTypes.bool,
323
+ customPopupRender: PropTypes.bool,
324
+ onConfirm: PropTypes.func,
325
+ onCancel: PropTypes.func,
113
326
  };
114
327
 
115
328
  CapUnifiedSelect.defaultProps = {
116
329
  type: 'select',
117
330
  allowClear: false,
118
- showSearch: false,
331
+ customPopupRender: false,
332
+ treeCheckable: false,
119
333
  };
120
334
 
121
- export default CapUnifiedSelect;
335
+ export default withStyles(CapUnifiedSelect, selectStyles);
@@ -23,21 +23,72 @@ export const HeaderWrapper = styled.div`
23
23
  }
24
24
  `;
25
25
 
26
+ export const DropdownFooter = styled.div`
27
+ display: flex;
28
+ justify-content: space-between;
29
+ align-items: center;
30
+ padding: 8px;
31
+ border-top: 1px solid #f0f0f0;
32
+
33
+ .footer-buttons {
34
+ display: flex;
35
+ align-items: center;
36
+ gap: 8px;
37
+ }
38
+
39
+ .total-count {
40
+ color: #8c8c8c;
41
+ font-size: 12px;
42
+ }
43
+ `;
44
+
45
+ export const SearchInputWrapper = styled.div`
46
+ padding: 8px;
47
+ border-bottom: 1px solid #f0f0f0;
48
+ `;
49
+
50
+ export const SelectAllCheckbox = styled.div`
51
+ display: flex;
52
+ align-items: center;
53
+ padding: 8px 12px;
54
+ cursor: pointer;
55
+ font-weight: 500;
56
+ border-bottom: 1px solid #f0f0f0;
57
+ user-select: none;
58
+
59
+ input[type="checkbox"] {
60
+ cursor: pointer;
61
+ }
62
+ `;
63
+
64
+ export const NoResultWrapper = styled.div`
65
+ display: flex;
66
+ flex-direction: column;
67
+ align-items: center;
68
+ justify-content: center;
69
+ height: 200px;
70
+ color: #8c8c8c;
71
+ font-size: 14px;
72
+ `;
73
+
26
74
 
27
75
 
28
76
  export const StyledInfoIcon = styled.span`
29
- color: ${styledVars.CAP_G2};
77
+ color: ${styledVars.CAP_G05};
30
78
  font-size: 16px;
31
79
  cursor: help;
80
+ margin-left: 4px;
81
+ display: flex;
82
+ align-items: center;
32
83
 
33
84
  &:hover {
34
- color: ${styledVars.CAP_G1};
85
+ color: ${styledVars.CAP_G03};
35
86
  }
36
87
 
37
88
  &.disabled {
38
89
  cursor: not-allowed;
39
90
  &:hover {
40
- color: ${styledVars.CAP_G2};
91
+ color: ${styledVars.CAP_G05};
41
92
  }
42
93
  }
43
94
  `;
@@ -91,30 +142,46 @@ export const selectStyles = css`
91
142
  }
92
143
 
93
144
  /* Dropdown styles */
94
- .ant-select-dropdown {
95
- padding: 4px;
96
- border-radius: ${styledVars.RADIUS_04};
97
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
98
-
99
- .ant-select-item {
100
- border-radius: ${styledVars.RADIUS_04};
101
- padding: 8px 12px;
102
- transition: ${styledVars.TRANSITION_ALL};
145
+ .ant-select-dropdown,
146
+ &.ant-select-dropdown,
147
+ &.ant-select-dropdown-placement-bottomLeft {
148
+ padding: 0;
149
+ border-radius: 12px !important;
150
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
151
+ overflow: hidden;
152
+
153
+ /* Option base style - no background */
154
+ .ant-select-item-option {
155
+ padding: 10px 16px;
156
+ font-size: 14px;
157
+ color: #1c2530;
158
+ font-weight: 500;
159
+ background-color: transparent !important;
160
+
161
+ /* Hover state only */
162
+ &:not(.ant-select-item-option-disabled):hover {
163
+ background-color: #fffbe6 !important;
164
+ border-radius: 4px;
165
+ color: #1c2530 !important;
166
+ }
103
167
 
104
- &-option-selected {
105
- background-color: ${styledVars.CAP_PRIMARY.light};
168
+ /* Selected state */
169
+ &-selected {
106
170
  color: ${styledVars.CAP_PRIMARY.base};
107
171
  font-weight: 500;
108
172
  }
109
173
 
110
- &-option-active {
111
- background-color: ${styledVars.CAP_G08};
174
+ /* Remove active state background unless hovered */
175
+ &-active:not(:hover):not(.ant-select-item-option-disabled) {
176
+ background-color: transparent !important;
112
177
  }
113
178
  }
114
179
 
115
180
  /* Search input styles */
181
+ .ant-select-dropdown-search,
116
182
  .ant-select-search {
117
- padding: 8px;
183
+ padding: 8px 12px;
184
+ border-bottom: 1px solid #f0f0f0;
118
185
 
119
186
  input {
120
187
  border-radius: ${styledVars.RADIUS_04};
@@ -126,6 +193,64 @@ export const selectStyles = css`
126
193
  }
127
194
  }
128
195
  }
196
+
197
+ /* Scrollbar */
198
+ .rc-virtual-list-scrollbar-thumb {
199
+ background-color: #dcdcdc;
200
+ border-radius: 4px;
201
+ }
202
+
203
+ /* Divider */
204
+ .ant-divider-horizontal {
205
+ margin: 0;
206
+ }
207
+
208
+ /* No result UI */
209
+ .no-result {
210
+ display: flex;
211
+ flex-direction: column;
212
+ align-items: center;
213
+ justify-content: center;
214
+ height: 200px;
215
+ color: #8c8c8c;
216
+ font-size: 14px;
217
+ }
218
+
219
+ /* Dropdown search inside custom popup */
220
+ .dropdown-search {
221
+ padding: 8px;
222
+ border-bottom: 1px solid #f0f0f0;
223
+ }
224
+
225
+ /* Select all checkbox */
226
+ .select-all-checkbox {
227
+ display: flex;
228
+ align-items: center;
229
+ padding: 8px 12px;
230
+ cursor: pointer;
231
+ font-weight: 500;
232
+ border-bottom: 1px solid #f0f0f0;
233
+ user-select: none;
234
+
235
+ input[type="checkbox"] {
236
+ cursor: pointer;
237
+ }
238
+ }
239
+
240
+ /* Footer buttons */
241
+ .dropdown-footer {
242
+ display: flex;
243
+ justify-content: space-between;
244
+ align-items: center;
245
+ padding: 8px;
246
+ border-top: 1px solid #f0f0f0;
247
+ }
248
+
249
+ /* Selected counter */
250
+ .selected-count {
251
+ color: #8c8c8c;
252
+ font-size: 12px;
253
+ }
129
254
  }
130
255
 
131
256
  /* Multiple selection styles */
@@ -162,14 +287,15 @@ export const selectStyles = css`
162
287
  .ant-select-tree-node-content-wrapper {
163
288
  padding: 4px 8px;
164
289
  border-radius: ${styledVars.RADIUS_04};
165
- transition: ${styledVars.TRANSITION_ALL};
290
+ background-color: transparent !important;
166
291
 
167
292
  &:hover {
168
- background-color: ${styledVars.CAP_G08};
293
+ background-color: #fffbe6 !important;
294
+ color: #1c2530 !important;
295
+ border-radius: 4px;
169
296
  }
170
297
 
171
298
  &.ant-select-tree-node-selected {
172
- background-color: ${styledVars.CAP_PRIMARY.light};
173
299
  color: ${styledVars.CAP_PRIMARY.base};
174
300
  font-weight: 500;
175
301
  }
@@ -184,6 +310,19 @@ export const selectStyles = css`
184
310
  .ant-select-tree-checkbox {
185
311
  margin: 4px 8px 4px 0;
186
312
  }
313
+
314
+ .ant-select-tree-treenode {
315
+ padding: 2px 0;
316
+
317
+ &:hover {
318
+ background-color: transparent;
319
+ }
320
+ }
321
+
322
+ .ant-select-tree-checkbox-checked .ant-select-tree-checkbox-inner {
323
+ background-color: ${styledVars.CAP_PRIMARY.base};
324
+ border-color: ${styledVars.CAP_PRIMARY.base};
325
+ }
187
326
  }
188
327
 
189
328
  /* Size variations */
@@ -219,5 +358,17 @@ export const selectStyles = css`
219
358
  }
220
359
  }
221
360
  }
361
+
362
+ &.cap-unified-tree-select .cap-unified-select .ant-select-selection-overflow,
363
+ &.cap-unified-select .ant-select-selection-overflow {
364
+ display: none; /* hides pill wrapper */
365
+ }
366
+
367
+ &.cap-unified-tree-select .cap-unified-select .suffix-counter,
368
+ &.cap-unified-select .suffix-counter {
369
+ color: #1c2530;
370
+ font-weight: 500;
371
+ margin-right: 12px; /* optional, adjust spacing */
372
+ }
222
373
  }
223
374
  `;