cynx-lib 1.3.28

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.
Files changed (46) hide show
  1. package/index.js +3 -0
  2. package/package.json +36 -0
  3. package/ui/components/ActionsMenu.jsx +158 -0
  4. package/ui/components/ActivityCard.jsx +184 -0
  5. package/ui/components/AuthOverlay.jsx +94 -0
  6. package/ui/components/Badge.jsx +186 -0
  7. package/ui/components/BrandingCard.jsx +146 -0
  8. package/ui/components/Button.jsx +148 -0
  9. package/ui/components/Card.jsx +132 -0
  10. package/ui/components/Checkbox.jsx +59 -0
  11. package/ui/components/ChipSelect.jsx +54 -0
  12. package/ui/components/CircularProgress.jsx +216 -0
  13. package/ui/components/Confirm.jsx +79 -0
  14. package/ui/components/DatePicker.jsx +605 -0
  15. package/ui/components/Divider.jsx +96 -0
  16. package/ui/components/Dropzone.jsx +203 -0
  17. package/ui/components/FilePicker.jsx +205 -0
  18. package/ui/components/HoverCard.jsx +81 -0
  19. package/ui/components/ImageUpload.jsx +147 -0
  20. package/ui/components/InfoBadge.jsx +204 -0
  21. package/ui/components/Input.jsx +229 -0
  22. package/ui/components/LineProgress.jsx +133 -0
  23. package/ui/components/LinearProgress.jsx +132 -0
  24. package/ui/components/Modal.jsx +212 -0
  25. package/ui/components/Pagination.jsx +52 -0
  26. package/ui/components/PulseDot.jsx +77 -0
  27. package/ui/components/Select.jsx +415 -0
  28. package/ui/components/Skeleton.jsx +118 -0
  29. package/ui/components/Spinner.jsx +108 -0
  30. package/ui/components/StatBarCard.jsx +25 -0
  31. package/ui/components/StatsBar.jsx +88 -0
  32. package/ui/components/StatsCard.jsx +171 -0
  33. package/ui/components/TabBar.jsx +404 -0
  34. package/ui/components/Tabs.jsx +29 -0
  35. package/ui/components/TimePicker.jsx +1069 -0
  36. package/ui/components/Toast.jsx +106 -0
  37. package/ui/components/Toggle.jsx +67 -0
  38. package/ui/components/UptimeBar.jsx +135 -0
  39. package/ui/components/XTable.jsx +44 -0
  40. package/ui/index.js +97 -0
  41. package/utils/index.js +11 -0
  42. package/utils/utils/oidc.js +209 -0
  43. package/utils/utils/realtime.js +1040 -0
  44. package/utils/utils/storage.js +393 -0
  45. package/utils/utils/tz.js +625 -0
  46. package/utils/utils/uid.js +252 -0
@@ -0,0 +1,106 @@
1
+ import { useState, useEffect, useCallback } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+
4
+ const STYLES = `
5
+ #toast-root { position: fixed; top: 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.success { border-left: 3px solid var(--success, #27ae60); }
16
+ .toast.error { border-left: 3px solid var(--danger, #f64747); }
17
+ .toast.ok { border-left: 3px solid var(--success, #27ae60); }
18
+ .toast.err { border-left: 3px solid var(--danger, #f64747); }
19
+ @keyframes cx-toastSlide { from { opacity: 0; transform: translateX(16px); } to { opacity: 1; transform: translateX(0); } }
20
+ `;
21
+
22
+ let _id = 'cynx-toast-styles';
23
+ function ensureStyles() {
24
+ if (typeof document === 'undefined') return;
25
+ if (document.getElementById(_id)) return;
26
+ const tag = document.createElement('style');
27
+ tag.id = _id;
28
+ tag.textContent = STYLES;
29
+ document.head.appendChild(tag);
30
+ }
31
+
32
+ // Auto-inject on module load
33
+ if (typeof document !== 'undefined') ensureStyles();
34
+
35
+ // ── Module-level state ────────────────────────────────────────────────────────
36
+ let _toasts = [];
37
+ let _listeners = new Set();
38
+
39
+ function notify() {
40
+ const snapshot = [..._toasts];
41
+ _listeners.forEach(fn => fn(snapshot));
42
+ }
43
+
44
+ // Global toast — call from anywhere, no hook needed
45
+ export function toast(msg, type = 'ok') {
46
+ ensureStyles();
47
+ const id = Date.now() + Math.random();
48
+ _toasts = [..._toasts, { id, msg, type }];
49
+ notify();
50
+ setTimeout(() => {
51
+ _toasts = _toasts.filter(t => t.id !== id);
52
+ notify();
53
+ }, 3500);
54
+ }
55
+
56
+ // ── Hook — for components that want reactive state ────────────────────────────
57
+ export function useToast() {
58
+ const [toasts, setToasts] = useState(_toasts);
59
+
60
+ useEffect(() => {
61
+ _listeners.add(setToasts);
62
+ return () => _listeners.delete(setToasts);
63
+ }, []);
64
+
65
+ const remove = useCallback(id => {
66
+ _toasts = _toasts.filter(t => t.id !== id);
67
+ notify();
68
+ }, []);
69
+
70
+ return { toasts, toast, remove };
71
+ }
72
+
73
+ const POSITIONS = {
74
+ 'top-right': { top: 20, right: 20, bottom: 'auto', left: 'auto' },
75
+ 'top-left': { top: 20, left: 20, bottom: 'auto', right: 'auto' },
76
+ 'top-center': { top: 20, left: '50%', transform: 'translateX(-50%)', bottom: 'auto', right: 'auto' },
77
+ 'bottom-right': { bottom: 20, right: 20, top: 'auto', left: 'auto' },
78
+ 'bottom-left': { bottom: 20, left: 20, top: 'auto', right: 'auto' },
79
+ 'bottom-center': { bottom: 20, left: '50%', transform: 'translateX(-50%)', top: 'auto', right: 'auto' },
80
+ 'top': { top: 20, right: 20, bottom: 'auto', left: 'auto' },
81
+ 'bottom': { bottom: 20, right: 20, top: 'auto', left: 'auto' },
82
+ };
83
+
84
+ // ── Render component ──────────────────────────────────────────────────────────
85
+ export function Toast({ toasts, remove, position = 'top-right', style = {} }) {
86
+ ensureStyles();
87
+ const internal = useToast();
88
+ const activeToasts = Array.isArray(toasts) ? toasts : internal.toasts;
89
+ const activeRemove = typeof remove === 'function' ? remove : internal.remove;
90
+ const posStyle = POSITIONS[position] || POSITIONS['top-right'];
91
+
92
+ return createPortal(
93
+ <div id="toast-root" style={{ ...posStyle, ...style }}>
94
+ {(activeToasts || []).map(t => (
95
+ <div key={t.id} className={`toast ${t.type}`} onClick={() => activeRemove(t.id)}>
96
+ {t.type === 'ok' || t.type === 'success'
97
+ ? <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--success, #27ae60)" strokeWidth="2.5" strokeLinecap="round"><polyline points="20 6 9 17 4 12"/></svg>
98
+ : <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--danger, #f64747)" 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>
99
+ }
100
+ {t.msg}
101
+ </div>
102
+ ))}
103
+ </div>,
104
+ document.body
105
+ );
106
+ }
@@ -0,0 +1,67 @@
1
+ const SIZES = {
2
+ sm: { w: 28, h: 16, r: 8, dot: 12, off: 2, on: 14 },
3
+ md: { w: 34, h: 18, r: 9, dot: 14, off: 2, on: 18 },
4
+ lg: { w: 44, h: 24, r: 12, dot: 18, off: 3, on: 23 },
5
+ };
6
+
7
+ const COLORS = {
8
+ primary: 'var(--accent, #f99e2c)',
9
+ success: 'var(--s, #27ae60)',
10
+ warning: 'var(--w, #f59e0b)',
11
+ danger: 'var(--d, #f64747)',
12
+ };
13
+
14
+ const OFF_COLOR = 'var(--bd, #d7d8e0)';
15
+
16
+ export default function Toggle({
17
+ checked = false,
18
+ onChange,
19
+ disabled = false,
20
+ size = 'md',
21
+ color = 'primary',
22
+ label = null,
23
+ labelStyle = {},
24
+ onClick = null,
25
+ className = '',
26
+ style = {},
27
+ }) {
28
+ const s = SIZES[size] || SIZES.md;
29
+ const trackBg = checked ? (COLORS[color] || COLORS.primary) : OFF_COLOR;
30
+
31
+ return (
32
+ <label
33
+ className={className}
34
+ style={{
35
+ display:'inline-flex', alignItems:'center', gap:8,
36
+ cursor: disabled ? 'not-allowed' : 'pointer',
37
+ userSelect:'none', opacity: disabled ? .4 : 1,
38
+ ...style,
39
+ }}
40
+ onClick={onClick}
41
+ >
42
+ <input type="checkbox" checked={checked} disabled={disabled}
43
+ onChange={e => onChange?.(e.target.checked)}
44
+ style={{ position:'absolute', opacity:0, width:0, height:0 }} />
45
+ <span style={{
46
+ position:'relative', width: s.w, height: s.h, borderRadius: s.r,
47
+ background: trackBg, flexShrink: 0,
48
+ transition: 'background .25s ease',
49
+ boxShadow: 'var(--shadow, 0 1px 3px rgba(20,22,41,.1))',
50
+ }}>
51
+ <span style={{
52
+ position:'absolute', top:'50%',
53
+ transform: checked
54
+ ? 'translateY(-50%) scale(1)'
55
+ : 'translateY(-50%) scale(1)',
56
+ left: checked ? s.on : s.off,
57
+ width: s.dot, height: s.dot,
58
+ background: 'var(--surface,#fff)', borderRadius: '50%',
59
+ boxShadow: 'var(--shadow, 0 1px 3px rgba(0,0,0,.25))',
60
+ transition: 'left .3s cubic-bezier(.34,1.56,.64,1), transform .2s ease',
61
+ willChange: 'left',
62
+ }} />
63
+ </span>
64
+ {label && <span style={labelStyle}>{label}</span>}
65
+ </label>
66
+ );
67
+ }
@@ -0,0 +1,135 @@
1
+ import { useState, useRef } from 'react';
2
+
3
+ const card = {
4
+ background: 'var(--surface,#fff)',
5
+ border: '1px solid var(--border, #e2e8f0)',
6
+ borderRadius: 'var(--rad, 10px)',
7
+ boxShadow: 'var(--shadow, 0 1px 3px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04))',
8
+ };
9
+
10
+ export default function UptimeBar({
11
+ title,
12
+ subtitle,
13
+ items = [],
14
+ value,
15
+ leftLabel,
16
+ rightLabel,
17
+ legend,
18
+ showDot = true,
19
+ dotColor = 'var(--success, #27ae60)',
20
+ barHeight = 28,
21
+ barRadius = 3,
22
+ emptyColor = 'var(--grey-1, #f5f5f8)',
23
+ style,
24
+ renderTooltip,
25
+ }) {
26
+ const [hovered, setHovered] = useState(null);
27
+ const [tooltipPos, setTooltipPos] = useState({ x: 0, y: 0 });
28
+ const containerRef = useRef(null);
29
+
30
+ function handleMouseEnter(e, idx) {
31
+ const rect = e.currentTarget.getBoundingClientRect();
32
+ const containerRect = containerRef.current.getBoundingClientRect();
33
+ setTooltipPos({
34
+ x: rect.left - containerRect.left + rect.width / 2,
35
+ y: rect.top - containerRect.top - 8,
36
+ });
37
+ setHovered(idx);
38
+ }
39
+
40
+ // Smart tooltip shift so edge items are never clipped
41
+ const tooltipShift = hovered !== null
42
+ ? (hovered > items.length - 8 ? '-92%' : hovered < 8 ? '-8%' : '-50%')
43
+ : '-50%';
44
+
45
+ return (
46
+ <div ref={containerRef} style={{ ...card, padding: '14px 18px 14px', position: 'relative', ...style }}>
47
+ {/* Header */}
48
+ {(title || subtitle || (Array.isArray(legend) && legend.length > 0)) && (
49
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
50
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
51
+ {showDot && (
52
+ <span style={{
53
+ width: 8, height: 8, borderRadius: '50%',
54
+ background: dotColor, flexShrink: 0,
55
+ }} />
56
+ )}
57
+ {title && (
58
+ <span style={{ fontSize: '.78rem', fontWeight: 700, color: 'var(--primary-text, #0f172a)', letterSpacing: '-.01em' }}>
59
+ {title}
60
+ </span>
61
+ )}
62
+ {subtitle && (
63
+ <span style={{ fontSize: '.65rem', color: 'var(--muted, #797b8d)', fontWeight: 500 }}>
64
+ {subtitle}
65
+ </span>
66
+ )}
67
+ </div>
68
+ {/* Legend */}
69
+ {Array.isArray(legend) && legend.length > 0 && (
70
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
71
+ {legend.map((item, idx) => (
72
+ <div key={idx} style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
73
+ <span style={{ width: 7, height: 7, borderRadius: 2, background: item.color, flexShrink: 0 }} />
74
+ <span style={{ fontSize: '.55rem', color: 'var(--muted, #797b8d)', fontWeight: 500 }}>{item.label}</span>
75
+ </div>
76
+ ))}
77
+ </div>
78
+ )}
79
+ </div>
80
+ )}
81
+
82
+ {/* Bars: purely renders precomputed items */}
83
+ <div style={{ display: 'flex', alignItems: 'flex-end', gap: 2, height: barHeight, marginBottom: 6 }}>
84
+ {items.map((item, i) => (
85
+ <div
86
+ key={item.id || i}
87
+ onMouseEnter={(e) => handleMouseEnter(e, i)}
88
+ onMouseLeave={() => setHovered(null)}
89
+ style={{
90
+ flex: 1,
91
+ height: '100%',
92
+ borderRadius: barRadius,
93
+ background: item.color || emptyColor,
94
+ }}
95
+ />
96
+ ))}
97
+ </div>
98
+
99
+ {/* Tooltip: renders caller content only */}
100
+ {hovered !== null && items[hovered] && (renderTooltip || items[hovered].tooltip) && (
101
+ <div style={{
102
+ position: 'absolute',
103
+ left: tooltipPos.x,
104
+ top: tooltipPos.y,
105
+ transform: `translate(${tooltipShift}, -100%)`,
106
+ background: 'var(--surface,#fff)',
107
+ border: '1px solid var(--border, #e2e8f0)',
108
+ borderRadius: 'var(--rads, 8px)',
109
+ boxShadow: 'var(--smd, 0 4px 12px rgba(0,0,0,.12))',
110
+ padding: '8px 12px',
111
+ pointerEvents: 'none',
112
+ zIndex: 1000,
113
+ whiteSpace: 'nowrap',
114
+ }}>
115
+ {renderTooltip
116
+ ? renderTooltip(items[hovered], hovered)
117
+ : typeof items[hovered].tooltip === 'function'
118
+ ? items[hovered].tooltip(items[hovered], hovered)
119
+ : items[hovered].tooltip}
120
+ </div>
121
+ )}
122
+
123
+ {/* Footer */}
124
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
125
+ <span style={{ fontSize: '.58rem', color: 'var(--muted, #797b8d)', fontWeight: 500 }}>{leftLabel}</span>
126
+ {value && (
127
+ <span style={{ fontSize: '.78rem', fontWeight: 700, color: dotColor, fontFamily: 'var(--mono, ui-monospace, monospace)' }}>
128
+ {value}
129
+ </span>
130
+ )}
131
+ <span style={{ fontSize: '.58rem', color: 'var(--muted, #797b8d)', fontWeight: 500 }}>{rightLabel}</span>
132
+ </div>
133
+ </div>
134
+ );
135
+ }
@@ -0,0 +1,44 @@
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
+ const SKELETON_STYLES = `
17
+ @keyframes shimmer{0%{background-position:-600px 0}100%{background-position:600px 0}}
18
+ @keyframes sk-shimmer{0%{background-position:-600px 0}100%{background-position:600px 0}}
19
+ `;
20
+ let _tbl_sk_id = 'cynx-table-skeleton-styles';
21
+ function ensureSkeletonStyles() {
22
+ if (typeof document === 'undefined') return;
23
+ if (document.getElementById(_tbl_sk_id)) return;
24
+ const tag = document.createElement('style');
25
+ tag.id = _tbl_sk_id;
26
+ tag.textContent = SKELETON_STYLES;
27
+ document.head.appendChild(tag);
28
+ }
29
+ if (typeof document !== 'undefined') ensureSkeletonStyles();
30
+
31
+ export function SkeletonTable({ rows = 6, cols = 5 }) {
32
+ ensureSkeletonStyles();
33
+ return (
34
+ <div>
35
+ {Array.from({ length: rows }).map((_, i) => (
36
+ <div key={i} style={{ display: 'flex', gap: 8, padding: '8px 10px', borderBottom: '1px solid var(--border-light, #ebebf0)' }}>
37
+ {Array.from({ length: cols }).map((_, j) => (
38
+ <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)' }} />
39
+ ))}
40
+ </div>
41
+ ))}
42
+ </div>
43
+ );
44
+ }
package/ui/index.js ADDED
@@ -0,0 +1,97 @@
1
+ // cynx/ui — shared component library
2
+ import { default as _Button } from './components/Button.jsx';
3
+ import { default as _Input } from './components/Input.jsx';
4
+ import { default as _Toggle } from './components/Toggle.jsx';
5
+ import { default as _Spinner, Spinner as _SpinnerNamed } from './components/Spinner.jsx';
6
+ import { default as _TabBar } from './components/TabBar.jsx';
7
+ import { default as _ImageUpload } from './components/ImageUpload.jsx';
8
+ import { default as _ChipSelect } from './components/ChipSelect.jsx';
9
+ import { default as _Checkbox } from './components/Checkbox.jsx';
10
+ import { default as _AuthOverlay } from './components/AuthOverlay.jsx';
11
+ import { default as _UptimeBar } from './components/UptimeBar.jsx';
12
+ import { default as _LinearProgress } from './components/LinearProgress.jsx';
13
+ import { default as _StatBarCard } from './components/StatBarCard.jsx';
14
+ import { default as _StatsCard } from './components/StatsCard.jsx';
15
+ import { default as _ActivityCard } from './components/ActivityCard.jsx';
16
+ import { default as _HoverCard } from './components/HoverCard.jsx';
17
+ import { default as _StatsBar } from './components/StatsBar.jsx';
18
+ import { default as _Select } from './components/Select.jsx';
19
+ import { default as _Badge } from './components/Badge.jsx';
20
+ import { default as _PulseDot } from './components/PulseDot.jsx';
21
+ import { default as _CircularProgress } from './components/CircularProgress.jsx';
22
+ import { default as _Skeleton } from './components/Skeleton.jsx';
23
+ import { Modal as _Modal, Confirm as _Confirm } from './components/Modal.jsx';
24
+ import { toast as _toast, useToast as _useToast, Toast as _Toast } from './components/Toast.jsx';
25
+ import { ConfirmRoot as _ConfirmRoot, confirm as _confirm } from './components/Confirm.jsx';
26
+ import { Pagination as _Pagination } from './components/Pagination.jsx';
27
+ import { DatePicker as _DatePicker } from './components/DatePicker.jsx';
28
+ import { TimePicker as _TimePicker } from './components/TimePicker.jsx';
29
+ import { ActionsMenu as _ActionsMenu } from './components/ActionsMenu.jsx';
30
+ import { BrandingCard as _BrandingCard } from './components/BrandingCard.jsx';
31
+ import { Dropzone as _Dropzone } from './components/Dropzone.jsx';
32
+ import { Tabs as _Tabs } from './components/Tabs.jsx';
33
+ import { SkeletonText as _SkeletonText, SkeletonCard as _SkeletonCard, SkeletonRows as _SkeletonRows, SkeletonStats as _SkeletonStats } from './components/Skeleton.jsx';
34
+ import { XTable as _XTable, XThead as _XThead, XTbody as _XTbody, XTr as _XTr, XTh as _XTh, XTd as _XTd, SkeletonTable as _SkeletonTable } from './components/XTable.jsx';
35
+ import { PageShell as _PageShell, TableCard as _TableCard, TableCardFilters as _TableCardFilters, TableCardBody as _TableCardBody, TableCardFooter as _TableCardFooter, CardHdr as _CardHdr, StatCard as _StatCard, StatCardSkeleton as _StatCardSkeleton } from './components/Card.jsx';
36
+
37
+ import { default as _FilePicker } from './components/FilePicker.jsx';
38
+ import { default as _Divider } from './components/Divider.jsx';
39
+ import { default as _InfoBadge } from './components/InfoBadge.jsx';
40
+
41
+ export const Button = _Button;
42
+ export const FilePicker = _FilePicker;
43
+ export const Divider = _Divider;
44
+ export const InfoBadge = _InfoBadge;
45
+ export const Input = _Input;
46
+ export const Toggle = _Toggle;
47
+ export const Spinner = _Spinner || _SpinnerNamed;
48
+ export const TabBar = _TabBar;
49
+ export const ImageUpload = _ImageUpload;
50
+ export const ChipSelect = _ChipSelect;
51
+ export const Checkbox = _Checkbox;
52
+ export const AuthOverlay = _AuthOverlay;
53
+ export const UptimeBar = _UptimeBar;
54
+ export const LinearProgress = _LinearProgress;
55
+ export const StatBarCard = _StatBarCard;
56
+ export const StatsCard = _StatsCard;
57
+ export const ActivityCard = _ActivityCard;
58
+ export const HoverCard = _HoverCard;
59
+ export const StatsBar = _StatsBar;
60
+ export const Select = _Select;
61
+ export const Badge = _Badge;
62
+ export const PulseDot = _PulseDot;
63
+ export const CircularProgress = _CircularProgress;
64
+ export const Skeleton = _Skeleton;
65
+ export const Modal = _Modal;
66
+ export const Confirm = _Confirm;
67
+ export const toast = _toast;
68
+ export const useToast = _useToast;
69
+ export const Toast = _Toast;
70
+ export const ConfirmRoot = _ConfirmRoot;
71
+ export const confirm = _confirm;
72
+ export const Pagination = _Pagination;
73
+ export const DatePicker = _DatePicker;
74
+ export const TimePicker = _TimePicker;
75
+ export const ActionsMenu = _ActionsMenu;
76
+ export const BrandingCard = _BrandingCard;
77
+ export const Dropzone = _Dropzone;
78
+ export const Tabs = _Tabs;
79
+ export const SkeletonText = _SkeletonText;
80
+ export const SkeletonCard = _SkeletonCard;
81
+ export const SkeletonRows = _SkeletonRows;
82
+ export const SkeletonStats = _SkeletonStats;
83
+ export const XTable = _XTable;
84
+ export const XThead = _XThead;
85
+ export const XTbody = _XTbody;
86
+ export const XTr = _XTr;
87
+ export const XTh = _XTh;
88
+ export const XTd = _XTd;
89
+ export const SkeletonTable = _SkeletonTable;
90
+ export const PageShell = _PageShell;
91
+ export const TableCard = _TableCard;
92
+ export const TableCardFilters = _TableCardFilters;
93
+ export const TableCardBody = _TableCardBody;
94
+ export const TableCardFooter = _TableCardFooter;
95
+ export const CardHdr = _CardHdr;
96
+ export const StatCard = _StatCard;
97
+ export const StatCardSkeleton = _StatCardSkeleton;
package/utils/index.js ADDED
@@ -0,0 +1,11 @@
1
+ // cynx/utils — shared utility functions
2
+ export * from './utils/uid.js';
3
+ export * from './utils/realtime.js';
4
+ export * from './utils/tz.js';
5
+ export * from './utils/oidc.js';
6
+
7
+ export { default as uidUtil } from './utils/uid.js';
8
+ export { default as realtimeUtil } from './utils/realtime.js';
9
+ export { default as tzUtil } from './utils/tz.js';
10
+ export { default as oidcUtil } from './utils/oidc.js';
11
+
@@ -0,0 +1,209 @@
1
+ /**
2
+ * cynx/utils — OpenID Connect & OAuth 2.0 Engine (oidc.js)
3
+ *
4
+ * Lightweight, zero-external-dependency OIDC/OAuth 2.0 client utility for Node.js / SSR.
5
+ * Supports Discovery, PKCE (RFC 7636), signed state tokens (CSRF protection),
6
+ * token exchange, and profile resolution.
7
+ */
8
+
9
+ import crypto from 'crypto';
10
+
11
+ const DEFAULT_SECRET = (typeof process !== 'undefined' && (process.env?.OIDC_STATE_SECRET || process.env?.JWT_SECRET)) || 'cynx-oidc-state-secret-salt';
12
+
13
+ /**
14
+ * Generate a cryptographically signed state token with embedded payload and timestamp.
15
+ *
16
+ * @param {object} [payload={}] Data to embed (e.g. { codeVerifier, returnTo })
17
+ * @param {string} [secret] Custom HMAC signing secret
18
+ * @returns {string} Signed state token in format "base64urlData.base64urlSig"
19
+ */
20
+ export function generateState(payload = {}, secret = DEFAULT_SECRET) {
21
+ const data = Buffer.from(JSON.stringify({ ...payload, ts: Date.now() })).toString('base64url');
22
+ const sig = crypto.createHmac('sha256', secret).update(data).digest('base64url');
23
+ return `${data}.${sig}`;
24
+ }
25
+
26
+ /**
27
+ * Verify, parse, and consume a signed state token with replay & TTL validation.
28
+ *
29
+ * @param {string} stateStr The state token received from redirect
30
+ * @param {string} [secret] Custom HMAC signing secret
31
+ * @param {number} [ttlMs=600000] Maximum token lifetime in ms (default: 10 minutes)
32
+ * @returns {object|null} Decoded payload if valid and not expired, null otherwise
33
+ */
34
+ export function consumeState(stateStr, secret = DEFAULT_SECRET, ttlMs = 600_000) {
35
+ if (!stateStr || typeof stateStr !== 'string') return null;
36
+ const parts = stateStr.split('.');
37
+ if (parts.length !== 2) return null;
38
+ const [data, sig] = parts;
39
+ const expectedSig = crypto.createHmac('sha256', secret).update(data).digest('base64url');
40
+
41
+ if (sig.length !== expectedSig.length) return null;
42
+ if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expectedSig))) return null;
43
+
44
+ try {
45
+ const payload = JSON.parse(Buffer.from(data, 'base64url').toString('utf8'));
46
+ if (!payload.ts || Date.now() - payload.ts > ttlMs) return null;
47
+ return payload;
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Generate a secure PKCE (Proof Key for Code Exchange) pair for Authorization Code flow.
55
+ *
56
+ * @returns {{ codeVerifier: string, codeChallenge: string }}
57
+ */
58
+ export function generatePKCE() {
59
+ const codeVerifier = crypto.randomBytes(32).toString('base64url');
60
+ const codeChallenge = crypto.createHash('sha256')
61
+ .update(codeVerifier)
62
+ .digest('base64url');
63
+ return { codeVerifier, codeChallenge };
64
+ }
65
+
66
+ /**
67
+ * Fetch and discover OpenID Connect provider configuration document.
68
+ *
69
+ * @param {string} issuer Base issuer URL (e.g. "https://auth.example.com/realms/master")
70
+ * @param {string} [discoveryPath='/.well-known/openid-configuration'] Custom discovery path
71
+ * @returns {Promise<object>} Discovery metadata (authorization_endpoint, token_endpoint, etc.)
72
+ */
73
+ export async function discover(issuer, discoveryPath = '/.well-known/openid-configuration') {
74
+ let base = issuer.replace(/\/+$/, '');
75
+ let path = discoveryPath || '/.well-known/openid-configuration';
76
+ if (base.endsWith(path)) {
77
+ base = base.slice(0, -path.length).replace(/\/+$/, '');
78
+ }
79
+ if (!path.startsWith('/')) path = '/' + path;
80
+ const url = base + path;
81
+ const res = await fetch(url);
82
+ if (!res.ok) throw new Error(`OIDC Discovery failed: ${res.status} — ${url}`);
83
+ return res.json();
84
+ }
85
+
86
+ /**
87
+ * Build the authorization redirect URL to initiate login.
88
+ *
89
+ * @param {object} config
90
+ * @param {string} config.issuer Issuer base URL
91
+ * @param {string} config.clientId Client ID
92
+ * @param {string} config.redirectUri Callback redirect URI
93
+ * @param {string} [config.scopes='openid profile email'] Space-separated scopes
94
+ * @param {boolean} [config.pkceEnabled=true] Whether PKCE is enabled
95
+ * @param {string} [config.discoveryPath]
96
+ * @param {string} state Signed state parameter
97
+ * @param {string} [codeChallenge] PKCE code challenge
98
+ * @returns {string} Full authorization URL
99
+ */
100
+ export function buildAuthorizeUrl(config, state, codeChallenge) {
101
+ const params = new URLSearchParams({
102
+ response_type: 'code',
103
+ client_id: config.clientId,
104
+ redirect_uri: config.redirectUri,
105
+ scope: config.scopes || 'openid profile email',
106
+ state,
107
+ });
108
+ if (config.pkceEnabled !== false && codeChallenge) {
109
+ params.set('code_challenge', codeChallenge);
110
+ params.set('code_challenge_method', 'S256');
111
+ }
112
+ const base = config.issuer.replace(/\/+$/, '');
113
+ return `${base}/authorize?${params.toString()}`;
114
+ }
115
+
116
+ /**
117
+ * Exchange an authorization code for access, ID, and refresh tokens.
118
+ *
119
+ * @param {object} config
120
+ * @param {string} config.issuer Issuer base URL
121
+ * @param {string} config.clientId Client ID
122
+ * @param {string} [config.clientSecret] Client secret (for confidential clients)
123
+ * @param {string} config.redirectUri Callback redirect URI
124
+ * @param {boolean} [config.pkceEnabled=true]
125
+ * @param {string} [config.discoveryPath]
126
+ * @param {string} code Authorization code from callback
127
+ * @param {string} [codeVerifier] PKCE code verifier
128
+ * @returns {Promise<{ access_token: string, id_token?: string, refresh_token?: string, expires_in?: number, token_type?: string }>}
129
+ */
130
+ export async function exchangeCode(config, code, codeVerifier) {
131
+ const discoverDoc = await discover(config.issuer, config.discoveryPath);
132
+ const tokenUrl = discoverDoc.token_endpoint;
133
+
134
+ const body = new URLSearchParams({
135
+ grant_type: 'authorization_code',
136
+ code,
137
+ redirect_uri: config.redirectUri,
138
+ client_id: config.clientId,
139
+ });
140
+
141
+ if (config.clientSecret) {
142
+ body.set('client_secret', config.clientSecret);
143
+ }
144
+
145
+ if (config.pkceEnabled !== false && codeVerifier) {
146
+ body.set('code_verifier', codeVerifier);
147
+ }
148
+
149
+ const res = await fetch(tokenUrl, {
150
+ method: 'POST',
151
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
152
+ body,
153
+ });
154
+
155
+ if (!res.ok) {
156
+ const err = await res.text();
157
+ throw new Error(`Token exchange failed: ${res.status} ${err}`);
158
+ }
159
+ return res.json();
160
+ }
161
+
162
+ /**
163
+ * Fetch user profile from the UserInfo endpoint using an access token.
164
+ *
165
+ * @param {object} config
166
+ * @param {string} config.issuer Issuer base URL
167
+ * @param {string} [config.discoveryPath]
168
+ * @param {string} accessToken Bearer access token
169
+ * @returns {Promise<object>} User claims and profile info
170
+ */
171
+ export async function getUserInfo(config, accessToken) {
172
+ const discoverDoc = await discover(config.issuer, config.discoveryPath);
173
+ const userinfoUrl = discoverDoc.userinfo_endpoint;
174
+ if (!userinfoUrl) throw new Error('No userinfo_endpoint in discovery document');
175
+
176
+ const res = await fetch(userinfoUrl, {
177
+ headers: { Authorization: `Bearer ${accessToken}` },
178
+ });
179
+
180
+ if (!res.ok) throw new Error(`Userinfo failed: ${res.status}`);
181
+ return res.json();
182
+ }
183
+
184
+ /**
185
+ * Safely decode payload from an ID token (JWT) without verification.
186
+ *
187
+ * @param {string} idToken Raw JWT id_token string
188
+ * @returns {object} Decoded JSON payload claims
189
+ */
190
+ export function decodeIdToken(idToken) {
191
+ if (!idToken || typeof idToken !== 'string') throw new Error('Invalid id_token');
192
+ const parts = idToken.split('.');
193
+ if (parts.length !== 3) throw new Error('Invalid id_token format (expected 3 parts)');
194
+ const payload = Buffer.from(parts[1], 'base64url').toString('utf8');
195
+ return JSON.parse(payload);
196
+ }
197
+
198
+ // ─── MAIN EXPORT ────────────────────────────────────────────────────────────────
199
+
200
+ export default {
201
+ generateState,
202
+ consumeState,
203
+ generatePKCE,
204
+ discover,
205
+ buildAuthorizeUrl,
206
+ exchangeCode,
207
+ getUserInfo,
208
+ decodeIdToken,
209
+ };