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,412 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import { cn } from './utils';
|
|
4
|
+
import { useGoogleMapsLoader } from './google-maps-loader';
|
|
5
|
+
import { useMapLayers, MapLayersConfig } from './map-layers';
|
|
6
|
+
|
|
7
|
+
export interface MapProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
8
|
+
center?: { lat: number; lng: number };
|
|
9
|
+
zoom?: number;
|
|
10
|
+
markers?: Array<{
|
|
11
|
+
position: { lat: number; lng: number };
|
|
12
|
+
label?: string;
|
|
13
|
+
title?: string;
|
|
14
|
+
info?: string;
|
|
15
|
+
customColor?: string;
|
|
16
|
+
icon?: string;
|
|
17
|
+
iconSvg?: string;
|
|
18
|
+
iconColor?: string;
|
|
19
|
+
infoWindowContent?: string; // Custom HTML content for InfoWindow
|
|
20
|
+
richContent?: React.ReactNode; // Custom React component for InfoWindow
|
|
21
|
+
}>[];
|
|
22
|
+
circle?: {
|
|
23
|
+
center: { lat: number; lng: number };
|
|
24
|
+
radius: number;
|
|
25
|
+
fillColor?: string;
|
|
26
|
+
strokeColor?: string;
|
|
27
|
+
};
|
|
28
|
+
polygon?: {
|
|
29
|
+
paths: Array<{ lat: number; lng: number }>[];
|
|
30
|
+
fillColor?: string;
|
|
31
|
+
strokeColor?: string;
|
|
32
|
+
};
|
|
33
|
+
layers?: MapLayersConfig;
|
|
34
|
+
height?: string;
|
|
35
|
+
apiKey?: string;
|
|
36
|
+
mapContainerClassName?: string;
|
|
37
|
+
disableDefaultUI?: boolean;
|
|
38
|
+
zoomControl?: boolean;
|
|
39
|
+
streetViewControl?: boolean;
|
|
40
|
+
mapTypeControl?: boolean;
|
|
41
|
+
fullscreenControl?: boolean;
|
|
42
|
+
gestureHandling?: 'cooperative' | 'greedy' | 'none' | 'auto';
|
|
43
|
+
onMapLoad?: (map: google.maps.Map) => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const DEFAULT_CENTER = { lat: -23.5505, lng: -46.6333 };
|
|
47
|
+
const DEFAULT_ZOOM = 12;
|
|
48
|
+
|
|
49
|
+
const MapContent = React.forwardRef<HTMLDivElement, MapProps & { apiKey: string }>(
|
|
50
|
+
({ apiKey, ...props }, ref) => {
|
|
51
|
+
const { isLoaded, loadError } = useGoogleMapsLoader();
|
|
52
|
+
const {
|
|
53
|
+
center = DEFAULT_CENTER,
|
|
54
|
+
zoom = DEFAULT_ZOOM,
|
|
55
|
+
markers = [],
|
|
56
|
+
circle,
|
|
57
|
+
polygon,
|
|
58
|
+
layers,
|
|
59
|
+
height = "400px",
|
|
60
|
+
mapContainerClassName,
|
|
61
|
+
disableDefaultUI = false,
|
|
62
|
+
zoomControl = true,
|
|
63
|
+
streetViewControl = false,
|
|
64
|
+
mapTypeControl = false,
|
|
65
|
+
fullscreenControl = true,
|
|
66
|
+
gestureHandling = 'cooperative',
|
|
67
|
+
onMapLoad,
|
|
68
|
+
className,
|
|
69
|
+
...divProps
|
|
70
|
+
} = props;
|
|
71
|
+
|
|
72
|
+
const [selectedMarker, setSelectedMarker] = useState<number | null>(null);
|
|
73
|
+
const mapRef = useRef<google.maps.Map | null>(null);
|
|
74
|
+
const mapContainerRef = useRef<HTMLDivElement>(null);
|
|
75
|
+
const markersRef = useRef<google.maps.marker.AdvancedMarkerElement[]>([]);
|
|
76
|
+
const infoWindowRef = useRef<google.maps.InfoWindow | null>(null);
|
|
77
|
+
const circleRef = useRef<google.maps.Circle | null>(null);
|
|
78
|
+
const polygonRef = useRef<google.maps.Polygon | null>(null);
|
|
79
|
+
const isInitializedRef = useRef(false);
|
|
80
|
+
|
|
81
|
+
// Resolve theme colors for Map shapes (Circle, Polygon)
|
|
82
|
+
const [themeColors, setThemeColors] = useState({
|
|
83
|
+
primary: '#4F46E5',
|
|
84
|
+
chart2: '#10B981'
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (typeof window !== 'undefined') {
|
|
89
|
+
const styles = getComputedStyle(document.documentElement);
|
|
90
|
+
setThemeColors({
|
|
91
|
+
primary: styles.getPropertyValue('--primary').trim() || '#4F46E5',
|
|
92
|
+
chart2: styles.getPropertyValue('--chart-2').trim() || '#10B981'
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}, []);
|
|
96
|
+
|
|
97
|
+
// Initialize map once
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (!isLoaded || !mapContainerRef.current || isInitializedRef.current) return;
|
|
100
|
+
|
|
101
|
+
isInitializedRef.current = true;
|
|
102
|
+
const map = new google.maps.Map(mapContainerRef.current, {
|
|
103
|
+
center,
|
|
104
|
+
zoom,
|
|
105
|
+
mapId: 'xertica-map',
|
|
106
|
+
disableDefaultUI,
|
|
107
|
+
zoomControl,
|
|
108
|
+
streetViewControl,
|
|
109
|
+
mapTypeControl,
|
|
110
|
+
fullscreenControl,
|
|
111
|
+
gestureHandling,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
mapRef.current = map;
|
|
115
|
+
|
|
116
|
+
if (onMapLoad) {
|
|
117
|
+
onMapLoad(map);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return () => {
|
|
121
|
+
isInitializedRef.current = false;
|
|
122
|
+
mapRef.current = null;
|
|
123
|
+
};
|
|
124
|
+
}, [isLoaded]);
|
|
125
|
+
|
|
126
|
+
// Update markers
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
const map = mapRef.current;
|
|
129
|
+
if (!map || !isLoaded || !window.google?.maps?.marker) return;
|
|
130
|
+
|
|
131
|
+
// Clear old markers
|
|
132
|
+
markersRef.current.forEach(m => { m.map = null; });
|
|
133
|
+
markersRef.current = [];
|
|
134
|
+
|
|
135
|
+
// Create new markers
|
|
136
|
+
markers.forEach((markerData, idx) => {
|
|
137
|
+
const markerColor = markerData.customColor || 'var(--primary)';
|
|
138
|
+
const iconColor = markerData.iconColor || 'white';
|
|
139
|
+
|
|
140
|
+
const markerContent = document.createElement('div');
|
|
141
|
+
markerContent.className = 'flex items-center justify-center w-8 h-8 border-[3px] border-background shadow-lg cursor-pointer origin-center transition-transform duration-200';
|
|
142
|
+
// Custom shape using standard border-radius classes if possible, or style for specific shape
|
|
143
|
+
markerContent.style.borderRadius = '50% 50% 50% 0';
|
|
144
|
+
markerContent.style.transform = 'rotate(-45deg)';
|
|
145
|
+
markerContent.style.backgroundColor = markerColor;
|
|
146
|
+
// box-shadow is handled by shadow-lg, border by border-background
|
|
147
|
+
|
|
148
|
+
const iconContainer = document.createElement('div');
|
|
149
|
+
iconContainer.className = 'flex items-center justify-center rotate-45';
|
|
150
|
+
|
|
151
|
+
if (markerData.iconSvg) {
|
|
152
|
+
const div = document.createElement('div');
|
|
153
|
+
div.innerHTML = markerData.iconSvg;
|
|
154
|
+
const svg = div.querySelector('svg');
|
|
155
|
+
if (svg) {
|
|
156
|
+
svg.setAttribute('width', '16');
|
|
157
|
+
svg.setAttribute('height', '16');
|
|
158
|
+
svg.style.color = iconColor;
|
|
159
|
+
svg.style.fill = 'currentColor';
|
|
160
|
+
iconContainer.appendChild(svg);
|
|
161
|
+
}
|
|
162
|
+
} else if (markerData.icon || markerData.label) {
|
|
163
|
+
const span = document.createElement('span');
|
|
164
|
+
span.textContent = markerData.icon || markerData.label || '';
|
|
165
|
+
span.style.color = iconColor;
|
|
166
|
+
span.className = 'font-semibold text-sm';
|
|
167
|
+
iconContainer.appendChild(span);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
markerContent.appendChild(iconContainer);
|
|
171
|
+
|
|
172
|
+
markerContent.addEventListener('mouseenter', () => {
|
|
173
|
+
markerContent.style.transform = 'rotate(-45deg) scale(1.15)';
|
|
174
|
+
});
|
|
175
|
+
markerContent.addEventListener('mouseleave', () => {
|
|
176
|
+
markerContent.style.transform = 'rotate(-45deg) scale(1)';
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const marker = new google.maps.marker.AdvancedMarkerElement({
|
|
180
|
+
map,
|
|
181
|
+
position: markerData.position,
|
|
182
|
+
content: markerContent,
|
|
183
|
+
title: markerData.title,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
marker.addListener('click', () => {
|
|
187
|
+
setSelectedMarker(idx);
|
|
188
|
+
map.panTo(markerData.position);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
markersRef.current.push(marker);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
return () => {
|
|
195
|
+
markersRef.current.forEach(m => { m.map = null; });
|
|
196
|
+
markersRef.current = [];
|
|
197
|
+
};
|
|
198
|
+
}, [isLoaded, markers]);
|
|
199
|
+
|
|
200
|
+
// Update InfoWindow
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
const map = mapRef.current;
|
|
203
|
+
if (!map || selectedMarker === null) {
|
|
204
|
+
infoWindowRef.current?.close();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const markerData = markers[selectedMarker];
|
|
209
|
+
if (!markerData) return;
|
|
210
|
+
|
|
211
|
+
// Determine content to render
|
|
212
|
+
let contentToRender = markerData.richContent;
|
|
213
|
+
|
|
214
|
+
// If no richContent but title/info exists, create default standard content
|
|
215
|
+
if (!contentToRender && (markerData.title || markerData.info)) {
|
|
216
|
+
contentToRender = (
|
|
217
|
+
<div className="p-4 pr-8 min-w-[200px] max-w-[300px]">
|
|
218
|
+
{markerData.title && <h4 className="font-semibold text-base mb-1">{markerData.title}</h4>}
|
|
219
|
+
{markerData.info && <p className="text-sm text-muted-foreground">{markerData.info}</p>}
|
|
220
|
+
</div>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Render content using React Root if we have something to render
|
|
225
|
+
if (contentToRender) {
|
|
226
|
+
if (!infoWindowRef.current) {
|
|
227
|
+
infoWindowRef.current = new google.maps.InfoWindow();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const container = document.createElement('div');
|
|
231
|
+
const root = createRoot(container);
|
|
232
|
+
root.render(contentToRender);
|
|
233
|
+
|
|
234
|
+
infoWindowRef.current.setContent(container);
|
|
235
|
+
infoWindowRef.current.open({
|
|
236
|
+
map,
|
|
237
|
+
anchor: markersRef.current[selectedMarker],
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const listener = infoWindowRef.current.addListener('closeclick', () => {
|
|
241
|
+
setSelectedMarker(null);
|
|
242
|
+
// Small delay to ensure close animation finishes
|
|
243
|
+
setTimeout(() => root.unmount(), 0);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
return () => {
|
|
247
|
+
google.maps.event.removeListener(listener);
|
|
248
|
+
// Safe unmount
|
|
249
|
+
setTimeout(() => root.unmount(), 0);
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}, [selectedMarker, markers]);
|
|
253
|
+
|
|
254
|
+
// Update circle
|
|
255
|
+
useEffect(() => {
|
|
256
|
+
const map = mapRef.current;
|
|
257
|
+
if (!map || !isLoaded) return;
|
|
258
|
+
|
|
259
|
+
circleRef.current?.setMap(null);
|
|
260
|
+
circleRef.current = null;
|
|
261
|
+
|
|
262
|
+
if (circle && circle.center && circle.radius) {
|
|
263
|
+
circleRef.current = new google.maps.Circle({
|
|
264
|
+
map,
|
|
265
|
+
center: circle.center,
|
|
266
|
+
radius: circle.radius,
|
|
267
|
+
fillColor: circle.fillColor || themeColors.primary,
|
|
268
|
+
fillOpacity: 0.2,
|
|
269
|
+
strokeColor: circle.strokeColor || themeColors.primary,
|
|
270
|
+
strokeOpacity: 0.8,
|
|
271
|
+
strokeWeight: 2,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return () => {
|
|
276
|
+
circleRef.current?.setMap(null);
|
|
277
|
+
};
|
|
278
|
+
}, [circle, isLoaded]);
|
|
279
|
+
|
|
280
|
+
// Update polygon
|
|
281
|
+
useEffect(() => {
|
|
282
|
+
const map = mapRef.current;
|
|
283
|
+
if (!map || !isLoaded) return;
|
|
284
|
+
|
|
285
|
+
polygonRef.current?.setMap(null);
|
|
286
|
+
polygonRef.current = null;
|
|
287
|
+
|
|
288
|
+
if (polygon && polygon.paths) {
|
|
289
|
+
polygonRef.current = new google.maps.Polygon({
|
|
290
|
+
map,
|
|
291
|
+
paths: polygon.paths,
|
|
292
|
+
fillColor: polygon.fillColor || themeColors.chart2,
|
|
293
|
+
fillOpacity: 0.2,
|
|
294
|
+
strokeColor: polygon.strokeColor || themeColors.chart2,
|
|
295
|
+
strokeOpacity: 0.8,
|
|
296
|
+
strokeWeight: 2,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return () => {
|
|
301
|
+
polygonRef.current?.setMap(null);
|
|
302
|
+
};
|
|
303
|
+
}, [polygon, isLoaded]);
|
|
304
|
+
|
|
305
|
+
// Layers
|
|
306
|
+
useMapLayers(mapRef.current, layers || {});
|
|
307
|
+
|
|
308
|
+
if (loadError) {
|
|
309
|
+
return (
|
|
310
|
+
<div
|
|
311
|
+
ref={ref}
|
|
312
|
+
className={cn(
|
|
313
|
+
"relative rounded-[var(--radius-card)] border border-destructive/50 overflow-hidden bg-destructive/5",
|
|
314
|
+
className
|
|
315
|
+
)}
|
|
316
|
+
style={{ height }}
|
|
317
|
+
{...divProps}
|
|
318
|
+
>
|
|
319
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
320
|
+
<div className="text-center space-y-2 p-6">
|
|
321
|
+
<div className="text-destructive">
|
|
322
|
+
<svg className="w-12 h-12 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
323
|
+
<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" />
|
|
324
|
+
</svg>
|
|
325
|
+
</div>
|
|
326
|
+
<p className="text-sm text-destructive">Failed to load Google Maps</p>
|
|
327
|
+
<p className="text-xs text-muted-foreground">Check API key in Settings</p>
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
</div>
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (!isLoaded) {
|
|
335
|
+
return (
|
|
336
|
+
<div
|
|
337
|
+
ref={ref}
|
|
338
|
+
className={cn(
|
|
339
|
+
"relative rounded-[var(--radius-card)] border border-border overflow-hidden bg-muted animate-pulse",
|
|
340
|
+
className
|
|
341
|
+
)}
|
|
342
|
+
style={{ height }}
|
|
343
|
+
{...divProps}
|
|
344
|
+
/>
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return (
|
|
349
|
+
<div
|
|
350
|
+
ref={ref}
|
|
351
|
+
className={cn(
|
|
352
|
+
"relative rounded-[var(--radius-card)] border border-border overflow-hidden",
|
|
353
|
+
className
|
|
354
|
+
)}
|
|
355
|
+
{...divProps}
|
|
356
|
+
>
|
|
357
|
+
<div
|
|
358
|
+
ref={mapContainerRef}
|
|
359
|
+
style={{ height }}
|
|
360
|
+
className={cn("rounded-[var(--radius-card)] w-full", mapContainerClassName)}
|
|
361
|
+
/>
|
|
362
|
+
</div>
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
);
|
|
366
|
+
MapContent.displayName = "MapContent";
|
|
367
|
+
|
|
368
|
+
export const Map = React.forwardRef<HTMLDivElement, MapProps>(
|
|
369
|
+
(props, ref) => {
|
|
370
|
+
const effectiveApiKey = props.apiKey || "";
|
|
371
|
+
|
|
372
|
+
const isValidKey = effectiveApiKey &&
|
|
373
|
+
effectiveApiKey !== "YOUR_GOOGLE_MAPS_API_KEY_HERE" &&
|
|
374
|
+
effectiveApiKey.startsWith("AIza");
|
|
375
|
+
|
|
376
|
+
if (!isValidKey) {
|
|
377
|
+
const {
|
|
378
|
+
center, zoom, markers, circle, polygon, height, apiKey,
|
|
379
|
+
mapContainerClassName, disableDefaultUI, zoomControl,
|
|
380
|
+
streetViewControl, mapTypeControl, fullscreenControl,
|
|
381
|
+
gestureHandling, layers, ...divProps
|
|
382
|
+
} = props;
|
|
383
|
+
|
|
384
|
+
return (
|
|
385
|
+
<div
|
|
386
|
+
ref={ref}
|
|
387
|
+
className={cn(
|
|
388
|
+
"relative rounded-[var(--radius-card)] border border-border overflow-hidden bg-muted",
|
|
389
|
+
props.className
|
|
390
|
+
)}
|
|
391
|
+
style={{ height: props.height || "400px" }}
|
|
392
|
+
{...divProps}
|
|
393
|
+
>
|
|
394
|
+
<div className="absolute inset-0 flex items-center justify-center bg-muted">
|
|
395
|
+
<div className="text-center space-y-3 p-6">
|
|
396
|
+
<div className="w-16 h-16 mx-auto bg-primary/10 rounded-full flex items-center justify-center">
|
|
397
|
+
<svg className="w-8 h-8 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
398
|
+
<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" />
|
|
399
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
400
|
+
</svg>
|
|
401
|
+
</div>
|
|
402
|
+
<p className="text-sm text-muted-foreground">Configure Google Maps API Key in Settings</p>
|
|
403
|
+
</div>
|
|
404
|
+
</div>
|
|
405
|
+
</div>
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return <MapContent ref={ref} {...props} apiKey={effectiveApiKey} />;
|
|
410
|
+
}
|
|
411
|
+
);
|
|
412
|
+
Map.displayName = "Map";
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as MenubarPrimitive from "@radix-ui/react-menubar";
|
|
5
|
+
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
|
6
|
+
|
|
7
|
+
import { cn } from "./utils";
|
|
8
|
+
|
|
9
|
+
function Menubar({
|
|
10
|
+
className,
|
|
11
|
+
...props
|
|
12
|
+
}: React.ComponentProps<typeof MenubarPrimitive.Root>) {
|
|
13
|
+
return (
|
|
14
|
+
<MenubarPrimitive.Root
|
|
15
|
+
data-slot="menubar"
|
|
16
|
+
className={cn(
|
|
17
|
+
"bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs",
|
|
18
|
+
className,
|
|
19
|
+
)}
|
|
20
|
+
{...props}
|
|
21
|
+
/>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function MenubarMenu({
|
|
26
|
+
...props
|
|
27
|
+
}: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
|
|
28
|
+
return <MenubarPrimitive.Menu data-slot="menubar-menu" {...props} />;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function MenubarGroup({
|
|
32
|
+
...props
|
|
33
|
+
}: React.ComponentProps<typeof MenubarPrimitive.Group>) {
|
|
34
|
+
return <MenubarPrimitive.Group data-slot="menubar-group" {...props} />;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function MenubarPortal({
|
|
38
|
+
...props
|
|
39
|
+
}: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
|
|
40
|
+
return <MenubarPrimitive.Portal data-slot="menubar-portal" {...props} />;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function MenubarRadioGroup({
|
|
44
|
+
...props
|
|
45
|
+
}: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
|
|
46
|
+
return (
|
|
47
|
+
<MenubarPrimitive.RadioGroup data-slot="menubar-radio-group" {...props} />
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function MenubarTrigger({
|
|
52
|
+
className,
|
|
53
|
+
...props
|
|
54
|
+
}: React.ComponentProps<typeof MenubarPrimitive.Trigger>) {
|
|
55
|
+
return (
|
|
56
|
+
<MenubarPrimitive.Trigger
|
|
57
|
+
data-slot="menubar-trigger"
|
|
58
|
+
className={cn(
|
|
59
|
+
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none",
|
|
60
|
+
className,
|
|
61
|
+
)}
|
|
62
|
+
{...props}
|
|
63
|
+
/>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function MenubarContent({
|
|
68
|
+
className,
|
|
69
|
+
align = "start",
|
|
70
|
+
alignOffset = -4,
|
|
71
|
+
sideOffset = 8,
|
|
72
|
+
...props
|
|
73
|
+
}: React.ComponentProps<typeof MenubarPrimitive.Content>) {
|
|
74
|
+
return (
|
|
75
|
+
<MenubarPortal>
|
|
76
|
+
<MenubarPrimitive.Content
|
|
77
|
+
data-slot="menubar-content"
|
|
78
|
+
align={align}
|
|
79
|
+
alignOffset={alignOffset}
|
|
80
|
+
sideOffset={sideOffset}
|
|
81
|
+
className={cn(
|
|
82
|
+
"bg-popover text-popover-foreground data-[state=open]:animate-in 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 z-50 min-w-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-md",
|
|
83
|
+
className,
|
|
84
|
+
)}
|
|
85
|
+
{...props}
|
|
86
|
+
/>
|
|
87
|
+
</MenubarPortal>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function MenubarItem({
|
|
92
|
+
className,
|
|
93
|
+
inset,
|
|
94
|
+
variant = "default",
|
|
95
|
+
...props
|
|
96
|
+
}: React.ComponentProps<typeof MenubarPrimitive.Item> & {
|
|
97
|
+
inset?: boolean;
|
|
98
|
+
variant?: "default" | "destructive";
|
|
99
|
+
}) {
|
|
100
|
+
return (
|
|
101
|
+
<MenubarPrimitive.Item
|
|
102
|
+
data-slot="menubar-item"
|
|
103
|
+
data-inset={inset}
|
|
104
|
+
data-variant={variant}
|
|
105
|
+
className={cn(
|
|
106
|
+
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
107
|
+
className,
|
|
108
|
+
)}
|
|
109
|
+
{...props}
|
|
110
|
+
/>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function MenubarCheckboxItem({
|
|
115
|
+
className,
|
|
116
|
+
children,
|
|
117
|
+
checked,
|
|
118
|
+
...props
|
|
119
|
+
}: React.ComponentProps<typeof MenubarPrimitive.CheckboxItem>) {
|
|
120
|
+
return (
|
|
121
|
+
<MenubarPrimitive.CheckboxItem
|
|
122
|
+
data-slot="menubar-checkbox-item"
|
|
123
|
+
className={cn(
|
|
124
|
+
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 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",
|
|
125
|
+
className,
|
|
126
|
+
)}
|
|
127
|
+
checked={checked}
|
|
128
|
+
{...props}
|
|
129
|
+
>
|
|
130
|
+
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
|
131
|
+
<MenubarPrimitive.ItemIndicator>
|
|
132
|
+
<CheckIcon className="size-4" />
|
|
133
|
+
</MenubarPrimitive.ItemIndicator>
|
|
134
|
+
</span>
|
|
135
|
+
{children}
|
|
136
|
+
</MenubarPrimitive.CheckboxItem>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function MenubarRadioItem({
|
|
141
|
+
className,
|
|
142
|
+
children,
|
|
143
|
+
...props
|
|
144
|
+
}: React.ComponentProps<typeof MenubarPrimitive.RadioItem>) {
|
|
145
|
+
return (
|
|
146
|
+
<MenubarPrimitive.RadioItem
|
|
147
|
+
data-slot="menubar-radio-item"
|
|
148
|
+
className={cn(
|
|
149
|
+
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 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",
|
|
150
|
+
className,
|
|
151
|
+
)}
|
|
152
|
+
{...props}
|
|
153
|
+
>
|
|
154
|
+
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
|
155
|
+
<MenubarPrimitive.ItemIndicator>
|
|
156
|
+
<CircleIcon className="size-2 fill-current" />
|
|
157
|
+
</MenubarPrimitive.ItemIndicator>
|
|
158
|
+
</span>
|
|
159
|
+
{children}
|
|
160
|
+
</MenubarPrimitive.RadioItem>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function MenubarLabel({
|
|
165
|
+
className,
|
|
166
|
+
inset,
|
|
167
|
+
...props
|
|
168
|
+
}: React.ComponentProps<typeof MenubarPrimitive.Label> & {
|
|
169
|
+
inset?: boolean;
|
|
170
|
+
}) {
|
|
171
|
+
return (
|
|
172
|
+
<MenubarPrimitive.Label
|
|
173
|
+
data-slot="menubar-label"
|
|
174
|
+
data-inset={inset}
|
|
175
|
+
className={cn(
|
|
176
|
+
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
|
177
|
+
className,
|
|
178
|
+
)}
|
|
179
|
+
{...props}
|
|
180
|
+
/>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function MenubarSeparator({
|
|
185
|
+
className,
|
|
186
|
+
...props
|
|
187
|
+
}: React.ComponentProps<typeof MenubarPrimitive.Separator>) {
|
|
188
|
+
return (
|
|
189
|
+
<MenubarPrimitive.Separator
|
|
190
|
+
data-slot="menubar-separator"
|
|
191
|
+
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
|
192
|
+
{...props}
|
|
193
|
+
/>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function MenubarShortcut({
|
|
198
|
+
className,
|
|
199
|
+
...props
|
|
200
|
+
}: React.ComponentProps<"span">) {
|
|
201
|
+
return (
|
|
202
|
+
<span
|
|
203
|
+
data-slot="menubar-shortcut"
|
|
204
|
+
className={cn(
|
|
205
|
+
"text-muted-foreground ml-auto text-xs tracking-widest",
|
|
206
|
+
className,
|
|
207
|
+
)}
|
|
208
|
+
{...props}
|
|
209
|
+
/>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function MenubarSub({
|
|
214
|
+
...props
|
|
215
|
+
}: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
|
|
216
|
+
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function MenubarSubTrigger({
|
|
220
|
+
className,
|
|
221
|
+
inset,
|
|
222
|
+
children,
|
|
223
|
+
...props
|
|
224
|
+
}: React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & {
|
|
225
|
+
inset?: boolean;
|
|
226
|
+
}) {
|
|
227
|
+
return (
|
|
228
|
+
<MenubarPrimitive.SubTrigger
|
|
229
|
+
data-slot="menubar-sub-trigger"
|
|
230
|
+
data-inset={inset}
|
|
231
|
+
className={cn(
|
|
232
|
+
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[inset]:pl-8",
|
|
233
|
+
className,
|
|
234
|
+
)}
|
|
235
|
+
{...props}
|
|
236
|
+
>
|
|
237
|
+
{children}
|
|
238
|
+
<ChevronRightIcon className="ml-auto h-4 w-4" />
|
|
239
|
+
</MenubarPrimitive.SubTrigger>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function MenubarSubContent({
|
|
244
|
+
className,
|
|
245
|
+
...props
|
|
246
|
+
}: React.ComponentProps<typeof MenubarPrimitive.SubContent>) {
|
|
247
|
+
return (
|
|
248
|
+
<MenubarPrimitive.SubContent
|
|
249
|
+
data-slot="menubar-sub-content"
|
|
250
|
+
className={cn(
|
|
251
|
+
"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 z-50 min-w-[8rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
|
252
|
+
className,
|
|
253
|
+
)}
|
|
254
|
+
{...props}
|
|
255
|
+
/>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export {
|
|
260
|
+
Menubar,
|
|
261
|
+
MenubarPortal,
|
|
262
|
+
MenubarMenu,
|
|
263
|
+
MenubarTrigger,
|
|
264
|
+
MenubarContent,
|
|
265
|
+
MenubarGroup,
|
|
266
|
+
MenubarSeparator,
|
|
267
|
+
MenubarLabel,
|
|
268
|
+
MenubarItem,
|
|
269
|
+
MenubarShortcut,
|
|
270
|
+
MenubarCheckboxItem,
|
|
271
|
+
MenubarRadioGroup,
|
|
272
|
+
MenubarRadioItem,
|
|
273
|
+
MenubarSub,
|
|
274
|
+
MenubarSubTrigger,
|
|
275
|
+
MenubarSubContent,
|
|
276
|
+
};
|