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.
@@ -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 STYLES = `
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
- useEffect(() => {
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 className={`sel-wrap${disabled ? ' disabled' : ''}${multi ? ' multi' : ''}`} ref={wrapRef} style={style}>
169
- <div className={`sel-trigger${open ? ' open' : ''}`} onClick={handleOpen}>
170
- <div className="sel-trigger-inner">
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 className="sel-tags">
146
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, flex: 1, minWidth: 0 }}>
173
147
  {selectedValues.map(v => (
174
- <span key={v} className="sel-tag">
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 size={11} className="sel-tag-x" onMouseDown={(e) => removeTag(e, v)} />
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 className="sel-value">{getLabel(selectedValues[0])}</span>
173
+ <span style={{ fontWeight: 400, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
174
+ {getLabel(selectedValues[0])}
175
+ </span>
182
176
  ) : (
183
- <span className="sel-placeholder">{placeholder}</span>
177
+ <span style={{ color: 'var(--t3,var(--grey-3))', fontWeight: 400 }}>{placeholder}</span>
184
178
  )}
185
179
  </div>
186
- <ChevronDown size={13} className="sel-chevron" />
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 ref={dropRef} className="sel-dropdown" style={{ top: pos.top, left: pos.left, width: pos.width }}>
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 className="sel-search-wrap">
193
- <input ref={inputRef} className="sel-search" type="text" placeholder="Search..."
194
- value={query} onChange={e => setQuery(e.target.value)}
195
- onMouseDown={e => e.stopPropagation()} />
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 className="sel-options">
235
+ <div style={{ maxHeight: 200, overflowY: 'auto' }}>
199
236
  {filtered.length === 0 ? (
200
- <div className="sel-empty">No options found</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 key={opt.value}
204
- className={`sel-option${isSelected(opt.value) ? ' selected' : ''}${opt.disabled ? ' disabled' : ''}`}
205
- onMouseDown={(e) => { e.preventDefault(); e.stopPropagation(); if (!opt.disabled) toggleOption(opt.value); }}>
206
- {multi && <span className={`sel-check-box${isSelected(opt.value) ? ' checked' : ''}`} />}
207
- <span className="sel-opt-label">{opt.label}</span>
208
- {!multi && isSelected(opt.value) && <Check size={13} className="sel-check" />}
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
  )}
@@ -1,58 +1,31 @@
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
- const STYLES = `
14
- @keyframes sk-shimmer {
15
- 0% { background-position: -600px 0; }
16
- 100% { background-position: 600px 0; }
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
- }
36
- `;
37
-
38
- let injected = false;
1
+ const KEYFRAMES = `@keyframes sk-shimmer{0%{background-position:-600px 0}100%{background-position:600px 0}}`;
2
+ let styleInjected = false;
39
3
 
40
4
  function ensureStyles() {
41
- if (injected || typeof document === 'undefined') return;
5
+ if (styleInjected || typeof document === 'undefined') return;
42
6
  const tag = document.createElement('style');
43
- tag.textContent = STYLES;
7
+ tag.textContent = KEYFRAMES;
44
8
  document.head.appendChild(tag);
45
- injected = true;
9
+ styleInjected = true;
46
10
  }
47
11
 
12
+ const SK_BASE = {
13
+ background: 'linear-gradient(90deg, var(--grey-1, #f0f1f5) 25%, var(--border-light, #e8e8ec) 50%, var(--grey-1, #f0f1f5) 75%)',
14
+ backgroundSize: '600px 100%',
15
+ animation: 'sk-shimmer 1.4s ease infinite',
16
+ borderRadius: 'var(--radxs, 4px)',
17
+ flexShrink: 0,
18
+ };
19
+
48
20
  export function Skeleton({ width = '100%', height = 12, circle, size, radius, style = {}, className = '' }) {
49
21
  ensureStyles();
50
22
  const w = circle ? size : width;
51
23
  const h = circle ? size : height;
52
24
  return (
53
25
  <div
54
- className={`sk ${className}`}
26
+ className={className}
55
27
  style={{
28
+ ...SK_BASE,
56
29
  width: w,
57
30
  height: h,
58
31
  borderRadius: circle ? '50%' : radius,
@@ -71,11 +44,10 @@ export function SkeletonText({ lines = 3, widths, style = {} }) {
71
44
  {Array.from({ length: lines }, (_, i) => (
72
45
  <div
73
46
  key={i}
74
- className="sk"
75
47
  style={{
48
+ ...SK_BASE,
76
49
  width: ws[i % ws.length],
77
50
  height: 12,
78
- borderRadius: 'var(--radxs, 4px)',
79
51
  }}
80
52
  />
81
53
  ))}
@@ -86,8 +58,20 @@ export function SkeletonText({ lines = 3, widths, style = {} }) {
86
58
  export function SkeletonCard({ rows = 3, style = {} }) {
87
59
  ensureStyles();
88
60
  return (
89
- <div className="sk-card" style={style}>
90
- <div className="sk-card-head">
61
+ <div style={{
62
+ background: 'var(--surface, #fff)',
63
+ border: '1px solid var(--border-light, #ebebf0)',
64
+ borderRadius: 'var(--rad, 10px)',
65
+ padding: '14px 16px',
66
+ boxShadow: '0 1px 3px rgba(0,0,0,.04)',
67
+ ...style,
68
+ }}>
69
+ <div style={{
70
+ display: 'flex',
71
+ alignItems: 'center',
72
+ justifyContent: 'space-between',
73
+ marginBottom: 12,
74
+ }}>
91
75
  <Skeleton width={140} height={14} />
92
76
  <Skeleton width={70} height={26} radius="var(--rads, 6px)" />
93
77
  </div>
@@ -103,7 +87,7 @@ export function SkeletonRows({ cols = 5, rows = 5 }) {
103
87
  <tr key={i}>
104
88
  {Array.from({ length: cols }).map((_, j) => (
105
89
  <td key={j}>
106
- <div className="sk" style={{ width: `${50 + Math.random() * 40}%`, height: 12 }} />
90
+ <div style={{ ...SK_BASE, width: `${50 + Math.random() * 40}%`, height: 12 }} />
107
91
  </td>
108
92
  ))}
109
93
  </tr>
@@ -1,36 +1,36 @@
1
- export function Spinner({ size = 20, inline = false, color = 'currentColor', fullPage = false }) {
2
- const s = inline ? 14 : size;
3
- const svg = (
4
- <svg className="spin" width={s} height={s} viewBox="0 0 24 24" fill="none"
5
- stroke={color} strokeWidth="2.5" strokeLinecap="round">
6
- <path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/>
7
- </svg>
8
- );
9
-
10
- if (fullPage) {
11
- return (
12
- <div style={{
13
- position: 'fixed', inset: 0,
14
- background: 'var(--bg)',
15
- display: 'flex', flexDirection: 'column',
16
- alignItems: 'center', justifyContent: 'center', gap: 16,
17
- zIndex: 9999,
18
- }}>
19
- {svg}
20
- <div style={{ fontSize: '.75rem', fontFamily: 'var(--mono)', color: 'var(--muted)' }}>
21
- Loading...
22
- </div>
23
- </div>
24
- );
25
- }
26
-
27
- if (inline) return svg;
28
-
29
- return (
30
- <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '48px 0' }}>
31
- {svg}
32
- </div>
33
- );
34
- }
35
-
36
- export default Spinner;
1
+ export function Spinner({ size = 20, inline = false, color = 'currentColor', fullPage = false }) {
2
+ const s = inline ? 14 : size;
3
+ const svg = (
4
+ <svg className="spin" width={s} height={s} viewBox="0 0 24 24" fill="none"
5
+ stroke={color} strokeWidth="2.5" strokeLinecap="round">
6
+ <path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/>
7
+ </svg>
8
+ );
9
+
10
+ if (fullPage) {
11
+ return (
12
+ <div style={{
13
+ position: 'fixed', inset: 0,
14
+ background: 'var(--bg)',
15
+ display: 'flex', flexDirection: 'column',
16
+ alignItems: 'center', justifyContent: 'center', gap: 16,
17
+ zIndex: 9999,
18
+ }}>
19
+ {svg}
20
+ <div style={{ fontSize: '.75rem', fontFamily: 'var(--mono)', color: 'var(--muted)' }}>
21
+ Loading...
22
+ </div>
23
+ </div>
24
+ );
25
+ }
26
+
27
+ if (inline) return svg;
28
+
29
+ return (
30
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '48px 0' }}>
31
+ {svg}
32
+ </div>
33
+ );
34
+ }
35
+
36
+ export default Spinner;
@@ -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
+ }