even-toolkit 1.4.2 → 1.5.1
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/CHANGELOG.md +27 -0
- package/README.md +7 -5
- package/dist/glasses/action-bar.d.ts +3 -3
- package/dist/glasses/action-bar.js +7 -7
- package/dist/glasses/action-bar.js.map +1 -1
- package/dist/glasses/action-map.js +3 -3
- package/dist/glasses/action-map.js.map +1 -1
- package/dist/glasses/gestures.d.ts +4 -0
- package/dist/glasses/gestures.d.ts.map +1 -1
- package/dist/glasses/gestures.js +44 -0
- package/dist/glasses/gestures.js.map +1 -1
- package/dist/glasses/glass-chat-display.d.ts +43 -0
- package/dist/glasses/glass-chat-display.d.ts.map +1 -0
- package/dist/glasses/glass-chat-display.js +102 -0
- package/dist/glasses/glass-chat-display.js.map +1 -0
- package/dist/glasses/glass-format.d.ts +50 -0
- package/dist/glasses/glass-format.d.ts.map +1 -0
- package/dist/glasses/glass-format.js +65 -0
- package/dist/glasses/glass-format.js.map +1 -0
- package/dist/glasses/index.d.ts +2 -0
- package/dist/glasses/index.d.ts.map +1 -1
- package/dist/glasses/index.js +2 -0
- package/dist/glasses/index.js.map +1 -1
- package/dist/glasses/useGlasses.js +1 -1
- package/dist/glasses/useGlasses.js.map +1 -1
- package/dist/stt/providers/deepgram.d.ts +1 -0
- package/dist/stt/providers/deepgram.d.ts.map +1 -1
- package/dist/stt/providers/deepgram.js +25 -8
- package/dist/stt/providers/deepgram.js.map +1 -1
- package/dist/web/components/dialog.d.ts.map +1 -1
- package/dist/web/components/dialog.js +13 -1
- package/dist/web/components/dialog.js.map +1 -1
- package/dist/web/components/drawer-shell.d.ts.map +1 -1
- package/dist/web/components/drawer-shell.js +11 -1
- package/dist/web/components/drawer-shell.js.map +1 -1
- package/dist/web/components/list-item.d.ts +1 -1
- package/dist/web/components/list-item.d.ts.map +1 -1
- package/dist/web/components/list-item.js +20 -5
- package/dist/web/components/list-item.js.map +1 -1
- package/dist/web/components/multi-select.d.ts +22 -0
- package/dist/web/components/multi-select.d.ts.map +1 -0
- package/dist/web/components/multi-select.js +52 -0
- package/dist/web/components/multi-select.js.map +1 -0
- package/dist/web/components/paged-carousel.d.ts +20 -0
- package/dist/web/components/paged-carousel.d.ts.map +1 -0
- package/dist/web/components/paged-carousel.js +140 -0
- package/dist/web/components/paged-carousel.js.map +1 -0
- package/dist/web/components/select.d.ts +13 -3
- package/dist/web/components/select.d.ts.map +1 -1
- package/dist/web/components/select.js +36 -3
- package/dist/web/components/select.js.map +1 -1
- package/dist/web/icons/svg-icons.js +1 -1
- package/dist/web/icons/svg-icons.js.map +1 -1
- package/dist/web/index.d.ts +4 -0
- package/dist/web/index.d.ts.map +1 -1
- package/dist/web/index.js +2 -0
- package/dist/web/index.js.map +1 -1
- package/glasses/action-bar.ts +7 -7
- package/glasses/action-map.ts +3 -3
- package/glasses/gestures.ts +50 -0
- package/glasses/glass-chat-display.ts +152 -0
- package/glasses/glass-format.ts +75 -0
- package/glasses/index.ts +2 -0
- package/glasses/useGlasses.ts +1 -1
- package/package.json +10 -1
- package/stt/providers/deepgram.ts +23 -7
- package/web/components/dialog.tsx +14 -3
- package/web/components/drawer-shell.tsx +11 -5
- package/web/components/list-item.tsx +25 -10
- package/web/components/multi-select.tsx +118 -0
- package/web/components/paged-carousel.tsx +201 -0
- package/web/components/select.tsx +90 -20
- package/web/icons/svg-icons.tsx +1 -2
- package/web/index.ts +6 -0
- package/web/theme/utilities.css +11 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
2
|
+
import { cn } from '../utils/cn';
|
|
3
|
+
import { IcStatusCheckbox, IcStatusSelectedBox } from '../icons/svg-icons';
|
|
4
|
+
|
|
5
|
+
interface MultiSelectOption {
|
|
6
|
+
value: string;
|
|
7
|
+
label: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface MultiSelectProps {
|
|
11
|
+
values: string[];
|
|
12
|
+
options: MultiSelectOption[];
|
|
13
|
+
onValuesChange: (values: string[]) => void;
|
|
14
|
+
placeholder?: string;
|
|
15
|
+
className?: string;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Custom multi-select dropdown with checkboxes. No native <select>.
|
|
21
|
+
*/
|
|
22
|
+
function MultiSelect({ values, options, onValuesChange, placeholder, className, disabled }: MultiSelectProps) {
|
|
23
|
+
const [open, setOpen] = useState(false);
|
|
24
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
25
|
+
|
|
26
|
+
const selectedLabels = options
|
|
27
|
+
.filter((o) => values.includes(o.value))
|
|
28
|
+
.map((o) => o.label);
|
|
29
|
+
|
|
30
|
+
const displayText = selectedLabels.length > 0
|
|
31
|
+
? selectedLabels.join(', ')
|
|
32
|
+
: placeholder ?? 'Select...';
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!open) return;
|
|
36
|
+
const handler = (e: MouseEvent) => {
|
|
37
|
+
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
|
|
38
|
+
};
|
|
39
|
+
document.addEventListener('mousedown', handler);
|
|
40
|
+
return () => document.removeEventListener('mousedown', handler);
|
|
41
|
+
}, [open]);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (!open) return;
|
|
45
|
+
const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') setOpen(false); };
|
|
46
|
+
document.addEventListener('keydown', handler);
|
|
47
|
+
return () => document.removeEventListener('keydown', handler);
|
|
48
|
+
}, [open]);
|
|
49
|
+
|
|
50
|
+
const toggleValue = useCallback((v: string) => {
|
|
51
|
+
if (values.includes(v)) {
|
|
52
|
+
onValuesChange(values.filter((x) => x !== v));
|
|
53
|
+
} else {
|
|
54
|
+
onValuesChange([...values, v]);
|
|
55
|
+
}
|
|
56
|
+
}, [values, onValuesChange]);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div ref={ref} className={cn('relative', className)} style={{ minWidth: 0 }}>
|
|
60
|
+
<button
|
|
61
|
+
type="button"
|
|
62
|
+
disabled={disabled}
|
|
63
|
+
onClick={() => !disabled && setOpen(!open)}
|
|
64
|
+
className={cn(
|
|
65
|
+
'h-9 w-full bg-input-bg text-text rounded-[6px] pl-3 pr-8 text-[13px] tracking-[-0.13px] text-left cursor-pointer border-none flex items-center',
|
|
66
|
+
'transition-colors hover:bg-surface-light',
|
|
67
|
+
disabled && 'opacity-50 cursor-default',
|
|
68
|
+
selectedLabels.length === 0 && 'text-text-dim',
|
|
69
|
+
)}
|
|
70
|
+
>
|
|
71
|
+
<span className="truncate flex-1">{displayText}</span>
|
|
72
|
+
<svg
|
|
73
|
+
className="absolute right-3 top-1/2 text-text-dim shrink-0"
|
|
74
|
+
width="10"
|
|
75
|
+
height="10"
|
|
76
|
+
viewBox="0 0 10 10"
|
|
77
|
+
fill="none"
|
|
78
|
+
style={{ transform: open ? 'translateY(-50%) rotate(180deg)' : 'translateY(-50%)', transition: 'transform 150ms ease' }}
|
|
79
|
+
>
|
|
80
|
+
<path d="M2.5 3.75L5 6.25L7.5 3.75" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
81
|
+
</svg>
|
|
82
|
+
</button>
|
|
83
|
+
|
|
84
|
+
{open && (
|
|
85
|
+
<div
|
|
86
|
+
className="absolute z-50 top-full left-0 right-0 mt-1 bg-surface rounded-[6px] border border-border overflow-hidden"
|
|
87
|
+
style={{ maxHeight: 200, overflowY: 'auto', boxShadow: '0 4px 12px rgba(0,0,0,0.1)' }}
|
|
88
|
+
>
|
|
89
|
+
{options.map((o) => {
|
|
90
|
+
const checked = values.includes(o.value);
|
|
91
|
+
return (
|
|
92
|
+
<button
|
|
93
|
+
key={o.value}
|
|
94
|
+
type="button"
|
|
95
|
+
className={cn(
|
|
96
|
+
'w-full text-left px-3 py-2 text-[13px] tracking-[-0.13px] cursor-pointer border-none transition-colors font-normal flex items-center gap-2',
|
|
97
|
+
checked ? 'bg-accent/5' : 'bg-transparent hover:bg-surface-light',
|
|
98
|
+
)}
|
|
99
|
+
onClick={() => toggleValue(o.value)}
|
|
100
|
+
>
|
|
101
|
+
{checked
|
|
102
|
+
? <IcStatusSelectedBox width={18} height={18} className="shrink-0" />
|
|
103
|
+
: <IcStatusCheckbox width={18} height={18} className="shrink-0 text-text-dim" />
|
|
104
|
+
}
|
|
105
|
+
<span className="text-text">{o.label}</span>
|
|
106
|
+
</button>
|
|
107
|
+
);
|
|
108
|
+
})}
|
|
109
|
+
</div>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
MultiSelect.displayName = 'MultiSelect';
|
|
116
|
+
|
|
117
|
+
export { MultiSelect };
|
|
118
|
+
export type { MultiSelectProps, MultiSelectOption };
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cn } from '../utils/cn';
|
|
3
|
+
|
|
4
|
+
interface PagedCarouselProps {
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
currentIndex?: number;
|
|
7
|
+
defaultIndex?: number;
|
|
8
|
+
onIndexChange?: (index: number) => void;
|
|
9
|
+
className?: string;
|
|
10
|
+
viewportClassName?: string;
|
|
11
|
+
trackClassName?: string;
|
|
12
|
+
slideClassName?: string;
|
|
13
|
+
allowWheel?: boolean;
|
|
14
|
+
allowSwipe?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface CardCarouselProps extends PagedCarouselProps {}
|
|
18
|
+
|
|
19
|
+
function useCarouselIndex(
|
|
20
|
+
slideCount: number,
|
|
21
|
+
currentIndex?: number,
|
|
22
|
+
defaultIndex?: number,
|
|
23
|
+
onIndexChange?: (index: number) => void,
|
|
24
|
+
) {
|
|
25
|
+
const [internalIndex, setInternalIndex] = React.useState(defaultIndex ?? 0);
|
|
26
|
+
const controlled = typeof currentIndex === 'number';
|
|
27
|
+
const activeIndex = controlled ? currentIndex! : internalIndex;
|
|
28
|
+
|
|
29
|
+
const setIndex = React.useCallback((nextIndex: number) => {
|
|
30
|
+
if (slideCount <= 0) return;
|
|
31
|
+
const clamped = Math.max(0, Math.min(nextIndex, slideCount - 1));
|
|
32
|
+
if (!controlled) setInternalIndex(clamped);
|
|
33
|
+
onIndexChange?.(clamped);
|
|
34
|
+
}, [controlled, onIndexChange, slideCount]);
|
|
35
|
+
|
|
36
|
+
return { activeIndex, setIndex };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function PagedCarousel({
|
|
40
|
+
children,
|
|
41
|
+
currentIndex,
|
|
42
|
+
defaultIndex = 0,
|
|
43
|
+
onIndexChange,
|
|
44
|
+
className,
|
|
45
|
+
viewportClassName,
|
|
46
|
+
trackClassName,
|
|
47
|
+
slideClassName,
|
|
48
|
+
allowWheel = true,
|
|
49
|
+
allowSwipe = true,
|
|
50
|
+
}: PagedCarouselProps) {
|
|
51
|
+
const slides = React.Children.toArray(children);
|
|
52
|
+
const slideCount = slides.length;
|
|
53
|
+
const { activeIndex, setIndex } = useCarouselIndex(slideCount, currentIndex, defaultIndex, onIndexChange);
|
|
54
|
+
|
|
55
|
+
const touchStartRef = React.useRef<{ x: number; y: number } | null>(null);
|
|
56
|
+
const pointerStartRef = React.useRef<{ x: number; y: number } | null>(null);
|
|
57
|
+
const wheelLockRef = React.useRef(0);
|
|
58
|
+
|
|
59
|
+
const moveBy = React.useCallback((delta: number) => {
|
|
60
|
+
setIndex(activeIndex + delta);
|
|
61
|
+
}, [activeIndex, setIndex]);
|
|
62
|
+
|
|
63
|
+
const handleTouchStart = React.useCallback((event: React.TouchEvent<HTMLDivElement>) => {
|
|
64
|
+
if (!allowSwipe) return;
|
|
65
|
+
const touch = event.touches[0];
|
|
66
|
+
touchStartRef.current = { x: touch.clientX, y: touch.clientY };
|
|
67
|
+
}, [allowSwipe]);
|
|
68
|
+
|
|
69
|
+
const handleTouchMove = React.useCallback((event: React.TouchEvent<HTMLDivElement>) => {
|
|
70
|
+
if (!allowSwipe) return;
|
|
71
|
+
const start = touchStartRef.current;
|
|
72
|
+
const touch = event.touches[0];
|
|
73
|
+
if (!start || !touch) return;
|
|
74
|
+
|
|
75
|
+
const deltaX = touch.clientX - start.x;
|
|
76
|
+
const deltaY = touch.clientY - start.y;
|
|
77
|
+
if (Math.abs(deltaX) < 32 || Math.abs(deltaX) <= Math.abs(deltaY)) return;
|
|
78
|
+
|
|
79
|
+
touchStartRef.current = null;
|
|
80
|
+
event.preventDefault();
|
|
81
|
+
if (deltaX < 0) moveBy(1);
|
|
82
|
+
else moveBy(-1);
|
|
83
|
+
}, [allowSwipe, moveBy]);
|
|
84
|
+
|
|
85
|
+
const handleTouchEnd = React.useCallback((event: React.TouchEvent<HTMLDivElement>) => {
|
|
86
|
+
if (!allowSwipe) return;
|
|
87
|
+
const start = touchStartRef.current;
|
|
88
|
+
const touch = event.changedTouches[0];
|
|
89
|
+
touchStartRef.current = null;
|
|
90
|
+
if (!start || !touch) return;
|
|
91
|
+
|
|
92
|
+
const deltaX = touch.clientX - start.x;
|
|
93
|
+
const deltaY = touch.clientY - start.y;
|
|
94
|
+
if (Math.abs(deltaX) < 32 || Math.abs(deltaX) <= Math.abs(deltaY)) return;
|
|
95
|
+
|
|
96
|
+
if (deltaX < 0) moveBy(1);
|
|
97
|
+
else moveBy(-1);
|
|
98
|
+
}, [allowSwipe, moveBy]);
|
|
99
|
+
|
|
100
|
+
const handlePointerDown = React.useCallback((event: React.PointerEvent<HTMLDivElement>) => {
|
|
101
|
+
if (!allowSwipe) return;
|
|
102
|
+
if (event.pointerType !== 'mouse' && event.pointerType !== 'pen') return;
|
|
103
|
+
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
104
|
+
pointerStartRef.current = { x: event.clientX, y: event.clientY };
|
|
105
|
+
}, [allowSwipe]);
|
|
106
|
+
|
|
107
|
+
const handlePointerMove = React.useCallback((event: React.PointerEvent<HTMLDivElement>) => {
|
|
108
|
+
if (!allowSwipe) return;
|
|
109
|
+
const start = pointerStartRef.current;
|
|
110
|
+
if (!start) return;
|
|
111
|
+
|
|
112
|
+
const deltaX = event.clientX - start.x;
|
|
113
|
+
const deltaY = event.clientY - start.y;
|
|
114
|
+
if (Math.abs(deltaX) < 32 || Math.abs(deltaX) <= Math.abs(deltaY)) return;
|
|
115
|
+
|
|
116
|
+
pointerStartRef.current = null;
|
|
117
|
+
if (deltaX < 0) moveBy(1);
|
|
118
|
+
else moveBy(-1);
|
|
119
|
+
}, [allowSwipe, moveBy]);
|
|
120
|
+
|
|
121
|
+
const handlePointerUp = React.useCallback((event: React.PointerEvent<HTMLDivElement>) => {
|
|
122
|
+
if (!allowSwipe) return;
|
|
123
|
+
const start = pointerStartRef.current;
|
|
124
|
+
pointerStartRef.current = null;
|
|
125
|
+
if (!start) return;
|
|
126
|
+
|
|
127
|
+
const deltaX = event.clientX - start.x;
|
|
128
|
+
const deltaY = event.clientY - start.y;
|
|
129
|
+
if (Math.abs(deltaX) < 32 || Math.abs(deltaX) <= Math.abs(deltaY)) return;
|
|
130
|
+
|
|
131
|
+
if (deltaX < 0) moveBy(1);
|
|
132
|
+
else moveBy(-1);
|
|
133
|
+
}, [allowSwipe, moveBy]);
|
|
134
|
+
|
|
135
|
+
const handleWheel = React.useCallback((event: React.WheelEvent<HTMLDivElement>) => {
|
|
136
|
+
if (!allowWheel) return;
|
|
137
|
+
if (Math.abs(event.deltaX) < 18 || Math.abs(event.deltaX) <= Math.abs(event.deltaY)) return;
|
|
138
|
+
|
|
139
|
+
const now = Date.now();
|
|
140
|
+
if (now - wheelLockRef.current < 280) return;
|
|
141
|
+
wheelLockRef.current = now;
|
|
142
|
+
|
|
143
|
+
event.preventDefault();
|
|
144
|
+
if (event.deltaX > 0) moveBy(1);
|
|
145
|
+
else moveBy(-1);
|
|
146
|
+
}, [allowWheel, moveBy]);
|
|
147
|
+
|
|
148
|
+
const handleKeyDown = React.useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
149
|
+
if (event.key === 'ArrowRight') {
|
|
150
|
+
event.preventDefault();
|
|
151
|
+
moveBy(1);
|
|
152
|
+
} else if (event.key === 'ArrowLeft') {
|
|
153
|
+
event.preventDefault();
|
|
154
|
+
moveBy(-1);
|
|
155
|
+
}
|
|
156
|
+
}, [moveBy]);
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<div className={cn('min-w-0', className)}>
|
|
160
|
+
<div
|
|
161
|
+
role="region"
|
|
162
|
+
aria-roledescription="carousel"
|
|
163
|
+
tabIndex={0}
|
|
164
|
+
onKeyDown={handleKeyDown}
|
|
165
|
+
onWheel={handleWheel}
|
|
166
|
+
onPointerDown={handlePointerDown}
|
|
167
|
+
onPointerMove={handlePointerMove}
|
|
168
|
+
onPointerUp={handlePointerUp}
|
|
169
|
+
onTouchStart={handleTouchStart}
|
|
170
|
+
onTouchMove={handleTouchMove}
|
|
171
|
+
onTouchEnd={handleTouchEnd}
|
|
172
|
+
className={cn('min-w-0 overflow-hidden outline-none', viewportClassName)}
|
|
173
|
+
style={{ touchAction: 'none' }}
|
|
174
|
+
>
|
|
175
|
+
<div className={cn('min-w-0', trackClassName)}>
|
|
176
|
+
<div
|
|
177
|
+
key={activeIndex}
|
|
178
|
+
className={cn('w-full min-w-0', slideClassName)}
|
|
179
|
+
aria-hidden={false}
|
|
180
|
+
>
|
|
181
|
+
{slides[activeIndex] ?? null}
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function CardCarousel(props: CardCarouselProps) {
|
|
190
|
+
return (
|
|
191
|
+
<PagedCarousel
|
|
192
|
+
{...props}
|
|
193
|
+
className={cn('min-w-0', props.className)}
|
|
194
|
+
viewportClassName={cn('rounded-[6px]', props.viewportClassName)}
|
|
195
|
+
slideClassName={cn('min-w-0', props.slideClassName)}
|
|
196
|
+
/>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export { PagedCarousel, CardCarousel };
|
|
201
|
+
export type { PagedCarouselProps, CardCarouselProps };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
2
2
|
import { cn } from '../utils/cn';
|
|
3
3
|
|
|
4
4
|
interface SelectOption {
|
|
@@ -6,30 +6,100 @@ interface SelectOption {
|
|
|
6
6
|
label: string;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
interface SelectProps
|
|
9
|
+
interface SelectProps {
|
|
10
|
+
value?: string;
|
|
10
11
|
options: SelectOption[];
|
|
11
12
|
onValueChange?: (value: string) => void;
|
|
13
|
+
placeholder?: string;
|
|
14
|
+
className?: string;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
dropdownPosition?: 'top' | 'bottom';
|
|
12
17
|
}
|
|
13
18
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Custom dropdown — no native <select>. Fully styled, no browser arrow issues.
|
|
21
|
+
*/
|
|
22
|
+
function Select({ value, options, onValueChange, placeholder, className, disabled, dropdownPosition = 'bottom' }: SelectProps) {
|
|
23
|
+
const [open, setOpen] = useState(false);
|
|
24
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
25
|
+
|
|
26
|
+
const selected = options.find((o) => o.value === value);
|
|
27
|
+
const label = selected?.label ?? placeholder ?? 'Select...';
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (!open) return;
|
|
31
|
+
const handler = (e: MouseEvent) => {
|
|
32
|
+
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
|
|
33
|
+
};
|
|
34
|
+
document.addEventListener('mousedown', handler);
|
|
35
|
+
return () => document.removeEventListener('mousedown', handler);
|
|
36
|
+
}, [open]);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!open) return;
|
|
40
|
+
const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') setOpen(false); };
|
|
41
|
+
document.addEventListener('keydown', handler);
|
|
42
|
+
return () => document.removeEventListener('keydown', handler);
|
|
43
|
+
}, [open]);
|
|
44
|
+
|
|
45
|
+
const handleSelect = useCallback((v: string) => {
|
|
46
|
+
onValueChange?.(v);
|
|
47
|
+
setOpen(false);
|
|
48
|
+
}, [onValueChange]);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div ref={ref} className={cn('relative', className)} style={{ minWidth: 0 }}>
|
|
52
|
+
<button
|
|
53
|
+
type="button"
|
|
54
|
+
disabled={disabled}
|
|
55
|
+
onClick={() => !disabled && setOpen(!open)}
|
|
56
|
+
className={cn(
|
|
57
|
+
'h-9 w-full bg-input-bg text-text rounded-[6px] pl-3 pr-8 text-[13px] tracking-[-0.13px] text-left cursor-pointer border-none flex items-center',
|
|
58
|
+
'transition-colors hover:bg-surface-light',
|
|
59
|
+
disabled && 'opacity-50 cursor-default',
|
|
60
|
+
)}
|
|
61
|
+
>
|
|
62
|
+
<span className="truncate flex-1">{label}</span>
|
|
63
|
+
<svg
|
|
64
|
+
className="absolute right-3 top-1/2 text-text-dim shrink-0"
|
|
65
|
+
width="10"
|
|
66
|
+
height="10"
|
|
67
|
+
viewBox="0 0 10 10"
|
|
68
|
+
fill="none"
|
|
69
|
+
style={{ transform: open ? 'translateY(-50%) rotate(180deg)' : 'translateY(-50%)', transition: 'transform 150ms ease' }}
|
|
70
|
+
>
|
|
71
|
+
<path d="M2.5 3.75L5 6.25L7.5 3.75" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
72
|
+
</svg>
|
|
73
|
+
</button>
|
|
74
|
+
|
|
75
|
+
{open && (
|
|
76
|
+
<div
|
|
77
|
+
className={cn(
|
|
78
|
+
'absolute z-50 left-0 right-0 bg-surface rounded-[6px] border border-border overflow-hidden',
|
|
79
|
+
dropdownPosition === 'top' ? 'bottom-full mb-1' : 'top-full mt-1',
|
|
80
|
+
)}
|
|
81
|
+
style={{ maxHeight: 200, overflowY: 'auto', boxShadow: '0 4px 12px rgba(0,0,0,0.1)' }}
|
|
82
|
+
>
|
|
83
|
+
{options.map((o) => (
|
|
84
|
+
<button
|
|
85
|
+
key={o.value}
|
|
86
|
+
type="button"
|
|
87
|
+
className={cn(
|
|
88
|
+
'w-full text-left px-3 py-2 text-[13px] tracking-[-0.13px] cursor-pointer border-none transition-colors font-normal',
|
|
89
|
+
o.value === value
|
|
90
|
+
? 'bg-accent text-text-highlight'
|
|
91
|
+
: 'bg-transparent text-text hover:bg-surface-light',
|
|
92
|
+
)}
|
|
93
|
+
onClick={() => handleSelect(o.value)}
|
|
94
|
+
>
|
|
95
|
+
{o.label}
|
|
96
|
+
</button>
|
|
97
|
+
))}
|
|
98
|
+
</div>
|
|
21
99
|
)}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
{options.map((o) => (
|
|
26
|
-
<option key={o.value} value={o.value}>
|
|
27
|
-
{o.label}
|
|
28
|
-
</option>
|
|
29
|
-
))}
|
|
30
|
-
</select>
|
|
31
|
-
),
|
|
32
|
-
);
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
33
103
|
|
|
34
104
|
Select.displayName = 'Select';
|
|
35
105
|
|
package/web/icons/svg-icons.tsx
CHANGED
|
@@ -486,7 +486,7 @@ export const IcFeatMessage: SvgIcon = (props) => (
|
|
|
486
486
|
export const IcFeatNavigate: SvgIcon = (props) => (
|
|
487
487
|
<svg viewBox="0 0 32 32" fill="none" {...props}>
|
|
488
488
|
<g clipPath="url(#clip0_10001_76324)">
|
|
489
|
-
<path d="M6 30H4V12H6V30ZM22 20H20V18H22V20ZM24 18H22V16H24V18ZM26 16H24V14H26V16ZM28 14H26V12H28V14ZM26 12H6V10H26V12ZM30 12H28V10H30V12ZM28 10H26V8H28V10ZM26 8H24V6H26V8ZM24 6H22V4H24V6ZM22 2V4H20V2H22Z" fill="
|
|
489
|
+
<path d="M6 30H4V12H6V30ZM22 20H20V18H22V20ZM24 18H22V16H24V18ZM26 16H24V14H26V16ZM28 14H26V12H28V14ZM26 12H6V10H26V12ZM30 12H28V10H30V12ZM28 10H26V8H28V10ZM26 8H24V6H26V8ZM24 6H22V4H24V6ZM22 2V4H20V2H22Z" fill="currentColor"/>
|
|
490
490
|
</g>
|
|
491
491
|
<defs>
|
|
492
492
|
<clipPath id="clip0_10001_76324">
|
|
@@ -2163,4 +2163,3 @@ export const IcShare = IcEditShare;
|
|
|
2163
2163
|
export const IcCopy = IcEditCopy;
|
|
2164
2164
|
export const IcCheck = IcStatusCheckmark;
|
|
2165
2165
|
export const IcMore = IcStatusMore;
|
|
2166
|
-
|
package/web/index.ts
CHANGED
|
@@ -40,6 +40,9 @@ export type { PillProps } from './components/pill';
|
|
|
40
40
|
export { Toggle } from './components/toggle';
|
|
41
41
|
export type { ToggleProps } from './components/toggle';
|
|
42
42
|
|
|
43
|
+
export { MultiSelect } from './components/multi-select';
|
|
44
|
+
export type { MultiSelectProps, MultiSelectOption } from './components/multi-select';
|
|
45
|
+
|
|
43
46
|
export { SegmentedControl } from './components/segmented-control';
|
|
44
47
|
export type { SegmentedControlProps, SegmentedControlOption } from './components/segmented-control';
|
|
45
48
|
|
|
@@ -118,6 +121,9 @@ export type { RadioGroupProps, RadioOption } from './components/radio-group';
|
|
|
118
121
|
export { Slider } from './components/slider';
|
|
119
122
|
export type { SliderProps } from './components/slider';
|
|
120
123
|
|
|
124
|
+
export { PagedCarousel, CardCarousel } from './components/paged-carousel';
|
|
125
|
+
export type { PagedCarouselProps, CardCarouselProps } from './components/paged-carousel';
|
|
126
|
+
|
|
121
127
|
export { Skeleton, skeletonVariants } from './components/skeleton';
|
|
122
128
|
export type { SkeletonProps } from './components/skeleton';
|
|
123
129
|
|
package/web/theme/utilities.css
CHANGED
|
@@ -23,6 +23,17 @@
|
|
|
23
23
|
transition: stroke-dashoffset 0.5s ease;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/* Force hide native select arrow on all browsers */
|
|
27
|
+
select {
|
|
28
|
+
-webkit-appearance: none !important;
|
|
29
|
+
-moz-appearance: none !important;
|
|
30
|
+
appearance: none !important;
|
|
31
|
+
background-image: none !important;
|
|
32
|
+
}
|
|
33
|
+
select::-ms-expand {
|
|
34
|
+
display: none;
|
|
35
|
+
}
|
|
36
|
+
|
|
26
37
|
/* Bottom sheet slide-up animation */
|
|
27
38
|
@keyframes slideUp {
|
|
28
39
|
from { transform: translateY(100%); }
|