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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cynx-lib",
3
- "version": "1.3.32",
3
+ "version": "1.3.34",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "exports": {
@@ -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}
@@ -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 calculatedMaxHeight = Math.max(90, Math.min(220, availableSpace - (searchable ? 46 : 14)));
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
- {filtered.length === 0 ? (
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
- {!multi && isSelected(opt.value) && (
425
- <Check size={12} style={{ flexShrink: 0, color: 'var(--accent, #f99e2c)' }} />
453
+ {isSelected(opt.value) && (
454
+ <Check size={13} style={{ flexShrink: 0, color: 'var(--accent, #f99e2c)' }} />
426
455
  )}
427
456
  </div>
428
457
  ))