cynx-ui 1.2.43 → 1.2.45
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/Badge.jsx +49 -25
- package/components/DatePicker.jsx +1 -1
- package/components/Dropzone.jsx +193 -19
- package/components/FilePicker.jsx +205 -0
- package/package.json +1 -1
package/components/Badge.jsx
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
const VARIANTS = {
|
|
2
|
-
ok:
|
|
3
|
-
err:
|
|
4
|
-
warn:
|
|
5
|
-
blue:
|
|
6
|
-
dim:
|
|
7
|
-
purple:
|
|
8
|
-
teal:
|
|
9
|
-
pink:
|
|
10
|
-
ghost:
|
|
11
|
-
paid:
|
|
2
|
+
ok: { bg: 'var(--success-bg,rgba(39,174,96,.1))', color: 'var(--success,#27ae60)', border: 'var(--sd, rgba(39,174,96,.2))', dot: 'var(--success,#27ae60)' },
|
|
3
|
+
err: { bg: 'var(--danger-bg,rgba(246,71,71,.1))', color: 'var(--danger,#f64747)', border: 'var(--dd, rgba(246,71,71,.2))', dot: 'var(--danger,#f64747)' },
|
|
4
|
+
warn: { bg: 'var(--accent-bg,rgba(249,158,44,.1))', color: 'var(--accent,#f99e2c)', border: 'var(--accent-border,rgba(249,158,44,.25))', dot: 'var(--accent,#f99e2c)' },
|
|
5
|
+
blue: { bg: 'var(--blue-bg,rgba(39,130,228,.1))', color: 'var(--blue,#2782e4)', border: 'var(--infd, rgba(39,130,228,.2))', dot: 'var(--blue,#2782e4)' },
|
|
6
|
+
dim: { bg: 'var(--grey-1,#f5f5f8)', color: 'var(--grey-4,#797b8d)', border: 'var(--border,#d7d8e0)', dot: 'var(--grey-4,#797b8d)' },
|
|
7
|
+
purple: { bg: 'rgba(139,92,246,.1)', color: '#8b5cf6', border: 'rgba(139,92,246,.2)', dot: '#8b5cf6' },
|
|
8
|
+
teal: { bg: 'rgba(20,184,166,.1)', color: '#14b8a6', border: 'rgba(20,184,166,.2)', dot: '#14b8a6' },
|
|
9
|
+
pink: { bg: 'rgba(236,72,153,.1)', color: '#ec4899', border: 'rgba(236,72,153,.2)', dot: '#ec4899' },
|
|
10
|
+
ghost: { bg: 'transparent', color: 'var(--t2,var(--grey-4,#797b8d))', border: 'var(--bd,var(--border,#d7d8e0))', dot: 'var(--t3,var(--grey-4,#797b8d))' },
|
|
11
|
+
paid: { bg: 'var(--pd, rgba(30,41,59,.08))', color: 'var(--t, #1e293b)', border: 'var(--accent-border, rgba(30,41,59,.15))', dot: 'var(--t, #1e293b)' },
|
|
12
12
|
invoiced: { bg: 'rgba(120,120,130,.08)', color: 'var(--t2, #787882)', border: 'rgba(120,120,130,.15)', dot: 'var(--t2, #787882)' },
|
|
13
13
|
verified: { bg: 'var(--sd, rgba(34,197,94,.08))', color: 'var(--s, #22c55e)', border: 'var(--sd, rgba(34,197,94,.15))', dot: 'var(--s, #22c55e)' },
|
|
14
|
-
partial:
|
|
15
|
-
deleted:
|
|
16
|
-
update:
|
|
17
|
-
ready:
|
|
18
|
-
new:
|
|
19
|
-
active:
|
|
14
|
+
partial: { bg: 'rgba(100,100,110,.08)', color: '#64646e', border: 'rgba(100,100,110,.15)', dot: '#64646e' },
|
|
15
|
+
deleted: { bg: 'var(--dd, rgba(239,68,68,.08))', color: 'var(--d, #ef4444)', border: 'var(--dd, rgba(239,68,68,.15))', dot: 'var(--d, #ef4444)' },
|
|
16
|
+
update: { bg: 'var(--wd, rgba(249,115,22,.08))', color: 'var(--w, #f97316)', border: 'var(--wd, rgba(249,115,22,.15))', dot: 'var(--w, #f97316)' },
|
|
17
|
+
ready: { bg: 'var(--wd, rgba(234,179,8,.08))', color: 'var(--w, #eab308)', border: 'var(--wd, rgba(234,179,8,.15))', dot: 'var(--w, #eab308)' },
|
|
18
|
+
new: { bg: 'var(--infd, rgba(59,130,246,.08))', color: 'var(--inf, #3b82f6)', border: 'var(--infd, rgba(59,130,246,.15))', dot: 'var(--inf, #3b82f6)' },
|
|
19
|
+
active: { bg: 'transparent', color: 'var(--s, #22c55e)', border: 'none', dot: 'var(--s, #22c55e)', noPad: true },
|
|
20
20
|
inactive: { bg: 'transparent', color: 'var(--d, #ef4444)', border: 'none', dot: 'var(--d, #ef4444)', noPad: true },
|
|
21
|
-
pending:
|
|
21
|
+
pending: { bg: 'transparent', color: 'var(--w, #d97706)', border: 'none', dot: 'var(--w, #d97706)', noPad: true, pulse: true },
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
const SHAPES = {
|
|
@@ -48,23 +48,47 @@ const SIZE_MAP = {
|
|
|
48
48
|
};
|
|
49
49
|
|
|
50
50
|
const SHAPE_STYLE = {
|
|
51
|
-
circle:
|
|
51
|
+
circle: (c) => ({ width: 7, height: 7, borderRadius: '50%', background: c, border: 'none' }),
|
|
52
52
|
triangle: (c) => ({ width: 0, height: 0, borderLeft: '4px solid transparent', borderRight: '4px solid transparent', borderBottom: `7px solid ${c}`, background: 'none', borderTop: 'none' }),
|
|
53
|
-
diamond:
|
|
54
|
-
semicircle:(c) => ({ width: 7, height: 7, borderRadius: '50%', border: 'none', borderTop: '2px solid var(--bg,#fff)', background: c }),
|
|
53
|
+
diamond: (c) => ({ width: 7, height: 7, borderRadius: 1, transform: 'rotate(45deg)', background: c, border: 'none' }),
|
|
54
|
+
semicircle: (c) => ({ width: 7, height: 7, borderRadius: '50%', border: 'none', borderTop: '2px solid var(--bg,#fff)', background: c }),
|
|
55
55
|
};
|
|
56
56
|
|
|
57
57
|
const COLOR_MAP = {
|
|
58
|
-
green: '#10b981',
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
green: '#10b981',
|
|
59
|
+
passed: '#10b981',
|
|
60
|
+
ok: '#10b981',
|
|
61
|
+
success: '#10b981',
|
|
62
|
+
emerald: '#10b981',
|
|
63
|
+
yellow: '#f59e0b',
|
|
64
|
+
violation: '#f97316',
|
|
65
|
+
warn: '#f97316',
|
|
66
|
+
warning: '#f97316',
|
|
67
|
+
amber: '#f59e0b',
|
|
68
|
+
orange: '#f97316',
|
|
69
|
+
medium: '#f59e0b',
|
|
70
|
+
red: '#ef4444',
|
|
71
|
+
failed: '#ef4444',
|
|
72
|
+
err: '#ef4444',
|
|
73
|
+
danger: '#ef4444',
|
|
74
|
+
error: '#ef4444',
|
|
75
|
+
high: '#ef4444',
|
|
76
|
+
critical: '#ef4444',
|
|
77
|
+
blue: '#3b82f6',
|
|
78
|
+
conditional: '#3b82f6',
|
|
79
|
+
info: '#3b82f6',
|
|
80
|
+
low: '#3b82f6',
|
|
81
|
+
sky: '#0284c7',
|
|
62
82
|
purple: '#8b5cf6',
|
|
83
|
+
violet: '#8b5cf6',
|
|
63
84
|
indigo: '#6366f1',
|
|
64
85
|
cyan: '#06b6d4',
|
|
65
86
|
teal: '#14b8a6',
|
|
66
87
|
pink: '#ec4899',
|
|
67
|
-
|
|
88
|
+
rose: '#f43f5e',
|
|
89
|
+
grey: '#6b7280',
|
|
90
|
+
gray: '#6b7280',
|
|
91
|
+
dim: '#6b7280',
|
|
68
92
|
};
|
|
69
93
|
|
|
70
94
|
function resolveBadgeColor(cStr) {
|
|
@@ -375,7 +375,7 @@ export function DatePicker({ value, onChange, placeholder = 'Select date', badge
|
|
|
375
375
|
</button>
|
|
376
376
|
);
|
|
377
377
|
|
|
378
|
-
const actualPos = stepperPosition ||
|
|
378
|
+
const actualPos = stepperPosition || 'around';
|
|
379
379
|
const showStepperEffective = showStepper && actualPos !== 'off';
|
|
380
380
|
const posMode = actualPos === 'left' || actualPos === 'start' ? 'left'
|
|
381
381
|
: actualPos === 'right' || actualPos === 'end' ? 'right'
|
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;
|