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,184 +1,184 @@
1
- import { useMemo, useState, useRef } from 'react';
2
-
3
- const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
4
-
5
- function LegendDot({ color, label }) {
6
- return (
7
- <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
8
- <span style={{ width: 7, height: 7, borderRadius: 2, background: color, flexShrink: 0 }} />
9
- <span style={{ fontSize: '.55rem', color: 'var(--muted)', fontWeight: 500 }}>{label}</span>
10
- </div>
11
- );
12
- }
13
-
14
- /**
15
- * ActivityCard — day-block bar with hover tooltip
16
- */
17
- export default function ActivityCard({
18
- title = 'Activity',
19
- subtitle,
20
- primary = [],
21
- additional = [],
22
- totalSlots = 90,
23
- greenColor = 'var(--success)',
24
- redColor = 'var(--danger)',
25
- emptyColor = 'var(--grey-1)',
26
- barHeight = 28,
27
- barRadius = 3,
28
- leftLabel,
29
- rightLabel,
30
- showDot = true,
31
- dotColor,
32
- style,
33
- }) {
34
- const [hovered, setHovered] = useState(null);
35
- const [tooltipPos, setTooltipPos] = useState({ x: 0, y: 0 });
36
- const containerRef = useRef(null);
37
-
38
- const bars = useMemo(() => {
39
- const len = Math.max(primary.length, additional.length, totalSlots);
40
- const result = [];
41
- const stepS = primary.length / len;
42
- const stepF = additional.length / len;
43
- for (let i = 0; i < len; i++) {
44
- const si = Math.min(Math.floor(i * stepS), primary.length - 1);
45
- const fi = Math.min(Math.floor(i * stepF), additional.length - 1);
46
- result.push({ success: primary[si] || 0, failed: additional[fi] || 0 });
47
- }
48
- return result;
49
- }, [primary, additional, totalSlots]);
50
-
51
- const dates = useMemo(() => {
52
- const result = [];
53
- const now = new Date();
54
- for (let i = 0; i < bars.length; i++) {
55
- const d = new Date(now);
56
- d.setDate(d.getDate() - (bars.length - 1 - i));
57
- result.push(d);
58
- }
59
- return result;
60
- }, [bars.length]);
61
-
62
- const totalSuccess = bars.reduce((a, b) => a + b.success, 0);
63
- const totalFailed = bars.reduce((a, b) => a + b.failed, 0);
64
- const totalAll = totalSuccess + totalFailed;
65
- const uptimePercent = totalAll > 0 ? Math.round((totalSuccess / totalAll) * 100) : 100;
66
-
67
- function handleMouseEnter(e, idx) {
68
- const rect = e.currentTarget.getBoundingClientRect();
69
- const containerRect = containerRef.current.getBoundingClientRect();
70
- setTooltipPos({
71
- x: rect.left - containerRect.left + rect.width / 2,
72
- y: rect.top - containerRect.top - 8,
73
- });
74
- setHovered(idx);
75
- }
76
-
77
- const h = hovered !== null ? bars[hovered] : null;
78
- const hTotal = h ? h.success + h.failed : 0;
79
-
80
- return (
81
- <div ref={containerRef} style={{
82
- background: '#fff', border: '1px solid var(--border)', borderRadius: 'var(--rad)',
83
- boxShadow: '0 1px 3px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04)',
84
- padding: '14px 18px', position: 'relative', ...style,
85
- }}>
86
- {/* Header */}
87
- <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
88
- <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
89
- {showDot && (
90
- <span style={{ width: 8, height: 8, borderRadius: '50%', background: dotColor || greenColor, flexShrink: 0 }} />
91
- )}
92
- <span style={{ fontSize: '.78rem', fontWeight: 700, color: 'var(--primary-text)', letterSpacing: '-.01em' }}>{title}</span>
93
- {subtitle && <span style={{ fontSize: '.65rem', color: 'var(--muted)', fontWeight: 500 }}>{subtitle}</span>}
94
- </div>
95
- <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
96
- <LegendDot color={greenColor} label="Success" />
97
- <LegendDot color={redColor} label="Failed" />
98
- <LegendDot color={emptyColor} label="None" />
99
- </div>
100
- </div>
101
-
102
- {/* Bars */}
103
- <div style={{ display: 'flex', alignItems: 'flex-end', gap: 2, height: barHeight, marginBottom: 6 }}>
104
- {bars.map((b, i) => {
105
- const total = b.success + b.failed;
106
- const hasData = total > 0;
107
- const successPct = hasData ? (b.success / total) * 100 : 0;
108
- const failedPct = hasData ? (b.failed / total) * 100 : 0;
109
-
110
- return (
111
- <div
112
- key={i}
113
- onMouseEnter={(e) => handleMouseEnter(e, i)}
114
- onMouseLeave={() => setHovered(null)}
115
- style={{
116
- flex: 1, height: '100%', borderRadius: barRadius, overflow: 'hidden',
117
- display: 'flex', flexDirection: 'column',
118
- background: hasData ? 'transparent' : emptyColor,
119
- cursor: hasData ? 'pointer' : 'default',
120
- }}
121
- >
122
- {hasData && (
123
- <>
124
- {b.failed > 0 && <div style={{ height: `${failedPct}%`, background: redColor, marginTop: 'auto' }} />}
125
- {b.success > 0 && <div style={{ height: `${successPct}%`, background: greenColor }} />}
126
- </>
127
- )}
128
- </div>
129
- );
130
- })}
131
- </div>
132
-
133
- {/* Tooltip */}
134
- {hovered !== null && (
135
- <div style={{
136
- position: 'absolute', left: tooltipPos.x, top: tooltipPos.y,
137
- transform: 'translate(-50%, -100%)', background: '#fff',
138
- border: '1px solid var(--border)', borderRadius: 'var(--rads, 8px)',
139
- boxShadow: '0 4px 12px rgba(0,0,0,.12)', padding: '10px 14px',
140
- pointerEvents: 'none', zIndex: 100, whiteSpace: 'nowrap',
141
- animation: 'hoverCardIn .15s ease',
142
- }}>
143
- <div style={{ fontSize: '.65rem', color: 'var(--muted)', fontWeight: 500, marginBottom: 4 }}>
144
- {dates[hovered]?.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
145
- </div>
146
- {h.success > 0 && (
147
- <div style={{ fontSize: '.7rem', fontWeight: 700, color: greenColor }}>
148
- {h.success} success{h.success !== 1 ? 'es' : ''}
149
- {h.failed > 0 && <span style={{ fontWeight: 500, color: 'var(--muted)' }}> ({Math.round((h.success / hTotal) * 100)}%)</span>}
150
- </div>
151
- )}
152
- {h.failed > 0 && (
153
- <div style={{ fontSize: '.7rem', fontWeight: 700, color: redColor }}>
154
- {h.failed} failed{h.failed !== 1 ? 's' : ''}
155
- {h.success > 0 && <span style={{ fontWeight: 500, color: 'var(--muted)' }}> ({Math.round((h.failed / hTotal) * 100)}%)</span>}
156
- </div>
157
- )}
158
- {!h.success && !h.failed && (
159
- <div style={{ fontSize: '.7rem', fontWeight: 500, color: 'var(--muted)' }}>No activity</div>
160
- )}
161
- <div style={{
162
- position: 'absolute', left: '50%', bottom: -5,
163
- transform: 'translateX(-50%) rotate(45deg)',
164
- width: 8, height: 8, background: '#fff',
165
- borderRight: '1px solid var(--border)', borderBottom: '1px solid var(--border)',
166
- }} />
167
- </div>
168
- )}
169
-
170
- {/* Footer */}
171
- <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
172
- <span style={{ fontSize: '.58rem', color: 'var(--muted)', fontWeight: 500 }}>{leftLabel || ''}</span>
173
- <span style={{ fontSize: '.78rem', fontWeight: 700, color: greenColor, fontFamily: 'var(--mono)' }}>{uptimePercent}%</span>
174
- <span style={{ fontSize: '.58rem', color: 'var(--muted)', fontWeight: 500 }}>{rightLabel || ''}</span>
175
- </div>
176
- <style>{`
177
- @keyframes hoverCardIn {
178
- from { opacity: 0; transform: translate(-50%, -100%) translateY(4px); }
179
- to { opacity: 1; transform: translate(-50%, -100%) translateY(0); }
180
- }
181
- `}</style>
182
- </div>
183
- );
184
- }
1
+ import { useMemo, useState, useRef } from 'react';
2
+
3
+ const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
4
+
5
+ function LegendDot({ color, label }) {
6
+ return (
7
+ <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
8
+ <span style={{ width: 7, height: 7, borderRadius: 2, background: color, flexShrink: 0 }} />
9
+ <span style={{ fontSize: '.55rem', color: 'var(--muted)', fontWeight: 500 }}>{label}</span>
10
+ </div>
11
+ );
12
+ }
13
+
14
+ /**
15
+ * ActivityCard — day-block bar with hover tooltip
16
+ */
17
+ export default function ActivityCard({
18
+ title = 'Activity',
19
+ subtitle,
20
+ primary = [],
21
+ additional = [],
22
+ totalSlots = 90,
23
+ greenColor = 'var(--success)',
24
+ redColor = 'var(--danger)',
25
+ emptyColor = 'var(--grey-1)',
26
+ barHeight = 28,
27
+ barRadius = 3,
28
+ leftLabel,
29
+ rightLabel,
30
+ showDot = true,
31
+ dotColor,
32
+ style,
33
+ }) {
34
+ const [hovered, setHovered] = useState(null);
35
+ const [tooltipPos, setTooltipPos] = useState({ x: 0, y: 0 });
36
+ const containerRef = useRef(null);
37
+
38
+ const bars = useMemo(() => {
39
+ const len = Math.max(primary.length, additional.length, totalSlots);
40
+ const result = [];
41
+ const stepS = primary.length / len;
42
+ const stepF = additional.length / len;
43
+ for (let i = 0; i < len; i++) {
44
+ const si = Math.min(Math.floor(i * stepS), primary.length - 1);
45
+ const fi = Math.min(Math.floor(i * stepF), additional.length - 1);
46
+ result.push({ success: primary[si] || 0, failed: additional[fi] || 0 });
47
+ }
48
+ return result;
49
+ }, [primary, additional, totalSlots]);
50
+
51
+ const dates = useMemo(() => {
52
+ const result = [];
53
+ const now = new Date();
54
+ for (let i = 0; i < bars.length; i++) {
55
+ const d = new Date(now);
56
+ d.setDate(d.getDate() - (bars.length - 1 - i));
57
+ result.push(d);
58
+ }
59
+ return result;
60
+ }, [bars.length]);
61
+
62
+ const totalSuccess = bars.reduce((a, b) => a + b.success, 0);
63
+ const totalFailed = bars.reduce((a, b) => a + b.failed, 0);
64
+ const totalAll = totalSuccess + totalFailed;
65
+ const uptimePercent = totalAll > 0 ? Math.round((totalSuccess / totalAll) * 100) : 100;
66
+
67
+ function handleMouseEnter(e, idx) {
68
+ const rect = e.currentTarget.getBoundingClientRect();
69
+ const containerRect = containerRef.current.getBoundingClientRect();
70
+ setTooltipPos({
71
+ x: rect.left - containerRect.left + rect.width / 2,
72
+ y: rect.top - containerRect.top - 8,
73
+ });
74
+ setHovered(idx);
75
+ }
76
+
77
+ const h = hovered !== null ? bars[hovered] : null;
78
+ const hTotal = h ? h.success + h.failed : 0;
79
+
80
+ return (
81
+ <div ref={containerRef} style={{
82
+ background: '#fff', border: '1px solid var(--border)', borderRadius: 'var(--rad)',
83
+ boxShadow: '0 1px 3px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04)',
84
+ padding: '14px 18px', position: 'relative', ...style,
85
+ }}>
86
+ {/* Header */}
87
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
88
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
89
+ {showDot && (
90
+ <span style={{ width: 8, height: 8, borderRadius: '50%', background: dotColor || greenColor, flexShrink: 0 }} />
91
+ )}
92
+ <span style={{ fontSize: '.78rem', fontWeight: 700, color: 'var(--primary-text)', letterSpacing: '-.01em' }}>{title}</span>
93
+ {subtitle && <span style={{ fontSize: '.65rem', color: 'var(--muted)', fontWeight: 500 }}>{subtitle}</span>}
94
+ </div>
95
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
96
+ <LegendDot color={greenColor} label="Success" />
97
+ <LegendDot color={redColor} label="Failed" />
98
+ <LegendDot color={emptyColor} label="None" />
99
+ </div>
100
+ </div>
101
+
102
+ {/* Bars */}
103
+ <div style={{ display: 'flex', alignItems: 'flex-end', gap: 2, height: barHeight, marginBottom: 6 }}>
104
+ {bars.map((b, i) => {
105
+ const total = b.success + b.failed;
106
+ const hasData = total > 0;
107
+ const successPct = hasData ? (b.success / total) * 100 : 0;
108
+ const failedPct = hasData ? (b.failed / total) * 100 : 0;
109
+
110
+ return (
111
+ <div
112
+ key={i}
113
+ onMouseEnter={(e) => handleMouseEnter(e, i)}
114
+ onMouseLeave={() => setHovered(null)}
115
+ style={{
116
+ flex: 1, height: '100%', borderRadius: barRadius, overflow: 'hidden',
117
+ display: 'flex', flexDirection: 'column',
118
+ background: hasData ? 'transparent' : emptyColor,
119
+ cursor: hasData ? 'pointer' : 'default',
120
+ }}
121
+ >
122
+ {hasData && (
123
+ <>
124
+ {b.failed > 0 && <div style={{ height: `${failedPct}%`, background: redColor, marginTop: 'auto' }} />}
125
+ {b.success > 0 && <div style={{ height: `${successPct}%`, background: greenColor }} />}
126
+ </>
127
+ )}
128
+ </div>
129
+ );
130
+ })}
131
+ </div>
132
+
133
+ {/* Tooltip */}
134
+ {hovered !== null && (
135
+ <div style={{
136
+ position: 'absolute', left: tooltipPos.x, top: tooltipPos.y,
137
+ transform: 'translate(-50%, -100%)', background: '#fff',
138
+ border: '1px solid var(--border)', borderRadius: 'var(--rads, 8px)',
139
+ boxShadow: '0 4px 12px rgba(0,0,0,.12)', padding: '10px 14px',
140
+ pointerEvents: 'none', zIndex: 100, whiteSpace: 'nowrap',
141
+ animation: 'hoverCardIn .15s ease',
142
+ }}>
143
+ <div style={{ fontSize: '.65rem', color: 'var(--muted)', fontWeight: 500, marginBottom: 4 }}>
144
+ {dates[hovered]?.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
145
+ </div>
146
+ {h.success > 0 && (
147
+ <div style={{ fontSize: '.7rem', fontWeight: 700, color: greenColor }}>
148
+ {h.success} success{h.success !== 1 ? 'es' : ''}
149
+ {h.failed > 0 && <span style={{ fontWeight: 500, color: 'var(--muted)' }}> ({Math.round((h.success / hTotal) * 100)}%)</span>}
150
+ </div>
151
+ )}
152
+ {h.failed > 0 && (
153
+ <div style={{ fontSize: '.7rem', fontWeight: 700, color: redColor }}>
154
+ {h.failed} failed{h.failed !== 1 ? 's' : ''}
155
+ {h.success > 0 && <span style={{ fontWeight: 500, color: 'var(--muted)' }}> ({Math.round((h.failed / hTotal) * 100)}%)</span>}
156
+ </div>
157
+ )}
158
+ {!h.success && !h.failed && (
159
+ <div style={{ fontSize: '.7rem', fontWeight: 500, color: 'var(--muted)' }}>No activity</div>
160
+ )}
161
+ <div style={{
162
+ position: 'absolute', left: '50%', bottom: -5,
163
+ transform: 'translateX(-50%) rotate(45deg)',
164
+ width: 8, height: 8, background: '#fff',
165
+ borderRight: '1px solid var(--border)', borderBottom: '1px solid var(--border)',
166
+ }} />
167
+ </div>
168
+ )}
169
+
170
+ {/* Footer */}
171
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
172
+ <span style={{ fontSize: '.58rem', color: 'var(--muted)', fontWeight: 500 }}>{leftLabel || ''}</span>
173
+ <span style={{ fontSize: '.78rem', fontWeight: 700, color: greenColor, fontFamily: 'var(--mono)' }}>{uptimePercent}%</span>
174
+ <span style={{ fontSize: '.58rem', color: 'var(--muted)', fontWeight: 500 }}>{rightLabel || ''}</span>
175
+ </div>
176
+ <style>{`
177
+ @keyframes hoverCardIn {
178
+ from { opacity: 0; transform: translate(-50%, -100%) translateY(4px); }
179
+ to { opacity: 1; transform: translate(-50%, -100%) translateY(0); }
180
+ }
181
+ `}</style>
182
+ </div>
183
+ );
184
+ }
@@ -1,105 +1,129 @@
1
- const STYLES = `
2
- .badge{display:inline-flex;align-items:center;gap:5px;padding:3px 9px;border-radius:var(--rfull,99px);font-size:10.5px;font-weight:600;border:1px solid;white-space:nowrap;transition:all .15s}
3
- .badge-sm{padding:2px 7px;font-size:10px}
4
- .badge-lg{padding:4px 12px;font-size:12px}
5
- .badge-icon{display:inline-flex;align-items:center;justify-content:center;width:14px;height:14px;flex-shrink:0}
6
- .badge-icon svg{width:12px;height:12px}
7
- .badge-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0;margin-top:-2px}
8
- .badge-shape{width:0;height:0;flex-shrink:0}
9
- .badge-shape.circle{width:7px;height:7px;border-radius:50%;border:none}
10
- .badge-shape.triangle{width:0;height:0;border-left:4px solid transparent;border-right:4px solid transparent;border-bottom:7px solid;background:none;border-top:none}
11
- .badge-shape.diamond{width:7px;height:7px;border-radius:1px;transform:rotate(45deg);border:none}
12
- .badge-shape.semicircle{width:7px;height:7px;border-radius:50%;border:none;border-top:2px solid var(--bg,#fff)}
13
-
14
- .badge-ok{background:var(--success-bg,rgba(39,174,96,.1));color:var(--success,#27ae60);border-color:rgba(39,174,96,.2)}
15
- .badge-ok .badge-dot,.badge-ok .badge-shape.circle{background:var(--success,#27ae60)}
16
- .badge-err{background:var(--danger-bg,rgba(246,71,71,.1));color:var(--danger,#f64747);border-color:rgba(246,71,71,.2)}
17
- .badge-err .badge-dot,.badge-err .badge-shape.circle{background:var(--danger,#f64747)}
18
- .badge-warn{background:var(--accent-bg,rgba(249,158,44,.1));color:var(--accent,#f99e2c);border-color:var(--accent-border,rgba(249,158,44,.25))}
19
- .badge-warn .badge-dot,.badge-warn .badge-shape.circle{background:var(--accent,#f99e2c)}
20
- .badge-blue{background:var(--blue-bg,rgba(39,130,228,.1));color:var(--blue,#2782e4);border-color:rgba(39,130,228,.2)}
21
- .badge-blue .badge-dot,.badge-blue .badge-shape.circle{background:var(--blue,#2782e4)}
22
- .badge-dim{background:var(--grey-1,#f5f5f8);color:var(--grey-4,#797b8d);border-color:var(--border,#d7d8e0)}
23
- .badge-dim .badge-dot,.badge-dim .badge-shape.circle{background:var(--grey-4,#797b8d)}
24
- .badge-purple{background:rgba(139,92,246,.1);color:#8b5cf6;border-color:rgba(139,92,246,.2)}
25
- .badge-purple .badge-dot,.badge-purple .badge-shape.circle{background:#8b5cf6}
26
- .badge-teal{background:rgba(20,184,166,.1);color:#14b8a6;border-color:rgba(20,184,166,.2)}
27
- .badge-teal .badge-dot,.badge-teal .badge-shape.circle{background:#14b8a6}
28
- .badge-pink{background:rgba(236,72,153,.1);color:#ec4899;border-color:rgba(236,72,153,.2)}
29
- .badge-pink .badge-dot,.badge-pink .badge-shape.circle{background:#ec4899}
30
- .badge-ghost{background:transparent;color:var(--t2,var(--grey-4,#797b8d));border-color:var(--bd,var(--border,#d7d8e0))}
31
- .badge-ghost .badge-dot,.badge-ghost .badge-shape.circle{background:var(--t3,var(--grey-4,#797b8d))}
32
-
33
- .badge-paid{background:rgba(30,41,59,.08);color:#1e293b;border-color:rgba(30,41,59,.15)}
34
- .badge-paid .badge-shape.circle{background:#1e293b}
35
- .badge-invoiced{background:rgba(120,120,130,.08);color:#787882;border-color:rgba(120,120,130,.15)}
36
- .badge-invoiced .badge-shape.circle{background:#787882}
37
- .badge-verified{background:rgba(34,197,94,.08);color:#22c55e;border-color:rgba(34,197,94,.15)}
38
- .badge-verified .badge-shape.circle{background:#22c55e}
39
- .badge-partial{background:rgba(100,100,110,.08);color:#64646e;border-color:rgba(100,100,110,.15)}
40
- .badge-partial .badge-shape.semicircle{background:#64646e}
41
- .badge-deleted{background:rgba(239,68,68,.08);color:#ef4444;border-color:rgba(239,68,68,.15)}
42
- .badge-deleted .badge-shape.circle{background:#ef4444}
43
- .badge-update{background:rgba(249,115,22,.08);color:#f97316;border-color:rgba(249,115,22,.15)}
44
- .badge-update .badge-shape.triangle{border-bottom-color:#f97316}
45
- .badge-ready{background:rgba(234,179,8,.08);color:#eab308;border-color:rgba(234,179,8,.15)}
46
- .badge-ready .badge-shape.diamond{background:#eab308}
47
- .badge-new{background:rgba(59,130,246,.08);color:#3b82f6;border-color:rgba(59,130,246,.15)}
48
- .badge-new .badge-shape.circle{background:#3b82f6}
49
-
50
- .badge-active{background:transparent;color:#22c55e;border:none;padding:3px 0}
51
- .badge-active .badge-dot,.badge-active .badge-icon svg{color:#22c55e}
52
- .badge-inactive{background:transparent;color:#ef4444;border:none;padding:3px 0}
53
- .badge-inactive .badge-dot,.badge-inactive .badge-icon svg{color:#ef4444}
54
- .badge-pending{background:transparent;color:#d97706;border:none;padding:3px 0}
55
- .badge-pending .badge-dot{background:#d97706;animation:badge-pulse 1.5s ease-in-out infinite}
56
- .badge-pending .badge-icon svg{color:#d97706}
57
- @keyframes badge-pulse{0%,100%{opacity:1}50%{opacity:.4}}
58
-
59
- .badge-clickable{cursor:pointer}
60
- .badge-clickable:hover{filter:brightness(.92)}
61
- .badge-clickable:active{}
62
- `;
63
-
64
- const SHAPE_MAP = {
65
- ok: 'circle', err: 'circle', warn: 'circle', blue: 'circle', dim: 'circle',
66
- purple: 'circle', teal: 'circle', pink: 'circle', ghost: 'circle',
67
- paid: 'circle', invoiced: 'circle', verified: 'circle', partial: 'semicircle',
68
- deleted: 'circle', update: 'triangle', ready: 'diamond', new: 'circle',
69
- };
70
-
71
- let styleInjected = false;
72
-
73
- export default function Badge({
74
- children,
75
- variant = 'dim',
76
- size = 'md',
77
- dot = false,
78
- shape = null,
79
- icon = null,
80
- onClick = null,
81
- style = {},
82
- className = '',
83
- }) {
84
- if (!styleInjected && typeof document !== 'undefined') {
85
- const tag = document.createElement('style');
86
- tag.textContent = STYLES;
87
- document.head.appendChild(tag);
88
- styleInjected = true;
89
- }
90
-
91
- const sizeClass = size === 'sm' ? ' badge-sm' : size === 'lg' ? ' badge-lg' : '';
92
- const clickClass = onClick ? ' badge-clickable' : '';
93
- const classes = `badge badge-${variant}${sizeClass}${clickClass}${className ? ' ' + className : ''}`;
94
-
95
- const shapeType = shape || SHAPE_MAP[variant] || null;
96
-
97
- return (
98
- <span className={classes} style={style} onClick={onClick}>
99
- {dot && <span className="badge-dot" />}
100
- {shapeType && !dot && <span className={`badge-shape ${shapeType}`} />}
101
- {icon && <span className="badge-icon">{icon}</span>}
102
- {children}
103
- </span>
104
- );
105
- }
1
+ const VARIANTS = {
2
+ ok: { bg: 'var(--success-bg,rgba(39,174,96,.1))', color: 'var(--success,#27ae60)', border: 'rgba(39,174,96,.2)', dot: 'var(--success,#27ae60)' },
3
+ err: { bg: 'var(--danger-bg,rgba(246,71,71,.1))', color: 'var(--danger,#f64747)', border: 'rgba(246,71,71,.2)', dot: 'var(--danger,#f64747)' },
4
+ warn: { bg: 'var(--accent-bg,rgba(249,158,44,.1))', color: 'var(--accent,#f99e2c)', border: 'var(--accent-border,rgba(249,158,44,.25))', dot: 'var(--accent,#f99e2c)' },
5
+ blue: { bg: 'var(--blue-bg,rgba(39,130,228,.1))', color: 'var(--blue,#2782e4)', border: 'rgba(39,130,228,.2)', dot: 'var(--blue,#2782e4)' },
6
+ dim: { bg: 'var(--grey-1,#f5f5f8)', color: 'var(--grey-4,#797b8d)', border: 'var(--border,#d7d8e0)', dot: 'var(--grey-4,#797b8d)' },
7
+ purple: { bg: 'rgba(139,92,246,.1)', color: '#8b5cf6', border: 'rgba(139,92,246,.2)', dot: '#8b5cf6' },
8
+ teal: { bg: 'rgba(20,184,166,.1)', color: '#14b8a6', border: 'rgba(20,184,166,.2)', dot: '#14b8a6' },
9
+ pink: { bg: 'rgba(236,72,153,.1)', color: '#ec4899', border: 'rgba(236,72,153,.2)', dot: '#ec4899' },
10
+ ghost: { bg: 'transparent', color: 'var(--t2,var(--grey-4,#797b8d))', border: 'var(--bd,var(--border,#d7d8e0))', dot: 'var(--t3,var(--grey-4,#797b8d))' },
11
+ paid: { bg: 'rgba(30,41,59,.08)', color: '#1e293b', border: 'rgba(30,41,59,.15)', dot: '#1e293b' },
12
+ invoiced: { bg: 'rgba(120,120,130,.08)', color: '#787882', border: 'rgba(120,120,130,.15)', dot: '#787882' },
13
+ verified: { bg: 'rgba(34,197,94,.08)', color: '#22c55e', border: 'rgba(34,197,94,.15)', dot: '#22c55e' },
14
+ partial: { bg: 'rgba(100,100,110,.08)', color: '#64646e', border: 'rgba(100,100,110,.15)', dot: '#64646e' },
15
+ deleted: { bg: 'rgba(239,68,68,.08)', color: '#ef4444', border: 'rgba(239,68,68,.15)', dot: '#ef4444' },
16
+ update: { bg: 'rgba(249,115,22,.08)', color: '#f97316', border: 'rgba(249,115,22,.15)', dot: '#f97316' },
17
+ ready: { bg: 'rgba(234,179,8,.08)', color: '#eab308', border: 'rgba(234,179,8,.15)', dot: '#eab308' },
18
+ new: { bg: 'rgba(59,130,246,.08)', color: '#3b82f6', border: 'rgba(59,130,246,.15)', dot: '#3b82f6' },
19
+ active: { bg: 'transparent', color: '#22c55e', border: 'none', dot: '#22c55e', noPad: true },
20
+ inactive: { bg: 'transparent', color: '#ef4444', border: 'none', dot: '#ef4444', noPad: true },
21
+ pending: { bg: 'transparent', color: '#d97706', border: 'none', dot: '#d97706', noPad: true, pulse: true },
22
+ };
23
+
24
+ const SHAPES = {
25
+ ok: 'circle', err: 'circle', warn: 'circle', blue: 'circle', dim: 'circle',
26
+ purple: 'circle', teal: 'circle', pink: 'circle', ghost: 'circle',
27
+ paid: 'circle', invoiced: 'circle', verified: 'circle', partial: 'semicircle',
28
+ deleted: 'circle', update: 'triangle', ready: 'diamond', new: 'circle',
29
+ };
30
+
31
+ const KEYFRAMES = `@keyframes badge-pulse{0%,100%{opacity:1}50%{opacity:.4}}`;
32
+ let styleInjected = false;
33
+
34
+ function ensureStyles() {
35
+ if (styleInjected || typeof document === 'undefined') return;
36
+ const tag = document.createElement('style');
37
+ tag.textContent = KEYFRAMES;
38
+ document.head.appendChild(tag);
39
+ styleInjected = true;
40
+ }
41
+
42
+ const SIZE_MAP = {
43
+ sm: { padding: '2px 7px', fontSize: '10px' },
44
+ md: { padding: '3px 9px', fontSize: '10.5px' },
45
+ lg: { padding: '4px 12px', fontSize: '12px' },
46
+ };
47
+
48
+ const SHAPE_STYLE = {
49
+ circle: (c) => ({ width: 7, height: 7, borderRadius: '50%', background: c, border: 'none' }),
50
+ triangle: (c) => ({ width: 0, height: 0, borderLeft: '4px solid transparent', borderRight: '4px solid transparent', borderBottom: `7px solid ${c}`, background: 'none', borderTop: 'none' }),
51
+ diamond: (c) => ({ width: 7, height: 7, borderRadius: 1, transform: 'rotate(45deg)', background: c, border: 'none' }),
52
+ semicircle:(c) => ({ width: 7, height: 7, borderRadius: '50%', border: 'none', borderTop: '2px solid var(--bg,#fff)', background: c }),
53
+ };
54
+
55
+ export default function Badge({
56
+ children,
57
+ variant = 'dim',
58
+ size = 'md',
59
+ dot = false,
60
+ shape = null,
61
+ icon = null,
62
+ onClick = null,
63
+ style = {},
64
+ className = '',
65
+ }) {
66
+ ensureStyles();
67
+
68
+ const v = VARIANTS[variant] || VARIANTS.dim;
69
+ const sz = SIZE_MAP[size] || SIZE_MAP.md;
70
+ const shapeType = shape || SHAPES[variant] || null;
71
+ const dotColor = v.dot || v.color;
72
+
73
+ return (
74
+ <span
75
+ className={className}
76
+ style={{
77
+ display: 'inline-flex',
78
+ alignItems: 'center',
79
+ gap: 5,
80
+ padding: v.noPad ? '3px 0' : sz.padding,
81
+ borderRadius: 'var(--rfull,99px)',
82
+ fontSize: sz.fontSize,
83
+ fontWeight: 600,
84
+ border: `1px solid ${v.border}`,
85
+ background: v.bg,
86
+ color: v.color,
87
+ whiteSpace: 'nowrap',
88
+ transition: 'all .15s',
89
+ cursor: onClick ? 'pointer' : undefined,
90
+ ...style,
91
+ }}
92
+ onClick={onClick}
93
+ >
94
+ {dot && (
95
+ <span style={{
96
+ width: 6,
97
+ height: 6,
98
+ borderRadius: '50%',
99
+ flexShrink: 0,
100
+ marginTop: -2,
101
+ background: dotColor,
102
+ animation: v.pulse ? 'badge-pulse 1.5s ease-in-out infinite' : undefined,
103
+ }} />
104
+ )}
105
+ {shapeType && !dot && (
106
+ <span style={{
107
+ width: 0,
108
+ height: 0,
109
+ flexShrink: 0,
110
+ ...(SHAPE_STYLE[shapeType] ? SHAPE_STYLE[shapeType](dotColor) : {}),
111
+ }} />
112
+ )}
113
+ {icon && (
114
+ <span style={{
115
+ display: 'inline-flex',
116
+ alignItems: 'center',
117
+ justifyContent: 'center',
118
+ width: 14,
119
+ height: 14,
120
+ flexShrink: 0,
121
+ color: dotColor,
122
+ }}>
123
+ {icon}
124
+ </span>
125
+ )}
126
+ {children}
127
+ </span>
128
+ );
129
+ }