cynx-ui 1.3.10 → 1.3.12

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.
@@ -55,6 +55,7 @@ export function BrandingCard({ label, hint, value, onUpload, onRemove, saving, o
55
55
  <div>
56
56
  <div style={{
57
57
  display: 'flex', alignItems: 'center', justifyContent: 'space-between',
58
+ gap: 10,
58
59
  padding: '10px 12px', borderRadius: 'var(--rads)',
59
60
  border: '1px solid var(--border-light)', background: 'var(--surface)',
60
61
  boxShadow: 'var(--shadow, 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.06))',
@@ -71,7 +72,7 @@ export function BrandingCard({ label, hint, value, onUpload, onRemove, saving, o
71
72
  </div>
72
73
  )}
73
74
  {!fullview && (
74
- <div style={{ flex: 1, fontSize: '.78rem', color: filename ? 'var(--primary-text)' : 'var(--muted)', fontFamily: filename ? 'var(--mono)' : 'inherit', wordBreak: 'break-all', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
75
+ <div style={{ flex: 1, minWidth: 0, fontSize: '.78rem', color: filename ? 'var(--primary-text)' : 'var(--muted)', fontFamily: filename ? 'var(--mono)' : 'inherit', wordBreak: 'break-all', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
75
76
  {filename || 'Not set'}
76
77
  </div>
77
78
  )}
@@ -49,15 +49,23 @@ export default function Dropdown({ value, onChange, options, placeholder = 'Sele
49
49
 
50
50
  const selected = options.find(o => String(o.value) === String(value));
51
51
 
52
+ const spaceBelow = rect ? window.innerHeight - rect.bottom : 200;
53
+ const spaceAbove = rect ? rect.top : 200;
54
+ const estimatedH = Math.min(options.length * 36 + 10, 220);
55
+ const openUp = spaceBelow < estimatedH + 10 && spaceAbove > spaceBelow;
56
+ const calculatedTop = rect ? (openUp ? Math.max(8, rect.top - estimatedH - 4) : rect.bottom + 4) : 0;
57
+
52
58
  const menu = open && rect && createPortal(
53
59
  <div
54
60
  ref={menuRef}
55
61
  data-dropdown="true"
56
62
  style={{
57
63
  position: 'fixed',
58
- top: rect.bottom + 4,
64
+ top: calculatedTop,
59
65
  left: rect.left,
60
66
  width: rect.width,
67
+ maxHeight: 220,
68
+ overflowY: 'auto',
61
69
  background: 'var(--surface)',
62
70
  border: '1px solid var(--border)',
63
71
  borderRadius: 'var(--rads)',
@@ -70,12 +70,18 @@ export default function MultiSelect({ value = [], onChange, options = [], placeh
70
70
  if (open) setQuery('');
71
71
  }
72
72
 
73
+ const spaceBelow = rect ? window.innerHeight - rect.bottom : 200;
74
+ const spaceAbove = rect ? rect.top : 200;
75
+ const estimatedH = 220;
76
+ const openUp = spaceBelow < estimatedH + 10 && spaceAbove > spaceBelow;
77
+ const top = rect ? (openUp ? Math.max(8, rect.top - estimatedH - 4) : rect.bottom + 4) : 0;
78
+
73
79
  const menu = open && rect && createPortal(
74
80
  <div
75
81
  ref={menuRef}
76
82
  style={{
77
83
  position: 'fixed',
78
- top: rect.bottom + 4,
84
+ top,
79
85
  left: rect.left,
80
86
  width: rect.width,
81
87
  zIndex: 999999,
@@ -48,9 +48,14 @@ export function SearchableDropdown({ value, onChange, options = [], placeholder
48
48
  const btn = btnRef.current;
49
49
  if (!btn) return;
50
50
  const rect = btn.getBoundingClientRect();
51
+ const spaceBelow = window.innerHeight - rect.bottom;
52
+ const spaceAbove = rect.top;
53
+ const estimatedH = 220;
54
+ const openUp = spaceBelow < estimatedH + 10 && spaceAbove > spaceBelow;
55
+ const top = openUp ? Math.max(8, rect.top - estimatedH - 4) : rect.bottom + 4;
51
56
  setMenuStyle({
52
57
  position: 'fixed',
53
- top: rect.bottom + 4,
58
+ top,
54
59
  left: rect.left,
55
60
  width: rect.width,
56
61
  zIndex: 999999,
@@ -1,8 +1,11 @@
1
- import { useState, useRef, useEffect, useMemo } from 'react';
1
+ import { useState, useRef, useEffect, useLayoutEffect, useMemo, useCallback } from 'react';
2
2
  import { createPortal } from 'react-dom';
3
3
  import { ChevronDown, Check, X } from 'lucide-react';
4
4
 
5
- const KEYFRAMES = `@keyframes selFadeIn{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}`;
5
+ const KEYFRAMES = `
6
+ @keyframes selFadeInDown{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}
7
+ @keyframes selFadeInUp{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}
8
+ `;
6
9
  let _id = 'cynx-select-styles';
7
10
 
8
11
  function ensureStyles() {
@@ -30,35 +33,73 @@ export default function Select({
30
33
  }) {
31
34
  const [open, setOpen] = useState(false);
32
35
  const [query, setQuery] = useState('');
33
- const [pos, setPos] = useState({ top: 0, left: 0, width: 0 });
36
+ const [pos, setPos] = useState({ top: 0, left: 0, width: 0, openUp: false, maxHeight: 200 });
34
37
  const wrapRef = useRef(null);
35
38
  const dropRef = useRef(null);
36
39
  const inputRef = useRef(null);
37
40
 
38
41
  ensureStyles();
39
42
 
43
+ const calcPosition = useCallback(() => {
44
+ if (!wrapRef.current) return;
45
+ const r = wrapRef.current.getBoundingClientRect();
46
+ const dropEl = dropRef.current;
47
+ const dropHeight = dropEl ? dropEl.offsetHeight : 220;
48
+ const spaceBelow = window.innerHeight - r.bottom;
49
+ const spaceAbove = r.top;
50
+
51
+ // If not enough space below (< dropHeight + 10) and more space above, open up
52
+ const openUp = spaceBelow < Math.min(dropHeight, 220) + 12 && spaceAbove > spaceBelow;
53
+
54
+ // Dynamic max-height for inner list to prevent overflow
55
+ const availableSpace = openUp ? spaceAbove - 16 : spaceBelow - 16;
56
+ const calculatedMaxHeight = Math.max(90, Math.min(220, availableSpace - (searchable ? 46 : 14)));
57
+
58
+ let top;
59
+ if (openUp) {
60
+ top = Math.max(8, r.top - (dropEl ? dropEl.offsetHeight : dropHeight) - 4);
61
+ } else {
62
+ top = Math.min(window.innerHeight - 40, r.bottom + 4);
63
+ }
64
+
65
+ let left = r.left;
66
+ const width = r.width;
67
+ if (left + width > window.innerWidth - 8) {
68
+ left = Math.max(8, window.innerWidth - width - 8);
69
+ }
70
+
71
+ setPos({
72
+ top,
73
+ left,
74
+ width,
75
+ openUp,
76
+ maxHeight: calculatedMaxHeight,
77
+ });
78
+ }, [searchable]);
79
+
40
80
  useEffect(() => {
41
81
  if (!open) return;
42
- function updatePos() {
43
- if (wrapRef.current) {
44
- const r = wrapRef.current.getBoundingClientRect();
45
- setPos({ top: r.bottom + 4, left: r.left, width: r.width });
46
- }
47
- }
48
82
  function close(e) {
49
83
  const inTrigger = wrapRef.current && wrapRef.current.contains(e.target);
50
84
  const inDrop = dropRef.current && dropRef.current.contains(e.target);
51
85
  if (!inTrigger && !inDrop) setOpen(false);
52
86
  }
53
87
  document.addEventListener('mousedown', close, true);
54
- window.addEventListener('scroll', updatePos, true);
55
- window.addEventListener('resize', updatePos);
88
+ window.addEventListener('scroll', calcPosition, true);
89
+ window.addEventListener('resize', calcPosition);
56
90
  return () => {
57
91
  document.removeEventListener('mousedown', close, true);
58
- window.removeEventListener('scroll', updatePos, true);
59
- window.removeEventListener('resize', updatePos);
92
+ window.removeEventListener('scroll', calcPosition, true);
93
+ window.removeEventListener('resize', calcPosition);
60
94
  };
61
- }, [open]);
95
+ }, [open, calcPosition]);
96
+
97
+ useLayoutEffect(() => {
98
+ if (open) {
99
+ calcPosition();
100
+ requestAnimationFrame(calcPosition);
101
+ }
102
+ }, [open, calcPosition]);
62
103
 
63
104
  useEffect(() => {
64
105
  if (open && searchable && inputRef.current) inputRef.current.focus();
@@ -85,8 +126,7 @@ export default function Select({
85
126
  function handleOpen() {
86
127
  if (disabled) return;
87
128
  if (!open && wrapRef.current) {
88
- const r = wrapRef.current.getBoundingClientRect();
89
- setPos({ top: r.bottom + 4, left: r.left, width: r.width });
129
+ calcPosition();
90
130
  }
91
131
  setOpen(o => !o);
92
132
  }
@@ -254,7 +294,7 @@ export default function Select({
254
294
  borderRadius: radiusVal,
255
295
  boxShadow: 'var(--shadow,0 1px 3px rgba(20,22,41,.1))',
256
296
  padding: 3,
257
- animation: 'selFadeIn .15s ease',
297
+ animation: `${pos.openUp ? 'selFadeInUp' : 'selFadeInDown'} .15s ease`,
258
298
  overflow: 'hidden',
259
299
  }}
260
300
  >
@@ -285,7 +325,7 @@ export default function Select({
285
325
  />
286
326
  </div>
287
327
  )}
288
- <div style={{ maxHeight: 200, overflowY: 'auto' }}>
328
+ <div style={{ maxHeight: pos.maxHeight || 200, overflowY: 'auto' }}>
289
329
  {filtered.length === 0 ? (
290
330
  <div style={{ padding: '10px 8px', textAlign: 'center', color: 'var(--t3,var(--grey-3))', fontSize: 11 }}>
291
331
  Ничего не найдено
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cynx-ui",
3
- "version": "1.3.10",
3
+ "version": "1.3.12",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./index.js"