xertica-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.
- package/App.tsx +182 -0
- package/README.md +330 -0
- package/assets/xertica-logo.svg +38 -0
- package/assets/xertica-x-logo.svg +21 -0
- package/bin/cli.ts +193 -0
- package/components/AssistenteXertica.tsx +2003 -0
- package/components/AudioPlayer.tsx +203 -0
- package/components/CodeBlock.tsx +242 -0
- package/components/DocumentEditor.tsx +504 -0
- package/components/ForgotPasswordPage.tsx +170 -0
- package/components/FormattedDocument.tsx +87 -0
- package/components/HomeContent.tsx +123 -0
- package/components/HomePage.tsx +70 -0
- package/components/LanguageSelector.tsx +54 -0
- package/components/LoginPage.tsx +199 -0
- package/components/MarkdownMessage.tsx +62 -0
- package/components/ModernChatInput.tsx +502 -0
- package/components/PodcastPlayer.tsx +409 -0
- package/components/ResetPasswordPage.tsx +234 -0
- package/components/Sidebar.tsx +489 -0
- package/components/TemplateContent.tsx +629 -0
- package/components/TemplatePage.tsx +70 -0
- package/components/ThemeToggle.tsx +65 -0
- package/components/VerifyEmailPage.tsx +187 -0
- package/components/XerticaLogo.tsx +69 -0
- package/components/XerticaOrbe.tsx +1339 -0
- package/components/XerticaXLogo.tsx +53 -0
- package/components/examples/DrawingMapExample.tsx +530 -0
- package/components/examples/FilterableMapExample.tsx +380 -0
- package/components/examples/LocationPickerExample.tsx +330 -0
- package/components/examples/MapExamples.tsx +280 -0
- package/components/examples/MapShowcase.tsx +446 -0
- package/components/examples/RouteMapExamples.tsx +329 -0
- package/components/examples/SimpleFilterableMap.tsx +192 -0
- package/components/examples/index.ts +52 -0
- package/components/figma/ImageWithFallback.tsx +27 -0
- package/components/index.ts +44 -0
- package/components/media/AudioPlayer.tsx +278 -0
- package/components/media/FloatingMediaWrapper.tsx +166 -0
- package/components/media/VideoPlayer.tsx +285 -0
- package/components/ui/accordion.tsx +66 -0
- package/components/ui/alert-dialog.tsx +159 -0
- package/components/ui/alert.tsx +91 -0
- package/components/ui/aspect-ratio.tsx +11 -0
- package/components/ui/avatar.tsx +65 -0
- package/components/ui/badge.tsx +55 -0
- package/components/ui/breadcrumb.tsx +109 -0
- package/components/ui/button.tsx +78 -0
- package/components/ui/calendar.tsx +235 -0
- package/components/ui/card.tsx +92 -0
- package/components/ui/carousel.tsx +241 -0
- package/components/ui/chart.tsx +353 -0
- package/components/ui/checkbox.tsx +32 -0
- package/components/ui/collapsible.tsx +33 -0
- package/components/ui/command.tsx +177 -0
- package/components/ui/context-menu.tsx +252 -0
- package/components/ui/dialog.tsx +138 -0
- package/components/ui/drawer.tsx +134 -0
- package/components/ui/dropdown-menu.tsx +257 -0
- package/components/ui/empty.tsx +90 -0
- package/components/ui/file-upload.tsx +152 -0
- package/components/ui/form.tsx +195 -0
- package/components/ui/google-maps-loader.tsx +379 -0
- package/components/ui/hover-card.tsx +44 -0
- package/components/ui/index.ts +242 -0
- package/components/ui/input-otp.tsx +77 -0
- package/components/ui/input.tsx +38 -0
- package/components/ui/label.tsx +24 -0
- package/components/ui/map-config.ts +12 -0
- package/components/ui/map-layers.tsx +129 -0
- package/components/ui/map.exports.ts +31 -0
- package/components/ui/map.tsx +412 -0
- package/components/ui/menubar.tsx +276 -0
- package/components/ui/navigation-menu.tsx +162 -0
- package/components/ui/notification-badge.tsx +61 -0
- package/components/ui/page-header.tsx +229 -0
- package/components/ui/pagination.tsx +127 -0
- package/components/ui/popover.tsx +48 -0
- package/components/ui/progress.tsx +31 -0
- package/components/ui/radio-group.tsx +56 -0
- package/components/ui/rating.tsx +102 -0
- package/components/ui/resizable.tsx +405 -0
- package/components/ui/route-map.tsx +246 -0
- package/components/ui/scroll-area.tsx +58 -0
- package/components/ui/search.tsx +70 -0
- package/components/ui/select.tsx +176 -0
- package/components/ui/separator.tsx +28 -0
- package/components/ui/sheet.tsx +138 -0
- package/components/ui/sidebar.tsx +726 -0
- package/components/ui/simple-map.tsx +92 -0
- package/components/ui/skeleton.tsx +13 -0
- package/components/ui/slider.tsx +58 -0
- package/components/ui/sonner.tsx +77 -0
- package/components/ui/stats-card.tsx +84 -0
- package/components/ui/stepper.tsx +126 -0
- package/components/ui/switch.tsx +34 -0
- package/components/ui/table.tsx +116 -0
- package/components/ui/tabs.tsx +66 -0
- package/components/ui/textarea.tsx +26 -0
- package/components/ui/timeline.tsx +140 -0
- package/components/ui/toggle-group.tsx +71 -0
- package/components/ui/toggle.tsx +46 -0
- package/components/ui/tooltip.tsx +61 -0
- package/components/ui/tree-view.tsx +123 -0
- package/components/ui/use-mobile.ts +24 -0
- package/components/ui/utils.ts +6 -0
- package/components/ui/xertica-assistant.tsx +1420 -0
- package/contexts/ApiKeyContext.tsx +123 -0
- package/contexts/AssistenteContext.tsx +118 -0
- package/contexts/BrandColorsContext.tsx +551 -0
- package/contexts/LanguageContext.tsx +36 -0
- package/contexts/ThemeContext.tsx +85 -0
- package/dist/cli.js +20922 -0
- package/eslint.config.js +41 -0
- package/guidelines/Guidelines.md +61 -0
- package/hooks/useTheme.ts +4 -0
- package/imports/Podcast.tsx +389 -0
- package/imports/XerticaAi.tsx +46 -0
- package/imports/XerticaX.tsx +20 -0
- package/imports/svg-aueiaqngck.ts +11 -0
- package/imports/svg-v9krss1ozd.ts +16 -0
- package/imports/svg-vhrdofe3qe.ts +5 -0
- package/index.css +4448 -0
- package/index.html +14 -0
- package/main.tsx +10 -0
- package/package.json +119 -0
- package/postcss.config.js +6 -0
- package/routes.tsx +33 -0
- package/styles/globals.css +15 -0
- package/styles/xertica/app-overrides/chat.css +61 -0
- package/styles/xertica/app-overrides/scrollbar.css +33 -0
- package/styles/xertica/base.css +70 -0
- package/styles/xertica/integrations/google-maps.css +76 -0
- package/styles/xertica/integrations/sonner.css +73 -0
- package/styles/xertica/theme-map.css +88 -0
- package/styles/xertica/tokens.css +190 -0
- package/tsconfig.json +31 -0
- package/tsconfig.node.json +10 -0
- package/utils/gemini.ts +140 -0
- package/vite-env.d.ts +12 -0
- package/vite.config.ts +36 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import { cn } from './utils';
|
|
3
|
+
import { useGoogleMapsLoader } from './google-maps-loader';
|
|
4
|
+
|
|
5
|
+
export interface RouteMapProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
6
|
+
origin: { lat: number; lng: number };
|
|
7
|
+
destination: { lat: number; lng: number };
|
|
8
|
+
waypoints?: Array<{ lat: number; lng: number }>;
|
|
9
|
+
travelMode?: 'DRIVING' | 'WALKING' | 'BICYCLING' | 'TRANSIT';
|
|
10
|
+
height?: string;
|
|
11
|
+
apiKey?: string;
|
|
12
|
+
mapContainerClassName?: string;
|
|
13
|
+
disableDefaultUI?: boolean;
|
|
14
|
+
zoomControl?: boolean;
|
|
15
|
+
streetViewControl?: boolean;
|
|
16
|
+
mapTypeControl?: boolean;
|
|
17
|
+
fullscreenControl?: boolean;
|
|
18
|
+
onRouteCalculated?: (distance: string, duration: string) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const RouteMapContent = React.forwardRef<HTMLDivElement, RouteMapProps & { apiKey: string }>(
|
|
22
|
+
({ apiKey, ...props }, ref) => {
|
|
23
|
+
const { isLoaded, loadError } = useGoogleMapsLoader();
|
|
24
|
+
|
|
25
|
+
const {
|
|
26
|
+
origin,
|
|
27
|
+
destination,
|
|
28
|
+
waypoints = [],
|
|
29
|
+
travelMode = 'DRIVING',
|
|
30
|
+
height = "450px",
|
|
31
|
+
mapContainerClassName,
|
|
32
|
+
disableDefaultUI = false,
|
|
33
|
+
zoomControl = true,
|
|
34
|
+
streetViewControl = false,
|
|
35
|
+
mapTypeControl = false,
|
|
36
|
+
fullscreenControl = true,
|
|
37
|
+
onRouteCalculated,
|
|
38
|
+
className,
|
|
39
|
+
...divProps
|
|
40
|
+
} = props;
|
|
41
|
+
|
|
42
|
+
const mapRef = useRef<google.maps.Map | null>(null);
|
|
43
|
+
const mapContainerRef = useRef<HTMLDivElement>(null);
|
|
44
|
+
const directionsRendererRef = useRef<google.maps.DirectionsRenderer | null>(null);
|
|
45
|
+
const isInitializedRef = useRef(false);
|
|
46
|
+
const isCalculatingRef = useRef(false);
|
|
47
|
+
|
|
48
|
+
// Initialize map once
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!isLoaded || !mapContainerRef.current || isInitializedRef.current) return;
|
|
51
|
+
|
|
52
|
+
isInitializedRef.current = true;
|
|
53
|
+
const map = new google.maps.Map(mapContainerRef.current, {
|
|
54
|
+
center: origin,
|
|
55
|
+
zoom: 13,
|
|
56
|
+
mapId: 'xertica-route-map',
|
|
57
|
+
disableDefaultUI,
|
|
58
|
+
zoomControl,
|
|
59
|
+
streetViewControl,
|
|
60
|
+
mapTypeControl,
|
|
61
|
+
fullscreenControl,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
mapRef.current = map;
|
|
65
|
+
|
|
66
|
+
// Create directions renderer
|
|
67
|
+
const computedStyle = getComputedStyle(document.documentElement);
|
|
68
|
+
const primaryColor = computedStyle.getPropertyValue('--primary').trim() || '#4F46E5';
|
|
69
|
+
|
|
70
|
+
directionsRendererRef.current = new google.maps.DirectionsRenderer({
|
|
71
|
+
map: map,
|
|
72
|
+
suppressMarkers: false,
|
|
73
|
+
polylineOptions: {
|
|
74
|
+
strokeColor: primaryColor,
|
|
75
|
+
strokeWeight: 5,
|
|
76
|
+
strokeOpacity: 0.8,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return () => {
|
|
81
|
+
if (directionsRendererRef.current) {
|
|
82
|
+
directionsRendererRef.current.setMap(null);
|
|
83
|
+
directionsRendererRef.current = null;
|
|
84
|
+
}
|
|
85
|
+
isInitializedRef.current = false;
|
|
86
|
+
mapRef.current = null;
|
|
87
|
+
};
|
|
88
|
+
}, [isLoaded]);
|
|
89
|
+
|
|
90
|
+
// Calculate route
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
const map = mapRef.current;
|
|
93
|
+
const renderer = directionsRendererRef.current;
|
|
94
|
+
|
|
95
|
+
if (!map || !renderer || !isLoaded || isCalculatingRef.current) return;
|
|
96
|
+
if (!origin || !destination) return;
|
|
97
|
+
|
|
98
|
+
isCalculatingRef.current = true;
|
|
99
|
+
|
|
100
|
+
const directionsService = new google.maps.DirectionsService();
|
|
101
|
+
|
|
102
|
+
const request: google.maps.DirectionsRequest = {
|
|
103
|
+
origin: origin,
|
|
104
|
+
destination: destination,
|
|
105
|
+
waypoints: waypoints.map(wp => ({
|
|
106
|
+
location: wp,
|
|
107
|
+
stopover: true
|
|
108
|
+
})),
|
|
109
|
+
travelMode: google.maps.TravelMode[travelMode],
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
directionsService.route(request, (result, status) => {
|
|
113
|
+
isCalculatingRef.current = false;
|
|
114
|
+
|
|
115
|
+
if (status === 'OK' && result) {
|
|
116
|
+
renderer.setDirections(result);
|
|
117
|
+
|
|
118
|
+
// Calculate total distance and duration
|
|
119
|
+
const route = result.routes[0];
|
|
120
|
+
if (route?.legs?.length > 0 && onRouteCalculated) {
|
|
121
|
+
let totalDistance = 0;
|
|
122
|
+
let totalDuration = 0;
|
|
123
|
+
|
|
124
|
+
route.legs.forEach(leg => {
|
|
125
|
+
if (leg.distance) totalDistance += leg.distance.value;
|
|
126
|
+
if (leg.duration) totalDuration += leg.duration.value;
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const distanceKm = (totalDistance / 1000).toFixed(1);
|
|
130
|
+
const distanceText = `${distanceKm} km`;
|
|
131
|
+
|
|
132
|
+
const hours = Math.floor(totalDuration / 3600);
|
|
133
|
+
const minutes = Math.floor((totalDuration % 3600) / 60);
|
|
134
|
+
const durationText = hours > 0 ? `${hours}h ${minutes}min` : `${minutes} min`;
|
|
135
|
+
|
|
136
|
+
onRouteCalculated(distanceText, durationText);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}, [isLoaded, origin.lat, origin.lng, destination.lat, destination.lng, travelMode]);
|
|
141
|
+
|
|
142
|
+
if (loadError) {
|
|
143
|
+
return (
|
|
144
|
+
<div
|
|
145
|
+
ref={ref}
|
|
146
|
+
className={cn(
|
|
147
|
+
"relative rounded-[var(--radius-card)] border border-destructive/50 overflow-hidden bg-destructive/5",
|
|
148
|
+
className
|
|
149
|
+
)}
|
|
150
|
+
style={{ height }}
|
|
151
|
+
{...divProps}
|
|
152
|
+
>
|
|
153
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
154
|
+
<div className="text-center space-y-2 p-6">
|
|
155
|
+
<div className="text-destructive">
|
|
156
|
+
<svg className="w-12 h-12 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
157
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
158
|
+
</svg>
|
|
159
|
+
</div>
|
|
160
|
+
<p className="text-sm text-destructive">Failed to load Google Maps</p>
|
|
161
|
+
<p className="text-xs text-muted-foreground">Check API key in Settings</p>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!isLoaded) {
|
|
169
|
+
return (
|
|
170
|
+
<div
|
|
171
|
+
ref={ref}
|
|
172
|
+
className={cn(
|
|
173
|
+
"relative rounded-[var(--radius-card)] border border-border overflow-hidden bg-muted animate-pulse",
|
|
174
|
+
className
|
|
175
|
+
)}
|
|
176
|
+
style={{ height }}
|
|
177
|
+
{...divProps}
|
|
178
|
+
/>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<div
|
|
184
|
+
ref={ref}
|
|
185
|
+
className={cn(
|
|
186
|
+
"relative rounded-[var(--radius-card)] border border-border overflow-hidden",
|
|
187
|
+
className
|
|
188
|
+
)}
|
|
189
|
+
{...divProps}
|
|
190
|
+
>
|
|
191
|
+
<div
|
|
192
|
+
ref={mapContainerRef}
|
|
193
|
+
style={{ height }}
|
|
194
|
+
className={cn("rounded-[var(--radius-card)] w-full", mapContainerClassName)}
|
|
195
|
+
/>
|
|
196
|
+
</div>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
);
|
|
200
|
+
RouteMapContent.displayName = "RouteMapContent";
|
|
201
|
+
|
|
202
|
+
export const RouteMap = React.forwardRef<HTMLDivElement, RouteMapProps>(
|
|
203
|
+
(props, ref) => {
|
|
204
|
+
const effectiveApiKey = props.apiKey || "";
|
|
205
|
+
|
|
206
|
+
const isValidKey = effectiveApiKey &&
|
|
207
|
+
effectiveApiKey !== "YOUR_GOOGLE_MAPS_API_KEY_HERE" &&
|
|
208
|
+
effectiveApiKey.startsWith("AIza");
|
|
209
|
+
|
|
210
|
+
if (!isValidKey) {
|
|
211
|
+
const {
|
|
212
|
+
origin, destination, waypoints, travelMode, height, apiKey,
|
|
213
|
+
mapContainerClassName, disableDefaultUI, zoomControl,
|
|
214
|
+
streetViewControl, mapTypeControl, fullscreenControl,
|
|
215
|
+
onRouteCalculated, ...divProps
|
|
216
|
+
} = props;
|
|
217
|
+
|
|
218
|
+
return (
|
|
219
|
+
<div
|
|
220
|
+
ref={ref}
|
|
221
|
+
className={cn(
|
|
222
|
+
"relative rounded-[var(--radius-card)] border border-border overflow-hidden bg-muted",
|
|
223
|
+
props.className
|
|
224
|
+
)}
|
|
225
|
+
style={{ height: props.height || "450px" }}
|
|
226
|
+
{...divProps}
|
|
227
|
+
>
|
|
228
|
+
<div className="absolute inset-0 flex items-center justify-center bg-gradient-to-br from-muted/50 to-muted">
|
|
229
|
+
<div className="text-center space-y-3 p-6">
|
|
230
|
+
<div className="w-16 h-16 mx-auto bg-primary/10 rounded-full flex items-center justify-center">
|
|
231
|
+
<svg className="w-8 h-8 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
232
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
|
233
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
234
|
+
</svg>
|
|
235
|
+
</div>
|
|
236
|
+
<p className="text-sm text-muted-foreground">Configure Google Maps API Key in Settings</p>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return <RouteMapContent ref={ref} {...props} apiKey={effectiveApiKey} />;
|
|
244
|
+
}
|
|
245
|
+
);
|
|
246
|
+
RouteMap.displayName = "RouteMap";
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
|
5
|
+
|
|
6
|
+
import { cn } from "./utils";
|
|
7
|
+
|
|
8
|
+
function ScrollArea({
|
|
9
|
+
className,
|
|
10
|
+
children,
|
|
11
|
+
...props
|
|
12
|
+
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
|
13
|
+
return (
|
|
14
|
+
<ScrollAreaPrimitive.Root
|
|
15
|
+
data-slot="scroll-area"
|
|
16
|
+
className={cn("relative", className)}
|
|
17
|
+
{...props}
|
|
18
|
+
>
|
|
19
|
+
<ScrollAreaPrimitive.Viewport
|
|
20
|
+
data-slot="scroll-area-viewport"
|
|
21
|
+
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
|
|
22
|
+
>
|
|
23
|
+
{children}
|
|
24
|
+
</ScrollAreaPrimitive.Viewport>
|
|
25
|
+
<ScrollBar />
|
|
26
|
+
<ScrollAreaPrimitive.Corner />
|
|
27
|
+
</ScrollAreaPrimitive.Root>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function ScrollBar({
|
|
32
|
+
className,
|
|
33
|
+
orientation = "vertical",
|
|
34
|
+
...props
|
|
35
|
+
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
|
36
|
+
return (
|
|
37
|
+
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
|
38
|
+
data-slot="scroll-area-scrollbar"
|
|
39
|
+
orientation={orientation}
|
|
40
|
+
className={cn(
|
|
41
|
+
"flex touch-none p-px transition-colors select-none",
|
|
42
|
+
orientation === "vertical" &&
|
|
43
|
+
"h-full w-2.5 border-l border-l-transparent",
|
|
44
|
+
orientation === "horizontal" &&
|
|
45
|
+
"h-2.5 flex-col border-t border-t-transparent",
|
|
46
|
+
className,
|
|
47
|
+
)}
|
|
48
|
+
{...props}
|
|
49
|
+
>
|
|
50
|
+
<ScrollAreaPrimitive.ScrollAreaThumb
|
|
51
|
+
data-slot="scroll-area-thumb"
|
|
52
|
+
className="bg-border relative flex-1 rounded-full"
|
|
53
|
+
/>
|
|
54
|
+
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export { ScrollArea, ScrollBar };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Search as SearchIcon, X } from "lucide-react";
|
|
3
|
+
import { cn } from "./utils";
|
|
4
|
+
|
|
5
|
+
export interface SearchProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
6
|
+
onSearch?: (value: string) => void;
|
|
7
|
+
onClear?: () => void;
|
|
8
|
+
containerClassName?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const Search = React.forwardRef<HTMLInputElement, SearchProps>(
|
|
12
|
+
({ className, containerClassName, onSearch, onClear, onChange, ...props }, ref) => {
|
|
13
|
+
const [value, setValue] = React.useState(props.value || "");
|
|
14
|
+
|
|
15
|
+
React.useEffect(() => {
|
|
16
|
+
if (props.value !== undefined) {
|
|
17
|
+
setValue(props.value);
|
|
18
|
+
}
|
|
19
|
+
}, [props.value]);
|
|
20
|
+
|
|
21
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
22
|
+
const newValue = e.target.value;
|
|
23
|
+
setValue(newValue);
|
|
24
|
+
onChange?.(e);
|
|
25
|
+
onSearch?.(newValue);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const handleClear = () => {
|
|
29
|
+
setValue("");
|
|
30
|
+
onClear?.();
|
|
31
|
+
onSearch?.("");
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
35
|
+
if (e.key === "Escape") {
|
|
36
|
+
handleClear();
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className={cn("relative w-full", containerClassName)}>
|
|
42
|
+
<SearchIcon className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
43
|
+
<input
|
|
44
|
+
ref={ref}
|
|
45
|
+
type="search"
|
|
46
|
+
value={value}
|
|
47
|
+
onChange={handleChange}
|
|
48
|
+
onKeyDown={handleKeyDown}
|
|
49
|
+
className={cn(
|
|
50
|
+
"flex h-10 w-full rounded-[var(--radius)] border border-input bg-input-background px-10 py-2 text-foreground ring-offset-background transition-colors file:border-0 file:bg-transparent placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
|
51
|
+
className
|
|
52
|
+
)}
|
|
53
|
+
{...props}
|
|
54
|
+
/>
|
|
55
|
+
{value && (
|
|
56
|
+
<button
|
|
57
|
+
type="button"
|
|
58
|
+
onClick={handleClear}
|
|
59
|
+
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground transition-colors hover:text-foreground focus:outline-none"
|
|
60
|
+
>
|
|
61
|
+
<X className="h-4 w-4" />
|
|
62
|
+
</button>
|
|
63
|
+
)}
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
Search.displayName = "Search";
|
|
69
|
+
|
|
70
|
+
export { Search };
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as SelectPrimitive from "@radix-ui/react-select"
|
|
5
|
+
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
|
6
|
+
|
|
7
|
+
import { cn } from "./utils"
|
|
8
|
+
|
|
9
|
+
const Select = SelectPrimitive.Root
|
|
10
|
+
|
|
11
|
+
const SelectGroup = SelectPrimitive.Group
|
|
12
|
+
|
|
13
|
+
const SelectValue = SelectPrimitive.Value
|
|
14
|
+
|
|
15
|
+
const SelectTrigger = React.forwardRef<
|
|
16
|
+
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
|
17
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & {
|
|
18
|
+
size?: "sm" | "default";
|
|
19
|
+
}
|
|
20
|
+
>(({ className, children, size = "default", ...props }, ref) => (
|
|
21
|
+
<SelectPrimitive.Trigger
|
|
22
|
+
ref={ref}
|
|
23
|
+
data-slot="select-trigger"
|
|
24
|
+
data-size={size}
|
|
25
|
+
className={cn(
|
|
26
|
+
"flex w-full items-center justify-between gap-2 px-3 py-2 text-sm bg-background rounded-[var(--radius)] border border-border transition-colors outline-none whitespace-nowrap text-foreground",
|
|
27
|
+
"data-[placeholder]:text-muted-foreground",
|
|
28
|
+
"focus:ring-2 focus:ring-primary focus:border-transparent",
|
|
29
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
30
|
+
"data-[size=default]:h-9 data-[size=sm]:h-8",
|
|
31
|
+
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
32
|
+
"*:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2",
|
|
33
|
+
className
|
|
34
|
+
)}
|
|
35
|
+
{...props}
|
|
36
|
+
>
|
|
37
|
+
{children}
|
|
38
|
+
<SelectPrimitive.Icon asChild>
|
|
39
|
+
<ChevronDown className="size-4 opacity-50" />
|
|
40
|
+
</SelectPrimitive.Icon>
|
|
41
|
+
</SelectPrimitive.Trigger>
|
|
42
|
+
))
|
|
43
|
+
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
|
44
|
+
|
|
45
|
+
const SelectScrollUpButton = React.forwardRef<
|
|
46
|
+
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
|
47
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
|
48
|
+
>(({ className, ...props }, ref) => (
|
|
49
|
+
<SelectPrimitive.ScrollUpButton
|
|
50
|
+
ref={ref}
|
|
51
|
+
data-slot="select-scroll-up-button"
|
|
52
|
+
className={cn(
|
|
53
|
+
"flex cursor-default items-center justify-center py-1",
|
|
54
|
+
className
|
|
55
|
+
)}
|
|
56
|
+
{...props}
|
|
57
|
+
>
|
|
58
|
+
<ChevronUp className="size-4" />
|
|
59
|
+
</SelectPrimitive.ScrollUpButton>
|
|
60
|
+
))
|
|
61
|
+
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
|
62
|
+
|
|
63
|
+
const SelectScrollDownButton = React.forwardRef<
|
|
64
|
+
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
|
65
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
|
66
|
+
>(({ className, ...props }, ref) => (
|
|
67
|
+
<SelectPrimitive.ScrollDownButton
|
|
68
|
+
ref={ref}
|
|
69
|
+
data-slot="select-scroll-down-button"
|
|
70
|
+
className={cn(
|
|
71
|
+
"flex cursor-default items-center justify-center py-1",
|
|
72
|
+
className
|
|
73
|
+
)}
|
|
74
|
+
{...props}
|
|
75
|
+
>
|
|
76
|
+
<ChevronDown className="size-4" />
|
|
77
|
+
</SelectPrimitive.ScrollDownButton>
|
|
78
|
+
))
|
|
79
|
+
SelectScrollDownButton.displayName =
|
|
80
|
+
SelectPrimitive.ScrollDownButton.displayName
|
|
81
|
+
|
|
82
|
+
const SelectContent = React.forwardRef<
|
|
83
|
+
React.ElementRef<typeof SelectPrimitive.Content>,
|
|
84
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
|
85
|
+
>(({ className, children, position = "popper", ...props }, ref) => (
|
|
86
|
+
<SelectPrimitive.Portal>
|
|
87
|
+
<SelectPrimitive.Content
|
|
88
|
+
ref={ref}
|
|
89
|
+
data-slot="select-content"
|
|
90
|
+
className={cn(
|
|
91
|
+
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-[var(--radius)] border border-border shadow-md",
|
|
92
|
+
position === "popper" &&
|
|
93
|
+
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
|
94
|
+
className
|
|
95
|
+
)}
|
|
96
|
+
position={position}
|
|
97
|
+
{...props}
|
|
98
|
+
>
|
|
99
|
+
<SelectScrollUpButton />
|
|
100
|
+
<SelectPrimitive.Viewport
|
|
101
|
+
className={cn(
|
|
102
|
+
"p-1",
|
|
103
|
+
position === "popper" &&
|
|
104
|
+
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
|
105
|
+
)}
|
|
106
|
+
>
|
|
107
|
+
{children}
|
|
108
|
+
</SelectPrimitive.Viewport>
|
|
109
|
+
<SelectScrollDownButton />
|
|
110
|
+
</SelectPrimitive.Content>
|
|
111
|
+
</SelectPrimitive.Portal>
|
|
112
|
+
))
|
|
113
|
+
SelectContent.displayName = SelectPrimitive.Content.displayName
|
|
114
|
+
|
|
115
|
+
const SelectLabel = React.forwardRef<
|
|
116
|
+
React.ElementRef<typeof SelectPrimitive.Label>,
|
|
117
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
|
118
|
+
>(({ className, ...props }, ref) => (
|
|
119
|
+
<SelectPrimitive.Label
|
|
120
|
+
ref={ref}
|
|
121
|
+
data-slot="select-label"
|
|
122
|
+
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
|
123
|
+
{...props}
|
|
124
|
+
/>
|
|
125
|
+
))
|
|
126
|
+
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
|
127
|
+
|
|
128
|
+
const SelectItem = React.forwardRef<
|
|
129
|
+
React.ElementRef<typeof SelectPrimitive.Item>,
|
|
130
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
|
131
|
+
>(({ className, children, ...props }, ref) => (
|
|
132
|
+
<SelectPrimitive.Item
|
|
133
|
+
ref={ref}
|
|
134
|
+
data-slot="select-item"
|
|
135
|
+
className={cn(
|
|
136
|
+
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
|
137
|
+
className
|
|
138
|
+
)}
|
|
139
|
+
{...props}
|
|
140
|
+
>
|
|
141
|
+
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
|
142
|
+
<SelectPrimitive.ItemIndicator>
|
|
143
|
+
<Check className="size-4" />
|
|
144
|
+
</SelectPrimitive.ItemIndicator>
|
|
145
|
+
</span>
|
|
146
|
+
|
|
147
|
+
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
|
148
|
+
</SelectPrimitive.Item>
|
|
149
|
+
))
|
|
150
|
+
SelectItem.displayName = SelectPrimitive.Item.displayName
|
|
151
|
+
|
|
152
|
+
const SelectSeparator = React.forwardRef<
|
|
153
|
+
React.ElementRef<typeof SelectPrimitive.Separator>,
|
|
154
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
|
155
|
+
>(({ className, ...props }, ref) => (
|
|
156
|
+
<SelectPrimitive.Separator
|
|
157
|
+
ref={ref}
|
|
158
|
+
data-slot="select-separator"
|
|
159
|
+
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
|
160
|
+
{...props}
|
|
161
|
+
/>
|
|
162
|
+
))
|
|
163
|
+
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
|
164
|
+
|
|
165
|
+
export {
|
|
166
|
+
Select,
|
|
167
|
+
SelectGroup,
|
|
168
|
+
SelectValue,
|
|
169
|
+
SelectTrigger,
|
|
170
|
+
SelectContent,
|
|
171
|
+
SelectLabel,
|
|
172
|
+
SelectItem,
|
|
173
|
+
SelectSeparator,
|
|
174
|
+
SelectScrollUpButton,
|
|
175
|
+
SelectScrollDownButton,
|
|
176
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
|
5
|
+
|
|
6
|
+
import { cn } from "./utils";
|
|
7
|
+
|
|
8
|
+
function Separator({
|
|
9
|
+
className,
|
|
10
|
+
orientation = "horizontal",
|
|
11
|
+
decorative = true,
|
|
12
|
+
...props
|
|
13
|
+
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
|
14
|
+
return (
|
|
15
|
+
<SeparatorPrimitive.Root
|
|
16
|
+
data-slot="separator-root"
|
|
17
|
+
decorative={decorative}
|
|
18
|
+
orientation={orientation}
|
|
19
|
+
className={cn(
|
|
20
|
+
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
|
21
|
+
className,
|
|
22
|
+
)}
|
|
23
|
+
{...props}
|
|
24
|
+
/>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { Separator };
|