cynx-ui 1.2.16 → 1.2.18

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.
@@ -1,163 +1,163 @@
1
- import { useEffect, useRef } from 'react';
2
-
3
- const SIZES = {
4
- xs: { svg: 48, stroke: 4, fontSize: '.7rem' },
5
- sm: { svg: 64, stroke: 5, fontSize: '.85rem' },
6
- md: { svg: 80, stroke: 6, fontSize: '1rem' },
7
- lg: { svg: 100, stroke: 7, fontSize: '1.15rem' },
8
- xl: { svg: 120, stroke: 8, fontSize: '1.3rem' },
9
- };
10
-
11
- export default function CircularProgress({
12
- value = 0,
13
- size = 'md',
14
- customSize,
15
- color = 'var(--blue)',
16
- trackColor = '#e0e0e4',
17
- label = '',
18
- showPercent = true,
19
- animated = true,
20
- rounded = false,
21
- indeterminate = false,
22
- speed = 1,
23
- strokeWidth,
24
- gap = 4,
25
- className = '',
26
- style = {},
27
- }) {
28
- const base = SIZES[size] || SIZES.md;
29
- const svg = customSize || base.svg;
30
- const stroke = strokeWidth ?? (customSize ? Math.max(3, svg * 0.06) : base.stroke);
31
- const fontSize = customSize ? `${Math.max(11, svg * 0.13)}px` : base.fontSize;
32
- const cx = svg / 2;
33
- const cy = svg / 2;
34
- const r = (svg - stroke) / 2;
35
- const c = 2 * Math.PI * r;
36
- const pct = Math.min(100, Math.max(0, value));
37
- const BIG = c * 4;
38
- const cap = rounded ? 'round' : 'butt';
39
-
40
- const userGap = gap !== undefined ? gap : (rounded ? c * 0.05 : c * 0.03);
41
- const capOverrun = rounded ? stroke : 0;
42
- const gapSize = userGap + capOverrun;
43
- const total = c - 2 * gapSize;
44
-
45
- const isFull = pct >= 100;
46
- const isEmpty = pct <= 0;
47
-
48
- let indDash, indOffset, trackDash, trackOffset, indOpacity;
49
- let indStartDeg, indEndDeg, trackStartDeg, trackEndDeg;
50
-
51
- if (isEmpty) {
52
- indDash = `0,${BIG}`; indOffset = 0;
53
- trackDash = `${c},${BIG}`; trackOffset = 0;
54
- indOpacity = 0;
55
- indStartDeg = indEndDeg = trackStartDeg = trackEndDeg = 0;
56
- } else if (isFull) {
57
- indDash = `${c + stroke * 2},${BIG}`; indOffset = 0;
58
- trackDash = `0,${BIG}`; trackOffset = 0;
59
- indOpacity = 1;
60
- indStartDeg = 0; indEndDeg = 360;
61
- trackStartDeg = trackEndDeg = 0;
62
- } else {
63
- const activeLen = (pct / 100) * total;
64
- const inactiveLen = total - activeLen;
65
- indDash = `${activeLen},${BIG}`; indOffset = 0;
66
- trackDash = `${inactiveLen},${BIG}`; trackOffset = -(activeLen + gapSize);
67
- indOpacity = 1;
68
- indStartDeg = 0; indEndDeg = (pct / 100) * 360;
69
- trackStartDeg = (pct / 100) * 360; trackEndDeg = 360;
70
- }
71
-
72
- const gRef = useRef(null);
73
- const indRef = useRef(null);
74
- const trackRef = useRef(null);
75
-
76
- useEffect(() => {
77
- if (!indeterminate) return;
78
- const capRad = (180 / Math.PI) * (stroke / r) / 2;
79
- const gapRad = (180 / Math.PI) * (gapSize / r);
80
- const gapArcLen = (capRad + gapRad) / 360 * c;
81
- let angle = -90, arcFrac = 0.45, arcDir = 1, raf;
82
- let lastTime = performance.now();
83
- function tick(now) {
84
- const dt = (now - lastTime) / 16.67;
85
- lastTime = now;
86
- angle = (angle + 3.6 * speed * dt) % 360;
87
- arcFrac += 0.006 * arcDir * speed * dt;
88
- if (arcFrac >= 0.72) { arcFrac = 0.72; arcDir = -1; }
89
- if (arcFrac <= 0.18) { arcFrac = 0.18; arcDir = 1; }
90
- const indLen = c * arcFrac;
91
- const trackLen = Math.max(0, c - indLen - gapArcLen * 2);
92
- if (gRef.current) gRef.current.style.transform = `rotate(${angle}deg)`;
93
- if (indRef.current) indRef.current.setAttribute('stroke-dasharray', `${indLen.toFixed(2)},9999`);
94
- if (trackRef.current) {
95
- trackRef.current.setAttribute('stroke-dasharray', `${trackLen.toFixed(2)},9999`);
96
- trackRef.current.setAttribute('stroke-dashoffset', `${(-(indLen + gapArcLen)).toFixed(2)}`);
97
- }
98
- raf = requestAnimationFrame(tick);
99
- }
100
- raf = requestAnimationFrame(tick);
101
- return () => cancelAnimationFrame(raf);
102
- }, [indeterminate, c, stroke, r, gapSize, speed]);
103
-
104
- return (
105
- <div className={className} style={{ display: 'inline-flex', flexDirection: 'column', alignItems: 'center', gap: 4, ...style }}>
106
- <div style={{ position: 'relative', width: svg, height: svg }}>
107
- <svg width={svg} height={svg} viewBox={`0 0 ${svg} ${svg}`}>
108
- {indeterminate ? (
109
- <g ref={gRef} style={{ transformOrigin: `${cx}px ${cy}px` }}>
110
- <circle
111
- ref={trackRef}
112
- cx={cx} cy={cy} r={r}
113
- fill="none" stroke={trackColor} strokeWidth={stroke} strokeLinecap={cap}
114
- />
115
- <circle
116
- ref={indRef}
117
- cx={cx} cy={cy} r={r}
118
- fill="none" stroke={color} strokeWidth={stroke} strokeLinecap={cap}
119
- />
120
- </g>
121
- ) : (
122
- <>
123
- <circle
124
- cx={cx} cy={cy} r={r}
125
- fill="none" stroke={trackColor} strokeWidth={stroke}
126
- strokeLinecap={cap}
127
- strokeDasharray={trackDash} strokeDashoffset={trackOffset}
128
- style={{
129
- transform: 'rotate(-90deg)', transformOrigin: '50% 50%',
130
- transition: animated ? 'stroke-dasharray .5s cubic-bezier(0.4,0,0.2,1), stroke-dashoffset .5s cubic-bezier(0.4,0,0.2,1)' : 'none',
131
- }}
132
- />
133
- <circle
134
- cx={cx} cy={cy} r={r}
135
- fill="none" stroke={color} strokeWidth={stroke}
136
- strokeLinecap={cap}
137
- strokeDasharray={indDash} strokeDashoffset={indOffset}
138
- style={{
139
- transform: 'rotate(-90deg)', transformOrigin: '50% 50%',
140
- opacity: indOpacity,
141
- transition: animated ? 'stroke-dasharray .5s cubic-bezier(0.4,0,0.2,1), opacity .2s' : 'none',
142
- }}
143
- />
144
- </>
145
- )}
146
- </svg>
147
- {showPercent && !indeterminate && (
148
- <div style={{
149
- position: 'absolute', inset: 0,
150
- display: 'flex', alignItems: 'center', justifyContent: 'center',
151
- fontSize, fontWeight: 600, color: 'var(--primary-text)',
152
- fontVariantNumeric: 'tabular-nums',
153
- }}>
154
- {Math.round(pct)}%
155
- </div>
156
- )}
157
- </div>
158
- {label && (
159
- <span style={{ fontSize: '.6rem', color: 'var(--muted)', fontWeight: 500 }}>{label}</span>
160
- )}
161
- </div>
162
- );
163
- }
1
+ import { useEffect, useRef } from 'react';
2
+
3
+ const SIZES = {
4
+ xs: { svg: 48, stroke: 4, fontSize: '.7rem' },
5
+ sm: { svg: 64, stroke: 5, fontSize: '.85rem' },
6
+ md: { svg: 80, stroke: 6, fontSize: '1rem' },
7
+ lg: { svg: 100, stroke: 7, fontSize: '1.15rem' },
8
+ xl: { svg: 120, stroke: 8, fontSize: '1.3rem' },
9
+ };
10
+
11
+ export default function CircularProgress({
12
+ value = 0,
13
+ size = 'md',
14
+ customSize,
15
+ color = 'var(--blue)',
16
+ trackColor = '#e0e0e4',
17
+ label = '',
18
+ showPercent = true,
19
+ animated = true,
20
+ rounded = false,
21
+ indeterminate = false,
22
+ speed = 1,
23
+ strokeWidth,
24
+ gap = 4,
25
+ className = '',
26
+ style = {},
27
+ }) {
28
+ const base = SIZES[size] || SIZES.md;
29
+ const svg = customSize || base.svg;
30
+ const stroke = strokeWidth ?? (customSize ? Math.max(3, svg * 0.06) : base.stroke);
31
+ const fontSize = customSize ? `${Math.max(11, svg * 0.13)}px` : base.fontSize;
32
+ const cx = svg / 2;
33
+ const cy = svg / 2;
34
+ const r = (svg - stroke) / 2;
35
+ const c = 2 * Math.PI * r;
36
+ const pct = Math.min(100, Math.max(0, value));
37
+ const BIG = c * 4;
38
+ const cap = rounded ? 'round' : 'butt';
39
+
40
+ const userGap = gap !== undefined ? gap : (rounded ? c * 0.05 : c * 0.03);
41
+ const capOverrun = rounded ? stroke : 0;
42
+ const gapSize = userGap + capOverrun;
43
+ const total = c - 2 * gapSize;
44
+
45
+ const isFull = pct >= 100;
46
+ const isEmpty = pct <= 0;
47
+
48
+ let indDash, indOffset, trackDash, trackOffset, indOpacity;
49
+ let indStartDeg, indEndDeg, trackStartDeg, trackEndDeg;
50
+
51
+ if (isEmpty) {
52
+ indDash = `0,${BIG}`; indOffset = 0;
53
+ trackDash = `${c},${BIG}`; trackOffset = 0;
54
+ indOpacity = 0;
55
+ indStartDeg = indEndDeg = trackStartDeg = trackEndDeg = 0;
56
+ } else if (isFull) {
57
+ indDash = `${c + stroke * 2},${BIG}`; indOffset = 0;
58
+ trackDash = `0,${BIG}`; trackOffset = 0;
59
+ indOpacity = 1;
60
+ indStartDeg = 0; indEndDeg = 360;
61
+ trackStartDeg = trackEndDeg = 0;
62
+ } else {
63
+ const activeLen = (pct / 100) * total;
64
+ const inactiveLen = total - activeLen;
65
+ indDash = `${activeLen},${BIG}`; indOffset = 0;
66
+ trackDash = `${inactiveLen},${BIG}`; trackOffset = -(activeLen + gapSize);
67
+ indOpacity = 1;
68
+ indStartDeg = 0; indEndDeg = (pct / 100) * 360;
69
+ trackStartDeg = (pct / 100) * 360; trackEndDeg = 360;
70
+ }
71
+
72
+ const gRef = useRef(null);
73
+ const indRef = useRef(null);
74
+ const trackRef = useRef(null);
75
+
76
+ useEffect(() => {
77
+ if (!indeterminate) return;
78
+ const capRad = (180 / Math.PI) * (stroke / r) / 2;
79
+ const gapRad = (180 / Math.PI) * (gapSize / r);
80
+ const gapArcLen = (capRad + gapRad) / 360 * c;
81
+ let angle = -90, arcFrac = 0.45, arcDir = 1, raf;
82
+ let lastTime = performance.now();
83
+ function tick(now) {
84
+ const dt = (now - lastTime) / 16.67;
85
+ lastTime = now;
86
+ angle = (angle + 3.6 * speed * dt) % 360;
87
+ arcFrac += 0.006 * arcDir * speed * dt;
88
+ if (arcFrac >= 0.72) { arcFrac = 0.72; arcDir = -1; }
89
+ if (arcFrac <= 0.18) { arcFrac = 0.18; arcDir = 1; }
90
+ const indLen = c * arcFrac;
91
+ const trackLen = Math.max(0, c - indLen - gapArcLen * 2);
92
+ if (gRef.current) gRef.current.style.transform = `rotate(${angle}deg)`;
93
+ if (indRef.current) indRef.current.setAttribute('stroke-dasharray', `${indLen.toFixed(2)},9999`);
94
+ if (trackRef.current) {
95
+ trackRef.current.setAttribute('stroke-dasharray', `${trackLen.toFixed(2)},9999`);
96
+ trackRef.current.setAttribute('stroke-dashoffset', `${(-(indLen + gapArcLen)).toFixed(2)}`);
97
+ }
98
+ raf = requestAnimationFrame(tick);
99
+ }
100
+ raf = requestAnimationFrame(tick);
101
+ return () => cancelAnimationFrame(raf);
102
+ }, [indeterminate, c, stroke, r, gapSize, speed]);
103
+
104
+ return (
105
+ <div className={className} style={{ display: 'inline-flex', flexDirection: 'column', alignItems: 'center', gap: 4, ...style }}>
106
+ <div style={{ position: 'relative', width: svg, height: svg }}>
107
+ <svg width={svg} height={svg} viewBox={`0 0 ${svg} ${svg}`}>
108
+ {indeterminate ? (
109
+ <g ref={gRef} style={{ transformOrigin: `${cx}px ${cy}px` }}>
110
+ <circle
111
+ ref={trackRef}
112
+ cx={cx} cy={cy} r={r}
113
+ fill="none" stroke={trackColor} strokeWidth={stroke} strokeLinecap={cap}
114
+ />
115
+ <circle
116
+ ref={indRef}
117
+ cx={cx} cy={cy} r={r}
118
+ fill="none" stroke={color} strokeWidth={stroke} strokeLinecap={cap}
119
+ />
120
+ </g>
121
+ ) : (
122
+ <>
123
+ <circle
124
+ cx={cx} cy={cy} r={r}
125
+ fill="none" stroke={trackColor} strokeWidth={stroke}
126
+ strokeLinecap={cap}
127
+ strokeDasharray={trackDash} strokeDashoffset={trackOffset}
128
+ style={{
129
+ transform: 'rotate(-90deg)', transformOrigin: '50% 50%',
130
+ transition: animated ? 'stroke-dasharray .5s cubic-bezier(0.4,0,0.2,1), stroke-dashoffset .5s cubic-bezier(0.4,0,0.2,1)' : 'none',
131
+ }}
132
+ />
133
+ <circle
134
+ cx={cx} cy={cy} r={r}
135
+ fill="none" stroke={color} strokeWidth={stroke}
136
+ strokeLinecap={cap}
137
+ strokeDasharray={indDash} strokeDashoffset={indOffset}
138
+ style={{
139
+ transform: 'rotate(-90deg)', transformOrigin: '50% 50%',
140
+ opacity: indOpacity,
141
+ transition: animated ? 'stroke-dasharray .5s cubic-bezier(0.4,0,0.2,1), opacity .2s' : 'none',
142
+ }}
143
+ />
144
+ </>
145
+ )}
146
+ </svg>
147
+ {showPercent && !indeterminate && (
148
+ <div style={{
149
+ position: 'absolute', inset: 0,
150
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
151
+ fontSize, fontWeight: 600, color: 'var(--primary-text)',
152
+ fontVariantNumeric: 'tabular-nums',
153
+ }}>
154
+ {Math.round(pct)}%
155
+ </div>
156
+ )}
157
+ </div>
158
+ {label && (
159
+ <span style={{ fontSize: '.6rem', color: 'var(--muted)', fontWeight: 500 }}>{label}</span>
160
+ )}
161
+ </div>
162
+ );
163
+ }
@@ -1,116 +1,116 @@
1
- import { useState, useEffect, useRef, useCallback } from 'react';
2
- import { createPortal } from 'react-dom';
3
- import { ChevronDown, Check } from 'lucide-react';
4
-
5
- export default function Dropdown({ value, onChange, options, placeholder = 'Select...', disabled }) {
6
- const [open, setOpen] = useState(false);
7
- const [rect, setRect] = useState(null);
8
- const btnRef = useRef();
9
- const menuRef = useRef();
10
-
11
- const updateRect = useCallback(() => {
12
- if (btnRef.current) setRect(btnRef.current.getBoundingClientRect());
13
- }, []);
14
-
15
- // Close on outside click
16
- useEffect(() => {
17
- if (!open) return;
18
- function onDown(e) {
19
- const inBtn = btnRef.current?.contains(e.target);
20
- const inMenu = menuRef.current?.contains(e.target);
21
- if (!inBtn && !inMenu) setOpen(false);
22
- }
23
- // Use setTimeout to avoid closing on the same click that opened
24
- const timer = setTimeout(() => {
25
- document.addEventListener('mousedown', onDown, true);
26
- }, 0);
27
- return () => {
28
- clearTimeout(timer);
29
- document.removeEventListener('mousedown', onDown, true);
30
- };
31
- }, [open]);
32
-
33
- // Update position on scroll/resize while open
34
- useEffect(() => {
35
- if (!open) return;
36
- window.addEventListener('scroll', updateRect, true);
37
- window.addEventListener('resize', updateRect);
38
- return () => {
39
- window.removeEventListener('scroll', updateRect, true);
40
- window.removeEventListener('resize', updateRect);
41
- };
42
- }, [open, updateRect]);
43
-
44
- function toggle() {
45
- if (disabled) return;
46
- updateRect();
47
- setOpen(o => !o);
48
- }
49
-
50
- const selected = options.find(o => String(o.value) === String(value));
51
-
52
- const menu = open && rect && createPortal(
53
- <div
54
- ref={menuRef}
55
- data-dropdown="true"
56
- style={{
57
- position: 'fixed',
58
- top: rect.bottom + 4,
59
- left: rect.left,
60
- width: rect.width,
61
- background: 'var(--surface)',
62
- border: '1px solid var(--border)',
63
- borderRadius: 'var(--rads)',
64
- boxShadow: 'var(--shadow-lg)',
65
- zIndex: 999999,
66
- animation: 'dropdownOpen .2s cubic-bezier(.16,1,.3,1)',
67
- }}>
68
- {options.map(o => {
69
- const active = String(o.value) === String(value);
70
- return (
71
- <div key={o.value}
72
- onMouseDown={e => e.stopPropagation()}
73
- onClick={() => { onChange(o.value); setOpen(false); }}
74
- onMouseEnter={e => { if (!active) e.currentTarget.style.background = 'var(--hover-bg)'; }}
75
- onMouseLeave={e => { e.currentTarget.style.background = active ? 'var(--accent-bg)' : 'transparent'; }}
76
- style={{
77
- display: 'flex', alignItems: 'center', justifyContent: 'space-between',
78
- padding: '9px 12px', cursor: 'pointer', fontSize: '.82rem',
79
- color: active ? 'var(--accent)' : 'var(--primary-text)',
80
- background: active ? 'var(--accent-bg)' : 'transparent',
81
- fontWeight: active ? 600 : 400,
82
- }}>
83
- <span>{o.label}</span>
84
- {active && <Check size={12} />}
85
- </div>
86
- );
87
- })}
88
- </div>,
89
- document.body
90
- );
91
-
92
- return (
93
- <div ref={btnRef} style={{ position: 'relative' }}>
94
- <button
95
- type="button"
96
- disabled={disabled}
97
- onClick={toggle}
98
- style={{
99
- width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
100
- padding: '8px 11px', background: 'var(--input-bg)',
101
- border: `1px solid ${open ? 'var(--accent)' : 'var(--border)'}`,
102
- boxShadow: open ? '0 0 0 3px rgba(249,158,44,.12)' : 'var(--shadow)',
103
- borderRadius: 'var(--rads)', cursor: disabled ? 'not-allowed' : 'pointer',
104
- color: selected ? 'var(--primary-text)' : 'var(--muted)',
105
- fontSize: '.82rem', fontFamily: 'var(--sans)', transition: 'all .14s',
106
- opacity: disabled ? .5 : 1, gap: 8,
107
- }}>
108
- <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
109
- {selected ? selected.label : placeholder}
110
- </span>
111
- <ChevronDown size={13} style={{ color: 'var(--muted)', flexShrink: 0, transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .2s' }} />
112
- </button>
113
- {menu}
114
- </div>
115
- );
116
- }
1
+ import { useState, useEffect, useRef, useCallback } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import { ChevronDown, Check } from 'lucide-react';
4
+
5
+ export default function Dropdown({ value, onChange, options, placeholder = 'Select...', disabled }) {
6
+ const [open, setOpen] = useState(false);
7
+ const [rect, setRect] = useState(null);
8
+ const btnRef = useRef();
9
+ const menuRef = useRef();
10
+
11
+ const updateRect = useCallback(() => {
12
+ if (btnRef.current) setRect(btnRef.current.getBoundingClientRect());
13
+ }, []);
14
+
15
+ // Close on outside click
16
+ useEffect(() => {
17
+ if (!open) return;
18
+ function onDown(e) {
19
+ const inBtn = btnRef.current?.contains(e.target);
20
+ const inMenu = menuRef.current?.contains(e.target);
21
+ if (!inBtn && !inMenu) setOpen(false);
22
+ }
23
+ // Use setTimeout to avoid closing on the same click that opened
24
+ const timer = setTimeout(() => {
25
+ document.addEventListener('mousedown', onDown, true);
26
+ }, 0);
27
+ return () => {
28
+ clearTimeout(timer);
29
+ document.removeEventListener('mousedown', onDown, true);
30
+ };
31
+ }, [open]);
32
+
33
+ // Update position on scroll/resize while open
34
+ useEffect(() => {
35
+ if (!open) return;
36
+ window.addEventListener('scroll', updateRect, true);
37
+ window.addEventListener('resize', updateRect);
38
+ return () => {
39
+ window.removeEventListener('scroll', updateRect, true);
40
+ window.removeEventListener('resize', updateRect);
41
+ };
42
+ }, [open, updateRect]);
43
+
44
+ function toggle() {
45
+ if (disabled) return;
46
+ updateRect();
47
+ setOpen(o => !o);
48
+ }
49
+
50
+ const selected = options.find(o => String(o.value) === String(value));
51
+
52
+ const menu = open && rect && createPortal(
53
+ <div
54
+ ref={menuRef}
55
+ data-dropdown="true"
56
+ style={{
57
+ position: 'fixed',
58
+ top: rect.bottom + 4,
59
+ left: rect.left,
60
+ width: rect.width,
61
+ background: 'var(--surface)',
62
+ border: '1px solid var(--border)',
63
+ borderRadius: 'var(--rads)',
64
+ boxShadow: 'var(--shadow-lg)',
65
+ zIndex: 999999,
66
+ animation: 'dropdownOpen .2s cubic-bezier(.16,1,.3,1)',
67
+ }}>
68
+ {options.map(o => {
69
+ const active = String(o.value) === String(value);
70
+ return (
71
+ <div key={o.value}
72
+ onMouseDown={e => e.stopPropagation()}
73
+ onClick={() => { onChange(o.value); setOpen(false); }}
74
+ onMouseEnter={e => { if (!active) e.currentTarget.style.background = 'var(--hover-bg)'; }}
75
+ onMouseLeave={e => { e.currentTarget.style.background = active ? 'var(--accent-bg)' : 'transparent'; }}
76
+ style={{
77
+ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
78
+ padding: '9px 12px', cursor: 'pointer', fontSize: '.82rem',
79
+ color: active ? 'var(--accent)' : 'var(--primary-text)',
80
+ background: active ? 'var(--accent-bg)' : 'transparent',
81
+ fontWeight: active ? 600 : 400,
82
+ }}>
83
+ <span>{o.label}</span>
84
+ {active && <Check size={12} />}
85
+ </div>
86
+ );
87
+ })}
88
+ </div>,
89
+ document.body
90
+ );
91
+
92
+ return (
93
+ <div ref={btnRef} style={{ position: 'relative' }}>
94
+ <button
95
+ type="button"
96
+ disabled={disabled}
97
+ onClick={toggle}
98
+ style={{
99
+ width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
100
+ padding: '8px 11px', background: 'var(--input-bg)',
101
+ border: `1px solid ${open ? 'var(--accent)' : 'var(--border)'}`,
102
+ boxShadow: open ? '0 0 0 3px rgba(249,158,44,.12)' : 'var(--shadow)',
103
+ borderRadius: 'var(--rads)', cursor: disabled ? 'not-allowed' : 'pointer',
104
+ color: selected ? 'var(--primary-text)' : 'var(--muted)',
105
+ fontSize: '.82rem', fontFamily: 'var(--sans)', transition: 'all .14s',
106
+ opacity: disabled ? .5 : 1, gap: 8,
107
+ }}>
108
+ <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
109
+ {selected ? selected.label : placeholder}
110
+ </span>
111
+ <ChevronDown size={13} style={{ color: 'var(--muted)', flexShrink: 0, transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .2s' }} />
112
+ </button>
113
+ {menu}
114
+ </div>
115
+ );
116
+ }
@@ -1,29 +1,29 @@
1
- import { useState, useRef } from 'react';
2
-
3
- export function Dropzone({ onFile, accept, hint, file }) {
4
- const [over, setOver] = useState(false);
5
- const inputRef = useRef(null);
6
- return (
7
- <div className={`dropzone${over ? ' over' : ''}`}
8
- onDragOver={e => { e.preventDefault(); setOver(true); }}
9
- onDragLeave={() => setOver(false)}
10
- onDrop={e => { e.preventDefault(); setOver(false); const f = e.dataTransfer.files[0]; if (f) onFile(f); }}
11
- onClick={() => inputRef.current?.click()}>
12
- <input ref={inputRef} type="file" accept={accept}
13
- onChange={e => e.target.files[0] && onFile(e.target.files[0])} style={{ display: 'none' }} />
14
- <div className="dropzone-icon">
15
- <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="var(--grey-3)" strokeWidth="1.5" strokeLinecap="round">
16
- <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/>
17
- </svg>
18
- </div>
19
- {file ? (
20
- <div className="dropzone-file">{file.name}</div>
21
- ) : (
22
- <>
23
- <div className="dropzone-text"><b>Click to select</b> or drag &amp; drop</div>
24
- {hint && <div className="dropzone-hint">{hint}</div>}
25
- </>
26
- )}
27
- </div>
28
- );
29
- }
1
+ import { useState, useRef } from 'react';
2
+
3
+ export function Dropzone({ onFile, accept, hint, file }) {
4
+ const [over, setOver] = useState(false);
5
+ const inputRef = useRef(null);
6
+ return (
7
+ <div className={`dropzone${over ? ' over' : ''}`}
8
+ onDragOver={e => { e.preventDefault(); setOver(true); }}
9
+ onDragLeave={() => setOver(false)}
10
+ onDrop={e => { e.preventDefault(); setOver(false); const f = e.dataTransfer.files[0]; if (f) onFile(f); }}
11
+ onClick={() => inputRef.current?.click()}>
12
+ <input ref={inputRef} type="file" accept={accept}
13
+ onChange={e => e.target.files[0] && onFile(e.target.files[0])} style={{ display: 'none' }} />
14
+ <div className="dropzone-icon">
15
+ <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="var(--grey-3)" strokeWidth="1.5" strokeLinecap="round">
16
+ <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/>
17
+ </svg>
18
+ </div>
19
+ {file ? (
20
+ <div className="dropzone-file">{file.name}</div>
21
+ ) : (
22
+ <>
23
+ <div className="dropzone-text"><b>Click to select</b> or drag &amp; drop</div>
24
+ {hint && <div className="dropzone-hint">{hint}</div>}
25
+ </>
26
+ )}
27
+ </div>
28
+ );
29
+ }