cynx-ui 1.0.0

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.
@@ -0,0 +1,160 @@
1
+ import { useState, useRef, useEffect, useCallback } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import { ChevronDown, Search, Check, X } from 'lucide-react';
4
+
5
+ export default function MultiSelect({ value = [], onChange, options = [], placeholder = 'Select...' }) {
6
+ const [open, setOpen] = useState(false);
7
+ const [query, setQuery] = useState('');
8
+ const [rect, setRect] = useState(null);
9
+ const btnRef = useRef(null);
10
+ const menuRef = useRef(null);
11
+ const inputRef = useRef(null);
12
+
13
+ const selected = value || [];
14
+
15
+ const filtered = query
16
+ ? options.filter(o => o.label.toLowerCase().includes(query.toLowerCase()))
17
+ : options;
18
+
19
+ const updateRect = useCallback(() => {
20
+ if (btnRef.current) setRect(btnRef.current.getBoundingClientRect());
21
+ }, []);
22
+
23
+ useEffect(() => {
24
+ if (!open) return;
25
+ function close(e) {
26
+ if (!btnRef.current?.contains(e.target) && !menuRef.current?.contains(e.target)) {
27
+ setOpen(false);
28
+ setQuery('');
29
+ }
30
+ }
31
+ document.addEventListener('mousedown', close);
32
+ return () => document.removeEventListener('mousedown', close);
33
+ }, [open]);
34
+
35
+ useEffect(() => {
36
+ if (!open) return;
37
+ window.addEventListener('scroll', updateRect, true);
38
+ window.addEventListener('resize', updateRect);
39
+ return () => {
40
+ window.removeEventListener('scroll', updateRect, true);
41
+ window.removeEventListener('resize', updateRect);
42
+ };
43
+ }, [open, updateRect]);
44
+
45
+ useEffect(() => {
46
+ if (open) { updateRect(); setTimeout(() => inputRef.current?.focus(), 50); }
47
+ }, [open, updateRect]);
48
+
49
+ function toggle(opt) {
50
+ const isSelected = selected.some(s => s.value === opt.value);
51
+ if (isSelected) {
52
+ onChange(selected.filter(s => s.value !== opt.value));
53
+ } else {
54
+ onChange([...selected, opt]);
55
+ }
56
+ }
57
+
58
+ function remove(val) {
59
+ onChange(selected.filter(s => s.value !== val));
60
+ }
61
+
62
+ function toggleOpen() {
63
+ updateRect();
64
+ setOpen(o => !o);
65
+ if (open) setQuery('');
66
+ }
67
+
68
+ const menu = open && rect && createPortal(
69
+ <div
70
+ ref={menuRef}
71
+ style={{
72
+ position: 'fixed',
73
+ top: rect.bottom + 4,
74
+ left: rect.left,
75
+ width: rect.width,
76
+ zIndex: 999999,
77
+ background: 'var(--surface)',
78
+ border: '1px solid var(--border)',
79
+ borderRadius: 'var(--rads)',
80
+ boxShadow: 'var(--shadow-lg)',
81
+ overflow: 'hidden',
82
+ animation: 'dropdownOpen .12s ease',
83
+ }}>
84
+ <div style={{ padding: '6px 8px', borderBottom: '1px solid var(--border-light)', display: 'flex', alignItems: 'center', gap: 6 }}>
85
+ <Search size={12} style={{ color: 'var(--muted)', flexShrink: 0 }} />
86
+ <input
87
+ ref={inputRef}
88
+ value={query}
89
+ onChange={e => setQuery(e.target.value)}
90
+ placeholder="Search..."
91
+ style={{ border: 'none', background: 'transparent', outline: 'none', fontSize: '.78rem', color: 'var(--primary-text)', width: '100%', fontFamily: 'var(--sans)' }}
92
+ />
93
+ </div>
94
+ <div style={{ maxHeight: 200, overflowY: 'auto', scrollbarWidth: 'thin' }}>
95
+ {filtered.length === 0 ? (
96
+ <div style={{ padding: '10px 12px', fontSize: '.78rem', color: 'var(--muted)', textAlign: 'center' }}>No results</div>
97
+ ) : filtered.map(o => {
98
+ const isSelected = selected.some(s => s.value === o.value);
99
+ return (
100
+ <div key={o.value} onClick={() => toggle(o)} style={{
101
+ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
102
+ padding: '8px 12px', fontSize: '.8rem', cursor: 'pointer',
103
+ background: isSelected ? 'var(--accent-bg)' : 'transparent',
104
+ color: isSelected ? 'var(--accent)' : 'var(--primary-text)',
105
+ fontWeight: isSelected ? 500 : 400,
106
+ transition: 'background .1s',
107
+ }}
108
+ onMouseEnter={e => { if (!isSelected) e.currentTarget.style.background = 'var(--hover-bg)'; }}
109
+ onMouseLeave={e => { if (!isSelected) e.currentTarget.style.background = 'transparent'; }}>
110
+ <span>{o.label}</span>
111
+ {isSelected && <Check size={12} />}
112
+ </div>
113
+ );
114
+ })}
115
+ </div>
116
+ </div>,
117
+ document.body
118
+ );
119
+
120
+ return (
121
+ <div ref={btnRef} style={{ position: 'relative' }}>
122
+ <button type="button" onClick={toggleOpen} style={{
123
+ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 6,
124
+ width: '100%', padding: '6px 10px', minHeight: 34,
125
+ border: `1px solid ${open ? 'var(--accent)' : 'var(--border)'}`,
126
+ borderRadius: 'var(--rads)',
127
+ background: 'var(--surface)',
128
+ boxShadow: open ? '0 0 0 3px rgba(249,158,44,.12)' : 'var(--shadow)',
129
+ cursor: 'pointer', fontSize: '.8rem', fontFamily: 'var(--sans)',
130
+ transition: 'border-color .15s, box-shadow .15s',
131
+ }}
132
+ onMouseEnter={e => { if (!open) e.currentTarget.style.borderColor = 'var(--accent)'; }}
133
+ onMouseLeave={e => { if (!open) e.currentTarget.style.borderColor = 'var(--border)'; }}>
134
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, flex: 1, minWidth: 0 }}>
135
+ {selected.length === 0 ? (
136
+ <span style={{ color: 'var(--muted)', fontSize: '.8rem' }}>{placeholder}</span>
137
+ ) : selected.map(s => (
138
+ <span key={s.value} style={{
139
+ display: 'inline-flex', alignItems: 'center', gap: 4,
140
+ padding: '2px 6px', borderRadius: 'var(--radxs)',
141
+ background: 'var(--accent-bg)', color: 'var(--accent)',
142
+ fontSize: '.72rem', fontWeight: 500,
143
+ border: '1px solid var(--accent-border)',
144
+ }}>
145
+ {s.label}
146
+ <span
147
+ role="button"
148
+ onClick={e => { e.stopPropagation(); remove(s.value); }}
149
+ style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', lineHeight: 1 }}>
150
+ <X size={10} />
151
+ </span>
152
+ </span>
153
+ ))}
154
+ </div>
155
+ <ChevronDown size={13} style={{ color: 'var(--muted)', flexShrink: 0, transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .2s' }} />
156
+ </button>
157
+ {menu}
158
+ </div>
159
+ );
160
+ }
@@ -0,0 +1,118 @@
1
+ import { useState, useEffect, useRef } from 'react';
2
+ import { useEvents } from '../hooks/useEvents.jsx';
3
+
4
+ const MAX_NOTIFS = 50;
5
+
6
+ function eventToNotif(type, data) {
7
+ const id = Date.now() + Math.random();
8
+ const now = new Date();
9
+ switch (type) {
10
+ // Phase B: wire up notification types for new events (passkey registered,
11
+ // new sign-in detected, one-time code requested, etc.) here.
12
+ default:
13
+ return null;
14
+ }
15
+ }
16
+
17
+ export default function Notifications() {
18
+ const [notifs, setNotifs] = useState([]);
19
+ const [open, setOpen] = useState(false);
20
+ const ref = useRef(null);
21
+
22
+ const unread = notifs.filter(n => !n.read).length;
23
+
24
+ useEvents({});
25
+
26
+ function addNotif(type, data) {
27
+ const n = eventToNotif(type, data);
28
+ if (!n) return;
29
+ setNotifs(prev => [n, ...prev].slice(0, MAX_NOTIFS));
30
+ // Browser notification if permission granted
31
+ if ('Notification' in window && Notification.permission === 'granted') {
32
+ new Notification(n.title, { body: n.message });
33
+ }
34
+ }
35
+
36
+ function markAllRead() {
37
+ setNotifs(prev => prev.map(n => ({ ...n, read: true })));
38
+ }
39
+
40
+ function clearAll() {
41
+ setNotifs([]);
42
+ setOpen(false);
43
+ }
44
+
45
+ // Close on outside click
46
+ useEffect(() => {
47
+ function handler(e) { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }
48
+ document.addEventListener('mousedown', handler);
49
+ return () => document.removeEventListener('mousedown', handler);
50
+ }, []);
51
+
52
+ function timeAgo(date) {
53
+ const s = Math.floor((Date.now() - date) / 1000);
54
+ if (s < 60) return `${s}s ago`;
55
+ if (s < 3600) return `${Math.floor(s/60)}m ago`;
56
+ if (s < 86400)return `${Math.floor(s/3600)}h ago`;
57
+ return `${Math.floor(s/86400)}d ago`;
58
+ }
59
+
60
+ const iconFor = () => (
61
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
62
+ );
63
+
64
+ return (
65
+ <div ref={ref} style={{ position:'relative', height:'100%', display:'flex', alignItems:'center' }}>
66
+ <button
67
+ onClick={() => { setOpen(o => !o); if (!open && unread) markAllRead(); }}
68
+ style={{ position:'relative', background:'none', border:'none', cursor:'pointer', color:'var(--muted)', padding:'6px', display:'flex', alignItems:'center', borderRadius:'var(--rads)', transition:'color .15s' }}
69
+ onMouseEnter={e => e.currentTarget.style.color = 'var(--primary-text)'}
70
+ onMouseLeave={e => e.currentTarget.style.color = 'var(--muted)'}
71
+ >
72
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
73
+ <path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/>
74
+ <path d="M13.73 21a2 2 0 0 1-3.46 0"/>
75
+ </svg>
76
+ {unread > 0 && (
77
+ <span style={{ position:'absolute', top:2, right:2, minWidth:16, height:16, borderRadius:8, background:'var(--danger)', color:'#fff', fontSize:10, fontWeight:700, display:'flex', alignItems:'center', justifyContent:'center', padding:'0 3px', lineHeight:1 }}>
78
+ {unread > 99 ? '99+' : unread}
79
+ </span>
80
+ )}
81
+ </button>
82
+
83
+ <div style={{
84
+ position:'absolute', right:0, top:'100%', width:320, zIndex:1000,
85
+ background:'var(--surface)', border:'1px solid var(--border)',
86
+ borderRadius:'var(--rads)', boxShadow:'var(--shadow-lg)',
87
+ opacity: open ? 1 : 0,
88
+ transform: open ? 'translateY(0)' : 'translateY(-6px)',
89
+ pointerEvents: open ? 'auto' : 'none',
90
+ transition: 'opacity .15s ease, transform .15s ease',
91
+ }}>
92
+ <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', padding:'10px 14px', borderBottom:'1px solid var(--border)' }}>
93
+ <span style={{ fontSize:'.82rem', fontWeight:600 }}>Notifications</span>
94
+ {notifs.length > 0 && (
95
+ <button onClick={clearAll} style={{ fontSize:'.7rem', color:'var(--muted)', background:'none', border:'none', cursor:'pointer' }}>Clear all</button>
96
+ )}
97
+ </div>
98
+
99
+ <div style={{ maxHeight:360, overflowY:'auto' }}>
100
+ {notifs.length === 0 ? (
101
+ <div style={{ padding:'24px 16px', textAlign:'center', color:'var(--muted)', fontSize:'.8rem' }}>No notifications</div>
102
+ ) : notifs.map(n => (
103
+ <div key={n.id} style={{ display:'flex', gap:10, padding:'10px 14px', borderBottom:'1px solid var(--border-light)', background: n.read ? 'transparent' : 'var(--accent-bg)', transition:'background .2s' }}>
104
+ <div style={{ flexShrink:0, width:28, height:28, borderRadius:'50%', background:'var(--grey-1)', display:'flex', alignItems:'center', justifyContent:'center', color:'var(--accent)', marginTop:1 }}>
105
+ {iconFor(n.type)}
106
+ </div>
107
+ <div style={{ flex:1, minWidth:0 }}>
108
+ <div style={{ fontSize:'.78rem', fontWeight:600, marginBottom:2 }}>{n.title}</div>
109
+ <div style={{ fontSize:'.72rem', color:'var(--muted)', lineHeight:1.4 }}>{n.message}</div>
110
+ <div style={{ fontSize:'.68rem', color:'var(--muted)', marginTop:4, opacity:.7 }}>{timeAgo(n.time)}</div>
111
+ </div>
112
+ </div>
113
+ ))}
114
+ </div>
115
+ </div>
116
+ </div>
117
+ );
118
+ }
package/Pagination.jsx ADDED
@@ -0,0 +1,41 @@
1
+ import Button from './Button';
2
+ import { ChevronLeft, ChevronRight } from 'lucide-react';
3
+
4
+ export function Pagination({ page, pages, total, perPage, onChange }) {
5
+ const totalPages = pages || (total && perPage ? Math.ceil(total / perPage) : 0);
6
+ if (!totalPages || totalPages <= 1) return null;
7
+
8
+ const items = [];
9
+ for (let i = 1; i <= totalPages; i++) {
10
+ if (i === 1 || i === totalPages || (i >= page - 1 && i <= page + 1)) items.push(i);
11
+ else if (items[items.length - 1] !== '...') items.push('...');
12
+ }
13
+
14
+ const start = perPage ? (page - 1) * perPage + 1 : null;
15
+ const end = perPage ? Math.min(page * perPage, total) : null;
16
+
17
+ return (
18
+ <div style={{
19
+ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
20
+ padding: '10px 16px',
21
+ borderTop: '1px solid var(--border-light)',
22
+ background: 'var(--surface)',
23
+ borderRadius: '0 0 var(--rad) var(--rad)',
24
+ }}>
25
+ {start !== null && (
26
+ <div style={{ fontSize: '.72rem', color: 'var(--muted)', fontFamily: 'var(--mono)' }}>
27
+ {start}–{end} of {total}
28
+ </div>
29
+ )}
30
+ <div style={{ display: 'flex', gap: 4, alignItems: 'center', marginLeft: start !== null ? 'auto' : 0 }}>
31
+ <Button variant="ghost" size="xs" icon={<ChevronLeft size={14} />} onClick={() => onChange(page - 1)} disabled={page <= 1} />
32
+ {items.map((item, i) =>
33
+ item === '...'
34
+ ? <span key={`e${i}`} style={{ padding: '0 4px', color: 'var(--muted)', fontSize: '.78rem' }}>…</span>
35
+ : <Button key={item} variant={item === page ? 'primary' : 'no-line'} size="xs" onClick={() => onChange(item)} style={{ minWidth: 28 }}>{item}</Button>
36
+ )}
37
+ <Button variant="ghost" size="xs" icon={<ChevronRight size={14} />} onClick={() => onChange(page + 1)} disabled={page >= totalPages} />
38
+ </div>
39
+ </div>
40
+ );
41
+ }
package/PulseDot.jsx ADDED
@@ -0,0 +1,58 @@
1
+ const STYLES = `
2
+ .pdot{position:relative;display:inline-flex;align-items:center;justify-content:center;width:8px;height:8px;flex-shrink:0}
3
+ .pdot-core{width:8px;height:8px;border-radius:50%;background:var(--grey-4,#797b8d)}
4
+ .pdot-ring{position:absolute;inset:-3px;border-radius:50%;border:2px solid var(--grey-4,#797b8d);opacity:0;animation:pdot-pulse 1.1s ease-out infinite}
5
+ .pdot-live .pdot-ring{animation-play-state:running}
6
+ .pdot-paused .pdot-ring{animation-play-state:paused}
7
+
8
+ .pdot-sm .pdot-core{width:6px;height:6px}
9
+ .pdot-sm .pdot-ring{inset:-2px;border-width:1.5px}
10
+ .pdot-lg .pdot-core{width:10px;height:10px}
11
+ .pdot-lg .pdot-ring{inset:-4px}
12
+
13
+ .pdot-ok .pdot-core{background:var(--success,#27ae60)}
14
+ .pdot-ok .pdot-ring{border-color:var(--success,#27ae60)}
15
+ .pdot-err .pdot-core{background:var(--danger,#f64747)}
16
+ .pdot-err .pdot-ring{border-color:var(--danger,#f64747)}
17
+ .pdot-warn .pdot-core{background:var(--accent,#f99e2c)}
18
+ .pdot-warn .pdot-ring{border-color:var(--accent,#f99e2c)}
19
+ .pdot-blue .pdot-core{background:var(--blue,#2782e4)}
20
+ .pdot-blue .pdot-ring{border-color:var(--blue,#2782e4)}
21
+ .pdot-purple .pdot-core{background:#8b5cf6}
22
+ .pdot-purple .pdot-ring{border-color:#8b5cf6}
23
+ .pdot-teal .pdot-core{background:#14b8a6}
24
+ .pdot-teal .pdot-ring{border-color:#14b8a6}
25
+ .pdot-pink .pdot-core{background:#ec4899}
26
+ .pdot-pink .pdot-ring{border-color:#ec4899}
27
+
28
+ .pdot-off .pdot-core{background:var(--t3,#9b9daa)}
29
+ .pdot-off .pdot-ring{display:none}
30
+
31
+ @keyframes pdot-pulse{0%{transform:scale(1);opacity:.7}100%{transform:scale(1.3);opacity:0}}
32
+ `;
33
+
34
+ let styleInjected = false;
35
+
36
+ export default function PulseDot({
37
+ variant = 'dim',
38
+ size = 'md',
39
+ pulse = true,
40
+ className = '',
41
+ style = {},
42
+ }) {
43
+ if (!styleInjected && typeof document !== 'undefined') {
44
+ const tag = document.createElement('style');
45
+ tag.textContent = STYLES;
46
+ document.head.appendChild(tag);
47
+ styleInjected = true;
48
+ }
49
+
50
+ const classes = `pdot pdot-${variant} pdot-${size}${pulse ? ' pdot-live' : ' pdot-paused'}${className ? ' ' + className : ''}`;
51
+
52
+ return (
53
+ <span className={classes} style={style}>
54
+ <span className="pdot-core" />
55
+ <span className="pdot-ring" />
56
+ </span>
57
+ );
58
+ }
@@ -0,0 +1,109 @@
1
+ import { useState, useRef, useEffect } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import { ChevronDown, Search, Check } from 'lucide-react';
4
+
5
+ // SearchableDropdown — reusable searchable select
6
+ // Props: value, onChange, options [{value, label}], placeholder
7
+ export function SearchableDropdown({ value, onChange, options = [], placeholder = 'Select...' }) {
8
+ const [open, setOpen] = useState(false);
9
+ const [query, setQuery] = useState('');
10
+ const ref = useRef(null);
11
+ const inputRef = useRef(null);
12
+ const btnRef = useRef(null);
13
+ const [menuStyle, setMenuStyle] = useState({});
14
+
15
+ const selected = options.find(o => o.value === value);
16
+
17
+ const filtered = query
18
+ ? options.filter(o => o.label.toLowerCase().includes(query.toLowerCase()))
19
+ : options;
20
+
21
+ useEffect(() => {
22
+ if (!open) return;
23
+ function close(e) { if (!ref.current?.contains(e.target)) { setOpen(false); setQuery(''); } }
24
+ document.addEventListener('mousedown', close);
25
+ return () => document.removeEventListener('mousedown', close);
26
+ }, [open]);
27
+
28
+ useEffect(() => {
29
+ if (!open) return;
30
+ const btn = btnRef.current;
31
+ if (!btn) return;
32
+ const rect = btn.getBoundingClientRect();
33
+ setMenuStyle({
34
+ position: 'fixed',
35
+ top: rect.bottom + 4,
36
+ left: rect.left,
37
+ width: rect.width,
38
+ zIndex: 9999,
39
+ });
40
+ setTimeout(() => inputRef.current?.focus(), 50);
41
+ }, [open]);
42
+
43
+ function select(val) {
44
+ onChange(val);
45
+ setOpen(false);
46
+ setQuery('');
47
+ }
48
+
49
+ return (
50
+ <div ref={ref} style={{ position: 'relative' }}>
51
+ <button type="button" ref={btnRef} onClick={() => setOpen(o => !o)} style={{
52
+ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 6,
53
+ width: '100%', padding: '6px 10px', height: 32,
54
+ border: '1px solid var(--border)', borderRadius: 'var(--rads)',
55
+ background: 'var(--surface)', color: selected ? 'var(--primary-text)' : 'var(--muted)',
56
+ cursor: 'pointer', fontSize: '.8rem', fontFamily: 'var(--sans)',
57
+ transition: 'border-color .15s', boxShadow: 'var(--shadow)',
58
+ }}
59
+ onMouseEnter={e => e.currentTarget.style.borderColor = 'var(--accent)'}
60
+ onMouseLeave={e => e.currentTarget.style.borderColor = 'var(--border)'}>
61
+ <span style={{ flex: 1, textAlign: 'left', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
62
+ {selected?.label || placeholder}
63
+ </span>
64
+ <ChevronDown size={13} style={{ color: 'var(--muted)', flexShrink: 0 }} />
65
+ </button>
66
+
67
+ {open && createPortal(
68
+ <div style={{
69
+ ...menuStyle,
70
+ background: 'var(--surface)', border: '1px solid var(--border)',
71
+ borderRadius: 'var(--rads)', boxShadow: 'var(--shadow-lg)',
72
+ overflow: 'hidden', animation: 'dropdownOpen .12s ease',
73
+ }}>
74
+ {/* Search input */}
75
+ <div style={{ padding: '6px 8px', borderBottom: '1px solid var(--border-light)', display: 'flex', alignItems: 'center', gap: 6 }}>
76
+ <Search size={12} style={{ color: 'var(--muted)', flexShrink: 0 }} />
77
+ <input
78
+ ref={inputRef}
79
+ value={query}
80
+ onChange={e => setQuery(e.target.value)}
81
+ placeholder="Search..."
82
+ style={{ border: 'none', background: 'transparent', outline: 'none', fontSize: '.78rem', color: 'var(--primary-text)', width: '100%', fontFamily: 'var(--sans)' }}
83
+ />
84
+ </div>
85
+ {/* Options */}
86
+ <div style={{ maxHeight: 200, overflowY: 'auto', scrollbarWidth: 'thin' }}>
87
+ {filtered.length === 0 ? (
88
+ <div style={{ padding: '10px 12px', fontSize: '.78rem', color: 'var(--muted)', textAlign: 'center' }}>No results</div>
89
+ ) : filtered.map(o => (
90
+ <div key={o.value} onClick={() => select(o.value)} style={{
91
+ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
92
+ padding: '8px 12px', fontSize: '.8rem', cursor: 'pointer',
93
+ background: o.value === value ? 'var(--accent-bg)' : 'transparent',
94
+ color: o.value === value ? 'var(--accent)' : 'var(--primary-text)',
95
+ transition: 'background .1s',
96
+ }}
97
+ onMouseEnter={e => { if (o.value !== value) e.currentTarget.style.background = 'var(--grey-1)'; }}
98
+ onMouseLeave={e => { if (o.value !== value) e.currentTarget.style.background = 'transparent'; }}>
99
+ <span>{o.label}</span>
100
+ {o.value === value && <Check size={12} />}
101
+ </div>
102
+ ))}
103
+ </div>
104
+ </div>,
105
+ document.body
106
+ )}
107
+ </div>
108
+ );
109
+ }