@arcblock/ux 2.1.6 → 2.1.7

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.
@@ -0,0 +1,154 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ exports.handleCSVDownload = handleCSVDownload;
8
+
9
+ var _find = _interopRequireDefault(require("lodash/find"));
10
+
11
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
12
+
13
+ function escapeDangerousCSVCharacters(data) {
14
+ if (typeof data === 'string') {
15
+ // Places single quote before the appearance of dangerous characters if they
16
+ // are the first in the data string.
17
+ // eslint-disable-next-line no-useless-escape
18
+ return data.replace(/^\+|^\-|^\=|^\@/g, "'$&");
19
+ }
20
+
21
+ return data;
22
+ }
23
+
24
+ function buildCSV(columns, data, options) {
25
+ const replaceDoubleQuoteInString = columnData => typeof columnData === 'string' ? columnData.replace(/"/g, '""') : columnData; // eslint-disable-next-line no-shadow
26
+
27
+
28
+ const buildHead = columns => {
29
+ return "".concat(columns.reduce((soFar, column) => column.download ? "".concat(soFar, "\"").concat(escapeDangerousCSVCharacters(replaceDoubleQuoteInString(column.label || column.name)), "\"").concat(options.downloadOptions.separator) : soFar, '').slice(0, -1), "\r\n");
30
+ };
31
+
32
+ const CSVHead = buildHead(columns); // eslint-disable-next-line no-shadow
33
+
34
+ const buildBody = data => {
35
+ if (!data.length) return '';
36
+ return data.reduce((soFar, row) => "".concat(soFar, "\"").concat(row.data.filter((_, index) => columns[index].download).map(columnData => escapeDangerousCSVCharacters(replaceDoubleQuoteInString(columnData))).join("\"".concat(options.downloadOptions.separator, "\"")), "\"\r\n"), '').trim();
37
+ };
38
+
39
+ const CSVBody = buildBody(data);
40
+ const csv = options.onDownload ? options.onDownload(buildHead, buildBody, columns, data) : "".concat(CSVHead).concat(CSVBody).trim();
41
+ return csv;
42
+ }
43
+
44
+ function downloadCSV(csv, filename) {
45
+ const blob = new Blob([csv], {
46
+ type: 'text/csv'
47
+ });
48
+ /* taken from react-csv */
49
+
50
+ if (navigator && navigator.msSaveOrOpenBlob) {
51
+ navigator.msSaveOrOpenBlob(blob, filename);
52
+ } else {
53
+ const dataURI = "data:text/csv;charset=utf-8,".concat(csv);
54
+ const URL = window.URL || window.webkitURL;
55
+ const downloadURI = typeof URL.createObjectURL === 'undefined' ? dataURI : URL.createObjectURL(blob);
56
+ const link = document.createElement('a');
57
+ link.setAttribute('href', downloadURI);
58
+ link.setAttribute('download', filename);
59
+ document.body.appendChild(link);
60
+ link.click();
61
+ document.body.removeChild(link);
62
+ }
63
+ } // eslint-disable-next-line no-shadow
64
+
65
+
66
+ function createCSVDownload(columns, data, options, downloadCSV) {
67
+ const csv = buildCSV(columns, data, options);
68
+
69
+ if (options.onDownload && csv === false) {
70
+ return;
71
+ }
72
+
73
+ downloadCSV(csv, options.downloadOptions.filename);
74
+ }
75
+
76
+ function handleCSVDownload(props) {
77
+ const {
78
+ data,
79
+ displayData,
80
+ columns,
81
+ options,
82
+ columnOrder
83
+ } = props;
84
+ let dataToDownload = [];
85
+ let columnsToDownload = [];
86
+ let columnOrderCopy = Array.isArray(columnOrder) ? columnOrder.slice(0) : [];
87
+
88
+ if (columnOrderCopy.length === 0) {
89
+ columnOrderCopy = columns.map((item, idx) => idx);
90
+ }
91
+
92
+ data.forEach(row => {
93
+ const newRow = {
94
+ index: row.index,
95
+ data: []
96
+ };
97
+ columnOrderCopy.forEach(idx => {
98
+ newRow.data.push(row.data[idx]);
99
+ });
100
+ dataToDownload.push(newRow);
101
+ });
102
+ columnOrderCopy.forEach(idx => {
103
+ columnsToDownload.push(columns[idx]);
104
+ });
105
+
106
+ if (options.downloadOptions && options.downloadOptions.filterOptions) {
107
+ // check rows first:
108
+ if (options.downloadOptions.filterOptions.useDisplayedRowsOnly) {
109
+ const filteredDataToDownload = displayData.map((row, index) => {
110
+ let i = -1; // Help to preserve sort order in custom render columns
111
+
112
+ row.index = index;
113
+ return {
114
+ data: row.data.map(column => {
115
+ i += 1; // if we have a custom render, which will appear as a react element, we must grab the actual value from data
116
+ // that matches the dataIndex and column
117
+ // TODO: Create a utility function for checking whether or not something is a react object
118
+
119
+ let val = typeof column === 'object' && column !== null && !Array.isArray(column) ? (0, _find.default)(data, d => d.index === row.dataIndex).data[i] : column;
120
+ val = typeof val === 'function' ? (0, _find.default)(data, d => d.index === row.dataIndex).data[i] : val;
121
+ return val;
122
+ })
123
+ };
124
+ });
125
+ dataToDownload = [];
126
+ filteredDataToDownload.forEach(row => {
127
+ const newRow = {
128
+ index: row.index,
129
+ data: []
130
+ };
131
+ columnOrderCopy.forEach(idx => {
132
+ newRow.data.push(row.data[idx]);
133
+ });
134
+ dataToDownload.push(newRow);
135
+ });
136
+ } // now, check columns:
137
+
138
+
139
+ if (options.downloadOptions.filterOptions.useDisplayedColumnsOnly) {
140
+ columnsToDownload = columnsToDownload.filter(_ => _.display === 'true');
141
+ dataToDownload = dataToDownload.map(row => {
142
+ row.data = row.data.filter((_, index) => columns[columnOrderCopy[index]].display === 'true');
143
+ return row;
144
+ });
145
+ }
146
+ }
147
+
148
+ createCSVDownload(columnsToDownload, dataToDownload, options, downloadCSV);
149
+ }
150
+
151
+ var _default = {
152
+ handleCSVDownload
153
+ };
154
+ exports.default = _default;
package/lib/index.js CHANGED
@@ -69,6 +69,12 @@ Object.defineProperty(exports, "CountDown", {
69
69
  return _CountDown.default;
70
70
  }
71
71
  });
72
+ Object.defineProperty(exports, "Datatable", {
73
+ enumerable: true,
74
+ get: function get() {
75
+ return _Datatable.default;
76
+ }
77
+ });
72
78
  Object.defineProperty(exports, "Earth", {
73
79
  enumerable: true,
74
80
  get: function get() {
@@ -230,6 +236,8 @@ var _Logo = _interopRequireDefault(require("./Logo"));
230
236
 
231
237
  var _RelativeTime = _interopRequireDefault(require("./RelativeTime"));
232
238
 
239
+ var _Datatable = _interopRequireDefault(require("./Datatable"));
240
+
233
241
  var _Tabs = _interopRequireDefault(require("./Tabs"));
234
242
 
235
243
  var _Tag = _interopRequireDefault(require("./Tag"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcblock/ux",
3
- "version": "2.1.6",
3
+ "version": "2.1.7",
4
4
  "description": "Common used react components for arcblock products",
5
5
  "keywords": [
6
6
  "react",
@@ -52,10 +52,10 @@
52
52
  "react": ">=18.1.0",
53
53
  "react-ga": "^2.7.0"
54
54
  },
55
- "gitHead": "2b8a9723f5280a51416f34afd78472d2e4dbb353",
55
+ "gitHead": "a54ef3a9335aae1bc7404b5f4a8c9c508b6ff81f",
56
56
  "dependencies": {
57
- "@arcblock/icons": "^2.1.6",
58
- "@arcblock/react-hooks": "^2.1.6",
57
+ "@arcblock/icons": "^2.1.7",
58
+ "@arcblock/react-hooks": "^2.1.7",
59
59
  "@babel/plugin-syntax-dynamic-import": "^7.8.3",
60
60
  "@emotion/react": "^11.9.0",
61
61
  "@emotion/styled": "^11.8.1",
@@ -75,6 +75,7 @@
75
75
  "js-cookie": "^2.2.0",
76
76
  "lodash": "^4.17.21",
77
77
  "mdi-material-ui": "^7.2.0",
78
+ "mui-datatables": "^4.2.2",
78
79
  "notistack": "^2.0.5",
79
80
  "react-cookie-consent": "^6.4.1",
80
81
  "react-helmet": "^6.1.0",
@@ -0,0 +1,371 @@
1
+ import React, { useState, useRef, isValidElement } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { TableFilter, TableViewCol } from 'mui-datatables';
4
+ import styled from 'styled-components';
5
+ import ReactToPrint, { PrintContextConsumer } from 'react-to-print';
6
+ import IconButton from '@mui/material/IconButton';
7
+ import Tooltip from '@mui/material/Tooltip';
8
+ import DownloadIcon from '@mui/icons-material/CloudDownload';
9
+ import PrintIcon from '@mui/icons-material/Print';
10
+ import ViewColumnIcon from '@mui/icons-material/ViewColumn';
11
+ import FilterIcon from '@mui/icons-material/FilterList';
12
+ import Popover from '@mui/material/Popover';
13
+ import MoreVertIcon from '@mui/icons-material/MoreVert';
14
+ import Menu from '@mui/material/Menu';
15
+ import MenuItem from '@mui/material/MenuItem';
16
+ import ListItemIcon from '@mui/material/ListItemIcon';
17
+ import ListItemText from '@mui/material/ListItemText';
18
+ import useMediaQuery from '@mui/material/useMediaQuery';
19
+ import { useTheme } from '@mui/material/styles';
20
+ import { handleCSVDownload } from './utils';
21
+ import TableSearch from './TableSearch';
22
+
23
+ function useMobile() {
24
+ const theme = useTheme();
25
+ return useMediaQuery(theme.breakpoints.down('sm'));
26
+ }
27
+
28
+ export default function CustomToolbar(props) {
29
+ const [menuIconEl, setMenuIconEl] = useState(null);
30
+ const moreBtn = useRef(null);
31
+ const isMobile = useMobile();
32
+ const toolbarId = useRef(Math.random().toString(32).slice(2));
33
+
34
+ const {
35
+ data,
36
+ options,
37
+ components,
38
+ columns,
39
+ filterList,
40
+ filterData,
41
+ filterUpdate,
42
+ resetFilters,
43
+ updateFilterByType,
44
+ toggleViewColumn,
45
+ updateColumns,
46
+ title,
47
+ searchText,
48
+ searchTextUpdate,
49
+ searchClose,
50
+ customButtons,
51
+ } = props;
52
+
53
+ const { search, downloadCsv, print, viewColumns, filterTable } = options.textLabels.toolbar;
54
+
55
+ const hideSearch = options.search === false || options.search === 'false';
56
+ const hidePrint = options.print === false || options.print === 'false';
57
+
58
+ const TableFilterComponent = components.TableFilter || TableFilter;
59
+ const TableViewColComponent = components.TableViewCol || TableViewCol;
60
+
61
+ const printArea = func => {
62
+ return (
63
+ <ReactToPrint content={() => props.tableRef()}>
64
+ <PrintContextConsumer>{func}</PrintContextConsumer>
65
+ </ReactToPrint>
66
+ );
67
+ };
68
+
69
+ const getPopId = key => `toolbar-pop-${toolbarId.current}-${key}`;
70
+
71
+ const defaultButtons = [];
72
+
73
+ if (!(options.download === false || options.download === 'false')) {
74
+ defaultButtons.push({
75
+ Icon: DownloadIcon,
76
+ title: downloadCsv,
77
+ onClick: () => {
78
+ handleCSVDownload(props);
79
+ },
80
+ });
81
+ }
82
+
83
+ if (!(options.viewColumns === false || options.viewColumns === 'false')) {
84
+ defaultButtons.push({
85
+ Icon: ViewColumnIcon,
86
+ title: viewColumns,
87
+ popRender() {
88
+ return (
89
+ <TableViewColComponent
90
+ data={data}
91
+ columns={columns}
92
+ options={options}
93
+ onColumnUpdate={toggleViewColumn}
94
+ updateColumns={updateColumns}
95
+ components={components}
96
+ />
97
+ );
98
+ },
99
+ });
100
+ }
101
+
102
+ if (!(options.filter === false || options.filter === 'false')) {
103
+ defaultButtons.push({
104
+ Icon: FilterIcon,
105
+ title: filterTable,
106
+ popRender() {
107
+ return (
108
+ <TableFilterComponent
109
+ customFooter={options.customFilterDialogFooter}
110
+ columns={columns}
111
+ options={options}
112
+ filterList={filterList}
113
+ filterData={filterData}
114
+ onFilterUpdate={filterUpdate}
115
+ onFilterReset={resetFilters}
116
+ updateFilterByType={updateFilterByType}
117
+ components={components}
118
+ />
119
+ );
120
+ },
121
+ });
122
+ }
123
+
124
+ const showMore =
125
+ [!hidePrint, ...defaultButtons, ...customButtons].filter(e => !!e).length > 1 && isMobile;
126
+
127
+ const allPops = [];
128
+ const [allPopsEl, setAllPopsEl] = useState({});
129
+
130
+ const toolbarButtons = [...defaultButtons, ...customButtons].map((e, index) => {
131
+ if (isValidElement(e)) {
132
+ return e;
133
+ }
134
+
135
+ const popId = getPopId(index);
136
+
137
+ if (e.Icon) {
138
+ const { Icon, popRender } = e;
139
+ if (popRender) {
140
+ allPops.push(
141
+ <Popover
142
+ open={!!allPopsEl[popId]}
143
+ anchorEl={() => allPopsEl[popId]}
144
+ onClose={() => {
145
+ setAllPopsEl({});
146
+ }}
147
+ key={popId}
148
+ anchorOrigin={{
149
+ vertical: 'bottom',
150
+ horizontal: 'right',
151
+ }}>
152
+ <div>{popRender()}</div>
153
+ </Popover>
154
+ );
155
+ }
156
+
157
+ return (
158
+ <Tooltip title={e.title} key={popId}>
159
+ <IconButton
160
+ data-testid={`${e.title}-iconButton`}
161
+ id={`btn-${popId}`}
162
+ aria-label={e.title}
163
+ onClick={() => {
164
+ if (e.onClick) {
165
+ e.onClick();
166
+ }
167
+
168
+ if (popRender) {
169
+ setAllPopsEl({
170
+ [popId]: document.getElementById(`btn-${popId}`),
171
+ });
172
+ }
173
+ }}>
174
+ <Icon />
175
+ </IconButton>
176
+ </Tooltip>
177
+ );
178
+ }
179
+
180
+ return e;
181
+ });
182
+
183
+ const menuItems = [...defaultButtons, ...customButtons].map((e, index) => {
184
+ const popId = getPopId(index);
185
+
186
+ let content;
187
+
188
+ if (isValidElement(e)) {
189
+ content = e;
190
+ } else if (e.Icon) {
191
+ const { Icon } = e;
192
+
193
+ content = (
194
+ <>
195
+ <ListItemIcon>
196
+ <Icon fontSize="small" />
197
+ </ListItemIcon>
198
+ <ListItemText>{e.title}</ListItemText>
199
+ </>
200
+ );
201
+ }
202
+
203
+ return (
204
+ <MenuItem
205
+ key={popId}
206
+ onClick={() => {
207
+ setMenuIconEl(null);
208
+ if (e.onClick) {
209
+ e.onClick();
210
+ }
211
+
212
+ if (e.popRender) {
213
+ setAllPopsEl({
214
+ [popId]: moreBtn.current,
215
+ });
216
+ }
217
+ }}>
218
+ {content}
219
+ </MenuItem>
220
+ );
221
+ });
222
+
223
+ return (
224
+ <div>
225
+ <Container>
226
+ <div className="custom-toobar-title">
227
+ <div className="custom-toobar-title-inner">
228
+ <span>{title}</span>
229
+ </div>
230
+ </div>
231
+ <div className="custom-toobar-right">
232
+ <div className="custom-toobar-btns">
233
+ {!hideSearch && (
234
+ <TableSearch
235
+ search={search}
236
+ options={options}
237
+ searchText={searchText}
238
+ searchTextUpdate={searchTextUpdate}
239
+ searchClose={searchClose}
240
+ isMobile={isMobile}
241
+ />
242
+ )}
243
+ {!showMore && (
244
+ <>
245
+ {!hidePrint &&
246
+ printArea(({ handlePrint }) => (
247
+ <span>
248
+ <Tooltip title={print}>
249
+ <IconButton
250
+ data-testid={`${print}-iconButton`}
251
+ aria-label={print}
252
+ disabled={options.print === 'disabled'}
253
+ onClick={handlePrint}>
254
+ <PrintIcon />
255
+ </IconButton>
256
+ </Tooltip>
257
+ </span>
258
+ ))}
259
+
260
+ {toolbarButtons}
261
+ </>
262
+ )}
263
+ {showMore && (
264
+ <IconButton
265
+ ref={moreBtn}
266
+ aria-haspopup="true"
267
+ aria-expanded={menuIconEl ? 'true' : undefined}
268
+ onClick={event => setMenuIconEl(event.currentTarget)}>
269
+ <MoreVertIcon />
270
+ </IconButton>
271
+ )}
272
+ </div>
273
+ </div>
274
+ </Container>
275
+
276
+ <Menu
277
+ anchorEl={menuIconEl}
278
+ open={!!menuIconEl}
279
+ onClose={() => setMenuIconEl(null)}
280
+ MenuListProps={{
281
+ 'aria-labelledby': 'more-button',
282
+ }}>
283
+ {!hidePrint &&
284
+ printArea(({ handlePrint }) => (
285
+ <MenuItem
286
+ onClick={() => {
287
+ setMenuIconEl(null);
288
+ handlePrint();
289
+ }}>
290
+ <ListItemIcon>
291
+ <PrintIcon fontSize="small" />
292
+ </ListItemIcon>
293
+ <ListItemText>{print}</ListItemText>
294
+ </MenuItem>
295
+ ))}
296
+ {menuItems}
297
+ </Menu>
298
+ {allPops.map((e, index) => (
299
+ <div key={getPopId(index)}>{e}</div>
300
+ ))}
301
+ </div>
302
+ );
303
+ }
304
+
305
+ CustomToolbar.propTypes = {
306
+ data: PropTypes.array,
307
+ options: PropTypes.object.isRequired,
308
+ components: PropTypes.object,
309
+ columns: PropTypes.array.isRequired,
310
+ filterList: PropTypes.array,
311
+ filterData: PropTypes.array,
312
+ filterUpdate: PropTypes.func.isRequired,
313
+ resetFilters: PropTypes.func.isRequired,
314
+ updateFilterByType: PropTypes.func.isRequired,
315
+ toggleViewColumn: PropTypes.func.isRequired,
316
+ updateColumns: PropTypes.func.isRequired,
317
+ title: PropTypes.string,
318
+ searchText: PropTypes.any,
319
+ searchTextUpdate: PropTypes.func.isRequired,
320
+ searchClose: PropTypes.func.isRequired,
321
+ tableRef: PropTypes.func.isRequired,
322
+ customButtons: PropTypes.array.isRequired,
323
+ };
324
+
325
+ CustomToolbar.defaultProps = {
326
+ data: [],
327
+ components: {},
328
+ filterList: [],
329
+ filterData: [],
330
+ title: '',
331
+ searchText: null,
332
+ };
333
+
334
+ const Container = styled.div`
335
+ display: flex;
336
+ align-items: center;
337
+ height: 56px;
338
+ .custom-toobar {
339
+ &-title {
340
+ position: relative;
341
+ flex: 1;
342
+ font-size: 18px;
343
+ font-weight: 800;
344
+ height: 56px;
345
+ &-inner {
346
+ line-height: 56px;
347
+ width: 100%;
348
+ height: 56px;
349
+ position: absolute;
350
+ left: 0;
351
+ top: 0;
352
+ span {
353
+ display: inline-block;
354
+ max-width: 100%;
355
+ white-space: nowrap;
356
+ text-overflow: ellipsis;
357
+ overflow: hidden;
358
+ }
359
+ }
360
+ }
361
+ &-right {
362
+ display: flex;
363
+ margin-left: auto;
364
+ }
365
+ &-btns {
366
+ display: flex;
367
+ justify-content: center;
368
+ align-items: center;
369
+ }
370
+ }
371
+ `;
@@ -0,0 +1,127 @@
1
+ import React, { useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import IconButton from '@mui/material/IconButton';
4
+ import Tooltip from '@mui/material/Tooltip';
5
+ import SearchIcon from '@mui/icons-material/Search';
6
+ import TextField from '@mui/material/TextField';
7
+ import ClearIcon from '@mui/icons-material/Clear';
8
+ import styled from 'styled-components';
9
+
10
+ export default function TableSearch({
11
+ search,
12
+ options,
13
+ searchText,
14
+ searchTextUpdate,
15
+ searchClose,
16
+ isMobile,
17
+ onSearchOpen,
18
+ }) {
19
+ const [inputMode, setInputMode] = useState(false);
20
+
21
+ const clickSearchIcon = () => {
22
+ setInputMode(true);
23
+ onSearchOpen(true);
24
+ };
25
+
26
+ const onChange = event => {
27
+ searchTextUpdate(event.currentTarget.value);
28
+ };
29
+
30
+ const clickClose = () => {
31
+ setInputMode(false);
32
+ searchClose();
33
+ onSearchOpen(false);
34
+ };
35
+
36
+ return (
37
+ <Container>
38
+ {inputMode ? (
39
+ <div className="toolbar-search-icon-placeholder-">
40
+ <SearchIcon />
41
+ </div>
42
+ ) : (
43
+ <Tooltip title={search} disableFocusListener>
44
+ <IconButton
45
+ aria-label={search}
46
+ data-testid={`${search}-iconButton`}
47
+ disabled={options.search === 'disabled'}
48
+ onClick={clickSearchIcon}>
49
+ <SearchIcon />
50
+ </IconButton>
51
+ </Tooltip>
52
+ )}
53
+
54
+ <div
55
+ className={`toolbar-search-area ${inputMode ? 'toolbar-btn-show' : ''} ${
56
+ isMobile ? 'small-textfield' : ''
57
+ }`}>
58
+ {inputMode && (
59
+ <TextField
60
+ variant="standard"
61
+ spacing={2}
62
+ onChange={onChange}
63
+ value={searchText || ''}
64
+ autoFocus
65
+ />
66
+ )}
67
+ </div>
68
+ <div className={`toolbar-search-close ${inputMode ? 'toolbar-btn-show' : ''}`}>
69
+ <IconButton onClick={clickClose}>
70
+ <ClearIcon />
71
+ </IconButton>
72
+ </div>
73
+ </Container>
74
+ );
75
+ }
76
+
77
+ TableSearch.propTypes = {
78
+ search: PropTypes.string,
79
+ searchText: PropTypes.string,
80
+ onSearchOpen: PropTypes.func,
81
+ options: PropTypes.object.isRequired,
82
+ searchTextUpdate: PropTypes.func.isRequired,
83
+ searchClose: PropTypes.func.isRequired,
84
+ isMobile: PropTypes.bool.isRequired,
85
+ };
86
+
87
+ TableSearch.defaultProps = {
88
+ search: '',
89
+ searchText: '',
90
+ onSearchOpen: () => {},
91
+ };
92
+
93
+ const Container = styled.div`
94
+ display: flex;
95
+ align-items: center;
96
+ .toolbar-search-area {
97
+ width: 0;
98
+ transition: all ease 0.3s;
99
+ overflow: hidden;
100
+ .MuiFormControl-root {
101
+ width: inherit;
102
+ margin: 0 12px;
103
+ }
104
+ &.toolbar-btn-show {
105
+ width: 260px;
106
+ padding-left: 8px;
107
+ &.small-textfield {
108
+ width: 200px;
109
+ }
110
+ }
111
+ }
112
+ .toolbar-search-close {
113
+ width: 0;
114
+ transition: all ease 0.3s;
115
+ overflow: hidden;
116
+ &.toolbar-btn-show {
117
+ width: 40px;
118
+ }
119
+ }
120
+ .toolbar-search-icon-placeholder {
121
+ display: flex;
122
+ justify-content: center;
123
+ align-items: center;
124
+ width: 40px;
125
+ height: 40px;
126
+ }
127
+ `;