cynx-lib 1.3.32 → 1.3.34
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/package.json +1 -1
- package/ui/components/Input.jsx +15 -0
- package/ui/components/Select.jsx +64 -35
package/package.json
CHANGED
package/ui/components/Input.jsx
CHANGED
|
@@ -13,6 +13,7 @@ export default function Input({
|
|
|
13
13
|
hint,
|
|
14
14
|
error,
|
|
15
15
|
size = 'md',
|
|
16
|
+
type = 'text',
|
|
16
17
|
icon,
|
|
17
18
|
iconRight,
|
|
18
19
|
clearable = false,
|
|
@@ -29,6 +30,18 @@ export default function Input({
|
|
|
29
30
|
const [hovered, setHovered] = useState(false);
|
|
30
31
|
const s = SIZES[size] || SIZES.md;
|
|
31
32
|
|
|
33
|
+
const isPasswd = type === 'passwd';
|
|
34
|
+
const resolvedType = isPasswd ? 'text' : type;
|
|
35
|
+
const hasValue = value !== undefined && value !== null && value !== '';
|
|
36
|
+
|
|
37
|
+
const passwdStyle = isPasswd ? {
|
|
38
|
+
WebkitTextSecurity: 'disc',
|
|
39
|
+
textSecurity: 'disc',
|
|
40
|
+
fontFamily: hasValue ? 'monospace' : 'var(--f,var(--sans,inherit))',
|
|
41
|
+
fontWeight: hasValue ? 700 : 'normal',
|
|
42
|
+
letterSpacing: hasValue ? '0.15em' : 'normal',
|
|
43
|
+
} : {};
|
|
44
|
+
|
|
32
45
|
function handleClear() {
|
|
33
46
|
if (props.onChange) props.onChange({ target: { value: '' } });
|
|
34
47
|
if (onClear) onClear();
|
|
@@ -137,6 +150,7 @@ export default function Input({
|
|
|
137
150
|
) : (
|
|
138
151
|
<input
|
|
139
152
|
ref={inputRef}
|
|
153
|
+
type={resolvedType}
|
|
140
154
|
className={className}
|
|
141
155
|
style={{
|
|
142
156
|
display: 'block',
|
|
@@ -160,6 +174,7 @@ export default function Input({
|
|
|
160
174
|
: 'var(--shadow,0 1px 3px rgba(20,22,41,.1))',
|
|
161
175
|
opacity: props.disabled ? 0.5 : 1,
|
|
162
176
|
cursor: props.disabled ? 'not-allowed' : undefined,
|
|
177
|
+
...passwdStyle,
|
|
163
178
|
...style,
|
|
164
179
|
}}
|
|
165
180
|
value={value}
|
package/ui/components/Select.jsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState, useRef, useEffect, useLayoutEffect, useMemo, useCallback } from 'react';
|
|
2
2
|
import { createPortal } from 'react-dom';
|
|
3
|
-
import { ChevronDown, Check, X } from 'lucide-react';
|
|
3
|
+
import { ChevronDown, Check, X, Plus } from 'lucide-react';
|
|
4
4
|
|
|
5
5
|
const SIZES = {
|
|
6
6
|
xs: { height: 24, fontSize: 11, paddingH: 6, searchHeight: 22, searchFont: 10.5, itemMinHeight: 18, itemPadding: '3px 5px' },
|
|
@@ -40,6 +40,8 @@ export default function Select({
|
|
|
40
40
|
showCount = false,
|
|
41
41
|
searchMode = 'includes',
|
|
42
42
|
filterOption,
|
|
43
|
+
creatable = false,
|
|
44
|
+
createLabel,
|
|
43
45
|
}) {
|
|
44
46
|
const [open, setOpen] = useState(false);
|
|
45
47
|
const [query, setQuery] = useState('');
|
|
@@ -63,7 +65,8 @@ export default function Select({
|
|
|
63
65
|
|
|
64
66
|
// Dynamic max-height for inner list to prevent overflow
|
|
65
67
|
const availableSpace = openUp ? spaceAbove - 16 : spaceBelow - 16;
|
|
66
|
-
const
|
|
68
|
+
const isSearchActive = searchable || creatable;
|
|
69
|
+
const calculatedMaxHeight = Math.max(90, Math.min(220, availableSpace - (isSearchActive ? 46 : 14)));
|
|
67
70
|
|
|
68
71
|
let top;
|
|
69
72
|
if (openUp) {
|
|
@@ -112,8 +115,8 @@ export default function Select({
|
|
|
112
115
|
}, [open, calcPosition]);
|
|
113
116
|
|
|
114
117
|
useEffect(() => {
|
|
115
|
-
if (open && searchable && inputRef.current) inputRef.current.focus();
|
|
116
|
-
}, [open, searchable]);
|
|
118
|
+
if (open && (searchable || creatable) && inputRef.current) inputRef.current.focus();
|
|
119
|
+
}, [open, searchable, creatable]);
|
|
117
120
|
|
|
118
121
|
useEffect(() => {
|
|
119
122
|
if (!open) setQuery('');
|
|
@@ -338,14 +341,33 @@ export default function Select({
|
|
|
338
341
|
overflow: 'hidden',
|
|
339
342
|
}}
|
|
340
343
|
>
|
|
341
|
-
{searchable && (
|
|
344
|
+
{(searchable || creatable) && (
|
|
342
345
|
<div style={{ padding: 3 }}>
|
|
343
346
|
<input
|
|
344
347
|
ref={inputRef}
|
|
345
348
|
type="text"
|
|
346
|
-
placeholder=
|
|
349
|
+
placeholder={creatable ? 'Поиск или ввод текста...' : 'Поиск...'}
|
|
347
350
|
value={query}
|
|
348
351
|
onChange={e => setQuery(e.target.value)}
|
|
352
|
+
onKeyDown={(e) => {
|
|
353
|
+
if (e.key === 'Enter') {
|
|
354
|
+
e.preventDefault();
|
|
355
|
+
e.stopPropagation();
|
|
356
|
+
const trimmed = query.trim();
|
|
357
|
+
if (!trimmed) return;
|
|
358
|
+
const exact = filtered.find(o => String(o.label || o.value || '').toLowerCase() === trimmed.toLowerCase());
|
|
359
|
+
if (exact && !exact.disabled) {
|
|
360
|
+
toggleOption(exact.value);
|
|
361
|
+
setQuery('');
|
|
362
|
+
} else if (creatable) {
|
|
363
|
+
toggleOption(trimmed);
|
|
364
|
+
setQuery('');
|
|
365
|
+
} else if (filtered.length > 0 && !filtered[0].disabled) {
|
|
366
|
+
toggleOption(filtered[0].value);
|
|
367
|
+
setQuery('');
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}}
|
|
349
371
|
onMouseDown={e => e.stopPropagation()}
|
|
350
372
|
style={{
|
|
351
373
|
width: '100%',
|
|
@@ -366,7 +388,37 @@ export default function Select({
|
|
|
366
388
|
</div>
|
|
367
389
|
)}
|
|
368
390
|
<div style={{ maxHeight: pos.maxHeight || 200, overflowY: 'auto' }}>
|
|
369
|
-
{
|
|
391
|
+
{creatable && query.trim() && !options.some(o => String(o.label || o.value || '').toLowerCase() === query.trim().toLowerCase()) && (
|
|
392
|
+
<div
|
|
393
|
+
onMouseDown={(e) => {
|
|
394
|
+
e.preventDefault();
|
|
395
|
+
e.stopPropagation();
|
|
396
|
+
toggleOption(query.trim());
|
|
397
|
+
setQuery('');
|
|
398
|
+
}}
|
|
399
|
+
style={{
|
|
400
|
+
display: 'flex',
|
|
401
|
+
alignItems: 'center',
|
|
402
|
+
gap: 6,
|
|
403
|
+
minHeight: s.itemMinHeight,
|
|
404
|
+
padding: s.itemPadding,
|
|
405
|
+
borderRadius: s.height <= 26 ? '3px' : s.height <= 28 ? '4px' : 'var(--rmd,var(--rads, 8px))',
|
|
406
|
+
cursor: 'pointer',
|
|
407
|
+
color: 'var(--accent, #f99e2c)',
|
|
408
|
+
background: 'var(--pd,var(--accent-bg, rgba(249,158,44,.08)))',
|
|
409
|
+
fontSize: `${s.searchFont}px`,
|
|
410
|
+
fontWeight: 600,
|
|
411
|
+
marginBottom: filtered.length > 0 ? 3 : 0,
|
|
412
|
+
userSelect: 'none',
|
|
413
|
+
}}
|
|
414
|
+
>
|
|
415
|
+
<Plus size={12} style={{ flexShrink: 0 }} />
|
|
416
|
+
<span style={{ flex: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
|
417
|
+
{typeof createLabel === 'function' ? createLabel(query.trim()) : `Ввести: "${query.trim()}"`}
|
|
418
|
+
</span>
|
|
419
|
+
</div>
|
|
420
|
+
)}
|
|
421
|
+
{filtered.length === 0 && (!creatable || !query.trim()) ? (
|
|
370
422
|
<div style={{ padding: '10px 8px', textAlign: 'center', color: 'var(--t3,var(--grey-3, #cbd5e1))', fontSize: 11 }}>
|
|
371
423
|
Ничего не найдено
|
|
372
424
|
</div>
|
|
@@ -374,6 +426,8 @@ export default function Select({
|
|
|
374
426
|
filtered.map(opt => (
|
|
375
427
|
<div
|
|
376
428
|
key={opt.value}
|
|
429
|
+
onMouseEnter={e => { if (!opt.disabled) e.currentTarget.style.background = 'var(--grey-1, #f5f5f8)'; }}
|
|
430
|
+
onMouseLeave={e => { if (!opt.disabled) e.currentTarget.style.background = 'transparent'; }}
|
|
377
431
|
onMouseDown={(e) => { e.preventDefault(); e.stopPropagation(); if (!opt.disabled) toggleOption(opt.value); }}
|
|
378
432
|
style={{
|
|
379
433
|
display: 'flex',
|
|
@@ -384,7 +438,7 @@ export default function Select({
|
|
|
384
438
|
borderRadius: s.height <= 26 ? '3px' : s.height <= 28 ? '4px' : 'var(--rmd,var(--rads, 8px))',
|
|
385
439
|
cursor: opt.disabled ? 'not-allowed' : 'pointer',
|
|
386
440
|
color: isSelected(opt.value) ? 'var(--accent, #f99e2c)' : 'var(--t,var(--primary-text, #0f172a))',
|
|
387
|
-
background: isSelected(opt.value) ? 'var(--pd,var(--accent-bg, rgba(249,158,44,.1)))' : undefined,
|
|
441
|
+
// background: isSelected(opt.value) ? 'var(--pd,var(--accent-bg, rgba(249,158,44,.1)))' : undefined,
|
|
388
442
|
fontSize: `${s.searchFont}px`,
|
|
389
443
|
fontWeight: 400,
|
|
390
444
|
transition: 'background .1s',
|
|
@@ -393,36 +447,11 @@ export default function Select({
|
|
|
393
447
|
pointerEvents: opt.disabled ? 'none' : undefined,
|
|
394
448
|
}}
|
|
395
449
|
>
|
|
396
|
-
{multi && (
|
|
397
|
-
<span style={{
|
|
398
|
-
width: 13,
|
|
399
|
-
height: 13,
|
|
400
|
-
border: `1.5px solid ${isSelected(opt.value) ? 'var(--accent, #f99e2c)' : 'var(--bd2,var(--border, #e2e8f0))'}`,
|
|
401
|
-
borderRadius: 3,
|
|
402
|
-
flexShrink: 0,
|
|
403
|
-
position: 'relative',
|
|
404
|
-
transition: 'all .15s',
|
|
405
|
-
background: isSelected(opt.value) ? 'var(--accent, #f99e2c)' : undefined,
|
|
406
|
-
}}>
|
|
407
|
-
{isSelected(opt.value) && (
|
|
408
|
-
<span style={{
|
|
409
|
-
position: 'absolute',
|
|
410
|
-
top: 1,
|
|
411
|
-
left: 3.5,
|
|
412
|
-
width: 4,
|
|
413
|
-
height: 6,
|
|
414
|
-
border: 'solid var(--def-btn-text,#fff)',
|
|
415
|
-
borderWidth: '0 1.5px 1.5px 0',
|
|
416
|
-
transform: 'rotate(45deg)',
|
|
417
|
-
}} />
|
|
418
|
-
)}
|
|
419
|
-
</span>
|
|
420
|
-
)}
|
|
421
450
|
<span style={{ flex: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', minHeight: '1em' }}>
|
|
422
451
|
{opt.displayLabel || opt.label || '\u00A0'}
|
|
423
452
|
</span>
|
|
424
|
-
{
|
|
425
|
-
<Check size={
|
|
453
|
+
{isSelected(opt.value) && (
|
|
454
|
+
<Check size={13} style={{ flexShrink: 0, color: 'var(--accent, #f99e2c)' }} />
|
|
426
455
|
)}
|
|
427
456
|
</div>
|
|
428
457
|
))
|