cynx-ui 1.2.6 → 1.2.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/ActionsMenu.jsx +149 -149
- package/components/ActivityCard.jsx +184 -184
- package/components/Badge.jsx +129 -105
- package/components/Button.jsx +126 -43
- package/components/Card.jsx +62 -62
- package/components/CircularProgress.jsx +163 -163
- package/components/Dropzone.jsx +29 -29
- package/components/HoverCard.jsx +81 -81
- package/components/Input.jsx +181 -121
- package/components/LineProgress.jsx +109 -109
- package/components/Modal.jsx +124 -8
- package/components/Pagination.jsx +41 -41
- package/components/PulseDot.jsx +76 -58
- package/components/Select.jsx +175 -91
- package/components/Skeleton.jsx +32 -44
- package/components/Spinner.jsx +12 -0
- package/components/StatBarCard.jsx +25 -25
- package/components/StatsCard.jsx +114 -40
- package/components/Tabs.jsx +16 -16
- package/components/Toast.jsx +27 -0
- package/components/Toggle.jsx +56 -56
- package/components/UptimeBar.jsx +200 -200
- package/components/XTable.jsx +22 -22
- package/index.js +94 -94
- package/package.json +18 -18
package/components/Select.jsx
CHANGED
|
@@ -2,69 +2,17 @@ import { useState, useRef, useEffect, useMemo } from 'react';
|
|
|
2
2
|
import { createPortal } from 'react-dom';
|
|
3
3
|
import { ChevronDown, Check, X } from 'lucide-react';
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
.sel-wrap{position:relative;width:100%;font-family:var(--f,var(--sans));font-size:var(--fsz,13px)}
|
|
7
|
-
.sel-wrap.disabled{opacity:.5;pointer-events:none}
|
|
8
|
-
.sel-trigger{
|
|
9
|
-
width:100%;display:flex;align-items:center;justify-content:space-between;gap:8px;
|
|
10
|
-
padding:8px 11px;height:35px;
|
|
11
|
-
background:var(--sf,var(--surface));border:1.5px solid var(--bd2,var(--border));
|
|
12
|
-
border-radius:var(--rmd,var(--rads));cursor:pointer;
|
|
13
|
-
font-family:var(--f,var(--sans,inherit));font-size:12.5px;line-height:1;
|
|
14
|
-
color:var(--t,var(--primary-text));outline:none;
|
|
15
|
-
transition:border-color var(--tr,.15s),box-shadow var(--tr,.15s);
|
|
16
|
-
box-shadow:var(--shadow,0 1px 3px rgba(20,22,41,.1))
|
|
17
|
-
}
|
|
18
|
-
.sel-trigger:hover{border-color:var(--bd2,var(--grey-2))}
|
|
19
|
-
.sel-trigger.open{border-color:var(--accent);box-shadow:0 0 0 3px rgba(37,99,235,.1)}
|
|
20
|
-
.sel-trigger-inner{flex:1;min-width:0;display:flex;align-items:center}
|
|
21
|
-
.sel-value{font-weight:400;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
22
|
-
.sel-placeholder{color:var(--t3,var(--grey-3));font-weight:400}
|
|
23
|
-
.sel-chevron{flex-shrink:0;color:var(--t3,var(--grey-3));transition:transform .2s ease}
|
|
24
|
-
.sel-trigger.open .sel-chevron{transform:rotate(180deg)}
|
|
25
|
-
.sel-tags{display:flex;flex-wrap:wrap;gap:4px;flex:1;min-width:0}
|
|
26
|
-
.sel-tag{display:inline-flex;align-items:center;gap:4px;padding:2px 6px;background:var(--pd,var(--accent-bg));border:1px solid var(--accent);border-radius:4px;font-size:11px;font-weight:600;color:var(--accent);white-space:nowrap;max-width:140px;opacity:.85}
|
|
27
|
-
.sel-tag-x{cursor:pointer;flex-shrink:0;opacity:.5;transition:opacity .15s}
|
|
28
|
-
.sel-tag-x:hover{opacity:1}
|
|
29
|
-
.sel-dropdown{
|
|
30
|
-
position:fixed;z-index:9000;
|
|
31
|
-
background:var(--sf,var(--surface));border:1px solid var(--bd,var(--border));
|
|
32
|
-
border-radius:var(--rmd,var(--rads));box-shadow:var(--shadow,0 1px 3px rgba(20,22,41,.1));
|
|
33
|
-
padding:4px;animation:selFadeIn .15s ease;overflow:hidden
|
|
34
|
-
}
|
|
35
|
-
@keyframes selFadeIn{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}
|
|
36
|
-
.sel-search-wrap{padding:4px}
|
|
37
|
-
.sel-search{
|
|
38
|
-
width:100%;padding:7px 10px;height:35px;
|
|
39
|
-
background:var(--sf2,var(--grey-1));border:1.5px solid var(--bd2,var(--border));
|
|
40
|
-
border-radius:var(--rmd,var(--rads));font-family:var(--f,var(--sans));font-size:12.5px;line-height:1;
|
|
41
|
-
color:var(--t,var(--primary-text));outline:none;
|
|
42
|
-
transition:border-color var(--tr,.15s),box-shadow var(--tr,.15s)
|
|
43
|
-
}
|
|
44
|
-
.sel-search::placeholder{color:var(--t3,var(--grey-3))}
|
|
45
|
-
.sel-search:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(37,99,235,.1)}
|
|
46
|
-
.sel-options{max-height:200px;overflow-y:auto}
|
|
47
|
-
.sel-options::-webkit-scrollbar{width:4px}
|
|
48
|
-
.sel-options::-webkit-scrollbar-thumb{background:var(--bd2,var(--grey-2));border-radius:99px}
|
|
49
|
-
.sel-option{
|
|
50
|
-
display:flex;align-items:center;gap:8px;padding:8px 10px;
|
|
51
|
-
border-radius:var(--rmd,var(--rads));cursor:pointer;
|
|
52
|
-
color:var(--t,var(--primary-text));font-size:12.5px;
|
|
53
|
-
font-weight:400 !important;transition:background .1s;user-select:none
|
|
54
|
-
}
|
|
55
|
-
.sel-option:hover{background:var(--sf2,var(--hover-bg))}
|
|
56
|
-
.sel-option.selected{color:var(--accent);background:var(--pd,var(--accent-bg))}
|
|
57
|
-
.sel-option.disabled{opacity:.4;cursor:not-allowed;pointer-events:none}
|
|
58
|
-
.sel-opt-label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
59
|
-
.sel-check{flex-shrink:0;color:var(--accent)}
|
|
60
|
-
.sel-check-box{width:15px;height:15px;border:1.5px solid var(--bd2,var(--border));border-radius:3px;flex-shrink:0;position:relative;transition:all .15s}
|
|
61
|
-
.sel-check-box.checked{background:var(--accent);border-color:var(--accent)}
|
|
62
|
-
.sel-check-box.checked::after{content:'';position:absolute;top:1px;left:3.5px;width:4.5px;height:7px;border:solid #fff;border-width:0 1.5px 1.5px 0;transform:rotate(45deg)}
|
|
63
|
-
.sel-empty{padding:14px 10px;text-align:center;color:var(--t3,var(--grey-3));font-size:11px}
|
|
64
|
-
`;
|
|
65
|
-
|
|
5
|
+
const KEYFRAMES = `@keyframes selFadeIn{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}`;
|
|
66
6
|
let styleInjected = false;
|
|
67
7
|
|
|
8
|
+
function ensureStyles() {
|
|
9
|
+
if (styleInjected || typeof document === 'undefined') return;
|
|
10
|
+
const tag = document.createElement('style');
|
|
11
|
+
tag.textContent = KEYFRAMES;
|
|
12
|
+
document.head.appendChild(tag);
|
|
13
|
+
styleInjected = true;
|
|
14
|
+
}
|
|
15
|
+
|
|
68
16
|
export default function Select({
|
|
69
17
|
options = [],
|
|
70
18
|
value,
|
|
@@ -82,14 +30,7 @@ export default function Select({
|
|
|
82
30
|
const dropRef = useRef(null);
|
|
83
31
|
const inputRef = useRef(null);
|
|
84
32
|
|
|
85
|
-
|
|
86
|
-
if (!styleInjected) {
|
|
87
|
-
const tag = document.createElement('style');
|
|
88
|
-
tag.textContent = STYLES;
|
|
89
|
-
document.head.appendChild(tag);
|
|
90
|
-
styleInjected = true;
|
|
91
|
-
}
|
|
92
|
-
}, []);
|
|
33
|
+
ensureStyles();
|
|
93
34
|
|
|
94
35
|
useEffect(() => {
|
|
95
36
|
if (!open) return;
|
|
@@ -165,47 +106,190 @@ export default function Select({
|
|
|
165
106
|
const selectedValues = multi ? (Array.isArray(value) ? value : []) : (value ? [value] : []);
|
|
166
107
|
|
|
167
108
|
return (
|
|
168
|
-
<div
|
|
169
|
-
|
|
170
|
-
|
|
109
|
+
<div
|
|
110
|
+
ref={wrapRef}
|
|
111
|
+
style={{
|
|
112
|
+
position: 'relative',
|
|
113
|
+
width: '100%',
|
|
114
|
+
fontFamily: 'var(--f,var(--sans))',
|
|
115
|
+
fontSize: 'var(--fsz,13px)',
|
|
116
|
+
opacity: disabled ? 0.5 : 1,
|
|
117
|
+
pointerEvents: disabled ? 'none' : undefined,
|
|
118
|
+
...style,
|
|
119
|
+
}}
|
|
120
|
+
>
|
|
121
|
+
<div
|
|
122
|
+
onClick={handleOpen}
|
|
123
|
+
style={{
|
|
124
|
+
width: '100%',
|
|
125
|
+
display: 'flex',
|
|
126
|
+
alignItems: 'center',
|
|
127
|
+
justifyContent: 'space-between',
|
|
128
|
+
gap: 8,
|
|
129
|
+
padding: '8px 11px',
|
|
130
|
+
height: 35,
|
|
131
|
+
background: 'var(--sf,var(--surface))',
|
|
132
|
+
border: `1.5px solid ${open ? 'var(--accent)' : 'var(--bd2,var(--border))'}`,
|
|
133
|
+
borderRadius: 'var(--rmd,var(--rads))',
|
|
134
|
+
cursor: 'pointer',
|
|
135
|
+
fontFamily: 'var(--f,var(--sans,inherit))',
|
|
136
|
+
fontSize: '12.5px',
|
|
137
|
+
lineHeight: 1,
|
|
138
|
+
color: 'var(--t,var(--primary-text))',
|
|
139
|
+
outline: 'none',
|
|
140
|
+
transition: 'border-color var(--tr,.15s), box-shadow var(--tr,.15s)',
|
|
141
|
+
boxShadow: open ? '0 0 0 3px rgba(37,99,235,.1)' : 'var(--shadow,0 1px 3px rgba(20,22,41,.1))',
|
|
142
|
+
}}
|
|
143
|
+
>
|
|
144
|
+
<div style={{ flex: 1, minWidth: 0, display: 'flex', alignItems: 'center' }}>
|
|
171
145
|
{multi && selectedValues.length > 0 ? (
|
|
172
|
-
<div
|
|
146
|
+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, flex: 1, minWidth: 0 }}>
|
|
173
147
|
{selectedValues.map(v => (
|
|
174
|
-
<span key={v}
|
|
148
|
+
<span key={v} style={{
|
|
149
|
+
display: 'inline-flex',
|
|
150
|
+
alignItems: 'center',
|
|
151
|
+
gap: 4,
|
|
152
|
+
padding: '2px 6px',
|
|
153
|
+
background: 'var(--pd,var(--accent-bg))',
|
|
154
|
+
border: '1px solid var(--accent)',
|
|
155
|
+
borderRadius: 4,
|
|
156
|
+
fontSize: 11,
|
|
157
|
+
fontWeight: 600,
|
|
158
|
+
color: 'var(--accent)',
|
|
159
|
+
whiteSpace: 'nowrap',
|
|
160
|
+
maxWidth: 140,
|
|
161
|
+
opacity: 0.85,
|
|
162
|
+
}}>
|
|
175
163
|
{getLabel(v)}
|
|
176
|
-
<X
|
|
164
|
+
<X
|
|
165
|
+
size={11}
|
|
166
|
+
style={{ cursor: 'pointer', flexShrink: 0, opacity: 0.5 }}
|
|
167
|
+
onMouseDown={(e) => removeTag(e, v)}
|
|
168
|
+
/>
|
|
177
169
|
</span>
|
|
178
170
|
))}
|
|
179
171
|
</div>
|
|
180
172
|
) : !multi && selectedValues.length > 0 ? (
|
|
181
|
-
<span
|
|
173
|
+
<span style={{ fontWeight: 400, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
|
174
|
+
{getLabel(selectedValues[0])}
|
|
175
|
+
</span>
|
|
182
176
|
) : (
|
|
183
|
-
<span
|
|
177
|
+
<span style={{ color: 'var(--t3,var(--grey-3))', fontWeight: 400 }}>{placeholder}</span>
|
|
184
178
|
)}
|
|
185
179
|
</div>
|
|
186
|
-
<ChevronDown
|
|
180
|
+
<ChevronDown
|
|
181
|
+
size={13}
|
|
182
|
+
style={{
|
|
183
|
+
flexShrink: 0,
|
|
184
|
+
color: 'var(--t3,var(--grey-3))',
|
|
185
|
+
transition: 'transform .2s ease',
|
|
186
|
+
transform: open ? 'rotate(180deg)' : undefined,
|
|
187
|
+
}}
|
|
188
|
+
/>
|
|
187
189
|
</div>
|
|
188
190
|
|
|
189
191
|
{open && createPortal(
|
|
190
|
-
<div
|
|
192
|
+
<div
|
|
193
|
+
ref={dropRef}
|
|
194
|
+
style={{
|
|
195
|
+
position: 'fixed',
|
|
196
|
+
zIndex: 9000,
|
|
197
|
+
top: pos.top,
|
|
198
|
+
left: pos.left,
|
|
199
|
+
width: pos.width,
|
|
200
|
+
background: 'var(--sf,var(--surface))',
|
|
201
|
+
border: '1px solid var(--bd,var(--border))',
|
|
202
|
+
borderRadius: 'var(--rmd,var(--rads))',
|
|
203
|
+
boxShadow: 'var(--shadow,0 1px 3px rgba(20,22,41,.1))',
|
|
204
|
+
padding: 4,
|
|
205
|
+
animation: 'selFadeIn .15s ease',
|
|
206
|
+
overflow: 'hidden',
|
|
207
|
+
}}
|
|
208
|
+
>
|
|
191
209
|
{searchable && (
|
|
192
|
-
<div
|
|
193
|
-
<input
|
|
194
|
-
|
|
195
|
-
|
|
210
|
+
<div style={{ padding: 4 }}>
|
|
211
|
+
<input
|
|
212
|
+
ref={inputRef}
|
|
213
|
+
type="text"
|
|
214
|
+
placeholder="Search..."
|
|
215
|
+
value={query}
|
|
216
|
+
onChange={e => setQuery(e.target.value)}
|
|
217
|
+
onMouseDown={e => e.stopPropagation()}
|
|
218
|
+
style={{
|
|
219
|
+
width: '100%',
|
|
220
|
+
padding: '7px 10px',
|
|
221
|
+
height: 35,
|
|
222
|
+
background: 'var(--sf2,var(--grey-1))',
|
|
223
|
+
border: '1.5px solid var(--bd2,var(--border))',
|
|
224
|
+
borderRadius: 'var(--rmd,var(--rads))',
|
|
225
|
+
fontFamily: 'var(--f,var(--sans))',
|
|
226
|
+
fontSize: '12.5px',
|
|
227
|
+
lineHeight: 1,
|
|
228
|
+
color: 'var(--t,var(--primary-text))',
|
|
229
|
+
outline: 'none',
|
|
230
|
+
transition: 'border-color var(--tr,.15s), box-shadow var(--tr,.15s)',
|
|
231
|
+
}}
|
|
232
|
+
/>
|
|
196
233
|
</div>
|
|
197
234
|
)}
|
|
198
|
-
<div
|
|
235
|
+
<div style={{ maxHeight: 200, overflowY: 'auto' }}>
|
|
199
236
|
{filtered.length === 0 ? (
|
|
200
|
-
<div
|
|
237
|
+
<div style={{ padding: '14px 10px', textAlign: 'center', color: 'var(--t3,var(--grey-3))', fontSize: 11 }}>
|
|
238
|
+
No options found
|
|
239
|
+
</div>
|
|
201
240
|
) : (
|
|
202
241
|
filtered.map(opt => (
|
|
203
|
-
<div
|
|
204
|
-
|
|
205
|
-
onMouseDown={(e) => { e.preventDefault(); e.stopPropagation(); if (!opt.disabled) toggleOption(opt.value); }}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
242
|
+
<div
|
|
243
|
+
key={opt.value}
|
|
244
|
+
onMouseDown={(e) => { e.preventDefault(); e.stopPropagation(); if (!opt.disabled) toggleOption(opt.value); }}
|
|
245
|
+
style={{
|
|
246
|
+
display: 'flex',
|
|
247
|
+
alignItems: 'center',
|
|
248
|
+
gap: 8,
|
|
249
|
+
padding: '8px 10px',
|
|
250
|
+
borderRadius: 'var(--rmd,var(--rads))',
|
|
251
|
+
cursor: opt.disabled ? 'not-allowed' : 'pointer',
|
|
252
|
+
color: isSelected(opt.value) ? 'var(--accent)' : 'var(--t,var(--primary-text))',
|
|
253
|
+
background: isSelected(opt.value) ? 'var(--pd,var(--accent-bg))' : undefined,
|
|
254
|
+
fontSize: '12.5px',
|
|
255
|
+
fontWeight: 400,
|
|
256
|
+
transition: 'background .1s',
|
|
257
|
+
userSelect: 'none',
|
|
258
|
+
opacity: opt.disabled ? 0.4 : 1,
|
|
259
|
+
pointerEvents: opt.disabled ? 'none' : undefined,
|
|
260
|
+
}}
|
|
261
|
+
>
|
|
262
|
+
{multi && (
|
|
263
|
+
<span style={{
|
|
264
|
+
width: 15,
|
|
265
|
+
height: 15,
|
|
266
|
+
border: `1.5px solid ${isSelected(opt.value) ? 'var(--accent)' : 'var(--bd2,var(--border))'}`,
|
|
267
|
+
borderRadius: 3,
|
|
268
|
+
flexShrink: 0,
|
|
269
|
+
position: 'relative',
|
|
270
|
+
transition: 'all .15s',
|
|
271
|
+
background: isSelected(opt.value) ? 'var(--accent)' : undefined,
|
|
272
|
+
}}>
|
|
273
|
+
{isSelected(opt.value) && (
|
|
274
|
+
<span style={{
|
|
275
|
+
position: 'absolute',
|
|
276
|
+
top: 1,
|
|
277
|
+
left: 3.5,
|
|
278
|
+
width: 4.5,
|
|
279
|
+
height: 7,
|
|
280
|
+
border: 'solid #fff',
|
|
281
|
+
borderWidth: '0 1.5px 1.5px 0',
|
|
282
|
+
transform: 'rotate(45deg)',
|
|
283
|
+
}} />
|
|
284
|
+
)}
|
|
285
|
+
</span>
|
|
286
|
+
)}
|
|
287
|
+
<span style={{ flex: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
|
288
|
+
{opt.label}
|
|
289
|
+
</span>
|
|
290
|
+
{!multi && isSelected(opt.value) && (
|
|
291
|
+
<Check size={13} style={{ flexShrink: 0, color: 'var(--accent)' }} />
|
|
292
|
+
)}
|
|
209
293
|
</div>
|
|
210
294
|
))
|
|
211
295
|
)}
|
package/components/Skeleton.jsx
CHANGED
|
@@ -1,58 +1,35 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reusable skeleton loading component.
|
|
3
|
-
*
|
|
4
|
-
* Usage:
|
|
5
|
-
* <Skeleton /> — single block (default 100% x 12px)
|
|
6
|
-
* <Skeleton width={80} height={12} /> — custom size
|
|
7
|
-
* <Skeleton circle size={32} /> — circle (avatar, icon)
|
|
8
|
-
* <SkeletonText lines={3} /> — multiple text lines
|
|
9
|
-
* <SkeletonCard /> — card-shaped skeleton
|
|
10
|
-
* <SkeletonCard rows={4} /> — card with more rows
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
1
|
const STYLES = `
|
|
14
|
-
@keyframes sk-shimmer {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
.sk {
|
|
19
|
-
background: linear-gradient(90deg, var(--grey-1, #f0f1f5) 25%, var(--border-light, #e8e8ec) 50%, var(--grey-1, #f0f1f5) 75%);
|
|
20
|
-
background-size: 600px 100%;
|
|
21
|
-
animation: sk-shimmer 1.4s ease infinite;
|
|
22
|
-
border-radius: var(--radxs, 4px);
|
|
23
|
-
flex-shrink: 0;
|
|
24
|
-
}
|
|
25
|
-
.sk-card {
|
|
26
|
-
background: var(--surface, #fff);
|
|
27
|
-
border: 1px solid var(--border-light, #ebebf0);
|
|
28
|
-
border-radius: var(--rad, 10px);
|
|
29
|
-
padding: 14px 16px;
|
|
30
|
-
box-shadow: 0 1px 3px rgba(0,0,0,.04);
|
|
31
|
-
}
|
|
32
|
-
.sk-card-head {
|
|
33
|
-
display: flex; align-items: center; justify-content: space-between;
|
|
34
|
-
margin-bottom: 12px;
|
|
35
|
-
}
|
|
2
|
+
@keyframes sk-shimmer{0%{background-position:-600px 0}100%{background-position:600px 0}}
|
|
3
|
+
.skeleton{background:linear-gradient(90deg,var(--grey-1,#f0f1f5) 25%,var(--border-light,#e8e8ec) 50%,var(--grey-1,#f0f1f5) 75%);background-size:600px 100%;animation:sk-shimmer 1.4s ease infinite;border-radius:var(--radxs,4px)}
|
|
4
|
+
.skeleton-text{height:12px;margin-bottom:6px}
|
|
36
5
|
`;
|
|
37
|
-
|
|
38
|
-
let injected = false;
|
|
6
|
+
let styleInjected = false;
|
|
39
7
|
|
|
40
8
|
function ensureStyles() {
|
|
41
|
-
if (
|
|
9
|
+
if (styleInjected || typeof document === 'undefined') return;
|
|
42
10
|
const tag = document.createElement('style');
|
|
43
11
|
tag.textContent = STYLES;
|
|
44
12
|
document.head.appendChild(tag);
|
|
45
|
-
|
|
13
|
+
styleInjected = true;
|
|
46
14
|
}
|
|
47
15
|
|
|
16
|
+
const SK_BASE = {
|
|
17
|
+
background: 'linear-gradient(90deg, var(--grey-1, #f0f1f5) 25%, var(--border-light, #e8e8ec) 50%, var(--grey-1, #f0f1f5) 75%)',
|
|
18
|
+
backgroundSize: '600px 100%',
|
|
19
|
+
animation: 'sk-shimmer 1.4s ease infinite',
|
|
20
|
+
borderRadius: 'var(--radxs, 4px)',
|
|
21
|
+
flexShrink: 0,
|
|
22
|
+
};
|
|
23
|
+
|
|
48
24
|
export function Skeleton({ width = '100%', height = 12, circle, size, radius, style = {}, className = '' }) {
|
|
49
25
|
ensureStyles();
|
|
50
26
|
const w = circle ? size : width;
|
|
51
27
|
const h = circle ? size : height;
|
|
52
28
|
return (
|
|
53
29
|
<div
|
|
54
|
-
className={
|
|
30
|
+
className={className}
|
|
55
31
|
style={{
|
|
32
|
+
...SK_BASE,
|
|
56
33
|
width: w,
|
|
57
34
|
height: h,
|
|
58
35
|
borderRadius: circle ? '50%' : radius,
|
|
@@ -71,11 +48,10 @@ export function SkeletonText({ lines = 3, widths, style = {} }) {
|
|
|
71
48
|
{Array.from({ length: lines }, (_, i) => (
|
|
72
49
|
<div
|
|
73
50
|
key={i}
|
|
74
|
-
className="sk"
|
|
75
51
|
style={{
|
|
52
|
+
...SK_BASE,
|
|
76
53
|
width: ws[i % ws.length],
|
|
77
54
|
height: 12,
|
|
78
|
-
borderRadius: 'var(--radxs, 4px)',
|
|
79
55
|
}}
|
|
80
56
|
/>
|
|
81
57
|
))}
|
|
@@ -86,8 +62,20 @@ export function SkeletonText({ lines = 3, widths, style = {} }) {
|
|
|
86
62
|
export function SkeletonCard({ rows = 3, style = {} }) {
|
|
87
63
|
ensureStyles();
|
|
88
64
|
return (
|
|
89
|
-
<div
|
|
90
|
-
|
|
65
|
+
<div style={{
|
|
66
|
+
background: 'var(--surface, #fff)',
|
|
67
|
+
border: '1px solid var(--border-light, #ebebf0)',
|
|
68
|
+
borderRadius: 'var(--rad, 10px)',
|
|
69
|
+
padding: '14px 16px',
|
|
70
|
+
boxShadow: '0 1px 3px rgba(0,0,0,.04)',
|
|
71
|
+
...style,
|
|
72
|
+
}}>
|
|
73
|
+
<div style={{
|
|
74
|
+
display: 'flex',
|
|
75
|
+
alignItems: 'center',
|
|
76
|
+
justifyContent: 'space-between',
|
|
77
|
+
marginBottom: 12,
|
|
78
|
+
}}>
|
|
91
79
|
<Skeleton width={140} height={14} />
|
|
92
80
|
<Skeleton width={70} height={26} radius="var(--rads, 6px)" />
|
|
93
81
|
</div>
|
|
@@ -103,7 +91,7 @@ export function SkeletonRows({ cols = 5, rows = 5 }) {
|
|
|
103
91
|
<tr key={i}>
|
|
104
92
|
{Array.from({ length: cols }).map((_, j) => (
|
|
105
93
|
<td key={j}>
|
|
106
|
-
<div
|
|
94
|
+
<div style={{ ...SK_BASE, width: `${50 + Math.random() * 40}%`, height: 12 }} />
|
|
107
95
|
</td>
|
|
108
96
|
))}
|
|
109
97
|
</tr>
|
package/components/Spinner.jsx
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
|
+
const STYLES = `.spin{animation:cx-spin .7s linear infinite}@keyframes cx-spin{to{transform:rotate(360deg)}}`;
|
|
2
|
+
|
|
3
|
+
let injected = false;
|
|
4
|
+
function ensureStyles() {
|
|
5
|
+
if (injected || typeof document === 'undefined') return;
|
|
6
|
+
const tag = document.createElement('style');
|
|
7
|
+
tag.textContent = STYLES;
|
|
8
|
+
document.head.appendChild(tag);
|
|
9
|
+
injected = true;
|
|
10
|
+
}
|
|
11
|
+
|
|
1
12
|
export function Spinner({ size = 20, inline = false, color = 'currentColor', fullPage = false }) {
|
|
13
|
+
ensureStyles();
|
|
2
14
|
const s = inline ? 14 : size;
|
|
3
15
|
const svg = (
|
|
4
16
|
<svg className="spin" width={s} height={s} viewBox="0 0 24 24" fill="none"
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
import LineProgress from './LineProgress';
|
|
2
|
-
|
|
3
|
-
export default function StatBarCard({ title, icon, items, style }) {
|
|
4
|
-
const total = items.reduce((s, i) => s + (i.value || 0), 0);
|
|
5
|
-
|
|
6
|
-
return (
|
|
7
|
-
<div style={{
|
|
8
|
-
background: '#fff', border: '1px solid var(--border)', borderRadius: 'var(--rad)',
|
|
9
|
-
boxShadow: '0 1px 3px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04)',
|
|
10
|
-
padding: '12px 14px', minWidth: 220, ...style,
|
|
11
|
-
}}>
|
|
12
|
-
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 10 }}>
|
|
13
|
-
<div style={{ fontSize: '.58rem', fontWeight: 700, color: 'var(--muted, #9ca3af)', textTransform: 'uppercase', letterSpacing: '.08em' }}>
|
|
14
|
-
{title}{total ? ` (${total})` : ''}
|
|
15
|
-
</div>
|
|
16
|
-
{icon && <span style={{ color: 'var(--muted, #9ca3af)' }}>{icon}</span>}
|
|
17
|
-
</div>
|
|
18
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
19
|
-
{items.map((item, i) => (
|
|
20
|
-
<LineProgress key={i} value={item.value} total={total} color={item.color} label={item.label} size="sm" />
|
|
21
|
-
))}
|
|
22
|
-
</div>
|
|
23
|
-
</div>
|
|
24
|
-
);
|
|
25
|
-
}
|
|
1
|
+
import LineProgress from './LineProgress';
|
|
2
|
+
|
|
3
|
+
export default function StatBarCard({ title, icon, items, style }) {
|
|
4
|
+
const total = items.reduce((s, i) => s + (i.value || 0), 0);
|
|
5
|
+
|
|
6
|
+
return (
|
|
7
|
+
<div style={{
|
|
8
|
+
background: '#fff', border: '1px solid var(--border)', borderRadius: 'var(--rad)',
|
|
9
|
+
boxShadow: '0 1px 3px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04)',
|
|
10
|
+
padding: '12px 14px', minWidth: 220, ...style,
|
|
11
|
+
}}>
|
|
12
|
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 10 }}>
|
|
13
|
+
<div style={{ fontSize: '.58rem', fontWeight: 700, color: 'var(--muted, #9ca3af)', textTransform: 'uppercase', letterSpacing: '.08em' }}>
|
|
14
|
+
{title}{total ? ` (${total})` : ''}
|
|
15
|
+
</div>
|
|
16
|
+
{icon && <span style={{ color: 'var(--muted, #9ca3af)' }}>{icon}</span>}
|
|
17
|
+
</div>
|
|
18
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
19
|
+
{items.map((item, i) => (
|
|
20
|
+
<LineProgress key={i} value={item.value} total={total} color={item.color} label={item.label} size="sm" />
|
|
21
|
+
))}
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
}
|