cynx-ui 1.3.5 → 1.3.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.
@@ -1,205 +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;
1
+ import { useState, useRef } from 'react';
2
+ import { UploadCloud, Paperclip, FileCheck, X, Eye, Trash2 } from 'lucide-react';
3
+ import LinearProgress from './LinearProgress.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
+ <LinearProgress
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;
@@ -0,0 +1,132 @@
1
+ import { useEffect, useRef } from 'react';
2
+
3
+ const SIZES = {
4
+ xs: { height: 4, fontSize: '.68rem' },
5
+ sm: { height: 5, fontSize: '.75rem' },
6
+ md: { height: 6, fontSize: '.82rem' },
7
+ lg: { height: 8, fontSize: '.9rem' },
8
+ xl: { height: 10, fontSize: '1rem' },
9
+ };
10
+
11
+ export default function LinearProgress({
12
+ value = 0,
13
+ segments,
14
+ size = 'md',
15
+ customSize,
16
+ color = 'var(--blue, #3b82f6)',
17
+ trackColor = 'var(--bd2, #e0e0e4)',
18
+ label = '',
19
+ showPercent = true,
20
+ animated = true,
21
+ rounded = true,
22
+ indeterminate = false,
23
+ speed = 1,
24
+ height,
25
+ gap = 5,
26
+ labelGap = 2,
27
+ className = '',
28
+ style = {},
29
+ }) {
30
+ const base = SIZES[size] || SIZES.md;
31
+ const barHeight = height ?? (customSize || base.height);
32
+ const fontSize = customSize ? `${Math.max(11, customSize * 1.6)}px` : base.fontSize;
33
+ const pct = Math.min(100, Math.max(0, value));
34
+ const radius = rounded ? barHeight / 2 : 0;
35
+
36
+ const barRef = useRef(null);
37
+
38
+ useEffect(() => {
39
+ if (!indeterminate || !barRef.current) return;
40
+ let pos = -40, direction = 1, raf;
41
+ let lastTime = performance.now();
42
+
43
+ function tick(now) {
44
+ const dt = (now - lastTime) / 16.67;
45
+ lastTime = now;
46
+ pos += direction * 1.2 * speed * dt;
47
+ if (pos > 100) { pos = 100; direction = -1; }
48
+ if (pos < -40) { pos = -40; direction = 1; }
49
+ if (barRef.current) {
50
+ barRef.current.style.left = pos + '%';
51
+ barRef.current.style.width = '40%';
52
+ }
53
+ raf = requestAnimationFrame(tick);
54
+ }
55
+ raf = requestAnimationFrame(tick);
56
+ return () => cancelAnimationFrame(raf);
57
+ }, [indeterminate, speed]);
58
+
59
+ const transitionStyle = animated ? 'flex .5s cubic-bezier(0.4,0,0.2,1)' : 'none';
60
+ const hasSegments = Array.isArray(segments) && segments.length > 0;
61
+ const rawSum = hasSegments ? segments.reduce((sum, s) => sum + (Number(s.value) || 0), 0) : 0;
62
+
63
+ return (
64
+ <div className={className} style={{ display: 'inline-flex', flexDirection: 'column', gap: labelGap, width: '100%', ...style }}>
65
+ {(label || showPercent) && (
66
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
67
+ {label && <span style={{ fontSize, fontWeight: 600, color: 'var(--primary-text, #1a1a2e)' }}>{label}</span>}
68
+ {showPercent && !indeterminate && !hasSegments && (
69
+ <span style={{ fontSize, fontWeight: 600, color: 'var(--muted, #9ca3af)', fontVariantNumeric: 'tabular-nums', marginLeft: label ? 0 : 'auto' }}>
70
+ {Math.round(pct)}%
71
+ </span>
72
+ )}
73
+ </div>
74
+ )}
75
+ <div style={{
76
+ height: barHeight, borderRadius: radius,
77
+ background: 'transparent', position: 'relative',
78
+ display: 'flex', gap: gap, overflow: 'hidden',
79
+ }}>
80
+ {indeterminate ? (
81
+ <>
82
+ <div style={{ flex: 1, borderRadius: radius, background: trackColor }} />
83
+ <div
84
+ ref={barRef}
85
+ style={{
86
+ position: 'absolute', top: 0, height: '100%',
87
+ width: '40%', borderRadius: radius,
88
+ background: color, opacity: 0.85,
89
+ }}
90
+ />
91
+ </>
92
+ ) : hasSegments && rawSum > 0 ? (
93
+ <>
94
+ {segments.map((seg, idx) => {
95
+ const segVal = Number(seg.value) || 0;
96
+ if (segVal <= 0) return null;
97
+ const ratioPct = (segVal / rawSum) * 100;
98
+ return (
99
+ <div
100
+ key={idx}
101
+ title={seg.label ? `${seg.label}: ${segVal}` : undefined}
102
+ style={{
103
+ flex: ratioPct,
104
+ height: '100%',
105
+ borderRadius: radius,
106
+ background: seg.color || color,
107
+ transition: transitionStyle,
108
+ }}
109
+ />
110
+ );
111
+ })}
112
+ </>
113
+ ) : (
114
+ <>
115
+ <div ref={barRef} style={{
116
+ flex: pct > 0 ? pct : 0, minWidth: pct > 0 ? barHeight : 0,
117
+ borderRadius: radius, background: color,
118
+ transition: transitionStyle,
119
+ }} />
120
+ {pct < 100 && (
121
+ <div style={{
122
+ flex: 100 - pct,
123
+ borderRadius: radius, background: trackColor,
124
+ transition: transitionStyle,
125
+ }} />
126
+ )}
127
+ </>
128
+ )}
129
+ </div>
130
+ </div>
131
+ );
132
+ }
@@ -24,7 +24,9 @@ export default function Select({
24
24
  searchable = false,
25
25
  clearable = false,
26
26
  onClear,
27
+ size = 'md',
27
28
  style = {},
29
+ rounded = false,
28
30
  }) {
29
31
  const [open, setOpen] = useState(false);
30
32
  const [query, setQuery] = useState('');
@@ -116,14 +118,26 @@ export default function Select({
116
118
 
117
119
  const selectedValues = multi ? (Array.isArray(value) ? value : []) : (value !== undefined && value !== null ? [value] : []);
118
120
 
121
+ const heightVal = size === 'xs' ? 24 : size === 'sm' ? 28 : 32;
122
+ const paddingVal = size === 'xs' ? '0 6px' : size === 'sm' ? '0 8px' : '0 10px';
123
+ const fontSizeVal = size === 'xs' ? '11px' : size === 'sm' ? '11.5px' : '13px';
124
+
125
+ let radiusVal = rounded
126
+ ? (typeof rounded === 'string'
127
+ ? (rounded.endsWith('px') || rounded.endsWith('rem') || rounded.startsWith('var') ? rounded : `${rounded}px`)
128
+ : typeof rounded === 'number'
129
+ ? `${rounded}px`
130
+ : '6px')
131
+ : (size === 'xs' ? '4px' : size === 'sm' ? '6px' : 'var(--rmd,var(--rads))');
132
+
119
133
  return (
120
134
  <div
121
135
  ref={wrapRef}
122
136
  style={{
123
137
  position: 'relative',
124
138
  width: '100%',
139
+ boxSizing: 'border-box',
125
140
  fontFamily: 'var(--f,var(--sans))',
126
- fontSize: 'var(--fsz,13px)',
127
141
  opacity: disabled ? 0.5 : 1,
128
142
  pointerEvents: disabled ? 'none' : undefined,
129
143
  ...style,
@@ -133,19 +147,20 @@ export default function Select({
133
147
  onClick={handleOpen}
134
148
  style={{
135
149
  width: '100%',
150
+ boxSizing: 'border-box',
136
151
  display: 'flex',
137
152
  alignItems: 'center',
138
153
  justifyContent: 'space-between',
139
- gap: 8,
140
- padding: '8px 11px',
141
- height: 32,
154
+ gap: 4,
155
+ padding: paddingVal,
156
+ height: heightVal,
142
157
  background: 'var(--sf,var(--surface))',
143
- border: `1.5px solid ${open ? 'var(--accent)' : 'var(--bd2,var(--border))'}`,
144
- borderRadius: 'var(--rmd,var(--rads))',
158
+ border: `1px solid ${open ? 'var(--accent)' : 'var(--bd2,var(--border))'}`,
159
+ borderRadius: radiusVal,
145
160
  cursor: 'pointer',
146
161
  fontFamily: 'var(--f,var(--sans,inherit))',
147
- fontSize: '12.5px',
148
- lineHeight: 1,
162
+ fontSize: fontSizeVal,
163
+ lineHeight: 1.2,
149
164
  color: 'var(--t,var(--primary-text))',
150
165
  outline: 'none',
151
166
  transition: 'border-color var(--tr,.15s), box-shadow var(--tr,.15s)',
@@ -182,7 +197,7 @@ export default function Select({
182
197
  </div>
183
198
  ) : !multi && selectedValues.length > 0 ? (
184
199
  <span style={{ fontWeight: 400, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
185
- {getLabel(selectedValues[0])}
200
+ {getLabel(selectedValues[0]) || '\u00A0'}
186
201
  </span>
187
202
  ) : (
188
203
  <span style={{ color: 'var(--t3,var(--grey-3))', fontWeight: 400 }}>{placeholder}</span>
@@ -209,11 +224,11 @@ export default function Select({
209
224
  onMouseEnter={(e) => { e.currentTarget.style.color = 'var(--danger, #ef4444)'; }}
210
225
  onMouseLeave={(e) => { e.currentTarget.style.color = 'var(--muted, #64748b)'; }}
211
226
  >
212
- <X size={13} />
227
+ <X size={12} />
213
228
  </span>
214
229
  ) : (
215
230
  <ChevronDown
216
- size={13}
231
+ size={12}
217
232
  style={{
218
233
  flexShrink: 0,
219
234
  color: 'var(--t3,var(--grey-3))',
@@ -233,33 +248,35 @@ export default function Select({
233
248
  top: pos.top,
234
249
  left: pos.left,
235
250
  width: pos.width,
251
+ boxSizing: 'border-box',
236
252
  background: 'var(--sf,var(--surface))',
237
253
  border: '1px solid var(--bd,var(--border))',
238
- borderRadius: 'var(--rmd,var(--rads))',
254
+ borderRadius: radiusVal,
239
255
  boxShadow: 'var(--shadow,0 1px 3px rgba(20,22,41,.1))',
240
- padding: 4,
256
+ padding: 3,
241
257
  animation: 'selFadeIn .15s ease',
242
258
  overflow: 'hidden',
243
259
  }}
244
260
  >
245
261
  {searchable && (
246
- <div style={{ padding: 4 }}>
262
+ <div style={{ padding: 3 }}>
247
263
  <input
248
264
  ref={inputRef}
249
265
  type="text"
250
- placeholder="Search..."
266
+ placeholder="Поиск..."
251
267
  value={query}
252
268
  onChange={e => setQuery(e.target.value)}
253
269
  onMouseDown={e => e.stopPropagation()}
254
270
  style={{
255
271
  width: '100%',
256
- padding: '7px 10px',
257
- height: 32,
272
+ boxSizing: 'border-box',
273
+ padding: size === 'xs' ? '2px 5px' : size === 'sm' ? '3px 6px' : '7px 10px',
274
+ height: size === 'xs' ? 22 : size === 'sm' ? 26 : 32,
258
275
  background: 'var(--sf2,var(--grey-1))',
259
276
  border: '1.5px solid var(--bd2,var(--border))',
260
- borderRadius: 'var(--rmd,var(--rads))',
277
+ borderRadius: size === 'xs' ? '3px' : size === 'sm' ? '4px' : 'var(--rmd,var(--rads))',
261
278
  fontFamily: 'var(--f,var(--sans))',
262
- fontSize: '12.5px',
279
+ fontSize: size === 'xs' ? '10.5px' : size === 'sm' ? '11.5px' : '12.5px',
263
280
  lineHeight: 1,
264
281
  color: 'var(--t,var(--primary-text))',
265
282
  outline: 'none',
@@ -270,8 +287,8 @@ export default function Select({
270
287
  )}
271
288
  <div style={{ maxHeight: 200, overflowY: 'auto' }}>
272
289
  {filtered.length === 0 ? (
273
- <div style={{ padding: '14px 10px', textAlign: 'center', color: 'var(--t3,var(--grey-3))', fontSize: 11 }}>
274
- No options found
290
+ <div style={{ padding: '10px 8px', textAlign: 'center', color: 'var(--t3,var(--grey-3))', fontSize: 11 }}>
291
+ Ничего не найдено
275
292
  </div>
276
293
  ) : (
277
294
  filtered.map(opt => (
@@ -281,13 +298,14 @@ export default function Select({
281
298
  style={{
282
299
  display: 'flex',
283
300
  alignItems: 'center',
284
- gap: 8,
285
- padding: '8px 10px',
286
- borderRadius: 'var(--rmd,var(--rads))',
301
+ gap: 6,
302
+ minHeight: size === 'xs' ? 18 : size === 'sm' ? 22 : 28,
303
+ padding: size === 'xs' ? '3px 5px' : size === 'sm' ? '4.5px 7px' : '8px 10px',
304
+ borderRadius: size === 'xs' ? '3px' : size === 'sm' ? '4px' : 'var(--rmd,var(--rads))',
287
305
  cursor: opt.disabled ? 'not-allowed' : 'pointer',
288
306
  color: isSelected(opt.value) ? 'var(--accent)' : 'var(--t,var(--primary-text))',
289
307
  background: isSelected(opt.value) ? 'var(--pd,var(--accent-bg))' : undefined,
290
- fontSize: '12.5px',
308
+ fontSize: size === 'xs' ? '10.5px' : size === 'sm' ? '11.5px' : '12.5px',
291
309
  fontWeight: 400,
292
310
  transition: 'background .1s',
293
311
  userSelect: 'none',
@@ -297,8 +315,8 @@ export default function Select({
297
315
  >
298
316
  {multi && (
299
317
  <span style={{
300
- width: 15,
301
- height: 15,
318
+ width: 13,
319
+ height: 13,
302
320
  border: `1.5px solid ${isSelected(opt.value) ? 'var(--accent)' : 'var(--bd2,var(--border))'}`,
303
321
  borderRadius: 3,
304
322
  flexShrink: 0,
@@ -311,8 +329,8 @@ export default function Select({
311
329
  position: 'absolute',
312
330
  top: 1,
313
331
  left: 3.5,
314
- width: 4.5,
315
- height: 7,
332
+ width: 4,
333
+ height: 6,
316
334
  border: 'solid var(--def-btn-text,#fff)',
317
335
  borderWidth: '0 1.5px 1.5px 0',
318
336
  transform: 'rotate(45deg)',
@@ -320,11 +338,11 @@ export default function Select({
320
338
  )}
321
339
  </span>
322
340
  )}
323
- <span style={{ flex: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
324
- {opt.displayLabel || opt.label}
341
+ <span style={{ flex: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', minHeight: '1em' }}>
342
+ {opt.displayLabel || opt.label || '\u00A0'}
325
343
  </span>
326
344
  {!multi && isSelected(opt.value) && (
327
- <Check size={13} style={{ flexShrink: 0, color: 'var(--accent)' }} />
345
+ <Check size={12} style={{ flexShrink: 0, color: 'var(--accent)' }} />
328
346
  )}
329
347
  </div>
330
348
  ))
package/index.js CHANGED
@@ -1,99 +1,99 @@
1
- // cynx-ui — shared component library
2
- import { default as _Button } from './components/Button.jsx';
3
- import { default as _Input } from './components/Input.jsx';
4
- import { default as _Toggle } from './components/Toggle.jsx';
5
- import { default as _Spinner, Spinner as _SpinnerNamed } from './components/Spinner.jsx';
6
- import { default as _TabBar } from './components/TabBar.jsx';
7
- import { default as _Dropdown } from './components/Dropdown.jsx';
8
- import { default as _MultiSelect } from './components/MultiSelect.jsx';
9
- import { default as _ImageUpload } from './components/ImageUpload.jsx';
10
- import { default as _ChipSelect } from './components/ChipSelect.jsx';
11
- import { default as _Checkbox } from './components/Checkbox.jsx';
12
- import { default as _AuthOverlay } from './components/AuthOverlay.jsx';
13
- import { default as _UptimeBar } from './components/UptimeBar.jsx';
14
- import { default as _LineProgress } from './components/LineProgress.jsx';
15
- import { default as _StatBarCard } from './components/StatBarCard.jsx';
16
- import { default as _StatsCard } from './components/StatsCard.jsx';
17
- import { default as _ActivityCard } from './components/ActivityCard.jsx';
18
- import { default as _HoverCard } from './components/HoverCard.jsx';
19
- import { default as _StatsBar } from './components/StatsBar.jsx';
20
- import { default as _Select } from './components/Select.jsx';
21
- import { default as _Badge } from './components/Badge.jsx';
22
- import { default as _PulseDot } from './components/PulseDot.jsx';
23
- import { default as _CircularProgress } from './components/CircularProgress.jsx';
24
- import { default as _Skeleton } from './components/Skeleton.jsx';
25
- import { Modal as _Modal, Confirm as _Confirm } from './components/Modal.jsx';
26
- import { toast as _toast, useToast as _useToast, Toast as _Toast } from './components/Toast.jsx';
27
- import { ConfirmRoot as _ConfirmRoot, confirm as _confirm } from './components/Confirm.jsx';
28
- import { SearchableDropdown as _SearchableDropdown } from './components/SearchableDropdown.jsx';
29
- import { Pagination as _Pagination } from './components/Pagination.jsx';
30
- import { DatePicker as _DatePicker } from './components/DatePicker.jsx';
31
- import { TimePicker as _TimePicker } from './components/TimePicker.jsx';
32
- import { ActionsMenu as _ActionsMenu } from './components/ActionsMenu.jsx';
33
- import { BrandingCard as _BrandingCard } from './components/BrandingCard.jsx';
34
- import { Dropzone as _Dropzone } from './components/Dropzone.jsx';
35
- import { Tabs as _Tabs } from './components/Tabs.jsx';
36
- import { SkeletonText as _SkeletonText, SkeletonCard as _SkeletonCard, SkeletonRows as _SkeletonRows, SkeletonStats as _SkeletonStats } from './components/Skeleton.jsx';
37
- import { XTable as _XTable, XThead as _XThead, XTbody as _XTbody, XTr as _XTr, XTh as _XTh, XTd as _XTd, SkeletonTable as _SkeletonTable } from './components/XTable.jsx';
38
- import { PageShell as _PageShell, TableCard as _TableCard, TableCardFilters as _TableCardFilters, TableCardBody as _TableCardBody, TableCardFooter as _TableCardFooter, CardHdr as _CardHdr, StatCard as _StatCard, StatCardSkeleton as _StatCardSkeleton } from './components/Card.jsx';
39
-
40
- import { default as _FilePicker } from './components/FilePicker.jsx';
41
-
42
- export const Button = _Button;
43
- export const FilePicker = _FilePicker;
44
- export const Input = _Input;
45
- export const Toggle = _Toggle;
46
- export const Spinner = _Spinner || _SpinnerNamed;
47
- export const TabBar = _TabBar;
48
- export const Dropdown = _Dropdown;
49
- export const MultiSelect = _MultiSelect;
50
- export const ImageUpload = _ImageUpload;
51
- export const ChipSelect = _ChipSelect;
52
- export const Checkbox = _Checkbox;
53
- export const AuthOverlay = _AuthOverlay;
54
- export const UptimeBar = _UptimeBar;
55
- export const LineProgress = _LineProgress;
56
- export const StatBarCard = _StatBarCard;
57
- export const StatsCard = _StatsCard;
58
- export const ActivityCard = _ActivityCard;
59
- export const HoverCard = _HoverCard;
60
- export const StatsBar = _StatsBar;
61
- export const Select = _Select;
62
- export const Badge = _Badge;
63
- export const PulseDot = _PulseDot;
64
- export const CircularProgress = _CircularProgress;
65
- export const Skeleton = _Skeleton;
66
- export const Modal = _Modal;
67
- export const Confirm = _Confirm;
68
- export const toast = _toast;
69
- export const useToast = _useToast;
70
- export const Toast = _Toast;
71
- export const ConfirmRoot = _ConfirmRoot;
72
- export const confirm = _confirm;
73
- export const SearchableDropdown = _SearchableDropdown;
74
- export const Pagination = _Pagination;
75
- export const DatePicker = _DatePicker;
76
- export const TimePicker = _TimePicker;
77
- export const ActionsMenu = _ActionsMenu;
78
- export const BrandingCard = _BrandingCard;
79
- export const Dropzone = _Dropzone;
80
- export const Tabs = _Tabs;
81
- export const SkeletonText = _SkeletonText;
82
- export const SkeletonCard = _SkeletonCard;
83
- export const SkeletonRows = _SkeletonRows;
84
- export const SkeletonStats = _SkeletonStats;
85
- export const XTable = _XTable;
86
- export const XThead = _XThead;
87
- export const XTbody = _XTbody;
88
- export const XTr = _XTr;
89
- export const XTh = _XTh;
90
- export const XTd = _XTd;
91
- export const SkeletonTable = _SkeletonTable;
92
- export const PageShell = _PageShell;
93
- export const TableCard = _TableCard;
94
- export const TableCardFilters = _TableCardFilters;
95
- export const TableCardBody = _TableCardBody;
96
- export const TableCardFooter = _TableCardFooter;
97
- export const CardHdr = _CardHdr;
98
- export const StatCard = _StatCard;
99
- export const StatCardSkeleton = _StatCardSkeleton;
1
+ // cynx-ui — shared component library
2
+ import { default as _Button } from './components/Button.jsx';
3
+ import { default as _Input } from './components/Input.jsx';
4
+ import { default as _Toggle } from './components/Toggle.jsx';
5
+ import { default as _Spinner, Spinner as _SpinnerNamed } from './components/Spinner.jsx';
6
+ import { default as _TabBar } from './components/TabBar.jsx';
7
+ import { default as _Dropdown } from './components/Dropdown.jsx';
8
+ import { default as _MultiSelect } from './components/MultiSelect.jsx';
9
+ import { default as _ImageUpload } from './components/ImageUpload.jsx';
10
+ import { default as _ChipSelect } from './components/ChipSelect.jsx';
11
+ import { default as _Checkbox } from './components/Checkbox.jsx';
12
+ import { default as _AuthOverlay } from './components/AuthOverlay.jsx';
13
+ import { default as _UptimeBar } from './components/UptimeBar.jsx';
14
+ import { default as _LinearProgress } from './components/LinearProgress.jsx';
15
+ import { default as _StatBarCard } from './components/StatBarCard.jsx';
16
+ import { default as _StatsCard } from './components/StatsCard.jsx';
17
+ import { default as _ActivityCard } from './components/ActivityCard.jsx';
18
+ import { default as _HoverCard } from './components/HoverCard.jsx';
19
+ import { default as _StatsBar } from './components/StatsBar.jsx';
20
+ import { default as _Select } from './components/Select.jsx';
21
+ import { default as _Badge } from './components/Badge.jsx';
22
+ import { default as _PulseDot } from './components/PulseDot.jsx';
23
+ import { default as _CircularProgress } from './components/CircularProgress.jsx';
24
+ import { default as _Skeleton } from './components/Skeleton.jsx';
25
+ import { Modal as _Modal, Confirm as _Confirm } from './components/Modal.jsx';
26
+ import { toast as _toast, useToast as _useToast, Toast as _Toast } from './components/Toast.jsx';
27
+ import { ConfirmRoot as _ConfirmRoot, confirm as _confirm } from './components/Confirm.jsx';
28
+ import { SearchableDropdown as _SearchableDropdown } from './components/SearchableDropdown.jsx';
29
+ import { Pagination as _Pagination } from './components/Pagination.jsx';
30
+ import { DatePicker as _DatePicker } from './components/DatePicker.jsx';
31
+ import { TimePicker as _TimePicker } from './components/TimePicker.jsx';
32
+ import { ActionsMenu as _ActionsMenu } from './components/ActionsMenu.jsx';
33
+ import { BrandingCard as _BrandingCard } from './components/BrandingCard.jsx';
34
+ import { Dropzone as _Dropzone } from './components/Dropzone.jsx';
35
+ import { Tabs as _Tabs } from './components/Tabs.jsx';
36
+ import { SkeletonText as _SkeletonText, SkeletonCard as _SkeletonCard, SkeletonRows as _SkeletonRows, SkeletonStats as _SkeletonStats } from './components/Skeleton.jsx';
37
+ import { XTable as _XTable, XThead as _XThead, XTbody as _XTbody, XTr as _XTr, XTh as _XTh, XTd as _XTd, SkeletonTable as _SkeletonTable } from './components/XTable.jsx';
38
+ import { PageShell as _PageShell, TableCard as _TableCard, TableCardFilters as _TableCardFilters, TableCardBody as _TableCardBody, TableCardFooter as _TableCardFooter, CardHdr as _CardHdr, StatCard as _StatCard, StatCardSkeleton as _StatCardSkeleton } from './components/Card.jsx';
39
+
40
+ import { default as _FilePicker } from './components/FilePicker.jsx';
41
+
42
+ export const Button = _Button;
43
+ export const FilePicker = _FilePicker;
44
+ export const Input = _Input;
45
+ export const Toggle = _Toggle;
46
+ export const Spinner = _Spinner || _SpinnerNamed;
47
+ export const TabBar = _TabBar;
48
+ export const Dropdown = _Dropdown;
49
+ export const MultiSelect = _MultiSelect;
50
+ export const ImageUpload = _ImageUpload;
51
+ export const ChipSelect = _ChipSelect;
52
+ export const Checkbox = _Checkbox;
53
+ export const AuthOverlay = _AuthOverlay;
54
+ export const UptimeBar = _UptimeBar;
55
+ export const LinearProgress = _LinearProgress;
56
+ export const StatBarCard = _StatBarCard;
57
+ export const StatsCard = _StatsCard;
58
+ export const ActivityCard = _ActivityCard;
59
+ export const HoverCard = _HoverCard;
60
+ export const StatsBar = _StatsBar;
61
+ export const Select = _Select;
62
+ export const Badge = _Badge;
63
+ export const PulseDot = _PulseDot;
64
+ export const CircularProgress = _CircularProgress;
65
+ export const Skeleton = _Skeleton;
66
+ export const Modal = _Modal;
67
+ export const Confirm = _Confirm;
68
+ export const toast = _toast;
69
+ export const useToast = _useToast;
70
+ export const Toast = _Toast;
71
+ export const ConfirmRoot = _ConfirmRoot;
72
+ export const confirm = _confirm;
73
+ export const SearchableDropdown = _SearchableDropdown;
74
+ export const Pagination = _Pagination;
75
+ export const DatePicker = _DatePicker;
76
+ export const TimePicker = _TimePicker;
77
+ export const ActionsMenu = _ActionsMenu;
78
+ export const BrandingCard = _BrandingCard;
79
+ export const Dropzone = _Dropzone;
80
+ export const Tabs = _Tabs;
81
+ export const SkeletonText = _SkeletonText;
82
+ export const SkeletonCard = _SkeletonCard;
83
+ export const SkeletonRows = _SkeletonRows;
84
+ export const SkeletonStats = _SkeletonStats;
85
+ export const XTable = _XTable;
86
+ export const XThead = _XThead;
87
+ export const XTbody = _XTbody;
88
+ export const XTr = _XTr;
89
+ export const XTh = _XTh;
90
+ export const XTd = _XTd;
91
+ export const SkeletonTable = _SkeletonTable;
92
+ export const PageShell = _PageShell;
93
+ export const TableCard = _TableCard;
94
+ export const TableCardFilters = _TableCardFilters;
95
+ export const TableCardBody = _TableCardBody;
96
+ export const TableCardFooter = _TableCardFooter;
97
+ export const CardHdr = _CardHdr;
98
+ export const StatCard = _StatCard;
99
+ export const StatCardSkeleton = _StatCardSkeleton;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cynx-ui",
3
- "version": "1.3.5",
3
+ "version": "1.3.7",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./index.js"