cynx-ui 1.2.7 → 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/Skeleton.jsx +6 -2
- package/components/Spinner.jsx +48 -36
- package/components/Toast.jsx +84 -57
- package/package.json +1 -1
package/components/Skeleton.jsx
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
const
|
|
1
|
+
const STYLES = `
|
|
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}
|
|
5
|
+
`;
|
|
2
6
|
let styleInjected = false;
|
|
3
7
|
|
|
4
8
|
function ensureStyles() {
|
|
5
9
|
if (styleInjected || typeof document === 'undefined') return;
|
|
6
10
|
const tag = document.createElement('style');
|
|
7
|
-
tag.textContent =
|
|
11
|
+
tag.textContent = STYLES;
|
|
8
12
|
document.head.appendChild(tag);
|
|
9
13
|
styleInjected = true;
|
|
10
14
|
}
|
package/components/Spinner.jsx
CHANGED
|
@@ -1,36 +1,48 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
);
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
+
|
|
12
|
+
export function Spinner({ size = 20, inline = false, color = 'currentColor', fullPage = false }) {
|
|
13
|
+
ensureStyles();
|
|
14
|
+
const s = inline ? 14 : size;
|
|
15
|
+
const svg = (
|
|
16
|
+
<svg className="spin" width={s} height={s} viewBox="0 0 24 24" fill="none"
|
|
17
|
+
stroke={color} strokeWidth="2.5" strokeLinecap="round">
|
|
18
|
+
<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"/>
|
|
19
|
+
</svg>
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
if (fullPage) {
|
|
23
|
+
return (
|
|
24
|
+
<div style={{
|
|
25
|
+
position: 'fixed', inset: 0,
|
|
26
|
+
background: 'var(--bg)',
|
|
27
|
+
display: 'flex', flexDirection: 'column',
|
|
28
|
+
alignItems: 'center', justifyContent: 'center', gap: 16,
|
|
29
|
+
zIndex: 9999,
|
|
30
|
+
}}>
|
|
31
|
+
{svg}
|
|
32
|
+
<div style={{ fontSize: '.75rem', fontFamily: 'var(--mono)', color: 'var(--muted)' }}>
|
|
33
|
+
Loading...
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (inline) return svg;
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '48px 0' }}>
|
|
43
|
+
{svg}
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default Spinner;
|
package/components/Toast.jsx
CHANGED
|
@@ -1,57 +1,84 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
-
import { createPortal } from 'react-dom';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
|
|
4
|
+
const STYLES = `
|
|
5
|
+
#toast-root { position: fixed; bottom: 20px; right: 20px; display: flex; flex-direction: column; gap: 8px; z-index: 99999; }
|
|
6
|
+
.toast {
|
|
7
|
+
background: var(--surface, #fff); border: 1px solid var(--border, #d7d8e0);
|
|
8
|
+
border-radius: var(--rads, 8px); padding: 10px 14px;
|
|
9
|
+
font-size: .78rem; display: flex; align-items: center; gap: 8px;
|
|
10
|
+
box-shadow: 0px 40px 100px 0px rgba(0,0,0,.15); max-width: 300px;
|
|
11
|
+
color: var(--primary-text, #0f111c); font-weight: 500;
|
|
12
|
+
animation: cx-toastSlide .18s ease;
|
|
13
|
+
cursor: pointer;
|
|
14
|
+
}
|
|
15
|
+
.toast.ok { border-left: 3px solid var(--success, #27ae60); }
|
|
16
|
+
.toast.err { border-left: 3px solid var(--danger, #f64747); }
|
|
17
|
+
@keyframes cx-toastSlide { from { opacity: 0; transform: translateX(16px); } to { opacity: 1; transform: translateX(0); } }
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
let injected = false;
|
|
21
|
+
function ensureStyles() {
|
|
22
|
+
if (injected || typeof document === 'undefined') return;
|
|
23
|
+
const tag = document.createElement('style');
|
|
24
|
+
tag.textContent = STYLES;
|
|
25
|
+
document.head.appendChild(tag);
|
|
26
|
+
injected = true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ── Module-level state ────────────────────────────────────────────────────────
|
|
30
|
+
let _toasts = [];
|
|
31
|
+
let _listeners = new Set();
|
|
32
|
+
|
|
33
|
+
function notify() {
|
|
34
|
+
const snapshot = [..._toasts];
|
|
35
|
+
_listeners.forEach(fn => fn(snapshot));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Global toast — call from anywhere, no hook needed
|
|
39
|
+
export function toast(msg, type = 'ok') {
|
|
40
|
+
ensureStyles();
|
|
41
|
+
const id = Date.now() + Math.random();
|
|
42
|
+
_toasts = [..._toasts, { id, msg, type }];
|
|
43
|
+
notify();
|
|
44
|
+
setTimeout(() => {
|
|
45
|
+
_toasts = _toasts.filter(t => t.id !== id);
|
|
46
|
+
notify();
|
|
47
|
+
}, 3500);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── Hook — for components that want reactive state ────────────────────────────
|
|
51
|
+
export function useToast() {
|
|
52
|
+
const [toasts, setToasts] = useState(_toasts);
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
_listeners.add(setToasts);
|
|
56
|
+
return () => _listeners.delete(setToasts);
|
|
57
|
+
}, []);
|
|
58
|
+
|
|
59
|
+
const remove = useCallback(id => {
|
|
60
|
+
_toasts = _toasts.filter(t => t.id !== id);
|
|
61
|
+
notify();
|
|
62
|
+
}, []);
|
|
63
|
+
|
|
64
|
+
return { toasts, toast, remove };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── Render component ──────────────────────────────────────────────────────────
|
|
68
|
+
export function Toast({ toasts, remove }) {
|
|
69
|
+
ensureStyles();
|
|
70
|
+
return createPortal(
|
|
71
|
+
<div id="toast-root">
|
|
72
|
+
{toasts.map(t => (
|
|
73
|
+
<div key={t.id} className={`toast ${t.type}`} onClick={() => remove(t.id)}>
|
|
74
|
+
{t.type === 'ok'
|
|
75
|
+
? <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--success)" strokeWidth="2.5" strokeLinecap="round"><polyline points="20 6 9 17 4 12"/></svg>
|
|
76
|
+
: <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--danger)" strokeWidth="2.5" strokeLinecap="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>
|
|
77
|
+
}
|
|
78
|
+
{t.msg}
|
|
79
|
+
</div>
|
|
80
|
+
))}
|
|
81
|
+
</div>,
|
|
82
|
+
document.body
|
|
83
|
+
);
|
|
84
|
+
}
|