cynx-ui 1.2.42 → 1.2.44
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/components/Dropzone.jsx +193 -19
- package/components/FilePicker.jsx +205 -0
- package/components/Select.jsx +36 -9
- package/package.json +1 -1
package/components/Dropzone.jsx
CHANGED
|
@@ -1,28 +1,202 @@
|
|
|
1
1
|
import { useState, useRef } from 'react';
|
|
2
|
+
import { UploadCloud, Paperclip, FileCheck, X, Eye, Trash2 } from 'lucide-react';
|
|
3
|
+
import LineProgress from './LineProgress.jsx';
|
|
4
|
+
import Badge from './Badge.jsx';
|
|
5
|
+
import Button from './Button.jsx';
|
|
2
6
|
|
|
3
|
-
|
|
7
|
+
function formatBytes(bytes, decimals = 1) {
|
|
8
|
+
if (!bytes || bytes === 0) return '0 B';
|
|
9
|
+
const k = 1024;
|
|
10
|
+
const dm = decimals < 0 ? 0 : decimals;
|
|
11
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
12
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
13
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function Dropzone({
|
|
17
|
+
onFiles,
|
|
18
|
+
onFile,
|
|
19
|
+
accept = 'image/*,application/pdf,.*',
|
|
20
|
+
multiple = true,
|
|
21
|
+
hint = 'Поддерживаются изображения, PDF документы и любые файлы',
|
|
22
|
+
uploadingFiles = [],
|
|
23
|
+
existingFiles = [],
|
|
24
|
+
onRemoveUploading,
|
|
25
|
+
onRemoveExisting,
|
|
26
|
+
onPreview,
|
|
27
|
+
label = 'Прикрепить файлы (Фото, PDF, документы)',
|
|
28
|
+
}) {
|
|
4
29
|
const [over, setOver] = useState(false);
|
|
5
30
|
const inputRef = useRef(null);
|
|
31
|
+
|
|
32
|
+
const handleFiles = (fileList) => {
|
|
33
|
+
const arr = Array.from(fileList || []);
|
|
34
|
+
if (arr.length === 0) return;
|
|
35
|
+
if (onFiles) onFiles(arr);
|
|
36
|
+
else if (onFile) arr.forEach(f => onFile(f));
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const handleDrop = (e) => {
|
|
40
|
+
e.preventDefault();
|
|
41
|
+
e.stopPropagation();
|
|
42
|
+
setOver(false);
|
|
43
|
+
if (e.dataTransfer && e.dataTransfer.files) {
|
|
44
|
+
handleFiles(e.dataTransfer.files);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
6
48
|
return (
|
|
7
|
-
<div
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
<div
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
49
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
50
|
+
{label && (
|
|
51
|
+
<label style={{ fontSize: 11, fontWeight: 600, color: 'var(--t2)', textTransform: 'uppercase', letterSpacing: '.08em', display: 'block' }}>
|
|
52
|
+
{label}
|
|
53
|
+
</label>
|
|
54
|
+
)}
|
|
55
|
+
|
|
56
|
+
<div
|
|
57
|
+
className={`dropzone${over ? ' over' : ''}`}
|
|
58
|
+
onDragOver={e => { e.preventDefault(); e.stopPropagation(); setOver(true); }}
|
|
59
|
+
onDragLeave={e => { e.preventDefault(); e.stopPropagation(); setOver(false); }}
|
|
60
|
+
onDrop={handleDrop}
|
|
61
|
+
onClick={() => inputRef.current?.click()}
|
|
62
|
+
style={{
|
|
63
|
+
display: 'flex',
|
|
64
|
+
flexDirection: 'column',
|
|
65
|
+
alignItems: 'center',
|
|
66
|
+
justifyContent: 'center',
|
|
67
|
+
padding: '20px 20px',
|
|
68
|
+
border: `2px dashed ${over ? 'var(--accent, #3b82f6)' : 'var(--border-light)'}`,
|
|
69
|
+
borderRadius: 'var(--rad)',
|
|
70
|
+
cursor: 'pointer',
|
|
71
|
+
background: over ? 'rgba(59, 130, 246, 0.08)' : 'var(--grey-1)',
|
|
72
|
+
transition: 'all .2s ease',
|
|
73
|
+
textAlign: 'center',
|
|
74
|
+
}}
|
|
75
|
+
>
|
|
76
|
+
<input
|
|
77
|
+
ref={inputRef}
|
|
78
|
+
type="file"
|
|
79
|
+
accept={accept}
|
|
80
|
+
multiple={multiple}
|
|
81
|
+
onChange={e => {
|
|
82
|
+
handleFiles(e.target.files);
|
|
83
|
+
e.target.value = '';
|
|
84
|
+
}}
|
|
85
|
+
style={{ display: 'none' }}
|
|
86
|
+
/>
|
|
87
|
+
<UploadCloud size={24} color="var(--accent, #3b82f6)" style={{ marginBottom: 6, pointerEvents: 'none' }} />
|
|
88
|
+
<span style={{ fontSize: '.82rem', fontWeight: 600, pointerEvents: 'none' }}>
|
|
89
|
+
Выберите файлы, перетащите их сюда или вставьте через Ctrl+V
|
|
90
|
+
</span>
|
|
91
|
+
{hint && <span style={{ fontSize: '.72rem', color: 'var(--muted)', marginTop: 2, pointerEvents: 'none' }}>{hint}</span>}
|
|
18
92
|
</div>
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
93
|
+
|
|
94
|
+
{/* Uploading progress items */}
|
|
95
|
+
{uploadingFiles.length > 0 && (
|
|
96
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
97
|
+
{uploadingFiles.map((item) => (
|
|
98
|
+
<div
|
|
99
|
+
key={item.tempId || item.name}
|
|
100
|
+
style={{
|
|
101
|
+
display: 'flex',
|
|
102
|
+
flexDirection: 'column',
|
|
103
|
+
gap: 4,
|
|
104
|
+
padding: '8px 10px',
|
|
105
|
+
background: 'var(--surface)',
|
|
106
|
+
border: '1px solid var(--border-light)',
|
|
107
|
+
borderRadius: 'var(--rads)',
|
|
108
|
+
fontSize: '.8rem',
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
112
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 6, minWidth: 0 }}>
|
|
113
|
+
{item.status === 'uploading' && <Paperclip size={14} color="var(--accent)" />}
|
|
114
|
+
{item.status === 'done' && <FileCheck size={14} color="var(--success, #22c55e)" />}
|
|
115
|
+
{item.status === 'error' && <X size={14} color="var(--err, #ef4444)" />}
|
|
116
|
+
<span style={{ textOverflow: 'ellipsis', overflow: 'hidden', whiteSpace: 'nowrap', fontWeight: 500 }}>
|
|
117
|
+
{item.name} <span style={{ color: 'var(--muted)', fontSize: '.72rem' }}>({formatBytes(item.size)})</span>
|
|
118
|
+
</span>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
|
122
|
+
{item.status === 'uploading' && (
|
|
123
|
+
<span style={{ fontSize: '.75rem', fontWeight: 600, color: 'var(--accent, #3b82f6)' }}>
|
|
124
|
+
{item.progress}%
|
|
125
|
+
</span>
|
|
126
|
+
)}
|
|
127
|
+
{item.status === 'done' && <Badge variant="ok" size="xs">Загружен</Badge>}
|
|
128
|
+
{item.status === 'error' && <Badge variant="err" size="xs">{item.error || 'Ошибка'}</Badge>}
|
|
129
|
+
{onRemoveUploading && (
|
|
130
|
+
<button
|
|
131
|
+
onClick={(e) => { e.stopPropagation(); onRemoveUploading(item.tempId); }}
|
|
132
|
+
style={{ border: 'none', background: 'none', cursor: 'pointer', padding: 2, color: 'var(--muted)' }}
|
|
133
|
+
>
|
|
134
|
+
<X size={14} />
|
|
135
|
+
</button>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
{item.status === 'uploading' && (
|
|
141
|
+
<LineProgress
|
|
142
|
+
value={item.progress}
|
|
143
|
+
color="var(--accent, #3b82f6)"
|
|
144
|
+
height={4}
|
|
145
|
+
showPercent={false}
|
|
146
|
+
style={{ marginTop: 2 }}
|
|
147
|
+
/>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
))}
|
|
151
|
+
</div>
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
{/* Existing saved files list */}
|
|
155
|
+
{existingFiles.length > 0 && (
|
|
156
|
+
<div style={{ marginTop: 4 }}>
|
|
157
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
158
|
+
{existingFiles.map((f) => (
|
|
159
|
+
<div
|
|
160
|
+
key={f.id}
|
|
161
|
+
style={{
|
|
162
|
+
display: 'flex',
|
|
163
|
+
alignItems: 'center',
|
|
164
|
+
justifyContent: 'space-between',
|
|
165
|
+
padding: '6px 10px',
|
|
166
|
+
background: 'var(--surface)',
|
|
167
|
+
border: '1px solid var(--border-light)',
|
|
168
|
+
borderRadius: 'var(--rads)',
|
|
169
|
+
fontSize: '.8rem',
|
|
170
|
+
}}
|
|
171
|
+
>
|
|
172
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 6, minWidth: 0 }}>
|
|
173
|
+
<Paperclip size={14} color="var(--accent)" />
|
|
174
|
+
<span style={{ textOverflow: 'ellipsis', overflow: 'hidden', whiteSpace: 'nowrap', fontWeight: 500 }}>
|
|
175
|
+
{f.original_name || f.originalName} <span style={{ color: 'var(--muted)', fontSize: '.72rem' }}>({formatBytes(f.file_size || f.fileSize)})</span>
|
|
176
|
+
</span>
|
|
177
|
+
</div>
|
|
178
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
|
179
|
+
{onPreview && (
|
|
180
|
+
<Button
|
|
181
|
+
variant="ghost"
|
|
182
|
+
size="xs"
|
|
183
|
+
icon={<Eye size={13} />}
|
|
184
|
+
onClick={(e) => { e.stopPropagation(); onPreview(f); }}
|
|
185
|
+
/>
|
|
186
|
+
)}
|
|
187
|
+
{onRemoveExisting && (
|
|
188
|
+
<Button
|
|
189
|
+
variant="ghost"
|
|
190
|
+
size="xs"
|
|
191
|
+
icon={<Trash2 size={13} color="var(--err)" />}
|
|
192
|
+
onClick={(e) => { e.stopPropagation(); onRemoveExisting(f.id); }}
|
|
193
|
+
/>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
))}
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
26
200
|
)}
|
|
27
201
|
</div>
|
|
28
202
|
);
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { useState, useRef } from 'react';
|
|
2
|
+
import { UploadCloud, Paperclip, FileCheck, X, Eye, Trash2 } from 'lucide-react';
|
|
3
|
+
import LineProgress from './LineProgress.jsx';
|
|
4
|
+
import Badge from './Badge.jsx';
|
|
5
|
+
import Button from './Button.jsx';
|
|
6
|
+
|
|
7
|
+
function formatBytes(bytes, decimals = 1) {
|
|
8
|
+
if (!bytes || bytes === 0) return '0 B';
|
|
9
|
+
const k = 1024;
|
|
10
|
+
const dm = decimals < 0 ? 0 : decimals;
|
|
11
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
12
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
13
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function FilePicker({
|
|
17
|
+
onFiles,
|
|
18
|
+
onFile,
|
|
19
|
+
accept = 'image/*,application/pdf,.*',
|
|
20
|
+
multiple = true,
|
|
21
|
+
hint = 'Поддерживаются изображения, PDF документы и любые файлы',
|
|
22
|
+
uploadingFiles = [],
|
|
23
|
+
existingFiles = [],
|
|
24
|
+
onRemoveUploading,
|
|
25
|
+
onRemoveExisting,
|
|
26
|
+
onPreview,
|
|
27
|
+
label = 'Прикрепить файлы (Фото, PDF, документы)',
|
|
28
|
+
}) {
|
|
29
|
+
const [over, setOver] = useState(false);
|
|
30
|
+
const inputRef = useRef(null);
|
|
31
|
+
|
|
32
|
+
const handleFiles = (fileList) => {
|
|
33
|
+
const arr = Array.from(fileList || []);
|
|
34
|
+
if (arr.length === 0) return;
|
|
35
|
+
if (onFiles) onFiles(arr);
|
|
36
|
+
else if (onFile) arr.forEach(f => onFile(f));
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const handleDrop = (e) => {
|
|
40
|
+
e.preventDefault();
|
|
41
|
+
e.stopPropagation();
|
|
42
|
+
setOver(false);
|
|
43
|
+
if (e.dataTransfer && e.dataTransfer.files) {
|
|
44
|
+
handleFiles(e.dataTransfer.files);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
50
|
+
{label && (
|
|
51
|
+
<label style={{ fontSize: 11, fontWeight: 600, color: 'var(--t2)', textTransform: 'uppercase', letterSpacing: '.08em', display: 'block' }}>
|
|
52
|
+
{label}
|
|
53
|
+
</label>
|
|
54
|
+
)}
|
|
55
|
+
|
|
56
|
+
<div
|
|
57
|
+
className={`file-picker${over ? ' over' : ''}`}
|
|
58
|
+
onDragOver={e => { e.preventDefault(); e.stopPropagation(); setOver(true); }}
|
|
59
|
+
onDragLeave={e => { e.preventDefault(); e.stopPropagation(); setOver(false); }}
|
|
60
|
+
onDrop={handleDrop}
|
|
61
|
+
onClick={() => inputRef.current?.click()}
|
|
62
|
+
style={{
|
|
63
|
+
display: 'flex',
|
|
64
|
+
flexDirection: 'column',
|
|
65
|
+
alignItems: 'center',
|
|
66
|
+
justifyContent: 'center',
|
|
67
|
+
padding: '20px 20px',
|
|
68
|
+
border: `2px dashed ${over ? 'var(--accent, #3b82f6)' : 'var(--border-light)'}`,
|
|
69
|
+
borderRadius: 'var(--rad)',
|
|
70
|
+
cursor: 'pointer',
|
|
71
|
+
background: over ? 'rgba(59, 130, 246, 0.08)' : 'var(--grey-1)',
|
|
72
|
+
transition: 'all .2s ease',
|
|
73
|
+
textAlign: 'center',
|
|
74
|
+
}}
|
|
75
|
+
>
|
|
76
|
+
<input
|
|
77
|
+
ref={inputRef}
|
|
78
|
+
type="file"
|
|
79
|
+
accept={accept}
|
|
80
|
+
multiple={multiple}
|
|
81
|
+
onChange={e => {
|
|
82
|
+
handleFiles(e.target.files);
|
|
83
|
+
e.target.value = '';
|
|
84
|
+
}}
|
|
85
|
+
style={{ display: 'none' }}
|
|
86
|
+
/>
|
|
87
|
+
<UploadCloud size={24} color="var(--accent, #3b82f6)" style={{ marginBottom: 6, pointerEvents: 'none' }} />
|
|
88
|
+
<span style={{ fontSize: '.82rem', fontWeight: 600, pointerEvents: 'none' }}>
|
|
89
|
+
Выберите файлы, перетащите их сюда или вставьте через Ctrl+V
|
|
90
|
+
</span>
|
|
91
|
+
{hint && <span style={{ fontSize: '.72rem', color: 'var(--muted)', marginTop: 2, pointerEvents: 'none' }}>{hint}</span>}
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
{/* Uploading progress items */}
|
|
95
|
+
{uploadingFiles.length > 0 && (
|
|
96
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
97
|
+
{uploadingFiles.map((item) => (
|
|
98
|
+
<div
|
|
99
|
+
key={item.tempId || item.name}
|
|
100
|
+
style={{
|
|
101
|
+
display: 'flex',
|
|
102
|
+
flexDirection: 'column',
|
|
103
|
+
gap: 4,
|
|
104
|
+
padding: '8px 10px',
|
|
105
|
+
background: 'var(--surface)',
|
|
106
|
+
border: '1px solid var(--border-light)',
|
|
107
|
+
borderRadius: 'var(--rads)',
|
|
108
|
+
fontSize: '.8rem',
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
112
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 6, minWidth: 0 }}>
|
|
113
|
+
{item.status === 'uploading' && <Paperclip size={14} color="var(--accent)" />}
|
|
114
|
+
{item.status === 'done' && <FileCheck size={14} color="var(--success, #22c55e)" />}
|
|
115
|
+
{item.status === 'error' && <X size={14} color="var(--err, #ef4444)" />}
|
|
116
|
+
<span style={{ textOverflow: 'ellipsis', overflow: 'hidden', whiteSpace: 'nowrap', fontWeight: 500 }}>
|
|
117
|
+
{item.name} <span style={{ color: 'var(--muted)', fontSize: '.72rem' }}>({formatBytes(item.size)})</span>
|
|
118
|
+
</span>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
|
122
|
+
{item.status === 'uploading' && (
|
|
123
|
+
<span style={{ fontSize: '.75rem', fontWeight: 600, color: 'var(--accent, #3b82f6)' }}>
|
|
124
|
+
{item.progress}%
|
|
125
|
+
</span>
|
|
126
|
+
)}
|
|
127
|
+
{item.status === 'done' && <Badge variant="ok" size="xs">Загружен</Badge>}
|
|
128
|
+
{item.status === 'error' && <Badge variant="err" size="xs">{item.error || 'Ошибка'}</Badge>}
|
|
129
|
+
{onRemoveUploading && (
|
|
130
|
+
<button
|
|
131
|
+
onClick={(e) => { e.stopPropagation(); onRemoveUploading(item.tempId); }}
|
|
132
|
+
style={{ border: 'none', background: 'none', cursor: 'pointer', padding: 2, color: 'var(--muted)' }}
|
|
133
|
+
>
|
|
134
|
+
<X size={14} />
|
|
135
|
+
</button>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
{item.status === 'uploading' && (
|
|
141
|
+
<LineProgress
|
|
142
|
+
value={item.progress}
|
|
143
|
+
color="var(--accent, #3b82f6)"
|
|
144
|
+
height={4}
|
|
145
|
+
showPercent={false}
|
|
146
|
+
style={{ marginTop: 2 }}
|
|
147
|
+
/>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
))}
|
|
151
|
+
</div>
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
{/* Existing saved files list */}
|
|
155
|
+
{existingFiles.length > 0 && (
|
|
156
|
+
<div style={{ marginTop: 4 }}>
|
|
157
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
158
|
+
{existingFiles.map((f) => (
|
|
159
|
+
<div
|
|
160
|
+
key={f.id}
|
|
161
|
+
style={{
|
|
162
|
+
display: 'flex',
|
|
163
|
+
alignItems: 'center',
|
|
164
|
+
justifyContent: 'space-between',
|
|
165
|
+
padding: '6px 10px',
|
|
166
|
+
background: 'var(--surface)',
|
|
167
|
+
border: '1px solid var(--border-light)',
|
|
168
|
+
borderRadius: 'var(--rads)',
|
|
169
|
+
fontSize: '.8rem',
|
|
170
|
+
}}
|
|
171
|
+
>
|
|
172
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 6, minWidth: 0 }}>
|
|
173
|
+
<Paperclip size={14} color="var(--accent)" />
|
|
174
|
+
<span style={{ textOverflow: 'ellipsis', overflow: 'hidden', whiteSpace: 'nowrap', fontWeight: 500 }}>
|
|
175
|
+
{f.original_name || f.originalName} <span style={{ color: 'var(--muted)', fontSize: '.72rem' }}>({formatBytes(f.file_size || f.fileSize)})</span>
|
|
176
|
+
</span>
|
|
177
|
+
</div>
|
|
178
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
|
179
|
+
{onPreview && (
|
|
180
|
+
<Button
|
|
181
|
+
variant="ghost"
|
|
182
|
+
size="xs"
|
|
183
|
+
icon={<Eye size={13} />}
|
|
184
|
+
onClick={(e) => { e.stopPropagation(); onPreview(f); }}
|
|
185
|
+
/>
|
|
186
|
+
)}
|
|
187
|
+
{onRemoveExisting && (
|
|
188
|
+
<Button
|
|
189
|
+
variant="ghost"
|
|
190
|
+
size="xs"
|
|
191
|
+
icon={<Trash2 size={13} color="var(--err)" />}
|
|
192
|
+
onClick={(e) => { e.stopPropagation(); onRemoveExisting(f.id); }}
|
|
193
|
+
/>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
))}
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
)}
|
|
201
|
+
</div>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export default FilePicker;
|
package/components/Select.jsx
CHANGED
|
@@ -22,6 +22,8 @@ export default function Select({
|
|
|
22
22
|
disabled = false,
|
|
23
23
|
multi = false,
|
|
24
24
|
searchable = false,
|
|
25
|
+
clearable = false,
|
|
26
|
+
onClear,
|
|
25
27
|
style = {},
|
|
26
28
|
}) {
|
|
27
29
|
const [open, setOpen] = useState(false);
|
|
@@ -186,15 +188,40 @@ export default function Select({
|
|
|
186
188
|
<span style={{ color: 'var(--t3,var(--grey-3))', fontWeight: 400 }}>{placeholder}</span>
|
|
187
189
|
)}
|
|
188
190
|
</div>
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
191
|
+
{!multi && selectedValues.length > 0 && selectedValues[0] !== '' && selectedValues[0] !== null && (clearable || !!onClear) ? (
|
|
192
|
+
<span
|
|
193
|
+
onClick={(e) => {
|
|
194
|
+
e.stopPropagation();
|
|
195
|
+
if (onClear) onClear();
|
|
196
|
+
else onChange?.('');
|
|
197
|
+
}}
|
|
198
|
+
title="Сбросить выбор"
|
|
199
|
+
style={{
|
|
200
|
+
display: 'inline-flex',
|
|
201
|
+
alignItems: 'center',
|
|
202
|
+
justifyContent: 'center',
|
|
203
|
+
cursor: 'pointer',
|
|
204
|
+
color: 'var(--muted, #64748b)',
|
|
205
|
+
padding: '0 2px',
|
|
206
|
+
flexShrink: 0,
|
|
207
|
+
borderRadius: 'var(--radxs, 4px)',
|
|
208
|
+
}}
|
|
209
|
+
onMouseEnter={(e) => { e.currentTarget.style.color = 'var(--danger, #ef4444)'; }}
|
|
210
|
+
onMouseLeave={(e) => { e.currentTarget.style.color = 'var(--muted, #64748b)'; }}
|
|
211
|
+
>
|
|
212
|
+
<X size={13} />
|
|
213
|
+
</span>
|
|
214
|
+
) : (
|
|
215
|
+
<ChevronDown
|
|
216
|
+
size={13}
|
|
217
|
+
style={{
|
|
218
|
+
flexShrink: 0,
|
|
219
|
+
color: 'var(--t3,var(--grey-3))',
|
|
220
|
+
transition: 'transform .2s ease',
|
|
221
|
+
transform: open ? 'rotate(180deg)' : undefined,
|
|
222
|
+
}}
|
|
223
|
+
/>
|
|
224
|
+
)}
|
|
198
225
|
</div>
|
|
199
226
|
|
|
200
227
|
{open && createPortal(
|