cynx-ui 1.3.13 → 1.3.15

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,200 +1,200 @@
1
- import { useMemo, useState, useRef } from 'react';
2
-
3
- const card = {
4
- background: 'var(--surface,#fff)',
5
- border: '1px solid var(--border)',
6
- borderRadius: 'var(--rad)',
7
- boxShadow: 'var(--shadow, 0 1px 3px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04))',
8
- };
9
-
10
- const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
11
-
12
- function formatDate(date) {
13
- return `${MONTHS[date.getMonth()]} ${date.getDate()}`;
14
- }
15
-
16
- export default function UptimeBar({
17
- title = 'Uptime',
18
- subtitle,
19
- successValues = [],
20
- failedValues = [],
21
- totalSlots = 90,
22
- greenColor = 'var(--success)',
23
- redColor = 'var(--danger)',
24
- emptyColor = 'var(--grey-1)',
25
- barHeight = 28,
26
- barRadius = 3,
27
- leftLabel,
28
- rightLabel,
29
- showDot = true,
30
- dotColor,
31
- style,
32
- }) {
33
- const [hovered, setHovered] = useState(null);
34
- const [tooltipPos, setTooltipPos] = useState({ x: 0, y: 0 });
35
- const containerRef = useRef(null);
36
-
37
- const bars = useMemo(() => {
38
- const len = Math.max(successValues.length, failedValues.length, totalSlots);
39
- const result = [];
40
- const stepS = successValues.length / len;
41
- const stepF = failedValues.length / len;
42
- for (let i = 0; i < len; i++) {
43
- const si = Math.min(Math.floor(i * stepS), successValues.length - 1);
44
- const fi = Math.min(Math.floor(i * stepF), failedValues.length - 1);
45
- const s = successValues[si] || 0;
46
- const f = failedValues[fi] || 0;
47
- result.push({ success: s, failed: f });
48
- }
49
- return result;
50
- }, [successValues, failedValues, totalSlots]);
51
-
52
- const dates = useMemo(() => {
53
- const result = [];
54
- const now = new Date();
55
- for (let i = 0; i < bars.length; i++) {
56
- const d = new Date(now);
57
- d.setDate(d.getDate() - (bars.length - 1 - i));
58
- result.push(d);
59
- }
60
- return result;
61
- }, [bars.length]);
62
-
63
- const totalSuccess = bars.reduce((a, b) => a + b.success, 0);
64
- const totalFailed = bars.reduce((a, b) => a + b.failed, 0);
65
- const totalAll = totalSuccess + totalFailed;
66
- const uptimePercent = totalAll > 0 ? Math.round((totalSuccess / totalAll) * 100) : 100;
67
-
68
- function handleMouseEnter(e, idx) {
69
- const rect = e.currentTarget.getBoundingClientRect();
70
- const containerRect = containerRef.current.getBoundingClientRect();
71
- setTooltipPos({
72
- x: rect.left - containerRect.left + rect.width / 2,
73
- y: rect.top - containerRect.top - 8,
74
- });
75
- setHovered(idx);
76
- }
77
-
78
- return (
79
- <div ref={containerRef} style={{ ...card, padding: '14px 18px 14px', position: 'relative', ...style }}>
80
- {/* Header */}
81
- <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
82
- <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
83
- {showDot && (
84
- <span style={{
85
- width: 8, height: 8, borderRadius: '50%',
86
- background: dotColor || greenColor, flexShrink: 0,
87
- }} />
88
- )}
89
- <span style={{ fontSize: '.78rem', fontWeight: 700, color: 'var(--primary-text)', letterSpacing: '-.01em' }}>{title}</span>
90
- {subtitle && <span style={{ fontSize: '.65rem', color: 'var(--muted)', fontWeight: 500 }}>{subtitle}</span>}
91
- </div>
92
- {/* Legend */}
93
- <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
94
- <LegendDot color={greenColor} label="Success" />
95
- <LegendDot color={redColor} label="Failed" />
96
- <LegendDot color={emptyColor} label="None" />
97
- </div>
98
- </div>
99
-
100
- {/* Bars */}
101
- <div style={{ display: 'flex', alignItems: 'flex-end', gap: 2, height: barHeight, marginBottom: 6 }}>
102
- {bars.map((b, i) => {
103
- const total = b.success + b.failed;
104
- const hasData = total > 0;
105
- const successPct = hasData ? (b.success / total) * 100 : 0;
106
- const failedPct = hasData ? (b.failed / total) * 100 : 0;
107
-
108
- return (
109
- <div
110
- key={i}
111
- onMouseEnter={(e) => handleMouseEnter(e, i)}
112
- onMouseLeave={() => setHovered(null)}
113
- style={{
114
- flex: 1,
115
- height: '100%',
116
- borderRadius: barRadius,
117
- overflow: 'hidden',
118
- display: 'flex',
119
- flexDirection: 'column',
120
- background: hasData ? 'transparent' : emptyColor,
121
- }}
122
- >
123
- {hasData && (
124
- <>
125
- {/* Failed on bottom */}
126
- {b.failed > 0 && (
127
- <div style={{ height: `${failedPct}%`, background: redColor, marginTop: 'auto' }} />
128
- )}
129
- {/* Success on top */}
130
- {b.success > 0 && (
131
- <div style={{ height: `${successPct}%`, background: greenColor }} />
132
- )}
133
- </>
134
- )}
135
- </div>
136
- );
137
- })}
138
- </div>
139
-
140
- {/* Tooltip */}
141
- {hovered !== null && (
142
- <div style={{
143
- position: 'absolute',
144
- left: tooltipPos.x,
145
- top: tooltipPos.y,
146
- transform: 'translate(-50%, -100%)',
147
- background: 'var(--surface,#fff)',
148
- border: '1px solid var(--border)',
149
- borderRadius: 'var(--rads)',
150
- boxShadow: 'var(--smd, 0 4px 12px rgba(0,0,0,.12))',
151
- padding: '8px 12px',
152
- pointerEvents: 'none',
153
- zIndex: 100,
154
- whiteSpace: 'nowrap',
155
- }}>
156
- <div style={{ fontSize: '.65rem', color: 'var(--muted)', fontWeight: 500, marginBottom: 4 }}>
157
- {formatDate(dates[hovered])}
158
- </div>
159
- <div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
160
- {bars[hovered].success > 0 && (
161
- <div style={{ fontSize: '.7rem', fontWeight: 700, color: greenColor }}>
162
- {bars[hovered].success} success login{bars[hovered].success !== 1 ? 's' : ''}
163
- {bars[hovered].failed > 0 && <span style={{ fontWeight: 500, color: 'var(--muted)' }}> ({Math.round((bars[hovered].success / (bars[hovered].success + bars[hovered].failed)) * 100)}%)</span>}
164
- </div>
165
- )}
166
- {bars[hovered].failed > 0 && (
167
- <div style={{ fontSize: '.7rem', fontWeight: 700, color: redColor }}>
168
- {bars[hovered].failed} failed login{bars[hovered].failed !== 1 ? 's' : ''}
169
- {bars[hovered].success > 0 && <span style={{ fontWeight: 500, color: 'var(--muted)' }}> ({Math.round((bars[hovered].failed / (bars[hovered].success + bars[hovered].failed)) * 100)}%)</span>}
170
- </div>
171
- )}
172
- {bars[hovered].success === 0 && bars[hovered].failed === 0 && (
173
- <div style={{ fontSize: '.7rem', fontWeight: 500, color: 'var(--muted)' }}>
174
- No logins
175
- </div>
176
- )}
177
- </div>
178
- </div>
179
- )}
180
-
181
- {/* Footer */}
182
- <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
183
- <span style={{ fontSize: '.58rem', color: 'var(--muted)', fontWeight: 500 }}>{leftLabel || '90 days ago'}</span>
184
- <span style={{ fontSize: '.78rem', fontWeight: 700, color: greenColor, fontFamily: 'var(--mono)' }}>
185
- {uptimePercent}%
186
- </span>
187
- <span style={{ fontSize: '.58rem', color: 'var(--muted)', fontWeight: 500 }}>{rightLabel || 'Today'}</span>
188
- </div>
189
- </div>
190
- );
191
- }
192
-
193
- function LegendDot({ color, label }) {
194
- return (
195
- <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
196
- <span style={{ width: 7, height: 7, borderRadius: 2, background: color, flexShrink: 0 }} />
197
- <span style={{ fontSize: '.55rem', color: 'var(--muted)', fontWeight: 500 }}>{label}</span>
198
- </div>
199
- );
200
- }
1
+ import { useMemo, useState, useRef } from 'react';
2
+
3
+ const card = {
4
+ background: 'var(--surface,#fff)',
5
+ border: '1px solid var(--border)',
6
+ borderRadius: 'var(--rad)',
7
+ boxShadow: 'var(--shadow, 0 1px 3px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04))',
8
+ };
9
+
10
+ const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
11
+
12
+ function formatDate(date) {
13
+ return `${MONTHS[date.getMonth()]} ${date.getDate()}`;
14
+ }
15
+
16
+ export default function UptimeBar({
17
+ title = 'Uptime',
18
+ subtitle,
19
+ successValues = [],
20
+ failedValues = [],
21
+ totalSlots = 90,
22
+ greenColor = 'var(--success)',
23
+ redColor = 'var(--danger)',
24
+ emptyColor = 'var(--grey-1)',
25
+ barHeight = 28,
26
+ barRadius = 3,
27
+ leftLabel,
28
+ rightLabel,
29
+ showDot = true,
30
+ dotColor,
31
+ style,
32
+ }) {
33
+ const [hovered, setHovered] = useState(null);
34
+ const [tooltipPos, setTooltipPos] = useState({ x: 0, y: 0 });
35
+ const containerRef = useRef(null);
36
+
37
+ const bars = useMemo(() => {
38
+ const len = Math.max(successValues.length, failedValues.length, totalSlots);
39
+ const result = [];
40
+ const stepS = successValues.length / len;
41
+ const stepF = failedValues.length / len;
42
+ for (let i = 0; i < len; i++) {
43
+ const si = Math.min(Math.floor(i * stepS), successValues.length - 1);
44
+ const fi = Math.min(Math.floor(i * stepF), failedValues.length - 1);
45
+ const s = successValues[si] || 0;
46
+ const f = failedValues[fi] || 0;
47
+ result.push({ success: s, failed: f });
48
+ }
49
+ return result;
50
+ }, [successValues, failedValues, totalSlots]);
51
+
52
+ const dates = useMemo(() => {
53
+ const result = [];
54
+ const now = new Date();
55
+ for (let i = 0; i < bars.length; i++) {
56
+ const d = new Date(now);
57
+ d.setDate(d.getDate() - (bars.length - 1 - i));
58
+ result.push(d);
59
+ }
60
+ return result;
61
+ }, [bars.length]);
62
+
63
+ const totalSuccess = bars.reduce((a, b) => a + b.success, 0);
64
+ const totalFailed = bars.reduce((a, b) => a + b.failed, 0);
65
+ const totalAll = totalSuccess + totalFailed;
66
+ const uptimePercent = totalAll > 0 ? Math.round((totalSuccess / totalAll) * 100) : 100;
67
+
68
+ function handleMouseEnter(e, idx) {
69
+ const rect = e.currentTarget.getBoundingClientRect();
70
+ const containerRect = containerRef.current.getBoundingClientRect();
71
+ setTooltipPos({
72
+ x: rect.left - containerRect.left + rect.width / 2,
73
+ y: rect.top - containerRect.top - 8,
74
+ });
75
+ setHovered(idx);
76
+ }
77
+
78
+ return (
79
+ <div ref={containerRef} style={{ ...card, padding: '14px 18px 14px', position: 'relative', ...style }}>
80
+ {/* Header */}
81
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
82
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
83
+ {showDot && (
84
+ <span style={{
85
+ width: 8, height: 8, borderRadius: '50%',
86
+ background: dotColor || greenColor, flexShrink: 0,
87
+ }} />
88
+ )}
89
+ <span style={{ fontSize: '.78rem', fontWeight: 700, color: 'var(--primary-text)', letterSpacing: '-.01em' }}>{title}</span>
90
+ {subtitle && <span style={{ fontSize: '.65rem', color: 'var(--muted)', fontWeight: 500 }}>{subtitle}</span>}
91
+ </div>
92
+ {/* Legend */}
93
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
94
+ <LegendDot color={greenColor} label="Success" />
95
+ <LegendDot color={redColor} label="Failed" />
96
+ <LegendDot color={emptyColor} label="None" />
97
+ </div>
98
+ </div>
99
+
100
+ {/* Bars */}
101
+ <div style={{ display: 'flex', alignItems: 'flex-end', gap: 2, height: barHeight, marginBottom: 6 }}>
102
+ {bars.map((b, i) => {
103
+ const total = b.success + b.failed;
104
+ const hasData = total > 0;
105
+ const successPct = hasData ? (b.success / total) * 100 : 0;
106
+ const failedPct = hasData ? (b.failed / total) * 100 : 0;
107
+
108
+ return (
109
+ <div
110
+ key={i}
111
+ onMouseEnter={(e) => handleMouseEnter(e, i)}
112
+ onMouseLeave={() => setHovered(null)}
113
+ style={{
114
+ flex: 1,
115
+ height: '100%',
116
+ borderRadius: barRadius,
117
+ overflow: 'hidden',
118
+ display: 'flex',
119
+ flexDirection: 'column',
120
+ background: hasData ? 'transparent' : emptyColor,
121
+ }}
122
+ >
123
+ {hasData && (
124
+ <>
125
+ {/* Failed on bottom */}
126
+ {b.failed > 0 && (
127
+ <div style={{ height: `${failedPct}%`, background: redColor, marginTop: 'auto' }} />
128
+ )}
129
+ {/* Success on top */}
130
+ {b.success > 0 && (
131
+ <div style={{ height: `${successPct}%`, background: greenColor }} />
132
+ )}
133
+ </>
134
+ )}
135
+ </div>
136
+ );
137
+ })}
138
+ </div>
139
+
140
+ {/* Tooltip */}
141
+ {hovered !== null && (
142
+ <div style={{
143
+ position: 'absolute',
144
+ left: tooltipPos.x,
145
+ top: tooltipPos.y,
146
+ transform: 'translate(-50%, -100%)',
147
+ background: 'var(--surface,#fff)',
148
+ border: '1px solid var(--border)',
149
+ borderRadius: 'var(--rads)',
150
+ boxShadow: 'var(--smd, 0 4px 12px rgba(0,0,0,.12))',
151
+ padding: '8px 12px',
152
+ pointerEvents: 'none',
153
+ zIndex: 100,
154
+ whiteSpace: 'nowrap',
155
+ }}>
156
+ <div style={{ fontSize: '.65rem', color: 'var(--muted)', fontWeight: 500, marginBottom: 4 }}>
157
+ {formatDate(dates[hovered])}
158
+ </div>
159
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
160
+ {bars[hovered].success > 0 && (
161
+ <div style={{ fontSize: '.7rem', fontWeight: 700, color: greenColor }}>
162
+ {bars[hovered].success} success login{bars[hovered].success !== 1 ? 's' : ''}
163
+ {bars[hovered].failed > 0 && <span style={{ fontWeight: 500, color: 'var(--muted)' }}> ({Math.round((bars[hovered].success / (bars[hovered].success + bars[hovered].failed)) * 100)}%)</span>}
164
+ </div>
165
+ )}
166
+ {bars[hovered].failed > 0 && (
167
+ <div style={{ fontSize: '.7rem', fontWeight: 700, color: redColor }}>
168
+ {bars[hovered].failed} failed login{bars[hovered].failed !== 1 ? 's' : ''}
169
+ {bars[hovered].success > 0 && <span style={{ fontWeight: 500, color: 'var(--muted)' }}> ({Math.round((bars[hovered].failed / (bars[hovered].success + bars[hovered].failed)) * 100)}%)</span>}
170
+ </div>
171
+ )}
172
+ {bars[hovered].success === 0 && bars[hovered].failed === 0 && (
173
+ <div style={{ fontSize: '.7rem', fontWeight: 500, color: 'var(--muted)' }}>
174
+ No logins
175
+ </div>
176
+ )}
177
+ </div>
178
+ </div>
179
+ )}
180
+
181
+ {/* Footer */}
182
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
183
+ <span style={{ fontSize: '.58rem', color: 'var(--muted)', fontWeight: 500 }}>{leftLabel || '90 days ago'}</span>
184
+ <span style={{ fontSize: '.78rem', fontWeight: 700, color: greenColor, fontFamily: 'var(--mono)' }}>
185
+ {uptimePercent}%
186
+ </span>
187
+ <span style={{ fontSize: '.58rem', color: 'var(--muted)', fontWeight: 500 }}>{rightLabel || 'Today'}</span>
188
+ </div>
189
+ </div>
190
+ );
191
+ }
192
+
193
+ function LegendDot({ color, label }) {
194
+ return (
195
+ <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
196
+ <span style={{ width: 7, height: 7, borderRadius: 2, background: color, flexShrink: 0 }} />
197
+ <span style={{ fontSize: '.55rem', color: 'var(--muted)', fontWeight: 500 }}>{label}</span>
198
+ </div>
199
+ );
200
+ }
@@ -1,28 +1,28 @@
1
- export function XTable({ children, style }) {
2
- return <table style={{ width: '100%', borderCollapse: 'collapse', tableLayout: 'fixed', fontSize: '.8rem', ...style }}>{children}</table>;
3
- }
4
- export function XThead({ children }) { return <thead>{children}</thead>; }
5
- export function XTbody({ children }) { return <tbody>{children}</tbody>; }
6
- export function XTr({ children, onClick }) {
7
- return <tr onClick={onClick} style={{ borderBottom: '1px solid var(--border-light, #ebebf0)', transition: 'background .12s', cursor: onClick ? 'pointer' : undefined }}>{children}</tr>;
8
- }
9
- export function XTh({ children, style }) {
10
- return <th style={{ textAlign: 'left', padding: '8px 10px', fontSize: '.68rem', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '.06em', color: 'var(--muted, #9b9daa)', borderBottom: '1px solid var(--border-light, #ebebf0)', ...style }}>{children}</th>;
11
- }
12
- export function XTd({ children, style, className = '', colSpan }) {
13
- return <td style={{ padding: '9px 10px', verticalAlign: 'middle', color: 'var(--primary-text, #0f111c)', ...style }} colSpan={colSpan}>{children}</td>;
14
- }
15
-
16
- export function SkeletonTable({ rows = 6, cols = 5 }) {
17
- return (
18
- <div>
19
- {Array.from({ length: rows }).map((_, i) => (
20
- <div key={i} style={{ display: 'flex', gap: 8, padding: '8px 10px', borderBottom: '1px solid var(--border-light, #ebebf0)' }}>
21
- {Array.from({ length: cols }).map((_, j) => (
22
- <div key={j} style={{ height: 12, flex: 1, background: 'linear-gradient(90deg, var(--grey-1, #f5f5f8) 25%, var(--border-light, #ebebf0) 50%, var(--grey-1, #f5f5f8) 75%)', backgroundSize: '600px 100%', animation: 'shimmer 1.4s ease infinite', borderRadius: 'var(--radxs, 6px)' }} />
23
- ))}
24
- </div>
25
- ))}
26
- </div>
27
- );
28
- }
1
+ export function XTable({ children, style }) {
2
+ return <table style={{ width: '100%', borderCollapse: 'collapse', tableLayout: 'fixed', fontSize: '.8rem', ...style }}>{children}</table>;
3
+ }
4
+ export function XThead({ children }) { return <thead>{children}</thead>; }
5
+ export function XTbody({ children }) { return <tbody>{children}</tbody>; }
6
+ export function XTr({ children, onClick }) {
7
+ return <tr onClick={onClick} style={{ borderBottom: '1px solid var(--border-light, #ebebf0)', transition: 'background .12s', cursor: onClick ? 'pointer' : undefined }}>{children}</tr>;
8
+ }
9
+ export function XTh({ children, style }) {
10
+ return <th style={{ textAlign: 'left', padding: '8px 10px', fontSize: '.68rem', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '.06em', color: 'var(--muted, #9b9daa)', borderBottom: '1px solid var(--border-light, #ebebf0)', ...style }}>{children}</th>;
11
+ }
12
+ export function XTd({ children, style, className = '', colSpan }) {
13
+ return <td style={{ padding: '9px 10px', verticalAlign: 'middle', color: 'var(--primary-text, #0f111c)', ...style }} colSpan={colSpan}>{children}</td>;
14
+ }
15
+
16
+ export function SkeletonTable({ rows = 6, cols = 5 }) {
17
+ return (
18
+ <div>
19
+ {Array.from({ length: rows }).map((_, i) => (
20
+ <div key={i} style={{ display: 'flex', gap: 8, padding: '8px 10px', borderBottom: '1px solid var(--border-light, #ebebf0)' }}>
21
+ {Array.from({ length: cols }).map((_, j) => (
22
+ <div key={j} style={{ height: 12, flex: 1, background: 'linear-gradient(90deg, var(--grey-1, #f5f5f8) 25%, var(--border-light, #ebebf0) 50%, var(--grey-1, #f5f5f8) 75%)', backgroundSize: '600px 100%', animation: 'shimmer 1.4s ease infinite', borderRadius: 'var(--radxs, 6px)' }} />
23
+ ))}
24
+ </div>
25
+ ))}
26
+ </div>
27
+ );
28
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cynx-ui",
3
- "version": "1.3.13",
3
+ "version": "1.3.15",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./index.js"