cynx-ui 1.2.6 → 1.2.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,29 +1,29 @@
1
- import { useState, useRef } from 'react';
2
-
3
- export function Dropzone({ onFile, accept, hint, file }) {
4
- const [over, setOver] = useState(false);
5
- const inputRef = useRef(null);
6
- return (
7
- <div className={`dropzone${over ? ' over' : ''}`}
8
- onDragOver={e => { e.preventDefault(); setOver(true); }}
9
- onDragLeave={() => setOver(false)}
10
- onDrop={e => { e.preventDefault(); setOver(false); const f = e.dataTransfer.files[0]; if (f) onFile(f); }}
11
- onClick={() => inputRef.current?.click()}>
12
- <input ref={inputRef} type="file" accept={accept}
13
- onChange={e => e.target.files[0] && onFile(e.target.files[0])} style={{ display: 'none' }} />
14
- <div className="dropzone-icon">
15
- <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="var(--grey-3)" strokeWidth="1.5" strokeLinecap="round">
16
- <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/>
17
- </svg>
18
- </div>
19
- {file ? (
20
- <div className="dropzone-file">{file.name}</div>
21
- ) : (
22
- <>
23
- <div className="dropzone-text"><b>Click to select</b> or drag &amp; drop</div>
24
- {hint && <div className="dropzone-hint">{hint}</div>}
25
- </>
26
- )}
27
- </div>
28
- );
29
- }
1
+ import { useState, useRef } from 'react';
2
+
3
+ export function Dropzone({ onFile, accept, hint, file }) {
4
+ const [over, setOver] = useState(false);
5
+ const inputRef = useRef(null);
6
+ return (
7
+ <div className={`dropzone${over ? ' over' : ''}`}
8
+ onDragOver={e => { e.preventDefault(); setOver(true); }}
9
+ onDragLeave={() => setOver(false)}
10
+ onDrop={e => { e.preventDefault(); setOver(false); const f = e.dataTransfer.files[0]; if (f) onFile(f); }}
11
+ onClick={() => inputRef.current?.click()}>
12
+ <input ref={inputRef} type="file" accept={accept}
13
+ onChange={e => e.target.files[0] && onFile(e.target.files[0])} style={{ display: 'none' }} />
14
+ <div className="dropzone-icon">
15
+ <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="var(--grey-3)" strokeWidth="1.5" strokeLinecap="round">
16
+ <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/>
17
+ </svg>
18
+ </div>
19
+ {file ? (
20
+ <div className="dropzone-file">{file.name}</div>
21
+ ) : (
22
+ <>
23
+ <div className="dropzone-text"><b>Click to select</b> or drag &amp; drop</div>
24
+ {hint && <div className="dropzone-hint">{hint}</div>}
25
+ </>
26
+ )}
27
+ </div>
28
+ );
29
+ }
@@ -1,81 +1,81 @@
1
- import { useState, useRef, useEffect } from 'react';
2
-
3
- /**
4
- * HoverCard — reusable tooltip/popover on hover
5
- *
6
- * Props:
7
- * trigger — React node as hover target
8
- * children — popup content
9
- * position — 'top' | 'bottom' (default: 'top')
10
- * width — min-width (default: 180)
11
- * style — extra popup styles
12
- * triggerStyle — extra trigger wrapper styles
13
- */
14
- export default function HoverCard({ trigger, children, position = 'top', width = 180, style, triggerStyle }) {
15
- const [open, setOpen] = useState(false);
16
- const [pos, setPos] = useState({ x: 0, y: 0 });
17
- const wrapRef = useRef(null);
18
-
19
- useEffect(() => {
20
- if (open && wrapRef.current) {
21
- const r = wrapRef.current.getBoundingClientRect();
22
- setPos({ x: r.left + r.width / 2, y: position === 'top' ? r.top : r.bottom });
23
- }
24
- }, [open, position]);
25
-
26
- useEffect(() => {
27
- if (!open) return;
28
- const close = (e) => {
29
- if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false);
30
- };
31
- document.addEventListener('mousedown', close);
32
- return () => document.removeEventListener('mousedown', close);
33
- }, [open]);
34
-
35
- return (
36
- <div
37
- ref={wrapRef}
38
- onMouseEnter={() => setOpen(true)}
39
- onMouseLeave={() => setOpen(false)}
40
- style={{ display: 'inline-block', position: 'relative', ...triggerStyle }}
41
- >
42
- {trigger}
43
- {open && (
44
- <div style={{
45
- position: 'fixed',
46
- left: pos.x,
47
- top: position === 'top' ? 'auto' : pos.y + 6,
48
- bottom: position === 'top' ? window.innerHeight - pos.y + 6 : 'auto',
49
- transform: 'translateX(-50%)',
50
- zIndex: 9999,
51
- minWidth: width,
52
- background: '#fff',
53
- border: '1px solid var(--border)',
54
- borderRadius: 'var(--rads, 8px)',
55
- padding: '10px 14px',
56
- boxShadow: '0 4px 12px rgba(0,0,0,.12)',
57
- pointerEvents: 'none',
58
- animation: 'hoverCardIn .15s ease',
59
- ...style,
60
- }}>
61
- {children}
62
- <div style={{
63
- position: 'absolute', left: '50%',
64
- [position === 'top' ? 'bottom' : 'top']: -5,
65
- transform: 'translateX(-50%) rotate(45deg)',
66
- width: 8, height: 8, background: '#fff',
67
- borderRight: '1px solid var(--border)',
68
- borderBottom: position === 'top' ? 'none' : '1px solid var(--border)',
69
- borderTop: position === 'top' ? '1px solid var(--border)' : 'none',
70
- }} />
71
- </div>
72
- )}
73
- <style>{`
74
- @keyframes hoverCardIn {
75
- from { opacity: 0; transform: translateX(-50%) translateY(4px); }
76
- to { opacity: 1; transform: translateX(-50%) translateY(0); }
77
- }
78
- `}</style>
79
- </div>
80
- );
81
- }
1
+ import { useState, useRef, useEffect } from 'react';
2
+
3
+ /**
4
+ * HoverCard — reusable tooltip/popover on hover
5
+ *
6
+ * Props:
7
+ * trigger — React node as hover target
8
+ * children — popup content
9
+ * position — 'top' | 'bottom' (default: 'top')
10
+ * width — min-width (default: 180)
11
+ * style — extra popup styles
12
+ * triggerStyle — extra trigger wrapper styles
13
+ */
14
+ export default function HoverCard({ trigger, children, position = 'top', width = 180, style, triggerStyle }) {
15
+ const [open, setOpen] = useState(false);
16
+ const [pos, setPos] = useState({ x: 0, y: 0 });
17
+ const wrapRef = useRef(null);
18
+
19
+ useEffect(() => {
20
+ if (open && wrapRef.current) {
21
+ const r = wrapRef.current.getBoundingClientRect();
22
+ setPos({ x: r.left + r.width / 2, y: position === 'top' ? r.top : r.bottom });
23
+ }
24
+ }, [open, position]);
25
+
26
+ useEffect(() => {
27
+ if (!open) return;
28
+ const close = (e) => {
29
+ if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false);
30
+ };
31
+ document.addEventListener('mousedown', close);
32
+ return () => document.removeEventListener('mousedown', close);
33
+ }, [open]);
34
+
35
+ return (
36
+ <div
37
+ ref={wrapRef}
38
+ onMouseEnter={() => setOpen(true)}
39
+ onMouseLeave={() => setOpen(false)}
40
+ style={{ display: 'inline-block', position: 'relative', ...triggerStyle }}
41
+ >
42
+ {trigger}
43
+ {open && (
44
+ <div style={{
45
+ position: 'fixed',
46
+ left: pos.x,
47
+ top: position === 'top' ? 'auto' : pos.y + 6,
48
+ bottom: position === 'top' ? window.innerHeight - pos.y + 6 : 'auto',
49
+ transform: 'translateX(-50%)',
50
+ zIndex: 9999,
51
+ minWidth: width,
52
+ background: '#fff',
53
+ border: '1px solid var(--border)',
54
+ borderRadius: 'var(--rads, 8px)',
55
+ padding: '10px 14px',
56
+ boxShadow: '0 4px 12px rgba(0,0,0,.12)',
57
+ pointerEvents: 'none',
58
+ animation: 'hoverCardIn .15s ease',
59
+ ...style,
60
+ }}>
61
+ {children}
62
+ <div style={{
63
+ position: 'absolute', left: '50%',
64
+ [position === 'top' ? 'bottom' : 'top']: -5,
65
+ transform: 'translateX(-50%) rotate(45deg)',
66
+ width: 8, height: 8, background: '#fff',
67
+ borderRight: '1px solid var(--border)',
68
+ borderBottom: position === 'top' ? 'none' : '1px solid var(--border)',
69
+ borderTop: position === 'top' ? '1px solid var(--border)' : 'none',
70
+ }} />
71
+ </div>
72
+ )}
73
+ <style>{`
74
+ @keyframes hoverCardIn {
75
+ from { opacity: 0; transform: translateX(-50%) translateY(4px); }
76
+ to { opacity: 1; transform: translateX(-50%) translateY(0); }
77
+ }
78
+ `}</style>
79
+ </div>
80
+ );
81
+ }
@@ -1,121 +1,181 @@
1
- import { useRef } from 'react';
2
-
3
- const STYLES = `
4
- .cinp{display:block;width:100%;background:var(--input-bg,#fff);border:1px solid var(--bd,#d7d8e0);border-radius:var(--rads,8px);color:var(--t1,#1a1a2e);font-family:var(--f,var(--sans,inherit));font-size:13px;padding:0 11px;outline:none;transition:border-color .14s,box-shadow .14s;box-shadow:var(--shadow,0 1px 3px rgba(20,22,41,.1))}
5
- .cinp::placeholder{color:var(--t3,#9b9daa)}
6
- .cinp:focus{border-color:var(--accent,#f99e2c);box-shadow:0 0 0 3px rgba(249,158,44,.12),var(--shadow,0 1px 3px rgba(20,22,41,.1))}
7
- .cinp:disabled{opacity:.5;cursor:not-allowed;background:var(--grey-1,#f5f5f8)}
8
- .cinp-error{border-color:var(--danger,#f64747)}
9
- .cinp-error:focus{border-color:var(--danger,#f64747);box-shadow:0 0 0 3px rgba(246,71,71,.12)}
10
-
11
- .cinp-xs{height:30px;font-size:11px;padding:0 8px}
12
- .cinp-sm{height:32px;font-size:12px;padding:0 9px}
13
- .cinp-md{height:34px;font-size:13px}
14
- .cinp-lg{height:40px;font-size:14px;padding:0 14px}
15
-
16
- .cinp-wrap{display:flex;flex-direction:column;gap:5px}
17
- .cinp-wrap-xs .cinp-label{font-size:9px}
18
- .cinp-label{font-size:11px;font-weight:600;color:var(--t2,#797b8d);text-transform:uppercase;letter-spacing:.08em}
19
- .cinp-req{color:var(--danger,#f64747);margin-left:2px}
20
- .cinp-hint{font-size:11px;color:var(--t3,#9b9daa)}
21
- .cinp-err{font-size:11px;color:var(--danger,#f64747)}
22
-
23
- .cinp-icon-left{position:relative}
24
- .cinp-icon-left .cinp{padding-left:34px}
25
- .cinp-icon-left .cinp-icon{position:absolute;left:10px;top:50%;transform:translateY(-50%);color:var(--t3,#9b9daa);pointer-events:none;line-height:0}
26
- .cinp-icon-left .cinp-icon svg{width:14px;height:14px}
27
-
28
- .cinp-icon-right{position:relative}
29
- .cinp-icon-right .cinp{padding-right:34px}
30
- .cinp-icon-right .cinp-icon{position:absolute;right:10px;top:50%;transform:translateY(-50%);color:var(--t3,#9b9daa);pointer-events:none}
31
- .cinp-icon-right .cinp-icon svg{width:14px;height:14px}
32
-
33
- .cinp-wrap-clearable{position:relative}
34
- .cinp-wrap-clearable .cinp{padding-right:30px}
35
- .cinp-clear{position:absolute;right:8px;top:50%;transform:translateY(-50%);display:flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;background:transparent;border:none;cursor:pointer;color:var(--t3,#9b9daa);padding:0;transition:background .12s,color .12s;line-height:0}
36
- .cinp-clear:hover{background:var(--grey-1,#f0f0f4);color:var(--t1,#1a1a2e)}
37
-
38
- .cinp-wrap-suffix{position:relative}
39
- .cinp-wrap-suffix .cinp{padding-right:40px}
40
- .cinp-suffix{position:absolute;right:8px;top:50%;transform:translateY(-50%);display:flex;align-items:center;gap:4px;pointer-events:none}
41
-
42
- textarea.cinp{resize:vertical;min-height:72px;height:auto;padding-top:8px;padding-bottom:8px;line-height:1.5}
43
- `;
44
-
45
- let styleInjected = false;
46
-
47
- export default function Input({
48
- label,
49
- required = false,
50
- hint,
51
- error,
52
- size = 'md',
53
- icon,
54
- iconRight,
55
- clearable = false,
56
- suffix,
57
- onClear,
58
- className = '',
59
- style = {},
60
- value,
61
- ...props
62
- }) {
63
- const inputRef = useRef(null);
64
-
65
- if (!styleInjected && typeof document !== 'undefined') {
66
- const tag = document.createElement('style');
67
- tag.textContent = STYLES;
68
- document.head.appendChild(tag);
69
- styleInjected = true;
70
- }
71
-
72
- function handleClear() {
73
- if (props.onChange) props.onChange({ target: { value: '' } });
74
- if (onClear) onClear();
75
- inputRef.current?.focus();
76
- }
77
-
78
- const hasIconLeft = !!icon;
79
- const hasIconRight = !!iconRight;
80
- const showClear = clearable && !!value && !props.disabled;
81
- const hasSuffix = !!suffix;
82
- const showRightSlot = showClear || hasSuffix;
83
-
84
- const wrapClasses = [
85
- 'cinp-wrap',
86
- `cinp-wrap-${size}`,
87
- hasIconLeft && 'cinp-icon-left',
88
- (hasIconRight || showRightSlot) && 'cinp-icon-right',
89
- showRightSlot && !hasIconRight && 'cinp-wrap-clearable',
90
- ].filter(Boolean).join(' ');
91
-
92
- const inputClass = `cinp cinp-${size}${error ? ' cinp-error' : ''}${className ? ' ' + className : ''}`;
93
-
94
- const input = (
95
- <input ref={inputRef} className={inputClass} style={style} value={value} {...props} />
96
- );
97
-
98
- return (
99
- <div className={wrapClasses}>
100
- {label && (
101
- <label className="cinp-label">
102
- {label}
103
- {required && <span className="cinp-req">*</span>}
104
- </label>
105
- )}
106
- {hasIconLeft && <span className="cinp-icon">{icon}</span>}
107
- {hasIconRight && <span className="cinp-icon">{iconRight}</span>}
108
- {input}
109
- {showClear && (
110
- <button type="button" className="cinp-clear" tabIndex={-1} onClick={handleClear}>
111
- <svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round">
112
- <line x1="1" y1="1" x2="9" y2="9" />
113
- <line x1="9" y1="1" x2="1" y2="9" />
114
- </svg>
115
- </button>
116
- )}
117
- {hint && !error && <span className="cinp-hint">{hint}</span>}
118
- {error && <span className="cinp-err">{error}</span>}
119
- </div>
120
- );
121
- }
1
+ import { useRef } from 'react';
2
+
3
+ const SIZES = {
4
+ xs: { height: 30, fontSize: 11, paddingH: 8 },
5
+ sm: { height: 32, fontSize: 12, paddingH: 9 },
6
+ md: { height: 34, fontSize: 13, paddingH: 11 },
7
+ lg: { height: 40, fontSize: 14, paddingH: 14 },
8
+ };
9
+
10
+ export default function Input({
11
+ label,
12
+ required = false,
13
+ hint,
14
+ error,
15
+ size = 'md',
16
+ icon,
17
+ iconRight,
18
+ clearable = false,
19
+ suffix,
20
+ onClear,
21
+ className = '',
22
+ style = {},
23
+ value,
24
+ ...props
25
+ }) {
26
+ const inputRef = useRef(null);
27
+ const s = SIZES[size] || SIZES.md;
28
+
29
+ function handleClear() {
30
+ if (props.onChange) props.onChange({ target: { value: '' } });
31
+ if (onClear) onClear();
32
+ inputRef.current?.focus();
33
+ }
34
+
35
+ const hasIconLeft = !!icon;
36
+ const hasIconRight = !!iconRight;
37
+ const showClear = clearable && !!value && !props.disabled;
38
+ const hasSuffix = !!suffix;
39
+ const showRightSlot = showClear || hasSuffix;
40
+
41
+ return (
42
+ <div style={{
43
+ display: 'flex',
44
+ flexDirection: 'column',
45
+ gap: 5,
46
+ }}>
47
+ {label && (
48
+ <label style={{
49
+ fontSize: size === 'xs' ? 9 : 11,
50
+ fontWeight: 600,
51
+ color: 'var(--t2,#797b8d)',
52
+ textTransform: 'uppercase',
53
+ letterSpacing: '.08em',
54
+ }}>
55
+ {label}
56
+ {required && <span style={{ color: 'var(--danger,#f64747)', marginLeft: 2 }}>*</span>}
57
+ </label>
58
+ )}
59
+
60
+ <div style={{
61
+ position: 'relative',
62
+ display: 'flex',
63
+ alignItems: 'center',
64
+ }}>
65
+ {hasIconLeft && (
66
+ <span style={{
67
+ position: 'absolute',
68
+ left: 10,
69
+ top: '50%',
70
+ transform: 'translateY(-50%)',
71
+ color: 'var(--t3,#9b9daa)',
72
+ pointerEvents: 'none',
73
+ lineHeight: 0,
74
+ display: 'flex',
75
+ alignItems: 'center',
76
+ }}>
77
+ {icon}
78
+ </span>
79
+ )}
80
+
81
+ {hasIconRight && (
82
+ <span style={{
83
+ position: 'absolute',
84
+ right: 10,
85
+ top: '50%',
86
+ transform: 'translateY(-50%)',
87
+ color: 'var(--t3,#9b9daa)',
88
+ pointerEvents: 'none',
89
+ display: 'flex',
90
+ alignItems: 'center',
91
+ }}>
92
+ {iconRight}
93
+ </span>
94
+ )}
95
+
96
+ <input
97
+ ref={inputRef}
98
+ className={className}
99
+ style={{
100
+ display: 'block',
101
+ width: '100%',
102
+ background: 'var(--input-bg,#fff)',
103
+ border: `1px solid ${error ? 'var(--danger,#f64747)' : 'var(--bd,#d7d8e0)'}`,
104
+ borderRadius: 'var(--rads,8px)',
105
+ color: 'var(--t1,#1a1a2e)',
106
+ fontFamily: 'var(--f,var(--sans,inherit))',
107
+ fontSize: s.fontSize,
108
+ height: s.height,
109
+ padding: `0 ${s.paddingH}px`,
110
+ paddingLeft: hasIconLeft ? 34 : s.paddingH,
111
+ paddingRight: (hasIconRight || showRightSlot) ? 34 : s.paddingH,
112
+ outline: 'none',
113
+ transition: 'border-color .14s, box-shadow .14s',
114
+ boxShadow: error
115
+ ? '0 0 0 3px rgba(246,71,71,.12), var(--shadow,0 1px 3px rgba(20,22,41,.1))'
116
+ : 'var(--shadow,0 1px 3px rgba(20,22,41,.1))',
117
+ opacity: props.disabled ? 0.5 : 1,
118
+ cursor: props.disabled ? 'not-allowed' : undefined,
119
+ background: props.disabled ? 'var(--grey-1,#f5f5f8)' : undefined,
120
+ ...style,
121
+ }}
122
+ value={value}
123
+ {...props}
124
+ />
125
+
126
+ {showClear && (
127
+ <button
128
+ type="button"
129
+ tabIndex={-1}
130
+ onClick={handleClear}
131
+ style={{
132
+ position: 'absolute',
133
+ right: 8,
134
+ top: '50%',
135
+ transform: 'translateY(-50%)',
136
+ display: 'flex',
137
+ alignItems: 'center',
138
+ justifyContent: 'center',
139
+ width: 18,
140
+ height: 18,
141
+ borderRadius: '50%',
142
+ background: 'transparent',
143
+ border: 'none',
144
+ cursor: 'pointer',
145
+ color: 'var(--t3,#9b9daa)',
146
+ padding: 0,
147
+ lineHeight: 0,
148
+ }}
149
+ >
150
+ <svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round">
151
+ <line x1="1" y1="1" x2="9" y2="9" />
152
+ <line x1="9" y1="1" x2="1" y2="9" />
153
+ </svg>
154
+ </button>
155
+ )}
156
+
157
+ {hasSuffix && !showClear && (
158
+ <span style={{
159
+ position: 'absolute',
160
+ right: 8,
161
+ top: '50%',
162
+ transform: 'translateY(-50%)',
163
+ display: 'flex',
164
+ alignItems: 'center',
165
+ gap: 4,
166
+ pointerEvents: 'none',
167
+ }}>
168
+ {suffix}
169
+ </span>
170
+ )}
171
+ </div>
172
+
173
+ {hint && !error && (
174
+ <span style={{ fontSize: 11, color: 'var(--t3,#9b9daa)' }}>{hint}</span>
175
+ )}
176
+ {error && (
177
+ <span style={{ fontSize: 11, color: 'var(--danger,#f64747)' }}>{error}</span>
178
+ )}
179
+ </div>
180
+ );
181
+ }