cynx-ui 1.3.6 → 1.3.8
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/components/FilePicker.jsx +205 -205
- package/components/LineProgress.jsx +133 -133
- package/components/LinearProgress.jsx +132 -132
- package/components/Select.jsx +50 -32
- package/components/TabBar.jsx +308 -74
- package/index.js +99 -99
- package/package.json +1 -1
package/components/Select.jsx
CHANGED
|
@@ -24,7 +24,9 @@ export default function Select({
|
|
|
24
24
|
searchable = false,
|
|
25
25
|
clearable = false,
|
|
26
26
|
onClear,
|
|
27
|
+
size = 'md',
|
|
27
28
|
style = {},
|
|
29
|
+
rounded = false,
|
|
28
30
|
}) {
|
|
29
31
|
const [open, setOpen] = useState(false);
|
|
30
32
|
const [query, setQuery] = useState('');
|
|
@@ -116,14 +118,26 @@ export default function Select({
|
|
|
116
118
|
|
|
117
119
|
const selectedValues = multi ? (Array.isArray(value) ? value : []) : (value !== undefined && value !== null ? [value] : []);
|
|
118
120
|
|
|
121
|
+
const heightVal = size === 'xs' ? 24 : size === 'sm' ? 28 : 32;
|
|
122
|
+
const paddingVal = size === 'xs' ? '0 6px' : size === 'sm' ? '0 8px' : '0 10px';
|
|
123
|
+
const fontSizeVal = size === 'xs' ? '11px' : size === 'sm' ? '11.5px' : '13px';
|
|
124
|
+
|
|
125
|
+
let radiusVal = rounded
|
|
126
|
+
? (typeof rounded === 'string'
|
|
127
|
+
? (rounded.endsWith('px') || rounded.endsWith('rem') || rounded.startsWith('var') ? rounded : `${rounded}px`)
|
|
128
|
+
: typeof rounded === 'number'
|
|
129
|
+
? `${rounded}px`
|
|
130
|
+
: '6px')
|
|
131
|
+
: (size === 'xs' ? '4px' : size === 'sm' ? '6px' : 'var(--rmd,var(--rads))');
|
|
132
|
+
|
|
119
133
|
return (
|
|
120
134
|
<div
|
|
121
135
|
ref={wrapRef}
|
|
122
136
|
style={{
|
|
123
137
|
position: 'relative',
|
|
124
138
|
width: '100%',
|
|
139
|
+
boxSizing: 'border-box',
|
|
125
140
|
fontFamily: 'var(--f,var(--sans))',
|
|
126
|
-
fontSize: 'var(--fsz,13px)',
|
|
127
141
|
opacity: disabled ? 0.5 : 1,
|
|
128
142
|
pointerEvents: disabled ? 'none' : undefined,
|
|
129
143
|
...style,
|
|
@@ -133,19 +147,20 @@ export default function Select({
|
|
|
133
147
|
onClick={handleOpen}
|
|
134
148
|
style={{
|
|
135
149
|
width: '100%',
|
|
150
|
+
boxSizing: 'border-box',
|
|
136
151
|
display: 'flex',
|
|
137
152
|
alignItems: 'center',
|
|
138
153
|
justifyContent: 'space-between',
|
|
139
|
-
gap:
|
|
140
|
-
padding:
|
|
141
|
-
height:
|
|
154
|
+
gap: 4,
|
|
155
|
+
padding: paddingVal,
|
|
156
|
+
height: heightVal,
|
|
142
157
|
background: 'var(--sf,var(--surface))',
|
|
143
|
-
border: `
|
|
144
|
-
borderRadius:
|
|
158
|
+
border: `1px solid ${open ? 'var(--accent)' : 'var(--bd2,var(--border))'}`,
|
|
159
|
+
borderRadius: radiusVal,
|
|
145
160
|
cursor: 'pointer',
|
|
146
161
|
fontFamily: 'var(--f,var(--sans,inherit))',
|
|
147
|
-
fontSize:
|
|
148
|
-
lineHeight: 1,
|
|
162
|
+
fontSize: fontSizeVal,
|
|
163
|
+
lineHeight: 1.2,
|
|
149
164
|
color: 'var(--t,var(--primary-text))',
|
|
150
165
|
outline: 'none',
|
|
151
166
|
transition: 'border-color var(--tr,.15s), box-shadow var(--tr,.15s)',
|
|
@@ -182,7 +197,7 @@ export default function Select({
|
|
|
182
197
|
</div>
|
|
183
198
|
) : !multi && selectedValues.length > 0 ? (
|
|
184
199
|
<span style={{ fontWeight: 400, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
|
185
|
-
{getLabel(selectedValues[0])}
|
|
200
|
+
{getLabel(selectedValues[0]) || '\u00A0'}
|
|
186
201
|
</span>
|
|
187
202
|
) : (
|
|
188
203
|
<span style={{ color: 'var(--t3,var(--grey-3))', fontWeight: 400 }}>{placeholder}</span>
|
|
@@ -209,11 +224,11 @@ export default function Select({
|
|
|
209
224
|
onMouseEnter={(e) => { e.currentTarget.style.color = 'var(--danger, #ef4444)'; }}
|
|
210
225
|
onMouseLeave={(e) => { e.currentTarget.style.color = 'var(--muted, #64748b)'; }}
|
|
211
226
|
>
|
|
212
|
-
<X size={
|
|
227
|
+
<X size={12} />
|
|
213
228
|
</span>
|
|
214
229
|
) : (
|
|
215
230
|
<ChevronDown
|
|
216
|
-
size={
|
|
231
|
+
size={12}
|
|
217
232
|
style={{
|
|
218
233
|
flexShrink: 0,
|
|
219
234
|
color: 'var(--t3,var(--grey-3))',
|
|
@@ -233,33 +248,35 @@ export default function Select({
|
|
|
233
248
|
top: pos.top,
|
|
234
249
|
left: pos.left,
|
|
235
250
|
width: pos.width,
|
|
251
|
+
boxSizing: 'border-box',
|
|
236
252
|
background: 'var(--sf,var(--surface))',
|
|
237
253
|
border: '1px solid var(--bd,var(--border))',
|
|
238
|
-
borderRadius:
|
|
254
|
+
borderRadius: radiusVal,
|
|
239
255
|
boxShadow: 'var(--shadow,0 1px 3px rgba(20,22,41,.1))',
|
|
240
|
-
padding:
|
|
256
|
+
padding: 3,
|
|
241
257
|
animation: 'selFadeIn .15s ease',
|
|
242
258
|
overflow: 'hidden',
|
|
243
259
|
}}
|
|
244
260
|
>
|
|
245
261
|
{searchable && (
|
|
246
|
-
<div style={{ padding:
|
|
262
|
+
<div style={{ padding: 3 }}>
|
|
247
263
|
<input
|
|
248
264
|
ref={inputRef}
|
|
249
265
|
type="text"
|
|
250
|
-
placeholder="
|
|
266
|
+
placeholder="Поиск..."
|
|
251
267
|
value={query}
|
|
252
268
|
onChange={e => setQuery(e.target.value)}
|
|
253
269
|
onMouseDown={e => e.stopPropagation()}
|
|
254
270
|
style={{
|
|
255
271
|
width: '100%',
|
|
256
|
-
|
|
257
|
-
|
|
272
|
+
boxSizing: 'border-box',
|
|
273
|
+
padding: size === 'xs' ? '2px 5px' : size === 'sm' ? '3px 6px' : '7px 10px',
|
|
274
|
+
height: size === 'xs' ? 22 : size === 'sm' ? 26 : 32,
|
|
258
275
|
background: 'var(--sf2,var(--grey-1))',
|
|
259
276
|
border: '1.5px solid var(--bd2,var(--border))',
|
|
260
|
-
borderRadius: 'var(--rmd,var(--rads))',
|
|
277
|
+
borderRadius: size === 'xs' ? '3px' : size === 'sm' ? '4px' : 'var(--rmd,var(--rads))',
|
|
261
278
|
fontFamily: 'var(--f,var(--sans))',
|
|
262
|
-
fontSize: '12.5px',
|
|
279
|
+
fontSize: size === 'xs' ? '10.5px' : size === 'sm' ? '11.5px' : '12.5px',
|
|
263
280
|
lineHeight: 1,
|
|
264
281
|
color: 'var(--t,var(--primary-text))',
|
|
265
282
|
outline: 'none',
|
|
@@ -270,8 +287,8 @@ export default function Select({
|
|
|
270
287
|
)}
|
|
271
288
|
<div style={{ maxHeight: 200, overflowY: 'auto' }}>
|
|
272
289
|
{filtered.length === 0 ? (
|
|
273
|
-
<div style={{ padding: '
|
|
274
|
-
|
|
290
|
+
<div style={{ padding: '10px 8px', textAlign: 'center', color: 'var(--t3,var(--grey-3))', fontSize: 11 }}>
|
|
291
|
+
Ничего не найдено
|
|
275
292
|
</div>
|
|
276
293
|
) : (
|
|
277
294
|
filtered.map(opt => (
|
|
@@ -281,13 +298,14 @@ export default function Select({
|
|
|
281
298
|
style={{
|
|
282
299
|
display: 'flex',
|
|
283
300
|
alignItems: 'center',
|
|
284
|
-
gap:
|
|
285
|
-
|
|
286
|
-
|
|
301
|
+
gap: 6,
|
|
302
|
+
minHeight: size === 'xs' ? 18 : size === 'sm' ? 22 : 28,
|
|
303
|
+
padding: size === 'xs' ? '3px 5px' : size === 'sm' ? '4.5px 7px' : '8px 10px',
|
|
304
|
+
borderRadius: size === 'xs' ? '3px' : size === 'sm' ? '4px' : 'var(--rmd,var(--rads))',
|
|
287
305
|
cursor: opt.disabled ? 'not-allowed' : 'pointer',
|
|
288
306
|
color: isSelected(opt.value) ? 'var(--accent)' : 'var(--t,var(--primary-text))',
|
|
289
307
|
background: isSelected(opt.value) ? 'var(--pd,var(--accent-bg))' : undefined,
|
|
290
|
-
fontSize: '12.5px',
|
|
308
|
+
fontSize: size === 'xs' ? '10.5px' : size === 'sm' ? '11.5px' : '12.5px',
|
|
291
309
|
fontWeight: 400,
|
|
292
310
|
transition: 'background .1s',
|
|
293
311
|
userSelect: 'none',
|
|
@@ -297,8 +315,8 @@ export default function Select({
|
|
|
297
315
|
>
|
|
298
316
|
{multi && (
|
|
299
317
|
<span style={{
|
|
300
|
-
width:
|
|
301
|
-
height:
|
|
318
|
+
width: 13,
|
|
319
|
+
height: 13,
|
|
302
320
|
border: `1.5px solid ${isSelected(opt.value) ? 'var(--accent)' : 'var(--bd2,var(--border))'}`,
|
|
303
321
|
borderRadius: 3,
|
|
304
322
|
flexShrink: 0,
|
|
@@ -311,8 +329,8 @@ export default function Select({
|
|
|
311
329
|
position: 'absolute',
|
|
312
330
|
top: 1,
|
|
313
331
|
left: 3.5,
|
|
314
|
-
width: 4
|
|
315
|
-
height:
|
|
332
|
+
width: 4,
|
|
333
|
+
height: 6,
|
|
316
334
|
border: 'solid var(--def-btn-text,#fff)',
|
|
317
335
|
borderWidth: '0 1.5px 1.5px 0',
|
|
318
336
|
transform: 'rotate(45deg)',
|
|
@@ -320,11 +338,11 @@ export default function Select({
|
|
|
320
338
|
)}
|
|
321
339
|
</span>
|
|
322
340
|
)}
|
|
323
|
-
<span style={{ flex: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
|
324
|
-
{opt.displayLabel || opt.label}
|
|
341
|
+
<span style={{ flex: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', minHeight: '1em' }}>
|
|
342
|
+
{opt.displayLabel || opt.label || '\u00A0'}
|
|
325
343
|
</span>
|
|
326
344
|
{!multi && isSelected(opt.value) && (
|
|
327
|
-
<Check size={
|
|
345
|
+
<Check size={12} style={{ flexShrink: 0, color: 'var(--accent)' }} />
|
|
328
346
|
)}
|
|
329
347
|
</div>
|
|
330
348
|
))
|
package/components/TabBar.jsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useRef, useEffect } from 'react';
|
|
1
|
+
import { useState, useRef, useEffect } from 'react';
|
|
2
2
|
|
|
3
3
|
const STYLES = `
|
|
4
4
|
@keyframes cx-tabSlide {
|
|
@@ -19,10 +19,64 @@ function ensureStyles() {
|
|
|
19
19
|
|
|
20
20
|
if (typeof document !== 'undefined') ensureStyles();
|
|
21
21
|
|
|
22
|
+
function ChevronIcon({ isOpen }) {
|
|
23
|
+
return (
|
|
24
|
+
<svg
|
|
25
|
+
width="12"
|
|
26
|
+
height="12"
|
|
27
|
+
viewBox="0 0 24 24"
|
|
28
|
+
fill="none"
|
|
29
|
+
stroke="currentColor"
|
|
30
|
+
strokeWidth="2.2"
|
|
31
|
+
strokeLinecap="round"
|
|
32
|
+
strokeLinejoin="round"
|
|
33
|
+
style={{
|
|
34
|
+
transition: 'transform 0.18s cubic-bezier(0.4, 0, 0.2, 1)',
|
|
35
|
+
transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)',
|
|
36
|
+
opacity: 0.75,
|
|
37
|
+
marginLeft: 2,
|
|
38
|
+
flexShrink: 0,
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
<polyline points="6 9 12 15 18 9" />
|
|
42
|
+
</svg>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function CheckIcon() {
|
|
47
|
+
return (
|
|
48
|
+
<svg
|
|
49
|
+
width="13"
|
|
50
|
+
height="13"
|
|
51
|
+
viewBox="0 0 24 24"
|
|
52
|
+
fill="none"
|
|
53
|
+
stroke="var(--accent, #3b82f6)"
|
|
54
|
+
strokeWidth="2.5"
|
|
55
|
+
strokeLinecap="round"
|
|
56
|
+
strokeLinejoin="round"
|
|
57
|
+
style={{ flexShrink: 0 }}
|
|
58
|
+
>
|
|
59
|
+
<polyline points="20 6 9 17 4 12" />
|
|
60
|
+
</svg>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
22
64
|
export default function TabBar({ tabs, active, onChange, capitalize = false, padding, variant = 'underline' }) {
|
|
23
65
|
const barRef = useRef(null);
|
|
24
66
|
const indRef = useRef(null);
|
|
25
67
|
const initDone = useRef(false);
|
|
68
|
+
const [openDropdownId, setOpenDropdownId] = useState(null);
|
|
69
|
+
|
|
70
|
+
// Click outside to close dropdowns
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
const handleClickOutside = (e) => {
|
|
73
|
+
if (barRef.current && !barRef.current.contains(e.target)) {
|
|
74
|
+
setOpenDropdownId(null);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
78
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
79
|
+
}, []);
|
|
26
80
|
|
|
27
81
|
// Underline indicator positioning
|
|
28
82
|
useEffect(() => {
|
|
@@ -31,7 +85,7 @@ export default function TabBar({ tabs, active, onChange, capitalize = false, pad
|
|
|
31
85
|
const ind = indRef.current;
|
|
32
86
|
if (!bar || !ind) return;
|
|
33
87
|
|
|
34
|
-
const activeBtn = bar.querySelector(`[data-tabid="${active}"]`);
|
|
88
|
+
const activeBtn = bar.querySelector(`[data-tabid="${active}"]`) || bar.querySelector(`[data-active="true"]`);
|
|
35
89
|
if (!activeBtn) return;
|
|
36
90
|
|
|
37
91
|
const barRect = bar.getBoundingClientRect();
|
|
@@ -53,49 +107,143 @@ export default function TabBar({ tabs, active, onChange, capitalize = false, pad
|
|
|
53
107
|
|
|
54
108
|
if (variant === 'pill') {
|
|
55
109
|
return (
|
|
56
|
-
<div
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
110
|
+
<div
|
|
111
|
+
ref={barRef}
|
|
112
|
+
style={{
|
|
113
|
+
display: 'inline-flex',
|
|
114
|
+
gap: 4,
|
|
115
|
+
background: 'var(--grey-1, #f5f5f8)',
|
|
116
|
+
borderRadius: 'var(--radxs, 6px)',
|
|
117
|
+
padding: '3px',
|
|
118
|
+
border: '1px solid var(--border-light, #ebebf0)',
|
|
119
|
+
position: 'relative',
|
|
120
|
+
alignItems: 'center',
|
|
121
|
+
flexWrap: 'wrap',
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
63
124
|
{tabs.map(t => {
|
|
64
|
-
const
|
|
125
|
+
const hasItems = Array.isArray(t.items) && t.items.length > 0;
|
|
126
|
+
const activeSubItem = hasItems ? t.items.find(item => item.id === active) : null;
|
|
127
|
+
const isActive = active === t.id || !!activeSubItem;
|
|
128
|
+
const isOpen = openDropdownId === t.id;
|
|
129
|
+
|
|
130
|
+
const handleTabClick = () => {
|
|
131
|
+
if (hasItems) {
|
|
132
|
+
setOpenDropdownId(isOpen ? null : t.id);
|
|
133
|
+
} else {
|
|
134
|
+
setOpenDropdownId(null);
|
|
135
|
+
onChange(t.id);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
65
139
|
return (
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
140
|
+
<div key={t.id} style={{ position: 'relative' }}>
|
|
141
|
+
<button
|
|
142
|
+
type="button"
|
|
143
|
+
data-tabid={t.id}
|
|
144
|
+
data-active={isActive}
|
|
145
|
+
onClick={handleTabClick}
|
|
146
|
+
style={{
|
|
147
|
+
display: 'flex', alignItems: 'center', gap: 5,
|
|
148
|
+
height: 26,
|
|
149
|
+
padding: padding || '0 10px',
|
|
150
|
+
border: 'none',
|
|
151
|
+
borderRadius: 'var(--radxs, 6px)',
|
|
152
|
+
background: isActive ? 'var(--surface,#fff)' : isOpen ? 'var(--grey-2, #e5e7eb)' : 'transparent',
|
|
153
|
+
boxShadow: isActive ? 'var(--shadow, 0 1px 3px rgba(0,0,0,.08))' : 'none',
|
|
154
|
+
fontSize: '.75rem', fontWeight: isActive ? 600 : 500,
|
|
155
|
+
color: isActive ? 'var(--primary-text, #0f111c)' : 'var(--muted, #9b9daa)',
|
|
156
|
+
cursor: 'pointer',
|
|
157
|
+
transition: 'all .18s ease',
|
|
158
|
+
fontFamily: 'inherit',
|
|
159
|
+
whiteSpace: 'nowrap',
|
|
160
|
+
textTransform: capitalize ? 'capitalize' : 'none',
|
|
161
|
+
}}
|
|
162
|
+
>
|
|
163
|
+
{t.icon}{t.label}
|
|
164
|
+
{activeSubItem && (
|
|
165
|
+
<span style={{
|
|
166
|
+
fontSize: '.68rem',
|
|
167
|
+
fontWeight: 600,
|
|
168
|
+
color: 'var(--accent, #3b82f6)',
|
|
169
|
+
background: 'var(--accent-bg, rgba(59,130,246,0.12))',
|
|
170
|
+
padding: '1px 6px',
|
|
171
|
+
borderRadius: 4,
|
|
172
|
+
marginLeft: 2,
|
|
173
|
+
}}>
|
|
174
|
+
{activeSubItem.label}
|
|
175
|
+
</span>
|
|
176
|
+
)}
|
|
177
|
+
{hasItems && <ChevronIcon isOpen={isOpen} />}
|
|
178
|
+
{t.count != null && t.count > 0 && (
|
|
179
|
+
<span style={{
|
|
180
|
+
background: isActive ? 'var(--accent-bg)' : 'var(--grey-2, #d7d8e0)',
|
|
181
|
+
color: isActive ? 'var(--accent)' : 'var(--muted)',
|
|
182
|
+
borderRadius: 10,
|
|
183
|
+
padding: '1px 6px',
|
|
184
|
+
fontSize: '.68rem',
|
|
185
|
+
fontWeight: 600,
|
|
186
|
+
}}>{t.count}</span>
|
|
187
|
+
)}
|
|
188
|
+
</button>
|
|
189
|
+
|
|
190
|
+
{/* Dropdown Menu for Pill Tab */}
|
|
191
|
+
{isOpen && hasItems && (
|
|
192
|
+
<div style={{
|
|
193
|
+
position: 'absolute',
|
|
194
|
+
top: 'calc(100% + 4px)',
|
|
195
|
+
left: 0,
|
|
196
|
+
zIndex: 9999,
|
|
197
|
+
minWidth: 185,
|
|
198
|
+
background: 'var(--surface, #fff)',
|
|
199
|
+
border: '1px solid var(--border-light, #ebebf0)',
|
|
200
|
+
borderRadius: 'var(--radxs, 6px)',
|
|
201
|
+
boxShadow: '0 8px 24px rgba(0,0,0,0.12)',
|
|
202
|
+
padding: '4px',
|
|
203
|
+
display: 'flex',
|
|
204
|
+
flexDirection: 'column',
|
|
205
|
+
gap: 2,
|
|
206
|
+
}}>
|
|
207
|
+
{t.items.map(subItem => {
|
|
208
|
+
const isSubSelected = active === subItem.id;
|
|
209
|
+
return (
|
|
210
|
+
<button
|
|
211
|
+
key={subItem.id}
|
|
212
|
+
type="button"
|
|
213
|
+
onClick={() => {
|
|
214
|
+
onChange(subItem.id);
|
|
215
|
+
setOpenDropdownId(null);
|
|
216
|
+
}}
|
|
217
|
+
style={{
|
|
218
|
+
display: 'flex',
|
|
219
|
+
alignItems: 'center',
|
|
220
|
+
justifyContent: 'space-between',
|
|
221
|
+
padding: '6px 10px',
|
|
222
|
+
fontSize: '.75rem',
|
|
223
|
+
fontWeight: isSubSelected ? 600 : 400,
|
|
224
|
+
color: isSubSelected ? 'var(--accent, #3b82f6)' : 'var(--primary-text, #0f111c)',
|
|
225
|
+
background: isSubSelected ? 'var(--accent-bg, rgba(59,130,246,0.08))' : 'transparent',
|
|
226
|
+
borderRadius: 4,
|
|
227
|
+
border: 'none',
|
|
228
|
+
cursor: 'pointer',
|
|
229
|
+
textAlign: 'left',
|
|
230
|
+
transition: 'background .15s',
|
|
231
|
+
}}
|
|
232
|
+
onMouseEnter={(e) => {
|
|
233
|
+
if (!isSubSelected) e.currentTarget.style.background = 'var(--grey-1, #f5f5f8)';
|
|
234
|
+
}}
|
|
235
|
+
onMouseLeave={(e) => {
|
|
236
|
+
if (!isSubSelected) e.currentTarget.style.background = 'transparent';
|
|
237
|
+
}}
|
|
238
|
+
>
|
|
239
|
+
<span>{subItem.label}</span>
|
|
240
|
+
{isSubSelected && <CheckIcon />}
|
|
241
|
+
</button>
|
|
242
|
+
);
|
|
243
|
+
})}
|
|
244
|
+
</div>
|
|
97
245
|
)}
|
|
98
|
-
</
|
|
246
|
+
</div>
|
|
99
247
|
);
|
|
100
248
|
})}
|
|
101
249
|
</div>
|
|
@@ -106,7 +254,7 @@ export default function TabBar({ tabs, active, onChange, capitalize = false, pad
|
|
|
106
254
|
const defaultPadding = padding || '10px 16px';
|
|
107
255
|
|
|
108
256
|
return (
|
|
109
|
-
<div ref={barRef} style={{ display:'flex', position:'relative' }}>
|
|
257
|
+
<div ref={barRef} style={{ display:'flex', position:'relative', alignItems: 'center' }}>
|
|
110
258
|
<div ref={indRef} style={{
|
|
111
259
|
position:'absolute', bottom:'-1px', left:0,
|
|
112
260
|
height:2, width:0,
|
|
@@ -117,39 +265,125 @@ export default function TabBar({ tabs, active, onChange, capitalize = false, pad
|
|
|
117
265
|
}} />
|
|
118
266
|
|
|
119
267
|
{tabs.map(t => {
|
|
120
|
-
const
|
|
268
|
+
const hasItems = Array.isArray(t.items) && t.items.length > 0;
|
|
269
|
+
const activeSubItem = hasItems ? t.items.find(item => item.id === active) : null;
|
|
270
|
+
const isActive = active === t.id || !!activeSubItem;
|
|
271
|
+
const isOpen = openDropdownId === t.id;
|
|
272
|
+
|
|
273
|
+
const handleTabClick = () => {
|
|
274
|
+
if (hasItems) {
|
|
275
|
+
setOpenDropdownId(isOpen ? null : t.id);
|
|
276
|
+
} else {
|
|
277
|
+
setOpenDropdownId(null);
|
|
278
|
+
onChange(t.id);
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
121
282
|
return (
|
|
122
|
-
<
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
283
|
+
<div key={t.id} style={{ position: 'relative' }}>
|
|
284
|
+
<button
|
|
285
|
+
type="button"
|
|
286
|
+
data-tabid={t.id}
|
|
287
|
+
data-active={isActive}
|
|
288
|
+
onClick={handleTabClick}
|
|
289
|
+
style={{
|
|
290
|
+
display:'flex', alignItems:'center', gap:6,
|
|
291
|
+
padding: defaultPadding,
|
|
292
|
+
border:'none', background:'none',
|
|
293
|
+
fontSize:'.82rem', fontWeight:500,
|
|
294
|
+
color: isActive ? 'var(--accent)' : 'var(--muted)',
|
|
295
|
+
cursor:'pointer',
|
|
296
|
+
transition:'color .18s',
|
|
297
|
+
marginBottom:'-1px',
|
|
298
|
+
position:'relative', zIndex:1,
|
|
299
|
+
fontFamily:'inherit',
|
|
300
|
+
whiteSpace:'nowrap',
|
|
301
|
+
textTransform: capitalize ? 'capitalize' : 'none',
|
|
302
|
+
}}
|
|
303
|
+
>
|
|
304
|
+
{t.icon}{t.label}
|
|
305
|
+
{activeSubItem && (
|
|
306
|
+
<span style={{
|
|
307
|
+
fontSize: '.68rem',
|
|
308
|
+
fontWeight: 600,
|
|
309
|
+
color: 'var(--accent, #3b82f6)',
|
|
310
|
+
background: 'var(--accent-bg, rgba(59,130,246,0.12))',
|
|
311
|
+
padding: '1px 6px',
|
|
312
|
+
borderRadius: 4,
|
|
313
|
+
}}>
|
|
314
|
+
{activeSubItem.label}
|
|
315
|
+
</span>
|
|
316
|
+
)}
|
|
317
|
+
{hasItems && <ChevronIcon isOpen={isOpen} />}
|
|
318
|
+
{t.count != null && t.count > 0 && (
|
|
319
|
+
<span style={{
|
|
320
|
+
background: isActive ? 'var(--accent-bg)' : 'var(--grey-1)',
|
|
321
|
+
color: isActive ? 'var(--accent)' : 'var(--muted)',
|
|
322
|
+
borderRadius: 10,
|
|
323
|
+
padding: '1px 6px',
|
|
324
|
+
fontSize: '.68rem',
|
|
325
|
+
transition: 'background .18s, color .18s',
|
|
326
|
+
}}>{t.count}</span>
|
|
327
|
+
)}
|
|
328
|
+
</button>
|
|
329
|
+
|
|
330
|
+
{/* Dropdown Menu for Underline Tab */}
|
|
331
|
+
{isOpen && hasItems && (
|
|
332
|
+
<div style={{
|
|
333
|
+
position: 'absolute',
|
|
334
|
+
top: 'calc(100% + 2px)',
|
|
335
|
+
left: 0,
|
|
336
|
+
zIndex: 9999,
|
|
337
|
+
minWidth: 190,
|
|
338
|
+
background: 'var(--surface, #fff)',
|
|
339
|
+
border: '1px solid var(--border-light, #ebebf0)',
|
|
340
|
+
borderRadius: 'var(--radxs, 6px)',
|
|
341
|
+
boxShadow: '0 6px 20px rgba(0,0,0,0.12)',
|
|
342
|
+
padding: '4px',
|
|
343
|
+
display: 'flex',
|
|
344
|
+
flexDirection: 'column',
|
|
345
|
+
gap: 2,
|
|
346
|
+
}}>
|
|
347
|
+
{t.items.map(subItem => {
|
|
348
|
+
const isSubSelected = active === subItem.id;
|
|
349
|
+
return (
|
|
350
|
+
<button
|
|
351
|
+
key={subItem.id}
|
|
352
|
+
type="button"
|
|
353
|
+
onClick={() => {
|
|
354
|
+
onChange(subItem.id);
|
|
355
|
+
setOpenDropdownId(null);
|
|
356
|
+
}}
|
|
357
|
+
style={{
|
|
358
|
+
display: 'flex',
|
|
359
|
+
alignItems: 'center',
|
|
360
|
+
justify: 'space-between',
|
|
361
|
+
padding: '6px 10px',
|
|
362
|
+
fontSize: '.78rem',
|
|
363
|
+
fontWeight: isSubSelected ? 600 : 400,
|
|
364
|
+
color: isSubSelected ? 'var(--accent, #3b82f6)' : 'var(--primary-text, #0f111c)',
|
|
365
|
+
background: isSubSelected ? 'var(--accent-bg, rgba(59,130,246,0.08))' : 'transparent',
|
|
366
|
+
borderRadius: 4,
|
|
367
|
+
border: 'none',
|
|
368
|
+
cursor: 'pointer',
|
|
369
|
+
textAlign: 'left',
|
|
370
|
+
transition: 'background .15s',
|
|
371
|
+
}}
|
|
372
|
+
onMouseEnter={(e) => {
|
|
373
|
+
if (!isSubSelected) e.currentTarget.style.background = 'var(--grey-1, #f5f5f8)';
|
|
374
|
+
}}
|
|
375
|
+
onMouseLeave={(e) => {
|
|
376
|
+
if (!isSubSelected) e.currentTarget.style.background = 'transparent';
|
|
377
|
+
}}
|
|
378
|
+
>
|
|
379
|
+
<span>{subItem.label}</span>
|
|
380
|
+
{isSubSelected && <CheckIcon />}
|
|
381
|
+
</button>
|
|
382
|
+
);
|
|
383
|
+
})}
|
|
384
|
+
</div>
|
|
151
385
|
)}
|
|
152
|
-
</
|
|
386
|
+
</div>
|
|
153
387
|
);
|
|
154
388
|
})}
|
|
155
389
|
</div>
|