cynx-ui 1.2.13 → 1.2.14

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 (2) hide show
  1. package/components/TabBar.jsx +156 -92
  2. package/package.json +1 -1
@@ -1,92 +1,156 @@
1
- import { useRef, useEffect } from 'react';
2
-
3
- /**
4
- * TabBar — sliding underline indicator tabs
5
- * Props:
6
- * tabs: [{ id, label, icon?, count? }]
7
- * active: string (active tab id)
8
- * onChange: fn(id)
9
- * capitalize: bool — when true, tab labels are text-transform: capitalize
10
- * padding: string override button padding (default '10px 16px')
11
- */
12
- export default function TabBar({ tabs, active, onChange, capitalize = false, padding = '10px 16px' }) {
13
- const barRef = useRef(null);
14
- const indRef = useRef(null);
15
- const initDone = useRef(false);
16
-
17
- useEffect(() => {
18
- const bar = barRef.current;
19
- const ind = indRef.current;
20
- if (!bar || !ind) return;
21
-
22
- const activeBtn = bar.querySelector(`[data-tabid="${active}"]`);
23
- if (!activeBtn) return;
24
-
25
- const barRect = bar.getBoundingClientRect();
26
- const btnRect = activeBtn.getBoundingClientRect();
27
- const x = btnRect.left - barRect.left;
28
- const w = btnRect.width;
29
-
30
- if (!initDone.current) {
31
- ind.style.transition = 'none';
32
- ind.style.width = w + 'px';
33
- ind.style.transform = `translateX(${x}px)`;
34
- requestAnimationFrame(() => { ind.style.transition = ''; });
35
- initDone.current = true;
36
- } else {
37
- ind.style.width = w + 'px';
38
- ind.style.transform = `translateX(${x}px)`;
39
- }
40
- }, [active, tabs]);
41
-
42
- return (
43
- <div ref={barRef} style={{ display:'flex', position:'relative' }}>
44
- {/* sliding underline */}
45
- <div ref={indRef} style={{
46
- position:'absolute', bottom:'-1px', left:0,
47
- height:2, width:0,
48
- background:'var(--accent)',
49
- borderRadius:'2px 2px 0 0',
50
- transition:'transform .22s cubic-bezier(.4,0,.2,1), width .22s cubic-bezier(.4,0,.2,1)',
51
- pointerEvents:'none', zIndex:2,
52
- }} />
53
-
54
- {tabs.map(t => {
55
- const isActive = active === t.id;
56
- return (
57
- <button
58
- key={t.id}
59
- data-tabid={t.id}
60
- onClick={() => onChange(t.id)}
61
- style={{
62
- display:'flex', alignItems:'center', gap:6,
63
- padding,
64
- border:'none', background:'none',
65
- fontSize:'.82rem', fontWeight:500,
66
- color: isActive ? 'var(--accent)' : 'var(--muted)',
67
- cursor:'pointer',
68
- transition:'color .18s',
69
- marginBottom:'-1px',
70
- position:'relative', zIndex:1,
71
- fontFamily:'inherit',
72
- whiteSpace:'nowrap',
73
- textTransform: capitalize ? 'capitalize' : 'none',
74
- }}
75
- >
76
- {t.icon}{t.label}
77
- {t.count != null && t.count > 0 && (
78
- <span style={{
79
- background: isActive ? 'var(--accent-bg)' : 'var(--grey-1)',
80
- color: isActive ? 'var(--accent)' : 'var(--muted)',
81
- borderRadius: 10,
82
- padding: '1px 6px',
83
- fontSize: '.68rem',
84
- transition: 'background .18s, color .18s',
85
- }}>{t.count}</span>
86
- )}
87
- </button>
88
- );
89
- })}
90
- </div>
91
- );
92
- }
1
+ import { 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
+ export default function TabBar({ tabs, active, onChange, capitalize = false, padding, variant = 'underline' }) {
23
+ const barRef = useRef(null);
24
+ const indRef = useRef(null);
25
+ const initDone = useRef(false);
26
+
27
+ // Underline indicator positioning
28
+ useEffect(() => {
29
+ if (variant !== 'underline') return;
30
+ const bar = barRef.current;
31
+ const ind = indRef.current;
32
+ if (!bar || !ind) return;
33
+
34
+ const activeBtn = bar.querySelector(`[data-tabid="${active}"]`);
35
+ if (!activeBtn) return;
36
+
37
+ const barRect = bar.getBoundingClientRect();
38
+ const btnRect = activeBtn.getBoundingClientRect();
39
+ const x = btnRect.left - barRect.left;
40
+ const w = btnRect.width;
41
+
42
+ if (!initDone.current) {
43
+ ind.style.transition = 'none';
44
+ ind.style.width = w + 'px';
45
+ ind.style.transform = `translateX(${x}px)`;
46
+ requestAnimationFrame(() => { ind.style.transition = ''; });
47
+ initDone.current = true;
48
+ } else {
49
+ ind.style.width = w + 'px';
50
+ ind.style.transform = `translateX(${x}px)`;
51
+ }
52
+ }, [active, tabs, variant]);
53
+
54
+ if (variant === 'pill') {
55
+ return (
56
+ <div style={{
57
+ display: 'flex', gap: 4,
58
+ background: 'var(--grey-1, #f5f5f8)',
59
+ borderRadius: 'var(--rads, 8px)',
60
+ padding: '4px',
61
+ border: '1px solid var(--border-light, #ebebf0)',
62
+ }}>
63
+ {tabs.map(t => {
64
+ const isActive = active === t.id;
65
+ return (
66
+ <button
67
+ key={t.id}
68
+ data-tabid={t.id}
69
+ onClick={() => onChange(t.id)}
70
+ style={{
71
+ display: 'flex', alignItems: 'center', gap: 6,
72
+ padding: padding || '8px 16px',
73
+ border: 'none',
74
+ borderRadius: 'var(--radxs, 6px)',
75
+ background: isActive ? '#fff' : 'transparent',
76
+ boxShadow: isActive ? '0 1px 3px rgba(0,0,0,.08)' : 'none',
77
+ fontSize: '.82rem', fontWeight: isActive ? 600 : 500,
78
+ color: isActive ? 'var(--primary-text, #0f111c)' : 'var(--muted, #9b9daa)',
79
+ cursor: 'pointer',
80
+ transition: 'all .18s ease',
81
+ fontFamily: 'inherit',
82
+ whiteSpace: 'nowrap',
83
+ textTransform: capitalize ? 'capitalize' : 'none',
84
+ }}
85
+ >
86
+ {t.icon}{t.label}
87
+ {t.count != null && t.count > 0 && (
88
+ <span style={{
89
+ background: isActive ? 'var(--accent-bg)' : 'var(--grey-2, #d7d8e0)',
90
+ color: isActive ? 'var(--accent)' : 'var(--muted)',
91
+ borderRadius: 10,
92
+ padding: '1px 6px',
93
+ fontSize: '.68rem',
94
+ fontWeight: 600,
95
+ }}>{t.count}</span>
96
+ )}
97
+ </button>
98
+ );
99
+ })}
100
+ </div>
101
+ );
102
+ }
103
+
104
+ // Default: underline variant
105
+ const defaultPadding = padding || '10px 16px';
106
+
107
+ return (
108
+ <div ref={barRef} style={{ display:'flex', position:'relative' }}>
109
+ <div ref={indRef} style={{
110
+ position:'absolute', bottom:'-1px', left:0,
111
+ height:2, width:0,
112
+ background:'var(--accent)',
113
+ borderRadius:'2px 2px 0 0',
114
+ transition:'transform .22s cubic-bezier(.4,0,.2,1), width .22s cubic-bezier(.4,0,.2,1)',
115
+ pointerEvents:'none', zIndex:2,
116
+ }} />
117
+
118
+ {tabs.map(t => {
119
+ const isActive = active === t.id;
120
+ return (
121
+ <button
122
+ key={t.id}
123
+ data-tabid={t.id}
124
+ onClick={() => onChange(t.id)}
125
+ style={{
126
+ display:'flex', alignItems:'center', gap:6,
127
+ padding: defaultPadding,
128
+ border:'none', background:'none',
129
+ fontSize:'.82rem', fontWeight:500,
130
+ color: isActive ? 'var(--accent)' : 'var(--muted)',
131
+ cursor:'pointer',
132
+ transition:'color .18s',
133
+ marginBottom:'-1px',
134
+ position:'relative', zIndex:1,
135
+ fontFamily:'inherit',
136
+ whiteSpace:'nowrap',
137
+ textTransform: capitalize ? 'capitalize' : 'none',
138
+ }}
139
+ >
140
+ {t.icon}{t.label}
141
+ {t.count != null && t.count > 0 && (
142
+ <span style={{
143
+ background: isActive ? 'var(--accent-bg)' : 'var(--grey-1)',
144
+ color: isActive ? 'var(--accent)' : 'var(--muted)',
145
+ borderRadius: 10,
146
+ padding: '1px 6px',
147
+ fontSize: '.68rem',
148
+ transition: 'background .18s, color .18s',
149
+ }}>{t.count}</span>
150
+ )}
151
+ </button>
152
+ );
153
+ })}
154
+ </div>
155
+ );
156
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cynx-ui",
3
- "version": "1.2.13",
3
+ "version": "1.2.14",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./index.js"