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,88 @@
1
+ import { Users, Monitor, ShieldAlert, Activity, WifiOff } from 'lucide-react';
2
+
3
+ const itemStyle = {
4
+ display: 'inline-flex',
5
+ alignItems: 'center',
6
+ gap: 5,
7
+ padding: '4px 8px',
8
+ fontSize: '.8rem',
9
+ fontWeight: 600,
10
+ color: 'var(--t, #0f172a)',
11
+ };
12
+
13
+ const iconWrap = (color) => ({
14
+ display: 'inline-flex',
15
+ alignItems: 'center',
16
+ justifyContent: 'center',
17
+ width: 20,
18
+ height: 20,
19
+ borderRadius: 5,
20
+ background: color + '18',
21
+ color: color,
22
+ flexShrink: 0,
23
+ });
24
+
25
+ const sep = {
26
+ width: 1,
27
+ height: 14,
28
+ background: 'var(--border-light, #ebebf0)',
29
+ margin: '0 3px',
30
+ flexShrink: 0,
31
+ };
32
+
33
+ const groupStyle = {
34
+ display: 'inline-flex',
35
+ alignItems: 'center',
36
+ gap: 0,
37
+ padding: '3px 8px',
38
+ minHeight: 32,
39
+ borderRadius: 7,
40
+ border: '1px solid var(--border, #e2e8f0)',
41
+ background: 'var(--surface, #ffffff)',
42
+ boxShadow: 'var(--shadow, 0 1px 2px rgba(0,0,0,.05))',
43
+ };
44
+
45
+ export default function StatsBar({ total = 0, active = 0, blocked = 0, actions = 0, type }) {
46
+ const dotted = type === 'dotted';
47
+ const dot = (color) => dotted ? <span style={{ color, fontSize: '.8rem', fontWeight: 700 }}>•</span> : null;
48
+
49
+ return (
50
+ <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
51
+ {/* Device group */}
52
+ <div style={groupStyle}>
53
+ <div style={itemStyle}>
54
+ <span style={iconWrap('#6366f1')}><Monitor size={10} /></span>
55
+ {dot('#6366f1')}
56
+ <span>{total}</span>
57
+ </div>
58
+ <div style={sep} />
59
+ <div style={itemStyle}>
60
+ <span style={iconWrap('var(--s, #22c55e)')}><Users size={10} /></span>
61
+ {dot('var(--s, #22c55e)')}
62
+ <span>{active}</span>
63
+ </div>
64
+ </div>
65
+
66
+ {/* Status group */}
67
+ <div style={groupStyle}>
68
+ <div style={itemStyle}>
69
+ <span style={iconWrap('var(--d, #ef4444)')}><ShieldAlert size={10} /></span>
70
+ {dot('var(--d, #ef4444)')}
71
+ <span>{blocked}</span>
72
+ </div>
73
+ <div style={sep} />
74
+ <div style={itemStyle}>
75
+ <span style={iconWrap('var(--w, #f59e0b)')}><Activity size={10} /></span>
76
+ {dot('var(--w, #f59e0b)')}
77
+ <span>{actions}</span>
78
+ </div>
79
+ <div style={sep} />
80
+ <div style={itemStyle}>
81
+ <span style={iconWrap('var(--t3, #94a3b8)')}><WifiOff size={10} /></span>
82
+ {dot('var(--t3, #94a3b8)')}
83
+ <span>{total - active}</span>
84
+ </div>
85
+ </div>
86
+ </div>
87
+ );
88
+ }
@@ -0,0 +1,171 @@
1
+ import CircularProgress from './CircularProgress';
2
+
3
+ const BASE = {
4
+ display: 'flex',
5
+ alignItems: 'center',
6
+ justifyContent: 'space-between',
7
+ padding: '10px 14px',
8
+ minHeight: 68,
9
+ borderRadius: 'var(--rad,10px)',
10
+ background: 'var(--surface,#fff)',
11
+ border: '1px solid var(--border, #e2e8f0)',
12
+ boxShadow: 'var(--shadow, 0 1px 3px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04))',
13
+ minWidth: 0,
14
+ transition: 'border-color .15s',
15
+ };
16
+
17
+ const SIMPLE_BASE = {
18
+ ...BASE,
19
+ padding: '12px 14px',
20
+ flexDirection: 'column',
21
+ alignItems: 'flex-start',
22
+ justifyContent: 'flex-start',
23
+ };
24
+
25
+ export default function StatsCard({
26
+ variant = 'default',
27
+ icon,
28
+ iconColor = 'var(--blue, #2782e4)',
29
+ iconBg = 'var(--infd, rgba(39,130,228,.1))',
30
+ title,
31
+ value,
32
+ subtitle,
33
+ percent,
34
+ percentColor,
35
+ progressSize = 'sm',
36
+ progressTrackColor = 'var(--bd2, #e8e8ec)',
37
+ headRight,
38
+ className = '',
39
+ style = {},
40
+ children,
41
+ }) {
42
+ if (variant === 'simple') {
43
+ return (
44
+ <div className={className} style={{ ...SIMPLE_BASE, ...style }}>
45
+ <div style={{
46
+ width: '100%',
47
+ marginBottom: 8,
48
+ display: 'flex',
49
+ alignItems: 'center',
50
+ justifyContent: 'space-between',
51
+ }}>
52
+ {title && (
53
+ <div style={{
54
+ fontSize: '.58rem',
55
+ fontWeight: 600,
56
+ textTransform: 'uppercase',
57
+ letterSpacing: '.08em',
58
+ color: 'var(--muted,#9b9daa)',
59
+ }}>{title}</div>
60
+ )}
61
+ {icon && (
62
+ <div style={{
63
+ display: 'inline-flex',
64
+ alignItems: 'center',
65
+ justifyContent: 'center',
66
+ flexShrink: 0,
67
+ color: iconColor,
68
+ }}>
69
+ {icon}
70
+ </div>
71
+ )}
72
+ </div>
73
+ {value !== undefined && (
74
+ <div style={{
75
+ fontSize: '1.25rem',
76
+ fontWeight: 800,
77
+ letterSpacing: '-.03em',
78
+ lineHeight: 1.1,
79
+ minHeight: '1.375em',
80
+ }}>{value}</div>
81
+ )}
82
+ {children}
83
+ </div>
84
+ );
85
+ }
86
+
87
+ return (
88
+ <div className={className} style={{ ...BASE, ...style }}>
89
+ <div style={{
90
+ display: 'flex',
91
+ flexDirection: 'column',
92
+ minWidth: 0,
93
+ flex: 1,
94
+ }}>
95
+ <div style={{
96
+ display: 'flex',
97
+ alignItems: 'center',
98
+ justifyContent: 'space-between',
99
+ marginBottom: 8,
100
+ }}>
101
+ <div style={{
102
+ display: 'flex',
103
+ alignItems: 'center',
104
+ gap: 6,
105
+ }}>
106
+ {icon && (
107
+ <div style={{
108
+ width: 24,
109
+ height: 24,
110
+ borderRadius: 6,
111
+ display: 'inline-flex',
112
+ alignItems: 'center',
113
+ justifyContent: 'center',
114
+ flexShrink: 0,
115
+ background: iconBg,
116
+ color: iconColor,
117
+ }}>
118
+ {icon}
119
+ </div>
120
+ )}
121
+ {title && (
122
+ <div style={{
123
+ fontSize: '.58rem',
124
+ fontWeight: 600,
125
+ textTransform: 'uppercase',
126
+ letterSpacing: '.08em',
127
+ color: 'var(--muted,#9b9daa)',
128
+ }}>{title}</div>
129
+ )}
130
+ </div>
131
+ {headRight}
132
+ </div>
133
+ {value !== undefined && (
134
+ <div style={{
135
+ fontSize: '1.1rem',
136
+ fontWeight: 800,
137
+ letterSpacing: '-.02em',
138
+ color: 'var(--primary-text,#0f111c)',
139
+ lineHeight: 1.2,
140
+ minHeight: '1.32em',
141
+ }}>{value}</div>
142
+ )}
143
+ <div style={{
144
+ fontSize: '.62rem',
145
+ color: 'var(--muted,#9b9daa)',
146
+ fontWeight: 500,
147
+ marginTop: 1,
148
+ minHeight: 14,
149
+ }}>{subtitle || '\u00A0'}</div>
150
+ {children}
151
+ </div>
152
+ <div style={{
153
+ display: 'flex',
154
+ alignItems: 'center',
155
+ gap: 8,
156
+ flexShrink: 0,
157
+ }}>
158
+ {percent !== undefined && (
159
+ <CircularProgress
160
+ value={percent}
161
+ size={progressSize}
162
+ color={percentColor}
163
+ trackColor={progressTrackColor}
164
+ showPercent
165
+ rounded
166
+ />
167
+ )}
168
+ </div>
169
+ </div>
170
+ );
171
+ }
@@ -0,0 +1,404 @@
1
+ import { useState, useRef, useEffect } from 'react';
2
+
3
+ const STYLES = `
4
+ @keyframes cx-tabSlide {
5
+ from { opacity: 0; transform: scaleX(.8); }
6
+ to { opacity: 1; transform: scaleX(1); }
7
+ }
8
+ `;
9
+
10
+ let _id = 'cynx-tabbar-styles';
11
+ function ensureStyles() {
12
+ if (typeof document === 'undefined') return;
13
+ if (document.getElementById(_id)) return;
14
+ const tag = document.createElement('style');
15
+ tag.id = _id;
16
+ tag.textContent = STYLES;
17
+ document.head.appendChild(tag);
18
+ }
19
+
20
+ if (typeof document !== 'undefined') ensureStyles();
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
+
64
+ export default function TabBar({ tabs, active, onChange, capitalize = false, padding, variant = 'underline' }) {
65
+ const barRef = useRef(null);
66
+ const indRef = useRef(null);
67
+ const initDone = useRef(false);
68
+ const [openDropdownId, setOpenDropdownId] = useState(null);
69
+
70
+ // Click outside to close dropdowns
71
+ useEffect(() => {
72
+ if (!openDropdownId) return;
73
+ const handleClickOutside = (e) => {
74
+ if (barRef.current && !barRef.current.contains(e.target)) {
75
+ setOpenDropdownId(null);
76
+ }
77
+ };
78
+ window.addEventListener('mousedown', handleClickOutside, true);
79
+ window.addEventListener('pointerdown', handleClickOutside, true);
80
+ window.addEventListener('click', handleClickOutside, true);
81
+ return () => {
82
+ window.removeEventListener('mousedown', handleClickOutside, true);
83
+ window.removeEventListener('pointerdown', handleClickOutside, true);
84
+ window.removeEventListener('click', handleClickOutside, true);
85
+ };
86
+ }, [openDropdownId]);
87
+
88
+ // Underline indicator positioning
89
+ useEffect(() => {
90
+ if (variant !== 'underline') return;
91
+ const bar = barRef.current;
92
+ const ind = indRef.current;
93
+ if (!bar || !ind) return;
94
+
95
+ const activeBtn = bar.querySelector(`[data-tabid="${active}"]`) || bar.querySelector(`[data-active="true"]`);
96
+ if (!activeBtn) return;
97
+
98
+ const barRect = bar.getBoundingClientRect();
99
+ const btnRect = activeBtn.getBoundingClientRect();
100
+ const x = btnRect.left - barRect.left;
101
+ const w = btnRect.width;
102
+
103
+ if (!initDone.current) {
104
+ ind.style.transition = 'none';
105
+ ind.style.width = w + 'px';
106
+ ind.style.transform = `translateX(${x}px)`;
107
+ requestAnimationFrame(() => { ind.style.transition = ''; });
108
+ initDone.current = true;
109
+ } else {
110
+ ind.style.width = w + 'px';
111
+ ind.style.transform = `translateX(${x}px)`;
112
+ }
113
+ }, [active, tabs, variant]);
114
+
115
+ if (variant === 'pill') {
116
+ return (
117
+ <div
118
+ ref={barRef}
119
+ style={{
120
+ display: 'inline-flex',
121
+ gap: 4,
122
+ background: 'var(--grey-1, #f5f5f8)',
123
+ borderRadius: 'var(--radxs, 6px)',
124
+ padding: '3px',
125
+ border: '1px solid var(--border-light, #ebebf0)',
126
+ position: 'relative',
127
+ alignItems: 'center',
128
+ flexWrap: 'wrap',
129
+ }}
130
+ >
131
+ {tabs.map(t => {
132
+ const hasItems = Array.isArray(t.items) && t.items.length > 0;
133
+ const activeSubItem = hasItems ? t.items.find(item => String(item.id) === String(active)) : null;
134
+ const isActive = (active != null && t.id != null && String(active) === String(t.id)) || !!activeSubItem;
135
+ const isOpen = openDropdownId === t.id;
136
+
137
+ const handleTabClick = () => {
138
+ if (t.disabled) return;
139
+ if (hasItems) {
140
+ setOpenDropdownId(isOpen ? null : t.id);
141
+ } else {
142
+ setOpenDropdownId(null);
143
+ onChange(t.id);
144
+ }
145
+ };
146
+
147
+ return (
148
+ <div key={t.id} style={{ position: 'relative' }}>
149
+ <button
150
+ type="button"
151
+ data-tabid={t.id}
152
+ data-active={isActive}
153
+ disabled={t.disabled}
154
+ onClick={handleTabClick}
155
+ style={{
156
+ display: 'flex', alignItems: 'center', gap: 5,
157
+ height: 26,
158
+ padding: padding || '0 10px',
159
+ border: 'none',
160
+ borderRadius: 'var(--radxs, 6px)',
161
+ background: isActive ? 'var(--surface,#fff)' : isOpen ? 'var(--grey-2, #e5e7eb)' : 'transparent',
162
+ boxShadow: isActive ? '0 1px 3px rgba(0,0,0,.1), 0 1px 2px rgba(0,0,0,.06)' : 'none',
163
+ fontSize: '.75rem', fontWeight: isActive ? 600 : 500,
164
+ color: isActive ? 'var(--primary-text, #0f111c)' : 'var(--muted, #9b9daa)',
165
+ cursor: t.disabled ? 'not-allowed' : 'pointer',
166
+ opacity: t.disabled ? 0.38 : 1,
167
+ transition: 'all .18s ease',
168
+ fontFamily: 'inherit',
169
+ whiteSpace: 'nowrap',
170
+ textTransform: capitalize ? 'capitalize' : 'none',
171
+ }}
172
+ >
173
+ {t.icon}{t.label}
174
+ {activeSubItem && (
175
+ <span style={{
176
+ fontSize: '.68rem',
177
+ fontWeight: 600,
178
+ color: 'var(--accent, #3b82f6)',
179
+ background: 'var(--accent-bg, rgba(59,130,246,0.12))',
180
+ padding: '1px 6px',
181
+ borderRadius: 4,
182
+ marginLeft: 2,
183
+ }}>
184
+ {activeSubItem.label}
185
+ </span>
186
+ )}
187
+ {hasItems && <ChevronIcon isOpen={isOpen} />}
188
+ {t.count != null && t.count > 0 && (
189
+ <span style={{
190
+ background: isActive ? 'var(--accent-bg, rgba(249,158,44,.1))' : 'var(--grey-2, #d7d8e0)',
191
+ color: isActive ? 'var(--accent, #f99e2c)' : 'var(--muted, #797b8d)',
192
+ borderRadius: 10,
193
+ padding: '1px 6px',
194
+ fontSize: '.68rem',
195
+ fontWeight: 600,
196
+ }}>{t.count}</span>
197
+ )}
198
+ </button>
199
+
200
+ {/* Dropdown Menu for Pill Tab */}
201
+ {isOpen && hasItems && (
202
+ <div style={{
203
+ position: 'absolute',
204
+ top: 'calc(100% + 4px)',
205
+ left: 0,
206
+ zIndex: 9999,
207
+ minWidth: 185,
208
+ background: 'var(--surface, #fff)',
209
+ border: '1px solid var(--border-light, #ebebf0)',
210
+ borderRadius: 'var(--radxs, 6px)',
211
+ boxShadow: '0 8px 24px rgba(0,0,0,0.12)',
212
+ padding: '4px',
213
+ display: 'flex',
214
+ flexDirection: 'column',
215
+ gap: 2,
216
+ }}>
217
+ {t.items.map(subItem => {
218
+ const isSubSelected = active === subItem.id;
219
+ return (
220
+ <button
221
+ key={subItem.id}
222
+ type="button"
223
+ onClick={() => {
224
+ onChange(subItem.id);
225
+ setOpenDropdownId(null);
226
+ }}
227
+ style={{
228
+ display: 'flex',
229
+ alignItems: 'center',
230
+ justifyContent: 'space-between',
231
+ padding: '6px 10px',
232
+ fontSize: '.75rem',
233
+ fontWeight: isSubSelected ? 600 : 400,
234
+ color: isSubSelected ? 'var(--accent, #3b82f6)' : 'var(--primary-text, #0f111c)',
235
+ background: isSubSelected ? 'var(--accent-bg, rgba(59,130,246,0.08))' : 'transparent',
236
+ borderRadius: 4,
237
+ border: 'none',
238
+ cursor: 'pointer',
239
+ textAlign: 'left',
240
+ transition: 'background .15s',
241
+ }}
242
+ onMouseEnter={(e) => {
243
+ if (!isSubSelected) e.currentTarget.style.background = 'var(--grey-1, #f5f5f8)';
244
+ }}
245
+ onMouseLeave={(e) => {
246
+ if (!isSubSelected) e.currentTarget.style.background = 'transparent';
247
+ }}
248
+ >
249
+ <span>{subItem.label}</span>
250
+ {isSubSelected && <CheckIcon />}
251
+ </button>
252
+ );
253
+ })}
254
+ </div>
255
+ )}
256
+ </div>
257
+ );
258
+ })}
259
+ </div>
260
+ );
261
+ }
262
+
263
+ // Default: underline variant
264
+ const defaultPadding = padding || '10px 16px';
265
+
266
+ return (
267
+ <div ref={barRef} style={{ display:'flex', position:'relative', alignItems: 'center' }}>
268
+ <div ref={indRef} style={{
269
+ position:'absolute', bottom:'-1px', left:0,
270
+ height:2, width:0,
271
+ background:'var(--accent, #f99e2c)',
272
+ borderRadius:'2px 2px 0 0',
273
+ transition:'transform .22s cubic-bezier(.4,0,.2,1), width .22s cubic-bezier(.4,0,.2,1)',
274
+ pointerEvents:'none', zIndex:2,
275
+ }} />
276
+
277
+ {tabs.map(t => {
278
+ const hasItems = Array.isArray(t.items) && t.items.length > 0;
279
+ const activeSubItem = hasItems ? t.items.find(item => String(item.id) === String(active)) : null;
280
+ const isActive = (active != null && t.id != null && String(active) === String(t.id)) || !!activeSubItem;
281
+ const isOpen = openDropdownId === t.id;
282
+
283
+ const handleTabClick = () => {
284
+ if (t.disabled) return;
285
+ if (hasItems) {
286
+ setOpenDropdownId(isOpen ? null : t.id);
287
+ } else {
288
+ setOpenDropdownId(null);
289
+ onChange(t.id);
290
+ }
291
+ };
292
+
293
+ return (
294
+ <div key={t.id} style={{ position: 'relative' }}>
295
+ <button
296
+ type="button"
297
+ data-tabid={t.id}
298
+ data-active={isActive}
299
+ disabled={t.disabled}
300
+ onClick={handleTabClick}
301
+ style={{
302
+ display:'flex', alignItems:'center', gap:6,
303
+ padding: defaultPadding,
304
+ border:'none', background:'none',
305
+ fontSize:'.82rem', fontWeight:500,
306
+ color: isActive ? 'var(--accent, #f99e2c)' : 'var(--muted, #797b8d)',
307
+ cursor: t.disabled ? 'not-allowed' : 'pointer',
308
+ opacity: t.disabled ? 0.38 : 1,
309
+ transition:'color .18s',
310
+ marginBottom:'-1px',
311
+ position:'relative', zIndex:1,
312
+ fontFamily:'inherit',
313
+ whiteSpace:'nowrap',
314
+ textTransform: capitalize ? 'capitalize' : 'none',
315
+ }}
316
+ >
317
+ {t.icon}{t.label}
318
+ {activeSubItem && (
319
+ <span style={{
320
+ fontSize: '.68rem',
321
+ fontWeight: 600,
322
+ color: 'var(--accent, #3b82f6)',
323
+ background: 'var(--accent-bg, rgba(59,130,246,0.12))',
324
+ padding: '1px 6px',
325
+ borderRadius: 4,
326
+ }}>
327
+ {activeSubItem.label}
328
+ </span>
329
+ )}
330
+ {hasItems && <ChevronIcon isOpen={isOpen} />}
331
+ {t.count != null && t.count > 0 && (
332
+ <span style={{
333
+ background: isActive ? 'var(--accent-bg, rgba(249,158,44,.1))' : 'var(--grey-1, #f5f5f8)',
334
+ color: isActive ? 'var(--accent, #f99e2c)' : 'var(--muted, #797b8d)',
335
+ borderRadius: 10,
336
+ padding: '1px 6px',
337
+ fontSize: '.68rem',
338
+ transition: 'background .18s, color .18s',
339
+ }}>{t.count}</span>
340
+ )}
341
+ </button>
342
+
343
+ {/* Dropdown Menu for Underline Tab */}
344
+ {isOpen && hasItems && (
345
+ <div style={{
346
+ position: 'absolute',
347
+ top: 'calc(100% + 2px)',
348
+ left: 0,
349
+ zIndex: 9999,
350
+ minWidth: 190,
351
+ background: 'var(--surface, #fff)',
352
+ border: '1px solid var(--border-light, #ebebf0)',
353
+ borderRadius: 'var(--radxs, 6px)',
354
+ boxShadow: '0 6px 20px rgba(0,0,0,0.12)',
355
+ padding: '4px',
356
+ display: 'flex',
357
+ flexDirection: 'column',
358
+ gap: 2,
359
+ }}>
360
+ {t.items.map(subItem => {
361
+ const isSubSelected = active === subItem.id;
362
+ return (
363
+ <button
364
+ key={subItem.id}
365
+ type="button"
366
+ onClick={() => {
367
+ onChange(subItem.id);
368
+ setOpenDropdownId(null);
369
+ }}
370
+ style={{
371
+ display: 'flex',
372
+ alignItems: 'center',
373
+ justify: 'space-between',
374
+ padding: '6px 10px',
375
+ fontSize: '.78rem',
376
+ fontWeight: isSubSelected ? 600 : 400,
377
+ color: isSubSelected ? 'var(--accent, #3b82f6)' : 'var(--primary-text, #0f111c)',
378
+ background: isSubSelected ? 'var(--accent-bg, rgba(59,130,246,0.08))' : 'transparent',
379
+ borderRadius: 4,
380
+ border: 'none',
381
+ cursor: 'pointer',
382
+ textAlign: 'left',
383
+ transition: 'background .15s',
384
+ }}
385
+ onMouseEnter={(e) => {
386
+ if (!isSubSelected) e.currentTarget.style.background = 'var(--grey-1, #f5f5f8)';
387
+ }}
388
+ onMouseLeave={(e) => {
389
+ if (!isSubSelected) e.currentTarget.style.background = 'transparent';
390
+ }}
391
+ >
392
+ <span>{subItem.label}</span>
393
+ {isSubSelected && <CheckIcon />}
394
+ </button>
395
+ );
396
+ })}
397
+ </div>
398
+ )}
399
+ </div>
400
+ );
401
+ })}
402
+ </div>
403
+ );
404
+ }
@@ -0,0 +1,29 @@
1
+ export function Tabs({ tabs, value, onChange, style }) {
2
+ return (
3
+ <div style={{ display: 'inline-flex', gap: 3, background: 'var(--grey-1, #f5f5f8)', borderRadius: 'var(--radxs, 6px)', padding: '3px', border: '1px solid var(--border-light, #ebebf0)', ...style }}>
4
+ {tabs.map(t => {
5
+ const label = typeof t === 'string' ? t : t.label;
6
+ const key = typeof t === 'string' ? t : t.value;
7
+ const active = key === value;
8
+ return (
9
+ <button key={key} onClick={() => onChange(key)} style={{
10
+ display: 'flex', alignItems: 'center', gap: 5,
11
+ padding: '5px 12px',
12
+ border: 'none',
13
+ borderRadius: 'var(--radxs, 6px)',
14
+ background: active ? 'var(--surface,#fff)' : 'transparent',
15
+ boxShadow: active ? 'var(--shadow, 0 1px 2px rgba(0,0,0,.06))' : 'none',
16
+ fontSize: '.75rem', fontWeight: active ? 600 : 500,
17
+ color: active ? 'var(--primary-text, #0f111c)' : 'var(--muted, #9b9daa)',
18
+ cursor: 'pointer',
19
+ transition: 'all .18s ease',
20
+ fontFamily: 'inherit',
21
+ whiteSpace: 'nowrap',
22
+ }}>
23
+ {label}
24
+ </button>
25
+ );
26
+ })}
27
+ </div>
28
+ );
29
+ }