cynx-lib 1.3.28

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.
Files changed (46) hide show
  1. package/index.js +3 -0
  2. package/package.json +36 -0
  3. package/ui/components/ActionsMenu.jsx +158 -0
  4. package/ui/components/ActivityCard.jsx +184 -0
  5. package/ui/components/AuthOverlay.jsx +94 -0
  6. package/ui/components/Badge.jsx +186 -0
  7. package/ui/components/BrandingCard.jsx +146 -0
  8. package/ui/components/Button.jsx +148 -0
  9. package/ui/components/Card.jsx +132 -0
  10. package/ui/components/Checkbox.jsx +59 -0
  11. package/ui/components/ChipSelect.jsx +54 -0
  12. package/ui/components/CircularProgress.jsx +216 -0
  13. package/ui/components/Confirm.jsx +79 -0
  14. package/ui/components/DatePicker.jsx +605 -0
  15. package/ui/components/Divider.jsx +96 -0
  16. package/ui/components/Dropzone.jsx +203 -0
  17. package/ui/components/FilePicker.jsx +205 -0
  18. package/ui/components/HoverCard.jsx +81 -0
  19. package/ui/components/ImageUpload.jsx +147 -0
  20. package/ui/components/InfoBadge.jsx +204 -0
  21. package/ui/components/Input.jsx +229 -0
  22. package/ui/components/LineProgress.jsx +133 -0
  23. package/ui/components/LinearProgress.jsx +132 -0
  24. package/ui/components/Modal.jsx +212 -0
  25. package/ui/components/Pagination.jsx +52 -0
  26. package/ui/components/PulseDot.jsx +77 -0
  27. package/ui/components/Select.jsx +415 -0
  28. package/ui/components/Skeleton.jsx +118 -0
  29. package/ui/components/Spinner.jsx +108 -0
  30. package/ui/components/StatBarCard.jsx +25 -0
  31. package/ui/components/StatsBar.jsx +88 -0
  32. package/ui/components/StatsCard.jsx +171 -0
  33. package/ui/components/TabBar.jsx +404 -0
  34. package/ui/components/Tabs.jsx +29 -0
  35. package/ui/components/TimePicker.jsx +1069 -0
  36. package/ui/components/Toast.jsx +106 -0
  37. package/ui/components/Toggle.jsx +67 -0
  38. package/ui/components/UptimeBar.jsx +135 -0
  39. package/ui/components/XTable.jsx +44 -0
  40. package/ui/index.js +97 -0
  41. package/utils/index.js +11 -0
  42. package/utils/utils/oidc.js +209 -0
  43. package/utils/utils/realtime.js +1040 -0
  44. package/utils/utils/storage.js +393 -0
  45. package/utils/utils/tz.js +625 -0
  46. package/utils/utils/uid.js +252 -0
@@ -0,0 +1,147 @@
1
+ import { useState, useRef, useEffect } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import { Pencil, Upload, X, Trash2 } from 'lucide-react';
4
+ import Button from './Button.jsx';
5
+
6
+ export default function ImageUpload({ value, file, onChange, onRemove, label = 'Image' }) {
7
+ const [editOpen, setEditOpen] = useState(false);
8
+ const [urlInput, setUrlInput] = useState(value || '');
9
+ const [pendingFile, setPendingFile] = useState(null);
10
+ const [dragOver, setDragOver] = useState(false);
11
+ const [closing, setClosing] = useState(false);
12
+
13
+ const previewUrl = file ? URL.createObjectURL(file) : value;
14
+ const filename = file ? file.name : value ? value.split('/').pop() : null;
15
+ const hasImage = !!(file || value);
16
+
17
+ useEffect(() => {
18
+ if (!editOpen) return;
19
+ function onKey(e) { if (e.key === 'Escape') handleClose(); }
20
+ document.addEventListener('keydown', onKey);
21
+ return () => document.removeEventListener('keydown', onKey);
22
+ }, [editOpen]);
23
+
24
+ function handleFile(f) {
25
+ if (f && f.type.startsWith('image/')) {
26
+ setPendingFile(f);
27
+ setUrlInput('');
28
+ }
29
+ }
30
+
31
+ function handleSave() {
32
+ if (pendingFile) {
33
+ onChange({ file: pendingFile, url: '' });
34
+ } else if (urlInput.trim()) {
35
+ onChange({ file: null, url: urlInput.trim() });
36
+ }
37
+ handleClose();
38
+ }
39
+
40
+ function handleClose() {
41
+ setClosing(true);
42
+ setTimeout(() => {
43
+ setEditOpen(false);
44
+ setClosing(false);
45
+ setPendingFile(null);
46
+ setUrlInput(value || '');
47
+ }, 260);
48
+ }
49
+
50
+ function openEdit() {
51
+ setUrlInput(value || '');
52
+ setPendingFile(null);
53
+ setEditOpen(true);
54
+ }
55
+
56
+ const modal = editOpen && createPortal(
57
+ <div className="modal-backdrop"
58
+ style={{ animation: `${closing ? 'backdropClose' : 'backdropFade'} .26s ease forwards` }}
59
+ onClick={e => { if (e.target === e.currentTarget) handleClose(); }}>
60
+ <div className="modal" style={{
61
+ maxWidth: 420,
62
+ animation: `${closing ? 'modalClose' : 'modalPop'} .26s cubic-bezier(.4,0,.2,1) forwards`,
63
+ }}>
64
+ <div className="modal-hdr">
65
+ <div className="modal-title">Edit {label}</div>
66
+ <Button variant="no-line" size="sm" icon={<X size={14} />} onClick={handleClose} />
67
+ </div>
68
+ <div className="modal-body" style={{ overflow: 'visible' }}>
69
+ <label
70
+ style={{
71
+ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
72
+ gap: 8, padding: '24px 16px', borderRadius: 'var(--rads, 8px)',
73
+ border: `1.5px dashed ${dragOver ? 'var(--accent, #f99e2c)' : 'var(--border, #e2e8f0)'}`,
74
+ background: 'var(--grey-1, #f5f5f8)', cursor: 'pointer', transition: 'all .15s', textAlign: 'center',
75
+ }}
76
+ onDragOver={e => { e.preventDefault(); setDragOver(true); }}
77
+ onDragLeave={() => setDragOver(false)}
78
+ onDrop={e => { e.preventDefault(); setDragOver(false); handleFile(e.dataTransfer.files[0]); }}
79
+ >
80
+ <input type="file" accept="image/*,.ico" style={{ display: 'none' }}
81
+ onChange={e => handleFile(e.target.files[0])} />
82
+ {pendingFile ? (
83
+ <>
84
+ <img src={URL.createObjectURL(pendingFile)} alt="preview" style={{ height: 48, maxWidth: 160, objectFit: 'contain', borderRadius: 4 }} />
85
+ <div style={{ fontSize: '.76rem', fontWeight: 600, color: 'var(--accent, #f99e2c)' }}>{pendingFile.name}</div>
86
+ <div style={{ fontSize: '.68rem', color: 'var(--muted, #797b8d)' }}>{(pendingFile.size / 1024).toFixed(1)} KB — click to change</div>
87
+ </>
88
+ ) : (
89
+ <>
90
+ <Upload size={20} style={{ color: 'var(--muted, #797b8d)' }} />
91
+ <div style={{ fontSize: '.82rem', fontWeight: 600 }}>Click or drag to upload</div>
92
+ <div style={{ fontSize: '.72rem', color: 'var(--muted, #797b8d)' }}>PNG, JPG, SVG, ICO up to 2MB</div>
93
+ </>
94
+ )}
95
+ </label>
96
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '12px 0 4px' }}>
97
+ <div style={{ flex: 1, height: 1, background: 'var(--border, #e2e8f0)' }} />
98
+ <span style={{ fontSize: '.72rem', color: 'var(--muted, #797b8d)' }}>or enter URL directly</span>
99
+ <div style={{ flex: 1, height: 1, background: 'var(--border, #e2e8f0)' }} />
100
+ </div>
101
+ <input
102
+ type="url" placeholder="https://example.com/logo.png"
103
+ value={urlInput}
104
+ onChange={e => { setUrlInput(e.target.value); setPendingFile(null); }}
105
+ style={{
106
+ width: '100%', padding: '8px 10px', fontSize: '.82rem',
107
+ border: '1px solid var(--border, #e2e8f0)', borderRadius: 'var(--rads, 8px)',
108
+ background: 'var(--input-bg, #ffffff)', color: 'var(--primary-text, #0f172a)',
109
+ fontFamily: 'var(--sans, inherit)', outline: 'none',
110
+ }}
111
+ />
112
+ </div>
113
+ <div className="modal-footer">
114
+ <Button variant="ghost" size="xs" onClick={handleClose}>Cancel</Button>
115
+ <Button variant="primary" size="xs" onClick={handleSave} disabled={!pendingFile && !urlInput.trim()}>Save</Button>
116
+ </div>
117
+ </div>
118
+ </div>,
119
+ document.body
120
+ );
121
+
122
+ return (
123
+ <>
124
+ <div style={{
125
+ display: 'flex', alignItems: 'center', gap: 12,
126
+ padding: '10px 12px', borderRadius: 'var(--rads, 8px)',
127
+ border: '1px solid var(--border, #e2e8f0)', background: 'var(--grey-1, #f5f5f8)',
128
+ }}>
129
+ {hasImage ? (
130
+ <img src={previewUrl} alt={label} style={{ height: 32, maxWidth: 80, objectFit: 'contain', borderRadius: 4, flexShrink: 0 }} />
131
+ ) : (
132
+ <div style={{ width: 32, height: 32, borderRadius: 4, background: 'var(--grey-2, #ebebee)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
133
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--muted, #797b8d)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
134
+ </div>
135
+ )}
136
+ <div style={{ flex: 1, fontSize: '.78rem', color: filename ? 'var(--primary-text, #0f172a)' : 'var(--muted, #797b8d)', fontFamily: filename ? 'var(--mono, ui-monospace, monospace)' : 'inherit', wordBreak: 'break-all' }}>
137
+ {filename || 'Not set yet'}
138
+ </div>
139
+ <Button variant="ghost" size="xs" icon={<Pencil size={12} />} onClick={openEdit} style={{ flexShrink: 0 }}>Edit</Button>
140
+ {hasImage && (
141
+ <Button variant="ghost" size="xs" icon={<Trash2 size={12} />} style={{ color: 'var(--danger, #f64747)', flexShrink: 0 }} onClick={onRemove}>Remove</Button>
142
+ )}
143
+ </div>
144
+ {modal}
145
+ </>
146
+ );
147
+ }
@@ -0,0 +1,204 @@
1
+ import React from 'react';
2
+
3
+ const SIZES = {
4
+ xs: {
5
+ height: 26,
6
+ padding: '0 8px',
7
+ fontSize: '.74rem',
8
+ iconSize: 13,
9
+ dotSize: '.7rem',
10
+ sepHeight: 12,
11
+ gap: 6,
12
+ borderRadius: 'var(--radxs, 6px)',
13
+ },
14
+ sm: {
15
+ height: 32,
16
+ padding: '0 12px',
17
+ fontSize: '.82rem',
18
+ iconSize: 15,
19
+ dotSize: '.75rem',
20
+ sepHeight: 14,
21
+ gap: 8,
22
+ borderRadius: 'var(--rads, 8px)',
23
+ },
24
+ md: {
25
+ height: 36,
26
+ padding: '0 14px',
27
+ fontSize: '.88rem',
28
+ iconSize: 16,
29
+ dotSize: '.8rem',
30
+ sepHeight: 16,
31
+ gap: 10,
32
+ borderRadius: 'var(--rad, 10px)',
33
+ },
34
+ lg: {
35
+ height: 40,
36
+ padding: '0 16px',
37
+ fontSize: '.95rem',
38
+ iconSize: 18,
39
+ dotSize: '.85rem',
40
+ sepHeight: 18,
41
+ gap: 12,
42
+ borderRadius: 'var(--rad, 10px)',
43
+ },
44
+ };
45
+
46
+ const VARIANTS = {
47
+ default: {
48
+ background: 'var(--surface, #ffffff)',
49
+ border: '1px solid var(--border, #d7d8e0)',
50
+ boxShadow: 'var(--shadow, 0 1px 2px rgba(0, 0, 0, 0.05))',
51
+ },
52
+ surface: {
53
+ background: 'var(--surface, #ffffff)',
54
+ border: '1px solid var(--border, #d7d8e0)',
55
+ boxShadow: 'var(--shadow, 0 1px 2px rgba(0, 0, 0, 0.05))',
56
+ },
57
+ flat: {
58
+ background: 'var(--grey-1, #f5f5f8)',
59
+ border: '1px solid var(--border, #d7d8e0)',
60
+ boxShadow: 'none',
61
+ },
62
+ ghost: {
63
+ background: 'transparent',
64
+ border: '1px solid var(--border, #d7d8e0)',
65
+ boxShadow: 'none',
66
+ },
67
+ };
68
+
69
+ export default function InfoBadge({
70
+ items,
71
+ icon,
72
+ label,
73
+ value,
74
+ color,
75
+ title,
76
+ dot = true,
77
+ size = 'sm',
78
+ variant = 'default',
79
+ style,
80
+ className = '',
81
+ children,
82
+ onClick,
83
+ ...props
84
+ }) {
85
+ const sz = SIZES[size] || SIZES.sm;
86
+ const vr = VARIANTS[variant] || VARIANTS.default;
87
+
88
+ const containerStyle = {
89
+ display: 'inline-flex',
90
+ alignItems: 'center',
91
+ boxSizing: 'border-box',
92
+ height: sz.height,
93
+ padding: sz.padding,
94
+ borderRadius: sz.borderRadius,
95
+ background: vr.background,
96
+ border: vr.border,
97
+ boxShadow: vr.boxShadow,
98
+ gap: sz.gap,
99
+ cursor: onClick ? 'pointer' : 'default',
100
+ userSelect: 'none',
101
+ ...style,
102
+ };
103
+
104
+ if (children) {
105
+ return (
106
+ <div style={containerStyle} className={className} onClick={onClick} title={title} {...props}>
107
+ {children}
108
+ </div>
109
+ );
110
+ }
111
+
112
+ const resolvedItems = Array.isArray(items) && items.length > 0
113
+ ? items
114
+ : (icon != null || value != null || label != null)
115
+ ? [{ icon, label, value, color, title, onClick }]
116
+ : [];
117
+
118
+ return (
119
+ <div style={containerStyle} className={className} onClick={onClick} title={title} {...props}>
120
+ {resolvedItems.map((it, idx) => {
121
+ const itemColor = it.color || 'inherit';
122
+ const showDot = it.dot !== undefined ? it.dot : dot;
123
+
124
+ return (
125
+ <React.Fragment key={it.id || it.key || idx}>
126
+ {idx > 0 && (
127
+ <div
128
+ style={{
129
+ width: 1,
130
+ height: sz.sepHeight,
131
+ background: 'var(--border, #d7d8e0)',
132
+ flexShrink: 0,
133
+ }}
134
+ />
135
+ )}
136
+ <div
137
+ style={{
138
+ display: 'inline-flex',
139
+ alignItems: 'center',
140
+ gap: 5,
141
+ cursor: it.onClick ? 'pointer' : 'inherit',
142
+ }}
143
+ title={it.title}
144
+ onClick={it.onClick}
145
+ >
146
+ {it.icon && (
147
+ <span
148
+ style={{
149
+ display: 'inline-flex',
150
+ alignItems: 'center',
151
+ justifyContent: 'center',
152
+ color: itemColor,
153
+ flexShrink: 0,
154
+ }}
155
+ >
156
+ {typeof it.icon === 'function'
157
+ ? React.createElement(it.icon, { size: sz.iconSize })
158
+ : React.isValidElement(it.icon)
159
+ ? React.cloneElement(it.icon, { size: it.icon.props?.size || sz.iconSize })
160
+ : it.icon}
161
+ </span>
162
+ )}
163
+ {it.label && (
164
+ <span
165
+ style={{
166
+ fontSize: sz.fontSize,
167
+ fontWeight: 500,
168
+ color: itemColor !== 'inherit' ? itemColor : 'var(--primary-text, #0f111c)',
169
+ }}
170
+ >
171
+ {it.label}
172
+ </span>
173
+ )}
174
+ {showDot && (it.icon || it.label) && it.value !== undefined && (
175
+ <span
176
+ style={{
177
+ fontSize: sz.dotSize,
178
+ color: 'var(--muted, #9b9daa)',
179
+ lineHeight: 1,
180
+ }}
181
+ >
182
+ •
183
+ </span>
184
+ )}
185
+ {it.value !== undefined && (
186
+ <span
187
+ style={{
188
+ fontSize: sz.fontSize,
189
+ fontWeight: 500,
190
+ color: 'var(--primary-text, #0f111c)',
191
+ fontVariantNumeric: 'tabular-nums',
192
+ lineHeight: 1,
193
+ }}
194
+ >
195
+ {it.value}
196
+ </span>
197
+ )}
198
+ </div>
199
+ </React.Fragment>
200
+ );
201
+ })}
202
+ </div>
203
+ );
204
+ }
@@ -0,0 +1,229 @@
1
+ import { useRef, useState } from 'react';
2
+
3
+ const SIZES = {
4
+ xs: { height: 24, fontSize: 11, paddingH: 6 },
5
+ sm: { height: 28, fontSize: 11.5, paddingH: 8 },
6
+ md: { height: 32, fontSize: 13, paddingH: 11 },
7
+ lg: { height: 36, 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
+ multiline = false,
21
+ onClear,
22
+ className = '',
23
+ style = {},
24
+ value,
25
+ ...props
26
+ }) {
27
+ const inputRef = useRef(null);
28
+ const [focused, setFocused] = useState(false);
29
+ const [hovered, setHovered] = useState(false);
30
+ const s = SIZES[size] || SIZES.md;
31
+
32
+ function handleClear() {
33
+ if (props.onChange) props.onChange({ target: { value: '' } });
34
+ if (onClear) onClear();
35
+ inputRef.current?.focus();
36
+ }
37
+
38
+ const hasIconLeft = !!icon;
39
+ const hasIconRight = !!iconRight;
40
+ const showClear = clearable && !!value && !props.disabled && !multiline;
41
+ const hasSuffix = !!suffix && !multiline;
42
+ const showRightSlot = showClear || hasSuffix;
43
+
44
+ return (
45
+ <div style={{
46
+ display: 'flex',
47
+ flexDirection: 'column',
48
+ gap: 5,
49
+ }}>
50
+ {label && (
51
+ <label style={{
52
+ fontSize: size === 'xs' ? 9 : 11,
53
+ fontWeight: 600,
54
+ color: 'var(--t2,#797b8d)',
55
+ textTransform: 'uppercase',
56
+ letterSpacing: '.08em',
57
+ }}>
58
+ {label}
59
+ {required && <span style={{ color: 'var(--danger,#f64747)', marginLeft: 2 }}>*</span>}
60
+ </label>
61
+ )}
62
+
63
+ <div style={{
64
+ position: 'relative',
65
+ display: 'flex',
66
+ alignItems: 'center',
67
+ }}>
68
+ {hasIconLeft && (
69
+ <span style={{
70
+ position: 'absolute',
71
+ left: 10,
72
+ top: '50%',
73
+ transform: 'translateY(-50%)',
74
+ color: 'var(--t3,#9b9daa)',
75
+ pointerEvents: 'none',
76
+ lineHeight: 0,
77
+ display: 'flex',
78
+ alignItems: 'center',
79
+ }}>
80
+ {icon}
81
+ </span>
82
+ )}
83
+
84
+ {hasIconRight && (
85
+ <span style={{
86
+ position: 'absolute',
87
+ right: 10,
88
+ top: '50%',
89
+ transform: 'translateY(-50%)',
90
+ color: 'var(--t3,#9b9daa)',
91
+ pointerEvents: 'none',
92
+ display: 'flex',
93
+ alignItems: 'center',
94
+ }}>
95
+ {iconRight}
96
+ </span>
97
+ )}
98
+
99
+ {multiline ? (
100
+ <textarea
101
+ ref={inputRef}
102
+ className={className}
103
+ style={{
104
+ display: 'block',
105
+ width: '100%',
106
+ minHeight: 70,
107
+ resize: 'vertical',
108
+ whiteSpace: 'pre-wrap',
109
+ overflowWrap: 'break-word',
110
+ background: props.disabled ? 'var(--grey-1,#f5f5f8)' : 'var(--input-bg,#fff)',
111
+ border: `1px solid ${error ? 'var(--danger,#f64747)' : focused ? 'var(--accent,#f99e2c)' : hovered ? 'var(--accent,#f99e2c)' : 'var(--bd,#d7d8e0)'}`,
112
+ borderRadius: 'var(--rads,8px)',
113
+ color: 'var(--t1,#1a1a2e)',
114
+ fontFamily: 'var(--f,var(--sans,inherit))',
115
+ fontSize: s.fontSize,
116
+ padding: 0,
117
+ paddingTop: 8,
118
+ paddingBottom: 8,
119
+ paddingLeft: hasIconLeft ? 34 : 12,
120
+ paddingRight: (hasIconRight || showRightSlot) ? 34 : 12,
121
+ outline: 'none',
122
+ transition: 'border-color .14s, box-shadow .14s',
123
+ boxShadow: error
124
+ ? 'var(--dd, rgba(246,71,71,.12)), var(--shadow,0 1px 3px rgba(20,22,41,.1))'
125
+ : focused
126
+ ? 'var(--pd, rgba(249,158,44,.12)), var(--shadow,0 1px 3px rgba(20,22,41,.1))'
127
+ : 'var(--shadow,0 1px 3px rgba(20,22,41,.1))',
128
+ opacity: props.disabled ? 0.5 : 1,
129
+ cursor: props.disabled ? 'not-allowed' : undefined,
130
+ ...style,
131
+ }}
132
+ value={value}
133
+ {...props}
134
+ onFocus={e => { setFocused(true); props.onFocus?.(e); }}
135
+ onBlur={e => { setFocused(false); setHovered(false); props.onBlur?.(e); }}
136
+ />
137
+ ) : (
138
+ <input
139
+ ref={inputRef}
140
+ className={className}
141
+ style={{
142
+ display: 'block',
143
+ width: '100%',
144
+ background: props.disabled ? 'var(--grey-1,#f5f5f8)' : 'var(--input-bg,#fff)',
145
+ border: `1px solid ${error ? 'var(--danger,#f64747)' : focused ? 'var(--accent,#f99e2c)' : hovered ? 'var(--accent,#f99e2c)' : 'var(--bd,#d7d8e0)'}`,
146
+ borderRadius: 'var(--rads,8px)',
147
+ color: 'var(--t1,#1a1a2e)',
148
+ fontFamily: 'var(--f,var(--sans,inherit))',
149
+ fontSize: s.fontSize,
150
+ height: s.height,
151
+ padding: `0 ${s.paddingH}px`,
152
+ paddingLeft: hasIconLeft ? 34 : s.paddingH,
153
+ paddingRight: (hasIconRight || showRightSlot) ? 34 : s.paddingH,
154
+ outline: 'none',
155
+ transition: 'border-color .14s, box-shadow .14s',
156
+ boxShadow: error
157
+ ? 'var(--dd, rgba(246,71,71,.12)), var(--shadow,0 1px 3px rgba(20,22,41,.1))'
158
+ : focused
159
+ ? 'var(--pd, rgba(249,158,44,.12)), var(--shadow,0 1px 3px rgba(20,22,41,.1))'
160
+ : 'var(--shadow,0 1px 3px rgba(20,22,41,.1))',
161
+ opacity: props.disabled ? 0.5 : 1,
162
+ cursor: props.disabled ? 'not-allowed' : undefined,
163
+ ...style,
164
+ }}
165
+ value={value}
166
+ {...props}
167
+ onFocus={e => { setFocused(true); props.onFocus?.(e); }}
168
+ onBlur={e => { setFocused(false); setHovered(false); props.onBlur?.(e); }}
169
+ // onMouseEnter={() => setHovered(true)}
170
+ // onMouseLeave={() => setHovered(false)}
171
+ />
172
+ )}
173
+
174
+ {showClear && (
175
+ <button
176
+ type="button"
177
+ tabIndex={-1}
178
+ onClick={handleClear}
179
+ style={{
180
+ position: 'absolute',
181
+ right: 8,
182
+ top: '50%',
183
+ transform: 'translateY(-50%)',
184
+ display: 'flex',
185
+ alignItems: 'center',
186
+ justifyContent: 'center',
187
+ width: 18,
188
+ height: 18,
189
+ borderRadius: '50%',
190
+ background: 'transparent',
191
+ border: 'none',
192
+ cursor: 'pointer',
193
+ color: 'var(--t3,#9b9daa)',
194
+ padding: 0,
195
+ lineHeight: 0,
196
+ }}
197
+ >
198
+ <svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round">
199
+ <line x1="1" y1="1" x2="9" y2="9" />
200
+ <line x1="9" y1="1" x2="1" y2="9" />
201
+ </svg>
202
+ </button>
203
+ )}
204
+
205
+ {hasSuffix && !showClear && (
206
+ <span style={{
207
+ position: 'absolute',
208
+ right: 8,
209
+ top: '50%',
210
+ transform: 'translateY(-50%)',
211
+ display: 'flex',
212
+ alignItems: 'center',
213
+ gap: 4,
214
+ pointerEvents: 'none',
215
+ }}>
216
+ {suffix}
217
+ </span>
218
+ )}
219
+ </div>
220
+
221
+ {hint && !error && (
222
+ <span style={{ fontSize: 11, color: 'var(--t3,#9b9daa)' }}>{hint}</span>
223
+ )}
224
+ {error && (
225
+ <span style={{ fontSize: 11, color: 'var(--danger,#f64747)' }}>{error}</span>
226
+ )}
227
+ </div>
228
+ );
229
+ }